Guanghu Domestic Migration d1e47f4565
Some checks are pending
自动更新代码和重启 / update-and-restart (push) Waiting to run
CI检查 + 自动部署 / check (push) Waiting to run
CI检查 + 自动部署 / deploy (push) Blocked by required conditions
重启聊天服务 / restart (push) Waiting to run
chore: import sanitized domestic snapshot for REPO-002
Source snapshot: ca48d3ddf926d79aa138306164169baf764bb829
2026-07-17 15:54:41 +08:00

411 lines
12 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});