Add Brave CDP automation, replace Oracle browser mode

Connects to user's running Brave via Chrome DevTools Protocol
to automate ChatGPT interaction. Uses puppeteer-core to open a
tab, send the prompt, wait for response, and extract the result.

No cookies, no separate profiles, no copy/paste. Just connects
to the browser where the user is already logged in.

One-time setup: relaunch Brave with --remote-debugging-port=9222

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Taylor Eernisse
2026-02-07 16:16:41 -05:00
parent d776a266a8
commit e7882b917b
4163 changed files with 782828 additions and 148 deletions

View File

@@ -0,0 +1,37 @@
/**
* Copyright 2024 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 { type Bluetooth, type EmptyResult } from '../../../protocol/protocol.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class BluetoothProcessor {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage);
simulateAdapter(params: Bluetooth.SimulateAdapterParameters): Promise<EmptyResult>;
disableSimulation(params: Bluetooth.DisableSimulationParameters): Promise<EmptyResult>;
simulatePreconnectedPeripheral(params: Bluetooth.SimulatePreconnectedPeripheralParameters): Promise<EmptyResult>;
simulateAdvertisement(params: Bluetooth.SimulateAdvertisementParameters): Promise<EmptyResult>;
simulateCharacteristic(params: Bluetooth.SimulateCharacteristicParameters): Promise<EmptyResult>;
simulateCharacteristicResponse(params: Bluetooth.SimulateCharacteristicResponseParameters): Promise<EmptyResult>;
simulateDescriptor(params: Bluetooth.SimulateDescriptorParameters): Promise<EmptyResult>;
simulateDescriptorResponse(params: Bluetooth.SimulateDescriptorResponseParameters): Promise<EmptyResult>;
simulateGattConnectionResponse(params: Bluetooth.SimulateGattConnectionResponseParameters): Promise<EmptyResult>;
simulateGattDisconnection(params: Bluetooth.SimulateGattDisconnectionParameters): Promise<EmptyResult>;
simulateService(params: Bluetooth.SimulateServiceParameters): Promise<EmptyResult>;
onCdpTargetCreated(cdpTarget: CdpTarget): void;
handleRequestDevicePrompt(params: Bluetooth.HandleRequestDevicePromptParameters): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,411 @@
"use strict";
/**
* Copyright 2024 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BluetoothProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
/** Represents a base Bluetooth GATT item. */
class BluetoothGattItem {
id;
uuid;
constructor(id, uuid) {
this.id = id;
this.uuid = uuid;
}
}
/** Represents a Bluetooth descriptor. */
class BluetoothDescriptor extends BluetoothGattItem {
characteristic;
constructor(id, uuid, characteristic) {
super(id, uuid);
this.characteristic = characteristic;
}
}
/** Represents a Bluetooth characteristic. */
class BluetoothCharacteristic extends BluetoothGattItem {
descriptors = new Map();
service;
constructor(id, uuid, service) {
super(id, uuid);
this.service = service;
}
}
/** Represents a Bluetooth service. */
class BluetoothService extends BluetoothGattItem {
characteristics = new Map();
device;
constructor(id, uuid, device) {
super(id, uuid);
this.device = device;
}
}
/** Represents a Bluetooth device. */
class BluetoothDevice {
address;
services = new Map();
constructor(address) {
this.address = address;
}
}
class BluetoothProcessor {
#eventManager;
#browsingContextStorage;
#bluetoothDevices = new Map();
// A map from a characteristic id from CDP to its BluetoothCharacteristic object.
#bluetoothCharacteristics = new Map();
// A map from a descriptor id from CDP to its BluetoothDescriptor object.
#bluetoothDescriptors = new Map();
constructor(eventManager, browsingContextStorage) {
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
}
#getDevice(address) {
const device = this.#bluetoothDevices.get(address);
if (!device) {
throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${address} does not exist`);
}
return device;
}
#getService(device, serviceUuid) {
const service = device.services.get(serviceUuid);
if (!service) {
throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${serviceUuid} on device ${device.address} does not exist`);
}
return service;
}
#getCharacteristic(service, characteristicUuid) {
const characteristic = service.characteristics.get(characteristicUuid);
if (!characteristic) {
throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${characteristicUuid} does not exist for service ${service.uuid} on device ${service.device.address}`);
}
return characteristic;
}
#getDescriptor(characteristic, descriptorUuid) {
const descriptor = characteristic.descriptors.get(descriptorUuid);
if (!descriptor) {
throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${descriptorUuid} does not exist for characteristic ${characteristic.uuid} on service ${characteristic.service.uuid} on device ${characteristic.service.device.address}`);
}
return descriptor;
}
async simulateAdapter(params) {
if (params.state === undefined) {
// The bluetooth.simulateAdapter Command
// Step 4.2. If params["state"] does not exist, return error with error code invalid argument.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
throw new protocol_js_1.InvalidArgumentException(`Parameter "state" is required for creating a Bluetooth adapter`);
}
const context = this.#browsingContextStorage.getContext(params.context);
// Bluetooth spec requires overriding the existing adapter (step 6). From the CDP
// perspective, we need to disable the emulation first.
// https://webbluetoothcg.github.io/web-bluetooth/#bluetooth-simulateAdapter-command
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.enable', {
state: params.state,
leSupported: params.leSupported ?? true,
});
return {};
}
async disableSimulation(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.disable');
this.#bluetoothDevices.clear();
this.#bluetoothCharacteristics.clear();
this.#bluetoothDescriptors.clear();
return {};
}
async simulatePreconnectedPeripheral(params) {
if (this.#bluetoothDevices.has(params.address)) {
throw new protocol_js_1.InvalidArgumentException(`Bluetooth device with address ${params.address} already exists`);
}
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulatePreconnectedPeripheral', {
address: params.address,
name: params.name,
knownServiceUuids: params.knownServiceUuids,
manufacturerData: params.manufacturerData,
});
this.#bluetoothDevices.set(params.address, new BluetoothDevice(params.address));
return {};
}
async simulateAdvertisement(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateAdvertisement', {
entry: params.scanEntry,
});
return {};
}
async simulateCharacteristic(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (params.characteristicProperties === undefined) {
throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" is required for adding a Bluetooth characteristic`);
}
if (service.characteristics.has(params.characteristicUuid)) {
throw new protocol_js_1.InvalidArgumentException(`Characteristic with UUID ${params.characteristicUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addCharacteristic', {
serviceId: service.id,
characteristicUuid: params.characteristicUuid,
properties: params.characteristicProperties,
});
const characteristic = new BluetoothCharacteristic(response.characteristicId, params.characteristicUuid, service);
service.characteristics.set(params.characteristicUuid, characteristic);
this.#bluetoothCharacteristics.set(characteristic.id, characteristic);
return {};
}
case 'remove': {
if (params.characteristicProperties !== undefined) {
throw new protocol_js_1.InvalidArgumentException(`Parameter "characteristicProperties" should not be provided for removing a Bluetooth characteristic`);
}
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeCharacteristic', {
characteristicId: characteristic.id,
});
service.characteristics.delete(params.characteristicUuid);
this.#bluetoothCharacteristics.delete(characteristic.id);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateCharacteristicResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateCharacteristicOperationResponse', {
characteristicId: characteristic.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateDescriptor(params) {
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (characteristic.descriptors.has(params.descriptorUuid)) {
throw new protocol_js_1.InvalidArgumentException(`Descriptor with UUID ${params.descriptorUuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addDescriptor', {
characteristicId: characteristic.id,
descriptorUuid: params.descriptorUuid,
});
const descriptor = new BluetoothDescriptor(response.descriptorId, params.descriptorUuid, characteristic);
characteristic.descriptors.set(params.descriptorUuid, descriptor);
this.#bluetoothDescriptors.set(descriptor.id, descriptor);
return {};
}
case 'remove': {
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeDescriptor', {
descriptorId: descriptor.id,
});
characteristic.descriptors.delete(params.descriptorUuid);
this.#bluetoothDescriptors.delete(descriptor.id);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
async simulateDescriptorResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const device = this.#getDevice(params.address);
const service = this.#getService(device, params.serviceUuid);
const characteristic = this.#getCharacteristic(service, params.characteristicUuid);
const descriptor = this.#getDescriptor(characteristic, params.descriptorUuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateDescriptorOperationResponse', {
descriptorId: descriptor.id,
type: params.type,
code: params.code,
...(params.data && {
data: btoa(String.fromCharCode(...params.data)),
}),
});
return {};
}
async simulateGattConnectionResponse(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: params.address,
type: 'connection',
code: params.code,
});
return {};
}
async simulateGattDisconnection(params) {
const context = this.#browsingContextStorage.getContext(params.context);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTDisconnection', {
address: params.address,
});
return {};
}
async simulateService(params) {
const device = this.#getDevice(params.address);
const context = this.#browsingContextStorage.getContext(params.context);
switch (params.type) {
case 'add': {
if (device.services.has(params.uuid)) {
throw new protocol_js_1.InvalidArgumentException(`Service with UUID ${params.uuid} already exists`);
}
const response = await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.addService', {
address: params.address,
serviceUuid: params.uuid,
});
device.services.set(params.uuid, new BluetoothService(response.serviceId, params.uuid, device));
return {};
}
case 'remove': {
const service = this.#getService(device, params.uuid);
await context.cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.removeService', {
serviceId: service.id,
});
device.services.delete(params.uuid);
return {};
}
default:
throw new protocol_js_1.InvalidArgumentException(`Parameter "type" of ${params.type} is not supported`);
}
}
onCdpTargetCreated(cdpTarget) {
cdpTarget.cdpClient.on('DeviceAccess.deviceRequestPrompted', (event) => {
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.requestDevicePromptUpdated',
params: {
context: cdpTarget.id,
prompt: event.id,
devices: event.devices,
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.gattOperationReceived', async (event) => {
switch (event.type) {
case 'connection':
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.gattConnectionAttempted',
params: {
context: cdpTarget.id,
address: event.address,
},
}, cdpTarget.id);
return;
case 'discovery':
// Chromium Web Bluetooth simulation generates this GATT discovery event when
// a page attempts to get services for a given Bluetooth device for the first time.
// This 'get services' operation is put on hold until a GATT discovery response
// is sent to the simulation.
// Note: Web Bluetooth automation (see https://webbluetoothcg.github.io/web-bluetooth/#automated-testing)
// does not support simulating a GATT discovery response. This is because simulated services, characteristics,
// or descriptors are immediately visible to the simulation, meaning it doesn't have a distinct
// DISCOVERY state. Therefore, this code simulates a successful GATT discovery
// response upon receiving this event.
await cdpTarget.browserCdpClient.sendCommand('BluetoothEmulation.simulateGATTOperationResponse', {
address: event.address,
type: 'discovery',
code: 0x0,
});
}
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.characteristicOperationReceived', (event) => {
if (!this.#bluetoothCharacteristics.has(event.characteristicId)) {
return;
}
let type;
if (event.type === 'write') {
// write-default-deprecated comes from
// https://webbluetoothcg.github.io/web-bluetooth/#dom-bluetoothremotegattcharacteristic-writevalue,
// which is deprecated so not supported.
if (event.writeType === 'write-default-deprecated') {
return;
}
type = event.writeType;
}
else {
type = event.type;
}
const characteristic = this.#bluetoothCharacteristics.get(event.characteristicId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.characteristicEventGenerated',
params: {
context: cdpTarget.id,
address: characteristic.service.device.address,
serviceUuid: characteristic.service.uuid,
characteristicUuid: characteristic.uuid,
type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
cdpTarget.browserCdpClient.on('BluetoothEmulation.descriptorOperationReceived', (event) => {
if (!this.#bluetoothDescriptors.has(event.descriptorId)) {
return;
}
const descriptor = this.#bluetoothDescriptors.get(event.descriptorId);
this.#eventManager.registerEvent({
type: 'event',
method: 'bluetooth.descriptorEventGenerated',
params: {
context: cdpTarget.id,
address: descriptor.characteristic.service.device.address,
serviceUuid: descriptor.characteristic.service.uuid,
characteristicUuid: descriptor.characteristic.uuid,
descriptorUuid: descriptor.uuid,
type: event.type,
...(event.data && {
data: Array.from(atob(event.data), (c) => c.charCodeAt(0)),
}),
},
}, cdpTarget.id);
});
}
async handleRequestDevicePrompt(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (params.accept) {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.selectPrompt', {
id: params.prompt,
deviceId: params.device,
});
}
else {
await context.cdpTarget.cdpClient.sendCommand('DeviceAccess.cancelPrompt', {
id: params.prompt,
});
}
return {};
}
}
exports.BluetoothProcessor = BluetoothProcessor;
//# sourceMappingURL=BluetoothProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
/**
* 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 { type Browser, type EmptyResult, type Session } from '../../../protocol/protocol.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ContextConfigStorage } from './ContextConfigStorage.js';
import type { UserContextStorage } from './UserContextStorage.js';
export declare class BrowserProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, userContextStorage: UserContextStorage);
close(): EmptyResult;
createUserContext(params: Record<string, any>): Promise<Browser.CreateUserContextResult>;
removeUserContext(params: Browser.RemoveUserContextParameters): Promise<EmptyResult>;
getUserContexts(): Promise<Browser.GetUserContextsResult>;
setClientWindowState(params: Browser.SetClientWindowStateParameters): Promise<Browser.SetClientWindowStateResult>;
getClientWindows(): Promise<Browser.GetClientWindowsResult>;
setDownloadBehavior(params: Browser.SetDownloadBehaviorParameters): Promise<EmptyResult>;
}
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
export declare function getProxyStr(proxyConfig: Session.ProxyConfiguration): string | undefined;

View File

@@ -0,0 +1,294 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowserProcessor = void 0;
exports.getProxyStr = getProxyStr;
const protocol_js_1 = require("../../../protocol/protocol.js");
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.
// Always catch uncaught exceptions.
setTimeout(() => this.#browserCdpClient.sendCommand('Browser.close').catch(() => { }), 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 protocol_js_1.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 protocol_js_1.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 protocol_js_1.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 setClientWindowState(params) {
const { clientWindow } = params;
const bounds = {
windowState: params.state,
};
if (params.state === 'normal') {
if (params.width !== undefined) {
bounds.width = params.width;
}
if (params.height !== undefined) {
bounds.height = params.height;
}
if (params.x !== undefined) {
bounds.left = params.x;
}
if (params.y !== undefined) {
bounds.top = params.y;
}
}
const windowId = Number.parseInt(clientWindow);
if (isNaN(windowId)) {
throw new protocol_js_1.InvalidArgumentException('no such client window');
}
await this.#browserCdpClient.sendCommand('Browser.setWindowBounds', {
windowId,
bounds,
});
const result = await this.#browserCdpClient.sendCommand('Browser.getWindowBounds', {
windowId,
});
return {
active: false,
clientWindow: `${windowId}`,
state: result.bounds.windowState ?? 'normal',
height: result.bounds.height ?? 0,
width: result.bounds.width ?? 0,
x: result.bounds.left ?? 0,
y: result.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 protocol_js_1.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 {};
}
}
exports.BrowserProcessor = BrowserProcessor;
/**
* Proxy config parse implementation:
* https://source.chromium.org/chromium/chromium/src/+/main:net/proxy_resolution/proxy_config.h;drc=743a82d08e59d803c94ee1b8564b8b11dd7b462f;l=107
*/
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 protocol_js_1.UnsupportedOperationException(`PAC proxy configuration is not supported per user context`);
}
if (proxyConfig.proxyType === 'autodetect') {
throw new protocol_js_1.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 protocol_js_1.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 protocol_js_1.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 protocol_js_1.UnknownErrorException(`Unknown proxy type`);
}
//# sourceMappingURL=BrowserProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,50 @@
/**
* Copyright 2025 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 type { Protocol } from 'devtools-protocol';
import type { Browser, BrowsingContext, Emulation, Session, UAClientHints } from '../../../protocol/protocol.js';
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
export declare class ContextConfig {
acceptInsecureCerts?: boolean;
clientHints?: UAClientHints.Emulation.ClientHintsMetadata | null;
devicePixelRatio?: number | null;
disableNetworkDurableMessages?: true;
downloadBehavior?: Browser.DownloadBehavior | null;
emulatedNetworkConditions?: Emulation.NetworkConditions | null;
extraHeaders?: Protocol.Network.Headers;
geolocation?: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null;
locale?: string | null;
maxTouchPoints?: number | null;
prerenderingDisabled?: boolean;
screenArea?: Emulation.ScreenArea | null;
screenOrientation?: Emulation.ScreenOrientation | null;
scriptingEnabled?: false | null;
timezone?: string | null;
userAgent?: string | null;
userPromptHandler?: Session.UserPromptHandler;
viewport?: BrowsingContext.Viewport | null;
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs: (ContextConfig | undefined)[]): ContextConfig;
}

View File

@@ -0,0 +1,74 @@
"use strict";
/**
* Copyright 2025 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextConfig = void 0;
/**
* Represents a context configurations. It can be global, per User Context, or per
* Browsing Context. The undefined value means the config will be taken from the upstream
* config. `null` values means the value should be default regardless of the upstream.
*/
class ContextConfig {
// keep-sorted start block=yes
acceptInsecureCerts;
clientHints;
devicePixelRatio;
disableNetworkDurableMessages;
downloadBehavior;
emulatedNetworkConditions;
// Extra headers are kept in CDP format.
extraHeaders;
geolocation;
locale;
maxTouchPoints;
prerenderingDisabled;
screenArea;
screenOrientation;
scriptingEnabled;
// Timezone is kept in CDP format with GMT prefix for offset values.
timezone;
userAgent;
userPromptHandler;
viewport;
// keep-sorted end
/**
* Merges multiple `ContextConfig` objects. The configs are merged in the order they are
* provided. For each property, the value from the last config that defines it will be
* used. The final result will not contain any `undefined` or `null` properties.
* `undefined` values are ignored. `null` values remove the already set value.
*/
static merge(...configs) {
const result = new ContextConfig();
for (const config of configs) {
if (!config) {
continue;
}
for (const key in config) {
const value = config[key];
if (value === null) {
delete result[key];
}
else if (value !== undefined) {
result[key] = value;
}
}
}
return result;
}
}
exports.ContextConfig = ContextConfig;
//# sourceMappingURL=ContextConfig.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContextConfig.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/ContextConfig.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAYH;;;;GAIG;AACH,MAAa,aAAa;IACxB,8BAA8B;IAC9B,mBAAmB,CAAW;IAC9B,WAAW,CAAsD;IACjE,gBAAgB,CAAiB;IACjC,6BAA6B,CAAQ;IACrC,gBAAgB,CAAmC;IACnD,yBAAyB,CAAsC;IAC/D,wCAAwC;IACxC,YAAY,CAA4B;IACxC,WAAW,CAGF;IACT,MAAM,CAAiB;IACvB,cAAc,CAAiB;IAC/B,oBAAoB,CAAW;IAC/B,UAAU,CAA+B;IACzC,iBAAiB,CAAsC;IACvD,gBAAgB,CAAgB;IAChC,oEAAoE;IACpE,QAAQ,CAAiB;IACzB,SAAS,CAAiB;IAC1B,iBAAiB,CAA6B;IAC9C,QAAQ,CAAmC;IAC3C,kBAAkB;IAElB;;;;;OAKG;IACH,MAAM,CAAC,KAAK,CAAC,GAAG,OAAsC;QACpD,MAAM,MAAM,GAAG,IAAI,aAAa,EAAE,CAAC;QAEnC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,SAAS;YACX,CAAC;YACD,KAAK,MAAM,GAAG,IAAI,MAAM,EAAE,CAAC;gBACzB,MAAM,KAAK,GAAG,MAAM,CAAC,GAA0B,CAAC,CAAC;gBACjD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;oBACnB,OAAQ,MAAc,CAAC,GAAG,CAAC,CAAC;gBAC9B,CAAC;qBAAM,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBAC9B,MAAc,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;gBAC/B,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAnDD,sCAmDC"}

View File

@@ -0,0 +1,42 @@
import { ContextConfig } from './ContextConfig.js';
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
export declare class ContextConfigStorage {
#private;
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config: ContextConfig): void;
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId: string, config: ContextConfig): void;
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext: string, config: ContextConfig): void;
/**
* Returns the current global configuration.
*/
getGlobalConfig(): ContextConfig;
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId: string | undefined, userContext: string): ContextConfig;
}

View File

@@ -0,0 +1,96 @@
"use strict";
/*
* Copyright 2025 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ContextConfigStorage = void 0;
const ContextConfig_js_1 = require("./ContextConfig.js");
/**
* Manages context-specific configurations. This class allows setting
* configurations at three levels: global, user context, and browsing context.
*
* When `getActiveConfig` is called, it merges the configurations in a specific
* order of precedence: `global -> user context -> browsing context`. For each
* configuration property, the value from the highest-precedence level that has a
* non-`undefined` value is used.
*
* The `update` methods (`updateGlobalConfig`, `updateUserContextConfig`,
* `updateBrowsingContextConfig`) merge the provided configuration with the
* existing one at the corresponding level. Properties with `undefined` values in
* the provided configuration are ignored, preserving the existing value.
*/
class ContextConfigStorage {
#global = new ContextConfig_js_1.ContextConfig();
#userContextConfigs = new Map();
#browsingContextConfigs = new Map();
/**
* Updates the global configuration. Properties with `undefined` values in the
* provided `config` are ignored.
*/
updateGlobalConfig(config) {
this.#global = ContextConfig_js_1.ContextConfig.merge(this.#global, config);
}
/**
* Updates the configuration for a specific browsing context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateBrowsingContextConfig(browsingContextId, config) {
this.#browsingContextConfigs.set(browsingContextId, ContextConfig_js_1.ContextConfig.merge(this.#browsingContextConfigs.get(browsingContextId), config));
}
/**
* Updates the configuration for a specific user context. Properties with
* `undefined` values in the provided `config` are ignored.
*/
updateUserContextConfig(userContext, config) {
this.#userContextConfigs.set(userContext, ContextConfig_js_1.ContextConfig.merge(this.#userContextConfigs.get(userContext), config));
}
/**
* Returns the current global configuration.
*/
getGlobalConfig() {
return this.#global;
}
/**
* Extra headers is a special case. The headers from the different levels have to be
* merged instead of being overridden.
*/
#getExtraHeaders(topLevelBrowsingContextId, userContext) {
const globalHeaders = this.#global.extraHeaders ?? {};
const userContextHeaders = this.#userContextConfigs.get(userContext)?.extraHeaders ?? {};
const browsingContextHeaders = topLevelBrowsingContextId === undefined
? {}
: (this.#browsingContextConfigs.get(topLevelBrowsingContextId)
?.extraHeaders ?? {});
return { ...globalHeaders, ...userContextHeaders, ...browsingContextHeaders };
}
/**
* Calculates the active configuration by merging global, user context, and
* browsing context settings.
*/
getActiveConfig(topLevelBrowsingContextId, userContext) {
let result = ContextConfig_js_1.ContextConfig.merge(this.#global, this.#userContextConfigs.get(userContext));
if (topLevelBrowsingContextId !== undefined) {
result = ContextConfig_js_1.ContextConfig.merge(result, this.#browsingContextConfigs.get(topLevelBrowsingContextId));
}
// Extra headers is a special case which have to be treated in a special way.
const extraHeaders = this.#getExtraHeaders(topLevelBrowsingContextId, userContext);
result.extraHeaders =
Object.keys(extraHeaders).length > 0 ? extraHeaders : undefined;
return result;
}
}
exports.ContextConfigStorage = ContextConfigStorage;
//# sourceMappingURL=ContextConfigStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ContextConfigStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/ContextConfigStorage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,yDAAiD;AAEjD;;;;;;;;;;;;;GAaG;AACH,MAAa,oBAAoB;IAC/B,OAAO,GAAG,IAAI,gCAAa,EAAE,CAAC;IAC9B,mBAAmB,GAAG,IAAI,GAAG,EAAyB,CAAC;IACvD,uBAAuB,GAAG,IAAI,GAAG,EAAyB,CAAC;IAE3D;;;OAGG;IACH,kBAAkB,CAAC,MAAqB;QACtC,IAAI,CAAC,OAAO,GAAG,gCAAa,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAC3D,CAAC;IAED;;;OAGG;IACH,2BAA2B,CACzB,iBAAyB,EACzB,MAAqB;QAErB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAC9B,iBAAiB,EACjB,gCAAa,CAAC,KAAK,CACjB,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,iBAAiB,CAAC,EACnD,MAAM,CACP,CACF,CAAC;IACJ,CAAC;IAED;;;OAGG;IACH,uBAAuB,CAAC,WAAmB,EAAE,MAAqB;QAChE,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAC1B,WAAW,EACX,gCAAa,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC,CACvE,CAAC;IACJ,CAAC;IAED;;OAEG;IACH,eAAe;QACb,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED;;;OAGG;IACH,gBAAgB,CACd,yBAA6C,EAC7C,WAAmB;QAEnB,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,YAAY,IAAI,EAAE,CAAC;QACtD,MAAM,kBAAkB,GACtB,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,YAAY,IAAI,EAAE,CAAC;QAChE,MAAM,sBAAsB,GAC1B,yBAAyB,KAAK,SAAS;YACrC,CAAC,CAAC,EAAE;YACJ,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC;gBAC1D,EAAE,YAAY,IAAI,EAAE,CAAC,CAAC;QAE9B,OAAO,EAAC,GAAG,aAAa,EAAE,GAAG,kBAAkB,EAAE,GAAG,sBAAsB,EAAC,CAAC;IAC9E,CAAC;IAED;;;OAGG;IACH,eAAe,CACb,yBAA6C,EAC7C,WAAmB;QAEnB,IAAI,MAAM,GAAG,gCAAa,CAAC,KAAK,CAC9B,IAAI,CAAC,OAAO,EACZ,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,WAAW,CAAC,CAC1C,CAAC;QACF,IAAI,yBAAyB,KAAK,SAAS,EAAE,CAAC;YAC5C,MAAM,GAAG,gCAAa,CAAC,KAAK,CAC1B,MAAM,EACN,IAAI,CAAC,uBAAuB,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAC5D,CAAC;QACJ,CAAC;QAED,6EAA6E;QAC7E,MAAM,YAAY,GAAG,IAAI,CAAC,gBAAgB,CACxC,yBAAyB,EACzB,WAAW,CACZ,CAAC;QACF,MAAM,CAAC,YAAY;YACjB,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;QAElE,OAAO,MAAM,CAAC;IAChB,CAAC;CACF;AAjGD,oDAiGC"}

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2025 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 type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser } from '../../../protocol/protocol.js';
export declare class UserContextStorage {
#private;
constructor(browserClient: CdpClient);
getUserContexts(): Promise<[
Browser.UserContextInfo,
...Browser.UserContextInfo[]
]>;
verifyUserContextIdList(userContextIds: Browser.UserContext[]): Promise<Set<string>>;
}

View File

@@ -0,0 +1,56 @@
"use strict";
/**
* Copyright 2025 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.UserContextStorage = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
class UserContextStorage {
#browserClient;
constructor(browserClient) {
this.#browserClient = browserClient;
}
async getUserContexts() {
const result = await this.#browserClient.sendCommand('Target.getBrowserContexts');
return [
{
userContext: 'default',
},
...result.browserContextIds.map((id) => {
return {
userContext: id,
};
}),
];
}
async verifyUserContextIdList(userContextIds) {
const foundContexts = new Set();
if (!userContextIds.length) {
return foundContexts;
}
const userContexts = await this.getUserContexts();
const knownUserContextIds = new Set(userContexts.map((userContext) => userContext.userContext));
for (const userContextId of userContextIds) {
if (!knownUserContextIds.has(userContextId)) {
throw new protocol_js_1.NoSuchUserContextException(`User context ${userContextId} not found`);
}
foundContexts.add(userContextId);
}
return foundContexts;
}
}
exports.UserContextStorage = UserContextStorage;
//# sourceMappingURL=UserContextStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"UserContextStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/browser/UserContextStorage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAGH,+DAGuC;AAEvC,MAAa,kBAAkB;IAC7B,cAAc,CAAY;IAC1B,YAAY,aAAwB;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,eAAe;QAGnB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,cAAc,CAAC,WAAW,CAClD,2BAA2B,CAC5B,CAAC;QACF,OAAO;YACL;gBACE,WAAW,EAAE,SAAS;aACvB;YACD,GAAG,MAAM,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE;gBACrC,OAAO;oBACL,WAAW,EAAE,EAAE;iBAChB,CAAC;YACJ,CAAC,CAAC;SACH,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,uBAAuB,CAAC,cAAqC;QACjE,MAAM,aAAa,GAAG,IAAI,GAAG,EAAuB,CAAC;QACrD,IAAI,CAAC,cAAc,CAAC,MAAM,EAAE,CAAC;YAC3B,OAAO,aAAa,CAAC;QACvB,CAAC;QAED,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,eAAe,EAAE,CAAC;QAClD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CACjC,YAAY,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,WAAW,CAAC,WAAW,CAAC,CAC3D,CAAC;QACF,KAAK,MAAM,aAAa,IAAI,cAAc,EAAE,CAAC;YAC3C,IAAI,CAAC,mBAAmB,CAAC,GAAG,CAAC,aAAa,CAAC,EAAE,CAAC;gBAC5C,MAAM,IAAI,wCAA0B,CAClC,gBAAgB,aAAa,YAAY,CAC1C,CAAC;YACJ,CAAC;YACD,aAAa,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;QACnC,CAAC;QAED,OAAO,aAAa,CAAC;IACvB,CAAC;CACF;AA7CD,gDA6CC"}

View File

@@ -0,0 +1,27 @@
/**
* 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 { type Cdp } from '../../../protocol/protocol.js';
import type { CdpClient, CdpConnection } from '../../BidiMapper.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
export declare class CdpProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, cdpConnection: CdpConnection, browserCdpClient: CdpClient);
getSession(params: Cdp.GetSessionParameters): Cdp.GetSessionResult;
resolveRealm(params: Cdp.ResolveRealmParameters): Cdp.ResolveRealmResult;
sendCommand(params: Cdp.SendCommandParameters): Promise<Cdp.SendCommandResult>;
}

View File

@@ -0,0 +1,60 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
class CdpProcessor {
#browsingContextStorage;
#realmStorage;
#cdpConnection;
#browserCdpClient;
constructor(browsingContextStorage, realmStorage, cdpConnection, browserCdpClient) {
this.#browsingContextStorage = browsingContextStorage;
this.#realmStorage = realmStorage;
this.#cdpConnection = cdpConnection;
this.#browserCdpClient = browserCdpClient;
}
getSession(params) {
const context = params.context;
const sessionId = this.#browsingContextStorage.getContext(context).cdpTarget.cdpSessionId;
if (sessionId === undefined) {
return {};
}
return { session: sessionId };
}
resolveRealm(params) {
const context = params.realm;
const realm = this.#realmStorage.getRealm({ realmId: context });
if (realm === undefined) {
throw new protocol_js_1.UnknownErrorException(`Could not find realm ${params.realm}`);
}
return { executionContextId: realm.executionContextId };
}
async sendCommand(params) {
const client = params.session
? this.#cdpConnection.getCdpClient(params.session)
: this.#browserCdpClient;
const result = await client.sendCommand(params.method, params.params);
return {
result,
session: params.session,
};
}
}
exports.CdpProcessor = CdpProcessor;
//# sourceMappingURL=CdpProcessor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CdpProcessor.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/cdp/CdpProcessor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,+DAA8E;AAK9E,MAAa,YAAY;IACd,uBAAuB,CAAyB;IAChD,aAAa,CAAe;IAC5B,cAAc,CAAgB;IAC9B,iBAAiB,CAAY;IAEtC,YACE,sBAA8C,EAC9C,YAA0B,EAC1B,aAA4B,EAC5B,gBAA2B;QAE3B,IAAI,CAAC,uBAAuB,GAAG,sBAAsB,CAAC;QACtD,IAAI,CAAC,aAAa,GAAG,YAAY,CAAC;QAClC,IAAI,CAAC,cAAc,GAAG,aAAa,CAAC;QACpC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,UAAU,CAAC,MAAgC;QACzC,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;QAC/B,MAAM,SAAS,GACb,IAAI,CAAC,uBAAuB,CAAC,UAAU,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC;QAC1E,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO,EAAE,CAAC;QACZ,CAAC;QACD,OAAO,EAAC,OAAO,EAAE,SAAS,EAAC,CAAC;IAC9B,CAAC;IAED,YAAY,CAAC,MAAkC;QAC7C,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC;QAC7B,MAAM,KAAK,GAAG,IAAI,CAAC,aAAa,CAAC,QAAQ,CAAC,EAAC,OAAO,EAAE,OAAO,EAAC,CAAC,CAAC;QAC9D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;YACxB,MAAM,IAAI,mCAAqB,CAAC,wBAAwB,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,OAAO,EAAC,kBAAkB,EAAE,KAAK,CAAC,kBAAkB,EAAC,CAAC;IACxD,CAAC;IAED,KAAK,CAAC,WAAW,CACf,MAAiC;QAEjC,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO;YAC3B,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,MAAM,CAAC,OAAO,CAAC;YAClD,CAAC,CAAC,IAAI,CAAC,iBAAiB,CAAC;QAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;QACtE,OAAO;YACL,MAAM;YACN,OAAO,EAAE,MAAM,CAAC,OAAO;SACxB,CAAC;IACJ,CAAC;CACF;AAjDD,oCAiDC"}

View File

@@ -0,0 +1,57 @@
import type { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { type Browser, type BrowsingContext, type ChromiumBidi, Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { Result } from '../../../utils/result.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import { type NetworkStorage } from '../network/NetworkStorage.js';
import type { ChannelProxy } from '../script/ChannelProxy.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class CdpTarget {
#private;
readonly userContext: Browser.UserContext;
readonly contextConfigStorage: ContextConfigStorage;
static create(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, realmStorage: RealmStorage, eventManager: EventManager, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn): CdpTarget;
constructor(targetId: Protocol.Target.TargetID, cdpClient: CdpClient, browserCdpClient: CdpClient, parentCdpClient: CdpClient, eventManager: EventManager, realmStorage: RealmStorage, preloadScriptStorage: PreloadScriptStorage, browsingContextStorage: BrowsingContextStorage, configStorage: ContextConfigStorage, networkStorage: NetworkStorage, userContext: Browser.UserContext, defaultUserAgent: string, logger: LoggerFn | undefined);
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked(): Deferred<Result<void>>;
get id(): Protocol.Target.TargetID;
get cdpClient(): CdpClient;
get parentCdpClient(): CdpClient;
get browserCdpClient(): CdpClient;
/** Needed for CDP escape path. */
get cdpSessionId(): Protocol.Target.SessionID;
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId(): number;
toggleFetchIfNeeded(): Promise<void>;
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
toggleNetworkIfNeeded(): Promise<void>;
toggleSetCacheDisabled(disable?: boolean): Promise<void>;
toggleDeviceAccessIfNeeded(): Promise<void>;
togglePreloadIfNeeded(): Promise<void>;
toggleNetwork(): Promise<void>;
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels(): ChannelProxy[];
setDeviceMetricsOverride(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null, screenArea: Emulation.ScreenArea | null): Promise<void>;
get topLevelId(): string;
isSubscribedTo(moduleOrEvent: ChromiumBidi.EventNames): boolean;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setExtraHeaders(headers: Protocol.Network.Headers): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints?: UAClientHints.Emulation.ClientHintsMetadata | null): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
}

View File

