From dfa41d090260eed63f3d8510571a2f6236a5ff45 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Sat, 17 Nov 2007 05:58:42 +0000 Subject: Initial import. --- src/org/theb/provider/HostDb.java | 18 ++ src/org/theb/ssh/HostDbProvider.java | 211 +++++++++++++ src/org/theb/ssh/HostEditor.java | 194 ++++++++++++ src/org/theb/ssh/HostsList.java | 213 +++++++++++++ src/org/theb/ssh/InteractiveHostKeyVerifier.java | 14 + src/org/theb/ssh/PasswordDialog.java | 29 ++ src/org/theb/ssh/R.java | 57 ++++ src/org/theb/ssh/SecureShell.java | 363 +++++++++++++++++++++++ src/org/theb/ssh/ShellView.java | 77 +++++ 9 files changed, 1176 insertions(+) create mode 100644 src/org/theb/provider/HostDb.java create mode 100644 src/org/theb/ssh/HostDbProvider.java create mode 100644 src/org/theb/ssh/HostEditor.java create mode 100644 src/org/theb/ssh/HostsList.java create mode 100644 src/org/theb/ssh/InteractiveHostKeyVerifier.java create mode 100644 src/org/theb/ssh/PasswordDialog.java create mode 100644 src/org/theb/ssh/R.java create mode 100644 src/org/theb/ssh/SecureShell.java create mode 100644 src/org/theb/ssh/ShellView.java (limited to 'src/org/theb') 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 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(); + 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"); + } + +} -- cgit v1.2.3