找回密码
 立即注册
首页 业界区 业界 WebSSH的简单实现

WebSSH的简单实现

喜及眩 3 小时前
因为web的便利性,很多传统功能都有了web端的实现,WebSSH就是其中之一,我是第一次接触,所以来记录一下使用。
WebSSH支持终端交互,主要可以分为两部分,第一是页面输入命令行并传递给远程终端,第二是展示命令执行结果,这两部分现在都已经有具体实现的库了,所以我们只需要把它们组合起来。
在具体实现之前,需要先准备一个远程终端,我这里用的是VMware创建的虚拟机
1.png

可以在Mac的终端直接登录
2.png

接下来我们就来看代码的实现,前端页面使用三方库xtermjs实现终端界面,远程连接使用nodejs的ssh2模块。
前端实现

我们先来看web端的实现。
前端代码主要做三件事,第一初始化终端对象terminal,第二增加监听事件监听用户的输入,第三建立web socket连接实现实时交互。我这里用react项目做简单的演示。
先在页面上准备一个div,模拟终端背景。
  1. import React, {useEffect, useRef, useState} from 'react';
  2. import { Terminal } from 'xterm';
  3. import 'xterm/css/xterm.css';
  4. const FontSize = 14;
  5. const Col = 80;
  6. const WebTerminal = () => {
  7.   const terminalRef = useRef(null);
  8.   const webTerminal = useRef(null);
  9.   const ws = useRef(null);
  10.   useEffect(() => {
  11.     const ele = terminalRef.current;
  12.     if (ele) {
  13.     }
  14.   }, [terminalRef.current]);
  15.   return ;
  16. };
  17. export default WebTerminal;
复制代码
然后我们对终端进行初始化。
  1. import React, {useEffect, useRef, useState} from 'react';
  2. import { Terminal } from 'xterm';
  3. import 'xterm/css/xterm.css';
  4. const FontSize = 14;
  5. const Col = 80;
  6. const WebTerminal = () => {
  7.   const terminalRef = useRef(null);
  8.   const webTerminal = useRef(null);
  9.   const ws = useRef(null);
  10.   useEffect(() => {
  11.     const ele = terminalRef.current;
  12.     if (ele && !webTerminal.current) {
  13.       const height = ele.clientHeight; // +
  14.       const terminal = new Terminal({ // +
  15.         cursorBlink: true, // +
  16.         cols: Col, // +
  17.         rows: Math.ceil(height / FontSize), // +
  18.       }); // +
  19.                         // +
  20.       terminal.open(ele); // +
  21.       // +
  22.       webTerminal.current = terminal; // +
  23.       
  24.     }
  25.   }, [terminalRef.current]);
  26.   return ;
  27. };
  28. export default WebTerminal;
复制代码
这个时候可以看到页面上出现了一个闪烁的光标,就像input输入框被聚焦时候的状态,cols属性指定了一行可以输入的字符数,rows指定了展示的行数,这里做了一个简单的取整的计算。这个时候还不能输入内容,因为还没加上事件监听,那么现在我们给它加上。
  1. import React, {useEffect, useRef, useState} from 'react';
  2. import { Terminal } from 'xterm';
  3. import 'xterm/css/xterm.css';
  4. const FontSize = 14;
  5. const Col = 80;
  6. const WebTerminal = () => {
  7.   const terminalRef = useRef(null);
  8.   const webTerminal = useRef(null);
  9.   const ws = useRef(null);
  10.   useEffect(() => {
  11.     const ele = terminalRef.current;
  12.     if (ele && !webTerminal.current) {
  13.       const height = ele.clientHeight;
  14.       const terminal = new Terminal({
  15.         cursorBlink: true,
  16.         cols: Col,
  17.         rows: Math.ceil(height / FontSize),
  18.       });
  19.       terminal.open(ele);
  20.       
  21.       webTerminal.current = terminal;
  22.       terminal.onData((val) => { // 键盘输入 // +
  23.         if (val === '\x03') { // +
  24.           // nothig todo // +
  25.         } else { // +
  26.           terminal.write(val); // +
  27.         } // +
  28.       }); // +
  29.     }
  30.   }, [terminalRef.current]);
  31.   return ;
  32. };
  33. export default WebTerminal;
