Teach you to use pure java to realize a web version of xshell (with source code)

Time:2020-6-25

I collated Java advanced materials for free, including Java, redis, mongodb, mysql, zookeeper, spring cloud, Dubbo high concurrency distributed and other tutorials, a total of 30g, which needs to be collected by myself.
Transfer gate:https://mp.weixin.qq.com/s/osB-BOl6W-ZLTSttTkqMPQ

preface

Recently, due to the needs of the project, the project needs to implement a websssh connection terminal function. Since I first did this type of function, I first went to GitHub to find out if there is any ready-made wheel to use directly. At that time, I saw many projects in this area, such as gateone, webssh, shellinabox, etc., which can well realize the functions of webssh.

But it was not adopted in the end. The reason is that most of the underlying layers are written by python, which needs to rely on many files. This scheme can be used for its own use, which is fast and easy. However, when the project is used by users, users cannot always be required to include these underlying dependencies in the server, which is obviously unreasonable. So I decided to write a websssh by myself And open source as an independent project.

Open source address of GitHub project: https://github.com/NoCortY/WebSSH

Technology selection

Because webssh needs real-time data interaction, it will choose websocket with long connection. For the convenience of development, the framework chooses spring boot. In addition, it also knows the jsch of Java users connecting SSH and the implementation of front-end shell page xterm.js .

Therefore, the final technology selection is springboot + websocket + jsch+ xterm.js 。

Import dependency

org.springframework.boot
    spring-boot-starter-parent
    2.1.7.RELEASE
     


    
    
        org.springframework.boot
        spring-boot-starter-web
    
    
    
        com.jcraft
        jsch
        0.1.54
    
    
    
        org.springframework.boot
        spring-boot-starter-websocket
    
    
    
        commons-io
        commons-io
        1.4
    
    
        commons-fileupload
        commons-fileupload
        1.3.1

A simple case of xterm

Because xterm is a popular technology, many students do not have the knowledge support in this area. I also learned it temporarily to realize this function, so I will introduce it to you here.

xterm.js Is a websocket based container, which can help us to implement the command line style in the front end. It’s like when we usually connect to the server with SecureCRT or xshell.

Here is the introduction case on the official website:

var term = new Terminal();
      term.open(document.getElementById('terminal'));
      term.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ')

In the final test, the page looks like this:

 

Getting started with xterm

You can see that the page has appeared similar to the shell style, so continue to deepen according to this, and implement a webssh.

Back end implementation

As long as xterm only implements the front-end style, it can’t really realize the interaction with the server, and the interaction with the server is mainly controlled by our Java back-end, so we start from the back-end and use jsch + websocket to realize this part.

Websocket configuration

Because websocket is needed for real-time message push to the front end, students who don’t know about websocket can go to learn about it by themselves first. This is just a little more. Let’s start to configure websocket directly.

/**
  *Description: websocket configuration
  * @Author: NoCortY
  * @Date: 2020/3/8
  */
  @Configuration
  @EnableWebSocket
  public class WebSSHWebSocketConfig implements WebSocketConfigurer{
      @Autowired
      WebSSHWebSocketHandler webSSHWebSocketHandler;
      @Override
      public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
          //Socket channel
          //Specify processor and path, and set cross domain
          webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                  .addInterceptors(new WebSocketInterceptor())
                  .setAllowedOrigins("*");
      }
  }

Implementation of handler and interceptor

Just now we have finished the configuration of websocket and specified a processor and interceptor. So the next step is the implementation of the processor and interceptor.

Interceptor:

public class WebSocketInterceptor implements HandshakeInterceptor {
      /**
       * @Description: Handler before processing.
       * @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
       * @return: boolean
       * @Author: NoCortY
       * @Date: 2020/3/1
       */
      @Override
      public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map map) throws Exception {
          if (serverHttpRequest instanceof ServletServerHttpRequest) {
              ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
              //Generate a UUID. Because it is an independent project and there is no user module, random UUIDs can be used
              //But if you want to integrate into your own project, you need to change it to your own user identification
              String uuid = UUID.randomUUID().toString().replace("-","");
              //Put UUID in websocketsession
              map.put(ConstantPool.USER_UUID_KEY, uuid);
              return true;
          } else {
              return false;
          }
      }

      @Override
      public void afterHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Exception e) {

      }
  }

Processor:

