/** * Copyright 2017 Google Inc. All rights reserved. * Modifications copyright (c) Microsoft Corporation. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import fs from 'fs'; import { rewriteErrorMessage } from '@utils/stackTrace'; import { debugMode, isUnderTest } from '@utils/debug'; import { Clock } from './clock'; import { Credentials } from './credentials'; import { Debugger } from './debugger'; import { DialogManager } from './dialog'; import { BrowserContextAPIRequestContext } from './fetch'; import { helper } from './helper'; import { EventMap, SdkObject } from './instrumentation'; import * as network from './network'; import { InitScript } from './page'; import { Page, PageBinding } from './page'; import { RecorderApp } from './recorder/recorderApp'; import { Selectors } from './selectors'; import { Tracing } from './trace/recorder/tracing'; import * as rawStorageSource from '../generated/storageScriptSource'; import { nullProgress } from './progress'; import type { Browser, BrowserOptions } from './browser'; import type { ConsoleMessage } from './console'; import type { Download } from './download'; import type * as frames from './frames'; import type { PageError } from './page'; import type { Progress } from './progress'; import type { ClientCertificatesProxy } from './socksClientCertificatesInterceptor'; import type { SerializedStorage } from '@injected/storageScript'; import type * as types from './types'; import type * as channels from './channels'; import type { HttpCredentials } from '@protocol/structs'; const BrowserContextEvent = { Console: 'console', Close: 'close', Page: 'page', // Can't use just 'error' due to node.js special treatment of error events. // @see https://nodejs.org/api/events.html#events_error_events PageError: 'pageerror', Request: 'request', Response: 'response', RequestFailed: 'requestfailed', RequestFinished: 'requestfinished', RequestAborted: 'requestaborted', RequestFulfilled: 'requestfulfilled', RequestContinued: 'requestcontinued', BeforeClose: 'beforeclose', PageClosed: 'pageclosed', InternalFrameNavigatedToNewDocument: 'internalframenavigatedtonewdocument', FrameAttached: 'frameattached', WebSocket: 'websocket', } as const; export type BrowserContextEventMap = { [BrowserContextEvent.Console]: [message: ConsoleMessage]; [BrowserContextEvent.Close]: []; [BrowserContextEvent.Page]: [page: Page]; [BrowserContextEvent.PageError]: [pageError: PageError, page: Page]; [BrowserContextEvent.Request]: [request: network.Request]; [BrowserContextEvent.Response]: [response: network.Response]; [BrowserContextEvent.RequestFailed]: [request: network.Request]; [BrowserContextEvent.RequestFinished]: [requestAndResponse: { request: network.Request, response: network.Response | null }]; [BrowserContextEvent.RequestAborted]: [request: network.Request]; [BrowserContextEvent.RequestFulfilled]: [request: network.Request]; [BrowserContextEvent.RequestContinued]: [request: network.Request]; [BrowserContextEvent.BeforeClose]: []; [BrowserContextEvent.PageClosed]: [page: Page]; [BrowserContextEvent.InternalFrameNavigatedToNewDocument]: [frame: frames.Frame]; [BrowserContextEvent.FrameAttached]: [frame: frames.Frame]; [BrowserContextEvent.WebSocket]: [webSocket: network.WebSocket, page: Page]; }; export abstract class BrowserContext extends SdkObject { static Events = BrowserContextEvent; readonly _pageBindings = new Map(); readonly _options: types.BrowserContextOptions; readonly requestInterceptors: network.RouteHandler[] = []; private _isPersistentContext: boolean; private _closedStatus: 'open' | 'closing' | 'closed' = 'open'; readonly _closePromise: Promise; private _closePromiseFulfill: ((error: Error) => void) | undefined; readonly _permissions = new Map(); readonly _downloads = new Set(); readonly _browser: Browser; readonly _browserContextId: string | undefined; private _selectors: Selectors; private _origins = new Set(); readonly tracing: Tracing; readonly fetchRequest: BrowserContextAPIRequestContext; private _customCloseHandler?: () => Promise; readonly _tempDirs: string[] = []; private _creatingStorageStatePage = false; bindingsInitScript?: InitScript; initScripts: InitScript[] = []; private _routesInFlight = new Set(); private _debugger!: Debugger; _closeReason: string | undefined; readonly clock: Clock; readonly credentials: Credentials; _clientCertificatesProxy: ClientCertificatesProxy | undefined; private _playwrightBindingExposed?: Promise; readonly dialogManager: DialogManager; private _consoleApiExposed = false; constructor(browser: Browser, options: types.BrowserContextOptions, browserContextId: string | undefined) { super(browser, 'browser-context'); this.attribution.context = this; this._browser = browser; this._options = options; this._browserContextId = browserContextId; this._isPersistentContext = !browserContextId; this._closePromise = new Promise(fulfill => this._closePromiseFulfill = fulfill); this._selectors = new Selectors(options.selectorEngines || [], options.testIdAttributeName); this.fetchRequest = new BrowserContextAPIRequestContext(this); this.tracing = new Tracing(this, browser.options.tracesDir); this.clock = new Clock(this); this.credentials = new Credentials(this); this.dialogManager = new DialogManager(this.instrumentation); } isPersistentContext(): boolean { return this._isPersistentContext; } selectors(): Selectors { return this._selectors; } async initialize() { if (this.attribution.playwright.options.isInternalPlaywright) return; // Debugger will pause execution upon page.pause in headed mode. this._debugger = new Debugger(this); // When paused, show inspector. const shouldEnableDebugger = !this.attribution.playwright.options.isServer && (isUnderTest() || !!this._browser.options.headful); if (shouldEnableDebugger) { this._debugger.setPauseAt(); this._debugger.on(Debugger.Events.PausedStateChanged, () => { if (this._debugger.isPaused()) RecorderApp.show(this, {}).catch(() => {}); }); } // When PWDEBUG=1, show inspector for each context. if (debugMode() === 'inspector') { this._debugger.setPauseAt({ next: true }); await RecorderApp.show(this, { pauseOnNextStatement: true }); } if (debugMode() === 'console') await this._exposeConsoleApi(); if (this._options.serviceWorkers === 'block') await this.addInitScript(nullProgress, `\nif (navigator.serviceWorker) navigator.serviceWorker.register = async () => { console.warn('Service Worker registration blocked by Playwright'); };\n`); if (this._options.permissions) await this.grantPermissions(this._options.permissions); } debugger(): Debugger { return this._debugger; } async exposeConsoleApi(progress: Progress) { await progress.race(this._exposeConsoleApi()); } private async _exposeConsoleApi() { if (this._consoleApiExposed) return; this._consoleApiExposed = true; await this.extendInjectedScript(` function installConsoleApi(injectedScript) { injectedScript.consoleApi.install(); } module.exports = { default: () => installConsoleApi }; `); } canResetForReuse(): boolean { if (this._closedStatus !== 'open') return false; return true; } static reusableContextHash(params: channels.BrowserNewContextForReuseParams): string { const paramsCopy = { ...params }; if (paramsCopy.selectorEngines?.length === 0) delete paramsCopy.selectorEngines; for (const k of Object.keys(paramsCopy)) { const key = k as keyof channels.BrowserNewContextForReuseParams; if (paramsCopy[key] === defaultNewContextParamValues[key]) delete paramsCopy[key]; } for (const key of paramsThatAllowContextReuse) delete paramsCopy[key]; return JSON.stringify(paramsCopy); } async resetForReuse(progress: Progress, params: channels.BrowserNewContextForReuseParams | null) { await this.tracing.resetForReuse(progress); await this.fetchRequest.tracing().resetForReuse(progress); if (params) { for (const key of paramsThatAllowContextReuse) (this._options as any)[key] = params[key]; if (params.testIdAttributeName) this.selectors().setTestIdAttributeName(params.testIdAttributeName); } // Close extra pages early. let page: Page | undefined = this.pages()[0]; const otherPages = this.possiblyUninitializedPages().filter(p => p !== page); for (const p of otherPages) await p.close(progress); if (page && page.isClosedOrClosingOrCrashed()) { await page.close(progress); page = undefined; } // Navigate to about:blank first to ensure no page scripts are running after this point. await page?.mainFrame().gotoImpl(progress, 'about:blank', {}); // Note: we only need to reset properties from the "paramsThatAllowContextReuse" list. // All other properties force a new context. await this.clock.uninstall(progress); await progress.race(this.setUserAgent(this._options.userAgent)); await progress.race(this.doUpdateDefaultEmulatedMedia()); await progress.race(this.doUpdateDefaultViewport()); await this.setStorageState(progress, this._options.storageState, 'resetForReuse'); await page?.resetForReuse(progress); } browserClosed() { for (const page of this.pages()) page._didClose(); this._didCloseInternal(); } private _didCloseInternal() { if (this._closedStatus === 'closed') { // We can come here twice if we close browser context and browser // at the same time. return; } this._closedStatus = 'closed'; this._clientCertificatesProxy?.close().catch(() => {}); this.tracing.abort(); this.fetchRequest.tracing().abort(); this._closePromiseFulfill!(new Error('Context closed')); this.emit(BrowserContext.Events.Close); } pages(): Page[] { return this.possiblyUninitializedPages().filter(page => page.initializedOrUndefined()); } // BrowserContext methods. abstract possiblyUninitializedPages(): Page[]; abstract doCreateNewPage(): Promise; abstract addCookies(cookies: channels.SetNetworkCookie[]): Promise; abstract setGeolocation(geolocation?: types.Geolocation): Promise; abstract setUserAgent(userAgent: string | undefined): Promise; abstract cancelDownload(uuid: string): Promise; abstract clearCache(): Promise; protected abstract doGetCookies(urls: string[]): Promise; protected abstract doClearCookies(): Promise; protected abstract doGrantPermissions(origin: string, permissions: string[]): Promise; protected abstract doClearPermissions(): Promise; protected abstract doSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise; protected abstract doAddInitScript(initScript: InitScript): Promise; protected abstract doRemoveInitScripts(initScripts: InitScript[]): Promise; protected abstract doUpdateExtraHTTPHeaders(): Promise; protected abstract doUpdateOffline(): Promise; protected abstract doUpdateRequestInterception(): Promise; protected abstract doUpdateDefaultViewport(): Promise; protected abstract doUpdateDefaultEmulatedMedia(): Promise; protected abstract doExposePlaywrightBinding(): Promise; protected abstract doClose(reason: string | undefined): Promise; async cookies(progress: Progress, urls: string | string[] | undefined = []): Promise { return await progress.race(this._cookies(urls)); } private async _cookies(urls: string | string[] | undefined = []): Promise { if (urls && !Array.isArray(urls)) urls = [urls]; return await this.doGetCookies(urls as string[]); } async clearCookies(options: {name?: string | RegExp, domain?: string | RegExp, path?: string | RegExp}): Promise { const hasFilter = options.name !== undefined || options.domain !== undefined || options.path !== undefined; if (!hasFilter) { await this.doClearCookies(); return; } const matches = (cookie: channels.NetworkCookie, prop: 'name' | 'domain' | 'path', value: string | RegExp | undefined) => { if (!value) return true; if (value instanceof RegExp) { value.lastIndex = 0; return value.test(cookie[prop]); } return cookie[prop] === value; }; const currentCookies = await this._cookies(); const cookiesToExpire = currentCookies.filter(cookie => { return matches(cookie, 'name', options.name) && matches(cookie, 'domain', options.domain) && matches(cookie, 'path', options.path); }); if (!cookiesToExpire.length) return; await this.addCookies(cookiesToExpire.map(cookie => ({ ...cookie, value: '', expires: 0, }))); } setHTTPCredentials(progress: Progress, httpCredentials?: HttpCredentials[]): Promise { return progress.race(this.innerSetHTTPCredentials(httpCredentials)); } innerSetHTTPCredentials(httpCredentials?: HttpCredentials[]): Promise { return this.doSetHTTPCredentials(httpCredentials); } getBindingClient(name: string): unknown | undefined { return this._pageBindings.get(name)?.forClient; } async exposePlaywrightBindingIfNeeded() { this._playwrightBindingExposed ??= (async () => { await this.doExposePlaywrightBinding(); this.bindingsInitScript = PageBinding.createInitScript(this); this.initScripts.push(this.bindingsInitScript); await this.doAddInitScript(this.bindingsInitScript); await this.safeNonStallingEvaluateInAllFrames(this.bindingsInitScript.source, 'main'); })(); return await this._playwrightBindingExposed; } needsPlaywrightBinding(): boolean { return this._playwrightBindingExposed !== undefined; } async exposeBinding(progress: Progress, name: string, playwrightBinding: frames.FunctionWithSource, forClient?: unknown, noGlobal?: boolean): Promise { if (this._pageBindings.has(name)) throw new Error(`Function "${name}" has been already registered`); for (const page of this.pages()) { if (page.getBinding(name)) throw new Error(`Function "${name}" has been already registered in one of the pages`); } await progress.race(this.exposePlaywrightBindingIfNeeded()); const binding = new PageBinding(this, name, playwrightBinding, noGlobal); binding.forClient = forClient; this._pageBindings.set(name, binding); try { await progress.race(this.doAddInitScript(binding.initScript)); await progress.race(this.safeNonStallingEvaluateInAllFrames(binding.initScript.source, 'main')); return binding; } catch (error) { this._pageBindings.delete(name); throw error; } } async removeExposedBinding(binding: PageBinding) { if (this._pageBindings.get(binding.name) !== binding) return; this._pageBindings.delete(binding.name); await this.doRemoveInitScripts([binding.initScript]); const cleanup = `{ ${binding.cleanupScript} };`; await this.safeNonStallingEvaluateInAllFrames(cleanup, 'main'); } async grantPermissions(permissions: string[], origin?: string) { let resolvedOrigin = '*'; if (origin) { const url = new URL(origin); resolvedOrigin = url.origin; } const existing = new Set(this._permissions.get(resolvedOrigin) || []); permissions.forEach(p => existing.add(p)); const list = [...existing.values()]; this._permissions.set(resolvedOrigin, list); await this.doGrantPermissions(resolvedOrigin, list); } async clearPermissions() { this._permissions.clear(); await this.doClearPermissions(); } async setExtraHTTPHeaders(progress: Progress, headers: types.HeadersArray) { const oldHeaders = this._options.extraHTTPHeaders; this._options.extraHTTPHeaders = headers; try { await progress.race(this.doUpdateExtraHTTPHeaders()); } catch (error) { this._options.extraHTTPHeaders = oldHeaders; // Note: no await, headers will be reset in the background as soon as possible. this.doUpdateExtraHTTPHeaders().catch(() => {}); throw error; } } async setOffline(progress: Progress, offline: boolean) { const oldOffline = this._options.offline; this._options.offline = offline; try { await progress.race(this.doUpdateOffline()); } catch (error) { this._options.offline = oldOffline; // Note: no await, offline will be reset in the background as soon as possible. this.doUpdateOffline().catch(() => {}); throw error; } } async loadDefaultContextAsIs(progress: Progress): Promise { if (!this.possiblyUninitializedPages().length) { const waitForEvent = helper.waitForEvent(progress, this, BrowserContext.Events.Page); // Race against BrowserContext.close await progress.race(Promise.race([waitForEvent.promise, this._closePromise])); } const page = this.possiblyUninitializedPages()[0]; if (!page) return; const pageOrError = await progress.race(page.waitForInitializedOrError()); if (pageOrError instanceof Error) throw pageOrError; await page.mainFrame().waitForLoadState(progress, 'load'); return page; } async loadDefaultContext(progress: Progress) { const defaultPage = await this.loadDefaultContextAsIs(progress); if (!defaultPage) return; const browserName = this._browser.options.name; if ((this._options.isMobile && browserName === 'chromium') || (this._options.locale && browserName === 'webkit')) { // Workaround for: // - chromium fails to change isMobile for existing page; // - webkit fails to change locale for existing page. await this.newPage(progress); await defaultPage.close(progress); } } protected authenticateProxyViaHeader() { const proxy = this._options.proxy || this._browser.options.proxy || { username: undefined, password: undefined }; const { username, password } = proxy; if (username) { this._options.httpCredentials = [{ username, password: password! }]; const token = Buffer.from(`${username}:${password}`).toString('base64'); this._options.extraHTTPHeaders = network.mergeHeaders([ this._options.extraHTTPHeaders, network.singleHeader('Proxy-Authorization', `Basic ${token}`), ]); } } protected authenticateProxyViaCredentials() { const proxy = this._options.proxy || this._browser.options.proxy; if (!proxy) return; const { username, password } = proxy; if (username) this._options.httpCredentials = [{ username, password: password || '' }]; } async addInitScript(progress: Progress, source: string): Promise { return await progress.race(this._internalAddInitScript(source)); } private async _internalAddInitScript(source: string) { const initScript = new InitScript(this, source); this.initScripts.push(initScript); try { await this.doAddInitScript(initScript); return initScript; } catch (error) { // Note: no await, init script will be removed in the background as soon as possible. initScript.dispose().catch(() => {}); throw error; } } async removeInitScript(initScript: InitScript) { this.initScripts = this.initScripts.filter(script => initScript !== script); await this.doRemoveInitScripts([initScript]); } async addRequestInterceptor(progress: Progress, handler: network.RouteHandler): Promise { // Note: progress is intentionally ignored, because this operation is not cancellable and should not block in the browser anyway. this.requestInterceptors.push(handler); await progress.race(this.doUpdateRequestInterception()); } async removeRequestInterceptor(handler: network.RouteHandler): Promise { const index = this.requestInterceptors.indexOf(handler); if (index === -1) return; this.requestInterceptors.splice(index, 1); await this.notifyRoutesInFlightAboutRemovedHandler(handler); await this.doUpdateRequestInterception(); } isClosingOrClosed() { return this._closedStatus !== 'open'; } private async _deleteAllDownloads(): Promise { await Promise.all(Array.from(this._downloads).map(download => download.artifact.deleteOnContextClose())); } private async _deleteAllTempDirs(): Promise { await Promise.all(this._tempDirs.map(async dir => await fs.promises.rm(dir, { recursive: true, force: true }).catch(e => {}))); } setCustomCloseHandler(handler: (() => Promise) | undefined) { this._customCloseHandler = handler; } async close(progress: Progress, options: { reason?: string }) { let flushError: Error | undefined; if (this._closedStatus === 'open') { if (options.reason) this._closeReason = options.reason; this.emit(BrowserContext.Events.BeforeClose); this._closedStatus = 'closing'; await progress.race(Promise.all([ this.tracing.flush().catch(e => flushError = flushError ?? e), this.fetchRequest.tracing().flush().catch(e => flushError = flushError ?? e), ])); await progress.race(Promise.all(this.pages().map(page => page.screencast.handlePageOrContextClose()))); if (this._customCloseHandler) { await progress.race(this._customCloseHandler()); } else { // Close the context. const disposition = await progress.race(this.doClose(options.reason)); if (disposition === 'close-browser') await this._browser.close(progress, { reason: options.reason }); } // We delete downloads after context closure // so that browser does not write to the download file anymore. const promises: Promise[] = []; promises.push(this._deleteAllDownloads()); promises.push(this._deleteAllTempDirs()); await progress.race(Promise.all(promises)); // Custom handler should trigger didCloseInternal itself. if (!this._customCloseHandler) this._didCloseInternal(); } await this._closePromise; if (flushError) throw flushError; } async newPage(progress: Progress, forStorageState?: boolean): Promise { let page: Page | undefined; try { this._creatingStorageStatePage = !!forStorageState; page = await progress.race(this.doCreateNewPage()); const pageOrError = await progress.race(page.waitForInitializedOrError()); if (pageOrError instanceof Page) { if (pageOrError.isClosed()) throw new Error('Page has been closed.'); return pageOrError; } throw pageOrError; } catch (error) { await page?.close(progress, { reason: 'Failed to create page' }).catch(() => {}); throw error; } finally { this._creatingStorageStatePage = false; } } addVisitedOrigin(origin: string) { this._origins.add(origin); } async storageState(progress: Progress, { indexedDB = false, opfs = false, credentials = false }: { indexedDB?: boolean, opfs?: boolean, credentials?: boolean } = {}): Promise { const result: channels.BrowserContextStorageStateResult = { cookies: await this.cookies(progress), origins: [] }; if (credentials) result.credentials = await progress.race(this.credentials.get()); const originsToSave = new Set(this._origins); const hasStorage = (storage: SerializedStorage) => !!(storage.localStorage.length || storage.indexedDB?.length || storage.opfs?.length); const collectScript = `(() => { const module = {}; ${rawStorageSource.source} const script = new (module.exports.StorageScript())(${JSON.stringify(this._browser.options.name)}); return script.collect({ indexedDB: ${indexedDB}, opfs: ${opfs} }); })()`; // First try collecting storage stage from existing pages. for (const page of this.pages()) { const origin = page.mainFrame().origin(); if (!origin || !originsToSave.has(origin)) continue; try { const storage: SerializedStorage = await progress.race(page.mainFrame().nonStallingEvaluateInExistingContext(collectScript, 'utility')); if (hasStorage(storage)) result.origins.push({ origin, ...storage }); originsToSave.delete(origin); } catch { // When failed on the live page, we'll retry on the blank page below. } } // If there are still origins to save, create a blank page to iterate over origins. if (originsToSave.size) { const page = await this.newPage(progress, true /* forStorageState */); try { await page.addRequestInterceptor(progress, route => { route.fulfill({ body: '' }).catch(() => {}); }, 'prepend'); for (const origin of originsToSave) { const frame = page.mainFrame(); await frame.gotoImpl(progress, origin, {}); const storage: SerializedStorage = await frame.evaluateExpression(progress, collectScript, { world: 'utility' }); if (hasStorage(storage)) result.origins.push({ origin, ...storage }); } } finally { await page.close(progress); } } return result; } isCreatingStorageStatePage(): boolean { return this._creatingStorageStatePage; } async setStorageState(progress: Progress, state: channels.BrowserNewContextParams['storageState'], mode: 'initial' | 'resetForReuse' | 'api') { let page: Page | undefined; let interceptor: network.RouteHandler | undefined; try { if (mode !== 'initial') { await progress.race(this.clearCache()); await progress.race(this.doClearCookies()); if (state?.credentials?.length) this.credentials.clear(); else await this.credentials.dispose(progress); } if (state?.cookies) await progress.race(this.addCookies(state.cookies)); if (state?.credentials?.length) { for (const credential of state.credentials) await progress.race(this.credentials.create(credential)); await this.credentials.install(progress); } const newOrigins = new Map(state?.origins?.map(p => [p.origin, p]) || []); const allOrigins = new Set([...this._origins, ...newOrigins.keys()]); if (allOrigins.size) { if (mode === 'resetForReuse') page = this.pages()[0]; if (!page) page = await this.newPage(progress, mode !== 'resetForReuse' /* forStorageState */); interceptor = (route: network.Route) => { route.fulfill({ body: '' }).catch(() => {}); }; await page.addRequestInterceptor(progress, interceptor, 'prepend'); for (const origin of allOrigins) { const frame = page.mainFrame(); await frame.gotoImpl(progress, origin, {}); const restoreScript = `(() => { const module = {}; ${rawStorageSource.source} const script = new (module.exports.StorageScript())(${JSON.stringify(this._browser.options.name)}); return script.restore(${JSON.stringify(newOrigins.get(origin))}); })()`; await frame.evaluateExpression(progress, restoreScript, { world: 'utility' }); } } this._origins = new Set([...newOrigins.keys()]); } catch (error) { rewriteErrorMessage(error, `Error setting storage state:\n` + error.message); throw error; } finally { if (mode !== 'resetForReuse') await page?.close(progress); else if (interceptor) await page?.removeRequestInterceptor(interceptor); } } async extendInjectedScript(source: string, arg?: any) { const installInFrame = (frame: frames.Frame) => frame.extendInjectedScript(source, arg).catch(() => {}); const installInPage = (page: Page) => { page.on(Page.Events.InternalFrameNavigatedToNewDocument, installInFrame); return Promise.all(page.frames().map(installInFrame)); }; this.on(BrowserContext.Events.Page, installInPage); return Promise.all(this.pages().map(installInPage)); } async safeNonStallingEvaluateInAllFrames(expression: string, world: types.World, options: { throwOnJSErrors?: boolean } = {}) { await Promise.all(this.pages().map(page => page.safeNonStallingEvaluateInAllFrames(expression, world, options))); } addRouteInFlight(route: network.Route) { this._routesInFlight.add(route); } removeRouteInFlight(route: network.Route) { this._routesInFlight.delete(route); } async notifyRoutesInFlightAboutRemovedHandler(handler: network.RouteHandler): Promise { await Promise.all([...this._routesInFlight].map(route => route.removeHandler(handler))); } } 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) throw new Error(`"isMobile" option is not supported with null "viewport"`); if (options.acceptDownloads === undefined && browserOptions.name !== 'electron') options.acceptDownloads = 'accept'; // Electron requires explicit acceptDownloads: true since we wait for // https://github.com/electron/electron/pull/41718 to be widely shipped. // In 6-12 months, we can remove this check. else if (options.acceptDownloads === undefined && browserOptions.name === 'electron') options.acceptDownloads = 'internal-browser-default'; if (!options.viewport && !options.noDefaultViewport) options.viewport = { width: 1280, height: 720 }; if (options.proxy) options.proxy = normalizeProxySettings(options.proxy); verifyGeolocation(options.geolocation); } export function findMatchingHttpCredentials(credentials: HttpCredentials[] | undefined, url: string): HttpCredentials | undefined { const origin = new URL(url).origin.toLowerCase(); return credentials?.find(c => !c.origin || c.origin.toLowerCase() === origin); } export function verifyGeolocation(geolocation?: types.Geolocation): asserts geolocation is types.Geolocation { if (!geolocation) return; geolocation.accuracy = geolocation.accuracy || 0; const { longitude, latitude, accuracy } = geolocation; if (longitude < -180 || longitude > 180) throw new Error(`geolocation.longitude: precondition -180 <= LONGITUDE <= 180 failed.`); if (latitude < -90 || latitude > 90) throw new Error(`geolocation.latitude: precondition -90 <= LATITUDE <= 90 failed.`); if (accuracy < 0) throw new Error(`geolocation.accuracy: precondition 0 <= ACCURACY failed.`); } export function verifyClientCertificates(clientCertificates?: types.BrowserContextOptions['clientCertificates']) { if (!clientCertificates) return; for (const cert of clientCertificates) { if (!cert.origin) throw new Error(`clientCertificates.origin is required`); if (cert.noCertificate) { if (cert.cert || cert.key || cert.passphrase || cert.pfx) throw new Error('noCertificate is set together with cert, key, passphrase or pfx'); continue; } if (!cert.cert && !cert.key && !cert.passphrase && !cert.pfx) throw new Error('None of cert, key, passphrase or pfx is specified'); if (cert.cert && !cert.key) throw new Error('cert is specified without key'); if (!cert.cert && cert.key) throw new Error('key is specified without cert'); if (cert.pfx && (cert.cert || cert.key)) throw new Error('pfx is specified together with cert, key or passphrase'); } } export function normalizeProxySettings(proxy: types.ProxySettings): types.ProxySettings { let { server, bypass } = proxy; let url; try { // new URL('127.0.0.1:8080') throws // new URL('localhost:8080') fails to parse host or protocol // In both of these cases, we need to try re-parse URL with `http://` prefix. url = new URL(server); if (!url.host || !url.protocol) url = new URL('http://' + server); } catch (e) { url = new URL('http://' + server); } if (url.protocol === 'socks4:' && (proxy.username || proxy.password)) throw new Error(`Socks4 proxy protocol does not support authentication`); if (url.protocol === 'socks5:' && (proxy.username || proxy.password)) throw new Error(`Browser does not support socks5 proxy authentication`); server = url.protocol + '//' + url.host; if (bypass) bypass = bypass.split(',').map(t => t.trim()).join(','); return { ...proxy, server, bypass }; } const paramsThatAllowContextReuse: (keyof channels.BrowserNewContextForReuseParams)[] = [ 'colorScheme', 'forcedColors', 'reducedMotion', 'contrast', 'screen', 'userAgent', 'viewport', 'testIdAttributeName', ]; const defaultNewContextParamValues: channels.BrowserNewContextForReuseParams = { noDefaultViewport: false, ignoreHTTPSErrors: false, javaScriptEnabled: true, bypassCSP: false, offline: false, isMobile: false, hasTouch: false, acceptDownloads: 'accept', strictSelectors: false, serviceWorkers: 'allow', locale: 'en-US', };