Goodbye, xShell, I use Java to make a web version, and the netizens call: 666

Follow the official account backstage to reply to pay or mall to obtain actual project information + video

Preface

Recently, due to project requirements, the project needs to implement a WebSSH connection terminal function. Since I am doing this type of function for the first time, I first went to GitHub to find out if there are any ready-made wheels that can be used directly. I saw a lot of this at the time. Some projects, such as: GateOne, webssh, shellinabox, etc., these projects can achieve the function of webssh very well, but they were not adopted in the end. The reason is that most of the underlying layers are written in python and need to rely on many files and use them by themselves Sometimes you can use this solution, which is quick and easy, but when it is used by the user in the project, the user cannot always be required to include these underlying dependencies in the server. This is obviously not reasonable, so I decided to write a WebSSH function by myself. , And open source as an independent project.

github project open source address: https://github.com/NoCortY/WebSSH

Technology selection

Because webssh needs real-time data interaction, it will choose the long-connected WebSocket. For the convenience of development, the framework chooses SpringBoot. In addition, I also understand the jsch for Java users to connect to ssh and xterm.js for the front-end shell page.

Therefore, the final technology selection is SpringBoot+Websocket+jsch+xterm.js .

Import dependencies

<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相关 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- jsch支持 -->
    <dependency>
        <groupId>com.jcraft</groupId>
        <artifactId>jsch</artifactId>
        <version>0.1.54</version>
    </dependency>
    <!-- WebSocket 支持 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-websocket</artifactId>
    </dependency>
    <!-- 文件上传解析器 -->
    <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>
1234567891011121314151617181920212223242526272829303132333435

A simple xterm case

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

xterm.js is a WebSocket-based container, which can help us implement the command line style on the front end. Just like when we usually use SecureCRT or XShell to connect to the server.

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

In the final test, the page looks like this:

You can see that the page has already appeared similar to the shell style, then continue to go deeper and implement a webssh based on this.

Backend implementation

As long as xterm only implements the front-end style, it cannot 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 achieve this part of the content.

WebSocket configuration

Since the real-time push of messages to the front-end requires the use of WebSocket, students who do not understand WebSocket can learn about it by themselves. I won't introduce too much here. Let's start the configuration of WebSocket directly.

/**
* @Description: websocket配置
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Configuration
@EnableWebSocket
public class WebSSHWebSocketConfig implements WebSocketConfigurer{
    @Autowired
    WebSSHWebSocketHandler webSSHWebSocketHandler;
    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) {
        //socket通道
        //指定处理器和路径,并设置跨域
        webSocketHandlerRegistry.addHandler(webSSHWebSocketHandler, "/webssh")
                .addInterceptors(new WebSocketInterceptor())
                .setAllowedOrigins("*");
    }
}
12345678910111213141516171819

Implementation of Handler 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 the processor and interceptor.

Interceptor :

public class WebSocketInterceptor implements HandshakeInterceptor {
    /**
     * @Description: Handler处理前调用
     * @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;
            //生成一个UUID,这里由于是独立的项目,没有用户模块,所以可以用随机的UUID
            //但是如果要集成到自己的项目中,需要将其改为自己识别用户的标识
            String uuid = UUID.randomUUID().toString().replace("-","");
            //将uuid放到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) {

    }
}
12345678910111213141516171819202122232425262728

Processor :

/**
* @Description: WebSSH的WebSocket处理器
* @Author: NoCortY
* @Date: 2020/3/8
*/
@Component
public class WebSSHWebSocketHandler implements WebSocketHandler{
    @Autowired
    private WebSSHService webSSHService;
    private Logger logger = LoggerFactory.getLogger(WebSSHWebSocketHandler.class);

