2026-10-13 16:58:15 +02:00
|
|
|
'use strict';
|
|
|
|
|
const {spawn}=require('node:child_process');
|
|
|
|
|
const path=require('node:path');
|
|
|
|
|
const crypto=require('node:crypto');
|
|
|
|
|
const fs=require('node:fs');
|
|
|
|
|
const VERSION='safetynet-upload-lease-v1';
|
|
|
|
|
const LIMIT=10*1024*1024;
|
|
|
|
|
let disabled=false;
|
|
|
|
|
function trustedFile(p){
|
|
|
|
|
if(typeof p!=='string'||!path.win32.isAbsolute(p)||p.startsWith('\\\\'))return false;
|
|
|
|
|
try{const real=fs.realpathSync.native(p);return real.toLowerCase()===p.toLowerCase()&&fs.statSync(p).isFile();}catch{return false;}
|
|
|
|
|
}
|
|
|
|
|
function refused(){return Object.assign(new Error('Upload lease unavailable'),{code:'upload-lease-refused'});}
|
|
|
|
|
async function acquireUploadLease({paths,maxBytes=LIMIT,pythonPath,helperPath,onLost=()=>{},signal}){
|
2026-10-13 23:19:22 +02:00
|
|
|
// eslint-disable-next-line no-control-regex -- Explicitly reject control characters in security-sensitive input.
|
2026-10-13 16:58:15 +02:00
|
|
|
if(process.platform!=='win32'||!Array.isArray(paths)||paths.length<1||paths.length>16||!Number.isInteger(maxBytes)||maxBytes<1||maxBytes>LIMIT||typeof onLost!=='function'||![pythonPath,helperPath].every(p=>typeof p==='string'&&path.win32.isAbsolute(p)&&!p.startsWith('\\\\'))||!paths.every(p=>typeof p==='string'&&/^[A-Za-z]:\\[^\x00-\x1f:]*$/.test(p)&&p.length<=240&&path.win32.normalize(p)===p))throw refused();
|
|
|
|
|
if(disabled||signal?.aborted||!trustedFile(pythonPath)||!trustedFile(helperPath))throw refused();
|
|
|
|
|
const child=spawn(pythonPath,['-I','-B',helperPath],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR},shell:false});
|
|
|
|
|
let live=true,releasing=false,ready=false,buffer=Buffer.alloc(0),total=0,settled=false,releaseResolve,releaseReject,receipt;
|
|
|
|
|
let resolve,reject;
|
|
|
|
|
const acquired=new Promise((a,b)=>{resolve=a;reject=b;});
|
|
|
|
|
const limit=Math.ceil(maxBytes*4/3)+65536;
|
|
|
|
|
const timer=setTimeout(()=>fail(),30000);timer.unref();
|
|
|
|
|
const lost=()=>{if(live&&!releasing){try{onLost();}catch{} }live=false;};
|
|
|
|
|
function fail(){disabled=true;lost();clearTimeout(timer);if(!settled){settled=true;reject(refused());}releaseReject?.(refused());child.kill();}
|
|
|
|
|
const abort=()=>fail();signal?.addEventListener('abort',abort,{once:true});
|
|
|
|
|
child.on('error',fail);
|
|
|
|
|
child.on('exit',()=>{clearTimeout(timer);signal?.removeEventListener('abort',abort);if(!releasing)fail();else{live=false;releaseReject?.(refused());}});
|
|
|
|
|
child.stdin.on('error',fail);child.stderr.on('data',()=>fail());
|
|
|
|
|
child.stdout.on('data',chunk=>{
|
|
|
|
|
total+=chunk.length;if(total>limit){fail();return;}
|
|
|
|
|
buffer=Buffer.concat([buffer,chunk]);
|
|
|
|
|
let newline;
|
|
|
|
|
while((newline=buffer.indexOf(10))!==-1){
|
|
|
|
|
const line=buffer.subarray(0,newline);buffer=Buffer.from(buffer.subarray(newline+1));
|
|
|
|
|
try{
|
|
|
|
|
const msg=JSON.parse(line.toString('utf8'));
|
|
|
|
|
if(!ready&&JSON.stringify(Object.keys(msg).sort())===JSON.stringify(['code','ok','version'])&&msg.version===VERSION&&msg.ok===false&&msg.code==='upload-lease-refused'){
|
|
|
|
|
releasing=true;live=false;settled=true;clearTimeout(timer);reject(refused());continue;
|
|
|
|
|
}
|
|
|
|
|
if(ready){
|
|
|
|
|
if(!releasing||JSON.stringify(Object.keys(msg).sort())!==JSON.stringify(['released','version'])||msg.version!==VERSION||msg.released!==true)throw refused();
|
|
|
|
|
live=false;receipt=null;releaseResolve?.();releaseResolve=null;releaseReject=null;continue;
|
|
|
|
|
}
|
|
|
|
|
if(JSON.stringify(Object.keys(msg).sort())!==JSON.stringify(['files','handles','ok','version'])||msg.version!==VERSION||msg.ok!==true||!Array.isArray(msg.files)||msg.files.length!==paths.length||!Array.isArray(msg.handles)||msg.handles.length<2||msg.handles.length>4096||!msg.handles.every(v=>typeof v==='string'&&/^[1-9][0-9]{0,19}$/.test(v)&&BigInt(v)<2n**64n)||new Set(msg.handles).size!==msg.handles.length)throw refused();
|
|
|
|
|
receipt=msg.handles;
|
|
|
|
|
let bytesTotal=0;
|
|
|
|
|
const files=msg.files.map((f,i)=>{
|
|
|
|
|
if(JSON.stringify(Object.keys(f).sort())!==JSON.stringify(['bytes','fileId','path','sha256','size','volume'])||f.path!==paths[i]||!Number.isInteger(f.size)||f.size<0||!Number.isInteger(f.volume)||f.volume<0||f.volume>0xffffffff||typeof f.fileId!=='string'||!/^[a-f0-9]{16}$/.test(f.fileId)||typeof f.sha256!=='string'||!/^[a-f0-9]{64}$/.test(f.sha256)||typeof f.bytes!=='string')throw refused();
|
|
|
|
|
const bytes=Buffer.from(f.bytes,'base64');bytesTotal+=bytes.length;
|
|
|
|
|
if(bytesTotal>maxBytes||bytes.length!==f.size||bytes.toString('base64')!==f.bytes||crypto.createHash('sha256').update(bytes).digest('hex')!==f.sha256)throw refused();
|
|
|
|
|
return Object.freeze({...f,bytes});
|
|
|
|
|
});
|
|
|
|
|
ready=true;settled=true;clearTimeout(timer);
|
|
|
|
|
resolve({files:Object.freeze(files),isAlive:()=>live&&!releasing,release:()=>{
|
|
|
|
|
if(releasing)return Promise.reject(refused());
|
|
|
|
|
if(!live){
|
|
|
|
|
releasing=true;
|
|
|
|
|
return new Promise((a,b)=>{
|
|
|
|
|
const cleanup=spawn(pythonPath,['-I','-B',helperPath],{windowsHide:true,stdio:['pipe','pipe','pipe'],env:{SystemRoot:process.env.SystemRoot,WINDIR:process.env.WINDIR},shell:false});
|
|
|
|
|
const cleanupTimer=setTimeout(()=>{disabled=true;cleanup.kill();b(refused());},5000);cleanupTimer.unref();
|
|
|
|
|
let output='';cleanup.stdout.on('data',c=>{output+=c.toString('utf8');if(output.length>1024)cleanup.kill();});cleanup.stderr.on('data',()=>cleanup.kill());cleanup.on('error',()=>{clearTimeout(cleanupTimer);disabled=true;b(refused());});cleanup.on('exit',code=>{clearTimeout(cleanupTimer);try{const msg=JSON.parse(output);if(code!==0||msg.version!==VERSION||msg.released!==true||Object.keys(msg).length!==2)throw refused();receipt=null;a();}catch{disabled=true;b(refused());}});
|
|
|
|
|
cleanup.stdin.on('error',()=>b(refused()));cleanup.stdin.end(JSON.stringify({version:VERSION,parentPid:process.pid,closeHandles:receipt})+'\n');
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
releasing=true;
|
|
|
|
|
return new Promise((a,b)=>{const deadline=setTimeout(()=>fail(),5000);deadline.unref();releaseResolve=()=>{clearTimeout(deadline);a();};releaseReject=e=>{clearTimeout(deadline);b(e);};child.stdin.end('{"release":true}\n');});
|
|
|
|
|
}});
|
|
|
|
|
}catch{fail();}finally{line.fill(0);}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
child.stdin.write(JSON.stringify({version:VERSION,paths,maxBytes,parentPid:process.pid})+'\n');
|
|
|
|
|
return acquired;
|
|
|
|
|
}
|
|
|
|
|
module.exports={acquireUploadLease};
|