@@ -0,0 +1,695 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpTarget = void 0;
const chromium_bidi_js_1 = require("../../../protocol/chromium-bidi.js");
const protocol_js_1 = require("../../../protocol/protocol.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
const LogManager_js_1 = require("../log/LogManager.js");
const NetworkStorage_js_1 = require("../network/NetworkStorage.js");
class CdpTarget {
#id;
userContext;
#cdpClient;
#browserCdpClient;
#parentCdpClient;
#realmStorage;
#eventManager;
#preloadScriptStorage;
#browsingContextStorage;
#networkStorage;
contextConfigStorage;
#unblocked = new Deferred_js_1.Deferred();
// Default user agent for the target. Required, as emulating client hints without user
// agent is not possible. Cache it to avoid round trips to the browser for every target override.
#defaultUserAgent;
#logger;
/**
* Target's window id. Is filled when the CDP target is created and do not reflect
* moving targets from one window to another. The actual values
* will be set during `#unblock`.
* */
#windowId;
#deviceAccessEnabled = false;
#cacheDisableState = false;
#preloadEnabled = false;
#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
static create(targetId, cdpClient, browserCdpClient, parentCdpClient, realmStorage, eventManager, preloadScriptStorage, browsingContextStorage, networkStorage, configStorage, userContext, defaultUserAgent, logger) {
const cdpTarget = new CdpTarget(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger);
LogManager_js_1.LogManager.create(cdpTarget, realmStorage, eventManager, logger);
cdpTarget.#setEventListeners();
// No need to await.
// Deferred will be resolved when the target is unblocked.
void cdpTarget.#unblock();
return cdpTarget;
}
constructor(targetId, cdpClient, browserCdpClient, parentCdpClient, eventManager, realmStorage, preloadScriptStorage, browsingContextStorage, configStorage, networkStorage, userContext, defaultUserAgent, logger) {
this.#defaultUserAgent = defaultUserAgent;
this.userContext = userContext;
this.#id = targetId;
this.#cdpClient = cdpClient;
this.#browserCdpClient = browserCdpClient;
this.#parentCdpClient = parentCdpClient;
this.#eventManager = eventManager;
this.#realmStorage = realmStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#browsingContextStorage = browsingContextStorage;
this.contextConfigStorage = configStorage;
this.#logger = logger;
}
/** Returns a deferred that resolves when the target is unblocked. */
get unblocked() {
return this.#unblocked;
}
get id() {
return this.#id;
}
get cdpClient() {
return this.#cdpClient;
}
get parentCdpClient() {
return this.#parentCdpClient;
}
get browserCdpClient() {
return this.#browserCdpClient;
}
/** Needed for CDP escape path. */
get cdpSessionId() {
// SAFETY we got the client by it's id for creating
return this.#cdpClient.sessionId;
}
/**
* Window id the target belongs to. If not known, returns 0.
*/
get windowId() {
if (this.#windowId === undefined) {
this.#logger?.(log_js_1.LogType.debugError, 'Getting windowId before it was set, returning 0');
}
return this.#windowId ?? 0;
}
/**
* Enables all the required CDP domains and unblocks the target.
*/
async #unblock() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
const results = await Promise.allSettled([
this.#cdpClient.sendCommand('Page.enable', {
enableFileChooserOpenedEvent: true,
}),
...(this.#ignoreFileDialog()
? []
: [
this.#cdpClient.sendCommand('Page.setInterceptFileChooserDialog', {
enabled: true,
// The intercepted dialog should be canceled.
cancel: true,
}),
]),
// There can be some existing frames in the target, if reconnecting to an
// existing browser instance, e.g. via Puppeteer. Need to restore the browsing
// contexts for the frames to correctly handle further events, like
// `Runtime.executionContextCreated`.
// It's important to schedule this task together with enabling domains commands to
// prepare the tree before the events (e.g. Runtime.executionContextCreated) start
// coming.
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2282
this.#cdpClient
.sendCommand('Page.getFrameTree')
.then((frameTree) => this.#restoreFrameTreeState(frameTree.frameTree)),
this.#cdpClient.sendCommand('Runtime.enable'),
this.#cdpClient.sendCommand('Page.setLifecycleEventsEnabled', {
enabled: true,
}),
// Enabling CDP Network domain is required for navigation detection:
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/2856.
this.#cdpClient
.sendCommand('Network.enable', {
// If `googDisableNetworkDurableMessages` flag is set, do not enable durable
// messages.
enableDurableMessages: config.disableNetworkDurableMessages !== true,
maxTotalBufferSize: NetworkStorage_js_1.MAX_TOTAL_COLLECTED_SIZE,
})
.then(() => this.toggleNetworkIfNeeded()),
this.#cdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
}),
this.#updateWindowId(),
this.#setUserContextConfig(config),
this.#initAndEvaluatePreloadScripts(),
this.#cdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
// Resume tab execution as well if it was paused by the debugger.
this.#parentCdpClient.sendCommand('Runtime.runIfWaitingForDebugger'),
this.toggleDeviceAccessIfNeeded(),
this.togglePreloadIfNeeded(),
]);
for (const result of results) {
if (result instanceof Error) {
// Ignore errors during configuring targets, just log them.
this.#logger?.(log_js_1.LogType.debugError, 'Error happened when configuring a new target', result);
}
}
this.#unblocked.resolve({
kind: 'success',
value: undefined,
});
}
#restoreFrameTreeState(frameTree) {
const frame = frameTree.frame;
const maybeContext = this.#browsingContextStorage.findContext(frame.id);
if (maybeContext !== undefined) {
// Restoring parent of already known browsing context. This means the target is
// OOPiF and the BiDi session was connected to already existing browser instance.
if (maybeContext.parentId === null &&
frame.parentId !== null &&
frame.parentId !== undefined) {
maybeContext.parentId = frame.parentId;
}
}
if (maybeContext === undefined && frame.parentId !== undefined) {
// Restore not yet known nested frames. The top-level frame is created when the
// target is attached.
const parentBrowsingContext = this.#browsingContextStorage.getContext(frame.parentId);
BrowsingContextImpl_js_1.BrowsingContextImpl.create(frame.id, frame.parentId, this.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.contextConfigStorage, frame.url, undefined, this.#logger);
}
frameTree.childFrames?.map((frameTree) => this.#restoreFrameTreeState(frameTree));
}
async toggleFetchIfNeeded() {
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
if (this.#fetchDomainStages.request === stages.request &&
this.#fetchDomainStages.response === stages.response &&
this.#fetchDomainStages.auth === stages.auth) {
return;
}
const patterns = [];
this.#fetchDomainStages = stages;
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
else {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
void Promise.allSettled(blockedRequest.map((request) => request.waitNextPhase))
.then(async () => {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length) {
return await this.toggleFetchIfNeeded();
}
return await this.#cdpClient.sendCommand('Fetch.disable');
})
.catch((error) => {
this.#logger?.(log_js_1.LogType.bidi, 'Disable failed', error);
});
}
}
/**
* Toggles CDP "Fetch" domain and enable/disable network cache.
*/
async toggleNetworkIfNeeded() {
// Although the Network domain remains active, Fetch domain activation and caching
// settings should be managed dynamically.
try {
await Promise.all([
this.toggleSetCacheDisabled(),
this.toggleFetchIfNeeded(),
]);
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleSetCacheDisabled(disable) {
const defaultCacheDisabled = this.#networkStorage.defaultCacheBehavior === 'bypass';
const cacheDisabled = disable ?? defaultCacheDisabled;
if (this.#cacheDisableState === cacheDisabled) {
return;
}
this.#cacheDisableState = cacheDisabled;
try {
await this.#cdpClient.sendCommand('Network.setCacheDisabled', {
cacheDisabled,
});
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#cacheDisableState = !cacheDisabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async toggleDeviceAccessIfNeeded() {
const enabled = this.isSubscribedTo(chromium_bidi_js_1.Bluetooth.EventNames.RequestDevicePromptUpdated);
if (this.#deviceAccessEnabled === enabled) {
return;
}
this.#deviceAccessEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'DeviceAccess.enable' : 'DeviceAccess.disable');
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#deviceAccessEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
async togglePreloadIfNeeded() {
const enabled = this.isSubscribedTo(chromium_bidi_js_1.Speculation.EventNames.PrefetchStatusUpdated);
if (this.#preloadEnabled === enabled) {
return;
}
this.#preloadEnabled = enabled;
try {
await this.#cdpClient.sendCommand(enabled ? 'Preload.enable' : 'Preload.disable');
}
catch (err) {
this.#logger?.(log_js_1.LogType.debugError, err);
this.#preloadEnabled = !enabled;
if (!this.#isExpectedError(err)) {
throw err;
}
}
}
/**
* Heuristic checking if the error is due to the session being closed. If so, ignore the
* error.
*/
#isExpectedError(err) {
const error = err;
return ((error.code === -32001 &&
error.message === 'Session with given id not found.') ||
this.#cdpClient.isCloseError(err));
}
#setEventListeners() {
this.#cdpClient.on('*', (event, params) => {
// We may encounter uses for EventEmitter other than CDP events,
// which we want to skip.
if (typeof event !== 'string') {
return;
}
this.#eventManager.registerEvent({
type: 'event',
method: `goog:cdp.${event}`,
params: {
event,
params,
session: this.cdpSessionId,
},
}, this.id);
});
}
async #enableFetch(stages) {
const patterns = [];
if (stages.request || stages.auth) {
// CDP quirk we need request interception when we intercept auth
patterns.push({
urlPattern: '*',
requestStage: 'Request',
});
}
if (stages.response) {
patterns.push({
urlPattern: '*',
requestStage: 'Response',
});
}
if (patterns.length) {
const oldStages = this.#fetchDomainStages;
this.#fetchDomainStages = stages;
try {
await this.#cdpClient.sendCommand('Fetch.enable', {
patterns,
handleAuthRequests: stages.auth,
});
}
catch {
this.#fetchDomainStages = oldStages;
}
}
}
async #disableFetch() {
const blockedRequest = this.#networkStorage
.getRequestsByTarget(this)
.filter((request) => request.interceptPhase);
if (blockedRequest.length === 0) {
this.#fetchDomainStages = {
request: false,
response: false,
auth: false,
};
await this.#cdpClient.sendCommand('Fetch.disable');
}
}
async toggleNetwork() {
// TODO: respect the data collectors once CDP Network domain is enabled on-demand:
// const networkEnable = this.#networkStorage.getCollectorsForBrowsingContext(this.topLevelId).length > 0;
const stages = this.#networkStorage.getInterceptionStages(this.topLevelId);
const fetchEnable = Object.values(stages).some((value) => value);
const fetchChanged = this.#fetchDomainStages.request !== stages.request ||
this.#fetchDomainStages.response !== stages.response ||
this.#fetchDomainStages.auth !== stages.auth;
this.#logger?.(log_js_1.LogType.debugInfo, 'Toggle Network', `Fetch (${fetchEnable}) ${fetchChanged}`);
if (fetchEnable && fetchChanged) {
await this.#enableFetch(stages);
}
if (!fetchEnable && fetchChanged) {
await this.#disableFetch();
}
}
/**
* All the ProxyChannels from all the preload scripts of the given
* BrowsingContext.
*/
getChannels() {
return this.#preloadScriptStorage
.find()
.flatMap((script) => script.channels);
}
async #updateWindowId() {
const { windowId } = await this.#browserCdpClient.sendCommand('Browser.getWindowForTarget', { targetId: this.id });
this.#windowId = windowId;
}
/** Loads all top-level preload scripts. */
async #initAndEvaluatePreloadScripts() {
await Promise.all(this.#preloadScriptStorage
.find({
// Needed for OOPIF
targetId: this.topLevelId,
})
.map((script) => {
return script.initInTarget(this, true);
}));
}
async setDeviceMetricsOverride(viewport, devicePixelRatio, screenOrientation, screenArea) {
if (viewport === null &&
devicePixelRatio === null &&
screenOrientation === null &&
screenArea === null) {
await this.cdpClient.sendCommand('Emulation.clearDeviceMetricsOverride');
return;
}
const metricsOverride = {
width: viewport?.width ?? 0,
height: viewport?.height ?? 0,
deviceScaleFactor: devicePixelRatio ?? 0,
screenOrientation: this.#toCdpScreenOrientationAngle(screenOrientation) ?? undefined,
mobile: false,
screenWidth: screenArea?.width,
screenHeight: screenArea?.height,
};
await this.cdpClient.sendCommand('Emulation.setDeviceMetricsOverride', metricsOverride);
}
/**
* Immediately schedules all the required commands to configure user context
* configuration and waits for them to finish. It's important to schedule them
* in parallel, so that they are enqueued before any page's scripts.
*/
async #setUserContextConfig(config) {
const promises = [];
promises.push(this.#cdpClient
.sendCommand('Page.setPrerenderingAllowed', {
isAllowed: !config.prerenderingDisabled,
})
.catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets or
// prerendered pages. Generic catch, as the error can vary between CdpClient
// implementations: Tab vs Puppeteer.
}));
if (config.viewport !== undefined ||
config.devicePixelRatio !== undefined ||
config.screenOrientation !== undefined ||
config.screenArea !== undefined) {
promises.push(this.setDeviceMetricsOverride(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null, config.screenArea ?? null).catch(() => {
// Ignore CDP errors, as the command is not supported by iframe targets. Generic
// catch, as the error can vary between CdpClient implementations: Tab vs
// Puppeteer.
}));
}
if (config.geolocation !== undefined && config.geolocation !== null) {
promises.push(this.setGeolocationOverride(config.geolocation));
}
if (config.locale !== undefined) {
promises.push(this.setLocaleOverride(config.locale));
}
if (config.timezone !== undefined) {
promises.push(this.setTimezoneOverride(config.timezone));
}
if (config.extraHeaders !== undefined) {
promises.push(this.setExtraHeaders(config.extraHeaders));
}
if (config.userAgent !== undefined ||
config.locale !== undefined ||
config.clientHints !== undefined) {
promises.push(this.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints));
}
if (config.scriptingEnabled !== undefined) {
promises.push(this.setScriptingEnabled(config.scriptingEnabled));
}
if (config.acceptInsecureCerts !== undefined) {
promises.push(this.cdpClient.sendCommand('Security.setIgnoreCertificateErrors', {
ignore: config.acceptInsecureCerts,
}));
}
if (config.emulatedNetworkConditions !== undefined) {
promises.push(this.setEmulatedNetworkConditions(config.emulatedNetworkConditions));
}
if (config.maxTouchPoints !== undefined) {
promises.push(this.setTouchOverride(config.maxTouchPoints));
}
await Promise.all(promises);
}
get topLevelId() {
return (this.#browsingContextStorage.findTopLevelContextId(this.id) ?? this.id);
}
isSubscribedTo(moduleOrEvent) {
return this.#eventManager.subscriptionManager.isSubscribedTo(moduleOrEvent, this.topLevelId);
}
#ignoreFileDialog() {
const config = this.contextConfigStorage.getActiveConfig(this.topLevelId, this.userContext);
return ((config.userPromptHandler?.file ??
config.userPromptHandler?.default ??
"ignore" /* Session.UserPromptHandlerType.Ignore */) ===
"ignore" /* Session.UserPromptHandlerType.Ignore */);
}
async setGeolocationOverride(geolocation) {
if (geolocation === null) {
await this.cdpClient.sendCommand('Emulation.clearGeolocationOverride');
}
else if ('type' in geolocation) {
if (geolocation.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new protocol_js_1.UnknownErrorException(`Unknown geolocation error ${geolocation.type}`);
}
// Omitting latitude, longitude or accuracy emulates position unavailable.
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {});
}
else if ('latitude' in geolocation) {
await this.cdpClient.sendCommand('Emulation.setGeolocationOverride', {
latitude: geolocation.latitude,
longitude: geolocation.longitude,
accuracy: geolocation.accuracy ?? 1,
// `null` value is treated as "missing".
altitude: geolocation.altitude ?? undefined,
altitudeAccuracy: geolocation.altitudeAccuracy ?? undefined,
heading: geolocation.heading ?? undefined,
speed: geolocation.speed ?? undefined,
});
}
else {
// Unreachable. Handled by params parser.
throw new protocol_js_1.UnknownErrorException('Unexpected geolocation coordinates value');
}
}
async setTouchOverride(maxTouchPoints) {
const touchEmulationParams = {
enabled: maxTouchPoints !== null,
};
if (maxTouchPoints !== null) {
touchEmulationParams.maxTouchPoints = maxTouchPoints;
}
await this.cdpClient.sendCommand('Emulation.setTouchEmulationEnabled', touchEmulationParams);
}
#toCdpScreenOrientationAngle(orientation) {
if (orientation === null) {
return null;
}
// https://w3c.github.io/screen-orientation/#the-current-screen-orientation-type-and-angle
if (orientation.natural === "portrait" /* Emulation.ScreenOrientationNatural.Portrait */) {
switch (orientation.type) {
case 'portrait-primary':
return {
angle: 0,
type: 'portraitPrimary',
};
case 'landscape-primary':
return {
angle: 90,
type: 'landscapePrimary',
};
case 'portrait-secondary':
return {
angle: 180,
type: 'portraitSecondary',
};
case 'landscape-secondary':
return {
angle: 270,
type: 'landscapeSecondary',
};
default:
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
if (orientation.natural === "landscape" /* Emulation.ScreenOrientationNatural.Landscape */) {
switch (orientation.type) {
case 'landscape-primary':
return {
angle: 0,
type: 'landscapePrimary',
};
case 'portrait-primary':
return {
angle: 90,
type: 'portraitPrimary',
};
case 'landscape-secondary':
return {
angle: 180,
type: 'landscapeSecondary',
};
case 'portrait-secondary':
return {
angle: 270,
type: 'portraitSecondary',
};
default:
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected screen orientation type ${orientation.type}`);
}
}
// Unreachable.
throw new protocol_js_1.UnknownErrorException(`Unexpected orientation natural ${orientation.natural}`);
}
async setLocaleOverride(locale) {
if (locale === null) {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {});
}
else {
await this.cdpClient.sendCommand('Emulation.setLocaleOverride', {
locale,
});
}
}
async setScriptingEnabled(scriptingEnabled) {
await this.cdpClient.sendCommand('Emulation.setScriptExecutionDisabled', {
value: scriptingEnabled === false,
});
}
async setTimezoneOverride(timezone) {
if (timezone === null) {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
// If empty, disables the override and restores default host system timezone.
timezoneId: '',
});
}
else {
await this.cdpClient.sendCommand('Emulation.setTimezoneOverride', {
timezoneId: timezone,
});
}
}
async setExtraHeaders(headers) {
await this.cdpClient.sendCommand('Network.setExtraHTTPHeaders', {
headers,
});
}
async setUserAgentAndAcceptLanguage(userAgent, acceptLanguage, clientHints) {
const userAgentMetadata = clientHints
? {
brands: clientHints.brands?.map((b) => ({
brand: b.brand,
version: b.version,
})),
fullVersionList: clientHints.fullVersionList,
platform: clientHints.platform ?? '',
platformVersion: clientHints.platformVersion ?? '',
architecture: clientHints.architecture ?? '',
model: clientHints.model ?? '',
mobile: clientHints.mobile ?? false,
bitness: clientHints.bitness ?? undefined,
wow64: clientHints.wow64 ?? undefined,
formFactors: clientHints.formFactors ?? undefined,
}
: undefined;
await this.cdpClient.sendCommand('Emulation.setUserAgentOverride', {
// `userAgent` is required if `userAgentMetadata` is provided.
userAgent: userAgent || (userAgentMetadata ? this.#defaultUserAgent : ''),
acceptLanguage: acceptLanguage ?? undefined,
// We need to provide the platform to enable platform emulation.
// Note that the value might be different from the one expected by the
// legacy `navigator.platform` (e.g. `Win32` vs `Windows`).
// https://github.com/w3c/webdriver-bidi/issues/1065
platform: clientHints?.platform ?? undefined,
userAgentMetadata,
});
}
async setEmulatedNetworkConditions(networkConditions) {
if (networkConditions !== null && networkConditions.type !== 'offline') {
throw new protocol_js_1.UnsupportedOperationException(`Unsupported network conditions ${networkConditions.type}`);
}
await Promise.all([
this.cdpClient.sendCommand('Network.emulateNetworkConditionsByRule', {
offline: networkConditions?.type === 'offline',
matchedNetworkConditions: [
{
urlPattern: '',
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
},
],
}),
this.cdpClient.sendCommand('Network.overrideNetworkState', {
offline: networkConditions?.type === 'offline',
// TODO: restore the original `latency` value when emulation is removed.
latency: 0,
downloadThroughput: -1,
uploadThroughput: -1,
}),
]);
}
}
exports.CdpTarget = CdpTarget;
//# sourceMappingURL=CdpTarget.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,16 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import type { CdpConnection } from '../../../cdp/CdpConnection.js';
import type { Browser } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { BluetoothProcessor } from '../bluetooth/BluetoothProcessor.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from '../network/NetworkStorage.js';
import type { PreloadScriptStorage } from '../script/PreloadScriptStorage.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { SpeculationProcessor } from '../speculation/SpeculationProcessor.js';
export declare class CdpTargetManager {
#private;
constructor(cdpConnection: CdpConnection, browserCdpClient: CdpClient, selfTargetId: string, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, networkStorage: NetworkStorage, configStorage: ContextConfigStorage, bluetoothProcessor: BluetoothProcessor, speculationProcessor: SpeculationProcessor, preloadScriptStorage: PreloadScriptStorage, defaultUserContextId: Browser.UserContext, defaultUserAgent: string, logger?: LoggerFn);
}

View File

@@ -0,0 +1,252 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CdpTargetManager = void 0;
const log_js_1 = require("../../../utils/log.js");
const BrowsingContextImpl_js_1 = require("../context/BrowsingContextImpl.js");
const WorkerRealm_js_1 = require("../script/WorkerRealm.js");
const CdpTarget_js_1 = require("./CdpTarget.js");
const cdpToBidiTargetTypes = {
service_worker: 'service-worker',
shared_worker: 'shared-worker',
worker: 'dedicated-worker',
};
class CdpTargetManager {
#browserCdpClient;
#cdpConnection;
#targetKeysToBeIgnoredByAutoAttach = new Set();
#selfTargetId;
#eventManager;
#browsingContextStorage;
#networkStorage;
#bluetoothProcessor;
#preloadScriptStorage;
#realmStorage;
#configStorage;
#speculationProcessor;
#defaultUserContextId;
#defaultUserAgent;
#logger;
constructor(cdpConnection, browserCdpClient, selfTargetId, eventManager, browsingContextStorage, realmStorage, networkStorage, configStorage, bluetoothProcessor, speculationProcessor, preloadScriptStorage, defaultUserContextId, defaultUserAgent, logger) {
this.#cdpConnection = cdpConnection;
this.#browserCdpClient = browserCdpClient;
this.#targetKeysToBeIgnoredByAutoAttach.add(selfTargetId);
this.#selfTargetId = selfTargetId;
this.#eventManager = eventManager;
this.#browsingContextStorage = browsingContextStorage;
this.#preloadScriptStorage = preloadScriptStorage;
this.#networkStorage = networkStorage;
this.#configStorage = configStorage;
this.#bluetoothProcessor = bluetoothProcessor;
this.#speculationProcessor = speculationProcessor;
this.#realmStorage = realmStorage;
this.#defaultUserContextId = defaultUserContextId;
this.#defaultUserAgent = defaultUserAgent;
this.#logger = logger;
this.#setEventListeners(browserCdpClient);
}
/**
* This method is called for each CDP session, since this class is responsible
* for creating and destroying all targets and browsing contexts.
*/
#setEventListeners(cdpClient) {
cdpClient.on('Target.attachedToTarget', (params) => {
this.#handleAttachedToTargetEvent(params, cdpClient);
});
cdpClient.on('Target.detachedFromTarget', this.#handleDetachedFromTargetEvent.bind(this));
cdpClient.on('Target.targetInfoChanged', this.#handleTargetInfoChangedEvent.bind(this));
cdpClient.on('Inspector.targetCrashed', () => {
this.#handleTargetCrashedEvent(cdpClient);
});
cdpClient.on('Page.frameAttached', this.#handleFrameAttachedEvent.bind(this));
cdpClient.on('Page.frameSubtreeWillBeDetached', this.#handleFrameSubtreeWillBeDetached.bind(this));
}
#handleFrameAttachedEvent(params) {
const parentBrowsingContext = this.#browsingContextStorage.findContext(params.parentFrameId);
if (parentBrowsingContext !== undefined) {
BrowsingContextImpl_js_1.BrowsingContextImpl.create(params.frameId, params.parentFrameId, parentBrowsingContext.userContext, parentBrowsingContext.cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// At this point, we don't know the URL of the frame yet, so it will be updated
// later.
'about:blank', undefined, this.#logger);
}
}
#handleFrameSubtreeWillBeDetached(params) {
this.#browsingContextStorage.findContext(params.frameId)?.dispose(true);
}
#handleAttachedToTargetEvent(params, parentSessionCdpClient) {
const { sessionId, targetInfo } = params;
const targetCdpClient = this.#cdpConnection.getCdpClient(sessionId);
const detach = async () => {
// Detaches and resumes the target suppressing errors.
await targetCdpClient
.sendCommand('Runtime.runIfWaitingForDebugger')
.then(() => parentSessionCdpClient.sendCommand('Target.detachFromTarget', params))
.catch((error) => this.#logger?.(log_js_1.LogType.debugError, error));
};
// Do not attach to the Mapper target.
if (this.#selfTargetId === targetInfo.targetId) {
void detach();
return;
}
// Service workers are special case because they attach to the
// browser target and the page target (so twice per worker) during
// the regular auto-attach and might hang if the CDP session on
// the browser level is not detached. The logic to detach the
// right session is handled in the switch below.
const targetKey = targetInfo.type === 'service_worker'
? `${parentSessionCdpClient.sessionId}_${targetInfo.targetId}`
: targetInfo.targetId;
// Mapper generally only needs one session per target. If we
// receive additional auto-attached sessions, that is very likely
// coming from custom CDP sessions.
if (this.#targetKeysToBeIgnoredByAutoAttach.has(targetKey)) {
// Return to leave the session untouched.
return;
}
this.#targetKeysToBeIgnoredByAutoAttach.add(targetKey);
const userContext = targetInfo.browserContextId &&
targetInfo.browserContextId !== this.#defaultUserContextId
? targetInfo.browserContextId
: 'default';
switch (targetInfo.type) {
case 'tab': {
// Tab targets are required only to handle page targets beneath them.
this.#setEventListeners(targetCdpClient);
// Auto-attach to the page target. No need in resuming tab target debugger, as it
// should preserve the page target debugger state, and will be resumed by the page
// target.
void (async () => {
await targetCdpClient.sendCommand('Target.setAutoAttach', {
autoAttach: true,
waitForDebuggerOnStart: true,
flatten: true,
});
})();
return;
}
case 'page':
case 'iframe': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
const maybeContext = this.#browsingContextStorage.findContext(targetInfo.targetId);
if (maybeContext && targetInfo.type === 'iframe') {
// OOPiF.
maybeContext.updateCdpTarget(cdpTarget);
}
else {
// If attaching to existing browser instance, there could be OOPiF targets. This
// case is handled by the `findFrameParentId` method.
const parentId = this.#findFrameParentId(targetInfo, parentSessionCdpClient.sessionId);
// New context.
BrowsingContextImpl_js_1.BrowsingContextImpl.create(targetInfo.targetId, parentId, userContext, cdpTarget, this.#eventManager, this.#browsingContextStorage, this.#realmStorage, this.#configStorage,
// Hack: when a new target created, CDP emits targetInfoChanged with an empty
// url, and navigates it to about:blank later. When the event is emitted for
// an existing target (reconnect), the url is already known, and navigation
// events will not be emitted anymore. Replacing empty url with `about:blank`
// allows to handle both cases in the same way.
// "7.3.2.1 Creating browsing contexts".
// https://html.spec.whatwg.org/multipage/document-sequences.html#creating-browsing-contexts
// TODO: check who to deal with non-null creator and its `creatorOrigin`.
targetInfo.url === '' ? 'about:blank' : targetInfo.url, targetInfo.openerFrameId ?? targetInfo.openerId, this.#logger);
}
return;
}
case 'service_worker':
case 'worker': {
const realm = this.#realmStorage.findRealm({
cdpSessionId: parentSessionCdpClient.sessionId,
sandbox: null, // Non-sandboxed realms.
});
// If there is no browsing context, this worker is already terminated.
if (!realm) {
void detach();
return;
}
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget, realm);
return;
}
// In CDP, we only emit shared workers on the browser and not the set of
// frames that use the shared worker. If we change this in the future to
// behave like service workers (emits on both browser and frame targets),
// we can remove this block and merge service workers with the above one.
case 'shared_worker': {
const cdpTarget = this.#createCdpTarget(targetCdpClient, parentSessionCdpClient, targetInfo, userContext);
this.#handleWorkerTarget(cdpToBidiTargetTypes[targetInfo.type], cdpTarget);
return;
}
}
// DevTools or some other not supported by BiDi target. Just release
// debugger and ignore them.
void detach();
}
/** Try to find the parent browsing context ID for the given attached target. */
#findFrameParentId(targetInfo, parentSessionId) {
if (targetInfo.type !== 'iframe') {
return null;
}
const parentId = targetInfo.openerFrameId ?? targetInfo.openerId;
if (parentId !== undefined) {
return parentId;
}
if (parentSessionId !== undefined) {
return (this.#browsingContextStorage.findContextBySession(parentSessionId)
?.id ?? null);
}
return null;
}
#createCdpTarget(targetCdpClient, parentCdpClient, targetInfo, userContext) {
this.#setEventListeners(targetCdpClient);
this.#preloadScriptStorage.onCdpTargetCreated(targetInfo.targetId, userContext);
const target = CdpTarget_js_1.CdpTarget.create(targetInfo.targetId, targetCdpClient, this.#browserCdpClient, parentCdpClient, this.#realmStorage, this.#eventManager, this.#preloadScriptStorage, this.#browsingContextStorage, this.#networkStorage, this.#configStorage, userContext,
// Pass the cached default User Agent to the new target.
this.#defaultUserAgent, this.#logger);
this.#networkStorage.onCdpTargetCreated(target);
this.#bluetoothProcessor.onCdpTargetCreated(target);
this.#speculationProcessor.onCdpTargetCreated(target);
return target;
}
#workers = new Map();
#handleWorkerTarget(realmType, cdpTarget, ownerRealm) {
cdpTarget.cdpClient.on('Runtime.executionContextCreated', (params) => {
const { uniqueId, id, origin } = params.context;
const workerRealm = new WorkerRealm_js_1.WorkerRealm(cdpTarget.cdpClient, this.#eventManager, id, this.#logger, (0, BrowsingContextImpl_js_1.serializeOrigin)(origin), ownerRealm ? [ownerRealm] : [], uniqueId, this.#realmStorage, realmType);
this.#workers.set(cdpTarget.cdpSessionId, workerRealm);
});
}
#handleDetachedFromTargetEvent({ sessionId, targetId, }) {
if (targetId) {
this.#preloadScriptStorage.find({ targetId }).map((preloadScript) => {
preloadScript.dispose(targetId);
});
}
const context = this.#browsingContextStorage.findContextBySession(sessionId);
if (context) {
context.dispose(true);
return;
}
const worker = this.#workers.get(sessionId);
if (worker) {
this.#realmStorage.deleteRealms({
cdpSessionId: worker.cdpClient.sessionId,
});
}
}
#handleTargetInfoChangedEvent(params) {
const context = this.#browsingContextStorage.findContext(params.targetInfo.targetId);
if (context) {
context.onTargetInfoChanged(params);
}
}
#handleTargetCrashedEvent(cdpClient) {
// This is primarily used for service and shared workers. CDP tends to not
// signal they closed gracefully and instead says they crashed to signal
// they are closed.
const realms = this.#realmStorage.findRealms({
cdpSessionId: cdpClient.sessionId,
});
for (const realm of realms) {
realm.dispose();
}
}
}
exports.CdpTargetManager = CdpTargetManager;
//# sourceMappingURL=CdpTargetManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,91 @@
/**
* Copyright 2022 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 type { Protocol } from 'devtools-protocol';
import { BrowsingContext, type Emulation, type UAClientHints } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { Realm } from '../script/Realm.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextImpl {
#private;
static readonly LOGGER_PREFIX: "debug:browsingContext";
readonly userContext: string;
private constructor();
static create(id: BrowsingContext.BrowsingContext, parentId: BrowsingContext.BrowsingContext | null, userContext: string, cdpTarget: CdpTarget, eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, realmStorage: RealmStorage, configStorage: ContextConfigStorage, url: string, originalOpener?: string, logger?: LoggerFn): BrowsingContextImpl;
/**
* @see https://html.spec.whatwg.org/multipage/document-sequences.html#navigable
*/
get navigableId(): string | undefined;
get navigationId(): string;
dispose(emitContextDestroyed: boolean): void;
/** Returns the ID of this context. */
get id(): BrowsingContext.BrowsingContext;
/** Returns the parent context ID. */
get parentId(): BrowsingContext.BrowsingContext | null;
/** Sets the parent context ID and updates parent's children. */
set parentId(parentId: BrowsingContext.BrowsingContext | null);
/** Returns the parent context. */
get parent(): BrowsingContextImpl | null;
/** Returns all direct children contexts. */
get directChildren(): BrowsingContextImpl[];
/** Returns all children contexts, flattened. */
get allChildren(): BrowsingContextImpl[];
/**
* Returns true if this is a top-level context.
* This is the case whenever the parent context ID is null.
*/
isTopLevelContext(): boolean;
get top(): BrowsingContextImpl;
addChild(childId: BrowsingContext.BrowsingContext): void;
get cdpTarget(): CdpTarget;
updateCdpTarget(cdpTarget: CdpTarget): void;
get url(): string;
lifecycleLoaded(): Promise<void>;
targetUnblockedOrThrow(): Promise<void>;
/** Returns a sandbox for internal helper scripts which is not exposed to the user.*/
getOrCreateHiddenSandbox(): Promise<Realm>;
/** Returns a sandbox which is exposed to user. */
getOrCreateUserSandbox(sandbox: string | undefined): Promise<Realm>;
/**
* Implements https://w3c.github.io/webdriver-bidi/#get-the-navigable-info.
*/
serializeToBidiValue(maxDepth?: number | null, addParentField?: boolean): BrowsingContext.Info;
onTargetInfoChanged(params: Protocol.Target.TargetInfoChangedEvent): void;
navigate(url: string, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
reload(ignoreCache: boolean, wait: BrowsingContext.ReadinessState): Promise<BrowsingContext.NavigateResult>;
setViewport(viewport: BrowsingContext.Viewport | null, devicePixelRatio: number | null, screenOrientation: Emulation.ScreenOrientation | null): Promise<void>;
handleUserPrompt(accept?: boolean, userText?: string): Promise<void>;
activate(): Promise<void>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
close(): Promise<void>;
traverseHistory(delta: number): Promise<void>;
toggleModulesIfNeeded(): Promise<void>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
setTimezoneOverride(timezone: string | null): Promise<void>;
setLocaleOverride(locale: string | null): Promise<void>;
setGeolocationOverride(geolocation: Emulation.GeolocationCoordinates | Emulation.GeolocationPositionError | null): Promise<void>;
setScriptingEnabled(scriptingEnabled: false | null): Promise<void>;
setUserAgentAndAcceptLanguage(userAgent: string | null | undefined, acceptLanguage: string | null | undefined, clientHints: UAClientHints.Emulation.ClientHintsMetadata | null | undefined): Promise<void>;
setEmulatedNetworkConditions(networkConditions: Emulation.NetworkConditions | null): Promise<void>;
setTouchOverride(maxTouchPoints: number | null): Promise<void>;
setExtraHeaders(cdpExtraHeaders: Protocol.Network.Headers): Promise<Promise<any>>;
}
export declare function serializeOrigin(origin: string): string;

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { BrowsingContext, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import type { BrowsingContextStorage } from './BrowsingContextStorage.js';
export declare class BrowsingContextProcessor {
#private;
constructor(browserCdpClient: CdpClient, browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage, eventManager: EventManager);
getTree(params: BrowsingContext.GetTreeParameters): BrowsingContext.GetTreeResult;
create(params: BrowsingContext.CreateParameters): Promise<BrowsingContext.CreateResult>;
navigate(params: BrowsingContext.NavigateParameters): Promise<BrowsingContext.NavigateResult>;
reload(params: BrowsingContext.ReloadParameters): Promise<EmptyResult>;
activate(params: BrowsingContext.ActivateParameters): Promise<EmptyResult>;
captureScreenshot(params: BrowsingContext.CaptureScreenshotParameters): Promise<BrowsingContext.CaptureScreenshotResult>;
print(params: BrowsingContext.PrintParameters): Promise<BrowsingContext.PrintResult>;
setViewport(params: BrowsingContext.SetViewportParameters): Promise<EmptyResult>;
traverseHistory(params: BrowsingContext.TraverseHistoryParameters): Promise<BrowsingContext.TraverseHistoryResult>;
handleUserPrompt(params: BrowsingContext.HandleUserPromptParameters): Promise<EmptyResult>;
close(params: BrowsingContext.CloseParameters): Promise<EmptyResult>;
locateNodes(params: BrowsingContext.LocateNodesParameters): Promise<BrowsingContext.LocateNodesResult>;
}

View File

@@ -0,0 +1,267 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingContextProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
class BrowsingContextProcessor {
#browserCdpClient;
#browsingContextStorage;
#contextConfigStorage;
#eventManager;
#userContextStorage;
constructor(browserCdpClient, browsingContextStorage, userContextStorage, contextConfigStorage, eventManager) {
this.#contextConfigStorage = contextConfigStorage;
this.#userContextStorage = userContextStorage;
this.#browserCdpClient = browserCdpClient;
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
this.#eventManager.addSubscribeHook(protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.ContextCreated, this.#onContextCreatedSubscribeHook.bind(this));
}
getTree(params) {
const resultContexts = params.root === undefined
? this.#browsingContextStorage.getTopLevelContexts()
: [this.#browsingContextStorage.getContext(params.root)];
return {
contexts: resultContexts.map((c) => c.serializeToBidiValue(params.maxDepth ?? Number.MAX_VALUE)),
};
}
async create(params) {
let referenceContext;
let userContext = 'default';
if (params.referenceContext !== undefined) {
referenceContext = this.#browsingContextStorage.getContext(params.referenceContext);
if (!referenceContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException(`referenceContext should be a top-level context`);
}
userContext = referenceContext.userContext;
}
if (params.userContext !== undefined) {
userContext = params.userContext;
}
const existingContexts = this.#browsingContextStorage
.getAllContexts()
.filter((context) => context.userContext === userContext);
let newWindow = false;
switch (params.type) {
case "tab" /* BrowsingContext.CreateType.Tab */:
newWindow = false;
break;
case "window" /* BrowsingContext.CreateType.Window */:
newWindow = true;
break;
}
if (!existingContexts.length) {
// If there are no contexts in the given user context, we need to set
// newWindow to true as newWindow=false will be rejected.
newWindow = true;
}
let result;
try {
result = await this.#browserCdpClient.sendCommand('Target.createTarget', {
url: 'about:blank',
newWindow,
browserContextId: userContext === 'default' ? undefined : userContext,
background: params.background === true,
});
}
catch (err) {
if (
// See https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/devtools/protocol/target_handler.cc;l=90;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message.startsWith('Failed to find browser context with id') ||
// See https://source.chromium.org/chromium/chromium/src/+/main:headless/lib/browser/protocol/target_handler.cc;l=49;drc=e80392ac11e48a691f4309964cab83a3a59e01c8
err.message === 'browserContextId') {
throw new protocol_js_1.NoSuchUserContextException(`The context ${userContext} was not found`);
}
throw err;
}
// Wait for the new target to be attached and to be added to the browsing context
// storage.
const context = await this.#browsingContextStorage.waitForContext(result.targetId);
// Wait for the new tab to be loaded to avoid race conditions in the
// `browsingContext` events, when the `browsingContext.domContentLoaded` and
// `browsingContext.load` events from the initial `about:blank` navigation
// are emitted after the next navigation is started.
// Details: https://github.com/web-platform-tests/wpt/issues/35846
await context.lifecycleLoaded();
return { context: context.id };
}
navigate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.navigate(params.url, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
reload(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return context.reload(params.ignoreCache ?? false, params.wait ?? "none" /* BrowsingContext.ReadinessState.None */);
}
async activate(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('Activation is only supported on the top-level context');
}
await context.activate();
return {};
}
async captureScreenshot(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.captureScreenshot(params);
}
async print(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.print(params);
}
async setViewport(params) {
// Check the The viewport size limits is not checked by protocol parser, so we need to validate
// it manually:
// https://crsrc.org/c/content/browser/devtools/protocol/emulation_handler.cc;drc=f49e23d8e2bd190b42ec62284b8be10dcccd0446;l=660
const maxDimensionSize = 10_000_000;
if ((params.viewport?.height ?? 0) > maxDimensionSize ||
(params.viewport?.width ?? 0) > maxDimensionSize) {
throw new protocol_js_1.UnsupportedOperationException(`Viewport dimension over ${maxDimensionSize} are not supported`);
}
const config = {};
// `undefined` means no changes should be done to the config.
if (params.devicePixelRatio !== undefined) {
config.devicePixelRatio = params.devicePixelRatio;
}
if (params.viewport !== undefined) {
config.viewport = params.viewport;
}
const impactedTopLevelContexts = await this.#getRelatedTopLevelBrowsingContexts(params.context, params.userContexts);
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, config);
}
if (params.context !== undefined) {
this.#contextConfigStorage.updateBrowsingContextConfig(params.context, config);
}
await Promise.all(impactedTopLevelContexts.map(async (context) => {
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing context ids.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextId, userContextIds) {
if (browsingContextId === undefined && userContextIds === undefined) {
throw new protocol_js_1.InvalidArgumentException('Either userContexts or context must be provided');
}
if (browsingContextId !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('userContexts and context are mutually exclusive');
}
if (browsingContextId !== undefined) {
const context = this.#browsingContextStorage.getContext(browsingContextId);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('Emulating viewport is only supported on the top-level context');
}
return [context];
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
const result = [];
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async traverseHistory(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context) {
throw new protocol_js_1.InvalidArgumentException(`No browsing context with id ${params.context}`);
}
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('Traversing history is only supported on the top-level context');
}
await context.traverseHistory(params.delta);
return {};
}
async handleUserPrompt(params) {
const context = this.#browsingContextStorage.getContext(params.context);
try {
await context.handleUserPrompt(params.accept, params.userText);
}
catch (error) {
// Heuristically determine the error
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/page_handler.cc;l=1085?q=%22No%20dialog%20is%20showing%22&ss=chromium
if (error.message?.includes('No dialog is showing')) {
throw new protocol_js_1.NoSuchAlertException('No dialog is showing');
}
throw error;
}
return {};
}
async close(params) {
const context = this.#browsingContextStorage.getContext(params.context);
if (!context.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException(`Non top-level browsing context ${context.id} cannot be closed.`);
}
// Parent session of a page target session can be a `browser` or a `tab` session.
const parentCdpClient = context.cdpTarget.parentCdpClient;
try {
const detachedFromTargetPromise = new Promise((resolve) => {
const onContextDestroyed = (event) => {
if (event.targetId === params.context) {
parentCdpClient.off('Target.detachedFromTarget', onContextDestroyed);
resolve();
}
};
parentCdpClient.on('Target.detachedFromTarget', onContextDestroyed);
});
try {
if (params.promptUnload) {
await context.close();
}
else {
await parentCdpClient.sendCommand('Target.closeTarget', {
targetId: params.context,
});
}
}
catch (error) {
// Swallow error that arise from the session being destroyed. Rely on the
// `detachedFromTargetPromise` event to be resolved.
if (!parentCdpClient.isCloseError(error)) {
throw error;
}
}
// Sometimes CDP command finishes before `detachedFromTarget` event,
// sometimes after. Wait for the CDP command to be finished, and then wait
// for `detachedFromTarget` if it hasn't emitted.
await detachedFromTargetPromise;
}
catch (error) {
// Swallow error that arise from the page being destroyed
// Example is navigating to faulty SSL certificate
if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'Not attached to an active page')) {
throw error;
}
}
return {};
}
async locateNodes(params) {
const context = this.#browsingContextStorage.getContext(params.context);
return await context.locateNodes(params);
}
#onContextCreatedSubscribeHook(contextId) {
const context = this.#browsingContextStorage.getContext(contextId);
const contextsToReport = [
context,
...this.#browsingContextStorage.getContext(contextId).allChildren,
];
contextsToReport.forEach((context) => {
this.#eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.ContextCreated,
params: context.serializeToBidiValue(),
}, context.id);
});
return Promise.resolve();
}
}
exports.BrowsingContextProcessor = BrowsingContextProcessor;
//# sourceMappingURL=BrowsingContextProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,47 @@
/**
* Copyright 2022 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 { type BrowsingContext } from '../../../protocol/protocol.js';
import type { BrowsingContextImpl } from './BrowsingContextImpl.js';
/** Container class for browsing contexts. */
export declare class BrowsingContextStorage {
#private;
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts(): BrowsingContextImpl[];
/** Gets all contexts. */
getAllContexts(): BrowsingContextImpl[];
/** Deletes the context with the given ID. */
deleteContextById(id: BrowsingContext.BrowsingContext): void;
/** Deletes the given context. */
deleteContext(context: BrowsingContextImpl): void;
/** Tracks the given context. */
addContext(context: BrowsingContextImpl): void;
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId: BrowsingContext.BrowsingContext): Promise<BrowsingContextImpl>;
/** Returns true whether there is an existing context with the given ID. */
hasContext(id: BrowsingContext.BrowsingContext): boolean;
/** Gets the context with the given ID, if any. */
findContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl | undefined;
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id: BrowsingContext.BrowsingContext | null): BrowsingContext.BrowsingContext | null;
findContextBySession(sessionId: string): BrowsingContextImpl | undefined;
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id: BrowsingContext.BrowsingContext): BrowsingContextImpl;
verifyTopLevelContextsList(contexts: BrowsingContext.BrowsingContext[] | undefined): Set<BrowsingContextImpl>;
verifyContextsList(contexts: BrowsingContext.BrowsingContext[]): void;
}

View File

@@ -0,0 +1,134 @@
"use strict";
/**
* Copyright 2022 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.BrowsingContextStorage = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const EventEmitter_js_1 = require("../../../utils/EventEmitter.js");
/** Container class for browsing contexts. */
class BrowsingContextStorage {
/** Map from context ID to context implementation. */
#contexts = new Map();
/** Event emitter for browsing context storage eventsis not expected to be exposed to
* the outside world. */
#eventEmitter = new EventEmitter_js_1.EventEmitter();
/** Gets all top-level contexts, i.e. those with no parent. */
getTopLevelContexts() {
return this.getAllContexts().filter((context) => context.isTopLevelContext());
}
/** Gets all contexts. */
getAllContexts() {
return Array.from(this.#contexts.values());
}
/** Deletes the context with the given ID. */
deleteContextById(id) {
this.#contexts.delete(id);
}
/** Deletes the given context. */
deleteContext(context) {
this.#contexts.delete(context.id);
}
/** Tracks the given context. */
addContext(context) {
this.#contexts.set(context.id, context);
this.#eventEmitter.emit("added" /* BrowsingContextStorageEvents.Added */, {
browsingContext: context,
});
}
/**
* Waits for a context with the given ID to be added and returns it.
*/
waitForContext(browsingContextId) {
if (this.#contexts.has(browsingContextId)) {
return Promise.resolve(this.getContext(browsingContextId));
}
return new Promise((resolve) => {
const listener = (event) => {
if (event.browsingContext.id === browsingContextId) {
this.#eventEmitter.off("added" /* BrowsingContextStorageEvents.Added */, listener);
resolve(event.browsingContext);
}
};
this.#eventEmitter.on("added" /* BrowsingContextStorageEvents.Added */, listener);
});
}
/** Returns true whether there is an existing context with the given ID. */
hasContext(id) {
return this.#contexts.has(id);
}
/** Gets the context with the given ID, if any. */
findContext(id) {
return this.#contexts.get(id);
}
/** Returns the top-level context ID of the given context, if any. */
findTopLevelContextId(id) {
if (id === null) {
return null;
}
const maybeContext = this.findContext(id);
if (!maybeContext) {
return null;
}
const parentId = maybeContext.parentId ?? null;
if (parentId === null) {
return id;
}
return this.findTopLevelContextId(parentId);
}
findContextBySession(sessionId) {
for (const context of this.#contexts.values()) {
if (context.cdpTarget.cdpSessionId === sessionId) {
return context;
}
}
return;
}
/** Gets the context with the given ID, if any, otherwise throws. */
getContext(id) {
const result = this.findContext(id);
if (result === undefined) {
throw new protocol_js_1.NoSuchFrameException(`Context ${id} not found`);
}
return result;
}
verifyTopLevelContextsList(contexts) {
const foundContexts = new Set();
if (!contexts) {
return foundContexts;
}
for (const contextId of contexts) {
const context = this.getContext(contextId);
if (context.isTopLevelContext()) {
foundContexts.add(context);
}
else {
throw new protocol_js_1.InvalidArgumentException(`Non top-level context '${contextId}' given.`);
}
}
return foundContexts;
}
verifyContextsList(contexts) {
if (!contexts.length) {
return;
}
for (const contextId of contexts) {
this.getContext(contextId);
}
}
}
exports.BrowsingContextStorage = BrowsingContextStorage;
//# sourceMappingURL=BrowsingContextStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"BrowsingContextStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/context/BrowsingContextStorage.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,87 @@
import type { Protocol } from 'devtools-protocol';
import { type BrowsingContext } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { EventManager } from '../session/EventManager.js';
export declare const enum NavigationEventName {
FragmentNavigated = "browsingContext.fragmentNavigated",
NavigationAborted = "browsingContext.navigationAborted",
NavigationFailed = "browsingContext.navigationFailed",
Load = "browsingContext.load"
}
export declare class NavigationResult {
readonly eventName: NavigationEventName;
readonly message?: string;
constructor(eventName: NavigationEventName, message?: string);
}
export declare class NavigationState {
#private;
readonly navigationId: `${string}-${string}-${string}-${string}-${string}`;
url: string;
loaderId?: string;
committed: Deferred<void>;
isFragmentNavigation?: boolean;
get finished(): Promise<NavigationResult>;
constructor(url: string, browsingContextId: string, isInitial: boolean, eventManager: EventManager);
navigationInfo(): BrowsingContext.NavigationInfo;
start(): void;
frameNavigated(): void;
fragmentNavigated(): void;
load(): void;
fail(message: string): void;
}
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
export declare class NavigationTracker {
#private;
constructor(url: string, browsingContextId: string, eventManager: EventManager, logger?: LoggerFn);
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId(): `${string}-${string}-${string}-${string}-${string}`;
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation(): boolean;
/**
* Url of the last navigated navigation.
*/
get url(): string;
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url: string, canBeInitialNavigation?: boolean): NavigationState;
dispose(): void;
onTargetInfoChanged(url: string): void;
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url: string, loaderId: string, unreachableUrl?: string): void;
navigatedWithinDocument(url: string, navigationType: Protocol.Page.NavigatedWithinDocumentEvent['navigationType']): void;
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId: string): void;
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation: NavigationState, errorText: string): void;
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation: NavigationState, loaderId?: string): void;
frameStartedNavigating(url: string, loaderId: string, navigationType: string): void;
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId: string, errorText: string): void;
}

View File

@@ -0,0 +1,331 @@
"use strict";
/*
* Copyright 2024 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NavigationTracker = exports.NavigationState = exports.NavigationResult = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const time_js_1 = require("../../../utils/time.js");
const urlHelpers_js_1 = require("../../../utils/urlHelpers.js");
const uuid_js_1 = require("../../../utils/uuid.js");
class NavigationResult {
eventName;
message;
constructor(eventName, message) {
this.eventName = eventName;
this.message = message;
}
}
exports.NavigationResult = NavigationResult;
class NavigationState {
navigationId = (0, uuid_js_1.uuidv4)();
#browsingContextId;
#started = false;
#finished = new Deferred_js_1.Deferred();
url;
loaderId;
#isInitial;
#eventManager;
committed = new Deferred_js_1.Deferred();
isFragmentNavigation;
get finished() {
return this.#finished;
}
constructor(url, browsingContextId, isInitial, eventManager) {
this.#browsingContextId = browsingContextId;
this.url = url;
this.#isInitial = isInitial;
this.#eventManager = eventManager;
}
navigationInfo() {
return {
context: this.#browsingContextId,
navigation: this.navigationId,
timestamp: (0, time_js_1.getTimestamp)(),
url: this.url,
};
}
start() {
if (
// Initial navigation should not be reported.
!this.#isInitial &&
// No need in reporting started navigation twice.
!this.#started &&
// No need for reporting fragment navigations. Step 13 vs step 16 of the spec:
// https://html.spec.whatwg.org/#beginning-navigation:webdriver-bidi-navigation-started
!this.isFragmentNavigation) {
this.#eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationStarted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#started = true;
}
#finish(navigationResult) {
this.#started = true;
if (!this.#isInitial &&
!this.#finished.isFinished &&
navigationResult.eventName !== "browsingContext.load" /* NavigationEventName.Load */) {
this.#eventManager.registerEvent({
type: 'event',
method: navigationResult.eventName,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
this.#finished.resolve(navigationResult);
}
frameNavigated() {
this.committed.resolve();
if (!this.#isInitial) {
this.#eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.BrowsingContext.EventNames.NavigationCommitted,
params: this.navigationInfo(),
}, this.#browsingContextId);
}
}
fragmentNavigated() {
this.committed.resolve();
this.#finish(new NavigationResult("browsingContext.fragmentNavigated" /* NavigationEventName.FragmentNavigated */));
}
load() {
this.#finish(new NavigationResult("browsingContext.load" /* NavigationEventName.Load */));
}
fail(message) {
this.#finish(new NavigationResult(this.committed.isFinished
? "browsingContext.navigationAborted" /* NavigationEventName.NavigationAborted */
: "browsingContext.navigationFailed" /* NavigationEventName.NavigationFailed */, message));
}
}
exports.NavigationState = NavigationState;
/**
* Keeps track of navigations. Details: http://go/webdriver:bidi-navigation
*/
class NavigationTracker {
#eventManager;
#logger;
#loaderIdToNavigationsMap = new Map();
#browsingContextId;
/**
* Last committed navigation is committed, but is not guaranteed to be finished, as it
* can still wait for `load` or `DOMContentLoaded` events.
*/
#lastCommittedNavigation;
/**
* Pending navigation is a navigation that is started but not yet committed.
*/
#pendingNavigation;
// Flags if the initial navigation to `about:blank` is in progress.
#isInitialNavigation = true;
constructor(url, browsingContextId, eventManager, logger) {
this.#browsingContextId = browsingContextId;
this.#eventManager = eventManager;
this.#logger = logger;
this.#isInitialNavigation = true;
// The initial navigation is always committed.
this.#lastCommittedNavigation = new NavigationState(url, browsingContextId, (0, urlHelpers_js_1.urlMatchesAboutBlank)(url), this.#eventManager);
}
/**
* Returns current started ongoing navigation. It can be either a started pending
* navigation, or one is already navigated.
*/
get currentNavigationId() {
if (this.#pendingNavigation?.isFragmentNavigation === false) {
// Use pending navigation if it is started and it is not a fragment navigation.
return this.#pendingNavigation.navigationId;
}
// If the pending navigation is a fragment one, or if it is not exists, the last
// committed navigation should be used.
return this.#lastCommittedNavigation.navigationId;
}
/**
* Flags if the current navigation relates to the initial to `about:blank` navigation.
*/
get isInitialNavigation() {
return this.#isInitialNavigation;
}
/**
* Url of the last navigated navigation.
*/
get url() {
return this.#lastCommittedNavigation.url;
}
/**
* Creates a pending navigation e.g. when navigation command is called. Required to
* provide navigation id before the actual navigation is started. It will be used when
* navigation started. Can be aborted, failed, fragment navigated, or became a current
* navigation.
*/
createPendingNavigation(url, canBeInitialNavigation = false) {
this.#logger?.(log_js_1.LogType.debug, 'createCommandNavigation');
this.#isInitialNavigation =
canBeInitialNavigation &&
this.#isInitialNavigation &&
(0, urlHelpers_js_1.urlMatchesAboutBlank)(url);
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
const navigation = new NavigationState(url, this.#browsingContextId, this.#isInitialNavigation, this.#eventManager);
this.#pendingNavigation = navigation;
return navigation;
}
dispose() {
this.#pendingNavigation?.fail('navigation canceled by context disposal');
this.#lastCommittedNavigation.fail('navigation canceled by context disposal');
}
// Update the current url.
onTargetInfoChanged(url) {
this.#logger?.(log_js_1.LogType.debug, `onTargetInfoChanged ${url}`);
this.#lastCommittedNavigation.url = url;
}
#getNavigationForFrameNavigated(url, loaderId) {
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
return this.#loaderIdToNavigationsMap.get(loaderId);
}
if (this.#pendingNavigation !== undefined &&
this.#pendingNavigation.loaderId === undefined) {
// This can be a pending navigation to `about:blank` created by a command. Use the
// pending navigation in this case.
return this.#pendingNavigation;
}
// Create a new pending navigation.
return this.createPendingNavigation(url, true);
}
/**
* @param {string} unreachableUrl indicated the navigation is actually failed.
*/
frameNavigated(url, loaderId, unreachableUrl) {
this.#logger?.(log_js_1.LogType.debug, `frameNavigated ${url}`);
if (unreachableUrl !== undefined) {
// The navigation failed.
const navigation = this.#loaderIdToNavigationsMap.get(loaderId) ??
this.#pendingNavigation ??
this.createPendingNavigation(unreachableUrl, true);
navigation.url = unreachableUrl;
navigation.start();
navigation.fail('the requested url is unreachable');
return;
}
const navigation = this.#getNavigationForFrameNavigated(url, loaderId);
if (navigation !== this.#lastCommittedNavigation) {
// Even though the `lastCommittedNavigation` is navigated, it still can be waiting
// for `load` or `DOMContentLoaded` events.
this.#lastCommittedNavigation.fail('navigation canceled by concurrent navigation');
}
navigation.url = url;
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
navigation.start();
navigation.frameNavigated();
this.#lastCommittedNavigation = navigation;
if (this.#pendingNavigation === navigation) {
this.#pendingNavigation = undefined;
}
}
navigatedWithinDocument(url, navigationType) {
this.#logger?.(log_js_1.LogType.debug, `navigatedWithinDocument ${url}, ${navigationType}`);
// Current navigation URL should be updated.
this.#lastCommittedNavigation.url = url;
if (navigationType !== 'fragment') {
// TODO: check for other navigation types, like `javascript`.
return;
}
// There is no way to map `navigatedWithinDocument` to a specific navigation. Consider
// it is the pending navigation, if it is a fragment one.
const fragmentNavigation = this.#pendingNavigation?.isFragmentNavigation === true
? this.#pendingNavigation
: new NavigationState(url, this.#browsingContextId, false, this.#eventManager);
// Finish ongoing navigation.
fragmentNavigation.fragmentNavigated();
if (fragmentNavigation === this.#pendingNavigation) {
this.#pendingNavigation = undefined;
}
}
/**
* Required to mark navigation as fully complete.
* TODO: navigation should be complete when it became the current one on
* `Page.frameNavigated` or on navigating command finished with a new loader Id.
*/
loadPageEvent(loaderId) {
this.#logger?.(log_js_1.LogType.debug, 'loadPageEvent');
// Even if it was an initial navigation, it is finished.
this.#isInitialNavigation = false;
this.#loaderIdToNavigationsMap.get(loaderId)?.load();
}
/**
* Fail navigation due to navigation command failed.
*/
failNavigation(navigation, errorText) {
this.#logger?.(log_js_1.LogType.debug, 'failCommandNavigation');
navigation.fail(errorText);
}
/**
* Updates the navigation's `loaderId` and sets it as current one, if it is a
* cross-document navigation.
*/
navigationCommandFinished(navigation, loaderId) {
this.#logger?.(log_js_1.LogType.debug, `finishCommandNavigation ${navigation.navigationId}, ${loaderId}`);
if (loaderId !== undefined) {
navigation.loaderId = loaderId;
this.#loaderIdToNavigationsMap.set(loaderId, navigation);
}
navigation.isFragmentNavigation = loaderId === undefined;
}
frameStartedNavigating(url, loaderId, navigationType) {
this.#logger?.(log_js_1.LogType.debug, `frameStartedNavigating ${url}, ${loaderId}`);
if (this.#pendingNavigation &&
this.#pendingNavigation?.loaderId !== undefined &&
this.#pendingNavigation?.loaderId !== loaderId) {
// If there is a pending navigation with loader id set, but not equal to the new
// loader id, cancel pending navigation.
this.#pendingNavigation?.fail('navigation canceled by concurrent navigation');
this.#pendingNavigation = undefined;
}
if (this.#loaderIdToNavigationsMap.has(loaderId)) {
const existingNavigation = this.#loaderIdToNavigationsMap.get(loaderId);
// Navigation can be changed from `sameDocument` to `differentDocument`.
existingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
this.#pendingNavigation = existingNavigation;
return;
}
const pendingNavigation = this.#pendingNavigation ?? this.createPendingNavigation(url, true);
this.#loaderIdToNavigationsMap.set(loaderId, pendingNavigation);
pendingNavigation.isFragmentNavigation =
NavigationTracker.#isFragmentNavigation(navigationType);
pendingNavigation.url = url;
pendingNavigation.loaderId = loaderId;
pendingNavigation.start();
}
static #isFragmentNavigation(navigationType) {
// Page.frameStartedNavigating.navigationType can be one of the following values:
// reload, reloadBypassingCache, restore, restoreWithPost, historySameDocument,
// historyDifferentDocument, sameDocument, differentDocument.
// https://chromedevtools.github.io/devtools-protocol/tot/Page/#event-frameStartedNavigating
return ['historySameDocument', 'sameDocument'].includes(navigationType);
}
/**
* If there is a navigation with the loaderId equals to the network request id, it means
* that the navigation failed.
*/
networkLoadingFailed(loaderId, errorText) {
this.#loaderIdToNavigationsMap.get(loaderId)?.fail(errorText);
}
}
exports.NavigationTracker = NavigationTracker;
//# sourceMappingURL=NavigationTracker.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,37 @@
/**
* Copyright 2025 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 type { EmptyResult, Emulation, UAClientHints } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class EmulationProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
setGeolocationOverride(params: Emulation.SetGeolocationOverrideParameters): Promise<EmptyResult>;
setLocaleOverride(params: Emulation.SetLocaleOverrideParameters): Promise<EmptyResult>;
setScriptingEnabled(params: Emulation.SetScriptingEnabledParameters): Promise<EmptyResult>;
setScreenOrientationOverride(params: Emulation.SetScreenOrientationOverrideParameters): Promise<EmptyResult>;
setScreenSettingsOverride(params: Emulation.SetScreenSettingsOverrideParameters): Promise<EmptyResult>;
setTimezoneOverride(params: Emulation.SetTimezoneOverrideParameters): Promise<EmptyResult>;
setTouchOverride(params: Emulation.SetTouchOverrideParameters): Promise<EmptyResult>;
setUserAgentOverrideParams(params: Emulation.SetUserAgentOverrideParameters): Promise<EmptyResult>;
setClientHintsOverride(params: UAClientHints.Emulation.SetClientHintsOverrideParameters): Promise<EmptyResult>;
setNetworkConditions(params: Emulation.SetNetworkConditionsParameters): Promise<EmptyResult>;
}
export declare function isValidLocale(locale: string): boolean;
export declare function isValidTimezone(timezone: string): boolean;
export declare function isTimeZoneOffsetString(timezone: string): boolean;

View File

@@ -0,0 +1,384 @@
"use strict";
/**
* Copyright 2025 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.EmulationProcessor = void 0;
exports.isValidLocale = isValidLocale;
exports.isValidTimezone = isValidTimezone;
exports.isTimeZoneOffsetString = isTimeZoneOffsetString;
const protocol_js_1 = require("../../../protocol/protocol.js");
class EmulationProcessor {
#userContextStorage;
#browsingContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async setGeolocationOverride(params) {
if ('coordinates' in params && 'error' in params) {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException('Coordinates and error cannot be set at the same time');
}
let geolocation = null;
if ('coordinates' in params) {
if ((params.coordinates?.altitude ?? null) === null &&
(params.coordinates?.altitudeAccuracy ?? null) !== null) {
throw new protocol_js_1.InvalidArgumentException('Geolocation altitudeAccuracy can be set only with altitude');
}
geolocation = params.coordinates;
}
else if ('error' in params) {
if (params.error.type !== 'positionUnavailable') {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException(`Unknown geolocation error ${params.error.type}`);
}
geolocation = params.error;
}
else {
// Unreachable. Handled by params parser.
throw new protocol_js_1.InvalidArgumentException(`Coordinates or error should be set`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
geolocation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
geolocation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setGeolocationOverride(config.geolocation ?? null);
}));
return {};
}
async setLocaleOverride(params) {
const locale = params.locale ?? null;
if (locale !== null && !isValidLocale(locale)) {
throw new protocol_js_1.InvalidArgumentException(`Invalid locale "${locale}"`);
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
locale,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
locale,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await Promise.all([
context.setLocaleOverride(config.locale ?? null),
// Set `AcceptLanguage` to locale.
context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints),
]);
}));
return {};
}
async setScriptingEnabled(params) {
const scriptingEnabled = params.enabled;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
scriptingEnabled,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
scriptingEnabled,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setScriptingEnabled(config.scriptingEnabled ?? null);
}));
return {};
}
async setScreenOrientationOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenOrientation: params.screenOrientation,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenOrientation: params.screenOrientation,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
async setScreenSettingsOverride(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
screenArea: params.screenArea,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
screenArea: params.screenArea,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setViewport(config.viewport ?? null, config.devicePixelRatio ?? null, config.screenOrientation ?? null);
}));
return {};
}
/**
* Returns a list of top-level browsing contexts.
*/
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds, allowGlobal = false) {
if (browsingContextIds === undefined && userContextIds === undefined) {
if (allowGlobal) {
return this.#browsingContextStorage.getTopLevelContexts();
}
throw new protocol_js_1.InvalidArgumentException('Either user contexts or browsing contexts must be provided');
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (browsingContextIds === undefined) {
// userContextIds !== undefined
if (userContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
else {
if (browsingContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setTimezoneOverride(params) {
let timezone = params.timezone ?? null;
if (timezone !== null && !isValidTimezone(timezone)) {
throw new protocol_js_1.InvalidArgumentException(`Invalid timezone "${timezone}"`);
}
if (timezone !== null && isTimeZoneOffsetString(timezone)) {
// CDP supports offset timezone with `GMT` prefix.
timezone = `GMT${timezone}`;
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
timezone,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
timezone,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTimezoneOverride(config.timezone ?? null);
}));
return {};
}
async setTouchOverride(params) {
const maxTouchPoints = params.maxTouchPoints;
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
maxTouchPoints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
maxTouchPoints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
maxTouchPoints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setTouchOverride(config.maxTouchPoints ?? null);
}));
return {};
}
async setUserAgentOverrideParams(params) {
if (params.userAgent === '') {
throw new protocol_js_1.UnsupportedOperationException('empty user agent string is not supported');
}
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
userAgent: params.userAgent,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
userAgent: params.userAgent,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
userAgent: params.userAgent,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setClientHintsOverride(params) {
const clientHints = params.clientHints ?? null;
// Get all relevant contexts to update:
// 1. Specific browsing contexts (if provided).
// 2. All contexts for specific user contexts (if provided).
// 3. All top-level contexts (if global).
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
clientHints,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
clientHints,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
clientHints,
});
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setUserAgentAndAcceptLanguage(config.userAgent, config.locale, config.clientHints);
}));
return {};
}
async setNetworkConditions(params) {
const browsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts, true);
for (const browsingContextId of params.contexts ?? []) {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
for (const userContextId of params.userContexts ?? []) {
this.#contextConfigStorage.updateUserContextConfig(userContextId, {
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.contexts === undefined && params.userContexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
emulatedNetworkConditions: params.networkConditions,
});
}
if (params.networkConditions !== null &&
params.networkConditions.type !== 'offline') {
throw new protocol_js_1.UnsupportedOperationException(`Unsupported network conditions ${params.networkConditions.type}`);
}
await Promise.all(browsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing more granular setting.
const config = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext);
await context.setEmulatedNetworkConditions(config.emulatedNetworkConditions ?? null);
}));
return {};
}
}
exports.EmulationProcessor = EmulationProcessor;
// Export for testing.
function isValidLocale(locale) {
try {
new Intl.Locale(locale);
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isValidTimezone(timezone) {
try {
Intl.DateTimeFormat(undefined, { timeZone: timezone });
return true;
}
catch (e) {
if (e instanceof RangeError) {
return false;
}
// Re-throw other errors
throw e;
}
}
// Export for testing.
function isTimeZoneOffsetString(timezone) {
return /^[+-](?:2[0-3]|[01]\d)(?::[0-5]\d)?$/.test(timezone);
}
//# sourceMappingURL=EmulationProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,27 @@
/**
* 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 type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { ActionOption } from './ActionOption.js';
import type { InputState } from './InputState.js';
export declare class ActionDispatcher {
#private;
static isMacOS: (context: BrowsingContextImpl) => Promise<boolean>;
constructor(inputState: InputState, browsingContextStorage: BrowsingContextStorage, contextId: string, isMacOS: boolean);
dispatchActions(optionsByTick: readonly (readonly Readonly<ActionOption>[])[]): Promise<void>;
dispatchTickActions(options: readonly Readonly<ActionOption>[]): Promise<void>;
}

View File

@@ -0,0 +1,744 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ActionDispatcher = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const graphemeTools_js_1 = require("../../../utils/graphemeTools.js");
const InputSource_js_1 = require("./InputSource.js");
const keyUtils_js_1 = require("./keyUtils.js");
const USKeyboardLayout_js_1 = require("./USKeyboardLayout.js");
/** https://w3c.github.io/webdriver/#dfn-center-point */
const CALCULATE_IN_VIEW_CENTER_PT_DECL = ((i) => {
const t = i.getClientRects()[0], e = Math.max(0, Math.min(t.x, t.x + t.width)), n = Math.min(window.innerWidth, Math.max(t.x, t.x + t.width)), h = Math.max(0, Math.min(t.y, t.y + t.height)), m = Math.min(window.innerHeight, Math.max(t.y, t.y + t.height));
return [e + ((n - e) >> 1), h + ((m - h) >> 1)];
}).toString();
const IS_MAC_DECL = (() => {
return navigator.platform.toLowerCase().includes('mac');
}).toString();
async function getElementCenter(context, element) {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(CALCULATE_IN_VIEW_CENTER_PT_DECL, false, { type: 'undefined' }, [element]);
if (result.type === 'exception') {
throw new protocol_js_1.NoSuchElementException(`Origin element ${element.sharedId} was not found`);
}
(0, assert_js_1.assert)(result.result.type === 'array');
(0, assert_js_1.assert)(result.result.value?.[0]?.type === 'number');
(0, assert_js_1.assert)(result.result.value?.[1]?.type === 'number');
const { result: { value: [{ value: x }, { value: y }], }, } = result;
return { x: x, y: y };
}
class ActionDispatcher {
static isMacOS = async (context) => {
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
const result = await hiddenSandboxRealm.callFunction(IS_MAC_DECL, false);
(0, assert_js_1.assert)(result.type !== 'exception');
(0, assert_js_1.assert)(result.result.type === 'boolean');
return result.result.value;
};
#browsingContextStorage;
#tickStart = 0;
#tickDuration = 0;
#inputState;
#contextId;
#isMacOS;
constructor(inputState, browsingContextStorage, contextId, isMacOS) {
this.#browsingContextStorage = browsingContextStorage;
this.#inputState = inputState;
this.#contextId = contextId;
this.#isMacOS = isMacOS;
}
/**
* The context can be disposed between action ticks, so need to get it each time.
*/
get #context() {
return this.#browsingContextStorage.getContext(this.#contextId);
}
async dispatchActions(optionsByTick) {
await this.#inputState.queue.run(async () => {
for (const options of optionsByTick) {
await this.dispatchTickActions(options);
}
});
}
async dispatchTickActions(options) {
this.#tickStart = performance.now();
this.#tickDuration = 0;
for (const { action } of options) {
if ('duration' in action && action.duration !== undefined) {
this.#tickDuration = Math.max(this.#tickDuration, action.duration);
}
}
const promises = [
new Promise((resolve) => setTimeout(resolve, this.#tickDuration)),
];
for (const option of options) {
// In theory we have to wait for each action to happen, but CDP is serial,
// so as an optimization, we queue all CDP commands at once and await all
// of them.
promises.push(this.#dispatchAction(option));
}
await Promise.all(promises);
}
async #dispatchAction({ id, action }) {
const source = this.#inputState.get(id);
const keyState = this.#inputState.getGlobalKeyState();
switch (action.type) {
case 'keyDown': {
// SAFETY: The source is validated before.
await this.#dispatchKeyDownAction(source, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'keyUp',
},
});
break;
}
case 'keyUp': {
// SAFETY: The source is validated before.
await this.#dispatchKeyUpAction(source, action);
break;
}
case 'pause': {
// TODO: Implement waiting on the input source.
break;
}
case 'pointerDown': {
// SAFETY: The source is validated before.
await this.#dispatchPointerDownAction(source, keyState, action);
this.#inputState.cancelList.push({
id,
action: {
...action,
type: 'pointerUp',
},
});
break;
}
case 'pointerMove': {
// SAFETY: The source is validated before.
await this.#dispatchPointerMoveAction(source, keyState, action);
break;
}
case 'pointerUp': {
// SAFETY: The source is validated before.
await this.#dispatchPointerUpAction(source, keyState, action);
break;
}
case 'scroll': {
// SAFETY: The source is validated before.
await this.#dispatchScrollAction(source, keyState, action);
break;
}
}
}
async #dispatchPointerDownAction(source, keyState, action) {
const { button } = action;
if (source.pressed.has(button)) {
return;
}
source.pressed.add(button);
const { x, y, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure } = action;
const { tiltX, tiltY } = getTilt(action);
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mousePressed',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.setClickCount(button, new InputSource_js_1.PointerSource.ClickContext(x, y, performance.now())),
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "touch" /* Input.PointerType.Touch */:
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
break;
}
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
// --- Platform-specific code ends here ---
}
#dispatchPointerUpAction(source, keyState, action) {
const { button } = action;
if (!source.pressed.has(button)) {
return;
}
source.pressed.delete(button);
const { x, y, force, radiusX, radiusY, subtype: pointerType } = source;
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
case "pen" /* Input.PointerType.Pen */:
// TODO: Implement width and height when available.
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseReleased',
x,
y,
modifiers,
button: getCdpButton(button),
buttons: source.buttons,
clickCount: source.getClickCount(button),
pointerType,
});
case "touch" /* Input.PointerType.Touch */:
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: [
{
x,
y,
id: source.pointerId,
force,
radiusX,
radiusY,
},
],
modifiers,
});
}
// --- Platform-specific code ends here ---
}
async #dispatchPointerMoveAction(source, keyState, action) {
const { x: startX, y: startY, subtype: pointerType } = source;
const { width, height, pressure, twist, tangentialPressure, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
const { tiltX, tiltY } = getTilt(action);
const { radiusX, radiusY } = getRadii(width ?? 1, height ?? 1);
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY);
if (targetX < 0 || targetY < 0) {
throw new protocol_js_1.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let x;
let y;
if (last) {
x = targetX;
y = targetY;
}
else {
x = Math.round(ratio * (targetX - startX) + startX);
y = Math.round(ratio * (targetY - startY) + startY);
}
if (source.x !== x || source.y !== y) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
switch (pointerType) {
case "mouse" /* Input.PointerType.Mouse */:
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
});
break;
case "pen" /* Input.PointerType.Pen */:
if (source.pressed.size !== 0) {
// Empty `source.pressed.size` means the pen is not detected by digitizer.
// Dispatch a mouse event for the pen only if either:
// 1. the pen is hovering over the digitizer (0);
// 2. the pen is in contact with the digitizer (1);
// 3. the pen has at least one button pressed (2, 4, etc).
// https://www.w3.org/TR/pointerevents/#the-buttons-property
// TODO: Implement width and height when available.
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseMoved',
x,
y,
modifiers,
clickCount: 0,
button: getCdpButton(source.pressed.values().next().value ?? 5),
buttons: source.buttons,
pointerType,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure ?? 0.5,
});
}
break;
case "touch" /* Input.PointerType.Touch */:
if (source.pressed.size !== 0) {
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [
{
x,
y,
radiusX,
radiusY,
tangentialPressure,
tiltX,
tiltY,
twist,
force: pressure,
id: source.pointerId,
},
],
modifiers,
});
}
break;
}
// --- Platform-specific code ends here ---
source.x = x;
source.y = y;
source.radiusX = radiusX;
source.radiusY = radiusY;
source.force = pressure;
}
} while (!last);
}
async #getFrameOffset() {
if (this.#context.id === this.#context.cdpTarget.id) {
return { x: 0, y: 0 };
}
// https://github.com/w3c/webdriver/pull/1847 proposes dispatching events from
// the top-level browsing context. This implementation dispatches it on the top-most
// same-target frame, which is not top-level one in case of OOPiF.
// TODO: switch to the top-level browsing context.
const { backendNodeId } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getFrameOwner', { frameId: this.#context.id });
const { model: frameBoxModel } = await this.#context.cdpTarget.cdpClient.sendCommand('DOM.getBoxModel', {
backendNodeId,
});
return { x: frameBoxModel.content[0], y: frameBoxModel.content[1] };
}
async #getCoordinateFromOrigin(origin, offsetX, offsetY, startX, startY) {
let targetX;
let targetY;
const frameOffset = await this.#getFrameOffset();
switch (origin) {
case 'viewport':
targetX = offsetX + frameOffset.x;
targetY = offsetY + frameOffset.y;
break;
case 'pointer':
targetX = startX + offsetX + frameOffset.x;
targetY = startY + offsetY + frameOffset.y;
break;
default: {
const { x: posX, y: posY } = await getElementCenter(this.#context, origin.element);
// SAFETY: These can never be special numbers.
targetX = posX + offsetX + frameOffset.x;
targetY = posY + offsetY + frameOffset.y;
break;
}
}
return { targetX, targetY };
}
async #dispatchScrollAction(_source, keyState, action) {
const { deltaX: targetDeltaX, deltaY: targetDeltaY, x: offsetX, y: offsetY, origin = 'viewport', duration = this.#tickDuration, } = action;
if (origin === 'pointer') {
throw new protocol_js_1.InvalidArgumentException('"pointer" origin is invalid for scrolling.');
}
const { targetX, targetY } = await this.#getCoordinateFromOrigin(origin, offsetX, offsetY, 0, 0);
if (targetX < 0 || targetY < 0) {
throw new protocol_js_1.MoveTargetOutOfBoundsException(`Cannot move beyond viewport (x: ${targetX}, y: ${targetY})`);
}
let currentDeltaX = 0;
let currentDeltaY = 0;
let last;
do {
const ratio = duration > 0 ? (performance.now() - this.#tickStart) / duration : 1;
last = ratio >= 1;
let deltaX;
let deltaY;
if (last) {
deltaX = targetDeltaX - currentDeltaX;
deltaY = targetDeltaY - currentDeltaY;
}
else {
deltaX = Math.round(ratio * targetDeltaX - currentDeltaX);
deltaY = Math.round(ratio * targetDeltaY - currentDeltaY);
}
if (deltaX !== 0 || deltaY !== 0) {
// --- Platform-specific code begins here ---
const { modifiers } = keyState;
await this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchMouseEvent', {
type: 'mouseWheel',
deltaX,
deltaY,
x: targetX,
y: targetY,
modifiers,
});
// --- Platform-specific code ends here ---
currentDeltaX += deltaX;
currentDeltaY += deltaY;
}
} while (!last);
}
async #dispatchKeyDownAction(source, action) {
const rawKey = action.value;
if (!(0, graphemeTools_js_1.isSingleGrapheme)(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new protocol_js_1.InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = (0, graphemeTools_js_1.isSingleComplexGrapheme)(rawKey);
const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
const repeat = source.pressed.has(key);
const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
switch (key) {
case 'Alt':
source.alt = true;
break;
case 'Shift':
source.shift = true;
break;
case 'Control':
source.ctrl = true;
break;
case 'Meta':
source.meta = true;
break;
}
source.pressed.add(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
let command;
// The following commands need to be declared because Chromium doesn't
// handle them. See
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:third_party/blink/renderer/core/editing/editing_behavior.cc;l=169;drc=b8143cf1dfd24842890fcd831c4f5d909bef4fc4;bpv=0;bpt=1.
if (this.#isMacOS && source.meta) {
switch (code) {
case 'KeyA':
command = 'SelectAll';
break;
case 'KeyC':
command = 'Copy';
break;
case 'KeyV':
command = source.shift ? 'PasteAndMatchStyle' : 'Paste';
break;
case 'KeyX':
command = 'Cut';
break;
case 'KeyZ':
command = source.shift ? 'Redo' : 'Undo';
break;
default:
// Intentionally empty.
}
}
const promises = [
this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: text ? 'keyDown' : 'rawKeyDown',
windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
autoRepeat: repeat,
isSystemKey: source.alt || undefined,
location: location < 3 ? location : undefined,
isKeypad: location === 3,
modifiers,
commands: command ? [command] : undefined,
}),
];
// Drag cancelling happens on escape.
if (key === 'Escape') {
if (!source.alt &&
((this.#isMacOS && !source.ctrl && !source.meta) || !this.#isMacOS)) {
promises.push(this.#context.cdpTarget.cdpClient.sendCommand('Input.cancelDragging'));
}
}
await Promise.all(promises);
// --- Platform-specific code ends here ---
}
#dispatchKeyUpAction(source, action) {
const rawKey = action.value;
if (!(0, graphemeTools_js_1.isSingleGrapheme)(rawKey)) {
// https://w3c.github.io/webdriver/#dfn-process-a-key-action
// WebDriver spec allows a grapheme to be used.
throw new protocol_js_1.InvalidArgumentException(`Invalid key value: ${rawKey}`);
}
const isGrapheme = (0, graphemeTools_js_1.isSingleComplexGrapheme)(rawKey);
const key = (0, keyUtils_js_1.getNormalizedKey)(rawKey);
if (!source.pressed.has(key)) {
return;
}
const code = (0, keyUtils_js_1.getKeyCode)(rawKey);
const location = (0, keyUtils_js_1.getKeyLocation)(rawKey);
switch (key) {
case 'Alt':
source.alt = false;
break;
case 'Shift':
source.shift = false;
break;
case 'Control':
source.ctrl = false;
break;
case 'Meta':
source.meta = false;
break;
}
source.pressed.delete(key);
const { modifiers } = source;
// --- Platform-specific code begins here ---
// The spread is a little hack so JS gives us an array of unicode characters
// to measure.
const unmodifiedText = getKeyEventUnmodifiedText(key, source, isGrapheme);
const text = getKeyEventText(code ?? '', source) ?? unmodifiedText;
return this.#context.cdpTarget.cdpClient.sendCommand('Input.dispatchKeyEvent', {
type: 'keyUp',
windowsVirtualKeyCode: USKeyboardLayout_js_1.KeyToKeyCode[key],
key,
code,
text,
unmodifiedText,
location: location < 3 ? location : undefined,
isSystemKey: source.alt || undefined,
isKeypad: location === 3,
modifiers,
});
// --- Platform-specific code ends here ---
}
}
exports.ActionDispatcher = ActionDispatcher;
/**
* Translates a non-grapheme key to either an `undefined` for a special keys, or a single
* character modified by shift if needed.
*/
const getKeyEventUnmodifiedText = (key, source, isGrapheme) => {
if (isGrapheme) {
// Graphemes should be presented as text in the CDP command.
return key;
}
if (key === 'Enter') {
return '\r';
}
// If key is not a single character, it is a normalized key value, and should be
// presented as key, not text in the CDP command.
return [...key].length === 1
? source.shift
? key.toLocaleUpperCase('en-US')
: key
: undefined;
};
const getKeyEventText = (code, source) => {
if (source.ctrl) {
switch (code) {
case 'Digit2':
if (source.shift) {
return '\x00';
}
break;
case 'KeyA':
return '\x01';
case 'KeyB':
return '\x02';
case 'KeyC':
return '\x03';
case 'KeyD':
return '\x04';
case 'KeyE':
return '\x05';
case 'KeyF':
return '\x06';
case 'KeyG':
return '\x07';
case 'KeyH':
return '\x08';
case 'KeyI':
return '\x09';
case 'KeyJ':
return '\x0A';
case 'KeyK':
return '\x0B';
case 'KeyL':
return '\x0C';
case 'KeyM':
return '\x0D';
case 'KeyN':
return '\x0E';
case 'KeyO':
return '\x0F';
case 'KeyP':
return '\x10';
case 'KeyQ':
return '\x11';
case 'KeyR':
return '\x12';
case 'KeyS':
return '\x13';
case 'KeyT':
return '\x14';
case 'KeyU':
return '\x15';
case 'KeyV':
return '\x16';
case 'KeyW':
return '\x17';
case 'KeyX':
return '\x18';
case 'KeyY':
return '\x19';
case 'KeyZ':
return '\x1A';
case 'BracketLeft':
return '\x1B';
case 'Backslash':
return '\x1C';
case 'BracketRight':
return '\x1D';
case 'Digit6':
if (source.shift) {
return '\x1E';
}
break;
case 'Minus':
return '\x1F';
}
return '';
}
if (source.alt) {
return '';
}
return;
};
function getCdpButton(button) {
// https://www.w3.org/TR/pointerevents/#the-button-property
switch (button) {
case 0:
return 'left';
case 1:
return 'middle';
case 2:
return 'right';
case 3:
return 'back';
case 4:
return 'forward';
default:
return 'none';
}
}
function getTilt(action) {
// https://w3c.github.io/pointerevents/#converting-between-tiltx-tilty-and-altitudeangle-azimuthangle
const altitudeAngle = action.altitudeAngle ?? Math.PI / 2;
const azimuthAngle = action.azimuthAngle ?? 0;
let tiltXRadians = 0;
let tiltYRadians = 0;
if (altitudeAngle === 0) {
// the pen is in the X-Y plane
if (azimuthAngle === 0 || azimuthAngle === 2 * Math.PI) {
// pen is on positive X axis
tiltXRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI / 2) {
// pen is on positive Y axis
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle === Math.PI) {
// pen is on negative X axis
tiltXRadians = -Math.PI / 2;
}
if (azimuthAngle === (3 * Math.PI) / 2) {
// pen is on negative Y axis
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > 0 && azimuthAngle < Math.PI / 2) {
tiltXRadians = Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI / 2 && azimuthAngle < Math.PI) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = Math.PI / 2;
}
if (azimuthAngle > Math.PI && azimuthAngle < (3 * Math.PI) / 2) {
tiltXRadians = -Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
if (azimuthAngle > (3 * Math.PI) / 2 && azimuthAngle < 2 * Math.PI) {
tiltXRadians = Math.PI / 2;
tiltYRadians = -Math.PI / 2;
}
}
if (altitudeAngle !== 0) {
const tanAlt = Math.tan(altitudeAngle);
tiltXRadians = Math.atan(Math.cos(azimuthAngle) / tanAlt);
tiltYRadians = Math.atan(Math.sin(azimuthAngle) / tanAlt);
}
const factor = 180 / Math.PI;
return {
tiltX: Math.round(tiltXRadians * factor),
tiltY: Math.round(tiltYRadians * factor),
};
}
function getRadii(width, height) {
return {
radiusX: width ? width / 2 : 0.5,
radiusY: height ? height / 2 : 0.5,
};
}
//# sourceMappingURL=ActionDispatcher.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,22 @@
/**
* 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 type { Input } from '../../../protocol/protocol.js';
export type ActionOption = ActionOptionFor<Input.NoneSourceAction | Input.KeySourceAction | Input.PointerSourceAction | Input.WheelSourceAction>;
export interface ActionOptionFor<A> {
id: string;
action: A;
}

View File

@@ -0,0 +1,19 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
//# sourceMappingURL=ActionOption.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"ActionOption.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/ActionOption.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG"}

View File

@@ -0,0 +1,9 @@
import { Input, type EmptyResult } from '../../../protocol/protocol.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
export declare class InputProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage);
performActions(params: Input.PerformActionsParameters): Promise<EmptyResult>;
releaseActions(params: Input.ReleaseActionsParameters): Promise<EmptyResult>;
setFiles(params: Input.SetFilesParameters): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,194 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputProcessor = void 0;
/*
* 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.
*/
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const ActionDispatcher_js_1 = require("../input/ActionDispatcher.js");
const InputStateManager_js_1 = require("../input/InputStateManager.js");
class InputProcessor {
#browsingContextStorage;
#inputStateManager = new InputStateManager_js_1.InputStateManager();
constructor(browsingContextStorage) {
this.#browsingContextStorage = browsingContextStorage;
}
async performActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const inputState = this.#inputStateManager.get(context.top);
const actionsByTick = this.#getActionsByTick(params, inputState);
const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchActions(actionsByTick);
return {};
}
async releaseActions(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const topContext = context.top;
const inputState = this.#inputStateManager.get(topContext);
const dispatcher = new ActionDispatcher_js_1.ActionDispatcher(inputState, this.#browsingContextStorage, params.context, await ActionDispatcher_js_1.ActionDispatcher.isMacOS(context).catch(() => false));
await dispatcher.dispatchTickActions(inputState.cancelList.reverse());
this.#inputStateManager.delete(topContext);
return {};
}
async setFiles(params) {
const context = this.#browsingContextStorage.getContext(params.context);
const hiddenSandboxRealm = await context.getOrCreateHiddenSandbox();
let result;
try {
result = await hiddenSandboxRealm.callFunction(String(function getFiles(fileListLength) {
if (!(this instanceof HTMLInputElement)) {
if (this instanceof Element) {
return 1 /* ErrorCode.Element */;
}
return 0 /* ErrorCode.Node */;
}
if (this.type !== 'file') {
return 2 /* ErrorCode.Type */;
}
if (this.disabled) {
return 3 /* ErrorCode.Disabled */;
}
if (fileListLength > 1 && !this.multiple) {
return 4 /* ErrorCode.Multiple */;
}
return;
}), false, params.element, [{ type: 'number', value: params.files.length }]);
}
catch {
throw new protocol_js_1.NoSuchNodeException(`Could not find element ${params.element.sharedId}`);
}
(0, assert_js_1.assert)(result.type === 'success');
if (result.result.type === 'number') {
switch (result.result.value) {
case 0 /* ErrorCode.Node */: {
throw new protocol_js_1.NoSuchElementException(`Could not find element ${params.element.sharedId}`);
}
case 1 /* ErrorCode.Element */: {
throw new protocol_js_1.UnableToSetFileInputException(`Element ${params.element.sharedId} is not a input`);
}
case 2 /* ErrorCode.Type */: {
throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is not a file type`);
}
case 3 /* ErrorCode.Disabled */: {
throw new protocol_js_1.UnableToSetFileInputException(`Input element ${params.element.sharedId} is disabled`);
}
case 4 /* ErrorCode.Multiple */: {
throw new protocol_js_1.UnableToSetFileInputException(`Cannot set multiple files on a non-multiple input element`);
}
}
}
/**
* The zero-length array is a special case, it seems that
* DOM.setFileInputFiles does not actually update the files in that case, so
* the solution is to eval the element value to a new FileList directly.
*/
if (params.files.length === 0) {
// XXX: These events should converted to trusted events. Perhaps do this
// in `DOM.setFileInputFiles`?
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
if (this.files?.length === 0) {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
return;
}
this.files = new DataTransfer().files;
// Dispatch events for this case because it should behave akin to a user action.
this.dispatchEvent(new Event('input', { bubbles: true, composed: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}), false, params.element);
return {};
}
// Our goal here is to iterate over the input element files and get their
// file paths.
const paths = [];
for (let i = 0; i < params.files.length; ++i) {
const result = await hiddenSandboxRealm.callFunction(String(function getFiles(index) {
return this.files?.item(index);
}), false, params.element, [{ type: 'number', value: 0 }], "root" /* Script.ResultOwnership.Root */);
(0, assert_js_1.assert)(result.type === 'success');
if (result.result.type !== 'object') {
break;
}
const { handle } = result.result;
(0, assert_js_1.assert)(handle !== undefined);
const { path } = await hiddenSandboxRealm.cdpClient.sendCommand('DOM.getFileInfo', {
objectId: handle,
});
paths.push(path);
// Cleanup the handle.
void hiddenSandboxRealm.disown(handle).catch(undefined);
}
paths.sort();
// We create a new array so we preserve the order of the original files.
const sortedFiles = [...params.files].sort();
if (paths.length !== params.files.length ||
sortedFiles.some((path, index) => {
return paths[index] !== path;
})) {
const { objectId } = await hiddenSandboxRealm.deserializeForCdp(params.element);
// This cannot throw since this was just used in `callFunction` above.
(0, assert_js_1.assert)(objectId !== undefined);
await hiddenSandboxRealm.cdpClient.sendCommand('DOM.setFileInputFiles', {
files: params.files,
objectId,
});
}
else {
// XXX: We should dispatch a trusted event.
await hiddenSandboxRealm.callFunction(String(function dispatchEvent() {
this.dispatchEvent(new Event('cancel', {
bubbles: true,
}));
}), false, params.element);
}
return {};
}
#getActionsByTick(params, inputState) {
const actionsByTick = [];
for (const action of params.actions) {
switch (action.type) {
case "pointer" /* SourceType.Pointer */: {
action.parameters ??= { pointerType: "mouse" /* Input.PointerType.Mouse */ };
action.parameters.pointerType ??= "mouse" /* Input.PointerType.Mouse */;
const source = inputState.getOrCreate(action.id, "pointer" /* SourceType.Pointer */, action.parameters.pointerType);
if (source.subtype !== action.parameters.pointerType) {
throw new protocol_js_1.InvalidArgumentException(`Expected input source ${action.id} to be ${source.subtype}; got ${action.parameters.pointerType}.`);
}
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043
source.resetClickCount();
break;
}
default:
inputState.getOrCreate(action.id, action.type);
}
const actions = action.actions.map((item) => ({
id: action.id,
action: item,
}));
for (let i = 0; i < actions.length; i++) {
if (actionsByTick.length === i) {
actionsByTick.push([]);
}
actionsByTick[i].push(actions[i]);
}
}
return actionsByTick;
}
}
exports.InputProcessor = InputProcessor;
//# sourceMappingURL=InputProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,78 @@
/**
* 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 type { Input } from '../../../protocol/protocol.js';
export declare const enum SourceType {
Key = "key",
Pointer = "pointer",
Wheel = "wheel",
None = "none"
}
export declare class NoneSource {
type: SourceType.None;
}
export declare class KeySource {
#private;
type: SourceType.Key;
pressed: Set<string>;
get modifiers(): number;
get alt(): boolean;
set alt(value: boolean);
get ctrl(): boolean;
set ctrl(value: boolean);
get meta(): boolean;
set meta(value: boolean);
get shift(): boolean;
set shift(value: boolean);
}
export declare class PointerSource {
#private;
type: SourceType.Pointer;
subtype: Input.PointerType;
pointerId: number;
pressed: Set<number>;
x: number;
y: number;
radiusX?: number;
radiusY?: number;
force?: number;
constructor(id: number, subtype: Input.PointerType);
get buttons(): number;
static ClickContext: {
new (x: number, y: number, time: number): {
count: number;
"__#private@#x": number;
"__#private@#y": number;
"__#private@#time": number;
compare(context: /*elided*/ any): boolean;
};
"__#private@#DOUBLE_CLICK_TIME_MS": number;
"__#private@#MAX_DOUBLE_CLICK_RADIUS": number;
};
setClickCount(button: number, context: InstanceType<typeof PointerSource.ClickContext>): number;
getClickCount(button: number): number;
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount(): void;
}
export declare class WheelSource {
type: SourceType.Wheel;
}
export type InputSource = NoneSource | KeySource | PointerSource | WheelSource;
export type InputSourceFor<Type extends SourceType> = Type extends SourceType.Key ? KeySource : Type extends SourceType.Pointer ? PointerSource : Type extends SourceType.Wheel ? WheelSource : NoneSource;

