chore: introduce option overrides on context/browser

This commit is contained in:
Max Schmitt 2024-09-13 14:44:31 +02:00
parent cd4dabef8b
commit 6747844a33
13 changed files with 56 additions and 40 deletions

View file

@ -91,7 +91,7 @@ export class BidiChromium extends BrowserType {
} }
private _innerDefaultArgs(options: types.LaunchOptions): string[] { private _innerDefaultArgs(options: types.LaunchOptions): string[] {
const { args = [], proxy } = options; const { args = [] } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg) if (userDataDirArg)
throw this._createUserDataDirArgMisuseError('--user-data-dir'); throw this._createUserDataDirArgMisuseError('--user-data-dir');
@ -125,6 +125,7 @@ export class BidiChromium extends BrowserType {
} }
if (options.chromiumSandbox !== true) if (options.chromiumSandbox !== true)
chromeArguments.push('--no-sandbox'); chromeArguments.push('--no-sandbox');
const proxy = options.proxyOverride || options.proxy;
if (proxy) { if (proxy) {
const proxyURL = new URL(proxy.server); const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:'; const isSocks = proxyURL.protocol === 'socks5:';

View file

@ -74,15 +74,19 @@ export abstract class Browser extends SdkObject {
this.instrumentation.onBrowserOpen(this); this.instrumentation.onBrowserOpen(this);
} }
abstract doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext>; abstract doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext>;
abstract contexts(): BrowserContext[]; abstract contexts(): BrowserContext[];
abstract isConnected(): boolean; abstract isConnected(): boolean;
abstract version(): string; abstract version(): string;
abstract userAgent(): string; abstract userAgent(): string;
async newContext(metadata: CallMetadata, options: channels.BrowserNewContextParams): Promise<BrowserContext> { async newContext(metadata: CallMetadata, options: types.BrowserContextOptions): Promise<BrowserContext> {
validateBrowserContextOptions(options, this.options); validateBrowserContextOptions(options, this.options);
const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options); const clientCertificatesProxy = await createClientCertificatesProxyIfNeeded(options, this.options);
if (clientCertificatesProxy) {
options.proxyOverride = await clientCertificatesProxy.listen();
options.ignoreHTTPSErrorsOverride = true;
}
let context; let context;
try { try {
context = await this.doCreateNewContext(options); context = await this.doCreateNewContext(options);

View file

@ -68,7 +68,7 @@ export abstract class BrowserContext extends SdkObject {
readonly _timeoutSettings = new TimeoutSettings(); readonly _timeoutSettings = new TimeoutSettings();
readonly _pageBindings = new Map<string, PageBinding>(); readonly _pageBindings = new Map<string, PageBinding>();
readonly _activeProgressControllers = new Set<ProgressController>(); readonly _activeProgressControllers = new Set<ProgressController>();
readonly _options: channels.BrowserNewContextParams; readonly _options: types.BrowserContextOptions;
_requestInterceptor?: network.RouteHandler; _requestInterceptor?: network.RouteHandler;
private _isPersistentContext: boolean; private _isPersistentContext: boolean;
private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; private _closedStatus: 'open' | 'closing' | 'closed' = 'open';
@ -665,10 +665,7 @@ export async function createClientCertificatesProxyIfNeeded(options: channels.Br
if ((options.proxy?.server && options.proxy?.server !== 'per-context') || (browserOptions?.proxy?.server && browserOptions?.proxy?.server !== 'http://per-context')) 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'); throw new Error('Cannot specify both proxy and clientCertificates');
verifyClientCertificates(options.clientCertificates); verifyClientCertificates(options.clientCertificates);
const clientCertificatesProxy = new ClientCertificatesProxy(options); return new ClientCertificatesProxy(options);
options.proxy = { server: await clientCertificatesProxy.listen() };
options.ignoreHTTPSErrors = true;
return clientCertificatesProxy;
} }
export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) { export function validateBrowserContextOptions(options: channels.BrowserNewContextParams, browserOptions: BrowserOptions) {

View file

@ -92,23 +92,22 @@ export abstract class BrowserType extends SdkObject {
return browser; return browser;
} }
async launchPersistentContext(metadata: CallMetadata, userDataDir: string, options: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> { async launchPersistentContext(metadata: CallMetadata, userDataDir: string, persistentContextOptions: channels.BrowserTypeLaunchPersistentContextOptions & { useWebSocket?: boolean }): Promise<BrowserContext> {
options = this._validateLaunchOptions(options); const launchOptions = this._validateLaunchOptions(persistentContextOptions);
if (this._useBidi) if (this._useBidi)
options.useWebSocket = true; launchOptions.useWebSocket = true;
const controller = new ProgressController(metadata, this); const controller = new ProgressController(metadata, this);
const persistent: channels.BrowserNewContextParams = { ...options };
controller.setLogName('browser'); controller.setLogName('browser');
const browser = await controller.run(async progress => { 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. // 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) if (clientCertificatesProxy)
options.proxy = persistent.proxy; launchOptions.proxyOverride = await clientCertificatesProxy?.listen();
progress.cleanupWhenAborted(() => clientCertificatesProxy?.close()); 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; browser._defaultContext!._clientCertificatesProxy = clientCertificatesProxy;
return browser; return browser;
}, TimeoutSettings.launchTimeout(options)); }, TimeoutSettings.launchTimeout(launchOptions));
return browser._defaultContext!; return browser._defaultContext!;
} }
@ -289,7 +288,7 @@ export abstract class BrowserType extends SdkObject {
throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium'); throw new Error('Connecting to SELENIUM_REMOTE_URL is only supported by Chromium');
} }
private _validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): Options { private _validateLaunchOptions<Options extends types.LaunchOptions>(options: Options): types.LaunchOptions {
const { devtools = false } = options; const { devtools = false } = options;
let { headless = !devtools, downloadsPath, proxy } = options; let { headless = !devtools, downloadsPath, proxy } = options;
if (debugMode()) if (debugMode())

View file

@ -287,7 +287,7 @@ export class Chromium extends BrowserType {
} }
private _innerDefaultArgs(options: types.LaunchOptions): string[] { private _innerDefaultArgs(options: types.LaunchOptions): string[] {
const { args = [], proxy } = options; const { args = [] } = options;
const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir')); const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg) if (userDataDirArg)
throw this._createUserDataDirArgMisuseError('--user-data-dir'); throw this._createUserDataDirArgMisuseError('--user-data-dir');
@ -321,6 +321,7 @@ export class Chromium extends BrowserType {
} }
if (options.chromiumSandbox !== true) if (options.chromiumSandbox !== true)
chromeArguments.push('--no-sandbox'); chromeArguments.push('--no-sandbox');
const proxy = options.proxyOverride || options.proxy;
if (proxy) { if (proxy) {
const proxyURL = new URL(proxy.server); const proxyURL = new URL(proxy.server);
const isSocks = proxyURL.protocol === 'socks5:'; const isSocks = proxyURL.protocol === 'socks5:';

View file

@ -100,18 +100,19 @@ export class CRBrowser extends Browser {
this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this)); this._session.on('Browser.downloadProgress', this._onDownloadProgress.bind(this));
} }
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> { async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
const proxy = options.proxyOverride || options.proxy;
let proxyBypassList = undefined; let proxyBypassList = undefined;
if (options.proxy) { if (proxy) {
if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK) if (process.env.PLAYWRIGHT_DISABLE_FORCED_CHROMIUM_PROXIED_LOOPBACK)
proxyBypassList = options.proxy.bypass; proxyBypassList = proxy.bypass;
else else
proxyBypassList = '<-loopback>' + (options.proxy.bypass ? `,${options.proxy.bypass}` : ''); proxyBypassList = '<-loopback>' + (proxy.bypass ? `,${proxy.bypass}` : '');
} }
const { browserContextId } = await this._session.send('Target.createBrowserContext', { const { browserContextId } = await this._session.send('Target.createBrowserContext', {
disposeOnDetach: true, disposeOnDetach: true,
proxyServer: options.proxy ? options.proxy.server : undefined, proxyServer: proxy ? proxy.server : undefined,
proxyBypassList, proxyBypassList,
}); });
const context = new CRBrowserContext(this, browserContextId, options); const context = new CRBrowserContext(this, browserContextId, options);
@ -340,7 +341,7 @@ export class CRBrowserContext extends BrowserContext {
declare readonly _browser: CRBrowser; 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); super(browser, options, browserContextId);
this._authenticateProxyViaCredentials(); this._authenticateProxyViaCredentials();
} }

