Backend Development 17 min read

Implementing a WebSSH Terminal with SpringBoot, WebSocket, JSch, and xterm.js

This article demonstrates how to build a WebSSH terminal from scratch using SpringBoot for the backend, WebSocket for real‑time communication, JSch for SSH connections, and xterm.js for the front‑end terminal UI, including dependency setup, server‑side logic, and client‑side integration.

Java Captain
Java Captain
Java Captain
Implementing a WebSSH Terminal with SpringBoot, WebSocket, JSch, and xterm.js

Due to a project requirement for a web‑based SSH terminal, the author decided to implement a custom WebSSH solution instead of using existing Python‑based tools, and open‑sourced the project.

The chosen technology stack consists of SpringBoot for the backend, WebSocket for real‑time data exchange, JSch for SSH handling, and xterm.js for rendering a terminal UI in the browser.

Required Maven dependencies are added to the pom.xml :

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
</parent>
<dependencies>
    <!-- Web related -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- JSch for SSH -->
    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.54</version>
    </dependency>
    <!-- WebSocket support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- File upload utilities -->
    <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>1.4</version>
    </dependency>
    <dependency>
        <groupId>commons-fileupload</groupId>
        <artifactId>commons-fileupload</artifactId>
        <version>1.3.1</version>
    </dependency>
</dependencies>

A minimal xterm.js example shows how to embed the terminal component:

<!doctype html>
<html>
  <head>
    <link rel="stylesheet" href="node_modules/xterm/css/xterm.css" />
    <script src="node_modules/xterm/lib/xterm.js"></script>
  </head>
  <body>
    <div id="terminal"></div>
    <script>
      var term = new Terminal();
      term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');
    </script>
  </body>
</html>

The backend implementation starts with a WebSocket configuration class that registers a handler and enables cross‑origin requests:

@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer {
    @Autowired
    WebSSHWebSocketHandler webSSHWebSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSSHWebSocketHandler, "/webssh")
                .addInterceptors(new WebSocketInterceptor())
                .setAllowedOrigins("*");
    }
}

The WebSocketInterceptor generates a random UUID for each session and stores it in the WebSocket attributes:

public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                 WebSocketHandler wsHandler, Map
attributes) throws Exception {
        if (request instanceof ServletServerHttpRequest) {
            String uuid = UUID.randomUUID().toString().replace("-", "");
            attributes.put(ConstantPool.USER_UUID_KEY, uuid);
            return true;
        }
        return false;
    }
    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
                               WebSocketHandler wsHandler, Exception ex) {}
}

The core handler WebSSHWebSocketHandler manages connection lifecycle events and forwards messages to the WebSSHService :

@Component
public class WebSSHWebSocketHandler implements WebSocketHandler {
    @Autowired
    private WebSSHService webSSHService;
    private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        logger.info("User:{}, connected to WebSSH", session.getAttributes().get(ConstantPool.USER_UUID_KEY));
        webSSHService.initConnection(session);
    }

    @Override
    public void handleMessage(WebSocketSession session, WebSocketMessage
message) throws Exception {
        if (message instanceof TextMessage) {
            logger.info("User:{}, sent command:{}", session.getAttributes().get(ConstantPool.USER_UUID_KEY), message.toString());
            webSSHService.recvHandle(((TextMessage) message).getPayload(), session);
        } else {
            System.out.println("Unexpected WebSocket message type: " + message);
        }
    }

    @Override
    public void handleTransportError(WebSocketSession session, Throwable ex) throws Exception {
        logger.error("Data transmission error");
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
        logger.info("User:{} disconnected", session.getAttributes().get(ConstantPool.USER_UUID_KEY));
        webSSHService.close(session);
    }

    @Override
    public boolean supportsPartialMessages() { return false; }
}

The service interface defines the essential operations for a WebSSH session:

public interface WebSSHService {
    void initConnection(WebSocketSession session);
    void recvHandle(String buffer, WebSocketSession session);
    void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;
    void close(WebSocketSession session);
}

Connection initialization stores a JSch instance and the WebSocket session in a map keyed by the generated UUID:

public void initConnection(WebSocketSession session) {
    JSch jSch = new JSch();
    SSHConnectInfo info = new SSHConnectInfo();
    info.setjSch(jSch);
    info.setWebSocketSession(session);
    String uuid = (String) session.getAttributes().get(ConstantPool.USER_UUID_KEY);
    sshMap.put(uuid, info);
}

The recvHandle method parses incoming JSON, distinguishes between a connection request and a command execution request, and either starts an asynchronous SSH connection or forwards the command to the remote shell:

public void recvHandle(String buffer, WebSocketSession session) {
    ObjectMapper mapper = new ObjectMapper();
    WebSSHData data;
    try { data = mapper.readValue(buffer, WebSSHData.class); }
    catch (IOException e) { logger.error("JSON conversion error", e); return; }
    String userId = (String) session.getAttributes().get(ConstantPool.USER_UUID_KEY);
    if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(data.getOperate())) {
        SSHConnectInfo info = sshMap.get(userId);
        executorService.execute(() -> {
            try { connectToSSH(info, data, session); }
            catch (Exception e) { logger.error("WebSSH connection error", e); close(session); }
        });
    } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(data.getOperate())) {
        SSHConnectInfo info = sshMap.get(userId);
        if (info != null) {
            try { transToSSH(info.getChannel(), data.getCommand()); }
            catch (IOException e) { logger.error("WebSSH command error", e); close(session); }
        }
    } else {
        logger.error("Unsupported operation");
        close(session);
    }
}

Sending data back to the browser is a thin wrapper around WebSocketSession.sendMessage :

public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException {
    session.sendMessage(new TextMessage(buffer));
}

Closing a session removes the SSH connection from the map and disconnects the channel if it exists:

public void close(WebSocketSession session) {
    String userId = (String) session.getAttributes().get(ConstantPool.USER_UUID_KEY);
    SSHConnectInfo info = sshMap.get(userId);
    if (info != null) {
        if (info.getChannel() != null) info.getChannel().disconnect();
        sshMap.remove(userId);
    }
}

On the front‑end, a simple HTML page loads the xterm.css and JavaScript files and provides a div element for the terminal:

<!doctype html>
<html>
<head>
    <title>WebSSH</title>
    <link rel="stylesheet" href="../css/xterm.css" />
</head>
<body>
    <div id="terminal" style="width:100%;height:100%"></div>
    <script src="../js/xterm.js" charset="utf-8"></script>
    <script src="../js/webssh.js" charset="utf-8"></script>
</body>
</html>

The JavaScript client creates a WSSHClient , instantiates an xterm Terminal , and wires up data flow between the terminal and the WebSocket endpoint:

function openTerminal(options) {
    var client = new WSSHClient();
    var term = new Terminal({cols:97, rows:37, cursorBlink:true, cursorStyle:"block", scrollback:800, tabStopWidth:8});
    term.on('data', function(data) { client.sendClientData(data); });
    term.open(document.getElementById('terminal'));
    term.write('Connecting...');
    client.connect({
        onError: function(err) { term.write('Error: ' + err + '\r\n'); },
        onConnect: function() { client.sendInitData(options); },
        onClose: function() { term.write('\rconnection closed'); },
        onData: function(data) { term.write(data); }
    });
}

Running the project displays a full‑screen terminal in the browser; screenshots in the original article show successful connection, command execution (e.g., ls , vim , top ), and a responsive UI.

In conclusion, the article provides a complete, dependency‑free WebSSH solution built with SpringBoot, WebSocket, JSch, and xterm.js, and suggests possible extensions such as file upload/download capabilities.

frontendjavaWebSocketSpringBootSSHWebSSHxterm.js
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.