Java creates an SSH client, which has been open source ~

Keywords: Java

Source: https://blog.csdn.net/NoCortY...

preface

Recently, due to the project requirements, the project needs to implement the function of a webssh connection terminal. Because I did this type of function for the first time, I first went to GitHub to find out whether there are ready-made wheels that can be used directly. At that time, I saw many projects in this field, such as GateOne, websh, shellinabox, etc.

These projects can well realize the functions of webssh, but they are not adopted in the end. The reason is that most of these bottom layers are written in python and need to rely on many files. This scheme can be used when you use it yourself, which is fast and easy. However, when the project is used by users, you can not require users to include these bottom dependencies in the server, This is obviously unreasonable, so I decided to write a web SSH function myself and open source it as an independent project. (the source address of the project is attached at the end of the text)

Technology selection

Because webssh needs real-time data interaction, it will choose the long connected WebSocket. For the convenience of development, the framework selects SpringBoot. In addition, it also knows the jsch for Java users to connect ssh and xterm.js for realizing the front-end shell page. Therefore, the final technology selection is SpringBoot+Websocket+jsch+xterm.js.

Import dependency

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.7.RELEASE</version>
    <relativePath /> <!-- lookup parent from repository -->
</parent>
<dependencies>
    <!-- Web relevant -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- jsch support -->
    <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 parser -->
    <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 simple xterm case

Because xterm is a popular technology, many students do not have this knowledge support. I learned it temporarily to realize this function, so I'll introduce it to you here.

xterm.js is a WebSocket based container, which can help us implement the command line style at the front end. Just like when we usually connect to the server with SecureCRT or XShell. The following is an introductory case on the official website:

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

In the final test, the page looks like this:You can see that the page has a shell like style, so continue to drill down and implement a webssh.

Backend implementation

As long as xterm only implements the front-end style, it can not really interact with the server. 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

Since WebSockets are required for real-time message push to the front end, students who do not know WebSockets can first learn about them by themselves. There is no more introduction here. We will start to configure WebSockets directly.

/**
* @Description: websocket to configure
* @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 the processor and path, and set cross domain
        webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                .addInterceptors(new WebSocketInterceptor())
                .setAllowedOrigins("*");
    }
}

Implementation of processor and interceptor

Just now, we have completed the configuration of WebSocket and specified a processor and interceptor. So the next step is the implementation of processor and interceptor. Interceptor:

public class WebSocketInterceptor implements HandshakeInterceptor {
    /**
     * @Description: Handler Pre processing call
     * @Param: [serverHttpRequest, serverHttpResponse, webSocketHandler, map]
     * @return: boolean
     * @Author: NoCortY
     * @Date: 2020/3/1
     */
    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler webSocketHandler, Map<String, Object> map) throws Exception {
        if (serverHttpRequest instanceof ServletServerHttpRequest) {
            ServletServerHttpRequest request = (ServletServerHttpRequest) serverHttpRequest;
            //Generate a UUID. Since it is an independent project and there is no user module, you can use a random UUID
            //However, if you want to integrate it into your own project, you need to change it to your own identification
            String uuid = UUID.randomUUID().toString().replace("-","");
            //Put uuid into 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: WebSSH WebSocket processor
* @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:{},connect WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
        //Call initialize connection
        webSSHService.initConnection(webSocketSession);
    }

    /**
     * @Description: Callback 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:{},dispatch orders:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
            //Call service to receive message
            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: Callback with error
     * @Param: [webSocketSession, throwable]
     * @return: void
     * @Author: Object
     * @Date: 2020/3/8
     */
    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
        logger.error("Data transmission error");
    }

    /**
     * @Description: Callback for connection closure
     * @Param: [webSocketSession, closeStatus]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/8
     */
    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
        logger.info("user:{}to break off webssh connect", 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 uses a random UUID, 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 to change it to the user ID used to identify a user in your own project.

Business logic implementation of WebSSH (core)

Just now, we have implemented the configuration of websocket, which is all dead code. After implementing the interface, we can implement it according to our own needs. Now we will implement the main business logic of the back-end. Before implementing this logic, let's think about what effect we want to achieve with WebSSH. Here is a summary:

1. First, we have to connect the terminal (initialize the connection) 2. Secondly, our server needs to process the messages from the front end (receive and process the front end messages) 3. We need to write back the messages returned by the terminal to the front end (data write back front end) 4. Close the connection

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

/**
 * @Description: WebSSH Business logic
 * @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 the customer segment
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void recvHandle(String buffer, WebSocketSession session);

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

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

Now we can implement our defined functions according to this interface. 1. Initialize the connection. Because our bottom layer depends on jsch, we need to use jsch to establish the connection. The so-called initialization connection actually saves the connection information we need in a Map, where no real connection operation is performed. Why not connect directly here? Because the front end is only connected to WebSocket, but we also need the front end to send us the user name and password of the linux terminal. 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. Process 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, we connect the terminal. The second branch: if the client sends the command to operate the 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("Abnormal 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 is a connection request
            //Find the ssh connection object 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("Abnormal information:{}", e.getMessage());
                        close(session);
                    }
                }
            });
        } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
            //If it is 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("Abnormal 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);
        }
}

So far, our whole back-end implementation is over. Due to the limited space, some operations are encapsulated into methods, so we won't show 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. Page implementation
  2. Connect to WebSocket and complete data reception and write back
  3. Data transmission

So we implement 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. We just need to create a div, and then put the terminal instance into the div through xterm.

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

Connect to WebSocket and complete data sending, receiving and writing back

openTerminal( {
    //The content here can be written dead, but when it is integrated into the project, it needs to be passed in through 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, // Currsor blinks 
            cursorStyle: "block", // Cursor style null | 'block' | 'underline' | 'bar'
            scrollback: 800, //RollBACK 
            tabStopWidth: 8, //Tabulation width
            screenKeys: true
        });

        term.on('data', function (data) {
            //Callback function for 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

Connection succeeded

Command operation

ls command:

vim editor:

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. Due to the use of SpringBoot, it is very easy to deploy.

However, we can also extend this project, such as adding upload or download files. Just like Xftp, you can easily drag and drop upload and download files.

Github project open source address: https://github.com/NoCortY/We...

Recent hot article recommendations:

1.1000 + Java interview questions and answers (2021 latest version)

2.Stop playing if/ else on the full screen. Try the strategy mode. It's really fragrant!!

3.what the fuck! What is the new syntax of xx ≠ null in Java?

4.Spring Boot 2.5 heavy release, dark mode is too explosive!

5.Java development manual (Songshan version) is the latest release. Download it quickly!

Feel good, don't forget to like + forward!

Posted by danwguy on Thu, 11 Nov 2021 01:37:24 -0800