可远程连接系统终端或开启SSH登录的路由器和交换机。 相关资料: xtermjs/xterm.js: A terminal for the web (github)admin管理员组文章数量:1122855
后端实现(NestJS):
1、安装依赖: npm install node-ssh @nestjs/websockets @nestjs/platform-socket.io 2、我们将创建一个名为 RemoteControlModule 的 NestJS 模块,该模块将包括 SSH 服务、WebSocket 网关和必要的配置,运行cli: nest g module remoteControl nest g service remoteControl nest g gateway remoteControl --no-spec 3、remote-control.module.ts 模块import { Module } from '@nestjs/common';
import { RemoteControlService } from './remote-control.service';
import { RemoteControlGateway } from './remote-control.gateway';
@Module({
providers: [RemoteControlService, RemoteControlGateway],
})
export class RemoteControlModule {}
4、remote-control.service.ts SSH服务:
import { Injectable } from '@nestjs/common'; // 导入 NestJS 的 Injectable 装饰器,用于定义依赖注入的服务
import { NodeSSH } from 'node-ssh'; // 导入 node-ssh 库,用于实现 SSH 连接和命令执行
import { Duplex } from 'stream'; // 引入 Duplex 类型
@Injectable() // 使用 @Injectable 装饰器标记该类为可注入的服务
export class RemoteControlService {
// 定义一个私有属性 sshSessions,它是一个 Map 类型的集合,用于存储 SSH 会话信息。
// Map 的键是一个字符串,代表客户端的唯一标识符。
// Map 的值是一个对象,包含以下属性:
// ssh: NodeSSH 类型,表示一个 SSH 连接实例,用于执行 SSH 命令。
// currentDirectory: 字符串类型,表示当前工作目录的路径。
// homeDirectory: 字符串类型,表示用户的家目录路径。
// shellStream: 可选的 Duplex 流类型,表示一个双向流,用于与 SSH 会话进行交互。
private sshSessions = new Map<
string,
{
ssh: NodeSSH;
currentDirectory: string;
homeDirectory: string;
shellStream?: Duplex;
}
>();
// 定义一个设备类型的字段,用于区分不同类型的设备
private deviceType = 'linux'; // 默认设备类型为 Linux
// 初始化会话
initializeSession(clientId: string) {
try {
// 检查是否已存在会话,避免重复初始化
if (this.sshSessions.has(clientId)) {
console.log(`会话已存在: ${clientId}`);
return;
}
// 创建新的会话状态
this.sshSessions.set(clientId, {
ssh: new NodeSSH(), // 创建一个新的 NodeSSH 实例
currentDirectory: '/', // 默认当前目录为根目录
homeDirectory: '/', // 默认家目录为根目录
shellStream: undefined, // 初始时没有 shellStream
});
console.log(`会话初始化完成: ${clientId}`);
} catch (error) {
console.log('初始化会话时发生错误:', error);
}
}
// 定义一个异步方法 startSSHSession,用于启动一个 SSH 会话
async startSSHSession(
host: string, // 主机地址
username: string, // 用户名
password: string, // 密码
clientId: string, // 客户端标识符
type: string, // 接收设备类型参数
): Promise<string> {
// 设置设备类型
this.deviceType = type;
// 检查会话是否已经初始化
const session = this.sshSessions.get(clientId);
if (!session) {
// 断开连接
this.disconnect(clientId);
return '会话未初始化, 请先初始化会话';
}
try {
// 连接到 SSH 服务器
await session.ssh.connect({
host, // 服务器地址
username, // 用户名
password, // 密码
port: 3122, // 指定端口为3122
// 需要了解服务器支持哪些密钥交换算法。这可以通过使用 SSH 命令行工具(如 ssh)与 -Q kex 选项来查看,或者联系你的服务器管理员获取这些信息。
algorithms: {
kex: [
'ecdh-sha2-nistp256',
'ecdh-sha2-nistp384',
'ecdh-sha2-nistp521',
'diffie-hellman-group-exchange-sha256',
'diffie-hellman-group14-sha1',
'diffie-hellman-group1-sha1', // 这是一个较旧的算法,安全性较低,最后尝试
],
cipher: [
'aes128-ctr',
'aes192-ctr',
'aes256-ctr',
'aes128-gcm@openssh',
'aes256-gcm@openssh',
'aes128-cbc',
'aes192-cbc',
'aes256-cbc', // CBC 模式的算法安全性较低,建议谨慎使用
],
hmac: [
'hmac-sha2-256',
'hmac-sha2-512',
'hmac-sha1', // SHA1 的 HMAC 也是较旧的选择
],
},
});
// 请求一个 shell 流,可以指定终端类型。终端类型决定了终端的行为和功能,比如字符编码、颜色支持等。
// 常见的终端类型包括 'xterm', 'vt100', 'ansi' 等。
// 'xterm' 是最常用的终端类型,支持颜色和鼠标事件。
// 'vt100' 是较旧的终端类型,功能较为基础。
// 'ansi' 也是一种常见的终端类型,支持 ANSI 转义序列。
const shellStream = await session.ssh.requestShell({
term: 'xterm',
});
// 更新会话信息
session.shellStream = shellStream; // shell 流
// 如果是 Linux 终端
if (this.deviceType == 'linux') {
// 执行命令获取用户的家目录,并去除两端的空白字符
const homeDirectory = (
await session.ssh.execCommand('echo $HOME')
).stdout.trim();
session.currentDirectory = homeDirectory; // 当前目录设置为家目录
session.homeDirectory = homeDirectory; // 家目录
} else {
// 如果是路由器或交换机,执行其他初始化设置
// 例如:session.currentDirectory = '/';
}
// 返回一个字符串,表示 SSH 会话已经启动
return `SSH 会话成功启动, 主机: ${host}, 用户: ${username}`;
} catch (error) {
// 如果设备类型是路由器交换机,发送退出命令
if (this.deviceType === 'device') {
this.sendExitCommands(clientId);
} else {
this.disconnect(clientId);
}
return 'SSH 会话启动失败,失败原因:' + error.message;
}
}
/**
* 根据客户端标识符获取对应的 SSH 会话的 shell 流。
* 如果会话不存在或 shell 流未初始化,则返回 undefined。
* @param clientId 客户端标识符
* @returns 返回对应的 Duplex 流,如果会话不存在或 shell 流未初始化则返回 undefined。
*/
getShellStream(clientId: string): Duplex | undefined {
try {
const session = this.sshSessions.get(clientId);
if (!session) {
console.error(`未找到客户端ID为 ${clientId} 的会话`);
return undefined;
}
if (!session.shellStream) {
console.error(`客户端ID为 ${clientId} 的会话中未初始化 shell 流`);
return undefined;
}
return session.shellStream;
} catch (error) {
console.log('获取 shell 流时发生错误:', error);
return undefined;
}
}
async sendExitCommands(clientId: string): Promise<string> {
const session = this.sshSessions.get(clientId);
if (!session) {
return '会话不存在';
}
if (!session.ssh.isConnected()) {
return 'SSH连接已关闭';
}
if (session.shellStream && session.shellStream.writable) {
try {
// 监听错误事件
session.shellStream.on('error', (error) => {
console.error('Shell流错误:', error);
this.cleanupSession(clientId);
});
// 监听关闭事件
session.shellStream.on('close', () => {
console.log('Shell 流已关闭');
// 移除所有监听器
session.shellStream.removeAllListeners();
this.cleanupSession(clientId);
});
session.shellStream.on('data', (data) => {
console.log('从路由器接收到的数据:', data.toString());
});
console.log('-----发送退出命令-----');
// 发送退出命令
// await session.ssh.execCommand('\x1A'); // Ctrl+Z
// await session.ssh.execCommand('quit');
session.shellStream.write('\x1A');
// 等待一段时间以确保命令被处理,执行quit命令会导致Shell流关闭,从而触发 close 事件
session.shellStream.write('quit\n');
// 确保命令发送完成
await new Promise((resolve) => setTimeout(resolve, 500));
session.shellStream.end(); // 关闭写入流
console.log('-----退出命令已发送-----');
return '退出命令已发送';
} catch (error) {
console.error('设备执行退出命令时发生错误:', error);
return '设备执行退出命令时发生错误';
}
} else {
// 如果Shell流不可写或不存在,则清理会话
this.cleanupSession(clientId);
return 'Shell流不可写或不存在';
}
}
// 只释放ssh连接,不释放shell流
async cleanupSession(clientId: string): Promise<void> {
const session = this.sshSessions.get(clientId);
if (session && session.ssh.isConnected()) {
try {
session.ssh.dispose();
} catch (error) {
console.error('释放SSH连接时发生错误:', error);
}
}
this.sshSessions.delete(clientId);
console.log('cleanupSession SSH会话和资源已成功释放');
}
// 释放ssh连接和shell流
async disconnect(clientId: string): Promise<string> {
console.log(`设备断开: ${clientId}`);
const session = this.sshSessions.get(clientId);
if (!session) {
return '会话不存在';
}
try {
// 关闭 shell 流并清除监听器
if (session.shellStream) {
//监听流结束事件
session.shellStream.end();
session.shellStream.removeAllListeners();
}
// 释放 SSH 连接
if (session.ssh.isConnected()) {
try {
session.ssh.dispose();
} catch (disposeError) {
console.error('释放 SSH 连接时发生错误:', disposeError);
}
}
// 从映射中删除会话
this.sshSessions.delete(clientId);
console.log('disconnect SSH会话和资源已成功释放');
return 'SSH会话和资源已成功释放';
} catch (error) {
console.error('断开连接时发生错误:', error);
return '断开连接时发生错误';
}
}
}
5、remote-control.gateway.ts WebSocket网关:
import {
WebSocketGateway, // 导入WebSocketGateway装饰器,用于定义WebSocket网关
WebSocketServer, // 导入WebSocketServer装饰器,用于注入WebSocket服务器实例
SubscribeMessage, // 导入SubscribeMessage装饰器,用于定义处理WebSocket消息的方法
ConnectedSocket, // 导入ConnectedSocket装饰器,用于获取连接的WebSocket客户端
MessageBody,
} from '@nestjs/websockets'; // 从NestJS的WebSocket模块中导入装饰器
import { Socket, Server } from 'socket.io'; // 导入Socket.IO的Socket类型
// 导入远程控制服务
import { RemoteControlService } from './remote-control.service';
// 使用WebSocketGateway装饰器定义一个WebSocket网关,监听8113端口
@WebSocketGateway(8113, {
// 允许跨域
cors: {
origin: '*', // 允许所有来源
},
// 定义命名空间
namespace: 'control', // 默认是 /,如果设置成 /control,那么客户端连接的时候,就需要使用 ws://localhost:8113/control 这种形式
})
export class RemoteControlGateway {
// 注入WebSocket服务器实例,需要向所有客户端广播消息时使用
@WebSocketServer() server: Server;
// 定义一个设备类型的字段,用于区分不同类型的设备
private deviceType = 'linux'; // 默认设备类型为 Linux
// 构造函数,注入远程控制服务
constructor(private remoteControlService: RemoteControlService) {}
// 当客户端连接时触发
handleConnection(client: Socket) {
console.log(`客户端接入: ${client.id}`);
// 初始化 SSH 会话
this.remoteControlService.initializeSession(client.id);
}
// 当客户端断开连接时触发
handleDisconnect(client: Socket) {
console.log(`客户端断开: ${client.id}`);
}
// 处理启动终端会话的请求,传入主机地址、用户名和密码、设备类型
@SubscribeMessage('startTerminalSession')
async handleStartTerminalSession(
@MessageBody()
data: { host: string; username: string; password: string; type: string },
@ConnectedSocket() client: Socket,
) {
const clientId = client.id;
this.deviceType = data.type;
try {
// 启动 SSH 会话
const message = await this.remoteControlService.startSSHSession(
data.host,
data.username,
data.password,
clientId,
data.type, // 传递设备类型到服务层
);
// 获取 SSH 会话的 shell 流
const shellStream = this.remoteControlService.getShellStream(clientId);
if (shellStream) {
// 监听 shell 流的 data 事件,当主机SSH会话有输出时触发
shellStream.on('data', (data: Buffer) => {
// 确保发送的是字符串格式的数据
client.emit('terminalData', data.toString('utf8'));
});
}
// 发送启动终端会话的成功消息
client.emit('terminalSessionStarted', { message, clientId });
} catch (error) {
// 发送启动终端会话的失败消息
client.emit('terminalSessionError', error.message);
// 如果设备类型是路由器交换机,发送退出命令
if (this.deviceType === 'device') {
await this.remoteControlService.sendExitCommands(clientId);
} else {
// 执行断开设备连接
this.remoteControlService.disconnect(clientId);
}
}
}
// 处理终端输入,传入客户端ID和输入的命令
@SubscribeMessage('input')
async handleInput(
@MessageBody() data: { clientId: string; input: string },
@ConnectedSocket() client: Socket,
) {
if (client.disconnected) {
console.log(`客户端 ${client.id} 已断开连接,停止处理输入`);
return;
}
try {
// 根据客户端 ID 获取 SSH 会话的 shell 流
const shellStream = this.remoteControlService.getShellStream(
data.clientId,
);
// 如果 shell 流存在且可写,将输入写入 shell 流
if (shellStream && shellStream.writable) {
// 检查输入是否为退格键,并发送正确的字符
shellStream.write(data.input);
} else {
// 如果 shell 流不存在或不可写,发送错误消息
client.emit(
'terminalSessionError',
'Shell终端不可用,请检查是否已启动终端会话.',
);
// 如果设备类型是路由器交换机,发送退出命令
if (this.deviceType === 'device') {
await this.remoteControlService.sendExitCommands(data.clientId);
} else {
// 执行断开设备连接
this.remoteControlService.disconnect(data.clientId);
}
}
} catch (error) {
console.log('处理终端输入时发生错误:', error);
}
}
// 处理断开终端连接的请求,传入客户端ID
@SubscribeMessage('disconnectTerminal')
async handleDisconnect1(
@MessageBody() clientId: string,
@ConnectedSocket() client: Socket,
) {
console.log('进入 sendExitCommands 断开设备的方法:', clientId);
// 如果设备类型是路由器交换机,发送退出命令
if (this.deviceType == 'device') {
client.emit('terminalDisconnected', '设备终端已断开');
const message = await this.remoteControlService.sendExitCommands(
clientId,
);
console.log('执行 sendExitCommands 设备命令之后:', message);
} else {
client.emit('terminalDisconnected', '系统终端已断开');
// 执行断开设备连接
this.remoteControlService.disconnect(clientId);
}
}
}
6、main.ts 未捕获异常进行捕获(如果连接的是路由器终端就需要这个配置):
import { NestFactory} from '@nestjs/core';
import { AppModule } from './app.module';
import { NestExpressApplication } from '@nestjs/platform-express';
async function bootstrap() {
// 使用NestFactory.create()方法创建一个Nest应用实例
const app = await NestFactory.create<NestExpressApplication>(AppModule);
// 启用 CORS 跨域
app.enableCors();
// 为所有路由设置前缀
app.setGlobalPrefix('api');
// 在程序的入口点或适当的位置添加全局未捕获异常的监听器
process.on('uncaughtException', (error) => {
console.error('未捕获的异常:', error);
});
// 未处理的 Promise 拒绝的监听
process.on('unhandledRejection', (reason, promise) => {
console.error('未处理的拒绝:', promise, '原因:', reason);
});
// 使用app.listen()方法启动应用
await app.listen(3000);
}
bootstrap();
前端实现(VUE3+xterm.js):
1、安装依赖: npm install @xterm/xterm @xterm/addon-fit @xterm/addon-attach socket.io-client 2、xterm终端实现:<template>
<div ref="terminalRef" class="terminal"></div>
</template>
<script setup>
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { FitAddon } from '@xterm/addon-fit';
// import { AttachAddon } from '@xterm/addon-attach'
import { io } from 'socket.io-client';
import { useEventListener } from '@vueuse/core';
const terminalRef = ref(null);
// 创建终端实例
const terminal = new Terminal({
disableStdin: false, // 是否禁用输入
fontSize: 16, // 字体大小
fontFamily: 'Consolas, "Courier New", monospace', // 字体
fontWeight: 'normal', // 字体粗细
fontWeightBold: 'bold', // 粗体字体
letterSpacing: 0, // 字符间距
lineHeight: 1.0, // 行高
scrollback: 1000, // 设置终端可以回溯的最大行数为1000行
scrollSensitivity: 1, // 设置滚动条滚动时的灵敏度,数值越大滚动越快
fastScrollModifier: 'Shift', // 设置快速滚动的修饰键,按住Shift键滚动滚动条时滚动速度会加快
fastScrollSensitivity: 5, // 设置快速滚动的灵敏度,数值越大滚动越快
logLevel: 'info', // 日志级别
allowTransparency: true, // 背景是否应支持非不透明颜色,开启后支持 theme中使用 rgba
theme: {
cursor: '#ffffff', // 光标颜色
cursorAccent: '#000000', // 光标的强调色
foreground: '#ffffff', // 默认的前景色,即文本颜色
background: '#1e1e1e', // 背景颜色
selection: '#ffffff40', // 选择文本的颜色
},
convertEol: true, // 是否将回车符转换为换行符
cursorBlink: true, // 光标闪烁
cursorStyle: 'block', // 光标样式
cursorWidth: 1, // 光标宽度
altClickMovesCursor: true, // 如果启用,alt+click会将提示光标移动到鼠标下方的位置。默认值为true
rightClickSelectsWord: true, // 右键点击选择单词
windowsMode: true, // 是否启用Windows模式,如果启用,将使用Windows快捷键
});
// 创建适应容器的插件实例
const fitAddon = new FitAddon();
// 加载适应容器的插件
terminal.loadAddon(fitAddon);
// 创建socket连接
const socket = io('http://localhost:8113/control', {
autoConnect: false, // 禁止自动连接
});
// const socket = io('wss://121.36.55.244:30446/ws?id=140035370511824')
// 存储从后端接收的 clientId
let clientId = null;
// 创建一个变量来存储累积的输入
let inputBuffer = '';
onMounted(() => {
// 在页面上打开终端实例
terminal.open(terminalRef.value);
// 在下一个事件循环中调整终端以适应容器
nextTick(() => {
// 调整终端尺寸以适应容器
fitAddon.fit();
const charWidth = 14; // 字符的大概宽度,单位为像素
const charHeight = 19; // 字符的大概高度,单位为像素
const containerWidth = terminalRef.value.offsetWidth; // 终端容器的宽度
const containerHeight = terminalRef.value.offsetHeight; // 终端容器的高度
const cols = Math.floor(containerWidth / charWidth);
const rows = Math.floor(containerHeight / charHeight);
console.log('cols:', cols, 'rows:', rows);
// 手动调整终端的列数和行数
terminal.resize(cols, rows);
// 明确调用 connect 方法连接服务器
setTimeout(() => {
socket.connect();
}, 1000);
});
// 监听 socket 连接事件,会触发后端的 handleConnect 事件
socket.on('connect', () => {
console.log('connect:Socket 链接成功');
// 向终端写入文本并换行
terminal.writeln('connect:Socket 链接成功');
// 创建 AttachAddon 实例,用于将终端连接到服务器的 shell
// const attachAddon = new AttachAddon(socket)
// 加载 AttachAddon 插件
// terminal.loadAddon(attachAddon)
// 连接到远程主机
// const sshInfo = {
// host: '172.16.250.103',
// username: 'root',
// password: 'huawei@123',
// type: 'linux' // 连接的设备类型
// }
// 连接到路由器或交换机,3122
const sshInfo = {
host: '172.16.250.203',
username: 'liuwenzhao',
password: '1234@abcd',
type: 'device',
};
// 发送开始终端会话的请求
socket.emit('startTerminalSession', sshInfo);
});
// 监听 socket 断开连接事件,会触发后端的 handleDisconnect 事件
socket.on('disconnect', () => {
console.log('disconnect:Socket 断开连接. 请检查您的连接.');
terminal.writeln('disconnect:Socket 断开连接. 请检查您的连接.');
});
// 监听 terminalSessionStarted 事件, 用于显示终端会话已经开始
socket.on('terminalSessionStarted', (data) => {
console.log('terminalSessionStarted:' + data.message);
terminal.writeln('terminalSessionStarted:' + data.message);
// 存储从后端接收的 clientId
clientId = data.clientId;
});
// 监听 terminalSessionError 事件, 用于显示终端会话错误
socket.on('terminalSessionError', (error) => {
console.error('terminalSessionError:连接到主机失败:', error);
terminal.writeln('terminalSessionError:连接到主机失败:', error);
});
// 监听 terminalDisconnected 事件, 用于显示终端已经断开连接
socket.on('terminalDisconnected', (message) => {
console.log('terminalDisconnected:' + message); // 可以在这里处理断开连接后的逻辑,如显示消息、清理资源等
terminal.writeln('terminalDisconnected:' + message);
});
// 监听从服务器发送的消息
socket.on('terminalData', (data) => {
// 将从服务器接收到的数据写入终端
terminal.write(data);
});
// onData 事件用于监听用户输入的命令
terminal.onData((data) => {
if (socket.connected && clientId) {
socket.emit('input', { clientId, input: data });
} else {
console.error('Socket 未连接.');
terminal.writeln('Socket 未连接.');
}
// 检测到回车键
if (data == '\r') {
// 如果用户输入的命令是 'clear',则清除终端内容
if (inputBuffer.trim() == 'clear') {
// 检查用户是否输入了 'clear' 命令
terminal.clear(); // 清除终端内容
}
// 清空积累的输入
inputBuffer = '';
} else {
// 累积输入到缓冲区
inputBuffer += data;
}
});
// onKey(callback({ key: string, domEvent: KeyboardEvent })) // key: 键盘按键的值,domEvent: 键盘事件
// onData(callback(key: String)) // 类似于input的oninput事件,key代表的是输入的字符
// onCursorMove(callback()) // 输入光标位置变动会触发,比如输入,换行等
// onLineFeed(callback()) // 操作回车按钮换行时触发,自然输入换行不会触发
// onScroll(callback(scrollLineNumber: number)) // 当输入的行数超过设定的行数后会触发内容的滚动,输入换行以及回车换行均会触发
// onSelectionChange(callback()) // 操作鼠标左键选中/取消选中会触发
// onRender(callback({start: number, end: number})) // 鼠标移出点击,移入点击以及输入模式下键盘按下都会触发,范围从“0”到“Terminal.rows-1”
// onResize(callback({cols: number, rows: number})) // 在 open() 之后如果调用 resize 设置行列会触发改事件,返回新的行列数值
// onTitleChange(callback()) // 标题更改触发,未找到对应的触发条件
// onBell(callback()) // 为触发铃声时添加事件侦听器
});
// 断开终端连接
function disconnectTerminal() {
// 发送断开终端连接的请求
socket.emit('disconnectTerminal', clientId);
}
// 页面刷新或关闭时断开连接
useEventListener(window, 'beforeunload', (event) => {
// 在这里执行你需要的操作
console.log('页面即将刷新或关闭');
disconnectTerminal();
if (terminal) {
// 销毁终端实例
terminal.dispose();
}
// 如果你需要阻止页面关闭,可以使用以下代码
event.preventDefault();
event.returnValue = '';
});
// onUnmounted 组件卸载时断开连接,会触发 socket 的 disconnect 事件和后端的 handleDisconnect 事件
// onUnmounted(() => {
// disconnectTerminal()
// if (terminal) {
// // 销毁终端实例
// terminal.dispose()
// }
// })
</script>
<style scoped lang="scss">
.terminal {
width: 60%;
height: 600px;
overflow: hidden;
}
</style>
版权声明:本文标题:VUE3 + xterm + nestjs实现web远程终端 或 连接开启SSH登录的路由器和交换机。 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/biancheng/1726801584a1167508.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论