@@ -115,6 +115,71 @@ describe('Website Clone browser broker security boundary', () => {
115115 }
116116 } ) ;
117117
118+ it ( 'tears down an ordinary HTTP upstream when the browser client disconnects' , async ( ) => {
119+ const targetSockets = new Set < Socket > ( ) ;
120+ const target = createServer ( ( _request , response ) => {
121+ response . writeHead ( 200 ) ;
122+ response . write ( 'partial response' ) ;
123+ } ) ;
124+ target . on ( 'connection' , ( socket ) => {
125+ targetSockets . add ( socket ) ;
126+ socket . on ( 'error' , ( ) => undefined ) ;
127+ socket . once ( 'close' , ( ) => targetSockets . delete ( socket ) ) ;
128+ } ) ;
129+ await new Promise < void > ( ( resolve ) => target . listen ( 0 , '127.0.0.1' , resolve ) ) ;
130+ const targetAddress = target . address ( ) ;
131+ if ( ! targetAddress || typeof targetAddress === 'string' ) throw new Error ( 'target did not bind' ) ;
132+ const proxy = await createBrowserNetworkProxy ( { allowPrivateNetwork : true } ) ;
133+ const client = openHttpStreamThroughProxy ( proxy . port , targetAddress . port ) ;
134+
135+ try {
136+ await client . responseStarted ;
137+ expect ( targetSockets . size ) . toBe ( 1 ) ;
138+ client . request . destroy ( ) ;
139+ await waitFor ( ( ) => targetSockets . size === 0 ) ;
140+ } finally {
141+ client . request . destroy ( ) ;
142+ await proxy . close ( ) ;
143+ for ( const socket of targetSockets ) socket . destroy ( ) ;
144+ await new Promise < void > ( ( resolve ) => target . close ( ( ) => resolve ( ) ) ) ;
145+ }
146+ } ) ;
147+
148+ it ( 'does not retain ordinary HTTP upstreams across repeated proxy lifecycles' , async ( ) => {
149+ const targetSockets = new Set < Socket > ( ) ;
150+ const target = createServer ( ( _request , response ) => {
151+ response . writeHead ( 200 ) ;
152+ response . write ( 'partial response' ) ;
153+ } ) ;
154+ target . on ( 'connection' , ( socket ) => {
155+ targetSockets . add ( socket ) ;
156+ socket . on ( 'error' , ( ) => undefined ) ;
157+ socket . once ( 'close' , ( ) => targetSockets . delete ( socket ) ) ;
158+ } ) ;
159+ await new Promise < void > ( ( resolve ) => target . listen ( 0 , '127.0.0.1' , resolve ) ) ;
160+ const targetAddress = target . address ( ) ;
161+ if ( ! targetAddress || typeof targetAddress === 'string' ) throw new Error ( 'target did not bind' ) ;
162+
163+ try {
164+ for ( let iteration = 0 ; iteration < 3 ; iteration += 1 ) {
165+ const proxy = await createBrowserNetworkProxy ( { allowPrivateNetwork : true } ) ;
166+ const client = openHttpStreamThroughProxy ( proxy . port , targetAddress . port ) ;
167+ try {
168+ await client . responseStarted ;
169+ expect ( targetSockets . size ) . toBe ( 1 ) ;
170+ await proxy . close ( ) ;
171+ await waitFor ( ( ) => targetSockets . size === 0 ) ;
172+ } finally {
173+ client . request . destroy ( ) ;
174+ await proxy . close ( ) ;
175+ }
176+ }
177+ } finally {
178+ for ( const socket of targetSockets ) socket . destroy ( ) ;
179+ await new Promise < void > ( ( resolve ) => target . close ( ( ) => resolve ( ) ) ) ;
180+ }
181+ } ) ;
182+
118183 it ( 'exposes only the CDP methods required by the staged recon adapter' , ( ) => {
119184 expect ( WEB_CLONE_CDP_METHODS ) . toEqual ( new Set ( [
120185 'Emulation.setDeviceMetricsOverride' ,
@@ -179,6 +244,29 @@ async function connectTunnel(proxyPort: number, targetPort: number): Promise<Soc
179244 } ) ;
180245}
181246
247+ function openHttpStreamThroughProxy ( proxyPort : number , targetPort : number ) : {
248+ request : ReturnType < typeof request > ;
249+ responseStarted : Promise < void > ;
250+ } {
251+ let responseStartedResolve : ( ( ) => void ) | undefined ;
252+ let responseStartedReject : ( ( error : Error ) => void ) | undefined ;
253+ const responseStarted = new Promise < void > ( ( resolve , reject ) => {
254+ responseStartedResolve = resolve ;
255+ responseStartedReject = reject ;
256+ } ) ;
257+ const outbound = request ( {
258+ headers : { host : `127.0.0.1:${ targetPort } ` } ,
259+ host : '127.0.0.1' ,
260+ path : `http://127.0.0.1:${ targetPort } /stream` ,
261+ port : proxyPort ,
262+ } , ( response ) => {
263+ response . once ( 'data' , ( ) => responseStartedResolve ?.( ) ) ;
264+ } ) ;
265+ outbound . on ( 'error' , ( error ) => responseStartedReject ?.( error ) ) ;
266+ outbound . end ( ) ;
267+ return { request : outbound , responseStarted } ;
268+ }
269+
182270async function waitFor ( condition : ( ) => boolean , timeoutMs = 1_000 ) : Promise < void > {
183271 const startedAt = Date . now ( ) ;
184272 while ( ! condition ( ) ) {
0 commit comments