From e9a394007434f95af3974c75e839b9b59503188e Mon Sep 17 00:00:00 2001 From: bingshuo <565183519@qq.com> Date: Sat, 30 May 2026 18:08:14 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20gatekeeper=20v2.0=20with=20/hlpd=20HLDP?= =?UTF-8?q?=E7=BF=BB=E8=AF=91=E7=AB=AF=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tools/gatekeeper.js | 335 ++++++++++++++++++++++++-------------------- 1 file changed, 181 insertions(+), 154 deletions(-) diff --git a/tools/gatekeeper.js b/tools/gatekeeper.js index 2dd9fc6..1d765c9 100644 --- a/tools/gatekeeper.js +++ b/tools/gatekeeper.js @@ -6,182 +6,209 @@ 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.PORT||process.argv[2]||'3910',10),DATA=path.join(os.homedir(),'.gk'),TIMEOUT=30000,MAX=100000,RATE=60; +const PORT=parseInt(process.env.GATEKEEPER_PORT||process.argv[2]||'3910',10); +const DATA_DIR=path.join(os.homedir(),'.gk'); +const CMD_TIMEOUT=30000,MAX_OUTPUT=100000,RATE_LIMIT=60; -if(!fs.existsSync(DATA))fs.mkdirSync(DATA,{recursive:true,mode:0o700}); -const SF=path.join(DATA,'secret'); -let SECRET; -if(fs.existsSync(SF))SECRET=fs.readFileSync(SF,'utf-8').trim(); -if(!SECRET){SECRET='zy_gtw_'+crypto.randomBytes(24).toString('hex');fs.writeFileSync(SF,SECRET,{mode:0o600});} +if(!fs.existsSync(DATA_DIR))fs.mkdirSync(DATA_DIR,{recursive:true,mode:0o700}); -function log(a,d){const l=`[${new Date().toISOString()}] [${a}]${d?' | '+d:''}`;console.log(l);try{fs.appendFileSync(path.join(DATA,'gk.log'),l+'\n')}catch(e){}} +/* ═══ 密钥 ═══ */ +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,'gk.log');fs.appendFileSync(lf,line+'\n');}catch(e){} +} const RC={}; -function checkIP(ip){const n=Date.now(),w=60000;if(!RC[ip]){RC[ip]={c:1,r:n+w};return 1}if(n>RC[ip].r){RC[ip]={c:1,r:n+w};return 1}RC[ip].c++;return RC[ip].c<=RATE} -function auth(h){const t=(h['authorization']||h['Authorization']||'').replace(/^Bearer\s+/i,'').trim();if(!t)return 0;if(t.length!==SECRET.length)return 0;for(let i=0;i{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(r,s,d){r.writeHead(s,{'Content-Type':'application/json','Access-Control-Allow-Origin':'*'});r.end(JSON.stringify(d))} +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{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];} -async function execCmd(cmd,to){return new Promise(r=>{exec(cmd,{timeout:to||TIMEOUT,maxBuffer:MAX,shell:'/bin/bash'},(e,o,se)=>{r({stdout:(o||'').slice(0,MAX),stderr:(se||'').slice(0,MAX),code:e?(e.code||e.signal||1):0,err:e?e.message:null})})})} /* ═══════════════════════════════════════ HLDP 翻译层 v1.0 — 守门人 v2.0 新增 ═══════════════════════════════════════ */ -/* - 翻译规则: - @执行: shell → 提取缩进内容 → bash 执行 - @执行: python → 提取缩进内容 → python3 -c 执行 - @部署: 写入 <路径> → 提取缩进内容 → 写入文件 - @部署: 命令 → 直接执行 -*/ - -function parseHLDP(text) { - const results = []; - const lines = text.split('\n'); - let i = 0; - while (i < lines.length) { - const line = lines[i]; - // @执行: shell 或 @执行: python - const execMatch = line.match(/^@执行:\s*(shell|python)\s*$/); - if (execMatch) { - const lang = execMatch[1]; - let code = ''; - i++; - while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i].trim() === '')) { - code += (code ? '\n' : '') + lines[i].replace(/^ /, ''); - i++; +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]; + const contentIdx=text.indexOf('\n',text.indexOf(t)+t.length)+1; + const content=text.slice(contentIdx).replace(/^```[\s\S]*?\n/,'').replace(/\n```$/,''); + ops.push({type:'write',path:filepath,content:content}); + }else if(parts[0]==='命令'){ + ops.push({type:'shell',cmd:parts.slice(1).join(' ')}); } - if (code.trim()) { - if (lang === 'shell') { - results.push({ type: 'exec', cmd: code.trim() }); - } else if (lang === 'python') { - const safe = code.trim().replace(/'/g, "'\"'\"'"); - results.push({ type: 'exec', cmd: `python3 -c '${safe}'` }); - } - } - continue; } - // @部署: 写入 <路径> - const deployMatch = line.match(/^@部署:\s*写入\s+(.+)$/); - if (deployMatch) { - const filePath = deployMatch[1].trim(); - let content = ''; - i++; - while (i < lines.length && (lines[i].startsWith(' ') || lines[i].startsWith('\t') || lines[i].trim() === '')) { - content += (content ? '\n' : '') + lines[i].replace(/^ /, ''); - i++; - } - if (content.trim()) { - results.push({ type: 'write', path: filePath, content: content.trim() }); - } - continue; - } - // @部署: 命令 - const cmdMatch = line.match(/^@部署:\s*命令\s+(.+)$/); - if (cmdMatch) { - results.push({ type: 'exec', cmd: cmdMatch[1].trim() }); - } - i++; } - return results; + 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}); + fs.writeFileSync(op.path,op.content,'utf-8'); + results.push({type:'write',path:op.path,ok:true,size:Buffer.byteLength(op.content,'utf-8')}); + }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 服务器 + 路由 ═══════════════════════════════════════ */ -async function route(rt,rq,rs){ - const b=await pb(rq); - switch(rt){ - case '/exec': - if(!b||!b.cmd){jr(rs,400,{error:'no cmd'});return} - log('CMD',b.cmd.slice(0,200)); - const r=await execCmd(b.cmd,b.timeout); - jr(rs,200,{ok:r.code===0,stdout:r.stdout,stderr:r.stderr,code:r.code}); - return; +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;} - case '/hlpd': - if(!b||!b.hldp){jr(rs,400,{error:'no hldp'});return} - log('HLDP','解析中'); - const steps = parseHLDP(b.hldp); - const results = []; - for (let i=0; i+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}); } - const allOk = results.every(r => r.ok); - jr(rs, 200, { ok: allOk, steps: results.length, results }); - return; - - case '/status': - log('INFO','status'); - jr(rs,200,{ok:true,...await sysInfo()}); - return; - - case '/health': - jr(rs,200,{ok:true,svc:'gk',v:'2.0',up:process.uptime().toFixed(0)+'s',hldp:true,ts:new Date().toISOString()}); - return; - - case '/file/read': - if(!b||!b.path){jr(rs,400,{error:'no path'});return} - try{const c=fs.readFileSync(b.path,'utf-8'),s=fs.statSync(b.path);jr(rs,200,{ok:true,path:b.path,size:s.size,modified:s.mtime.toISOString(),content:c.slice(0,MAX)})}catch(e){jr(rs,404,{ok:false,error:e.message})} - return; - - case '/file/write': - if(!b||!b.path||b.content===undefined){jr(rs,400,{error:'need path+content'});return} - 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(rs,200,{ok:true,path:b.path,size:Buffer.byteLength(b.content,'utf-8')})}catch(e){jr(rs,500,{ok:false,error:e.message})} - return; - - case '/file/list': - try{const p=(b&&b.path)||'.',i=fs.readdirSync(p,{withFileTypes:true});jr(rs,200,{ok:true,path:p,files:i.map(x=>({name:x.name,type:x.isDirectory()?'dir':'file'})))}catch(e){jr(rs,404,{ok:false,error:e.message})} - return; - - case '/ping': - jr(rs,200,{ok:true,pong:true}); - return; - - default: - jr(rs,404,{error:'unknown:'+rt}); - } -} - -async function sysInfo(){const h=os.hostname(),u=Math.floor(os.uptime()),tm=os.totalmem(),fm=os.freemem(),l=os.loadavg(),c=os.cpus().length,pl=os.platform(),ar=os.arch();let di='';try{di=require('child_process').execSync('df -h /',{timeout:5000}).toString()}catch(e){}return{hostname:h,platform:pl,arch:ar,cpus:c,uptime:u,mem:{total:fb(tm),free:fb(fm),used:fb(tm-fm),pct:((tm-fm)/tm*100).toFixed(1)+'%'},load:l.map(n=>n.toFixed(2)),disk:di,gk:{v:'2.0',port:PORT,up:process.uptime().toFixed(0)+'s',hldp:true}}} -function fb(b){if(b===0)return'0 B';const k=1024,s=['B','KB','MB','GB','TB'],i=Math.floor(Math.log(b)/Math.log(k));return parseFloat((b/Math.pow(k,i)).toFixed(1))+' '+s[i]} - -const sv=http.createServer(async(rq,rs)=>{ - if(rq.method==='OPTIONS'){jr(rs,204,{});return} - if(rq.method!=='POST'){jr(rs,405,{error:'POST only'});return} - const ip=rq.socket.remoteAddress||''; - if(!checkIP(ip)){log('WARN','rate',ip);jr(rs,429,{error:'too fast'});return} - if(!auth(rq.headers)){log('WARN','auth','fail');jr(rs,401,{error:'bad key'});return} - const u=new URL(rq.url,'http://h'),rt=u.pathname; - try{await route(rt,rq,rs)}catch(e){log('ERR','route',e.message);jr(rs,500,{error:e.message})} + }catch(e){log('ERROR','unhandled',e.message);jr(res,500,{error:'内部错误: '+e.message});} }); -sv.listen(PORT,'0.0.0.0',()=>{ +server.listen(PORT,'0.0.0.0',()=>{ const h=os.hostname(); - console.log(`\n ⚔️ 铸渊守门人 v2.0 · HLDP语言接口\n ──────────────────────\n 主机: ${h}\n 端口: ${PORT}\n 新增: /hlpd — HLDP模块翻译执行\n 翻译: @执行 shell|python → 执行\n @部署 写入 → 文件写入\n ──────────────────────\n`); - log('UP',`v2.0 ${h}:${PORT} +HLDP`); + log('INFO','startup','host='+h+' port='+PORT+' v2.0'); + console.log(' ⚔️ 铸渊守门人 v2.0 · HLDP 已就绪'); + console.log(' 主机名: '+h+' 端口: '+PORT); }); -process.on('SIGTERM',()=>{log('DOWN','sigterm');sv.close(()=>process.exit(0))}); -process.on('SIGINT',()=>{log('DOWN','sigint');sv.close(()=>process.exit(0))}); -process.on('uncaughtException',e=>log('ERR','ex',e.message)); +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);});