Add packaged DLP broker and protected Windows service foundation

This commit is contained in:
Karl committed 2026-10-14 14:19:22 +02:00
1 parent 95d23f4e0b
commit 7b5057901a
140 files changed
+11215 -60

No files matched your search

+104
View File
@@ -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'));
}
});