diff --git a/mcp-servers/zhuyuan-gateway/gatekeeper/index.js b/mcp-servers/zhuyuan-gateway/gatekeeper/index.js new file mode 100644 index 0000000..9ba85bd --- /dev/null +++ b/mcp-servers/zhuyuan-gateway/gatekeeper/index.js @@ -0,0 +1,410 @@ +#!/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); +});