ssh-mcp-devops 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ export class SecurityChecker {
2
+ constructor() {
3
+ // 危险命令模式
4
+ this.dangerousPatterns = [
5
+ { pattern: /rm\s+-rf\s+\//, reason: '删除根目录', level: 'critical' },
6
+ { pattern: /rm\s+-rf/, reason: '强制递归删除', level: 'high' },
7
+ { pattern: /dd\s+if=.*of=\/dev/, reason: '磁盘写入操作', level: 'critical' },
8
+ { pattern: /mkfs/, reason: '格式化文件系统', level: 'critical' },
9
+ { pattern: /:\(\)\{.*\}/, reason: 'Fork炸弹', level: 'critical' },
10
+ { pattern: /chmod\s+777/, reason: '设置最大权限', level: 'medium' },
11
+ { pattern: /shutdown|reboot|halt/, reason: '系统关机/重启', level: 'high' },
12
+ { pattern: /iptables.*-F/, reason: '清空防火墙规则', level: 'high' },
13
+ { pattern: /userdel|deluser/, reason: '删除用户', level: 'medium' },
14
+ { pattern: />.*\/dev\/sda/, reason: '直接写入磁盘', level: 'critical' },
15
+ ];
16
+ }
17
+
18
+ checkCommand(command) {
19
+ for (const { pattern, reason, level } of this.dangerousPatterns) {
20
+ if (pattern.test(command)) {
21
+ return {
22
+ isDangerous: true,
23
+ level,
24
+ reason,
25
+ command,
26
+ };
27
+ }
28
+ }
29
+
30
+ return {
31
+ isDangerous: false,
32
+ level: 'safe',
33
+ reason: '命令安全',
34
+ command,
35
+ };
36
+ }
37
+
38
+ // 检查路径是否安全
39
+ isPathSafe(path) {
40
+ const dangerousPaths = ['/', '/etc', '/boot', '/sys', '/proc'];
41
+ return !dangerousPaths.some(dp => path === dp || path.startsWith(dp + '/'));
42
+ }
43
+ }
@@ -0,0 +1,299 @@
1
+ import { Client } from 'ssh2';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ export class SSHManager {
6
+ constructor() {
7
+ this.client = null;
8
+ this.connected = false;
9
+ this.config = null;
10
+ this.reconnectAttempts = 0;
11
+ this.maxReconnectAttempts = 3;
12
+ this.commandTimeout = 300000; // 5分钟超时
13
+ }
14
+
15
+ async autoConnect(config) {
16
+ this.config = config;
17
+ if (config.privateKeyPath) {
18
+ try {
19
+ config.privateKey = await fs.readFile(config.privateKeyPath, 'utf-8');
20
+ } catch (error) {
21
+ throw new Error(`无法读取私钥文件: ${error.message}`);
22
+ }
23
+ }
24
+ return this.connect(config);
25
+ }
26
+
27
+ async connect(config) {
28
+ return new Promise((resolve, reject) => {
29
+ // 如果已连接,先断开
30
+ if (this.client) {
31
+ this.client.end();
32
+ }
33
+
34
+ this.client = new Client();
35
+
36
+ const connectionConfig = {
37
+ host: config.host,
38
+ port: config.port || 22,
39
+ username: config.username,
40
+ readyTimeout: 30000, // 30秒连接超时
41
+ keepaliveInterval: 10000, // 10秒心跳
42
+ };
43
+
44
+ if (config.password) {
45
+ connectionConfig.password = config.password;
46
+ } else if (config.privateKey) {
47
+ connectionConfig.privateKey = config.privateKey;
48
+ } else {
49
+ return reject(new Error('必须提供密码或私钥'));
50
+ }
51
+
52
+ const timeout = setTimeout(() => {
53
+ this.client.end();
54
+ reject(new Error('连接超时'));
55
+ }, 30000);
56
+
57
+ this.client
58
+ .on('ready', () => {
59
+ clearTimeout(timeout);
60
+ this.connected = true;
61
+ this.reconnectAttempts = 0;
62
+ resolve(`✅ 成功连接到 ${config.username}@${config.host}:${config.port || 22}`);
63
+ })
64
+ .on('error', (err) => {
65
+ clearTimeout(timeout);
66
+ this.connected = false;
67
+ reject(new Error(`SSH连接失败: ${err.message}`));
68
+ })
69
+ .on('close', () => {
70
+ this.connected = false;
71
+ console.error('SSH连接已关闭');
72
+ })
73
+ .connect(connectionConfig);
74
+ });
75
+ }
76
+
77
+ async ensureConnected() {
78
+ if (!this.connected && this.config && this.reconnectAttempts < this.maxReconnectAttempts) {
79
+ console.error('连接已断开,尝试重新连接...');
80
+ this.reconnectAttempts++;
81
+ try {
82
+ await this.connect(this.config);
83
+ } catch (error) {
84
+ throw new Error(`重连失败 (${this.reconnectAttempts}/${this.maxReconnectAttempts}): ${error.message}`);
85
+ }
86
+ }
87
+
88
+ if (!this.connected) {
89
+ throw new Error('未连接到SSH服务器,请先使用 ssh_connect');
90
+ }
91
+ }
92
+
93
+ async execCommand(command, options = {}) {
94
+ await this.ensureConnected();
95
+
96
+ const timeout = options.timeout || this.commandTimeout;
97
+
98
+ return new Promise((resolve, reject) => {
99
+ const timer = setTimeout(() => {
100
+ reject(new Error(`命令执行超时 (${timeout}ms): ${command}`));
101
+ }, timeout);
102
+
103
+ this.client.exec(command, (err, stream) => {
104
+ if (err) {
105
+ clearTimeout(timer);
106
+ return reject(err);
107
+ }
108
+
109
+ let stdout = '';
110
+ let stderr = '';
111
+
112
+ stream
113
+ .on('close', (code) => {
114
+ clearTimeout(timer);
115
+ if (code !== 0 && stderr) {
116
+ reject(new Error(`命令执行失败 (退出码: ${code})\n${stderr}`));
117
+ } else {
118
+ const output = stdout || stderr || `命令执行完成 (退出码: ${code})`;
119
+ resolve(output);
120
+ }
121
+ })
122
+ .on('data', (data) => {
123
+ stdout += data.toString();
124
+ })
125
+ .stderr.on('data', (data) => {
126
+ stderr += data.toString();
127
+ });
128
+ });
129
+ });
130
+ }
131
+
132
+ async uploadFile(localPath, remotePath, options = {}) {
133
+ await this.ensureConnected();
134
+
135
+ // 检查本地文件是否存在
136
+ try {
137
+ await fs.access(localPath);
138
+ } catch (error) {
139
+ throw new Error(`本地文件不存在: ${localPath}`);
140
+ }
141
+
142
+ return new Promise((resolve, reject) => {
143
+ this.client.sftp((err, sftp) => {
144
+ if (err) return reject(err);
145
+
146
+ const transferOptions = {
147
+ concurrency: 64,
148
+ chunkSize: 32768,
149
+ step: options.onProgress || undefined,
150
+ };
151
+
152
+ sftp.fastPut(localPath, remotePath, transferOptions, (err) => {
153
+ sftp.end();
154
+ if (err) return reject(new Error(`上传失败: ${err.message}`));
155
+ resolve(`✅ 文件上传成功: ${localPath} -> ${remotePath}`);
156
+ });
157
+ });
158
+ });
159
+ }
160
+
161
+ async downloadFile(remotePath, localPath, options = {}) {
162
+ await this.ensureConnected();
163
+
164
+ // 确保本地目录存在
165
+ const localDir = path.dirname(localPath);
166
+ await fs.mkdir(localDir, { recursive: true });
167
+
168
+ return new Promise((resolve, reject) => {
169
+ this.client.sftp((err, sftp) => {
170
+ if (err) return reject(err);
171
+
172
+ const transferOptions = {
173
+ concurrency: 64,
174
+ chunkSize: 32768,
175
+ step: options.onProgress || undefined,
176
+ };
177
+
178
+ sftp.fastGet(remotePath, localPath, transferOptions, (err) => {
179
+ sftp.end();
180
+ if (err) return reject(new Error(`下载失败: ${err.message}`));
181
+ resolve(`✅ 文件下载成功: ${remotePath} -> ${localPath}`);
182
+ });
183
+ });
184
+ });
185
+ }
186
+
187
+ async uploadDirectory(localDir, remoteDir) {
188
+ await this.ensureConnected();
189
+
190
+ return new Promise((resolve, reject) => {
191
+ this.client.sftp(async (err, sftp) => {
192
+ if (err) return reject(err);
193
+
194
+ try {
195
+ // 创建远程目录
196
+ await this.execCommand(`mkdir -p ${remoteDir}`);
197
+
198
+ const files = await fs.readdir(localDir, { withFileTypes: true });
199
+
200
+ for (const file of files) {
201
+ const localPath = path.join(localDir, file.name);
202
+ const remotePath = `${remoteDir}/${file.name}`;
203
+
204
+ if (file.isDirectory()) {
205
+ await this.uploadDirectory(localPath, remotePath);
206
+ } else {
207
+ await new Promise((res, rej) => {
208
+ sftp.fastPut(localPath, remotePath, (err) => {
209
+ if (err) rej(err);
210
+ else res();
211
+ });
212
+ });
213
+ }
214
+ }
215
+
216
+ sftp.end();
217
+ resolve(`✅ 目录上传成功: ${localDir} -> ${remoteDir}`);
218
+ } catch (error) {
219
+ sftp.end();
220
+ reject(error);
221
+ }
222
+ });
223
+ });
224
+ }
225
+
226
+ async deployProject(projectPath, remotePath, deployScript) {
227
+ const steps = [];
228
+
229
+ try {
230
+ // 1. 在远程创建临时目录
231
+ steps.push('📦 准备部署环境...');
232
+ const timestamp = Date.now();
233
+ const tempDir = `/tmp/deploy-${timestamp}`;
234
+ await this.execCommand(`mkdir -p ${tempDir}`);
235
+
236
+ // 2. 上传项目文件
237
+ steps.push('⬆️ 上传项目文件...');
238
+ await this.uploadDirectory(projectPath, tempDir);
239
+
240
+ // 3. 备份现有部署(如果存在)
241
+ steps.push('💾 备份现有部署...');
242
+ const backupPath = `${remotePath}.backup.${timestamp}`;
243
+ await this.execCommand(`if [ -d "${remotePath}" ]; then cp -r ${remotePath} ${backupPath}; fi`);
244
+
245
+ // 4. 移动到目标目录
246
+ steps.push('📂 部署到目标目录...');
247
+ await this.execCommand(`mkdir -p ${remotePath} && cp -r ${tempDir}/* ${remotePath}/`);
248
+
249
+ // 5. 执行部署脚本
250
+ steps.push('🚀 执行部署脚本...');
251
+ const result = await this.execCommand(`cd ${remotePath} && ${deployScript}`, { timeout: 600000 });
252
+
253
+ // 6. 清理临时文件
254
+ steps.push('🧹 清理临时文件...');
255
+ await this.execCommand(`rm -rf ${tempDir}`);
256
+
257
+ steps.push('✅ 部署完成!');
258
+ steps.push(`\n备份位置: ${backupPath}`);
259
+
260
+ return steps.join('\n') + '\n\n部署输出:\n' + result;
261
+ } catch (error) {
262
+ steps.push(`❌ 部署失败: ${error.message}`);
263
+ throw new Error(steps.join('\n'));
264
+ }
265
+ }
266
+
267
+ async getSystemInfo() {
268
+ await this.ensureConnected();
269
+
270
+ const commands = {
271
+ hostname: 'hostname',
272
+ os: 'cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d \'"\'',
273
+ kernel: 'uname -r',
274
+ uptime: 'uptime -p',
275
+ cpu: 'lscpu | grep "Model name" | cut -d: -f2 | xargs',
276
+ memory: 'free -h | grep Mem | awk \'{print $2}\'',
277
+ disk: 'df -h / | tail -1 | awk \'{print $2}\'',
278
+ };
279
+
280
+ const info = {};
281
+ for (const [key, cmd] of Object.entries(commands)) {
282
+ try {
283
+ info[key] = (await this.execCommand(cmd)).trim();
284
+ } catch (error) {
285
+ info[key] = 'N/A';
286
+ }
287
+ }
288
+
289
+ return info;
290
+ }
291
+
292
+ disconnect() {
293
+ if (this.client) {
294
+ this.client.end();
295
+ this.connected = false;
296
+ this.client = null;
297
+ }
298
+ }
299
+ }
package/ssh-manager.js ADDED
@@ -0,0 +1,144 @@
1
+ import { Client } from 'ssh2';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+
5
+ export class SSHManager {
6
+ constructor() {
7
+ this.client = null;
8
+ this.connected = false;
9
+ this.config = null;
10
+ }
11
+
12
+ async autoConnect(config) {
13
+ this.config = config;
14
+ if (config.privateKeyPath) {
15
+ const fs = await import('fs/promises');
16
+ config.privateKey = await fs.readFile(config.privateKeyPath, 'utf-8');
17
+ }
18
+ return this.connect(config);
19
+ }
20
+
21
+ async connect(config) {
22
+ return new Promise((resolve, reject) => {
23
+ this.client = new Client();
24
+
25
+ const connectionConfig = {
26
+ host: config.host,
27
+ port: config.port || 22,
28
+ username: config.username,
29
+ };
30
+
31
+ if (config.password) {
32
+ connectionConfig.password = config.password;
33
+ } else if (config.privateKey) {
34
+ connectionConfig.privateKey = config.privateKey;
35
+ }
36
+
37
+ this.client
38
+ .on('ready', () => {
39
+ this.connected = true;
40
+ resolve(`✅ 成功连接到 ${config.username}@${config.host}:${config.port || 22}`);
41
+ })
42
+ .on('error', (err) => {
43
+ this.connected = false;
44
+ reject(new Error(`SSH连接失败: ${err.message}`));
45
+ })
46
+ .connect(connectionConfig);
47
+ });
48
+ }
49
+
50
+ async execCommand(command) {
51
+ if (!this.connected) {
52
+ throw new Error('未连接到SSH服务器,请先使用 ssh_connect');
53
+ }
54
+
55
+ return new Promise((resolve, reject) => {
56
+ this.client.exec(command, (err, stream) => {
57
+ if (err) return reject(err);
58
+
59
+ let stdout = '';
60
+ let stderr = '';
61
+
62
+ stream
63
+ .on('close', (code) => {
64
+ const output = stdout || stderr || `命令执行完成 (退出码: ${code})`;
65
+ resolve(output);
66
+ })
67
+ .on('data', (data) => {
68
+ stdout += data.toString();
69
+ })
70
+ .stderr.on('data', (data) => {
71
+ stderr += data.toString();
72
+ });
73
+ });
74
+ });
75
+ }
76
+
77
+ async uploadFile(localPath, remotePath) {
78
+ if (!this.connected) {
79
+ throw new Error('未连接到SSH服务器');
80
+ }
81
+
82
+ return new Promise((resolve, reject) => {
83
+ this.client.sftp((err, sftp) => {
84
+ if (err) return reject(err);
85
+
86
+ sftp.fastPut(localPath, remotePath, (err) => {
87
+ if (err) return reject(new Error(`上传失败: ${err.message}`));
88
+ resolve(`✅ 文件上传成功: ${localPath} -> ${remotePath}`);
89
+ });
90
+ });
91
+ });
92
+ }
93
+
94
+ async downloadFile(remotePath, localPath) {
95
+ if (!this.connected) {
96
+ throw new Error('未连接到SSH服务器');
97
+ }
98
+
99
+ return new Promise((resolve, reject) => {
100
+ this.client.sftp((err, sftp) => {
101
+ if (err) return reject(err);
102
+
103
+ sftp.fastGet(remotePath, localPath, (err) => {
104
+ if (err) return reject(new Error(`下载失败: ${err.message}`));
105
+ resolve(`✅ 文件下载成功: ${remotePath} -> ${localPath}`);
106
+ });
107
+ });
108
+ });
109
+ }
110
+
111
+ async deployProject(projectPath, remotePath, deployScript) {
112
+ const steps = [];
113
+
114
+ // 1. 压缩项目
115
+ steps.push('📦 压缩项目文件...');
116
+ const archiveName = `deploy-${Date.now()}.tar.gz`;
117
+ await this.execCommand(`cd ${projectPath} && tar -czf /tmp/${archiveName} .`);
118
+
119
+ // 2. 上传压缩包
120
+ steps.push('⬆️ 上传到远程服务器...');
121
+ await this.uploadFile(`/tmp/${archiveName}`, `/tmp/${archiveName}`);
122
+
123
+ // 3. 解压到目标目录
124
+ steps.push('📂 解压文件...');
125
+ await this.execCommand(`mkdir -p ${remotePath} && tar -xzf /tmp/${archiveName} -C ${remotePath}`);
126
+
127
+ // 4. 执行部署脚本
128
+ steps.push('🚀 执行部署脚本...');
129
+ const result = await this.execCommand(`cd ${remotePath} && ${deployScript}`);
130
+
131
+ // 5. 清理临时文件
132
+ await this.execCommand(`rm /tmp/${archiveName}`);
133
+
134
+ steps.push('✅ 部署完成!');
135
+ return steps.join('\n') + '\n\n' + result;
136
+ }
137
+
138
+ disconnect() {
139
+ if (this.client) {
140
+ this.client.end();
141
+ this.connected = false;
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,136 @@
1
+ export class SystemMonitor {
2
+ constructor(sshManager) {
3
+ this.ssh = sshManager;
4
+ }
5
+
6
+ async getCPUUsage() {
7
+ const output = await this.ssh.execCommand("top -bn1 | grep 'Cpu(s)' | awk '{print $2}' | cut -d'%' -f1");
8
+ return parseFloat(output.trim());
9
+ }
10
+
11
+ async getMemoryUsage() {
12
+ const output = await this.ssh.execCommand("free | grep Mem | awk '{printf \"%.2f\", $3/$2 * 100.0}'");
13
+ return parseFloat(output.trim());
14
+ }
15
+
16
+ async getDiskUsage(path = '/') {
17
+ const output = await this.ssh.execCommand(`df -h ${path} | tail -1 | awk '{print $5}' | cut -d'%' -f1`);
18
+ return parseFloat(output.trim());
19
+ }
20
+
21
+ async getLoadAverage() {
22
+ const output = await this.ssh.execCommand("uptime | awk -F'load average:' '{print $2}' | xargs");
23
+ const [load1, load5, load15] = output.trim().split(',').map(s => parseFloat(s.trim()));
24
+ return { load1, load5, load15 };
25
+ }
26
+
27
+ async getNetworkStats() {
28
+ const output = await this.ssh.execCommand("cat /proc/net/dev | grep -E 'eth0|ens|enp' | head -1 | awk '{print $2,$10}'");
29
+ const [received, transmitted] = output.trim().split(' ').map(Number);
30
+
31
+ return {
32
+ received: this.formatBytes(received),
33
+ transmitted: this.formatBytes(transmitted),
34
+ };
35
+ }
36
+
37
+ async getProcessList(limit = 10) {
38
+ const output = await this.ssh.execCommand(`ps aux --sort=-%mem | head -${limit + 1}`);
39
+ const lines = output.trim().split('\n');
40
+
41
+ const processes = lines.slice(1).map(line => {
42
+ const parts = line.trim().split(/\s+/);
43
+ return {
44
+ user: parts[0],
45
+ pid: parts[1],
46
+ cpu: parts[2],
47
+ memory: parts[3],
48
+ command: parts.slice(10).join(' '),
49
+ };
50
+ });
51
+
52
+ return processes;
53
+ }
54
+
55
+ async getServiceStatus(serviceName) {
56
+ try {
57
+ const output = await this.ssh.execCommand(`systemctl is-active ${serviceName}`);
58
+ return output.trim() === 'active';
59
+ } catch (error) {
60
+ return false;
61
+ }
62
+ }
63
+
64
+ async restartService(serviceName) {
65
+ const output = await this.ssh.execCommand(`sudo systemctl restart ${serviceName}`);
66
+ return `✅ 服务已重启: ${serviceName}\n${output}`;
67
+ }
68
+
69
+ async getSystemLogs(lines = 50) {
70
+ const output = await this.ssh.execCommand(`journalctl -n ${lines} --no-pager`);
71
+ return output;
72
+ }
73
+
74
+ async getFileContent(filePath, lines = 50) {
75
+ const output = await this.ssh.execCommand(`tail -n ${lines} ${filePath}`);
76
+ return output;
77
+ }
78
+
79
+ async searchInLogs(pattern, logFile = '/var/log/syslog', lines = 20) {
80
+ const output = await this.ssh.execCommand(`grep -i "${pattern}" ${logFile} | tail -n ${lines}`);
81
+ return output;
82
+ }
83
+
84
+ async getOpenPorts() {
85
+ const output = await this.ssh.execCommand("ss -tuln | grep LISTEN");
86
+ const lines = output.trim().split('\n');
87
+
88
+ const ports = lines.map(line => {
89
+ const parts = line.trim().split(/\s+/);
90
+ const address = parts[4];
91
+ const [ip, port] = address.split(':');
92
+ return { protocol: parts[0], port, ip: ip || '*' };
93
+ });
94
+
95
+ return ports;
96
+ }
97
+
98
+ async checkDiskIO() {
99
+ const output = await this.ssh.execCommand("iostat -x 1 2 | tail -n +4 | grep -E 'sda|vda|nvme' | tail -1");
100
+ const parts = output.trim().split(/\s+/);
101
+
102
+ return {
103
+ device: parts[0],
104
+ readKBps: parts[5],
105
+ writeKBps: parts[6],
106
+ utilization: parts[13],
107
+ };
108
+ }
109
+
110
+ async getSystemHealth() {
111
+ const [cpu, memory, disk, load] = await Promise.all([
112
+ this.getCPUUsage(),
113
+ this.getMemoryUsage(),
114
+ this.getDiskUsage(),
115
+ this.getLoadAverage(),
116
+ ]);
117
+
118
+ const health = {
119
+ cpu: { usage: cpu, status: cpu > 80 ? 'warning' : 'ok' },
120
+ memory: { usage: memory, status: memory > 80 ? 'warning' : 'ok' },
121
+ disk: { usage: disk, status: disk > 80 ? 'warning' : 'ok' },
122
+ load: { ...load, status: load.load1 > 4 ? 'warning' : 'ok' },
123
+ };
124
+
125
+ const overallStatus = Object.values(health).some(h => h.status === 'warning') ? 'warning' : 'healthy';
126
+
127
+ return { ...health, overall: overallStatus };
128
+ }
129
+
130
+ formatBytes(bytes) {
131
+ const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
132
+ if (bytes === 0) return '0 B';
133
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
134
+ return `${(bytes / Math.pow(1024, i)).toFixed(2)} ${sizes[i]}`;
135
+ }
136
+ }