fix(socks): use happy-eyeballs to create a connection (#21847)
This commit is contained in:
parent
21e1c50bcd
commit
80a37ec171
|
|
@ -14,17 +14,13 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import dns from 'dns';
|
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
import type { AddressInfo } from 'net';
|
import type { AddressInfo } from 'net';
|
||||||
import net from 'net';
|
import net from 'net';
|
||||||
import util from 'util';
|
|
||||||
import { debugLogger } from './debugLogger';
|
import { debugLogger } from './debugLogger';
|
||||||
import { createSocket } from '../utils/network';
|
import { createSocket } from '../utils/happy-eyeballs';
|
||||||
import { assert, createGuid, } from '../utils';
|
import { assert, createGuid, } from '../utils';
|
||||||
|
|
||||||
const dnsLookupAsync = util.promisify(dns.lookup);
|
|
||||||
|
|
||||||
// https://tools.ietf.org/html/rfc1928
|
// https://tools.ietf.org/html/rfc1928
|
||||||
|
|
||||||
enum SocksAuth {
|
enum SocksAuth {
|
||||||
|
|
@ -412,9 +408,7 @@ export class SocksProxy extends EventEmitter implements SocksConnectionClient {
|
||||||
|
|
||||||
private async _handleDirect(request: SocksSocketRequestedPayload) {
|
private async _handleDirect(request: SocksSocketRequestedPayload) {
|
||||||
try {
|
try {
|
||||||
// TODO: Node.js 17 does resolve localhost to ipv6
|
const socket = await createSocket(request.host, request.port);
|
||||||
const { address } = await dnsLookupAsync(request.host === 'localhost' ? '127.0.0.1' : request.host);
|
|
||||||
const socket = await createSocket(address, request.port);
|
|
||||||
socket.on('data', data => this._connections.get(request.uid)?.sendData(data));
|
socket.on('data', data => this._connections.get(request.uid)?.sendData(data));
|
||||||
socket.on('error', error => {
|
socket.on('error', error => {
|
||||||
this._connections.get(request.uid)?.error(error.message);
|
this._connections.get(request.uid)?.error(error.message);
|
||||||
|
|
@ -538,15 +532,11 @@ export class SocksProxyHandler extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (host === 'local.playwright')
|
if (host === 'local.playwright')
|
||||||
host = '127.0.0.1';
|
host = 'localhost';
|
||||||
// Node.js 17 does resolve localhost to ipv6
|
|
||||||
if (host === 'localhost')
|
|
||||||
host = '127.0.0.1';
|
|
||||||
try {
|
try {
|
||||||
if (this._redirectPortForTest)
|
if (this._redirectPortForTest)
|
||||||
port = this._redirectPortForTest;
|
port = this._redirectPortForTest;
|
||||||
const { address } = await dnsLookupAsync(host);
|
const socket = await createSocket(host, port);
|
||||||
const socket = await createSocket(address, port);
|
|
||||||
socket.on('data', data => {
|
socket.on('data', data => {
|
||||||
const payload: SocksSocketDataPayload = { uid, data };
|
const payload: SocksSocketDataPayload = { uid, data };
|
||||||
this.emit(SocksProxyHandler.Events.SocksData, payload);
|
this.emit(SocksProxyHandler.Events.SocksData, payload);
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,23 @@ class HttpsHappyEyeballsAgent extends https.Agent {
|
||||||
export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
|
export const httpsHappyEyeballsAgent = new HttpsHappyEyeballsAgent();
|
||||||
export const httpHappyEyeballsAgent = new HttpHappyEyeballsAgent();
|
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) {
|
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 lookup = (options as any).__testHookLookup || lookupAddresses;
|
||||||
const hostname = clientRequestArgsToHostName(options);
|
const hostname = clientRequestArgsToHostName(options);
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@
|
||||||
|
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import net from 'net';
|
|
||||||
import { getProxyForUrl } from '../utilsBundle';
|
import { getProxyForUrl } from '../utilsBundle';
|
||||||
import { HttpsProxyAgent } from '../utilsBundle';
|
import { HttpsProxyAgent } from '../utilsBundle';
|
||||||
import * as URL from 'url';
|
import * as URL from 'url';
|
||||||
|
|
@ -26,14 +25,6 @@ import { isString, isRegExp } from './rtti';
|
||||||
import { globToRegex } from './glob';
|
import { globToRegex } from './glob';
|
||||||
import { httpHappyEyeballsAgent, httpsHappyEyeballsAgent } from './happy-eyeballs';
|
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 = {
|
export type HTTPRequestParams = {
|
||||||
url: string,
|
url: string,
|
||||||
method?: string,
|
method?: string,
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ import type { Browser, ConnectOptions } from 'playwright-core';
|
||||||
type ExtraFixtures = {
|
type ExtraFixtures = {
|
||||||
connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>,
|
connect: (wsEndpoint: string, options?: ConnectOptions, redirectPortForTest?: number) => Promise<Browser>,
|
||||||
dummyServerPort: number,
|
dummyServerPort: number,
|
||||||
ipV6ServerUrl: string,
|
ipV6ServerPort: number,
|
||||||
};
|
};
|
||||||
const test = playwrightTest.extend<ExtraFixtures>({
|
const test = playwrightTest.extend<ExtraFixtures>({
|
||||||
connect: async ({ browserType }, use) => {
|
connect: async ({ browserType }, use) => {
|
||||||
|
|
@ -56,14 +56,14 @@ const test = playwrightTest.extend<ExtraFixtures>({
|
||||||
await new Promise<Error>(resolve => server.close(resolve));
|
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');
|
test.skip(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default');
|
||||||
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
const server = http.createServer((req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
res.end('<html><body>from-ipv6-server</body></html>');
|
res.end('<html><body>from-ipv6-server</body></html>');
|
||||||
});
|
});
|
||||||
await new Promise<void>(resolve => server.listen(0, '::1', resolve));
|
await new Promise<void>(resolve => server.listen(0, '::1', resolve));
|
||||||
const address = server.address() as net.AddressInfo;
|
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));
|
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');
|
test.fail(!!process.env.INSIDE_DOCKER, 'docker does not support IPv6 by default');
|
||||||
const remoteServer = await startRemoteServer(kind);
|
const remoteServer = await startRemoteServer(kind);
|
||||||
const browser = await connect(remoteServer.wsEndpoint());
|
const browser = await connect(remoteServer.wsEndpoint());
|
||||||
const page = await browser.newPage();
|
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');
|
expect(await page.content()).toContain('from-ipv6-server');
|
||||||
await browser.close();
|
await browser.close();
|
||||||
});
|
});
|
||||||
|
|
@ -718,6 +730,26 @@ for (const kind of ['launchServer', 'run-server'] as const) {
|
||||||
expect(reachedOriginalTarget).toBe(false);
|
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('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');
|
test.skip(browserName === 'webkit' && platform === 'darwin', 'no localhost proxying');
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue