aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--AndroidManifest.xml25
-rw-r--r--res/layout/act_about.xml37
-rw-r--r--res/xml/preferences.xml20
-rw-r--r--src/org/connectbot/AboutActivity.java17
-rw-r--r--src/org/connectbot/Console.java145
-rw-r--r--src/org/connectbot/FrontPage.java2
-rw-r--r--src/org/connectbot/HostList.java86
-rw-r--r--src/org/connectbot/R.java24
-rw-r--r--src/org/connectbot/SettingsActivity.java16
-rw-r--r--src/org/connectbot/service/TerminalBridge.java264
-rw-r--r--src/org/connectbot/service/TerminalManager.java64
-rw-r--r--src/org/connectbot/util/HostDatabase.java7
-rw-r--r--src/org/theb/ssh/HostsList.java2
13 files changed, 556 insertions, 153 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index a046a20..32caf4b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -8,12 +8,29 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
+ <intent-filter>
+ <action android:name="android.intent.action.CREATE_SHORTCUT" />
+ <category android:name="android.intent.category.DEFAULT" />
+ </intent-filter>
</activity>
- <activity android:name=".HostEditor">
+ <activity android:name=".HostEditor" />
+ <activity android:name=".SettingsActivity" />
+ <activity android:name=".AboutActivity" />
+
+ <service android:name="org.connectbot.service.TerminalManager" />
+
+ <activity android:name="org.connectbot.Console" android:screenOrientation="landscape">
+ <intent-filter>
+ <action android:name="android.intent.action.VIEW" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <data android:scheme="ssh" />
+ <!-- format: ssh://user@host:port/#nickname -->
+ </intent-filter>
</activity>
-<!-- pack=org.theb.ssh
+
+<!-- package=org.theb.ssh
<activity android:name=".Console" android:screenOrientation="landscape">
</activity>
@@ -21,10 +38,6 @@
-->
- <service android:name="org.connectbot.service.TerminalManager" />
-
- <activity android:name="org.connectbot.Console" android:screenOrientation="landscape">
- </activity>
<provider android:name="org.theb.ssh.HostDbProvider" android:authorities="org.theb.provider.HostDb"/>
diff --git a/res/layout/act_about.xml b/res/layout/act_about.xml
new file mode 100644
index 0000000..ad71137
--- /dev/null
+++ b/res/layout/act_about.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ScrollView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ >
+
+<LinearLayout
+ android:orientation="vertical"
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ >
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="5dip"
+ android:text="ConnectBot"
+ android:textAppearance="?android:attr/textAppearanceLarge"
+ />
+
+ <TextView
+ android:layout_width="fill_parent"
+ android:layout_height="wrap_content"
+ android:padding="5dip"
+ android:text="Copyright (c) 2007-2008 Kenny Root http://the-b.org/, Jeffrey Sharkey http://jsharkey.org/\nReleased under the GPLv3 license.\n\nBased in part on the Trilead SSH2 client, provided under a BSD-style license. Copyright (c) 2007 Trilead AG. http://www.trilead.com/\n\nAlso based on JTA Telnet/SSH client, provided under the GPLv2 license. Copyright (c) Matthias L. Jugel, Marcus Meiner 1996-2005. http://www.javassh.org/"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ />
+
+
+
+
+
+</LinearLayout>
+
+</ScrollView> \ No newline at end of file
diff --git a/res/xml/preferences.xml b/res/xml/preferences.xml
new file mode 100644
index 0000000..2d19670
--- /dev/null
+++ b/res/xml/preferences.xml
@@ -0,0 +1,20 @@
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <ListPreference
+ android:key="emulation"
+ android:title="Emulation mode"
+ android:summary="Terminal emulation mode to use for PTY connections"
+ android:entries="@array/list_emulation_modes"
+ android:defaultValue="screen"
+ android:entryValues="@array/list_emulation_modes"
+ />
+
+ <EditTextPreference
+ android:key="scrollback"
+ android:title="Scrollback size"
+ android:summary="Size of scrollback buffer to keep in memory for each console"
+ android:defaultValue="140"
+ />
+
+
+</PreferenceScreen> \ No newline at end of file
diff --git a/src/org/connectbot/AboutActivity.java b/src/org/connectbot/AboutActivity.java
new file mode 100644
index 0000000..a62c0ed
--- /dev/null
+++ b/src/org/connectbot/AboutActivity.java
@@ -0,0 +1,17 @@
+package org.connectbot;
+
+import android.app.Activity;
+import android.os.Bundle;
+
+import org.connectbot.R;
+
+
+public class AboutActivity extends Activity {
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.act_about);
+
+ }
+} \ No newline at end of file
diff --git a/src/org/connectbot/Console.java b/src/org/connectbot/Console.java
index 4493ac7..0561848 100644
--- a/src/org/connectbot/Console.java
+++ b/src/org/connectbot/Console.java
@@ -15,16 +15,21 @@ import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Color;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
+import android.text.ClipboardManager;
import android.util.Log;
import android.view.GestureDetector;
import android.view.KeyEvent;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
+import android.view.MenuItem.OnMenuItemClickListener;
import android.view.View.OnKeyListener;
import android.view.View.OnTouchListener;
import android.view.animation.Animation;
@@ -49,7 +54,26 @@ public class Console extends Activity {
// clear out any existing bridges and record requested index
flip.removeAllViews();
+ String requestedNickname = (requested != null) ? requested.getFragment() : null;
int requestedIndex = 0;
+
+
+ // first check if we need to create a new session for requested
+ boolean found = false;
+ for(TerminalBridge bridge : bound.bridges) {
+ if(bridge.nickname.equals(requestedNickname))
+ found = true;
+ }
+
+ if(!found) {
+ try {
+ Log.d(this.getClass().toString(), "onServiceConnected() is openConnection() because of unknown requested uri="+requested.toString());
+ bound.openConnection(requested);
+ } catch(Exception e) {
+ e.printStackTrace();
+ }
+ }
+
// create views for all bridges on this service
for(TerminalBridge bridge : bound.bridges) {
@@ -59,7 +83,7 @@ public class Console extends Activity {
// set the terminal overlay text
TextView overlay = (TextView)view.findViewById(R.id.terminal_overlay);
- overlay.setText(bridge.overlay);
+ overlay.setText(bridge.nickname);
// and add our terminal view control, using index to place behind overlay
TerminalView terminal = new TerminalView(Console.this, bridge);
@@ -70,7 +94,7 @@ public class Console extends Activity {
flip.addView(view);
// check to see if this bridge was requested
- if(bridge.overlay.equals(requestedBridge))
+ if(bridge.nickname.equals(requestedNickname))
requestedIndex = flip.getChildCount() - 1;
}
@@ -89,7 +113,7 @@ public class Console extends Activity {
};
public Animation fade_out = null;
- public String requestedBridge = null;
+ //public String requestedBridge = null;
public void updateDefault() {
// update the current default terminal
@@ -98,6 +122,33 @@ public class Console extends Activity {
bound.defaultBridge = terminal.bridge;
}
+ public Uri requested;
+
+ @Override
+ public void onStart() {
+ super.onStart();
+
+ // connect with manager service to find all bridges
+ // when connected it will insert all views
+ this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ this.unbindService(connection);
+
+ }
+
+ protected View findCurrentView(int id) {
+ View view = this.flip.getCurrentView();
+ if(view == null) return null;
+ return view.findViewById(id);
+ }
+
+ public ClipboardManager clipboard;
+
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
@@ -105,18 +156,19 @@ public class Console extends Activity {
this.requestWindowFeature(Window.FEATURE_NO_TITLE);
this.setContentView(R.layout.act_console);
+ this.clipboard = (ClipboardManager)this.getSystemService(CLIPBOARD_SERVICE);
+
// pull out any requested bridge
- this.requestedBridge = this.getIntent().getExtras().getString(Intent.EXTRA_TEXT);
+ //this.requestedBridge = this.getIntent().getExtras().getString(Intent.EXTRA_TEXT);
+
+ // handle requested console from incoming intent
+ this.requested = this.getIntent().getData();
this.inflater = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
this.flip = (ViewFlipper)this.findViewById(R.id.console_flip);
this.fade_out = AnimationUtils.loadAnimation(this, R.anim.fade_out);
-
- // connect with manager service to find all bridges
- // when connected it will insert all views
- this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
-
+
// preload animations for terminal switching
final Animation slide_left_in = AnimationUtils.loadAnimation(this, R.anim.slide_left_in);
@@ -141,7 +193,9 @@ public class Console extends Activity {
// activate consider if within x tolerance
if(Math.abs(e1.getX() - e2.getX()) < 100) {
- TerminalView terminal = (TerminalView)flip.getCurrentView().findViewById(R.id.console_flip);
+ View flip = findCurrentView(R.id.console_flip);
+ if(flip == null) return false;
+ TerminalView terminal = (TerminalView)flip;
// estimate how many rows we have scrolled through
// accumulate distance that doesn't trigger immediate scroll
@@ -193,7 +247,8 @@ public class Console extends Activity {
if(distx > goalwidth) {
// keep current overlay from popping up again
- flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_stay_hidden);
+ View overlay = findCurrentView(R.id.terminal_overlay);
+ if(overlay != null) overlay.startAnimation(fade_stay_hidden);
flip.setInAnimation(slide_right_in);
flip.setOutAnimation(slide_right_out);
@@ -201,14 +256,16 @@ public class Console extends Activity {
Console.this.updateDefault();
// show overlay on new slide and start fade
- flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out);
+ overlay = findCurrentView(R.id.terminal_overlay);
+ if(overlay != null) overlay.startAnimation(fade_out);
return true;
}
if(distx < -goalwidth) {
// keep current overlay from popping up again
- flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_stay_hidden);
+ View overlay = findCurrentView(R.id.terminal_overlay);
+ if(overlay != null) overlay.startAnimation(fade_stay_hidden);
flip.setInAnimation(slide_left_in);
flip.setOutAnimation(slide_left_out);
@@ -216,7 +273,8 @@ public class Console extends Activity {
Console.this.updateDefault();
// show overlay on new slide and start fade
- flip.getCurrentView().findViewById(R.id.terminal_overlay).startAnimation(fade_out);
+ overlay = findCurrentView(R.id.terminal_overlay);
+ if(overlay != null) overlay.startAnimation(fade_out);
return true;
}
@@ -245,5 +303,64 @@ public class Console extends Activity {
}
+
+ public MenuItem copy, paste;
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ super.onCreateOptionsMenu(menu);
+
+ MenuItem add = menu.add(0, 0, Menu.NONE, "Disconnect");
+ add.setIcon(android.R.drawable.ic_menu_close_clear_cancel);
+ add.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // close the currently visible session
+ View view = findCurrentView(R.id.console_flip);
+ if(view == null) return false;
+ TerminalView terminal = (TerminalView)view;
+ bound.disconnect(terminal.bridge);
+ flip.removeView(flip.getCurrentView());
+ return true;
+ }
+ });
+
+ copy = menu.add(0, 0, Menu.NONE, "Copy");
+ copy.setIcon(android.R.drawable.ic_menu_set_as);
+ copy.setEnabled(false);
+
+
+ paste = menu.add(0, 0, Menu.NONE, "Paste");
+ paste.setIcon(android.R.drawable.ic_menu_edit);
+ paste.setEnabled(clipboard.hasText());
+ paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ // force insert of clipboard text into current console
+ View view = findCurrentView(R.id.console_flip);
+ if(view == null) return false;
+ TerminalView terminal = (TerminalView)view;
+
+ // pull string from clipboard and generate all events to force down
+ String clip = clipboard.getText().toString();
+ KeyEvent[] events = terminal.bridge.keymap.getEvents(clip.toCharArray());
+ for(KeyEvent event : events) {
+ terminal.bridge.onKey(terminal, event.getKeyCode(), event);
+ }
+
+ return true;
+ }
+ });
+
+ return true;
+
+ }
+
+ @Override
+ public boolean onPrepareOptionsMenu(Menu menu) {
+ super.onPrepareOptionsMenu(menu);
+
+ paste.setEnabled(clipboard.hasText());
+
+ return true;
+ }
}
diff --git a/src/org/connectbot/FrontPage.java b/src/org/connectbot/FrontPage.java
index 4fa7734..e854c13 100644
--- a/src/org/connectbot/FrontPage.java
+++ b/src/org/connectbot/FrontPage.java
@@ -27,7 +27,7 @@ import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
-import org.theb.ssh.R;
+
public class FrontPage extends Activity {
diff --git a/src/org/connectbot/HostList.java b/src/org/connectbot/HostList.java
index 3689fbc..446346a 100644
--- a/src/org/connectbot/HostList.java
+++ b/src/org/connectbot/HostList.java
@@ -22,6 +22,7 @@ import android.content.DialogInterface;
import android.content.Intent;
import android.content.ServiceConnection;
import android.database.Cursor;
+import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.view.ContextMenu;
@@ -47,20 +48,8 @@ public class HostList extends Activity {
public void onServiceConnected(ComponentName className, IBinder service) {
bound = ((TerminalManager.TerminalBinder) service).getService();
- // TODO: update our green bulb icons by checking for existing
- // bridges
- // open up some test sessions
- // try {
- // bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt",
- // "screen", 100);
- // bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt",
- // "screen", 100);
- // bound.openConnection("192.168.254.230", 22, "connectbot", "b0tt",
- // "screen", 100);
- // } catch(Exception e) {
- // e.printStackTrace();
- // }
-
+ // TODO: update our green bulb icons by checking for existing bridges
+
}
public void onServiceDisconnected(ComponentName className) {
@@ -73,17 +62,29 @@ public class HostList extends Activity {
public ListView list;
public HostAdapter adapter;
- public int COL_ID, COL_NICKNAME, COL_USERNAME, COL_HOSTNAME, COL_CONNECTED;
+ public int COL_ID, COL_NICKNAME, COL_USERNAME, COL_HOSTNAME, COL_CONNECTED, COL_PORT;
@Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- setContentView(R.layout.act_frontpage);
+ public void onStart() {
+ super.onStart();
// start the terminal manager service
this.startService(new Intent(this, TerminalManager.class));
- this.bindService(new Intent(this, TerminalManager.class), connection,
- Context.BIND_AUTO_CREATE);
+ this.bindService(new Intent(this, TerminalManager.class), connection, Context.BIND_AUTO_CREATE);
+
+ }
+
+ @Override
+ public void onStop() {
+ super.onStop();
+ this.unbindService(connection);
+
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setContentView(R.layout.act_frontpage);
// connect with hosts database and populate list
this.hostdb = new HostDatabase(this);
@@ -96,17 +97,23 @@ public class HostList extends Activity {
this.COL_NICKNAME = hosts.getColumnIndexOrThrow(HostDatabase.FIELD_HOST_NICKNAME);
this.COL_USERNAME = hosts.getColumnIndexOrThrow(HostDatabase.FIELD_HOST_USERNAME);
this.COL_HOSTNAME = hosts.getColumnIndexOrThrow(HostDatabase.FIELD_HOST_HOSTNAME);
+ this.COL_PORT = hosts.getColumnIndexOrThrow(HostDatabase.FIELD_HOST_PORT);
this.COL_CONNECTED = hosts.getColumnIndexOrThrow(HostDatabase.FIELD_HOST_LASTCONNECT);
this.list.setOnItemClickListener(new OnItemClickListener() {
- public void onItemClick(AdapterView<?> parent, View view,
- int position, long id) {
+ public synchronized void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// launch off to console details
- // TODO: package information about connection selected
- HostList.this.startActivity(new Intent(HostList.this,
- Console.class));
+ Cursor c = (Cursor)parent.getAdapter().getItem(position);
+ String username = c.getString(COL_USERNAME);
+ String hostname = c.getString(COL_HOSTNAME);
+ int port = c.getInt(COL_PORT);
+ String nickname = c.getString(COL_NICKNAME);
+
+ Intent intent = new Intent(HostList.this, Console.class);
+ intent.setData(Uri.parse(String.format("ssh://%s@%s:%s/#%s", username, hostname, port, nickname)));
+ HostList.this.startActivity(intent);
}
@@ -115,8 +122,7 @@ public class HostList extends Activity {
this.registerForContextMenu(this.list);
final Pattern hostmask = Pattern.compile(".+@.+(:\\d+)?");
- final TextView text = (TextView) this
- .findViewById(R.id.front_quickconnect);
+ final TextView text = (TextView) this.findViewById(R.id.front_quickconnect);
text.setOnKeyListener(new OnKeyListener() {
public boolean onKey(View v, int keyCode, KeyEvent event) {
@@ -178,7 +184,7 @@ public class HostList extends Activity {
add.setIcon(android.R.drawable.ic_menu_add);
add.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
- return false;
+ return true;
}
});
@@ -188,7 +194,7 @@ public class HostList extends Activity {
public boolean onMenuItemClick(MenuItem item) {
sortedByColor = true;
updateCursor();
- return false;
+ return true;
}
});
@@ -198,7 +204,7 @@ public class HostList extends Activity {
public boolean onMenuItemClick(MenuItem item) {
sortedByColor = false;
updateCursor();
- return false;
+ return true;
}
});
@@ -207,13 +213,31 @@ public class HostList extends Activity {
MenuItem settings = menu.add(0, 0, Menu.NONE, "Settings");
settings.setIcon(android.R.drawable.ic_menu_preferences);
+ settings.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ HostList.this.startActivity(new Intent(HostList.this, SettingsActivity.class));
+ return true;
+ }
+ });
MenuItem about = menu.add(0, 0, Menu.NONE, "About");
about.setIcon(android.R.drawable.ic_menu_help);
+ about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
+ public boolean onMenuItemClick(MenuItem item) {
+ HostList.this.startActivity(new Intent(HostList.this, AboutActivity.class));
+ return true;
+ }
+ });
return true;
}
+
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ this.updateCursor();
+ }
+
+ public final static int REQUEST_EDIT = 1;
@Override
public void onCreateContextMenu(ContextMenu menu, View v,
@@ -242,7 +266,7 @@ public class HostList extends Activity {
public boolean onMenuItemClick(MenuItem item) {
Intent intent = new Intent(HostList.this, HostEditor.class);
intent.putExtra(Intent.EXTRA_TITLE, id);
- HostList.this.startActivity(intent);
+ HostList.this.startActivityForResult(intent, REQUEST_EDIT);
return false;
}
});
diff --git a/src/org/connectbot/R.java b/src/org/connectbot/R.java
index b1a337f..027ba6c 100644
--- a/src/org/connectbot/R.java
+++ b/src/org/connectbot/R.java
@@ -70,17 +70,18 @@ public final class R {
}
public static final class layout {
public static final int about_dialog=0x7f030000;
- public static final int act_console=0x7f030001;
- public static final int act_frontpage=0x7f030002;
- public static final int act_hosteditor=0x7f030003;
- public static final int host_editor=0x7f030004;
- public static final int item_host=0x7f030005;
- public static final int item_terminal=0x7f030006;
- public static final int main=0x7f030007;
- public static final int message_dialog=0x7f030008;
- public static final int password_dialog=0x7f030009;
- public static final int pubkey=0x7f03000a;
- public static final int secure_shell=0x7f03000b;
+ public static final int act_about=0x7f030001;
+ public static final int act_console=0x7f030002;
+ public static final int act_frontpage=0x7f030003;
+ public static final int act_hosteditor=0x7f030004;
+ public static final int host_editor=0x7f030005;
+ public static final int item_host=0x7f030006;
+ public static final int item_terminal=0x7f030007;
+ public static final int main=0x7f030008;
+ public static final int message_dialog=0x7f030009;
+ public static final int password_dialog=0x7f03000a;
+ public static final int pubkey=0x7f03000b;
+ public static final int secure_shell=0x7f03000c;
}
public static final class string {
public static final int alert_disconnect_msg=0x7f070014;
@@ -109,5 +110,6 @@ public final class R {
}
public static final class xml {
public static final int host_prefs=0x7f050000;
+ public static final int preferences=0x7f050001;
}
}
diff --git a/src/org/connectbot/SettingsActivity.java b/src/org/connectbot/SettingsActivity.java
new file mode 100644
index 0000000..2036958
--- /dev/null
+++ b/src/org/connectbot/SettingsActivity.java
@@ -0,0 +1,16 @@
+package org.connectbot;
+
+import android.os.Bundle;
+import android.preference.PreferenceActivity;
+
+public class SettingsActivity extends PreferenceActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.preferences);
+
+ }
+
+}
+
diff --git a/src/org/connectbot/service/TerminalBridge.java b/src/org/connectbot/service/TerminalBridge.java
index ed2424c..cadaf58 100644
--- a/src/org/connectbot/service/TerminalBridge.java
+++ b/src/org/connectbot/service/TerminalBridge.java
@@ -4,6 +4,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
+import org.theb.ssh.InteractiveHostKeyVerifier;
import org.theb.ssh.JTATerminalView;
import android.graphics.Bitmap;
@@ -21,8 +22,12 @@ import android.view.SurfaceHolder;
import android.view.View;
import android.view.View.OnKeyListener;
+import com.trilead.ssh2.InteractiveCallback;
+import com.trilead.ssh2.KnownHosts;
+import com.trilead.ssh2.ServerHostKeyVerifier;
import com.trilead.ssh2.Session;
import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.signature.RSAPublicKey;
import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
@@ -33,72 +38,170 @@ import de.mud.terminal.vt320;
public class TerminalBridge implements VDUDisplay, OnKeyListener {
public final Connection connection;
- public final Session session;
- public final String overlay;
+ public final String nickname, username;
+ public final Paint defaultPaint;
+ public Session session;
public final static int TERM_WIDTH_CHARS = 80,
TERM_HEIGHT_CHARS = 24,
DEFAULT_FONT_SIZE = 10;
public final static String ENCODING = "ASCII";
+ public static final String AUTH_PUBLICKEY = "publickey",
+ AUTH_PASSWORD = "password";
- public final OutputStream stdin;
- public final InputStream stdout;
-
- public final Paint defaultPaint;
+ public OutputStream stdin;
+ public InputStream stdout;
- public final Thread relay;
+ public Thread relay;
public View parent = null;
public Bitmap bitmap = null;
public Canvas canvas = new Canvas();
public VDUBuffer buffer = null;
- public TerminalBridge(Connection connection, String overlay, String emulation, int scrollback) throws Exception {
- // create a terminal bridge from an SSH connection over to a SurfaceHolder
- // will open a new session and handle rendering to the Surface if present
+
+ public class HostKeyVerifier implements ServerHostKeyVerifier {
- try {
- this.connection = connection;
- this.session = connection.openSession();
- this.session.requestPTY(emulation, 0, 0, 0, 0, null); // previously tried vt100, xterm, but "screen" works the best
- this.session.startShell();
-
- this.overlay = overlay;
+ // hex routine adapted from
+ // http://forums.sun.com/thread.jspa?threadID=252591&messageID=2272668
+
+ private final char[] hex = "0123456789abcdef".toCharArray();
+
+ public String hexdump(byte[] buf) {
+ StringBuffer out = new StringBuffer();
+ for(int i = 0; i < buf.length; i++) {
+ int value = buf[i] + 127;
+ out.append(":" + hex[(value>>>4)&0xf] + hex[value&0xf]);
+ }
+ return out.toString().substring(1);
+ }
+
+ public boolean verifyServerHostKey(String hostname, int port, String serverHostKeyAlgorithm, byte[] serverHostKey) throws Exception {
+ // TODO: check against known key, prompt user if unknown or missing key
- // grab stdin/out from newly formed session
- this.stdin = this.session.getStdin();
- this.stdout = this.session.getStdout();
+ KnownHosts hosts = new KnownHosts();
+ switch(hosts.verifyHostkey(hostname, serverHostKeyAlgorithm, serverHostKey)) {
+ case KnownHosts.HOSTKEY_IS_OK:
+ return true;
+
+ case KnownHosts.HOSTKEY_IS_NEW:
+ // prompt user
+ outputLine(String.format("The authenticity of host '%s' can't be established.", hostname));
+ outputLine(String.format("RSA key fingerprint is %s", hosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey)));
+ outputLine("Are you sure you want to continue connecting (yes/no)? ");
+ return true;
+
+ case KnownHosts.HOSTKEY_HAS_CHANGED:
+ return false;
+
+ }
- // create our default paint
- this.defaultPaint = new Paint();
- this.defaultPaint.setAntiAlias(true);
- this.defaultPaint.setTypeface(Typeface.MONOSPACE);
- this.setFontSize(DEFAULT_FONT_SIZE);
+ return false;
- // create terminal buffer and handle outgoing data
- // this is probably status reply information
- this.buffer = new vt320() {
- public void write(byte[] b) {
- try {
- //Log.d("STDIN", new String(b));
- TerminalBridge.this.stdin.write(b);
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
+ }
+
+ }
+
+ public TerminalBridge(final String nickname, final String username, final String hostname, final int port) throws Exception {
+ // newer version of TerminalBridge that will dump all connection progress to the display
+
+ this.nickname = nickname;
+ this.username = username;
+
+ // create our default paint
+ this.defaultPaint = new Paint();
+ this.defaultPaint.setAntiAlias(true);
+ this.defaultPaint.setTypeface(Typeface.MONOSPACE);
+ this.setFontSize(DEFAULT_FONT_SIZE);
- public void sendTelnetCommand(byte cmd) {
+ for(int i = 0; i < color.length; i++)
+ this.darkerColor[i] = darken(color[i]);
+
+ // create terminal buffer and handle outgoing data
+ // this is probably status reply information
+ this.buffer = new vt320() {
+ public void write(byte[] b) {
+ try {
+ TerminalBridge.this.stdin.write(b);
+ } catch (IOException e) {
+ e.printStackTrace();
}
+ }
+
+ public void sendTelnetCommand(byte cmd) {
+ }
+
+ public void setWindowSize(int c, int r) {
+ }
+ };
- public void setWindowSize(int c, int r) {
+ this.scrollback = scrollback;
+ this.buffer.setScreenSize(TERM_WIDTH_CHARS, TERM_HEIGHT_CHARS, true);
+ this.buffer.setBufferSize(scrollback);
+ this.buffer.setDisplay(this);
+ this.buffer.setCursorPosition(0, 0);
+
+ // try opening ssh connection
+ this.outputLine(String.format("Connecting to %s:%d", hostname, port));
+ this.connection = new Connection(hostname, port);
+
+ new Thread(new Runnable() {
+
+ public void run() {
+ try {
+ connection.connect(new HostKeyVerifier());
+ outputLine("Trying to authenticate");
+ if(connection.isAuthMethodAvailable(username, AUTH_PASSWORD)) {
+ // show auth prompt in window
+ promptPassword();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
}
- };
- this.scrollback = scrollback;
- this.buffer.setScreenSize(TERM_WIDTH_CHARS, TERM_HEIGHT_CHARS, true);
- this.buffer.setBufferSize(scrollback);
- this.buffer.setDisplay(this);
+ }
+ }).start();
+
+ }
+
+ public void outputLine(String line) {
+ this.buffer.putString(0, this.buffer.getCursorRow(), line);
+ this.buffer.setCursorPosition(0, this.buffer.getCursorRow() + 1);
+ this.redraw();
+ }
+
+ public void promptPassword() {
+ this.outputLine("Password: ");
+ }
+
+ public void tryPassword(String password) {
+ try {
+ // try authenticating with given password
+ Log.d(this.getClass().toString(), String.format("tryPassword(password=%s) and username=%s", password, username));
+ if(this.connection.authenticateWithPassword(this.username, password)) {
+ finishConnection();
+ return;
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ this.outputLine("Permission denied, please try again.");
+ this.promptPassword();
+ }
+
+
+ public void finishConnection() {
+
+ try {
+ this.session = connection.openSession();
+ this.session.requestPTY("screen", 0, 0, 0, 0, null); // previously tried vt100, xterm, but "screen" works the best
+ this.session.startShell();
+
+ // grab stdin/out from newly formed session
+ this.stdin = this.session.getStdin();
+ this.stdout = this.session.getStdout();
+
// create thread to relay incoming connection data to buffer
this.relay = new Thread(new Runnable() {
public void run() {
@@ -120,25 +223,31 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener {
}
});
this.relay.start();
-
- } catch(Exception e) {
- throw e;
+
+ // force font-size to make sure we resizePTY as needed
+ this.setFontSize(this.fontSize);
+
+ } catch (IOException e1) {
+ // TODO Auto-generated catch block
+ e1.printStackTrace();
}
- for(int i = 0; i < color.length; i++)
- this.darkerColor[i] = darken(color[i]);
-
}
public void dispose() {
- this.session.close();
+ if(this.session != null)
+ this.session.close();
this.connection.close();
}
- KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+ public KeyCharacterMap keymap = KeyCharacterMap.load(KeyCharacterMap.BUILT_IN_KEYBOARD);
+
+ StringBuffer collected = new StringBuffer();
public boolean onKey(View v, int keyCode, KeyEvent event) {
// pass through any keystrokes to output stream
+
+ Log.d(this.getClass().toString(), "onKey() code="+keyCode);
if(event.getAction() == KeyEvent.ACTION_UP) return false;
try {
@@ -151,21 +260,42 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener {
return true;
}
- // print normal keys
- if(keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE) {
- int key = keymap.get(keyCode, event.getMetaState());
- this.stdin.write(key);
- return true;
- }
+ boolean printing = (keymap.isPrintingKey(keyCode) || keyCode == KeyEvent.KEYCODE_SPACE);
- // look for special chars
- switch(keyCode) {
- case KeyEvent.KEYCODE_DEL: stdin.write(0x08); return true;
- case KeyEvent.KEYCODE_ENTER: ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', event.getMetaState()); return true;
- case KeyEvent.KEYCODE_DPAD_LEFT: ((vt320)buffer).keyPressed(vt320.KEY_LEFT, ' ', event.getMetaState()); return true;
- case KeyEvent.KEYCODE_DPAD_UP: ((vt320)buffer).keyPressed(vt320.KEY_UP, ' ', event.getMetaState()); return true;
- case KeyEvent.KEYCODE_DPAD_DOWN: ((vt320)buffer).keyPressed(vt320.KEY_DOWN, ' ', event.getMetaState()); return true;
- case KeyEvent.KEYCODE_DPAD_RIGHT: ((vt320)buffer).keyPressed(vt320.KEY_RIGHT, ' ', event.getMetaState()); return true;
+ if(this.session == null) {
+ // check to see if we are collecting password information
+ if(keyCode == KeyEvent.KEYCODE_ENTER) {
+ this.tryPassword(collected.toString());
+ collected = new StringBuffer();
+ return true;
+ } else if(printing) {
+ collected.appendCodePoint(keymap.get(keyCode, event.getMetaState()));
+ return true;
+ } else if(keyCode == KeyEvent.KEYCODE_DEL && collected.length() > 0) {
+ collected.deleteCharAt(collected.length() - 1);
+ return true;
+ }
+
+ } else {
+
+ // otherwise pass through to existing session
+ // print normal keys
+ if (printing) {
+ int key = keymap.get(keyCode, event.getMetaState());
+ this.stdin.write(key);
+ return true;
+ }
+
+ // look for special chars
+ switch(keyCode) {
+ case KeyEvent.KEYCODE_DEL: stdin.write(0x08); return true;
+ case KeyEvent.KEYCODE_ENTER: ((vt320)buffer).keyTyped(vt320.KEY_ENTER, ' ', event.getMetaState()); return true;
+ case KeyEvent.KEYCODE_DPAD_LEFT: ((vt320)buffer).keyPressed(vt320.KEY_LEFT, ' ', event.getMetaState()); return true;
+ case KeyEvent.KEYCODE_DPAD_UP: ((vt320)buffer).keyPressed(vt320.KEY_UP, ' ', event.getMetaState()); return true;
+ case KeyEvent.KEYCODE_DPAD_DOWN: ((vt320)buffer).keyPressed(vt320.KEY_DOWN, ' ', event.getMetaState()); return true;
+ case KeyEvent.KEYCODE_DPAD_RIGHT: ((vt320)buffer).keyPressed(vt320.KEY_RIGHT, ' ', event.getMetaState()); return true;
+ }
+
}
} catch (IOException e) {
@@ -173,6 +303,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener {
}
return false;
}
+
public int charWidth = -1,
charHeight = -1,
@@ -227,7 +358,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener {
try {
buffer.setScreenSize(termWidth, termHeight, true);
- session.resizePTY(termWidth, termHeight);
+ if(session != null)
+ session.resizePTY(termWidth, termHeight);
} catch(Exception e) {
e.printStackTrace();
}
diff --git a/src/org/connectbot/service/TerminalManager.java b/src/org/connectbot/service/TerminalManager.java
index 72203ba..0f4da75 100644
--- a/src/org/connectbot/service/TerminalManager.java
+++ b/src/org/connectbot/service/TerminalManager.java
@@ -9,6 +9,7 @@ import com.trilead.ssh2.Connection;
import android.app.Service;
import android.content.Intent;
+import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
@@ -31,37 +32,58 @@ public class TerminalManager extends Service {
bridge.dispose();
}
- public void openConnection(Connection conn, String nickname, String emulation, int scrollback) throws Exception {
- try {
- TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback);
- this.bridges.add(bridge);
- } catch (Exception e) {
- throw e;
- }
+// public void openConnection(Connection conn, String nickname, String emulation, int scrollback) throws Exception {
+// try {
+// TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback);
+// this.bridges.add(bridge);
+// } catch (Exception e) {
+// throw e;
+// }
+// }
+//
+// public void openConnection(String nickname, String host, int port, String user, String pass, String emulation, int scrollback) throws Exception {
+// try {
+// Connection conn = new Connection(host, port);
+// conn.connect(new InteractiveHostKeyVerifier());
+// if(conn.isAuthMethodAvailable(user, "password")) {
+// conn.authenticateWithPassword(user, pass);
+// }
+// TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback);
+// this.bridges.add(bridge);
+// } catch (Exception e) {
+// throw e;
+// }
+// }
+
+ public void openConnection(String nickname, String hostname, String username, int port) throws Exception {
+ TerminalBridge bridge = new TerminalBridge(nickname, username, hostname, port);
+ this.bridges.add(bridge);
}
-
- public void openConnection(String nickname, String host, int port, String user, String pass, String emulation, int scrollback) throws Exception {
- try {
- Connection conn = new Connection(host, port);
- conn.connect(new InteractiveHostKeyVerifier());
- if(conn.isAuthMethodAvailable(user, "password")) {
- conn.authenticateWithPassword(user, pass);
- }
- TerminalBridge bridge = new TerminalBridge(conn, nickname, emulation, scrollback);
- this.bridges.add(bridge);
- } catch (Exception e) {
- throw e;
- }
+
+ public void openConnection(Uri uri) throws Exception {
+ String nickname = uri.getFragment();
+ String username = uri.getUserInfo();
+ String hostname = uri.getHost();
+ int port = uri.getPort();
+
+ TerminalBridge bridge = new TerminalBridge(nickname, username, hostname, port);
+ this.bridges.add(bridge);
}
public TerminalBridge findBridge(String nickname) {
// find the first active bridge with given nickname
for(TerminalBridge bridge : bridges) {
- if(bridge.overlay.equals(nickname))
+ if(bridge.nickname.equals(nickname))
return bridge;
}
return null;
}
+
+ public void disconnect(TerminalBridge bridge) {
+ bridge.dispose();
+ this.bridges.remove(bridge);
+
+ }
public class TerminalBinder extends Binder {
diff --git a/src/org/connectbot/util/HostDatabase.java b/src/org/connectbot/util/HostDatabase.java
index bbc27f9..586246b 100644
--- a/src/org/connectbot/util/HostDatabase.java
+++ b/src/org/connectbot/util/HostDatabase.java
@@ -13,7 +13,7 @@ import android.database.sqlite.SQLiteOpenHelper;
public class HostDatabase extends SQLiteOpenHelper {
public final static String DB_NAME = "hosts";
- public final static int DB_VERSION = 4;
+ public final static int DB_VERSION = 5;
public final static String TABLE_HOSTS = "hosts";
public final static String FIELD_HOST_NICKNAME = "nickname";
@@ -23,6 +23,7 @@ public class HostDatabase extends SQLiteOpenHelper {
public final static String FIELD_HOST_HOSTKEY = "hostkey";
public final static String FIELD_HOST_LASTCONNECT = "lastconnect";
public final static String FIELD_HOST_COLOR = "color";
+ public final static String FIELD_HOST_USEKEYS = "usekeys";
public final static String TABLE_PRIVKEYS = "keys";
public final static String FIELD_KEY_NAME = "name";
@@ -46,7 +47,8 @@ public class HostDatabase extends SQLiteOpenHelper {
+ FIELD_HOST_PORT + " INTEGER, "
+ FIELD_HOST_HOSTKEY + " TEXT, "
+ FIELD_HOST_LASTCONNECT + " INTEGER, "
- + FIELD_HOST_COLOR + " TEXT)");
+ + FIELD_HOST_COLOR + " TEXT, "
+ + FIELD_HOST_USEKEYS + " TEXT)");
db.execSQL("CREATE TABLE " + TABLE_PRIVKEYS
+ " (_id INTEGER PRIMARY KEY, "
@@ -78,6 +80,7 @@ public class HostDatabase extends SQLiteOpenHelper {
values.put(FIELD_HOST_HOSTNAME, hostname);
values.put(FIELD_HOST_PORT, port);
values.put(FIELD_HOST_LASTCONNECT, Integer.MAX_VALUE);
+ values.put(FIELD_HOST_USEKEYS, Boolean.toString(true));
if(color != null)
values.put(FIELD_HOST_COLOR, color);
diff --git a/src/org/theb/ssh/HostsList.java b/src/org/theb/ssh/HostsList.java
index 5760f46..eb0bc53 100644
--- a/src/org/theb/ssh/HostsList.java
+++ b/src/org/theb/ssh/HostsList.java
@@ -344,7 +344,7 @@ public class HostsList extends ListActivity {
//Connection conn;
//bound.openConnection(conn, nickname, emulation, scrollback);
if (password != null) {
- bound.openConnection(nickname, hostname, port, username, password, "screen", 100);
+ //bound.openConnection(nickname, hostname, port, username, password, "screen", 100);
// open the console view and select this specific terminal
Intent intent = new Intent(HostsList.this, Console.class);