复制代码
这里我们利用terminal对象的onData方法对用户输入进行监听,然后调用terminal的write方法将内容输出到页面上,这里因为x03代表了Ctrl+C的组合键,所以把它做了过滤。这个时候你可能会发现一个问题,就是我们点击回退键删除内容的时候,控制台会出现报错,这是因为编码的问题,onData返回的是utf16/ucs2编码的内容,需要转换为utf8编码,这个后面我们交给node端去处理。
最后我们来实现web socket的实时交互,这块因为需要建立web socket链接,需要web和node一起配合实现。
Webscoket实现

我们先写node端的代码。
  1. const express = require('express');
  2. const app = express();
  3. const expressWs = require('express-ws')(app);
  4. import { createNewServer } from './utils/createNewServer';
  5. app.get('/', function (req: any, res: any, next: any) {
  6.   res.end();
  7. });
  8. app.ws('/', function (ws: any, req: any) {
  9.   createNewServer({
  10.     host: '172.16.215.129',
  11.     username: 'root',
  12.     password: '123456'
  13.   }, ws);
  14. });
  15. app.listen(3001)
复制代码
这里我们使用express-ws模块来实现socket通信,监听3001端口,接下来我们主要看createNewServer方法的实现。
首先引入ssh2模块,构造一个ssh客户端,并与远程主机、也就是我前面创建的虚拟机建立连接。
  1. const SSHClient = require('ssh2').Client;
  2. const utf8 = require('utf8');
  3. const termCols = 80;
  4. const termRows = 30;
  5. export const createNewServer = (machineConfig: any, socket: any) => {
  6.   const ssh = new SSHClient(); // +
  7.   const { host, username, password } = machineConfig; // +
  8.    // +
  9.   ssh.connect({ // +
  10.     port: 22, // +
  11.     host, // +
  12.     username, // +
  13.     password, // +
  14.   }).on('ready', function() { // +
  15.     console.log('ssh连接已建立'); // +
  16.   }) // +
  17. }
复制代码
这里端口22是SSH提供远程连接服务的默认端口,当ssh连接建立成功后,就会打印出”ssh连接已建立“。现在我们到web端去创建web socket连接来查看效果。
  1. import React, {useEffect, useRef, useState} from 'react';
  2. import { Terminal } from 'xterm';
  3. import 'xterm/css/xterm.css';
  4. const FontSize = 14;
  5. const Col = 80;
  6. const WebTerminal = () => {
  7.   const terminalRef = useRef(null);
  8.   const webTerminal = useRef(null);
  9.   const ws = useRef(null);
  10.   useEffect(() => {
  11.     const ele = terminalRef.current;
  12.     if (ele && !webTerminal.current) {
  13.       const height = ele.clientHeight;
  14.       const terminal = new Terminal({
  15.         cursorBlink: true,
  16.         cols: Col,
  17.         rows: Math.ceil(height / FontSize),
  18.       });
  19.       terminal.open(ele);
  20.       
  21.       webTerminal.current = terminal;
  22.       terminal.onData((val) => { // 键盘输入
  23.         if (val === '\x03') {
  24.           // nothig todo
  25.         } else {
  26.           terminal.write(val);
  27.         }
  28.       });
  29.       const socket = new WebSocket(`ws://127.0.0.1:3001`); // +
  30.       socket.onopen = () => { // +
  31.         socket.send('connect success'); // +
  32.       }; // +
  33.       ws.current = socket; // +
  34.     }
  35.   }, [terminalRef.current]);
  36.   return ;
  37. };
  38. export default WebTerminal;
