fix(socks): use happy-eyeballs to create a connection (#21847)

This commit is contained in:
Dmitry Gozman 2023-03-21 14:12:24 -07:00 committed by GitHub
parent 21e1c50bcd
commit 80a37ec171
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 58 additions and 28 deletions

View file

@ -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);

View file

@ -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<net.Socket> {
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);

View file

@ -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<net.Socket> {
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,

View file

@ -30,7 +30,7 @@ import type { Browser, ConnectOptions } from 'playwright-core';
type ExtraFixtures = {
connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>,
dummyServerPort: number,
ipV6ServerUrl: string,
ipV6ServerPort: number,
};
const test = playwrightTest.extend<ExtraFixtures>({
connect: async ({ browserType }, use) => {
@ -56,14 +56,14 @@ const test = playwrightTest.extend<ExtraFixtures>({
await new Promise<Error>(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('<html><body>from-ipv6-server</body></html>');
});
await new Promise<void>(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<Error>(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('<html><body></body></html>');
});
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');