browserProxyLease.test.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  1. // @vitest-environment node
  2. import {expect,it,vi} from 'vitest'
  3. import {createServer,request,type IncomingMessage,type ServerResponse} from 'node:http'
  4. const target=vi.hoisted(()=>({port:0,resolve:undefined as undefined|(()=>Promise<void>)}))
  5. vi.mock('../../src/runner/network/publicDestination.js',async importOriginal=>({
  6. ...await importOriginal<object>(),publicDestination:async()=>{await target.resolve?.();return {host:'fixture.test',address:'127.0.0.1',family:4,port:target.port}},
  7. }))
  8. import {BrowserPublicProxy} from '../../src/runner/browser/publicProxy.js'
  9. const deferred=()=>{let resolve!:()=>void;const promise=new Promise<void>(done=>{resolve=done});return {promise,resolve}}
  10. it.each(['expired','parked'])('disconnects an existing response stream when %s and allows an authorized reconnect',async mode=>{
  11. const server=createServer((req,res)=>{res.writeHead(200);if(req.url==='/once')res.end('ok');else res.write('stream-start')})
  12. await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve));target.port=(server.address() as {port:number}).port
  13. let allowed=true
  14. const proxy=await BrowserPublicProxy.start(async()=>{if(!allowed)throw new Error('grant expired')},undefined,mode==='parked'?60000:20)
  15. const headers={'Proxy-Authorization':'Basic '+Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}
  16. try{
  17. let interrupted!:()=>void;const ended=new Promise<void>(resolve=>{interrupted=resolve})
  18. await new Promise<void>((resolve,reject)=>{
  19. const req=request(proxy.serverURL,{path:'http://fixture.test/stream',headers},res=>{res.once('data',()=>resolve());res.once('aborted',interrupted);res.on('error',()=>{})})
  20. req.on('error',reject);req.end()
  21. })
  22. allowed=false
  23. if(mode==='parked')proxy.disconnect()
  24. await Promise.race([ended,new Promise((_,reject)=>setTimeout(()=>reject(new Error('expired stream stayed connected')),1000))])
  25. allowed=true
  26. const status=await new Promise<number>((resolve,reject)=>{const req=request(proxy.serverURL,{path:'http://fixture.test/once',headers},res=>{res.resume();res.on('end',()=>resolve(res.statusCode!))});req.on('error',reject);req.end()})
  27. expect(status).toBe(200)
  28. }finally{await proxy.close();server.closeAllConnections();await new Promise<void>(resolve=>server.close(()=>resolve()))}
  29. })
  30. it.each([
  31. ['http','authorization'],['connect','authorization'],['http','destination'],['connect','destination'],
  32. ] as const)('does not create an upstream for an old %s request resumed after parking during %s',async(protocol,stage)=>{
  33. let connections=0,authorizations=0,allowed=true
  34. const server=createServer((_req,res)=>res.end('new grant'))
  35. server.on('connection',()=>{connections++})
  36. await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve));target.port=(server.address() as {port:number}).port
  37. const entered=deferred(),release=deferred()
  38. let resolveOnce=true
  39. if(stage==='destination')target.resolve=async()=>{if(resolveOnce){resolveOnce=false;entered.resolve();await release.promise}}
  40. const proxy=await BrowserPublicProxy.start(async()=>{
  41. if(!allowed)throw new Error('parked')
  42. if(++authorizations===2&&stage==='authorization'){entered.resolve();await release.promise}
  43. },undefined,60000)
  44. const headers={'Proxy-Authorization':'Basic '+Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}
  45. const options={path:protocol==='connect'?'fixture.test:443':'http://fixture.test/old-request',method:protocol==='connect'?'CONNECT':'GET',headers,agent:false as const}
  46. const stale=request(proxy.serverURL,options)
  47. stale.on('error',()=>{});stale.on('response',response=>response.resume());stale.on('connect',(_response,socket)=>socket.destroy())
  48. const staleClosed=new Promise<void>(resolve=>stale.once('close',resolve))
  49. try{
  50. stale.end();await entered.promise
  51. allowed=false;proxy.disconnect();await staleClosed
  52. // A new task has acquired this Bot's browser before the old async check
  53. // returns. Current authorization alone cannot identify the stale request.
  54. allowed=true;release.resolve()
  55. const result=await new Promise<number>((resolve,reject)=>{
  56. const fresh=request(proxy.serverURL,{...options,path:protocol==='connect'?'fixture.test:443':'http://fixture.test/new-request'},response=>{response.resume();response.once('end',()=>resolve(response.statusCode!))})
  57. fresh.on('connect',(response,socket)=>{socket.destroy();resolve(response.statusCode!)})
  58. fresh.on('error',reject);fresh.end()
  59. })
  60. expect(result).toBe(200)
  61. expect(connections).toBe(1)
  62. }finally{
  63. release.resolve();target.resolve=undefined;stale.destroy()
  64. await proxy.close();server.closeAllConnections();await new Promise<void>(resolve=>server.close(()=>resolve()))
  65. }
  66. })
  67. it('keeps a new stream connected when a previous epoch timer check fails late',async()=>{
  68. let freshResponse:ServerResponse|undefined,delayNextCheck=false
  69. const server=createServer((req,res)=>{res.writeHead(200);if(req.url==='/new')freshResponse=res;res.write('started')})
  70. await new Promise<void>(resolve=>server.listen(0,'127.0.0.1',resolve));target.port=(server.address() as {port:number}).port
  71. const entered=deferred()
  72. let rejectOld!:(error:Error)=>void
  73. const delayed=new Promise<void>((_resolve,reject)=>{rejectOld=reject})
  74. void delayed.catch(()=>{})
  75. const proxy=await BrowserPublicProxy.start(async()=>{
  76. if(delayNextCheck){delayNextCheck=false;entered.resolve();await delayed}
  77. },undefined,20)
  78. const headers={'Proxy-Authorization':'Basic '+Buffer.from(`${proxy.username}:${proxy.password}`).toString('base64')}
  79. const stream=(path:string)=>new Promise<IncomingMessage>((resolve,reject)=>{
  80. const req=request(proxy.serverURL,{path:'http://fixture.test'+path,headers,agent:false},res=>{res.on('error',()=>{});res.once('data',()=>resolve(res))})
  81. req.on('error',reject);req.end()
  82. })
  83. try{
  84. const old=await stream('/old')
  85. delayNextCheck=true;await entered.promise
  86. const oldClosed=new Promise<void>(resolve=>old.once('close',resolve))
  87. proxy.disconnect();await oldClosed
  88. const fresh=await stream('/new')
  89. let timer:ReturnType<typeof setTimeout>|undefined
  90. const survives=new Promise<string>((resolve,reject)=>{
  91. timer=setTimeout(()=>reject(new Error('new stream stopped receiving data')),1000)
  92. fresh.once('data',chunk=>{clearTimeout(timer);resolve(String(chunk))})
  93. fresh.once('aborted',()=>{clearTimeout(timer);reject(new Error('old timer revoked the new stream'))})
  94. })
  95. void survives.catch(()=>{})
  96. rejectOld(new Error('old task expired'))
  97. await new Promise<void>(resolve=>setImmediate(resolve))
  98. freshResponse!.write('new epoch survives')
  99. expect(await survives).toBe('new epoch survives')
  100. fresh.destroy()
  101. }finally{
  102. rejectOld(new Error('test finished'))
  103. await proxy.close();server.closeAllConnections();await new Promise<void>(resolve=>server.close(()=>resolve()))
  104. }
  105. })