From 79cba7d70455132f0069a8cf1ef099f356b2eb41 Mon Sep 17 00:00:00 2001 From: Max Schmitt Date: Fri, 13 Sep 2024 17:34:34 +0200 Subject: [PATCH] chore: introduce option overrides on context/browser (#32606) --- .../src/server/android/android.ts | 3 ++- .../src/server/bidi/bidiBrowser.ts | 4 ++-- .../src/server/bidi/bidiChromium.ts | 3 ++- .../playwright-core/src/server/browser.ts | 10 ++++++--- .../src/server/browserContext.ts | 15 ++++++------- .../playwright-core/src/server/browserType.ts | 21 +++++++++---------- .../src/server/chromium/chromium.ts | 6 +++--- .../src/server/chromium/crBrowser.ts | 13 ++++++------ .../src/server/chromium/crPage.ts | 4 ++-- .../src/server/electron/electron.ts | 3 ++- packages/playwright-core/src/server/fetch.ts | 2 +- .../src/server/firefox/ffBrowser.ts | 16 +++++++------- .../socksClientCertificatesInterceptor.ts | 16 +++++++------- packages/playwright-core/src/server/types.ts | 10 ++++++++- .../src/server/webkit/webkit.ts | 3 ++- .../src/server/webkit/wkBrowser.ts | 15 ++++++------- tests/library/client-certificates.spec.ts | 1 + 17 files changed, 81 insertions(+), 64 deletions(-) diff --git a/packages/playwright-core/src/server/android/android.ts b/packages/playwright-core/src/server/android/android.ts index 0b4cb331b0..1af083916c 100644 --- a/packages/playwright-core/src/server/android/android.ts +++ b/packages/playwright-core/src/server/android/android.ts @@ -29,6 +29,7 @@ import { validateBrowserContextOptions } from '../browserContext'; import { ProgressController } from '../progress'; import { CRBrowser } from '../chromium/crBrowser'; import { helper } from '../helper'; +import type * as types from '../types'; import { PipeTransport } from '../../protocol/transport'; import { RecentLogsCollector } from '../../utils/debugLogger'; import { gracefullyCloseSet } from '../../utils/processLauncher'; @@ -309,7 +310,7 @@ export class AndroidDevice extends SdkObject { return await this._connectToBrowser(socketName); } - private async _connectToBrowser(socketName: string, options: channels.BrowserNewContextParams = {}): Promise { + private async _connectToBrowser(socketName: string, options: types.BrowserContextOptions = {}): Promise { const socket = await this._waitForLocalAbstract(socketName); const androidBrowser = new AndroidBrowser(this, socket); await androidBrowser._init(); diff --git a/packages/playwright-core/src/server/bidi/bidiBrowser.ts b/packages/playwright-core/src/server/bidi/bidiBrowser.ts index 0c658a82b4..cc98e2ff3a 100644 --- a/packages/playwright-core/src/server/bidi/bidiBrowser.ts +++ b/packages/playwright-core/src/server/bidi/bidiBrowser.ts @@ -111,7 +111,7 @@ export class BidiBrowser extends Browser { this._didClose(); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { const { userContext } = await this._browserSession.send('browser.createUserContext', {}); const context = new BidiBrowserContext(this, userContext, options); await context._initialize(); @@ -190,7 +190,7 @@ export class BidiBrowser extends Browser { export class BidiBrowserContext extends BrowserContext { declare readonly _browser: BidiBrowser; - constructor(browser: BidiBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: BidiBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._authenticateProxyViaHeader(); } diff --git a/packages/playwright-core/src/server/bidi/bidiChromium.ts b/packages/playwright-core/src/server/bidi/bidiChromium.ts index e94dabf072..32751bd51a 100644 --- a/packages/playwright-core/src/server/bidi/bidiChromium.ts +++ b/packages/playwright-core/src/server/bidi/bidiChromium.ts @@ -91,7 +91,7 @@ export class BidiChromium extends BrowserType { } private _innerDefaultArgs(options: types.LaunchOptions): string[] { - const { args = [], proxy } = options; + const { args = [] } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -125,6 +125,7 @@ export class BidiChromium extends BrowserType { } if (options.chromiumSandbox !== true) chromeArguments.push('--no-sandbox'); + const proxy = options.proxyOverride || options.proxy; if (proxy) { const proxyURL = new URL(proxy.server); const isSocks = proxyURL.protocol === 'socks5:'; diff --git a/packages/playwright-core/src/server/browser.ts b/packages/playwright-core/src/server/browser.ts index 663b9be377..04a23e7eac 100644 --- a/packages/playwright-core/src/server/browser.ts +++ b/packages/playwright-core/src/server/browser.ts @@ -41,7 +41,7 @@ export type BrowserOptions = { downloadsPath: string, tracesDir: string, headful?: boolean, - persistent?: channels.BrowserNewContextParams, // Undefined means no persistent context. + persistent?: types.BrowserContextOptions, // Undefined means no persistent context. browserProcess: BrowserProcess, customExecutablePath?: string; proxy?: ProxySettings, @@ -74,15 +74,19 @@ export abstract class Browser extends SdkObject { this.instrumentation.onBrowserOpen(this); } - abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise; + abstract doCreateNewContext(options: types.BrowserContextOptions): Promise; abstract contexts(): BrowserContext[]; abstract isConnected(): boolean; abstract version(): string; abstract userAgent(): string; - async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise { + async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise { validateBrowserContextOptions(options, this.options); const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options); + if (clientCertificatesProxy) { + options.proxyOverride = await clientCertificatesProxy.listen(); + options.internalIgnoreHTTPSErrors = true; + } let context; try { context = await this.doCreateNewContext(options); diff --git a/packages/playwright-core/src/server/browserContext.ts b/packages/playwright-core/src/server/browserContext.ts index 8ddbe68f89..e1166f3e97 100644 --- a/packages/playwright-core/src/server/browserContext.ts +++ b/packages/playwright-core/src/server/browserContext.ts @@ -68,7 +68,7 @@ export abstract class BrowserContext extends SdkObject { readonly _timeoutSettings = new TimeoutSettings(); readonly _pageBindings = new Map(); readonly _activeProgressControllers = new Set(); - readonly _options: channels.BrowserNewContextParams; + readonly _options: types.BrowserContextOptions; _requestInterceptor?: network.RouteHandler; private _isPersistentContext: boolean; private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; @@ -93,7 +93,7 @@ export abstract class BrowserContext extends SdkObject { readonly clock: Clock; _clientCertificatesProxy: ClientCertificatesProxy | undefined; - constructor(browser: Browser, options: channels.BrowserNewContextParams, browserContextId: string | undefined) { + constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); this.attribution.context = this; this._browser = browser; @@ -659,19 +659,16 @@ export function assertBrowserContextIsNotOwned(context: BrowserContext) { } } -export async function createClientCertificatesProxyIfNeeded(options: channels.BrowserNewContextOptions, browserOptions?: BrowserOptions) { +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); - const clientCertificatesProxy = new ClientCertificatesProxy(options); - options.proxy = { server: await clientCertificatesProxy.listen() }; - options.ignoreHTTPSErrors = true; - return clientCertificatesProxy; + return new ClientCertificatesProxy(options); } -export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) { +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"`); if (options.noDefaultViewport && !!options.isMobile) @@ -720,7 +717,7 @@ export function verifyGeolocation(geolocation?: types.Geolocation) { throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); } -export function verifyClientCertificates(clientCertificates?: channels.BrowserNewContextParams['clientCertificates']) { +export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) { if (!clientCertificates) return; for (const cert of clientCertificates) { diff --git a/packages/playwright-core/src/server/browserType.ts b/packages/playwright-core/src/server/browserType.ts index d0c3174a59..286ec27c29 100644 --- a/packages/playwright-core/src/server/browserType.ts +++ b/packages/playwright-core/src/server/browserType.ts @@ -92,27 +92,26 @@ export abstract class BrowserType extends SdkObject { return browser; } - async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { - options = this._validateLaunchOptions(options); + async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise { + const launchOptions = this._validateLaunchOptions(persistentContextOptions); if (this._useBidi) - options.useWebSocket = true; + launchOptions.useWebSocket = true; const controller = new ProgressController(metadata, this); - const persistent: channels.BrowserNewContextParams = { ...options }; 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(persistent); + const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(persistentContextOptions); if (clientCertificatesProxy) - options.proxy = persistent.proxy; + launchOptions.proxyOverride = await clientCertificatesProxy?.listen(); progress.cleanupWhenAborted(() => clientCertificatesProxy?.close()); - const browser = await this._innerLaunchWithRetries(progress, options, persistent, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); + const browser = await this._innerLaunchWithRetries(progress, launchOptions, persistentContextOptions, helper.debugProtocolLogger(), userDataDir).catch(e => { throw this._rewriteStartupLog(e); }); browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy; return browser; - }, TimeoutSettings.launchTimeout(options)); + }, TimeoutSettings.launchTimeout(launchOptions)); return browser._defaultContext!; } - async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { + async _innerLaunchWithRetries(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, userDataDir?: string): Promise { try { return await this._innerLaunch(progress, options, persistent, protocolLogger, userDataDir); } catch (error) { @@ -126,7 +125,7 @@ export abstract class BrowserType extends SdkObject { } } - async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: channels.BrowserNewContextParams | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { + async _innerLaunch(progress: Progress, options: types.LaunchOptions, persistent: types.BrowserContextOptions | undefined, protocolLogger: types.ProtocolLogger, maybeUserDataDir?: string): Promise { options.proxy = options.proxy ? normalizeProxySettings(options.proxy) : undefined; const browserLogsCollector = new RecentLogsCollector(); const { browserProcess, userDataDir, artifactsDir, transport } = await this._launchProcess(progress, options, !!persistent, browserLogsCollector, maybeUserDataDir); @@ -289,7 +288,7 @@ export abstract class BrowserType extends SdkObject { throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); } - private _validateLaunchOptions(options: Options): Options { + private _validateLaunchOptions(options: types.LaunchOptions): types.LaunchOptions { const { devtools = false } = options; let { headless = !devtools, downloadsPath, proxy } = options; if (debugMode()) diff --git a/packages/playwright-core/src/server/chromium/chromium.ts b/packages/playwright-core/src/server/chromium/chromium.ts index d84ae3952e..30bd2871b8 100644 --- a/packages/playwright-core/src/server/chromium/chromium.ts +++ b/packages/playwright-core/src/server/chromium/chromium.ts @@ -31,7 +31,6 @@ import { CRDevTools } from './crDevTools'; import type { BrowserOptions, BrowserProcess } from '../browser'; import { Browser } from '../browser'; import type * as types from '../types'; -import type * as channels from '@protocol/channels'; import type { HTTPRequestParams } from '../../utils/network'; import { fetchData } from '../../utils/network'; import { getUserAgent } from '../../utils/userAgent'; @@ -98,7 +97,7 @@ export class Chromium extends BrowserType { await cleanedUp; }; const browserProcess: BrowserProcess = { close: doClose, kill: doClose }; - const persistent: channels.BrowserNewContextParams = { noDefaultViewport: true }; + const persistent: types.BrowserContextOptions = { noDefaultViewport: true }; const browserOptions: BrowserOptions = { slowMo: options.slowMo, name: 'chromium', @@ -287,7 +286,7 @@ export class Chromium extends BrowserType { } private _innerDefaultArgs(options: types.LaunchOptions): string[] { - const { args = [], proxy } = options; + const { args = [] } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -321,6 +320,7 @@ export class Chromium extends BrowserType { } if (options.chromiumSandbox !== true) chromeArguments.push('--no-sandbox'); + const proxy = options.proxyOverride || options.proxy; if (proxy) { const proxyURL = new URL(proxy.server); const isSocks = proxyURL.protocol === 'socks5:'; diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index 42b916c186..e0409c1b16 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -100,18 +100,19 @@ export class CRBrowser extends Browser { this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this)); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { + const proxy = options.proxyOverride || options.proxy; let proxyBypassList = undefined; - if (options.proxy) { + if (proxy) { if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) - proxyBypassList = options.proxy.bypass; + proxyBypassList = proxy.bypass; else - proxyBypassList = '<-loopback>' + (options.proxy.bypass ? `,${options.proxy.bypass}` : ''); + proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : ''); } const { browserContextId } = await this._session.send('Target.createBrowserContext', { disposeOnDetach: true, - proxyServer: options.proxy ? options.proxy.server : undefined, + proxyServer: proxy ? proxy.server : undefined, proxyBypassList, }); const context = new CRBrowserContext(this, browserContextId, options); @@ -340,7 +341,7 @@ export class CRBrowserContext extends BrowserContext { declare readonly _browser: CRBrowser; - constructor(browser: CRBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: CRBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._authenticateProxyViaCredentials(); } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index 002dfa09be..5a7fb5e4af 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -543,7 +543,7 @@ class FrameSession { const options = this._crPage._browserContext._options; if (options.bypassCSP) promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); - if (options.ignoreHTTPSErrors) + if (options.ignoreHTTPSErrors || options.internalIgnoreHTTPSErrors) promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); if (this._isMainFrame()) promises.push(this._updateViewport()); @@ -1213,7 +1213,7 @@ async function emulateTimezone(session: CRSession, timezoneId: string) { const contextDelegateSymbol = Symbol('delegate'); // Chromium reference: https://source.chromium.org/chromium/chromium/src/+/main:components/embedder_support/user_agent_utils.cc;l=434;drc=70a6711e08e9f9e0d8e4c48e9ba5cab62eb010c2 -function calculateUserAgentMetadata(options: channels.BrowserNewContextParams) { +function calculateUserAgentMetadata(options: types.BrowserContextOptions) { const ua = options.userAgent; if (!ua) return undefined; diff --git a/packages/playwright-core/src/server/electron/electron.ts b/packages/playwright-core/src/server/electron/electron.ts index b8f361b48a..1606c407d5 100644 --- a/packages/playwright-core/src/server/electron/electron.ts +++ b/packages/playwright-core/src/server/electron/electron.ts @@ -36,6 +36,7 @@ import type { BrowserWindow } from 'electron'; import type { Progress } from '../progress'; import { ProgressController } from '../progress'; import { helper } from '../helper'; +import type * as types from '../types'; import { eventsHelper } from '../../utils/eventsHelper'; import type { BrowserOptions, BrowserProcess } from '../browser'; import type { Playwright } from '../playwright'; @@ -265,7 +266,7 @@ export class Electron extends SdkObject { close: gracefullyClose, kill }; - const contextOptions: channels.BrowserNewContextParams = { + const contextOptions: types.BrowserContextOptions = { ...options, noDefaultViewport: true, }; diff --git a/packages/playwright-core/src/server/fetch.ts b/packages/playwright-core/src/server/fetch.ts index 7a6102a50d..5d00dc05a6 100644 --- a/packages/playwright-core/src/server/fetch.ts +++ b/packages/playwright-core/src/server/fetch.ts @@ -50,7 +50,7 @@ type FetchRequestOptions = { timeoutSettings: TimeoutSettings; ignoreHTTPSErrors?: boolean; baseURL?: string; - clientCertificates?: channels.BrowserNewContextOptions['clientCertificates']; + clientCertificates?: types.BrowserContextOptions['clientCertificates']; }; type HeadersObject = Readonly<{ [name: string]: string }>; diff --git a/packages/playwright-core/src/server/firefox/ffBrowser.ts b/packages/playwright-core/src/server/firefox/ffBrowser.ts index 94b90bbcea..b26a2850ee 100644 --- a/packages/playwright-core/src/server/firefox/ffBrowser.ts +++ b/packages/playwright-core/src/server/firefox/ffBrowser.ts @@ -58,8 +58,9 @@ export class FFBrowser extends Browser { browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent); promises.push((browser._defaultContext as FFBrowserContext)._initialize()); } - if (options.proxy) - promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy))); + const proxy = options.originalLaunchOptions.proxyOverride || options.proxy; + if (proxy) + promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy))); await Promise.all(promises); return browser; } @@ -88,7 +89,7 @@ export class FFBrowser extends Browser { return !this._connection._closed; } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { + async doCreateNewContext(options: types.BrowserContextOptions): Promise { if (options.isMobile) throw new Error('options.isMobile is not supported in Firefox'); const { browserContextId } = await this.session.send('Browser.createBrowserContext', { removeOnDetach: true }); @@ -172,7 +173,7 @@ export class FFBrowser extends Browser { export class FFBrowserContext extends BrowserContext { declare readonly _browser: FFBrowser; - constructor(browser: FFBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: FFBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); } @@ -205,7 +206,7 @@ export class FFBrowserContext extends BrowserContext { promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); if (this._options.bypassCSP) promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); - if (this._options.ignoreHTTPSErrors) + if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); if (this._options.javaScriptEnabled === false) promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); @@ -251,10 +252,11 @@ export class FFBrowserContext extends BrowserContext { }); })); } - if (this._options.proxy) { + const proxy = this._options.proxyOverride || this._options.proxy; + if (proxy) { promises.push(this._browser.session.send('Browser.setContextProxy', { browserContextId: this._browserContextId, - ...toJugglerProxyOptions(this._options.proxy) + ...toJugglerProxyOptions(proxy) })); } diff --git a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts index 2dd900bf89..6d4b334dba 100644 --- a/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts +++ b/packages/playwright-core/src/server/socksClientCertificatesInterceptor.ts @@ -23,7 +23,7 @@ import { createSocket, createTLSSocket } from '../utils/happy-eyeballs'; import { escapeHTML, generateSelfSignedCertificate, ManualPromise, rewriteErrorMessage } from '../utils'; import type { SocksSocketClosedPayload, SocksSocketDataPayload, SocksSocketRequestedPayload } from '../common/socksProxy'; import { SocksProxy } from '../common/socksProxy'; -import type * as channels from '@protocol/channels'; +import type * as types from './types'; import { debugLogger } from '../utils/debugLogger'; let dummyServerTlsOptions: tls.TlsOptions | undefined = undefined; @@ -235,7 +235,7 @@ export class ClientCertificatesProxy { alpnCache: ALPNCache; constructor( - contextOptions: Pick + contextOptions: Pick ) { this.alpnCache = new ALPNCache(); this.ignoreHTTPSErrors = contextOptions.ignoreHTTPSErrors; @@ -261,9 +261,9 @@ export class ClientCertificatesProxy { loadDummyServerCertsIfNeeded(); } - _initSecureContexts(clientCertificates: channels.BrowserNewContextOptions['clientCertificates']) { + _initSecureContexts(clientCertificates: types.BrowserContextOptions['clientCertificates']) { // Step 1. Group certificates by origin. - const origin2certs = new Map(); + const origin2certs = new Map(); for (const cert of clientCertificates || []) { const origin = normalizeOrigin(cert.origin); const certs = origin2certs.get(origin) || []; @@ -282,9 +282,9 @@ export class ClientCertificatesProxy { } } - public async listen(): Promise { + public async listen() { const port = await this._socksProxy.listen(0, '127.0.0.1'); - return `socks5://127.0.0.1:${port}`; + return { server: `socks5://127.0.0.1:${port}` }; } public async close() { @@ -301,7 +301,7 @@ function normalizeOrigin(origin: string): string { } function convertClientCertificatesToTLSOptions( - clientCertificates: channels.BrowserNewContextOptions['clientCertificates'] + clientCertificates: types.BrowserContextOptions['clientCertificates'] ): Pick | undefined { if (!clientCertificates || !clientCertificates.length) return; @@ -322,7 +322,7 @@ function convertClientCertificatesToTLSOptions( } export function getMatchingTLSOptionsForOrigin( - clientCertificates: channels.BrowserNewContextOptions['clientCertificates'], + clientCertificates: types.BrowserContextOptions['clientCertificates'], origin: string ): Pick | undefined { const matchingCerts = clientCertificates?.filter(c => diff --git a/packages/playwright-core/src/server/types.ts b/packages/playwright-core/src/server/types.ts index 226216c397..b58ea5af83 100644 --- a/packages/playwright-core/src/server/types.ts +++ b/packages/playwright-core/src/server/types.ts @@ -150,7 +150,15 @@ export type NormalizedContinueOverrides = { export type EmulatedSize = { viewport: Size, screen: Size }; -export type LaunchOptions = channels.BrowserTypeLaunchOptions & { useWebSocket?: boolean }; +export type LaunchOptions = channels.BrowserTypeLaunchOptions & { + useWebSocket?: boolean, + proxyOverride?: ProxySettings, +}; + +export type BrowserContextOptions = channels.BrowserNewContextOptions & { + proxyOverride?: ProxySettings; + internalIgnoreHTTPSErrors?: boolean; +}; export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void; diff --git a/packages/playwright-core/src/server/webkit/webkit.ts b/packages/playwright-core/src/server/webkit/webkit.ts index b25b62a421..9a11f6c56d 100644 --- a/packages/playwright-core/src/server/webkit/webkit.ts +++ b/packages/playwright-core/src/server/webkit/webkit.ts @@ -53,7 +53,7 @@ export class WebKit extends BrowserType { } override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] { - const { args = [], proxy, headless } = options; + const { args = [], headless } = options; const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); if (userDataDirArg) throw this._createUserDataDirArgMisuseError('--user-data-dir'); @@ -68,6 +68,7 @@ export class WebKit extends BrowserType { webkitArguments.push(`--user-data-dir=${userDataDir}`); else webkitArguments.push(`--no-startup-window`); + const proxy = options.proxyOverride || options.proxy; if (proxy) { if (process.platform === 'darwin') { webkitArguments.push(`--proxy=${proxy.server}`); diff --git a/packages/playwright-core/src/server/webkit/wkBrowser.ts b/packages/playwright-core/src/server/webkit/wkBrowser.ts index 92231edd75..c9bda10ddd 100644 --- a/packages/playwright-core/src/server/webkit/wkBrowser.ts +++ b/packages/playwright-core/src/server/webkit/wkBrowser.ts @@ -81,12 +81,13 @@ export class WKBrowser extends Browser { this._didClose(); } - async doCreateNewContext(options: channels.BrowserNewContextParams): Promise { - const createOptions = options.proxy ? { - // Enable socks5 hostname resolution on Windows. Workaround can be removed once fixed upstream. + async doCreateNewContext(options: types.BrowserContextOptions): Promise { + const proxy = options.proxyOverride || options.proxy; + const createOptions = proxy ? { + // Enable socks5 hostname resolution on Windows. // See https://github.com/microsoft/playwright/issues/20451 - proxyServer: process.platform === 'win32' ? options.proxy.server.replace(/^socks5:\/\//, 'socks5h://') : options.proxy.server, - proxyBypassList: options.proxy.bypass + proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server, + proxyBypassList: proxy.bypass } : undefined; const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions); options.userAgent = options.userAgent || DEFAULT_USER_AGENT; @@ -206,7 +207,7 @@ export class WKBrowser extends Browser { export class WKBrowserContext extends BrowserContext { declare readonly _browser: WKBrowser; - constructor(browser: WKBrowser, browserContextId: string | undefined, options: channels.BrowserNewContextParams) { + constructor(browser: WKBrowser, browserContextId: string | undefined, options: types.BrowserContextOptions) { super(browser, options, browserContextId); this._validateEmulatedViewport(options.viewport); this._authenticateProxyViaHeader(); @@ -221,7 +222,7 @@ export class WKBrowserContext extends BrowserContext { downloadPath: this._browser.options.downloadsPath, browserContextId })); - if (this._options.ignoreHTTPSErrors) + if (this._options.ignoreHTTPSErrors || this._options.internalIgnoreHTTPSErrors) promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true })); if (this._options.locale) promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] })); diff --git a/tests/library/client-certificates.spec.ts b/tests/library/client-certificates.spec.ts index 682df0b00f..899b9819be 100644 --- a/tests/library/client-certificates.spec.ts +++ b/tests/library/client-certificates.spec.ts @@ -546,6 +546,7 @@ test.describe('browser', () => { keyPath: asset('client-certificates/client/trusted/key.pem'), }; const page = await browser.newPage({ + ignoreHTTPSErrors: true, clientCertificates: [{ origin: new URL(serverURL).origin, ...baseOptions,