/* * Copyright 2010 netling project <http://netling.org> * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.netling.ssh; import java.io.File; import java.io.IOException; import java.net.SocketAddress; import java.security.KeyPair; import java.security.PublicKey; import java.util.Arrays; import java.util.LinkedList; import java.util.List; import org.netling.SocketClient; import org.netling.ssh.common.DisconnectReason; import org.netling.ssh.common.Factory; import org.netling.ssh.common.SSHException; import org.netling.ssh.common.SecurityUtils; import org.netling.ssh.connection.Connection; import org.netling.ssh.connection.ConnectionException; import org.netling.ssh.connection.ConnectionImpl; import org.netling.ssh.connection.channel.direct.LocalPortForwarder; import org.netling.ssh.connection.channel.direct.Session; import org.netling.ssh.connection.channel.direct.SessionChannel; import org.netling.ssh.connection.channel.direct.SessionFactory; import org.netling.ssh.connection.channel.forwarded.ConnectListener; import org.netling.ssh.connection.channel.forwarded.RemotePortForwarder; import org.netling.ssh.connection.channel.forwarded.RemotePortForwarder.ForwardedTCPIPChannel; import org.netling.ssh.connection.channel.forwarded.X11Forwarder; import org.netling.ssh.connection.channel.forwarded.X11Forwarder.X11Channel; import org.netling.ssh.transport.Transport; import org.netling.ssh.transport.TransportException; import org.netling.ssh.transport.TransportImpl; import org.netling.ssh.transport.compression.DelayedZlibCompression; import org.netling.ssh.transport.compression.NoneCompression; import org.netling.ssh.transport.compression.ZlibCompression; import org.netling.ssh.transport.verification.HostKeyVerifier; import org.netling.ssh.transport.verification.OpenSSHKnownHosts; import org.netling.ssh.userauth.UserAuth; import org.netling.ssh.userauth.UserAuthException; import org.netling.ssh.userauth.UserAuthImpl; import org.netling.ssh.userauth.keyprovider.FileKeyProvider; import org.netling.ssh.userauth.keyprovider.KeyPairWrapper; import org.netling.ssh.userauth.keyprovider.KeyProvider; import org.netling.ssh.userauth.keyprovider.KeyProviderUtil; import org.netling.ssh.userauth.method.AuthKeyboardInteractive; import org.netling.ssh.userauth.method.AuthMethod; import org.netling.ssh.userauth.method.AuthPassword; import org.netling.ssh.userauth.method.AuthPublickey; import org.netling.ssh.userauth.method.PasswordResponseProvider; import org.netling.ssh.userauth.password.PasswordFinder; import org.netling.ssh.userauth.password.PasswordUtils; import org.netling.ssh.userauth.password.Resource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Secure SHell client API. * <p/> * Before connection is established, host key verification needs to be accounted for. This is done by {@link * #addHostKeyVerifier(HostKeyVerifier) specifying} one or more {@link HostKeyVerifier} objects. Database of known * hostname-key pairs in the OpenSSH {@code "known_hosts"} format can be {@link #loadKnownHosts(File) loaded} for host * key verification. * <p/> * User authentication can be performed by any of the {@code auth*()} method. * <p/> * {@link #startSession()} caters to the most typical use case of starting a {@code session} channel and executing a * remote command, starting a subsystem, etc. If you wish to request X11 forwarding for some session, first {@link * #registerX11Forwarder(ConnectListener) register} a {@link ConnectListener} for {@code x11} channels. * <p/> * {@link #newLocalPortForwarder Local} and {@link #getRemotePortForwarder() remote} port forwarding is possible. * <p/> * <em>A simple example:</em> * <p/> * <pre> * client = new SSHClient(); * client.initUserKnownHosts(); * client.connect("hostname"); * try { * client.authPassword("username", "password"); * final Session session = client.startSession(); * try { * session.exec("true"); * client.getConnection().join(); * } finally { * session.close(); * } finally { * client.disconnect(); * } * </pre> * <p/> * Where a password or passphrase is required, if you're extra-paranoid use the {@code char[]} based method. The {@code * char[]} will be blanked out after use. */ public class SSHClient extends SocketClient implements SessionFactory { /** Default port for SSH */ public static final int DEFAULT_PORT = 22; /** Logger */ protected final Logger log = LoggerFactory.getLogger(getClass()); /** Transport layer */ protected final Transport trans; /** {@code ssh-userauth} service */ protected final UserAuth auth; /** {@code ssh-connection} service */ protected final Connection conn; /** Default constructor. Initializes this object using {@link DefaultConfig}. */ public SSHClient() { this(new DefaultConfig()); } /** * Constructor that allows specifying a {@code config} to be used. * * @param config {@link Config} instance */ public SSHClient(Config config) { super(DEFAULT_PORT); this.trans = new TransportImpl(config); this.auth = new UserAuthImpl(trans); this.conn = new ConnectionImpl(trans); } /** * Add a {@link HostKeyVerifier} which will be invoked for verifying host key during connection establishment and * future key exchanges. * * @param hostKeyVerifier {@link HostKeyVerifier} instance */ public void addHostKeyVerifier(HostKeyVerifier hostKeyVerifier) { trans.addHostKeyVerifier(hostKeyVerifier); } /** * Add a {@link HostKeyVerifier} that will verify any host that's able to claim a host key with the given {@code * fingerprint}, e.g. {@code "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"} * * @param fingerprint expected fingerprint in colon-delimited format (16 octets in hex delimited by a colon) * * @see SecurityUtils#getFingerprint */ public void addHostKeyVerifier(final String fingerprint) { addHostKeyVerifier(new HostKeyVerifier() { @Override public boolean verify(String h, int p, PublicKey k) { return SecurityUtils.getFingerprint(k).equals(fingerprint); } }); } /** * Authenticate {@code username} using the supplied {@code methods}. * * @param username user to authenticate * @param methods one or more authentication method * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void auth(String username, AuthMethod... methods) throws UserAuthException, TransportException { assert isConnected(); auth(username, Arrays.<AuthMethod>asList(methods)); } /** * Authenticate {@code username} using the supplied {@code methods}. * * @param username user to authenticate * @param methods one or more authentication method * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void auth(String username, Iterable<AuthMethod> methods) throws UserAuthException, TransportException { assert isConnected(); auth.authenticate(username, (Service) conn, methods); } /** * Authenticate {@code username} using the {@code "password"} authentication method and as a fallback basic * challenge-response authentication. * * @param username user to authenticate * @param password the password to use for authentication * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPassword(String username, String password) throws UserAuthException, TransportException { authPassword(username, password.toCharArray()); } /** * Authenticate {@code username} using the {@code "password"} authentication method and as a fallback basic * challenge-response authentication.. The {@code password} array is blanked out after use. * * @param username user to authenticate * @param password the password to use for authentication * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPassword(final String username, final char[] password) throws UserAuthException, TransportException { try { authPassword(username, new PasswordFinder() { @Override public char[] reqPassword(Resource<?> resource) { return password.clone(); } @Override public boolean shouldRetry(Resource<?> resource) { return false; } }); } finally { PasswordUtils.blankOut(password); } } /** * Authenticate {@code username} using the {@code "password"} authentication method and as a fallback basic * challenge-response authentication. * * @param username user to authenticate * @param pfinder the {@link PasswordFinder} to use for authentication * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPassword(String username, PasswordFinder pfinder) throws UserAuthException, TransportException { auth(username, new AuthPassword(pfinder), new AuthKeyboardInteractive(new PasswordResponseProvider(pfinder))); } /** * Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from some common * locations on the file system. This method relies on {@code ~/.ssh/id_rsa} and {@code ~/.ssh/id_dsa}. * <p/> * This method does not provide a way to specify a passphrase. * * @param username user to authenticate * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPublickey(String username) throws UserAuthException, TransportException { final String base = System.getProperty("user.home") + File.separator + ".ssh" + File.separator; authPublickey(username, base + "id_rsa", base + "id_dsa"); } /** * Authenticate {@code username} using the {@code "publickey"} authentication method. * <p/> * {@link KeyProvider} instances can be created using any of the of the {@code loadKeys()} method provided in this * class. In case multiple {@code keyProviders} are specified; authentication is attempted in order as long as the * {@code "publickey"} authentication method is available. * * @param username user to authenticate * @param keyProviders one or more {@link KeyProvider} instances * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPublickey(String username, Iterable<KeyProvider> keyProviders) throws UserAuthException, TransportException { final List<AuthMethod> am = new LinkedList<AuthMethod>(); for (KeyProvider kp : keyProviders) am.add(new AuthPublickey(kp)); auth(username, am); } /** * Authenticate {@code username} using the {@code "publickey"} authentication method. * <p/> * {@link KeyProvider} instances can be created using any of the {@code loadKeys()} method provided in this class. * In case multiple {@code keyProviders} are specified; authentication is attempted in order as long as the {@code * "publickey"} authentication method is available. * * @param username user to authenticate * @param keyProviders one or more {@link KeyProvider} instances * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPublickey(String username, KeyProvider... keyProviders) throws UserAuthException, TransportException { authPublickey(username, Arrays.<KeyProvider>asList(keyProviders)); } /** * Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from one or more * {@code locations} in the file system. * <p/> * In case multiple {@code locations} are specified; authentication is attempted in order as long as the {@code * "publickey"} authentication method is available. If there is an error loading keys from any of them (e.g. file * could not be read, file format not recognized) that key file it is ignored. * <p/> * This method does not provide a way to specify a passphrase. * * @param username user to authenticate * @param locations one or more locations in the file system containing the private key * * @throws UserAuthException in case of authentication failure * @throws TransportException if there was a transport-layer error */ public void authPublickey(String username, String... locations) throws UserAuthException, TransportException { final List<KeyProvider> keyProviders = new LinkedList<KeyProvider>(); for (String loc : locations) try { log.debug("Attempting to load key from: {}", loc); keyProviders.add(loadKeys(loc)); } catch (IOException logged) { log.warn("Could not load keys due to: {}", logged); } authPublickey(username, keyProviders); } /** * Disconnects from the connected SSH server. {@code SSHClient} objects are not reusable therefore it is incorrect * to attempt connection after this method has been called. * <p/> * This method should be called from a {@code finally} construct after connection is established; so that proper * cleanup is done and the thread spawned by the transport layer for dealing with incoming packets is stopped. */ @Override public void disconnect() throws IOException { assert isConnected(); trans.disconnect(); super.disconnect(); assert !isConnected(); } /** @return the associated {@link Connection} instance. */ public Connection getConnection() { return conn; } /** @return a {@link RemotePortForwarder} that allows requesting remote forwarding over this connection. */ public RemotePortForwarder getRemotePortForwarder() { synchronized (conn) { RemotePortForwarder rpf = (RemotePortForwarder) conn.get(ForwardedTCPIPChannel.TYPE); if (rpf == null) conn.attach(rpf = new RemotePortForwarder(conn)); return rpf; } } /** @return the associated {@link Transport} instance. */ public Transport getTransport() { return trans; } /** * @return the associated {@link UserAuth} instance. This allows access to information like the {@link * UserAuth#getBanner() authentication banner}, whether authentication was at least {@link * UserAuth#hadPartialSuccess() partially successful}, and any {@link UserAuth#getSavedExceptions() saved * exceptions} that were ignored because there were more authentication method that could be tried. */ public UserAuth getUserAuth() { return auth; } /** @return whether authenticated. */ public boolean isAuthenticated() { return trans.isAuthenticated(); } /** @return whether connected. */ @Override public boolean isConnected() { return super.isConnected() && trans.isRunning(); } /** * Creates a {@link KeyProvider} from supplied {@link KeyPair}. * * @param kp the key pair * * @return the key provider ready for use in authentication */ public KeyProvider loadKeys(KeyPair kp) { return new KeyPairWrapper(kp); } /** * Returns a {@link KeyProvider} instance created from a location on the file system where an <em>unencrypted</em> * private key file (does not require a passphrase) can be found. Simply calls {@link #loadKeys(String, * PasswordFinder)} with the {@link PasswordFinder} argument as {@code null}. * * @param location the location for the key file * * @return the key provider ready for use in authentication * * @throws SSHException if there was no suitable key provider available for the file format; typically because * BouncyCastle is not in the classpath * @throws IOException if the key file format is not known, if the file could not be read, etc. */ public KeyProvider loadKeys(String location) throws IOException { return loadKeys(location, (PasswordFinder) null); } /** * Utility function for createing a {@link KeyProvider} instance from given location on the file system. Creates a * one-off {@link PasswordFinder} using {@link PasswordUtils#createOneOff(char[])}, and calls {@link * #loadKeys(String,PasswordFinder)}. * * @param location location of the key file * @param passphrase passphrase as a char-array * * @return the key provider ready for use in authentication * * @throws SSHException if there was no suitable key provider available for the file format; typically because * BouncyCastle is not in the classpath * @throws IOException if the key file format is not known, if the file could not be read, etc. */ public KeyProvider loadKeys(String location, char[] passphrase) throws IOException { return loadKeys(location, PasswordUtils.createOneOff(passphrase)); } /** * Creates a {@link KeyProvider} instance from given location on the file system. Currently only PKCS8 format * private key files are supported (OpenSSH uses this format). * <p/> * * @param location the location of the key file * @param passwordFinder the {@link PasswordFinder} that can supply the passphrase for decryption (may be {@code * null} in case keyfile is not encrypted) * * @return the key provider ready for use in authentication * * @throws SSHException if there was no suitable key provider available for the file format; typically because * BouncyCastle is not in the classpath * @throws IOException if the key file format is not known, if the file could not be read, etc. */ public KeyProvider loadKeys(String location, PasswordFinder passwordFinder) throws IOException { final File loc = new File(location); final FileKeyProvider.Format format = KeyProviderUtil.detectKeyFileFormat(loc); final FileKeyProvider fkp = Factory.Named.Util.create(trans.getConfig().getFileKeyProviderFactories(), format .toString()); if (fkp == null) throw new SSHException("No provider available for " + format + " key file"); fkp.init(loc, passwordFinder); return fkp; } /** * Convenience method for creating a {@link KeyProvider} instance from a {@code location} where an <i>encrypted</i> * key file is located. Calls {@link #loadKeys(String, char[])} with a character array created from the supplied * {@code passphrase} string. * * @param location location of the key file * @param passphrase passphrase as a string * * @return the key provider for use in authentication * * @throws IOException if the key file format is not known, if the file could not be read etc. */ public KeyProvider loadKeys(String location, String passphrase) throws IOException { return loadKeys(location, passphrase.toCharArray()); } /** * Attempts loading the user's {@code known_hosts} file from the default locations, i.e. {@code ~/.ssh/known_hosts} * and {@code ~/.ssh/known_hosts2} on most platforms. Adds the resulting {@link OpenSSHKnownHosts} object as a host * key verifier. * <p/> * For finer control over which file is used, see {@link #loadKnownHosts(File)}. * * @throws IOException if there is an error loading from <em>both</em> locations */ public void loadKnownHosts() throws IOException { boolean loaded = false; final File sshDir = OpenSSHKnownHosts.detectSSHDir(); if (sshDir != null) { for (File loc : Arrays.asList(new File(sshDir, "known_hosts"), new File(sshDir, "known_hosts2"))) { try { loadKnownHosts(loc); loaded = true; } catch (IOException e) { // Ignore for now } } } if (!loaded) throw new IOException("Could not load known_hosts"); } /** * Adds a {@link OpenSSHKnownHosts} object created from the specified location as a host key verifier. * * @param location location for {@code known_hosts} file * * @throws IOException if there is an error loading from any of these locations */ public void loadKnownHosts(File location) throws IOException { addHostKeyVerifier(new OpenSSHKnownHosts(location)); } /** * Create a {@link LocalPortForwarder} that will listen on {@code address} and forward incoming connections to the * server; which will further forward them to {@code host:port}. * <p/> * The returned forwarder's {@link LocalPortForwarder#listen() listen()} method should be called to actually start * listening, this method just creates an instance. * * @param address defines where the {@link LocalPortForwarder} listens * @param host hostname to which the server will forward * @param port the port at {@code hostname} to which the server wil forward * * @return a {@link LocalPortForwarder} * * @throws IOException if there is an error opening a local server socket */ public LocalPortForwarder newLocalPortForwarder(SocketAddress address, String host, int port) throws IOException { return new LocalPortForwarder(getServerSocketFactory(), conn, address, host, port); } /** * Register a {@code listener} for handling forwarded X11 channels. Without having done this, an incoming X11 * forwarding will be summarily rejected. * <p/> * It should be clarified that multiple listeners for X11 forwarding over a single SSH connection are not supported * (and don't make much sense). So a subsequent call to this method is only going to replace the registered {@code * listener}. * * @param listener the {@link ConnectListener} that should be delegated the responsibility of handling forwarded * {@link X11Channel} 's * * @return an {@link X11Forwarder} that allows to {@link X11Forwarder#stop() stop acting} on X11 requests from * server */ public X11Forwarder registerX11Forwarder(ConnectListener listener) { final X11Forwarder x11f = new X11Forwarder(conn, listener); conn.attach(x11f); return x11f; } /** * Does key re-exchange. * * @throws TransportException if an error occurs during key exchange */ public void rekey() throws TransportException { doKex(); } @Override public Session startSession() throws ConnectionException, TransportException { assert isConnected() && isAuthenticated(); final SessionChannel sess = new SessionChannel(conn); sess.open(); assert sess.isOpen(); return sess; } /** * Adds {@code zlib} compression to preferred compression algorithms. There is no guarantee that it will be * successfully negotiatied. * <p/> * If the client is already connected renegotiation is done; otherwise this method simply returns (and compression * will be negotiated during connection establishment). * * @throws ClassNotFoundException if {@code JZlib} is not in classpath * @throws TransportException if an error occurs during renegotiation */ public void useCompression() throws TransportException { trans.getConfig().setCompressionFactories(Arrays.asList( new DelayedZlibCompression.Factory(), new ZlibCompression.Factory(), new NoneCompression.Factory())); if (isConnected()) rekey(); } /** On connection establishment, also initializes the SSH transport via {@link Transport#init} and {@link #doKex()}. */ @Override protected void onConnect() throws IOException { super.onConnect(); trans.init(getRemoteHostname(), getRemotePort(), getInputStream(), getOutputStream()); doKex(); } /** * Do key exchange. * * @throws TransportException if error during kex */ protected void doKex() throws TransportException { assert trans.isRunning(); final long start = System.currentTimeMillis(); try { trans.doKex(); } catch (TransportException te) { trans.disconnect(DisconnectReason.KEY_EXCHANGE_FAILED); throw te; } log.info("Key exchange took {} seconds", (System.currentTimeMillis() - start) / 1000.0); } }