import { Module } from '@nestjs/common';
import { RemoteControlService } from './remote-control.service';
import { RemoteControlGateway } from './remote-control.gateway';
@Module({
providers: [RemoteControlService, RemoteControlGateway],
})
export class RemoteControlModule {}
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 {
// 设置设备类型
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',
'[email protected]',
'[email protected]',
'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 {
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 {
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 {
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(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();