From: Robert Gallas Date: Mon, 4 Nov 2013 14:42:24 +0000 (+0100) Subject: Ganymed SSH library provisioning X-Git-Tag: jenkins-controller-bulk-release-prepare-only-2-1~483 X-Git-Url: https://git.opendaylight.org/gerrit/gitweb?p=controller.git;a=commitdiff_plain;h=3965334ee0661e87ca6c9e12d91bd77d493e9c6d Ganymed SSH library provisioning Creation of Ganymed OSGI wrapper bundle Updated pom.xml in relevant projects Enforcing Ganymed version 261 Change-Id: I2615f074aa463e49e18a30ef4824b7d09dc87876 Signed-off-by: Robert Gallas --- diff --git a/pom.xml b/pom.xml index 5a7f41b2c1..b95cdfd62a 100644 --- a/pom.xml +++ b/pom.xml @@ -57,6 +57,7 @@ + third-party/commons/thirdparty diff --git a/third-party/commons/thirdparty/pom.xml b/third-party/commons/thirdparty/pom.xml index b69704ff20..70f3b27873 100644 --- a/third-party/commons/thirdparty/pom.xml +++ b/third-party/commons/thirdparty/pom.xml @@ -25,6 +25,7 @@ 2.3.2 2.13 2.3.2 + 1.3.1 diff --git a/third-party/ganymed/pom.xml b/third-party/ganymed/pom.xml new file mode 100644 index 0000000000..98a6596e0e --- /dev/null +++ b/third-party/ganymed/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + org.opendaylight.controller + commons.thirdparty + 1.1.1-SNAPSHOT + ../commons/thirdparty + + + org.opendaylight.controller.thirdparty + ganymed + 1.0-SNAPSHOT + bundle + + + build209 + + + + + org.osgi + org.osgi.core + 4.1.0 + + + ch.ethz.ganymed + ganymed-ssh2 + 261 + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + ch.ethz.ssh2 + ganymed-ssh2;scope=compile + true + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${enforcer.version} + + + enforce-no-snapshots + + enforce + + + + + + ch.ethz.ganymed:ganymed-ssh2:* + + + ch.ethz.ganymed:ganymed-ssh2:[261] + + + + + + + + + + + + diff --git a/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java b/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java new file mode 100644 index 0000000000..bf742c5f5f --- /dev/null +++ b/third-party/ganymed/src/main/java/ch/ethz/ssh2/Connection.java @@ -0,0 +1,1409 @@ +/* + * Copyright (c) 2006-2011 Christian Plattner. All rights reserved. + * Please refer to the LICENSE.txt for licensing details. + */ + +package ch.ethz.ssh2; + +import java.io.CharArrayWriter; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketTimeoutException; +import java.security.SecureRandom; +import java.util.List; +import java.util.Vector; + +import ch.ethz.ssh2.auth.AuthenticationManager; +import ch.ethz.ssh2.channel.ChannelManager; +import ch.ethz.ssh2.crypto.CryptoWishList; +import ch.ethz.ssh2.crypto.cipher.BlockCipherFactory; +import ch.ethz.ssh2.crypto.digest.MAC; +import ch.ethz.ssh2.packets.PacketIgnore; +import ch.ethz.ssh2.transport.KexManager; +import ch.ethz.ssh2.transport.TransportManager; +import ch.ethz.ssh2.util.TimeoutService; +import ch.ethz.ssh2.util.TimeoutService.TimeoutToken; + +/** + * A Connection is used to establish an encrypted TCP/IP + * connection to a SSH-2 server. + *

