Implementation of webssh (web side xshell) based on xterm. JS + expers WS + websocket + SSH2 + typescript

Posted by aniesh82 on Tue, 23 Nov 2021 01:53:18 +0100

To realize the web version of xshell, you need to learn a little more. The following is a brief introduction

Xterm.js

This is an open source framework on the Internet. Its main function is the interface, such as creating a new small black window and setting various styles. The usage is also very simple and specific https://xtermjs.org/

experss

express is an open source framework. Most companies are also using this framework as the Node middle tier or server, which can quickly build a server,

var express = require('express');
var app = express();
app.get('/', function(req, res){
    res.send(' Hello World! ');
});
app.listen(3000);

Access with browser http://localhost:3000 See Hello World! Indicates successful startup
It can set various access paths, request methods, and integrate other open source frameworks

websocket

WebSocket is a network technology for full duplex communication between browser and server provided by HTML5. WebSocket communication protocol was set as standard RFC 6455 by IETF in 2011, and WebSocket API was set as standard by W3C. In the WebSocket API, the browser and server only need to shake hands, and then a fast channel is formed between the browser and the server. The two can directly transmit data to each other.

Once a connection is established (until it is disconnected or an error occurs), the server will always maintain the connection state after shaking hands with the client, which is a persistent connection; I'm familiar with this. I don't say much

ssh2

SSH2 is an open source framework developed specifically for connecting servers. See https://github.com/mscdex/ssh2

Interface

code

Client code

//Call the client component first
<SSHClient host={params.host} username={params.username} password={params.password} port={params.port}/>
//SSHClient 
import React, { useEffect, useState,FunctionComponent } from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';

type Props = {//Pass the four parameters, which are the information of the server you need to connect to.
  host?:string;
  port?:number;
  username?:string;
  password?:string;
}

const WebTerminal :FunctionComponent<Props> = (props) => {
  const {host,port,username,password} = props
  const [webTerminal, setWebTerminal] = useState<Terminal | null>(null);
  const [ws, setWs] = useState<WebSocket | null>(null);

  useEffect(() => {
    // Add listening event
    if (webTerminal && ws) {
      // monitor
      webTerminal.onKey(e => {
        const { key } = e;
        ws.send(key);
      });
      // ws listening
      ws.onmessage = e => {
        console.log(e);
        if (webTerminal) {
          if (typeof e.data === 'string') {
            webTerminal.write(e.data);
          } else {
            console.error('Format error');
          }
        }
      };
    }
  }, [webTerminal, ws]);

  
  useEffect(() => {
    // Initialize terminal
    const ele = document.getElementById('terminal');
    while(ele && ele.hasChildNodes()){ //When there are still child nodes under the table, the loop continues
     //This is to correct the parameter in case of wrong parameter input. Click Connect to create a new window
      ele && ele.firstChild && ele.removeChild(ele.firstChild);
    }
    if (ele) {
      // initialization
      const terminal = new Terminal({
        cursorBlink: true,
        cols: 175,
        rows: 40,
      });
      terminal.focus();
      terminal.onKey(e => {
        // terminal.write(e.key);
        if (e.key== '\r') { 
        //   terminal.write('\nroot\x1b[33m$\x1b[0m');
        } else if (e.key== '\x7F') {
          terminal.write('\b \b');
        } 
      });

      terminal.open(ele);
      terminal.write('Connecting....');
      setWebTerminal(terminal);
    }
    // Initialize ws connection
    if (ws) ws.close();

    const socket = new WebSocket('ws://127.0.0.1:3888');
    socket.onopen = () => {//Establish socket connection with the server
      let message = {
          host:host,
          port:port,
          username:username,
          password:password
      };
      socket.send(JSON.stringify(message));
    };
    setWs(socket);
  }, [host,port,username,password]);

  return <div id="terminal"  />; 
};

export default WebTerminal;
//Just follow the project launch

Server code

const express = require('express');
const app = express();
const expressWs = require('express-ws')(app);

const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');


const createNewServer = (machineConfig, socket) => {
  const ssh = new SSHClient();
  const { host, username, password,port } = machineConfig;
  // Connection succeeded
  ssh.on('ready', function () { //Get ready and establish an ssh connection
    socket.send('\r\nSSH Connection succeeded \r\n');

    ssh.shell(function (err, stream) {
      // error
      if (err) {
        return socket.send('\r\nSSH connection failed: ' + err.message + '\r\n');
      }

      // Front end send message
      socket.on('message', function (data) {
        stream.write(data);
      });

      // Send messages to the front end through sh
      stream.on('data', function (d) {
        socket.send(utf8.decode(d.toString('binary')));

        // Close connection
      }).on('close', function () {
        ssh.end();
      });
    })

    // Close connection
  }).on('close', function () {
    socket.send('\r\nSSH Connection closed \r\n');

    // Connection error
  }).on('error', function (err) {
    socket.send('\r\nSSH connection failed: ' + err.message);

    // connect
  }).connect({
    port,
    host,
    username,
    password
  });
}

const isJSON = (str) => { //Judge whether it is json, otherwise it is easy to report errors, and the service will hang up
  if (typeof str == 'string') {
      try {
          JSON.parse(str);
          return true;
      } catch(e) {
          // console.log(str);
          return false;
      }
  }
  console.log('It is not a string!')    
}
app.ws('/',  (ws, req) => { //Establish connection
  ws.on("message", (data) => { //After establishing the connection, obtain the address and other information sent by the client
    try {
      isJSON(data) && createNewServer({
        port: JSON.parse(data).port,
        host: JSON.parse(data).host,
        username: JSON.parse(data).username,
        password: JSON.parse(data).password
      }, ws)
    } catch(e) {
        console.log(e);
    }
  });
});

app.listen(3888,()=>{
  console.log('3888 port is listening')
})

explain

  1. The server needs to create a new project to start, otherwise it cannot start. After starting, test whether the websocket connection is connected. If it is connected, test whether the ssh server can be connected. The start command of the server is node server.js (that is, JS file name), and the client is started normally
  2. The connection requested by the client here is 127.0.0.1, because I run the client and server on the same computer. If not, I have to change the corresponding one

Topics: TypeScript express