    /**
     * @Description: 用户连接上WebSocket的回调
     * @Param: [webSocketSession]
     * @return: void
     * @Author: Object
     * @Date: 2020/3/8
     */
    @Override
    public void afterConnectionEstablished(WebSocketSession webSocketSession) throws Exception {
        logger.info("用户:{},连接WebSSH", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY));
        //调用初始化连接
        webSSHService.initConnection(webSocketSession);
    }

    /**
     * @Description: 收到消息的回调
     * @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("用户:{},发送命令:{}", webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY), webSocketMessage.toString());
            //调用service接收消息
            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: 出现错误的回调
     * @Param: [webSocketSession, throwable]
     * @return: void
     * @Author: Object
     * @Date: 2020/3/8
     */
    @Override
    public void handleTransportError(WebSocketSession webSocketSession, Throwable throwable) throws Exception {
        logger.error("数据传输错误");
    }

    /**
     * @Description: 连接关闭的回调
     * @Param: [webSocketSession, closeStatus]
     * @return: void
     * @Author: NoCortY
     * @Date: 2020/3/8
     */
    @Override
    public void afterConnectionClosed(WebSocketSession webSocketSession, CloseStatus closeStatus) throws Exception {
        logger.info("用户:{}断开webssh连接", String.valueOf(webSocketSession.getAttributes().get(ConstantPool.USER_UUID_KEY)));
        //调用service关闭连接
        webSSHService.close(webSocketSession);
    }

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

It should be noted that the user ID I added in the interceptor uses 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, change it to the user ID used to identify a user in your own project.

WebSSH business logic implementation (core)

Just now we have implemented the configuration of websocket. It 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 it. WebSSH , We mainly want to present an effect.

I made a summary here:

1. First we have to connect to the terminal (initialize the 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 message 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, so that the requirements can be clear.

/**
 * @Description: WebSSH的业务逻辑
 * @Author: NoCortY
 * @Date: 2020/3/7
 */
public interface WebSSHService {
    /**
     * @Description: 初始化ssh连接
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void initConnection(WebSocketSession session);

    /**
     * @Description: 处理客户段发的数据
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void recvHandle(String buffer, WebSocketSession session);

    /**
     * @Description: 数据写回前端 for websocket
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void sendMessage(WebSocketSession session, byte[] buffer) throws IOException;

    /**
     * @Description: 关闭连接
     * @Param:
     * @return:
     * @Author: NoCortY
     * @Date: 2020/3/7
     */
    public void close(WebSocketSession session);
}
123456789101112131415161718192021222324252627282930313233343536373839404142

Now we can implement the functions we defined based on this interface.

Initialize the connection

Since our bottom layer is implemented by jsch, we need to use jsch to establish a connection here. The so-called initial connection is actually to save the connection information we need in a Map, where no real connection operation is performed. Why not connect directly here? Because the front end here is only connected to the WebSocket, but we also need the front end to send us the username and password of the Linux terminal. Without this information, we cannot 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));
             //将这个ssh连接信息放入map中
             sshMap.put(uuid, sshConnectInfo);
     }
     123456789
     ```

  2. 处理客户端发送的数据

     在这一步骤中,我们会分为两个分支。

     第一个分支:如果客户端发来的是终端的用户名和密码等信息,那么我们进行终端的连接。

     第二个分支:如果客户端发来的是操作终端的命令,那么我们就直接转发到终端并且获取终端的执行结果。

     具体代码实现:

     ```java
     public void recvHandle(String buffer, WebSocketSession session) {
             ObjectMapper objectMapper = new ObjectMapper();
             WebSSHData webSSHData = null;
             try {
                 //转换前端发送的JSON
                 webSSHData = objectMapper.readValue(buffer, WebSSHData.class);
             } catch (IOException e) {
                 logger.error("Json转换异常");
                 logger.error("异常信息:{}", e.getMessage());
                 return;
             }
         //获取刚才设置的随机的uuid
             String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
             if (ConstantPool.WEBSSH_OPERATE_CONNECT.equals(webSSHData.getOperate())) {
                 //如果是连接请求
                 //找到刚才存储的ssh连接对象
                 SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                 //启动线程异步处理
                 WebSSHData finalWebSSHData = webSSHData;
                 executorService.execute(new Runnable() {
                     @Override
                     public void run() {
                         try {
                             //连接到终端
                             connectToSSH(sshConnectInfo, finalWebSSHData, session);
                         } catch (JSchException | IOException e) {
                             logger.error("webssh连接异常");
                             logger.error("异常信息:{}", e.getMessage());
                             close(session);
                         }
                     }
                 });
             } else if (ConstantPool.WEBSSH_OPERATE_COMMAND.equals(webSSHData.getOperate())) {
                 //如果是发送命令的请求
                 String command = webSSHData.getCommand();
                 SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
                 if (sshConnectInfo != null) {
                     try {
                         //发送命令到终端
                         transToSSH(sshConnectInfo.getChannel(), command);
                     } catch (IOException e) {
                         logger.error("webssh连接异常");
                         logger.error("异常信息:{}", e.getMessage());
                         close(session);
                     }
                 }
             } else {
                 logger.error("不支持的操作");
                 close(session);
             }
     }
     123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051
     ```

  3. 数据通过websocket发送到前端

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

  4. 关闭连接

     ```java
     public void close(WebSocketSession session) {
         //获取随机生成的uuid
             String userId = String.valueOf(session.getAttributes().get(ConstantPool.USER_UUID_KEY));
             SSHConnectInfo sshConnectInfo = (SSHConnectInfo) sshMap.get(userId);
             if (sshConnectInfo != null) {
                 //断开连接
                 if (sshConnectInfo.getChannel() != null) sshConnectInfo.getChannel().disconnect();
                 //map中移除该ssh连接信息
                 sshMap.remove(userId);
             }
     }
     1234567891011
     ```

  至此,我们的整个后端实现就结束了,由于篇幅有限,这里将一些操作封装成了方法,就不做过多展示了,重点讲逻辑实现的思路吧。接下来我们将进行前端的实现。

## 前端实现

前端工作主要分为这么几个步骤:

1. 页面的实现
2. 连接WebSocket并完成数据的接收并回写
3. 数据的发送

所以我们一步一步来实现它。

- ### 页面实现

  页面的实现很简单,我们只不过需要在一整个屏幕上都显示终端那种大黑屏幕,所以我们并不用写什么样式,只需要创建一个div,之后将terminal实例通过xterm放到这个div中,就可以实现了。

  ```html
  <!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>
  123456789101112131415

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

