import test from 'node:test'; import assert from 'node:assert/strict'; import https from 'node:https'; import fs from 'node:fs'; import {X509Certificate} from 'node:crypto'; import {once} from 'node:events'; import {createRequire} from 'node:module'; const require=createRequire(import.meta.url); const {bindConnectAuthority}=require('../apps/network-broker/tls-authority.cjs'); const {createInspectionServer}=require('../apps/network-broker/https-inspection-server.cjs'); const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)); const key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url)); async function fixture(t,inspect,current=()=>true,customForward,config={}) { const received=[];let connections=0; const receiver=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push(Buffer.concat(chunks).toString());res.writeHead(201,{'content-type':'text/plain','set-cookie':['session=synthetic; Secure; HttpOnly','locale=en; Secure']});res.end('accepted');}); receiver.on('connection',()=>connections++); receiver.listen(0,'127.0.0.1');await once(receiver,'listening'); const destination=`localhost:${receiver.address().port}`; const broker=createInspectionServer({key,cert,binding:bindConnectAuthority(destination),authority:'signed-policy-1',current,inspect,deadlineMs:1000,...config,forward:customForward||(({request,body,signal})=>new Promise((resolve,reject)=>{ assert.equal(new URL(request.url).protocol,'https:'); const outgoing=https.request({host:'127.0.0.1',port:receiver.address().port,path:new URL(request.url).pathname,method:request.method,servername:'localhost',ca:cert,rejectUnauthorized:true,signal,headers:{host:destination,'content-length':body.length}},res=>{const chunks=[];res.on('data',chunk=>chunks.push(chunk));res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));});outgoing.on('error',reject);outgoing.end(body); }))}); const port=await broker.listen(); t.after(async()=>{await broker.close();receiver.closeAllConnections();await new Promise(r=>receiver.close(r));}); const send=(body,{host=destination,servername='localhost',signal}={})=>new Promise((resolve,reject)=>{ const req=https.request({host:'127.0.0.1',port,method:'POST',path:'/submit',servername,ca:cert,rejectUnauthorized:true,agent:false,signal,headers:{host,'content-length':Buffer.byteLength(body)}},res=>{let result='';res.on('data',c=>result+=c);res.on('end',()=>resolve({status:res.statusCode,result}));});req.setTimeout(2000,()=>req.destroy(new Error('test-deadline')));req.on('error',reject);req.end(body); }); return {send,received,destination,get connections(){return connections;}}; } test('synthetic TLS certificate explicitly covers localhost and loopback',()=>{ const certificate=new X509Certificate(cert); assert.equal(certificate.checkHost('localhost'),'localhost');assert.equal(certificate.checkIP('127.0.0.1'),'127.0.0.1'); }); test('verified TLS delivers benign original and blocks secret before upstream TCP dial',async t=>{ const f=await fixture(t,async({body,request})=>{assert.equal(new URL(request.url).protocol,'https:');const decision=body.toString().includes('SECRET')?'block':'allow';body.fill(120);return {decision};}); assert.equal((await f.send('SECRET')).status,403);assert.equal(f.connections,0);assert.deepEqual(f.received,[]); assert.deepEqual(await f.send('public'),{status:201,result:'accepted'});assert.equal(f.connections,1);assert.deepEqual(f.received,['public']); }); test('HTTPS inspection delay releases no TCP connection until complete approval',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 pending=f.send('public');await waiting;assert.equal(f.connections,0);release();assert.equal((await pending).status,201);assert.equal(f.connections,1); }); test('revocation during HTTPS inspection blocks without dialing',async t=>{ let valid=true; const f=await fixture(t,async()=>{valid=false;return {decision:'allow'};},()=>valid); assert.deepEqual(await f.send('public'),{status:403,result:'authority-expired'});assert.equal(f.connections,0); }); test('inner Host mismatch and missing DNS SNI reject before inspection or dial',async t=>{ let inspections=0; const f=await fixture(t,async()=>{inspections++;return {decision:'allow'};}); assert.deepEqual(await f.send('public',{host:'evil.example:443'}),{status:403,result:'authority-mismatch'}); assert.deepEqual(await f.send('public',{servername:''}),{status:403,result:'missing-sni'}); assert.equal(inspections,0);assert.equal(f.connections,0);assert.deepEqual(f.received,[]); }); test('upstream failure returns fixed 502 without retry or private errors',async t=>{ let attempts=0; const f=await fixture(t,async()=>({decision:'allow'}),()=>true,async()=>{attempts++;throw new Error('private upstream credentials');}); assert.deepEqual(await f.send('public'),{status:502,result:'upstream-failed'});assert.equal(attempts,1);assert.equal(f.connections,0); }); test('client disconnect aborts in-flight forward once without retry',async t=>{ let entered,aborted,attempts=0; const started=new Promise(r=>entered=r),cancelled=new Promise(r=>aborted=r); const f=await fixture(t,async()=>({decision:'allow'}),()=>true,({signal})=>new Promise((_,reject)=>{attempts++;signal.addEventListener('abort',()=>{aborted();reject(new Error('cancelled'));},{once:true});entered();})); const controller=new AbortController(); const pending=f.send('public',{signal:controller.signal});const rejection=assert.rejects(pending); await started;controller.abort();await rejection; await Promise.race([cancelled,new Promise((_,reject)=>{const timer=setTimeout(()=>reject(new Error('abort-deadline')),1000);timer.unref();})]); assert.equal(attempts,1); }); test('forwarding wall clock deadline cancels a continuously active upstream once',async t=>{ let attempts=0,aborts=0; const f=await fixture(t,async()=>({decision:'allow'}),()=>true,({signal})=>new Promise((_,reject)=>{ attempts++; // No completion, just work continuing independently of socket idle. const activity=setInterval(()=>{},2); signal.addEventListener('abort',()=>{aborts++;clearInterval(activity);reject(new Error('private deadline detail'));},{once:true}); }),{deadlineMs:100}); const result=await f.send('public'); assert.deepEqual(result,{status:502,result:'upstream-failed'});assert.equal(attempts,1);assert.equal(aborts,1); }); test('response bounds reject invalid construction immediately',()=>{ for(const maxResponseBytes of [-1,1.5,16777217])assert.throws(()=>createInspectionServer({key,cert,authority:'p',current:()=>true,inspect:async()=>({decision:'allow'}),forward:async()=>{},maxResponseBytes}),{code:'invalid-configuration'}); }); test('standalone HTTPS job budget survives timed-out requests on new sockets',async t=>{ let calls=0; const f=await fixture(t,()=>{calls++;return new Promise(()=>{});},()=>true,undefined,{maxClients:1,deadlineMs:100}); for(let index=0;index<4;index++)assert.equal((await f.send('public')).status,403); assert.equal(calls,1);assert.equal(f.connections,0); }); test('one absolute deadline spans inspection and forwarding without resetting the budget',async t=>{ let inspectEntered,releaseInspect,forwardEntered,releaseForward;const inspected=new Promise(resolve=>inspectEntered=resolve),first=new Promise(resolve=>releaseInspect=resolve),forwarded=new Promise(resolve=>forwardEntered=resolve),second=new Promise(resolve=>releaseForward=resolve);let signal; const f=await fixture(t,async()=>{inspectEntered();await first;return {decision:'allow'};},()=>true,async value=>{signal=value.signal;forwardEntered();await second;return {statusCode:201,headers:{},body:Buffer.from('must not release')};},{deadlineMs:100}); const pending=f.send('public');await inspected;await new Promise(resolve=>setTimeout(resolve,55));releaseInspect();await forwarded; const result=await pending;assert.equal(result.status,502);assert.equal(result.result,'upstream-failed');assert.equal(signal.aborted,true);releaseForward();assert.equal(f.connections,0); });