/**
  *Description: websocket processor of websssh
  * @Author: NoCortY
  * @Date: 2020/3/8
  */
  @Component
  public class WebSSHWebSocketHandler implements WebSocketHandler{
      @Autowired
      private WebSSHService webSSHService;
      private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);

      /**
       *@ Description: callback of websocket on user connection
       * @Param: [webSocketSession]
       * @return: void
       * @Author: Object
       * @Date: 2020/3/8
       */
      @Override
      public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
          logger.info ("user: {}, connecting to websssh", webSocketSession.getAttributes ().get( ConstantPool.USER_ UUID_ KEY));
          //Call initialize connection
          webSSHService.initConnection(webSocketSession);
      }

      /**
       *@ Description: callback of received message
       * @Param: [webSocketSession, webSocketMessage]
       * @return: void
       * @Author: NoCortY
       * @Date: 2020/3/8
       */
      @Override
      public void handleMessage(WebSocketSession webSocketSession, WebSocketMessage> webSocketMessage) throws Exception {
          if (webSocketMessage instanceof TextMessage) {
              logger.info ("user: {}, send command: {}", webSocketSession.getAttributes ().get( ConstantPool.USER_ UUID_ KEY),  webSocketMessage.toString ());
              //Call service to receive messages
              webSSHService.recvHandle(((TextMessage) webSocketMessage).getPayload(), webSocketSession);
          } else if (webSocketMessage instanceof BinaryMessage) {

          } else if (webSocketMessage instanceof PongMessage) {

          } else {
              System.out.println("Unexpected WebSocket message type: " + webSocketMessage);
          }
      }

      /**
       *@ Description: wrong callback occurred
       * @Param: [webSocketSession, throwable]
       * @return: void
       * @Author: Object
       * @Date: 2020/3/8
       */
      @Override
      public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
          logger.error ("data transfer error");
      }

      /**
       *@ Description: callback for connection closing
       * @Param: [webSocketSession, closeStatus]
       * @return: void
       * @Author: NoCortY
       * @Date: 2020/3/8
       */
      @Override
      public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
          logger.info ("user: {} disconnects webssh", String.valueOf ( webSocketSession.getAttributes ().get( ConstantPool.USER_ UUID_ KEY)));
          //Call service to close the connection
          webSSHService.close(webSocketSession);
      }

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

It should be noted that the user ID I added to the interceptor is a random UUID. This is because as an independent websocket project, there is no user module. If you need to integrate this project into your own project, you need to modify this part of the code and change it to the user ID used to identify a user in your own project.

Business logic implementation of websssh (core)

Just now, we have realized the configuration of websocket, which is all dead code. After realizing the interface, we can realize it according to our own needs. Now, we will implement the main business logic of the back-end. Before realizing this logic, let’s think about what effect WebSSH mainly wants to present.

Here I make a summary:

1. First we have to connect the terminal (initial connection)

2. Secondly, our server needs to process messages from the front end (receive and process front-end messages)

3. We need to write back the messages returned by the terminal to the front end (data write back to the front end)

4. Close the connection

According to these four requirements, we first define an interface to make the requirements clear.

/**
   *@ Description: business logic of websssh
   * @Author: NoCortY
   * @Date: 2020/3/7
   */
  public interface WebSSHService {
      /**
       *@ Description: initialize SSH connection
       * @Param:
       * @return:
       * @Author: NoCortY
       * @Date: 2020/3/7
       */
      public void initConnection(WebSocketSession session);

      /**
       *@ Description: process the data sent by customers
       * @Param:
       * @return:
       * @Author: NoCortY
       * @Date: 2020/3/7
       */
      public void recvHandle(String buffer, WebSocketSession session);

      /**
       *@ Description: write data back to the front end for websocket
       * @Param:
       * @return:
       * @Author: NoCortY
       * @Date: 2020/3/7
       */
      public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;

      /**
       *@ Description: close the connection
       * @Param:
       * @return:
       * @Author: NoCortY
       * @Date: 2020/3/7
       */
      public void close(WebSocketSession session);
  }

Now we can implement the functions we defined according to this interface.

1. Initialize connection

Because our underlying layer relies on jsch implementation, we need to use jsch to establish the connection here. The so-called initial connection is actually to save the connection information we need in a map, and there is no real connection operation here. Why not connect directly here? Because the front-end is only connected to websocket, but we need to send us the user name and password of Linux terminal from the front-end. Without this information, we can’t connect.

public void initConnection(WebSocketSession session) {
             JSch jSch = new JSch();
             SSHConnectInfo sshConnectInfo = new SSHConnectInfo();
             sshConnectInfo.setjSch(jSch);
             sshConnectInfo.setWebSocketSession(session);
             String uuid = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
             //Put the SSH connection information into the map
             sshMap.put(uuid, sshConnectInfo);
     }

