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,104 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import http from 'node:http';
|
||||
import net from 'node:net';
|
||||
import {once} from 'node:events';
|
||||
import {PassThrough} from 'node:stream';
|
||||
import {createRequire} from 'node:module';
|
||||
const {gateRequest}=createRequire(import.meta.url)('../apps/network-broker/http-request-gate.cjs');
|
||||
|
||||
async function fixture(t,inspect,options={}) {
|
||||
const received=[];
|
||||
let connections=0;
|
||||
const receiver=http.createServer(async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.end('ok');});
|
||||
receiver.on('connection',()=>connections++);
|
||||
receiver.listen(0,'127.0.0.1');await once(receiver,'listening');
|
||||
const target=`http://127.0.0.1:${receiver.address().port}/submit`;
|
||||
const decisions=[];
|
||||
const broker=http.createServer((req,res)=>{
|
||||
gateRequest(req,{authority:'policy-1',current:()=>true,inspect,...options,forward:({request,body})=>new Promise((resolve,reject)=>{
|
||||
const outgoing=http.request(request.url,{method:request.method,headers:{'content-length':body.length}},response=>{response.resume();response.on('end',resolve);});
|
||||
outgoing.on('error',reject);outgoing.end(body);
|
||||
})}).then(()=>{decisions.push('allow');res.end('ok');},error=>{decisions.push(error.code);res.statusCode=403;res.end(error.code);});
|
||||
});
|
||||
broker.listen(0,'127.0.0.1');await once(broker,'listening');
|
||||
t.after(async()=>{broker.closeAllConnections();receiver.closeAllConnections();await Promise.all([new Promise(r=>broker.close(r)),new Promise(r=>receiver.close(r))]);});
|
||||
const send=body=>new Promise((resolve,reject)=>{const req=http.request({host:'127.0.0.1',port:broker.address().port,path:target,method:'POST',headers:{host:new URL(target).host,'content-length':Buffer.byteLength(body)}},res=>{res.resume();res.on('end',()=>resolve(res.statusCode));});req.on('error',reject);req.end(body);});
|
||||
return {received,decisions,send,target,broker,get connections(){return connections;}};
|
||||
}
|
||||
|
||||
async function rawRequest(t,broker,payload) {
|
||||
const socket=net.connect(broker.address().port,'127.0.0.1');
|
||||
t.after(()=>socket.destroy());
|
||||
return await new Promise((resolve,reject)=>{
|
||||
let text='',finished=false;
|
||||
const timer=setTimeout(()=>finish(new Error('raw-request-deadline')),2000);
|
||||
const finish=error=>{if(finished)return;finished=true;clearTimeout(timer);socket.destroy();if(error)reject(error);else resolve(text);};
|
||||
socket.on('connect',()=>socket.end(payload));
|
||||
socket.on('data',chunk=>text+=chunk);
|
||||
socket.on('error',finish);
|
||||
socket.on('end',()=>finish());
|
||||
socket.on('close',()=>finish());
|
||||
});
|
||||
}
|
||||
|
||||
test('controlled HTTP receiver sees benign original only; inspector mutation cannot alter release',async t=>{
|
||||
const f=await fixture(t,async({body})=>{const secret=body.toString().includes('SECRET');body.fill(120);return {decision:secret?'block':'allow'};});
|
||||
assert.equal(await f.send('public text'),200);assert.deepEqual(f.received,['public text']);
|
||||
assert.equal(await f.send('SECRET'),403);assert.deepEqual(f.received,['public text']);
|
||||
});
|
||||
test('no upstream bytes or dial before delayed allow',async t=>{
|
||||
let entered,release;const waiting=new Promise(r=>entered=r),hold=new Promise(r=>release=r);
|
||||
const f=await fixture(t,async()=>{entered();await hold;return {decision:'allow'};});
|
||||
const request=f.send('public');await waiting;assert.deepEqual(f.received,[]);assert.equal(f.connections,0);release();assert.equal(await request,200);assert.equal(f.connections,1);
|
||||
});
|
||||
test('policy revoked while inspection waits prevents release',async t=>{
|
||||
let valid=true;
|
||||
const f=await fixture(t,async()=>{valid=false;return {decision:'allow'};},{current:()=>valid});
|
||||
assert.equal(await f.send('public'),403);assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['authority-expired']);
|
||||
});
|
||||
test('bounded body and inspection deadline fail closed',async t=>{
|
||||
const f=await fixture(t,()=>new Promise(()=>{}),{maxBytes:8,deadlineMs:30});
|
||||
assert.equal(await f.send('too long for this bound'),403);assert.equal(await f.send('short'),403);
|
||||
assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['body-too-large','deadline']);
|
||||
});
|
||||
test('abort during inspection prevents release',async t=>{
|
||||
const controller=new AbortController();
|
||||
const f=await fixture(t,async()=>{controller.abort();return {decision:'allow'};},{signal:controller.signal});
|
||||
assert.equal(await f.send('public'),403);assert.deepEqual(f.received,[]);assert.deepEqual(f.decisions,['aborted']);
|
||||
});
|
||||
test('actual duplicate Host is denied, no upstream request',async t=>{
|
||||
const f=await fixture(t,async()=>({decision:'allow'}));
|
||||
const result=await rawRequest(t,f.broker,`POST ${f.target} HTTP/1.1\r\nHost: ${new URL(f.target).host}\r\nHost: attacker.invalid\r\nContent-Length: 0\r\nConnection: close\r\n\r\n`);
|
||||
assert.match(result,/403/);assert.deepEqual(f.received,[]);assert.equal(f.connections,0);assert.deepEqual(f.decisions,['duplicate-header']);
|
||||
});
|
||||
test('physical chunked and CL+TE requests never inspect or dial',async t=>{
|
||||
let inspected=0;
|
||||
const f=await fixture(t,async()=>{inspected++;return {decision:'allow'};});
|
||||
for(const length of ['', 'Content-Length: 6\r\n']) {
|
||||
const result=await rawRequest(t,f.broker,`POST ${f.target} HTTP/1.1\r\nHost: ${new URL(f.target).host}\r\n${length}Transfer-Encoding: chunked\r\nConnection: close\r\n\r\n6\r\nSECRET\r\n0\r\n\r\n`);
|
||||
assert.match(result,/HTTP\/1\.1 (400|403)/);
|
||||
}
|
||||
assert.equal(inspected,0);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
|
||||
});
|
||||
test('ambiguous framing and unsupported transports rejected before inspection',async()=>{
|
||||
for(const [headers,method,url] of [
|
||||
[['Host','example.test','Content-Length','1','Content-Length','1'],'POST','http://example.test/'],
|
||||
[['Host','example.test','Content-Length','1','Transfer-Encoding','chunked'],'POST','http://example.test/'],
|
||||
[['Host','example.test','Upgrade','websocket'],'GET','http://example.test/'],
|
||||
[['Host','example.test'],'CONNECT','example.test:443'],
|
||||
[['Host','example.test','Content-Encoding','gzip'],'POST','http://example.test/'],
|
||||
[['Host','evil.test'],'GET','http://example.test/']
|
||||
]) {
|
||||
const req=new PassThrough();Object.assign(req,{rawHeaders:headers,method,url,complete:true});
|
||||
await assert.rejects(gateRequest(req,{authority:'p',current:()=>true,inspect:()=>assert.fail('inspection'),forward:()=>assert.fail('forward')}));
|
||||
}
|
||||
});
|
||||
test('truncated body and stream error return fixed errors without release',async()=>{
|
||||
for(const kind of ['end','error','aborted']) {
|
||||
const req=new PassThrough();Object.assign(req,{rawHeaders:['Host','example.test','Content-Length','3'],method:'POST',url:'http://example.test/',complete:false});
|
||||
const promise=gateRequest(req,{authority:'p',current:()=>true,inspect:()=>assert.fail('inspection'),forward:()=>assert.fail('forward')});
|
||||
if(kind==='end')req.end('x');else req.emit(kind, new Error('confidential error message'));
|
||||
await assert.rejects(promise,error=>['incomplete-body','request-error','aborted'].includes(error.code)&&!error.message.includes('confidential'));
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user