@open-mercato/web-research-browser 0.10.1-develop.8050.1.45ae874fe7 → 0.10.1-develop.8053.1.97a8e36d1b
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -3
- package/src/__tests__/egressProxy.test.ts +100 -26
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@open-mercato/web-research-browser",
|
|
3
|
-
"version": "0.10.1-develop.
|
|
3
|
+
"version": "0.10.1-develop.8053.1.97a8e36d1b",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -38,12 +38,12 @@
|
|
|
38
38
|
"@open-mercato/web-research": "^0.6.0"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@open-mercato/web-research-serp": "0.10.1-develop.
|
|
41
|
+
"@open-mercato/web-research-serp": "0.10.1-develop.8053.1.97a8e36d1b",
|
|
42
42
|
"playwright": "^1.40.0",
|
|
43
43
|
"zod": "^4.6.6"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"@open-mercato/web-research": "0.10.1-develop.
|
|
46
|
+
"@open-mercato/web-research": "0.10.1-develop.8053.1.97a8e36d1b",
|
|
47
47
|
"@types/jest": "^30.0.0",
|
|
48
48
|
"@types/node": "^26.0.1",
|
|
49
49
|
"esbuild": "^0.28.1",
|
|
@@ -16,6 +16,7 @@ type Harness = {
|
|
|
16
16
|
proxy: EgressProxy
|
|
17
17
|
proxyPort: number
|
|
18
18
|
dials: EgressDialTarget[]
|
|
19
|
+
upstreamSockets: Socket[]
|
|
19
20
|
privateConnections: () => number
|
|
20
21
|
publicRequests: Recorded[]
|
|
21
22
|
publicSockets: Set<Socket>
|
|
@@ -31,6 +32,13 @@ async function listen(server: Server | ReturnType<typeof createTcpServer>): Prom
|
|
|
31
32
|
return (server.address() as AddressInfo).port
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
async function waitUntil(condition: () => boolean, timeoutMs = 5_000): Promise<void> {
|
|
36
|
+
const deadline = Date.now() + timeoutMs
|
|
37
|
+
while (!condition() && Date.now() < deadline) {
|
|
38
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 1))
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
34
42
|
function closeAll(): Promise<void[]> {
|
|
35
43
|
const closing = [...servers, ...tcpServers].map(
|
|
36
44
|
(server) =>
|
|
@@ -121,6 +129,7 @@ async function buildHarness(): Promise<Harness> {
|
|
|
121
129
|
}
|
|
122
130
|
|
|
123
131
|
const dials: EgressDialTarget[] = []
|
|
132
|
+
const upstreamSockets: Socket[] = []
|
|
124
133
|
const portFor = (addresses: readonly string[]): number => {
|
|
125
134
|
if (addresses.length === 1 && [PUBLIC_IP, PUBLIC_IPV6, PUBLIC_6TO4].includes(addresses[0])) return publicPort
|
|
126
135
|
if (addresses.length === 1 && addresses[0] === RESET_IP) return resetPort
|
|
@@ -132,13 +141,16 @@ async function buildHarness(): Promise<Harness> {
|
|
|
132
141
|
lookup,
|
|
133
142
|
connect: (target) => {
|
|
134
143
|
dials.push(target)
|
|
135
|
-
|
|
144
|
+
const upstream = netConnect({ host: '127.0.0.1', port: portFor(target.addresses) })
|
|
145
|
+
upstreamSockets.push(upstream)
|
|
146
|
+
return upstream
|
|
136
147
|
},
|
|
137
148
|
})
|
|
138
149
|
return {
|
|
139
150
|
proxy,
|
|
140
151
|
proxyPort: Number(new URL(proxy.url).port),
|
|
141
152
|
dials,
|
|
153
|
+
upstreamSockets,
|
|
142
154
|
privateConnections: () => privateHits,
|
|
143
155
|
publicRequests,
|
|
144
156
|
publicSockets,
|
|
@@ -382,11 +394,14 @@ describe('browser egress proxy (#7218)', () => {
|
|
|
382
394
|
expect(tunnel.status).toBe(200)
|
|
383
395
|
tunnel.socket.on('error', () => undefined)
|
|
384
396
|
const clientClosed = new Promise<void>((resolve) => tunnel.socket.once('close', () => resolve()))
|
|
397
|
+
await waitUntil(() => harness.publicSockets.size === 1)
|
|
398
|
+
expect(harness.publicSockets.size).toBe(1)
|
|
385
399
|
|
|
386
400
|
await harness.proxy.close()
|
|
401
|
+
expect(harness.upstreamSockets.map((socket) => socket.destroyed)).toEqual([true])
|
|
387
402
|
await clientClosed
|
|
388
403
|
|
|
389
|
-
await
|
|
404
|
+
await waitUntil(() => harness.publicSockets.size === 0)
|
|
390
405
|
expect(harness.publicSockets.size).toBe(0)
|
|
391
406
|
await expect(
|
|
392
407
|
new Promise((resolve, reject) => {
|
|
@@ -401,6 +416,59 @@ describe('browser egress proxy (#7218)', () => {
|
|
|
401
416
|
})
|
|
402
417
|
})
|
|
403
418
|
|
|
419
|
+
const REQUEST_FORMS = [
|
|
420
|
+
['CONNECT', 'CONNECT vetting.test:443 HTTP/1.1\r\nHost: vetting.test:443\r\n\r\n'],
|
|
421
|
+
['absolute-form', 'GET http://vetting.test/ HTTP/1.1\r\nHost: vetting.test\r\n\r\n'],
|
|
422
|
+
]
|
|
423
|
+
|
|
424
|
+
const CLIENT_DEPARTURES: Array<[string, (client: Socket) => void]> = [
|
|
425
|
+
['disconnect', (client) => client.destroy()],
|
|
426
|
+
['reset', (client) => client.resetAndDestroy()],
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
type LookupRecords = Awaited<ReturnType<LookupFn>>
|
|
430
|
+
|
|
431
|
+
type DeferredLookupProxy = {
|
|
432
|
+
proxy: EgressProxy
|
|
433
|
+
dials: EgressDialTarget[]
|
|
434
|
+
started: Promise<void>
|
|
435
|
+
answer: (records: LookupRecords) => void
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
function proxyRoundTrip(proxy: EgressProxy): Promise<void> {
|
|
439
|
+
return new Promise((resolve, reject) => {
|
|
440
|
+
const barrier = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
|
|
441
|
+
barrier.once('error', reject)
|
|
442
|
+
barrier.once('close', () => reject(new Error('[internal] barrier closed without a response')))
|
|
443
|
+
barrier.once('data', () => {
|
|
444
|
+
barrier.destroy()
|
|
445
|
+
resolve()
|
|
446
|
+
})
|
|
447
|
+
barrier.write('GET /barrier HTTP/1.1\r\nHost: barrier\r\nConnection: close\r\n\r\n')
|
|
448
|
+
})
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function startProxyWithDeferredLookup(): Promise<DeferredLookupProxy> {
|
|
452
|
+
const dials: EgressDialTarget[] = []
|
|
453
|
+
let resolveLookup: (records: LookupRecords) => void = () => undefined
|
|
454
|
+
let lookupStarted: () => void = () => undefined
|
|
455
|
+
const started = new Promise<void>((resolve) => {
|
|
456
|
+
lookupStarted = resolve
|
|
457
|
+
})
|
|
458
|
+
const proxy = await startEgressProxy({
|
|
459
|
+
lookup: () =>
|
|
460
|
+
new Promise((resolve) => {
|
|
461
|
+
resolveLookup = resolve
|
|
462
|
+
lookupStarted()
|
|
463
|
+
}),
|
|
464
|
+
connect: (dialTarget) => {
|
|
465
|
+
dials.push(dialTarget)
|
|
466
|
+
return netConnect({ host: '127.0.0.1', port: 9 })
|
|
467
|
+
},
|
|
468
|
+
})
|
|
469
|
+
return { proxy, dials, started, answer: (records) => resolveLookup(records) }
|
|
470
|
+
}
|
|
471
|
+
|
|
404
472
|
describe('pinned dial and vetting limits (#7218)', () => {
|
|
405
473
|
it('connects the default dial to the vetted address without resolving the hostname', async () => {
|
|
406
474
|
const target = createTcpServer((socket) => socket.end())
|
|
@@ -457,38 +525,44 @@ describe('pinned dial and vetting limits (#7218)', () => {
|
|
|
457
525
|
}
|
|
458
526
|
})
|
|
459
527
|
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
})
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
528
|
+
describe.each(CLIENT_DEPARTURES)('when the client leaves by %s', (_departure, leave) => {
|
|
529
|
+
it.each(REQUEST_FORMS)('does not dial for a client that left during vetting (%s)', async (_form, request) => {
|
|
530
|
+
const { proxy, dials, started, answer } = await startProxyWithDeferredLookup()
|
|
531
|
+
try {
|
|
532
|
+
const client = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
|
|
533
|
+
client.on('error', () => undefined)
|
|
534
|
+
client.write(request)
|
|
535
|
+
await started
|
|
536
|
+
const clientClosed = new Promise<void>((resolve) => client.once('close', () => resolve()))
|
|
537
|
+
leave(client)
|
|
538
|
+
await clientClosed
|
|
539
|
+
await proxyRoundTrip(proxy)
|
|
540
|
+
answer([{ address: PUBLIC_IP, family: 4 }])
|
|
541
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
542
|
+
|
|
543
|
+
expect(dials).toEqual([])
|
|
544
|
+
} finally {
|
|
545
|
+
await proxy.close()
|
|
546
|
+
}
|
|
477
547
|
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
it.each(REQUEST_FORMS)('does not dial a request still being vetted when the proxy closes (%s)', async (_form, request) => {
|
|
551
|
+
const { proxy, dials, started, answer } = await startProxyWithDeferredLookup()
|
|
552
|
+
const client = netConnect({ host: '127.0.0.1', port: Number(new URL(proxy.url).port) })
|
|
553
|
+
client.on('error', () => undefined)
|
|
478
554
|
try {
|
|
479
|
-
|
|
480
|
-
client.on('error', () => undefined)
|
|
481
|
-
client.write('CONNECT leaving.test:443 HTTP/1.1\r\nHost: leaving.test:443\r\n\r\n')
|
|
555
|
+
client.write(request)
|
|
482
556
|
await started
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
await clientClosed
|
|
486
|
-
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
557
|
+
|
|
558
|
+
const closing = proxy.close()
|
|
487
559
|
answer([{ address: PUBLIC_IP, family: 4 }])
|
|
560
|
+
await closing
|
|
488
561
|
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
489
562
|
|
|
490
563
|
expect(dials).toEqual([])
|
|
491
564
|
} finally {
|
|
565
|
+
client.destroy()
|
|
492
566
|
await proxy.close()
|
|
493
567
|
}
|
|
494
568
|
})
|