Executing commands on a remote machine from Java with JSch

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

JSch example execution

JSch example execution.

Software Requirements and Conventions Used

Software Requirements and Linux Command Line Conventions
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.

WARNING
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.



Creating new project

Creating new project.

We’ll choose the “Java” category, and the “Java Application” project.

Choosing category for the project

Choosing category for the project.

We need to provide a name for the project, in this case “sshRemoteExample”.

Naming the project

Naming the project.

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.

Adding a JAR as a library

Adding a JAR as a library.

After the selection, the archive should appear in the included libraries, if we open the “Libraries” node.

JSch imported successfully

JSch imported successfully.

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…”.

Adding new Java class to the package

Adding new Java class to the package.

We’ll provide the name “sshRemoteExampleUserinfo” as class name.

Naming the new Java class

Naming the new Java class.

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.



Comments and Discussions
Linux Forum