View File

@@ -0,0 +1,161 @@
"use strict";
/**
* 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.
*/
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.WheelSource = exports.PointerSource = exports.KeySource = exports.NoneSource = void 0;
class NoneSource {
type = "none" /* SourceType.None */;
}
exports.NoneSource = NoneSource;
class KeySource {
type = "key" /* SourceType.Key */;
pressed = new Set();
// This is a bitfield that matches the modifiers parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
#modifiers = 0;
get modifiers() {
return this.#modifiers;
}
get alt() {
return (this.#modifiers & 1) === 1;
}
set alt(value) {
this.#setModifier(value, 1);
}
get ctrl() {
return (this.#modifiers & 2) === 2;
}
set ctrl(value) {
this.#setModifier(value, 2);
}
get meta() {
return (this.#modifiers & 4) === 4;
}
set meta(value) {
this.#setModifier(value, 4);
}
get shift() {
return (this.#modifiers & 8) === 8;
}
set shift(value) {
this.#setModifier(value, 8);
}
#setModifier(value, bit) {
if (value) {
this.#modifiers |= bit;
}
else {
this.#modifiers &= ~bit;
}
}
}
exports.KeySource = KeySource;
class PointerSource {
type = "pointer" /* SourceType.Pointer */;
subtype;
pointerId;
pressed = new Set();
x = 0;
y = 0;
radiusX;
radiusY;
force;
constructor(id, subtype) {
this.pointerId = id;
this.subtype = subtype;
}
// This is a bitfield that matches the buttons parameter of
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
get buttons() {
let buttons = 0;
for (const button of this.pressed) {
switch (button) {
case 0:
buttons |= 1;
break;
case 1:
buttons |= 4;
break;
case 2:
buttons |= 2;
break;
case 3:
buttons |= 8;
break;
case 4:
buttons |= 16;
break;
}
}
return buttons;
}
// --- Platform-specific code starts here ---
// Input.dispatchMouseEvent doesn't know the concept of double click, so we
// need to create the logic, similar to how it's done for OSes:
// https://source.chromium.org/chromium/chromium/src/+/refs/heads/main:ui/events/event.cc;l=479
static ClickContext = class ClickContext {
static #DOUBLE_CLICK_TIME_MS = 500;
static #MAX_DOUBLE_CLICK_RADIUS = 2;
count = 0;
#x;
#y;
#time;
constructor(x, y, time) {
this.#x = x;
this.#y = y;
this.#time = time;
}
compare(context) {
return (
// The click needs to be within a certain amount of ms.
context.#time - this.#time > ClickContext.#DOUBLE_CLICK_TIME_MS ||
// The click needs to be within a certain square radius.
Math.abs(context.#x - this.#x) >
ClickContext.#MAX_DOUBLE_CLICK_RADIUS ||
Math.abs(context.#y - this.#y) > ClickContext.#MAX_DOUBLE_CLICK_RADIUS);
}
};
#clickContexts = new Map();
setClickCount(button, context) {
let storedContext = this.#clickContexts.get(button);
if (!storedContext || storedContext.compare(context)) {
storedContext = context;
}
++storedContext.count;
this.#clickContexts.set(button, storedContext);
return storedContext.count;
}
getClickCount(button) {
return this.#clickContexts.get(button)?.count ?? 0;
}
/**
* Resets click count. Resets consequent click counter. Prevents grouping clicks in
* different `performActions` calls, so that they are not grouped as double, triple etc
* clicks. Required for https://github.com/GoogleChromeLabs/chromium-bidi/issues/3043.
*/
resetClickCount() {
this.#clickContexts = new Map();
}
}
exports.PointerSource = PointerSource;
_a = PointerSource;
class WheelSource {
type = "wheel" /* SourceType.Wheel */;
}
exports.WheelSource = WheelSource;
//# sourceMappingURL=InputSource.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputSource.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputSource.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,29 @@
/**
* 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 { Input } from '../../../protocol/protocol.js';
import { Mutex } from '../../../utils/Mutex.js';
import type { ActionOption } from './ActionOption.js';
import { KeySource, PointerSource, SourceType, type InputSource, type InputSourceFor } from './InputSource.js';
export declare class InputState {
#private;
cancelList: ActionOption[];
getOrCreate(id: string, type: SourceType.Pointer, subtype: Input.PointerType): PointerSource;
getOrCreate<Type extends SourceType>(id: string, type: Type): InputSourceFor<Type>;
get(id: string): InputSource;
getGlobalKeyState(): KeySource;
get queue(): Mutex;
}

View File

@@ -0,0 +1,93 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputState = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const Mutex_js_1 = require("../../../utils/Mutex.js");
const InputSource_js_1 = require("./InputSource.js");
class InputState {
cancelList = [];
#sources = new Map();
#mutex = new Mutex_js_1.Mutex();
getOrCreate(id, type, subtype) {
let source = this.#sources.get(id);
if (!source) {
switch (type) {
case "none" /* SourceType.None */:
source = new InputSource_js_1.NoneSource();
break;
case "key" /* SourceType.Key */:
source = new InputSource_js_1.KeySource();
break;
case "pointer" /* SourceType.Pointer */: {
let pointerId = subtype === "mouse" /* Input.PointerType.Mouse */ ? 0 : 2;
const pointerIds = new Set();
for (const [, source] of this.#sources) {
if (source.type === "pointer" /* SourceType.Pointer */) {
pointerIds.add(source.pointerId);
}
}
while (pointerIds.has(pointerId)) {
++pointerId;
}
source = new InputSource_js_1.PointerSource(pointerId, subtype);
break;
}
case "wheel" /* SourceType.Wheel */:
source = new InputSource_js_1.WheelSource();
break;
default:
throw new protocol_js_1.InvalidArgumentException(`Expected "${"none" /* SourceType.None */}", "${"key" /* SourceType.Key */}", "${"pointer" /* SourceType.Pointer */}", or "${"wheel" /* SourceType.Wheel */}". Found unknown source type ${type}.`);
}
this.#sources.set(id, source);
return source;
}
if (source.type !== type) {
throw new protocol_js_1.InvalidArgumentException(`Input source type of ${id} is ${source.type}, but received ${type}.`);
}
return source;
}
get(id) {
const source = this.#sources.get(id);
if (!source) {
throw new protocol_js_1.UnknownErrorException(`Internal error.`);
}
return source;
}
getGlobalKeyState() {
const state = new InputSource_js_1.KeySource();
for (const [, source] of this.#sources) {
if (source.type !== "key" /* SourceType.Key */) {
continue;
}
for (const pressed of source.pressed) {
state.pressed.add(pressed);
}
state.alt ||= source.alt;
state.ctrl ||= source.ctrl;
state.meta ||= source.meta;
state.shift ||= source.shift;
}
return state;
}
get queue() {
return this.#mutex;
}
}
exports.InputState = InputState;
//# sourceMappingURL=InputState.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputState.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputState.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,21 @@
/**
* 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 type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import { InputState } from './InputState.js';
export declare class InputStateManager extends WeakMap<BrowsingContextImpl, InputState> {
get(context: BrowsingContextImpl): InputState;
}

View File

@@ -0,0 +1,34 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputStateManager = void 0;
const assert_js_1 = require("../../../utils/assert.js");
const InputState_js_1 = require("./InputState.js");
// We use a weak map here as specified here:
// https://www.w3.org/TR/webdriver/#dfn-browsing-context-input-state-map
class InputStateManager extends WeakMap {
get(context) {
(0, assert_js_1.assert)(context.isTopLevelContext());
if (!this.has(context)) {
this.set(context, new InputState_js_1.InputState());
}
return super.get(context);
}
}
exports.InputStateManager = InputStateManager;
//# sourceMappingURL=InputStateManager.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"InputStateManager.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/input/InputStateManager.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,wDAAgD;AAGhD,mDAA2C;AAE3C,4CAA4C;AAC5C,wEAAwE;AACxE,MAAa,iBAAkB,SAAQ,OAGtC;IACU,GAAG,CAAC,OAA4B;QACvC,IAAA,kBAAM,EAAC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;QAEpC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC;YACvB,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,IAAI,0BAAU,EAAE,CAAC,CAAC;QACtC,CAAC;QAED,OAAO,KAAK,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;IAC7B,CAAC;CACF;AAbD,8CAaC"}

View File

@@ -0,0 +1,17 @@
/**
* 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.
*/
export declare const KeyToKeyCode: Record<string, number | undefined>;

View File

@@ -0,0 +1,274 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.KeyToKeyCode = void 0;
// TODO: Remove this once https://crrev.com/c/4548290 is stably in Chromium.
// `Input.dispatchKeyboardEvent` will automatically handle these conversions.
exports.KeyToKeyCode = {
'0': 48,
'1': 49,
'2': 50,
'3': 51,
'4': 52,
'5': 53,
'6': 54,
'7': 55,
'8': 56,
'9': 57,
Abort: 3,
Help: 6,
Backspace: 8,
Tab: 9,
Numpad5: 12,
NumpadEnter: 13,
Enter: 13,
'\\r': 13,
'\\n': 13,
ShiftLeft: 16,
ShiftRight: 16,
ControlLeft: 17,
ControlRight: 17,
AltLeft: 18,
AltRight: 18,
Pause: 19,
CapsLock: 20,
Escape: 27,
Convert: 28,
NonConvert: 29,
Space: 32,
Numpad9: 33,
PageUp: 33,
Numpad3: 34,
PageDown: 34,
End: 35,
Numpad1: 35,
Home: 36,
Numpad7: 36,
ArrowLeft: 37,
Numpad4: 37,
Numpad8: 38,
ArrowUp: 38,
ArrowRight: 39,
Numpad6: 39,
Numpad2: 40,
ArrowDown: 40,
Select: 41,
Open: 43,
PrintScreen: 44,
Insert: 45,
Numpad0: 45,
Delete: 46,
NumpadDecimal: 46,
Digit0: 48,
Digit1: 49,
Digit2: 50,
Digit3: 51,
Digit4: 52,
Digit5: 53,
Digit6: 54,
Digit7: 55,
Digit8: 56,
Digit9: 57,
KeyA: 65,
KeyB: 66,
KeyC: 67,
KeyD: 68,
KeyE: 69,
KeyF: 70,
KeyG: 71,
KeyH: 72,
KeyI: 73,
KeyJ: 74,
KeyK: 75,
KeyL: 76,
KeyM: 77,
KeyN: 78,
KeyO: 79,
KeyP: 80,
KeyQ: 81,
KeyR: 82,
KeyS: 83,
KeyT: 84,
KeyU: 85,
KeyV: 86,
KeyW: 87,
KeyX: 88,
KeyY: 89,
KeyZ: 90,
MetaLeft: 91,
MetaRight: 92,
ContextMenu: 93,
NumpadMultiply: 106,
NumpadAdd: 107,
NumpadSubtract: 109,
NumpadDivide: 111,
F1: 112,
F2: 113,
F3: 114,
F4: 115,
F5: 116,
F6: 117,
F7: 118,
F8: 119,
F9: 120,
F10: 121,
F11: 122,
F12: 123,
F13: 124,
F14: 125,
F15: 126,
F16: 127,
F17: 128,
F18: 129,
F19: 130,
F20: 131,
F21: 132,
F22: 133,
F23: 134,
F24: 135,
NumLock: 144,
ScrollLock: 145,
AudioVolumeMute: 173,
AudioVolumeDown: 174,
AudioVolumeUp: 175,
MediaTrackNext: 176,
MediaTrackPrevious: 177,
MediaStop: 178,
MediaPlayPause: 179,
Semicolon: 186,
Equal: 187,
NumpadEqual: 187,
Comma: 188,
Minus: 189,
Period: 190,
Slash: 191,
Backquote: 192,
BracketLeft: 219,
Backslash: 220,
BracketRight: 221,
Quote: 222,
AltGraph: 225,
Props: 247,
Cancel: 3,
Clear: 12,
Shift: 16,
Control: 17,
Alt: 18,
Accept: 30,
ModeChange: 31,
' ': 32,
Print: 42,
Execute: 43,
'\\u0000': 46,
a: 65,
b: 66,
c: 67,
d: 68,
e: 69,
f: 70,
g: 71,
h: 72,
i: 73,
j: 74,
k: 75,
l: 76,
m: 77,
n: 78,
o: 79,
p: 80,
q: 81,
r: 82,
s: 83,
t: 84,
u: 85,
v: 86,
w: 87,
x: 88,
y: 89,
z: 90,
Meta: 91,
'*': 106,
'+': 107,
'-': 109,
'/': 111,
';': 186,
'=': 187,
',': 188,
'.': 190,
'`': 192,
'[': 219,
'\\\\': 220,
']': 221,
"'": 222,
Attn: 246,
CrSel: 247,
ExSel: 248,
EraseEof: 249,
Play: 250,
ZoomOut: 251,
')': 48,
'!': 49,
'@': 50,
'#': 51,
$: 52,
'%': 53,
'^': 54,
'&': 55,
'(': 57,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
':': 186,
'<': 188,
_: 189,
'>': 190,
'?': 191,
'~': 192,
'{': 219,
'|': 220,
'}': 221,
'"': 222,
Camera: 44,
EndCall: 95,
VolumeDown: 182,
VolumeUp: 183,
};
//# sourceMappingURL=USKeyboardLayout.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
/**
* 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.
*/
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
export declare function getNormalizedKey(value: string): string;
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
export declare function getKeyCode(key: string): string | undefined;
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
export declare function getKeyLocation(key: string): 0 | 1 | 2 | 3;

View File

@@ -0,0 +1,497 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.getNormalizedKey = getNormalizedKey;
exports.getKeyCode = getKeyCode;
exports.getKeyLocation = getKeyLocation;
/**
* Returns the normalized key value for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-normalized-key-value
*/
function getNormalizedKey(value) {
switch (value) {
case '\uE000':
return 'Unidentified';
case '\uE001':
return 'Cancel';
case '\uE002':
return 'Help';
case '\uE003':
return 'Backspace';
case '\uE004':
return 'Tab';
case '\uE005':
return 'Clear';
// Specification declares the '\uE006' to be `Return`, but it is not supported by
// Chrome, so fall back to `Enter`, which aligns with WPT.
case '\uE006':
case '\uE007':
return 'Enter';
case '\uE008':
return 'Shift';
case '\uE009':
return 'Control';
case '\uE00A':
return 'Alt';
case '\uE00B':
return 'Pause';
case '\uE00C':
return 'Escape';
case '\uE00D':
return ' ';
case '\uE00E':
return 'PageUp';
case '\uE00F':
return 'PageDown';
case '\uE010':
return 'End';
case '\uE011':
return 'Home';
case '\uE012':
return 'ArrowLeft';
case '\uE013':
return 'ArrowUp';
case '\uE014':
return 'ArrowRight';
case '\uE015':
return 'ArrowDown';
case '\uE016':
return 'Insert';
case '\uE017':
return 'Delete';
case '\uE018':
return ';';
case '\uE019':
return '=';
case '\uE01A':
return '0';
case '\uE01B':
return '1';
case '\uE01C':
return '2';
case '\uE01D':
return '3';
case '\uE01E':
return '4';
case '\uE01F':
return '5';
case '\uE020':
return '6';
case '\uE021':
return '7';
case '\uE022':
return '8';
case '\uE023':
return '9';
case '\uE024':
return '*';
case '\uE025':
return '+';
case '\uE026':
return ',';
case '\uE027':
return '-';
case '\uE028':
return '.';
case '\uE029':
return '/';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE03D':
return 'Meta';
case '\uE040':
return 'ZenkakuHankaku';
case '\uE050':
return 'Shift';
case '\uE051':
return 'Control';
case '\uE052':
return 'Alt';
case '\uE053':
return 'Meta';
case '\uE054':
return 'PageUp';
case '\uE055':
return 'PageDown';
case '\uE056':
return 'End';
case '\uE057':
return 'Home';
case '\uE058':
return 'ArrowLeft';
case '\uE059':
return 'ArrowUp';
case '\uE05A':
return 'ArrowRight';
case '\uE05B':
return 'ArrowDown';
case '\uE05C':
return 'Insert';
case '\uE05D':
return 'Delete';
default:
return value;
}
}
/**
* Returns the key code for a given key according to the table:
* https://w3c.github.io/webdriver/#dfn-shifted-character
*/
function getKeyCode(key) {
switch (key) {
case '`':
case '~':
return 'Backquote';
case '\\':
case '|':
return 'Backslash';
case '\uE003':
return 'Backspace';
case '[':
case '{':
return 'BracketLeft';
case ']':
case '}':
return 'BracketRight';
case ',':
case '<':
return 'Comma';
case '0':
case ')':
return 'Digit0';
case '1':
case '!':
return 'Digit1';
case '2':
case '@':
return 'Digit2';
case '3':
case '#':
return 'Digit3';
case '4':
case '$':
return 'Digit4';
case '5':
case '%':
return 'Digit5';
case '6':
case '^':
return 'Digit6';
case '7':
case '&':
return 'Digit7';
case '8':
case '*':
return 'Digit8';
case '9':
case '(':
return 'Digit9';
case '=':
case '+':
return 'Equal';
// The spec declares the '<' to be `IntlBackslash` as well, but it is already covered
// in the `Comma` above.
case '>':
return 'IntlBackslash';
case 'a':
case 'A':
return 'KeyA';
case 'b':
case 'B':
return 'KeyB';
case 'c':
case 'C':
return 'KeyC';
case 'd':
case 'D':
return 'KeyD';
case 'e':
case 'E':
return 'KeyE';
case 'f':
case 'F':
return 'KeyF';
case 'g':
case 'G':
return 'KeyG';
case 'h':
case 'H':
return 'KeyH';
case 'i':
case 'I':
return 'KeyI';
case 'j':
case 'J':
return 'KeyJ';
case 'k':
case 'K':
return 'KeyK';
case 'l':
case 'L':
return 'KeyL';
case 'm':
case 'M':
return 'KeyM';
case 'n':
case 'N':
return 'KeyN';
case 'o':
case 'O':
return 'KeyO';
case 'p':
case 'P':
return 'KeyP';
case 'q':
case 'Q':
return 'KeyQ';
case 'r':
case 'R':
return 'KeyR';
case 's':
case 'S':
return 'KeyS';
case 't':
case 'T':
return 'KeyT';
case 'u':
case 'U':
return 'KeyU';
case 'v':
case 'V':
return 'KeyV';
case 'w':
case 'W':
return 'KeyW';
case 'x':
case 'X':
return 'KeyX';
case 'y':
case 'Y':
return 'KeyY';
case 'z':
case 'Z':
return 'KeyZ';
case '-':
case '_':
return 'Minus';
case '.':
return 'Period';
case "'":
case '"':
return 'Quote';
case ';':
case ':':
return 'Semicolon';
case '/':
case '?':
return 'Slash';
case '\uE00A':
return 'AltLeft';
case '\uE052':
return 'AltRight';
case '\uE009':
return 'ControlLeft';
case '\uE051':
return 'ControlRight';
case '\uE006':
return 'Enter';
case '\uE00B':
return 'Pause';
case '\uE03D':
return 'MetaLeft';
case '\uE053':
return 'MetaRight';
case '\uE008':
return 'ShiftLeft';
case '\uE050':
return 'ShiftRight';
case ' ':
case '\uE00D':
return 'Space';
case '\uE004':
return 'Tab';
case '\uE017':
return 'Delete';
case '\uE010':
return 'End';
case '\uE002':
return 'Help';
case '\uE011':
return 'Home';
case '\uE016':
return 'Insert';
case '\uE00F':
return 'PageDown';
case '\uE00E':
return 'PageUp';
case '\uE015':
return 'ArrowDown';
case '\uE012':
return 'ArrowLeft';
case '\uE014':
return 'ArrowRight';
case '\uE013':
return 'ArrowUp';
case '\uE00C':
return 'Escape';
case '\uE031':
return 'F1';
case '\uE032':
return 'F2';
case '\uE033':
return 'F3';
case '\uE034':
return 'F4';
case '\uE035':
return 'F5';
case '\uE036':
return 'F6';
case '\uE037':
return 'F7';
case '\uE038':
return 'F8';
case '\uE039':
return 'F9';
case '\uE03A':
return 'F10';
case '\uE03B':
return 'F11';
case '\uE03C':
return 'F12';
case '\uE019':
return 'NumpadEqual';
case '\uE01A':
case '\uE05C':
return 'Numpad0';
case '\uE01B':
case '\uE056':
return 'Numpad1';
case '\uE01C':
case '\uE05B':
return 'Numpad2';
case '\uE01D':
case '\uE055':
return 'Numpad3';
case '\uE01E':
case '\uE058':
return 'Numpad4';
case '\uE01F':
return 'Numpad5';
case '\uE020':
case '\uE05A':
return 'Numpad6';
case '\uE021':
case '\uE057':
return 'Numpad7';
case '\uE022':
case '\uE059':
return 'Numpad8';
case '\uE023':
case '\uE054':
return 'Numpad9';
case '\uE025':
return 'NumpadAdd';
case '\uE026':
return 'NumpadComma';
case '\uE028':
case '\uE05D':
return 'NumpadDecimal';
case '\uE029':
return 'NumpadDivide';
case '\uE007':
return 'NumpadEnter';
case '\uE024':
return 'NumpadMultiply';
case '\uE027':
return 'NumpadSubtract';
default:
return;
}
}
/**
* Returns the location of the key according to the table:
* https://w3c.github.io/webdriver/#dfn-key-location
*/
function getKeyLocation(key) {
switch (key) {
case '\uE007':
case '\uE008':
case '\uE009':
case '\uE00A':
case '\uE03D':
return 1;
case '\uE019':
case '\uE01A':
case '\uE01B':
case '\uE01C':
case '\uE01D':
case '\uE01E':
case '\uE01F':
case '\uE020':
case '\uE021':
case '\uE022':
case '\uE023':
case '\uE024':
case '\uE025':
case '\uE026':
case '\uE027':
case '\uE028':
case '\uE029':
case '\uE054':
case '\uE055':
case '\uE056':
case '\uE057':
case '\uE058':
case '\uE059':
case '\uE05A':
case '\uE05B':
case '\uE05C':
case '\uE05D':
return 3;
case '\uE050':
case '\uE051':
case '\uE052':
case '\uE053':
return 2;
default:
return 0;
}
}
//# sourceMappingURL=keyUtils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,9 @@
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { RealmStorage } from '../script/RealmStorage.js';
import type { EventManager } from '../session/EventManager.js';
export declare class LogManager {
#private;
private constructor();
static create(cdpTarget: CdpTarget, realmStorage: RealmStorage, eventManager: EventManager, logger?: LoggerFn): LogManager;
}

View File

@@ -0,0 +1,187 @@
"use strict";
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.LogManager = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const log_js_1 = require("../../../utils/log.js");
const logHelper_js_1 = require("./logHelper.js");
/** Converts CDP StackTrace object to BiDi StackTrace object. */
function getBidiStackTrace(cdpStackTrace) {
const stackFrames = cdpStackTrace?.callFrames.map((callFrame) => {
return {
columnNumber: callFrame.columnNumber,
functionName: callFrame.functionName,
lineNumber: callFrame.lineNumber,
url: callFrame.url,
};
});
return stackFrames ? { callFrames: stackFrames } : undefined;
}
function getLogLevel(consoleApiType) {
if (["error" /* Log.Level.Error */, 'assert'].includes(consoleApiType)) {
return "error" /* Log.Level.Error */;
}
if (["debug" /* Log.Level.Debug */, 'trace'].includes(consoleApiType)) {
return "debug" /* Log.Level.Debug */;
}
if (["warn" /* Log.Level.Warn */, 'warning'].includes(consoleApiType)) {
return "warn" /* Log.Level.Warn */;
}
return "info" /* Log.Level.Info */;
}
function getLogMethod(consoleApiType) {
switch (consoleApiType) {
case 'warning':
return 'warn';
case 'startGroup':
return 'group';
case 'startGroupCollapsed':
return 'groupCollapsed';
case 'endGroup':
return 'groupEnd';
}
return consoleApiType;
}
class LogManager {
#eventManager;
#realmStorage;
#cdpTarget;
#logger;
constructor(cdpTarget, realmStorage, eventManager, logger) {
this.#cdpTarget = cdpTarget;
this.#realmStorage = realmStorage;
this.#eventManager = eventManager;
this.#logger = logger;
}
static create(cdpTarget, realmStorage, eventManager, logger) {
const logManager = new _a(cdpTarget, realmStorage, eventManager, logger);
logManager.#initializeEntryAddedEventListener();
return logManager;
}
/**
* Heuristic serialization of CDP remote object. If possible, return the BiDi value
* without deep serialization.
*/
async #heuristicSerializeArg(arg, realm) {
switch (arg.type) {
// TODO: Implement regexp, array, object, map and set heuristics base on
// preview.
case 'undefined':
return { type: 'undefined' };
case 'boolean':
return { type: 'boolean', value: arg.value };
case 'string':
return { type: 'string', value: arg.value };
case 'number':
// The value can be either a number or a string like `Infinity` or `-0`.
return { type: 'number', value: arg.unserializableValue ?? arg.value };
case 'bigint':
if (arg.unserializableValue !== undefined &&
arg.unserializableValue[arg.unserializableValue.length - 1] === 'n') {
return {
type: arg.type,
value: arg.unserializableValue.slice(0, -1),
};
}
// Unexpected bigint value, fall back to CDP deep serialization.
break;
case 'object':
if (arg.subtype === 'null') {
return { type: 'null' };
}
// Fall back to CDP deep serialization.
break;
default:
// Fall back to CDP deep serialization.
break;
}
// Fall back to CDP deep serialization.
return await realm.serializeCdpObject(arg, "none" /* Script.ResultOwnership.None */);
}
#initializeEntryAddedEventListener() {
this.#cdpTarget.cdpClient.on('Runtime.consoleAPICalled', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(log_js_1.LogType.cdp, params);
return;
}
const argsPromise = Promise.all(params.args.map((arg) => this.#heuristicSerializeArg(arg, realm)));
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(argsPromise.then((args) => ({
kind: 'success',
value: {
type: 'event',
method: protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: getLogLevel(params.type),
source: realm.source,
text: (0, logHelper_js_1.getRemoteValuesText)(args, true),
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.stackTrace),
type: 'console',
method: getLogMethod(params.type),
args,
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
this.#cdpTarget.cdpClient.on('Runtime.exceptionThrown', (params) => {
// Try to find realm by `cdpSessionId` and `executionContextId`,
// if provided.
const realm = this.#realmStorage.findRealm({
cdpSessionId: this.#cdpTarget.cdpSessionId,
executionContextId: params.exceptionDetails.executionContextId,
});
if (realm === undefined) {
// Ignore exceptions not attached to any realm.
this.#logger?.(log_js_1.LogType.cdp, params);
return;
}
for (const browsingContext of realm.associatedBrowsingContexts) {
this.#eventManager.registerPromiseEvent(_a.#getExceptionText(params, realm).then((text) => ({
kind: 'success',
value: {
type: 'event',
method: protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded,
params: {
level: "error" /* Log.Level.Error */,
source: realm.source,
text,
timestamp: Math.round(params.timestamp),
stackTrace: getBidiStackTrace(params.exceptionDetails.stackTrace),
type: 'javascript',
},
},
}), (error) => ({
kind: 'error',
error,
})), browsingContext.id, protocol_js_1.ChromiumBidi.Log.EventNames.LogEntryAdded);
}
});
}
/**
* Try the best to get the exception text.
*/
static async #getExceptionText(params, realm) {
if (!params.exceptionDetails.exception) {
return params.exceptionDetails.text;
}
if (realm === undefined) {
return JSON.stringify(params.exceptionDetails.exception);
}
return await realm.stringifyObject(params.exceptionDetails.exception);
}
}
exports.LogManager = LogManager;
_a = LogManager;
//# sourceMappingURL=LogManager.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2022 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 type { Script } from '../../../protocol/protocol.js';
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
export declare function logMessageFormatter(args: Script.RemoteValue[]): string;
export declare function getRemoteValuesText(args: Script.RemoteValue[], formatText: boolean): string;

