aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2009-06-15 16:35:59 +0000
committerKenny Root <kenny@the-b.org>2009-06-15 16:35:59 +0000
commit534184380cd7762bc1be3c5174af001e33f9b08d (patch)
treee0644be305ec82ce9ec51f66333deaec3a96070f
parent2adc14e72659787a09aa4b9b40334dc0e1df9194 (diff)
downloadconnectbot-534184380cd7762bc1be3c5174af001e33f9b08d.tar.gz
connectbot-534184380cd7762bc1be3c5174af001e33f9b08d.tar.bz2
connectbot-534184380cd7762bc1be3c5174af001e33f9b08d.zip
Add multiple transports
git-svn-id: https://connectbot.googlecode.com/svn/trunk/connectbot@298 df292f66-193f-0410-a5fc-6d59da041ff2
-rw-r--r--AndroidManifest.xml7
-rw-r--r--res/layout/act_hostlist.xml70
-rw-r--r--src/de/mud/telnet/TelnetProtocolHandler.java648
-rw-r--r--src/org/connectbot/ConsoleActivity.java21
-rw-r--r--src/org/connectbot/HostListActivity.java61
-rw-r--r--src/org/connectbot/bean/HostBean.java23
-rw-r--r--src/org/connectbot/service/Relay.java72
-rw-r--r--src/org/connectbot/service/TerminalBridge.java657
-rw-r--r--src/org/connectbot/service/TerminalManager.java38
-rw-r--r--src/org/connectbot/transport/AbsTransport.java237
-rw-r--r--src/org/connectbot/transport/Local.java215
-rw-r--r--src/org/connectbot/transport/SSH.java785
-rw-r--r--src/org/connectbot/transport/Telnet.java284
-rw-r--r--src/org/connectbot/transport/TransportFactory.java86
-rw-r--r--src/org/connectbot/util/HostDatabase.java237
15 files changed, 2644 insertions, 797 deletions
diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 3292306..64d8dc0 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -28,7 +28,7 @@
<activity android:name=".WizardActivity" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".HelpActivity" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".HelpTopicActivity" android:configChanges="keyboardHidden|orientation" />
-
+
<service android:name="org.connectbot.service.TerminalManager" android:configChanges="keyboardHidden|orientation" />
<activity android:name=".ConsoleActivity" android:configChanges="keyboardHidden|orientation"
@@ -37,7 +37,11 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:scheme="ssh" />
+ <data android:scheme="telnet" />
+ <data android:scheme="local" />
<!-- format: ssh://user@host:port/#nickname -->
+ <!-- format: telnet://host:port/#nickname -->
+ <!-- format: local:// -->
</intent-filter>
</activity>
@@ -49,5 +53,4 @@
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
-
</manifest>
diff --git a/res/layout/act_hostlist.xml b/res/layout/act_hostlist.xml
index 0b655bb..e9960c9 100644
--- a/res/layout/act_hostlist.xml
+++ b/res/layout/act_hostlist.xml
@@ -17,50 +17,46 @@
along with this program. If not, see <http://www.gnu.org/licenses/>
-->
-<LinearLayout
+<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
>
- <RelativeLayout
- android:layout_width="fill_parent"
- android:layout_height="0dip"
- android:layout_weight="1"
- >
-
- <ListView
- android:id="@android:id/list"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- />
-
- <TextView
- android:id="@android:id/empty"
- android:layout_width="fill_parent"
- android:layout_height="fill_parent"
- android:text="@string/list_host_empty"
- android:textAppearance="?android:attr/textAppearanceMedium"
- android:gravity="center"
- />
-
- </RelativeLayout>
-
- <FrameLayout
+ <Spinner
+ android:id="@+id/transport_selection"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_alignParentBottom="true"
+ android:layout_alignParentLeft="true"
+ />
+
+ <EditText
+ android:id="@+id/front_quickconnect"
+ android:singleLine="true"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
- android:padding="5dip"
- >
+ android:hint="@string/hint_userhost"
+ android:layout_toRightOf="@+id/transport_selection"
+ android:layout_alignTop="@+id/transport_selection"
+ />
- <EditText
- android:id="@+id/front_quickconnect"
- android:singleLine="true"
- android:layout_width="fill_parent"
- android:layout_height="wrap_content"
- android:hint="@string/hint_userhost"
- android:inputType="textEmailAddress"
- />
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:layout_above="@+id/transport_selection"
+ />
- </FrameLayout>
-</LinearLayout>
+ <TextView
+ android:id="@android:id/empty"
+ android:layout_width="fill_parent"
+ android:layout_height="fill_parent"
+ android:text="@string/list_host_empty"
+ android:textAppearance="?android:attr/textAppearanceMedium"
+ android:gravity="center"
+ android:layout_above="@+id/transport_selection"
+ />
+
+</RelativeLayout>
diff --git a/src/de/mud/telnet/TelnetProtocolHandler.java b/src/de/mud/telnet/TelnetProtocolHandler.java
new file mode 100644
index 0000000..3d24d12
--- /dev/null
+++ b/src/de/mud/telnet/TelnetProtocolHandler.java
@@ -0,0 +1,648 @@
+/*
+ * This file is part of "JTA - Telnet/SSH for the JAVA(tm) platform".
+ *
+ * (c) Matthias L. Jugel, Marcus Meißner 1996-2005. All Rights Reserved.
+ *
+ * Please visit http://javatelnet.org/ for updates and contact.
+ *
+ * --LICENSE NOTICE--
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+ * --LICENSE NOTICE--
+ *
+ */
+
+package de.mud.telnet;
+
+import java.io.IOException;
+/**
+ * This is a telnet protocol handler. The handler needs implementations
+ * for several methods to handle the telnet options and to be able to
+ * read and write the buffer.
+ * <P>
+ * <B>Maintainer:</B> Marcus Meissner
+ *
+ * @version $Id: TelnetProtocolHandler.java 503 2005-10-24 07:34:13Z marcus $
+ * @author Matthias L. Jugel, Marcus Meissner
+ */
+public abstract class TelnetProtocolHandler {
+ /** contains the current revision id */
+ public final static String ID = "$Id: TelnetProtocolHandler.java 503 2005-10-24 07:34:13Z marcus $";
+
+ /** debug level */
+ private final static int debug = 0;
+
+ /** temporary buffer for data-telnetstuff-data transformation */
+ private byte[] tempbuf = new byte[0];
+
+ /** the data sent on pressing <RETURN> \n */
+ private byte[] crlf = new byte[2];
+ /** the data sent on pressing <LineFeed> \r */
+ private byte[] cr = new byte[2];
+
+ /**
+ * Create a new telnet protocol handler.
+ */
+ public TelnetProtocolHandler() {
+ reset();
+
+ crlf[0] = 13; crlf[1] = 10;
+ cr[0] = 13; cr[1] = 0;
+ }
+
+ /**
+ * Get the current terminal type for TTYPE telnet option.
+ * @return the string id of the terminal
+ */
+ protected abstract String getTerminalType();
+
+ /**
+ * Get the current window size of the terminal for the
+ * NAWS telnet option.
+ * @return the size of the terminal as Dimension
+ */
+ protected abstract int[] getWindowSize();
+
+ /**
+ * Set the local echo option of telnet.
+ * @param echo true for local echo, false for no local echo
+ */
+ protected abstract void setLocalEcho(boolean echo);
+
+ /**
+ * Generate an EOR (end of record) request. For use by prompt displaying.
+ */
+ protected abstract void notifyEndOfRecord();
+
+ /**
+ * Send data to the remote host.
+ * @param b array of bytes to send
+ */
+ protected abstract void write(byte[] b) throws IOException;
+
+ /**
+ * Send one byte to the remote host.
+ * @param b the byte to be sent
+ * @see #write(byte[] b)
+ */
+ private static byte[] one = new byte[1];
+ private void write(byte b) throws IOException {
+ one[0] = b;
+ write(one);
+ }
+
+ /**
+ * Reset the protocol handler. This may be necessary after the
+ * connection was closed or some other problem occured.
+ */
+ public void reset() {
+ neg_state = 0;
+ receivedDX = new byte[256];
+ sentDX = new byte[256];
+ receivedWX = new byte[256];
+ sentWX = new byte[256];
+ }
+
+ // ===================================================================
+ // the actual negotiation handling for the telnet protocol follows:
+ // ===================================================================
+
+ /** state variable for telnet negotiation reader */
+ private byte neg_state = 0;
+
+ /** constants for the negotiation state */
+ private final static byte STATE_DATA = 0;
+ private final static byte STATE_IAC = 1;
+ private final static byte STATE_IACSB = 2;
+ private final static byte STATE_IACWILL = 3;
+ private final static byte STATE_IACDO = 4;
+ private final static byte STATE_IACWONT = 5;
+ private final static byte STATE_IACDONT = 6;
+ private final static byte STATE_IACSBIAC = 7;
+ private final static byte STATE_IACSBDATA = 8;
+ private final static byte STATE_IACSBDATAIAC = 9;
+
+ /** What IAC SB <xx> we are handling right now */
+ private byte current_sb;
+
+ /** current SB negotiation buffer */
+ private byte[] sbbuf;
+
+ /** IAC - init sequence for telnet negotiation. */
+ private final static byte IAC = (byte)255;
+ /** [IAC] End Of Record */
+ private final static byte EOR = (byte)239;
+ /** [IAC] WILL */
+ private final static byte WILL = (byte)251;
+ /** [IAC] WONT */
+ private final static byte WONT = (byte)252;
+ /** [IAC] DO */
+ private final static byte DO = (byte)253;
+ /** [IAC] DONT */
+ private final static byte DONT = (byte)254;
+ /** [IAC] Sub Begin */
+ private final static byte SB = (byte)250;
+ /** [IAC] Sub End */
+ private final static byte SE = (byte)240;
+ /** Telnet option: binary mode */
+ private final static byte TELOPT_BINARY= (byte)0; /* binary mode */
+ /** Telnet option: echo text */
+ private final static byte TELOPT_ECHO = (byte)1; /* echo on/off */
+ /** Telnet option: sga */
+ private final static byte TELOPT_SGA = (byte)3; /* supress go ahead */
+ /** Telnet option: End Of Record */
+ private final static byte TELOPT_EOR = (byte)25; /* end of record */
+ /** Telnet option: Negotiate About Window Size */
+ private final static byte TELOPT_NAWS = (byte)31; /* NA-WindowSize*/
+ /** Telnet option: Terminal Type */
+ private final static byte TELOPT_TTYPE = (byte)24; /* terminal type */
+
+ private final static byte[] IACWILL = { IAC, WILL };
+ private final static byte[] IACWONT = { IAC, WONT };
+ private final static byte[] IACDO = { IAC, DO };
+ private final static byte[] IACDONT = { IAC, DONT };
+ private final static byte[] IACSB = { IAC, SB };
+ private final static byte[] IACSE = { IAC, SE };
+
+ /** Telnet option qualifier 'IS' */
+ private final static byte TELQUAL_IS = (byte)0;
+ /** Telnet option qualifier 'SEND' */
+ private final static byte TELQUAL_SEND = (byte)1;
+
+ /** What IAC DO(NT) request do we have received already ? */
+ private byte[] receivedDX;
+ /** What IAC WILL/WONT request do we have received already ? */
+ private byte[] receivedWX;
+ /** What IAC DO/DONT request do we have sent already ? */
+ private byte[] sentDX;
+ /** What IAC WILL/WONT request do we have sent already ? */
+ private byte[] sentWX;
+
+ /**
+ * Send a Telnet Escape character (IAC <code>)
+ */
+ public void sendTelnetControl(byte code)
+ throws IOException {
+ byte[] b = new byte[2];
+
+ b[0] = IAC;
+ b[1] = code;
+ write(b);
+ }
+
+ /**
+ * Send the new Window Size (via NAWS)
+ */
+ public void setWindowSize(int columns,int rows)
+ throws IOException {
+ if(debug > 2) System.err.println("sending NAWS");
+
+ if (receivedDX[TELOPT_NAWS] != DO) {
+ System.err.println("not allowed to send NAWS? (DONT NAWS)");
+ return;
+ }
+ write(IAC);write(SB);write(TELOPT_NAWS);
+ write((byte) (columns >> 8));
+ write((byte) (columns & 0xff));
+ write((byte) (rows >> 8));
+ write((byte) (rows & 0xff));
+ write(IAC);write(SE);
+ }
+
+
+ /**
+ * Handle an incoming IAC SB &lt;type&gt; &lt;bytes&gt; IAC SE
+ * @param type type of SB
+ * @param sbata byte array as &lt;bytes&gt;
+ */
+ private void handle_sb(byte type, byte[] sbdata)
+ throws IOException {
+ if(debug > 1)
+ System.err.println("TelnetIO.handle_sb("+type+")");
+ switch (type) {
+ case TELOPT_TTYPE:
+ if (sbdata.length>0 && sbdata[0]==TELQUAL_SEND) {
+ write(IACSB);write(TELOPT_TTYPE);write(TELQUAL_IS);
+ /* FIXME: need more logic here if we use
+ * more than one terminal type
+ */
+ String ttype = getTerminalType();
+ if(ttype == null) ttype = "dumb";
+ write(ttype.getBytes());
+ write(IACSE);
+ }
+
+ }
+ }
+
+ /**
+ * Do not send any notifications at startup. We do not know,
+ * whether the remote client understands telnet protocol handling,
+ * so we are silent.
+ * (This used to send IAC WILL SGA, but this is false for a compliant
+ * client.)
+ */
+ public void startup() throws IOException {
+ }
+ /**
+ * Transpose special telnet codes like 0xff or newlines to values
+ * that are compliant to the protocol. This method will also send
+ * the buffer immediately after transposing the data.
+ * @param buf the data buffer to be sent
+ */
+ public void transpose(byte[] buf) throws IOException {
+ int i;
+
+ byte[] nbuf,xbuf;
+ int nbufptr=0;
+ nbuf = new byte[buf.length*2]; // FIXME: buffer overflows possible
+
+ for (i = 0; i < buf.length ; i++) {
+ switch (buf[i]) {
+ // Escape IAC twice in stream ... to be telnet protocol compliant
+ // this is there in binary and non-binary mode.
+ case IAC:
+ nbuf[nbufptr++]=IAC;
+ nbuf[nbufptr++]=IAC;
+ break;
+ // We need to heed RFC 854. LF (\n) is 10, CR (\r) is 13
+ // we assume that the Terminal sends \n for lf+cr and \r for just cr
+ // linefeed+carriage return is CR LF */
+ case 10: // \n
+ if (receivedDX[TELOPT_BINARY + 128 ] != DO) {
+ while (nbuf.length - nbufptr < crlf.length) {
+ xbuf = new byte[nbuf.length*2];
+ System.arraycopy(nbuf,0,xbuf,0,nbufptr);
+ nbuf = xbuf;
+ }
+ for (int j=0;j<crlf.length;j++)
+ nbuf[nbufptr++]=crlf[j];
+ break;
+ } else {
+ // copy verbatim in binary mode.
+ nbuf[nbufptr++]=buf[i];
+ }
+ break;
+ // carriage return is CR NUL */
+ case 13: // \r
+ if (receivedDX[TELOPT_BINARY + 128 ] != DO) {
+ while (nbuf.length - nbufptr < cr.length) {
+ xbuf = new byte[nbuf.length*2];
+ System.arraycopy(nbuf,0,xbuf,0,nbufptr);
+ nbuf = xbuf;
+ }
+ for (int j=0;j<cr.length;j++)
+ nbuf[nbufptr++]=cr[j];
+ } else {
+ // copy verbatim in binary mode.
+ nbuf[nbufptr++]=buf[i];
+ }
+ break;
+ // all other characters are just copied
+ default:
+ nbuf[nbufptr++]=buf[i];
+ break;
+ }
+ }
+ xbuf = new byte[nbufptr];
+ System.arraycopy(nbuf,0,xbuf,0,nbufptr);
+ write(xbuf);
+ }
+
+ public void setCRLF(String xcrlf) { crlf = xcrlf.getBytes(); }
+ public void setCR(String xcr) { cr = xcr.getBytes(); }
+
+ /**
+ * Handle telnet protocol negotiation. The buffer will be parsed
+ * and necessary actions are taken according to the telnet protocol.
+ * See <A HREF="RFC-Telnet-URL">RFC-Telnet</A>
+ * @param nbuf the byte buffer put out after negotiation
+ * @return number of bytes processed, 0 for none, and -1 for end of buffer.
+ */
+ public int negotiate(byte nbuf[], int offset)
+ throws IOException
+ {
+ int count = tempbuf.length;
+ byte[] buf = tempbuf;
+ byte sendbuf[] = new byte[3];
+ byte b,reply;
+ int boffset = 0, noffset = offset;
+ boolean dobreak = false;
+
+ if (count == 0) // buffer is empty.
+ return -1;
+
+ while(!dobreak && (boffset < count) && (noffset < nbuf.length)) {
+ b=buf[boffset++];
+ // of course, byte is a signed entity (-128 -> 127)
+ // but apparently the SGI Netscape 3.0 doesn't seem
+ // to care and provides happily values up to 255
+ if (b>=128)
+ b=(byte)(b-256);
+ if(debug > 2) {
+ Byte B = new Byte(b);
+ System.err.print("byte: " + B.intValue()+ " ");
+ }
+ switch (neg_state) {
+ case STATE_DATA:
+ if (b==IAC) {
+ neg_state = STATE_IAC;
+ dobreak = true; // leave the loop so we can sync.
+ } else
+ nbuf[noffset++]=b;
+ break;
+ case STATE_IAC:
+ switch (b) {
+ case IAC:
+ if(debug > 2) System.err.print("IAC ");
+ neg_state = STATE_DATA;
+ nbuf[noffset++]=IAC;
+ break;
+ case WILL:
+ if(debug > 2) System.err.print("WILL ");
+ neg_state = STATE_IACWILL;
+ break;
+ case WONT:
+ if(debug > 2) System.err.print("WONT ");
+ neg_state = STATE_IACWONT;
+ break;
+ case DONT:
+ if(debug > 2) System.err.print("DONT ");
+ neg_state = STATE_IACDONT;
+ break;
+ case DO:
+ if(debug > 2) System.err.print("DO ");
+ neg_state = STATE_IACDO;
+ break;
+ case EOR:
+ if(debug > 1) System.err.print("EOR ");
+ notifyEndOfRecord();
+ dobreak = true; // leave the loop so we can sync.
+ neg_state = STATE_DATA;
+ break;
+ case SB:
+ if(debug > 2) System.err.print("SB ");
+ neg_state = STATE_IACSB;
+ break;
+ default:
+ if(debug > 2) System.err.print("<UNKNOWN "+b+" > ");
+ neg_state = STATE_DATA;
+ break;
+ }
+ break;
+ case STATE_IACWILL:
+ switch(b) {
+ case TELOPT_ECHO:
+ if(debug > 2) System.err.println("ECHO");
+ reply = DO;
+ setLocalEcho(false);
+ break;
+ case TELOPT_SGA:
+ if(debug > 2) System.err.println("SGA");
+ reply = DO;
+ break;
+ case TELOPT_EOR:
+ if(debug > 2) System.err.println("EOR");
+ reply = DO;
+ break;
+ case TELOPT_BINARY:
+ if(debug > 2) System.err.println("BINARY");
+ reply = DO;
+ break;
+ default:
+ if(debug > 2) System.err.println("<UNKNOWN,"+b+">");
+ reply = DONT;
+ break;
+ }
+ if(debug > 1) System.err.println("<"+b+", WILL ="+WILL+">");
+ if (reply != sentDX[b+128] || WILL != receivedWX[b+128]) {
+ sendbuf[0]=IAC;
+ sendbuf[1]=reply;
+ sendbuf[2]=b;
+ write(sendbuf);
+ sentDX[b+128] = reply;
+ receivedWX[b+128] = WILL;
+ }
+ neg_state = STATE_DATA;
+ break;
+ case STATE_IACWONT:
+ switch(b) {
+ case TELOPT_ECHO:
+ if(debug > 2) System.err.println("ECHO");
+ setLocalEcho(true);
+ reply = DONT;
+ break;
+ case TELOPT_SGA:
+ if(debug > 2) System.err.println("SGA");
+ reply = DONT;
+ break;
+ case TELOPT_EOR:
+ if(debug > 2) System.err.println("EOR");
+ reply = DONT;
+ break;
+ case TELOPT_BINARY:
+ if(debug > 2) System.err.println("BINARY");
+ reply = DONT;
+ break;
+ default:
+ if(debug > 2) System.err.println("<UNKNOWN,"+b+">");
+ reply = DONT;
+ break;
+ }
+ if(reply != sentDX[b+128] || WONT != receivedWX[b+128]) {
+ sendbuf[0]=IAC;
+ sendbuf[1]=reply;
+ sendbuf[2]=b;
+ write(sendbuf);
+ sentDX[b+128] = reply;
+ receivedWX[b+128] = WILL;
+ }
+ neg_state = STATE_DATA;
+ break;
+ case STATE_IACDO:
+ switch (b) {
+ case TELOPT_ECHO:
+ if(debug > 2) System.err.println("ECHO");
+ reply = WILL;
+ setLocalEcho(true);
+ break;
+ case TELOPT_SGA:
+ if(debug > 2) System.err.println("SGA");
+ reply = WILL;
+ break;
+ case TELOPT_TTYPE:
+ if(debug > 2) System.err.println("TTYPE");
+ reply = WILL;
+ break;
+ case TELOPT_BINARY:
+ if(debug > 2) System.err.println("BINARY");
+ reply = WILL;
+ break;
+ case TELOPT_NAWS:
+ if(debug > 2) System.err.println("NAWS");
+ int[] size = getWindowSize();
+ receivedDX[b] = DO;
+ if(size == null) {
+ // this shouldn't happen
+ write(IAC);
+ write(WONT);
+ write(TELOPT_NAWS);
+ reply = WONT;
+ sentWX[b] = WONT;
+ break;
+ }
+ reply = WILL;
+ sentWX[b] = WILL;
+ sendbuf[0]=IAC;
+ sendbuf[1]=WILL;
+ sendbuf[2]=TELOPT_NAWS;
+ write(sendbuf);
+ write(IAC);write(SB);write(TELOPT_NAWS);
+ write((byte) (size[0] >> 8));
+ write((byte) (size[0] & 0xff));
+ write((byte) (size[1] >> 8));
+ write((byte) (size[1] & 0xff));
+ write(IAC);write(SE);
+ break;
+ default:
+ if(debug > 2) System.err.println("<UNKNOWN,"+b+">");
+ reply = WONT;
+ break;
+ }
+ if(reply != sentWX[128+b] || DO != receivedDX[128+b]) {
+ sendbuf[0]=IAC;
+ sendbuf[1]=reply;
+ sendbuf[2]=b;
+ write(sendbuf);
+ sentWX[b+128] = reply;
+ receivedDX[b+128] = DO;
+ }
+ neg_state = STATE_DATA;
+ break;
+ case STATE_IACDONT:
+ switch (b) {
+ case TELOPT_ECHO:
+ if(debug > 2) System.err.println("ECHO");
+ reply = WONT;
+ setLocalEcho(false);
+ break;
+ case TELOPT_SGA:
+ if(debug > 2) System.err.println("SGA");
+ reply = WONT;
+ break;
+ case TELOPT_NAWS:
+ if(debug > 2) System.err.println("NAWS");
+ reply = WONT;
+ break;
+ case TELOPT_BINARY:
+ if(debug > 2) System.err.println("BINARY");
+ reply = WONT;
+ break;
+ default:
+ if(debug > 2) System.err.println("<UNKNOWN,"+b+">");
+ reply = WONT;
+ break;
+ }
+ if(reply != sentWX[b+128] || DONT != receivedDX[b+128]) {
+ write(IAC);write(reply);write(b);
+ sentWX[b+128] = reply;
+ receivedDX[b+128] = DONT;
+ }
+ neg_state = STATE_DATA;
+ break;
+ case STATE_IACSBIAC:
+ if(debug > 2) System.err.println(""+b+" ");
+ if (b == IAC) {
+ sbbuf = new byte[0];
+ current_sb = b;
+ neg_state = STATE_IACSBDATA;
+ } else {
+ System.err.println("(bad) "+b+" ");
+ neg_state = STATE_DATA;
+ }
+ break;
+ case STATE_IACSB:
+ if(debug > 2) System.err.println(""+b+" ");
+ switch (b) {
+ case IAC:
+ neg_state = STATE_IACSBIAC;
+ break;
+ default:
+ current_sb = b;
+ sbbuf = new byte[0];
+ neg_state = STATE_IACSBDATA;
+ break;
+ }
+ break;
+ case STATE_IACSBDATA:
+ if (debug > 2) System.err.println(""+b+" ");
+ switch (b) {
+ case IAC:
+ neg_state = STATE_IACSBDATAIAC;
+ break;
+ default:
+ byte[] xsb = new byte[sbbuf.length+1];
+ System.arraycopy(sbbuf,0,xsb,0,sbbuf.length);
+ sbbuf = xsb;
+ sbbuf[sbbuf.length-1] = b;
+ break;
+ }
+ break;
+ case STATE_IACSBDATAIAC:
+ if (debug > 2) System.err.println(""+b+" ");
+ switch (b) {
+ case IAC:
+ neg_state = STATE_IACSBDATA;
+ byte[] xsb = new byte[sbbuf.length+1];
+ System.arraycopy(sbbuf,0,xsb,0,sbbuf.length);
+ sbbuf = xsb;
+ sbbuf[sbbuf.length-1] = IAC;
+ break;
+ case SE:
+ handle_sb(current_sb,sbbuf);
+ current_sb = 0;
+ neg_state = STATE_DATA;
+ break;
+ case SB:
+ handle_sb(current_sb,sbbuf);
+ neg_state = STATE_IACSB;
+ break;
+ default:
+ neg_state = STATE_DATA;
+ break;
+ }
+ break;
+ default:
+ if (debug > 1)
+ System.err.println("This should not happen: "+neg_state+" ");
+ neg_state = STATE_DATA;
+ break;
+ }
+ }
+ // shrink tempbuf to new processed size.
+ byte[] xb = new byte[count-boffset];
+ System.arraycopy(tempbuf,boffset,xb,0,count-boffset);
+ tempbuf = xb;
+ return noffset - offset;
+ }
+
+ public void inputfeed(byte[] b, int offset, int len) {
+ byte[] xb = new byte[tempbuf.length+offset+len];
+
+ System.arraycopy(b,0,xb,0,offset);
+ System.arraycopy(tempbuf,0,xb,offset,tempbuf.length);
+ System.arraycopy(b,offset,xb,offset+tempbuf.length,len);
+ tempbuf = xb;
+ }
+}
diff --git a/src/org/connectbot/ConsoleActivity.java b/src/org/connectbot/ConsoleActivity.java
index 663d75e..8f6d569 100644
--- a/src/org/connectbot/ConsoleActivity.java
+++ b/src/org/connectbot/ConsoleActivity.java
@@ -133,7 +133,7 @@ public class ConsoleActivity extends Activity {
// If we didn't find the requested connection, try opening it
if(!found) {
try {
- Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s, so creating one now", requested.toString()));
+ Log.d(TAG, String.format("We couldnt find an existing bridge with URI=%s (nickname=%s), so creating one now", requested.toString(), requestedNickname));
bound.openConnection(requested);
} catch(Exception e) {
Log.e(TAG, "Problem while trying to create new requested bridge from URI", e);
@@ -165,7 +165,6 @@ public class ConsoleActivity extends Activity {
// check to see if this bridge was requested
if(bridge.host.getNickname().equals(requestedNickname))
requestedIndex = flip.getChildCount() - 1;
-
}
try {
@@ -178,7 +177,6 @@ public class ConsoleActivity extends Activity {
updatePromptVisible();
updateEmptyVisible();
-
}
public void onServiceDisconnected(ComponentName className) {
@@ -189,7 +187,6 @@ public class ConsoleActivity extends Activity {
flip.removeAllViews();
updateEmptyVisible();
bound = null;
-
}
};
@@ -579,12 +576,10 @@ public class ConsoleActivity extends Activity {
final View view = findCurrentView(R.id.console_flip);
final boolean activeTerminal = (view instanceof TerminalView);
- boolean authenticated = false;
boolean sessionOpen = false;
boolean disconnected = false;
if (activeTerminal) {
- authenticated = ((TerminalView) view).bridge.isAuthenticated();
sessionOpen = ((TerminalView) view).bridge.isSessionOpen();
disconnected = ((TerminalView) view).bridge.isDisconnected();
}
@@ -632,7 +627,7 @@ public class ConsoleActivity extends Activity {
paste = menu.add(R.string.console_menu_paste);
paste.setAlphabeticShortcut('v');
paste.setIcon(android.R.drawable.ic_menu_edit);
- paste.setEnabled(clipboard.hasText() && activeTerminal && authenticated);
+ paste.setEnabled(clipboard.hasText() && sessionOpen);
paste.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
// force insert of clipboard text into current console
@@ -649,7 +644,7 @@ public class ConsoleActivity extends Activity {
portForward = menu.add(R.string.console_menu_portforwards);
portForward.setAlphabeticShortcut('f');
portForward.setIcon(android.R.drawable.ic_menu_manage);
- portForward.setEnabled(authenticated);
+ portForward.setEnabled(sessionOpen);
portForward.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
Intent intent = new Intent(ConsoleActivity.this, PortForwardListActivity.class);
@@ -662,7 +657,7 @@ public class ConsoleActivity extends Activity {
resize = menu.add(R.string.console_menu_resize);
resize.setAlphabeticShortcut('s');
resize.setIcon(android.R.drawable.ic_menu_crop);
- resize.setEnabled(activeTerminal && sessionOpen);
+ resize.setEnabled(sessionOpen);
resize.setOnMenuItemClickListener(new OnMenuItemClickListener() {
public boolean onMenuItemClick(MenuItem item) {
final TerminalView terminal = (TerminalView)view;
@@ -694,11 +689,9 @@ public class ConsoleActivity extends Activity {
final View view = findCurrentView(R.id.console_flip);
boolean activeTerminal = (view instanceof TerminalView);
- boolean authenticated = false;
boolean sessionOpen = false;
boolean disconnected = false;
if (activeTerminal) {
- authenticated = ((TerminalView)view).bridge.isAuthenticated();
sessionOpen = ((TerminalView)view).bridge.isSessionOpen();
disconnected = ((TerminalView)view).bridge.isDisconnected();
}
@@ -709,9 +702,9 @@ public class ConsoleActivity extends Activity {
else
disconnect.setTitle(R.string.console_menu_close);
copy.setEnabled(activeTerminal);
- paste.setEnabled(clipboard.hasText() && activeTerminal && sessionOpen);
- portForward.setEnabled(activeTerminal && authenticated);
- resize.setEnabled(activeTerminal && sessionOpen);
+ paste.setEnabled(clipboard.hasText() && sessionOpen);
+ portForward.setEnabled(sessionOpen);
+ resize.setEnabled(sessionOpen);
return true;
}
diff --git a/src/org/connectbot/HostListActivity.java b/src/org/connectbot/HostListActivity.java
index 571780d..7280176 100644
--- a/src/org/connectbot/HostListActivity.java
+++ b/src/org/connectbot/HostListActivity.java
@@ -19,12 +19,11 @@
package org.connectbot;
import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import org.connectbot.bean.HostBean;
import org.connectbot.service.TerminalBridge;
import org.connectbot.service.TerminalManager;
+import org.connectbot.transport.TransportFactory;
import org.connectbot.util.HostDatabase;
import org.connectbot.util.PreferenceConstants;
import org.connectbot.util.UpdateHelper;
@@ -61,6 +60,7 @@ import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ImageView;
import android.widget.ListView;
+import android.widget.Spinner;
import android.widget.TextView;
import android.widget.AdapterView.OnItemClickListener;
@@ -79,6 +79,8 @@ public class HostListActivity extends ListActivity {
private MenuItem sortlast;
+ private Spinner transportSpinner;
+
protected Handler updateHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
@@ -197,7 +199,8 @@ public class HostListActivity extends ListActivity {
HostBean host = (HostBean) parent.getAdapter().getItem(position);
// create a specific uri that represents this host
- Uri uri = Uri.parse(String.format("ssh://%s@%s:%d/#%s",
+ Uri uri = Uri.parse(String.format("%s://%s@%s:%d/#%s",
+ Uri.encode(host.getProtocol()),
Uri.encode(host.getUsername()),
Uri.encode(host.getHostname()),
host.getPort(),
@@ -227,7 +230,6 @@ public class HostListActivity extends ListActivity {
this.registerForContextMenu(list);
- final Pattern hostmask = Pattern.compile("^([^@]+)@([0-9A-Z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
final TextView text = (TextView) this.findViewById(R.id.front_quickconnect);
text.setVisibility(makingShortcut ? View.GONE : View.VISIBLE);
text.setOnKeyListener(new OnKeyListener() {
@@ -237,53 +239,40 @@ public class HostListActivity extends ListActivity {
if(event.getAction() == KeyEvent.ACTION_UP) return false;
if(keyCode != KeyEvent.KEYCODE_ENTER) return false;
- // make sure we follow pattern
- if (text.getText().length() < 3)
- return false;
+ Uri uri = TransportFactory.getUri((String) transportSpinner
+ .getSelectedItem(), text.getText().toString());
- // show error if poorly formed
- Matcher matcher = hostmask.matcher(text.getText().toString());
- if (!matcher.matches()) {
+ if (uri == null) {
text.setError(getString(R.string.list_format_error));
return false;
}
- // create new host for entered string and then launch
- String username = matcher.group(1);
- String hostname = matcher.group(2);
-
- int port = 22;
- try {
- port = Integer.parseInt(matcher.group(4));
- } catch (Exception e) {
- Log.i("HostListActivity", "Invalid format for port: "+ matcher.group(4));
- }
-
- String nickname;
- if (port == 22) {
- nickname = String.format("%s@%s", username, hostname);
- } else {
- nickname = String.format("%s@%s:%d", username, hostname, port);
- }
-
- HostBean host = new HostBean(nickname, username, hostname, port);
+ HostBean host = TransportFactory.getTransport(uri.getScheme()).createHost(uri);
host.setColor(HostDatabase.COLOR_GRAY);
host.setPubkeyId(HostDatabase.PUBKEYID_ANY);
hostdb.saveHost(host);
Intent intent = new Intent(HostListActivity.this, ConsoleActivity.class);
- intent.setData(host.getUri());
+ intent.setData(uri);
HostListActivity.this.startActivity(intent);
- // set list filter based on text
- // String filter = text.getText().toString();
- // list.setTextFilterEnabled((filter.length() > 0));
- // list.setFilterText(filter);
-
return true;
}
});
+ text.requestFocus();
+
+ transportSpinner = (Spinner)findViewById(R.id.transport_selection);
+ ArrayAdapter<String> transportSelection = new ArrayAdapter<String>(this,
+ android.R.layout.simple_spinner_item, TransportFactory.getTransportNames());
+ transportSelection.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ transportSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+ public void onItemSelected(AdapterView<?> arg0, View view, int position, long id) {
+ text.requestFocus();
+ }
+ public void onNothingSelected(AdapterView<?> arg0) { }
+ });
+ transportSpinner.setAdapter(transportSelection);
this.inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
}
@@ -388,6 +377,8 @@ public class HostListActivity extends ListActivity {
return true;
}
});
+ if (!TransportFactory.canForwardPorts(host.getProtocol()))
+ portForwards.setEnabled(false);
MenuItem delete = menu.add(R.string.list_host_delete);
delete.setOnMenuItemClickListener(new OnMenuItemClickListener() {
diff --git a/src/org/connectbot/bean/HostBean.java b/src/org/connectbot/bean/HostBean.java
index 4a22c91..8dd9d42 100644
--- a/src/org/connectbot/bean/HostBean.java
+++ b/src/org/connectbot/bean/HostBean.java
@@ -35,6 +35,7 @@ public class HostBean extends AbstractBean {
private String username = null;
private String hostname = null;
private int port = 22;
+ private String protocol = "ssh";
private String hostKeyAlgo = null;
private byte[] hostKey = null;
private long lastConnect = -1;
@@ -55,8 +56,9 @@ public class HostBean extends AbstractBean {
return BEAN_NAME;
}
- public HostBean(String nickname, String username, String hostname, int port) {
+ public HostBean(String nickname, String protocol, String username, String hostname, int port) {
this.nickname = nickname;
+ this.protocol = protocol;
this.username = username;
this.hostname = hostname;
this.port = port;
@@ -92,6 +94,15 @@ public class HostBean extends AbstractBean {
public int getPort() {
return port;
}
+
+ public void setProtocol(String protocol) {
+ this.protocol = protocol;
+ }
+
+ public String getProtocol() {
+ return protocol;
+ }
+
public void setHostKeyAlgo(String hostKeyAlgo) {
this.hostKeyAlgo = hostKeyAlgo;
}
@@ -175,6 +186,7 @@ public class HostBean extends AbstractBean {
ContentValues values = new ContentValues();
values.put(HostDatabase.FIELD_HOST_NICKNAME, nickname);
+ values.put(HostDatabase.FIELD_HOST_PROTOCOL, protocol);
values.put(HostDatabase.FIELD_HOST_USERNAME, username);
values.put(HostDatabase.FIELD_HOST_HOSTNAME, hostname);
values.put(HostDatabase.FIELD_HOST_PORT, port);
@@ -194,7 +206,7 @@ public class HostBean extends AbstractBean {
@Override
public boolean equals(Object o) {
- if (!(o instanceof HostBean))
+ if (o == null || !(o instanceof HostBean))
return false;
HostBean host = (HostBean)o;
@@ -208,6 +220,12 @@ public class HostBean extends AbstractBean {
} else if (!nickname.equals(host.getNickname()))
return false;
+ if (protocol == null) {
+ if (host.getProtocol() != null)
+ return false;
+ } else if (!protocol.equals(host.getProtocol()))
+ return false;
+
if (username == null) {
if (host.getUsername() != null)
return false;
@@ -234,6 +252,7 @@ public class HostBean extends AbstractBean {
return (int)id;
hash = 31 * hash + (null == nickname ? 0 : nickname.hashCode());
+ hash = 31 * hash + (null == protocol ? 0 : protocol.hashCode());
hash = 31 * hash + (null == username ? 0 : username.hashCode());
hash = 31 * hash + (null == hostname ? 0 : hostname.hashCode());
hash = 31 * hash + port;
diff --git a/src/org/connectbot/service/Relay.java b/src/org/connectbot/service/Relay.java
index 5b9fe3f..8ac4efc 100644
--- a/src/org/connectbot/service/Relay.java
+++ b/src/org/connectbot/service/Relay.java
@@ -21,18 +21,15 @@ package org.connectbot.service;
import gnu.java.nio.charset.Cp437;
import java.io.IOException;
-import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CodingErrorAction;
-import android.util.Log;
-
-import com.trilead.ssh2.ChannelCondition;
-import com.trilead.ssh2.Session;
+import org.connectbot.transport.AbsTransport;
+import android.util.Log;
import de.mud.terminal.vt320;
/**
@@ -43,21 +40,12 @@ public class Relay implements Runnable {
private static final int BUFFER_SIZE = 4096;
- private static final int CONDITIONS =
- ChannelCondition.STDOUT_DATA
- | ChannelCondition.STDERR_DATA
- | ChannelCondition.CLOSED
- | ChannelCondition.EOF;
-
private TerminalBridge bridge;
private Charset currentCharset;
private CharsetDecoder decoder;
- private Session session;
-
- private InputStream stdout;
- private InputStream stderr;
+ private AbsTransport transport;
private vt320 buffer;
@@ -67,12 +55,10 @@ public class Relay implements Runnable {
private byte[] byteArray;
private char[] charArray;
- public Relay(TerminalBridge bridge, Session session, InputStream stdout, InputStream stderr, vt320 buffer, String encoding) {
+ public Relay(TerminalBridge bridge, AbsTransport transport, vt320 buffer, String encoding) {
setCharset(encoding);
this.bridge = bridge;
- this.session = session;
- this.stdout = stdout;
- this.stderr = stderr;
+ this.transport = transport;
this.buffer = buffer;
}
@@ -104,47 +90,21 @@ public class Relay implements Runnable {
int bytesRead = 0;
- int newConditions = 0;
-
- while((newConditions & ChannelCondition.CLOSED) == 0) {
- try {
- newConditions = session.waitForCondition(CONDITIONS, 0);
- if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) {
- while (stdout.available() > 0) {
- bytesRead = stdout.read(byteArray, 0, BUFFER_SIZE);
-
- byteBuffer.position(0);
- byteBuffer.limit(bytesRead);
- decoder.decode(byteBuffer, charBuffer, false);
- buffer.putString(charArray, 0, charBuffer.position());
- charBuffer.clear();
- }
+ try {
+ while (true) {
+ bytesRead = transport.read(byteArray, 0, BUFFER_SIZE);
+ if (bytesRead > 0) {
+ byteBuffer.position(0);
+ byteBuffer.limit(bytesRead);
+ decoder.decode(byteBuffer, charBuffer, false);
+ buffer.putString(charArray, 0, charBuffer.position());
+ charBuffer.clear();
bridge.redraw();
}
-
- if ((newConditions & ChannelCondition.STDERR_DATA) != 0)
- logAndDiscard(stderr);
-
- if ((newConditions & ChannelCondition.EOF) != 0) {
- // The other side closed our channel, so let's disconnect.
- // TODO review whether any tunnel is in use currently.
- bridge.dispatchDisconnect(false);
- break;
- }
- } catch (IOException e) {
- Log.e(TAG, "Problem while handling incoming data in relay thread", e);
- break;
}
- }
-
- }
-
- private void logAndDiscard(InputStream stream) throws IOException {
- int bytesAvail;
- while ((bytesAvail = stream.available()) > 0) {
- stream.skip(bytesAvail);
- Log.d(TAG, String.format("Discarded %d bytes from stderr", bytesAvail));
+ } catch (IOException e) {
+ Log.e(TAG, "Problem while handling incoming data in relay thread", e);
}
}
}
diff --git a/src/org/connectbot/service/TerminalBridge.java b/src/org/connectbot/service/TerminalBridge.java
index 7a6e953..728e7eb 100644
--- a/src/org/connectbot/service/TerminalBridge.java
+++ b/src/org/connectbot/service/TerminalBridge.java
@@ -19,15 +19,6 @@
package org.connectbot.service;
import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.InetAddress;
-import java.net.InetSocketAddress;
-import java.security.NoSuchAlgorithmException;
-import java.security.PrivateKey;
-import java.security.PublicKey;
-import java.security.spec.InvalidKeySpecException;
-import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
@@ -35,12 +26,10 @@ import org.connectbot.R;
import org.connectbot.TerminalView;
import org.connectbot.bean.HostBean;
import org.connectbot.bean.PortForwardBean;
-import org.connectbot.bean.PubkeyBean;
import org.connectbot.bean.SelectionArea;
-import org.connectbot.util.HostDatabase;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.transport.TransportFactory;
import org.connectbot.util.PreferenceConstants;
-import org.connectbot.util.PubkeyDatabase;
-import org.connectbot.util.PubkeyUtils;
import android.content.Context;
import android.graphics.Bitmap;
@@ -56,18 +45,6 @@ import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnKeyListener;
-
-import com.trilead.ssh2.Connection;
-import com.trilead.ssh2.ConnectionInfo;
-import com.trilead.ssh2.ConnectionMonitor;
-import com.trilead.ssh2.DynamicPortForwarder;
-import com.trilead.ssh2.InteractiveCallback;
-import com.trilead.ssh2.KnownHosts;
-import com.trilead.ssh2.LocalPortForwarder;
-import com.trilead.ssh2.ServerHostKeyVerifier;
-import com.trilead.ssh2.Session;
-import com.trilead.ssh2.crypto.PEMDecoder;
-
import de.mud.terminal.VDUBuffer;
import de.mud.terminal.VDUDisplay;
import de.mud.terminal.vt320;
@@ -82,19 +59,11 @@ import de.mud.terminal.vt320;
* This class also provides SSH hostkey verification prompting, and password
* prompting.
*/
-public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCallback, ConnectionMonitor {
+public class TerminalBridge implements VDUDisplay, OnKeyListener {
public final static String TAG = "ConnectBot.TerminalBridge";
public final static int DEFAULT_FONT_SIZE = 10;
- public static final String AUTH_PUBLICKEY = "publickey",
- AUTH_PASSWORD = "password",
- AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
-
- protected final static int AUTH_TRIES = 20;
-
- private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
-
public int color[];
public final static int COLOR_FG_STD = 7;
@@ -104,16 +73,10 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
public HostBean host;
- public final Connection connection;
- protected Session session;
+ private AbsTransport transport;
private final Paint defaultPaint;
- protected OutputStream stdin;
- protected InputStream stdout;
-
- private InputStream stderr;
-
private Relay relay;
private final String emulation;
@@ -145,16 +108,12 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
public final static int META_TRANSIENT = META_CTRL_ON | META_ALT_ON
| META_SHIFT_ON;
- private boolean pubkeysExhausted = false;
-
- private boolean authenticated = false;
- private boolean sessionOpen = false;
private boolean disconnected = false;
private boolean awaitingClose = false;
private boolean forcedSize = false;
- private int termWidth;
- private int termHeight;
+ private int columns;
+ private int rows;
private String keymode = null;
@@ -184,78 +143,6 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
protected BridgeDisconnectedListener disconnectListener = null;
- protected ConnectionInfo connectionInfo;
-
- public class HostKeyVerifier implements ServerHostKeyVerifier {
-
- public boolean verifyServerHostKey(String hostname, int port,
- String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
-
- // read in all known hosts from hostdb
- KnownHosts hosts = manager.hostdb.getKnownHosts();
- Boolean result;
-
- String matchName = String.format("%s:%d", hostname, port);
-
- String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
-
- String algorithmName;
- if ("ssh-rsa".equals(serverHostKeyAlgorithm))
- algorithmName = "RSA";
- else if ("ssh-dss".equals(serverHostKeyAlgorithm))
- algorithmName = "DSA";
- else
- algorithmName = serverHostKeyAlgorithm;
-
- switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
- case KnownHosts.HOSTKEY_IS_OK:
- outputLine(String.format("Verified host %s key: %s", algorithmName, fingerprint));
- return true;
-
- case KnownHosts.HOSTKEY_IS_NEW:
- // prompt user
- outputLine(String.format(manager.res.getString(R.string.host_authenticity_warning), hostname));
- outputLine(String.format(manager.res.getString(R.string.host_fingerprint), algorithmName, fingerprint));
-
- result = promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
- if(result == null) return false;
- if(result.booleanValue()) {
- // save this key in known database
- manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
- }
- return result.booleanValue();
-
- case KnownHosts.HOSTKEY_HAS_CHANGED:
- String header = String.format("@ %s @",
- manager.res.getString(R.string.host_verification_failure_warning_header));
-
- char[] atsigns = new char[header.length()];
- Arrays.fill(atsigns, '@');
- String border = new String(atsigns);
-
- outputLine(border);
- outputLine(manager.res.getString(R.string.host_verification_failure_warning));
- outputLine(border);
-
- outputLine(String.format(manager.res.getString(R.string.host_fingerprint),
- algorithmName, fingerprint));
-
- // Users have no way to delete keys, so we'll prompt them for now.
- result = promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
- if(result == null) return false;
- if(result.booleanValue()) {
- // save this key in known database
- manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
- }
- return result.booleanValue();
-
- default:
- return false;
- }
- }
-
- }
-
/**
* Create a new terminal bridge suitable for unit testing.
*/
@@ -285,7 +172,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
fontSizeChangedListeners = new LinkedList<FontSizeChangedListener>();
- connection = null;
+ transport = null;
}
/**
@@ -327,8 +214,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
@Override
public void write(byte[] b) {
try {
- if (b != null && stdin != null)
- stdin.write(b);
+ if (b != null && transport != null)
+ transport.write(b);
} catch (IOException e) {
Log.e(TAG, "Problem writing outgoing data in vt320() thread", e);
}
@@ -337,8 +224,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
@Override
public void write(int b) {
try {
- if (stdin != null)
- stdin.write(b);
+ if (transport != null)
+ transport.write(b);
} catch (IOException e) {
Log.e(TAG, "Problem writing outgoing data in vt320() thread", e);
}
@@ -368,76 +255,36 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
buffer.setDisplay(this);
selectionArea = new SelectionArea();
+ }
- portForwards = manager.hostdb.getPortForwardsForHost(host);
-
- // prepare the ssh connection for opening
- // we perform the actual connection later in startConnection()
- outputLine(String.format("Connecting to %s:%d", host.getHostname(), host.getPort()));
- connection = new Connection(host.getHostname(), host.getPort());
- connection.addConnectionMonitor(this);
- connection.setCompression(host.getCompression());
+ public PromptHelper getPromptHelper() {
+ return promptHelper;
}
/**
* Spawn thread to open connection and start login process.
*/
+ @SuppressWarnings("static-access")
protected void startConnection() {
- Thread connectionThread = new Thread(new Runnable() {
- public void run() {
- try {
- /* Uncomment when debugging SSH protocol:
- DebugLogger logger = new DebugLogger() {
-
- public void log(int level, String className, String message) {
- Log.d("SSH", message);
- }
-
- };
- connection.enableDebugging(true, logger);
- */
- connectionInfo = connection.connect(new HostKeyVerifier());
-
- if (connectionInfo.clientToServerCryptoAlgorithm
- .equals(connectionInfo.serverToClientCryptoAlgorithm)
- && connectionInfo.clientToServerMACAlgorithm
- .equals(connectionInfo.serverToClientMACAlgorithm)) {
- outputLine(String.format("Using algorithm: %s %s",
- connectionInfo.clientToServerCryptoAlgorithm,
- connectionInfo.clientToServerMACAlgorithm));
- } else {
- outputLine(String.format(
- "Client-to-server algorithm: %s %s",
- connectionInfo.clientToServerCryptoAlgorithm,
- connectionInfo.clientToServerMACAlgorithm));
-
- outputLine(String.format(
- "Server-to-client algorithm: %s %s",
- connectionInfo.serverToClientCryptoAlgorithm,
- connectionInfo.serverToClientMACAlgorithm));
- }
- } catch (IOException e) {
- Log.e(TAG, "Problem in SSH connection thread during authentication", e);
-
- // Display the reason in the text.
- outputLine(e.getCause().getMessage());
-
- dispatchDisconnect(false);
- return;
- }
+ transport = TransportFactory.getTransport(host.getProtocol());
+ transport.setBridge(this);
+ transport.setManager(manager);
+ transport.setHost(host);
+
+ // Should be more abstract?
+ transport.setCompression(host.getCompression());
+ transport.setEmulation(emulation);
+
+ if (transport.canForwardPorts()) {
+ for (PortForwardBean portForward : manager.hostdb.getPortForwardsForHost(host))
+ transport.addPortForward(portForward);
+ }
- try {
- // enter a loop to keep trying until authentication
- int tries = 0;
- while(!connection.isAuthenticationComplete() && tries++ < AUTH_TRIES && !disconnected) {
- handleAuthentication();
+ outputLine(String.format("Connecting to %s:%d via %s", host.getHostname(), host.getPort(), host.getProtocol()));
- // sleep to make sure we dont kill system
- Thread.sleep(1000);
- }
- } catch(Exception e) {
- Log.e(TAG, "Problem in SSH connection thread during authentication", e);
- }
+ Thread connectionThread = new Thread(new Runnable() {
+ public void run() {
+ transport.connect();
}
});
connectionThread.setName("Connection");
@@ -445,154 +292,6 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
}
/**
- * Attempt connection with database row pointed to by cursor.
- * @param cursor
- * @return true for successful authentication
- * @throws NoSuchAlgorithmException
- * @throws InvalidKeySpecException
- * @throws IOException
- */
- private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
- Object trileadKey = null;
- if(manager.isKeyLoaded(pubkey.getNickname())) {
- // load this key from memory if its already there
- Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname()));
- trileadKey = manager.getKey(pubkey.getNickname());
-
- } else {
- // otherwise load key from database and prompt for password as needed
- String password = null;
- if (pubkey.isEncrypted()) {
- password = promptHelper.requestStringPrompt(null,
- String.format(manager.res.getString(R.string.prompt_pubkey_password),
- pubkey.getNickname()));
-
- // Something must have interrupted the prompt.
- if (password == null)
- return false;
- }
-
- if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) {
- // load specific key using pem format
- trileadKey = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password);
- } else {
- // load using internal generated format
- PrivateKey privKey;
- try {
- privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(),
- pubkey.getType(), password);
- } catch (Exception e) {
- String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname());
- Log.e(TAG, message, e);
- outputLine(message);
- return false;
- }
-
- PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(),
- pubkey.getType());
-
- // convert key to trilead format
- trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey);
- Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
- }
-
- Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
-
- // save this key in-memory if option enabled
- if(manager.isSavingKeys()) {
- manager.addKey(pubkey.getNickname(), trileadKey);
- }
- }
-
- return this.tryPublicKey(host.getUsername(), pubkey.getNickname(), trileadKey);
-
- }
-
- private boolean tryPublicKey(String username, String keyNickname, Object trileadKey) throws IOException {
- //outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
- boolean success = connection.authenticateWithPublicKey(username, trileadKey);
- if(!success)
- outputLine(String.format("Authentication method 'publickey' with key '%s' failed", keyNickname));
- return success;
- }
-
- protected void handleAuthentication() {
- try {
- if (connection.authenticateWithNone(host.getUsername())) {
- finishConnection();
- return;
- }
- } catch(Exception e) {
- Log.d(TAG, "Host does not support 'none' authentication.");
- }
-
- outputLine(manager.res.getString(R.string.terminal_auth));
-
- try {
- long pubkeyId = host.getPubkeyId();
-
- if (!pubkeysExhausted &&
- pubkeyId != HostDatabase.PUBKEYID_NEVER &&
- connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) {
-
- // if explicit pubkey defined for this host, then prompt for password as needed
- // otherwise just try all in-memory keys held in terminalmanager
-
- if (pubkeyId == HostDatabase.PUBKEYID_ANY) {
- // try each of the in-memory keys
- outputLine(manager.res.getString(R.string.terminal_auth_pubkey_any));
- for(String nickname : manager.loadedPubkeys.keySet()) {
- Object trileadKey = manager.loadedPubkeys.get(nickname);
- if(this.tryPublicKey(host.getUsername(), nickname, trileadKey)) {
- finishConnection();
- break;
- }
- }
- } else {
- outputLine(manager.res.getString(R.string.terminal_auth_pubkey_specific));
- // use a specific key for this host, as requested
- PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId);
-
- if (pubkey == null)
- outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid));
- else
- if (tryPublicKey(pubkey))
- finishConnection();
- }
-
- pubkeysExhausted = true;
- } else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
- outputLine(manager.res.getString(R.string.terminal_auth_pass));
- String password = promptHelper.requestStringPrompt(null,
- manager.res.getString(R.string.prompt_password));
- if (password != null
- && connection.authenticateWithPassword(host.getUsername(), password)) {
- finishConnection();
- } else {
- outputLine(manager.res.getString(R.string.terminal_auth_pass_fail));
- }
-
- } else if(connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
- // this auth method will talk with us using InteractiveCallback interface
- // it blocks until authentication finishes
- outputLine(manager.res.getString(R.string.terminal_auth_ki));
- if(connection.authenticateWithKeyboardInteractive(host.getUsername(), TerminalBridge.this)) {
- finishConnection();
- } else {
- outputLine(manager.res.getString(R.string.terminal_auth_ki_fail));
- }
-
- } else {
- outputLine(manager.res.getString(R.string.terminal_auth_fail));
-
- }
- } catch(Exception e) {
- Log.e(TAG, "Problem during handleAuthentication()", e);
- }
-
- }
-
- /**
* Handle challenges from keyboard-interactive authentication mode.
*/
public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
@@ -617,8 +316,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* Convenience method for writing a line into the underlying MUD buffer.
* Should never be called once the session is established.
*/
- protected final void outputLine(String line) {
- if (session != null)
+ public final void outputLine(String line) {
+ if (transport != null && transport.isSessionOpen())
Log.e(TAG, "Session established, cannot use outputLine!", new IOException("outputLine call traceback"));
synchronized (localOutput) {
@@ -653,68 +352,36 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* Internal method to request actual PTY terminal once we've finished
* authentication. If called before authenticated, it will just fail.
*/
- private void finishConnection() {
- setAuthenticated(true);
-
- // Start up predefined port forwards
- for (PortForwardBean pfb : portForwards) {
- try {
- enablePortForward(pfb);
- outputLine(String.format("Enable port forward: %s", pfb.getDescription()));
- } catch (Exception e) {
- Log.e(TAG, "Error setting up port forward during connect", e);
- }
- }
-
- if (!host.getWantSession()) {
- outputLine("Session will not be started due to host preference.");
- return;
- }
-
- try {
- session = connection.openSession();
- ((vt320) buffer).reset();
-
- // We no longer need our local output.
- localOutput.clear();
-
- // previously tried vt100 and xterm for emulation modes
- // "screen" works the best for color and escape codes
- // TODO: pull this value from the preferences
- ((vt320) buffer).setAnswerBack(emulation);
- session.requestPTY(emulation, termWidth, termHeight, 0, 0, null);
- session.startShell();
-
- // grab stdin/out from newly formed session
- stdin = session.getStdin();
- stdout = session.getStdout();
- stderr = session.getStderr();
+ public void onConnected() {
+ ((vt320) buffer).reset();
- // create thread to relay incoming connection data to buffer
- relay = new Relay(this, session, stdout, stderr, (vt320) buffer, host.getEncoding());
- Thread relayThread = new Thread(relay);
- relayThread.setName("Relay");
- relayThread.start();
+ // We no longer need our local output.
+ localOutput.clear();
- // force font-size to make sure we resizePTY as needed
- setFontSize(fontSize);
+ // previously tried vt100 and xterm for emulation modes
+ // "screen" works the best for color and escape codes
+ ((vt320) buffer).setAnswerBack(emulation);
- sessionOpen = true;
+ // create thread to relay incoming connection data to buffer
+ relay = new Relay(this, transport, (vt320) buffer, host.getEncoding());
+ Thread relayThread = new Thread(relay);
+ relayThread.setName("Relay");
+ relayThread.start();
- // finally send any post-login string, if requested
- injectString(host.getPostLogin());
-
- } catch (IOException e1) {
- Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1);
- }
+ // force font-size to make sure we resizePTY as needed
+ setFontSize(fontSize);
+ // finally send any post-login string, if requested
+ injectString(host.getPostLogin());
}
/**
* @return whether a session is open or not
*/
public boolean isSessionOpen() {
- return sessionOpen;
+ if (transport != null)
+ return transport.isSessionOpen();
+ return false;
}
public void setOnDisconnectedListener(BridgeDisconnectedListener disconnectListener) {
@@ -733,17 +400,14 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
// temporary fix is to just spawn disconnection into a thread
Thread disconnectThread = new Thread(new Runnable() {
public void run() {
- if(session != null)
- session.close();
- connection.close();
+ if(transport != null)
+ transport.close();
}
});
disconnectThread.setName("Disconnect");
disconnectThread.start();
disconnected = true;
- authenticated = false;
- sessionOpen = false;
if (immediate) {
awaitingClose = true;
@@ -783,31 +447,31 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
// Ignore all key-up events except for the special keys
if (event.getAction() == KeyEvent.ACTION_UP) {
// skip keys if we aren't connected yet or have been disconnected
- if (disconnected || !sessionOpen)
+ if (disconnected || transport == null)
return false;
if (PreferenceConstants.KEYMODE_RIGHT.equals(keymode)) {
if (keyCode == KeyEvent.KEYCODE_ALT_RIGHT
&& (metaState & META_SLASH) != 0) {
metaState &= ~(META_SLASH | META_TRANSIENT);
- stdin.write('/');
+ transport.write('/');
return true;
} else if (keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT
&& (metaState & META_TAB) != 0) {
metaState &= ~(META_TAB | META_TRANSIENT);
- stdin.write(0x09);
+ transport.write(0x09);
return true;
}
} else if (PreferenceConstants.KEYMODE_LEFT.equals(keymode)) {
if (keyCode == KeyEvent.KEYCODE_ALT_LEFT
&& (metaState & META_SLASH) != 0) {
metaState &= ~(META_SLASH | META_TRANSIENT);
- stdin.write('/');
+ transport.write('/');
return true;
} else if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT
&& (metaState & META_TAB) != 0) {
metaState &= ~(META_TAB | META_TRANSIENT);
- stdin.write(0x09);
+ transport.write(0x09);
return true;
}
}
@@ -827,7 +491,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
}
// skip keys if we aren't connected yet or have been disconnected
- if (disconnected || !sessionOpen)
+ if (disconnected || transport == null)
return false;
// if we're in scrollback, scroll to bottom of window on input
@@ -889,10 +553,10 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
}
if (key < 0x80)
- stdin.write(key);
+ transport.write(key);
else
// TODO write encoding routine that doesn't allocate each time
- stdin.write(new String(Character.toChars(key))
+ transport.write(new String(Character.toChars(key))
.getBytes(host.getEncoding()));
return true;
@@ -901,7 +565,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
if (keyCode == KeyEvent.KEYCODE_UNKNOWN &&
event.getAction() == KeyEvent.ACTION_MULTIPLE) {
byte[] input = event.getCharacters().getBytes(host.getEncoding());
- stdin.write(input);
+ transport.write(input);
}
// try handling keymode shortcuts
@@ -952,10 +616,10 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
PreferenceConstants.CAMERA,
PreferenceConstants.CAMERA_CTRLA_SPACE);
if(PreferenceConstants.CAMERA_CTRLA_SPACE.equals(camera)) {
- stdin.write(0x01);
- stdin.write(' ');
+ transport.write(0x01);
+ transport.write(' ');
} else if(PreferenceConstants.CAMERA_CTRLA.equals(camera)) {
- stdin.write(0x01);
+ transport.write(0x01);
} else if(PreferenceConstants.CAMERA_ESC.equals(camera)) {
((vt320)buffer).keyTyped(vt320.KEY_ESCAPE, ' ', 0);
}
@@ -1054,10 +718,9 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
} catch (IOException e) {
Log.e(TAG, "Problem while trying to handle an onKey() event", e);
try {
- stdin.flush();
+ transport.flush();
} catch (IOException ioe) {
- // Our stdin got blown away, so we must be closed.
- Log.d(TAG, "Our stdin was closed, dispatching disconnect event");
+ Log.d(TAG, "Our transport was closed, dispatching disconnect event");
dispatchDisconnect(false);
}
} catch (NullPointerException npe) {
@@ -1189,18 +852,18 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
if (!forcedSize) {
// recalculate buffer size
- int newTermWidth, newTermHeight;
+ int newColumns, newRows;
- newTermWidth = width / charWidth;
- newTermHeight = height / charHeight;
+ newColumns = width / charWidth;
+ newRows = height / charHeight;
// If nothing has changed in the terminal dimensions and not an intial
// draw then don't blow away scroll regions and such.
- if (newTermWidth == termWidth && newTermHeight == termHeight)
+ if (newColumns == columns && newRows == rows)
return;
- termWidth = newTermWidth;
- termHeight = newTermHeight;
+ columns = newColumns;
+ rows = newRows;
}
// reallocate new bitmap if needed
@@ -1209,8 +872,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
newBitmap = (bitmap.getWidth() != width || bitmap.getHeight() != height);
if (newBitmap) {
- if (bitmap != null)
- bitmap.recycle();
+ discardBitmap();
bitmap = Bitmap.createBitmap(width, height, Config.ARGB_8888);
canvas.setBitmap(bitmap);
}
@@ -1221,8 +883,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
// Stroke the border of the terminal if the size is being forced;
if (forcedSize) {
- int borderX = (termWidth * charWidth) + 1;
- int borderY = (termHeight * charHeight) + 1;
+ int borderX = (columns * charWidth) + 1;
+ int borderY = (rows * charHeight) + 1;
defaultPaint.setColor(Color.GRAY);
defaultPaint.setStrokeWidth(0.0f);
@@ -1235,19 +897,19 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
try {
// request a terminal pty resize
int prevRow = buffer.getCursorRow();
- buffer.setScreenSize(termWidth, termHeight, true);
+ buffer.setScreenSize(columns, rows, true);
// Work around weird vt320.java behavior where cursor is an offset from the bottom??
buffer.setCursorPosition(buffer.getCursorColumn(), prevRow);
- if(session != null)
- session.resizePTY(termWidth, termHeight, width, height);
+ if(transport != null)
+ transport.setDimensions(columns, rows, width, height);
} catch(Exception e) {
Log.e(TAG, "Problem while trying to resize screen or PTY", e);
}
// redraw local output if we don't have a sesson to receive our resize request
- if (session == null) {
+ if (transport == null) {
synchronized (localOutput) {
((vt320) buffer).reset();
@@ -1260,9 +922,9 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
fullRedraw = true;
redraw();
- parent.notifyUser(String.format("%d x %d", termWidth, termHeight));
+ parent.notifyUser(String.format("%d x %d", columns, rows));
- Log.i(TAG, String.format("parentChanged() now width=%d, height=%d", termWidth, termHeight));
+ Log.i(TAG, String.format("parentChanged() now width=%d, height=%d", columns, rows));
}
/**
@@ -1271,7 +933,10 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
*/
public synchronized void parentDestroyed() {
parent = null;
- canvas.setBitmap(null);
+ discardBitmap();
+ }
+
+ private void discardBitmap() {
if (bitmap != null)
bitmap.recycle();
bitmap = null;
@@ -1373,12 +1038,6 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
public void updateScrollBar() {
}
- public void connectionLost(Throwable reason) {
- // weve lost our ssh connection, so pass along to manager and gui
- Log.e(TAG, "Somehow our underlying SSH socket died", reason);
- dispatchDisconnect(false);
- }
-
/**
* Resize terminal to fit [rows]x[cols] in screen of size [width]x[height]
* @param rows
@@ -1418,8 +1077,8 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
size -= step;
forcedSize = true;
- termWidth = cols;
- termHeight = rows;
+ this.columns = cols;
+ this.rows = rows;
setFontSize(size);
}
@@ -1451,7 +1110,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* @return true on successful addition
*/
public boolean addPortForward(PortForwardBean portForward) {
- return portForwards.add(portForward);
+ return transport.addPortForward(portForward);
}
/**
@@ -1460,17 +1119,14 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* @return true on successful removal
*/
public boolean removePortForward(PortForwardBean portForward) {
- // Make sure we don't have a phantom forwarder.
- disablePortForward(portForward);
-
- return portForwards.remove(portForward);
+ return transport.removePortForward(portForward);
}
/**
* @return the list of port forwards
*/
public List<PortForwardBean> getPortForwards() {
- return portForwards;
+ return transport.getPortForwards();
}
/**
@@ -1480,58 +1136,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* @return true on successful port forward setup
*/
public boolean enablePortForward(PortForwardBean portForward) {
- if (!portForwards.contains(portForward)) {
- Log.e(TAG, "Attempt to enable port forward not in list");
- return false;
- }
-
- if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
- LocalPortForwarder lpf = null;
- try {
- lpf = connection.createLocalPortForwarder(
- new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()),
- portForward.getDestAddr(), portForward.getDestPort());
- } catch (IOException e) {
- Log.e(TAG, "Could not create local port forward", e);
- return false;
- }
-
- if (lpf == null) {
- Log.e(TAG, "returned LocalPortForwarder object is null");
- return false;
- }
-
- portForward.setIdentifier(lpf);
- portForward.setEnabled(true);
- return true;
- } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
- try {
- connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort());
- } catch (IOException e) {
- Log.e(TAG, "Could not create remote port forward", e);
- return false;
- }
-
- portForward.setEnabled(false);
- return true;
- } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
- DynamicPortForwarder dpf = null;
-
- try {
- dpf = connection.createDynamicPortForwarder(portForward.getSourcePort());
- } catch (IOException e) {
- Log.e(TAG, "Could not create dynamic port forward", e);
- return false;
- }
-
- portForward.setIdentifier(dpf);
- portForward.setEnabled(true);
- return true;
- } else {
- // Unsupported type
- Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
- return false;
- }
+ return transport.enablePortForward(portForward);
}
/**
@@ -1541,79 +1146,7 @@ public class TerminalBridge implements VDUDisplay, OnKeyListener, InteractiveCal
* @return true on successful port forward tear-down
*/
public boolean disablePortForward(PortForwardBean portForward) {
- if (!portForwards.contains(portForward)) {
- Log.e(TAG, "Attempt to disable port forward not in list");
- return false;
- }
-
- if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
- LocalPortForwarder lpf = null;
- lpf = (LocalPortForwarder)portForward.getIdentifier();
-
- if (!portForward.isEnabled() || lpf == null) {
- Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
- return false;
- }
-
- portForward.setEnabled(false);
-
- try {
- lpf.close();
- } catch (IOException e) {
- Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e);
- return false;
- }
-
- return true;
- } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
- portForward.setEnabled(false);
-
- try {
- connection.cancelRemotePortForwarding(portForward.getSourcePort());
- } catch (IOException e) {
- Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e);
- return false;
- }
-
- return true;
- } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
- DynamicPortForwarder dpf = null;
- dpf = (DynamicPortForwarder)portForward.getIdentifier();
-
- if (!portForward.isEnabled() || dpf == null) {
- Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
- return false;
- }
-
- portForward.setEnabled(false);
-
- try {
- dpf.close();
- } catch (IOException e) {
- Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e);
- return false;
- }
-
- return true;
- } else {
- // Unsupported type
- Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
- return false;
- }
- }
-
- /**
- * @param authenticated the authenticated to set
- */
- public void setAuthenticated(boolean authenticated) {
- this.authenticated = authenticated;
- }
-
- /**
- * @return the authenticated
- */
- public boolean isAuthenticated() {
- return authenticated;
+ return transport.disablePortForward(portForward);
}
/**
diff --git a/src/org/connectbot/service/TerminalManager.java b/src/org/connectbot/service/TerminalManager.java
index 8696b06..f7483dd 100644
--- a/src/org/connectbot/service/TerminalManager.java
+++ b/src/org/connectbot/service/TerminalManager.java
@@ -24,6 +24,7 @@ import java.security.PublicKey;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
@@ -31,6 +32,8 @@ import org.connectbot.ConsoleActivity;
import org.connectbot.R;
import org.connectbot.bean.HostBean;
import org.connectbot.bean.PubkeyBean;
+import org.connectbot.transport.AbsTransport;
+import org.connectbot.transport.TransportFactory;
import org.connectbot.util.HostDatabase;
import org.connectbot.util.PreferenceConstants;
import org.connectbot.util.PubkeyDatabase;
@@ -80,12 +83,12 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen
public Handler disconnectHandler = null;
- protected HashMap<String, Object> loadedPubkeys = new HashMap<String, Object>();
+ public HashMap<String, Object> loadedPubkeys = new HashMap<String, Object>();
- protected Resources res;
+ public Resources res;
- protected HostDatabase hostdb;
- protected PubkeyDatabase pubkeydb;
+ public HostDatabase hostdb;
+ public PubkeyDatabase pubkeydb;
protected SharedPreferences prefs;
@@ -235,21 +238,28 @@ public class TerminalManager extends Service implements BridgeDisconnectedListen
}
/**
- * Open a new SSH session by reading parameters from the given URI. Follows
- * format <code>ssh://user@host:port/#nickname</code>
+ * Open a new connection by reading parameters from the given URI. Follows
+ * format specified by an individual transport.
*/
public void openConnection(Uri uri) throws Exception {
- String nickname = uri.getFragment();
- String username = uri.getUserInfo();
- String hostname = uri.getHost();
- int port = uri.getPort();
+ AbsTransport transport = TransportFactory.getTransport(uri.getScheme());
- HostBean host = hostdb.findHost(nickname, username, hostname, port);
+ Map<String, String> selection = new HashMap<String, String>();
+
+ transport.getSelectionArgs(uri, selection);
+ if (selection.size() == 0) {
+ Log.e(TAG, String.format("Transport %s failed to do something useful with URI=%s",
+ uri.getScheme(), uri.toString()));
+ throw new IllegalStateException("Failed to get needed selection arguments");
+ }
+
+ HostBean host = hostdb.findHost(selection);
if (host == null) {
- Log.d(TAG, String.format("Didn't find existing host (nickname=%s, username=%s, hostname=%s, port=%d)",
- nickname, username, hostname, port));
- host = new HostBean(nickname, username, hostname, port);
+ Log.d(TAG, String.format(
+ "Didn't find existing host (selection=%s)",
+ selection.toString()));
+ host = transport.createHost(uri);
}
this.openConnection(host);
diff --git a/src/org/connectbot/transport/AbsTransport.java b/src/org/connectbot/transport/AbsTransport.java
new file mode 100644
index 0000000..f1a664a
--- /dev/null
+++ b/src/org/connectbot/transport/AbsTransport.java
@@ -0,0 +1,237 @@
+/*
+ ConnectBot: simple, powerful, open-source SSH client for Android
+ Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package org.connectbot.transport;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+
+import android.net.Uri;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public abstract class AbsTransport {
+ HostBean host;
+ TerminalBridge bridge;
+ TerminalManager manager;
+
+ String emulation;
+
+ public AbsTransport() {}
+
+ public AbsTransport(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ this.host = host;
+ this.bridge = bridge;
+ this.manager = manager;
+ }
+
+ /**
+ * @return protocol part of the URI
+ */
+ public static String getProtocolName() {
+ return "unknown";
+ }
+
+ /**
+ * Encode the current transport into a URI that can be passed via intent calls.
+ * @return URI to host
+ */
+ public static Uri getUri(String input) {
+ return null;
+ }
+
+ /**
+ * Causes transport to connect to the target host. After connecting but before a
+ * session is started, must call back to {@link TerminalBridge#onConnected()}.
+ * After that call a session may be opened.
+ */
+ public abstract void connect();
+
+ /**
+ * Reads from the transport. Transport must support reading into a the byte array
+ * <code>buffer</code> at the start of <code>offset</code> and a maximum of
+ * <code>length</code> bytes. If the remote host disconnects, throw an
+ * {@link IOException}.
+ * @param buffer byte buffer to store read bytes into
+ * @param offset where to start writing in the buffer
+ * @param length maximum number of bytes to read
+ * @return number of bytes read
+ * @throws IOException when remote host disconnects
+ */
+ public abstract int read(byte[] buffer, int offset, int length) throws IOException;
+
+ /**
+ * Writes to the transport. If the host is not yet connected, simply return without
+ * doing anything. An {@link IOException} should be thrown if there is an error after
+ * connection.
+ * @param buffer bytes to write to transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(byte[] buffer) throws IOException;
+
+ /**
+ * Writes to the transport. See {@link #write(byte[])} for behavior details.
+ * @param c character to write to the transport
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void write(int c) throws IOException;
+
+ /**
+ * Flushes the write commands to the transport.
+ * @throws IOException when there is a problem writing after connection
+ */
+ public abstract void flush() throws IOException;
+
+ /**
+ * Closes the connection to the terminal. Note that the resulting failure to read
+ * should call {@link TerminalBridge#dispatchDisconnect(boolean)}.
+ */
+ public abstract void close();
+
+ /**
+ * Tells the transport what dimensions the display is currently
+ * @param columns columns of text
+ * @param rows rows of text
+ * @param width width in pixels
+ * @param height height in pixels
+ */
+ public abstract void setDimensions(int columns, int rows, int width, int height);
+
+ public void setOptions(Map<String,String> options) {
+ // do nothing
+ }
+
+ public Map<String,String> getOptions() {
+ return null;
+ }
+
+ public void setCompression(boolean compression) {
+ // do nothing
+ }
+
+ public void setEmulation(String emulation) {
+ this.emulation = emulation;
+ }
+
+ public String getEmulation() {
+ return emulation;
+ }
+
+ public void setHost(HostBean host) {
+ this.host = host;
+ }
+
+ public void setBridge(TerminalBridge bridge) {
+ this.bridge = bridge;
+ }
+
+ public void setManager(TerminalManager manager) {
+ this.manager = manager;
+ }
+
+ /**
+ * Whether or not this transport type can forward ports.
+ * @return true on ability to forward ports
+ */
+ public static boolean canForwardPorts() {
+ return false;
+ }
+
+ /**
+ * Adds the {@link PortForwardBean} to the list.
+ * @param portForward the port forward bean to add
+ * @return true on successful addition
+ */
+ public boolean addPortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Enables a port forward member. After calling this method, the port forward should
+ * be operational iff it could be enabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward setup
+ */
+ public boolean enablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Disables a port forward member. After calling this method, the port forward should
+ * be non-functioning iff it could be disabled by the transport.
+ * @param portForward member of our current port forwards list to enable
+ * @return true on successful port forward tear-down
+ */
+ public boolean disablePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Removes the {@link PortForwardBean} from the available port forwards.
+ * @param portForward the port forward bean to remove
+ * @return true on successful removal
+ */
+ public boolean removePortForward(PortForwardBean portForward) {
+ return false;
+ }
+
+ /**
+ * Gets a list of the {@link PortForwardBean} currently used by this transport.
+ * @return the list of port forwards
+ */
+ public List<PortForwardBean> getPortForwards() {
+ return null;
+ }
+
+ public abstract boolean isConnected();
+ public abstract boolean isSessionOpen();
+
+ /**
+ * @return int default port for protocol
+ */
+ public abstract int getDefaultPort();
+
+ /**
+ * @param username
+ * @param hostname
+ * @param port
+ * @return
+ */
+ public abstract String getDefaultNickname(String username, String hostname, int port);
+
+ /**
+ * @param uri
+ * @param selectionKeys
+ * @param selectionValues
+ */
+ public abstract void getSelectionArgs(Uri uri, Map<String, String> selection);
+
+ /**
+ * @param uri
+ * @return
+ */
+ public abstract HostBean createHost(Uri uri);
+}
diff --git a/src/org/connectbot/transport/Local.java b/src/org/connectbot/transport/Local.java
new file mode 100644
index 0000000..71ccb45
--- /dev/null
+++ b/src/org/connectbot/transport/Local.java
@@ -0,0 +1,215 @@
+/*
+ ConnectBot: simple, powerful, open-source SSH client for Android
+ Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package org.connectbot.transport;
+
+import java.io.FileDescriptor;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.reflect.Method;
+import java.util.Map;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.net.Uri;
+import android.util.Log;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class Local extends AbsTransport {
+ private static final String TAG = "ConnectBot.Local";
+ private static final String PROTOCOL = "local";
+
+ private static final String DEFAULT_URI = "local:#Local";
+
+ private static Method mExec_openSubprocess;
+ private static Method mExec_waitFor;
+ private static Method mExec_setPtyWindowSize;
+
+ private FileDescriptor shellFd;
+
+ private FileInputStream is;
+ private FileOutputStream os;
+
+ static {
+ initPrivateAPI();
+ }
+
+ private static void initPrivateAPI() {
+ try {
+ Class<?> mExec = Class.forName("android.os.Exec");
+ mExec_openSubprocess = mExec.getMethod("createSubprocess",
+ String.class, String.class, String.class, int[].class);
+ mExec_waitFor = mExec.getMethod("waitFor", int.class);
+ mExec_setPtyWindowSize = mExec.getMethod("setPtyWindowSize",
+ FileDescriptor.class, int.class, int.class, int.class, int.class);
+ } catch (NoSuchMethodException e) {
+ // Give up
+ } catch (ClassNotFoundException e) {
+ // Give up
+ }
+ }
+
+ /**
+ *
+ */
+ public Local() {
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Local(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void close() {
+ try {
+ os.close();
+ is.close();
+ os = null;
+ is = null;
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't close shell", e);
+ }
+ }
+
+ @Override
+ public void connect() {
+ int[] pids = new int[1];
+
+ try {
+ shellFd = (FileDescriptor) mExec_openSubprocess.invoke(null,
+ "/system/bin/sh", "-", null, pids);
+ } catch (Exception e) {
+ bridge.outputLine(manager.res.getString(R.string.local_shell_unavailable));
+ return;
+ }
+
+ final int shellPid = pids[0];
+ Runnable exitWatcher = new Runnable() {
+ public void run() {
+ try {
+ mExec_waitFor.invoke(null, shellPid);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't wait for shell exit", e);
+ }
+
+ bridge.dispatchDisconnect(false);
+ }
+ };
+
+ Thread exitWatcherThread = new Thread(exitWatcher);
+ exitWatcherThread.setName("LocalExitWatcher");
+ exitWatcherThread.start();
+
+ is = new FileInputStream(shellFd);
+ os = new FileOutputStream(shellFd);
+
+ bridge.onConnected();
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ return DEFAULT_URI;
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return 0;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return is != null && os != null;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ if (is == null) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("session closed");
+ }
+ return is.read(buffer, start, len);
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ mExec_setPtyWindowSize.invoke(null, shellFd, columns, rows, width, height);
+ } catch (Exception e) {
+ Log.e(TAG, "Couldn't resize pty", e);
+ }
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (os != null)
+ os.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (os != null)
+ os.write(c);
+ }
+
+ public static Uri getUri(String input) {
+ return Uri.parse(DEFAULT_URI);
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+ host.setNickname(uri.getFragment());
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ }
+}
diff --git a/src/org/connectbot/transport/SSH.java b/src/org/connectbot/transport/SSH.java
new file mode 100644
index 0000000..276cc1b
--- /dev/null
+++ b/src/org/connectbot/transport/SSH.java
@@ -0,0 +1,785 @@
+/*
+ ConnectBot: simple, powerful, open-source SSH client for Android
+ Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package org.connectbot.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.R;
+import org.connectbot.bean.HostBean;
+import org.connectbot.bean.PortForwardBean;
+import org.connectbot.bean.PubkeyBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+import org.connectbot.util.PubkeyDatabase;
+import org.connectbot.util.PubkeyUtils;
+
+import android.net.Uri;
+import android.util.Log;
+
+import com.trilead.ssh2.ChannelCondition;
+import com.trilead.ssh2.Connection;
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.ConnectionMonitor;
+import com.trilead.ssh2.DynamicPortForwarder;
+import com.trilead.ssh2.InteractiveCallback;
+import com.trilead.ssh2.KnownHosts;
+import com.trilead.ssh2.LocalPortForwarder;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.Session;
+import com.trilead.ssh2.crypto.PEMDecoder;
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class SSH extends AbsTransport implements ConnectionMonitor, InteractiveCallback {
+ public SSH() {
+ super();
+ }
+
+ /**
+ * @param bridge
+ * @param db
+ */
+ public SSH(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ private static final String PROTOCOL = "ssh";
+ private static final String TAG = "ConnectBot.SSH";
+ private static final int DEFAULT_PORT = 22;
+
+ private static final String AUTH_PUBLICKEY = "publickey",
+ AUTH_PASSWORD = "password",
+ AUTH_KEYBOARDINTERACTIVE = "keyboard-interactive";
+
+ private final static int AUTH_TRIES = 20;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^(.+)@([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ private boolean compression = false;
+// private volatile boolean authenticated = false;
+ private volatile boolean connected = false;
+ private volatile boolean sessionOpen = false;
+
+ private boolean pubkeysExhausted = false;
+
+ private Connection connection;
+ private Session session;
+ private ConnectionInfo connectionInfo;
+
+ private OutputStream stdin;
+ private InputStream stdout;
+ private InputStream stderr;
+
+ private static final int conditions = ChannelCondition.STDOUT_DATA
+ | ChannelCondition.STDERR_DATA
+ | ChannelCondition.CLOSED
+ | ChannelCondition.EOF;
+
+ private List<PortForwardBean> portForwards = new LinkedList<PortForwardBean>();
+
+ private int columns;
+ private int rows;
+
+ private int width;
+ private int height;
+
+ public class HostKeyVerifier implements ServerHostKeyVerifier {
+ public boolean verifyServerHostKey(String hostname, int port,
+ String serverHostKeyAlgorithm, byte[] serverHostKey) throws IOException {
+
+ // read in all known hosts from hostdb
+ KnownHosts hosts = manager.hostdb.getKnownHosts();
+ Boolean result;
+
+ String matchName = String.format("%s:%d", hostname, port);
+
+ String fingerprint = KnownHosts.createHexFingerprint(serverHostKeyAlgorithm, serverHostKey);
+
+ String algorithmName;
+ if ("ssh-rsa".equals(serverHostKeyAlgorithm))
+ algorithmName = "RSA";
+ else if ("ssh-dss".equals(serverHostKeyAlgorithm))
+ algorithmName = "DSA";
+ else
+ algorithmName = serverHostKeyAlgorithm;
+
+ switch(hosts.verifyHostkey(matchName, serverHostKeyAlgorithm, serverHostKey)) {
+ case KnownHosts.HOSTKEY_IS_OK:
+ bridge.outputLine(String.format("Verified host %s key: %s", algorithmName, fingerprint));
+ return true;
+
+ case KnownHosts.HOSTKEY_IS_NEW:
+ // prompt user
+ bridge.outputLine(String.format(manager.res.getString(R.string.host_authenticity_warning), hostname));
+ bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint), algorithmName, fingerprint));
+
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ case KnownHosts.HOSTKEY_HAS_CHANGED:
+ String header = String.format("@ %s @",
+ manager.res.getString(R.string.host_verification_failure_warning_header));
+
+ char[] atsigns = new char[header.length()];
+ Arrays.fill(atsigns, '@');
+ String border = new String(atsigns);
+
+ bridge.outputLine(border);
+ bridge.outputLine(manager.res.getString(R.string.host_verification_failure_warning));
+ bridge.outputLine(border);
+
+ bridge.outputLine(String.format(manager.res.getString(R.string.host_fingerprint),
+ algorithmName, fingerprint));
+
+ // Users have no way to delete keys, so we'll prompt them for now.
+ result = bridge.promptHelper.requestBooleanPrompt(null, manager.res.getString(R.string.prompt_continue_connecting));
+ if(result == null) return false;
+ if(result.booleanValue()) {
+ // save this key in known database
+ manager.hostdb.saveKnownHost(hostname, port, serverHostKeyAlgorithm, serverHostKey);
+ }
+ return result.booleanValue();
+
+ default:
+ return false;
+ }
+ }
+
+ }
+
+ private void authenticate() {
+ try {
+ if (connection.authenticateWithNone(host.getUsername())) {
+ finishConnection();
+ return;
+ }
+ } catch(Exception e) {
+ Log.d(TAG, "Host does not support 'none' authentication.");
+ }
+
+ bridge.outputLine("Trying to authenticate");
+
+ try {
+ long pubkeyId = host.getPubkeyId();
+
+ if (!pubkeysExhausted &&
+ pubkeyId != HostDatabase.PUBKEYID_NEVER &&
+ connection.isAuthMethodAvailable(host.getUsername(), AUTH_PUBLICKEY)) {
+
+ // if explicit pubkey defined for this host, then prompt for password as needed
+ // otherwise just try all in-memory keys held in terminalmanager
+
+ if (pubkeyId == HostDatabase.PUBKEYID_ANY) {
+ // try each of the in-memory keys
+ bridge.outputLine(manager.res
+ .getString(R.string.terminal_auth_pubkey_any));
+ for(String nickname : manager.loadedPubkeys.keySet()) {
+ Object trileadKey = manager.loadedPubkeys.get(nickname);
+ if(this.tryPublicKey(host.getUsername(), nickname, trileadKey)) {
+ finishConnection();
+ break;
+ }
+ }
+ } else {
+ bridge.outputLine("Attempting 'publickey' authentication with a specific public key");
+ // use a specific key for this host, as requested
+ PubkeyBean pubkey = manager.pubkeydb.findPubkeyById(pubkeyId);
+
+ if (pubkey == null)
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pubkey_invalid));
+ else
+ if (tryPublicKey(pubkey))
+ finishConnection();
+ }
+
+ pubkeysExhausted = true;
+ } else if (connection.isAuthMethodAvailable(host.getUsername(), AUTH_PASSWORD)) {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass));
+ String password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.terminal_auth_pass_hint));
+ if (password != null
+ && connection.authenticateWithPassword(host.getUsername(), password)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_pass_fail));
+ }
+ } else if(connection.isAuthMethodAvailable(host.getUsername(), AUTH_KEYBOARDINTERACTIVE)) {
+ // this auth method will talk with us using InteractiveCallback interface
+ // it blocks until authentication finishes
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki));
+ if(connection.authenticateWithKeyboardInteractive(host.getUsername(), this)) {
+ finishConnection();
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_ki_fail));
+ }
+ } else {
+ bridge.outputLine(manager.res.getString(R.string.terminal_auth_fail));
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Problem during handleAuthentication()", e);
+ }
+ }
+
+ /**
+ * Attempt connection with database row pointed to by cursor.
+ * @param cursor
+ * @return true for successful authentication
+ * @throws NoSuchAlgorithmException
+ * @throws InvalidKeySpecException
+ * @throws IOException
+ */
+ private boolean tryPublicKey(PubkeyBean pubkey) throws NoSuchAlgorithmException, InvalidKeySpecException, IOException {
+ Object trileadKey = null;
+ if(manager.isKeyLoaded(pubkey.getNickname())) {
+ // load this key from memory if its already there
+ Log.d(TAG, String.format("Found unlocked key '%s' already in-memory", pubkey.getNickname()));
+ trileadKey = manager.getKey(pubkey.getNickname());
+
+ } else {
+ // otherwise load key from database and prompt for password as needed
+ String password = null;
+ if (pubkey.isEncrypted()) {
+ password = bridge.getPromptHelper().requestStringPrompt(null,
+ manager.res.getString(R.string.prompt_pubkey_password, pubkey.getNickname()));
+
+ // Something must have interrupted the prompt.
+ if (password == null)
+ return false;
+ }
+
+ if(PubkeyDatabase.KEY_TYPE_IMPORTED.equals(pubkey.getType())) {
+ // load specific key using pem format
+ trileadKey = PEMDecoder.decode(new String(pubkey.getPrivateKey()).toCharArray(), password);
+ } else {
+ // load using internal generated format
+ PrivateKey privKey;
+ try {
+ privKey = PubkeyUtils.decodePrivate(pubkey.getPrivateKey(),
+ pubkey.getType(), password);
+ } catch (Exception e) {
+ String message = String.format("Bad password for key '%s'. Authentication failed.", pubkey.getNickname());
+ Log.e(TAG, message, e);
+ bridge.outputLine(message);
+ return false;
+ }
+
+ PublicKey pubKey = PubkeyUtils.decodePublic(pubkey.getPublicKey(),
+ pubkey.getType());
+
+ // convert key to trilead format
+ trileadKey = PubkeyUtils.convertToTrilead(privKey, pubKey);
+ Log.d(TAG, "Unlocked key " + PubkeyUtils.formatKey(pubKey));
+ }
+
+ Log.d(TAG, String.format("Unlocked key '%s'", pubkey.getNickname()));
+
+ // save this key in-memory if option enabled
+ if(manager.isSavingKeys()) {
+ manager.addKey(pubkey.getNickname(), trileadKey);
+ }
+ }
+
+ return tryPublicKey(host.getUsername(), pubkey.getNickname(), trileadKey);
+ }
+
+ private boolean tryPublicKey(String username, String keyNickname, Object trileadKey) throws IOException {
+ //bridge.outputLine(String.format("Attempting 'publickey' with key '%s' [%s]...", keyNickname, trileadKey.toString()));
+ boolean success = connection.authenticateWithPublicKey(username, trileadKey);
+ if(!success)
+ bridge.outputLine(String.format("Authentication method 'publickey' with key '%s' failed", keyNickname));
+ return success;
+ }
+
+ /**
+ * Internal method to request actual PTY terminal once we've finished
+ * authentication. If called before authenticated, it will just fail.
+ */
+ private void finishConnection() {
+ for (PortForwardBean portForward : portForwards) {
+ try {
+ enablePortForward(portForward);
+ bridge.outputLine(String.format("Enable port forward: %s", portForward.getDescription()));
+ } catch (Exception e) {
+ Log.e(TAG, "Error setting up port forward during connect", e);
+ }
+ }
+
+ if (!host.getWantSession()) {
+ bridge.outputLine("Session will not be started due to host preference.");
+ bridge.onConnected();
+ return;
+ }
+
+ try {
+ session = connection.openSession();
+
+ session.requestPTY(getEmulation(), columns, rows, width, height, null);
+ session.startShell();
+
+ stdin = session.getStdin();
+ stdout = session.getStdout();
+ stderr = session.getStderr();
+
+ sessionOpen = true;
+
+ bridge.onConnected();
+ } catch (IOException e1) {
+ Log.e(TAG, "Problem while trying to create PTY in finishConnection()", e1);
+ }
+
+ }
+
+ @Override
+ public void connect() {
+ connection = new Connection(host.getHostname(), host.getPort());
+ connection.addConnectionMonitor(this);
+
+ try {
+ connection.setCompression(compression);
+ } catch (IOException e) {
+ Log.e(TAG, "Could not enable compression!", e);
+ }
+
+ try {
+ /* Uncomment when debugging SSH protocol:
+ DebugLogger logger = new DebugLogger() {
+
+ public void log(int level, String className, String message) {
+ Log.d("SSH", message);
+ }
+
+ };
+ Logger.enabled = true;
+ Logger.logger = logger;
+ */
+ connectionInfo = connection.connect(new HostKeyVerifier());
+ connected = true;
+
+ if (connectionInfo.clientToServerCryptoAlgorithm
+ .equals(connectionInfo.serverToClientCryptoAlgorithm)
+ && connectionInfo.clientToServerMACAlgorithm
+ .equals(connectionInfo.serverToClientMACAlgorithm)) {
+ bridge.outputLine(String.format("Using algorithm: %s %s",
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+ } else {
+ bridge.outputLine(String.format(
+ "Client-to-server algorithm: %s %s",
+ connectionInfo.clientToServerCryptoAlgorithm,
+ connectionInfo.clientToServerMACAlgorithm));
+
+ bridge.outputLine(String.format(
+ "Server-to-client algorithm: %s %s",
+ connectionInfo.serverToClientCryptoAlgorithm,
+ connectionInfo.serverToClientMACAlgorithm));
+ }
+ } catch (IOException e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+
+ // Display the reason in the text.
+ bridge.outputLine(e.getCause().getMessage());
+
+ onDisconnect();
+ return;
+ }
+
+ try {
+ // enter a loop to keep trying until authentication
+ int tries = 0;
+ while (connected && !connection.isAuthenticationComplete() && tries++ < AUTH_TRIES) {
+ authenticate();
+
+ // sleep to make sure we dont kill system
+ Thread.sleep(1000);
+ }
+ } catch(Exception e) {
+ Log.e(TAG, "Problem in SSH connection thread during authentication", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (session != null)
+ session.close();
+ if (connection != null)
+ connection.close();
+ }
+
+ private void onDisconnect() {
+ connected = false;
+ bridge.dispatchDisconnect(false);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ if (stdin != null)
+ stdin.flush();
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException{
+ int bytesRead = 0;
+
+ int newConditions = session.waitForCondition(conditions, 0);
+
+ if ((newConditions & ChannelCondition.STDOUT_DATA) != 0) {
+ bytesRead = stdout.read(buffer, start, len);
+ }
+
+ if ((newConditions & ChannelCondition.STDERR_DATA) != 0) {
+ byte discard[] = new byte[256];
+ while (stderr.available() > 0) {
+ stderr.read(discard);
+ }
+ }
+
+ if ((newConditions & ChannelCondition.EOF) != 0) {
+ throw new IOException("Remote end closed connection");
+ }
+
+ return bytesRead;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (stdin != null)
+ stdin.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (stdin != null)
+ stdin.write(c);
+ }
+
+ @Override
+ public Map<String, String> getOptions() {
+ Map<String, String> options = new HashMap<String, String>();
+
+ options.put("compression", Boolean.toString(compression));
+
+ return options;
+ }
+
+ @Override
+ public void setOptions(Map<String, String> options) {
+ if (options.containsKey("compression"))
+ compression = Boolean.parseBoolean(options.get("compression"));
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return sessionOpen;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ public void connectionLost(Throwable reason) {
+ bridge.dispatchDisconnect(true);
+ }
+
+ public static boolean canForwardPorts() {
+ return true;
+ }
+
+ @Override
+ public List<PortForwardBean> getPortForwards() {
+ return portForwards;
+ }
+
+ @Override
+ public boolean addPortForward(PortForwardBean portForward) {
+ return portForwards.add(portForward);
+ }
+
+ @Override
+ public boolean removePortForward(PortForwardBean portForward) {
+ // Make sure we don't have a phantom forwarder.
+ disablePortForward(portForward);
+
+ return portForwards.remove(portForward);
+ }
+
+ @Override
+ public boolean enablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to enable port forward not in list");
+ return false;
+ }
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ try {
+ lpf = connection.createLocalPortForwarder(
+ new InetSocketAddress(InetAddress.getLocalHost(), portForward.getSourcePort()),
+ portForward.getDestAddr(), portForward.getDestPort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not create local port forward", e);
+ return false;
+ }
+
+ if (lpf == null) {
+ Log.e(TAG, "returned LocalPortForwarder object is null");
+ return false;
+ }
+
+ portForward.setIdentifier(lpf);
+ portForward.setEnabled(true);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ try {
+ connection.requestRemotePortForwarding("", portForward.getSourcePort(), portForward.getDestAddr(), portForward.getDestPort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not create remote port forward", e);
+ return false;
+ }
+
+ portForward.setEnabled(false);
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+
+ try {
+ dpf = connection.createDynamicPortForwarder(portForward.getSourcePort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not create dynamic port forward", e);
+ return false;
+ }
+
+ portForward.setIdentifier(dpf);
+ portForward.setEnabled(true);
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public boolean disablePortForward(PortForwardBean portForward) {
+ if (!portForwards.contains(portForward)) {
+ Log.e(TAG, "Attempt to disable port forward not in list");
+ return false;
+ }
+
+ if (HostDatabase.PORTFORWARD_LOCAL.equals(portForward.getType())) {
+ LocalPortForwarder lpf = null;
+ lpf = (LocalPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || lpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ lpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop local port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_REMOTE.equals(portForward.getType())) {
+ portForward.setEnabled(false);
+
+ try {
+ connection.cancelRemotePortForwarding(portForward.getSourcePort());
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop remote port forwarding, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else if (HostDatabase.PORTFORWARD_DYNAMIC5.equals(portForward.getType())) {
+ DynamicPortForwarder dpf = null;
+ dpf = (DynamicPortForwarder)portForward.getIdentifier();
+
+ if (!portForward.isEnabled() || dpf == null) {
+ Log.d(TAG, String.format("Could not disable %s; it appears to be not enabled or have no handler", portForward.getNickname()));
+ return false;
+ }
+
+ portForward.setEnabled(false);
+
+ try {
+ dpf.close();
+ } catch (IOException e) {
+ Log.e(TAG, "Could not stop dynamic port forwarder, setting enabled to false", e);
+ return false;
+ }
+
+ return true;
+ } else {
+ // Unsupported type
+ Log.e(TAG, String.format("attempt to forward unknown type %s", portForward.getType()));
+ return false;
+ }
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ this.columns = columns;
+ this.rows = rows;
+
+ if (sessionOpen) {
+ try {
+ session.resizePTY(columns, rows, width, height);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't send resize PTY packet", e);
+ }
+ }
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format("%s@%s", username, hostname);
+ } else {
+ return String.format("%s@%s:%d", username, hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(Uri.encode(matcher.group(1)))
+ .append('@')
+ .append(matcher.group(2));
+
+ String portString = matcher.group(4);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':')
+ .append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ /**
+ * Handle challenges from keyboard-interactive authentication mode.
+ */
+ public String[] replyToChallenge(String name, String instruction, int numPrompts, String[] prompt, boolean[] echo) {
+ String[] responses = new String[numPrompts];
+ for(int i = 0; i < numPrompts; i++) {
+ // request response from user for each prompt
+ responses[i] = bridge.promptHelper.requestStringPrompt(instruction, prompt[i]);
+ }
+ return responses;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+ host.setNickname(uri.getFragment());
+ host.setHostname(uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+ host.setUsername(uri.getUserInfo());
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ selection.put(HostDatabase.FIELD_HOST_USERNAME, uri.getUserInfo());
+ }
+
+ @Override
+ public void setCompression(boolean compression) {
+ this.compression = compression;
+ }
+}
diff --git a/src/org/connectbot/transport/Telnet.java b/src/org/connectbot/transport/Telnet.java
new file mode 100644
index 0000000..e0d67a7
--- /dev/null
+++ b/src/org/connectbot/transport/Telnet.java
@@ -0,0 +1,284 @@
+/*
+ ConnectBot: simple, powerful, open-source SSH client for Android
+ Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package org.connectbot.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.net.UnknownHostException;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.connectbot.bean.HostBean;
+import org.connectbot.service.TerminalBridge;
+import org.connectbot.service.TerminalManager;
+import org.connectbot.util.HostDatabase;
+
+import android.net.Uri;
+import android.util.Log;
+import de.mud.telnet.TelnetProtocolHandler;
+
+/**
+ * Telnet transport implementation.<br/>
+ * Original idea from the JTA telnet package (de.mud.telnet)
+ *
+ * @author Kenny Root
+ *
+ */
+public class Telnet extends AbsTransport {
+ private static final String TAG = "ConnectBot.Telnet";
+ private static final String PROTOCOL = "telnet";
+
+ private static final int DEFAULT_PORT = 23;
+
+ private TelnetProtocolHandler handler;
+ private Socket socket;
+
+ private InputStream is;
+ private OutputStream os;
+ private int width;
+ private int height;
+
+ private boolean connected = false;
+
+ static final Pattern hostmask;
+ static {
+ hostmask = Pattern.compile("^([0-9a-z.-]+)(:(\\d+))?$", Pattern.CASE_INSENSITIVE);
+ }
+
+ public Telnet() {
+ handler = new TelnetProtocolHandler() {
+ /** get the current terminal type */
+ @Override
+ public String getTerminalType() {
+ return getEmulation();
+ }
+
+ /** get the current window size */
+ @Override
+ public int[] getWindowSize() {
+ return new int[] { width, height };
+ }
+
+ /** notify about local echo */
+ @Override
+ public void setLocalEcho(boolean echo) {
+ /* EMPTY */
+ }
+
+ /** write data to our back end */
+ @Override
+ public void write(byte[] b) throws IOException {
+ os.write(b);
+ }
+
+ /** sent on IAC EOR (prompt terminator for remote access systems). */
+ @Override
+ public void notifyEndOfRecord() {
+ }
+ };
+ }
+
+ /**
+ * @param host
+ * @param bridge
+ * @param manager
+ */
+ public Telnet(HostBean host, TerminalBridge bridge, TerminalManager manager) {
+ super(host, bridge, manager);
+ }
+
+ public static String getProtocolName() {
+ return PROTOCOL;
+ }
+
+ @Override
+ public void connect() {
+ try {
+ socket = new Socket(host.getHostname(), host.getPort());
+
+ connected = true;
+
+ is = socket.getInputStream();
+ os = socket.getOutputStream();
+
+ bridge.onConnected();
+ } catch (UnknownHostException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ } catch (IOException e) {
+ Log.d(TAG, "IO Exception connecting to host", e);
+ }
+ }
+
+ @Override
+ public void close() {
+ if (socket != null)
+ try {
+ socket.close();
+ } catch (IOException e) {
+ Log.d(TAG, "Error closing telnet socket.", e);
+ }
+ }
+
+ @Override
+ public void flush() throws IOException {
+ os.flush();
+ }
+
+ @Override
+ public int getDefaultPort() {
+ return DEFAULT_PORT;
+ }
+
+ @Override
+ public boolean isConnected() {
+ return connected;
+ }
+
+ @Override
+ public boolean isSessionOpen() {
+ return connected;
+ }
+
+ @Override
+ public int read(byte[] buffer, int start, int len) throws IOException {
+ /* process all already read bytes */
+ int n = 0;
+
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return start + n;
+ } while (n == 0);
+
+ while (n <= 0) {
+ do {
+ n = handler.negotiate(buffer, start);
+ if (n > 0)
+ return n;
+ } while (n == 0);
+ n = is.read(buffer, start, len);
+ if (n < 0) {
+ bridge.dispatchDisconnect(false);
+ throw new IOException("Remote end closed connection.");
+ }
+
+ handler.inputfeed(buffer, start, n - start);
+ n = handler.negotiate(buffer, start);
+ }
+ return n;
+ }
+
+ @Override
+ public void write(byte[] buffer) throws IOException {
+ if (os != null)
+ os.write(buffer);
+ }
+
+ @Override
+ public void write(int c) throws IOException {
+ if (os != null)
+ os.write(c);
+ }
+
+ @Override
+ public void setDimensions(int columns, int rows, int width, int height) {
+ try {
+ handler.setWindowSize(columns, rows);
+ } catch (IOException e) {
+ Log.e(TAG, "Couldn't resize remote terminal", e);
+ }
+ }
+
+ @Override
+ public String getDefaultNickname(String username, String hostname, int port) {
+ if (port == DEFAULT_PORT) {
+ return String.format("%s", hostname);
+ } else {
+ return String.format("%s:%d", hostname, port);
+ }
+ }
+
+ public static Uri getUri(String input) {
+ Matcher matcher = hostmask.matcher(input);
+
+ if (!matcher.matches())
+ return null;
+
+ StringBuilder sb = new StringBuilder();
+
+ sb.append(PROTOCOL)
+ .append("://")
+ .append(matcher.group(1));
+
+ String portString = matcher.group(3);
+ int port = DEFAULT_PORT;
+ if (portString != null) {
+ try {
+ port = Integer.parseInt(portString);
+ if (port < 1 || port > 65535) {
+ port = DEFAULT_PORT;
+ }
+ } catch (NumberFormatException nfe) {
+ // Keep the default port
+ }
+ }
+
+ if (port != DEFAULT_PORT) {
+ sb.append(':');
+ sb.append(port);
+ }
+
+ sb.append("/#")
+ .append(Uri.encode(input));
+
+ Uri uri = Uri.parse(sb.toString());
+
+ return uri;
+ }
+
+ @Override
+ public HostBean createHost(Uri uri) {
+ HostBean host = new HostBean();
+
+ host.setProtocol(PROTOCOL);
+ host.setNickname(uri.getFragment());
+ host.setHostname(uri.getHost());
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ host.setPort(port);
+
+ return host;
+ }
+
+ @Override
+ public void getSelectionArgs(Uri uri, Map<String, String> selection) {
+ selection.put(HostDatabase.FIELD_HOST_PROTOCOL, PROTOCOL);
+ selection.put(HostDatabase.FIELD_HOST_NICKNAME, uri.getFragment());
+ selection.put(HostDatabase.FIELD_HOST_HOSTNAME, uri.getHost());
+
+ int port = uri.getPort();
+ if (port < 0)
+ port = DEFAULT_PORT;
+ selection.put(HostDatabase.FIELD_HOST_PORT, Integer.toString(port));
+ }
+}
diff --git a/src/org/connectbot/transport/TransportFactory.java b/src/org/connectbot/transport/TransportFactory.java
new file mode 100644
index 0000000..e5b893a
--- /dev/null
+++ b/src/org/connectbot/transport/TransportFactory.java
@@ -0,0 +1,86 @@
+/*
+ ConnectBot: simple, powerful, open-source SSH client for Android
+ Copyright (C) 2007-2008 Kenny Root, Jeffrey Sharkey
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
+*/
+
+package org.connectbot.transport;
+
+import android.net.Uri;
+import android.util.Log;
+
+
+/**
+ * @author Kenny Root
+ *
+ */
+public class TransportFactory {
+ private static String[] transportNames = {
+ SSH.getProtocolName(),
+ Telnet.getProtocolName(),
+ Local.getProtocolName(),
+ };
+
+ /**
+ * @param protocol
+ * @return
+ */
+ public static AbsTransport getTransport(String protocol) {
+ if (SSH.getProtocolName().equals(protocol)) {
+ return new SSH();
+ } else if (Telnet.getProtocolName().equals(protocol)) {
+ return new Telnet();
+ } else if (Local.getProtocolName().equals(protocol)) {
+ return new Local();
+ } else {
+ return null;
+ }
+ }
+
+ public static Uri getUri(String scheme, String input) {
+ Log.d("TransportFactory", String.format(
+ "Attempting to discover URI for scheme=%s on input=%s", scheme,
+ input));
+ if (SSH.getProtocolName().equals(scheme))
+ return SSH.getUri(input);
+ else if (Telnet.getProtocolName().equals(scheme))
+ return Telnet.getUri(input);
+ else if (Local.getProtocolName().equals(scheme)) {
+ Log.d("TransportFactory", "Got to the local parsing area");
+ return Local.getUri(input);
+ } else
+ return null;
+ }
+
+ public static String[] getTransportNames() {
+ return transportNames;
+ }
+
+ public static boolean isSameTransportType(AbsTransport a, AbsTransport b) {
+ if (a == null || b == null)
+ return false;
+
+ return a.getClass().equals(b.getClass());
+ }
+
+ public static boolean canForwardPorts(String protocol) {
+ // TODO uh, make this have less knowledge about its children
+ if (SSH.getProtocolName().equals(protocol)) {
+ return SSH.canForwardPorts();
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/src/org/connectbot/util/HostDatabase.java b/src/org/connectbot/util/HostDatabase.java
index 18edf9e..d2586c0 100644
--- a/src/org/connectbot/util/HostDatabase.java
+++ b/src/org/connectbot/util/HostDatabase.java
@@ -18,22 +18,26 @@
package org.connectbot.util;
+import java.util.Iterator;
import java.nio.charset.Charset;
import java.util.LinkedList;
import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
import org.connectbot.bean.HostBean;
import org.connectbot.bean.PortForwardBean;
-import com.trilead.ssh2.KnownHosts;
-
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;
+import com.trilead.ssh2.KnownHosts;
+
/**
* Contains information about various SSH hosts, include public hostkey if known
* from previous sessions.
@@ -45,10 +49,11 @@ public class HostDatabase extends SQLiteOpenHelper {
public final static String TAG = "ConnectBot.HostDatabase";
public final static String DB_NAME = "hosts";
- public final static int DB_VERSION = 15;
+ public final static int DB_VERSION = 16;
public final static String TABLE_HOSTS = "hosts";
public final static String FIELD_HOST_NICKNAME = "nickname";
+ public final static String FIELD_HOST_PROTOCOL = "protocol";
public final static String FIELD_HOST_USERNAME = "username";
public final static String FIELD_HOST_HOSTNAME = "hostname";
public final static String FIELD_HOST_PORT = "port";
@@ -94,9 +99,12 @@ public class HostDatabase extends SQLiteOpenHelper {
@Override
public void onCreate(SQLiteDatabase db) {
+ dropAllTables(db);
+
db.execSQL("CREATE TABLE " + TABLE_HOSTS
+ " (_id INTEGER PRIMARY KEY, "
+ FIELD_HOST_NICKNAME + " TEXT, "
+ + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh', "
+ FIELD_HOST_USERNAME + " TEXT, "
+ FIELD_HOST_HOSTNAME + " TEXT, "
+ FIELD_HOST_PORT + " INTEGER, "
@@ -110,10 +118,6 @@ public class HostDatabase extends SQLiteOpenHelper {
+ FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "', "
+ FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "', "
+ FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "')");
- // insert a few sample hosts, none of which probably connect
- //this.createHost(db, "connectbot@bravo", "connectbot", "192.168.254.230", 22, COLOR_GRAY);
- //this.createHost(db, "cron@server.example.com", "cron", "server.example.com", 22, COLOR_GRAY, PUBKEYID_ANY);
- //this.createHost(db, "backup@example.net", "backup", "example.net", 22, COLOR_BLUE, PUBKEYID_ANY);
db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ " (_id INTEGER PRIMARY KEY, "
@@ -135,29 +139,102 @@ public class HostDatabase extends SQLiteOpenHelper {
return;
}
- switch (oldVersion) {
- case 10:
- db.execSQL("ALTER TABLE " + TABLE_HOSTS
- + " ADD COLUMN " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY);
- case 11:
- db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
- + " (_id INTEGER PRIMARY KEY, "
- + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
- + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
- + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
- + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
- + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
- + FIELD_PORTFORWARD_DESTPORT + " INTEGER)");
- case 12:
- db.execSQL("ALTER TABLE " + TABLE_HOSTS
- + " ADD COLUMN " + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "'");
- case 13:
- db.execSQL("ALTER TABLE " + TABLE_HOSTS
- + " ADD COLUMN " + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "'");
- case 14:
- db.execSQL("ALTER TABLE " + TABLE_HOSTS
- + " ADD COLUMN " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "'");
+ try {
+ switch (oldVersion) {
+ case 10:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PUBKEYID + " INTEGER DEFAULT " + PUBKEYID_ANY);
+ case 11:
+ db.execSQL("CREATE TABLE " + TABLE_PORTFORWARDS
+ + " (_id INTEGER PRIMARY KEY, "
+ + FIELD_PORTFORWARD_HOSTID + " INTEGER, "
+ + FIELD_PORTFORWARD_NICKNAME + " TEXT, "
+ + FIELD_PORTFORWARD_TYPE + " TEXT NOT NULL DEFAULT " + PORTFORWARD_LOCAL + ", "
+ + FIELD_PORTFORWARD_SOURCEPORT + " INTEGER NOT NULL DEFAULT 8080, "
+ + FIELD_PORTFORWARD_DESTADDR + " TEXT, "
+ + FIELD_PORTFORWARD_DESTPORT + " INTEGER)");
+ case 12:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_WANTSESSION + " TEXT DEFAULT '" + Boolean.toString(true) + "'");
+ case 13:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_COMPRESSION + " TEXT DEFAULT '" + Boolean.toString(false) + "'");
+ case 14:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_ENCODING + " TEXT DEFAULT '" + ENCODING_DEFAULT + "'");
+ case 15:
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS
+ + " ADD COLUMN " + FIELD_HOST_PROTOCOL + " TEXT DEFAULT 'ssh'");
+ }
+ } catch (SQLiteException e) {
+ // The database has entered an unknown state. Try to recover.
+ try {
+ regenerateTables(db);
+ } catch (SQLiteException e2) {
+ dropAndCreateTables(db);
+ }
+ }
+ }
+
+ private void regenerateTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "OLD_");
+ db.execSQL("ALTER TABLE " + TABLE_HOSTS + " RENAME TO OLD_"
+ + TABLE_HOSTS);
+ db.execSQL("ALTER TABLE " + TABLE_PORTFORWARDS + " RENAME TO OLD_"
+ + TABLE_PORTFORWARDS);
+
+ onCreate(db);
+
+ repopulateTable(db, TABLE_HOSTS);
+ repopulateTable(db, TABLE_PORTFORWARDS);
+
+ dropAllTablesWithPrefix(db, "OLD_");
+ }
+
+ private void repopulateTable(SQLiteDatabase db, String tableName) {
+ String columns = getTableColumnNames(db, tableName);
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("INSERT INTO ")
+ .append(tableName)
+ .append(" (")
+ .append(columns)
+ .append(") SELECT ")
+ .append(columns)
+ .append(" FROM OLD_")
+ .append(tableName);
+
+ String sql = sb.toString();
+ Log.d(TAG, "Attempting to execute repopulation command: " + sql);
+ db.execSQL(sql);
+ }
+
+ private String getTableColumnNames(SQLiteDatabase db, String tableName) {
+ StringBuilder sb = new StringBuilder();
+
+ Cursor fields = db.rawQuery("PRAGMA table_info(" + tableName + ")", null);
+ while (fields.moveToNext()) {
+ if (!fields.isFirst())
+ sb.append(", ");
+ sb.append(fields.getString(1));
}
+ fields.close();
+
+ return sb.toString();
+ }
+
+ private void dropAndCreateTables(SQLiteDatabase db) {
+ dropAllTables(db);
+ onCreate(db);
+ }
+
+ private void dropAllTablesWithPrefix(SQLiteDatabase db, String prefix) {
+ db.execSQL("DROP TABLE IF EXISTS " + prefix + TABLE_HOSTS);
+ db.execSQL("DROP TABLE IF EXISTS " + prefix + TABLE_PORTFORWARDS);
+ }
+
+ private void dropAllTables(SQLiteDatabase db) {
+ dropAllTablesWithPrefix(db, "");
}
/**
@@ -210,12 +287,26 @@ public class HostDatabase extends SQLiteOpenHelper {
String sortField = sortColors ? FIELD_HOST_COLOR : FIELD_HOST_NICKNAME;
SQLiteDatabase db = this.getReadableDatabase();
- List<HostBean> hosts = new LinkedList<HostBean>();
-
Cursor c = db.query(TABLE_HOSTS, null, null, null, null, null, sortField + " ASC");
+ List<HostBean> hosts = createHostBeans(c);
+
+ c.close();
+ db.close();
+
+ return hosts;
+ }
+
+ /**
+ * @param hosts
+ * @param c
+ */
+ private List<HostBean> createHostBeans(Cursor c) {
+ List<HostBean> hosts = new LinkedList<HostBean>();
+
final int COL_ID = c.getColumnIndexOrThrow("_id"),
COL_NICKNAME = c.getColumnIndexOrThrow(FIELD_HOST_NICKNAME),
+ COL_PROTOCOL = c.getColumnIndexOrThrow(FIELD_HOST_PROTOCOL),
COL_USERNAME = c.getColumnIndexOrThrow(FIELD_HOST_USERNAME),
COL_HOSTNAME = c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME),
COL_PORT = c.getColumnIndexOrThrow(FIELD_HOST_PORT),
@@ -233,6 +324,7 @@ public class HostDatabase extends SQLiteOpenHelper {
host.setId(c.getLong(COL_ID));
host.setNickname(c.getString(COL_NICKNAME));
+ host.setProtocol(c.getString(COL_PROTOCOL));
host.setUsername(c.getString(COL_USERNAME));
host.setHostname(c.getString(COL_HOSTNAME));
host.setPort(c.getInt(COL_PORT));
@@ -248,40 +340,62 @@ public class HostDatabase extends SQLiteOpenHelper {
hosts.add(host);
}
+ return hosts;
+ }
+
+ /**
+ * @param c
+ * @return
+ */
+ private HostBean getFirstHostBean(Cursor c) {
+ HostBean host = null;
+
+ List<HostBean> hosts = createHostBeans(c);
+ if (hosts.size() > 0)
+ host = hosts.get(0);
+
c.close();
- db.close();
- return hosts;
+ return host;
}
/**
* @param nickname
+ * @param protocol
* @param username
* @param hostname
+ * @param hostname2
* @param port
* @return
*/
- public HostBean findHost(String nickname, String username, String hostname,
- int port) {
+ public HostBean findHost(Map<String, String> selection) {
SQLiteDatabase db = this.getReadableDatabase();
- Cursor c = db.query(TABLE_HOSTS, null,
- FIELD_HOST_NICKNAME + " = ? AND " +
- FIELD_HOST_USERNAME + " = ? AND " +
- FIELD_HOST_HOSTNAME + " = ? AND " +
- FIELD_HOST_PORT + " = ?",
- new String[] { nickname, username, hostname, String.valueOf(port) },
- null, null, null);
+ StringBuilder selectionBuilder = new StringBuilder();
- HostBean host = null;
+ Iterator<Entry<String, String>> i = selection.entrySet().iterator();
- if (c != null) {
- if (c.moveToFirst())
- host = createHostBean(c);
+ String[] selectionValues = new String[selection.size()];
+ int n = 0;
+ while (i.hasNext()) {
+ Entry<String, String> entry = i.next();
- c.close();
+ if (n > 0)
+ selectionBuilder.append(" AND ");
+
+ selectionBuilder.append(entry.getKey())
+ .append(" = ?");
+
+ selectionValues[n++] = entry.getValue();
}
+ Cursor c = db.query(TABLE_HOSTS, null,
+ selectionBuilder.toString(),
+ selectionValues,
+ null, null, null);
+
+ HostBean host = getFirstHostBean(c);
+
db.close();
return host;
@@ -298,40 +412,13 @@ public class HostDatabase extends SQLiteOpenHelper {
"_id = ?", new String[] { String.valueOf(hostId) },
null, null, null);
- HostBean host = null;
-
- if (c != null) {
- if (c.moveToFirst())
- host = createHostBean(c);
-
- c.close();
- }
+ HostBean host = getFirstHostBean(c);
db.close();
return host;
}
- private HostBean createHostBean(Cursor c) {
- HostBean host = new HostBean();
-
- host.setId(c.getLong(c.getColumnIndexOrThrow("_id")));
- host.setNickname(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_NICKNAME)));
- host.setUsername(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_USERNAME)));
- host.setHostname(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_HOSTNAME)));
- host.setPort(c.getInt(c.getColumnIndexOrThrow(FIELD_HOST_PORT)));
- host.setLastConnect(c.getLong(c.getColumnIndexOrThrow(FIELD_HOST_LASTCONNECT)));
- host.setColor(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_COLOR)));
- host.setUseKeys(Boolean.valueOf(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_USEKEYS))));
- host.setPostLogin(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_POSTLOGIN)));
- host.setPubkeyId(c.getLong(c.getColumnIndexOrThrow(FIELD_HOST_PUBKEYID)));
- host.setWantSession(Boolean.valueOf(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_WANTSESSION))));
- host.setCompression(Boolean.valueOf(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_COMPRESSION))));
- host.setEncoding(c.getString(c.getColumnIndexOrThrow(FIELD_HOST_ENCODING)));
-
- return host;
- }
-
/**
* Record the given hostkey into database under this nickname.
* @param hostname