871 lines
39 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
/* ═══════════════════════════════════════════════════════════
光湖驱动引擎 v3.2 · 工作会话授权 · Guanghu Drive Engine v3
HLDP万能语言接口 + 集群串联 + 人类验证码审批
核心变更 (v2 → v3):
Token 不再是 root 密码 → Token 只是身份标识
任何操作需通过 /auth/request → 邮箱验证码 → /auth/confirm
三元组白名单 (服务器+人+人格体) + 来源服务器绑定
操作类型分级邮件 (code-repo / system-arch / api-access)
未注册请求直接抛弃,不响应,杜绝恶意刷邮箱
v3.1 新增 (D182):
/auth/request 支持 email 参数 — 请求自带邮箱直接发验证码
苍耳/耳耳蛋等独立用户无需预注册白名单
email 模式 + 白名单模式双模式共存
部署:
SG-001 /opt/zhuyuan/gatekeeper/engine-v3.js
监听: 3911 (环境变量 ENGINE_PORT 可配)
数据: /opt/zhuyuan/gatekeeper/whitelist.json
═══════════════════════════════════════════════════════════ */
const http = require('http'), fs = require('fs'), path = require('path'),
crypto = require('crypto'),
os = require('os'), tls = require('tls');
const { SessionManager } = require('./session-manager');
const { getAction, listActions } = require('./authorized-actions');
const PORT = parseInt(process.env.ENGINE_PORT || process.env.GATEKEEPER_PORT || process.argv[2] || '3911', 10);
const HOST = process.env.ENGINE_HOST || '0.0.0.0';
const DATA_DIR = process.env.GATEKEEPER_DATA_DIR || path.join('/opt/zhuyuan', 'gatekeeper');
const CMD_TIMEOUT = 30000, MAX_OUTPUT = 100000;
const CODE_TTL = 300; // 验证码 5 分钟过期
const RATE_LIMIT_WINDOW = 60;
const MAX_REQUESTS = 3;
// ═══════════════════════════════════════
// 初始化数据目录
// ═══════════════════════════════════════
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true, mode: 0o700 });
const sessions = new SessionManager({ absoluteTtl: 12 * 3600, idleTtl: 3600, stateFile: path.join(DATA_DIR, 'sessions.json') });
// ═══════════════════════════════════════
// 密钥 — 兼容所有历史数据目录
// ═══════════════════════════════════════
const SECRET_PATHS = [
...(process.env.GATEKEEPER_SECRET_PATH ? [process.env.GATEKEEPER_SECRET_PATH] : []),
path.join(os.homedir(), '.gatekeeper', 'secret'),
path.join(os.homedir(), '.gk', 'secret'),
path.join(os.homedir(), '.guanghu-engine', 'secret'),
];
let API_SECRET;
for (const sp of SECRET_PATHS) {
if (fs.existsSync(sp)) { const k = fs.readFileSync(sp, 'utf-8').trim(); if (k) { API_SECRET = k; break; } }
}
if (!API_SECRET) {
API_SECRET = ['zy', 'gtw', crypto.randomBytes(24).toString('hex')].join('_');
const dir = path.dirname(SECRET_PATHS[0]);
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
fs.writeFileSync(SECRET_PATHS[0], API_SECRET, { mode: 0o600 });
console.log('');
console.log('══════════════════════════════════════════════');
console.log(' 🔐 光湖驱动引擎 v3 · 首次启动');
console.log(' API 密钥: 已生成(不写入服务日志)');
console.log(' 已保存至: ' + SECRET_PATHS[0]);
console.log(' 监听端口: ' + PORT);
console.log('══════════════════════════════════════════════');
console.log('');
}
const HOSTNAME = os.hostname();
// ═══════════════════════════════════════
// 服务器标识映射
// ═══════════════════════════════════════
function getServerId() {
if (process.env.GATEKEEPER_SERVER_ID) return process.env.GATEKEEPER_SERVER_ID;
const map = {
'guanghu-lang-sg-001': 'BS-SG-001',
'VM-0-16-ubuntu': 'BS-GZ-006',
};
for (const [host, id] of Object.entries(map)) {
if (HOSTNAME.includes(host) || host.includes(HOSTNAME) || HOSTNAME === host) return id;
}
return HOSTNAME;
}
const SERVER_ID = getServerId();
// ═══════════════════════════════════════
// Token → 人格体身份映射
// ═══════════════════════════════════════
function loadTokens() {
const tf = path.join(DATA_DIR, 'tokens.json');
if (fs.existsSync(tf)) {
try {
const tokens = JSON.parse(fs.readFileSync(tf, 'utf-8'));
if (process.env.ZHULAN_API_TOKEN) tokens[process.env.ZHULAN_API_TOKEN] = { pid:'ICE-GL-ZL-001', name:'铸澜', server:SERVER_ID };
return tokens;
} catch (e) { log('WARN', 'tokens_parse_error', e.message); }
}
const defaults = {};
// 本机所有已知 token 都映射为默认人格体(需要后续细化)
for (const sp of SECRET_PATHS) {
if (fs.existsSync(sp)) {
const t = fs.readFileSync(sp, 'utf-8').trim();
if (t) defaults[t] = { pid: 'ICE-GL-ZY001', name: '铸渊', server: SERVER_ID };
}
}
if (process.env.ZHULAN_API_TOKEN) defaults[process.env.ZHULAN_API_TOKEN] = { pid:'ICE-GL-ZL-001', name:'铸澜', server:SERVER_ID };
return defaults;
}
// ═══════════════════════════════════════
// SMTP 配置
// ═══════════════════════════════════════
const SMTP = {
host: 'smtp.qq.com',
port: 465,
user: process.env.SMTP_USER || '',
pass: process.env.QQ_SMTP_AUTH_CODE || '',
};
// ═══════════════════════════════════════
// 运行时状态
// ═══════════════════════════════════════
const pendingOps = {}; // { challenge_id: { code, expires, op_type, action, caller, target_email, target_name, server } }
const rateLimitMap = {}; // { ip: [timestamp, ...] }
// ═══════════════════════════════════════
// 日志
// ═══════════════════════════════════════
function log(level, action, detail) {
const ts = new Date().toISOString();
const line = '[' + ts + '] [' + level + '] ' + action + (detail ? ' | ' + detail : '');
console.log(line);
try {
const lf = path.join(DATA_DIR, 'engine.log');
fs.appendFileSync(lf, line + '\n');
} catch (e) { /* ignore */ }
}
// ═══════════════════════════════════════
// 加载白名单
// ═══════════════════════════════════════
function loadWhitelist() {
const wf = path.join(DATA_DIR, 'whitelist.json');
const required = [
{ server:SERVER_ID, human:'ICE-GL∞', email:process.env.BINGSHUO_AUTH_EMAIL || '', personalities:['ICE-GL-ZL-001'], bound_servers:[SERVER_ID] },
{ server:SERVER_ID, human:'苍耳', human_id:'TCS-GL-009', email:process.env.CANGER_AUTH_EMAIL || '', personalities:['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers:[SERVER_ID] }
];
if (fs.existsSync(wf)) {
try {
const whitelist = JSON.parse(fs.readFileSync(wf, 'utf-8'));
whitelist.triads = Array.isArray(whitelist.triads) ? whitelist.triads : [];
for (const item of required) {
const found = whitelist.triads.find(t => t.server === item.server && (t.human_id === item.human_id || t.human === item.human));
if (!found) whitelist.triads.push(item);
else {
if (item.email) found.email = item.email;
found.personalities = [...new Set([...(found.personalities || []), ...item.personalities])];
found.bound_servers = [...new Set([...(found.bound_servers || []), ...item.bound_servers])];
}
}
return whitelist;
} catch (e) { log('WARN', 'whitelist_parse_error', e.message); }
}
// 默认白名单:冰朔 + 之之
return {
triads: [
{ server: SERVER_ID, human: 'ICE-GL∞', email: process.env.BINGSHUO_AUTH_EMAIL || '', personalities: ['ICE-GL-ZY001','ICE-GL-ZL-001'], bound_servers: [SERVER_ID] },
{ server: SERVER_ID, human: '苍耳', human_id: 'TCS-GL-009', email: process.env.CANGER_AUTH_EMAIL || '', personalities: ['PTS-VA-001-EED','ICE-GL-CA001'], bound_servers: [SERVER_ID] },
],
pending_ops: {}
};
}
// ═══════════════════════════════════════
// 操作类型定义
// ═══════════════════════════════════════
const OP_TYPES = {
'code-repo': { label: '操作代码仓库', risk: 'medium', color: '#4ec9b0', icon: '📦' },
'system-arch': { label: '操作系统架构', risk: 'high', color: '#e06c75', icon: '⚡' },
'api-access': { label: '调用API', risk: 'medium', color: '#61afef', icon: '🔌' },
};
// ═══════════════════════════════════════
// 工具函数
// ═══════════════════════════════════════
function jr(res, code, data) {
res.writeHead(code, {
'Content-Type': 'application/json; charset=utf-8',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(data));
}
function parseBody(req) {
return new Promise((resolve) => {
let body = '';
req.on('data', c => body += c);
req.on('end', () => {
try { resolve(JSON.parse(body)); } catch (e) { resolve(null); }
});
});
}
function generateCode() {
return Array.from({ length: 6 }, () => crypto.randomInt(0, 10)).join('');
}
function fmtUptime(s) {
const d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
const parts = [];
if (d > 0) parts.push(d + 'd');
if (h > 0) parts.push(h + 'h');
parts.push(m + 'm');
return parts.join(' ');
}
function fmtBytes(b) {
if (b < 1024) return b + ' B';
if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
if (b < 1073741824) return (b / 1048576).toFixed(1) + ' MB';
return (b / 1073741824).toFixed(1) + ' GB';
}
function runAuthorizedAction(actionId, timeout) {
const action = getAction(actionId);
if (!action) return Promise.resolve({ code: 126, stdout: '', stderr: 'action_not_allowed' });
return action.run(timeout);
}
// ═══════════════════════════════════════
// 速率限制
// ═══════════════════════════════════════
function checkRate(ip) {
const now = Date.now() / 1000;
if (!rateLimitMap[ip]) rateLimitMap[ip] = [];
rateLimitMap[ip] = rateLimitMap[ip].filter(t => now - t < RATE_LIMIT_WINDOW);
if (rateLimitMap[ip].length >= MAX_REQUESTS) return false;
rateLimitMap[ip].push(now);
return true;
}
// ═══════════════════════════════════════
// 身份认证 (Token → 人格体身份)
// ═══════════════════════════════════════
function auth(headers) {
const authHeader = headers['authorization'] || headers['Authorization'] || '';
const bearerMatch = authHeader.match(/^Bearer\s+(.+)$/i);
if (!bearerMatch) return { ok: false, reason: '缺少 Authorization: Bearer <token>' };
const token = bearerMatch[1];
const tokens = loadTokens();
const identity = tokens[token];
if (!identity) {
// 检查是否匹配本机的主密钥
if (token === API_SECRET) {
return { ok: true, identity: { pid: 'ICE-GL-ZY001', name: '铸渊(主密钥)', server: SERVER_ID } };
}
return { ok: false, reason: 'Token 未注册 · 请先通过 /auth/register 注册人格体身份' };
}
return { ok: true, identity: identity };
}
// ═══════════════════════════════════════
// 白名单校验 (服务器+人+人格体 三元组)
// ═══════════════════════════════════════
function checkWhitelist(sourceServer, personalityId) {
const whitelist = loadWhitelist();
const matches = [];
for (const triad of whitelist.triads) {
// 来源服务器必须在 bound_servers 中
if (!triad.bound_servers.includes(sourceServer)) continue;
// 人格体匹配:* 通配 或 精确匹配
if (triad.personalities.includes('*') || triad.personalities.includes(personalityId)) {
matches.push(triad);
}
}
if (matches.length === 0) {
return { ok: false, reason: '未注册的三元组 · 服务器 ' + sourceServer + ' + 人格体 ' + personalityId + ' 不在白名单中' };
}
return { ok: true, triad: matches[0] };
}
// ═══════════════════════════════════════
// 发送验证码邮件 (Node.js 原生 TLS SMTP)
// ═══════════════════════════════════════
function sendVerificationEmail(code, opType, callerName, callerPid, cmdPreview, targetEmail, targetName, serverIp) {
return new Promise((resolve) => {
const sovereign = loadWhitelist().triads.find(t => t.human === 'ICE-GL∞' && t.email);
const smtpUser = SMTP.user || (sovereign && sovereign.email) || '';
if (!SMTP.pass || !smtpUser) {
log('WARN', 'email_skip', 'SMTP 发件账号或授权码未配置');
resolve(false);
return;
}
const opInfo = OP_TYPES[opType] || OP_TYPES['code-repo'];
// 构建 MIME 邮件
const boundary = '----GuanghuBoundary' + crypto.randomBytes(8).toString('hex');
const htmlBody = `<!DOCTYPE html>
<html lang="zh">
<head><meta charset="utf-8"></head>
<body style="margin:0;padding:0;background:#0a1628;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0a1628;padding:40px 0">
<tr><td align="center">
<table width="520" cellpadding="0" cellspacing="0" style="background:linear-gradient(135deg,#152238 0%,#1a2d4a 100%);border-radius:16px;overflow:hidden;border:1px solid #2a3f5f">
<tr><td style="padding:32px 32px 20px;text-align:center;border-bottom:1px solid #2a3f5f">
<div style="font-size:13px;color:${opInfo.color};letter-spacing:3px;text-transform:uppercase;margin-bottom:8px">ICE-GL∞ 光湖语言系统 · Gatekeeper v3</div>
<div style="font-size:20px;color:#e0e8f0;font-weight:600">${opInfo.icon} ${opInfo.label} · 验证码</div>
</td></tr>
<tr><td style="padding:24px 32px">
<table width="100%" cellpadding="0" cellspacing="0" style="background:#0d1b2e;border-radius:8px;border:1px solid #2a3f5f">
<tr><td style="padding:14px 16px;font-size:14px">
<div style="color:#64748b;font-size:12px">调用者</div>
<div style="color:#e0e8f0;font-weight:600">${callerName} (${callerPid})</div>
<div style="color:#64748b;font-size:12px;margin-top:8px">操作类型</div>
<div style="color:${opInfo.color};font-weight:600">${opInfo.icon} ${opInfo.label} · 风险: ${opInfo.risk === 'high' ? '⚠️ 高' : '🟡 中'}</div>
<div style="color:#64748b;font-size:12px;margin-top:8px">命令预览</div>
<div style="color:#8899aa;font-family:monospace;font-size:13px;background:#060d18;padding:8px 12px;border-radius:6px;margin-top:4px;word-break:break-all">${cmdPreview}</div>
<div style="color:#64748b;font-size:12px;margin-top:8px">来源服务器</div>
<div style="color:#e0e8f0">${serverIp} · ${SERVER_ID}</div>
</td></tr>
</table>
<div style="text-align:center;margin:24px 0">
<div style="font-size:12px;color:#8899aa;margin-bottom:8px">验证码 · 5 分钟内有效</div>
<div style="font-size:38px;font-weight:bold;color:${opInfo.color};letter-spacing:8px;background:#0d1b2e;padding:14px 28px;border-radius:8px;display:inline-block;border:2px dashed ${opInfo.color}">${code}</div>
</div>
<div style="background:${opInfo.risk === 'high' ? '#3d1a1a' : '#1a2d1a'};border-left:3px solid ${opInfo.color};padding:10px 14px;border-radius:4px;font-size:13px;color:${opInfo.risk === 'high' ? '#e06c75' : '#4ec9b0'}">
${opInfo.risk === 'high' ? '⚠️ 高权限操作 · 确认前请仔细检查命令内容' : '💡 将此验证码发给铸渊确认 → 释放操作权限 · 仅当次有效'}
</div>
</td></tr>
<tr><td style="background:#0a1628;padding:14px 32px;text-align:center">
<div style="font-size:11px;color:#556677">ICE-GL∞ 光湖语言系统 · 小湖灯自动发送 · 国作登字-2026-A-00037559</div>
</td></tr>
</table>
</td></tr>
</table>
</body>
</html>`;
const plainBody = `光湖语言系统 · Gatekeeper v3 · 操作授权\n
操作类型: ${opInfo.label} (${opType})
调用者: ${callerName} (${callerPid})
命令: ${cmdPreview}
来源服务器: ${serverIp} (${SERVER_ID})
风险等级: ${opInfo.risk === 'high' ? '高' : '中'}
\n验证码: ${code}
有效期: 5 分钟
\n将此验证码发给铸渊确认 → 释放操作权限
如非本人操作,请忽略。
---
ICE-GL∞ 光湖语言系统 · 小湖灯自动发送
国作登字-2026-A-00037559`;
const rawEmail =
`From: 光湖小湖灯 <${smtpUser}>\r\n` +
`To: ${targetName} <${targetEmail}>\r\n` +
`Subject: =?UTF-8?B?${Buffer.from(`${opInfo.icon} Gatekeeper授权 · ${callerName} · ${opInfo.label}`, 'utf-8').toString('base64')}?=\r\n` +
`MIME-Version: 1.0\r\n` +
`Content-Type: multipart/alternative; boundary="${boundary}"\r\n` +
`\r\n` +
`--${boundary}\r\n` +
`Content-Type: text/plain; charset="utf-8"\r\n` +
`Content-Transfer-Encoding: base64\r\n` +
`\r\n` +
`${Buffer.from(plainBody, 'utf-8').toString('base64')}\r\n` +
`--${boundary}\r\n` +
`Content-Type: text/html; charset="utf-8"\r\n` +
`Content-Transfer-Encoding: base64\r\n` +
`\r\n` +
`${Buffer.from(htmlBody, 'utf-8').toString('base64')}\r\n` +
`--${boundary}--\r\n` +
`.\r\n`;
const authPlain = `\0${smtpUser}\0${SMTP.pass}`;
const socket = tls.connect({ host: SMTP.host, port: SMTP.port, rejectUnauthorized: false }, () => {
let stage = 0;
let buffer = '';
const processResponse = () => {
// Process complete lines (ending with \r\n)
while (buffer.includes('\r\n')) {
const idx = buffer.indexOf('\r\n');
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 2);
if (!line) continue;
const code = parseInt(line.slice(0, 3)) || 0;
const isLast = line.length > 3 && line[3] === ' '; // '250 OK' vs '250-AUTH'
if (stage === 0) {
if (code === 220) { stage = 1; socket.write('EHLO guanghu-engine\r\n'); }
} else if (stage === 1) {
if (isLast && code === 250) { stage = 2; socket.write('AUTH PLAIN ' + Buffer.from(authPlain).toString('base64') + '\r\n'); }
} else if (stage === 2) {
if (code === 235) { stage = 3; socket.write('MAIL FROM:<' + smtpUser + '>\r\n'); }
else if (code >= 500) { log('WARN', 'smtp_auth_failed', line); socket.end(); resolve(false); return; }
} else if (stage === 3) {
if (code === 250) { stage = 4; socket.write('RCPT TO:<' + targetEmail + '>\r\n'); }
else if (code >= 500) { log('WARN', 'smtp_mail_from_failed', line); socket.end(); resolve(false); return; }
} else if (stage === 4) {
if (code === 250) { stage = 5; socket.write('DATA\r\n'); }
else if (code >= 500) { log('WARN', 'smtp_rcpt_failed', line); socket.end(); resolve(false); return; }
} else if (stage === 5) {
if (code === 354) { socket.write(rawEmail); stage = 6; }
} else if (stage === 6) {
if (code === 250) {
socket.write('QUIT\r\n');
socket.end();
log('INFO', 'email_sent', 'to=' + targetEmail + ' op=' + opType);
resolve(true);
return;
}
}
if (code >= 500 && stage > 0 && stage !== 2) {
log('WARN', 'smtp_error', 'stage=' + stage + ' line=' + line.slice(0, 100));
socket.end();
resolve(false);
return;
}
}
};
socket.on('data', (data) => {
buffer += data.toString();
processResponse();
});
socket.on('error', (err) => {
log('WARN', 'smtp_socket_error', err.message);
resolve(false);
});
});
socket.on('error', (err) => {
log('WARN', 'smtp_connect_error', err.message);
resolve(false);
});
socket.setTimeout(15000, () => {
log('WARN', 'smtp_timeout', '');
socket.destroy();
resolve(false);
});
});
}
// ═══════════════════════════════════════
// 清理过期操作
// ═══════════════════════════════════════
function cleanExpiredOps() {
const now = Date.now() / 1000;
for (const [cid, op] of Object.entries(pendingOps)) {
if (op.expires < now) delete pendingOps[cid];
}
}
// ═══════════════════════════════════════
// HTTP 服务器
// ═══════════════════════════════════════
const server = http.createServer(async (req, res) => {
if (req.method === 'OPTIONS') { jr(res, 204, {}); return; }
const ip = req.socket.remoteAddress || 'unknown';
if (!checkRate(ip)) { log('WARN', 'rate_limit', ip); jr(res, 429, { error: '请求过于频繁 · 每分钟最多 ' + MAX_REQUESTS + ' 次' }); return; }
const url = new URL(req.url, 'http://' + ((req.headers.host) || 'localhost'));
const route = url.pathname;
// ═══ 无需认证的端点 ═══
if (req.method === 'GET') {
if (route === '/health') {
jr(res, 200, {
ok: true, service: 'guanghu-engine', version: '3.2.0',
uptime: process.uptime().toFixed(0) + 's',
timestamp: new Date().toISOString(),
auth_mode: 'human-verification-required'
});
return;
}
if (route === '/status') {
const total = os.totalmem(), free = os.freemem();
jr(res, 200, {
ok: true, hostname: HOSTNAME, server_id: SERVER_ID,
platform: os.platform(), arch: os.arch(), cpus: os.cpus().length,
uptime: os.uptime(), uptime_str: fmtUptime(os.uptime()),
memory: { total: fmtBytes(total), free: fmtBytes(free), used: fmtBytes(total - free), usage: ((total - free) / total * 100).toFixed(1) + '%' },
load: os.loadavg().map(n => +n.toFixed(2)),
engine: { version: '3.2.0', port: PORT, uptime: process.uptime().toFixed(0) + 's' }
});
return;
}
if (route === '/ping') { jr(res, 200, { ok: true, pong: true }); return; }
// ═══ GET /auth/whitelist — 查看白名单(需认证) ═══
if (route === '/auth/whitelist') {
const authResult = auth(req.headers);
if (!authResult.ok) { jr(res, 401, { error: authResult.reason }); return; }
const whitelist = loadWhitelist();
jr(res, 200, {
ok: true,
server_id: SERVER_ID,
triads: whitelist.triads.map(t => ({
server: t.server,
human: t.human,
personalities: t.personalities,
bound_servers: t.bound_servers
}))
});
return;
}
}
// ═══ 需要认证的 POST 端点 ═══
if (req.method !== 'POST') { jr(res, 405, { error: '只接受 POST 请求 · 可用端点: /auth/request, /auth/confirm, /auth/register, /auth/whitelist (GET或POST), /health (GET), /status (GET), /ping (GET)' }); return; }
const authResult = auth(req.headers);
if (!authResult.ok) {
log('WARN', 'auth_failed', authResult.reason);
jr(res, 401, { error: authResult.reason });
return;
}
const identity = authResult.identity;
log('INFO', 'auth_ok', identity.pid + ' (' + identity.name + ')');
const body = await parseBody(req);
if (!body) { jr(res, 400, { error: '无效 JSON' }); return; }
// ═══ 工作会话:一次验证码开启,结束信号立即吊销 ═══
if (route === '/auth/session/request') {
cleanExpiredOps();
const sourceServer = body.source_server || identity.server || SERVER_ID;
const scopes = Array.isArray(body.scopes) ? [...new Set(body.scopes)] : ['code-repo'];
const action = getAction(body.action);
if (!action) { jr(res, 400, { error: '必须指定已登记 action', available: listActions() }); return; }
if (!scopes.includes(action.scope)) { jr(res, 400, { error: 'action 与 scopes 不匹配', required_scope: action.scope }); return; }
if (!scopes.length || scopes.some(s => !OP_TYPES[s])) { jr(res, 400, { error: '无效 scopes', available: Object.keys(OP_TYPES) }); return; }
const wlResult = checkWhitelist(sourceServer, identity.pid);
if (!wlResult.ok) { jr(res, 403, { error: wlResult.reason }); return; }
const triad = wlResult.triad;
const code = generateCode(), challengeId = crypto.randomBytes(16).toString('hex');
pendingOps[challengeId] = { code, expires: Date.now()/1000 + CODE_TTL, is_session: true, action: body.action, scopes, caller: identity, server: sourceServer, target_email: triad.email, target_name: triad.human };
const sent = await sendVerificationEmail(code, scopes[0], identity.name, identity.pid, '开启工作会话 · 范围: ' + scopes.join(', '), triad.email, triad.human, SERVER_ID);
jr(res, 200, { ok:true, challenge_id:challengeId, email_sent:sent, email_mode:'whitelist', scopes, expires_in:CODE_TTL, message:'验证码已发送到固定登记邮箱' });
return;
}
if (route === '/auth/session/confirm') {
cleanExpiredOps();
const op = pendingOps[body.challenge_id || ''];
if (!op || !op.is_session) { jr(res, 404, { error:'会话挑战不存在或已过期' }); return; }
if (op.caller.pid !== identity.pid) { jr(res, 403, { error:'会话身份不匹配' }); return; }
if (op.code !== String(body.code || '')) { jr(res, 403, { error:'验证码错误' }); return; }
delete pendingOps[body.challenge_id];
const created = sessions.create(identity, op.server, op.scopes, [op.action]);
log('INFO','session_started','pid='+identity.pid+' scopes='+op.scopes.join(','));
jr(res,200,{ok:true,session_token:created.token,scopes:op.scopes,expires_in:created.expires_in,idle_timeout:created.idle_timeout,message:'工作会话已开启'});
return;
}
if (route === '/auth/session/exec') {
const sessionToken = req.headers['x-session-token'] || body.session_token || '';
const opType = body.op_type || 'code-repo';
const sourceServer = body.source_server || identity.server || SERVER_ID;
const action = getAction(body.action);
const verified = sessions.verify(sessionToken, identity, sourceServer, opType, body.action);
if (!verified.ok) { jr(res,403,{error:verified.reason}); return; }
if (!action) { jr(res,400,{error:'必须指定已登记 action',available:listActions()}); return; }
if (action.scope !== opType) { jr(res,400,{error:'action 与 op_type 不匹配',required_scope:action.scope}); return; }
const result = await runAuthorizedAction(body.action, body.timeout);
log('INFO','session_exec','pid='+identity.pid+' op='+opType+' code='+result.code);
jr(res,200,{ok:true,executed:true,session:true,exit_code:result.code,stdout:result.stdout,stderr:result.stderr});
return;
}
if (route === '/auth/session/end') {
const sessionToken = req.headers['x-session-token'] || body.session_token || '';
const ended = sessions.end(sessionToken, identity);
log('INFO','session_ended','pid='+identity.pid+' ended='+ended);
jr(res,ended?200:404,{ok:ended,message:ended?'工作会话已结束':'会话不存在'});
return;
}
// ═══ /auth/request — 发起操作请求 ═══
if (route === '/auth/request') {
cleanExpiredOps();
const opType = body.op_type || 'code-repo';
const description = body.description || '未提供描述';
const sourceServer = body.source_server || identity.server || SERVER_ID;
if (body.email || body.target_name) { jr(res, 400, { error: '禁止客户端指定邮箱 · 请先注册服务器白名单' }); return; }
if (!OP_TYPES[opType]) {
jr(res, 400, { error: '未知操作类型: ' + opType, available: Object.keys(OP_TYPES) });
return;
}
const action = getAction(body.action);
if (!action) { jr(res, 400, { error: '必须指定已登记 action', available: listActions() }); return; }
if (action.scope !== opType) { jr(res, 400, { error: 'action 与 op_type 不匹配', required_scope: action.scope }); return; }
let targetEmail, targetName;
{
const wlResult = checkWhitelist(sourceServer, identity.pid);
if (!wlResult.ok) {
log('WARN', 'whitelist_denied', wlResult.reason);
// 不在白名单也不带邮箱 → 不响应(防恶意刷邮箱)
jr(res, 403, { error: wlResult.reason });
return;
}
const triad = wlResult.triad;
targetEmail = triad.email;
targetName = triad.human;
}
// 生成验证码
const code = generateCode();
const challengeId = crypto.randomBytes(16).toString('hex');
const now = Date.now() / 1000;
pendingOps[challengeId] = {
code: code,
expires: now + CODE_TTL,
op_type: opType,
action: body.action,
caller: { pid: identity.pid, name: identity.name },
target_email: targetEmail,
target_name: targetName,
server: sourceServer
};
// 发送验证码邮件
const cmdPreview = 'action=' + body.action;
const sent = await sendVerificationEmail(code, opType, identity.name, identity.pid, cmdPreview, targetEmail, targetName, SERVER_ID);
log('INFO', 'auth_request', 'challenge=' + challengeId.slice(0, 16) + ' op=' + opType + ' email_sent=' + sent);
jr(res, 200, {
ok: true,
challenge_id: challengeId,
email_sent: sent,
email_mode: 'whitelist',
caller: { pid: identity.pid, name: identity.name },
op_type: opType,
op_label: OP_TYPES[opType].label,
cmd_preview: cmdPreview,
expires_in: CODE_TTL,
message: '验证码已发送到 ' + targetEmail + ',请查收后通过 /auth/confirm 确认操作'
});
return;
}
// ═══ /auth/confirm — 确认验证码并执行 ═══
if (route === '/auth/confirm') {
cleanExpiredOps();
const challengeId = body.challenge_id || '';
const code = body.code || '';
if (!challengeId || !code) {
jr(res, 400, { error: '缺少 challenge_id 或 code' });
return;
}
if (!pendingOps[challengeId]) {
jr(res, 404, { error: 'challenge_id 不存在或已过期' });
return;
}
const op = pendingOps[challengeId];
const now = Date.now() / 1000;
if (now > op.expires) {
delete pendingOps[challengeId];
jr(res, 410, { error: '验证码已过期 · 请重新发起 /auth/request' });
return;
}
if (op.code !== code) {
log('WARN', 'auth_confirm_wrong_code', challengeId.slice(0, 16));
jr(res, 403, { error: '验证码错误' });
return;
}
// 验证通过 → 执行命令
delete pendingOps[challengeId];
log('INFO', 'auth_confirm_ok', challengeId.slice(0, 16) + ' op=' + op.op_type + ' caller=' + op.caller.pid);
const result = await runAuthorizedAction(op.action, body.timeout);
jr(res, 200, {
ok: true,
executed: true,
op_type: op.op_type,
action: op.action,
caller: op.caller,
exit_code: result.code,
stdout: result.stdout,
stderr: result.stderr,
message: '命令已执行 · 操作完成'
});
return;
}
// ═══ /auth/whitelist — 查看白名单 ═══
if (route === '/auth/whitelist') {
const whitelist = loadWhitelist();
jr(res, 200, {
ok: true,
server_id: SERVER_ID,
triads: whitelist.triads.map(t => ({
server: t.server,
human: t.human,
personalities: t.personalities,
bound_servers: t.bound_servers
}))
});
return;
}
// ═══ /auth/register — 注册新三元组(需主权者验证码) ═══
if (route === '/auth/register') {
const newServer = body.server || '';
const newHuman = body.human || '';
const newEmail = body.email || '';
const newPersonalities = body.personalities || [];
if (!newServer || !newHuman || !newEmail) {
jr(res, 400, { error: '缺少必要字段: server, human, email' });
return;
}
// 注册也需要验证码(发给主权者 ICE-GL∞
const whitelist = loadWhitelist();
const sovereign = whitelist.triads.find(t => t.human === 'ICE-GL∞');
if (!sovereign) {
jr(res, 500, { error: '主权者信息缺失' });
return;
}
// 生成验证码发到主权者邮箱
const regCode = generateCode();
const regCid = crypto.randomBytes(16).toString('hex');
pendingOps[regCid] = {
code: regCode,
expires: Date.now() / 1000 + CODE_TTL,
op_type: 'system-arch',
cmd: 'REGISTER:' + JSON.stringify({ server: newServer, human: newHuman, email: newEmail, personalities: newPersonalities }),
caller: identity,
target_email: sovereign.email,
target_name: sovereign.human,
server: SERVER_ID,
is_register: true,
register_data: { server: newServer, human: newHuman, email: newEmail, personalities: newPersonalities }
};
const sent = await sendVerificationEmail(regCode, 'system-arch', identity.name, identity.pid,
'注册新三元组: ' + newServer + ' + ' + newHuman + ' + ' + newPersonalities.join(','),
sovereign.email, sovereign.human, SERVER_ID);
jr(res, 200, {
ok: true,
challenge_id: regCid,
email_sent: sent,
message: '注册请求验证码已发送到主权者邮箱 ' + sovereign.email + ',确认后生效'
});
return;
}
// ═══ /exec — 旧接口兼容(自动转发到验证码流程) ═══
if (route === '/exec') {
if (!body.action) { jr(res, 400, { error: '缺少 action 参数' }); return; }
// v3 安全策略: /exec 不再直接执行,返回提示要求走验证码流程
log('WARN', 'exec_blocked_v3', 'caller=' + identity.pid + ' 尝试绕过验证码调用 /exec');
jr(res, 403, {
error: 'Gatekeeper v3 安全策略: /exec 已停用 · 请使用分层授权流程',
required_flow: 'POST /auth/request → 邮箱验证码 → POST /auth/confirm',
help: '将原有的 /exec 请求改为: ① POST /auth/request (action + op_type) → ② 查收邮箱验证码 → ③ POST /auth/confirm (challenge_id + code)',
example: {
step1: 'curl -X POST http://' + SERVER_ID + ':' + PORT + '/auth/request -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d \'{"action":"sync-status","op_type":"code-repo"}\'',
step2: 'curl -X POST http://' + SERVER_ID + ':' + PORT + '/auth/confirm -H "Authorization: Bearer <token>" -H "Content-Type: application/json" -d \'{"challenge_id":"<从step1返回>","code":"<邮箱验证码>"}\''
}
});
return;
}
// ═══ /hlpd — HLDP 模块执行(同样需要验证码) ═══
if (route === '/hlpd') {
if (!body.hlpd) { jr(res, 400, { error: '缺少 hlpd 参数' }); return; }
log('WARN', 'hlpd_blocked_v3', 'HLDP 执行需要验证码授权');
jr(res, 403, {
error: 'Gatekeeper v3: HLDP 执行需要验证码授权',
required_flow: 'POST /auth/request → 邮箱验证码 → POST /auth/confirm'
});
return;
}
// ═══ /file/read /file/write /file/list — 文件操作(只读无需验证,写入需要) ═══
if (route === '/file/read') {
if (!body.path) { jr(res, 400, { error: '缺少 path 参数' }); return; }
log('INFO', 'file_read', body.path);
try {
const content = fs.readFileSync(body.path, 'utf-8');
const stat = fs.statSync(body.path);
jr(res, 200, { ok: true, path: body.path, size: stat.size, modified: stat.mtime.toISOString(), content: content.slice(0, MAX_OUTPUT) });
} catch (e) { jr(res, 404, { ok: false, error: e.message }); }
return;
}
if (route === '/file/list') {
const p = (body && body.path) || '.';
log('INFO', 'file_list', p);
try {
const items = fs.readdirSync(p, { withFileTypes: true });
const files = [];
for (const x of items) { files.push({ name: x.name, type: x.isDirectory() ? 'dir' : 'file' }); }
jr(res, 200, { ok: true, path: p, files: files });
} catch (e) { jr(res, 404, { ok: false, error: e.message }); }
return;
}
if (route === '/file/write') {
log('WARN', 'file_write_blocked_v3', '文件写入需要验证码授权');
jr(res, 403, {
error: 'Gatekeeper v3: 文件写入需要验证码授权',
required_flow: 'POST /auth/request { action: "已登记动作", op_type: "system-arch" } → 验证码 → /auth/confirm'
});
return;
}
// 未知路由
jr(res, 404, { error: '未知路径: ' + route, available: ['/auth/request', '/auth/confirm', '/auth/whitelist', '/auth/register', '/health', '/status', '/ping', '/file/read', '/file/list'] });
});
// ═══════════════════════════════════════
// 启动
// ═══════════════════════════════════════
server.on('error', (e) => {
if (e.code === 'EADDRINUSE') {
log('FATAL', 'eaddrinuse', '端口 ' + PORT + ' 已被占用');
console.error('❌ 端口 ' + PORT + ' 已被占用,无法启动。');
setTimeout(() => process.exit(1), 3000);
} else {
log('FATAL', 'server_error', e.message);
process.exit(1);
}
});
server.listen(PORT, HOST, () => {
log('INFO', 'startup', 'host=' + HOSTNAME + ' bind=' + HOST + ' port=' + PORT + ' v3.2.0 server_id=' + SERVER_ID);
console.log(' ⚔️ 光湖驱动引擎 v3.2 · 工作会话授权 · 已就绪');
console.log(' 主机名: ' + HOSTNAME + ' 标识: ' + SERVER_ID + ' 端口: ' + PORT);
console.log(' 安全策略: Token=身份标识 · 操作=验证码授权 · 白名单=三元组校验');
});
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', (e) => { log('ERROR', 'uncaught', e.message); });
process.on('unhandledRejection', (e) => { log('ERROR', 'unhandled', e.message); });