View file

@ -543,7 +543,7 @@ class FrameSession {
const options = this._crPage._browserContext._options; const options = this._crPage._browserContext._options;
if (options.bypassCSP) if (options.bypassCSP)
promises.push(this._client.send('Page.setBypassCSP', { enabled: true })); promises.push(this._client.send('Page.setBypassCSP', { enabled: true }));
if (options.ignoreHTTPSErrors) if (options.ignoreHTTPSErrors || options.ignoreHTTPSErrorsOverride)
promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true })); promises.push(this._client.send('Security.setIgnoreCertificateErrors', { ignore: true }));
if (this._isMainFrame()) if (this._isMainFrame())
promises.push(this._updateViewport()); promises.push(this._updateViewport());

View file

@ -58,8 +58,9 @@ export class FFBrowser extends Browser {
browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent); browser._defaultContext = new FFBrowserContext(browser, undefined, options.persistent);
promises.push((browser._defaultContext as FFBrowserContext)._initialize()); promises.push((browser._defaultContext as FFBrowserContext)._initialize());
} }
if (options.proxy) const proxy = options.originalLaunchOptions.proxyOverride || options.proxy;
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(options.proxy))); if (proxy)
promises.push(browser.session.send('Browser.setBrowserProxy', toJugglerProxyOptions(proxy)));
await Promise.all(promises); await Promise.all(promises);
return browser; return browser;
} }
@ -205,7 +206,7 @@ export class FFBrowserContext extends BrowserContext {
promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent })); promises.push(this._browser.session.send('Browser.setUserAgentOverride', { browserContextId, userAgent: this._options.userAgent }));
if (this._options.bypassCSP) if (this._options.bypassCSP)
promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true })); promises.push(this._browser.session.send('Browser.setBypassCSP', { browserContextId, bypassCSP: true }));
if (this._options.ignoreHTTPSErrors) if (this._options.ignoreHTTPSErrors || this._options.ignoreHTTPSErrorsOverride)
promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true })); promises.push(this._browser.session.send('Browser.setIgnoreHTTPSErrors', { browserContextId, ignoreHTTPSErrors: true }));
if (this._options.javaScriptEnabled === false) if (this._options.javaScriptEnabled === false)
promises.push(this._browser.session.send('Browser.setJavaScriptDisabled', { browserContextId, javaScriptDisabled: true })); 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', { promises.push(this._browser.session.send('Browser.setContextProxy', {
browserContextId: this._browserContextId, browserContextId: this._browserContextId,
...toJugglerProxyOptions(this._options.proxy) ...toJugglerProxyOptions(proxy)
})); }));
} }

