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

可以在Mac的终端直接登录

接下来我们就来看代码的实现,前端页面使用三方库xtermjs实现终端界面,远程连接使用nodejs的ssh2模块。
前端实现
我们先来看web端的实现。
前端代码主要做三件事,第一初始化终端对象terminal,第二增加监听事件监听用户的输入,第三建立web socket连接实现实时交互。我这里用react项目做简单的演示。
先在页面上准备一个div,模拟终端背景。
import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
const FontSize = 14;
const Col = 80;
const WebTerminal = () => {
const terminalRef = useRef(null);
const webTerminal = useRef(null);
const ws = useRef(null);
useEffect(() => {
const ele = terminalRef.current;
if (ele) {
}
}, [terminalRef.current]);
return <div ref={terminalRef} style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};
export default WebTerminal;
然后我们对终端进行初始化。
import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
const FontSize = 14;
const Col = 80;
const WebTerminal = () => {
const terminalRef = useRef(null);
const webTerminal = useRef(null);
const ws = useRef(null);
useEffect(() => {
const ele = terminalRef.current;
if (ele && !webTerminal.current) {
const height = ele.clientHeight; // +
const terminal = new Terminal({ // +
cursorBlink: true, // +
cols: Col, // +
rows: Math.ceil(height / FontSize), // +
}); // +
// +
terminal.open(ele); // +
// +
webTerminal.current = terminal; // +
}
}, [terminalRef.current]);
return <div ref={terminalRef} style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};
export default WebTerminal;
这个时候可以看到页面上出现了一个闪烁的光标,就像input输入框被聚焦时候的状态,cols属性指定了一行可以输入的字符数,rows指定了展示的行数,这里做了一个简单的取整的计算。这个时候还不能输入内容,因为还没加上事件监听,那么现在我们给它加上。
import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
const FontSize = 14;
const Col = 80;
const WebTerminal = () => {
const terminalRef = useRef(null);
const webTerminal = useRef(null);
const ws = useRef(null);
useEffect(() => {
const ele = terminalRef.current;
if (ele && !webTerminal.current) {
const height = ele.clientHeight;
const terminal = new Terminal({
cursorBlink: true,
cols: Col,
rows: Math.ceil(height / FontSize),
});
terminal.open(ele);
webTerminal.current = terminal;
terminal.onData((val) => { // 键盘输入 // +
if (val === '\x03') { // +
// nothig todo // +
} else { // +
terminal.write(val); // +
} // +
}); // +
}
}, [terminalRef.current]);
return <div ref={terminalRef} style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};
export default WebTerminal;
这里我们利用terminal对象的onData方法对用户输入进行监听,然后调用terminal的write方法将内容输出到页面上,这里因为x03代表了Ctrl+C的组合键,所以把它做了过滤。这个时候你可能会发现一个问题,就是我们点击回退键删除内容的时候,控制台会出现报错,这是因为编码的问题,onData返回的是utf16/ucs2编码的内容,需要转换为utf8编码,这个后面我们交给node端去处理。
最后我们来实现web socket的实时交互,这块因为需要建立web socket链接,需要web和node一起配合实现。
Webscoket实现
我们先写node端的代码。
const express = require('express');
const app = express();
const expressWs = require('express-ws')(app);
import { createNewServer } from './utils/createNewServer';
app.get('/', function (req: any, res: any, next: any) {
res.end();
});
app.ws('/', function (ws: any, req: any) {
createNewServer({
host: '172.16.215.129',
username: 'root',
password: '123456'
}, ws);
});
app.listen(3001)
这里我们使用express-ws模块来实现socket通信,监听3001端口,接下来我们主要看createNewServer方法的实现。
首先引入ssh2模块,构造一个ssh客户端,并与远程主机、也就是我前面创建的虚拟机建立连接。
const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');
const termCols = 80;
const termRows = 30;
export const createNewServer = (machineConfig: any, socket: any) => {
const ssh = new SSHClient(); // +
const { host, username, password } = machineConfig; // +
// +
ssh.connect({ // +
port: 22, // +
host, // +
username, // +
password, // +
}).on('ready', function() { // +
console.log('ssh连接已建立'); // +
}) // +
}
这里端口22是SSH提供远程连接服务的默认端口,当ssh连接建立成功后,就会打印出”ssh连接已建立“。现在我们到web端去创建web socket连接来查看效果。
import React, {useEffect, useRef, useState} from 'react';
import { Terminal } from 'xterm';
import 'xterm/css/xterm.css';
const FontSize = 14;
const Col = 80;
const WebTerminal = () => {
const terminalRef = useRef(null);
const webTerminal = useRef(null);
const ws = useRef(null);
useEffect(() => {
const ele = terminalRef.current;
if (ele && !webTerminal.current) {
const height = ele.clientHeight;
const terminal = new Terminal({
cursorBlink: true,
cols: Col,
rows: Math.ceil(height / FontSize),
});
terminal.open(ele);
webTerminal.current = terminal;
terminal.onData((val) => { // 键盘输入
if (val === '\x03') {
// nothig todo
} else {
terminal.write(val);
}
});
const socket = new WebSocket(`ws://127.0.0.1:3001`); // +
socket.onopen = () => { // +
socket.send('connect success'); // +
}; // +
ws.current = socket; // +
}
}, [terminalRef.current]);
return <div ref={terminalRef} style={{ backgroundColor: '#000', width: '100vw', height: '100vh' }}/>;
};
export default WebTerminal;
这个时候我们去刷新页面,就可以看到nodejs的控制台打印出了”ssh连接已建立“这句话。
与远程主机建立连接后,我们就可以使用ssh2客户端的shell方法,与主机终端开启交互。
const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');
const termCols = 80;
const termRows = 30;
export const createNewServer = (machineConfig: any, socket: any) => {
const ssh = new SSHClient();
const { host, username, password } = machineConfig;
ssh.connect({
port: 22,
host,
username,
password,
}).on('ready', function() {
console.log('ssh连接已建立');
ssh.shell({ // +
cols: termCols, // +
rows: termRows, // +
}, function(err: any, stream: any) { // +
if (err) { // +
return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
} // +
console.log('开启交互'); // +
}); // +
})
}
此时nodejs的控制台就打印出了”开启交互“这句话,stream用于控制终端的输入输出。现在我们需要在web和nodejs两端都加上对消息的监听和发送,这样才能开始真正的交互,我们就接着先写node端的监听和发送。
在node端接收到socket消息后,用on-message对前端传递过来的内容进行编码转换处理,并转换为原始字节流写入终端。
const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');
const termCols = 80;
const termRows = 30;
export const createNewServer = (machineConfig: any, socket: any) => {
const ssh = new SSHClient();
const { host, username, password } = machineConfig;
ssh.connect({
port: 22,
host,
username,
password,
}).on('ready', function() {
console.log('ssh连接已建立');
ssh.shell({
cols: termCols,
rows: termRows,
}, function(err: any, stream: any) {
if (err) {
return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
}
console.log('开启交互');
socket.on('message', function (data: any) { // +
stream.write(Buffer.from(data, 'utf8')); // +
}); // +
// +
stream.on('data', function (d: Buffer) { // +
socket.send(d.toString('binary')); // +
}); // +
});
})
}
同时监听终端的输出,对前端输入的内容进行处理,并通过socket连接返回给前端。toString binary表示保持原始字节,避免utf8解码异常。
接着我们来完成web端对socket的处理,首先把原来的用户输入显示到屏幕上改为发送socket消息。
terminal.onData((val) => { // 键盘输入
if (val === '\x03') {
// nothig todo
} else {
socket.send(val); // M
}
});
并增加对socket消息的监听。
socket.onmessage = e => {
terminal.write(e.data);
};
将socket返回的消息输出到页面模拟的终端容器上。
这个时候我们刷新页面看到如下图所示,就表示我们实现了最基本的交互功能。

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

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

