Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
@@ -0,0 +1,109 @@
|
||||
'use strict';
|
||||
|
||||
const {createHash}=require('node:crypto');
|
||||
const {performance}=require('node:perf_hooks');
|
||||
const digest=body=>createHash('sha256').update(body).digest('hex');
|
||||
class GateError extends Error {
|
||||
constructor(code) { super(code); this.name='GateError'; this.code=code; }
|
||||
}
|
||||
const fail=code=>{throw new GateError(code);};
|
||||
|
||||
function parseRequest(request) {
|
||||
const headers=Object.create(null);
|
||||
if(!Array.isArray(request.rawHeaders)||request.rawHeaders.length%2) fail('invalid-headers');
|
||||
for(let index=0;index<request.rawHeaders.length;index+=2) {
|
||||
const name=request.rawHeaders[index].toLowerCase(),value=request.rawHeaders[index+1];
|
||||
if(headers[name]!==undefined) fail('duplicate-header');
|
||||
headers[name]=value;
|
||||
}
|
||||
if(headers.upgrade!==undefined||headers['transfer-encoding']!==undefined||headers['content-encoding']!==undefined||headers.expect!==undefined) fail('unsupported-framing');
|
||||
if(headers.connection&&headers.connection.toLowerCase()!=='close'&&headers.connection.toLowerCase()!=='keep-alive') fail('unsupported-framing');
|
||||
if(!/^(GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS)$/.test(request.method)) fail('unsupported-method');
|
||||
let destination;
|
||||
try {destination=new URL(request.url);} catch {fail('invalid-destination');}
|
||||
// First gate supports explicit HTTP proxy requests only. TLS CONNECT,
|
||||
// streaming protocols and OS egress confinement need separate enforcement.
|
||||
if(destination.protocol!=='http:'||destination.username||destination.password||destination.hash) fail('invalid-destination');
|
||||
if(!headers.host||headers.host.toLowerCase()!==destination.host.toLowerCase()) fail('authority-mismatch');
|
||||
const length=headers['content-length'];
|
||||
if(length!==undefined&&!/^(0|[1-9][0-9]*)$/.test(length)) fail('invalid-length');
|
||||
const expected=length===undefined?0:Number(length);
|
||||
if(!Number.isSafeInteger(expected)) fail('invalid-length');
|
||||
return Object.freeze({method:request.method,url:destination.href,headers:Object.freeze(headers),expected});
|
||||
}
|
||||
|
||||
/** Provisional trusted broker API, not a public renderer/socket API.
|
||||
* current(authority) MUST synchronously verify enrollment, policy generation,
|
||||
* expiration and revocation. forward is trusted and must make its first dial
|
||||
* synchronously when called, without yielding before using the supplied bytes.
|
||||
* This module never forwards an IncomingMessage or an inspector-owned buffer.
|
||||
*/
|
||||
async function gateRequest(request,{inspect,current,forward,authority,maxBytes=1048576,deadlineMs=5000,signal}) {
|
||||
if(typeof authority!=='string'||!authority||typeof inspect!=='function'||typeof current!=='function'||typeof forward!=='function'||!Number.isSafeInteger(maxBytes)||maxBytes<0||!Number.isSafeInteger(deadlineMs)||deadlineMs<1) fail('invalid-configuration');
|
||||
const snapshot=parseRequest(request);
|
||||
if(snapshot.expected>maxBytes) fail('body-too-large');
|
||||
const isCurrent=()=>{try {return current(authority)===true;} catch {return false;}};
|
||||
if(!isCurrent()) fail('authority-expired');
|
||||
const expires=performance.now()+deadlineMs;
|
||||
let timer,abortListener,requestAbort,requestError,requestClose,data,end;
|
||||
let settled=false,body;
|
||||
const chunks=[];
|
||||
let bytes=0;
|
||||
const stop=new AbortController();
|
||||
try {
|
||||
const buffered=await new Promise((resolve,reject)=>{
|
||||
const deny=code=>{if(settled)return;settled=true;stop.abort();request.pause();reject(new GateError(code));};
|
||||
timer=setTimeout(()=>deny('deadline'),deadlineMs);
|
||||
abortListener=()=>deny('aborted');
|
||||
requestAbort=()=>deny('aborted');
|
||||
requestError=()=>deny('request-error');
|
||||
requestClose=()=>{if(!request.complete)deny('incomplete-body');};
|
||||
data=chunk=>{
|
||||
bytes+=chunk.length;
|
||||
if(bytes>maxBytes||bytes>snapshot.expected){deny('body-too-large');return;}
|
||||
chunks.push(Buffer.from(chunk));
|
||||
};
|
||||
end=()=>{
|
||||
if(settled)return;
|
||||
if(!request.complete||bytes!==snapshot.expected){deny('incomplete-body');return;}
|
||||
resolve(Buffer.concat(chunks));
|
||||
};
|
||||
signal?.addEventListener('abort',abortListener,{once:true});
|
||||
request.on('aborted',requestAbort).on('error',requestError).on('close',requestClose).on('data',data).once('end',end);
|
||||
if(signal?.aborted||request.aborted)deny('aborted');
|
||||
else if(request.readableEnded)end();
|
||||
});
|
||||
body=buffered;
|
||||
if(performance.now()>=expires)fail('deadline');
|
||||
if(!isCurrent())fail('authority-expired');
|
||||
const originalHash=digest(body);
|
||||
// Deadline and abort remain active during asynchronous inspection.
|
||||
const verdict=await Promise.race([
|
||||
Promise.resolve().then(()=>inspect(Object.freeze({request:snapshot,body:Buffer.from(body),signal:stop.signal}))),
|
||||
new Promise((_,reject)=>{
|
||||
if(stop.signal.aborted)reject(new GateError(signal?.aborted?'aborted':'deadline'));
|
||||
else stop.signal.addEventListener('abort',()=>reject(new GateError(signal?.aborted?'aborted':'deadline')),{once:true});
|
||||
})
|
||||
]);
|
||||
if(stop.signal.aborted)fail('aborted');
|
||||
if(performance.now()>=expires)fail('deadline');
|
||||
if(verdict?.decision!=='allow')fail('inspection-denied');
|
||||
if(!isCurrent())fail('authority-expired');
|
||||
if(digest(body)!==originalHash)fail('integrity-failed');
|
||||
settled=true;
|
||||
clearTimeout(timer);
|
||||
// Final verification and call have no asynchronous gap.
|
||||
return await forward(Object.freeze({request:snapshot,body:Buffer.from(body)}));
|
||||
} catch(error) {
|
||||
if(error instanceof GateError)throw error;
|
||||
throw new GateError('inspection-or-forward-failed');
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
signal?.removeEventListener('abort',abortListener);
|
||||
request.removeListener('aborted',requestAbort).removeListener('error',requestError).removeListener('close',requestClose).removeListener('data',data).removeListener('end',end);
|
||||
for(const chunk of chunks)chunk.fill(0);
|
||||
body?.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports={gateRequest,GateError};
|
||||
Reference in new issue
Block a user