aboutsummaryrefslogtreecommitdiffstats
path: root/sshlib/src/main/java/com/trilead/ssh2/transport
diff options
context:
space:
mode:
authorKenny Root <kenny@the-b.org>2015-07-18 11:30:18 -0700
committerKenny Root <kenny@the-b.org>2015-07-18 11:30:18 -0700
commit2f5f3754dce85212a71138fd80c2300b73461908 (patch)
treeac51a1c9f143bc77a0cfec2e7da1b7c184394a98 /sshlib/src/main/java/com/trilead/ssh2/transport
parent0cf7ac30faecc82e04b080f418b08758624b07f5 (diff)
downloadsshlib-2f5f3754dce85212a71138fd80c2300b73461908.tar.gz
sshlib-2f5f3754dce85212a71138fd80c2300b73461908.tar.bz2
sshlib-2f5f3754dce85212a71138fd80c2300b73461908.zip
Rename project to sshlib
Diffstat (limited to 'sshlib/src/main/java/com/trilead/ssh2/transport')
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java125
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java690
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java24
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java33
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java14
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java12
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java22
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java343
-rw-r--r--sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java750
9 files changed, 2013 insertions, 0 deletions
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java b/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java
new file mode 100644
index 0000000..d7a5ee5
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/ClientServerHello.java
@@ -0,0 +1,125 @@
+
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+import com.trilead.ssh2.Connection;
+
+/**
+ * ClientServerHello.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: ClientServerHello.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $
+ */
+public class ClientServerHello
+{
+ String server_line;
+ String client_line;
+
+ String server_versioncomment;
+
+ public final static int readLineRN(InputStream is, byte[] buffer) throws IOException
+ {
+ int pos = 0;
+ boolean need10 = false;
+ int len = 0;
+ while (true)
+ {
+ int c = is.read();
+ if (c == -1)
+ throw new IOException("Premature connection close");
+
+ buffer[pos++] = (byte) c;
+
+ if (c == 13)
+ {
+ need10 = true;
+ continue;
+ }
+
+ if (c == 10)
+ break;
+
+ if (need10 == true)
+ throw new IOException("Malformed line sent by the server, the line does not end correctly.");
+
+ len++;
+ if (pos >= buffer.length)
+ throw new IOException("The server sent a too long line.");
+ }
+
+ return len;
+ }
+
+ public ClientServerHello(InputStream bi, OutputStream bo) throws IOException
+ {
+ client_line = "SSH-2.0-" + Connection.identification;
+
+ bo.write((client_line + "\r\n").getBytes("ISO-8859-1"));
+ bo.flush();
+
+ byte[] serverVersion = new byte[512];
+
+ for (int i = 0; i < 50; i++)
+ {
+ int len = readLineRN(bi, serverVersion);
+
+ server_line = new String(serverVersion, 0, len, "ISO-8859-1");
+
+ if (server_line.startsWith("SSH-"))
+ break;
+ }
+
+ if (server_line.startsWith("SSH-") == false)
+ throw new IOException(
+ "Malformed server identification string. There was no line starting with 'SSH-' amongst the first 50 lines.");
+
+ if (server_line.startsWith("SSH-1.99-"))
+ server_versioncomment = server_line.substring(9);
+ else if (server_line.startsWith("SSH-2.0-"))
+ server_versioncomment = server_line.substring(8);
+ else
+ throw new IOException("Server uses incompatible protocol, it is not SSH-2 compatible.");
+ }
+
+ /**
+ * @return Returns the client_versioncomment.
+ */
+ public byte[] getClientString()
+ {
+ byte[] result;
+
+ try
+ {
+ result = client_line.getBytes("ISO-8859-1");
+ }
+ catch (UnsupportedEncodingException ign)
+ {
+ result = client_line.getBytes();
+ }
+
+ return result;
+ }
+
+ /**
+ * @return Returns the server_versioncomment.
+ */
+ public byte[] getServerString()
+ {
+ byte[] result;
+
+ try
+ {
+ result = server_line.getBytes("ISO-8859-1");
+ }
+ catch (UnsupportedEncodingException ign)
+ {
+ result = server_line.getBytes();
+ }
+
+ return result;
+ }
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java
new file mode 100644
index 0000000..ee0784a
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexManager.java
@@ -0,0 +1,690 @@
+
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.interfaces.DSAPublicKey;
+import java.security.interfaces.ECPublicKey;
+import java.security.interfaces.RSAPublicKey;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.TreeSet;
+
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.DHGexParameters;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.compression.CompressionFactory;
+import com.trilead.ssh2.compression.ICompressor;
+import com.trilead.ssh2.crypto.CryptoWishList;
+import com.trilead.ssh2.crypto.KeyMaterial;
+import com.trilead.ssh2.crypto.cipher.BlockCipher;
+import com.trilead.ssh2.crypto.cipher.BlockCipherFactory;
+import com.trilead.ssh2.crypto.dh.DhGroupExchange;
+import com.trilead.ssh2.crypto.dh.GenericDhExchange;
+import com.trilead.ssh2.crypto.digest.MAC;
+import com.trilead.ssh2.log.Logger;
+import com.trilead.ssh2.packets.PacketKexDHInit;
+import com.trilead.ssh2.packets.PacketKexDHReply;
+import com.trilead.ssh2.packets.PacketKexDhGexGroup;
+import com.trilead.ssh2.packets.PacketKexDhGexInit;
+import com.trilead.ssh2.packets.PacketKexDhGexReply;
+import com.trilead.ssh2.packets.PacketKexDhGexRequest;
+import com.trilead.ssh2.packets.PacketKexDhGexRequestOld;
+import com.trilead.ssh2.packets.PacketKexInit;
+import com.trilead.ssh2.packets.PacketNewKeys;
+import com.trilead.ssh2.packets.Packets;
+import com.trilead.ssh2.signature.DSASHA1Verify;
+import com.trilead.ssh2.signature.ECDSASHA2Verify;
+import com.trilead.ssh2.signature.RSASHA1Verify;
+
+
+/**
+ * KexManager.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: KexManager.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+public class KexManager
+{
+ private static final Logger log = Logger.getLogger(KexManager.class);
+
+ private static final boolean supportsEc;
+ static {
+ KeyFactory keyFact;
+ try {
+ keyFact = KeyFactory.getInstance("EC");
+ } catch (NoSuchAlgorithmException ignored) {
+ keyFact = null;
+ log.log(10, "Disabling EC support due to lack of KeyFactory");
+ }
+ supportsEc = keyFact != null;
+ }
+
+ private static final Set<String> HOSTKEY_ALGS = new LinkedHashSet<String>();
+ static {
+ if (supportsEc) {
+ HOSTKEY_ALGS.add("ecdsa-sha2-nistp256");
+ HOSTKEY_ALGS.add("ecdsa-sha2-nistp384");
+ HOSTKEY_ALGS.add("ecdsa-sha2-nistp521");
+ }
+ HOSTKEY_ALGS.add("ssh-rsa");
+ HOSTKEY_ALGS.add("ssh-dss");
+ }
+
+ private static final Set<String> KEX_ALGS = new LinkedHashSet<String>();
+ static {
+ if (supportsEc) {
+ KEX_ALGS.add("ecdh-sha2-nistp256");
+ KEX_ALGS.add("ecdh-sha2-nistp384");
+ KEX_ALGS.add("ecdh-sha2-nistp521");
+ }
+ KEX_ALGS.add("diffie-hellman-group-exchange-sha256");
+ KEX_ALGS.add("diffie-hellman-group-exchange-sha1");
+ KEX_ALGS.add("diffie-hellman-group14-sha1");
+ KEX_ALGS.add("diffie-hellman-group1-sha1");
+ }
+
+ KexState kxs;
+ int kexCount = 0;
+ KeyMaterial km;
+ byte[] sessionId;
+ ClientServerHello csh;
+
+ final Object accessLock = new Object();
+ ConnectionInfo lastConnInfo = null;
+
+ boolean connectionClosed = false;
+
+ boolean ignore_next_kex_packet = false;
+
+ final TransportManager tm;
+
+ CryptoWishList nextKEXcryptoWishList;
+ DHGexParameters nextKEXdhgexParameters;
+
+ ServerHostKeyVerifier verifier;
+ final String hostname;
+ final int port;
+ final SecureRandom rnd;
+
+ public KexManager(TransportManager tm, ClientServerHello csh, CryptoWishList initialCwl, String hostname, int port,
+ ServerHostKeyVerifier keyVerifier, SecureRandom rnd)
+ {
+ this.tm = tm;
+ this.csh = csh;
+ this.nextKEXcryptoWishList = initialCwl;
+ this.nextKEXdhgexParameters = new DHGexParameters();
+ this.hostname = hostname;
+ this.port = port;
+ this.verifier = keyVerifier;
+ this.rnd = rnd;
+ }
+
+ public ConnectionInfo getOrWaitForConnectionInfo(int minKexCount) throws IOException
+ {
+ synchronized (accessLock)
+ {
+ while (true)
+ {
+ if ((lastConnInfo != null) && (lastConnInfo.keyExchangeCounter >= minKexCount))
+ return lastConnInfo;
+
+ if (connectionClosed)
+ throw (IOException) new IOException("Key exchange was not finished, connection is closed.")
+ .initCause(tm.getReasonClosedCause());
+
+ try
+ {
+ accessLock.wait();
+ }
+ catch (InterruptedException e)
+ {
+ }
+ }
+ }
+ }
+
+ private String getFirstMatch(String[] client, String[] server) throws NegotiateException
+ {
+ if (client == null || server == null)
+ throw new IllegalArgumentException();
+
+ if (client.length == 0)
+ return null;
+
+ for (int i = 0; i < client.length; i++)
+ {
+ for (int j = 0; j < server.length; j++)
+ {
+ if (client[i].equals(server[j]))
+ return client[i];
+ }
+ }
+ throw new NegotiateException();
+ }
+
+ private boolean compareFirstOfNameList(String[] a, String[] b)
+ {
+ if (a == null || b == null)
+ throw new IllegalArgumentException();
+
+ if ((a.length == 0) && (b.length == 0))
+ return true;
+
+ if ((a.length == 0) || (b.length == 0))
+ return false;
+
+ return (a[0].equals(b[0]));
+ }
+
+ private boolean isGuessOK(KexParameters cpar, KexParameters spar)
+ {
+ if (cpar == null || spar == null)
+ throw new IllegalArgumentException();
+
+ if (compareFirstOfNameList(cpar.kex_algorithms, spar.kex_algorithms) == false)
+ {
+ return false;
+ }
+
+ if (compareFirstOfNameList(cpar.server_host_key_algorithms, spar.server_host_key_algorithms) == false)
+ {
+ return false;
+ }
+
+ /*
+ * We do NOT check here if the other algorithms can be agreed on, this
+ * is just a check if kex_algorithms and server_host_key_algorithms were
+ * guessed right!
+ */
+
+ return true;
+ }
+
+ private NegotiatedParameters mergeKexParameters(KexParameters client, KexParameters server)
+ {
+ NegotiatedParameters np = new NegotiatedParameters();
+
+ try
+ {
+ np.kex_algo = getFirstMatch(client.kex_algorithms, server.kex_algorithms);
+
+ log.log(20, "kex_algo=" + np.kex_algo);
+
+ np.server_host_key_algo = getFirstMatch(client.server_host_key_algorithms,
+ server.server_host_key_algorithms);
+
+ log.log(20, "server_host_key_algo=" + np.server_host_key_algo);
+
+ np.enc_algo_client_to_server = getFirstMatch(client.encryption_algorithms_client_to_server,
+ server.encryption_algorithms_client_to_server);
+ np.enc_algo_server_to_client = getFirstMatch(client.encryption_algorithms_server_to_client,
+ server.encryption_algorithms_server_to_client);
+
+ log.log(20, "enc_algo_client_to_server=" + np.enc_algo_client_to_server);
+ log.log(20, "enc_algo_server_to_client=" + np.enc_algo_server_to_client);
+
+ np.mac_algo_client_to_server = getFirstMatch(client.mac_algorithms_client_to_server,
+ server.mac_algorithms_client_to_server);
+ np.mac_algo_server_to_client = getFirstMatch(client.mac_algorithms_server_to_client,
+ server.mac_algorithms_server_to_client);
+
+ log.log(20, "mac_algo_client_to_server=" + np.mac_algo_client_to_server);
+ log.log(20, "mac_algo_server_to_client=" + np.mac_algo_server_to_client);
+
+ np.comp_algo_client_to_server = getFirstMatch(client.compression_algorithms_client_to_server,
+ server.compression_algorithms_client_to_server);
+ np.comp_algo_server_to_client = getFirstMatch(client.compression_algorithms_server_to_client,
+ server.compression_algorithms_server_to_client);
+
+ log.log(20, "comp_algo_client_to_server=" + np.comp_algo_client_to_server);
+ log.log(20, "comp_algo_server_to_client=" + np.comp_algo_server_to_client);
+
+ }
+ catch (NegotiateException e)
+ {
+ return null;
+ }
+
+ try
+ {
+ np.lang_client_to_server = getFirstMatch(client.languages_client_to_server,
+ server.languages_client_to_server);
+ }
+ catch (NegotiateException e1)
+ {
+ np.lang_client_to_server = null;
+ }
+
+ try
+ {
+ np.lang_server_to_client = getFirstMatch(client.languages_server_to_client,
+ server.languages_server_to_client);
+ }
+ catch (NegotiateException e2)
+ {
+ np.lang_server_to_client = null;
+ }
+
+ if (isGuessOK(client, server))
+ np.guessOK = true;
+
+ return np;
+ }
+
+ public synchronized void initiateKEX(CryptoWishList cwl, DHGexParameters dhgex) throws IOException
+ {
+ nextKEXcryptoWishList = cwl;
+ nextKEXdhgexParameters = dhgex;
+
+ if (kxs == null)
+ {
+ kxs = new KexState();
+
+ kxs.dhgexParameters = nextKEXdhgexParameters;
+ PacketKexInit kp = new PacketKexInit(nextKEXcryptoWishList);
+ kxs.localKEX = kp;
+ tm.sendKexMessage(kp.getPayload());
+ }
+ }
+
+ private boolean establishKeyMaterial()
+ {
+ try
+ {
+ int mac_cs_key_len = MAC.getKeyLen(kxs.np.mac_algo_client_to_server);
+ int enc_cs_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_client_to_server);
+ int enc_cs_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_client_to_server);
+
+ int mac_sc_key_len = MAC.getKeyLen(kxs.np.mac_algo_server_to_client);
+ int enc_sc_key_len = BlockCipherFactory.getKeySize(kxs.np.enc_algo_server_to_client);
+ int enc_sc_block_len = BlockCipherFactory.getBlockSize(kxs.np.enc_algo_server_to_client);
+
+ km = KeyMaterial.create(kxs.hashAlgo, kxs.H, kxs.K, sessionId, enc_cs_key_len, enc_cs_block_len, mac_cs_key_len,
+ enc_sc_key_len, enc_sc_block_len, mac_sc_key_len);
+ }
+ catch (IllegalArgumentException e)
+ {
+ return false;
+ }
+ return true;
+ }
+
+ private void finishKex() throws IOException
+ {
+ if (sessionId == null)
+ sessionId = kxs.H;
+
+ establishKeyMaterial();
+
+ /* Tell the other side that we start using the new material */
+
+ PacketNewKeys ign = new PacketNewKeys();
+ tm.sendKexMessage(ign.getPayload());
+
+ BlockCipher cbc;
+ MAC mac;
+ ICompressor comp;
+
+ try
+ {
+ cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_client_to_server, true, km.enc_key_client_to_server,
+ km.initial_iv_client_to_server);
+
+ mac = new MAC(kxs.np.mac_algo_client_to_server, km.integrity_key_client_to_server);
+
+ comp = CompressionFactory.createCompressor(kxs.np.comp_algo_client_to_server);
+
+ }
+ catch (IllegalArgumentException e1)
+ {
+ throw new IOException("Fatal error during MAC startup!");
+ }
+
+ tm.changeSendCipher(cbc, mac);
+ tm.changeSendCompression(comp);
+ tm.kexFinished();
+ }
+
+ public static final String[] getDefaultServerHostkeyAlgorithmList()
+ {
+ return HOSTKEY_ALGS.toArray(new String[HOSTKEY_ALGS.size()]);
+ }
+
+ public static final void checkServerHostkeyAlgorithmsList(String[] algos)
+ {
+ for (int i = 0; i < algos.length; i++)
+ {
+ if (!HOSTKEY_ALGS.contains(algos[i]))
+ throw new IllegalArgumentException("Unknown server host key algorithm '" + algos[i] + "'");
+ }
+ }
+
+ public static final String[] getDefaultKexAlgorithmList()
+ {
+ return KEX_ALGS.toArray(new String[KEX_ALGS.size()]);
+ }
+
+ public static final void checkKexAlgorithmList(String[] algos)
+ {
+ for (int i = 0; i < algos.length; i++)
+ {
+ if (!KEX_ALGS.contains(algos[i]))
+ throw new IllegalArgumentException("Unknown kex algorithm '" + algos[i] + "'");
+ }
+ }
+
+ private boolean verifySignature(byte[] sig, byte[] hostkey) throws IOException
+ {
+ if (kxs.np.server_host_key_algo.startsWith("ecdsa-sha2-"))
+ {
+ byte[] rs = ECDSASHA2Verify.decodeSSHECDSASignature(sig);
+ ECPublicKey epk = ECDSASHA2Verify.decodeSSHECDSAPublicKey(hostkey);
+
+ log.log(50, "Verifying ecdsa signature");
+
+ return ECDSASHA2Verify.verifySignature(kxs.H, rs, epk);
+ }
+
+ if (kxs.np.server_host_key_algo.equals("ssh-rsa"))
+ {
+ byte[] rs = RSASHA1Verify.decodeSSHRSASignature(sig);
+ RSAPublicKey rpk = RSASHA1Verify.decodeSSHRSAPublicKey(hostkey);
+
+ log.log(50, "Verifying ssh-rsa signature");
+
+ return RSASHA1Verify.verifySignature(kxs.H, rs, rpk);
+ }
+
+ if (kxs.np.server_host_key_algo.equals("ssh-dss"))
+ {
+ byte[] ds = DSASHA1Verify.decodeSSHDSASignature(sig);
+ DSAPublicKey dpk = DSASHA1Verify.decodeSSHDSAPublicKey(hostkey);
+
+ log.log(50, "Verifying ssh-dss signature");
+
+ return DSASHA1Verify.verifySignature(kxs.H, ds, dpk);
+ }
+
+ throw new IOException("Unknown server host key algorithm '" + kxs.np.server_host_key_algo + "'");
+ }
+
+ public synchronized void handleMessage(byte[] msg, int msglen) throws IOException
+ {
+ PacketKexInit kip;
+
+ if (msg == null)
+ {
+ synchronized (accessLock)
+ {
+ connectionClosed = true;
+ accessLock.notifyAll();
+ return;
+ }
+ }
+
+ if ((kxs == null) && (msg[0] != Packets.SSH_MSG_KEXINIT))
+ throw new IOException("Unexpected KEX message (type " + msg[0] + ")");
+
+ if (ignore_next_kex_packet)
+ {
+ ignore_next_kex_packet = false;
+ return;
+ }
+
+ if (msg[0] == Packets.SSH_MSG_KEXINIT)
+ {
+ if ((kxs != null) && (kxs.state != 0))
+ throw new IOException("Unexpected SSH_MSG_KEXINIT message during on-going kex exchange!");
+
+ if (kxs == null)
+ {
+ /*
+ * Ah, OK, peer wants to do KEX. Let's be nice and play
+ * together.
+ */
+ kxs = new KexState();
+ kxs.dhgexParameters = nextKEXdhgexParameters;
+ kip = new PacketKexInit(nextKEXcryptoWishList);
+ kxs.localKEX = kip;
+ tm.sendKexMessage(kip.getPayload());
+ }
+
+ kip = new PacketKexInit(msg, 0, msglen);
+ kxs.remoteKEX = kip;
+
+ kxs.np = mergeKexParameters(kxs.localKEX.getKexParameters(), kxs.remoteKEX.getKexParameters());
+
+ if (kxs.np == null)
+ throw new IOException("Cannot negotiate, proposals do not match.");
+
+ if (kxs.remoteKEX.isFirst_kex_packet_follows() && (kxs.np.guessOK == false))
+ {
+ /*
+ * Guess was wrong, we need to ignore the next kex packet.
+ */
+
+ ignore_next_kex_packet = true;
+ }
+
+ if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1")
+ || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256"))
+ {
+ if (kxs.dhgexParameters.getMin_group_len() == 0 || csh.server_versioncomment.matches("OpenSSH_2\\.([0-4]\\.|5\\.[0-2]).*"))
+ {
+ PacketKexDhGexRequestOld dhgexreq = new PacketKexDhGexRequestOld(kxs.dhgexParameters);
+ tm.sendKexMessage(dhgexreq.getPayload());
+ }
+ else
+ {
+ PacketKexDhGexRequest dhgexreq = new PacketKexDhGexRequest(kxs.dhgexParameters);
+ tm.sendKexMessage(dhgexreq.getPayload());
+ }
+ if (kxs.np.kex_algo.endsWith("sha1")) {
+ kxs.hashAlgo = "SHA1";
+ } else {
+ kxs.hashAlgo = "SHA-256";
+ }
+ kxs.state = 1;
+ return;
+ }
+
+ if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
+ || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp256")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp384")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp521")) {
+ kxs.dhx = GenericDhExchange.getInstance(kxs.np.kex_algo);
+
+ kxs.dhx.init(kxs.np.kex_algo);
+ kxs.hashAlgo = kxs.dhx.getHashAlgo();
+
+ PacketKexDHInit kp = new PacketKexDHInit(kxs.dhx.getE());
+ tm.sendKexMessage(kp.getPayload());
+ kxs.state = 1;
+ return;
+ }
+
+ throw new IllegalStateException("Unknown KEX method!");
+ }
+
+ if (msg[0] == Packets.SSH_MSG_NEWKEYS)
+ {
+ if (km == null)
+ throw new IOException("Peer sent SSH_MSG_NEWKEYS, but I have no key material ready!");
+
+ BlockCipher cbc;
+ MAC mac;
+ ICompressor comp;
+
+ try
+ {
+ cbc = BlockCipherFactory.createCipher(kxs.np.enc_algo_server_to_client, false,
+ km.enc_key_server_to_client, km.initial_iv_server_to_client);
+
+ mac = new MAC(kxs.np.mac_algo_server_to_client, km.integrity_key_server_to_client);
+
+ comp = CompressionFactory.createCompressor(kxs.np.comp_algo_server_to_client);
+ }
+ catch (IllegalArgumentException e1)
+ {
+ throw new IOException("Fatal error during MAC startup!");
+ }
+
+ tm.changeRecvCipher(cbc, mac);
+ tm.changeRecvCompression(comp);
+
+ ConnectionInfo sci = new ConnectionInfo();
+
+ kexCount++;
+
+ sci.keyExchangeAlgorithm = kxs.np.kex_algo;
+ sci.keyExchangeCounter = kexCount;
+ sci.clientToServerCryptoAlgorithm = kxs.np.enc_algo_client_to_server;
+ sci.serverToClientCryptoAlgorithm = kxs.np.enc_algo_server_to_client;
+ sci.clientToServerMACAlgorithm = kxs.np.mac_algo_client_to_server;
+ sci.serverToClientMACAlgorithm = kxs.np.mac_algo_server_to_client;
+ sci.serverHostKeyAlgorithm = kxs.np.server_host_key_algo;
+ sci.serverHostKey = kxs.hostkey;
+
+ synchronized (accessLock)
+ {
+ lastConnInfo = sci;
+ accessLock.notifyAll();
+ }
+
+ kxs = null;
+ return;
+ }
+
+ if ((kxs == null) || (kxs.state == 0))
+ throw new IOException("Unexpected Kex submessage!");
+
+ if (kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha1")
+ || kxs.np.kex_algo.equals("diffie-hellman-group-exchange-sha256"))
+ {
+ if (kxs.state == 1)
+ {
+ PacketKexDhGexGroup dhgexgrp = new PacketKexDhGexGroup(msg, 0, msglen);
+ kxs.dhgx = new DhGroupExchange(dhgexgrp.getP(), dhgexgrp.getG());
+ kxs.dhgx.init(rnd);
+ PacketKexDhGexInit dhgexinit = new PacketKexDhGexInit(kxs.dhgx.getE());
+ tm.sendKexMessage(dhgexinit.getPayload());
+ kxs.state = 2;
+ return;
+ }
+
+ if (kxs.state == 2)
+ {
+ PacketKexDhGexReply dhgexrpl = new PacketKexDhGexReply(msg, 0, msglen);
+
+ kxs.hostkey = dhgexrpl.getHostKey();
+
+ if (verifier != null)
+ {
+ boolean vres = false;
+
+ try
+ {
+ vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
+ }
+ catch (Exception e)
+ {
+ throw (IOException) new IOException(
+ "The server hostkey was not accepted by the verifier callback.").initCause(e);
+ }
+
+ if (vres == false)
+ throw new IOException("The server hostkey was not accepted by the verifier callback");
+ }
+
+ kxs.dhgx.setF(dhgexrpl.getF());
+
+ try
+ {
+ kxs.H = kxs.dhgx.calculateH(kxs.hashAlgo,
+ csh.getClientString(), csh.getServerString(),
+ kxs.localKEX.getPayload(), kxs.remoteKEX.getPayload(),
+ dhgexrpl.getHostKey(), kxs.dhgexParameters);
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw (IOException) new IOException("KEX error.").initCause(e);
+ }
+
+ boolean res = verifySignature(dhgexrpl.getSignature(), kxs.hostkey);
+
+ if (res == false)
+ throw new IOException("Hostkey signature sent by remote is wrong!");
+
+ kxs.K = kxs.dhgx.getK();
+
+ finishKex();
+ kxs.state = -1;
+ return;
+ }
+
+ throw new IllegalStateException("Illegal State in KEX Exchange!");
+ }
+
+ if (kxs.np.kex_algo.equals("diffie-hellman-group1-sha1")
+ || kxs.np.kex_algo.equals("diffie-hellman-group14-sha1")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp256")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp384")
+ || kxs.np.kex_algo.equals("ecdh-sha2-nistp521"))
+ {
+ if (kxs.state == 1)
+ {
+
+ PacketKexDHReply dhr = new PacketKexDHReply(msg, 0, msglen);
+
+ kxs.hostkey = dhr.getHostKey();
+
+ if (verifier != null)
+ {
+ boolean vres = false;
+
+ try
+ {
+ vres = verifier.verifyServerHostKey(hostname, port, kxs.np.server_host_key_algo, kxs.hostkey);
+ }
+ catch (Exception e)
+ {
+ throw (IOException) new IOException(
+ "The server hostkey was not accepted by the verifier callback.").initCause(e);
+ }
+
+ if (vres == false)
+ throw new IOException("The server hostkey was not accepted by the verifier callback");
+ }
+
+ kxs.dhx.setF(dhr.getF());
+
+ try
+ {
+ kxs.H = kxs.dhx.calculateH(csh.getClientString(), csh.getServerString(), kxs.localKEX.getPayload(),
+ kxs.remoteKEX.getPayload(), dhr.getHostKey());
+ }
+ catch (IllegalArgumentException e)
+ {
+ throw (IOException) new IOException("KEX error.").initCause(e);
+ }
+
+ boolean res = verifySignature(dhr.getSignature(), kxs.hostkey);
+
+ if (res == false)
+ throw new IOException("Hostkey signature sent by remote is wrong!");
+
+ kxs.K = kxs.dhx.getK();
+
+ finishKex();
+ kxs.state = -1;
+ return;
+ }
+ }
+
+ throw new IllegalStateException("Unkown KEX method! (" + kxs.np.kex_algo + ")");
+ }
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java
new file mode 100644
index 0000000..70bcf3e
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexParameters.java
@@ -0,0 +1,24 @@
+package com.trilead.ssh2.transport;
+
+/**
+ * KexParameters.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: KexParameters.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+public class KexParameters
+{
+ public byte[] cookie;
+ public String[] kex_algorithms;
+ public String[] server_host_key_algorithms;
+ public String[] encryption_algorithms_client_to_server;
+ public String[] encryption_algorithms_server_to_client;
+ public String[] mac_algorithms_client_to_server;
+ public String[] mac_algorithms_server_to_client;
+ public String[] compression_algorithms_client_to_server;
+ public String[] compression_algorithms_server_to_client;
+ public String[] languages_client_to_server;
+ public String[] languages_server_to_client;
+ public boolean first_kex_packet_follows;
+ public int reserved_field1;
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java b/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java
new file mode 100644
index 0000000..8611f3f
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/KexState.java
@@ -0,0 +1,33 @@
+package com.trilead.ssh2.transport;
+
+
+import java.math.BigInteger;
+
+import com.trilead.ssh2.DHGexParameters;
+import com.trilead.ssh2.crypto.dh.DhGroupExchange;
+import com.trilead.ssh2.crypto.dh.GenericDhExchange;
+import com.trilead.ssh2.packets.PacketKexInit;
+
+/**
+ * KexState.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: KexState.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $
+ */
+public class KexState
+{
+ public PacketKexInit localKEX;
+ public PacketKexInit remoteKEX;
+ public NegotiatedParameters np;
+ public int state = 0;
+
+ public BigInteger K;
+ public byte[] H;
+
+ public byte[] hostkey;
+
+ public String hashAlgo;
+ public GenericDhExchange dhx;
+ public DhGroupExchange dhgx;
+ public DHGexParameters dhgexParameters;
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java b/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java
new file mode 100644
index 0000000..039d473
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/MessageHandler.java
@@ -0,0 +1,14 @@
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+
+/**
+ * MessageHandler.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: MessageHandler.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+public interface MessageHandler
+{
+ public void handleMessage(byte[] msg, int msglen) throws IOException;
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java
new file mode 100644
index 0000000..ff53097
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiateException.java
@@ -0,0 +1,12 @@
+package com.trilead.ssh2.transport;
+
+/**
+ * NegotiateException.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: NegotiateException.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+public class NegotiateException extends Exception
+{
+ private static final long serialVersionUID = 3689910669428143157L;
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java
new file mode 100644
index 0000000..e9f3a0a
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/NegotiatedParameters.java
@@ -0,0 +1,22 @@
+package com.trilead.ssh2.transport;
+
+/**
+ * NegotiatedParameters.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: NegotiatedParameters.java,v 1.1 2007/10/15 12:49:57 cplattne Exp $
+ */
+public class NegotiatedParameters
+{
+ public boolean guessOK;
+ public String kex_algo;
+ public String server_host_key_algo;
+ public String enc_algo_client_to_server;
+ public String enc_algo_server_to_client;
+ public String mac_algo_client_to_server;
+ public String mac_algo_server_to_client;
+ public String comp_algo_client_to_server;
+ public String comp_algo_server_to_client;
+ public String lang_client_to_server;
+ public String lang_server_to_client;
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java
new file mode 100644
index 0000000..906c3c9
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportConnection.java
@@ -0,0 +1,343 @@
+
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.security.SecureRandom;
+
+import com.trilead.ssh2.compression.ICompressor;
+import com.trilead.ssh2.crypto.cipher.BlockCipher;
+import com.trilead.ssh2.crypto.cipher.CipherInputStream;
+import com.trilead.ssh2.crypto.cipher.CipherOutputStream;
+import com.trilead.ssh2.crypto.cipher.NullCipher;
+import com.trilead.ssh2.crypto.digest.MAC;
+import com.trilead.ssh2.log.Logger;
+import com.trilead.ssh2.packets.Packets;
+
+
+/**
+ * TransportConnection.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: TransportConnection.java,v 1.1 2007/10/15 12:49:56 cplattne Exp $
+ */
+public class TransportConnection
+{
+ private static final Logger log = Logger.getLogger(TransportConnection.class);
+
+ int send_seq_number = 0;
+
+ int recv_seq_number = 0;
+
+ CipherInputStream cis;
+
+ CipherOutputStream cos;
+
+ boolean useRandomPadding = false;
+
+ /* Depends on current MAC and CIPHER */
+
+ MAC send_mac;
+
+ byte[] send_mac_buffer;
+
+ int send_padd_blocksize = 8;
+
+ MAC recv_mac;
+
+ byte[] recv_mac_buffer;
+
+ byte[] recv_mac_buffer_cmp;
+
+ int recv_padd_blocksize = 8;
+
+ ICompressor recv_comp = null;
+
+ ICompressor send_comp = null;
+
+ boolean can_recv_compress = false;
+
+ boolean can_send_compress = false;
+
+ byte[] recv_comp_buffer;
+
+ byte[] send_comp_buffer;
+
+ /* won't change */
+
+ final byte[] send_padding_buffer = new byte[256];
+
+ final byte[] send_packet_header_buffer = new byte[5];
+
+ final byte[] recv_padding_buffer = new byte[256];
+
+ final byte[] recv_packet_header_buffer = new byte[5];
+
+ boolean recv_packet_header_present = false;
+
+ ClientServerHello csh;
+
+ final SecureRandom rnd;
+
+ public TransportConnection(InputStream is, OutputStream os, SecureRandom rnd)
+ {
+ this.cis = new CipherInputStream(new NullCipher(), is);
+ this.cos = new CipherOutputStream(new NullCipher(), os);
+ this.rnd = rnd;
+ }
+
+ public void changeRecvCipher(BlockCipher bc, MAC mac)
+ {
+ cis.changeCipher(bc);
+ recv_mac = mac;
+ recv_mac_buffer = (mac != null) ? new byte[mac.size()] : null;
+ recv_mac_buffer_cmp = (mac != null) ? new byte[mac.size()] : null;
+ recv_padd_blocksize = bc.getBlockSize();
+ if (recv_padd_blocksize < 8)
+ recv_padd_blocksize = 8;
+ }
+
+ public void changeSendCipher(BlockCipher bc, MAC mac)
+ {
+ if ((bc instanceof NullCipher) == false)
+ {
+ /* Only use zero byte padding for the first few packets */
+ useRandomPadding = true;
+ /* Once we start encrypting, there is no way back */
+ }
+
+ cos.changeCipher(bc);
+ send_mac = mac;
+ send_mac_buffer = (mac != null) ? new byte[mac.size()] : null;
+ send_padd_blocksize = bc.getBlockSize();
+ if (send_padd_blocksize < 8)
+ send_padd_blocksize = 8;
+ }
+
+ public void changeRecvCompression(ICompressor comp)
+ {
+ recv_comp = comp;
+
+ if (comp != null) {
+ recv_comp_buffer = new byte[comp.getBufferSize()];
+ can_recv_compress |= recv_comp.canCompressPreauth();
+ }
+ }
+
+ public void changeSendCompression(ICompressor comp)
+ {
+ send_comp = comp;
+
+ if (comp != null) {
+ send_comp_buffer = new byte[comp.getBufferSize()];
+ can_send_compress |= send_comp.canCompressPreauth();
+ }
+ }
+
+ public void sendMessage(byte[] message) throws IOException
+ {
+ sendMessage(message, 0, message.length, 0);
+ }
+
+ public void sendMessage(byte[] message, int off, int len) throws IOException
+ {
+ sendMessage(message, off, len, 0);
+ }
+
+ public int getPacketOverheadEstimate()
+ {
+ // return an estimate for the paket overhead (for send operations)
+ return 5 + 4 + (send_padd_blocksize - 1) + send_mac_buffer.length;
+ }
+
+ public void sendMessage(byte[] message, int off, int len, int padd) throws IOException
+ {
+ if (padd < 4)
+ padd = 4;
+ else if (padd > 64)
+ padd = 64;
+
+ if (send_comp != null && can_send_compress) {
+ if (send_comp_buffer.length < message.length + 1024)
+ send_comp_buffer = new byte[message.length + 1024];
+ len = send_comp.compress(message, off, len, send_comp_buffer);
+ message = send_comp_buffer;
+ }
+
+ int packet_len = 5 + len + padd; /* Minimum allowed padding is 4 */
+
+ int slack = packet_len % send_padd_blocksize;
+
+ if (slack != 0)
+ {
+ packet_len += (send_padd_blocksize - slack);
+ }
+
+ if (packet_len < 16)
+ packet_len = 16;
+
+ int padd_len = packet_len - (5 + len);
+
+ if (useRandomPadding)
+ {
+ for (int i = 0; i < padd_len; i = i + 4)
+ {
+ /*
+ * don't waste calls to rnd.nextInt() (by using only 8bit of the
+ * output). just believe me: even though we may write here up to 3
+ * bytes which won't be used, there is no "buffer overflow" (i.e.,
+ * arrayindexoutofbounds). the padding buffer is big enough =) (256
+ * bytes, and that is bigger than any current cipher block size + 64).
+ */
+
+ int r = rnd.nextInt();
+ send_padding_buffer[i] = (byte) r;
+ send_padding_buffer[i + 1] = (byte) (r >> 8);
+ send_padding_buffer[i + 2] = (byte) (r >> 16);
+ send_padding_buffer[i + 3] = (byte) (r >> 24);
+ }
+ }
+ else
+ {
+ /* use zero padding for unencrypted traffic */
+ for (int i = 0; i < padd_len; i++)
+ send_padding_buffer[i] = 0;
+ /* Actually this code is paranoid: we never filled any
+ * bytes into the padding buffer so far, therefore it should
+ * consist of zeros only.
+ */
+ }
+
+ send_packet_header_buffer[0] = (byte) ((packet_len - 4) >> 24);
+ send_packet_header_buffer[1] = (byte) ((packet_len - 4) >> 16);
+ send_packet_header_buffer[2] = (byte) ((packet_len - 4) >> 8);
+ send_packet_header_buffer[3] = (byte) ((packet_len - 4));
+ send_packet_header_buffer[4] = (byte) padd_len;
+
+ cos.write(send_packet_header_buffer, 0, 5);
+ cos.write(message, off, len);
+ cos.write(send_padding_buffer, 0, padd_len);
+
+ if (send_mac != null)
+ {
+ send_mac.initMac(send_seq_number);
+ send_mac.update(send_packet_header_buffer, 0, 5);
+ send_mac.update(message, off, len);
+ send_mac.update(send_padding_buffer, 0, padd_len);
+
+ send_mac.getMac(send_mac_buffer, 0);
+ cos.writePlain(send_mac_buffer, 0, send_mac_buffer.length);
+ }
+
+ cos.flush();
+
+ if (log.isEnabled())
+ {
+ log.log(90, "Sent " + Packets.getMessageName(message[off] & 0xff) + " " + len + " bytes payload");
+ }
+
+ send_seq_number++;
+ }
+
+ public int peekNextMessageLength() throws IOException
+ {
+ if (recv_packet_header_present == false)
+ {
+ cis.read(recv_packet_header_buffer, 0, 5);
+ recv_packet_header_present = true;
+ }
+
+ int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24)
+ | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8)
+ | ((recv_packet_header_buffer[3] & 0xff));
+
+ int padding_length = recv_packet_header_buffer[4] & 0xff;
+
+ if (packet_length > 35000 || packet_length < 12)
+ throw new IOException("Illegal packet size! (" + packet_length + ")");
+
+ int payload_length = packet_length - padding_length - 1;
+
+ if (payload_length < 0)
+ throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")");
+
+ return payload_length;
+ }
+
+ public int receiveMessage(byte buffer[], int off, int len) throws IOException
+ {
+ if (recv_packet_header_present == false)
+ {
+ cis.read(recv_packet_header_buffer, 0, 5);
+ }
+ else
+ recv_packet_header_present = false;
+
+ int packet_length = ((recv_packet_header_buffer[0] & 0xff) << 24)
+ | ((recv_packet_header_buffer[1] & 0xff) << 16) | ((recv_packet_header_buffer[2] & 0xff) << 8)
+ | ((recv_packet_header_buffer[3] & 0xff));
+
+ int padding_length = recv_packet_header_buffer[4] & 0xff;
+
+ if (packet_length > 35000 || packet_length < 12)
+ throw new IOException("Illegal packet size! (" + packet_length + ")");
+
+ int payload_length = packet_length - padding_length - 1;
+
+ if (payload_length < 0)
+ throw new IOException("Illegal padding_length in packet from remote (" + padding_length + ")");
+
+ if (payload_length >= len)
+ throw new IOException("Receive buffer too small (" + len + ", need " + payload_length + ")");
+
+ cis.read(buffer, off, payload_length);
+ cis.read(recv_padding_buffer, 0, padding_length);
+
+ if (recv_mac != null)
+ {
+ cis.readPlain(recv_mac_buffer, 0, recv_mac_buffer.length);
+
+ recv_mac.initMac(recv_seq_number);
+ recv_mac.update(recv_packet_header_buffer, 0, 5);
+ recv_mac.update(buffer, off, payload_length);
+ recv_mac.update(recv_padding_buffer, 0, padding_length);
+ recv_mac.getMac(recv_mac_buffer_cmp, 0);
+
+ for (int i = 0; i < recv_mac_buffer.length; i++)
+ {
+ if (recv_mac_buffer[i] != recv_mac_buffer_cmp[i])
+ throw new IOException("Remote sent corrupt MAC.");
+ }
+ }
+
+ recv_seq_number++;
+
+ if (log.isEnabled())
+ {
+ log.log(90, "Received " + Packets.getMessageName(buffer[off] & 0xff) + " " + payload_length
+ + " bytes payload");
+ }
+
+ if (recv_comp != null && can_recv_compress) {
+ int[] uncomp_len = new int[] { payload_length };
+ buffer = recv_comp.uncompress(buffer, off, uncomp_len);
+
+ if (buffer == null) {
+ throw new IOException("Error while inflating remote data");
+ } else {
+ return uncomp_len[0];
+ }
+ } else {
+ return payload_length;
+ }
+ }
+
+ /**
+ *
+ */
+ public void startCompression() {
+ can_recv_compress = true;
+ can_send_compress = true;
+ }
+}
diff --git a/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java
new file mode 100644
index 0000000..8f3406e
--- /dev/null
+++ b/sshlib/src/main/java/com/trilead/ssh2/transport/TransportManager.java
@@ -0,0 +1,750 @@
+
+package com.trilead.ssh2.transport;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Vector;
+
+import com.trilead.ssh2.ConnectionInfo;
+import com.trilead.ssh2.ConnectionMonitor;
+import com.trilead.ssh2.DHGexParameters;
+import com.trilead.ssh2.HTTPProxyData;
+import com.trilead.ssh2.HTTPProxyException;
+import com.trilead.ssh2.ProxyData;
+import com.trilead.ssh2.ServerHostKeyVerifier;
+import com.trilead.ssh2.compression.ICompressor;
+import com.trilead.ssh2.crypto.Base64;
+import com.trilead.ssh2.crypto.CryptoWishList;
+import com.trilead.ssh2.crypto.cipher.BlockCipher;
+import com.trilead.ssh2.crypto.digest.MAC;
+import com.trilead.ssh2.log.Logger;
+import com.trilead.ssh2.packets.PacketDisconnect;
+import com.trilead.ssh2.packets.Packets;
+import com.trilead.ssh2.packets.TypesReader;
+import com.trilead.ssh2.util.Tokenizer;
+
+
+/*
+ * Yes, the "standard" is a big mess. On one side, the say that arbitary channel
+ * packets are allowed during kex exchange, on the other side we need to blindly
+ * ignore the next _packet_ if the KEX guess was wrong. Where do we know from that
+ * the next packet is not a channel data packet? Yes, we could check if it is in
+ * the KEX range. But the standard says nothing about this. The OpenSSH guys
+ * block local "normal" traffic during KEX. That's fine - however, they assume
+ * that the other side is doing the same. During re-key, if they receive traffic
+ * other than KEX, they become horribly irritated and kill the connection. Since
+ * we are very likely going to communicate with OpenSSH servers, we have to play
+ * the same game - even though we could do better.
+ *
+ * btw: having stdout and stderr on the same channel, with a shared window, is
+ * also a VERY good idea... =(
+ */
+
+/**
+ * TransportManager.
+ *
+ * @author Christian Plattner, plattner@trilead.com
+ * @version $Id: TransportManager.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $
+ */
+public class TransportManager
+{
+ private static final Logger log = Logger.getLogger(TransportManager.class);
+
+ class HandlerEntry
+ {
+ MessageHandler mh;
+ int low;
+ int high;
+ }
+
+ private final Vector<byte[]> asynchronousQueue = new Vector<byte[]>();
+ private Thread asynchronousThread = null;
+
+ class AsynchronousWorker extends Thread
+ {
+ public void run()
+ {
+ while (true)
+ {
+ byte[] msg = null;
+
+ synchronized (asynchronousQueue)
+ {
+ if (asynchronousQueue.size() == 0)
+ {
+ /* After the queue is empty for about 2 seconds, stop this thread */
+
+ try
+ {
+ asynchronousQueue.wait(2000);
+ }
+ catch (InterruptedException e)
+ {
+ /* OKOK, if somebody interrupts us, then we may die earlier. */
+ }
+
+ if (asynchronousQueue.size() == 0)
+ {
+ asynchronousThread = null;
+ return;
+ }
+ }
+
+ msg = asynchronousQueue.remove(0);
+ }
+
+ /* The following invocation may throw an IOException.
+ * There is no point in handling it - it simply means
+ * that the connection has a problem and we should stop
+ * sending asynchronously messages. We do not need to signal that
+ * we have exited (asynchronousThread = null): further
+ * messages in the queue cannot be sent by this or any
+ * other thread.
+ * Other threads will sooner or later (when receiving or
+ * sending the next message) get the same IOException and
+ * get to the same conclusion.
+ */
+
+ try
+ {
+ sendMessage(msg);
+ }
+ catch (IOException e)
+ {
+ return;
+ }
+ }
+ }
+ }
+
+ String hostname;
+ int port;
+ final Socket sock = new Socket();
+
+ Object connectionSemaphore = new Object();
+
+ boolean flagKexOngoing = false;
+ boolean connectionClosed = false;
+
+ Throwable reasonClosedCause = null;
+
+ TransportConnection tc;
+ KexManager km;
+
+ Vector<HandlerEntry> messageHandlers = new Vector<HandlerEntry>();
+
+ Thread receiveThread;
+
+ Vector connectionMonitors = new Vector();
+ boolean monitorsWereInformed = false;
+
+ public TransportManager(String host, int port) throws IOException
+ {
+ this.hostname = host;
+ this.port = port;
+ }
+
+ public int getPacketOverheadEstimate()
+ {
+ return tc.getPacketOverheadEstimate();
+ }
+
+ public void setTcpNoDelay(boolean state) throws IOException
+ {
+ sock.setTcpNoDelay(state);
+ }
+
+ public void setSoTimeout(int timeout) throws IOException
+ {
+ sock.setSoTimeout(timeout);
+ }
+
+ public ConnectionInfo getConnectionInfo(int kexNumber) throws IOException
+ {
+ return km.getOrWaitForConnectionInfo(kexNumber);
+ }
+
+ public Throwable getReasonClosedCause()
+ {
+ synchronized (connectionSemaphore)
+ {
+ return reasonClosedCause;
+ }
+ }
+
+ public byte[] getSessionIdentifier()
+ {
+ return km.sessionId;
+ }
+
+ public void close(Throwable cause, boolean useDisconnectPacket)
+ {
+ if (useDisconnectPacket == false)
+ {
+ /* OK, hard shutdown - do not aquire the semaphore,
+ * perhaps somebody is inside (and waits until the remote
+ * side is ready to accept new data). */
+
+ try
+ {
+ sock.close();
+ }
+ catch (IOException ignore)
+ {
+ }
+
+ /* OK, whoever tried to send data, should now agree that
+ * there is no point in further waiting =)
+ * It is safe now to aquire the semaphore.
+ */
+ }
+
+ synchronized (connectionSemaphore)
+ {
+ if (connectionClosed == false)
+ {
+ if (useDisconnectPacket == true)
+ {
+ try
+ {
+ byte[] msg = new PacketDisconnect(Packets.SSH_DISCONNECT_BY_APPLICATION, cause.getMessage(), "")
+ .getPayload();
+ if (tc != null)
+ tc.sendMessage(msg);
+ }
+ catch (IOException ignore)
+ {
+ }
+
+ try
+ {
+ sock.close();
+ }
+ catch (IOException ignore)
+ {
+ }
+ }
+
+ connectionClosed = true;
+ reasonClosedCause = cause; /* may be null */
+ }
+ connectionSemaphore.notifyAll();
+ }
+
+ /* No check if we need to inform the monitors */
+
+ Vector monitors = null;
+
+ synchronized (this)
+ {
+ /* Short term lock to protect "connectionMonitors"
+ * and "monitorsWereInformed"
+ * (they may be modified concurrently)
+ */
+
+ if (monitorsWereInformed == false)
+ {
+ monitorsWereInformed = true;
+ monitors = (Vector) connectionMonitors.clone();
+ }
+ }
+
+ if (monitors != null)
+ {
+ for (int i = 0; i < monitors.size(); i++)
+ {
+ try
+ {
+ ConnectionMonitor cmon = (ConnectionMonitor) monitors.elementAt(i);
+ cmon.connectionLost(reasonClosedCause);
+ }
+ catch (Exception ignore)
+ {
+ }
+ }
+ }
+ }
+
+ private static void tryAllAddresses(Socket sock, String host, int port, int connectTimeout) throws IOException {
+ InetAddress[] addresses = InetAddress.getAllByName(host);
+ for (InetAddress addr : addresses) {
+ try {
+ sock.connect(new InetSocketAddress(addr, port), connectTimeout);
+ return;
+ } catch (SocketTimeoutException e) {
+ }
+ }
+ throw new SocketTimeoutException("Could not connect; socket timed out");
+ }
+
+ private void establishConnection(ProxyData proxyData, int connectTimeout) throws IOException
+ {
+ if (proxyData == null)
+ {
+ tryAllAddresses(sock, hostname, port, connectTimeout);
+ sock.setSoTimeout(0);
+ return;
+ }
+
+ if (proxyData instanceof HTTPProxyData)
+ {
+ HTTPProxyData pd = (HTTPProxyData) proxyData;
+
+ /* At the moment, we only support HTTP proxies */
+
+ tryAllAddresses(sock, pd.proxyHost, pd.proxyPort, connectTimeout);
+ sock.setSoTimeout(0);
+
+ /* OK, now tell the proxy where we actually want to connect to */
+
+ StringBuffer sb = new StringBuffer();
+
+ sb.append("CONNECT ");
+ sb.append(hostname);
+ sb.append(':');
+ sb.append(port);
+ sb.append(" HTTP/1.0\r\n");
+
+ if ((pd.proxyUser != null) && (pd.proxyPass != null))
+ {
+ String credentials = pd.proxyUser + ":" + pd.proxyPass;
+ char[] encoded = Base64.encode(credentials.getBytes("ISO-8859-1"));
+ sb.append("Proxy-Authorization: Basic ");
+ sb.append(encoded);
+ sb.append("\r\n");
+ }
+
+ if (pd.requestHeaderLines != null)
+ {
+ for (int i = 0; i < pd.requestHeaderLines.length; i++)
+ {
+ if (pd.requestHeaderLines[i] != null)
+ {
+ sb.append(pd.requestHeaderLines[i]);
+ sb.append("\r\n");
+ }
+ }
+ }
+
+ sb.append("\r\n");
+
+ OutputStream out = sock.getOutputStream();
+
+ out.write(sb.toString().getBytes("ISO-8859-1"));
+ out.flush();
+
+ /* Now parse the HTTP response */
+
+ byte[] buffer = new byte[1024];
+ InputStream in = sock.getInputStream();
+
+ int len = ClientServerHello.readLineRN(in, buffer);
+
+ String httpReponse = new String(buffer, 0, len, "ISO-8859-1");
+
+ if (httpReponse.startsWith("HTTP/") == false)
+ throw new IOException("The proxy did not send back a valid HTTP response.");
+
+ /* "HTTP/1.X XYZ X" => 14 characters minimum */
+
+ if ((httpReponse.length() < 14) || (httpReponse.charAt(8) != ' ') || (httpReponse.charAt(12) != ' '))
+ throw new IOException("The proxy did not send back a valid HTTP response.");
+
+ int errorCode = 0;
+
+ try
+ {
+ errorCode = Integer.parseInt(httpReponse.substring(9, 12));
+ }
+ catch (NumberFormatException ignore)
+ {
+ throw new IOException("The proxy did not send back a valid HTTP response.");
+ }
+
+ if ((errorCode < 0) || (errorCode > 999))
+ throw new IOException("The proxy did not send back a valid HTTP response.");
+
+ if (errorCode != 200)
+ {
+ throw new HTTPProxyException(httpReponse.substring(13), errorCode);
+ }
+
+ /* OK, read until empty line */
+
+ while (true)
+ {
+ len = ClientServerHello.readLineRN(in, buffer);
+ if (len == 0)
+ break;
+ }
+ return;
+ }
+
+ throw new IOException("Unsupported ProxyData");
+ }
+
+ public void initialize(CryptoWishList cwl, ServerHostKeyVerifier verifier, DHGexParameters dhgex,
+ int connectTimeout, SecureRandom rnd, ProxyData proxyData) throws IOException
+ {
+ /* First, establish the TCP connection to the SSH-2 server */
+
+ establishConnection(proxyData, connectTimeout);
+
+ /* Parse the server line and say hello - important: this information is later needed for the
+ * key exchange (to stop man-in-the-middle attacks) - that is why we wrap it into an object
+ * for later use.
+ */
+
+ ClientServerHello csh = new ClientServerHello(sock.getInputStream(), sock.getOutputStream());
+
+ tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd);
+
+ km = new KexManager(this, csh, cwl, hostname, port, verifier, rnd);
+ km.initiateKEX(cwl, dhgex);
+
+ receiveThread = new Thread(new Runnable()
+ {
+ public void run()
+ {
+ try
+ {
+ receiveLoop();
+ }
+ catch (IOException e)
+ {
+ close(e, false);
+
+ if (log.isEnabled())
+ log.log(10, "Receive thread: error in receiveLoop: " + e.getMessage());
+ }
+
+ if (log.isEnabled())
+ log.log(50, "Receive thread: back from receiveLoop");
+
+ /* Tell all handlers that it is time to say goodbye */
+
+ if (km != null)
+ {
+ try
+ {
+ km.handleMessage(null, 0);
+ }
+ catch (IOException e)
+ {
+ }
+ }
+
+ for (int i = 0; i < messageHandlers.size(); i++)
+ {
+ HandlerEntry he = messageHandlers.elementAt(i);
+ try
+ {
+ he.mh.handleMessage(null, 0);
+ }
+ catch (Exception ignore)
+ {
+ }
+ }
+ }
+ });
+
+ receiveThread.setDaemon(true);
+ receiveThread.start();
+ }
+
+ public void registerMessageHandler(MessageHandler mh, int low, int high)
+ {
+ HandlerEntry he = new HandlerEntry();
+ he.mh = mh;
+ he.low = low;
+ he.high = high;
+
+ synchronized (messageHandlers)
+ {
+ messageHandlers.addElement(he);
+ }
+ }
+
+ public void removeMessageHandler(MessageHandler mh, int low, int high)
+ {
+ synchronized (messageHandlers)
+ {
+ for (int i = 0; i < messageHandlers.size(); i++)
+ {
+ HandlerEntry he = messageHandlers.elementAt(i);
+ if ((he.mh == mh) && (he.low == low) && (he.high == high))
+ {
+ messageHandlers.removeElementAt(i);
+ break;
+ }
+ }
+ }
+ }
+
+ public void sendKexMessage(byte[] msg) throws IOException
+ {
+ synchronized (connectionSemaphore)
+ {
+ if (connectionClosed)
+ {
+ throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause);
+ }
+
+ flagKexOngoing = true;
+
+ try
+ {
+ tc.sendMessage(msg);
+ }
+ catch (IOException e)
+ {
+ close(e, false);
+ throw e;
+ }
+ }
+ }
+
+ public void kexFinished() throws IOException
+ {
+ synchronized (connectionSemaphore)
+ {
+ flagKexOngoing = false;
+ connectionSemaphore.notifyAll();
+ }
+ }
+
+ public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex) throws IOException
+ {
+ km.initiateKEX(cwl, dhgex);
+ }
+
+ public void changeRecvCipher(BlockCipher bc, MAC mac)
+ {
+ tc.changeRecvCipher(bc, mac);
+ }
+
+ public void changeSendCipher(BlockCipher bc, MAC mac)
+ {
+ tc.changeSendCipher(bc, mac);
+ }
+
+ /**
+ * @param comp
+ */
+ public void changeRecvCompression(ICompressor comp) {
+ tc.changeRecvCompression(comp);
+ }
+
+ /**
+ * @param comp
+ */
+ public void changeSendCompression(ICompressor comp) {
+ tc.changeSendCompression(comp);
+ }
+
+ /**
+ *
+ */
+ public void startCompression() {
+ tc.startCompression();
+ }
+
+ public void sendAsynchronousMessage(byte[] msg) throws IOException
+ {
+ synchronized (asynchronousQueue)
+ {
+ asynchronousQueue.addElement(msg);
+
+ /* This limit should be flexible enough. We need this, otherwise the peer
+ * can flood us with global requests (and other stuff where we have to reply
+ * with an asynchronous message) and (if the server just sends data and does not
+ * read what we send) this will probably put us in a low memory situation
+ * (our send queue would grow and grow and...) */
+
+ if (asynchronousQueue.size() > 100)
+ throw new IOException("Error: the peer is not consuming our asynchronous replies.");
+
+ /* Check if we have an asynchronous sending thread */
+
+ if (asynchronousThread == null)
+ {
+ asynchronousThread = new AsynchronousWorker();
+ asynchronousThread.setDaemon(true);
+ asynchronousThread.start();
+
+ /* The thread will stop after 2 seconds of inactivity (i.e., empty queue) */
+ }
+ }
+ }
+
+ public void setConnectionMonitors(Vector monitors)
+ {
+ synchronized (this)
+ {
+ connectionMonitors = (Vector) monitors.clone();
+ }
+ }
+
+ public void sendMessage(byte[] msg) throws IOException
+ {
+ if (Thread.currentThread() == receiveThread)
+ throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!");
+
+ synchronized (connectionSemaphore)
+ {
+ while (true)
+ {
+ if (connectionClosed)
+ {
+ throw (IOException) new IOException("Sorry, this connection is closed.")
+ .initCause(reasonClosedCause);
+ }
+
+ if (flagKexOngoing == false)
+ break;
+
+ try
+ {
+ connectionSemaphore.wait();
+ }
+ catch (InterruptedException e)
+ {
+ }
+ }
+
+ try
+ {
+ tc.sendMessage(msg);
+ }
+ catch (IOException e)
+ {
+ close(e, false);
+ throw e;
+ }
+ }
+ }
+
+ public void receiveLoop() throws IOException
+ {
+ byte[] msg = new byte[35000];
+
+ while (true)
+ {
+ int msglen = tc.receiveMessage(msg, 0, msg.length);
+
+ int type = msg[0] & 0xff;
+
+ if (type == Packets.SSH_MSG_IGNORE)
+ continue;
+
+ if (type == Packets.SSH_MSG_DEBUG)
+ {
+ if (log.isEnabled())
+ {
+ TypesReader tr = new TypesReader(msg, 0, msglen);
+ tr.readByte();
+ tr.readBoolean();
+ StringBuffer debugMessageBuffer = new StringBuffer();
+ debugMessageBuffer.append(tr.readString("UTF-8"));
+
+ for (int i = 0; i < debugMessageBuffer.length(); i++)
+ {
+ char c = debugMessageBuffer.charAt(i);
+
+ if ((c >= 32) && (c <= 126))
+ continue;
+ debugMessageBuffer.setCharAt(i, '\uFFFD');
+ }
+
+ log.log(50, "DEBUG Message from remote: '" + debugMessageBuffer.toString() + "'");
+ }
+ continue;
+ }
+
+ if (type == Packets.SSH_MSG_UNIMPLEMENTED)
+ {
+ throw new IOException("Peer sent UNIMPLEMENTED message, that should not happen.");
+ }
+
+ if (type == Packets.SSH_MSG_DISCONNECT)
+ {
+ TypesReader tr = new TypesReader(msg, 0, msglen);
+ tr.readByte();
+ int reason_code = tr.readUINT32();
+ StringBuffer reasonBuffer = new StringBuffer();
+ reasonBuffer.append(tr.readString("UTF-8"));
+
+ /*
+ * Do not get fooled by servers that send abnormal long error
+ * messages
+ */
+
+ if (reasonBuffer.length() > 255)
+ {
+ reasonBuffer.setLength(255);
+ reasonBuffer.setCharAt(254, '.');
+ reasonBuffer.setCharAt(253, '.');
+ reasonBuffer.setCharAt(252, '.');
+ }
+
+ /*
+ * Also, check that the server did not send charcaters that may
+ * screw up the receiver -> restrict to reasonable US-ASCII
+ * subset -> "printable characters" (ASCII 32 - 126). Replace
+ * all others with 0xFFFD (UNICODE replacement character).
+ */
+
+ for (int i = 0; i < reasonBuffer.length(); i++)
+ {
+ char c = reasonBuffer.charAt(i);
+
+ if ((c >= 32) && (c <= 126))
+ continue;
+ reasonBuffer.setCharAt(i, '\uFFFD');
+ }
+
+ throw new IOException("Peer sent DISCONNECT message (reason code " + reason_code + "): "
+ + reasonBuffer.toString());
+ }
+
+ /*
+ * Is it a KEX Packet?
+ */
+
+ if ((type == Packets.SSH_MSG_KEXINIT) || (type == Packets.SSH_MSG_NEWKEYS)
+ || ((type >= 30) && (type <= 49)))
+ {
+ km.handleMessage(msg, msglen);
+ continue;
+ }
+
+ if (type == Packets.SSH_MSG_USERAUTH_SUCCESS) {
+ tc.startCompression();
+ }
+
+ MessageHandler mh = null;
+
+ for (int i = 0; i < messageHandlers.size(); i++)
+ {
+ HandlerEntry he = messageHandlers.elementAt(i);
+ if ((he.low <= type) && (type <= he.high))
+ {
+ mh = he.mh;
+ break;
+ }
+ }
+
+ if (mh == null)
+ throw new IOException("Unexpected SSH message (type " + type + ")");
+
+ mh.handleMessage(msg, msglen);
+ }
+ }
+}