View File

@@ -0,0 +1,172 @@
"use strict";
/**
* Copyright 2022 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.logMessageFormatter = logMessageFormatter;
exports.getRemoteValuesText = getRemoteValuesText;
const assert_js_1 = require("../../../utils/assert.js");
const specifiers = ['%s', '%d', '%i', '%f', '%o', '%O', '%c'];
function isFormatSpecifier(str) {
return specifiers.some((spec) => str.includes(spec));
}
/**
* @param args input remote values to be format printed
* @return parsed text of the remote values in specific format
*/
function logMessageFormatter(args) {
let output = '';
const argFormat = args[0].value.toString();
const argValues = args.slice(1, undefined);
const tokens = argFormat.split(new RegExp(specifiers.map((spec) => `(${spec})`).join('|'), 'g'));
for (const token of tokens) {
if (token === undefined || token === '') {
continue;
}
if (isFormatSpecifier(token)) {
const arg = argValues.shift();
// raise an exception when less value is provided
(0, assert_js_1.assert)(arg, `Less value is provided: "${getRemoteValuesText(args, false)}"`);
if (token === '%s') {
output += stringFromArg(arg);
}
else if (token === '%d' || token === '%i') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseInt(arg.value.toString(), 10);
}
else {
output += 'NaN';
}
}
else if (token === '%f') {
if (arg.type === 'bigint' ||
arg.type === 'number' ||
arg.type === 'string') {
output += parseFloat(arg.value.toString());
}
else {
output += 'NaN';
}
}
else {
// %o, %O, %c
output += toJson(arg);
}
}
else {
output += token;
}
}
// raise an exception when more value is provided
if (argValues.length > 0) {
throw new Error(`More value is provided: "${getRemoteValuesText(args, false)}"`);
}
return output;
}
/**
* @param arg input remote value to be parsed
* @return parsed text of the remote value
*
* input: {"type": "number", "value": 1}
* output: 1
*
* input: {"type": "string", "value": "abc"}
* output: "abc"
*
* input: {"type": "object", "value": [["id", {"type": "number", "value": 1}]]}
* output: '{"id": 1}'
*
* input: {"type": "object", "value": [["font-size", {"type": "string", "value": "20px"}]]}
* output: '{"font-size": "20px"}'
*/
function toJson(arg) {
// arg type validation
if (arg.type !== 'array' &&
arg.type !== 'bigint' &&
arg.type !== 'date' &&
arg.type !== 'number' &&
arg.type !== 'object' &&
arg.type !== 'string') {
return stringFromArg(arg);
}
if (arg.type === 'bigint') {
return `${arg.value.toString()}n`;
}
if (arg.type === 'number') {
return arg.value.toString();
}
if (['date', 'string'].includes(arg.type)) {
return JSON.stringify(arg.value);
}
if (arg.type === 'object') {
return `{${arg.value
.map((pair) => {
return `${JSON.stringify(pair[0])}:${toJson(pair[1])}`;
})
.join(',')}}`;
}
if (arg.type === 'array') {
return `[${arg.value?.map((val) => toJson(val)).join(',') ?? ''}]`;
}
throw Error(`Invalid value type: ${arg}`);
}
function stringFromArg(arg) {
if (!Object.hasOwn(arg, 'value')) {
return arg.type;
}
switch (arg.type) {
case 'string':
case 'number':
case 'boolean':
case 'bigint':
return String(arg.value);
case 'regexp':
return `/${arg.value.pattern}/${arg.value.flags ?? ''}`;
case 'date':
return new Date(arg.value).toString();
case 'object':
return `Object(${arg.value?.length ?? ''})`;
case 'array':
return `Array(${arg.value?.length ?? ''})`;
case 'map':
return `Map(${arg.value?.length})`;
case 'set':
return `Set(${arg.value?.length})`;
default:
return arg.type;
}
}
function getRemoteValuesText(args, formatText) {
const arg = args[0];
if (!arg) {
return '';
}
// if args[0] is a format specifier, format the args as output
if (arg.type === 'string' &&
isFormatSpecifier(arg.value.toString()) &&
formatText) {
return logMessageFormatter(args);
}
// if args[0] is not a format specifier, just join the args with \u0020 (unicode 'SPACE')
return args
.map((arg) => {
return stringFromArg(arg);
})
.join('\u0020');
}
//# sourceMappingURL=logHelper.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"logHelper.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/log/logHelper.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,13 @@
import type { Browser, BrowsingContext } from '../../../protocol/generated/webdriver-bidi.js';
import { Network } from '../../../protocol/generated/webdriver-bidi.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { NetworkRequest } from './NetworkRequest.js';
export declare class CollectorsStorage {
#private;
constructor(maxEncodedDataSize: number, logger?: LoggerFn);
addDataCollector(params: Network.AddDataCollectorParameters): `${string}-${string}-${string}-${string}-${string}`;
isCollected(requestId: Network.Request, dataType?: Network.DataType, collectorId?: string): boolean;
disownData(requestId: Network.Request, dataType: Network.DataType, collectorId?: string): void;
collectIfNeeded(request: NetworkRequest, dataType: Network.DataType, topLevelBrowsingContext: BrowsingContext.BrowsingContext, userContext: Browser.UserContext): void;
removeDataCollector(collectorId: Network.Collector): Network.Request[];
}

