59 lines
7.3 KiB
JavaScript
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};
|