+ * Typically, one + *

    + *
  1. creates a {@link #Connection(String) Connection} object.
  2. + *
  3. calls the {@link #connect() connect()} method.
  4. + *
  5. calls some of the authentication methods (e.g., {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()}).
  6. + *
  7. calls one or several times the {@link #openSession() openSession()} method.
  8. + *
  9. finally, one must close the connection and release resources with the {@link #close() close()} method.
  10. + *
+ * + * @author Christian Plattner + * @version $Id: Connection.java 69 2013-08-09 06:39:56Z dkocher@sudo.ch $ + */ + +public class Connection +{ + /** + * The identifier presented to the SSH-2 server. This is the same + * as the "softwareversion" defined in RFC 4253. + *

+ * NOTE: As per the RFC, the "softwareversion" string MUST consist of printable + * US-ASCII characters, with the exception of whitespace characters and the minus sign (-). + */ + private String softwareversion = String.format("Ganymed_%s", Version.getSpecification()); + + /* Will be used to generate all random data needed for the current connection. + * Note: SecureRandom.nextBytes() is thread safe. + */ + + private SecureRandom generator; + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported cipher algorithms by this implementation. + */ + public static synchronized String[] getAvailableCiphers() + { + return BlockCipherFactory.getDefaultCipherList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported MAC algorthims by this implementation. + */ + public static synchronized String[] getAvailableMACs() + { + return MAC.getMacList(); + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @return The list of supported server host key algorthims by this implementation. + */ + public static synchronized String[] getAvailableServerHostKeyAlgorithms() + { + return KexManager.getDefaultServerHostkeyAlgorithmList(); + } + + private AuthenticationManager am; + + private boolean authenticated = false; + private ChannelManager cm; + + private CryptoWishList cryptoWishList = new CryptoWishList(); + + private DHGexParameters dhgexpara = new DHGexParameters(); + + private final String hostname; + + private final int port; + + private TransportManager tm; + + private boolean tcpNoDelay = false; + + private ProxyData proxyData = null; + + private List connectionMonitors = new Vector(); + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + *

+ * Same as {@link #Connection(String, int) Connection(hostname, 22)}. + * + * @param hostname the hostname of the SSH-2 server. + */ + public Connection(String hostname) + { + this(hostname, 22); + } + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + * + * @param hostname + * the host where we later want to connect to. + * @param port + * port on the server, normally 22. + */ + public Connection(String hostname, int port) + { + this.hostname = hostname; + this.port = port; + } + + /** + * Prepares a fresh Connection object which can then be used + * to establish a connection to the specified SSH-2 server. + * + * @param hostname + * the host where we later want to connect to. + * @param port + * port on the server, normally 22. + * @param softwareversion + * Allows you to set a custom "softwareversion" string as defined in RFC 4253. + * NOTE: As per the RFC, the "softwareversion" string MUST consist of printable + * US-ASCII characters, with the exception of whitespace characters and the minus sign (-). + */ + public Connection(String hostname, int port, String softwareversion) + { + this.hostname = hostname; + this.port = port; + this.softwareversion = softwareversion; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * is based on DSA (it uses DSA to sign a challenge sent by the server). + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * A String holding the username. + * @param pem + * A String containing the DSA private key of the + * user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" tag). The string may contain + * linefeeds. + * @param password + * If the PEM string is 3DES encrypted ("DES-EDE3-CBC"), then you + * must specify the password. Otherwise, this argument will be + * ignored and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + * + * @deprecated You should use one of the {@link #authenticateWithPublicKey(String, File, String) authenticateWithPublicKey()} + * methods, this method is just a wrapper for it and will + * disappear in future builds. + * + */ + public synchronized boolean authenticateWithDSA(String user, String pem, String password) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (pem == null) + throw new IllegalArgumentException("pem argument is null"); + + authenticated = am.authenticatePublicKey(user, pem.toCharArray(), password, getOrCreateSecureRND()); + + return authenticated; + } + + /** + * A wrapper that calls {@link #authenticateWithKeyboardInteractive(String, String[], InteractiveCallback) + * authenticateWithKeyboardInteractivewith} a null submethod list. + * + * @param user + * A String holding the username. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, InteractiveCallback cb) + throws IOException + { + return authenticateWithKeyboardInteractive(user, null, cb); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * is based on "keyboard-interactive", specified in + * draft-ietf-secsh-auth-kbdinteract-XX. Basically, you have to define a + * callback object which will be feeded with challenges generated by the + * server. Answers are then sent back to the server. It is possible that the + * callback will be called several times during the invocation of this + * method (e.g., if the server replies to the callback's answer(s) with + * another challenge...) + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: some SSH servers advertise "keyboard-interactive", however, any + * interactive request will be denied (without having sent any challenge to + * the client). + * + * @param user + * A String holding the username. + * @param submethods + * An array of submethod names, see + * draft-ietf-secsh-auth-kbdinteract-XX. May be null + * to indicate an empty list. + * @param cb + * An InteractiveCallback which will be used to + * determine the responses to the questions asked by the server. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithKeyboardInteractive(String user, String[] submethods, + InteractiveCallback cb) throws IOException + { + if (cb == null) + throw new IllegalArgumentException("Callback may not ne NULL!"); + + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + authenticated = am.authenticateInteractive(user, submethods, cb); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * sends username and password to the server. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * Note: if this method fails, then please double-check that it is actually + * offered by the server (use {@link #getRemainingAuthMethods(String) getRemainingAuthMethods()}. + *

+ * Often, password authentication is disabled, but users are not aware of it. + * Many servers only offer "publickey" and "keyboard-interactive". However, + * even though "keyboard-interactive" *feels* like password authentication + * (e.g., when using the putty or openssh clients) it is *not* the same mechanism. + * + * @param user + * @param password + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPassword(String user, String password) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (password == null) + throw new IllegalArgumentException("password argument is null"); + + authenticated = am.authenticatePassword(user, password); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. + * This method can be used to explicitly use the special "none" + * authentication method (where only a username has to be specified). + *

+ * Note 1: The "none" method may always be tried by clients, however as by + * the specs, the server will not explicitly announce it. In other words, + * the "none" token will never show up in the list returned by + * {@link #getRemainingAuthMethods(String)}. + *

+ * Note 2: no matter which one of the authenticateWithXXX() methods + * you call, the library will always issue exactly one initial "none" + * authentication request to retrieve the initially allowed list of + * authentication methods by the server. Please read RFC 4252 for the + * details. + *

+ * If the authentication phase is complete, true will be + * returned. If further authentication steps are needed, false + * is returned and one can retry by any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + * + * @param user + * @return if the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithNone(String user) throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + /* Trigger the sending of the PacketUserauthRequestNone packet */ + /* (if not already done) */ + + authenticated = am.authenticateNone(user); + + return authenticated; + } + + /** + * After a successful connect, one has to authenticate oneself. + * The authentication method "publickey" works by signing a challenge + * sent by the server. The signature is either DSA or RSA based - it + * just depends on the type of private key you specify, either a DSA + * or RSA private key in PEM format. And yes, this is may seem to be a + * little confusing, the method is called "publickey" in the SSH-2 protocol + * specification, however since we need to generate a signature, you + * actually have to supply a private key =). + *

+ * The private key contained in the PEM file may also be encrypted ("Proc-Type: 4,ENCRYPTED"). + * The library supports DES-CBC and DES-EDE3-CBC encryption, as well + * as the more exotic PEM encrpytions AES-128-CBC, AES-192-CBC and AES-256-CBC. + *

+ * If the authentication phase is complete, true will be + * returned. If the server does not accept the request (or if further + * authentication steps are needed), false is returned and + * one can retry either by using this or any other authentication method + * (use the getRemainingAuthMethods method to get a list of + * the remaining possible methods). + *

+ * NOTE PUTTY USERS: Event though your key file may start with "-----BEGIN..." + * it is not in the expected format. You have to convert it to the OpenSSH + * key format by using the "puttygen" tool (can be downloaded from the Putty + * website). Simply load your key and then use the "Conversions/Export OpenSSH key" + * functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemPrivateKey + * A char[] containing a DSA or RSA private key of the + * user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE KEY-----" + * tag). The char array may contain linebreaks/linefeeds. + * @param password + * If the PEM structure is encrypted ("Proc-Type: 4,ENCRYPTED") then + * you must specify a password. Otherwise, this argument will be ignored + * and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, char[] pemPrivateKey, String password) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + if (user == null) + throw new IllegalArgumentException("user argument is null"); + + if (pemPrivateKey == null) + throw new IllegalArgumentException("pemPrivateKey argument is null"); + + authenticated = am.authenticatePublicKey(user, pemPrivateKey, password, getOrCreateSecureRND()); + + return authenticated; + } + + /** + * A convenience wrapper function which reads in a private key (PEM format, either DSA or RSA) + * and then calls authenticateWithPublicKey(String, char[], String). + *

+ * NOTE PUTTY USERS: Event though your key file may start with "-----BEGIN..." + * it is not in the expected format. You have to convert it to the OpenSSH + * key format by using the "puttygen" tool (can be downloaded from the Putty + * website). Simply load your key and then use the "Conversions/Export OpenSSH key" + * functionality to get a proper PEM file. + * + * @param user + * A String holding the username. + * @param pemFile + * A File object pointing to a file containing a DSA or RSA + * private key of the user in OpenSSH key format (PEM, you can't miss the + * "-----BEGIN DSA PRIVATE KEY-----" or "-----BEGIN RSA PRIVATE KEY-----" + * tag). + * @param password + * If the PEM file is encrypted then you must specify the password. + * Otherwise, this argument will be ignored and can be set to null. + * + * @return whether the connection is now authenticated. + * @throws IOException + */ + public synchronized boolean authenticateWithPublicKey(String user, File pemFile, String password) + throws IOException + { + if (pemFile == null) + throw new IllegalArgumentException("pemFile argument is null"); + + char[] buff = new char[256]; + + CharArrayWriter cw = new CharArrayWriter(); + + FileReader fr = new FileReader(pemFile); + + while (true) + { + int len = fr.read(buff); + if (len < 0) + break; + cw.write(buff, 0, len); + } + + fr.close(); + + return authenticateWithPublicKey(user, cw.toCharArray(), password); + } + + /** + * Add a {@link ConnectionMonitor} to this connection. Can be invoked at any time, + * but it is best to add connection monitors before invoking + * connect() to avoid glitches (e.g., you add a connection monitor after + * a successful connect(), but the connection has died in the mean time. Then, + * your connection monitor won't be notified.) + *

+ * You can add as many monitors as you like. If a monitor has already been added, then + * this method does nothing. + * + * @see ConnectionMonitor + * + * @param cmon An object implementing the {@link ConnectionMonitor} interface. + */ + public synchronized void addConnectionMonitor(ConnectionMonitor cmon) + { + if (cmon == null) + throw new IllegalArgumentException("cmon argument is null"); + + if (!connectionMonitors.contains(cmon)) + { + connectionMonitors.add(cmon); + + if (tm != null) + tm.setConnectionMonitors(connectionMonitors); + } + } + + /** + * Remove a {@link ConnectionMonitor} from this connection. + * + * @param cmon + * @return whether the monitor could be removed + */ + public synchronized boolean removeConnectionMonitor(ConnectionMonitor cmon) + { + if (cmon == null) + throw new IllegalArgumentException("cmon argument is null"); + + boolean existed = connectionMonitors.remove(cmon); + + if (tm != null) + tm.setConnectionMonitors(connectionMonitors); + + return existed; + } + + /** + * Close the connection to the SSH-2 server. All assigned sessions will be + * closed, too. Can be called at any time. Don't forget to call this once + * you don't need a connection anymore - otherwise the receiver thread may + * run forever. + */ + public synchronized void close() + { + Throwable t = new Throwable("Closed due to user request."); + close(t, false); + } + + public synchronized void close(Throwable t, boolean hard) + { + if (cm != null) + cm.closeAllChannels(); + + if (tm != null) + { + tm.close(t, hard == false); + tm = null; + } + am = null; + cm = null; + authenticated = false; + } + + /** + * Same as {@link #connect(ServerHostKeyVerifier, int, int) connect(null, 0, 0)}. + * + * @return see comments for the {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} method. + * @throws IOException + */ + public synchronized ConnectionInfo connect() throws IOException + { + return connect(null, 0, 0); + } + + /** + * Same as {@link #connect(ServerHostKeyVerifier, int, int) connect(verifier, 0, 0)}. + * + * @return see comments for the {@link #connect(ServerHostKeyVerifier, int, int) connect(ServerHostKeyVerifier, int, int)} method. + * @throws IOException + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier) throws IOException + { + return connect(verifier, 0, 0); + } + + /** + * Connect to the SSH-2 server and, as soon as the server has presented its + * host key, use the {@link ServerHostKeyVerifier#verifyServerHostKey(String, + * int, String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} + * method of the verifier to ask for permission to proceed. + * If verifier is null, then any host key will be + * accepted - this is NOT recommended, since it makes man-in-the-middle attackes + * VERY easy (somebody could put a proxy SSH server between you and the real server). + *

+ * Note: The verifier will be called before doing any crypto calculations + * (i.e., diffie-hellman). Therefore, if you don't like the presented host key then + * no CPU cycles are wasted (and the evil server has less information about us). + *

+ * However, it is still possible that the server presented a fake host key: the server + * cheated (typically a sign for a man-in-the-middle attack) and is not able to generate + * a signature that matches its host key. Don't worry, the library will detect such + * a scenario later when checking the signature (the signature cannot be checked before + * having completed the diffie-hellman exchange). + *

+ * Note 2: The {@link ServerHostKeyVerifier#verifyServerHostKey(String, + * int, String, byte[]) ServerHostKeyVerifier.verifyServerHostKey()} method + * will *NOT* be called from the current thread, the call is being made from a + * background thread (there is a background dispatcher thread for every + * established connection). + *

+ * Note 3: This method will block as long as the key exchange of the underlying connection + * has not been completed (and you have not specified any timeouts). + *

+ * Note 4: If you want to re-use a connection object that was successfully connected, + * then you must call the {@link #close()} method before invoking connect() again. + * + * @param verifier + * An object that implements the + * {@link ServerHostKeyVerifier} interface. Pass null + * to accept any server host key - NOT recommended. + * + * @param connectTimeout + * Connect the underlying TCP socket to the server with the given timeout + * value (non-negative, in milliseconds). Zero means no timeout. If a proxy is being + * used (see {@link #setProxyData(ProxyData)}), then this timeout is used for the + * connection establishment to the proxy. + * + * @param kexTimeout + * Timeout for complete connection establishment (non-negative, + * in milliseconds). Zero means no timeout. The timeout counts from the + * moment you invoke the connect() method and is cancelled as soon as the + * first key-exchange round has finished. It is possible that + * the timeout event will be fired during the invocation of the + * verifier callback, but it will only have an effect after + * the verifier returns. + * + * @return A {@link ConnectionInfo} object containing the details of + * the established connection. + * + * @throws IOException + * If any problem occurs, e.g., the server's host key is not + * accepted by the verifier or there is problem during + * the initial crypto setup (e.g., the signature sent by the server is wrong). + *

+ * In case of a timeout (either connectTimeout or kexTimeout) + * a SocketTimeoutException is thrown. + *

+ * An exception may also be thrown if the connection was already successfully + * connected (no matter if the connection broke in the mean time) and you invoke + * connect() again without having called {@link #close()} first. + *

+ * If a HTTP proxy is being used and the proxy refuses the connection, + * then a {@link HTTPProxyException} may be thrown, which + * contains the details returned by the proxy. If the proxy is buggy and does + * not return a proper HTTP response, then a normal IOException is thrown instead. + */ + public synchronized ConnectionInfo connect(ServerHostKeyVerifier verifier, int connectTimeout, int kexTimeout) + throws IOException + { + final class TimeoutState + { + boolean isCancelled = false; + boolean timeoutSocketClosed = false; + } + + if (tm != null) + throw new IOException("Connection to " + hostname + " is already in connected state!"); + + if (connectTimeout < 0) + throw new IllegalArgumentException("connectTimeout must be non-negative!"); + + if (kexTimeout < 0) + throw new IllegalArgumentException("kexTimeout must be non-negative!"); + + final TimeoutState state = new TimeoutState(); + + tm = new TransportManager(); + tm.setSoTimeout(connectTimeout); + tm.setConnectionMonitors(connectionMonitors); + + /* Make sure that the runnable below will observe the new value of "tm" + * and "state" (the runnable will be executed in a different thread, which + * may be already running, that is why we need a memory barrier here). + * See also the comment in Channel.java if you + * are interested in the details. + * + * OKOK, this is paranoid since adding the runnable to the todo list + * of the TimeoutService will ensure that all writes have been flushed + * before the Runnable reads anything + * (there is a synchronized block in TimeoutService.addTimeoutHandler). + */ + + synchronized (tm) + { + /* We could actually synchronize on anything. */ + } + + try + { + TimeoutToken token = null; + + if (kexTimeout > 0) + { + final Runnable timeoutHandler = new Runnable() + { + public void run() + { + synchronized (state) + { + if (state.isCancelled) + return; + state.timeoutSocketClosed = true; + tm.close(new SocketTimeoutException("The connect timeout expired"), false); + } + } + }; + + long timeoutHorizont = System.currentTimeMillis() + kexTimeout; + + token = TimeoutService.addTimeoutHandler(timeoutHorizont, timeoutHandler); + } + + try + { + tm.clientInit(hostname, port, softwareversion, cryptoWishList, verifier, dhgexpara, connectTimeout, + getOrCreateSecureRND(), proxyData); + } + catch (SocketTimeoutException se) + { + throw (SocketTimeoutException) new SocketTimeoutException( + "The connect() operation on the socket timed out.").initCause(se); + } + + tm.setTcpNoDelay(tcpNoDelay); + + /* Wait until first KEX has finished */ + + ConnectionInfo ci = tm.getConnectionInfo(1); + + /* Now try to cancel the timeout, if needed */ + + if (token != null) + { + TimeoutService.cancelTimeoutHandler(token); + + /* Were we too late? */ + + synchronized (state) + { + if (state.timeoutSocketClosed) + throw new IOException("This exception will be replaced by the one below =)"); + /* Just in case the "cancelTimeoutHandler" invocation came just a little bit + * too late but the handler did not enter the semaphore yet - we can + * still stop it. + */ + state.isCancelled = true; + } + } + + return ci; + } + catch (SocketTimeoutException ste) + { + throw ste; + } + catch (IOException e1) + { + /* This will also invoke any registered connection monitors */ + close(new Throwable("There was a problem during connect."), false); + + synchronized (state) + { + /* Show a clean exception, not something like "the socket is closed!?!" */ + if (state.timeoutSocketClosed) + throw new SocketTimeoutException("The kexTimeout (" + kexTimeout + " ms) expired."); + } + + /* Do not wrap a HTTPProxyException */ + if (e1 instanceof HTTPProxyException) + throw e1; + + throw (IOException) new IOException("There was a problem while connecting to " + hostname + ":" + port) + .initCause(e1); + } + } + + /** + * Creates a new {@link LocalPortForwarder}. + * A LocalPortForwarder forwards TCP/IP connections that arrive at a local + * port via the secure tunnel to another host (which may or may not be + * identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the authentication step. + * There is no limit on the number of concurrent forwardings. + * + * @param local_port the local port the LocalPortForwarder shall bind to. + * @param host_to_connect target address (IP or hostname) + * @param port_to_connect target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(int local_port, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, local_port, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalPortForwarder}. + * A LocalPortForwarder forwards TCP/IP connections that arrive at a local + * port via the secure tunnel to another host (which may or may not be + * identical to the remote SSH-2 server). + *

+ * This method must only be called after one has passed successfully the authentication step. + * There is no limit on the number of concurrent forwardings. + * + * @param addr specifies the InetSocketAddress where the local socket shall be bound to. + * @param host_to_connect target address (IP or hostname) + * @param port_to_connect target port + * @return A {@link LocalPortForwarder} object. + * @throws IOException + */ + public synchronized LocalPortForwarder createLocalPortForwarder(InetSocketAddress addr, String host_to_connect, + int port_to_connect) throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward ports, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward ports, connection is not authenticated."); + + return new LocalPortForwarder(cm, addr, host_to_connect, port_to_connect); + } + + /** + * Creates a new {@link LocalStreamForwarder}. + * A LocalStreamForwarder manages an Input/Outputstream pair + * that is being forwarded via the secure tunnel into a TCP/IP connection to another host + * (which may or may not be identical to the remote SSH-2 server). + * + * @param host_to_connect + * @param port_to_connect + * @return A {@link LocalStreamForwarder} object. + * @throws IOException + */ + public synchronized LocalStreamForwarder createLocalStreamForwarder(String host_to_connect, int port_to_connect) + throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot forward, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot forward, connection is not authenticated."); + + return new LocalStreamForwarder(cm, host_to_connect, port_to_connect); + } + + /** + * Create a very basic {@link SCPClient} that can be used to copy + * files from/to the SSH-2 server. + *

+ * Works only after one has passed successfully the authentication step. + * There is no limit on the number of concurrent SCP clients. + *

+ * Note: This factory method will probably disappear in the future. + * + * @return A {@link SCPClient} object. + * @throws IOException + */ + public synchronized SCPClient createSCPClient() throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot create SCP client, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot create SCP client, connection is not authenticated."); + + return new SCPClient(this); + } + + /** + * Force an asynchronous key re-exchange (the call does not block). The + * latest values set for MAC, Cipher and DH group exchange parameters will + * be used. If a key exchange is currently in progress, then this method has + * the only effect that the so far specified parameters will be used for the + * next (server driven) key exchange. + *

+ * Note: This implementation will never start a key exchange (other than the initial one) + * unless you or the SSH-2 server ask for it. + * + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized void forceKeyExchange() throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + tm.forceKeyExchange(cryptoWishList, dhgexpara, null, null); + } + + /** + * Returns the hostname that was passed to the constructor. + * + * @return the hostname + */ + public synchronized String getHostname() + { + return hostname; + } + + /** + * Returns the port that was passed to the constructor. + * + * @return the TCP port + */ + public synchronized int getPort() + { + return port; + } + + /** + * Returns a {@link ConnectionInfo} object containing the details of + * the connection. Can be called as soon as the connection has been + * established (successfully connected). + * + * @return A {@link ConnectionInfo} object. + * @throws IOException + * In case of any failure behind the scenes. + */ + public synchronized ConnectionInfo getConnectionInfo() throws IOException + { + if (tm == null) + throw new IllegalStateException( + "Cannot get details of connection, you need to establish a connection first."); + return tm.getConnectionInfo(1); + } + + /** + * After a successful connect, one has to authenticate oneself. This method + * can be used to tell which authentication methods are supported by the + * server at a certain stage of the authentication process (for the given + * username). + *

+ * Note 1: the username will only be used if no authentication step was done + * so far (it will be used to ask the server for a list of possible + * authentication methods by sending the initial "none" request). Otherwise, + * this method ignores the user name and returns a cached method list + * (which is based on the information contained in the last negative server response). + *

+ * Note 2: the server may return method names that are not supported by this + * implementation. + *

+ * After a successful authentication, this method must not be called + * anymore. + * + * @param user + * A String holding the username. + * + * @return a (possibly emtpy) array holding authentication method names. + * @throws IOException + */ + public synchronized String[] getRemainingAuthMethods(String user) throws IOException + { + if (user == null) + throw new IllegalArgumentException("user argument may not be NULL!"); + + if (tm == null) + throw new IllegalStateException("Connection is not established!"); + + if (authenticated) + throw new IllegalStateException("Connection is already authenticated!"); + + if (am == null) + am = new AuthenticationManager(tm); + + if (cm == null) + cm = new ChannelManager(tm); + + return am.getRemainingMethods(user); + } + + /** + * Determines if the authentication phase is complete. Can be called at any + * time. + * + * @return true if no further authentication steps are + * needed. + */ + public synchronized boolean isAuthenticationComplete() + { + return authenticated; + } + + /** + * Returns true if there was at least one failed authentication request and + * the last failed authentication request was marked with "partial success" + * by the server. This is only needed in the rare case of SSH-2 server setups + * that cannot be satisfied with a single successful authentication request + * (i.e., multiple authentication steps are needed.) + *

+ * If you are interested in the details, then have a look at RFC4252. + * + * @return if the there was a failed authentication step and the last one + * was marked as a "partial success". + */ + public synchronized boolean isAuthenticationPartialSuccess() + { + if (am == null) + return false; + + return am.getPartialSuccess(); + } + + /** + * Checks if a specified authentication method is available. This method is + * actually just a wrapper for {@link #getRemainingAuthMethods(String) + * getRemainingAuthMethods()}. + * + * @param user + * A String holding the username. + * @param method + * An authentication method name (e.g., "publickey", "password", + * "keyboard-interactive") as specified by the SSH-2 standard. + * @return if the specified authentication method is currently available. + * @throws IOException + */ + public synchronized boolean isAuthMethodAvailable(String user, String method) throws IOException + { + if (method == null) + throw new IllegalArgumentException("method argument may not be NULL!"); + + String methods[] = getRemainingAuthMethods(user); + + for (int i = 0; i < methods.length; i++) + { + if (methods[i].compareTo(method) == 0) + return true; + } + + return false; + } + + private SecureRandom getOrCreateSecureRND() + { + if (generator == null) + generator = new SecureRandom(); + + return generator; + } + + /** + * Open a new {@link Session} on this connection. Works only after one has passed + * successfully the authentication step. There is no limit on the number of + * concurrent sessions. + * + * @return A {@link Session} object. + * @throws IOException + */ + public synchronized Session openSession() throws IOException + { + if (tm == null) + throw new IllegalStateException("Cannot open session, you need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("Cannot open session, connection is not authenticated."); + + return new Session(cm, getOrCreateSecureRND()); + } + + /** + * Send an SSH_MSG_IGNORE packet. This method will generate a random data attribute + * (length between 0 (invlusive) and 16 (exclusive) bytes, contents are random bytes). + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket() throws IOException + { + SecureRandom rnd = getOrCreateSecureRND(); + + byte[] data = new byte[rnd.nextInt(16)]; + rnd.nextBytes(data); + + sendIgnorePacket(data); + } + + /** + * Send an SSH_MSG_IGNORE packet with the given data attribute. + *

+ * This method must only be called once the connection is established. + * + * @throws IOException + */ + public synchronized void sendIgnorePacket(byte[] data) throws IOException + { + if (data == null) + throw new IllegalArgumentException("data argument must not be null."); + + if (tm == null) + throw new IllegalStateException( + "Cannot send SSH_MSG_IGNORE packet, you need to establish a connection first."); + + PacketIgnore pi = new PacketIgnore(); + pi.setData(data); + + tm.sendMessage(pi.getPayload()); + } + + /** + * Removes duplicates from a String array, keeps only first occurence + * of each element. Does not destroy order of elements; can handle nulls. + * Uses a very efficient O(N^2) algorithm =) + * + * @param list a String array. + * @return a cleaned String array. + */ + private String[] removeDuplicates(String[] list) + { + if ((list == null) || (list.length < 2)) + return list; + + String[] list2 = new String[list.length]; + + int count = 0; + + for (int i = 0; i < list.length; i++) + { + boolean duplicate = false; + + String element = list[i]; + + for (int j = 0; j < count; j++) + { + if (((element == null) && (list2[j] == null)) || ((element != null) && (element.equals(list2[j])))) + { + duplicate = true; + break; + } + } + + if (duplicate) + continue; + + list2[count++] = list[i]; + } + + if (count == list2.length) + return list2; + + String[] tmp = new String[count]; + System.arraycopy(list2, 0, tmp, 0, count); + + return tmp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setClient2ServerCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.c2s_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setClient2ServerMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + macs = removeDuplicates(macs); + MAC.checkMacList(macs); + cryptoWishList.c2s_mac_algos = macs; + } + + /** + * Sets the parameters for the diffie-hellman group exchange. Unless you + * know what you are doing, you will never need this. Default values are + * defined in the {@link DHGexParameters} class. + * + * @param dgp {@link DHGexParameters}, non null. + * + */ + public synchronized void setDHGexParameters(DHGexParameters dgp) + { + if (dgp == null) + throw new IllegalArgumentException(); + + dhgexpara = dgp; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param ciphers + */ + public synchronized void setServer2ClientCiphers(String[] ciphers) + { + if ((ciphers == null) || (ciphers.length == 0)) + throw new IllegalArgumentException(); + ciphers = removeDuplicates(ciphers); + BlockCipherFactory.checkCipherList(ciphers); + cryptoWishList.s2c_enc_algos = ciphers; + } + + /** + * Unless you know what you are doing, you will never need this. + * + * @param macs + */ + public synchronized void setServer2ClientMACs(String[] macs) + { + if ((macs == null) || (macs.length == 0)) + throw new IllegalArgumentException(); + + macs = removeDuplicates(macs); + MAC.checkMacList(macs); + cryptoWishList.s2c_mac_algos = macs; + } + + /** + * Define the set of allowed server host key algorithms to be used for + * the following key exchange operations. + *

+ * Unless you know what you are doing, you will never need this. + * + * @param algos An array of allowed server host key algorithms. + * SSH-2 defines ssh-dss and ssh-rsa. + * The entries of the array must be ordered after preference, i.e., + * the entry at index 0 is the most preferred one. You must specify + * at least one entry. + */ + public synchronized void setServerHostKeyAlgorithms(String[] algos) + { + if ((algos == null) || (algos.length == 0)) + throw new IllegalArgumentException(); + + algos = removeDuplicates(algos); + KexManager.checkServerHostkeyAlgorithmsList(algos); + cryptoWishList.serverHostKeyAlgorithms = algos; + } + + /** + * Enable/disable TCP_NODELAY (disable/enable Nagle's algorithm) on the underlying socket. + *

+ * Can be called at any time. If the connection has not yet been established + * then the passed value will be stored and set after the socket has been set up. + * The default value that will be used is false. + * + * @param enable the argument passed to the Socket.setTCPNoDelay() method. + * @throws IOException + */ + public synchronized void setTCPNoDelay(boolean enable) throws IOException + { + tcpNoDelay = enable; + + if (tm != null) + tm.setTcpNoDelay(enable); + } + + /** + * Used to tell the library that the connection shall be established through a proxy server. + * It only makes sense to call this method before calling the {@link #connect() connect()} + * method. + *

+ * At the moment, only HTTP proxies are supported. + *

+ * Note: This method can be called any number of times. The {@link #connect() connect()} + * method will use the value set in the last preceding invocation of this method. + * + * @see HTTPProxyData + * + * @param proxyData Connection information about the proxy. If null, then + * no proxy will be used (non surprisingly, this is also the default). + */ + public synchronized void setProxyData(ProxyData proxyData) + { + this.proxyData = proxyData; + } + + /** + * Request a remote port forwarding. + * If successful, then forwarded connections will be redirected to the given target address. + * You can cancle a requested remote port forwarding by calling + * {@link #cancelRemotePortForwarding(int) cancelRemotePortForwarding()}. + *

+ * A call of this method will block until the peer either agreed or disagreed to your request- + *

+ * Note 1: this method typically fails if you + *

    + *
  • pass a port number for which the used remote user has not enough permissions (i.e., port + * < 1024)
  • + *
  • or pass a port number that is already in use on the remote server
  • + *
  • or if remote port forwarding is disabled on the server.
  • + *
+ *

+ * Note 2: (from the openssh man page): By default, the listening socket on the server will be + * bound to the loopback interface only. This may be overriden by specifying a bind address. + * Specifying a remote bind address will only succeed if the server's GatewayPorts option + * is enabled (see sshd_config(5)). + * + * @param bindAddress address to bind to on the server: + *

    + *
  • "" means that connections are to be accepted on all protocol families + * supported by the SSH implementation
  • + *
  • "0.0.0.0" means to listen on all IPv4 addresses
  • + *
  • "::" means to listen on all IPv6 addresses
  • + *
  • "localhost" means to listen on all protocol families supported by the SSH + * implementation on loopback addresses only, [RFC3330] and RFC3513]
  • + *
  • "127.0.0.1" and "::1" indicate listening on the loopback interfaces for + * IPv4 and IPv6 respectively
  • + *
+ * @param bindPort port number to bind on the server (must be > 0) + * @param targetAddress the target address (IP or hostname) + * @param targetPort the target port + * @throws IOException + */ + public synchronized void requestRemotePortForwarding(String bindAddress, int bindPort, String targetAddress, + int targetPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + if ((bindAddress == null) || (targetAddress == null) || (bindPort <= 0) || (targetPort <= 0)) + throw new IllegalArgumentException(); + + cm.requestGlobalForward(bindAddress, bindPort, targetAddress, targetPort); + } + + /** + * Cancel an earlier requested remote port forwarding. + * Currently active forwardings will not be affected (e.g., disrupted). + * Note that further connection forwarding requests may be received until + * this method has returned. + * + * @param bindPort the allocated port number on the server + * @throws IOException if the remote side refuses the cancel request or another low + * level error occurs (e.g., the underlying connection is closed) + */ + public synchronized void cancelRemotePortForwarding(int bindPort) throws IOException + { + if (tm == null) + throw new IllegalStateException("You need to establish a connection first."); + + if (!authenticated) + throw new IllegalStateException("The connection is not authenticated."); + + cm.requestCancelGlobalForward(bindPort); + } + + /** + * Provide your own instance of SecureRandom. Can be used, e.g., if you + * want to seed the used SecureRandom generator manually. + *

+ * The SecureRandom instance is used during key exchanges, public key authentication, + * x11 cookie generation and the like. + * + * @param rnd a SecureRandom instance + */ + public synchronized void setSecureRandom(SecureRandom rnd) + { + if (rnd == null) + throw new IllegalArgumentException(); + + this.generator = rnd; + } +} diff --git a/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java b/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java new file mode 100644 index 0000000000..50e9b287ea --- /dev/null +++ b/third-party/ganymed/src/main/java/ch/ethz/ssh2/transport/TransportManager.java @@ -0,0 +1,965 @@ +/* + * Copyright (c) 2006-2013 Christian Plattner. All rights reserved. + * Please refer to the LICENSE.txt for licensing details. + */ + +package ch.ethz.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.List; +import java.util.Vector; + +import ch.ethz.ssh2.ConnectionInfo; +import ch.ethz.ssh2.ConnectionMonitor; +import ch.ethz.ssh2.DHGexParameters; +import ch.ethz.ssh2.HTTPProxyData; +import ch.ethz.ssh2.HTTPProxyException; +import ch.ethz.ssh2.ProxyData; +import ch.ethz.ssh2.ServerHostKeyVerifier; +import ch.ethz.ssh2.crypto.Base64; +import ch.ethz.ssh2.crypto.CryptoWishList; +import ch.ethz.ssh2.crypto.cipher.BlockCipher; +import ch.ethz.ssh2.crypto.digest.MAC; +import ch.ethz.ssh2.log.Logger; +import ch.ethz.ssh2.packets.PacketDisconnect; +import ch.ethz.ssh2.packets.Packets; +import ch.ethz.ssh2.packets.TypesReader; +import ch.ethz.ssh2.server.ServerConnectionState; +import ch.ethz.ssh2.signature.DSAPrivateKey; +import ch.ethz.ssh2.signature.RSAPrivateKey; +import ch.ethz.ssh2.util.StringEncoder; +import ch.ethz.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 + * @version $Id: TransportManager.java 47 2013-07-31 23:59:52Z cleondris@gmail.com $ + */ +public class TransportManager +{ + private static final Logger log = Logger.getLogger(TransportManager.class); + + private static class HandlerEntry + { + MessageHandler mh; + int low; + int high; + } + + private final List asynchronousQueue = new Vector(); + private Thread asynchronousThread = null; + private boolean asynchronousPending = false; + + class AsynchronousEntry + { + public byte[] msg; + public Runnable run; + + public AsynchronousEntry(byte[] msg, Runnable run) + { + this.msg = msg; + this.run = run; + } + } + + class AsynchronousWorker extends Thread + { + @Override + public void run() + { + while (true) + { + AsynchronousEntry item = null; + + synchronized (asynchronousQueue) + { + if (asynchronousQueue.size() == 0) + { + /* Only now we may reset the flag, since we are sure that all queued items + * have been sent (there is a slight delay between de-queuing and sending, + * this is why we need this flag! See code below. Sending takes place outside + * of this lock, this is why a test for size()==0 (from another thread) does not ensure + * that all messages have been sent. + */ + + asynchronousPending = false; + + /* Notify any senders that they can proceed, all async messages have been delivered */ + + asynchronousQueue.notifyAll(); + + /* After the queue is empty for about 2 seconds, stop this thread */ + + try + { + asynchronousQueue.wait(2000); + } + catch (InterruptedException ignore) + { + } + + if (asynchronousQueue.size() == 0) + { + asynchronousThread = null; + return; + } + } + + item = 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 + { + sendMessageImmediate(item.msg); + } + catch (IOException e) + { + return; + } + + if (item.run != null) + { + try + { + item.run.run(); + } + catch (Exception ignore) + { + } + + } + } + } + } + + private Socket sock = new Socket(); + + private final Object connectionSemaphore = new Object(); + + private boolean flagKexOngoing = false; + private boolean connectionClosed = false; + + private Throwable reasonClosedCause = null; + + private TransportConnection tc; + private KexManager km; + + private final List messageHandlers = new Vector(); + + private Thread receiveThread; + + private List connectionMonitors = new Vector(); + private boolean monitorsWereInformed = false; + + /** + * There were reports that there are JDKs which use + * the resolver even though one supplies a dotted IP + * address in the Socket constructor. That is why we + * try to generate the InetAdress "by hand". + * + * @param host + * @return the InetAddress + * @throws UnknownHostException + */ + private static InetAddress createInetAddress(String host) throws UnknownHostException + { + /* Check if it is a dotted IP4 address */ + + InetAddress addr = parseIPv4Address(host); + + if (addr != null) + { + return addr; + } + + return InetAddress.getByName(host); + } + + private static InetAddress parseIPv4Address(String host) throws UnknownHostException + { + if (host == null) + { + return null; + } + + String[] quad = Tokenizer.parseTokens(host, '.'); + + if ((quad == null) || (quad.length != 4)) + { + return null; + } + + byte[] addr = new byte[4]; + + for (int i = 0; i < 4; i++) + { + int part = 0; + + if ((quad[i].length() == 0) || (quad[i].length() > 3)) + { + return null; + } + + for (int k = 0; k < quad[i].length(); k++) + { + char c = quad[i].charAt(k); + + /* No, Character.isDigit is not the same */ + if ((c < '0') || (c > '9')) + { + return null; + } + + part = part * 10 + (c - '0'); + } + + if (part > 255) /* 300.1.2.3 is invalid =) */ + { + return null; + } + + addr[i] = (byte) part; + } + + return InetAddress.getByAddress(host, addr); + } + + 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 */ + + List monitors = new Vector(); + + synchronized (this) + { + /* Short term lock to protect "connectionMonitors" + * and "monitorsWereInformed" + * (they may be modified concurrently) + */ + + if (monitorsWereInformed == false) + { + monitorsWereInformed = true; + monitors.addAll(connectionMonitors); + } + } + + for (ConnectionMonitor cmon : monitors) + { + try + { + cmon.connectionLost(reasonClosedCause); + } + catch (Exception ignore) + { + } + } + } + + private static Socket establishConnection(String hostname, int port, ProxyData proxyData, int connectTimeout) + throws IOException + { + /* See the comment for createInetAddress() */ + + if (proxyData == null) + { + InetAddress addr = createInetAddress(hostname); + Socket s = new Socket(); + s.connect(new InetSocketAddress(addr, port), connectTimeout); + return s; + } + + if (proxyData instanceof HTTPProxyData) + { + HTTPProxyData pd = (HTTPProxyData) proxyData; + + /* At the moment, we only support HTTP proxies */ + + InetAddress addr = createInetAddress(pd.proxyHost); + Socket s = new Socket(); + s.connect(new InetSocketAddress(addr, pd.proxyPort), connectTimeout); + + /* OK, now tell the proxy where we actually want to connect to */ + + StringBuilder sb = new StringBuilder(); + + 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(StringEncoder.GetBytes(credentials)); + 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 = s.getOutputStream(); + + out.write(StringEncoder.GetBytes(sb.toString())); + out.flush(); + + /* Now parse the HTTP response */ + + byte[] buffer = new byte[1024]; + InputStream in = s.getInputStream(); + + int len = ClientServerHello.readLineRN(in, buffer); + + String httpReponse = StringEncoder.GetString(buffer, 0, len); + + 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 s; + } + + throw new IOException("Unsupported ProxyData"); + } + + private void startReceiver() throws IOException + { + receiveThread = new Thread(new Runnable() + { + public void run() + { + try + { + receiveLoop(); + } + catch (Exception e) + { + close(e, false); + + log.warning("Receive thread: error in receiveLoop: " + e.getMessage()); + } + + if (log.isDebugEnabled()) + { + log.debug("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 ignored) + { + } + } + + for (HandlerEntry he : messageHandlers) + { + try + { + he.mh.handleMessage(null, 0); + } + catch (Exception ignore) + { + } + } + } + }); + + receiveThread.setDaemon(true); + receiveThread.start(); + } + + public void clientInit(String hostname, int port, String softwareversion, CryptoWishList cwl, + ServerHostKeyVerifier verifier, DHGexParameters dhgex, int connectTimeout, SecureRandom rnd, + ProxyData proxyData) throws IOException + { + /* First, establish the TCP connection to the SSH-2 server */ + + sock = establishConnection(hostname, port, 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 = ClientServerHello.clientHello(softwareversion, sock.getInputStream(), + sock.getOutputStream()); + + tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), rnd); + + km = new ClientKexManager(this, csh, cwl, hostname, port, verifier, rnd); + km.initiateKEX(cwl, dhgex, null, null); + + startReceiver(); + } + + public void serverInit(ServerConnectionState state) throws IOException + { + /* TCP connection is already established */ + + this.sock = state.s; + + /* Parse the client 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. + */ + + state.csh = ClientServerHello.serverHello(state.softwareversion, sock.getInputStream(), sock.getOutputStream()); + + tc = new TransportConnection(sock.getInputStream(), sock.getOutputStream(), state.generator); + + km = new ServerKexManager(state); + km.initiateKEX(state.next_cryptoWishList, null, state.next_dsa_key, state.next_rsa_key); + + startReceiver(); + } + + 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.add(he); + } + } + + public void removeMessageHandler(MessageHandler mh, int low, int high) + { + synchronized (messageHandlers) + { + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.get(i); + if ((he.mh == mh) && (he.low == low) && (he.high == high)) + { + messageHandlers.remove(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(); + } + } + + /** + * + * @param cwl + * @param dhgex + * @param dsa may be null if this is a client connection + * @param rsa may be null if this is a client connection + * @throws IOException + */ + public void forceKeyExchange(CryptoWishList cwl, DHGexParameters dhgex, DSAPrivateKey dsa, RSAPrivateKey rsa) + throws IOException + { + synchronized (connectionSemaphore) + { + if (connectionClosed) + /* Inform the caller that there is no point in triggering a new kex */ + throw (IOException) new IOException("Sorry, this connection is closed.").initCause(reasonClosedCause); + } + + km.initiateKEX(cwl, dhgex, dsa, rsa); + } + + public void changeRecvCipher(BlockCipher bc, MAC mac) + { + tc.changeRecvCipher(bc, mac); + } + + public void changeSendCipher(BlockCipher bc, MAC mac) + { + tc.changeSendCipher(bc, mac); + } + + public void sendAsynchronousMessage(byte[] msg) throws IOException + { + sendAsynchronousMessage(msg, null); + } + + public void sendAsynchronousMessage(byte[] msg, Runnable run) throws IOException + { + synchronized (asynchronousQueue) + { + asynchronousQueue.add(new AsynchronousEntry(msg, run)); + asynchronousPending = true; + + /* 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) */ + } + + asynchronousQueue.notifyAll(); + } + } + + public void setConnectionMonitors(List monitors) + { + synchronized (this) + { + connectionMonitors = new Vector(); + connectionMonitors.addAll(monitors); + } + } + + /** + * True if no response message expected. + */ + private boolean idle; + + /** + * Send a message but ensure that all queued messages are being sent first. + * + * @param msg + * @throws IOException + */ + public void sendMessage(byte[] msg) throws IOException + { + synchronized (asynchronousQueue) + { + while (asynchronousPending) + { + try + { + asynchronousQueue.wait(1000); + } + catch (InterruptedException e) + { + } + } + } + + sendMessageImmediate(msg); + } + + /** + * Send message, ignore queued async messages that have not been delivered yet. + * Will be called directly from the asynchronousThread thread. + * + * @param msg + * @throws IOException + */ + public void sendMessageImmediate(byte[] msg) throws IOException + { + if (Thread.currentThread() == receiveThread) + { + throw new IOException("Assertion error: sendMessage may never be invoked by the receiver thread!"); + } + + boolean wasInterrupted = false; + + try + { + 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) + { + wasInterrupted = true; + } + } + + try + { + tc.sendMessage(msg); + idle = false; + } + catch (IOException e) + { + close(e, false); + throw e; + } + } + } + finally + { + if (wasInterrupted) + Thread.currentThread().interrupt(); + } + } + + public void receiveLoop() throws IOException + { + byte[] msg = new byte[35000]; + + while (true) + { + int msglen; + try + { + msglen = tc.receiveMessage(msg, 0, msg.length); + } + catch (SocketTimeoutException e) + { + // Timeout in read + if (idle) + { + log.debug("Ignoring socket timeout"); + continue; + } + throw e; + } + idle = true; + + int type = msg[0] & 0xff; + + if (type == Packets.SSH_MSG_IGNORE) + { + continue; + } + + if (type == Packets.SSH_MSG_DEBUG) + { + if (log.isDebugEnabled()) + { + TypesReader tr = new TypesReader(msg, 0, msglen); + tr.readByte(); + tr.readBoolean(); + StringBuilder debugMessageBuffer = new StringBuilder(); + 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.debug("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(); + StringBuilder reasonBuffer = new StringBuilder(); + 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 characters 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; + } + + MessageHandler mh = null; + + for (int i = 0; i < messageHandlers.size(); i++) + { + HandlerEntry he = messageHandlers.get(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); + } + } +}