
net.schmizz.sshj.SSHClient Maven / Gradle / Ivy
/*
* Copyright 2010 Shikhar Bhushan
*
* 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 net.schmizz.sshj;
import net.schmizz.sshj.common.DisconnectReason;
import net.schmizz.sshj.common.Factory;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.common.SecurityUtils;
import net.schmizz.sshj.connection.Connection;
import net.schmizz.sshj.connection.ConnectionException;
import net.schmizz.sshj.connection.ConnectionImpl;
import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder;
import net.schmizz.sshj.connection.channel.direct.Session;
import net.schmizz.sshj.connection.channel.direct.SessionChannel;
import net.schmizz.sshj.connection.channel.direct.SessionFactory;
import net.schmizz.sshj.connection.channel.forwarded.ConnectListener;
import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder;
import net.schmizz.sshj.connection.channel.forwarded.RemotePortForwarder.ForwardedTCPIPChannel;
import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder;
import net.schmizz.sshj.connection.channel.forwarded.X11Forwarder.X11Channel;
import net.schmizz.sshj.sftp.SFTPClient;
import net.schmizz.sshj.sftp.StatefulSFTPClient;
import net.schmizz.sshj.transport.Transport;
import net.schmizz.sshj.transport.TransportException;
import net.schmizz.sshj.transport.TransportImpl;
import net.schmizz.sshj.transport.compression.DelayedZlibCompression;
import net.schmizz.sshj.transport.compression.NoneCompression;
import net.schmizz.sshj.transport.compression.ZlibCompression;
import net.schmizz.sshj.transport.verification.HostKeyVerifier;
import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts;
import net.schmizz.sshj.userauth.UserAuth;
import net.schmizz.sshj.userauth.UserAuthException;
import net.schmizz.sshj.userauth.UserAuthImpl;
import net.schmizz.sshj.userauth.keyprovider.FileKeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyPairWrapper;
import net.schmizz.sshj.userauth.keyprovider.KeyProvider;
import net.schmizz.sshj.userauth.keyprovider.KeyProviderUtil;
import net.schmizz.sshj.userauth.method.AuthMethod;
import net.schmizz.sshj.userauth.method.AuthPassword;
import net.schmizz.sshj.userauth.method.AuthPublickey;
import net.schmizz.sshj.userauth.password.PasswordFinder;
import net.schmizz.sshj.userauth.password.PasswordUtils;
import net.schmizz.sshj.xfer.scp.SCPFileTransfer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
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;
/**
* Secure SHell client API.
*
* 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.
*
* User authentication can be performed by any of the {@code auth*()} method.
*
* {@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.
*
* {@link #newLocalPortForwarder Local} and {@link #getRemotePortForwarder() remote} port forwarding is possible. There
* are also utility method for easily creating {@link #newSCPFileTransfer SCP} and {@link #newSFTPClient() SFTP}
* implementations.
*
* A simple example:
*
*
*
* client = new SSHClient();
* client.initUserKnownHosts();
* client.connect("hostname");
* try
* {
* client.authPassword("username", "password");
* client.startSession().exec("true");
* client.getConnection().join();
* } finally
* {
* client.disconnect();
* }
*
*
* 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 at given {@code hostname:port} and a host key that has
* the given {@code fingerprint}, e.g. {@code "4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21"}
*
* @param host the hostname / IP address
* @param port the port for which the {@code fingerprint} applies
* @param fingerprint expected fingerprint in colon-delimited format (16 octets in hex delimited by a colon)
*
* @see SecurityUtils#getFingerprint
*/
public void addHostKeyVerifier(final String host, final int port, final String fingerprint) {
addHostKeyVerifier(new HostKeyVerifier() {
public boolean verify(String h, int p, PublicKey k) {
return host.equals(h) && port == p && 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.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 methods)
throws UserAuthException, TransportException {
assert isConnected();
auth.authenticate(username, (Service) conn, methods);
}
/**
* Authenticate {@code username} using the {@code "password"} authentication method. 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(String username, char[] password)
throws UserAuthException, TransportException {
authPassword(username, PasswordUtils.createOneOff(password));
}
/**
* Authenticate {@code username} using the {@code "password"} authentication method.
*
* @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));
}
/**
* Authenticate {@code username} using the {@code "password"} authentication method.
*
* @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 "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}.
*
* 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.
*
* {@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 keyProviders)
throws UserAuthException,
TransportException {
final List am = new LinkedList();
for (KeyProvider kp : keyProviders)
am.add(new AuthPublickey(kp));
auth(username, am);
}
/**
* Authenticate {@code username} using the {@code "publickey"} authentication method.
*
* {@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.asList(keyProviders));
}
/**
* Authenticate {@code username} using the {@code "publickey"} authentication method, with keys from one or more
* {@code locations} in the file system.
*
* 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.
*
* 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 keyProviders = new LinkedList();
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.
*
* 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 unencrypted
* 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).
*
*
* @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 encrypted
* 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.
*
* For finer control over which file is used, see {@link #loadKnownHosts(File)}.
*
* @throws IOException if there is an error loading from both 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}.
*
* 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.
*
* 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;
}
/** @return Instantiated {@link SCPFileTransfer} implementation. */
public SCPFileTransfer newSCPFileTransfer() {
assert isConnected() && isAuthenticated();
return new SCPFileTransfer(this);
}
/**
* @return Instantiated {@link SFTPClient} implementation.
*
* @throws IOException if there is an error starting the {@code sftp} subsystem
* @see StatefulSFTPClient
*/
public SFTPClient newSFTPClient()
throws IOException {
assert isConnected() && isAuthenticated();
return new SFTPClient(this);
}
/**
* Does key re-exchange.
*
* @throws TransportException if an error occurs during key exchange
*/
public void rekey()
throws TransportException {
doKex();
}
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.
*
* 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 ClassNotFoundException, 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);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy