/** * Nginx 配置文件上传脚本 * * ⚠️ 安全警告: * 1. 确保 .env 文件在 .gitignore 中,不要提交到版本控制 * 2. 强烈建议使用 SSH 密钥认证,而不是密码认证 * 3. 建议配置免密 sudo,而不是在 .env 中存储 sudo 密码 * 4. 密码会出现在进程列表中,使用 SSH 密钥更安全 * * 详细安全指南请参考 scripts/SECURITY.md */ const fs = require('fs'); const path = require('path'); const { execSync } = require('child_process'); // 加载 .env 配置文件 function loadEnvConfig() { const envPath = path.join(__dirname, '../.env'); const config = { DEPLOY_HOST: '', DEPLOY_USER: '', DEPLOY_PORT: '22', DEPLOY_SSH_KEY: '', DEPLOY_SSH_PASSWORD: '', DEPLOY_SUDO_PASSWORD: '', // sudo 密码(如果需要) }; if (fs.existsSync(envPath)) { const envContent = fs.readFileSync(envPath, 'utf8'); envContent.split('\n').forEach(line => { line = line.trim(); if (line && !line.startsWith('#')) { const match = line.match(/^([^=]+)=(.*)$/); if (match) { const key = match[1].trim(); let value = match[2].trim(); if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) { value = value.slice(1, -1); } if (config.hasOwnProperty(key)) { config[key] = value; } } } }); } return config; } // 检查 sshpass 是否安装 function checkSshpassInstalled() { try { execSync('which sshpass', { stdio: 'ignore' }); return true; } catch (e) { return false; } } // 构建 SSH 命令前缀 function buildSSHCommand(config, usePassword) { let sshCommand = ''; if (usePassword) { sshCommand = `sshpass -p "${config.DEPLOY_SSH_PASSWORD}" `; } sshCommand += 'ssh'; // 添加端口 if (config.DEPLOY_PORT && config.DEPLOY_PORT !== '22') { sshCommand += ` -p ${config.DEPLOY_PORT}`; } // 添加 SSH 密钥(如果没有使用密码) if (config.DEPLOY_SSH_KEY && !usePassword) { const sshKeyPath = config.DEPLOY_SSH_KEY.startsWith('/') ? config.DEPLOY_SSH_KEY : path.join(process.env.HOME || '', config.DEPLOY_SSH_KEY.replace(/^~/, '')); if (fs.existsSync(sshKeyPath)) { sshCommand += ` -i "${sshKeyPath}"`; } } // 添加 SSH 选项 sshCommand += ` -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`; return sshCommand; } // 构建 SCP 命令前缀 function buildSCPCommand(config, usePassword) { let scpCommand = ''; if (usePassword) { scpCommand = `sshpass -p "${config.DEPLOY_SSH_PASSWORD}" `; } scpCommand += 'scp'; // 添加端口 if (config.DEPLOY_PORT && config.DEPLOY_PORT !== '22') { scpCommand += ` -P ${config.DEPLOY_PORT}`; } // 添加 SSH 密钥(如果没有使用密码) if (config.DEPLOY_SSH_KEY && !usePassword) { const sshKeyPath = config.DEPLOY_SSH_KEY.startsWith('/') ? config.DEPLOY_SSH_KEY : path.join(process.env.HOME || '', config.DEPLOY_SSH_KEY.replace(/^~/, '')); if (fs.existsSync(sshKeyPath)) { scpCommand += ` -i "${sshKeyPath}"`; } } // 添加 SSH 选项 scpCommand += ` -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null`; return scpCommand; } // 上传配置文件 function uploadNginxConfig(config) { const configFile = path.join(__dirname, 'aitu-releases.conf'); const remotePath = '/etc/nginx/sites-enabled/aitu-releases.conf'; const tempPath = `/tmp/aitu-releases-${Date.now()}.conf`; if (!fs.existsSync(configFile)) { console.error(`❌ 配置文件不存在: ${configFile}`); process.exit(1); } console.log('📤 上传 Nginx 配置文件...'); console.log(` 本地文件: ${configFile}`); console.log(` 远程路径: ${remotePath}`); console.log(` 服务器: ${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${config.DEPLOY_PORT}`); // 判断是否使用密码 let usePassword = false; if (config.DEPLOY_SSH_PASSWORD) { if (!checkSshpassInstalled()) { console.error('\n❌ 未安装 sshpass,无法使用密码认证'); console.error(' 请安装: brew install hudochenkov/sshpass/sshpass (macOS)'); console.error(' 或者配置 SSH 密钥认证(更安全)'); process.exit(1); } usePassword = true; } // 显示认证方式 if (usePassword) { console.log(` 使用密码认证`); } else if (config.DEPLOY_SSH_KEY) { console.log(` 使用 SSH 密钥认证`); } else { console.log(` 使用默认 SSH 认证`); } // 步骤 1: 上传到临时目录 console.log(`\n 步骤 1/2: 上传到临时目录 ${tempPath}`); const scpCommand = buildSCPCommand(config, usePassword); const tempTarget = `${config.DEPLOY_USER}@${config.DEPLOY_HOST}:${tempPath}`; const uploadCommand = `${scpCommand} "${configFile}" "${tempTarget}"`; try { execSync(uploadCommand, { stdio: 'inherit' }); console.log(' ✅ 文件上传到临时目录成功'); } catch (error) { console.error('\n❌ 上传到临时目录失败'); if (usePassword) { console.error(' 可能的原因:'); console.error(' 1. 密码错误'); console.error(' 2. 服务器不允许密码认证'); console.error(' 3. 网络连接问题'); console.error(' 建议: 配置 SSH 密钥认证(更安全)'); } else { console.error(' 可能的原因:'); console.error(' 1. SSH 密钥未配置或权限不足'); console.error(' 2. 服务器连接失败'); } process.exit(1); } // 步骤 2: 使用 sudo 移动到目标位置 console.log(`\n 步骤 2/2: 移动到目标目录(需要 sudo 权限)`); const sshCommand = buildSSHCommand(config, usePassword); // 构建 sudo 命令,如果配置了 sudo 密码则使用 -S 选项 let sudoPrefix = 'sudo'; if (config.DEPLOY_SUDO_PASSWORD) { // 使用 echo 通过管道传递密码给 sudo -S // 注意:需要转义特殊字符,使用单引号包裹密码 const escapedPassword = config.DEPLOY_SUDO_PASSWORD.replace(/'/g, "'\\''"); sudoPrefix = `echo '${escapedPassword}' | sudo -S`; } // 备份旧文件(如果存在),然后移动新文件 const moveCommand = `${sshCommand} ${config.DEPLOY_USER}@${config.DEPLOY_HOST} "${sudoPrefix} cp ${tempPath} ${remotePath} && ${sudoPrefix} rm -f ${tempPath}"`; try { execSync(moveCommand, { stdio: 'inherit' }); console.log(' ✅ 文件已移动到目标目录'); console.log('✅ 配置文件上传成功'); return true; } catch (error) { console.error('\n❌ 移动到目标目录失败'); console.error(' 可能的原因:'); console.error(' 1. 用户没有 sudo 权限'); console.error(' 2. sudo 密码错误(如果配置了 DEPLOY_SUDO_PASSWORD)'); console.error(' 3. 目标目录不存在或权限不足'); if (!config.DEPLOY_SUDO_PASSWORD) { console.error(' 提示: 可以在 .env 中配置 DEPLOY_SUDO_PASSWORD 来自动输入 sudo 密码'); } console.error(`\n 临时文件位置: ${tempPath}`); console.error(' 可以手动执行:'); console.error(` ssh ${config.DEPLOY_USER}@${config.DEPLOY_HOST}`); console.error(` sudo cp ${tempPath} ${remotePath}`); process.exit(1); } } // 测试并重载 Nginx 配置 function testAndReloadNginx(config) { console.log('\n🔧 测试 Nginx 配置...'); const usePassword = !!config.DEPLOY_SSH_PASSWORD; const sshCommand = buildSSHCommand(config, usePassword); // 构建 sudo 命令,如果配置了 sudo 密码则使用 -S 选项 let sudoPrefix = 'sudo'; if (config.DEPLOY_SUDO_PASSWORD) { const escapedPassword = config.DEPLOY_SUDO_PASSWORD.replace(/'/g, "'\\''"); sudoPrefix = `echo '${escapedPassword}' | sudo -S`; } // 测试配置 const testCommand = `${sshCommand} ${config.DEPLOY_USER}@${config.DEPLOY_HOST} "${sudoPrefix} nginx -t"`; try { console.log(' 执行: sudo nginx -t'); execSync(testCommand, { stdio: 'inherit' }); console.log('✅ Nginx 配置测试通过'); } catch (error) { console.error('\n❌ Nginx 配置测试失败'); console.error(' 请检查配置文件语法是否正确'); if (!config.DEPLOY_SUDO_PASSWORD) { console.error(' 提示: 如果 sudo 需要密码,可以在 .env 中配置 DEPLOY_SUDO_PASSWORD'); } process.exit(1); } // 重载 Nginx console.log('\n🔄 重载 Nginx 配置...'); const reloadCommand = `${sshCommand} ${config.DEPLOY_USER}@${config.DEPLOY_HOST} "${sudoPrefix} systemctl reload nginx || ${sudoPrefix} service nginx reload || ${sudoPrefix} nginx -s reload"`; try { execSync(reloadCommand, { stdio: 'inherit' }); console.log('✅ Nginx 配置已重载'); } catch (error) { console.error('\n❌ Nginx 重载失败'); console.error(' 请手动执行: sudo systemctl reload nginx'); process.exit(1); } } // 主函数 function main() { console.log('🚀 上传 Nginx 配置文件\n'); const config = loadEnvConfig(); if (!config.DEPLOY_HOST || !config.DEPLOY_USER) { console.error('❌ 缺少必要的配置'); console.error(' 请在 .env 文件中配置:'); console.error(' DEPLOY_HOST=your-server.com'); console.error(' DEPLOY_USER=username'); console.error(' DEPLOY_PORT=22 (可选)'); console.error(' DEPLOY_SSH_KEY=~/.ssh/id_rsa (可选,推荐)'); console.error(' DEPLOY_SSH_PASSWORD=password (可选,不推荐)'); process.exit(1); } // 上传配置文件 uploadNginxConfig(config); // 询问是否测试并重载 const readline = require('readline'); const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question('\n是否测试并重载 Nginx 配置? (y/n): ', (answer) => { rl.close(); if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') { testAndReloadNginx(config); console.log('\n✅ 完成!'); } else { console.log('\n⚠️ 请手动测试并重载 Nginx:'); console.log(' sudo nginx -t'); console.log(' sudo systemctl reload nginx'); } }); } // 运行主函数 if (require.main === module) { main(); } module.exports = { uploadNginxConfig, testAndReloadNginx };