openTerminal( {
    //这里的内容可以写死,但是要整合到项目中时,需要通过参数的方式传入,可以动态连接某个终端。
        operate:'connect',
        host: 'ip地址',
        port: '端口号',
        username: '用户名',
        password: '密码'
    });
    function openTerminal(options){
        var client = new WSSHClient();
        var term = new Terminal({
            cols: 97,
            rows: 37,
            cursorBlink: true, // 光标闪烁
            cursorStyle: "block", // 光标样式  null | 'block' | 'underline' | 'bar'
            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 + '\r\n');
            },
            onConnect: function () {
                //连接成功回调
                client.sendInitData(options);
            },
            onClose: function () {
                //连接关闭回调
                term.write("\rconnection closed");
            },
            onData: function (data) {
                //收到数据时回调
                term.write(data);
            }
        });
    }
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647

Show results

connection

connection succeeded

Command operation

ls command:

vim editor:

top command:

Conclusion

In this way, we have completed the implementation of a webssh project, without relying on any other components, and the backend is completely implemented in Java. Because of the use of SpringBoot, it is very easy to deploy.

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

After this project, I will continue to update, and the above functions will be implemented slowly, Github: https://github.com/NoCortY/WebSSH

Author: ObjectSpace

Source: https://sourl.cn/apXiEy

Goodbye, xShell, I use Java to make a web version, and the netizens call: 666




有热门推荐????
2020 国内互联网公司的薪酬排名,加班时长排名 !IDEA这样 配置注释模板,让你高出一个逼格!!
Java后端线上问题排查常用命令收藏SpringBoot+Prometheus+Grafana实现应用监控和报警10个解放双手实用在线工具,有些代码真的不用手写!微信小程序练手实战:前端+后端(Java)
又一神操作,SpringBoot2.x 集成百度 uidgenerator搞定全局ID为什么要在2021年放弃Jenkins?我已经对他失去耐心了...Docker + FastDFS + Spring Boot 一键式搭建分布式文件服务器

点击阅读原文,前往学习SpringCloud实战项目

Guess you like

Origin blog.csdn.net/qq_17231297/article/details/114986499