diff options
author | Kenny Root <kenny@the-b.org> | 2009-06-15 16:35:59 +0000 |
---|---|---|
committer | Kenny Root <kenny@the-b.org> | 2009-06-15 16:35:59 +0000 |
commit | 534184380cd7762bc1be3c5174af001e33f9b08d (patch) | |
tree | e0644be305ec82ce9ec51f66333deaec3a96070f | |
parent | 2adc14e72659787a09aa4b9b40334dc0e1df9194 (diff) | |
download | connectbot-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.xml | 7 | ||||
-rw-r--r-- | res/layout/act_hostlist.xml | 70 | ||||
-rw-r--r-- | src/de/mud/telnet/TelnetProtocolHandler.java | 648 | ||||
-rw-r--r-- | src/org/connectbot/ConsoleActivity.java | 21 | ||||
-rw-r--r-- | src/org/connectbot/HostListActivity.java | 61 | ||||
-rw-r--r-- | src/org/connectbot/bean/HostBean.java | 23 | ||||
-rw-r--r-- | src/org/connectbot/service/Relay.java | 72 | ||||
-rw-r--r-- | src/org/connectbot/service/TerminalBridge.java | 657 | ||||
-rw-r--r-- | src/org/connectbot/service/TerminalManager.java | 38 | ||||
-rw-r--r-- | src/org/connectbot/transport/AbsTransport.java | 237 | ||||
-rw-r--r-- | src/org/connectbot/transport/Local.java | 215 | ||||
-rw-r--r-- | src/org/connectbot/transport/SSH.java | 785 | ||||
-rw-r--r-- | src/org/connectbot/transport/Telnet.java | 284 | ||||
-rw-r--r-- | src/org/connectbot/transport/TransportFactory.java | 86 | ||||
-rw-r--r-- | src/org/connectbot/util/HostDatabase.java | 237 |
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 <type> <bytes> IAC SE + * @param type type of SB + * @param sbata byte array as <bytes> + */ + 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 |