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,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