aboutsummaryrefslogtreecommitdiffstats
path: root/src/org/theb
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2007-11-17 05:58:42 +0000
committerKenny Root <kenny@the-b.org>2007-11-17 05:58:42 +0000
commitdfa41d090260eed63f3d8510571a2f6236a5ff45 (patch)
tree1eaca308ba9bb913161edf83bfef5f9295784e56 /src/org/theb
parentedfccaafe3e754ed124afa67465b6044eacd3987 (diff)
downloadconnectbot-dfa41d090260eed63f3d8510571a2f6236a5ff45.tar.gz
connectbot-dfa41d090260eed63f3d8510571a2f6236a5ff45.tar.bz2
connectbot-dfa41d090260eed63f3d8510571a2f6236a5ff45.zip
Initial import.
Diffstat (limited to 'src/org/theb')
-rw-r--r--src/org/theb/provider/HostDb.java18
-rw-r--r--src/org/theb/ssh/HostDbProvider.java211
-rw-r--r--src/org/theb/ssh/HostEditor.java194
-rw-r--r--src/org/theb/ssh/HostsList.java213
-rw-r--r--src/org/theb/ssh/InteractiveHostKeyVerifier.java14
-rw-r--r--src/org/theb/ssh/PasswordDialog.java29
-rw-r--r--src/org/theb/ssh/R.java57
-rw-r--r--src/org/theb/ssh/SecureShell.java363
-rw-r--r--src/org/theb/ssh/ShellView.java77
9 files changed, 1176 insertions, 0 deletions
diff --git a/src/org/theb/provider/HostDb.java b/src/org/theb/provider/HostDb.java
new file mode 100644
index 0000000..3bed790
--- /dev/null
+++ b/src/org/theb/provider/HostDb.java
@@ -0,0 +1,18 @@
+package org.theb.provider;
+
+import android.net.ContentURI;
+import android.provider.BaseColumns;
+
+public final class HostDb {
+ public static final class Hosts implements BaseColumns {
+ public static final ContentURI CONTENT_URI
+ = ContentURI.create("content://org.theb.provider.HostDb/hosts");
+
+ public static final String DEFAULT_SORT_ORDER = "hostname DESC";
+
+ public static final String USERNAME = "username";
+ public static final String HOSTNAME = "hostname";
+ public static final String PORT = "port";
+ public static final String HOSTKEY = "hostkey";
+ }
+}
diff --git a/src/org/theb/ssh/HostDbProvider.java b/src/org/theb/ssh/HostDbProvider.java
new file mode 100644
index 0000000..20ec224
--- /dev/null
+++ b/src/org/theb/ssh/HostDbProvider.java
@@ -0,0 +1,211 @@
+package org.theb.ssh;
+
+import java.util.HashMap;
+
+import org.theb.provider.HostDb;
+
+import android.content.ContentProvider;
+import android.content.ContentProviderDatabaseHelper;
+import android.content.ContentURIParser;
+import android.content.ContentValues;
+import android.content.QueryBuilder;
+import android.database.Cursor;
+import android.database.SQLException;
+import android.database.sqlite.SQLiteDatabase;
+import android.net.ContentURI;
+import android.text.TextUtils;
+import android.util.Log;
+
+public class HostDbProvider extends ContentProvider {
+
+ private SQLiteDatabase mDB;
+
+ private static final String TAG = "HostDbProvider";
+ private static final String DATABASE_NAME = "ssh_hosts.db";
+ private static final int DATABASE_VERSION = 2;
+
+ private static HashMap<String, String> HOSTS_LIST_PROJECTION_MAP;
+
+ private static final int HOSTS = 1;
+ private static final int HOST_ID = 2;
+
+ private static final ContentURIParser URL_MATCHER;
+
+ private static class DatabaseHelper extends ContentProviderDatabaseHelper {
+
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ db.execSQL("CREATE TABLE hosts (_id INTEGER PRIMARY KEY,"
+ + "hostname TEXT," + "username TEXT," + "port INTEGER,"
+ + "hostkey TEXT" + ")");
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
+ Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
+ + newVersion + ", which will destroy all old data");
+ db.execSQL("DROP TABLE IF EXISTS hosts");
+ onCreate(db);
+ }
+
+ }
+
+ @Override
+ public int delete(ContentURI uri, String where, String[] whereArgs) {
+ int count;
+ switch (URL_MATCHER.match(uri)) {
+ case HOSTS:
+ count = mDB.delete("ssh_hosts", where, whereArgs);
+ break;
+
+ case HOST_ID:
+ String segment = uri.getPathSegment(1);
+ count = mDB.delete("hosts", "_id="
+ + segment
+ + (!TextUtils.isEmpty(where) ? " AND (" + where
+ + ')' : ""), whereArgs);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown Delete " + uri);
+ }
+
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+ }
+
+ @Override
+ public String getType(ContentURI uri) {
+ switch (URL_MATCHER.match(uri)) {
+ case HOSTS:
+ return "vnd.android.cursor.dir/vnd.theb.host";
+ case HOST_ID:
+ return "vnd.android.cursor.item/vnd.theb.host";
+ default:
+ throw new IllegalArgumentException("Unknown getType " + uri);
+ }
+ }
+
+ @Override
+ public ContentURI insert(ContentURI uri, ContentValues initialValues) {
+ long rowID;
+
+ ContentValues values;
+ if (initialValues != null) {
+ values = new ContentValues(initialValues);
+ } else {
+ values = new ContentValues();
+ }
+ /*
+ if (URL_MATCHER.match(uri) != HOSTS) {
+ throw new IllegalArgumentException("Unknown Insert " + uri);
+ }
+ */
+ if (values.containsKey(HostDb.Hosts.HOSTNAME) == false) {
+ values.put(HostDb.Hosts.HOSTNAME, "");
+ }
+
+ if (values.containsKey(HostDb.Hosts.USERNAME) == false) {
+ values.put(HostDb.Hosts.USERNAME, "");
+ }
+
+ if (values.containsKey(HostDb.Hosts.PORT) == false) {
+ values.put(HostDb.Hosts.PORT, 22);
+ }
+
+ if (values.containsKey(HostDb.Hosts.HOSTKEY) == false) {
+ values.put(HostDb.Hosts.HOSTKEY, "");
+ }
+
+ rowID = mDB.insert("hosts", "host", values);
+ if (rowID > 0) {
+ ContentURI newUri = HostDb.Hosts.CONTENT_URI.addId(rowID);
+ getContext().getContentResolver().notifyChange(newUri, null);
+ return newUri;
+ }
+
+ throw new SQLException("Failed to insert row into " + uri);
+ }
+
+ @Override
+ public boolean onCreate() {
+ DatabaseHelper dbHelper = new DatabaseHelper();
+ mDB = dbHelper.openDatabase(getContext(), DATABASE_NAME, null, DATABASE_VERSION);
+ return (mDB == null) ? false : true;
+ }
+
+ @Override
+ public Cursor query(ContentURI uri, String[] projection, String selection,
+ String[] selectionArgs, String groupBy, String having,
+ String sortOrder) {
+ QueryBuilder qb = new QueryBuilder();
+
+ switch (URL_MATCHER.match(uri)) {
+ case HOSTS:
+ qb.setTables("hosts");
+ qb.setProjectionMap(HOSTS_LIST_PROJECTION_MAP);
+ break;
+
+ case HOST_ID:
+ qb.setTables("hosts");
+ qb.appendWhere("_id=" + uri.getPathSegment(1));
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown Query " + uri);
+ }
+
+ String orderBy;
+ if (TextUtils.isEmpty(sortOrder)) {
+ orderBy = HostDb.Hosts.DEFAULT_SORT_ORDER;
+ } else {
+ orderBy = sortOrder;
+ }
+
+ Cursor c = qb.query(mDB, projection, selection, selectionArgs, groupBy,
+ having, orderBy);
+ c.setNotificationUri(getContext().getContentResolver(), uri);
+ return c;
+ }
+
+ @Override
+ public int update(ContentURI uri, ContentValues values, String where,
+ String[] whereArgs) {
+ int count;
+
+ switch (URL_MATCHER.match(uri)) {
+ case HOSTS:
+ count = mDB.update("hosts", values, where, whereArgs);
+ break;
+
+ case HOST_ID:
+ String segment = uri.getPathSegment(1);
+ count = mDB
+ .update("hosts", values, "_id="
+ + segment
+ + (!TextUtils.isEmpty(where) ? " AND (" + where
+ + ')' : ""), whereArgs);
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown Update " + uri);
+ }
+
+ getContext().getContentResolver().notifyChange(uri, null);
+ return count;
+
+ }
+
+ static {
+ URL_MATCHER = new ContentURIParser(ContentURIParser.NO_MATCH);
+ URL_MATCHER.addURI("org.theb.provider.HostDb", "hosts", HOSTS);
+ URL_MATCHER.addURI("org.theb.provider.HostDb", "hosts/#", HOST_ID);
+
+ HOSTS_LIST_PROJECTION_MAP = new HashMap<String, String>();
+ HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts._ID, "_id");
+ HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.HOSTNAME, "hostname");
+ HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.USERNAME, "username");
+ HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.PORT, "port");
+ HOSTS_LIST_PROJECTION_MAP.put(HostDb.Hosts.HOSTKEY, "hostkey");
+ }
+}
diff --git a/src/org/theb/ssh/HostEditor.java b/src/org/theb/ssh/HostEditor.java
new file mode 100644
index 0000000..d642fd4
--- /dev/null
+++ b/src/org/theb/ssh/HostEditor.java
@@ -0,0 +1,194 @@
+package org.theb.ssh;
+
+import org.theb.provider.HostDb;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.ContentURI;
+import android.os.Bundle;
+import android.util.Log;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+
+public class HostEditor extends Activity {
+ public static final String EDIT_HOST_ACTION =
+ "com.theb.ssh.action.EDIT_HOST";
+
+ private static final String[] PROJECTION = new String[] {
+ HostDb.Hosts._ID, // 0
+ HostDb.Hosts.HOSTNAME, // 1
+ HostDb.Hosts.USERNAME, // 2
+ HostDb.Hosts.PORT, // 3
+ HostDb.Hosts.HOSTKEY, // 4
+ };
+
+ static final int HOSTNAME_INDEX = 1;
+ private static final int USERNAME_INDEX = 2;
+ private static final int PORT_INDEX = 3;
+ // Set up distinct states that the activity can be run in.
+ private static final int STATE_EDIT = 0;
+ private static final int STATE_INSERT = 1;
+
+ private EditText mHostname;
+ private EditText mUsername;
+ private EditText mPort;
+
+ // Cursor that will provide access to the host data we are editing
+ private Cursor mCursor;
+
+ private int mState;
+ private ContentURI mURI;
+
+ @Override
+ public void onCreate(Bundle savedValues) {
+ super.onCreate(savedValues);
+
+ // Have the system blur any windows behind this one.
+ getWindow().setFlags(WindowManager.LayoutParams.BLUR_BEHIND_FLAG,
+ WindowManager.LayoutParams.BLUR_BEHIND_FLAG);
+
+ // Apply a tint to any windows behind this one. Doing a tint this
+ // way is more efficient than using a translucent background. Note
+ // that the tint color really should come from a resource.
+ WindowManager.LayoutParams lp = getWindow().getAttributes();
+ lp.tintBehind = 0x60000820;
+ getWindow().setAttributes(lp);
+
+ this.setContentView(R.layout.host_editor);
+
+ // Set up click handlers for text fields and button
+ mHostname = (EditText) findViewById(R.id.hostname);
+ mUsername = (EditText) findViewById(R.id.username);
+ mPort = (EditText) findViewById(R.id.port);
+
+ Button addButton = (Button) findViewById(R.id.add);
+ addButton.setOnClickListener(mCommitListener);
+
+ Button cancelButton = (Button) findViewById(R.id.cancel);
+ cancelButton.setOnClickListener(mCancelListener);
+
+ final Intent intent = getIntent();
+
+ // Do some setup based on the action being performed.
+
+ final String action = intent.getAction();
+ if (Intent.INSERT_ACTION.equals(action)) {
+ mState = STATE_INSERT;
+ mURI = getContentResolver().insert(intent.getData(), null);
+
+ // If we were unable to create a new note, then just finish
+ // this activity. A RESULT_CANCELED will be sent back to the
+ // original activity if they requested a result.
+ if (mURI == null) {
+ Log.e("Notes", "Failed to insert new note into "
+ + getIntent().getData());
+ finish();
+ return;
+ }
+
+ // The new entry was created, so assume all will end well and
+ // set the result to be returned.
+ setResult(RESULT_OK, mURI.toString());
+ } else {
+ // Editing is the default state.
+ mState = STATE_EDIT;
+
+ // Get the URI of the host whose properties we want to edit
+ mURI = getIntent().getData();
+
+ // If were editing, change the Ok button to be Change instead.
+ addButton.setText(R.string.button_change);
+ }
+
+ // Get a cursor to access the host data
+ mCursor = managedQuery(mURI, PROJECTION, null, null);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+
+ // Initialize the text with the host data
+ if (mCursor != null) {
+ mCursor.first();
+
+ String hostname = mCursor.getString(HOSTNAME_INDEX);
+ mHostname.setText(hostname);
+
+ String username = mCursor.getString(USERNAME_INDEX);
+ mUsername.setText(username);
+
+ String port = mCursor.getString(PORT_INDEX);
+ mPort.setText(port);
+ }
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+
+ // Write the text back into the cursor
+ if (mCursor != null) {
+ String hostname = mHostname.getText().toString();
+ mCursor.updateString(HOSTNAME_INDEX, hostname);
+
+ String username = mUsername.getText().toString();
+ mCursor.updateString(USERNAME_INDEX, username);
+
+ String portStr = mPort.getText().toString();
+ int port = Integer.parseInt(portStr);
+ mCursor.updateInt(PORT_INDEX, port);
+
+ if (isFinishing()
+ && ((hostname.length() == 0)
+ || (username.length() == 0)
+ || (port == 0))) {
+ setResult(RESULT_CANCELED);
+ deleteHost();
+ } else {
+ managedCommitUpdates(mCursor);
+ }
+ }
+ }
+
+ private final void cancelEdit() {
+ if (mCursor != null) {
+ if (mState == STATE_EDIT) {
+ mCursor.deactivate();
+ mCursor = null;
+ } else if (mState == STATE_INSERT) {
+ deleteHost();
+ }
+ }
+ }
+
+ private final void deleteHost() {
+ if (mCursor != null) {
+ mHostname.setText("");
+ mUsername.setText("");
+ mPort.setText("");
+ mCursor.deleteRow();
+ mCursor.deactivate();
+ mCursor = null;
+ }
+ }
+
+ OnClickListener mCommitListener = new OnClickListener() {
+ public void onClick(View v) {
+ // When the user clicks, just finish this activity.
+ // onPause will be called, and we save our data there.
+ finish();
+ }
+ };
+
+ OnClickListener mCancelListener = new OnClickListener() {
+ public void onClick(View v) {
+ cancelEdit();
+ finish();
+ }
+ };
+}
diff --git a/src/org/theb/ssh/HostsList.java b/src/org/theb/ssh/HostsList.java
new file mode 100644
index 0000000..7a35fe2
--- /dev/null
+++ b/src/org/theb/ssh/HostsList.java
@@ -0,0 +1,213 @@
+package org.theb.ssh;
+
+import org.theb.provider.HostDb;
+import android.app.ListActivity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.ContentURI;
+import android.os.Bundle;
+import android.view.KeyEvent;
+import android.view.Menu;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.widget.ListAdapter;
+import android.widget.ListView;
+import android.widget.SimpleCursorAdapter;
+import android.widget.TextView;
+
+public class HostsList extends ListActivity {
+ public static final int DELETE_ID = Menu.FIRST;
+ public static final int INSERT_ID = Menu.FIRST + 1;
+
+ private static final String[] PROJECTION = new String[] {
+ HostDb.Hosts._ID,
+ HostDb.Hosts.HOSTNAME,
+ HostDb.Hosts.USERNAME,
+ HostDb.Hosts.PORT,
+ };
+
+ private Cursor mCursor;
+
+ public class HostListCursorAdapter extends SimpleCursorAdapter {
+
+ public HostListCursorAdapter(Context context, int layout, Cursor c,
+ String[] from, int[] to) {
+ super(context, layout, c, from, to);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ String label;
+ TextView textView = (TextView) view;
+
+ // Create a list display of "username@hostname:port" but exclude the port if
+ // it is already port 22 (default secure shell port).
+ label = cursor.getString(2)
+ + "@"
+ + cursor.getString(1);
+
+ int port = cursor.getInt(3);
+ if (port != 22) {
+ label = label + ":" + String.valueOf(port);
+ }
+
+ textView.setText(label);
+ }
+
+ }
+
+ /** Called when the activity is first created. */
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+
+ this.setDefaultKeyMode(SHORTCUT_DEFAULT_KEYS);
+
+ Intent intent = getIntent();
+ if (intent.getData() == null) {
+ intent.setData(HostDb.Hosts.CONTENT_URI);
+ }
+
+ setupListStripes();
+
+ mCursor = managedQuery(getIntent().getData(), PROJECTION, null, null);
+
+ ListAdapter adapter = new HostListCursorAdapter(this,
+ android.R.layout.simple_list_item_1, mCursor,
+ new String[] {HostDb.Hosts.HOSTNAME}, new int[] {android.R.id.text1});
+
+ setListAdapter(adapter);
+ }
+
+ /**
+ * Add stripes to the list view.
+ */
+ private void setupListStripes() {
+ // Get Drawables for alternating stripes
+ Drawable[] lineBackgrounds = new Drawable[2];
+
+ lineBackgrounds[0] = getResources().getDrawable(R.drawable.even_stripe);
+ lineBackgrounds[1] = getResources().getDrawable(R.drawable.odd_stripe);
+
+ // Make and measure a sample TextView of the sort our adapter will
+ // return
+ View view = getViewInflate().inflate(
+ android.R.layout.simple_list_item_1, null, null);
+
+ TextView v = (TextView) view.findViewById(android.R.id.text1);
+ v.setText("X");
+ // Make it 100 pixels wide, and let it choose its own height.
+ v.measure(MeasureSpec.makeMeasureSpec(View.MeasureSpec.EXACTLY, 100),
+ MeasureSpec.makeMeasureSpec(View.MeasureSpec.UNSPECIFIED, 0));
+ int height = v.getMeasuredHeight();
+ getListView().setStripes(lineBackgrounds, height);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ // This is our one standard application action -- inserting a
+ // new note into the list.
+ menu.add(0, INSERT_ID, R.string.menu_insert).setShortcut(
+ KeyEvent.KEYCODE_3, 0, KeyEvent.KEYCODE_A);
+
+ // Generate any additional actions that can be performed on the
+ // overall list. In a normal install, there are no additional
+ // actions found here, but this allows other applications to extend
+ // our menu with their own actions.
+ Intent intent = new Intent(null, getIntent().getData());
+ intent.addCategory(Intent.ALTERNATIVE_CATEGORY);
+ menu.addIntentOptions(
+ Menu.ALTERNATIVE, 0, new ComponentName(this, HostsList.class),
+ null, intent, 0, null);
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+ final boolean haveItems = mCursor.count() > 0;
+
+ // If there are any notes in the list (which implies that one of
+ // them is selected), then we need to generate the actions that
+ // can be performed on the current selection. This will be a combination
+ // of our own specific actions along with any extensions that can be
+ // found.
+ if (haveItems) {
+ // This is the selected item.
+ ContentURI uri = getIntent().getData().addId(getSelectionRowID());
+
+ // Build menu... always starts with the CONNECT action...
+ Intent[] specifics = new Intent[1];
+ specifics[0] = new Intent(Intent.PICK_ACTION, uri);
+ Menu.Item[] items = new Menu.Item[1];
+
+ // ... is followed by whatever other actions are available...
+ Intent intent = new Intent(null, uri);
+ intent.addCategory(Intent.SELECTED_ALTERNATIVE_CATEGORY);
+ menu.addIntentOptions(Menu.SELECTED_ALTERNATIVE, 0, null, specifics,
+ intent, Menu.NO_SEPARATOR_AFTER, items);
+
+ // ... and ends with the delete command.
+ menu.add(Menu.SELECTED_ALTERNATIVE, DELETE_ID, R.string.menu_delete)
+.
+ setShortcut(KeyEvent.KEYCODE_2, 0, KeyEvent.KEYCODE_D);
+ menu.addSeparator(Menu.SELECTED_ALTERNATIVE, 0);
+
+ // Give a shortcut to the connect action.
+ if (items[0] != null) {
+ items[0].setShortcut(KeyEvent.KEYCODE_1, 0, KeyEvent.KEYCODE_C);
+ }
+ } else {
+ menu.removeGroup(Menu.SELECTED_ALTERNATIVE);
+ }
+
+ // Make sure the delete action is disabled if there are no items.
+ menu.setItemShown(DELETE_ID, haveItems);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(Menu.Item item) {
+ switch (item.getId()) {
+ case DELETE_ID:
+ deleteItem();
+ return true;
+ case INSERT_ID:
+ insertItem();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ protected void onListItemClick(ListView l, View v, int position, long id) {
+ ContentURI url = getIntent().getData().addId(getSelectionRowID());
+
+ String action = getIntent().getAction();
+ if (Intent.PICK_ACTION.equals(action)
+ || Intent.GET_CONTENT_ACTION.equals(action)) {
+ // The caller is waiting for us to return a note selected by
+ // the user. The have clicked on one, so return it now.
+ setResult(RESULT_OK, url.toString());
+ } else {
+ // Launch activity to view/edit the currently selected item
+ startActivity(new Intent(Intent.PICK_ACTION, url));
+ }
+ }
+
+ private final void deleteItem() {
+ mCursor.moveTo(getSelection());
+ mCursor.deleteRow();
+ }
+
+ private final void insertItem() {
+ // Launch activity to insert a new item
+ startActivity(new Intent(Intent.INSERT_ACTION, getIntent().getData()));
+ }
+} \ No newline at end of file
diff --git a/src/org/theb/ssh/InteractiveHostKeyVerifier.java b/src/org/theb/ssh/InteractiveHostKeyVerifier.java
new file mode 100644
index 0000000..a461724
--- /dev/null
+++ b/src/org/theb/ssh/InteractiveHostKeyVerifier.java
@@ -0,0 +1,14 @@
+package org.theb.ssh;
+
+import com.trilead.ssh2.ServerHostKeyVerifier;
+
+public class InteractiveHostKeyVerifier implements ServerHostKeyVerifier {
+
+ public boolean verifyServerHostKey(String hostname, int port,
+ String serverHostKeyAlgorithm, byte[] serverHostKey)
+ throws Exception {
+ // TODO Auto-generated method stub
+ return true;
+ }
+
+}
diff --git a/src/org/theb/ssh/PasswordDialog.java b/src/org/theb/ssh/PasswordDialog.java
new file mode 100644
index 0000000..87839b7
--- /dev/null
+++ b/src/org/theb/ssh/PasswordDialog.java
@@ -0,0 +1,29 @@
+package org.theb.ssh;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+
+public class PasswordDialog extends Activity implements OnClickListener {
+ private EditText mPassword;
+ private Button mOK;
+
+ @Override
+ public void onCreate(Bundle savedValues) {
+ super.onCreate(savedValues);
+
+ setContentView(R.layout.password_dialog);
+
+ mPassword = (EditText) findViewById(R.id.password);
+ mOK = (Button) findViewById(R.id.ok);
+ mOK.setOnClickListener(this);
+ }
+
+ public void onClick(View arg0) {
+ setResult(RESULT_OK, mPassword.getText().toString());
+ finish();
+ }
+}
diff --git a/src/org/theb/ssh/R.java b/src/org/theb/ssh/R.java
new file mode 100644
index 0000000..55e280c
--- /dev/null
+++ b/src/org/theb/ssh/R.java
@@ -0,0 +1,57 @@
+/* AUTO-GENERATED FILE. DO NOT MODIFY.
+ *
+ * This class was automatically generated by the
+ * aapt tool from the resource data it found. It
+ * should not be modified by hand.
+ */
+
+package org.theb.ssh;
+
+public final class R {
+ public static final class attr {
+ }
+ public static final class drawable {
+ public static final int blue=0x7f020001;
+ public static final int even_stripe=0x7f020002;
+ public static final int icon=0x7f020000;
+ public static final int odd_stripe=0x7f020003;
+ }
+ public static final class id {
+ public static final int add=0x7f050006;
+ public static final int cancel=0x7f050007;
+ public static final int dismiss=0x7f05000a;
+ public static final int hostname=0x7f050003;
+ public static final int hostnameLabel=0x7f050002;
+ public static final int message=0x7f050009;
+ public static final int ok=0x7f05000d;
+ public static final int output=0x7f05000e;
+ public static final int password=0x7f05000c;
+ public static final int passwordLabel=0x7f05000b;
+ public static final int port=0x7f050005;
+ public static final int portLabel=0x7f050004;
+ public static final int shell=0x7f050008;
+ public static final int username=0x7f050001;
+ public static final int usernameLabel=0x7f050000;
+ }
+ public static final class layout {
+ public static final int host_editor=0x7f030000;
+ public static final int main=0x7f030001;
+ public static final int message_dialog=0x7f030002;
+ public static final int password_dialog=0x7f030003;
+ public static final int secure_shell=0x7f030004;
+ }
+ public static final class string {
+ public static final int app_name=0x7f040000;
+ public static final int button_add=0x7f040009;
+ public static final int button_cancel=0x7f04000a;
+ public static final int button_change=0x7f04000b;
+ public static final int button_ok=0x7f040008;
+ public static final int menu_delete=0x7f040007;
+ public static final int menu_insert=0x7f040006;
+ public static final int resolve_edit=0x7f040005;
+ public static final int title_host=0x7f040002;
+ public static final int title_hosts_list=0x7f040001;
+ public static final int title_password=0x7f040004;
+ public static final int title_shell=0x7f040003;
+ }
+}
diff --git a/src/org/theb/ssh/SecureShell.java b/src/org/theb/ssh/SecureShell.java
new file mode 100644
index 0000000..e1c5d80
--- /dev/null
+++ b/src/org/theb/ssh/SecureShell.java
@@ -0,0 +1,363 @@
+package org.theb.ssh;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.concurrent.Semaphore;
+
+import org.theb.provider.HostDb;
+
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ConnectionMonitor;
+import com.trilead.ssh2.Session;
+
+import android.app.Activity;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.Intent;
+import android.database.Cursor;
+import android.net.ContentURI;
+import android.os.Bundle;
+import android.os.Handler;
+import android.text.method.KeyCharacterMap;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class SecureShell extends Activity {
+ private Context mContext;
+ private TextView mOutput;
+ private ConnectionThread mConn;
+ private String mBuffer;
+ private KeyCharacterMap mKMap;
+
+ static final int PASSWORD_REQUEST = 0;
+
+ private static final int HOSTNAME_INDEX = 1;
+ private static final int USERNAME_INDEX = 2;
+ private static final int PORT_INDEX = 3;
+
+ private static final String[] PROJECTION = new String[] {
+ HostDb.Hosts._ID, // 0
+ HostDb.Hosts.HOSTNAME, // 1
+ HostDb.Hosts.USERNAME, // 2
+ HostDb.Hosts.PORT, // 3
+ };
+
+ private Cursor mCursor;
+
+ // This is for the password dialog.
+ Semaphore sPass;
+ String mPassword = null;
+
+ Connection conn;
+ Session sess;
+ InputStream in;
+ OutputStream out;
+ int x;
+ int y;
+
+ final Handler mHandler = new Handler();
+
+ final Runnable mUpdateView = new Runnable() {
+ public void run() {
+ updateViewInUI();
+ }
+ };
+
+ class ConnectionThread extends Thread {
+ String hostname;
+ String username;
+ int port;
+
+ char[][] lines;
+ int posy = 0;
+ int posx = 0;
+
+ public ConnectionThread(String hostname, String username, int port) {
+ this.hostname = hostname;
+ this.username = username;
+ this.port = port;
+ }
+
+ public void run() {
+ conn = new Connection(hostname, port);
+
+ conn.addConnectionMonitor(mConnectionMonitor);
+
+ Log.d("SSH", "Starting connection attempt...");
+ mBuffer = "Attemping to connect...";
+ mHandler.post(mUpdateView);
+
+ try {
+ conn.connect(new InteractiveHostKeyVerifier());
+
+ Log.d("SSH", "Starting authentication...");
+ mBuffer = "Attemping to authenticate...";
+ mHandler.post(mUpdateView);
+
+ boolean enableKeyboardInteractive = true;
+ boolean enableDSA = true;
+ boolean enableRSA = true;
+
+ while (true) {
+ /*
+ if ((enableDSA || enableRSA ) &&
+ mConn.isAuthMethodAvailable(username, "publickey");
+ */
+
+ if (conn.isAuthMethodAvailable(username, "password")) {
+ Log.d("SSH", "Trying password authentication...");
+
+ // Set a semaphore that is unset by the returning dialog.
+ sPass = new Semaphore(0);
+ askPassword();
+
+ // Wait for the user to answer.
+ sPass.acquire();
+ sPass = null;
+ if (mPassword == null)
+ continue;
+
+ boolean res = conn.authenticateWithPassword(username, mPassword);
+ if (res == true)
+ break;
+
+ continue;
+ }
+
+ throw new IOException("No supported authentication methods available.");
+ }
+
+ Log.d("SSH", "Opening session...");
+ mBuffer = "Opening session...";
+ mHandler.post(mUpdateView);
+
+ sess = conn.openSession();
+
+ y = (int)(mOutput.getHeight() / mOutput.getLineHeight());
+ // TODO: figure out how to get the width of monospace font characters.
+ x = y * 3;
+ Log.d("SSH", "Requesting PTY of size " + x + "x" + y);
+
+ sess.requestPTY("dumb", x, y, 0, 0, null);
+
+ Log.d("SSH", "Requesting shell...");
+ sess.startShell();
+
+ out = sess.getStdin();
+ in = sess.getStdout();
+
+ mBuffer = "Welcome...";
+ mHandler.post(mUpdateView);
+
+ } catch (IOException e) {
+ Log.e("SSH", e.getMessage());
+ mConnectionMonitor.connectionLost(e);
+ return;
+ } catch (InterruptedException e) {
+ // This thread is coming to an end. Let us exit.
+ Log.e("SSH", "Connection thread interrupted.");
+ return;
+ }
+
+ byte[] buff = new byte[8192];
+ lines = new char[y][];
+
+ try {
+ while (true) {
+ int len = in.read(buff);
+ if (len == -1)
+ return;
+ addText(buff, len);
+ }
+ } catch (Exception e) {
+ Log.e("SSH", "Got exception reading: " + e.getMessage());
+ }
+ }
+
+ public void addText(byte[] data, int len) {
+ for (int i = 0; i < len; i++) {
+ char c = (char) (data[i] & 0xff);
+
+ if (c == 8) { // Backspace, VERASE
+ if (posx < 0)
+ continue;
+ posx--;
+ continue;
+ }
+ if (c == '\r') {
+ posx = 0;
+ continue;
+ }
+
+ if (c == '\n') {
+ posy++;
+ if (posy >= y) {
+ for (int k = 1; k < y; k++)
+ lines[k - 1] = lines[k];
+
+ posy--;
+ lines[y - 1] = new char[x];
+
+ for (int k = 0; k < x; k++)
+ lines[y - 1][k] = ' ';
+ }
+ continue;
+ }
+
+ if (c < 32) {
+ continue;
+ }
+
+ if (posx >= x) {
+ posx = 0;
+ posy++;
+ if (posy >= y) {
+ posy--;
+
+ for (int k = 1; k < y; k++)
+ lines[k - 1] = lines[k];
+ lines[y - 1] = new char[x];
+ for (int k = 0; k < x; k++)
+ lines[y - 1][k] = ' ';
+ }
+ }
+
+ if (lines[posy] == null) {
+ lines[posy] = new char[x];
+ for (int k = 0; k < x; k++)
+ lines[posy][k] = ' ';
+ }
+
+ lines[posy][posx] = c;
+ posx++;
+ }
+
+ StringBuffer sb = new StringBuffer(x * y);
+
+ for (int i = 0; i < lines.length; i++) {
+ if (i != 0)
+ sb.append('\n');
+
+ if (lines[i] != null)
+ sb.append(lines[i]);
+ }
+
+ mBuffer = sb.toString();
+ mHandler.post(mUpdateView);
+ }
+ }
+
+ @Override
+ public void onCreate(Bundle savedValues) {
+ super.onCreate(savedValues);
+
+ mContext = this;
+
+ setContentView(R.layout.secure_shell);
+
+ Log.d("SSH", "using URI " + getIntent().getData().toString());
+
+ mCursor = managedQuery(getIntent().getData(), PROJECTION, null, null);
+ mCursor.first();
+
+ mOutput = (TextView) findViewById(R.id.output);
+
+ mKMap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+
+ mConn = new ConnectionThread(
+ mCursor.getString(HOSTNAME_INDEX),
+ mCursor.getString(USERNAME_INDEX),
+ mCursor.getInt(PORT_INDEX));
+
+ Log.d("SSH", "Starting new ConnectionThread");
+ mConn.start();
+ }
+
+ public String askPassword() {
+ Intent intent = new Intent(this, PasswordDialog.class);
+ this.startSubActivity(intent, PASSWORD_REQUEST);
+ return null;
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode,
+ String data, Bundle extras)
+ {
+ if (requestCode == PASSWORD_REQUEST) {
+
+ // If the request was cancelled, then we didn't get anything.
+ if (resultCode == RESULT_CANCELED) {
+ return;
+
+ // Otherwise, there now should be a password ready for us.
+ } else {
+ mPassword = data;
+ sPass.release();
+ }
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+
+ if (sess != null) {
+ sess.close();
+ sess = null;
+ }
+
+ if (conn != null) {
+ conn.close();
+ conn = null;
+ }
+
+ finish();
+ }
+
+ @Override
+ public boolean onKeyDown(int keyCode, KeyEvent msg) {
+ if (out != null) {
+ int c = mKMap.get(keyCode, msg.getMetaState());
+ try {
+ out.write(c);
+ } catch (IOException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ }
+ }
+
+ return super.onKeyDown(keyCode, msg);
+ }
+
+ public void updateViewInUI() {
+ mOutput.setText(mBuffer);
+ }
+
+ final ConnectionMonitor mConnectionMonitor = new ConnectionMonitor() {
+ public void connectionLost(Throwable reason) {
+ Log.d("SSH", "Connection ended.");
+ Dialog d = new Dialog(mContext);
+ d.setTitle("Connection Lost");
+ d.setContentView(R.layout.message_dialog);
+
+ TextView msg = (TextView) d.findViewById(R.id.message);
+ msg.setText(reason.getMessage());
+
+ Button b = (Button) d.findViewById(R.id.dismiss);
+ b.setOnClickListener(new OnClickListener() {
+ public void onClick(View v) {
+ // TODO Auto-generated method stub
+ finish();
+ }
+ });
+ d.show();
+ finish();
+ }
+ };
+}
diff --git a/src/org/theb/ssh/ShellView.java b/src/org/theb/ssh/ShellView.java
new file mode 100644
index 0000000..15d783b
--- /dev/null
+++ b/src/org/theb/ssh/ShellView.java
@@ -0,0 +1,77 @@
+package org.theb.ssh;
+
+import java.io.IOException;
+
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.Session;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.Log;
+import android.widget.EditText;
+import android.widget.TextView;
+
+public class ShellView extends EditText {
+
+ private Connection mConn;
+ private Session mSess;
+
+ private String mHostname;
+ private String mUsername;
+ private String mPassword;
+
+ public ShellView(Context context) {
+ super(context);
+ // TODO Auto-generated constructor stub
+
+ mPassword = "OEfmP07-";
+ }
+
+ @Override
+ public void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ append("Connecting... ");
+ mConn = new Connection(mHostname);
+
+ try {
+ mConn.connect(new InteractiveHostKeyVerifier());
+ append("OK\n");
+ } catch (IOException e) {
+ append("Failed.\n");
+ Log.e("SSH", e.getMessage());
+ append("\nWhoops: " + e.getCause().getMessage() + "\n");
+ }
+
+ boolean enableKeyboardInteractive = true;
+ boolean enableDSA = true;
+ boolean enableRSA = true;
+
+ try {
+ while (true) {
+ /*
+ if ((enableDSA || enableRSA ) &&
+ mConn.isAuthMethodAvailable(username, "publickey");
+ */
+
+ if (mConn.isAuthMethodAvailable(mUsername, "password")) {
+ boolean res = mConn.authenticateWithPassword(mUsername, mPassword);
+ if (res == true)
+ break;
+
+ append("Login failed.\n");
+ continue;
+ }
+
+ throw new IOException("No supported authentication methods available.");
+ }
+
+ mSess = mConn.openSession();
+ append("Logged in as " + mUsername + ".\n");
+ } catch (IOException e) {
+ Log.e("SSH", e.getMessage());
+ }
+ append("Exiting\n");
+ }
+
+}