diff --git a/packages/playwright-core/src/client/browserType.ts b/packages/playwright-core/src/client/browserType.ts index 27dd6aa5cf..d0c3e99d2a 100644 --- a/packages/playwright-core/src/client/browserType.ts +++ b/packages/playwright-core/src/client/browserType.ts @@ -69,7 +69,7 @@ export class BrowserType extends ChannelOwner imple async launch(options: LaunchOptions = {}): Promise { if (this._defaultConnectOptions) - return await this._connectInsteadOfLaunching(); + return await this._connectInsteadOfLaunching(this._defaultConnectOptions); const logger = options.logger || this._defaultLaunchOptions?.logger; assert(!(options as any).userDataDir, 'userDataDir option is not supported in `browserType.launch`. Use `browserType.launchPersistentContext` instead'); @@ -89,14 +89,15 @@ export class BrowserType extends ChannelOwner imple }); } - private async _connectInsteadOfLaunching(): Promise { - const connectOptions = this._defaultConnectOptions!; - return this._connect(connectOptions.wsEndpoint, { + private async _connectInsteadOfLaunching(connectOptions: ConnectOptions): Promise { + return this._connect({ + wsEndpoint: connectOptions.wsEndpoint, headers: { - 'x-playwright-browser': this.name(), 'x-playwright-launch-options': JSON.stringify(this._defaultLaunchOptions || {}), ...connectOptions.headers, }, + _exposeNetwork: connectOptions._exposeNetwork, + slowMo: connectOptions.slowMo, timeout: connectOptions.timeout ?? 3 * 60 * 1000, // 3 minutes }); } @@ -132,22 +133,28 @@ export class BrowserType extends ChannelOwner imple }); } - connect(options: api.ConnectOptions & { wsEndpoint?: string }): Promise; + connect(options: api.ConnectOptions & { wsEndpoint: string }): Promise; connect(wsEndpoint: string, options?: api.ConnectOptions): Promise; - async connect(optionsOrWsEndpoint: string|(api.ConnectOptions & { wsEndpoint?: string }), options?: api.ConnectOptions): Promise{ + async connect(optionsOrWsEndpoint: string | (api.ConnectOptions & { wsEndpoint: string }), options?: api.ConnectOptions): Promise{ if (typeof optionsOrWsEndpoint === 'string') - return this._connect(optionsOrWsEndpoint, options); + return this._connect({ ...options, wsEndpoint: optionsOrWsEndpoint }); assert(optionsOrWsEndpoint.wsEndpoint, 'options.wsEndpoint is required'); - return this._connect(optionsOrWsEndpoint.wsEndpoint, optionsOrWsEndpoint); + return this._connect(optionsOrWsEndpoint); } - async _connect(wsEndpoint: string, params: Partial = {}): Promise { + async _connect(params: ConnectOptions): Promise { const logger = params.logger; return await this._wrapApiCall(async () => { const deadline = params.timeout ? monotonicTime() + params.timeout : 0; const headers = { 'x-playwright-browser': this.name(), ...params.headers }; const localUtils = this._connection.localUtils(); - const connectParams: channels.LocalUtilsConnectParams = { wsEndpoint, headers, slowMo: params.slowMo, timeout: params.timeout }; + const connectParams: channels.LocalUtilsConnectParams = { + wsEndpoint: params.wsEndpoint, + headers, + exposeNetwork: params._exposeNetwork, + slowMo: params.slowMo, + timeout: params.timeout, + }; if ((params as any).__testHookRedirectPortForwarding) connectParams.socksProxyRedirectPortForTest = (params as any).__testHookRedirectPortForwarding; const { pipe } = await localUtils._channel.connect(connectParams); diff --git a/packages/playwright-core/src/client/types.ts b/packages/playwright-core/src/client/types.ts index cfdcfa3030..e9fdb61c09 100644 --- a/packages/playwright-core/src/client/types.ts +++ b/packages/playwright-core/src/client/types.ts @@ -87,6 +87,7 @@ export type LaunchPersistentContextOptions = Omit(); + private _pattern: string | undefined; private _redirectPortForTest: number | undefined; - constructor(redirectPortForTest?: number) { + constructor(pattern: string | undefined, redirectPortForTest?: number) { super(); + this._pattern = pattern; this._redirectPortForTest = redirectPortForTest; } + private _matchesPattern(host: string, port: number) { + return this._pattern === '*' || (this._pattern === 'localhost' && host === 'localhost'); + } + cleanup() { for (const uid of this._sockets.keys()) this.socketClosed({ uid }); } async socketRequested({ uid, host, port }: SocksSocketRequestedPayload): Promise { + if (!this._matchesPattern(host, port)) { + const payload: SocksSocketFailedPayload = { uid, errorCode: 'ECONNREFUSED' }; + this.emit(SocksProxyHandler.Events.SocksFailed, payload); + return; + } + if (host === 'local.playwright') host = '127.0.0.1'; // Node.js 17 does resolve localhost to ipv6 diff --git a/packages/playwright-core/src/containers/docker.ts b/packages/playwright-core/src/containers/docker.ts index de5ffdd0b6..79ffbe16da 100644 --- a/packages/playwright-core/src/containers/docker.ts +++ b/packages/playwright-core/src/containers/docker.ts @@ -288,7 +288,7 @@ async function tetherHostNetwork(endpoint: string) { 'x-playwright-proxy': '*', }; const transport = await WebSocketTransport.connect(undefined /* progress */, wsEndpoint, headers, true /* followRedirects */); - const socksInterceptor = new SocksInterceptor(transport, undefined); + const socksInterceptor = new SocksInterceptor(transport, '*', undefined); transport.onmessage = json => socksInterceptor.interceptMessage(json); transport.onclose = () => { socksInterceptor.cleanup(); @@ -387,7 +387,7 @@ export function addDockerCLI(program: Command) { .option('--browser ', 'browser to launch') .option('--endpoint ', 'server endpoint') .action(async function(options: { browser: string, endpoint: string }) { - let browserType: any; + let browserType: playwright.BrowserType | undefined; if (options.browser === 'chromium') browserType = playwright.chromium; else if (options.browser === 'firefox') @@ -404,9 +404,9 @@ export function addDockerCLI(program: Command) { headless: false, viewport: null, }), - 'x-playwright-proxy': '*', }, - }); + _exposeNetwork: '*', + } as any); const context = await browser.newContext(); context.on('page', (page: playwright.Page) => { page.on('dialog', () => {}); // Prevent dialogs from being automatically dismissed. diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 1781af6ae5..53ececed59 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -248,6 +248,7 @@ scheme.LocalUtilsHarUnzipResult = tOptional(tObject({})); scheme.LocalUtilsConnectParams = tObject({ wsEndpoint: tString, headers: tOptional(tAny), + exposeNetwork: tOptional(tString), slowMo: tOptional(tNumber), timeout: tOptional(tNumber), socksProxyRedirectPortForTest: tOptional(tNumber), diff --git a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts index 007d46bd3a..3979847247 100644 --- a/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/localUtilsDispatcher.ts @@ -156,11 +156,15 @@ export class LocalUtilsDispatcher extends Dispatcher<{ guid: string }, channels. const controller = new ProgressController(metadata, this._object as SdkObject); controller.setLogName('browser'); return await controller.run(async progress => { - const paramsHeaders = Object.assign({ 'User-Agent': getUserAgent() }, params.headers || {}); + const wsHeaders = { + 'User-Agent': getUserAgent(), + 'x-playwright-proxy': params.exposeNetwork ?? '', + ...params.headers, + }; const wsEndpoint = await urlToWSEndpoint(progress, params.wsEndpoint); - const transport = await WebSocketTransport.connect(progress, wsEndpoint, paramsHeaders, true); - const socksInterceptor = new SocksInterceptor(transport, params.socksProxyRedirectPortForTest); + const transport = await WebSocketTransport.connect(progress, wsEndpoint, wsHeaders, true); + const socksInterceptor = new SocksInterceptor(transport, params.exposeNetwork, params.socksProxyRedirectPortForTest); const pipe = new JsonPipeDispatcher(this); transport.onmessage = json => { if (socksInterceptor.interceptMessage(json)) diff --git a/packages/playwright-core/src/server/socksInterceptor.ts b/packages/playwright-core/src/server/socksInterceptor.ts index 2df184cfeb..0fd31645e5 100644 --- a/packages/playwright-core/src/server/socksInterceptor.ts +++ b/packages/playwright-core/src/server/socksInterceptor.ts @@ -27,8 +27,8 @@ export class SocksInterceptor { private _socksSupportObjectGuid?: string; private _ids = new Set(); - constructor(transport: WebSocketTransport, redirectPortForTest: number | undefined) { - this._handler = new socks.SocksProxyHandler(redirectPortForTest); + constructor(transport: WebSocketTransport, pattern: string | undefined, redirectPortForTest: number | undefined) { + this._handler = new socks.SocksProxyHandler(pattern, redirectPortForTest); let lastId = -1; this._channel = new Proxy(new EventEmitter(), { diff --git a/packages/playwright-test/src/index.ts b/packages/playwright-test/src/index.ts index 8416ba556b..5efdcb7ac5 100644 --- a/packages/playwright-test/src/index.ts +++ b/packages/playwright-test/src/index.ts @@ -87,8 +87,9 @@ const playwrightFixtures: Fixtures = ({ } return use({ wsEndpoint, - headers - }); + headers, + _exposeNetwork: process.env.PW_TEST_CONNECT_EXPOSE_NETWORK, + } as any); }, { scope: 'worker', option: true }], screenshot: ['off', { scope: 'worker', option: true }], video: ['off', { scope: 'worker', option: true }], diff --git a/packages/playwright-test/src/plugins/dockerPlugin.ts b/packages/playwright-test/src/plugins/dockerPlugin.ts index 32d6e7fc15..a20b39e85e 100644 --- a/packages/playwright-test/src/plugins/dockerPlugin.ts +++ b/packages/playwright-test/src/plugins/dockerPlugin.ts @@ -35,9 +35,7 @@ export const dockerPlugin: TestRunnerPlugin = { throw new Error('ERROR: please launch docker container separately!'); println(''); process.env.PW_TEST_CONNECT_WS_ENDPOINT = info.httpEndpoint; - process.env.PW_TEST_CONNECT_HEADERS = JSON.stringify({ - 'x-playwright-proxy': '*', - }); + process.env.PW_TEST_CONNECT_EXPOSE_NETWORK = '*'; }, }; diff --git a/packages/protocol/src/channels.ts b/packages/protocol/src/channels.ts index d5b555c68a..010da28b8c 100644 --- a/packages/protocol/src/channels.ts +++ b/packages/protocol/src/channels.ts @@ -446,12 +446,14 @@ export type LocalUtilsHarUnzipResult = void; export type LocalUtilsConnectParams = { wsEndpoint: string, headers?: any, + exposeNetwork?: string, slowMo?: number, timeout?: number, socksProxyRedirectPortForTest?: number, }; export type LocalUtilsConnectOptions = { headers?: any, + exposeNetwork?: string, slowMo?: number, timeout?: number, socksProxyRedirectPortForTest?: number, diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index 0c9e5daf47..bc29cb9674 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -535,6 +535,7 @@ LocalUtils: parameters: wsEndpoint: string headers: json? + exposeNetwork: string? slowMo: number? timeout: number? socksProxyRedirectPortForTest: number? diff --git a/tests/config/remoteServer.ts b/tests/config/remoteServer.ts index b0681f557c..50f9b9c1a9 100644 --- a/tests/config/remoteServer.ts +++ b/tests/config/remoteServer.ts @@ -15,7 +15,7 @@ */ import path from 'path'; -import type { BrowserType, Browser, LaunchOptions } from 'playwright-core'; +import type { BrowserType, Browser } from 'playwright-core'; import type { CommonFixtures, TestChildProcess } from './commonFixtures'; export interface PlaywrightServer { @@ -79,7 +79,7 @@ export class RemoteServer implements PlaywrightServer { const browserOptions = (browserType as any)._defaultLaunchOptions; // Copy options to prevent a large JSON string when launching subprocess. // Otherwise, we get `Error: spawn ENAMETOOLONG` on Windows. - const launchOptions: LaunchOptions = { + const launchOptions: Parameters[0] = { args: browserOptions.args, headless: browserOptions.headless, channel: browserOptions.channel, diff --git a/tests/library/browsertype-connect.spec.ts b/tests/library/browsertype-connect.spec.ts index 87713c87a4..c418912e26 100644 --- a/tests/library/browsertype-connect.spec.ts +++ b/tests/library/browsertype-connect.spec.ts @@ -679,7 +679,6 @@ for (const kind of ['launchServer', 'run-server'] as const) { test.describe('socks proxy', () => { test.fixme(({ platform, browserName }) => browserName === 'webkit' && platform === 'win32'); test.skip(({ mode }) => mode !== 'default'); - test.skip(kind === 'launchServer', 'This feature is not yet supported in launchServer'); test('should forward non-forwarded requests', async ({ server, startRemoteServer, connect }) => { let reachedOriginalTarget = false; @@ -688,9 +687,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { res.end('original-target'); }); const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': '*' } - }); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any); const page = await browser.newPage(); await page.goto(server.PREFIX + '/foo.html'); expect(await page.content()).toContain('original-target'); @@ -707,9 +704,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); const examplePort = 20_000 + testInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': '*' } - }, dummyServerPort); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort); const page = await browser.newPage(); await page.goto(`http://127.0.0.1:${examplePort}/foo.html`); expect(await page.content()).toContain('from-dummy-server'); @@ -726,9 +721,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); const examplePort = 20_000 + workerInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': '*' } - }, dummyServerPort); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort); const page = await browser.newPage(); const response = await page.request.get(`http://127.0.0.1:${examplePort}/foo.html`); expect(response.status()).toBe(200); @@ -744,9 +737,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); const examplePort = 20_000 + workerInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': '*' } - }, dummyServerPort); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any, dummyServerPort); const page = await browser.newPage(); await page.goto(`http://local.playwright:${examplePort}/foo.html`); expect(await page.content()).toContain('from-dummy-server'); @@ -756,9 +747,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { test('should lead to the error page for forwarded requests when the connection is refused', async ({ connect, startRemoteServer, browserName }, workerInfo) => { const examplePort = 20_000 + workerInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': '*' } - }); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: '*' } as any); const page = await browser.newPage(); const error = await page.goto(`http://127.0.0.1:${examplePort}`).catch(e => e); if (browserName === 'chromium') @@ -779,9 +768,7 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); const examplePort = 20_000 + workerInfo.workerIndex * 3; const remoteServer = await startRemoteServer(kind); - const browser = await connect(remoteServer.wsEndpoint(), { - headers: { 'x-playwright-proxy': 'localhost' } - }, dummyServerPort); + const browser = await connect(remoteServer.wsEndpoint(), { _exposeNetwork: 'localhost' } as any, dummyServerPort); const page = await browser.newPage(); // localhost should be proxied. @@ -801,6 +788,30 @@ for (const kind of ['launchServer', 'run-server'] as const) { }); expect(failed).toBe(true); }); + + test('should check proxy pattern on the client', async ({ connect, startRemoteServer, server, browserName, platform, dummyServerPort }, workerInfo) => { + let reachedOriginalTarget = false; + server.setRoute('/foo.html', async (req, res) => { + reachedOriginalTarget = true; + res.end('from-original-server'); + }); + const remoteServer = await startRemoteServer(kind); + const browser = await connect(remoteServer.wsEndpoint(), { + _exposeNetwork: 'localhost', + headers: { + 'x-playwright-proxy': '*', + }, + } as any, dummyServerPort); + const page = await browser.newPage(); + + // 127.0.0.1 should fail on the client side. + let failed = false; + await page.goto(`http://127.0.0.1:${server.PORT}/foo.html`).catch(e => { + failed = true; + }); + expect(failed).toBe(true); + expect(reachedOriginalTarget).toBe(false); + }); }); }); }