View file

@ -282,9 +282,9 @@ export class ClientCertificatesProxy {
} }
} }
public async listen(): Promise<string> { public async listen() {
const port = await this._socksProxy.listen(0, '127.0.0.1'); 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() { public async close() {

View file

@ -150,7 +150,15 @@ export type NormalizedContinueOverrides = {
export type EmulatedSize = { viewport: Size, screen: Size }; 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;
ignoreHTTPSErrorsOverride?: boolean;
};
export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void; export type ProtocolLogger = (direction: 'send' | 'receive', message: object) => void;

View file

@ -53,7 +53,7 @@ export class WebKit extends BrowserType {
} }
override defaultArgs(options: types.LaunchOptions, isPersistent: boolean, userDataDir: string): string[] { 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')); const userDataDirArg = args.find(arg => arg.startsWith('--user-data-dir'));
if (userDataDirArg) if (userDataDirArg)
throw this._createUserDataDirArgMisuseError('--user-data-dir'); throw this._createUserDataDirArgMisuseError('--user-data-dir');
@ -68,6 +68,7 @@ export class WebKit extends BrowserType {
webkitArguments.push(`--user-data-dir=${userDataDir}`); webkitArguments.push(`--user-data-dir=${userDataDir}`);
else else
webkitArguments.push(`--no-startup-window`); webkitArguments.push(`--no-startup-window`);
const proxy = options.proxyOverride || options.proxy;
if (proxy) { if (proxy) {
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
webkitArguments.push(`--proxy=${proxy.server}`); webkitArguments.push(`--proxy=${proxy.server}`);

View file

@ -81,12 +81,13 @@ export class WKBrowser extends Browser {
this._didClose(); this._didClose();
} }
async doCreateNewContext(options: channels.BrowserNewContextParams): Promise<BrowserContext> { async doCreateNewContext(options: types.BrowserContextOptions): Promise<BrowserContext> {
const createOptions = options.proxy ? { const proxy = options.proxyOverride || options.proxy;
// Enable socks5 hostname resolution on Windows. Workaround can be removed once fixed upstream. const createOptions = proxy ? {
// Enable socks5 hostname resolution on Windows.
// See https://github.com/microsoft/playwright/issues/20451 // See https://github.com/microsoft/playwright/issues/20451
proxyServer: process.platform === 'win32' ? options.proxy.server.replace(/^socks5:\/\//, 'socks5h://') : options.proxy.server, proxyServer: process.platform === 'win32' ? proxy.server.replace(/^socks5:\/\//, 'socks5h://') : proxy.server,
proxyBypassList: options.proxy.bypass proxyBypassList: proxy.bypass
} : undefined; } : undefined;
const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions); const { browserContextId } = await this._browserSession.send('Playwright.createContext', createOptions);
options.userAgent = options.userAgent || DEFAULT_USER_AGENT; options.userAgent = options.userAgent || DEFAULT_USER_AGENT;
@ -221,7 +222,7 @@ export class WKBrowserContext extends BrowserContext {
downloadPath: this._browser.options.downloadsPath, downloadPath: this._browser.options.downloadsPath,
browserContextId browserContextId
})); }));
if (this._options.ignoreHTTPSErrors) if (this._options.ignoreHTTPSErrors || this._options.ignoreHTTPSErrorsOverride)
promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true })); promises.push(this._browser._browserSession.send('Playwright.setIgnoreCertificateErrors', { browserContextId, ignore: true }));
if (this._options.locale) if (this._options.locale)
promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] })); promises.push(this._browser._browserSession.send('Playwright.setLanguages', { browserContextId, languages: [this._options.locale] }));

View file

@ -546,6 +546,7 @@ test.describe('browser', () => {
keyPath: asset('client-certificates/client/trusted/key.pem'), keyPath: asset('client-certificates/client/trusted/key.pem'),
}; };
const page = await browser.newPage({ const page = await browser.newPage({
ignoreHTTPSErrors: true,
clientCertificates: [{ clientCertificates: [{
origin: new URL(serverURL).origin, origin: new URL(serverURL).origin,
...baseOptions, ...baseOptions,