源文档地址:基于Express的WebSSH
后端基于基于node
,express
,ssh2
,websocket
前端基于react
,xterm.js
1. 服务端
1.1 下载npm
包
yarn add express-ws ssh2 utf8
1.2 服务端代码
src/utils/createNewServer.ts
const SSHClient = require('ssh2').Client;
const utf8 = require('utf8');
export const createNewServer = (machineConfig: any, socket: any) => {
const ssh = new SSHClient();
const { host, username, password } = machineConfig;
// 连接成功
ssh.on('ready', function () {
socket.send('\r\n*** SSH CONNECTION SUCCESS ***\r\n');
ssh.shell(function (err: any, stream: any) {
// 出错
if (err) {
return socket.send('\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
}
// 前端发送消息
socket.on('message', function (data: any) {
stream.write(data);
});
// 通过sh发送消息给前端
stream.on('data', function (d: any) {
socket.send(utf8.decode(d.toString('binary')));
// 关闭连接
}).on('close', function () {
ssh.end();
});
})
// 关闭连接
}).on('close', function () {
socket.send('\r\n*** SSH CONNECTION CLOSED ***\r\n');
// 连接错误
}).on('error', function (err: any) {
socket.send('\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
// 连接
}).connect({
port: 22,
host,
username,
password
});
}
src/main.ts
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: '192.168.2.94',
username: 'megaium',
password: 'Megaium!'
}, ws)
});
app.listen(3000)
2. 客户端
2.1 客户端代码
*.ts
import React, { useEffect, useState } from 'react';
import { Terminal } from 'xterm';
import { WebLinksAddon } from 'xterm-addon-web-links';
import { FitAddon } from 'xterm-addon-fit';
import 'xterm/css/xterm.css';
import styles from './index.less';
const FontSize: number = 14;
const Col = 80;
const WebTerminal = () => {
const [webTerminal, setWebTerminal] = useState<Terminal | null>(null);
const [ws, setWs] = useState<WebSocket | null>(null);
useEffect(() => {
// 新增监听事件
if (webTerminal && ws) {
// 监听
webTerminal.onKey(e => {
const { key } = e;
ws.send(key);
});
// ws监听
ws.onmessage = e => {
console.log(e);
if (webTerminal) {
if (typeof e.data === 'string') {
webTerminal.write(e.data);
} else {
console.error('格式错误');
}
}
};
}
}, [webTerminal, ws]);
useEffect(() => {
// 初始化终端
const ele = document.getElementById('terminal');
if (ele) {
const height = ele.clientHeight;
// 初始化
const terminal = new Terminal({
cursorBlink: true,
cols: Col,
rows: Math.ceil(height / FontSize),
});
// 辅助
const fitAddon = new FitAddon();
terminal.loadAddon(new WebLinksAddon());
terminal.loadAddon(fitAddon);
terminal.open(ele);
terminal.write('Hello from \x1B[1;3;31mxterm.js\x1B[0m $ ');
fitAddon.fit();
setWebTerminal(terminal);
}
// 初始化ws连接
if (ws) ws.close();
const socket = new WebSocket('ws://192.168.2.123:3000');
socket.onopen = () => {
socket.send('connect success');
};
setWs(socket);
}, []);
return <div id="terminal" className={styles.main} />;
};
export default WebTerminal;