'use strict'; /** * 部署辅助:用 ssh2 做密码登录的 SSH 执行与 SFTP 上传。 * * 为什么要自己写一个:Windows 自带 OpenSSH 但**没有 sshpass / plink**, * 无法在非交互模式下传密码,而部署脚本必须能无人值守地跑完。 * 用 ssh2 直接实现协议层就绕开了这个问题。 * * **脚本本身不含任何凭据**,全部走环境变量,避免密钥进仓库。 * * 用法: * node scripts/ssh-helper.js exec "<命令>" * node scripts/ssh-helper.js put <本地文件> <远程路径> * * 环境变量(也可以写进项目根目录的 .env,那里已被 .gitignore 忽略): * SSH_HOST 必填 * SSH_PASS 必填 * SSH_PORT 可选,默认 22 * SSH_USER 可选,默认 root * * 若未安装 ssh2(devDependencies,可选),会给出安装提示而不是裸崩。 */ const fs = require('fs'); const path = require('path'); // 与 deploy.js 一致:凭据可以从 .env 读;dotenv 不覆盖已存在的环境变量,显式传参仍然优先 require('dotenv').config({ path: path.join(__dirname, '..', '.env') }); let Client; try { ({ Client } = require('ssh2')); } catch { console.error('缺少可选依赖 ssh2。部署相关脚本需要它:\n\n npm install --save-dev ssh2\n'); process.exit(2); } const HOST = process.env.SSH_HOST; const PORT = Number(process.env.SSH_PORT || 22); const USER = process.env.SSH_USER || 'root'; const PASS = process.env.SSH_PASS; if (!HOST || !PASS) { console.error('缺少 SSH_HOST / SSH_PASS。请写进项目根目录的 .env(参考 .env.example),' + '或用环境变量临时传入。'); process.exit(2); } function connect() { return new Promise((resolve, reject) => { const client = new Client(); client.on('ready', () => resolve(client)); client.on('error', reject); client.connect({ host: HOST, port: PORT, username: USER, password: PASS, readyTimeout: 30000, algorithms: { // 老版本 OpenSSH 服务器可能只提供这些算法,放宽列表避免握手失败 kex: [ 'curve25519-sha256', 'curve25519-sha256@libssh.org', 'ecdh-sha2-nistp256', 'ecdh-sha2-nistp384', 'ecdh-sha2-nistp521', 'diffie-hellman-group-exchange-sha256', 'diffie-hellman-group14-sha256', 'diffie-hellman-group14-sha1', 'diffie-hellman-group1-sha1' ], serverHostKey: [ 'ssh-ed25519', 'ecdsa-sha2-nistp256', 'rsa-sha2-512', 'rsa-sha2-256', 'ssh-rsa' ] } }); }); } /** @returns {Promise<{code: number, stdout: string, stderr: string}>} */ function exec(client, command) { return new Promise((resolve, reject) => { client.exec(command, { pty: false }, (err, stream) => { if (err) { reject(err); return; } let stdout = ''; let stderr = ''; stream.on('data', (chunk) => { stdout += chunk.toString('utf8'); }); stream.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); }); stream.on('close', (code) => resolve({ code, stdout, stderr })); }); }); } function put(client, localFile, remotePath) { return new Promise((resolve, reject) => { client.sftp((err, sftp) => { if (err) { reject(err); return; } sftp.fastPut(localFile, remotePath, (putErr) => { if (putErr) reject(putErr); else resolve(remotePath); }); }); }); } async function main() { const [action, ...rest] = process.argv.slice(2); const client = await connect(); try { if (action === 'exec') { const result = await exec(client, rest.join(' ')); if (result.stdout) process.stdout.write(result.stdout); if (result.stderr) process.stderr.write(result.stderr); process.exitCode = result.code || 0; } else if (action === 'put') { const local = path.resolve(rest[0]); await put(client, local, rest[1]); console.log(`uploaded ${path.basename(local)} -> ${rest[1]} (${fs.statSync(local).size} bytes)`); } else { console.error('未知动作:' + action); process.exitCode = 2; } } finally { client.end(); } } main().catch((err) => { console.error('SSH 失败:' + (err && err.message ? err.message : err)); process.exit(1); });