2. Processing the data sent by the client

In this step, we will be divided into two branches.

  • The first branch: if the client sends the user name, password and other information of the terminal, then we connect the terminal.
  • The second branch: if the client sends the command of the operating terminal, we will directly forward it to the terminal and obtain the execution result of the terminal.

Specific code implementation:

public void recvHandle(String buffer, WebSocketSession session) {
             ObjectMapper objectMapper = new ObjectMapper();
             WebSSHData webSSHData = null;
             try {
                 //Convert JSON sent by the front end
                 webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
             } catch (IOException e) {
                 logger.error ("JSON conversion exception");
                 logger.error ("exception information: {}", e.getmessage());
                 return;
             }
         //Get the random UUID just set
             String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
             if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
                 //If it's a connection request
                 //Find the SSH connection object you just stored
                 SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                 //Start thread asynchronous processing
                 WebSSHData finalWebSSHData = webSSHData;
                 executorService.execute(new Runnable() {
                     @Override
                     public void run() {
                         try {
                             //Connect to terminal
                             connectToSSH(sshConnectInfo, finalWebSSHData, session);
                         } catch (JSchException | IOException e) {
                             logger.error ("webssh connection exception");
                             logger.error ("exception information: {}", e.getmessage());
                             close(session);
                         }
                     }
                 });
             } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
                 //If it's a request to send a command
                 String command = webSSHData.getCommand();
                 SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                 if (sshConnectInfo != null) {
                     try {
                         //Send command to terminal
                         transToSSH(sshConnectInfo.getChannel(), command);
                     } catch (IOException e) {
                         logger.error ("webssh connection exception");
                         logger.error ("exception information: {}", e.getmessage());
                         close(session);
                     }
                 }
             } else {
                 logger.error ("unsupported operation");
                 close(session);
             }
     }

3. Data is sent to the front end through websocket

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

4. Close the connection

public void close(WebSocketSession session) {
         //Get randomly generated UUID
             String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
             SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
             if (sshConnectInfo != null) {
                 //Disconnect
                 if (sshConnectInfo.getChannel() != null) sshConnectInfo.getChannel().disconnect();
                 //Remove the SSH connection information from the map
                 sshMap.remove(userId);
             }
     }

 

At this point, our whole back-end implementation is over. Due to the limited space, some operations are encapsulated as methods here, so we won’t show them too much. Let’s focus on the idea of logical implementation. Next we will implement the front end.

Front end implementation

The front-end work is mainly divided into the following steps:

  1. Implementation of page
  2. Connect websocket, receive and write back data
  3. Sending of data

So let’s do it step by step.

Page implementation

The implementation of the page is very simple. We just need to display the large black screen of the terminal on the whole screen, so we don’t need to write any style, just create a div, and then put the terminal instance into the div through xterm.

WebSSH

Connect websocket and complete data sending, receiving and updating

openTerminal( {
      //The content here can be written to death, but to be integrated into the project, it needs to be passed in by parameters, and a terminal can be dynamically connected.
          operate:'connect',
          Host: 'IP address',
          Port: 'port number',
          Username: 'user name',
          Password: 'password'
      });
      function openTerminal(options){
          var client = new WSSHClient();
          var term = new Terminal({
              cols: 97,
              rows: 37,
              Cursorblink: true, // cursor blinks
              Cursorstyle: "block", // cursor style null '|'block' |'underline '|'bar'
              Scrollback: 800, // rollback
              Tabstopwidth: 8, // tab width
              screenKeys: true
          });

          term.on('data', function (data) {
              //Callback function in keyboard input
              client.sendClientData(data);
          });
          term.open(document.getElementById('terminal'));
          //Show connections on page
          term.write('Connecting...');
          //Perform connection operation
          client.connect({
              onError: function (error) {
                  //Connection failure callback
                  term.write('Error: ' + error + '\r\n');
              },
              onConnect: function () {
                  //Connection success callback
                  client.sendInitData(options);
              },
              onClose: function () {
                  //Connection close callback
                  term.write("\rconnection closed");
              },
              onData: function (data) {
                  //Callback when data is received
                  term.write(data);
              }
          });
      }

Effect display

connect

 

connect

Connection successful

 

Connection successful

Command operation

Ls command:

 

Ls command

VIM editor:

 

VIM editor

Top command:

 

Top command

epilogue

In this way, we have completed the implementation of a webssh project without relying on any other components. The back-end is completely implemented in Java. Because of the use of spring boot, it is very easy to deploy.

However, we can also extend this project, such as adding upload or download files, just like xftp, which can easily drag and drop upload and download files.