View File

@@ -0,0 +1,153 @@
"use strict";
/*
* Copyright 2025 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.CollectorsStorage = void 0;
const ErrorResponse_js_1 = require("../../../protocol/ErrorResponse.js");
const log_js_1 = require("../../../utils/log.js");
const uuid_js_1 = require("../../../utils/uuid.js");
class CollectorsStorage {
#collectors = new Map();
#responseCollectors = new Map();
#requestBodyCollectors = new Map();
#maxEncodedDataSize;
#logger;
constructor(maxEncodedDataSize, logger) {
this.#maxEncodedDataSize = maxEncodedDataSize;
this.#logger = logger;
}
addDataCollector(params) {
if (params.maxEncodedDataSize < 1 ||
params.maxEncodedDataSize > this.#maxEncodedDataSize) {
// 200 MB is the default limit in CDP:
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;drc=da1f749634c9a401cc756f36c2e6ce233e1c9b4d;l=133
throw new ErrorResponse_js_1.InvalidArgumentException(`Max encoded data size should be between 1 and ${this.#maxEncodedDataSize}`);
}
const collectorId = (0, uuid_js_1.uuidv4)();
this.#collectors.set(collectorId, params);
return collectorId;
}
isCollected(requestId, dataType, collectorId) {
if (collectorId !== undefined && !this.#collectors.has(collectorId)) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (dataType === undefined) {
return (this.isCollected(requestId, "response" /* Network.DataType.Response */, collectorId) ||
this.isCollected(requestId, "request" /* Network.DataType.Request */, collectorId));
}
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType).get(requestId);
if (requestToCollectorsMap === undefined ||
requestToCollectorsMap.size === 0) {
return false;
}
if (collectorId === undefined) {
// There is at least 1 collector for the data.
return true;
}
if (!requestToCollectorsMap.has(collectorId)) {
return false;
}
return true;
}
#getRequestToCollectorMap(dataType) {
switch (dataType) {
case "response" /* Network.DataType.Response */:
return this.#responseCollectors;
case "request" /* Network.DataType.Request */:
return this.#requestBodyCollectors;
default:
throw new ErrorResponse_js_1.UnsupportedOperationException(`Unsupported data type ${dataType}`);
}
}
disownData(requestId, dataType, collectorId) {
const requestToCollectorsMap = this.#getRequestToCollectorMap(dataType);
if (collectorId !== undefined) {
requestToCollectorsMap.get(requestId)?.delete(collectorId);
}
if (collectorId === undefined ||
requestToCollectorsMap.get(requestId)?.size === 0) {
requestToCollectorsMap.delete(requestId);
}
}
#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext) {
const collector = this.#collectors.get(collectorId);
if (collector === undefined) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Unknown collector ${collectorId}`);
}
if (collector.userContexts &&
!collector.userContexts.includes(userContext)) {
// Collector is aimed for a different user context.
return false;
}
if (collector.contexts &&
!collector.contexts.includes(topLevelBrowsingContext)) {
// Collector is aimed for a different top-level browsing context.
return false;
}
if (!collector.dataTypes.includes(dataType)) {
// Collector is aimed for a different data type.
return false;
}
if (dataType === "request" /* Network.DataType.Request */ &&
request.bodySize > collector.maxEncodedDataSize) {
this.#logger?.(log_js_1.LogType.debug, `Request's ${request.id} body size is too big for the collector ${collectorId}`);
return false;
}
if (dataType === "response" /* Network.DataType.Response */ &&
request.encodedResponseBodySize > collector.maxEncodedDataSize) {
this.#logger?.(log_js_1.LogType.debug, `Request's ${request.id} response is too big for the collector ${collectorId}`);
return false;
}
this.#logger?.(log_js_1.LogType.debug, `Collector ${collectorId} collected ${dataType} of ${request.id}`);
return true;
}
collectIfNeeded(request, dataType, topLevelBrowsingContext, userContext) {
const collectorIds = [...this.#collectors.keys()].filter((collectorId) => this.#shouldCollectRequest(collectorId, request, dataType, topLevelBrowsingContext, userContext));
if (collectorIds.length > 0) {
this.#getRequestToCollectorMap(dataType).set(request.id, new Set(collectorIds));
}
}
removeDataCollector(collectorId) {
if (!this.#collectors.has(collectorId)) {
throw new ErrorResponse_js_1.NoSuchNetworkCollectorException(`Collector ${collectorId} does not exist`);
}
this.#collectors.delete(collectorId);
const affectedRequests = [];
// Clean up collected responses.
for (const [requestId, collectorIds] of this.#responseCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#responseCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
for (const [requestId, collectorIds] of this.#requestBodyCollectors) {
if (collectorIds.has(collectorId)) {
collectorIds.delete(collectorId);
if (collectorIds.size === 0) {
this.#requestBodyCollectors.delete(requestId);
affectedRequests.push(requestId);
}
}
}
return affectedRequests;
}
}
exports.CollectorsStorage = CollectorsStorage;
//# sourceMappingURL=CollectorsStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"CollectorsStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/network/CollectorsStorage.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAEH,yEAI4C;AAM5C,kDAA6D;AAC7D,oDAA8C;AAM9C,MAAa,iBAAiB;IACnB,WAAW,GAAG,IAAI,GAAG,EAA4B,CAAC;IAClD,mBAAmB,GAAG,IAAI,GAAG,EAAgC,CAAC;IAC9D,sBAAsB,GAAG,IAAI,GAAG,EAAgC,CAAC;IACjE,mBAAmB,CAAS;IAC5B,OAAO,CAAY;IAE5B,YAAY,kBAA0B,EAAE,MAAiB;QACvD,IAAI,CAAC,mBAAmB,GAAG,kBAAkB,CAAC;QAC9C,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;IACxB,CAAC;IAED,gBAAgB,CAAC,MAA0C;QACzD,IACE,MAAM,CAAC,kBAAkB,GAAG,CAAC;YAC7B,MAAM,CAAC,kBAAkB,GAAG,IAAI,CAAC,mBAAmB,EACpD,CAAC;YACD,sCAAsC;YACtC,mLAAmL;YACnL,MAAM,IAAI,2CAAwB,CAChC,iDAAiD,IAAI,CAAC,mBAAmB,EAAE,CAC5E,CAAC;QACJ,CAAC;QACD,MAAM,WAAW,GAAG,IAAA,gBAAM,GAAE,CAAC;QAC7B,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;QAC1C,OAAO,WAAW,CAAC;IACrB,CAAC;IAED,WAAW,CACT,SAA0B,EAC1B,QAA2B,EAC3B,WAAoB;QAEpB,IAAI,WAAW,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACpE,MAAM,IAAI,kDAA+B,CACvC,qBAAqB,WAAW,EAAE,CACnC,CAAC;QACJ,CAAC;QAED,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC3B,OAAO,CACL,IAAI,CAAC,WAAW,CAAC,SAAS,8CAA6B,WAAW,CAAC;gBACnE,IAAI,CAAC,WAAW,CAAC,SAAS,4CAA4B,WAAW,CAAC,CACnE,CAAC;QACJ,CAAC;QAED,MAAM,sBAAsB,GAC1B,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAE1D,IACE,sBAAsB,KAAK,SAAS;YACpC,sBAAsB,CAAC,IAAI,KAAK,CAAC,EACjC,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,8CAA8C;YAC9C,OAAO,IAAI,CAAC;QACd,CAAC;QAED,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YAC7C,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,yBAAyB,CAAC,QAA0B;QAClD,QAAQ,QAAQ,EAAE,CAAC;YACjB;gBACE,OAAO,IAAI,CAAC,mBAAmB,CAAC;YAClC;gBACE,OAAO,IAAI,CAAC,sBAAsB,CAAC;YACrC;gBACE,MAAM,IAAI,gDAA6B,CACrC,yBAAyB,QAAQ,EAAE,CACpC,CAAC;QACN,CAAC;IACH,CAAC;IAED,UAAU,CACR,SAA0B,EAC1B,QAA0B,EAC1B,WAAoB;QAEpB,MAAM,sBAAsB,GAAG,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC;QACxE,IAAI,WAAW,KAAK,SAAS,EAAE,CAAC;YAC9B,sBAAsB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,WAAW,CAAC,CAAC;QAC7D,CAAC;QACD,IACE,WAAW,KAAK,SAAS;YACzB,sBAAsB,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,IAAI,KAAK,CAAC,EACjD,CAAC;YACD,sBAAsB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,qBAAqB,CACnB,WAAmB,EACnB,OAAuB,EACvB,QAA0B,EAC1B,uBAAwD,EACxD,WAAgC;QAEhC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,CAAC;QAEpD,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;YAC5B,MAAM,IAAI,kDAA+B,CACvC,qBAAqB,WAAW,EAAE,CACnC,CAAC;QACJ,CAAC;QACD,IACE,SAAS,CAAC,YAAY;YACtB,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,WAAW,CAAC,EAC7C,CAAC;YACD,mDAAmD;YACnD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IACE,SAAS,CAAC,QAAQ;YAClB,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EACrD,CAAC;YACD,iEAAiE;YACjE,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC5C,gDAAgD;YAChD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IACE,QAAQ,6CAA6B;YACrC,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC,kBAAkB,EAC/C,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CACZ,gBAAO,CAAC,KAAK,EACb,aAAa,OAAO,CAAC,EAAE,2CAA2C,WAAW,EAAE,CAChF,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IACE,QAAQ,+CAA8B;YACtC,OAAO,CAAC,uBAAuB,GAAG,SAAS,CAAC,kBAAkB,EAC9D,CAAC;YACD,IAAI,CAAC,OAAO,EAAE,CACZ,gBAAO,CAAC,KAAK,EACb,aAAa,OAAO,CAAC,EAAE,0CAA0C,WAAW,EAAE,CAC/E,CAAC;YACF,OAAO,KAAK,CAAC;QACf,CAAC;QAED,IAAI,CAAC,OAAO,EAAE,CACZ,gBAAO,CAAC,KAAK,EACb,aAAa,WAAW,cAAc,QAAQ,OAAO,OAAO,CAAC,EAAE,EAAE,CAClE,CAAC;QACF,OAAO,IAAI,CAAC;IACd,CAAC;IAED,eAAe,CACb,OAAuB,EACvB,QAA0B,EAC1B,uBAAwD,EACxD,WAAgC;QAEhC,MAAM,YAAY,GAAG,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,WAAW,EAAE,EAAE,CACvE,IAAI,CAAC,qBAAqB,CACxB,WAAW,EACX,OAAO,EACP,QAAQ,EACR,uBAAuB,EACvB,WAAW,CACZ,CACF,CAAC;QACF,IAAI,YAAY,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC5B,IAAI,CAAC,yBAAyB,CAAC,QAAQ,CAAC,CAAC,GAAG,CAC1C,OAAO,CAAC,EAAE,EACV,IAAI,GAAG,CAAC,YAAY,CAAC,CACtB,CAAC;QACJ,CAAC;IACH,CAAC;IAED,mBAAmB,CAAC,WAA8B;QAChD,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;YACvC,MAAM,IAAI,kDAA+B,CACvC,aAAa,WAAW,iBAAiB,CAC1C,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;QAErC,MAAM,gBAAgB,GAAG,EAAE,CAAC;QAC5B,gCAAgC;QAChC,KAAK,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,IAAI,IAAI,CAAC,mBAAmB,EAAE,CAAC;YACjE,IAAI,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;gBACjC,IAAI,YAAY,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;oBAC5B,IAAI,CAAC,mBAAmB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBAC3C,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QACD,KAAK,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,IAAI,IAAI,CAAC,sBAAsB,EAAE,CAAC;YACpE,IAAI,YAAY,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,CAAC;gBAClC,YAAY,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;gBACjC,IAAI,YAAY,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;oBAC5B,IAAI,CAAC,sBAAsB,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;oBAC9C,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBACnC,CAAC;YACH,CAAC;QACH,CAAC;QACD,OAAO,gBAAgB,CAAC;IAC1B,CAAC;CACF;AArND,8CAqNC"}

View File

@@ -0,0 +1,54 @@
/**
* 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 type { Protocol } from 'devtools-protocol';
import { Network, type EmptyResult } from '../../../protocol/protocol.js';
import type { ContextConfigStorage } from '../browser/ContextConfigStorage.js';
import type { UserContextStorage } from '../browser/UserContextStorage.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { NetworkStorage } from './NetworkStorage.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
/** Dispatches Network module commands. */
export declare class NetworkProcessor {
#private;
constructor(browsingContextStorage: BrowsingContextStorage, networkStorage: NetworkStorage, userContextStorage: UserContextStorage, contextConfigStorage: ContextConfigStorage);
addIntercept(params: Network.AddInterceptParameters): Promise<Network.AddInterceptResult>;
continueRequest(params: Network.ContinueRequestParameters): Promise<EmptyResult>;
continueResponse(params: Network.ContinueResponseParameters): Promise<EmptyResult>;
continueWithAuth(params: Network.ContinueWithAuthParameters): Promise<EmptyResult>;
failRequest({ request: networkId, }: Network.FailRequestParameters): Promise<EmptyResult>;
provideResponse(params: Network.ProvideResponseParameters): Promise<EmptyResult>;
removeIntercept(params: Network.RemoveInterceptParameters): Promise<EmptyResult>;
setCacheBehavior(params: Network.SetCacheBehaviorParameters): Promise<EmptyResult>;
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers: Network.Header[]): void;
static isMethodValid(method: string): boolean;
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url: string): URL;
static parseUrlPatterns(urlPatterns: Network.UrlPattern[]): ParsedUrlPattern[];
static wrapInterceptionError(error: any): any;
addDataCollector(params: Network.AddDataCollectorParameters): Promise<Network.AddDataCollectorResult>;
getData(params: Network.GetDataParameters): Promise<Network.GetDataResult>;
removeDataCollector(params: Network.RemoveDataCollectorParameters): Promise<EmptyResult>;
disownData(params: Network.DisownDataParameters): EmptyResult;
setExtraHeaders(params: Network.SetExtraHeadersParameters): Promise<EmptyResult>;
}
export declare function parseBiDiHeaders(headers: Network.Header[]): Protocol.Network.Headers;

View File

