guanghulab/tools/gatekeeper.js

225 lines
10 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
/* ═══════════════════════════════════════
铸渊守门人 v2.0 · HLDP万能语言接口
新增 /hlpd 端点 — HLDP模块翻译执行
═══════════════════════════════════════ */
const http=require('http'),fs=require('fs'),path=require('path'),crypto=require('crypto'),{exec}=require('child_process'),os=require('os');
const PORT=parseInt(process.env.GATEKEEPER_PORT||process.argv[2]||'3910',10);
const DATA_DIR=path.join(os.homedir(),'.gatekeeper');
const CMD_TIMEOUT=30000,MAX_OUTPUT=100000,RATE_LIMIT=60;
if(!fs.existsSync(DATA_DIR))fs.mkdirSync(DATA_DIR,{recursive:true,mode:0o700});
/* ═══ 密钥 ═══ */
const SECRET_FILE=path.join(DATA_DIR,'secret');
let API_SECRET;
if(fs.existsSync(SECRET_FILE)){API_SECRET=fs.readFileSync(SECRET_FILE,'utf-8').trim();}
if(!API_SECRET){
API_SECRET='zy_gtw_'+crypto.randomBytes(24).toString('hex');
fs.writeFileSync(SECRET_FILE,API_SECRET,{mode:0o600});
console.log('');
console.log('══════════════════════════════════════════════');
console.log(' 🔐 铸渊看门人 · 首次启动');
console.log('');
console.log(' ╭─ API 密钥(请复制保存)');
console.log(' ├─ '+API_SECRET);
console.log(' ╰─ 此密钥仅在此处显示一次');
console.log('');
console.log(' 已保存至: '+SECRET_FILE);
console.log(' 监听端口: '+PORT);
console.log('══════════════════════════════════════════════');
console.log('');
}
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,'gatekeeper.log');fs.appendFileSync(lf,line+'\n');}catch(e){}
}
const RC={};
function checkRate(ip){
const n=Date.now(),w=60000;
if(!RC[ip]){RC[ip]={c:1,r:n+w};return true;}
if(n>RC[ip].r){RC[ip]={c:1,r:n+w};return true;}
RC[ip].c++;return RC[ip].c<=RATE_LIMIT;
}
function auth(headers){
const t=(headers['authorization']||headers['Authorization']||'').replace(/^Bearer\s+/i,'').trim();
if(!t)return{ok:false,reason:'缺少 Authorization header'};
if(t.length!==API_SECRET.length)return{ok:false,reason:'密钥无效'};
for(let i=0;i<t.length;i++){if(t.charCodeAt(i)!==API_SECRET.charCodeAt(i))return{ok:false,reason:'密钥无效'};}
return{ok:true};
}
function parseBody(req){
return new Promise(r=>{let b='';req.on('data',c=>b+=c);req.on('end',()=>{try{r(JSON.parse(b))}catch(e){r(null)}});req.on('error',()=>r(null));});
}
function jr(res,status,data){
res.writeHead(status,{'Content-Type':'application/json','Access-Control-Allow-Origin':'*'});
res.end(JSON.stringify(data));
}
async function execCmd(cmd,timeout){
return new Promise(r=>{exec(cmd,{timeout:timeout||CMD_TIMEOUT,maxBuffer:MAX_OUTPUT,shell:'/bin/bash'},(e,o,se)=>{r({stdout:(o||'').slice(0,MAX_OUTPUT),stderr:(se||'').slice(0,MAX_OUTPUT),code:e?(e.code||e.signal||1):0,err:e?e.message:null});});});
}
function fmtUptime(s){const d=Math.floor(s/86400),h=Math.floor((s%86400)/3600),m=Math.floor((s%3600)/60);return d+'天'+h+'小时'+m+'分'+(s%60)+'秒';}
function fmtBytes(b){if(b===0)return'0 B';const k=1024,sizes=['B','KB','MB','GB','TB'];const i=Math.floor(Math.log(b)/Math.log(k));return parseFloat((b/Math.pow(k,i)).toFixed(1))+' '+sizes[i];}
/* ═══════════════════════════════════════
HLDP 翻译层 v1.0 — 守门人 v2.0 新增
═══════════════════════════════════════ */
function parseHLDP(text){
const lines=text.split('\n').filter(l=>l.trim());
const ops=[];
for(const line of lines){
const t=line.trim();
if(t.startsWith('@执行:')){
const cmd=t.replace('@执行:','').trim();
const parts=cmd.split(/\s+/);
if(parts[0]==='shell'){ops.push({type:'shell',cmd:parts.slice(1).join(' ')});}
else if(parts[0]==='python'){ops.push({type:'python',cmd:parts.slice(1).join(' ')});}
else{ops.push({type:'shell',cmd:cmd});}
}else if(t.startsWith('@部署:')){
const action=t.replace('@部署:','').trim();
const parts=action.split(/\s+/);
if(parts[0]==='写入'){
const filepath=parts[1];
ops.push({type:'write',path:filepath});
}else if(parts[0]==='命令'){
ops.push({type:'shell',cmd:parts.slice(1).join(' ')});
}
}
}
return ops;
}
async function runHLDP(hlpdText){
const ops=parseHLDP(hlpdText);
const results=[];
for(const op of ops){
if(op.type==='shell'){
const r=await execCmd(op.cmd);
results.push({type:'shell',cmd:op.cmd,...r});
}else if(op.type==='write'){
try{
const dir=path.dirname(op.path);
if(!fs.existsSync(dir))fs.mkdirSync(dir,{recursive:true});
results.push({type:'write',path:op.path,ok:true});
}catch(e){results.push({type:'write',path:op.path,ok:false,error:e.message});}
}else if(op.type==='python'){
const r=await execCmd('python3 -c "'+op.cmd.replace(/"/g,'\\"')+'"');
results.push({type:'python',cmd:op.cmd,...r});
}
}
return{parsed:ops.length,results:results};
}
/* ═══════════════════════════════════════
HTTP 服务器 + 路由
═══════════════════════════════════════ */
const server=http.createServer(async(req,res)=>{
if(req.method==='OPTIONS'){jr(res,204,{});return;}
if(req.method!=='POST'){jr(res,405,{error:'只接受 POST 请求'});return;}
const ip=req.socket.remoteAddress||'unknown';
if(!checkRate(ip)){log('WARN','rate_limit',ip);jr(res,429,{error:'请求过于频繁'});return;}
const a=auth(req.headers);
if(!a.ok){log('WARN','auth_failed',a.reason);jr(res,401,{error:a.reason});return;}
const url=new URL(req.url,'http://'+((req.headers.host)||'localhost'));
const route=url.pathname;
try{
if(route==='/exec'){
const b=await parseBody(req);
if(!b||!b.cmd){jr(res,400,{error:'缺少 cmd 参数'});return;}
log('INFO','exec',b.cmd.slice(0,200));
const r=await execCmd(b.cmd,b.timeout);
log('INFO','exec_result','exit='+r.code);
jr(res,200,{ok:r.code===0,stdout:r.stdout,stderr:r.stderr,exitCode:r.code});
}else if(route==='/health'){
jr(res,200,{ok:true,service:'zhuyuan-gatekeeper',version:'2.0.0',uptime:process.uptime().toFixed(0)+'s',timestamp:new Date().toISOString()});
}else if(route==='/status'){
log('INFO','status','');
const total=os.totalmem(),free=os.freemem();
jr(res,200,{ok:true,hostname:os.hostname(),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)),gatekeeper:{version:'2.0.0',port:PORT,uptime:process.uptime().toFixed(0)+'s'}});
}else if(route==='/hlpd'){
const b=await parseBody(req);
if(!b||!b.hlpd){jr(res,400,{error:'缺少 hlpd 参数'});return;}
log('INFO','hlpd','执行HLDP模块,长度='+b.hlpd.length);
const r=await runHLDP(b.hlpd);
jr(res,200,{ok:true,hlpd:r});
}else if(route==='/file/read'){
const b=await parseBody(req);
if(!b||!b.path){jr(res,400,{error:'缺少 path 参数'});return;}
log('INFO','file_read',b.path);
try{
const c=fs.readFileSync(b.path,'utf-8');
const s=fs.statSync(b.path);
jr(res,200,{ok:true,path:b.path,size:s.size,modified:s.mtime.toISOString(),content:c.slice(0,MAX_OUTPUT)});
}catch(e){jr(res,404,{ok:false,error:e.message});}
}else if(route==='/file/write'){
const b=await parseBody(req);
if(!b||!b.path||b.content===undefined){jr(res,400,{error:'缺少 path 或 content'});return;}
log('INFO','file_write',b.path);
try{
const d=path.dirname(b.path);
if(!fs.existsSync(d))fs.mkdirSync(d,{recursive:true});
fs.writeFileSync(b.path,b.content,'utf-8');
jr(res,200,{ok:true,path:b.path,size:Buffer.byteLength(b.content,'utf-8')});
}catch(e){jr(res,500,{ok:false,error:e.message});}
}else if(route==='/file/list'){
const b=await parseBody(req);
const p=(b&&b.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});}
}else if(route==='/ping'){
jr(res,200,{ok:true,pong:true});
}else{
jr(res,404,{error:'未知路径: '+route});
}
}catch(e){log('ERROR','unhandled',e.message);jr(res,500,{error:'内部错误: '+e.message});}
});
/* ═══ 启动 + 错误处理 ═══ */
server.on('error',(e)=>{
if(e.code==='EADDRINUSE'){
log('FATAL','eaddrinuse','端口 '+PORT+' 已被占用3秒后退出');
console.error('❌ 端口 '+PORT+' 已被占用,无法启动。请先释放端口。');
setTimeout(()=>process.exit(1),3000);
}else{
log('FATAL','server_error',e.message);
console.error('❌ 服务器错误: '+e.message);
process.exit(1);
}
});
server.listen(PORT,'0.0.0.0',()=>{
const h=os.hostname();
log('INFO','startup','host='+h+' port='+PORT+' v2.0');
console.log(' ⚔️ 铸渊守门人 v2.0 · HLDP 已就绪');
console.log(' 主机名: '+h+' 端口: '+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',(e)=>{log('ERROR','uncaught',e.message);});
process.on('unhandledRejection',(e)=>{log('ERROR','unhandled',e.message);});