Build a WebSSH Terminal with Spring Boot, WebSocket and xterm.js

This article walks through creating a WebSSH solution from scratch using Spring Boot, WebSocket, JSch and the xterm.js front‑end library, covering dependency setup, backend WebSocket configuration, message handling, SSH session management, and a minimal HTML/JS client to display a browser‑based terminal.

Programmer DD
Programmer DD
Programmer DD
Build a WebSSH Terminal with Spring Boot, WebSocket and xterm.js

Introduction

To meet a project requirement for a web‑based SSH terminal, the author decided to implement a custom WebSSH service instead of using existing Python‑based tools that would add heavy dependencies to the server.

Technology Selection

Because WebSSH needs real‑time data exchange, a long‑lived WebSocket connection is used. The stack chosen is Spring Boot for the backend, JSch for SSH handling, and xterm.js for the front‑end terminal UI.

Dependency Import

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

Simple xterm.js Example

<!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 page renders a shell‑like interface, which serves as the foundation for the full WebSSH implementation.

Backend Implementation

WebSocket Configuration

@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer {
    @Autowired
    WebSSHWebSocketHandler webSSHWebSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(webSSHWebSocketHandler, "/webssh")
                .addInterceptors(new WebSocketInterceptor())
                .setAllowedOrigins("*");
    }
}

Interceptor

public class WebSocketInterceptor implements HandshakeInterceptor {
    @Override
    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
                                   WebSocketHandler wsHandler, Map<String, Object> 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) {}
}

Handler

@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 exception) throws Exception {
        logger.error("Data transport 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; }
}

Service Interface

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);
}

Core Business Logic (excerpt)

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

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

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

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

Front‑End Implementation

HTML Page

<!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="../lib/jquery-3.4.1/jquery-3.4.1.min.js"></script>
    <script src="../js/xterm.js" charset="utf-8"></script>
    <script src="../js/webssh.js" charset="utf-8"></script>
    <script src="../js/base64.js" charset="utf-8"></script>
  </body>
</html>

JavaScript Client

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

openTerminal({
    operate: 'connect',
    host: 'IP_ADDRESS',
    port: 'PORT',
    username: 'USERNAME',
    password: 'PASSWORD'
});

Demo Screenshots

WebSSH terminal screenshot
WebSSH terminal screenshot
Connection successful
Connection successful
Command execution result
Command execution result

Conclusion

The article demonstrates a complete, dependency‑light WebSSH project built entirely with Java and Spring Boot on the backend and xterm.js on the front‑end, and mentions possible extensions such as file upload/download.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

javabackend-developmentSpring BootWebSocketWebSSHxterm.js
Programmer DD
Written by

Programmer DD

A tinkering programmer and author of "Spring Cloud Microservices in Action"

0 followers
Reader feedback

How this landed with the community

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.