n8n / scripts /sandbox-runner.js
subham
Sync local n8n deployment: refactor to single Dockerfile, add runners, sandbox, cleanup legacy compose/nginx
4c98a98
Raw History Blame Contribute Delete
4.67 kB
#!/usr/bin/env node
// sandbox-runner.js — rootless isolated execution (bwrap if available, else direct)
// Listens 127.0.0.1:9091 POST /run {code, language, timeoutMs}
const http = require('http');
const fs = require('fs');
const os = require('os');
const path = require('path');
const { spawn } = require('child_process');
const PORT = parseInt(process.env.SANDBOX_RUNNER_PORT || '9091', 10);
const TMP_BASE = process.env.SANDBOX_TMP || '/tmp/sandbox';
const PYTHON_BIN = process.env.PYTHON_BIN || '/opt/runners/task-runner-python/.venv/bin/python';
const NODE_BIN = process.env.NODE_BIN || '/usr/local/bin/node';
function hasBwrap(){ try{ fs.accessSync('/usr/bin/bwrap', fs.constants.X_OK); return true;}catch{ return false; } }
function canUseBwrap(){
if (!hasBwrap()) return false;
// test if unprivileged namespaces allowed (kernel)
try{
const { spawnSync } = require('child_process');
const r = spawnSync('/usr/bin/bwrap', ['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--','/bin/true'], {timeout:2000});
return r.status===0;
}catch{ return false; }
}
const USE_BWRAP = canUseBwrap();
console.log(`[sandbox-runner] bwrap=${USE_BWRAP} python=${PYTHON_BIN}`);
function runInSandbox(code, language, timeoutMs=30000){
return new Promise((resolve)=>{
const id = Math.random().toString(36).slice(2,9);
const dir = path.join(TMP_BASE, id);
fs.mkdirSync(dir, {recursive:true});
const timeout = Math.min(Math.max(timeoutMs, 1000), 120000);
let cmd, args, opts={};
// Write code file
if (language==='python' || language==='py'){
const f = path.join(dir,'script.py');
fs.writeFileSync(f, code);
if (USE_BWRAP){
cmd='/usr/bin/bwrap';
args=['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--bind',dir,dir,'--chdir',dir,'--', PYTHON_BIN, '-I','-B', f];
} else {
cmd=PYTHON_BIN; args=['-I','-B', f]; opts.cwd=dir;
}
} else {
// javascript / typescript
const f = path.join(dir,'script.js');
fs.writeFileSync(f, code);
if (USE_BWRAP){
cmd='/usr/bin/bwrap';
args=['--unshare-all','--die-with-parent','--ro-bind','/','/','--tmpfs','/tmp','--proc','/proc','--dev','/dev','--bind',dir,dir,'--chdir',dir,'--', NODE_BIN, '--disallow-code-generation-from-strings','--disable-proto=delete', f];
} else {
cmd=NODE_BIN; args=['--disallow-code-generation-from-strings','--disable-proto=delete', f]; opts.cwd=dir;
}
}
let stdout='', stderr='', killed=false;
const child = spawn(cmd, args, {...opts, timeout});
const timer = setTimeout(()=>{ killed=true; try{child.kill('SIGKILL');}catch{} }, timeout+500);
child.stdout.on('data',d=>{ stdout += d.toString().slice(0,200000); });
child.stderr.on('data',d=>{ stderr += d.toString().slice(0,200000); });
child.on('close',(code)=>{
clearTimeout(timer);
// cleanup
try{ fs.rmSync(dir,{recursive:true,force:true});}catch{}
resolve({
exitCode: killed? 124 : (code===null?1:code),
stdout: stdout.slice(0,100000),
stderr: stderr.slice(0,100000) + (killed? `\n[timeout ${timeout}ms]`:'' ),
executionId: id,
bwrap: USE_BWRAP
});
});
child.on('error',(e)=>{
clearTimeout(timer);
try{ fs.rmSync(dir,{recursive:true,force:true});}catch{}
resolve({exitCode:1, stdout:'', stderr: e.message, executionId:id});
});
});
}
const server=http.createServer((req,res)=>{
const url=new URL(req.url,`http://localhost:${PORT}`);
if ((url.pathname==='/'||url.pathname==='/healthz'||url.pathname==='/health') && req.method==='GET'){
res.writeHead(200,{'Content-Type':'application/json'}); res.end(JSON.stringify({status:'ok', bwrap:USE_BWRAP, pid:process.pid})); return;
}
if (url.pathname==='/run' && req.method==='POST'){
let body=''; req.on('data',c=>body+=c); req.on('end', async()=>{
try{
const {code, language, timeoutMs}=JSON.parse(body||'{}');
if(!code){ res.writeHead(400); res.end(JSON.stringify({error:'missing code'})); return;}
const r=await runInSandbox(code, language||'python', timeoutMs||30000);
res.writeHead(200,{'Content-Type':'application/json'}); res.end(JSON.stringify(r));
}catch(e){ res.writeHead(500); res.end(JSON.stringify({error:e.message}));}
}); return;
}
res.writeHead(404); res.end(JSON.stringify({error:'not found'}));
});
server.listen(PORT,'127.0.0.1',()=>console.log(`[sandbox-runner] listening 127.0.0.1:${PORT}`));
process.on('SIGTERM',()=>server.close(()=>process.exit(0)));