Files
safetynet/apps/browser/native-transport-observer.cjs
T

59 lines
7.3 KiB
JavaScript

'use strict';
const {randomUUID}=require('node:crypto');
const phases=Object.freeze({'Network.webTransportCreated':'created','Network.webTransportClosed':'closed','Network.webTransportConnectionEstablished':'established'});
const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
const opaque=value=>typeof value==='string'&&value.length>0&&value.length<=128&&/^[a-zA-Z0-9._:-]+$/.test(value);
function transportHost(value){if(typeof value!=='string'||value.length>8192)throw Error('Transport metadata refused');const u=new URL(value);if(u.protocol!=='https:'||u.username||u.password||u.hostname.length>253)throw Error('Transport metadata refused');return u.hostname.toLowerCase();}
// Observability only: Chromium's mandatory private proxy remains the release
// boundary. No generic command runner, raw protocol events or page bridge escape.
function createNativeTransportObserver({channel,getContext,isCurrent,audit,onFailure,onEstablished,traceOperation=(_stage,fn)=>fn(),commandTimeoutMs=5000,maxTargets=64,maxAttempts=256,maxAuditQueue=256}={}){
if(!channel||['isHealthy','enableNetwork','autoAttachTargets','resumeTarget','subscribeMessage','subscribeDetach','release'].some(name=>typeof channel[name]!=='function')||[getContext,isCurrent,audit,onFailure,onEstablished,traceOperation].some(fn=>typeof fn!=='function')||!Number.isInteger(commandTimeoutMs)||commandTimeoutMs<50||commandTimeoutMs>10000||[maxTargets,maxAttempts,maxAuditQueue].some(n=>!Number.isInteger(n)||n<1||n>256))throw TypeError('Native transport observer callbacks required');
const targets=new Map(),attempts=new Map(),setups=new Set();let owned=false,installed=false,disposed=false,failed=false,installPromise,auditTail=Promise.resolve(),queued=0,unsubscribeMessage,unsubscribeDetach;
const active=()=>owned&&!disposed&&!failed;
function failure(reason='transport-observer-unavailable'){if(disposed||failed)return;failed=true;installed=false;attempts.clear();targets.clear();try{Promise.resolve(traceOperation('transport.monitor-unavailable',()=>onFailure(reason))).catch(()=>{});}catch{} }
async function command(method,_parameters={},sessionId){if(!active())throw Error('Native transport monitor unavailable');let timer;try{return await Promise.race([(method==='Network.enable'?channel.enableNetwork(sessionId):method==='Target.setAutoAttach'?channel.autoAttachTargets(sessionId):channel.resumeTarget(sessionId)),new Promise((_,reject)=>{timer=setTimeout(()=>reject(Object.assign(Error('Native transport command timeout'),{reason:'transport-observer-command-timeout'})),commandTimeoutMs);})]);}finally{clearTimeout(timer);}}
function context(){try{const value=getContext();if(!value)return null;const copy=Object.freeze({...value});if(!uuid.test(copy.sessionId)||!Number.isSafeInteger(copy.policyVersion)||copy.policyVersion<1||!isCurrent(copy))return null;return copy;}catch{return null;}}
function enqueue(row,phase){if(queued>=maxAuditQueue){failure('transport-observer-overflow');return;}const facts=Object.freeze({transportId:row.id,protocol:'webtransport',phase,realm:row.realm,sessionId:row.context.sessionId,connectionEstablished:row.established,host:row.host});queued++;
// Metadata retains the creation policy/owner across later navigation/policy
// changes. Rechecking release authority here would erase legitimate audit.
auditTail=auditTail.then(async()=>{await traceOperation('transport.'+phase,()=>audit(facts,row.context),row.context);}).catch(()=>failure('transport-observer-audit-failed')).finally(()=>queued--);
}
function event(_event,method,params,sessionId){if(!active())return;
if(method==='Target.attachedToTarget'){
const id=params?.sessionId,type=params?.targetInfo?.type;
if(!opaque(id)||targets.has(id)){failure();return;}if(targets.size>=maxTargets){failure('transport-observer-overflow');return;}
const realm=['worker','shared_worker','service_worker'].includes(type)?'worker':type==='iframe'?'iframe':null;
if(!realm){failure();return;}targets.set(id,{realm});
const work=Promise.resolve().then(()=>traceOperation('transport.target',async()=>{
// Issue setup before resume, but do not await responses while the fresh
// target is waiting for startup. Chromium may defer them until resume.
const network=command('Network.enable',{maxPostDataSize:0},id),children=command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true},id);
await Promise.all([network,children,command('Runtime.runIfWaitingForDebugger',{},id)]);
})).catch(error=>failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable')).finally(()=>setups.delete(work));setups.add(work);return;
}
// Target detachment can precede its final Network close event. Keep the
// already captured bounded attempt correlation until that event or disposal.
if(method==='Target.detachedFromTarget'){targets.delete(params?.sessionId);return;}
const phase=phases[method];if(!phase)return;
if(sessionId&&!targets.has(sessionId)&&phase==='created'){failure();return;}if(!opaque(params?.transportId)){failure();return;}
const key=(sessionId||'window')+':'+params.transportId;
if(phase==='created'){
if(attempts.has(key)){failure();return;}if(attempts.size>=maxAttempts){failure('transport-observer-overflow');return;}const owner=context();if(!owner)return;
try{const row={id:randomUUID(),context:owner,host:transportHost(params.url),realm:sessionId?targets.get(sessionId).realm:'window',target:sessionId,established:false};attempts.set(key,row);enqueue(row,phase);}catch{failure();}return;
}
const row=attempts.get(key);if(!row)return;
if(phase==='established'){if(row.established){failure();return;}row.established=true;try{onEstablished(row.context);}catch{failure();}enqueue(row,phase);return;}
attempts.delete(key);enqueue(row,phase);
}
function detach(){if(!disposed&&owned)failure('transport-observer-detached');owned=false;installed=false;}
async function install(){if(installPromise)return installPromise;if(disposed||failed||!channel.isHealthy())throw Error('Private transport observer unavailable');
// eslint-disable-next-line preserve-caught-error -- Expose only a fixed error; protocol causes may contain private URLs or target data.
installPromise=Promise.resolve(traceOperation('transport.install',async()=>{try{unsubscribeMessage=channel.subscribeMessage(event);unsubscribeDetach=channel.subscribeDetach(detach);owned=true;await command('Network.enable',{maxPostDataSize:0});await command('Target.setAutoAttach',{autoAttach:true,waitForDebuggerOnStart:true,flatten:true});if(!active())throw Error('Native transport monitor unavailable');installed=true;}catch(error){failure(error.reason==='transport-observer-command-timeout'?'transport-observer-command-timeout':'transport-observer-unavailable');throw Error('Native transport monitor installation failed');}}));return installPromise;
}
function dispose(){if(disposed)return;disposed=true;installed=false;attempts.clear();targets.clear();try{unsubscribeMessage?.();}catch{}try{unsubscribeDetach?.();}catch{}try{channel.release();}catch{}owned=false;}
async function flush(){await Promise.allSettled([...setups]);await auditTail;}
return {install,dispose,flush,status:()=>Object.freeze({installed,failed,disposed,attached:owned&&channel.isHealthy(),targets:targets.size,pending:attempts.size,queued})};
}
module.exports={createNativeTransportObserver};