复制代码
这个时候我们去刷新页面,就可以看到nodejs的控制台打印出了”ssh连接已建立“这句话。
与远程主机建立连接后,我们就可以使用ssh2客户端的shell方法,与主机终端开启交互。
  1. const SSHClient = require('ssh2').Client;
  2. const utf8 = require('utf8');
  3. const termCols = 80;
  4. const termRows = 30;
  5. export const createNewServer = (machineConfig: any, socket: any) => {
  6.   const ssh = new SSHClient();
  7.   const { host, username, password } = machineConfig;
  8.   ssh.connect({
  9.     port: 22,
  10.     host,
  11.     username,
  12.     password,
  13.   }).on('ready', function() {
  14.     console.log('ssh连接已建立');
  15.     ssh.shell({ // +
  16.       cols: termCols, // +
  17.       rows: termRows, // +
  18.     }, function(err: any, stream: any) { // +
  19.       if (err) { // +
  20.         return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
  21.       } // +
  22.       console.log('开启交互'); // +
  23.     }); // +
  24.   })
  25. }
复制代码
此时nodejs的控制台就打印出了”开启交互“这句话,stream用于控制终端的输入输出。现在我们需要在web和nodejs两端都加上对消息的监听和发送,这样才能开始真正的交互,我们就接着先写node端的监听和发送。
在node端接收到socket消息后,用on-message对前端传递过来的内容进行编码转换处理,并转换为原始字节流写入终端。
  1. const SSHClient = require('ssh2').Client;
  2. const utf8 = require('utf8');
  3. const termCols = 80;
  4. const termRows = 30;
  5. export const createNewServer = (machineConfig: any, socket: any) => {
  6.   const ssh = new SSHClient();
  7.   const { host, username, password } = machineConfig;
  8.   ssh.connect({
  9.     port: 22,
  10.     host,
  11.     username,
  12.     password,
  13.   }).on('ready', function() {
  14.     console.log('ssh连接已建立');
  15.     ssh.shell({
  16.       cols: termCols,
  17.       rows: termRows,
  18.     }, function(err: any, stream: any) {
  19.       if (err) {
  20.         return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
  21.       }
  22.       console.log('开启交互');
  23.       socket.on('message', function (data: any) { // +
  24.         stream.write(Buffer.from(data, 'utf8')); // +
  25.       }); // +
  26.       // +
  27.       stream.on('data', function (d: Buffer) { // +
  28.         socket.send(d.toString('binary')); // +
  29.       }); // +
  30.     });
  31.   })
  32. }
复制代码
同时监听终端的输出,对前端输入的内容进行处理,并通过socket连接返回给前端。toString binary表示保持原始字节,避免utf8解码异常。
接着我们来完成web端对socket的处理,首先把原来的用户输入显示到屏幕上改为发送socket消息。
  1. terminal.onData((val) => { // 键盘输入
  2.   if (val === '\x03') {
  3.     // nothig todo
  4.   } else {
  5.     socket.send(val); // M
  6.   }
  7. });
复制代码
并增加对socket消息的监听。
  1. socket.onmessage = e => {
  2.   terminal.write(e.data);
  3. };
复制代码
将socket返回的消息输出到页面模拟的终端容器上。
这个时候我们刷新页面看到如下图所示,就表示我们实现了最基本的交互功能。
3.png

这样就可以愉快地和远程终端开始交互了。
4.gif

到这里我们还可以简单优化一下,就是socket连接断开后,如果不退出终端,会在远程主机保留很多进程,我们可以用这个命令ps -aux | grep ssh看到。
5.png

为了避免占用内存,我们可以对socket的关闭进行监听,在socket连接关闭的时候调用end方法来结束ssh连接服务。
  1. socket.onclose = (event: any) => {
  2.   ssh.end();
  3. }
复制代码
这样我们再去查看的时候,就会看到只剩下一个正在运行的ssh服务。
到这里我们就实现了一个简单的WebSSH的交互了。当然这个例子比较简单,类似浏览器窗口尺寸变化对输出显示的影响这里也没有处理,以及node端ssh2模块的其他功能方法也没有涉及到,感兴趣的同学们可以去查阅文档自己尝试一下。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册