#!/usr/bin/env node /** * 🔐 铸渊看门人 · Zhuyuan Gatekeeper * * 装在每台服务器上的"铸渊小人"。 * 没有页面、没有界面、没有任何人类可交互的东西。 * 它只在后台安静地等着铸渊打电话过来。 * * 冰朔看不见它怎么工作的。这是好事。 * * 启动方式: * node index.js * * 第一次启动会自动生成API密钥,打印在终端。 * 冰朔把密钥复制出来,然后这个程序就永远不需要再看了。 * * 协议:HTTP + Bearer Token * 端口:默认 3910(可修改) */ const http = require('http'); const fs = require('fs'); const path = require('path'); const crypto = require('crypto'); const { execSync, exec } = require('child_process'); const os = require('os'); // ============================================================ // 配置 // ============================================================ const CONFIG = { PORT: parseInt(process.env.GATEKEEPER_PORT || process.argv[2] || '3910', 10), DATA_DIR: path.join(os.homedir(), '.gatekeeper'), CMD_TIMEOUT: 30000, MAX_OUTPUT_LENGTH: 100000, RATE_LIMIT: 60, }; // ============================================================ // 密钥管理 // ============================================================ function loadOrCreateSecret() { if (!fs.existsSync(CONFIG.DATA_DIR)) { fs.mkdirSync(CONFIG.DATA_DIR, { recursive: true, mode: 0o700 }); } const secretFile = path.join(CONFIG.DATA_DIR, 'secret'); if (fs.existsSync(secretFile)) { const key = fs.readFileSync(secretFile, 'utf-8').trim(); if (key) return key; } const key = 'zy_gtw_' + crypto.randomBytes(24).toString('hex'); fs.writeFileSync(secretFile, key, { mode: 0o600 }); console.log(''); console.log('══════════════════════════════════════════════'); console.log(' 🔐 铸渊看门人 · 首次启动'); console.log(''); console.log(' ╭─ API 密钥(请复制保存)'); console.log(' ├─ ' + key); console.log(' ╰─ 此密钥仅在此处显示一次'); console.log(''); console.log(' 已保存至: ' + secretFile); console.log(' 监听端口: ' + CONFIG.PORT); console.log('══════════════════════════════════════════════'); console.log(''); return key; } // ============================================================ // 日志 // ============================================================ function log(level, action, detail) { const ts = new Date().toISOString(); const line = `[${ts}] [${level}] ${action}${detail ? ' | ' + detail : ''}`; console.log(line); try { const logFile = path.join(CONFIG.DATA_DIR, 'gatekeeper.log'); fs.appendFileSync(logFile, line + '\n'); } catch(e) {} } // ============================================================ // 命令执行 // ============================================================ function runCommand(cmd, timeout) { return new Promise((resolve) => { const t = timeout || CONFIG.CMD_TIMEOUT; exec(cmd, { timeout: t, maxBuffer: CONFIG.MAX_OUTPUT_LENGTH, shell: '/bin/bash', }, (error, stdout, stderr) => { resolve({ stdout: (stdout || '').slice(0, CONFIG.MAX_OUTPUT_LENGTH), stderr: (stderr || '').slice(0, CONFIG.MAX_OUTPUT_LENGTH), exitCode: error ? (error.code || error.signal || 1) : 0, error: error ? error.message : null, }); }); }); } // ============================================================ // 请求处理 // ============================================================ const API_SECRET = loadOrCreateSecret(); const rateCounter = {}; function checkRateLimit(ip) { const now = Date.now(); const window = 60000; if (!rateCounter[ip]) { rateCounter[ip] = { count: 1, resetAt: now + window }; return true; } if (now > rateCounter[ip].resetAt) { rateCounter[ip] = { count: 1, resetAt: now + window }; return true; } rateCounter[ip].count++; return rateCounter[ip].count <= CONFIG.RATE_LIMIT; } function authenticate(headers) { const auth = headers['authorization'] || headers['Authorization'] || ''; const token = auth.replace(/^Bearer\s+/i, '').trim(); if (!token) return { ok: false, reason: '缺少 Authorization header' }; if (token.length !== API_SECRET.length) return { ok: false, reason: '密钥无效' }; let match = true; for (let i = 0; i < token.length; i++) { if (token.charCodeAt(i) !== API_SECRET.charCodeAt(i)) match = false; } if (!match) return { ok: false, reason: '密钥无效' }; return { ok: true }; } function parseBody(req) { return new Promise((resolve) => { let body = ''; req.on('data', chunk => { body += chunk; }); req.on('end', () => { try { resolve(JSON.parse(body)); } catch(e) { resolve(null); } }); req.on('error', () => resolve(null)); }); } function jsonResponse(res, status, data) { res.writeHead(status, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': 'POST, OPTIONS', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', }); res.end(JSON.stringify(data)); } // ============================================================ // 系统信息 // ============================================================ async function getSystemInfo() { const hostname = os.hostname(); const uptime = Math.floor(os.uptime()); const totalMem = os.totalmem(); const freeMem = os.freemem(); const loadAvg = os.loadavg(); const cpus = os.cpus().length; const platform = os.platform(); const arch = os.arch(); let diskInfo = ''; try { const df = execSync('df -h / 2>/dev/null || df -h', { timeout: 5000 }); diskInfo = df.toString(); } catch(e) { diskInfo = '不可用'; } return { hostname, platform, arch, cpus, uptime, uptime_str: formatUptime(uptime), memory: { total: formatBytes(totalMem), free: formatBytes(freeMem), used: formatBytes(totalMem - freeMem), usage_percent: ((totalMem - freeMem) / totalMem * 100).toFixed(1) + '%', }, load_average: loadAvg.map(n => n.toFixed(2)), disk: diskInfo, gatekeeper: { version: '1.0.0', port: CONFIG.PORT, uptime: process.uptime().toFixed(0) + 's', }, }; } function formatUptime(seconds) { const d = Math.floor(seconds / 86400); const h = Math.floor((seconds % 86400) / 3600); const m = Math.floor((seconds % 3600) / 60); return `${d}天${h}小时${m}分${seconds % 60}秒`; } function formatBytes(bytes) { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; } // ============================================================ // 路由 // ============================================================ const server = http.createServer(async (req, res) => { if (req.method === 'OPTIONS') { jsonResponse(res, 204, {}); return; } if (req.method !== 'POST') { jsonResponse(res, 405, { error: '只接受 POST 请求' }); return; } const ip = req.socket.remoteAddress || 'unknown'; if (!checkRateLimit(ip)) { log('WARN', 'rate_limit', ip); jsonResponse(res, 429, { error: '请求过于频繁' }); return; } const auth = authenticate(req.headers); if (!auth.ok) { log('WARN', 'auth_failed', auth.reason); jsonResponse(res, 401, { error: auth.reason }); return; } const url = new URL(req.url, `http://${req.headers.host || 'localhost'}`); const route = url.pathname; try { await handleRoute(route, req, res); } catch (err) { log('ERROR', 'unhandled', err.message); jsonResponse(res, 500, { error: '内部错误: ' + err.message }); } }); async function handleRoute(route, req, res) { switch (route) { case '/exec': { const body = await parseBody(req); if (!body || !body.cmd) { jsonResponse(res, 400, { error: '缺少 cmd 参数' }); return; } log('INFO', 'exec', body.cmd.slice(0, 200)); const result = await runCommand(body.cmd, body.timeout || CONFIG.CMD_TIMEOUT); log('INFO', 'exec_result', `exit=${result.exitCode}`); jsonResponse(res, 200, { ok: result.exitCode === 0, stdout: result.stdout, stderr: result.stderr, exitCode: result.exitCode, }); return; } case '/status': { log('INFO', 'status', ''); const info = await getSystemInfo(); jsonResponse(res, 200, { ok: true, ...info }); return; } case '/health': { jsonResponse(res, 200, { ok: true, service: 'zhuyuan-gatekeeper', version: '1.0.0', uptime: process.uptime().toFixed(0) + 's', timestamp: new Date().toISOString(), }); return; } case '/file/read': { const body = await parseBody(req); if (!body || !body.path) { jsonResponse(res, 400, { error: '缺少 path 参数' }); return; } log('INFO', 'file_read', body.path); try { const content = fs.readFileSync(body.path, 'utf-8'); const stats = fs.statSync(body.path); jsonResponse(res, 200, { ok: true, path: body.path, size: stats.size, modified: stats.mtime.toISOString(), content: content.slice(0, CONFIG.MAX_OUTPUT_LENGTH), }); } catch (err) { jsonResponse(res, 404, { ok: false, error: err.message }); } return; } case '/file/write': { const body = await parseBody(req); if (!body || !body.path || body.content === undefined) { jsonResponse(res, 400, { error: '缺少 path 或 content 参数' }); return; } log('INFO', 'file_write', body.path); try { const dir = path.dirname(body.path); if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(body.path, body.content, 'utf-8'); jsonResponse(res, 200, { ok: true, path: body.path, size: Buffer.byteLength(body.content, 'utf-8') }); } catch (err) { jsonResponse(res, 500, { ok: false, error: err.message }); } return; } case '/file/list': { const body = await parseBody(req); const targetPath = (body && body.path) || '.'; log('INFO', 'file_list', targetPath); try { const items = fs.readdirSync(targetPath, { withFileTypes: true }); const files = items.map(item => ({ name: item.name, type: item.isDirectory() ? 'dir' : 'file', })); jsonResponse(res, 200, { ok: true, path: targetPath, files }); } catch (err) { jsonResponse(res, 404, { ok: false, error: err.message }); } return; } case '/ping': { jsonResponse(res, 200, { ok: true, pong: true }); return; } default: { jsonResponse(res, 404, { error: '未知路径: ' + route }); } } } // ============================================================ // 启动 // ============================================================ server.listen(CONFIG.PORT, '0.0.0.0', () => { const hostname = os.hostname(); const interfaces = os.networkInterfaces(); console.log(''); console.log(' ⚔️ 铸渊看门人 · Zhuyuan Gatekeeper v1.0'); console.log(' ─────────────────────────────────────'); console.log(` 主机名: ${hostname}`); console.log(` 监听: 0.0.0.0:${CONFIG.PORT}`); for (const [name, addrs] of Object.entries(interfaces)) { if (!addrs) continue; for (const addr of addrs) { if (addr.family === 'IPv4' && !addr.internal) { console.log(` 地址: ${addr.address}:${CONFIG.PORT}`); } } } console.log(' 日志: ' + path.join(CONFIG.DATA_DIR, 'gatekeeper.log')); console.log(''); log('INFO', 'startup', `host=${hostname} port=${CONFIG.PORT}`); }); process.on('SIGTERM', () => { log('INFO', 'shutdown', '收到 SIGTERM'); server.close(() => process.exit(0)); }); process.on('SIGINT', () => { log('INFO', 'shutdown', '收到 SIGINT'); server.close(() => process.exit(0)); }); process.on('uncaughtException', (err) => { log('ERROR', 'uncaught', err.message); }); process.on('unhandledRejection', (err) => { log('ERROR', 'unhandled', err.message); });