// Copyright 2011 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. // TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration /* eslint-disable @typescript-eslint/naming-convention */ /* eslint-disable @devtools/no-imperative-dom-api */ import type {Chrome} from '../../../extension-api/ExtensionAPI.js'; import * as Common from '../../core/common/common.js'; import * as Host from '../../core/host/host.js'; import * as i18n from '../../core/i18n/i18n.js'; import * as Platform from '../../core/platform/platform.js'; import * as _ProtocolClient from '../../core/protocol_client/protocol_client.js'; // eslint-disable-line @typescript-eslint/no-unused-vars import * as SDK from '../../core/sdk/sdk.js'; import type * as Protocol from '../../generated/protocol.js'; import * as Bindings from '../../models/bindings/bindings.js'; import * as Extensions from '../../models/extensions/extensions.js'; import * as HAR from '../../models/har/har.js'; import * as Logs from '../../models/logs/logs.js'; import * as TextUtils from '../../models/text_utils/text_utils.js'; import * as Workspace from '../../models/workspace/workspace.js'; import * as Components from '../../ui/legacy/components/utils/utils.js'; import * as UI from '../../ui/legacy/legacy.js'; import * as ThemeSupport from '../../ui/legacy/theme_support/theme_support.js'; import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js'; const extensionOrigins = new WeakMap(); const kPermittedSchemes = ['http:', 'https:', 'file:', 'data:', 'chrome-extension:', 'about:']; declare global { interface Window { DevToolsAPI?: {getInspectedTabId?(): string|undefined, getOriginsForbiddenForExtensions?(): string[]}; } } let extensionServerInstance: ExtensionServer|null; export class HostsPolicy { static create(policy?: Host.InspectorFrontendHostAPI.ExtensionHostsPolicy): HostsPolicy|null { const runtimeAllowedHosts = []; const runtimeBlockedHosts = []; if (policy) { for (const pattern of policy.runtimeAllowedHosts) { const parsedPattern = Extensions.HostUrlPattern.HostUrlPattern.parse(pattern); if (!parsedPattern) { return null; } runtimeAllowedHosts.push(parsedPattern); } for (const pattern of policy.runtimeBlockedHosts) { const parsedPattern = Extensions.HostUrlPattern.HostUrlPattern.parse(pattern); if (!parsedPattern) { return null; } runtimeBlockedHosts.push(parsedPattern); } } return new HostsPolicy(runtimeAllowedHosts, runtimeBlockedHosts); } private constructor( readonly runtimeAllowedHosts: Extensions.HostUrlPattern.HostUrlPattern[], readonly runtimeBlockedHosts: Extensions.HostUrlPattern.HostUrlPattern[]) { } isAllowedOnURL(inspectedURL?: Platform.DevToolsPath.UrlString): boolean { if (!inspectedURL) { // If there aren't any blocked hosts retain the old behavior and don't worry about the inspectedURL return this.runtimeBlockedHosts.length === 0; } if (this.runtimeBlockedHosts.some(pattern => pattern.matchesUrl(inspectedURL)) && !this.runtimeAllowedHosts.some(pattern => pattern.matchesUrl(inspectedURL))) { return false; } return true; } } class RegisteredExtension { openResourceScheme: null|string = null; constructor(readonly name: string, readonly hostsPolicy: HostsPolicy, readonly allowFileAccess: boolean) { } isAllowedOnTarget(inspectedURL?: Platform.DevToolsPath.UrlString): boolean { if (!inspectedURL) { inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL(); } if (!inspectedURL) { return false; } if (this.openResourceScheme && inspectedURL.startsWith(this.openResourceScheme)) { return true; } if (!ExtensionServer.canInspectURL(inspectedURL)) { return false; } if (!this.hostsPolicy.isAllowedOnURL(inspectedURL)) { return false; } if (!this.allowFileAccess) { let parsedURL; try { parsedURL = new URL(inspectedURL); } catch { return false; } return parsedURL.protocol !== 'file:'; } return true; } } export class RevealableNetworkRequestFilter { constructor(readonly filter: string|undefined) { } } export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper { private readonly clientObjects: Map; private readonly handlers: Map unknown>; private readonly subscribers: Map>; private readonly subscriptionStartHandlers: Map unknown>; private readonly subscriptionStopHandlers: Map unknown>; private readonly extraHeaders: Map>; private requests: Map; private readonly requestIds: Map; private lastRequestId: number; private registeredExtensions: Map; private status: ExtensionStatus; readonly #sidebarPanes: ExtensionSidebarPane[]; private extensionsEnabled: boolean; private inspectedTabId?: string; private readonly extensionAPITestHook?: (server: unknown, api: unknown) => unknown; private themeChangeHandlers = new Map(); readonly #pendingExtensions: Host.InspectorFrontendHostAPI.ExtensionDescriptor[] = []; private constructor() { super(); this.clientObjects = new Map(); this.handlers = new Map(); this.subscribers = new Map(); this.subscriptionStartHandlers = new Map(); this.subscriptionStopHandlers = new Map(); this.extraHeaders = new Map(); this.requests = new Map(); this.requestIds = new Map(); this.lastRequestId = 0; this.registeredExtensions = new Map(); this.status = new ExtensionStatus(); this.#sidebarPanes = []; // TODO(caseq): properly unload extensions when we disable them. this.extensionsEnabled = true; this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders, this.onAddRequestHeaders.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel, this.onCreatePanel.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane, this.onCreateSidebarPane.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton, this.onCreateToolbarButton.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage, this.onEvaluateOnInspectedPage.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent, this.onForwardKeyboardEvent.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR, this.onGetHAR.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.GetPageResources, this.onGetPageResources.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent, this.onGetRequestContent.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent, this.onGetResourceContent.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Reload, this.onReload.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler, this.onSetOpenResourceHandler.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler, this.onSetThemeChangeHandler.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent, this.onSetResourceContent.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.AttachSourceMapToResource, this.onAttachSourceMapToResource.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight, this.onSetSidebarHeight.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent, this.onSetSidebarContent.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage, this.onSetSidebarPage.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel, this.onShowPanel.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe, this.onSubscribe.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource, this.onOpenResource.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe, this.onUnsubscribe.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton, this.onUpdateButton.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin, this.registerLanguageExtensionEndpoint.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory, this.onGetWasmLinearMemory.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal, this.onGetWasmGlobal.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal, this.onGetWasmLocal.bind(this)); this.registerHandler(Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp, this.onGetWasmOp.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin, this.registerRecorderExtensionEndpoint.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad, this.onReportResourceLoad.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript, this.onSetFunctionRangesForScript.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView, this.onCreateRecorderView.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView, this.onShowRecorderView.bind(this)); this.registerHandler( Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel, this.onShowNetworkPanel.bind(this)); window.addEventListener('message', this.onWindowMessage, false); // Only for main window. const existingTabId = window.DevToolsAPI?.getInspectedTabId?.(); if (existingTabId) { this.setInspectedTabId({data: existingTabId}); } Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.addEventListener( Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this.setInspectedTabId, this); this.initExtensions(); ThemeSupport.ThemeSupport.instance().addEventListener(ThemeSupport.ThemeChangeEvent.eventName, this.#onThemeChange); } get isEnabledForTest(): boolean { return this.extensionsEnabled; } dispose(): void { ThemeSupport.ThemeSupport.instance().removeEventListener( ThemeSupport.ThemeChangeEvent.eventName, this.#onThemeChange); // Set up by this.initExtensions in the constructor. SDK.TargetManager.TargetManager.instance().removeEventListener( SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this); Host.InspectorFrontendHost.InspectorFrontendHostInstance.events.removeEventListener( Host.InspectorFrontendHostAPI.Events.SetInspectedTabId, this.setInspectedTabId, this); window.removeEventListener('message', this.onWindowMessage, false); } #onThemeChange = (): void => { const themeName = ThemeSupport.ThemeSupport.instance().themeName(); for (const port of this.themeChangeHandlers.values()) { port.postMessage({command: Extensions.ExtensionAPI.PrivateAPI.Events.ThemeChange, themeName}); } }; static instance(opts: { forceNew: boolean|null, } = {forceNew: null}): ExtensionServer { const {forceNew} = opts; if (!extensionServerInstance || forceNew) { extensionServerInstance?.dispose(); extensionServerInstance = new ExtensionServer(); } return extensionServerInstance; } initializeExtensions(): void { // Defer initialization until DevTools is fully loaded. if (this.inspectedTabId !== null) { Host.InspectorFrontendHost.InspectorFrontendHostInstance.setAddExtensionCallback(this.addExtension.bind(this)); } } hasExtensions(): boolean { return Boolean(this.registeredExtensions.size); } notifySearchAction(panelId: string, action: string, searchString?: string): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.PanelSearch + panelId, [action, searchString]); } notifyViewShown(identifier: string, frameIndex?: number): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ViewShown + identifier, [frameIndex]); } notifyViewHidden(identifier: string): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ViewHidden + identifier, []); } notifyButtonClicked(identifier: string): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ButtonClicked + identifier, []); } profilingStarted(): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ProfilingStarted, []); } profilingStopped(): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.ProfilingStopped, []); } private registerLanguageExtensionEndpoint( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, _shared_port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterLanguageExtensionPlugin}`); } const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); const {pluginName, port, supportedScriptTypes: {language, symbol_types}} = message; const symbol_types_array = (Array.isArray(symbol_types) && symbol_types.every(e => typeof e === 'string') ? symbol_types : []); const extensionOrigin = this.getExtensionOrigin(_shared_port); const registration = this.registeredExtensions.get(extensionOrigin); if (!registration) { throw new Error('Received a message from an unregistered extension'); } const endpoint = new Extensions.LanguageExtensionEndpoint.LanguageExtensionEndpoint( registration.allowFileAccess, extensionOrigin, pluginName, {language, symbol_types: symbol_types_array}, port); pluginManager.addPlugin(endpoint); return this.status.OK(); } private async loadWasmValue( expectValue: boolean, convert: (result: Protocol.Runtime.RemoteObject) => Record | T, expression: string, stopId: unknown): Promise { const {pluginManager} = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); const callFrame = pluginManager.callFrameForStopId(stopId as Bindings.DebuggerLanguagePlugins.StopId); if (!callFrame) { return this.status.E_BADARG('stopId', 'Unknown stop id'); } const result = await callFrame.debuggerModel.agent.invoke_evaluateOnCallFrame({ callFrameId: callFrame.id, expression, silent: true, returnByValue: !expectValue, generatePreview: expectValue, throwOnSideEffect: true, }); if (!result.exceptionDetails && !result.getError()) { return convert(result.result); } return this.status.E_FAILED('Failed'); } private async onGetWasmLinearMemory(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Promise { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLinearMemory}`); } return await this.loadWasmValue( false, result => result.value, `[].slice.call(new Uint8Array(memories[0].buffer, ${Number(message.offset)}, ${Number(message.length)}))`, message.stopId); } private convertWasmValue(valueClass: 'local'|'global'|'operand', index: number): (obj: Protocol.Runtime.RemoteObject) => Chrome.DevTools.WasmValue | undefined | Record { return obj => { if (obj.type === 'undefined') { return; } if (obj.type !== 'object' || obj.subtype !== 'wasmvalue') { return this.status.E_FAILED('Bad object type'); } const type = obj?.description; const value: string = obj.preview?.properties?.find(o => o.name === 'value')?.value ?? ''; switch (type) { case 'i32': case 'f32': case 'f64': return {type, value: Number(value)}; case 'i64': return {type, value: BigInt(value.replace(/n$/, ''))}; case 'v128': return {type, value}; default: return {type: 'reftype', valueClass, index}; } }; } private async onGetWasmGlobal(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Promise { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmGlobal}`); } const global = Number(message.global); const result = await this.loadWasmValue( true, this.convertWasmValue('global', global), `globals[${global}]`, message.stopId); return result ?? this.status.E_BADARG('global', `No global with index ${global}`); } private async onGetWasmLocal(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Promise { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmLocal}`); } const local = Number(message.local); const result = await this.loadWasmValue( true, this.convertWasmValue('local', local), `locals[${local}]`, message.stopId); return result ?? this.status.E_BADARG('local', `No local with index ${local}`); } private async onGetWasmOp(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Promise { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetWasmOp}`); } const op = Number(message.op); const result = await this.loadWasmValue( true, this.convertWasmValue('operand', op), `stack[${op}]`, message.stopId); return result ?? this.status.E_BADARG('op', `No operand with index ${op}`); } private registerRecorderExtensionEndpoint( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, _shared_port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.RegisterRecorderExtensionPlugin}`); } const {pluginName, mediaType, port, capabilities} = message; Extensions.RecorderPluginManager.RecorderPluginManager.instance().addPlugin( new Extensions.RecorderExtensionEndpoint.RecorderExtensionEndpoint(pluginName, port, capabilities, mediaType)); return this.status.OK(); } private onReportResourceLoad(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ReportResourceLoad}`); } const {resourceUrl, extensionId, status} = message; const url = resourceUrl as Platform.DevToolsPath.UrlString; const initiator: SDK.PageResourceLoader.ExtensionInitiator = {target: null, frameId: null, initiatorUrl: extensionId as Platform.DevToolsPath.UrlString, extensionId}; const pageResource: SDK.PageResourceLoader.PageResource = { url, initiator, errorMessage: status.errorMessage, success: status.success ?? null, size: status.size ?? null, duration: null, }; SDK.PageResourceLoader.PageResourceLoader.instance().resourceLoadedThroughExtension(pageResource); return this.status.OK(); } private onSetFunctionRangesForScript( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetFunctionRangesForScript}`); } const {scriptUrl, ranges} = message; if (!scriptUrl || !ranges?.length) { return this.status.E_BADARG('command', 'expected valid scriptUrl and non-empty NamedFunctionRanges'); } const resource = this.lookupAllowedUISourceCode(scriptUrl as Platform.DevToolsPath.UrlString, port); if ('error' in resource) { return resource.error; } const {uiSourceCode} = resource; if (!uiSourceCode.contentType().isScript() || !uiSourceCode.contentType().isFromSourceMap()) { return this.status.E_BADARG('command', `expected a source map script resource for url: ${scriptUrl}`); } try { Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().setFunctionRanges(uiSourceCode, ranges); } catch (e) { return this.status.E_FAILED(e); } return this.status.OK(); } private onShowRecorderView(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record |undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowRecorderView}`); } Extensions.RecorderPluginManager.RecorderPluginManager.instance().showView(message.id); return undefined; } private onShowNetworkPanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record |undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowNetworkPanel}`); } void Common.Revealer.reveal(new RevealableNetworkRequestFilter(message.filter)); return this.status.OK(); } private onCreateRecorderView( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateRecorderView}`); } const id = message.id; // The ids are generated on the client API side and must be unique, so the check below // shouldn't be hit unless someone is bypassing the API. if (this.clientObjects.has(id)) { return this.status.E_EXISTS(id); } const pagePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.pagePath); if (pagePath === undefined) { return this.status.E_BADARG('pagePath', 'Resources paths cannot point to non-extension resources'); } const onShown = (): void => this.notifyViewShown(id); const onHidden = (): void => this.notifyViewHidden(id); Extensions.RecorderPluginManager.RecorderPluginManager.instance().registerView({ id, pagePath, title: message.title, onShown, onHidden, }); return this.status.OK(); } private inspectedURLChanged(event: Common.EventTarget.EventTargetEvent): void { if (!ExtensionServer.canInspectURL(event.data.inspectedURL())) { this.disableExtensions(); return; } if (event.data !== SDK.TargetManager.TargetManager.instance().primaryPageTarget()) { return; } this.requests = new Map(); this.enableExtensions(); const url = event.data.inspectedURL(); this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.InspectedURLChanged, [url]); const extensions = this.#pendingExtensions.splice(0); extensions.forEach(e => this.addExtension(e)); } hasSubscribers(type: string): boolean { return this.subscribers.has(type); } private postNotification(type: string, args: unknown[], filter?: (extension: RegisteredExtension) => boolean): void { if (!this.extensionsEnabled) { return; } const subscribers = this.subscribers.get(type); if (!subscribers) { return; } const message = {command: 'notify-' + type, arguments: args}; for (const subscriber of subscribers) { if (!this.extensionEnabled(subscriber)) { continue; } if (filter) { const origin = extensionOrigins.get(subscriber); const extension = origin && this.registeredExtensions.get(origin); if (!extension || !filter(extension)) { continue; } } subscriber.postMessage(message); } } private onSubscribe(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Subscribe}`); } const subscribers = this.subscribers.get(message.type); if (subscribers) { subscribers.add(port); } else { this.subscribers.set(message.type, new Set([port])); const handler = this.subscriptionStartHandlers.get(message.type); if (handler) { handler(); } } return undefined; } private onUnsubscribe(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Unsubscribe}`); } const subscribers = this.subscribers.get(message.type); if (!subscribers) { return; } subscribers.delete(port); if (!subscribers.size) { this.subscribers.delete(message.type); const handler = this.subscriptionStopHandlers.get(message.type); if (handler) { handler(); } } return undefined; } private onAddRequestHeaders(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record |undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.AddRequestHeaders}`); } const id = message.extensionId; if (typeof id !== 'string') { return this.status.E_BADARGTYPE('extensionId', typeof id, 'string'); } let extensionHeaders = this.extraHeaders.get(id); if (!extensionHeaders) { extensionHeaders = new Map(); this.extraHeaders.set(id, extensionHeaders); } for (const name in message.headers) { extensionHeaders.set(name, message.headers[name]); } const allHeaders = ({} as Protocol.Network.Headers); for (const headers of this.extraHeaders.values()) { for (const [name, value] of headers) { if (name !== '__proto__' && typeof value === 'string') { allHeaders[name] = value; } } } SDK.NetworkManager.MultitargetNetworkManager.instance().setExtraHTTPHeaders(allHeaders); return undefined; } private getExtensionOrigin(port: MessagePort): Platform.DevToolsPath.UrlString { const origin = extensionOrigins.get(port); if (!origin) { throw new Error('Received a message from an unregistered extension'); } return origin; } private onCreatePanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreatePanel}`); } const id = message.id; // The ids are generated on the client API side and must be unique, so the check below // shouldn't be hit unless someone is bypassing the API. if (this.clientObjects.has(id) || UI.InspectorView.InspectorView.instance().hasPanel(id)) { return this.status.E_EXISTS(id); } const page = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.page); if (page === undefined) { return this.status.E_BADARG('page', 'Resources paths cannot point to non-extension resources'); } let persistentId = this.getExtensionOrigin(port) + message.title; persistentId = persistentId.replace(/\s|:\d+/g, ''); const panelView = new ExtensionServerPanelView( persistentId, i18n.i18n.lockedString(message.title), new ExtensionPanel(this, persistentId, id, page)); this.clientObjects.set(id, panelView); UI.InspectorView.InspectorView.instance().addPanel(panelView); return this.status.OK(); } private onShowPanel(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ShowPanel}`); } let panelViewId = message.id; const panelView = this.clientObjects.get(message.id); if (panelView && panelView instanceof ExtensionServerPanelView) { panelViewId = panelView.viewId(); } void UI.InspectorView.InspectorView.instance().showPanel(panelViewId); return undefined; } private onCreateToolbarButton( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateToolbarButton}`); } const panelView = this.clientObjects.get(message.panel); if (!panelView || !(panelView instanceof ExtensionServerPanelView)) { return this.status.E_NOTFOUND(message.panel); } const resourcePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.icon); if (resourcePath === undefined) { return this.status.E_BADARG('icon', 'Resources paths cannot point to non-extension resources'); } const button = new ExtensionButton(this, message.id, resourcePath, message.tooltip, message.disabled); this.clientObjects.set(message.id, button); void panelView.widget().then(appendButton); function appendButton(panel: UI.Widget.Widget): void { (panel as ExtensionPanel).addToolbarItem(button.toolbarButton()); } return this.status.OK(); } private onUpdateButton(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.UpdateButton}`); } const button = this.clientObjects.get(message.id); if (!button || !(button instanceof ExtensionButton)) { return this.status.E_NOTFOUND(message.id); } const resourcePath = message.icon && ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.icon); if (message.icon && resourcePath === undefined) { return this.status.E_BADARG('icon', 'Resources paths cannot point to non-extension resources'); } button.update(resourcePath, message.tooltip, message.disabled); return this.status.OK(); } private onCreateSidebarPane(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.CreateSidebarPane}`); } const id = message.id; const sidebar = new ExtensionSidebarPane(this, message.panel, i18n.i18n.lockedString(message.title), id); this.#sidebarPanes.push(sidebar); this.clientObjects.set(id, sidebar); this.dispatchEventToListeners(Events.SidebarPaneAdded, sidebar); return this.status.OK(); } sidebarPanes(): ExtensionSidebarPane[] { return this.#sidebarPanes; } private onSetSidebarHeight(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarHeight}`); } const sidebar = this.clientObjects.get(message.id); if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) { return this.status.E_NOTFOUND(message.id); } sidebar.setHeight(message.height); return this.status.OK(); } private onSetSidebarContent( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarContent}`); } const {requestId, id, rootTitle, expression, evaluateOptions, evaluateOnPage} = message; const sidebar = this.clientObjects.get(id); if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) { return this.status.E_NOTFOUND(message.id); } function callback(this: ExtensionServer, error: unknown): void { const result = error ? this.status.E_FAILED(error) : this.status.OK(); this.dispatchCallback(requestId, port, result); } if (evaluateOnPage) { sidebar.setExpression(expression, rootTitle, evaluateOptions, this.getExtensionOrigin(port), callback.bind(this)); return undefined; } sidebar.setObject(message.expression, message.rootTitle, callback.bind(this)); return undefined; } private onSetSidebarPage( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetSidebarPage}`); } const sidebar = this.clientObjects.get(message.id); if (!sidebar || !(sidebar instanceof ExtensionSidebarPane)) { return this.status.E_NOTFOUND(message.id); } const resourcePath = ExtensionServer.expandResourcePath(this.getExtensionOrigin(port), message.page); if (resourcePath === undefined) { return this.status.E_BADARG('page', 'Resources paths cannot point to non-extension resources'); } sidebar.setPage(resourcePath); return undefined; } private onOpenResource(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.OpenResource}`); } const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(message.url); if (uiSourceCode) { void Common.Revealer.reveal(uiSourceCode.uiLocation(message.lineNumber, message.columnNumber)); return this.status.OK(); } const resource = Bindings.ResourceUtils.resourceForURL(message.url); if (resource) { void Common.Revealer.reveal(resource); return this.status.OK(); } const request = Logs.NetworkLog.NetworkLog.instance().requestForURL(message.url); if (request) { void Common.Revealer.reveal(request); return this.status.OK(); } return this.status.E_NOTFOUND(message.url); } private onSetOpenResourceHandler( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetOpenResourceHandler}`); } const extension = this.registeredExtensions.get(this.getExtensionOrigin(port)); if (!extension) { throw new Error('Received a message from an unregistered extension'); } if (message.urlScheme) { extension.openResourceScheme = message.urlScheme; } const extensionOrigin = this.getExtensionOrigin(port); const {name} = extension; const registration = { title: name, origin: extensionOrigin, scheme: message.urlScheme, handler: this.handleOpenURL.bind(this, port), shouldHandleOpenResource: (url: Platform.DevToolsPath.UrlString, schemes: Set) => Components.Linkifier.Linkifier.shouldHandleOpenResource(extension.openResourceScheme, url, schemes), }; if (message.handlerPresent) { Components.Linkifier.Linkifier.registerLinkHandler(registration); } else { Components.Linkifier.Linkifier.unregisterLinkHandler(registration); } return undefined; } private onSetThemeChangeHandler( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetThemeChangeHandler}`); } const extensionOrigin = this.getExtensionOrigin(port); const extension = this.registeredExtensions.get(extensionOrigin); if (!extension) { throw new Error('Received a message from an unregistered extension'); } if (message.handlerPresent) { this.themeChangeHandlers.set(extensionOrigin, port); } else { this.themeChangeHandlers.delete(extensionOrigin); } return undefined; } private handleOpenURL( port: MessagePort, contentProviderOrUrl: TextUtils.ContentProvider.ContentProvider|string, lineNumber?: number, columnNumber?: number): void { let resource: {url: string, type: string}; let isAllowed: boolean; if (typeof contentProviderOrUrl !== 'string') { resource = this.makeResource(contentProviderOrUrl); isAllowed = this.extensionAllowedOnContentProvider(contentProviderOrUrl, port); } else { const url = contentProviderOrUrl as Platform.DevToolsPath.UrlString; resource = {url, type: Common.ResourceType.resourceTypes.Other.name()}; isAllowed = this.extensionAllowedOnURL(url, port); } if (isAllowed) { port.postMessage({ command: 'open-resource', resource, lineNumber: lineNumber ? lineNumber + 1 : undefined, columnNumber: columnNumber ? columnNumber + 1 : undefined, }); } } private extensionAllowedOnURL(url: Platform.DevToolsPath.UrlString, port: MessagePort): boolean { const origin = extensionOrigins.get(port); const extension = origin && this.registeredExtensions.get(origin); return Boolean(extension?.isAllowedOnTarget(url)); } /** * Slightly more permissive as {@link extensionAllowedOnURL}: This method also permits * UISourceCodes that originate from a {@link SDK.Script.Script} with a sourceURL magic comment as * long as the corresponding target is permitted. */ private extensionAllowedOnContentProvider( contentProvider: TextUtils.ContentProvider.ContentProvider, port: MessagePort): boolean { if (!(contentProvider instanceof Workspace.UISourceCode.UISourceCode)) { return this.extensionAllowedOnURL(contentProvider.contentURL(), port); } if (contentProvider.contentType() !== Common.ResourceType.resourceTypes.Script) { // We only check sourceURL magic comments for scripts (excluding ones coming from source maps). return this.extensionAllowedOnURL(contentProvider.contentURL(), port); } const scripts = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance().scriptsForUISourceCode(contentProvider); if (scripts.length === 0) { return this.extensionAllowedOnURL(contentProvider.contentURL(), port); } return scripts.every(script => { if (script.hasSourceURL) { return this.extensionAllowedOnTarget(script.target(), port); } return this.extensionAllowedOnURL(script.contentURL(), port); }); } /** * This method prefers returning 'Permission denied' errors if restricted resources are not found, * rather then NOTFOUND. This prevents extensions from being able to fish for restricted resources. */ private lookupAllowedUISourceCode(url: Platform.DevToolsPath.UrlString, port: MessagePort): {uiSourceCode: Workspace.UISourceCode.UISourceCode}|{ error: Record, } { const uiSourceCode = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url); if (!uiSourceCode && !this.extensionAllowedOnURL(url, port)) { return {error: this.status.E_FAILED('Permission denied')}; } if (!uiSourceCode) { return {error: this.status.E_NOTFOUND(url)}; } if (!this.extensionAllowedOnContentProvider(uiSourceCode, port)) { return {error: this.status.E_FAILED('Permission denied')}; } return {uiSourceCode}; } private extensionAllowedOnTarget(target: SDK.Target.Target, port: MessagePort): boolean { return this.extensionAllowedOnURL(target.inspectedURL(), port); } private onReload(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.Reload) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.Reload}`); } const options = (message.options || {}); SDK.NetworkManager.MultitargetNetworkManager.instance().setUserAgentOverride( typeof options.userAgent === 'string' ? options.userAgent : '', null); let injectedScript; if (options.injectedScript) { injectedScript = '(function(){' + options.injectedScript + '})()'; } const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); if (!target) { return this.status.OK(); } const resourceTreeModel = target.model(SDK.ResourceTreeModel.ResourceTreeModel); if (!this.extensionAllowedOnTarget(target, port)) { return this.status.E_FAILED('Permission denied'); } resourceTreeModel?.reloadPage(Boolean(options.ignoreCache), injectedScript); return this.status.OK(); } private onEvaluateOnInspectedPage( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.EvaluateOnInspectedPage}`); } const {requestId, expression, evaluateOptions} = message; function callback( this: ExtensionServer, error: string|null, object: SDK.RemoteObject.RemoteObject|null, wasThrown: boolean): void { let result; if (error || !object) { result = this.status.E_PROTOCOLERROR(error?.toString()); } else if (wasThrown) { result = {isException: true, value: object.description}; } else { result = {value: object.value}; } this.dispatchCallback(requestId, port, result); } return this.evaluate(expression, true, true, evaluateOptions, this.getExtensionOrigin(port), callback.bind(this)); } private async onGetHAR(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Promise { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR) { return this.status.E_BADARG('command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetHAR}`); } const requests = Logs.NetworkLog.NetworkLog.instance().requests().filter(r => this.extensionAllowedOnURL(r.url(), port)); const harLog = await HAR.Log.Log.build(requests, {sanitize: false}); for (let i = 0; i < harLog.entries.length; ++i) { // @ts-expect-error harLog.entries[i]._requestId = this.requestId(requests[i]); } return harLog; } private makeResource(contentProvider: TextUtils.ContentProvider.ContentProvider): {url: string, type: string, buildId?: string} { let buildId: string|undefined = undefined; if (contentProvider instanceof Workspace.UISourceCode.UISourceCode) { // We use the first buildId we find searching in all Script objects that correspond to this UISourceCode. buildId = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance() .scriptsForUISourceCode(contentProvider) .find(script => Boolean(script.buildId)) ?.buildId ?? undefined; } return {url: contentProvider.contentURL(), type: contentProvider.contentType().name(), buildId}; } private onGetPageResources(_message: unknown, port: MessagePort): Array<{url: string, type: string}> { const resources = new Map(); function pushResourceData( this: ExtensionServer, contentProvider: TextUtils.ContentProvider.ContentProvider): boolean { if (!resources.has(contentProvider.contentURL()) && this.extensionAllowedOnContentProvider(contentProvider, port)) { resources.set(contentProvider.contentURL(), this.makeResource(contentProvider)); } return false; } let uiSourceCodes = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType( Workspace.Workspace.projectTypes.Network); uiSourceCodes = uiSourceCodes.concat(Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodesForProjectType( Workspace.Workspace.projectTypes.ContentScripts)); uiSourceCodes.forEach(pushResourceData.bind(this)); for (const resourceTreeModel of SDK.TargetManager.TargetManager.instance().models( SDK.ResourceTreeModel.ResourceTreeModel)) { if (this.extensionAllowedOnTarget(resourceTreeModel.target(), port)) { resourceTreeModel.forAllResources(pushResourceData.bind(this)); } } return [...resources.values()]; } private async getResourceContent( contentProvider: TextUtils.ContentProvider.ContentProvider, message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Promise { if (!this.extensionAllowedOnContentProvider(contentProvider, port)) { this.dispatchCallback(message.requestId, port, this.status.E_FAILED('Permission denied')); return undefined; } const contentData = await contentProvider.requestContentData(); if (TextUtils.ContentData.ContentData.isError(contentData)) { this.dispatchCallback(message.requestId, port, {encoding: '', content: null}); return; } const encoding = !contentData.isTextContent ? 'base64' : ''; const content = contentData.isTextContent ? contentData.text : contentData.base64; this.dispatchCallback(message.requestId, port, {encoding, content}); } private onGetRequestContent( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetRequestContent}`); } const request = this.requestById(message.id); if (!request) { return this.status.E_NOTFOUND(message.id); } void this.getResourceContent(request, message, port); return undefined; } private onGetResourceContent( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent}`); } const url = message.url as Platform.DevToolsPath.UrlString; const contentProvider = Workspace.Workspace.WorkspaceImpl.instance().uiSourceCodeForURL(url) || Bindings.ResourceUtils.resourceForURL(url); if (!contentProvider) { return this.status.E_NOTFOUND(url); } void this.getResourceContent(contentProvider, message, port); return undefined; } private onAttachSourceMapToResource( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.AttachSourceMapToResource) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.GetResourceContent}`); } if (!message.sourceMapURL) { return this.status.E_FAILED('Expected a source map URL but got null'); } const resource = this.lookupAllowedUISourceCode(message.contentUrl as Platform.DevToolsPath.UrlString, port); if ('error' in resource) { return resource.error; } const debuggerBindingsInstance = Bindings.DebuggerWorkspaceBinding.DebuggerWorkspaceBinding.instance(); const scriptFiles = debuggerBindingsInstance.scriptsForUISourceCode(resource.uiSourceCode); if (scriptFiles.length > 0) { for (const script of scriptFiles) { const resourceFile = debuggerBindingsInstance.scriptFile(resource.uiSourceCode, script.debuggerModel); resourceFile?.addSourceMapURL(message.sourceMapURL as Platform.DevToolsPath.UrlString); } } return this.status.OK(); } private onSetResourceContent( message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort): Record|undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.SetResourceContent}`); } const {url, requestId, content, commit} = message; function callbackWrapper(this: ExtensionServer, error: string|null): void { const response = error ? this.status.E_FAILED(error) : this.status.OK(); this.dispatchCallback(requestId, port, response); } const resource = this.lookupAllowedUISourceCode(url as Platform.DevToolsPath.UrlString, port); if ('error' in resource) { return resource.error; } const {uiSourceCode} = resource; if (!uiSourceCode.contentType().isDocumentOrScriptOrStyleSheet()) { const resource = SDK.ResourceTreeModel.ResourceTreeModel.resourceForURL(url as Platform.DevToolsPath.UrlString); if (!resource) { return this.status.E_NOTFOUND(url); } return this.status.E_NOTSUPPORTED('Resource is not editable'); } uiSourceCode.setWorkingCopy(content); if (commit) { uiSourceCode.commitWorkingCopy(); } callbackWrapper.call(this, null); return undefined; } private requestId(request: TextUtils.ContentProvider.ContentProvider): number { const requestId = this.requestIds.get(request); if (requestId === undefined) { const newId = ++this.lastRequestId; this.requestIds.set(request, newId); this.requests.set(newId, request); return newId; } return requestId; } private requestById(id: number): TextUtils.ContentProvider.ContentProvider|undefined { return this.requests.get(id); } private onForwardKeyboardEvent(message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage): Record |undefined { if (message.command !== Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent) { return this.status.E_BADARG( 'command', `expected ${Extensions.ExtensionAPI.PrivateAPI.Commands.ForwardKeyboardEvent}`); } message.entries.forEach(handleEventEntry); function handleEventEntry(entry: KeyboardEventInit&{eventType: string}): void { // Fool around closure compiler -- it has its own notion of both KeyboardEvent constructor // and initKeyboardEvent methods and overriding these in externs.js does not have effect. const event = new window.KeyboardEvent(entry.eventType, { key: entry.key, code: entry.code, keyCode: entry.keyCode, location: entry.location, ctrlKey: entry.ctrlKey, altKey: entry.altKey, shiftKey: entry.shiftKey, metaKey: entry.metaKey, }); // @ts-expect-error event.__keyCode = keyCodeForEntry(entry); document.dispatchEvent(event); } function keyCodeForEntry(entry: KeyboardEventInit): unknown { let keyCode = entry.keyCode; if (!keyCode) { // This is required only for synthetic events (e.g. dispatched in tests). if (entry.key === Platform.KeyboardUtilities.ESCAPE_KEY) { keyCode = 27; } } return keyCode || 0; } return undefined; } private dispatchCallback(requestId: unknown, port: MessagePort, result: unknown): void { if (requestId) { port.postMessage({command: 'callback', requestId, result}); } } private initExtensions(): void { this.registerAutosubscriptionHandler( Extensions.ExtensionAPI.PrivateAPI.Events.ResourceAdded, Workspace.Workspace.WorkspaceImpl.instance(), Workspace.Workspace.Events.UISourceCodeAdded, this.notifyResourceAdded); this.registerAutosubscriptionTargetManagerHandler( Extensions.ExtensionAPI.PrivateAPI.Events.NetworkRequestFinished, SDK.NetworkManager.NetworkManager, SDK.NetworkManager.Events.RequestFinished, this.notifyRequestFinished); function onElementsSubscriptionStarted(this: ExtensionServer): void { UI.Context.Context.instance().addFlavorChangeListener( SDK.DOMModel.DOMNode, this.notifyElementsSelectionChanged, this); } function onElementsSubscriptionStopped(this: ExtensionServer): void { UI.Context.Context.instance().removeFlavorChangeListener( SDK.DOMModel.DOMNode, this.notifyElementsSelectionChanged, this); } this.registerSubscriptionHandler( Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'elements', onElementsSubscriptionStarted.bind(this), onElementsSubscriptionStopped.bind(this)); this.registerResourceContentCommittedHandler(this.notifyUISourceCodeContentCommitted); SDK.TargetManager.TargetManager.instance().addEventListener( SDK.TargetManager.Events.INSPECTED_URL_CHANGED, this.inspectedURLChanged, this); } private notifyResourceAdded(event: Common.EventTarget.EventTargetEvent): void { const uiSourceCode = event.data; this.postNotification( Extensions.ExtensionAPI.PrivateAPI.Events.ResourceAdded, [this.makeResource(uiSourceCode)], extension => extension.isAllowedOnTarget(uiSourceCode.url())); } private notifyUISourceCodeContentCommitted( event: Common.EventTarget.EventTargetEvent): void { const {uiSourceCode, content} = event.data; this.postNotification( Extensions.ExtensionAPI.PrivateAPI.Events.ResourceContentCommitted, [this.makeResource(uiSourceCode), content], extension => extension.isAllowedOnTarget(uiSourceCode.url())); } private async notifyRequestFinished(event: Common.EventTarget.EventTargetEvent): Promise { const request = event.data; const entry = await HAR.Log.Entry.build(request, {sanitize: false}); this.postNotification( Extensions.ExtensionAPI.PrivateAPI.Events.NetworkRequestFinished, [this.requestId(request), entry], extension => extension.isAllowedOnTarget(entry.request.url)); } private notifyElementsSelectionChanged(): void { this.postNotification(Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'elements', []); } sourceSelectionChanged(url: Platform.DevToolsPath.UrlString, range: TextUtils.TextRange.TextRange): void { this.postNotification( Extensions.ExtensionAPI.PrivateAPI.Events.PanelObjectSelected + 'sources', [{ startLine: range.startLine, startColumn: range.startColumn, endLine: range.endLine, endColumn: range.endColumn, url, }], extension => extension.isAllowedOnTarget(url)); } private setInspectedTabId(event: Common.EventTarget.EventTargetEvent): void { const oldId = this.inspectedTabId; this.inspectedTabId = event.data; if (oldId === null) { // Run deferred init this.initializeExtensions(); } } addExtensionFrame({startPage, name}: Host.InspectorFrontendHostAPI.ExtensionDescriptor): void { const iframe = document.createElement('iframe'); iframe.src = startPage; iframe.dataset.devtoolsExtension = name; iframe.style.display = 'none'; document.body.appendChild(iframe); // Only for main window. } addExtension(extensionInfo: Host.InspectorFrontendHostAPI.ExtensionDescriptor): boolean|undefined { const startPage = extensionInfo.startPage; const inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL() ?? ''; if (inspectedURL === '') { this.#pendingExtensions.push(extensionInfo); return; } if (!ExtensionServer.canInspectURL(inspectedURL)) { this.disableExtensions(); } if (!this.extensionsEnabled) { this.#pendingExtensions.push(extensionInfo); return; } const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy); if (!hostsPolicy) { return; } try { const startPageURL = new URL((startPage)); const extensionOrigin = startPageURL.origin; const name = extensionInfo.name || `Extension ${extensionOrigin}`; const extensionRegistration = new RegisteredExtension(name, hostsPolicy, Boolean(extensionInfo.allowFileAccess)); if (!extensionRegistration.isAllowedOnTarget(inspectedURL)) { this.#pendingExtensions.push(extensionInfo); return; } if (!this.registeredExtensions.get(extensionOrigin)) { // See ExtensionAPI.js for details. const injectedAPI = self.buildExtensionAPIInjectedScript( extensionInfo, this.inspectedTabId as string, ThemeSupport.ThemeSupport.instance().themeName(), UI.ShortcutRegistry.ShortcutRegistry.instance().globalShortcutKeys(), ExtensionServer.instance().extensionAPITestHook); Host.InspectorFrontendHost.InspectorFrontendHostInstance.setInjectedScriptForOrigin( extensionOrigin, injectedAPI); this.registeredExtensions.set(extensionOrigin, extensionRegistration); } this.addExtensionFrame(extensionInfo); } catch (e) { console.error('Failed to initialize extension ' + startPage + ':' + e); return false; } return true; } private registerExtension(origin: Platform.DevToolsPath.UrlString, port: MessagePort): void { if (!this.registeredExtensions.has(origin)) { if (origin !== window.location.origin) { // Just ignore inspector frames. console.error('Ignoring unauthorized client request from ' + origin); } return; } extensionOrigins.set(port, origin); port.addEventListener('message', this.onmessage.bind(this), false); port.start(); } private onWindowMessage = (event: MessageEvent): void => { if (event.data === 'registerExtension') { this.registerExtension(event.origin as Platform.DevToolsPath.UrlString, event.ports[0]); } }; private extensionEnabled(port: MessagePort): boolean { if (!this.extensionsEnabled) { return false; } const origin = extensionOrigins.get(port); if (!origin) { return false; } const extension = this.registeredExtensions.get(origin); if (!extension) { return false; } return extension.isAllowedOnTarget(); } private async onmessage(event: MessageEvent): Promise { const message = event.data; let result; const port = event.currentTarget as MessagePort; const handler = this.handlers.get(message.command); if (!handler) { result = this.status.E_NOTSUPPORTED(message.command); } else if (!this.extensionEnabled(port)) { result = this.status.E_FAILED('Permission denied'); } else { try { result = await handler(message, event.target as MessagePort); } catch (e) { result = this.status.E_FAILED(e.message); } } if (result && message.requestId) { this.dispatchCallback(message.requestId, event.target as MessagePort, result); } } private registerHandler( command: string, callback: (message: Extensions.ExtensionAPI.PrivateAPI.ExtensionServerRequestMessage, port: MessagePort) => unknown): void { console.assert(Boolean(command)); this.handlers.set(command, callback); } private registerSubscriptionHandler( eventTopic: string, onSubscribeFirst: () => unknown, onUnsubscribeLast: () => unknown): void { this.subscriptionStartHandlers.set(eventTopic, onSubscribeFirst); this.subscriptionStopHandlers.set(eventTopic, onUnsubscribeLast); } private registerAutosubscriptionHandler( eventTopic: string, eventTarget: Common.EventTarget.EventTarget, frontendEventType: T, handler: Common.EventTarget.EventListener): void { this.registerSubscriptionHandler( eventTopic, () => eventTarget.addEventListener(frontendEventType, handler, this), () => eventTarget.removeEventListener(frontendEventType, handler, this)); } private registerAutosubscriptionTargetManagerHandler( eventTopic: string, modelClass: new(arg1: SDK.Target.Target) => SDK.SDKModel.SDKModel, frontendEventType: T, handler: Common.EventTarget.EventListener): void { this.registerSubscriptionHandler( eventTopic, () => SDK.TargetManager.TargetManager.instance().addModelListener(modelClass, frontendEventType, handler, this), () => SDK.TargetManager.TargetManager.instance().removeModelListener( modelClass, frontendEventType, handler, this)); } private registerResourceContentCommittedHandler( handler: (arg0: Common.EventTarget.EventTargetEvent) => unknown): void { function addFirstEventListener(this: ExtensionServer): void { Workspace.Workspace.WorkspaceImpl.instance().addEventListener( Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this); Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(true); } function removeLastEventListener(this: ExtensionServer): void { Workspace.Workspace.WorkspaceImpl.instance().setHasResourceContentTrackingExtensions(false); Workspace.Workspace.WorkspaceImpl.instance().removeEventListener( Workspace.Workspace.Events.WorkingCopyCommittedByUser, handler, this); } this.registerSubscriptionHandler( Extensions.ExtensionAPI.PrivateAPI.Events.ResourceContentCommitted, addFirstEventListener.bind(this), removeLastEventListener.bind(this)); } static expandResourcePath(extensionOrigin: Platform.DevToolsPath.UrlString, resourcePath: string): Platform.DevToolsPath.UrlString|undefined { const strippedOrigin = new URL(extensionOrigin).origin; const resourceURL = new URL(Common.ParsedURL.normalizePath(resourcePath), strippedOrigin); if (resourceURL.origin !== strippedOrigin) { return undefined; } return resourceURL.href as Platform.DevToolsPath.UrlString; } evaluate( expression: string, exposeCommandLineAPI: boolean, returnByValue: boolean, options: Extensions.ExtensionAPI.PrivateAPI.EvaluateOptions|undefined, securityOrigin: string, callback: (arg0: string|null, arg1: SDK.RemoteObject.RemoteObject|null, arg2: boolean) => unknown): Record |undefined { let context; function resolveURLToFrame(url: Platform.DevToolsPath.UrlString): SDK.ResourceTreeModel.ResourceTreeFrame|null { let found = null; function hasMatchingURL(frame: SDK.ResourceTreeModel.ResourceTreeFrame): SDK.ResourceTreeModel.ResourceTreeFrame| null { found = (frame.url === url) ? frame : null; return found; } SDK.ResourceTreeModel.ResourceTreeModel.frames().some(hasMatchingURL); return found; } options = options || {}; let frame; if (options.frameURL) { frame = resolveURLToFrame(options.frameURL as Platform.DevToolsPath.UrlString); } else { const target = SDK.TargetManager.TargetManager.instance().primaryPageTarget(); const resourceTreeModel = target?.model(SDK.ResourceTreeModel.ResourceTreeModel); frame = resourceTreeModel?.mainFrame; } if (!frame) { if (options.frameURL) { console.warn('evaluate: there is no frame with URL ' + options.frameURL); } else { console.warn('evaluate: the main frame is not yet available'); } return this.status.E_NOTFOUND(options.frameURL || ''); } // We shouldn't get here if the outermost frame can't be inspected by an extension, but // let's double check for subframes. const extension = this.registeredExtensions.get(securityOrigin); if (!extension?.isAllowedOnTarget(frame.url)) { return this.status.E_FAILED('Permission denied'); } let contextSecurityOrigin; if (options.useContentScriptContext) { contextSecurityOrigin = securityOrigin; } else if (options.scriptExecutionContext) { contextSecurityOrigin = options.scriptExecutionContext; } const runtimeModel = frame.resourceTreeModel().target().model(SDK.RuntimeModel.RuntimeModel); const executionContexts = runtimeModel ? runtimeModel.executionContexts() : []; if (contextSecurityOrigin) { for (let i = 0; i < executionContexts.length; ++i) { const executionContext = executionContexts[i]; if (executionContext.frameId === frame.id && executionContext.origin === contextSecurityOrigin && !executionContext.isDefault) { context = executionContext; } } if (!context) { console.warn('The JavaScript context ' + contextSecurityOrigin + ' was not found in the frame ' + frame.url); return this.status.E_NOTFOUND(contextSecurityOrigin); } } else { for (let i = 0; i < executionContexts.length; ++i) { const executionContext = executionContexts[i]; if (executionContext.frameId === frame.id && executionContext.isDefault) { context = executionContext; } } if (!context) { return this.status.E_FAILED(frame.url + ' has no execution context'); } } if (!extension?.isAllowedOnTarget(context.origin)) { return this.status.E_FAILED('Permission denied'); } void context .evaluate( { expression, objectGroup: 'extension', includeCommandLineAPI: exposeCommandLineAPI, silent: true, returnByValue, generatePreview: false, }, /* userGesture */ false, /* awaitPromise */ false) .then(onEvaluate); function onEvaluate(result: SDK.RuntimeModel.EvaluationResult): void { if ('error' in result) { callback(result.error, null, false); return; } callback(null, result.object || null, Boolean(result.exceptionDetails)); } return undefined; } static canInspectURL(url: Platform.DevToolsPath.UrlString): boolean { let parsedURL; // This is only to work around invalid URLs we're occasionally getting from some tests. // TODO(caseq): make sure tests supply valid URLs or we specifically handle invalid ones. try { parsedURL = new URL(url); } catch { return false; } if (!kPermittedSchemes.includes(parsedURL.protocol)) { return false; } if ((window.DevToolsAPI?.getOriginsForbiddenForExtensions?.() || []).includes(parsedURL.origin)) { return false; } if (this.#isUrlFromChromeWebStore(parsedURL)) { return false; } return true; } /** * Tests whether a given URL is from the Chrome web store to prevent the extension server from * being injected. This is treated as separate from the `getOriginsForbiddenForExtensions` API because * DevTools might not be being run from a native origin and we still want to lock down this specific * origin from DevTools extensions. * * @param parsedURL The URL to check * @returns `true` if the URL corresponds to the Chrome web store; otherwise `false` */ static #isUrlFromChromeWebStore(parsedURL: URL): boolean { if (parsedURL.protocol.startsWith('http') && parsedURL.hostname.match(/^chrome\.google\.com\.?$/) && parsedURL.pathname.startsWith('/webstore')) { return true; } if (parsedURL.protocol.startsWith('http') && parsedURL.hostname.match(/^chromewebstore\.google\.com\.?$/)) { return true; } return false; } private disableExtensions(): void { this.extensionsEnabled = false; } private enableExtensions(): void { this.extensionsEnabled = true; } } export const enum Events { SidebarPaneAdded = 'SidebarPaneAdded', } export interface EventTypes { [Events.SidebarPaneAdded]: ExtensionSidebarPane; } class ExtensionServerPanelView extends UI.View.SimpleView { private readonly name: string; private readonly panel: UI.Panel.Panel; constructor(name: string, title: Platform.UIString.LocalizedString, panel: UI.Panel.Panel) { // The `viewId` here is to satisfy the `SimpleView` constructor needs, but isn't actually // used anywhere, since we override the `viewId()` method below. Ideally we'd pass the // `name` as `viewId` to the constructor, but that doesn't work, since the `name` is not // necessarily in Kebab case. // // For non-ASCII titles (e.g., Chinese, Japanese, Arabic), the kebab-cased result may not // pass isExtendedKebabCase validation, so we fall back to 'extension-panel'. const kebabTitle = Platform.StringUtilities.toKebabCase(title); const viewId = Platform.StringUtilities.isExtendedKebabCase(kebabTitle) ? kebabTitle : 'extension-panel' as Lowercase; super({title, viewId}); this.name = name; this.panel = panel; } override viewId(): string { return this.name; } override widget(): Promise { return Promise.resolve(this.panel) as Promise; } } export class ExtensionStatus { OK: (...args: unknown[]) => Record; E_EXISTS: (...args: unknown[]) => Record; E_BADARG: (...args: unknown[]) => Record; E_BADARGTYPE: (...args: unknown[]) => Record; E_NOTFOUND: (...args: unknown[]) => Record; E_NOTSUPPORTED: (...args: unknown[]) => Record; E_PROTOCOLERROR: (...args: unknown[]) => Record; E_FAILED: (...args: unknown[]) => Record; constructor() { function makeStatus(code: string, description: string, ...details: unknown[]): Record { const status: Record = {code, description, details}; if (code !== 'OK') { status.isError = true; console.error('Extension server error: ' + Platform.StringUtilities.sprintf(description, ...details)); } return status; } this.OK = makeStatus.bind(null, 'OK', 'OK'); this.E_EXISTS = makeStatus.bind(null, 'E_EXISTS', 'Object already exists: %s'); this.E_BADARG = makeStatus.bind(null, 'E_BADARG', 'Invalid argument %s: %s'); this.E_BADARGTYPE = makeStatus.bind(null, 'E_BADARGTYPE', 'Invalid type for argument %s: got %s, expected %s'); this.E_NOTFOUND = makeStatus.bind(null, 'E_NOTFOUND', 'Object not found: %s'); this.E_NOTSUPPORTED = makeStatus.bind(null, 'E_NOTSUPPORTED', 'Object does not support requested operation: %s'); this.E_PROTOCOLERROR = makeStatus.bind(null, 'E_PROTOCOLERROR', 'Inspector protocol error: %s'); this.E_FAILED = makeStatus.bind(null, 'E_FAILED', 'Operation failed: %s'); } } export interface Record { code: string; description: string; details: unknown[]; isError?: boolean; }