From 80a37ec171f4c1fd72b2adabf10856942324d1ba Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Tue, 21 Mar 2023 14:12:24 -0700 Subject: [PATCH] fix(socks): use happy-eyeballs to create a connection (#21847) --- .../playwright-core/src/common/socksProxy.ts | 18 ++------ .../src/utils/happy-eyeballs.ts | 17 ++++++++ packages/playwright-core/src/utils/network.ts | 9 ---- tests/library/browsertype-connect.spec.ts | 42 ++++++++++++++++--- 4 files changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/playwright-core/src/common/socksProxy.ts b/packages/playwright-core/src/common/socksProxy.ts index dcc9879965..e0fe8bc73b 100644 --- a/packages/playwright-core/src/common/socksProxy.ts +++ b/packages/playwright-core/src/common/socksProxy.ts @@ -14,17 +14,13 @@ * limitations under the License. */ -import dns from 'dns'; import EventEmitter from 'events'; import type { AddressInfo } from 'net'; import net from 'net'; -import util from 'util'; import { debugLogger } from './debugLogger'; -import { createSocket } from '../utils/network'; +import { createSocket } from '../utils/happy-eyeballs'; import { assert, createGuid, } from '../utils'; -const dnsLookupAsync = util.promisify(dns.lookup); - // https://tools.ietf.org/html/rfc1928 enum SocksAuth { @@ -412,9 +408,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient { private async _handleDirect(request: SocksSocketRequestedPayload) { try { - // TODO: Node.js 17 does resolve localhost to ipv6 - const { address } = await dnsLookupAsync(request.host === 'localhost' ? '127.0.0.1' : request.host); - const socket = await createSocket(address, request.port); + const socket = await createSocket(request.host, request.port); socket.on('data', data => this._connections.get(request.uid)?.sendData(data)); socket.on('error', error => { this._connections.get(request.uid)?.error(error.message); @@ -538,15 +532,11 @@ export class SocksProxyHandler extends EventEmitter { } if (host === 'local.playwright') - host = '127.0.0.1'; - // Node.js 17 does resolve localhost to ipv6 - if (host === 'localhost') - host = '127.0.0.1'; + host = 'localhost'; try { if (this._redirectPortForTest) port = this._redirectPortForTest; - const { address } = await dnsLookupAsync(host); - const socket = await createSocket(address, port); + const socket = await createSocket(host, port); socket.on('data', data => { const payload: SocksSocketDataPayload = { uid, data }; this.emit(SocksProxyHandler.Events.SocksData, payload); diff --git a/packages/playwright-core/src/utils/happy-eyeballs.ts b/packages/playwright-core/src/utils/happy-eyeballs.ts index 936dfb739d..604fdcccf1 100644 --- a/packages/playwright-core/src/utils/happy-eyeballs.ts +++ b/packages/playwright-core/src/utils/happy-eyeballs.ts @@ -48,6 +48,23 @@ class HttpsHappyEyeballsAgent extends https.Agent { export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent(); export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent(); +export async function createSocket(host: string, port: number): Promise { + return new Promise((resolve, reject) => { + if (net.isIP(host)) { + const socket = net.createConnection({ host, port }); + socket.on('connect', () => resolve(socket)); + socket.on('error', error => reject(error)); + } else { + createConnectionAsync({ host, port }, (err, socket) => { + if (err) + reject(err); + if (socket) + resolve(socket); + }, /* useTLS */ false).catch(err => reject(err)); + } + }); +} + async function createConnectionAsync(options: http.ClientRequestArgs, oncreate: ((err: Error | null, socket?: net.Socket) => void) | undefined, useTLS: boolean) { const lookup = (options as any).__testHookLookup || lookupAddresses; const hostname = clientRequestArgsToHostName(options); diff --git a/packages/playwright-core/src/utils/network.ts b/packages/playwright-core/src/utils/network.ts index 1a85f88a75..0f12929707 100644 --- a/packages/playwright-core/src/utils/network.ts +++ b/packages/playwright-core/src/utils/network.ts @@ -17,7 +17,6 @@ import http from 'http'; import https from 'https'; -import net from 'net'; import { getProxyForUrl } from '../utilsBundle'; import { HttpsProxyAgent } from '../utilsBundle'; import * as URL from 'url'; @@ -26,14 +25,6 @@ import { isString, isRegExp } from './rtti'; import { globToRegex } from './glob'; import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs'; -export async function createSocket(host: string, port: number): Promise { - return new Promise((resolve, reject) => { - const socket = net.createConnection({ host, port }); - socket.on('connect', () => resolve(socket)); - socket.on('error', error => reject(error)); - }); -} - export type HTTPRequestParams = { url: string, method?: string, diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 5e8e8a940c..113a1c200c 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -30,7 +30,7 @@ import type { Browser, ConnectOptions } from 'playwright-core'; type ExtraFixtures = { connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise, dummyServerPort: number, - ipV6ServerUrl: string, + ipV6ServerPort: number, }; const test = playwrightTest.extend({ connect: async ({ browserType }, use) => { @@ -56,14 +56,14 @@ const test = playwrightTest.extend({ await new Promise(resolve => server.close(resolve)); }, - ipV6ServerUrl: async ({}, use) => { + ipV6ServerPort: async ({}, use) => { test.skip(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default'); const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => { res.end('from-ipv6-server'); }); await new Promise(resolve => server.listen(0, '::1', resolve)); const address = server.address() as net.AddressInfo; - await use('http://[::1]:' + address.port); + await use(address.port); await new Promise(resolve => server.close(resolve)); }, }); @@ -145,12 +145,24 @@ for (const kind of ['launchServer', 'run-server'] as const) { } }); - test('should be able to visit ipv6', async ({ connect, startRemoteServer, ipV6ServerUrl }) => { + test('should be able to visit ipv6', async ({ connect, startRemoteServer, ipV6ServerPort }) => { test.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default'); const remoteServer = await startRemoteServer(kind); const browser = await connect(remoteServer.wsEndpoint()); const page = await browser.newPage(); - await page.goto(ipV6ServerUrl); + const ipV6Url = 'http://[::1]:' + ipV6ServerPort; + await page.goto(ipV6Url); + expect(await page.content()).toContain('from-ipv6-server'); + await browser.close(); + }); + + test('should be able to visit ipv6 through localhost', async ({ connect, startRemoteServer, ipV6ServerPort }) => { + test.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default'); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint()); + const page = await browser.newPage(); + const ipV6Url = 'http://localhost:' + ipV6ServerPort; + await page.goto(ipV6Url); expect(await page.content()).toContain('from-ipv6-server'); await browser.close(); }); @@ -718,6 +730,26 @@ for (const kind of ['launchServer', 'run-server'] as const) { expect(reachedOriginalTarget).toBe(false); }); + test('should proxy ipv6 localhost requests @smoke', async ({ startRemoteServer, server, browserName, connect, platform, ipV6ServerPort }, testInfo) => { + test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying'); + + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end(''); + }); + const examplePort = 20_000 + testInfo.workerIndex * 3; + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, ipV6ServerPort); + const page = await browser.newPage(); + await page.goto(`http://[::1]:${examplePort}/foo.html`); + expect(await page.content()).toContain('from-ipv6-server'); + const page2 = await browser.newPage(); + await page2.goto(`http://localhost:${examplePort}/foo.html`); + expect(await page2.content()).toContain('from-ipv6-server'); + expect(reachedOriginalTarget).toBe(false); + }); + test('should proxy localhost requests from fetch api', async ({ startRemoteServer, server, browserName, connect, channel, platform, dummyServerPort }, workerInfo) => { test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');