@@ -0,0 +1,546 @@
"use strict";
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkProcessor = void 0;
exports.parseBiDiHeaders = parseBiDiHeaders;
const protocol_js_1 = require("../../../protocol/protocol.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
/** Dispatches Network module commands. */
class NetworkProcessor {
#browsingContextStorage;
#networkStorage;
#userContextStorage;
#contextConfigStorage;
constructor(browsingContextStorage, networkStorage, userContextStorage, contextConfigStorage) {
this.#userContextStorage = userContextStorage;
this.#browsingContextStorage = browsingContextStorage;
this.#networkStorage = networkStorage;
this.#contextConfigStorage = contextConfigStorage;
}
async addIntercept(params) {
this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
const urlPatterns = params.urlPatterns ?? [];
const parsedUrlPatterns = NetworkProcessor.parseUrlPatterns(urlPatterns);
const intercept = this.#networkStorage.addIntercept({
urlPatterns: parsedUrlPatterns,
phases: params.phases,
contexts: params.contexts,
});
// Adding interception may require enabling CDP Network domains.
await this.#toggleNetwork();
return {
intercept,
};
}
async continueRequest(params) {
if (params.url !== undefined) {
NetworkProcessor.parseUrlString(params.url);
}
if (params.method !== undefined) {
if (!NetworkProcessor.isMethodValid(params.method)) {
throw new protocol_js_1.InvalidArgumentException(`Method '${params.method}' is invalid.`);
}
}
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
]);
try {
await request.continueRequest(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
]);
try {
await request.continueResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
async continueWithAuth(params) {
const networkId = params.request;
const request = this.#getBlockedRequestOrFail(networkId, [
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
await request.continueWithAuth(params);
return {};
}
async failRequest({ request: networkId, }) {
const request = this.#getRequestOrFail(networkId);
if (request.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
throw new protocol_js_1.InvalidArgumentException(`Request '${networkId}' in 'authRequired' phase cannot be failed`);
}
if (!request.interceptPhase) {
throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${networkId}'`);
}
await request.failRequest('Failed');
return {};
}
async provideResponse(params) {
if (params.headers) {
NetworkProcessor.validateHeaders(params.headers);
}
const request = this.#getBlockedRequestOrFail(params.request, [
"beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */,
"responseStarted" /* Network.InterceptPhase.ResponseStarted */,
"authRequired" /* Network.InterceptPhase.AuthRequired */,
]);
try {
await request.provideResponse(params);
}
catch (error) {
throw NetworkProcessor.wrapInterceptionError(error);
}
return {};
}
/**
* In some states CDP Network and Fetch domains are not required, but in some they have
* to be updated. Whenever potential change in these kinds of states is introduced,
* update the states of all the CDP targets.
*/
async #toggleNetwork() {
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleNetwork();
}));
}
async removeIntercept(params) {
this.#networkStorage.removeIntercept(params.intercept);
// Removing interception may allow for disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
async setCacheBehavior(params) {
const contexts = this.#browsingContextStorage.verifyTopLevelContextsList(params.contexts);
// Change all targets
if (contexts.size === 0) {
this.#networkStorage.defaultCacheBehavior = params.cacheBehavior;
await Promise.all(this.#browsingContextStorage.getAllContexts().map((context) => {
return context.cdpTarget.toggleSetCacheDisabled();
}));
return {};
}
const cacheDisabled = params.cacheBehavior === 'bypass';
await Promise.all([...contexts.values()].map((context) => {
return context.cdpTarget.toggleSetCacheDisabled(cacheDisabled);
}));
return {};
}
#getRequestOrFail(id) {
const request = this.#networkStorage.getRequestById(id);
if (!request) {
throw new protocol_js_1.NoSuchRequestException(`Network request with ID '${id}' doesn't exist`);
}
return request;
}
#getBlockedRequestOrFail(id, phases) {
const request = this.#getRequestOrFail(id);
if (!request.interceptPhase) {
throw new protocol_js_1.NoSuchRequestException(`No blocked request found for network id '${id}'`);
}
if (request.interceptPhase && !phases.includes(request.interceptPhase)) {
throw new protocol_js_1.InvalidArgumentException(`Blocked request for network id '${id}' is in '${request.interceptPhase}' phase`);
}
return request;
}
/**
* Validate https://fetch.spec.whatwg.org/#header-value
*/
static validateHeaders(headers) {
for (const header of headers) {
let headerValue;
if (header.value.type === 'string') {
headerValue = header.value.value;
}
else {
headerValue = atob(header.value.value);
}
if (headerValue !== headerValue.trim() ||
headerValue.includes('\n') ||
headerValue.includes('\0')) {
throw new protocol_js_1.InvalidArgumentException(`Header value '${headerValue}' is not acceptable value`);
}
}
}
static isMethodValid(method) {
// https://httpwg.org/specs/rfc9110.html#method.overview
return /^[!#$%&'*+\-.^_`|~a-zA-Z\d]+$/.test(method);
}
/**
* Attempts to parse the given url.
* Throws an InvalidArgumentException if the url is invalid.
*/
static parseUrlString(url) {
try {
return new URL(url);
}
catch (error) {
throw new protocol_js_1.InvalidArgumentException(`Invalid URL '${url}': ${error}`);
}
}
static parseUrlPatterns(urlPatterns) {
return urlPatterns.map((urlPattern) => {
let patternUrl = '';
let hasProtocol = true;
let hasHostname = true;
let hasPort = true;
let hasPathname = true;
let hasSearch = true;
switch (urlPattern.type) {
case 'string': {
patternUrl = unescapeURLPattern(urlPattern.pattern);
break;
}
case 'pattern': {
if (urlPattern.protocol === undefined) {
hasProtocol = false;
patternUrl += 'http';
}
else {
if (urlPattern.protocol === '') {
throw new protocol_js_1.InvalidArgumentException('URL pattern must specify a protocol');
}
urlPattern.protocol = unescapeURLPattern(urlPattern.protocol);
if (!urlPattern.protocol.match(/^[a-zA-Z+-.]+$/)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.protocol;
}
const scheme = patternUrl.toLocaleLowerCase();
patternUrl += ':';
if ((0, NetworkUtils_js_1.isSpecialScheme)(scheme)) {
patternUrl += '//';
}
if (urlPattern.hostname === undefined) {
if (scheme !== 'file') {
patternUrl += 'placeholder';
}
hasHostname = false;
}
else {
if (urlPattern.hostname === '') {
throw new protocol_js_1.InvalidArgumentException('URL pattern must specify a hostname');
}
if (urlPattern.protocol === 'file') {
throw new protocol_js_1.InvalidArgumentException(`URL pattern protocol cannot be 'file'`);
}
urlPattern.hostname = unescapeURLPattern(urlPattern.hostname);
let insideBrackets = false;
for (const c of urlPattern.hostname) {
if (c === '/' || c === '?' || c === '#') {
throw new protocol_js_1.InvalidArgumentException(`'/', '?', '#' are forbidden in hostname`);
}
if (!insideBrackets && c === ':') {
throw new protocol_js_1.InvalidArgumentException(`':' is only allowed inside brackets in hostname`);
}
if (c === '[') {
insideBrackets = true;
}
if (c === ']') {
insideBrackets = false;
}
}
patternUrl += urlPattern.hostname;
}
if (urlPattern.port === undefined) {
hasPort = false;
}
else {
if (urlPattern.port === '') {
throw new protocol_js_1.InvalidArgumentException(`URL pattern must specify a port`);
}
urlPattern.port = unescapeURLPattern(urlPattern.port);
patternUrl += ':';
if (!urlPattern.port.match(/^\d+$/)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.port;
}
if (urlPattern.pathname === undefined) {
hasPathname = false;
}
else {
urlPattern.pathname = unescapeURLPattern(urlPattern.pathname);
if (urlPattern.pathname[0] !== '/') {
patternUrl += '/';
}
if (urlPattern.pathname.includes('#') ||
urlPattern.pathname.includes('?')) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.pathname;
}
if (urlPattern.search === undefined) {
hasSearch = false;
}
else {
urlPattern.search = unescapeURLPattern(urlPattern.search);
if (urlPattern.search[0] !== '?') {
patternUrl += '?';
}
if (urlPattern.search.includes('#')) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
patternUrl += urlPattern.search;
}
break;
}
}
const serializePort = (url) => {
const defaultPorts = {
'ftp:': 21,
'file:': null,
'http:': 80,
'https:': 443,
'ws:': 80,
'wss:': 443,
};
if ((0, NetworkUtils_js_1.isSpecialScheme)(url.protocol) &&
defaultPorts[url.protocol] !== null &&
(!url.port || String(defaultPorts[url.protocol]) === url.port)) {
return '';
}
else if (url.port) {
return url.port;
}
return undefined;
};
try {
const url = new URL(patternUrl);
return {
protocol: hasProtocol ? url.protocol.replace(/:$/, '') : undefined,
hostname: hasHostname ? url.hostname : undefined,
port: hasPort ? serializePort(url) : undefined,
pathname: hasPathname && url.pathname ? url.pathname : undefined,
search: hasSearch ? url.search : undefined,
};
}
catch (err) {
throw new protocol_js_1.InvalidArgumentException(`${err.message} '${patternUrl}'`);
}
});
}
static wrapInterceptionError(error) {
// https://source.chromium.org/chromium/chromium/src/+/main:content/browser/devtools/protocol/fetch_handler.cc;l=169
if (error?.message.includes('Invalid header') ||
error?.message.includes('Unsafe header')) {
return new protocol_js_1.InvalidArgumentException(error.message);
}
return error;
}
async addDataCollector(params) {
if (params.userContexts !== undefined && params.contexts !== undefined) {
throw new protocol_js_1.InvalidArgumentException("'contexts' and 'userContexts' are mutually exclusive");
}
if (params.userContexts !== undefined) {
// Assert the user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(params.userContexts);
}
if (params.contexts !== undefined) {
for (const browsingContextId of params.contexts) {
// Assert the browsing context exists and are top-level.
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException(`Data collectors are available only on top-level browsing contexts`);
}
}
}
const collectorId = this.#networkStorage.addDataCollector(params);
// Adding data collectors may require enabling CDP Network domains.
await this.#toggleNetwork();
return { collector: collectorId };
}
async getData(params) {
return await this.#networkStorage.getCollectedData(params);
}
async removeDataCollector(params) {
this.#networkStorage.removeDataCollector(params);
// Removing data collectors may allow disabling CDP Network domains.
await this.#toggleNetwork();
return {};
}
disownData(params) {
this.#networkStorage.disownData(params);
return {};
}
async #getRelatedTopLevelBrowsingContexts(browsingContextIds, userContextIds) {
// Duplicated with EmulationProcessor logic. Consider moving to ConfigStorage.
if (browsingContextIds === undefined && userContextIds === undefined) {
return this.#browsingContextStorage.getTopLevelContexts();
}
if (browsingContextIds !== undefined && userContextIds !== undefined) {
throw new protocol_js_1.InvalidArgumentException('User contexts and browsing contexts are mutually exclusive');
}
const result = [];
if (userContextIds !== undefined) {
if (userContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('user context should be provided');
}
// Verify that all user contexts exist.
await this.#userContextStorage.verifyUserContextIdList(userContextIds);
for (const userContextId of userContextIds) {
const topLevelBrowsingContexts = this.#browsingContextStorage
.getTopLevelContexts()
.filter((browsingContext) => browsingContext.userContext === userContextId);
result.push(...topLevelBrowsingContexts);
}
}
if (browsingContextIds !== undefined) {
if (browsingContextIds.length === 0) {
throw new protocol_js_1.InvalidArgumentException('browsing context should be provided');
}
for (const browsingContextId of browsingContextIds) {
const browsingContext = this.#browsingContextStorage.getContext(browsingContextId);
if (!browsingContext.isTopLevelContext()) {
throw new protocol_js_1.InvalidArgumentException('The command is only supported on the top-level context');
}
result.push(browsingContext);
}
}
// Remove duplicates. Compare `BrowsingContextImpl` by reference is correct here, as
// `browsingContextStorage` returns the same instance for the same id.
return [...new Set(result).values()];
}
async setExtraHeaders(params) {
const affectedBrowsingContexts = await this.#getRelatedTopLevelBrowsingContexts(params.contexts, params.userContexts);
const cdpExtraHeaders = parseBiDiHeaders(params.headers);
if (params.userContexts === undefined && params.contexts === undefined) {
this.#contextConfigStorage.updateGlobalConfig({
extraHeaders: cdpExtraHeaders,
});
}
if (params.userContexts !== undefined) {
params.userContexts.forEach((userContext) => {
this.#contextConfigStorage.updateUserContextConfig(userContext, {
extraHeaders: cdpExtraHeaders,
});
});
}
if (params.contexts !== undefined) {
params.contexts.forEach((browsingContextId) => {
this.#contextConfigStorage.updateBrowsingContextConfig(browsingContextId, { extraHeaders: cdpExtraHeaders });
});
}
await Promise.all(affectedBrowsingContexts.map(async (context) => {
// Actual value can be different from the one in params, e.g. in case of already
// existing setting.
const extraHeaders = this.#contextConfigStorage.getActiveConfig(context.id, context.userContext).extraHeaders ?? {};
await context.setExtraHeaders(extraHeaders);
}));
return {};
}
}
exports.NetworkProcessor = NetworkProcessor;
/**
* See https://w3c.github.io/webdriver-bidi/#unescape-url-pattern
*/
function unescapeURLPattern(pattern) {
const forbidden = new Set(['(', ')', '*', '{', '}']);
let result = '';
let isEscaped = false;
for (const c of pattern) {
if (!isEscaped) {
if (forbidden.has(c)) {
throw new protocol_js_1.InvalidArgumentException('Forbidden characters');
}
if (c === '\\') {
isEscaped = true;
continue;
}
}
result += c;
isEscaped = false;
}
return result;
}
// https://fetch.spec.whatwg.org/#header-name
const FORBIDDEN_HEADER_NAME_SYMBOLS = new Set([
' ',
'\t',
'\n',
'"',
'(',
')',
',',
'/',
':',
';',
'<',
'=',
'>',
'?',
'@',
'[',
'\\',
']',
'{',
'}',
]);
// https://fetch.spec.whatwg.org/#header-value
const FORBIDDEN_HEADER_VALUE_SYMBOLS = new Set(['\0', '\n', '\r']);
function includesChar(str, chars) {
for (const char of str) {
if (chars.has(char)) {
return true;
}
}
return false;
}
// Export for testing.
function parseBiDiHeaders(headers) {
const parsedHeaders = {};
for (const bidiHeader of headers) {
if (bidiHeader.value.type === 'string') {
const name = bidiHeader.name;
const value = bidiHeader.value.value;
if (name.length === 0) {
throw new protocol_js_1.InvalidArgumentException(`Empty header name is not allowed`);
}
if (includesChar(name, FORBIDDEN_HEADER_NAME_SYMBOLS)) {
throw new protocol_js_1.InvalidArgumentException(`Header name '${name}' contains forbidden symbols`);
}
if (includesChar(value, FORBIDDEN_HEADER_VALUE_SYMBOLS)) {
throw new protocol_js_1.InvalidArgumentException(`Header value '${value}' contains forbidden symbols`);
}
if (value.trim() !== value) {
throw new protocol_js_1.InvalidArgumentException(`Header value should not contain trailing or ending whitespaces`);
}
// BiDi spec does not combine but overrides the headers with the same names.
// https://www.w3.org/TR/webdriver-bidi/#update-headers
parsedHeaders[bidiHeader.name] = bidiHeader.value.value;
}
else {
throw new protocol_js_1.UnsupportedOperationException('Only string headers values are supported');
}
}
return parsedHeaders;
}
//# sourceMappingURL=NetworkProcessor.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,55 @@
/**
* @fileoverview `NetworkRequest` represents a single network request and keeps
* track of all the related CDP events.
*/
import type { Protocol } from 'devtools-protocol';
import { Network } from '../../../protocol/protocol.js';
import { Deferred } from '../../../utils/Deferred.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { EventManager } from '../session/EventManager.js';
import type { NetworkStorage } from './NetworkStorage.js';
/** Abstracts one individual network request. */
export declare class NetworkRequest {
#private;
static unknownParameter: string;
waitNextPhase: Deferred<void>;
constructor(id: Network.Request, eventManager: EventManager, networkStorage: NetworkStorage, cdpTarget: CdpTarget, redirectCount?: number, logger?: LoggerFn);
get id(): string;
get fetchId(): string | undefined;
/**
* When blocked returns the phase for it
*/
get interceptPhase(): Network.InterceptPhase | undefined;
get url(): string;
get redirectCount(): number;
get cdpTarget(): CdpTarget;
/** CdpTarget can be changed when frame is moving out of process. */
updateCdpTarget(cdpTarget: CdpTarget): void;
get cdpClient(): import("../../BidiMapper.js").CdpClient;
isRedirecting(): boolean;
get bodySize(): number;
handleRedirect(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentEvent(event: Protocol.Network.RequestWillBeSentEvent): void;
onRequestWillBeSentExtraInfoEvent(event: Protocol.Network.RequestWillBeSentExtraInfoEvent): void;
onResponseReceivedExtraInfoEvent(event: Protocol.Network.ResponseReceivedExtraInfoEvent): void;
onResponseReceivedEvent(event: Protocol.Network.ResponseReceivedEvent): void;
onServedFromCache(): void;
onLoadingFinishedEvent(event: Protocol.Network.LoadingFinishedEvent): void;
onDataReceivedEvent(event: Protocol.Network.DataReceivedEvent): void;
onLoadingFailedEvent(event: Protocol.Network.LoadingFailedEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
failRequest(errorReason: Protocol.Network.ErrorReason): Promise<void>;
onRequestPaused(event: Protocol.Fetch.RequestPausedEvent): void;
onAuthRequired(event: Protocol.Fetch.AuthRequiredEvent): void;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
continueRequest(overrides?: Omit<Network.ContinueRequestParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
continueResponse(overrides?: Omit<Network.ContinueResponseParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
continueWithAuth(authChallenge: Omit<Network.ContinueWithAuthParameters, 'request'>): Promise<void>;
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
provideResponse(overrides: Omit<Network.ProvideResponseParameters, 'request'>): Promise<void>;
dispose(): void;
get encodedResponseBodySize(): number;
}

View File

@@ -0,0 +1,894 @@
"use strict";
/*
* 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.
*
*/
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkRequest = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const assert_js_1 = require("../../../utils/assert.js");
const DefaultMap_js_1 = require("../../../utils/DefaultMap.js");
const Deferred_js_1 = require("../../../utils/Deferred.js");
const log_js_1 = require("../../../utils/log.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
const REALM_REGEX = /(?<=realm=").*(?=")/;
/** Abstracts one individual network request. */
class NetworkRequest {
static unknownParameter = 'UNKNOWN';
/**
* Each network request has an associated request id, which is a string
* uniquely identifying that request.
*
* The identifier for a request resulting from a redirect matches that of the
* request that initiated it.
*/
#id;
#fetchId;
/**
* Indicates the network intercept phase, if the request is currently blocked.
* Undefined necessarily implies that the request is not blocked.
*/
#interceptPhase;
#servedFromCache = false;
#redirectCount;
#request = {};
#requestOverrides;
#responseOverrides;
#response = {
decodedSize: 0,
encodedSize: 0,
};
#eventManager;
#networkStorage;
#cdpTarget;
#logger;
#emittedEvents = {
[protocol_js_1.ChromiumBidi.Network.EventNames.AuthRequired]: false,
[protocol_js_1.ChromiumBidi.Network.EventNames.BeforeRequestSent]: false,
[protocol_js_1.ChromiumBidi.Network.EventNames.FetchError]: false,
[protocol_js_1.ChromiumBidi.Network.EventNames.ResponseCompleted]: false,
[protocol_js_1.ChromiumBidi.Network.EventNames.ResponseStarted]: false,
};
waitNextPhase = new Deferred_js_1.Deferred();
constructor(id, eventManager, networkStorage, cdpTarget, redirectCount = 0, logger) {
this.#id = id;
this.#eventManager = eventManager;
this.#networkStorage = networkStorage;
this.#cdpTarget = cdpTarget;
this.#redirectCount = redirectCount;
this.#logger = logger;
}
get id() {
return this.#id;
}
get fetchId() {
return this.#fetchId;
}
/**
* When blocked returns the phase for it
*/
get interceptPhase() {
return this.#interceptPhase;
}
get url() {
const fragment = this.#request.info?.request.urlFragment ??
this.#request.paused?.request.urlFragment ??
'';
const url = this.#response.paused?.request.url ??
this.#requestOverrides?.url ??
this.#response.info?.url ??
this.#request.auth?.request.url ??
this.#request.info?.request.url ??
this.#request.paused?.request.url ??
_a.unknownParameter;
return `${url}${fragment}`;
}
get redirectCount() {
return this.#redirectCount;
}
get cdpTarget() {
return this.#cdpTarget;
}
/** CdpTarget can be changed when frame is moving out of process. */
updateCdpTarget(cdpTarget) {
if (cdpTarget !== this.#cdpTarget) {
this.#logger?.(log_js_1.LogType.debugInfo, `Request ${this.id} was moved from ${this.#cdpTarget.id} to ${cdpTarget.id}`);
this.#cdpTarget = cdpTarget;
}
}
get cdpClient() {
return this.#cdpTarget.cdpClient;
}
isRedirecting() {
return Boolean(this.#request.info);
}
#isDataUrl() {
return this.url.startsWith('data:');
}
#isNonInterceptable() {
return (
// We can't intercept data urls from CDP
this.#isDataUrl() ||
// Cached requests never hit the network
this.#servedFromCache);
}
get #method() {
return (this.#requestOverrides?.method ??
this.#request.info?.request.method ??
this.#request.paused?.request.method ??
this.#request.auth?.request.method ??
this.#response.paused?.request.method);
}
get #navigationId() {
// Heuristic to determine if this is a navigation request, and if not return null.
if (!this.#request.info ||
!this.#request.info.loaderId ||
// When we navigate all CDP network events have `loaderId`
// CDP's `loaderId` and `requestId` match when
// that request triggered the loading
this.#request.info.loaderId !== this.#request.info.requestId) {
return null;
}
// Get virtual navigation ID from the browsing context.
return this.#networkStorage.getNavigationId(this.#context ?? undefined);
}
get #cookies() {
let cookies = [];
if (this.#request.extraInfo) {
cookies = this.#request.extraInfo.associatedCookies
.filter(({ blockedReasons }) => {
return !Array.isArray(blockedReasons) || blockedReasons.length === 0;
})
.map(({ cookie }) => (0, NetworkUtils_js_1.cdpToBiDiCookie)(cookie));
}
return cookies;
}
#getBodySizeFromHeaders(headers) {
if (headers === undefined) {
return undefined;
}
if (headers['Content-Length'] !== undefined) {
const bodySize = Number.parseInt(headers['Content-Length']);
if (Number.isInteger(bodySize)) {
return bodySize;
}
this.#logger?.(log_js_1.LogType.debugError, "Unexpected non-integer 'Content-Length' header");
}
// TODO: process `Transfer-Encoding: chunked` case properly.
return undefined;
}
get bodySize() {
if (typeof this.#requestOverrides?.bodySize === 'number') {
return this.#requestOverrides.bodySize;
}
if (this.#request.info?.request.postDataEntries !== undefined) {
return (0, NetworkUtils_js_1.bidiBodySizeFromCdpPostDataEntries)(this.#request.info?.request.postDataEntries);
}
// Try to guess the body size based on the `Content-Length` header.
return (this.#getBodySizeFromHeaders(this.#request.info?.request.headers) ??
this.#getBodySizeFromHeaders(this.#request.extraInfo?.headers) ??
0);
}
get #context() {
const result = this.#response.paused?.frameId ??
this.#request.info?.frameId ??
this.#request.paused?.frameId ??
this.#request.auth?.frameId;
if (result !== undefined) {
return result;
}
// Heuristic for associating a preflight request with context via it's initiator
// request. Useful for preflight requests.
// https://github.com/GoogleChromeLabs/chromium-bidi/issues/3570
if (this.#request?.info?.initiator.type === 'preflight' &&
this.#request?.info?.initiator.requestId !== undefined) {
const maybeInitiator = this.#networkStorage.getRequestById(this.#request?.info?.initiator.requestId);
if (maybeInitiator !== undefined) {
return maybeInitiator.#request.info?.frameId ?? null;
}
}
return null;
}
/** Returns the HTTP status code associated with this request if any. */
get #statusCode() {
return (this.#responseOverrides?.statusCode ??
this.#response.paused?.responseStatusCode ??
this.#response.extraInfo?.statusCode ??
this.#response.info?.status);
}
get #requestHeaders() {
let headers = [];
if (this.#requestOverrides?.headers) {
const headerMap = new DefaultMap_js_1.DefaultMap(() => []);
for (const header of this.#requestOverrides.headers) {
headerMap.get(header.name).push(header.value.value);
}
for (const [name, value] of headerMap.entries()) {
headers.push({
name,
value: {
type: 'string',
value: value.join('\n').trimEnd(),
},
});
}
}
else {
headers = [
...(0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(this.#request.info?.request.headers),
...(0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(this.#request.extraInfo?.headers),
];
}
return headers;
}
get #authChallenges() {
// TODO: get headers from Fetch.requestPaused
if (!this.#response.info) {
return;
}
if (!(this.#statusCode === 401 || this.#statusCode === 407)) {
return undefined;
}
const headerName = this.#statusCode === 401 ? 'WWW-Authenticate' : 'Proxy-Authenticate';
const authChallenges = [];
for (const [header, value] of Object.entries(this.#response.info.headers)) {
// TODO: Do a proper match based on https://httpwg.org/specs/rfc9110.html#credentials
// Or verify this works
if (header.localeCompare(headerName, undefined, { sensitivity: 'base' }) === 0) {
authChallenges.push({
scheme: value.split(' ').at(0) ?? '',
realm: value.match(REALM_REGEX)?.at(0) ?? '',
});
}
}
return authChallenges;
}
get #timings() {
// The timing in the CDP events are provided relative to the event's baseline.
// However, the baseline can be different for different events, and the events have to
// be normalized throughout resource events. Normalize events timestamps by the
// request.
// TODO: Verify this is correct.
const responseTimeOffset = (0, NetworkUtils_js_1.getTiming)((0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.requestTime) -
(0, NetworkUtils_js_1.getTiming)(this.#request.info?.timestamp));
return {
// TODO: Verify this is correct
timeOrigin: Math.round((0, NetworkUtils_js_1.getTiming)(this.#request.info?.wallTime) * 1000),
// Timing baseline.
// TODO: Verify this is correct.
requestTime: 0,
// TODO: set if redirect detected.
redirectStart: 0,
// TODO: set if redirect detected.
redirectEnd: 0,
// TODO: Verify this is correct
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=145
fetchStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.workerFetchStart, responseTimeOffset),
// fetchStart: 0,
dnsStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.dnsStart, responseTimeOffset),
dnsEnd: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.dnsEnd, responseTimeOffset),
connectStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.connectStart, responseTimeOffset),
connectEnd: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.connectEnd, responseTimeOffset),
tlsStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.sslStart, responseTimeOffset),
requestStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.sendStart, responseTimeOffset),
// https://source.chromium.org/chromium/chromium/src/+/main:net/base/load_timing_info.h;l=196
responseStart: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.receiveHeadersStart, responseTimeOffset),
responseEnd: (0, NetworkUtils_js_1.getTiming)(this.#response.info?.timing?.receiveHeadersEnd, responseTimeOffset),
};
}
#phaseChanged() {
this.waitNextPhase.resolve();
this.waitNextPhase = new Deferred_js_1.Deferred();
}
#interceptsInPhase(phase) {
if (this.#isNonInterceptable() ||
!this.#cdpTarget.isSubscribedTo(`network.${phase}`)) {
return new Set();
}
return this.#networkStorage.getInterceptsForPhase(this, phase);
}
#isBlockedInPhase(phase) {
return this.#interceptsInPhase(phase).size > 0;
}
handleRedirect(event) {
// TODO: use event.redirectResponse;
// Temporary workaround to emit ResponseCompleted event for redirects
this.#response.hasExtraInfo = false;
this.#response.decodedSize = 0;
this.#response.encodedSize = 0;
this.#response.info = event.redirectResponse;
this.#emitEventsIfReady({
wasRedirected: true,
});
}
#emitEventsIfReady(options = {}) {
const requestExtraInfoCompleted =
// Flush redirects
options.wasRedirected ||
Boolean(this.#response.loadingFailed) ||
this.#isDataUrl() ||
Boolean(this.#request.extraInfo) ||
// If the request is intercepted during the `authRequired` phase, there
// will be no `Network.requestWillBeSentExtraInfo` CDP events.
this.#isBlockedInPhase("authRequired" /* Network.InterceptPhase.AuthRequired */) ||
// Requests from cache don't have extra info
this.#servedFromCache ||
// Sometimes there is no extra info and the response
// is the only place we can find out
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const noInterceptionExpected = this.#isNonInterceptable();
const requestInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
const requestInterceptionCompleted = !requestInterceptionExpected ||
(requestInterceptionExpected && Boolean(this.#request.paused));
if (Boolean(this.#request.info) &&
(requestInterceptionExpected
? requestInterceptionCompleted
: requestExtraInfoCompleted)) {
this.#emitEvent(this.#getBeforeRequestEvent.bind(this));
}
const responseExtraInfoCompleted = Boolean(this.#response.extraInfo) ||
// Response from cache don't have extra info
this.#servedFromCache ||
// Don't expect extra info if the flag is false
Boolean(this.#response.info && !this.#response.hasExtraInfo);
const responseInterceptionExpected = !noInterceptionExpected &&
this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
if (this.#response.info ||
(responseInterceptionExpected && Boolean(this.#response.paused))) {
this.#emitEvent(this.#getResponseStartedEvent.bind(this));
}
const responseInterceptionCompleted = !responseInterceptionExpected ||
(responseInterceptionExpected && Boolean(this.#response.paused));
const loadingFinished = Boolean(this.#response.loadingFailed) ||
Boolean(this.#response.loadingFinished);
if (Boolean(this.#response.info) &&
responseExtraInfoCompleted &&
responseInterceptionCompleted &&
(loadingFinished || options.wasRedirected)) {
this.#emitEvent(this.#getResponseReceivedEvent.bind(this));
this.#networkStorage.disposeRequest(this.id);
}
}
onRequestWillBeSentEvent(event) {
this.#request.info = event;
this.#networkStorage.collectIfNeeded(this, "request" /* Network.DataType.Request */);
this.#emitEventsIfReady();
}
onRequestWillBeSentExtraInfoEvent(event) {
this.#request.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedExtraInfoEvent(event) {
if (event.statusCode >= 300 &&
event.statusCode <= 399 &&
this.#request.info &&
event.headers['location'] === this.#request.info.request.url) {
// We received the Response Extra info for the redirect
// Too late so we need to skip it as it will
// fire wrongly for the last one
return;
}
this.#response.extraInfo = event;
this.#emitEventsIfReady();
}
onResponseReceivedEvent(event) {
this.#response.hasExtraInfo = event.hasExtraInfo;
this.#response.info = event.response;
this.#networkStorage.collectIfNeeded(this, "response" /* Network.DataType.Response */);
this.#emitEventsIfReady();
}
onServedFromCache() {
this.#servedFromCache = true;
this.#emitEventsIfReady();
}
onLoadingFinishedEvent(event) {
this.#response.loadingFinished = event;
this.#emitEventsIfReady();
}
onDataReceivedEvent(event) {
this.#response.decodedSize += event.dataLength;
this.#response.encodedSize += event.encodedDataLength;
}
onLoadingFailedEvent(event) {
this.#response.loadingFailed = event;
this.#emitEventsIfReady();
this.#emitEvent(() => {
return {
method: protocol_js_1.ChromiumBidi.Network.EventNames.FetchError,
params: {
...this.#getBaseEventParams(),
errorText: event.errorText,
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-failRequest */
async failRequest(errorReason) {
(0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.failRequest', {
requestId: this.#fetchId,
errorReason,
});
this.#interceptPhase = undefined;
}
onRequestPaused(event) {
this.#fetchId = event.requestId;
// CDP https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#event-requestPaused
if (event.responseStatusCode || event.responseErrorReason) {
this.#response.paused = event;
if (this.#isBlockedInPhase("responseStarted" /* Network.InterceptPhase.ResponseStarted */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[protocol_js_1.ChromiumBidi.Network.EventNames.ResponseStarted] &&
// Continue all response that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "responseStarted" /* Network.InterceptPhase.ResponseStarted */;
}
else {
void this.#continueResponse();
}
}
else {
this.#request.paused = event;
if (this.#isBlockedInPhase("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */) &&
// CDP may emit multiple events for a single request
!this.#emittedEvents[protocol_js_1.ChromiumBidi.Network.EventNames.BeforeRequestSent] &&
// Continue all requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */;
}
else {
void this.#continueRequest();
}
}
this.#emitEventsIfReady();
}
onAuthRequired(event) {
this.#fetchId = event.requestId;
this.#request.auth = event;
if (this.#isBlockedInPhase("authRequired" /* Network.InterceptPhase.AuthRequired */) &&
// Continue all auth requests that have not enabled Network domain
this.#fetchId !== this.id) {
this.#interceptPhase = "authRequired" /* Network.InterceptPhase.AuthRequired */;
// Make sure the `network.beforeRequestSent` is emitted before
// `network.authRequired`.
this.#emitEventsIfReady();
}
else {
void this.#continueWithAuth({
response: 'Default',
});
}
this.#emitEvent(() => {
return {
method: protocol_js_1.ChromiumBidi.Network.EventNames.AuthRequired,
params: {
...this.#getBaseEventParams("authRequired" /* Network.InterceptPhase.AuthRequired */),
response: this.#getResponseEventParams(),
},
};
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueRequest */
async continueRequest(overrides = {}) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const headers = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(overrideHeaders);
const postData = getCdpBodyFromBiDiBytesValue(overrides.body);
await this.#continueRequest({
url: overrides.url,
method: overrides.method,
headers,
postData,
});
this.#requestOverrides = {
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
cookies: overrides.cookies,
bodySize: getSizeFromBiDiBytesValue(overrides.body),
};
}
async #continueRequest(overrides = {}) {
(0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueRequest', {
requestId: this.#fetchId,
url: overrides.url,
method: overrides.method,
headers: overrides.headers,
postData: overrides.postData,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueResponse */
async continueResponse(overrides = {}) {
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
if (overrides.credentials) {
await Promise.all([
this.waitNextPhase,
await this.#continueWithAuth({
response: 'ProvideCredentials',
username: overrides.credentials.username,
password: overrides.credentials.password,
}),
]);
}
else {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
}
if (this.#interceptPhase === "responseStarted" /* Network.InterceptPhase.ResponseStarted */) {
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(overrideHeaders);
await this.#continueResponse({
responseCode: overrides.statusCode ?? this.#response.paused?.responseStatusCode,
responsePhrase: overrides.reasonPhrase ?? this.#response.paused?.responseStatusText,
responseHeaders: responseHeaders ?? this.#response.paused?.responseHeaders,
});
this.#responseOverrides = {
statusCode: overrides.statusCode,
headers: overrideHeaders,
};
}
}
async #continueResponse({ responseCode, responsePhrase, responseHeaders, } = {}) {
(0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueResponse', {
requestId: this.#fetchId,
responseCode,
responsePhrase,
responseHeaders,
});
this.#interceptPhase = undefined;
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-continueWithAuth */
async continueWithAuth(authChallenge) {
let username;
let password;
if (authChallenge.action === 'provideCredentials') {
const { credentials } = authChallenge;
username = credentials.username;
password = credentials.password;
}
const response = (0, NetworkUtils_js_1.cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction)(authChallenge.action);
await this.#continueWithAuth({
response,
username,
password,
});
}
/** @see https://chromedevtools.github.io/devtools-protocol/tot/Fetch/#method-provideResponse */
async provideResponse(overrides) {
(0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
// We need to pass through if the request is already in
// AuthRequired phase
if (this.interceptPhase === "authRequired" /* Network.InterceptPhase.AuthRequired */) {
// We need to use `ProvideCredentials`
// As `Default` may cancel the request
return await this.#continueWithAuth({
response: 'ProvideCredentials',
});
}
// If we don't modify the response
// just continue the request
if (!overrides.body && !overrides.headers) {
return await this.#continueRequest();
}
const overrideHeaders = this.#getOverrideHeader(overrides.headers, overrides.cookies);
const responseHeaders = (0, NetworkUtils_js_1.cdpFetchHeadersFromBidiNetworkHeaders)(overrideHeaders);
const responseCode = overrides.statusCode ?? this.#statusCode ?? 200;
await this.cdpClient.sendCommand('Fetch.fulfillRequest', {
requestId: this.#fetchId,
responseCode,
responsePhrase: overrides.reasonPhrase,
responseHeaders,
body: getCdpBodyFromBiDiBytesValue(overrides.body),
});
this.#interceptPhase = undefined;
}
dispose() {
this.waitNextPhase.reject(new Error('waitNextPhase disposed'));
}
async #continueWithAuth(authChallengeResponse) {
(0, assert_js_1.assert)(this.#fetchId, 'Network Interception not set-up.');
await this.cdpClient.sendCommand('Fetch.continueWithAuth', {
requestId: this.#fetchId,
authChallengeResponse,
});
this.#interceptPhase = undefined;
}
#emitEvent(getEvent) {
let event;
try {
event = getEvent();
}
catch (error) {
this.#logger?.(log_js_1.LogType.debugError, error);
return;
}
if (this.#isIgnoredEvent() ||
(this.#emittedEvents[event.method] &&
// Special case this event can be emitted multiple times
event.method !== protocol_js_1.ChromiumBidi.Network.EventNames.AuthRequired)) {
return;
}
this.#phaseChanged();
this.#emittedEvents[event.method] = true;
if (this.#context) {
this.#eventManager.registerEvent(Object.assign(event, {
type: 'event',
}), this.#context);
}
else {
this.#eventManager.registerGlobalEvent(Object.assign(event, {
type: 'event',
}));
}
}
#getBaseEventParams(phase) {
const interceptProps = {
isBlocked: false,
};
if (phase) {
const blockedBy = this.#interceptsInPhase(phase);
interceptProps.isBlocked = blockedBy.size > 0;
if (interceptProps.isBlocked) {
interceptProps.intercepts = [...blockedBy];
}
}
return {
context: this.#context,
navigation: this.#navigationId,
redirectCount: this.#redirectCount,
request: this.#getRequestData(),
// Timestamp should be in milliseconds, while CDP provides it in seconds.
timestamp: Math.round((0, NetworkUtils_js_1.getTiming)(this.#request.info?.wallTime) * 1000),
// Contains isBlocked and intercepts
...interceptProps,
};
}
#getResponseEventParams() {
// Chromium sends wrong extraInfo events for responses served from cache.
// See https://github.com/puppeteer/puppeteer/issues/9965 and
// https://crbug.com/1340398.
if (this.#response.info?.fromDiskCache) {
this.#response.extraInfo = undefined;
}
// TODO: Also this.#response.paused?.responseHeaders have to be merged here.
const cdpHeaders = this.#response.info?.headers ?? {};
const cdpRawHeaders = this.#response.extraInfo?.headers ?? {};
for (const [key, value] of Object.entries(cdpRawHeaders)) {
cdpHeaders[key] = value;
}
const headers = (0, NetworkUtils_js_1.bidiNetworkHeadersFromCdpNetworkHeaders)(cdpHeaders);
const authChallenges = this.#authChallenges;
const response = {
url: this.url,
protocol: this.#response.info?.protocol ?? '',
status: this.#statusCode ?? -1, // TODO: Throw an exception or use some other status code?
statusText: this.#response.info?.statusText ||
this.#response.paused?.responseStatusText ||
'',
fromCache: this.#response.info?.fromDiskCache ||
this.#response.info?.fromPrefetchCache ||
this.#servedFromCache,
headers: this.#responseOverrides?.headers ?? headers,
mimeType: this.#response.info?.mimeType || '',
// TODO: this should be the size for the entire HTTP response.
bytesReceived: this.encodedResponseBodySize,
headersSize: (0, NetworkUtils_js_1.computeHeadersSize)(headers),
bodySize: this.encodedResponseBodySize,
content: {
size: this.#response.decodedSize ?? 0,
},
...(authChallenges ? { authChallenges } : {}),
};
return {
...response,
'goog:securityDetails': this.#response.info?.securityDetails,
};
}
get encodedResponseBodySize() {
return (this.#response.loadingFinished?.encodedDataLength ??
this.#response.info?.encodedDataLength ??
this.#response.encodedSize ??
0);
}
#getRequestData() {
const headers = this.#requestHeaders;
const request = {
request: this.#id,
url: this.url,
method: this.#method ?? _a.unknownParameter,
headers,
cookies: this.#cookies,
headersSize: (0, NetworkUtils_js_1.computeHeadersSize)(headers),
bodySize: this.bodySize,
// TODO: populate
destination: this.#getDestination(),
// TODO: populate
initiatorType: this.#getInitiatorType(),
timings: this.#timings,
};
return {
...request,
'goog:postData': this.#request.info?.request?.postData,
'goog:hasPostData': this.#request.info?.request?.hasPostData,
'goog:resourceType': this.#request.info?.type,
'goog:resourceInitiator': this.#request.info?.initiator,
};
}
/**
* Heuristic trying to guess the destination.
* Specification: https://fetch.spec.whatwg.org/#concept-request-destination.
* Specified values: "audio", "audioworklet", "document", "embed", "font", "frame",
* "iframe", "image", "json", "manifest", "object", "paintworklet", "report", "script",
* "serviceworker", "sharedworker", "style", "track", "video", "webidentity", "worker",
* "xslt".
*/
#getDestination() {
switch (this.#request.info?.type) {
case 'Script':
return 'script';
case 'Stylesheet':
return 'style';
case 'Image':
return 'image';
case 'Document':
// If request to document is initiated by parser, assume it is expected to
// arrive in an iframe. Otherwise, consider it is a navigation and the request
// result will end up in the document.
return this.#request.info?.initiator.type === 'parser'
? 'iframe'
: 'document';
default:
return '';
}
}
/**
* Heuristic trying to guess the initiator type.
* Specification: https://fetch.spec.whatwg.org/#request-initiator-type.
* Specified values: "audio", "beacon", "body", "css", "early-hints", "embed", "fetch",
* "font", "frame", "iframe", "image", "img", "input", "link", "object", "ping",
* "script", "track", "video", "xmlhttprequest", "other".
*/
#getInitiatorType() {
if (this.#request.info?.initiator.type === 'parser') {
switch (this.#request.info?.type) {
case 'Document':
// The request to document is initiated by the parser. Assuming it's an iframe.
return 'iframe';
case 'Font':
// If the document's url is not the parser's url, assume the resource is loaded
// from css. Otherwise, it's a `font` element.
return this.#request.info?.initiator?.url ===
this.#request.info?.documentURL
? 'font'
: 'css';
case 'Image':
// If the document's url is not the parser's url, assume the resource is loaded
// from css. Otherwise, it's a `img` element.
return this.#request.info?.initiator?.url ===
this.#request.info?.documentURL
? 'img'
: 'css';
case 'Script':
return 'script';
case 'Stylesheet':
return 'link';
default:
return null;
}
}
if (this.#request?.info?.type === 'Fetch') {
return 'fetch';
}
return null;
}
#getBeforeRequestEvent() {
(0, assert_js_1.assert)(this.#request.info, 'RequestWillBeSentEvent is not set');
return {
method: protocol_js_1.ChromiumBidi.Network.EventNames.BeforeRequestSent,
params: {
...this.#getBaseEventParams("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */),
initiator: {
type: _a.#getInitiator(this.#request.info.initiator.type),
columnNumber: this.#request.info.initiator.columnNumber,
lineNumber: this.#request.info.initiator.lineNumber,
stackTrace: this.#request.info.initiator.stack,
request: this.#request.info.initiator.requestId,
},
},
};
}
#getResponseStartedEvent() {
return {
method: protocol_js_1.ChromiumBidi.Network.EventNames.ResponseStarted,
params: {
...this.#getBaseEventParams("responseStarted" /* Network.InterceptPhase.ResponseStarted */),
response: this.#getResponseEventParams(),
},
};
}
#getResponseReceivedEvent() {
return {
method: protocol_js_1.ChromiumBidi.Network.EventNames.ResponseCompleted,
params: {
...this.#getBaseEventParams(),
response: this.#getResponseEventParams(),
},
};
}
#isIgnoredEvent() {
const faviconUrl = '/favicon.ico';
return (this.#request.paused?.request.url.endsWith(faviconUrl) ??
this.#request.info?.request.url.endsWith(faviconUrl) ??
false);
}
#getOverrideHeader(headers, cookies) {
if (!headers && !cookies) {
return undefined;
}
let overrideHeaders = headers;
const cookieHeader = (0, NetworkUtils_js_1.networkHeaderFromCookieHeaders)(cookies);
if (cookieHeader && !overrideHeaders) {
overrideHeaders = this.#requestHeaders;
}
if (cookieHeader && overrideHeaders) {
overrideHeaders.filter((header) => header.name.localeCompare('cookie', undefined, {
sensitivity: 'base',
}) !== 0);
overrideHeaders.push(cookieHeader);
}
return overrideHeaders;
}
static #getInitiator(initiatorType) {
switch (initiatorType) {
case 'parser':
case 'script':
case 'preflight':
return initiatorType;
default:
return 'other';
}
}
}
exports.NetworkRequest = NetworkRequest;
_a = NetworkRequest;
function getCdpBodyFromBiDiBytesValue(body) {
let parsedBody;
if (body?.type === 'string') {
parsedBody = (0, NetworkUtils_js_1.stringToBase64)(body.value);
}
else if (body?.type === 'base64') {
parsedBody = body.value;
}
return parsedBody;
}
function getSizeFromBiDiBytesValue(body) {
if (body?.type === 'string') {
return body.value.length;
}
else if (body?.type === 'base64') {
return atob(body.value).length;
}
return 0;
}
//# sourceMappingURL=NetworkRequest.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,57 @@
import { type BrowsingContext, Network } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpClient } from '../../BidiMapper.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { BrowsingContextStorage } from '../context/BrowsingContextStorage.js';
import type { EventManager } from '../session/EventManager.js';
import { NetworkRequest } from './NetworkRequest.js';
import { type ParsedUrlPattern } from './NetworkUtils.js';
export declare const MAX_TOTAL_COLLECTED_SIZE = 200000000;
type NetworkInterception = Omit<Network.AddInterceptParameters, 'urlPatterns'> & {
urlPatterns: ParsedUrlPattern[];
};
/** Stores network and intercept maps. */
export declare class NetworkStorage {
#private;
constructor(eventManager: EventManager, browsingContextStorage: BrowsingContextStorage, browserClient: CdpClient, logger?: LoggerFn);
onCdpTargetCreated(cdpTarget: CdpTarget): void;
getCollectedData(params: Network.GetDataParameters): Promise<Network.GetDataResult>;
collectIfNeeded(request: NetworkRequest, dataType: Network.DataType): void;
getInterceptionStages(browsingContextId: BrowsingContext.BrowsingContext): {
request: boolean;
response: boolean;
auth: boolean;
};
getInterceptsForPhase(request: NetworkRequest, phase: Network.InterceptPhase): Set<Network.Intercept>;
disposeRequestMap(sessionId: string): void;
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value: NetworkInterception): Network.Intercept;
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept: Network.Intercept): void;
getRequestsByTarget(target: CdpTarget): NetworkRequest[];
getRequestById(id: Network.Request): NetworkRequest | undefined;
getRequestByFetchId(fetchId: Network.Request): NetworkRequest | undefined;
addRequest(request: NetworkRequest): void;
/**
* Disposes the given request, if no collectors targeting it are left.
*/
disposeRequest(id: Network.Request): void;
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId: string | undefined): string | null;
set defaultCacheBehavior(behavior: Network.SetCacheBehaviorParameters['cacheBehavior']);
get defaultCacheBehavior(): Network.SetCacheBehaviorParameters["cacheBehavior"];
addDataCollector(params: Network.AddDataCollectorParameters): string;
removeDataCollector(params: Network.RemoveDataCollectorParameters): void;
disownData(params: Network.DisownDataParameters): void;
}
export {};

