mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-30 09:54:02 +08:00
Apply runtime_blocked_hosts and runtime_allowed_hosts for extensions
Bug: 1429353 Change-Id: I2605e77f74f08e0e3619b88440641838adf34679 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/4637291 Reviewed-by: Danil Somsikov <dsv@chromium.org> Commit-Queue: Philip Pfaffe <pfaffe@chromium.org>
This commit is contained in:
committed by
Devtools-frontend LUCI CQ
parent
5406f386c0
commit
2d19bcb52a
@@ -324,6 +324,11 @@ export interface ExtensionDescriptor {
|
||||
startPage: string;
|
||||
name: string;
|
||||
exposeExperimentalAPIs: boolean;
|
||||
hostsPolicy?: ExtensionHostsPolicy;
|
||||
}
|
||||
export interface ExtensionHostsPolicy {
|
||||
runtimeAllowedHosts: string[];
|
||||
runtimeBlockedHosts: string[];
|
||||
}
|
||||
export interface ShowSurveyResult {
|
||||
surveyShown: boolean;
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
|
||||
// DevToolsAPI ----------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* @typedef {{runtimeAllowedHosts: !Array<string>, runtimeBlockedHosts: !Array<string>}} ExtensionHostsPolicy
|
||||
*/
|
||||
/**
|
||||
* @typedef {{startPage: string, name: string, exposeExperimentalAPIs: boolean, hostsPolicy?: ExtensionHostsPolicy}} ExtensionDescriptor
|
||||
*/
|
||||
const DevToolsAPIImpl = class {
|
||||
constructor() {
|
||||
/**
|
||||
@@ -24,7 +30,7 @@ const DevToolsAPIImpl = class {
|
||||
this._pendingExtensionDescriptors = [];
|
||||
|
||||
/**
|
||||
* @type {?function(!ExtensionDescriptor)}
|
||||
* @type {?function(!ExtensionDescriptor): void}
|
||||
*/
|
||||
this._addExtensionCallback = null;
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
|
||||
/* eslint-disable @typescript-eslint/naming-convention */
|
||||
|
||||
import {type Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
|
||||
import * as Common from '../../core/common/common.js';
|
||||
import * as Host from '../../core/host/host.js';
|
||||
import * as i18n from '../../core/i18n/i18n.js';
|
||||
@@ -38,6 +39,7 @@ 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 Root from '../../core/root/root.js';
|
||||
import * as SDK from '../../core/sdk/sdk.js';
|
||||
import type * as Protocol from '../../generated/protocol.js';
|
||||
import * as Logs from '../../models/logs/logs.js';
|
||||
import * as Components from '../../ui/legacy/components/utils/utils.js';
|
||||
import * as UI from '../../ui/legacy/legacy.js';
|
||||
@@ -46,15 +48,13 @@ import * as Bindings from '../bindings/bindings.js';
|
||||
import * as HAR from '../har/har.js';
|
||||
import type * as TextUtils from '../text_utils/text_utils.js';
|
||||
import * as Workspace from '../workspace/workspace.js';
|
||||
import type * as Protocol from '../../generated/protocol.js';
|
||||
|
||||
import {PrivateAPI} from './ExtensionAPI.js';
|
||||
import {ExtensionButton, ExtensionPanel, ExtensionSidebarPane} from './ExtensionPanel.js';
|
||||
|
||||
import {HostUrlPattern} from './HostUrlPattern.js';
|
||||
import {LanguageExtensionEndpoint} from './LanguageExtensionEndpoint.js';
|
||||
import {RecorderExtensionEndpoint} from './RecorderExtensionEndpoint.js';
|
||||
import {PrivateAPI} from './ExtensionAPI.js';
|
||||
import {RecorderPluginManager} from './RecorderPluginManager.js';
|
||||
import {type Chrome} from '../../../extension-api/ExtensionAPI.js'; // eslint-disable-line rulesdir/es_modules_import
|
||||
|
||||
const extensionOrigins: WeakMap<MessagePort, Platform.DevToolsPath.UrlString> = new WeakMap();
|
||||
|
||||
@@ -68,6 +68,32 @@ const kAllowedOrigins = [].map(url => (new URL(url)).origin);
|
||||
|
||||
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 = HostUrlPattern.parse(pattern);
|
||||
if (!parsedPattern) {
|
||||
return null;
|
||||
}
|
||||
runtimeAllowedHosts.push(parsedPattern);
|
||||
}
|
||||
for (const pattern of policy.runtimeBlockedHosts) {
|
||||
const parsedPattern = HostUrlPattern.parse(pattern);
|
||||
if (!parsedPattern) {
|
||||
return null;
|
||||
}
|
||||
runtimeBlockedHosts.push(parsedPattern);
|
||||
}
|
||||
}
|
||||
return new HostsPolicy(runtimeAllowedHosts, runtimeBlockedHosts);
|
||||
}
|
||||
private constructor(readonly runtimeAllowedHosts: HostUrlPattern[], readonly runtimeBlockedHosts: HostUrlPattern[]) {
|
||||
}
|
||||
}
|
||||
|
||||
export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTypes> {
|
||||
private readonly clientObjects: Map<string, unknown>;
|
||||
private readonly handlers:
|
||||
@@ -81,6 +107,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
private lastRequestId: number;
|
||||
private registeredExtensions: Map<string, {
|
||||
name: string,
|
||||
hostsPolicy: HostsPolicy,
|
||||
}>;
|
||||
private status: ExtensionStatus;
|
||||
private readonly sidebarPanesInternal: ExtensionSidebarPane[];
|
||||
@@ -363,7 +390,9 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
}
|
||||
const message = {command: 'notify-' + type, arguments: Array.prototype.slice.call(arguments, 1)};
|
||||
for (const subscriber of subscribers) {
|
||||
subscriber.postMessage(message);
|
||||
if (this.extensionEnabled(subscriber)) {
|
||||
subscriber.postMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -953,7 +982,11 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
addExtensionForTest(extensionInfo: Host.InspectorFrontendHostAPI.ExtensionDescriptor, origin: string): boolean
|
||||
|undefined {
|
||||
const name = extensionInfo.name || `Extension ${origin}`;
|
||||
this.registeredExtensions.set(origin, {name});
|
||||
const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy);
|
||||
if (!hostsPolicy) {
|
||||
return false;
|
||||
}
|
||||
this.registeredExtensions.set(origin, {name, hostsPolicy});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -967,6 +1000,10 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
if (!this.extensionsEnabled) {
|
||||
return;
|
||||
}
|
||||
const hostsPolicy = HostsPolicy.create(extensionInfo.hostsPolicy);
|
||||
if (!hostsPolicy) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const startPageURL = new URL((startPage as string));
|
||||
const extensionOrigin = startPageURL.origin;
|
||||
@@ -979,7 +1016,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
Host.InspectorFrontendHost.InspectorFrontendHostInstance.setInjectedScriptForOrigin(
|
||||
extensionOrigin, injectedAPI);
|
||||
const name = extensionInfo.name || `Extension ${extensionOrigin}`;
|
||||
this.registeredExtensions.set(extensionOrigin, {name});
|
||||
this.registeredExtensions.set(extensionOrigin, {name, hostsPolicy});
|
||||
}
|
||||
|
||||
const iframe = document.createElement('iframe');
|
||||
@@ -1012,15 +1049,42 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
}
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const inspectedURL = SDK.TargetManager.TargetManager.instance().primaryPageTarget()?.inspectedURL();
|
||||
if (!inspectedURL) {
|
||||
// If there aren't any blocked hosts retain the old behavior and don't worry about the inspectedURL
|
||||
return extension.hostsPolicy.runtimeBlockedHosts.length === 0;
|
||||
}
|
||||
if (extension.hostsPolicy.runtimeBlockedHosts.some(pattern => pattern.matchesUrl(inspectedURL)) &&
|
||||
!extension.hostsPolicy.runtimeAllowedHosts.some(pattern => pattern.matchesUrl(inspectedURL))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async onmessage(event: MessageEvent): Promise<void> {
|
||||
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.extensionsEnabled) {
|
||||
} else if (!this.extensionEnabled(port)) {
|
||||
result = this.status.E_FAILED('Permission denied');
|
||||
} else {
|
||||
result = await handler(message, event.target as MessagePort);
|
||||
@@ -1217,6 +1281,7 @@ export class ExtensionServer extends Common.ObjectWrapper.ObjectWrapper<EventTyp
|
||||
[]).includes(parsedURL.origin)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@ export async function initializeGlobalVars({reset = true} = {}) {
|
||||
Common.Settings.Settings.instance(
|
||||
{forceNew: reset, syncedStorage: storage, globalStorage: storage, localStorage: storage});
|
||||
|
||||
Root.Runtime.experiments.clearForTest();
|
||||
for (const experimentName of REGISTERED_EXPERIMENTS) {
|
||||
Root.Runtime.experiments.register(experimentName, '');
|
||||
}
|
||||
|
||||
@@ -2,17 +2,19 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import type * as Platform from '../../../../../front_end/core/platform/platform.js';
|
||||
import * as SDK from '../../../../../front_end/core/sdk/sdk.js';
|
||||
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
|
||||
import * as UI from '../../../../../front_end/ui/legacy/legacy.js';
|
||||
|
||||
import type * as Platform from '../../../../../front_end/core/platform/platform.js';
|
||||
|
||||
const {assert} = chai;
|
||||
|
||||
import {describeWithDummyExtension} from './helpers.js';
|
||||
import {describeWithDevtoolsExtension} from './helpers.js';
|
||||
import {type Chrome} from '../../../../../extension-api/ExtensionAPI.js';
|
||||
import {createTarget} from '../../helpers/EnvironmentHelpers.js';
|
||||
import {describeWithMockConnection} from '../../helpers/MockConnection.js';
|
||||
|
||||
describeWithDummyExtension('Extensions', context => {
|
||||
describeWithDevtoolsExtension('Extensions', {}, context => {
|
||||
it('can register a recorder extension for export', async () => {
|
||||
class RecorderPlugin {
|
||||
async stringify(recording: object) {
|
||||
@@ -197,6 +199,50 @@ describeWithDummyExtension('Extensions', context => {
|
||||
});
|
||||
});
|
||||
|
||||
const hostsPolicy = {
|
||||
runtimeAllowedHosts: ['http://example.com'],
|
||||
runtimeBlockedHosts: ['http://example.com', 'http://web.dev'],
|
||||
};
|
||||
|
||||
describeWithMockConnection('Extensions', () => {
|
||||
describeWithDevtoolsExtension('Runtime hosts policy', {hostsPolicy}, context => {
|
||||
it('blocks API calls on blocked hosts', async () => {
|
||||
const target = createTarget({type: SDK.Target.Type.Frame});
|
||||
|
||||
{
|
||||
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
|
||||
assert.strictEqual('isError' in result && result.isError, true);
|
||||
}
|
||||
|
||||
target.setInspectedURL('http://web.dev' as Platform.DevToolsPath.UrlString);
|
||||
{
|
||||
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
|
||||
assert.strictEqual('isError' in result && result.isError, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows API calls on allowlisted hosts', async () => {
|
||||
const target = createTarget({type: SDK.Target.Type.Frame});
|
||||
target.setInspectedURL('http://example.com' as Platform.DevToolsPath.UrlString);
|
||||
{
|
||||
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
|
||||
// eslint-disable-next-line rulesdir/compare_arrays_with_assert_deepequal
|
||||
assert.doesNotHaveAnyKeys(result, ['isError']);
|
||||
}
|
||||
});
|
||||
|
||||
it('allows API calls on non-blocked hosts', async () => {
|
||||
const target = createTarget({type: SDK.Target.Type.Frame});
|
||||
target.setInspectedURL('http://example.com2' as Platform.DevToolsPath.UrlString);
|
||||
{
|
||||
const result = await new Promise<object>(cb => context.chrome.devtools?.network.getHAR(cb));
|
||||
// eslint-disable-next-line rulesdir/compare_arrays_with_assert_deepequal
|
||||
assert.doesNotHaveAnyKeys(result, ['isError']);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('ExtensionServer', () => {
|
||||
it('can correctly expand resource paths', async () => {
|
||||
// Ideally this would be a chrome-extension://, but that doesn't work with URL in chrome headless.
|
||||
|
||||
@@ -2,15 +2,18 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
|
||||
import {type Chrome} from '../../../../../extension-api/ExtensionAPI.js';
|
||||
import type * as Host from '../../../../../front_end/core/host/host.js';
|
||||
import * as Extensions from '../../../../../front_end/models/extensions/extensions.js';
|
||||
import {describeWithEnvironment} from '../../helpers/EnvironmentHelpers.js';
|
||||
|
||||
interface ExtensionContext {
|
||||
chrome: Partial<Chrome.DevTools.Chrome>;
|
||||
}
|
||||
|
||||
export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
|
||||
export function describeWithDevtoolsExtension(
|
||||
title: string, extension: Partial<Host.InspectorFrontendHostAPI.ExtensionDescriptor>,
|
||||
fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
|
||||
const context: ExtensionContext = {
|
||||
chrome: {},
|
||||
};
|
||||
@@ -21,6 +24,7 @@ export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite
|
||||
startPage: 'blank.html',
|
||||
name: 'TestExtension',
|
||||
exposeExperimentalAPIs: true,
|
||||
...extension,
|
||||
};
|
||||
server.addExtensionForTest(extensionDescriptor, window.location.origin);
|
||||
const chrome: Partial<Chrome.DevTools.Chrome> = {};
|
||||
@@ -46,9 +50,11 @@ export function describeWithDummyExtension(title: string, fn: (this: Mocha.Suite
|
||||
});
|
||||
}
|
||||
|
||||
describeWithDummyExtension.only = function(title: string, fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
|
||||
describeWithDevtoolsExtension.only = function(
|
||||
title: string, extension: Partial<Host.InspectorFrontendHostAPI.ExtensionDescriptor>,
|
||||
fn: (this: Mocha.Suite, context: ExtensionContext) => void) {
|
||||
// eslint-disable-next-line rulesdir/no_only
|
||||
return describe.only('.only', function() {
|
||||
return describeWithDummyExtension(title, fn);
|
||||
return describeWithDevtoolsExtension(title, extension, fn);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user