Files
safetynet/tests/broker-connect-inspection.test.mjs

78 lines
6.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import http from 'node:http';
import https from 'node:https';
import tls from 'node:tls';
import fs from 'node:fs';
import {once} from 'node:events';
import {createRequire} from 'node:module';
const {createConnectProxy}=createRequire(import.meta.url)('../apps/network-broker/connect-inspection-proxy.cjs');
const cert=fs.readFileSync(new URL('./fixtures/icap/cert.pem',import.meta.url)),key=fs.readFileSync(new URL('./fixtures/icap/key.pem',import.meta.url));
async function fixture(t,config={}) {
let connections=0,resolutions=0;
const received=[];
const target=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'});res.end('accepted');});
target.on('connection',()=>connections++);target.listen(0,'127.0.0.1');await once(target,'listening');
const destination=`localhost:${target.address().port}`;
const proxy=createConnectProxy({authority:'policy-1',current:()=>true,resolveCertificate:async binding=>{resolutions++;if(binding.authority!==destination)return undefined;return {authority:destination,key,cert};},inspect:async({body})=>({decision:body.toString().includes('SECRET')?'block':'allow'}),forward:({request,body,signal})=>new Promise((resolve,reject)=>{
const req=https.request({host:'127.0.0.1',port:target.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',c=>chunks.push(c));res.on('end',()=>resolve({statusCode:res.statusCode,headers:res.headers,body:Buffer.concat(chunks)}));});req.on('error',reject);req.end(body);
}),deadlineMs:1000,authMode:'bearer',...config});
const port=await proxy.listen();
t.after(async()=>{await proxy.close();target.closeAllConnections();await new Promise(r=>target.close(r));});
async function send(body,{token=proxy.capability,authorization=`Bearer ${token}`,connectAuthority=destination,host=destination,servername='localhost'}={}) {
const result=await new Promise((resolve,reject)=>{
const req=http.request({host:'127.0.0.1',port,method:'CONNECT',path:connectAuthority,headers:{host:connectAuthority,'proxy-authorization':authorization}});
req.setTimeout(2000,()=>req.destroy(new Error('connect-test-deadline')));req.on('error',reject);
req.on('connect',(res,socket,head)=>{if(res.statusCode!==200){socket.destroy();resolve({status:res.statusCode,challenge:res.headers['proxy-authenticate']});return;}assert.equal(head.length,0);
const secure=tls.connect({socket,servername,ca:cert,rejectUnauthorized:true});let text='';
secure.setTimeout(2000,()=>secure.destroy(new Error('tls-test-deadline')));secure.on('error',reject);
secure.on('secureConnect',()=>secure.write(`POST /submit HTTP/1.1\r\nHost: ${host}\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`));
secure.on('data',c=>text+=c);secure.on('end',()=>{secure.destroy();resolve({status:Number(/^HTTP\/1.1 (\d+)/.exec(text)?.[1]),text});});
});req.end();
});return result;
}
return {send,received,credentials:proxy.credentials,get connections(){return connections;},get resolutions(){return resolutions;}};
}
test('real authenticated CONNECT wraps verified TLS; no opaque target dial for secret',async t=>{
const f=await fixture(t);
assert.equal((await f.send('SECRET')).status,403);assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
const allowed=await f.send('public');assert.equal(allowed.status,201);assert.match(allowed.text,/accepted$/);assert.equal(f.connections,1);assert.deepEqual(f.received,['public']);
});
test('incorrect token prevents certificate resolution, TLS establishment and target dial',async t=>{
const f=await fixture(t);assert.equal((await f.send('public',{token:'incorrect'})).status,407);assert.equal(f.resolutions,0);assert.equal(f.connections,0);
});
test('unapproved CONNECT authority, inner mismatch and absent SNI never dial target',async t=>{
const f=await fixture(t);
assert.equal((await f.send('public',{connectAuthority:'evil.example:443'})).status,403);
assert.equal((await f.send('public',{host:'evil.example:443'})).status,403);
assert.equal((await f.send('public',{servername:''})).status,403);
assert.equal(f.connections,0);assert.deepEqual(f.received,[]);
});
test('shared CONNECT work budget remains consumed when timed-out inspector ignores cancellation',async t=>{
const retained=[];
const f=await fixture(t,{maxClients:1,deadlineMs:100,inspect:({body})=>{retained.push(body);return new Promise(()=>{});}});
assert.equal((await f.send('public')).status,403);
for(let index=0;index<3;index++)assert.equal((await f.send('public')).status,403);
assert.equal(retained.length,1);assert.equal(retained[0].toString(),'public');assert.equal(f.connections,0);
});
test('shared CONNECT work budget retains an ignored forward promise after timeout',async t=>{
let calls=0;
const f=await fixture(t,{maxClients:1,deadlineMs:100,forward:()=>{calls++;return new Promise(()=>{});}});
assert.equal((await f.send('public')).status,502);
for(let index=0;index<3;index++)assert.equal((await f.send('public')).status,403);
assert.equal(calls,1);assert.equal(f.connections,0);
});
test('timed-out certificate resolver retains shared capacity across new CONNECT sockets',async t=>{
let calls=0,aborted=false;
const f=await fixture(t,{maxClients:1,deadlineMs:100,resolveCertificate:(_binding,{signal})=>{calls++;signal.addEventListener('abort',()=>aborted=true,{once:true});return new Promise(()=>{});}});
for(let index=0;index<4;index++)assert.equal((await f.send('public')).status,403);
assert.equal(calls,1);assert.equal(aborted,true);assert.equal(f.connections,0);
});
test('Basic mode challenges and rejects Bearer without downgrade then accepts exact random credentials',async t=>{
const f=await fixture(t,{authMode:'basic'});
const refused=await f.send('public');assert.equal(refused.status,407);assert.equal(refused.challenge,'Basic realm="SafetyNet private broker", charset="UTF-8"');assert.equal(f.connections,0);assert.equal(f.resolutions,0);
const authorization=`Basic ${Buffer.from(`${f.credentials.username}:${f.credentials.password}`).toString('base64')}`;
assert.equal((await f.send('public',{authorization})).status,201);assert.deepEqual(f.received,['public']);
});