View File

@@ -0,0 +1,353 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.NetworkStorage = exports.MAX_TOTAL_COLLECTED_SIZE = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const uuid_js_1 = require("../../../utils/uuid.js");
const CollectorsStorage_js_1 = require("./CollectorsStorage.js");
const NetworkRequest_js_1 = require("./NetworkRequest.js");
const NetworkUtils_js_1 = require("./NetworkUtils.js");
// The default total data size limit in CDP.
// https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/inspector/inspector_network_agent.cc;drc=da1f749634c9a401cc756f36c2e6ce233e1c9b4d;l=133
exports.MAX_TOTAL_COLLECTED_SIZE = 200_000_000;
/** Stores network and intercept maps. */
class NetworkStorage {
#browsingContextStorage;
#eventManager;
#collectorsStorage;
#logger;
/**
* A map from network request ID to Network Request objects.
* Needed as long as information about requests comes from different events.
*/
#requests = new Map();
/** A map from intercept ID to track active network intercepts. */
#intercepts = new Map();
#defaultCacheBehavior = 'default';
constructor(eventManager, browsingContextStorage, browserClient, logger) {
this.#browsingContextStorage = browsingContextStorage;
this.#eventManager = eventManager;
this.#collectorsStorage = new CollectorsStorage_js_1.CollectorsStorage(exports.MAX_TOTAL_COLLECTED_SIZE, logger);
browserClient.on('Target.detachedFromTarget', ({ sessionId }) => {
this.disposeRequestMap(sessionId);
});
this.#logger = logger;
}
/**
* Gets the network request with the given ID, if any.
* Otherwise, creates a new network request with the given ID and cdp target.
*/
#getOrCreateNetworkRequest(id, cdpTarget, redirectCount) {
let request = this.getRequestById(id);
if (redirectCount === undefined && request) {
// Force re-creating requests for redirects.
return request;
}
request = new NetworkRequest_js_1.NetworkRequest(id, this.#eventManager, this, cdpTarget, redirectCount, this.#logger);
this.addRequest(request);
return request;
}
onCdpTargetCreated(cdpTarget) {
const cdpClient = cdpTarget.cdpClient;
// TODO: Wrap into object
const listeners = [
[
'Network.requestWillBeSent',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
if (request && request.isRedirecting()) {
request.handleRedirect(params);
this.disposeRequest(params.requestId);
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget, request.redirectCount + 1).onRequestWillBeSentEvent(params);
}
else {
this.#getOrCreateNetworkRequest(params.requestId, cdpTarget).onRequestWillBeSentEvent(params);
}
},
],
[
'Network.requestWillBeSentExtraInfo',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onRequestWillBeSentExtraInfoEvent(params);
},
],
[
'Network.responseReceived',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onResponseReceivedEvent(params);
},
],
[
'Network.responseReceivedExtraInfo',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onResponseReceivedExtraInfoEvent(params);
},
],
[
'Network.requestServedFromCache',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onServedFromCache();
},
],
[
'Fetch.requestPaused',
(event) => {
const request = this.#getOrCreateNetworkRequest(
// CDP quirk if the Network domain is not present this is undefined
event.networkId ?? event.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onRequestPaused(event);
},
],
[
'Fetch.authRequired',
(event) => {
let request = this.getRequestByFetchId(event.requestId);
if (!request) {
request = this.#getOrCreateNetworkRequest(event.requestId, cdpTarget);
}
request.updateCdpTarget(cdpTarget);
request.onAuthRequired(event);
},
],
[
'Network.dataReceived',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
request?.onDataReceivedEvent(params);
},
],
[
'Network.loadingFailed',
(params) => {
const request = this.#getOrCreateNetworkRequest(params.requestId, cdpTarget);
request.updateCdpTarget(cdpTarget);
request.onLoadingFailedEvent(params);
},
],
[
'Network.loadingFinished',
(params) => {
const request = this.getRequestById(params.requestId);
request?.updateCdpTarget(cdpTarget);
request?.onLoadingFinishedEvent(params);
},
],
];
for (const [event, listener] of listeners) {
cdpClient.on(event, listener);
}
}
async getCollectedData(params) {
if (!this.#collectorsStorage.isCollected(params.request, params.dataType, params.collector)) {
throw new protocol_js_1.NoSuchNetworkDataException(params.collector === undefined
? `No collected ${params.dataType} data`
: `Collector ${params.collector} didn't collect ${params.dataType} data`);
}
if (params.disown && params.collector === undefined) {
throw new protocol_js_1.InvalidArgumentException('Cannot disown collected data without collector ID');
}
const request = this.getRequestById(params.request);
if (request === undefined) {
throw new protocol_js_1.NoSuchNetworkDataException(`No data for ${params.request}`);
}
let result = undefined;
switch (params.dataType) {
case "response" /* Network.DataType.Response */:
result = await this.#getCollectedResponseData(request);
break;
case "request" /* Network.DataType.Request */:
result = await this.#getCollectedRequestData(request);
break;
default:
throw new protocol_js_1.UnsupportedOperationException(`Unsupported data type ${params.dataType}`);
}
if (params.disown && params.collector !== undefined) {
this.#collectorsStorage.disownData(request.id, params.dataType, params.collector);
// `disposeRequest` disposes request only if no other collectors for it are left.
this.disposeRequest(request.id);
}
return result;
}
async #getCollectedResponseData(request) {
try {
const responseBody = await request.cdpClient.sendCommand('Network.getResponseBody', { requestId: request.id });
return {
bytes: {
type: responseBody.base64Encoded ? 'base64' : 'string',
value: responseBody.body,
},
};
}
catch (error) {
if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'No resource with given identifier found') {
// The data has be gone for whatever reason.
throw new protocol_js_1.NoSuchNetworkDataException(`Response data was disposed`);
}
if (error.code === -32001 /* CdpErrorConstants.CONNECTION_CLOSED */) {
// The request's CDP session is gone. http://b/450771615.
throw new protocol_js_1.NoSuchNetworkDataException(`Response data is disposed after the related page`);
}
throw error;
}
}
async #getCollectedRequestData(request) {
// TODO: handle CDP error in case of the renderer is gone.
const requestPostData = await request.cdpClient.sendCommand('Network.getRequestPostData', { requestId: request.id });
return {
bytes: {
type: 'string',
value: requestPostData.postData,
},
};
}
collectIfNeeded(request, dataType) {
this.#collectorsStorage.collectIfNeeded(request, dataType, request.cdpTarget.topLevelId, request.cdpTarget.userContext);
}
getInterceptionStages(browsingContextId) {
const stages = {
request: false,
response: false,
auth: false,
};
for (const intercept of this.#intercepts.values()) {
if (intercept.contexts &&
!intercept.contexts.includes(browsingContextId)) {
continue;
}
stages.request ||= intercept.phases.includes("beforeRequestSent" /* Network.InterceptPhase.BeforeRequestSent */);
stages.response ||= intercept.phases.includes("responseStarted" /* Network.InterceptPhase.ResponseStarted */);
stages.auth ||= intercept.phases.includes("authRequired" /* Network.InterceptPhase.AuthRequired */);
}
return stages;
}
getInterceptsForPhase(request, phase) {
if (request.url === NetworkRequest_js_1.NetworkRequest.unknownParameter) {
return new Set();
}
const intercepts = new Set();
for (const [interceptId, intercept] of this.#intercepts.entries()) {
if (!intercept.phases.includes(phase) ||
(intercept.contexts &&
!intercept.contexts.includes(request.cdpTarget.topLevelId))) {
continue;
}
if (intercept.urlPatterns.length === 0) {
intercepts.add(interceptId);
continue;
}
for (const pattern of intercept.urlPatterns) {
if ((0, NetworkUtils_js_1.matchUrlPattern)(pattern, request.url)) {
intercepts.add(interceptId);
break;
}
}
}
return intercepts;
}
disposeRequestMap(sessionId) {
for (const request of this.#requests.values()) {
if (request.cdpClient.sessionId === sessionId) {
this.#requests.delete(request.id);
request.dispose();
}
}
}
/**
* Adds the given entry to the intercept map.
* URL patterns are assumed to be parsed.
*
* @return The intercept ID.
*/
addIntercept(value) {
const interceptId = (0, uuid_js_1.uuidv4)();
this.#intercepts.set(interceptId, value);
return interceptId;
}
/**
* Removes the given intercept from the intercept map.
* Throws NoSuchInterceptException if the intercept does not exist.
*/
removeIntercept(intercept) {
if (!this.#intercepts.has(intercept)) {
throw new protocol_js_1.NoSuchInterceptException(`Intercept '${intercept}' does not exist.`);
}
this.#intercepts.delete(intercept);
}
getRequestsByTarget(target) {
const requests = [];
for (const request of this.#requests.values()) {
if (request.cdpTarget === target) {
requests.push(request);
}
}
return requests;
}
getRequestById(id) {
return this.#requests.get(id);
}
getRequestByFetchId(fetchId) {
for (const request of this.#requests.values()) {
if (request.fetchId === fetchId) {
return request;
}
}
return;
}
addRequest(request) {
this.#requests.set(request.id, request);
}
/**
* Disposes the given request, if no collectors targeting it are left.
*/
disposeRequest(id) {
if (this.#collectorsStorage.isCollected(id)) {
// Keep request, as it's data can be accessed later.
return;
}
// TODO: dispose Network data from Chromium once there is a CDP command for that.
this.#requests.delete(id);
}
/**
* Gets the virtual navigation ID for the given navigable ID.
*/
getNavigationId(contextId) {
if (contextId === undefined) {
return null;
}
return (this.#browsingContextStorage.findContext(contextId)?.navigationId ?? null);
}
set defaultCacheBehavior(behavior) {
this.#defaultCacheBehavior = behavior;
}
get defaultCacheBehavior() {
return this.#defaultCacheBehavior;
}
addDataCollector(params) {
return this.#collectorsStorage.addDataCollector(params);
}
removeDataCollector(params) {
const releasedRequests = this.#collectorsStorage.removeDataCollector(params.collector);
releasedRequests.map((request) => this.disposeRequest(request));
}
disownData(params) {
if (!this.#collectorsStorage.isCollected(params.request, params.dataType, params.collector)) {
throw new protocol_js_1.NoSuchNetworkDataException(`Collector ${params.collector} didn't collect ${params.dataType} data`);
}
this.#collectorsStorage.disownData(params.request, params.dataType, params.collector);
// `disposeRequest` disposes request only if no other collectors for it are left.
this.disposeRequest(params.request);
}
}
exports.NetworkStorage = NetworkStorage;
//# sourceMappingURL=NetworkStorage.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,59 @@
/**
* @fileoverview Utility functions for the Network module.
*/
import type { Protocol } from 'devtools-protocol';
import { Network, type Storage } from '../../../protocol/protocol.js';
export declare function computeHeadersSize(headers: Network.Header[]): number;
export declare function stringToBase64(str: string): string;
/** Converts from CDP Network domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeaders(headers?: Protocol.Network.Headers): Network.Header[];
/** Converts from CDP Fetch domain headers to BiDi network headers. */
export declare function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Network domain headers. */
export declare function cdpNetworkHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Network.Headers | undefined;
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
export declare function bidiNetworkHeadersFromCdpFetchHeaders(headers?: Protocol.Fetch.HeaderEntry[]): Network.Header[];
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
export declare function cdpFetchHeadersFromBidiNetworkHeaders(headers?: Network.Header[]): Protocol.Fetch.HeaderEntry[] | undefined;
export declare function networkHeaderFromCookieHeaders(headers?: Network.CookieHeader[]): Network.Header | undefined;
/** Converts from Bidi auth action to CDP auth challenge response. */
export declare function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action: 'default' | 'cancel' | 'provideCredentials'): "Default" | "CancelAuth" | "ProvideCredentials";
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
export declare function cdpToBiDiCookie(cookie: Protocol.Network.Cookie): Network.Cookie;
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
export declare function deserializeByteValue(value: Network.BytesValue): string;
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
export declare function bidiToCdpCookie(params: Storage.SetCookieParameters, partitionKey: Storage.PartitionKey): Protocol.Network.CookieParam;
export declare function sameSiteBiDiToCdp(sameSite: Network.SameSite): Protocol.Network.CookieSameSite;
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
export declare function isSpecialScheme(protocol: string): boolean;
export interface ParsedUrlPattern {
protocol?: string;
hostname?: string;
port?: string;
pathname?: string;
search?: string;
}
/** Matches the given URLPattern against the given URL. */
export declare function matchUrlPattern(pattern: ParsedUrlPattern, url: string): boolean;
export declare function bidiBodySizeFromCdpPostDataEntries(entries: Protocol.Network.PostDataEntry[]): number;
export declare function getTiming(timing: number | undefined, offset?: number): number;

View File

@@ -0,0 +1,322 @@
"use strict";
/*
* 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.computeHeadersSize = computeHeadersSize;
exports.stringToBase64 = stringToBase64;
exports.bidiNetworkHeadersFromCdpNetworkHeaders = bidiNetworkHeadersFromCdpNetworkHeaders;
exports.bidiNetworkHeadersFromCdpNetworkHeadersEntries = bidiNetworkHeadersFromCdpNetworkHeadersEntries;
exports.cdpNetworkHeadersFromBidiNetworkHeaders = cdpNetworkHeadersFromBidiNetworkHeaders;
exports.bidiNetworkHeadersFromCdpFetchHeaders = bidiNetworkHeadersFromCdpFetchHeaders;
exports.cdpFetchHeadersFromBidiNetworkHeaders = cdpFetchHeadersFromBidiNetworkHeaders;
exports.networkHeaderFromCookieHeaders = networkHeaderFromCookieHeaders;
exports.cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction = cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction;
exports.cdpToBiDiCookie = cdpToBiDiCookie;
exports.deserializeByteValue = deserializeByteValue;
exports.bidiToCdpCookie = bidiToCdpCookie;
exports.sameSiteBiDiToCdp = sameSiteBiDiToCdp;
exports.isSpecialScheme = isSpecialScheme;
exports.matchUrlPattern = matchUrlPattern;
exports.bidiBodySizeFromCdpPostDataEntries = bidiBodySizeFromCdpPostDataEntries;
exports.getTiming = getTiming;
const ErrorResponse_js_1 = require("../../../protocol/ErrorResponse.js");
const base64_js_1 = require("../../../utils/base64.js");
function computeHeadersSize(headers) {
const requestHeaders = headers.reduce((acc, header) => {
return `${acc}${header.name}: ${header.value.value}\r\n`;
}, '');
return new TextEncoder().encode(requestHeaders).length;
}
function stringToBase64(str) {
return typedArrayToBase64(new TextEncoder().encode(str));
}
function typedArrayToBase64(typedArray) {
// chunkSize should be less V8 limit on number of arguments!
// https://github.com/v8/v8/blob/d3de848bea727518aee94dd2fd42ba0b62037a27/src/objects/code.h#L444
const chunkSize = 65534;
const chunks = [];
for (let i = 0; i < typedArray.length; i += chunkSize) {
const chunk = typedArray.subarray(i, i + chunkSize);
chunks.push(String.fromCodePoint.apply(null, chunk));
}
const binaryString = chunks.join('');
return btoa(binaryString);
}
/** Converts from CDP Network domain headers to BiDi network headers. */
function bidiNetworkHeadersFromCdpNetworkHeaders(headers) {
if (!headers) {
return [];
}
return Object.entries(headers).map(([name, value]) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from CDP Fetch domain headers to BiDi network headers. */
function bidiNetworkHeadersFromCdpNetworkHeadersEntries(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Network domain headers. */
function cdpNetworkHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.reduce((result, header) => {
// TODO: Distinguish between string and bytes?
result[header.name] = header.value.value;
return result;
}, {});
}
/** Converts from CDP Fetch domain header entries to Bidi network headers. */
function bidiNetworkHeadersFromCdpFetchHeaders(headers) {
if (!headers) {
return [];
}
return headers.map(({ name, value }) => ({
name,
value: {
type: 'string',
value,
},
}));
}
/** Converts from Bidi network headers to CDP Fetch domain header entries. */
function cdpFetchHeadersFromBidiNetworkHeaders(headers) {
if (headers === undefined) {
return undefined;
}
return headers.map(({ name, value }) => ({
name,
value: value.value,
}));
}
function networkHeaderFromCookieHeaders(headers) {
if (headers === undefined) {
return undefined;
}
const value = headers.reduce((acc, value, index) => {
if (index > 0) {
acc += ';';
}
const cookieValue = value.value.type === 'base64'
? btoa(value.value.value)
: value.value.value;
acc += `${value.name}=${cookieValue}`;
return acc;
}, '');
return {
name: 'Cookie',
value: {
type: 'string',
value,
},
};
}
/** Converts from Bidi auth action to CDP auth challenge response. */
function cdpAuthChallengeResponseFromBidiAuthContinueWithAuthAction(action) {
switch (action) {
case 'default':
return 'Default';
case 'cancel':
return 'CancelAuth';
case 'provideCredentials':
return 'ProvideCredentials';
}
}
/**
* Converts from CDP Network domain cookie to BiDi network cookie.
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-Cookie
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
*/
function cdpToBiDiCookie(cookie) {
const result = {
name: cookie.name,
value: { type: 'string', value: cookie.value },
domain: cookie.domain,
path: cookie.path,
size: cookie.size,
httpOnly: cookie.httpOnly,
secure: cookie.secure,
sameSite: cookie.sameSite === undefined
? "none" /* Network.SameSite.None */
: sameSiteCdpToBiDi(cookie.sameSite),
...(cookie.expires >= 0 ? { expiry: Math.round(cookie.expires) } : undefined),
};
// Extending with CDP-specific properties with `goog:` prefix.
result[`goog:session`] = cookie.session;
result[`goog:priority`] = cookie.priority;
result[`goog:sourceScheme`] = cookie.sourceScheme;
result[`goog:sourcePort`] = cookie.sourcePort;
if (cookie.partitionKey !== undefined) {
result[`goog:partitionKey`] = cookie.partitionKey;
}
if (cookie.partitionKeyOpaque !== undefined) {
result[`goog:partitionKeyOpaque`] = cookie.partitionKeyOpaque;
}
return result;
}
/**
* Decodes a byte value to a string.
* @param {Network.BytesValue} value
* @return {string}
*/
function deserializeByteValue(value) {
if (value.type === 'base64') {
return (0, base64_js_1.base64ToString)(value.value);
}
return value.value;
}
/**
* Converts from BiDi set network cookie params to CDP Network domain cookie.
* * https://w3c.github.io/webdriver-bidi/#type-network-Cookie
* * https://chromedevtools.github.io/devtools-protocol/tot/Network/#type-CookieParam
*/
function bidiToCdpCookie(params, partitionKey) {
const deserializedValue = deserializeByteValue(params.cookie.value);
const result = {
name: params.cookie.name,
value: deserializedValue,
domain: params.cookie.domain,
path: params.cookie.path ?? '/',
secure: params.cookie.secure ?? false,
httpOnly: params.cookie.httpOnly ?? false,
...(partitionKey.sourceOrigin !== undefined && {
partitionKey: {
hasCrossSiteAncestor: false,
// CDP's `partitionKey.topLevelSite` is the BiDi's `partition.sourceOrigin`.
topLevelSite: partitionKey.sourceOrigin,
},
}),
...(params.cookie.expiry !== undefined && {
expires: params.cookie.expiry,
}),
...(params.cookie.sameSite !== undefined && {
sameSite: sameSiteBiDiToCdp(params.cookie.sameSite),
}),
};
// Extending with CDP-specific properties with `goog:` prefix.
if (params.cookie[`goog:url`] !== undefined) {
result.url = params.cookie[`goog:url`];
}
if (params.cookie[`goog:priority`] !== undefined) {
result.priority = params.cookie[`goog:priority`];
}
if (params.cookie[`goog:sourceScheme`] !== undefined) {
result.sourceScheme = params.cookie[`goog:sourceScheme`];
}
if (params.cookie[`goog:sourcePort`] !== undefined) {
result.sourcePort = params.cookie[`goog:sourcePort`];
}
return result;
}
function sameSiteCdpToBiDi(sameSite) {
switch (sameSite) {
case 'Strict':
return "strict" /* Network.SameSite.Strict */;
case 'None':
return "none" /* Network.SameSite.None */;
case 'Lax':
return "lax" /* Network.SameSite.Lax */;
default:
// Defaults to `Lax`:
// https://web.dev/articles/samesite-cookies-explained#samesitelax_by_default
return "lax" /* Network.SameSite.Lax */;
}
}
function sameSiteBiDiToCdp(sameSite) {
switch (sameSite) {
case "none" /* Network.SameSite.None */:
return 'None';
case "strict" /* Network.SameSite.Strict */:
return 'Strict';
// Defaults to `Lax`:
// https://web.dev/articles/samesite-cookies-explained#samesitelax_by_default
case "default" /* Network.SameSite.Default */:
case "lax" /* Network.SameSite.Lax */:
return 'Lax';
}
throw new ErrorResponse_js_1.InvalidArgumentException(`Unknown 'sameSite' value ${sameSite}`);
}
/**
* Returns true if the given protocol is special.
* Special protocols are those that have a default port.
*
* Example inputs: 'http', 'http:'
*
* @see https://url.spec.whatwg.org/#special-scheme
*/
function isSpecialScheme(protocol) {
return ['ftp', 'file', 'http', 'https', 'ws', 'wss'].includes(protocol.replace(/:$/, ''));
}
function getScheme(url) {
return url.protocol.replace(/:$/, '');
}
/** Matches the given URLPattern against the given URL. */
function matchUrlPattern(pattern, url) {
// Roughly https://w3c.github.io/webdriver-bidi/#match-url-pattern
// plus some differences based on the URL parsing methods.
const parsedUrl = new URL(url);
if (pattern.protocol !== undefined &&
pattern.protocol !== getScheme(parsedUrl)) {
return false;
}
if (pattern.hostname !== undefined &&
pattern.hostname !== parsedUrl.hostname) {
return false;
}
if (pattern.port !== undefined && pattern.port !== parsedUrl.port) {
return false;
}
if (pattern.pathname !== undefined &&
pattern.pathname !== parsedUrl.pathname) {
return false;
}
if (pattern.search !== undefined && pattern.search !== parsedUrl.search) {
return false;
}
return true;
}
function bidiBodySizeFromCdpPostDataEntries(entries) {
let size = 0;
for (const entry of entries) {
size += atob(entry.bytes ?? '').length;
}
return size;
}
function getTiming(timing, offset = 0) {
if (!timing) {
return 0;
}
if (timing <= 0 || timing + offset <= 0) {
return 0;
}
return timing + offset;
}
//# sourceMappingURL=NetworkUtils.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,23 @@
/**
* Copyright 2024 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 type { CdpClient } from '../../../cdp/CdpClient.js';
import { type EmptyResult, type Permissions } from '../../../protocol/protocol.js';
export declare class PermissionsProcessor {
#private;
constructor(browserCdpClient: CdpClient);
setPermissions(params: Permissions.SetPermissionParameters): Promise<EmptyResult>;
}

View File

@@ -0,0 +1,55 @@
"use strict";
/**
* Copyright 2024 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PermissionsProcessor = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
class PermissionsProcessor {
#browserCdpClient;
constructor(browserCdpClient) {
this.#browserCdpClient = browserCdpClient;
}
async setPermissions(params) {
try {
const userContextId = params['goog:userContext'] ||
params.userContext;
await this.#browserCdpClient.sendCommand('Browser.setPermission', {
origin: params.origin,
embeddedOrigin: params.embeddedOrigin,
browserContextId: userContextId && userContextId !== 'default'
? userContextId
: undefined,
permission: {
name: params.descriptor.name,
},
setting: params.state,
});
}
catch (err) {
if (err.message ===
`Permission can't be granted to opaque origins.`) {
// Return success if the origin is not valid (does not match any
// existing origins).
return {};
}
throw new protocol_js_1.InvalidArgumentException(err.message);
}
return {};
}
}
exports.PermissionsProcessor = PermissionsProcessor;
//# sourceMappingURL=PermissionsProcessor.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PermissionsProcessor.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/permissions/PermissionsProcessor.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;GAeG;;;AAGH,+DAIuC;AAEvC,MAAa,oBAAoB;IAC/B,iBAAiB,CAAY;IAE7B,YAAY,gBAA2B;QACrC,IAAI,CAAC,iBAAiB,GAAG,gBAAgB,CAAC;IAC5C,CAAC;IAED,KAAK,CAAC,cAAc,CAClB,MAA2C;QAE3C,IAAI,CAAC;YACH,MAAM,aAAa,GAChB,MAAwC,CAAC,kBAAkB,CAAC;gBAC7D,MAAM,CAAC,WAAW,CAAC;YACrB,MAAM,IAAI,CAAC,iBAAiB,CAAC,WAAW,CAAC,uBAAuB,EAAE;gBAChE,MAAM,EAAE,MAAM,CAAC,MAAM;gBACrB,cAAc,EAAE,MAAM,CAAC,cAAc;gBACrC,gBAAgB,EACd,aAAa,IAAI,aAAa,KAAK,SAAS;oBAC1C,CAAC,CAAC,aAAa;oBACf,CAAC,CAAC,SAAS;gBACf,UAAU,EAAE;oBACV,IAAI,EAAE,MAAM,CAAC,UAAU,CAAC,IAAI;iBAC7B;gBACD,OAAO,EAAE,MAAM,CAAC,KAAK;aACtB,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IACG,GAAa,CAAC,OAAO;gBACtB,gDAAgD,EAChD,CAAC;gBACD,gEAAgE;gBAChE,qBAAqB;gBACrB,OAAO,EAAE,CAAC;YACZ,CAAC;YACD,MAAM,IAAI,sCAAwB,CAAE,GAAa,CAAC,OAAO,CAAC,CAAC;QAC7D,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;CACF;AAvCD,oDAuCC"}

View File

@@ -0,0 +1,30 @@
import { Script } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { EventManager } from '../session/EventManager.js';
import type { Realm } from './Realm.js';
/**
* Used to send messages from realm to BiDi user.
*/
export declare class ChannelProxy {
#private;
constructor(channel: Script.ChannelProperties, logger?: LoggerFn);
/**
* Creates a channel proxy in the given realm, initialises listener and
* returns a handle to `sendMessage` delegate.
*/
init(realm: Realm, eventManager: EventManager): Promise<Script.Handle>;
/** Gets a ChannelProxy from window and returns its handle. */
startListenerFromWindow(realm: Realm, eventManager: EventManager): Promise<void>;
/**
* String to be evaluated to create a ProxyChannel and put it to window.
* Returns the delegate `sendMessage`. Used to provide an argument for preload
* script. Does the following:
* 1. Creates a ChannelProxy.
* 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise
* by calling delegate stored in window['${this.#id}'].
* This is needed because `#getHandleFromWindow` can be called before or
* after this method.
* 3. Returns the delegate `sendMessage` of the created ChannelProxy.
*/
getEvalInWindowStr(): string;
}

View File

@@ -0,0 +1,235 @@
"use strict";
/*
* 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.ChannelProxy = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const log_js_1 = require("../../../utils/log.js");
const uuid_js_1 = require("../../../utils/uuid.js");
/**
* Used to send messages from realm to BiDi user.
*/
class ChannelProxy {
#properties;
#id = (0, uuid_js_1.uuidv4)();
#logger;
constructor(channel, logger) {
this.#properties = channel;
this.#logger = logger;
}
/**
* Creates a channel proxy in the given realm, initialises listener and
* returns a handle to `sendMessage` delegate.
*/
async init(realm, eventManager) {
const channelHandle = await ChannelProxy.#createAndGetHandleInRealm(realm);
const sendMessageHandle = await ChannelProxy.#createSendMessageHandle(realm, channelHandle);
void this.#startListener(realm, channelHandle, eventManager);
return sendMessageHandle;
}
/** Gets a ChannelProxy from window and returns its handle. */
async startListenerFromWindow(realm, eventManager) {
try {
const channelHandle = await this.#getHandleFromWindow(realm);
void this.#startListener(realm, channelHandle, eventManager);
}
catch (error) {
this.#logger?.(log_js_1.LogType.debugError, error);
}
}
/**
* Evaluation string which creates a ChannelProxy object on the client side.
*/
static #createChannelProxyEvalStr() {
const functionStr = String(() => {
const queue = [];
let queueNonEmptyResolver = null;
return {
/**
* Gets a promise, which is resolved as soon as a message occurs
* in the queue.
*/
async getMessage() {
const onMessage = queue.length > 0
? Promise.resolve()
: new Promise((resolve) => {
queueNonEmptyResolver = resolve;
});
await onMessage;
return queue.shift();
},
/**
* Adds a message to the queue.
* Resolves the pending promise if needed.
*/
sendMessage(message) {
queue.push(message);
if (queueNonEmptyResolver !== null) {
queueNonEmptyResolver();
queueNonEmptyResolver = null;
}
},
};
});
return `(${functionStr})()`;
}
/** Creates a ChannelProxy in the given realm. */
static async #createAndGetHandleInRealm(realm) {
const createChannelHandleResult = await realm.cdpClient.sendCommand('Runtime.evaluate', {
expression: this.#createChannelProxyEvalStr(),
contextId: realm.executionContextId,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
if (createChannelHandleResult.exceptionDetails ||
createChannelHandleResult.result.objectId === undefined) {
throw new Error(`Cannot create channel`);
}
return createChannelHandleResult.result.objectId;
}
/** Gets a handle to `sendMessage` delegate from the ChannelProxy handle. */
static async #createSendMessageHandle(realm, channelHandle) {
const sendMessageArgResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((channelHandle) => {
return channelHandle.sendMessage;
}),
arguments: [{ objectId: channelHandle }],
executionContextId: realm.executionContextId,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
// TODO: check for exceptionDetails.
return sendMessageArgResult.result.objectId;
}
/** Starts listening for the channel events of the provided ChannelProxy. */
async #startListener(realm, channelHandle, eventManager) {
// noinspection InfiniteLoopJS
for (;;) {
try {
const message = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String(async (channelHandle) => await channelHandle.getMessage()),
arguments: [
{
objectId: channelHandle,
},
],
awaitPromise: true,
executionContextId: realm.executionContextId,
serializationOptions: {
serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */,
maxDepth: this.#properties.serializationOptions?.maxObjectDepth ??
undefined,
},
});
if (message.exceptionDetails) {
throw new Error('Runtime.callFunctionOn in ChannelProxy', {
cause: message.exceptionDetails,
});
}
for (const browsingContext of realm.associatedBrowsingContexts) {
eventManager.registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.Script.EventNames.Message,
params: {
channel: this.#properties.channel,
data: realm.cdpToBidiValue(message, this.#properties.ownership ?? "none" /* Script.ResultOwnership.None */),
source: realm.source,
},
}, browsingContext.id);
}
}
catch (error) {
// If an error is thrown, then the channel is permanently broken, so we
// exit the loop.
this.#logger?.(log_js_1.LogType.debugError, error);
break;
}
}
}
/**
* Returns a handle of ChannelProxy from window's property which was set there
* by `getEvalInWindowStr`. If window property is not set yet, sets a promise
* resolver to the window property, so that `getEvalInWindowStr` can resolve
* the promise later on with the channel.
* This is needed because `getEvalInWindowStr` can be called before or
* after this method.
*/
async #getHandleFromWindow(realm) {
const channelHandleResult = await realm.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((id) => {
const w = window;
if (w[id] === undefined) {
// The channelProxy is not created yet. Create a promise, put the
// resolver to window property and return the promise.
// `getEvalInWindowStr` will resolve the promise later.
return new Promise((resolve) => (w[id] = resolve));
}
// The channelProxy is already created by `getEvalInWindowStr` and
// is set into window property. Return it.
const channelProxy = w[id];
delete w[id];
return channelProxy;
}),
arguments: [{ value: this.#id }],
executionContextId: realm.executionContextId,
awaitPromise: true,
serializationOptions: {
serialization: "idOnly" /* Protocol.Runtime.SerializationOptionsSerialization.IdOnly */,
},
});
if (channelHandleResult.exceptionDetails !== undefined ||
channelHandleResult.result.objectId === undefined) {
throw new Error(`ChannelHandle not found in window["${this.#id}"]`);
}
return channelHandleResult.result.objectId;
}
/**
* String to be evaluated to create a ProxyChannel and put it to window.
* Returns the delegate `sendMessage`. Used to provide an argument for preload
* script. Does the following:
* 1. Creates a ChannelProxy.
* 2. Puts the ChannelProxy to window['${this.#id}'] or resolves the promise
* by calling delegate stored in window['${this.#id}'].
* This is needed because `#getHandleFromWindow` can be called before or
* after this method.
* 3. Returns the delegate `sendMessage` of the created ChannelProxy.
*/
getEvalInWindowStr() {
const delegate = String((id, channelProxy) => {
const w = window;
if (w[id] === undefined) {
// `#getHandleFromWindow` is not initialized yet, and will get the
// channelProxy later.
w[id] = channelProxy;
}
else {
// `#getHandleFromWindow` is already set a delegate to window property
// and is waiting for it to be called with the channelProxy.
w[id](channelProxy);
delete w[id];
}
return channelProxy.sendMessage;
});
const channelProxyEval = ChannelProxy.#createChannelProxyEvalStr();
return `(${delegate})('${this.#id}',${channelProxyEval})`;
}
}
exports.ChannelProxy = ChannelProxy;
//# sourceMappingURL=ChannelProxy.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,43 @@
import type { Protocol } from 'devtools-protocol';
import type { Browser, BrowsingContext, Script } from '../../../protocol/protocol.js';
import type { LoggerFn } from '../../../utils/log.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import { ChannelProxy } from './ChannelProxy.js';
/**
* BiDi IDs are generated by the server and are unique within contexts.
*
* CDP preload script IDs are generated by the client and are unique
* within sessions.
*
* The mapping between BiDi and CDP preload script IDs is 1:many.
* BiDi IDs are needed by the mapper to keep track of potential multiple CDP IDs
* in the client.
*/
export declare class PreloadScript {
#private;
get id(): string;
get targetIds(): Set<Protocol.Target.TargetID>;
constructor(params: Script.AddPreloadScriptParameters, logger?: LoggerFn);
/** Channels of the preload script. */
get channels(): ChannelProxy[];
/** Contexts of the preload script, if any */
get contexts(): BrowsingContext.BrowsingContext[] | undefined;
/** UserContexts of the preload script, if any */
get userContexts(): Browser.UserContext[] | undefined;
/**
* Adds the script to the given CDP targets by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
initInTargets(cdpTargets: Iterable<CdpTarget>, runImmediately: boolean): Promise<void>;
/**
* Adds the script to the given CDP target by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
initInTarget(cdpTarget: CdpTarget, runImmediately: boolean): Promise<void>;
/**
* Removes this script from all CDP targets.
*/
remove(): Promise<void>;
/** Removes the provided cdp target from the list of cdp preload scripts. */
dispose(cdpTargetId: Protocol.Target.TargetID): void;
}

