SSH is an every-day tool of any Linux System Administration job. It is an easy and secure way to access remote machines on the network, transfer data and execute remote commands. Apart from interactive mode, there are many tools exist that enable automation of remote tasks that also rely on the existing ssh
server/client architecture. For one such tool, you can read about ansible on Ubuntu for example. You can also find many implementations of the ssh client, but what about accessing the abilities ssh provides from code?
JSch is a project that implements the ssh protocol in Java. With it’s help, you can build applications that are capable to connect to and interact with a remote or local SSH Server. This way your application is capable of managing any aspect of the target machine that you could complete with your native ssh client, which gives yet another powerful addition to the already vast Java toolset.
In this article we will import JSch into our Java project, and develop the minimal necessary code pieces to create an application that can log in to a remote machine’s ssh server, execute some commands in the remote interactive shell, closes the session, then presents the output. This application will be minimal, however, it may give a hint of the power it provides.
In this tutorial you will learn:
- How to import JSch into your Java project
- How to setup the test environment
- How to implement the UserInfo interface in a custom class
- How to write an application that initiates interactive ssh session
Software Requirements and Conventions Used
Category | Requirements, Conventions or Software Version Used |
---|---|
System | Fedora 30 |
Software | OpenJDK 1.8, JSch 0.1.55, NetBeans 8.2 |
Other | Privileged access to your Linux system as root or via the sudo command. |
Conventions |
# – requires given linux commands to be executed with root privileges either directly as a root user or by use of sudo command$ – requires given linux commands to be executed as a regular non-privileged user |
Introduction
With the help of JSch, we’ll develop an application that will attempt to log in to localhost
via ssh
, using the username test
and password test
. We will assume the default port 22
the ssh server listens on, and will accept the server’s fingerprint without checking it’s validity. On successful login, we’ll execute a few commands we could issue in a remote shell, log out, then print all the output received.
The following source code is for demonstration purposes only; never use such code in production! Just to name two pitfalls, do not trust any server fingerprints by default, and handle exceptions correctly.
Our tools will consist of a Fedora desktop (both as client and server), a recent NetBeans IDE, and the (at the time of writing) latest stable JSch. Note however, that these are only the tools of choice. Java is platform-independent, and the target server could be on the other side of the planet, and could be any operating system that runs a proper ssh server
.
Setting up the test environment
We’ll need the above credentials to work on localhost
. In our example that means we need a user named “test”, with the password “test”. We’ll also need a running ssh server.
Adding the test user
We’ll execute useradd
as root
:
# useradd test
And set the new user’s password:
# passwd test
Here we need to provide the above password twice. This is suitable in a testing environment that is temporary and also unreachable from the outside world, but do not use easily guessed passwords when there may be a slightest chance of uncontrolled access.
Checking the ssh server
We can check the status of the ssh server
with systemd
:
# systemctl status sshd
And start it if it is not running:
# systemctl start sshd
This step may be necessary on desktop installations, as some of these setups does not run the ssh server by default.
Testing connectivity with native client
If our user is set and the service is running, we should be able to log in using the above information:
$ ssh test@localhost
We’ll need to accept the host’s fingerprint and provide the password. If we get to the shell, our test environment is completed.
Obtaining and importing JSch to our project
Downloading the archive
We’ll need to download the byte code of the JSch project in order to use it’s functionality. You can find the appropriate link on JSch home page. We’ll need the .jar
Java archive.
Creating the project in NetBeans
At the beginning, we create a new, empty project called sshRemoteExample
in NetBeans. We can simply choose “New Project” from the File menu.
We’ll choose the “Java” category, and the “Java Application” project.
We need to provide a name for the project, in this case “sshRemoteExample”.
On the default layout, we can find the “Projects” window on the left. There we’ll right-click on “Libraries” node under our newly created project, and select “Add JAR/Folder”. A file chooser window will open, where we need to browse for the .jar
file we downloaded from the developer’s site.
After the selection, the archive should appear in the included libraries, if we open the “Libraries” node.
We’ll need to implement the UserInfo
interface in order to use it in our application. To do so, we’ll need to add a new java class
to our project by right-clicking on our sshremoteexample
package in the project window, choose “New”, then “Java Class…”.
We’ll provide the name “sshRemoteExampleUserinfo” as class name.
Adding the source code
sshRemoteExampleUserinfo.java
For our interface implementation, consider the following source. This is where we accept the target’s fingerprint blindly. Do not do this in a real world scenario. You can edit the source code by clicking on the class in the project window, or if it is open already,switch to it with the tabs at the top of the source code window.
package sshremoteexample;
import com.jcraft.jsch.*;
public class sshRemoteExampleUserInfo implements UserInfo {
private final String pwd;
public sshRemoteExampleUserInfo (String userName, String password) {
pwd = password;
}
@Override
public String getPassphrase() {
throw new UnsupportedOperationException("getPassphrase Not supported yet.");
}
@Override
public String getPassword() {
return pwd;
}
@Override
public boolean promptPassword(String string) {
/*mod*/
return true;
}
@Override
public boolean promptPassphrase(String string) {
throw new UnsupportedOperationException("promptPassphrase Not supported yet.");
}
@Override
public boolean promptYesNo(String string) {
/*mod*/
return true;
}
@Override
public void showMessage (String string) {
}
}
SshRemoteExample.java
Our main class will be the sshRemoteExample
class with the following source:
package sshremoteexample;
import com.jcraft.jsch.*;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
public class SshRemoteExample {
public static void main(String[] args) {
String host = "localhost";
String user = "test";
String password = "test";
String command = "hostname\ndf -h\nexit\n";
try {
JSch jsch = new JSch();
Session session = jsch.getSession(user,host, 22);
session.setUserInfo(new sshRemoteExampleUserInfo(user, password));
session.connect();
Channel channel = session.openChannel("shell");
channel.setInputStream(new ByteArrayInputStream(command.getBytes(StandardCharsets.UTF_8)));
channel.setOutputStream(System.out);
InputStream in = channel.getInputStream();
StringBuilder outBuff = new StringBuilder();
int exitStatus = -1;
channel.connect();
while (true) {
for (int c; ((c = in.read()) >= 0);) {
outBuff.append((char) c);
}
if (channel.isClosed()) {
if (in.available() > 0) continue;
exitStatus = channel.getExitStatus();
break;
}
}
channel.disconnect();
session.disconnect();
// print the buffer's contents
System.out.print (outBuff.toString());
// print exit status
System.out.print ("Exit status of the execution: " + exitStatus);
if ( exitStatus == 0 ) {
System.out.print (" (OK)\n");
} else {
System.out.print (" (NOK)\n");
}
} catch (IOException | JSchException ioEx) {
System.err.println(ioEx.toString());
}
}
}
Note that in this example we hard-code every detail needed for the connection: target hostname, username/password, and the command string to be executed in the remote session. This is hardly a real life example, but it serves it’s demonstration purpose.
We could change the target and credentials to execute the command on a remote host. Also note that the remote session will have the privileges of the user that logs in. I would not advise to use a user with high privileges – such as root
– for testing, if the target machine does contain valuable data or services.
Running the application
We can run our application directly from the IDE by clicking on “Run project (sshRemoteExample)” in the “Run” menu, which will provide the output in the output window below the source code. We can also choose “Clean and build project (sshRemoteExample)” from the same menu, in which case the IDE will produce the .jar
Java archive the can be executed without the IDE.
The output provided will show the path to the archive, similar to the following (exact path may vary depending on your IDE settings):
To run this application from the command line without Ant, try: java -jar "/var/projects/sshRemoteExample/dist/sshRemoteExample.jar"
As it can be guessed, we can run our built application from the command line, and if all goes well, it will provide an output similar to the following.
$ java -jar "/var/projects/sshShellExample/dist/sshShellExample.jar" Last login: Mon Jul 29 14:27:08 2019 from 127.0.0.1 hostname df -h exit [test@test1 ~]$ hostname test1.linuxconfig.org [test@test1 ~]$ df -h Filesystem Size Used Avail Use% Mounted on devtmpfs 3,9G 0 3,9G 0% /dev tmpfs 3,9G 127M 3,8G 4% /dev/shm tmpfs 3,9G 1,7M 3,9G 1% /run tmpfs 3,9G 0 3,9G 0% /sys/fs/cgroup /dev/mapper/fedora_localhost--live-root 49G 15G 32G 32% / tmpfs 3,9G 6,1M 3,9G 1% /tmp /dev/sdb1 275G 121G 140G 47% /mnt/hdd_open /dev/sda2 976M 198M 711M 22% /boot /dev/mapper/fedora_localhost--live-home 60G 50G 6,9G 88% /home /dev/sda1 200M 18M 182M 9% /boot/efi tmpfs 789M 9,7M 779M 2% /run/user/1000 tmpfs 789M 0 789M 0% /run/user/1001 [test@test1 ~]$ exit logout Exit status of the execution: 0 (OK)
Note that your output will likely differ, if nothing else, in the hostname, volume names and sizes – but in general, you should see a complete df -h
output that you would get in an ssh session.
Final thoughts
This simple example meant to show the power of the JSch project, if in a somewhat oversimplified manner. With access to the test machine and a proper client, the following simple command would provide the same information:
$ ssh test@localhost "hostname; df -h"
And would also not create an interactive session. The same functionality is provided by JSch if you open the channel in command mode:
Channel channel = session.openChannel("command");
This way you don’t need to handle closing the session with the exit
shell command.
The true power of this project lies in the ability to connect to and interact with the remote machine trough native shell commands, process the output, and decide the next action programmatically. Imagine a multi-threaded application that manages possibly hundreds of servers by itself, and you’ll get the picture.