40 lines
7.5 KiB
JavaScript
40 lines
7.5 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import http from 'node:http';import https from 'node:https';import net from 'node:net';import fs from 'node:fs';import {once} from 'node:events';import {createRequire} from 'node:module';
|
|
const {createProxyForwarder,parseFixedProxy}=createRequire(import.meta.url)('../apps/network-broker/proxy-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,{rejectAuth=false,ca=cert,current=()=>true,tlsProxy=false,resolveOriginRoute}={}){
|
|
let originRequests=0,connects=0,proxyAuth,originAuth,connectTarget,originSni,proxyTcp=0;const sockets=new Set();
|
|
const origin=https.createServer({cert,key},(req,res)=>{originRequests++;originAuth=req.headers['proxy-authorization'];res.end('public');});origin.on('tlsClientError',()=>{});origin.listen(0,'127.0.0.1');await once(origin,'listening');
|
|
origin.on('secureConnection',s=>{originSni=s.servername;});
|
|
const proxy=tlsProxy?https.createServer({cert,key}):http.createServer();proxy.on('connect',(req,socket,head)=>{connects++;connectTarget=req.url;proxyAuth=req.headers['proxy-authorization'];if(rejectAuth){socket.end('HTTP/1.1 407 Proxy Authentication Required\r\nContent-Length: 0\r\n\r\n');return;}const upstream=net.connect(origin.address().port,'127.0.0.1',()=>{socket.write('HTTP/1.1 200 Connection Established\r\n\r\n');if(head.length)upstream.write(head);socket.pipe(upstream);upstream.pipe(socket);});sockets.add(upstream);upstream.on('error',()=>socket.destroy());});proxy.on('connection',s=>{proxyTcp++;sockets.add(s);});proxy.listen(0,'127.0.0.1');await once(proxy,'listening');
|
|
t.after(async()=>{for(const socket of sockets)socket.destroy();origin.closeAllConnections();await Promise.all([new Promise(r=>proxy.close(r)),new Promise(r=>origin.close(r))]);});
|
|
const forward=createProxyForwarder({authority:'p',current,proxyPolicy:{mode:'fixed_servers',rules:`${tlsProxy?"https":"http"}://localhost:${proxy.address().port}`,bypass:''},resolveProxyRoute:async d=>({approved:true,address:'127.0.0.1',port:d.port}),resolveOriginRoute,credentials:{username:'corp',password:'synthetic'},ca:ca===null?undefined:ca,proxyCa:cert,deadlineMs:1000});
|
|
return {forward,request:{method:'POST',url:`https://localhost:${origin.address().port}/`,headers:{'proxy-authorization':'must-not-reach-origin'}},get originRequests(){return originRequests;},get connects(){return connects;},get proxyAuth(){return proxyAuth;},get originAuth(){return originAuth;},get connectTarget(){return connectTarget;},get originSni(){return originSni;},get proxyTcp(){return proxyTcp;}};
|
|
}
|
|
test('real CONNECT proxy carries verified HTTPS and credentials stay proxy-only',async t=>{const f=await fixture(t);const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.originRequests,1);assert.equal(f.connects,1);assert.equal(f.proxyAuth,'Basic '+Buffer.from('corp:synthetic').toString('base64'));assert.equal(f.originAuth,undefined);});
|
|
test('proxy authentication rejection never falls back direct',async t=>{const f=await fixture(t,{rejectAuth:true});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.originRequests,0);assert.equal(f.connects,1);});
|
|
test('untrusted origin TLS prevents HTTP release',async t=>{const f=await fixture(t,{ca:null});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.originRequests,0);});
|
|
test('revoked authority never dials proxy or origin',async t=>{const f=await fixture(t,{current:()=>false});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.connects,0);assert.equal(f.originRequests,0);});
|
|
test('unsupported PAC fallback and bypass fail closed',()=>{for(const rules of ['socks://a:1080','http://a:80;http://b:80','a:80'])assert.throws(()=>parseFixedProxy({mode:'fixed_servers',rules,bypass:''}));assert.throws(()=>parseFixedProxy({mode:'fixed_servers',rules:'http://a:80',bypass:'*'}));});
|
|
|
|
|
|
test('real TLS proxy verifies outer endpoint and origin independently',async t=>{const f=await fixture(t,{tlsProxy:true});const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.originRequests,1);});
|
|
test('timed out resolver retains admission until actual settlement',async()=>{
|
|
let settle;const pending=new Promise(resolve=>{settle=resolve;});
|
|
const forward=createProxyForwarder({authority:'p',current:()=>true,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveProxyRoute:()=>pending,deadlineMs:5});
|
|
const arguments_={request:{method:'POST',url:'https://localhost:443/',headers:{}},body:Buffer.from('public')};
|
|
const requests=Array.from({length:8},()=>assert.rejects(forward(arguments_),{code:'upstream-cancelled'}));await Promise.all(requests);
|
|
await assert.rejects(forward(arguments_),{code:'proxy-capacity-exhausted'});settle({approved:false});await new Promise(resolve=>setImmediate(resolve));
|
|
await assert.rejects(forward(arguments_),{code:'proxy-route-unapproved'});
|
|
});
|
|
|
|
test('proxy URI defaults follow scheme and CA inputs are typed',()=>{
|
|
assert.equal(parseFixedProxy({mode:'fixed_servers',rules:'http://proxy.example',bypass:''}).port,80);
|
|
assert.equal(parseFixedProxy({mode:'fixed_servers',rules:'https://proxy.example',bypass:''}).port,443);
|
|
assert.throws(()=>createProxyForwarder({authority:'p',current:()=>true,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveProxyRoute:()=>{},ca:{untrusted:true}}));
|
|
});
|
|
test('approved numeric origin CONNECT preserves original TLS hostname and exact port',async t=>{const f=await fixture(t,{tlsProxy:true,resolveOriginRoute:async r=>({approved:true,address:'127.0.0.1',port:r.port})});const result=await f.forward({request:f.request,body:Buffer.from('public')});assert.equal(result.body.toString(),'public');assert.equal(f.connectTarget,'127.0.0.1:'+new URL(f.request.url).port);assert.equal(f.originSni,'localhost');});
|
|
test('blocked resolved origin prevents even proxy TCP and never falls back to hostname',async t=>{const {createDnsPolicyRoute}=createRequire(import.meta.url)('../apps/network-broker/dns-policy-route.cjs');const f=await fixture(t,{resolveOriginRoute:createDnsPolicyRoute({current:()=>true,blocked:a=>a==='127.0.0.1',lookup:async()=>[{address:'127.0.0.1',family:4}]})});await assert.rejects(f.forward({request:f.request,body:Buffer.from('secret')}));assert.equal(f.proxyTcp,0);assert.equal(f.connects,0);assert.equal(f.originRequests,0);});
|
|
test('origin resolver abort retains original slot and revocation prevents proxy dial',async()=>{let settle,active=true;const pending=new Promise(r=>{settle=r;});let proxyLookups=0;const forward=createProxyForwarder({authority:'p',current:()=>active,proxyPolicy:{mode:'fixed_servers',rules:'http://localhost:1234',bypass:''},resolveOriginRoute:()=>pending,resolveProxyRoute:async()=>{proxyLookups++;return {approved:false};},deadlineMs:5});const args={request:{method:'POST',url:'https://localhost:443/',headers:{}},body:Buffer.from('public')};await Promise.all(Array.from({length:8},()=>assert.rejects(forward(args),{code:'upstream-cancelled'})));active=false;await assert.rejects(forward(args),{code:'proxy-capacity-exhausted'});settle({approved:true,address:'127.0.0.1',port:443});await new Promise(r=>setImmediate(r));assert.equal(proxyLookups,0);await assert.rejects(forward(args),{code:'authority-expired'});});
|