View File

@@ -0,0 +1,133 @@
"use strict";
/*
* 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.
*
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.PreloadScript = void 0;
const uuid_js_1 = require("../../../utils/uuid.js");
const ChannelProxy_js_1 = require("./ChannelProxy.js");
/**
* BiDi IDs are generated by the server and are unique within contexts.
*
* CDP preload script IDs are generated by the client and are unique
* within sessions.
*
* The mapping between BiDi and CDP preload script IDs is 1:many.
* BiDi IDs are needed by the mapper to keep track of potential multiple CDP IDs
* in the client.
*/
class PreloadScript {
/** BiDi ID, an automatically generated UUID. */
#id = (0, uuid_js_1.uuidv4)();
/** CDP preload scripts. */
#cdpPreloadScripts = [];
/** The script itself, in a format expected by the spec i.e. a function. */
#functionDeclaration;
/** Targets, in which the preload script is initialized. */
#targetIds = new Set();
/** Channels to be added as arguments to functionDeclaration. */
#channels;
/** The script sandbox / world name. */
#sandbox;
/** The browsing contexts to execute the preload scripts in, if any. */
#contexts;
/** The browsing contexts to execute the preload scripts in, if any. */
#userContexts;
get id() {
return this.#id;
}
get targetIds() {
return this.#targetIds;
}
constructor(params, logger) {
this.#channels =
params.arguments?.map((a) => new ChannelProxy_js_1.ChannelProxy(a.value, logger)) ?? [];
this.#functionDeclaration = params.functionDeclaration;
this.#sandbox = params.sandbox;
this.#contexts = params.contexts;
this.#userContexts = params.userContexts;
}
/** Channels of the preload script. */
get channels() {
return this.#channels;
}
/** Contexts of the preload script, if any */
get contexts() {
return this.#contexts;
}
/** UserContexts of the preload script, if any */
get userContexts() {
return this.#userContexts;
}
/**
* String to be evaluated. Wraps user-provided function so that the following
* steps are run:
* 1. Create channels.
* 2. Store the created channels in window.
* 3. Call the user-provided function with channels as arguments.
*/
#getEvaluateString() {
const channelsArgStr = `[${this.channels
.map((c) => c.getEvalInWindowStr())
.join(', ')}]`;
return `(()=>{(${this.#functionDeclaration})(...${channelsArgStr})})()`;
}
/**
* Adds the script to the given CDP targets by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
async initInTargets(cdpTargets, runImmediately) {
await Promise.all(Array.from(cdpTargets).map((cdpTarget) => this.initInTarget(cdpTarget, runImmediately)));
}
/**
* Adds the script to the given CDP target by calling the
* `Page.addScriptToEvaluateOnNewDocument` command.
*/
async initInTarget(cdpTarget, runImmediately) {
const addCdpPreloadScriptResult = await cdpTarget.cdpClient.sendCommand('Page.addScriptToEvaluateOnNewDocument', {
source: this.#getEvaluateString(),
worldName: this.#sandbox,
runImmediately,
});
this.#cdpPreloadScripts.push({
target: cdpTarget,
preloadScriptId: addCdpPreloadScriptResult.identifier,
});
this.#targetIds.add(cdpTarget.id);
}
/**
* Removes this script from all CDP targets.
*/
async remove() {
await Promise.all([
this.#cdpPreloadScripts.map(async (cdpPreloadScript) => {
const cdpTarget = cdpPreloadScript.target;
const cdpPreloadScriptId = cdpPreloadScript.preloadScriptId;
return await cdpTarget.cdpClient.sendCommand('Page.removeScriptToEvaluateOnNewDocument', {
identifier: cdpPreloadScriptId,
});
}),
]);
}
/** Removes the provided cdp target from the list of cdp preload scripts. */
dispose(cdpTargetId) {
this.#cdpPreloadScripts = this.#cdpPreloadScripts.filter((cdpPreloadScript) => cdpPreloadScript.target?.id !== cdpTargetId);
this.#targetIds.delete(cdpTargetId);
}
}
exports.PreloadScript = PreloadScript;
//# sourceMappingURL=PreloadScript.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PreloadScript.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/script/PreloadScript.ts"],"names":[],"mappings":"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"}

View File

@@ -0,0 +1,23 @@
import type { Browser } from '../../../protocol/protocol.js';
import type { CdpTarget } from '../cdp/CdpTarget.js';
import type { PreloadScript } from './PreloadScript.js';
/** PreloadScripts can be filtered by BiDi ID or target ID. */
export interface PreloadScriptFilter {
targetId: CdpTarget['id'];
}
/**
* Container class for preload scripts.
*/
export declare class PreloadScriptStorage {
#private;
/**
* Finds all entries that match the given filter (OR logic).
*/
find(filter?: PreloadScriptFilter): PreloadScript[];
add(preloadScript: PreloadScript): void;
/** Deletes all BiDi preload script entries that match the given filter. */
remove(id: string): void;
/** Gets the preload script with the given ID, if any, otherwise throws. */
getPreloadScript(id: string): PreloadScript;
onCdpTargetCreated(targetId: string, userContext: Browser.UserContext): void;
}

View File

@@ -0,0 +1,79 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PreloadScriptStorage = void 0;
/*
* 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.
*/
const ErrorResponse_js_1 = require("../../../protocol/ErrorResponse.js");
/**
* Container class for preload scripts.
*/
class PreloadScriptStorage {
/** Tracks all BiDi preload scripts. */
#scripts = new Set();
/**
* Finds all entries that match the given filter (OR logic).
*/
find(filter) {
if (!filter) {
return [...this.#scripts];
}
return [...this.#scripts].filter((script) => {
// Global scripts have no contexts or userContext
if (script.contexts === undefined && script.userContexts === undefined) {
return true;
}
if (filter.targetId !== undefined &&
script.targetIds.has(filter.targetId)) {
return true;
}
return false;
});
}
add(preloadScript) {
this.#scripts.add(preloadScript);
}
/** Deletes all BiDi preload script entries that match the given filter. */
remove(id) {
const script = [...this.#scripts].find((script) => script.id === id);
if (script === undefined) {
throw new ErrorResponse_js_1.NoSuchScriptException(`No preload script with id '${id}'`);
}
this.#scripts.delete(script);
}
/** Gets the preload script with the given ID, if any, otherwise throws. */
getPreloadScript(id) {
const script = [...this.#scripts].find((script) => script.id === id);
if (script === undefined) {
throw new ErrorResponse_js_1.NoSuchScriptException(`No preload script with id '${id}'`);
}
return script;
}
onCdpTargetCreated(targetId, userContext) {
const scriptInUserContext = [...this.#scripts].filter((script) => {
// Global scripts
if (!script.userContexts && !script.contexts) {
return true;
}
return script.userContexts?.includes(userContext);
});
for (const script of scriptInUserContext) {
script.targetIds.add(targetId);
}
}
}
exports.PreloadScriptStorage = PreloadScriptStorage;
//# sourceMappingURL=PreloadScriptStorage.js.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"PreloadScriptStorage.js","sourceRoot":"","sources":["../../../../../src/bidiMapper/modules/script/PreloadScriptStorage.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;GAeG;AACH,yEAAyE;AAWzE;;GAEG;AACH,MAAa,oBAAoB;IAC/B,wCAAwC;IAC/B,QAAQ,GAAG,IAAI,GAAG,EAAiB,CAAC;IAE7C;;OAEG;IACH,IAAI,CAAC,MAA4B;QAC/B,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC;QAC5B,CAAC;QAED,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;YAC1C,iDAAiD;YACjD,IAAI,MAAM,CAAC,QAAQ,KAAK,SAAS,IAAI,MAAM,CAAC,YAAY,KAAK,SAAS,EAAE,CAAC;gBACvE,OAAO,IAAI,CAAC;YACd,CAAC;YAED,IACE,MAAM,CAAC,QAAQ,KAAK,SAAS;gBAC7B,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,EACrC,CAAC;gBACD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC,CAAC,CAAC;IACL,CAAC;IAED,GAAG,CAAC,aAA4B;QAC9B,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IACnC,CAAC;IAED,2EAA2E;IAC3E,MAAM,CAAC,EAAU;QACf,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,wCAAqB,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IAC/B,CAAC;IAED,2EAA2E;IAC3E,gBAAgB,CAAC,EAAU;QACzB,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACrE,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,IAAI,wCAAqB,CAAC,8BAA8B,EAAE,GAAG,CAAC,CAAC;QACvE,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED,kBAAkB,CAAC,QAAgB,EAAE,WAAgC;QACnE,MAAM,mBAAmB,GAAG,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE;YAC/D,iBAAiB;YACjB,IAAI,CAAC,MAAM,CAAC,YAAY,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC;gBAC7C,OAAO,IAAI,CAAC;YACd,CAAC;YACD,OAAO,MAAM,CAAC,YAAY,EAAE,QAAQ,CAAC,WAAW,CAAC,CAAC;QACpD,CAAC,CAAC,CAAC;QACH,KAAK,MAAM,MAAM,IAAI,mBAAmB,EAAE,CAAC;YACzC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;CACF;AA/DD,oDA+DC"}

View File

@@ -0,0 +1,67 @@
/**
* Copyright 2022 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 { Protocol } from 'devtools-protocol';
import type { CdpClient } from '../../../cdp/CdpClient.js';
import { Script } from '../../../protocol/protocol.js';
import { type LoggerFn } from '../../../utils/log.js';
import type { BrowsingContextImpl } from '../context/BrowsingContextImpl.js';
import type { EventManager } from '../session/EventManager.js';
import type { RealmStorage } from './RealmStorage.js';
export declare abstract class Realm {
#private;
protected realmStorage: RealmStorage;
constructor(cdpClient: CdpClient, eventManager: EventManager, executionContextId: Protocol.Runtime.ExecutionContextId, logger: LoggerFn | undefined, origin: string, realmId: Script.Realm, realmStorage: RealmStorage);
cdpToBidiValue(cdpValue: Protocol.Runtime.CallFunctionOnResponse | Protocol.Runtime.EvaluateResponse, resultOwnership: Script.ResultOwnership): Script.RemoteValue;
isHidden(): boolean;
/**
* Relies on the CDP to implement proper BiDi serialization, except:
* * CDP integer property `backendNodeId` is replaced with `sharedId` of
* `{documentId}_element_{backendNodeId}`;
* * CDP integer property `weakLocalObjectReference` is replaced with UUID `internalId`
* using unique-per serialization `internalIdMap`.
* * CDP type `platformobject` is replaced with `object`.
* @param deepSerializedValue - CDP value to be converted to BiDi.
* @param internalIdMap - Map from CDP integer `weakLocalObjectReference` to BiDi UUID
* `internalId`.
*/
protected serializeForBiDi(deepSerializedValue: Protocol.Runtime.DeepSerializedValue, internalIdMap: Map<number, string>): Script.RemoteValue;
get realmId(): Script.Realm;
get executionContextId(): Protocol.Runtime.ExecutionContextId;
get origin(): string;
get source(): Script.Source;
get cdpClient(): CdpClient;
abstract get associatedBrowsingContexts(): BrowsingContextImpl[];
abstract get realmType(): Script.RealmType;
protected get baseInfo(): Script.BaseRealmInfo;
abstract get realmInfo(): Script.RealmInfo;
evaluate(expression: string, awaitPromise: boolean, resultOwnership?: Script.ResultOwnership, serializationOptions?: Script.SerializationOptions, userActivation?: boolean, includeCommandLineApi?: boolean): Promise<Script.EvaluateResult>;
protected initialize(): void;
/**
* Serializes a given CDP object into BiDi, keeping references in the
* target's `globalThis`.
*/
serializeCdpObject(cdpRemoteObject: Protocol.Runtime.RemoteObject, resultOwnership: Script.ResultOwnership): Promise<Script.RemoteValue>;
/**
* Gets the string representation of an object. This is equivalent to
* calling `toString()` on the object value.
*/
stringifyObject(cdpRemoteObject: Protocol.Runtime.RemoteObject): Promise<string>;
callFunction(functionDeclaration: string, awaitPromise: boolean, thisLocalValue?: Script.LocalValue, argumentsLocalValues?: Script.LocalValue[], resultOwnership?: Script.ResultOwnership, serializationOptions?: Script.SerializationOptions, userActivation?: boolean): Promise<Script.EvaluateResult>;
deserializeForCdp(localValue: Script.LocalValue): Promise<Protocol.Runtime.CallArgument>;
disown(handle: Script.Handle): Promise<void>;
dispose(): void;
}

View File

@@ -0,0 +1,485 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Realm = void 0;
const protocol_js_1 = require("../../../protocol/protocol.js");
const log_js_1 = require("../../../utils/log.js");
const uuid_js_1 = require("../../../utils/uuid.js");
const ChannelProxy_js_1 = require("./ChannelProxy.js");
class Realm {
#cdpClient;
#eventManager;
#executionContextId;
#logger;
#origin;
#realmId;
realmStorage;
constructor(cdpClient, eventManager, executionContextId, logger, origin, realmId, realmStorage) {
this.#cdpClient = cdpClient;
this.#eventManager = eventManager;
this.#executionContextId = executionContextId;
this.#logger = logger;
this.#origin = origin;
this.#realmId = realmId;
this.realmStorage = realmStorage;
this.realmStorage.addRealm(this);
}
cdpToBidiValue(cdpValue, resultOwnership) {
const bidiValue = this.serializeForBiDi(cdpValue.result.deepSerializedValue, new Map());
if (cdpValue.result.objectId) {
const objectId = cdpValue.result.objectId;
if (resultOwnership === "root" /* Script.ResultOwnership.Root */) {
// Extend BiDi value with `handle` based on required `resultOwnership`
// and CDP response but not on the actual BiDi type.
bidiValue.handle = objectId;
// Remember all the handles sent to client.
this.realmStorage.knownHandlesToRealmMap.set(objectId, this.realmId);
}
else {
// No need to await for the object to be released.
void this.#releaseObject(objectId).catch((error) => this.#logger?.(log_js_1.LogType.debugError, error));
}
}
return bidiValue;
}
isHidden() {
return false;
}
/**
* Relies on the CDP to implement proper BiDi serialization, except:
* * CDP integer property `backendNodeId` is replaced with `sharedId` of
* `{documentId}_element_{backendNodeId}`;
* * CDP integer property `weakLocalObjectReference` is replaced with UUID `internalId`
* using unique-per serialization `internalIdMap`.
* * CDP type `platformobject` is replaced with `object`.
* @param deepSerializedValue - CDP value to be converted to BiDi.
* @param internalIdMap - Map from CDP integer `weakLocalObjectReference` to BiDi UUID
* `internalId`.
*/
serializeForBiDi(deepSerializedValue, internalIdMap) {
if (Object.hasOwn(deepSerializedValue, 'weakLocalObjectReference')) {
const weakLocalObjectReference = deepSerializedValue.weakLocalObjectReference;
if (!internalIdMap.has(weakLocalObjectReference)) {
internalIdMap.set(weakLocalObjectReference, (0, uuid_js_1.uuidv4)());
}
deepSerializedValue.internalId = internalIdMap.get(weakLocalObjectReference);
delete deepSerializedValue['weakLocalObjectReference'];
}
if (deepSerializedValue.type === 'node' &&
deepSerializedValue.value &&
Object.hasOwn(deepSerializedValue.value, 'frameId')) {
// `frameId` is not needed in BiDi as it is not yet specified.
delete deepSerializedValue.value['frameId'];
}
// Platform object is a special case. It should have only `{type: object}`
// without `value` field.
if (deepSerializedValue.type === 'platformobject') {
return { type: 'object' };
}
const bidiValue = deepSerializedValue.value;
if (bidiValue === undefined) {
return deepSerializedValue;
}
// Recursively update the nested values.
if (['array', 'set', 'htmlcollection', 'nodelist'].includes(deepSerializedValue.type)) {
for (const i in bidiValue) {
bidiValue[i] = this.serializeForBiDi(bidiValue[i], internalIdMap);
}
}
if (['object', 'map'].includes(deepSerializedValue.type)) {
for (const i in bidiValue) {
bidiValue[i] = [
this.serializeForBiDi(bidiValue[i][0], internalIdMap),
this.serializeForBiDi(bidiValue[i][1], internalIdMap),
];
}
}
return deepSerializedValue;
}
get realmId() {
return this.#realmId;
}
get executionContextId() {
return this.#executionContextId;
}
get origin() {
return this.#origin;
}
get source() {
return {
realm: this.realmId,
};
}
get cdpClient() {
return this.#cdpClient;
}
get baseInfo() {
return {
realm: this.realmId,
origin: this.origin,
};
}
async evaluate(expression, awaitPromise, resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false, includeCommandLineApi = false) {
const cdpEvaluateResult = await this.cdpClient.sendCommand('Runtime.evaluate', {
contextId: this.executionContextId,
expression,
awaitPromise,
serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions),
userGesture: userActivation,
includeCommandLineAPI: includeCommandLineApi,
});
if (cdpEvaluateResult.exceptionDetails) {
return await this.#getExceptionResult(cdpEvaluateResult.exceptionDetails, 0, resultOwnership);
}
return {
realm: this.realmId,
result: this.cdpToBidiValue(cdpEvaluateResult, resultOwnership),
type: 'success',
};
}
#registerEvent(event) {
if (this.associatedBrowsingContexts.length === 0) {
this.#eventManager.registerGlobalEvent(event);
}
else {
for (const browsingContext of this.associatedBrowsingContexts) {
this.#eventManager.registerEvent(event, browsingContext.id);
}
}
}
initialize() {
if (!this.isHidden()) {
// Report only not-hidden realms.
this.#registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.Script.EventNames.RealmCreated,
params: this.realmInfo,
});
}
}
/**
* Serializes a given CDP object into BiDi, keeping references in the
* target's `globalThis`.
*/
async serializeCdpObject(cdpRemoteObject, resultOwnership) {
// TODO: if the object is a primitive, return it directly without CDP roundtrip.
const argument = Realm.#cdpRemoteObjectToCallArgument(cdpRemoteObject);
const cdpValue = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((remoteObject) => remoteObject),
awaitPromise: false,
arguments: [argument],
serializationOptions: {
serialization: "deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */,
},
executionContextId: this.executionContextId,
});
return this.cdpToBidiValue(cdpValue, resultOwnership);
}
static #cdpRemoteObjectToCallArgument(cdpRemoteObject) {
if (cdpRemoteObject.objectId !== undefined) {
return { objectId: cdpRemoteObject.objectId };
}
if (cdpRemoteObject.unserializableValue !== undefined) {
return { unserializableValue: cdpRemoteObject.unserializableValue };
}
return { value: cdpRemoteObject.value };
}
/**
* Gets the string representation of an object. This is equivalent to
* calling `toString()` on the object value.
*/
async stringifyObject(cdpRemoteObject) {
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((remoteObject) => String(remoteObject)),
awaitPromise: false,
arguments: [cdpRemoteObject],
returnByValue: true,
executionContextId: this.executionContextId,
});
return result.value;
}
async #flattenKeyValuePairs(mappingLocalValue) {
const keyValueArray = await Promise.all(mappingLocalValue.map(async ([key, value]) => {
let keyArg;
if (typeof key === 'string') {
// Key is a string.
keyArg = { value: key };
}
else {
// Key is a serialized value.
keyArg = await this.deserializeForCdp(key);
}
const valueArg = await this.deserializeForCdp(value);
return [keyArg, valueArg];
}));
return keyValueArray.flat();
}
async #flattenValueList(listLocalValue) {
return await Promise.all(listLocalValue.map((localValue) => this.deserializeForCdp(localValue)));
}
async #serializeCdpExceptionDetails(cdpExceptionDetails, lineOffset, resultOwnership) {
const callFrames = cdpExceptionDetails.stackTrace?.callFrames.map((frame) => ({
url: frame.url,
functionName: frame.functionName,
lineNumber: frame.lineNumber - lineOffset,
columnNumber: frame.columnNumber,
})) ?? [];
// Exception should always be there.
const exception = cdpExceptionDetails.exception;
return {
exception: await this.serializeCdpObject(exception, resultOwnership),
columnNumber: cdpExceptionDetails.columnNumber,
lineNumber: cdpExceptionDetails.lineNumber - lineOffset,
stackTrace: {
callFrames,
},
text: (await this.stringifyObject(exception)) || cdpExceptionDetails.text,
};
}
async callFunction(functionDeclaration, awaitPromise, thisLocalValue = {
type: 'undefined',
}, argumentsLocalValues = [], resultOwnership = "none" /* Script.ResultOwnership.None */, serializationOptions = {}, userActivation = false) {
const callFunctionAndSerializeScript = `(...args) => {
function callFunction(f, args) {
const deserializedThis = args.shift();
const deserializedArgs = args;
return f.apply(deserializedThis, deserializedArgs);
}
return callFunction((
${functionDeclaration}
), args);
}`;
const thisAndArgumentsList = [
await this.deserializeForCdp(thisLocalValue),
...(await Promise.all(argumentsLocalValues.map(async (argumentLocalValue) => await this.deserializeForCdp(argumentLocalValue)))),
];
let cdpCallFunctionResult;
try {
cdpCallFunctionResult = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: callFunctionAndSerializeScript,
awaitPromise,
arguments: thisAndArgumentsList,
serializationOptions: Realm.#getSerializationOptions("deep" /* Protocol.Runtime.SerializationOptionsSerialization.Deep */, serializationOptions),
executionContextId: this.executionContextId,
userGesture: userActivation,
});
}
catch (error) {
// Heuristic to determine if the problem is in the argument.
// The check can be done on the `deserialization` step, but this approach
// helps to save round-trips.
if (error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
[
'Could not find object with given id',
'Argument should belong to the same JavaScript world as target object',
'Invalid remote object id',
].includes(error.message)) {
throw new protocol_js_1.NoSuchHandleException('Handle was not found.');
}
throw error;
}
if (cdpCallFunctionResult.exceptionDetails) {
return await this.#getExceptionResult(cdpCallFunctionResult.exceptionDetails, 1, resultOwnership);
}
return {
type: 'success',
result: this.cdpToBidiValue(cdpCallFunctionResult, resultOwnership),
realm: this.realmId,
};
}
async deserializeForCdp(localValue) {
if ('handle' in localValue && localValue.handle) {
return { objectId: localValue.handle };
// We tried to find a handle value but failed
// This allows us to have exhaustive switch on `localValue.type`
}
else if ('handle' in localValue || 'sharedId' in localValue) {
throw new protocol_js_1.NoSuchHandleException('Handle was not found.');
}
switch (localValue.type) {
case 'undefined':
return { unserializableValue: 'undefined' };
case 'null':
return { unserializableValue: 'null' };
case 'string':
return { value: localValue.value };
case 'number':
if (localValue.value === 'NaN') {
return { unserializableValue: 'NaN' };
}
else if (localValue.value === '-0') {
return { unserializableValue: '-0' };
}
else if (localValue.value === 'Infinity') {
return { unserializableValue: 'Infinity' };
}
else if (localValue.value === '-Infinity') {
return { unserializableValue: '-Infinity' };
}
return {
value: localValue.value,
};
case 'boolean':
return { value: Boolean(localValue.value) };
case 'bigint':
return {
unserializableValue: `BigInt(${JSON.stringify(localValue.value)})`,
};
case 'date':
return {
unserializableValue: `new Date(Date.parse(${JSON.stringify(localValue.value)}))`,
};
case 'regexp':
return {
unserializableValue: `new RegExp(${JSON.stringify(localValue.value.pattern)}, ${JSON.stringify(localValue.value.flags)})`,
};
case 'map': {
// TODO: If none of the nested keys and values has a remote
// reference, serialize to `unserializableValue` without CDP roundtrip.
const keyValueArray = await this.#flattenKeyValuePairs(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => {
const result = new Map();
for (let i = 0; i < args.length; i += 2) {
result.set(args[i], args[i + 1]);
}
return result;
}),
awaitPromise: false,
arguments: keyValueArray,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'object': {
// TODO: If none of the nested keys and values has a remote
// reference, serialize to `unserializableValue` without CDP roundtrip.
const keyValueArray = await this.#flattenKeyValuePairs(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => {
const result = {};
for (let i = 0; i < args.length; i += 2) {
// Key should be either `string`, `number`, or `symbol`.
const key = args[i];
result[key] = args[i + 1];
}
return result;
}),
awaitPromise: false,
arguments: keyValueArray,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'array': {
// TODO: If none of the nested items has a remote reference,
// serialize to `unserializableValue` without CDP roundtrip.
const args = await this.#flattenValueList(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => args),
awaitPromise: false,
arguments: args,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'set': {
// TODO: if none of the nested items has a remote reference,
// serialize to `unserializableValue` without CDP roundtrip.
const args = await this.#flattenValueList(localValue.value);
const { result } = await this.cdpClient.sendCommand('Runtime.callFunctionOn', {
functionDeclaration: String((...args) => new Set(args)),
awaitPromise: false,
arguments: args,
returnByValue: false,
executionContextId: this.executionContextId,
});
// TODO(#375): Release `result.objectId` after using.
return { objectId: result.objectId };
}
case 'channel': {
const channelProxy = new ChannelProxy_js_1.ChannelProxy(localValue.value, this.#logger);
const channelProxySendMessageHandle = await channelProxy.init(this, this.#eventManager);
return { objectId: channelProxySendMessageHandle };
}
// TODO(#375): Dispose of nested objects.
}
// Intentionally outside to handle unknown types
throw new Error(`Value ${JSON.stringify(localValue)} is not deserializable.`);
}
async #getExceptionResult(exceptionDetails, lineOffset, resultOwnership) {
return {
exceptionDetails: await this.#serializeCdpExceptionDetails(exceptionDetails, lineOffset, resultOwnership),
realm: this.realmId,
type: 'exception',
};
}
static #getSerializationOptions(serialization, serializationOptions) {
return {
serialization,
additionalParameters: Realm.#getAdditionalSerializationParameters(serializationOptions),
...Realm.#getMaxObjectDepth(serializationOptions),
};
}
static #getAdditionalSerializationParameters(serializationOptions) {
const additionalParameters = {};
if (serializationOptions.maxDomDepth !== undefined) {
additionalParameters['maxNodeDepth'] =
serializationOptions.maxDomDepth === null
? 1000
: serializationOptions.maxDomDepth;
}
if (serializationOptions.includeShadowTree !== undefined) {
additionalParameters['includeShadowTree'] =
serializationOptions.includeShadowTree;
}
return additionalParameters;
}
static #getMaxObjectDepth(serializationOptions) {
return serializationOptions.maxObjectDepth === undefined ||
serializationOptions.maxObjectDepth === null
? {}
: { maxDepth: serializationOptions.maxObjectDepth };
}
async #releaseObject(handle) {
try {
await this.cdpClient.sendCommand('Runtime.releaseObject', {
objectId: handle,
});
}
catch (error) {
// Heuristic to determine if the problem is in the unknown handler.
// Ignore the error if so.
if (!(error.code === -32000 /* CdpErrorConstants.GENERIC_ERROR */ &&
error.message === 'Invalid remote object id')) {
throw error;
}
}
}
async disown(handle) {
// Disowning an object from different realm does nothing.
if (this.realmStorage.knownHandlesToRealmMap.get(handle) !== this.realmId) {
return;
}
await this.#releaseObject(handle);
this.realmStorage.knownHandlesToRealmMap.delete(handle);
}
dispose() {
if (!this.isHidden()) {
this.#registerEvent({
type: 'event',
method: protocol_js_1.ChromiumBidi.Script.EventNames.RealmDestroyed,
params: {
realm: this.realmId,
},
});
}
}
}
exports.Realm = Realm;
//# sourceMappingURL=Realm.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,45 @@
/**
* 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 type { Protocol } from 'devtools-protocol';
import { type BrowsingContext, type Script } from '../../../protocol/protocol.js';
import type { Realm } from './Realm.js';
interface RealmFilter {
realmId?: Script.Realm;
browsingContextId?: BrowsingContext.BrowsingContext;
executionContextId?: Protocol.Runtime.ExecutionContextId;
origin?: string;
type?: Script.RealmType;
sandbox?: string | null;
cdpSessionId?: Protocol.Target.SessionID;
isHidden?: boolean;
}
/** Container class for browsing realms. */
export declare class RealmStorage {
#private;
/** List of the internal sandboxed realms which should not be reported to the user. */
readonly hiddenSandboxes: Set<string | undefined>;
get knownHandlesToRealmMap(): Map<string, string>;
addRealm(realm: Realm): void;
/** Finds all realms that match the given filter. */
findRealms(filter: RealmFilter): Realm[];
findRealm(filter: RealmFilter): Realm | undefined;
/** Gets the only realm that matches the given filter, if any, otherwise throws. */
getRealm(filter: RealmFilter): Realm;
/** Deletes all realms that match the given filter. */
deleteRealms(filter: RealmFilter): void;
}
export {};

Some files were not shown because too many files have changed in this diff Show More