SSHAPI is a clean and modern API for accessing SSH servers in Java. However, it does not supply the support for the protocol itself, or any encryption code, it instead delegates this to an SSH Provider, which in turn uses one of many already available SSH libraries.
try (SshClientclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(SftpClientsftp = client.sftp()) {
sftp.get("/home/myhome/stuff.txt", newFile("stuff.txt"));
}
}It was originally written with two purposes in mind.
- We wanted a way to compare SSH APIs for performance to highlight issues in our own products.
- An abstraction layer for our UniTTY product to work with multiple SSH libraries.
However, it is now a viable complete SSH api with other advantages :-
- Adds a modern API over the top of legacy APIs (e.g. try-with-resource support and non-blocking I/O).
- If a new cipher becomes fashionable, and one provider implements it first, in many cases you can simply switch to it with no code changes.
- If a vulnerability is discovered in one library, switch to another, again just by swapping the provider bridge in use.
- If you have different performance or security requirements based on configuration, you can leave the choice to users
- Different SSH libraries have different licenses, give yourself or your users choices based on these concerns.
- You can compare yourself the performance or behaviour of one API against another.
- Get near native SSH performance using the libssh provider (work in progress).
- If the provider supports non-blocking usage, it will be used, otherwise SSHAPI will simulate non-blocking usage.
- Java. As of version 2.0.0, SSHAPI requires Java 11
- Maven if building from source.
Installation of SSHAPI is no harder than any other SSH library for Java. Using your chosen build tool (below we use Maven), add the appropriate Provider Bridge library to your project, and all of the appropriate dependencies will be pulled in.
<dependency>
<groupId>com.sshtools</groupId>
<artifactId>sshapi-maverick-synergy</artifactId>
<version>2.0.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>Providers include :-
- sshapi-maverick-synergy (Modern, open source JAdaptive Maverick Synergy API)
- sshapi-maverick-synergy-hotfixes (Commercially supported version of Maverick Synergy)
- sshapi-maverick16 (Commercial, Legacy JAdaptive Maverick API)
- sshapi-sshj (Currently well maintained open source SSHJ API)
- sshapi-jsch (Now uses an updated fork of this well established API)
- sshapi-trilead (A fork of Ganymed, itself now apparently unmaintained)
- sshapi-ganymed (Now apparently unmaintained)
- sshapi-libssh (experimental)
- sshapi-openssh (incomplete experimental)
NOTE, if you are using SNAPSHOT versions of the library, you will need to add the Sonatype OSS snapshot repository too.
<repository>
<id>oss-snapshots</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
<releases>
<enabled>false</enabled>
</releases>
<snapshots />
</repository>For full usage, see the examples.
try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varshell = client.shell()) {
varin = shell.getInputStream();
varout = shell.getOutputStream();
// Do something with I/O streams
}
}try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varshell = client.shell("xterm", 80, 24, 0, 0, null)) {
varin = shell.getInputStream();
varout = shell.getOutputStream();
// Do something with I/O streams
}
}try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varsftp = client.sftp()) {
sftp.get("/home/myhome/stuff.txt", newFile("stuff.txt"));
}
}try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varscp = client.scp()) {
scp.put("remote-name", null, newFile("stuff.txt"), false);
}
}There are a few ways of doing this.
All files will be loaded into memory at once.
try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varsftp = client.sftp()) {
for(varfile : sftp.ls("/home/myhome/stuff.txt")) {
System.out.println(file.getName());
}
}
}For iterating over large directories.
try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varsftp = client.sftp()) {
try(varstream = client.directory("/home/myhome")) {
for(varfile : stream) {
System.out.println(file.getName());
} }
}
}Use a FileVisitor to recursively process an entire tree of files.
try (var client = Ssh.open("me@localhost", new ConsolePasswordAuthenticator())) {
try(var sftp = client.sftp()) {
sftp.visit("/home/myhome", new SftpFileVisitor() {
@Override
public FileVisitResult visitFile(SftpFile file, BasicFileAttributes attrs) throws IOException {
System.out.println(file.getName());
return FileVisitResult.CONTINUE;
}
});
}
}
try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varsftp = client.sftp()) {
// Write test filetry (varhandle = sftp.file("test-file.txt", Mode.SFTP_WRITE, Mode.SFTP_CREAT)) {
ByteBufferbuf = ByteBuffer.allocate(16);
buf.putInt(1);
buf.putInt(2);
buf.putInt(3);
buf.putInt(4);
buf.flip();
handle.writeTo(buf);
}
// Read 4 bytes from test file from 4th byte, which is the 2nd 'int' written abovetry (varhandle = sftp.file("test-file.txt", Mode.SFTP_READ)) {
ByteBufferbuf = ByteBuffer.allocate(4);
handle.position(4).readFrom(buf);
if(buf.getInt(0) != 2)
thrownewIllegalStateException("Expected to receive value of 2.");
}
}
}try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varcommand = client.command("ls /etc")) {
Util.joinStreams(command.getOutputStream(), System.out);
System.out.println("Exited with code: " + command.exitCode());
}
}try (varclient = Ssh.open("me@localhost", newConsolePasswordAuthenticator())) {
try(varcommand = client.localForward("0.0.0.0", 8443, "someremoteaddress", 443)) {
// Simply sleep to keep the tunnel open. Thread.sleep(600000);
}
}// Prompt for location of private keyvarpemFile = newFile(Util.prompt("Private key file",
System.getProperty("user.home") + File.separator + ".ssh" + File.separator + "id_rsa"));
try (varclient = Ssh.open("me@localhost", newDefaultPublicKeyAuthenticator(newConsolePasswordAuthenticator(), pemFile))) {
// Do SSH stuff
}varconfig = newSshConfiguration();
config.addRequiredCapability(Capability.SFTP);
config.setPreferredServerToClientMAC("hmac-sha1");
config.setHostKeyValidator(newConsoleHostKeyValidator());
config.setBannerHandler(newConsoleBannerHandler());
// Create the client using that configuration and connect and authenticatetry (varclient = config.open("me@localhost", newConsolePasswordAuthenticator()) {
// Do SSH stuff
}// List all of the providers and allow the user to select onevarproviders = DefaultProviderFactory.getAllProviders();
System.out.println("Providers :-");
for (inti = 0; i < providers.length; i++) {
System.out.println(" " + (i + 1) + ": " + providers[i].getClass().getName());
}
varprovider = providers[Integer.parseInt(Util.prompt("\nEnter the number for the provider you wish to use (1-"
+ providers.length + ")")) - 1];
// Create a client using that providertry (varclient = provider.open(newSshConfiguration(), "me@localhost", newConsolePasswordAuthenticator()) {
// Do SSH stuff
}// Need a provider that does IDENTITY_MANAGEMENTvarconfig = newSshConfiguration();
config.addRequiredCapability(Capability.IDENTITY_MANAGEMENT);
// Create the provider, then identity manager using that configurationvarprovider = DefaultProviderFactory.getInstance().getProvider(config);
System.out.println("Got provider " + provider.getClass());
varmgr = provider.createIdentityManager(config);
// Read and (optionally) decrypt key SshPrivateKeyFilepk;
varkeyFile = newFile("/home/myhome/.ssh/id_rsa");
try(varin = newFileInputStream(keyFile)) {
pk = mgr.createPrivateKeyFromStream(in); if (pk.isEncrypted()) {
varpw = Util.prompt("Old passphrase");
pk.decrypt(pw.toCharArray());
}
}
// Change passphrasepk.changePassphrase(newpw.toCharArray());
// Write the key back outtry(varfout = newFileOutputStream(keyFile)) {
fout.write(pk.getFormattedKey());
}
varconfig = newSshConfiguration();
config.addRequiredCapability(Capability.TUNNELED_SOCKET_FACTORY);
// Connect, authenticatetry (varclient = config.open("me@localhost", newConsolePasswordAuthenticator())) {
varsf = client.createTunneledSocketFactory();
/* * Make a connection back to the SSH server we are connecting from and read the * first line of output. This could be any host that is accessible from the * remote SSH server, localhost:22 is just used as we know something will be * running there! */try(varsocket = sf.createSocket("localhost", 22)) {
varreader = newBufferedReader(newInputStreamReader(socket.getInputStream()));
System.out.println("SSH ident: " + reader.readLine());
}
}varconfig = newSshConfiguration();
config.addRequiredCapability(Capability.AGENT);
try (varclient = config.open("me@localhost", newDefaultAgentAuthenticator())) {
// Do SSH stuff
}SSHAPI can use a non-blocking pattern if you prefer that. For most methods, there will be an equivalent Later method. For example shell() has a shellLater(). These methods will not block, nor will they throw an exception. Instead, a Future is returned, that may be used to retrieve the shell object when it is ready, or cancel the operation. If an exception occurs during the operation, the future will also return that.
varfuture = Ssh.openLater("me@localhost", newConsolePasswordAuthenticator())
// At some point after this you can attempt to retrieve the SshClient instance from the future. This will block until it's available. You can request a timeout toovarclient = future.get(10, TimeUnit.SECONDS);
// Shells act in the same wayvarshellFuture = client.shellLater();
varshell = shellFuture.get(); // To handle data coming from the remote server, set the input handlershell.setInput((buffer) -> {
// Do something with ByteBuffer (buffer is pre-flipped to limit() will be length of data, position() will be zero
});
// To send data to the shell simple use writeLater(). You use the future to // wait for when this actually happens.varwriteFuture = shell.writeLater(ByteBuffer.wrap("Test!".getBytes()));
writeFuture.get();
// Now you can close, and of course, wait for the close to complete if you wantvarcloseShellFuture = shell.closeLater();
closeShellFuture.get();
// And finally close the client toovarcloseFuture = client.closeLater();
closeFuture.get();