diff --git a/docs/src/api/params.md b/docs/src/api/params.md index 608855ab2a..de930ee97e 100644 --- a/docs/src/api/params.md +++ b/docs/src/api/params.md @@ -558,10 +558,6 @@ TLS Client Authentication allows the server to request a client certificate and An array of client certificates to be used. Each certificate object must have either both `certPath` and `keyPath`, a single `pfxPath`, or their corresponding direct value equivalents (`cert` and `key`, or `pfx`). Optionally, `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided with an exact match to the request origin that the certificate is valid for. -:::note -Using Client Certificates in combination with Proxy Servers is not supported. -::: - :::note When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it work by replacing `localhost` with `local.playwright`. ::: diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 04a23e7eac..252443bc44 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -16,7 +16,7 @@ import type * as types from './types'; import type * as channels from '@protocol/channels'; -import { BrowserContext, createClientCertificatesProxyIfNeeded, validateBrowserContextOptions } from './browserContext'; +import { BrowserContext, validateBrowserContextOptions } from './browserContext'; import { Page } from './page'; import { Download } from './download'; import type { ProxySettings } from './types'; @@ -25,6 +25,7 @@ import type { RecentLogsCollector } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; import { SdkObject } from './instrumentation'; import { Artifact } from './artifact'; +import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; export interface BrowserProcess { onclose?: ((exitCode: number | null, signal: string | null) => void); @@ -82,8 +83,10 @@ export abstract class Browser extends SdkObject { async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); - const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options); - if (clientCertificatesProxy) { + let clientCertificatesProxy: ClientCertificatesProxy | undefined; + if (options.clientCertificates?.length) { + clientCertificatesProxy = new ClientCertificatesProxy(options); + options = { ...options }; options.proxyOverride = await clientCertificatesProxy.listen(); options.internalIgnoreHTTPSErrors = true; } diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index e1166f3e97..499356ca49 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -42,7 +42,7 @@ import * as consoleApiSource from '../generated/consoleApiSource'; import { BrowserContextAPIRequestContext } from './fetch'; import type { Artifact } from './artifact'; import { Clock } from './clock'; -import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; +import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import { RecorderApp } from './recorder/recorderApp'; export abstract class BrowserContext extends SdkObject { @@ -659,15 +659,6 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) { } } -export async function createClientCertificatesProxyIfNeeded(options: types.BrowserContextOptions, browserOptions?: BrowserOptions) { - if (!options.clientCertificates?.length) - return; - if ((options.proxy?.server && options.proxy?.server !== 'per-context') || (browserOptions?.proxy?.server && browserOptions?.proxy?.server !== 'http://per-context')) - throw new Error('Cannot specify both proxy and clientCertificates'); - verifyClientCertificates(options.clientCertificates); - return new ClientCertificatesProxy(options); -} - export function validateBrowserContextOptions(options: types.BrowserContextOptions, browserOptions: BrowserOptions) { if (options.noDefaultViewport && options.deviceScaleFactor !== undefined) throw new Error(`"deviceScaleFactor" option is not supported with null "viewport"`); diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index 286ec27c29..abc15a20f4 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -18,7 +18,7 @@ import fs from 'fs'; import * as os from 'os'; import path from 'path'; import type { BrowserContext } from './browserContext'; -import { createClientCertificatesProxyIfNeeded, normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; +import { normalizeProxySettings, validateBrowserContextOptions } from './browserContext'; import type { BrowserName } from './registry'; import { registry } from './registry'; import type { ConnectionTransport } from './transport'; @@ -39,6 +39,7 @@ import { RecentLogsCollector } from '../utils/debugLogger'; import type { CallMetadata } from './instrumentation'; import { SdkObject } from './instrumentation'; import { type ProtocolError, isProtocolError } from './protocolError'; +import { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; export const kNoXServerRunningError = 'Looks like you launched a headed browser without having a XServer running.\n' + 'Set either \'headless: true\' or use \'xvfb-run \' before running Playwright.\n\n<3 Playwright Team'; @@ -92,19 +93,23 @@ export abstract class BrowserType extends SdkObject { return browser; } - async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { - const launchOptions = this._validateLaunchOptions(persistentContextOptions); + async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean, internalIgnoreHTTPSErrors?: boolean }): Promise { + const launchOptions = this._validateLaunchOptions(options); if (this._useBidi) launchOptions.useWebSocket = true; const controller = new ProgressController(metadata, this); controller.setLogName('browser'); const browser = await controller.run(async progress => { // Note: Any initial TLS requests will fail since we rely on the Page/Frames initialize which sets ignoreHTTPSErrors. - const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistentContextOptions); - if (clientCertificatesProxy) + let clientCertificatesProxy: ClientCertificatesProxy | undefined; + if (options.clientCertificates?.length) { + clientCertificatesProxy = new ClientCertificatesProxy(options); launchOptions.proxyOverride = await clientCertificatesProxy?.listen(); + options = { ...options }; + options.internalIgnoreHTTPSErrors = true; + } progress.cleanupWhenAborted(() => clientCertificatesProxy?.close()); - const browser = await this._innerLaunchWithRetries(progress, launchOptions, persistentContextOptions, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + const browser = await this._innerLaunchWithRetries(progress, launchOptions, options, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; return browser; }, TimeoutSettings.launchTimeout(launchOptions)); diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 01c6c397ab..fc4e2c027d 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -168,23 +168,11 @@ export abstract class APIRequestContext extends SdkObject { const method = params.method?.toUpperCase() || 'GET'; const proxy = defaults.proxy; let agent; - // When `clientCertificates` is present, we set the `proxy` property to our own socks proxy - // for the browser to use. However, we don't need it here, because we already respect - // `clientCertificates` when fetching from Node.js. - if (proxy && !defaults.clientCertificates?.length && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) { - const proxyOpts = url.parse(proxy.server); - if (proxyOpts.protocol?.startsWith('socks')) { - agent = new SocksProxyAgent({ - host: proxyOpts.hostname, - port: proxyOpts.port || undefined, - }); - } else { - if (proxy.username) - proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; - // TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. - agent = new HttpsProxyAgent(proxyOpts); - } - } + // We skip 'per-context' in order to not break existing users. 'per-context' was previously used to + // workaround an upstream Chromium bug. Can be removed in the future. + if (proxy && proxy.server !== 'per-context' && !shouldBypassProxy(requestUrl, proxy.bypass)) + agent = createProxyAgent(proxy); + const timeout = defaults.timeoutSettings.timeout(params); const deadline = timeout && (monotonicTime() + timeout); @@ -577,8 +565,6 @@ export class GlobalAPIRequestContext extends APIRequestContext { if (!/^\w+:\/\//.test(url)) url = 'http://' + url; proxy.server = url; - if (options.clientCertificates) - throw new Error('Cannot specify both proxy and clientCertificates'); } if (options.storageState) { this._origins = options.storageState.origins; @@ -629,6 +615,20 @@ export class GlobalAPIRequestContext extends APIRequestContext { } } +export function createProxyAgent(proxy: types.ProxySettings) { + const proxyOpts = url.parse(proxy.server); + if (proxyOpts.protocol?.startsWith('socks')) { + return new SocksProxyAgent({ + host: proxyOpts.hostname, + port: proxyOpts.port || undefined, + }); + } + if (proxy.username) + proxyOpts.auth = `${proxy.username}:${proxy.password || ''}`; + // TODO: We should use HttpProxyAgent conditional on proxyOpts.protocol instead of always using CONNECT method. + return new HttpsProxyAgent(proxyOpts); +} + function toHeadersArray(rawHeaders: string[]): types.HeadersArray { const result: types.HeadersArray = []; for (let i = 0; i < rawHeaders.length; i += 2) diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 6d4b334dba..08aaa4f25a 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -25,6 +25,9 @@ import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketReque import { SocksProxy } from '../common/socksProxy'; import type * as types from './types'; import { debugLogger } from '../utils/debugLogger'; +import { createProxyAgent } from './fetch'; +import { EventEmitter } from 'events'; +import { verifyClientCertificates } from './browserContext'; let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined; function loadDummyServerCertsIfNeeded() { @@ -94,7 +97,11 @@ class SocksProxyConnection { } async connect() { - this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); + if (this.socksProxy.proxyAgentFromOptions) + this.target = await this.socksProxy.proxyAgentFromOptions.callback(new EventEmitter() as any, { host: rewriteToLocalhostIfNeeded(this.host), port: this.port, secureEndpoint: false }); + else + this.target = await createSocket(rewriteToLocalhostIfNeeded(this.host), this.port); + this.target.once('close', this._targetCloseEventListener); this.target.once('error', error => this.socksProxy._socksProxy.sendSocketError({ uid: this.uid, error: error.message })); if (this._closed) { @@ -233,12 +240,15 @@ export class ClientCertificatesProxy { ignoreHTTPSErrors: boolean | undefined; secureContextMap: Map = new Map(); alpnCache: ALPNCache; + proxyAgentFromOptions: ReturnType | undefined; constructor( - contextOptions: Pick + contextOptions: Pick ) { + verifyClientCertificates(contextOptions.clientCertificates); this.alpnCache = new ALPNCache(); this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors; + this.proxyAgentFromOptions = contextOptions.proxy ? createProxyAgent(contextOptions.proxy) : undefined; this._initSecureContexts(contextOptions.clientCertificates); this._socksProxy = new SocksProxy(); this._socksProxy.setPattern('*'); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index 67bb51b192..f770fc70dc 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -9202,8 +9202,6 @@ export interface Browser { * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided * with an exact match to the request origin that the certificate is valid for. * - * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. - * * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it * work by replacing `localhost` with `local.playwright`. */ @@ -13924,8 +13922,6 @@ export interface BrowserType { * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided * with an exact match to the request origin that the certificate is valid for. * - * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. - * * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it * work by replacing `localhost` with `local.playwright`. */ @@ -16350,8 +16346,6 @@ export interface APIRequest { * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided * with an exact match to the request origin that the certificate is valid for. * - * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. - * * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it * work by replacing `localhost` with `local.playwright`. */ @@ -20712,8 +20706,6 @@ export interface BrowserContextOptions { * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided * with an exact match to the request origin that the certificate is valid for. * - * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. - * * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it * work by replacing `localhost` with `local.playwright`. */ diff --git a/packages/playwright/types/test.d.ts b/packages/playwright/types/test.d.ts index 73cc415a87..e17a43843c 100644 --- a/packages/playwright/types/test.d.ts +++ b/packages/playwright/types/test.d.ts @@ -5211,8 +5211,6 @@ export interface PlaywrightTestOptions { * `passphrase` property should be provided if the certificate is encrypted. The `origin` property should be provided * with an exact match to the request origin that the certificate is valid for. * - * **NOTE** Using Client Certificates in combination with Proxy Servers is not supported. - * * **NOTE** When using WebKit on macOS, accessing `localhost` will not pick up client certificates. You can make it * work by replacing `localhost` with `local.playwright`. * diff --git a/tests/config/proxy.ts b/tests/config/proxy.ts index 08546c9283..dc2d51b3ec 100644 --- a/tests/config/proxy.ts +++ b/tests/config/proxy.ts @@ -15,9 +15,11 @@ */ import type { IncomingMessage } from 'http'; -import type { Socket } from 'net'; import type { ProxyServer } from '../third_party/proxy'; import { createProxy } from '../third_party/proxy'; +import net from 'net'; +import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../../packages/playwright-core/src/common/socksProxy'; +import { SocksProxy } from '../../packages/playwright-core/lib/common/socksProxy'; export class TestProxy { readonly PORT: number; @@ -27,7 +29,7 @@ export class TestProxy { requestUrls: string[] = []; private readonly _server: ProxyServer; - private readonly _sockets = new Set(); + private readonly _sockets = new Set(); private _handlers: { event: string, handler: (...args: any[]) => void }[] = []; static async create(port: number): Promise { @@ -90,7 +92,7 @@ export class TestProxy { this._server.prependListener(event, handler); } - private _onSocket(socket: Socket) { + private _onSocket(socket: net.Socket) { this._sockets.add(socket); // ECONNRESET and HPE_INVALID_EOF_STATE are legit errors given // that tab closing aborts outgoing connections to the server. @@ -100,5 +102,46 @@ export class TestProxy { }); socket.once('close', () => this._sockets.delete(socket)); } - +} + +export async function setupSocksForwardingServer({ + port, forwardPort, allowedTargetPort +}: { + port: number, forwardPort: number, allowedTargetPort: number +}) { + const connectHosts = []; + const connections = new Map(); + const socksProxy = new SocksProxy(); + socksProxy.setPattern('*'); + socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { + if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io', 'localhost'].includes(payload.host) || payload.port !== allowedTargetPort) { + socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' }); + return; + } + const target = new net.Socket(); + target.on('error', error => socksProxy.sendSocketError({ uid: payload.uid, error: error.toString() })); + target.on('end', () => socksProxy.sendSocketEnd({ uid: payload.uid })); + target.on('data', data => socksProxy.sendSocketData({ uid: payload.uid, data })); + target.setKeepAlive(false); + target.connect(forwardPort, '127.0.0.1'); + target.on('connect', () => { + connections.set(payload.uid, target); + if (!connectHosts.includes(`${payload.host}:${payload.port}`)) + connectHosts.push(`${payload.host}:${payload.port}`); + socksProxy.socketConnected({ uid: payload.uid, host: target.localAddress, port: target.localPort }); + }); + }); + socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => { + connections.get(payload.uid)?.write(payload.data); + }); + socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => { + connections.get(payload.uid)?.destroy(); + connections.delete(payload.uid); + }); + await socksProxy.listen(port, 'localhost'); + return { + closeProxyServer: () => socksProxy.close(), + proxyServerAddr: `socks5://localhost:${port}`, + connectHosts, + }; } diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 899b9819be..156d80adfb 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -23,6 +23,7 @@ import type http from 'http'; import { expect, playwrightTest as base } from '../config/browserTest'; import type net from 'net'; import type { BrowserContextOptions } from 'packages/playwright-test'; +import { setupSocksForwardingServer } from '../config/proxy'; const { createHttpsServer, createHttp2Server } = require('../../packages/playwright-core/lib/utils'); type TestOptions = { @@ -88,14 +89,6 @@ const kValidationSubTests: [BrowserContextOptions, string][] = [ passphrase: kDummyFileName, }] }, 'pfx is specified together with cert, key or passphrase'], - [{ - proxy: { server: 'http://localhost:8080' }, - clientCertificates: [{ - origin: 'test', - certPath: kDummyFileName, - keyPath: kDummyFileName, - }] - }, 'Cannot specify both proxy and clientCertificates'], ]; test.describe('fetch', () => { @@ -180,6 +173,54 @@ test.describe('fetch', () => { await request.dispose(); }); + test('pass with trusted client certificates and when a http proxy is used', async ({ playwright, startCCServer, proxyServer, asset }) => { + const serverURL = await startCCServer(); + proxyServer.forwardTo(parseInt(new URL(serverURL).port, 10), { allowConnectRequests: true }); + const request = await playwright.request.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + proxy: { server: `localhost:${proxyServer.PORT}` } + }); + expect(proxyServer.connectHosts).toEqual([]); + const response = await request.get(serverURL); + expect(proxyServer.connectHosts).toEqual([new URL(serverURL).host]); + expect(response.url()).toBe(serverURL); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!'); + await request.dispose(); + }); + + test('pass with trusted client certificates and when a socks proxy is used', async ({ playwright, startCCServer, asset }) => { + const serverURL = await startCCServer({ host: '127.0.0.1' }); + const serverPort = parseInt(new URL(serverURL).port, 10); + const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({ + port: test.info().workerIndex + 2048 + 2, + forwardPort: serverPort, + allowedTargetPort: serverPort, + }); + const request = await playwright.request.newContext({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + proxy: { server: proxyServerAddr } + }); + expect(connectHosts).toEqual([]); + const response = await request.get(serverURL); + expect(connectHosts).toEqual([new URL(serverURL).host]); + expect(response.url()).toBe(serverURL); + expect(response.status()).toBe(200); + expect(await response.text()).toContain('Hello Alice, your certificate was issued by localhost!'); + await request.dispose(); + await closeProxyServer(); + }); + test('should throw a http error if the pfx passphrase is incorect', async ({ playwright, startCCServer, asset, browserName }) => { const serverURL = await startCCServer(); const request = await playwright.request.newContext({ @@ -311,6 +352,50 @@ test.describe('browser', () => { await page.close(); }); + test('should pass with matching certificates and when a http proxy is used', async ({ browser, startCCServer, asset, browserName, proxyServer }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin' }); + proxyServer.forwardTo(parseInt(new URL(serverURL).port, 10), { allowConnectRequests: true }); + const page = await browser.newPage({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + proxy: { server: `localhost:${proxyServer.PORT}` } + }); + expect(proxyServer.connectHosts).toEqual([]); + await page.goto(serverURL); + expect([...new Set(proxyServer.connectHosts)]).toEqual([`localhost:${new URL(serverURL).port}`]); + await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); + await page.close(); + }); + + test('should pass with matching certificates and when a socks proxy is used', async ({ browser, startCCServer, asset, browserName }) => { + const serverURL = await startCCServer({ useFakeLocalhost: browserName === 'webkit' && process.platform === 'darwin', host: '127.0.0.1' }); + const serverPort = parseInt(new URL(serverURL).port, 10); + const { proxyServerAddr, closeProxyServer, connectHosts } = await setupSocksForwardingServer({ + port: test.info().workerIndex + 2048 + 2, + forwardPort: serverPort, + allowedTargetPort: serverPort, + }); + const page = await browser.newPage({ + ignoreHTTPSErrors: true, + clientCertificates: [{ + origin: new URL(serverURL).origin, + certPath: asset('client-certificates/client/trusted/cert.pem'), + keyPath: asset('client-certificates/client/trusted/key.pem'), + }], + proxy: { server: proxyServerAddr } + }); + expect(connectHosts).toEqual([]); + await page.goto(serverURL); + expect(connectHosts).toEqual([`localhost:${serverPort}`]); + await expect(page.getByTestId('message')).toHaveText('Hello Alice, your certificate was issued by localhost!'); + await page.close(); + await closeProxyServer(); + }); + test('should not hang on tls errors during TLS 1.2 handshake', async ({ browser, asset, platform, browserName }) => { for (const tlsVersion of ['TLSv1.3', 'TLSv1.2'] as const) { await test.step(`TLS version: ${tlsVersion}`, async () => { diff --git a/tests/library/proxy.spec.ts b/tests/library/proxy.spec.ts index 947f3b8788..b2ddcadeba 100644 --- a/tests/library/proxy.spec.ts +++ b/tests/library/proxy.spec.ts @@ -14,10 +14,9 @@ * limitations under the License. */ +import { setupSocksForwardingServer } from '../config/proxy'; import { playwrightTest as it, expect } from '../config/browserTest'; import net from 'net'; -import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../../packages/playwright-core/src/common/socksProxy'; -import { SocksProxy } from '../../packages/playwright-core/lib/common/socksProxy'; it.skip(({ mode }) => mode.startsWith('service')); @@ -288,42 +287,13 @@ it('should use proxy with emulated user agent', async ({ browserType }) => { expect(requestText).toContain('MyUserAgent'); }); -async function setupSocksForwardingServer(port: number, forwardPort: number) { - const connections = new Map(); - const socksProxy = new SocksProxy(); - socksProxy.setPattern('*'); - socksProxy.addListener(SocksProxy.Events.SocksRequested, async (payload: SocksSocketRequestedPayload) => { - if (!['127.0.0.1', 'fake-localhost-127-0-0-1.nip.io'].includes(payload.host) || payload.port !== 1337) { - socksProxy.sendSocketError({ uid: payload.uid, error: 'ECONNREFUSED' }); - return; - } - const target = new net.Socket(); - target.on('error', error => socksProxy.sendSocketError({ uid: payload.uid, error: error.toString() })); - target.on('end', () => socksProxy.sendSocketEnd({ uid: payload.uid })); - target.on('data', data => socksProxy.sendSocketData({ uid: payload.uid, data })); - target.setKeepAlive(false); - target.connect(forwardPort, '127.0.0.1'); - target.on('connect', () => { - connections.set(payload.uid, target); - socksProxy.socketConnected({ uid: payload.uid, host: target.localAddress, port: target.localPort }); - }); - }); - socksProxy.addListener(SocksProxy.Events.SocksData, async (payload: SocksSocketDataPayload) => { - connections.get(payload.uid)?.write(payload.data); - }); - socksProxy.addListener(SocksProxy.Events.SocksClosed, (payload: SocksSocketClosedPayload) => { - connections.get(payload.uid)?.destroy(); - connections.delete(payload.uid); - }); - await socksProxy.listen(port, 'localhost'); - return { - closeProxyServer: () => socksProxy.close(), - proxyServerAddr: `socks5://localhost:${port}`, - }; -} -it('should use SOCKS proxy for websocket requests', async ({ browserName, platform, browserType, server }, testInfo) => { - const { proxyServerAddr, closeProxyServer } = await setupSocksForwardingServer(testInfo.workerIndex + 2048 + 2, server.PORT); +it('should use SOCKS proxy for websocket requests', async ({ browserType, server }) => { + const { proxyServerAddr, closeProxyServer } = await setupSocksForwardingServer({ + port: it.info().workerIndex + 2048 + 2, + forwardPort: server.PORT, + allowedTargetPort: 1337, + }); const browser = await browserType.launch({ proxy: { server: proxyServerAddr,