/** * Copyright 2023 Google LLC. * 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 { InvalidArgumentException, NoSuchUserContextException, UnknownErrorException, UnsupportedOperationException, } from '../../../protocol/protocol.js'; export class BrowserProcessor { #browserCdpClient; #browsingContextStorage; #configStorage; #userContextStorage; constructor(browserCdpClient, browsingContextStorage, configStorage, userContextStorage) { this.#browserCdpClient = browserCdpClient; this.#browsingContextStorage = browsingContextStorage; this.#configStorage = configStorage; this.#userContextStorage = userContextStorage; } close() { // Ensure that it is put at the end of the event loop. // This way we send back the response before closing the tab. setTimeout(() => this.#browserCdpClient.sendCommand('Browser.close'), 0); return {}; } async createUserContext(params) { // `params` is a record to provide legacy `goog:` parameters. Now as the `proxy` // parameter is specified, we should get rid of `goog:proxyServer` and // `goog:proxyBypassList` and make the params of type // `Browser.CreateUserContextParameters`. const w3cParams = params; const globalConfig = this.#configStorage.getGlobalConfig(); if (w3cParams.acceptInsecureCerts !== undefined) { if (w3cParams.acceptInsecureCerts === false && globalConfig.acceptInsecureCerts === true) // TODO: https://github.com/GoogleChromeLabs/chromium-bidi/issues/3398 throw new UnknownErrorException(`Cannot set user context's "acceptInsecureCerts" to false, when a capability "acceptInsecureCerts" is set to true`); } const request = {}; if (w3cParams.proxy) { const proxyStr = getProxyStr(w3cParams.proxy); if (proxyStr) { request.proxyServer = proxyStr; } if (w3cParams.proxy.noProxy) { request.proxyBypassList = w3cParams.proxy.noProxy.join(','); } } else { // TODO: remove after Puppeteer stops using it. if (params['goog:proxyServer'] !== undefined) { request.proxyServer = params['goog:proxyServer']; } const proxyBypassList = params['goog:proxyBypassList'] ?? undefined; if (proxyBypassList) { request.proxyBypassList = proxyBypassList.join(','); } } const context = await this.#browserCdpClient.sendCommand('Target.createBrowserContext', request); await this.#applyDownloadBehavior(globalConfig.downloadBehavior ?? null, context.browserContextId); this.#configStorage.updateUserContextConfig(context.browserContextId, { acceptInsecureCerts: params['acceptInsecureCerts'], userPromptHandler: params['unhandledPromptBehavior'], }); return { userContext: context.browserContextId, }; } async removeUserContext(params) { const userContext = params.userContext; if (userContext === 'default') { throw new InvalidArgumentException('`default` user context cannot be removed'); } try { await this.#browserCdpClient.sendCommand('Target.disposeBrowserContext', { browserContextId: userContext, }); } catch (err) { // https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/target_handler.cc;l=1424;drc=c686e8f4fd379312469fe018f5c390e9c8f20d0d if (err.message.startsWith('Failed to find context with id')) { throw new NoSuchUserContextException(err.message); } throw err; } return {}; } async getUserContexts() { return { userContexts: await this.#userContextStorage.getUserContexts(), }; } async #getWindowInfo(targetId) { const windowInfo = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId }); return { // `active` is not supported in CDP yet. active: false, clientWindow: `${windowInfo.windowId}`, state: windowInfo.bounds.windowState ?? 'normal', height: windowInfo.bounds.height ?? 0, width: windowInfo.bounds.width ?? 0, x: windowInfo.bounds.left ?? 0, y: windowInfo.bounds.top ?? 0, }; } async getClientWindows() { const topLevelTargetIds = this.#browsingContextStorage .getTopLevelContexts() .map((b) => b.cdpTarget.id); const clientWindows = await Promise.all(topLevelTargetIds.map(async (targetId) => await this.#getWindowInfo(targetId))); const uniqueClientWindowIds = new Set(); const uniqueClientWindows = new Array(); // Filter out duplicated client windows. for (const window of clientWindows) { if (!uniqueClientWindowIds.has(window.clientWindow)) { uniqueClientWindowIds.add(window.clientWindow); uniqueClientWindows.push(window); } } return { clientWindows: uniqueClientWindows }; } #toCdpDownloadBehavior(downloadBehavior) { if (downloadBehavior === null) // CDP "default" behavior. return { behavior: 'default', }; if (downloadBehavior?.type === 'denied') // Deny all the downloads. return { behavior: 'deny', }; if (downloadBehavior?.type === 'allowed') { // CDP behavior "allow" means "save downloaded files to the specific download path". return { behavior: 'allow', downloadPath: downloadBehavior.destinationFolder, }; } // Unreachable. Handled by params parser. throw new UnknownErrorException('Unexpected download behavior'); } async #applyDownloadBehavior(downloadBehavior, userContext) { await this.#browserCdpClient.sendCommand('Browser.setDownloadBehavior', { ...this.#toCdpDownloadBehavior(downloadBehavior), browserContextId: userContext === 'default' ? undefined : userContext, // Required for enabling download events. eventsEnabled: true, }); } async setDownloadBehavior(params) { let userContexts; if (params.userContexts === undefined) { // Global download behavior. userContexts = (await this.#userContextStorage.getUserContexts()).map((c) => c.userContext); } else { // Download behavior for the specific user contexts. userContexts = Array.from(await this.#userContextStorage.verifyUserContextIdList(params.userContexts)); } if (params.userContexts === undefined) { // Store the global setting to be applied for the future user contexts. this.#configStorage.updateGlobalConfig({ downloadBehavior: params.downloadBehavior, }); } else { params.userContexts.map((userContext) => this.#configStorage.updateUserContextConfig(userContext, { downloadBehavior: params.downloadBehavior, })); } await Promise.all(userContexts.map(async (userContext) => { // Download behavior can be already set per user context, in which case the global // one should not be applied. const downloadBehavior = this.#configStorage.getActiveConfig(undefined, userContext) .downloadBehavior ?? null; await this.#applyDownloadBehavior(downloadBehavior, userContext); })); return {}; } } /** * Proxy config parse implementation: * https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107 */ export function getProxyStr(proxyConfig) { if (proxyConfig.proxyType === 'direct' || proxyConfig.proxyType === 'system') { // These types imply that Chrome should use its default behavior (e.g., direct // connection or system-configured proxy). No specific `proxyServer` string is // needed. return undefined; } if (proxyConfig.proxyType === 'pac') { throw new UnsupportedOperationException(`PAC proxy configuration is not supported per user context`); } if (proxyConfig.proxyType === 'autodetect') { throw new UnsupportedOperationException(`Autodetect proxy is not supported per user context`); } if (proxyConfig.proxyType === 'manual') { const servers = []; // HTTP Proxy if (proxyConfig.httpProxy !== undefined) { // servers.push(proxyConfig.httpProxy); servers.push(`http=${proxyConfig.httpProxy}`); } // SSL Proxy (uses 'https' scheme) if (proxyConfig.sslProxy !== undefined) { // servers.push(proxyConfig.sslProxy); servers.push(`https=${proxyConfig.sslProxy}`); } // SOCKS Proxy if (proxyConfig.socksProxy !== undefined || proxyConfig.socksVersion !== undefined) { // socksVersion is mandatory and must be a valid integer if socksProxy is // specified. if (proxyConfig.socksProxy === undefined) { throw new InvalidArgumentException(`'socksVersion' cannot be set without 'socksProxy'`); } if (proxyConfig.socksVersion === undefined || typeof proxyConfig.socksVersion !== 'number' || !Number.isInteger(proxyConfig.socksVersion) || proxyConfig.socksVersion < 0 || proxyConfig.socksVersion > 255) { throw new InvalidArgumentException(`'socksVersion' must be between 0 and 255`); } servers.push(`socks=socks${proxyConfig.socksVersion}://${proxyConfig.socksProxy}`); } if (servers.length === 0) { // If 'manual' proxyType is chosen but no specific proxy servers (http, ssl, socks) // are provided, it means no proxy server should be configured. return undefined; } return servers.join(';'); } // Unreachable. throw new UnknownErrorException(`Unknown proxy type`); } //# sourceMappingURL=BrowserProcessor.js.map