225 lines
10 KiB
JavaScript
225 lines
10 KiB
JavaScript
#!/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);});
|