为了避免占用内存,我们可以对socket的关闭进行监听,在socket连接关闭的时候调用end方法来结束ssh连接服务。
socket.onclose = (event: any) => {
ssh.end();
}
这样我们再去查看的时候,就会看到只剩下一个正在运行的ssh服务。
到这里我们就实现了一个简单的WebSSH的交互了。当然这个例子比较简单,类似浏览器窗口尺寸变化对输出显示的影响这里也没有处理,以及node端ssh2模块的其他功能方法也没有涉及到,感兴趣的同学们可以去查阅文档自己尝试一下。
WebSSH的简单实现的更多相关文章
- 《HelloGitHub》第 68 期
兴趣是最好的老师,HelloGitHub 让你对编程感兴趣! 简介 HelloGitHub 分享 GitHub 上有趣.入门级的开源项目. https://github.com/521xueweiha ...
- 【webssh】网页上的SSH终端
[webssh] ——记两天来比较痛苦的历程 广义上来说,webssh泛指一种技术可以在网页上实现一个SSH终端.从而无需Xshell之类的模拟终端工具进行SSH连接,将SSH这一比较低层的操作也从C ...
- Django实现WebSSH操作Kubernetes Pod
优秀的系统都是根据反馈逐渐完善出来的 上篇文章介绍了我们为了应对安全和多分支频繁测试的问题而开发了一套Alodi系统,Alodi可以通过一个按钮快速构建一套测试环境,生成一个临时访问地址,详细信息可以 ...
- Django实现WebSSH操作物理机或虚拟机
我想用它替换掉xshell.crt之类的工具 WebSSH操作物理机或虚拟机 Django实现WebSSH操作Kubernetes Pod文章发布后,有小伙伴说咖啡哥,我们现在还没有用上Kuberne ...
- 堡垒机WebSSH进阶之实时监控和强制下线
这个功能我可以不用,但你不能没有 前几篇文章实现了对物理机.虚拟机以及Kubernetes中Pod的WebSSH操作,可以方便的在web端对系统进行管理,同时也支持对所有操作进行全程录像,以方便后续的 ...
- WebSSH画龙点睛之lrzsz上传下载文件
本篇文章没有太多的源码,主要讲一下实现思路和技术原理 当使用Xshell或者SecureCRT终端工具时,我的所有文件传输工作都是通过lrzsz来完成的,主要是因为其简单方便,不需要额外打开sftp之 ...
- Alodi:环境创建从未如此简单
一个满足你各种想象的快速方便生成临时环境的系统 在『Alodi:为了保密我开发了一个系统』文章中有讲到我们开发了一个系统用来快速生成临时测试环境,短短三个月已有数百个环境被创建,简化了工作,节省了时间 ...
- Python Django撸个WebSSH操作Kubernetes Pod
优秀的系统都是根据反馈逐渐完善出来的 上篇文章介绍了我们为了应对安全和多分支频繁测试的问题而开发了一套Alodi系统,Alodi可以通过一个按钮快速构建一套测试环境,生成一个临时访问地址,详细信息可以 ...
- 【造轮子】打造一个简单的万能Excel读写工具
大家工作或者平时是不是经常遇到要读写一些简单格式的Excel? shit!~很蛋疼,因为之前吹牛,就搞了个这东西,还算是挺实用,和大家分享下. 厌烦了每次搞简单类型的Excel读写?不怕~来,喜欢流式 ...
- Fabio 安装和简单使用
Fabio(Go 语言):https://github.com/eBay/fabio Fabio 是一个快速.现代.zero-conf 负载均衡 HTTP(S) 路由器,用于部署 Consul 管理的 ...
随机推荐
- 随机现象之Quanlitative+Quantitative研究: 样本空间的“分割”•随机事件(结果集)的“分布”•样本空间事件域(可测度性, 集合运算封闭性)
数量化: Quantitative: Qulifying the uncertainty of phenomenon: 抽取 现象的集合模型(判定是否随机性.是否可大量重复试验,样本空间及其样本点); ...
- FreeSwitch: esl inbound模式下外呼拨号
相信大家可能接到过一些电话,听上去不象是真人打过来的,比如:通知"您的信用卡到期了",或者"您订的飞机航班取消了,请尽快改签或取消行程",这种就是所谓的&quo ...
- CloudQuery 安全系列(一): Http 与 Https
随着当前大数据时代数据量激增,各种应用互联互通上云后Web安全也成了近几年关注的热点,各种扫描.渗透测试.检测围绕在各个web应用周围,随着云趋势的流行网络层的安全保障就显得格外重要. CloudQu ...
- P4231 三步必杀(差分)
洛谷测试链接:https://www.luogu.com.cn/problem/P4231 P4231 三步必杀 题目背景 (三)旧都 离开狭窄的洞穴,眼前豁然开朗. 天空飘着不寻常的雪花. 一反之前 ...
- redis主从环境搭建
1主从两台: 上传redis-4.0.14.tar.gz包到/usr/local目录解压tar包cd /usr/local/redis-4.0.14make && make insta ...
- 【UEFI】启动项
启动项(Boot Options)可以认为是Boot Loaders,每个平台默认的启动文件如下所示:在ESP目录下一般有 路径 说明 EFI/BOOT/bootx64.efi x86_64 平台默认 ...
- Java核心类——8.BigDecimal
目录 java.math.BigDecimal使用 BigDecimal构造 小数位数(scale) 精度控制与四舍五入 运算规则 比较与相等判断 总结 java.math.BigDecimal使用 ...
- Download:几款主流的全球范围的NDVI产品参数说明和下载
01 快速浏览 ps:大部分产品的网站链接需要魔法进入. 产品名称 时间范围 覆盖区域 时间分辨率 空间分辨率 坐标系 产品源信息 下载源 卫星平台/传感器 MOD13Q1 V6.1 2000/2/1 ...
- linux ls 只显示文件或者文件夹
ls -l 之后会得到下面的内容 drwx------ 4 jinwang users 4096 2012-02-09 15:00 .xchat2 -rw-r--r-- 1 jinwang users ...
- Use 'mysqld --thread_stack=#' to specify a bigger stack.
执行sql语句,报错了: ERROR 1436 (HY000): Thread stack overrun: 6656 bytes used of a 131072 byte stack, and 1 ...