93 lines
7.3 KiB
JavaScript
93 lines
7.3 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import https from 'node:https';
|
|
import fs from 'node:fs';
|
|
import {once} from 'node:events';
|
|
import {createRequire} from 'node:module';
|
|
const require=createRequire(import.meta.url),{createHttpsForwarder}=require('../apps/network-broker/https-forwarder.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,handler,configuration={}){
|
|
const received=[];let connections=0;
|
|
const server=https.createServer({key,cert},async(req,res)=>{const chunks=[];for await(const chunk of req)chunks.push(chunk);received.push({body:Buffer.concat(chunks).toString(),headers:req.headers,path:req.url});handler?.(req,res)||(!handler&&res.end('public response'));});
|
|
server.on('connection',()=>connections++);server.on('tlsClientError',()=>{});
|
|
server.listen(0,'127.0.0.1');await once(server,'listening');
|
|
t.after(async()=>{server.closeAllConnections();await new Promise(resolve=>server.close(resolve));});
|
|
const port=server.address().port;
|
|
const forward=createHttpsForwarder({authority:'policy-1',current:()=>true,resolveRoute:async destination=>({approved:true,address:'127.0.0.1',port:destination.port}),ca:cert,deadlineMs:1000,...configuration});
|
|
const request={method:'POST',url:`https://localhost:${port}/submit`,headers:{'content-type':'text/plain'}};
|
|
return {request,forward,received,get connections(){return connections;}};
|
|
}
|
|
test('numeric approved route uses original TLS hostname and strips proxy credentials',async t=>{
|
|
let release,entered;const wait=new Promise(resolve=>release=resolve),started=new Promise(resolve=>entered=resolve);
|
|
const f=await fixture(t,undefined,{resolveRoute:async destination=>{entered();await wait;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
|
const body=Buffer.from('public original');
|
|
f.request.headers['proxy-authorization']='Bearer PRIVATE_PROXY_KEY';f.request.headers.connection='x-hop';f.request.headers['x-hop']='PRIVATE_HOP_VALUE';
|
|
const pending=f.forward({request:f.request,body});await started;body.fill(120);release();
|
|
const result=await pending;assert.equal(result.body.toString(),'public response');
|
|
assert.equal(f.received[0].body,'public original');assert.equal(f.received[0].headers['proxy-authorization'],undefined);assert.equal(f.received[0].headers['x-hop'],undefined);
|
|
assert.equal(f.connections,1);
|
|
});
|
|
test('revocation during route resolution prevents any upstream TCP dial',async t=>{
|
|
let valid=true;const f=await fixture(t,undefined,{current:()=>valid,resolveRoute:async destination=>{valid=false;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.from('public')}),{code:'authority-expired'});
|
|
assert.equal(f.connections,0);
|
|
});
|
|
test('unapproved and nonnumeric routes cannot invoke system DNS or dial',async t=>{
|
|
const f=await fixture(t,undefined,{resolveRoute:async destination=>({approved:true,address:'localhost',port:destination.port})});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'route-unapproved'});assert.equal(f.connections,0);
|
|
});
|
|
test('revocation after TLS establishment prevents HTTP headers and payload release',async t=>{
|
|
let checks=0;const f=await fixture(t,undefined,{current:()=>++checks<4});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.from('must never transmit')}),{code:'authority-expired'});
|
|
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
|
});
|
|
test('untrusted server certificate is rejected with fixed error and no retry',async t=>{
|
|
const f=await fixture(t,undefined,{ca:undefined});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.from('public')}),{code:'upstream-failed'});
|
|
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
|
});
|
|
test('approved numeric route does not substitute certificate hostname',async t=>{
|
|
const f=await fixture(t);
|
|
await assert.rejects(f.forward({request:{...f.request,url:f.request.url.replace('localhost','wrong-host.example')},body:Buffer.alloc(0)}),{code:'upstream-failed'});
|
|
assert.equal(f.connections,1);assert.equal(f.received.length,0);
|
|
});
|
|
test('oversized streamed response is destroyed at configured limit',async t=>{
|
|
const f=await fixture(t,(_req,res)=>{res.write('1234');res.end('56789');},{maxResponseBytes:8});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-response-too-large'});
|
|
assert.equal(f.connections,1);
|
|
});
|
|
test('redirect response is returned without following location',async t=>{
|
|
const f=await fixture(t,(_req,res)=>{res.writeHead(302,{location:'/private'});res.end('redirect');});
|
|
const result=await f.forward({request:f.request,body:Buffer.alloc(0)});
|
|
assert.equal(result.statusCode,302);assert.equal(result.headers.location,'/private');assert.equal(f.received.length,1);assert.equal(f.received[0].path,'/submit');
|
|
});
|
|
test('abort during route resolution prevents dial and does not retry',async t=>{
|
|
let entered,release;const started=new Promise(resolve=>entered=resolve),wait=new Promise(resolve=>release=resolve);
|
|
const f=await fixture(t,undefined,{resolveRoute:async destination=>{entered();await wait;return {approved:true,address:'127.0.0.1',port:destination.port};}});
|
|
const controller=new AbortController(),pending=f.forward({request:f.request,body:Buffer.alloc(0),signal:controller.signal});
|
|
await started;controller.abort();await assert.rejects(pending,{code:'upstream-cancelled'});release();assert.equal(f.connections,0);
|
|
});
|
|
test('wall clock timeout terminates a stalled upstream connection',async t=>{
|
|
const f=await fixture(t,()=>{}, {deadlineMs:30});
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
|
assert.ok(f.connections<=1);
|
|
});
|
|
test('timed out unresolved routes retain bounded underlying resolution admission',async t=>{
|
|
let calls=0;const pending=[];
|
|
const f=await fixture(t,undefined,{deadlineMs:15,maxPendingResolutions:2,resolveRoute:destination=>{
|
|
calls++;return new Promise(resolve=>pending.push(()=>resolve({approved:true,address:'127.0.0.1',port:destination.port})));
|
|
}});
|
|
for(let index=0;index<2;index++)await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
|
for(let index=0;index<3;index++)await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'route-capacity-exhausted'});
|
|
assert.equal(calls,2);assert.equal(f.connections,0);
|
|
pending[0]();await new Promise(resolve=>setImmediate(resolve));
|
|
await assert.rejects(f.forward({request:f.request,body:Buffer.alloc(0)}),{code:'upstream-cancelled'});
|
|
assert.equal(calls,3);assert.equal(f.connections,0);
|
|
for(const release of pending)release();
|
|
});
|
|
test('invalid destinations and methods reject before resolver invocation',async t=>{
|
|
let resolutions=0;const f=await fixture(t,undefined,{resolveRoute:async()=>{resolutions++;}});
|
|
for(const change of [{url:'http://localhost/'},{url:'https://user:password@localhost/'},{url:'https://localhost/#fragment'},{url:'https://127.1/'},{method:'CONNECT'}])await assert.rejects(f.forward({request:{...f.request,...change},body:Buffer.alloc(0)}),{code:'invalid-forward-request'});
|
|
assert.equal(resolutions,0);assert.equal(f.connections,0);
|
|
});
|