Skip to content

Commit

Permalink
refactor[devtools/extension]: refactored messaging logic across diffe…
Browse files Browse the repository at this point in the history
…rent parts of the extension (#27417)

1.
bvaughn@9fc04ea#diff-2c5e1f5e80e74154e65b2813cf1c3638f85034530e99dae24809ab4ad70d0143
introduced a vulnerability: we listen to `'fetch-file-with-cache'` event
from `window` to fetch sources of the file, in which we want to parse
hook names. We send this event via `window`, which means any page can
also use this and manipulate the extension to perform some `fetch()`
calls. With these changes, instead of transporting message via `window`,
we have a distinct content script, which is responsible for fetching
sources. It is notified via `chrome.runtime.sendMessage` api, so it
can't be manipulated.
2. Consistent structure of messages `{source: string, payload: object}`
in different parts of the extension
3. Added some wrappers around `chrome.scripting.executeScript` API in
`packages/react-devtools-extensions/src/background/executeScript.js`,
which support custom flow for Firefox, to simulate support of
`ExecutionWorld.MAIN`.
  • Loading branch information
hoxyq authored Sep 25, 2023
1 parent 69728fd commit 09285d5
Show file tree
Hide file tree
Showing 14 changed files with 406 additions and 291 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ const contentScriptsToInject = IS_FIREFOX
persistAcrossSessions: true,
runAt: 'document_end',
},
{
id: '@react-devtools/file-fetcher',
js: ['build/fileFetcher.js'],
matches: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_end',
},
]
: [
{
Expand All @@ -23,6 +30,14 @@ const contentScriptsToInject = IS_FIREFOX
runAt: 'document_end',
world: chrome.scripting.ExecutionWorld.ISOLATED,
},
{
id: '@react-devtools/file-fetcher',
js: ['build/fileFetcher.js'],
matches: ['<all_urls>'],
persistAcrossSessions: true,
runAt: 'document_end',
world: chrome.scripting.ExecutionWorld.ISOLATED,
},
{
id: '@react-devtools/hook',
js: ['build/installHook.js'],
Expand Down
58 changes: 58 additions & 0 deletions packages/react-devtools-extensions/src/background/executeScript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/* global chrome */

import {IS_FIREFOX} from '../utils';

// Firefox doesn't support ExecutionWorld.MAIN yet
// https://bugzilla.mozilla.org/show_bug.cgi?id=1736575
function executeScriptForFirefoxInMainWorld({target, files}) {
return chrome.scripting.executeScript({
target,
func: fileNames => {
function injectScriptSync(src) {
let code = '';
const request = new XMLHttpRequest();
request.addEventListener('load', function () {
code = this.responseText;
});
request.open('GET', src, false);
request.send();

const script = document.createElement('script');
script.textContent = code;

// This script runs before the <head> element is created,
// so we add the script to <html> instead.
if (document.documentElement) {
document.documentElement.appendChild(script);
}

if (script.parentNode) {
script.parentNode.removeChild(script);
}
}

fileNames.forEach(file => injectScriptSync(chrome.runtime.getURL(file)));
},
args: [files],
});
}

export function executeScriptInIsolatedWorld({target, files}) {
return chrome.scripting.executeScript({
target,
files,
world: chrome.scripting.ExecutionWorld.ISOLATED,
});
}

export function executeScriptInMainWorld({target, files}) {
if (IS_FIREFOX) {
return executeScriptForFirefoxInMainWorld({target, files});
}

return chrome.scripting.executeScript({
target,
files,
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
84 changes: 21 additions & 63 deletions packages/react-devtools-extensions/src/background/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

'use strict';

import {IS_FIREFOX, EXTENSION_CONTAINED_VERSIONS} from '../utils';

import './dynamicallyInjectContentScripts';
import './tabsManager';
import setExtensionIconAndPopup from './setExtensionIconAndPopup';

import {
handleDevToolsPageMessage,
handleBackendManagerMessage,
handleReactDevToolsHookMessage,
handleFetchResourceContentScriptMessage,
} from './messageHandlers';

/*
{
Expand Down Expand Up @@ -173,67 +177,21 @@ function connectExtensionAndProxyPorts(extensionPort, proxyPort, tabId) {
}

chrome.runtime.onMessage.addListener((message, sender) => {
const tab = sender.tab;
// sender.tab.id from content script points to the tab that injected the content script
if (tab) {
const id = tab.id;
// This is sent from the hook content script.
// It tells us a renderer has attached.
if (message.hasDetectedReact) {
setExtensionIconAndPopup(message.reactBuildType, id);
} else {
const extensionPort = ports[id]?.extension;

switch (message.payload?.type) {
case 'fetch-file-with-cache-complete':
case 'fetch-file-with-cache-error':
// Forward the result of fetch-in-page requests back to the extension.
extensionPort?.postMessage(message);
break;
// This is sent from the backend manager running on a page
case 'react-devtools-required-backends':
const backendsToDownload = [];
message.payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
if (!IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId: id},
files: [`/build/react_devtools_backend_${version}.js`],
world: chrome.scripting.ExecutionWorld.MAIN,
});
}
} else {
backendsToDownload.push(version);
}
});

// Request the necessary backends in the extension DevTools UI
// TODO: handle this message in index.js to build the UI
extensionPort?.postMessage({
payload: {
type: 'react-devtools-additional-backends',
versions: backendsToDownload,
},
});
break;
}
switch (message?.source) {
case 'devtools-page': {
handleDevToolsPageMessage(message);
break;
}
}

// This is sent from the devtools page when it is ready for injecting the backend
if (message?.payload?.type === 'react-devtools-inject-backend-manager') {
// sender.tab.id from devtools page may not exist, or point to the undocked devtools window
// so we use the payload to get the tab id
const tabId = message.payload.tabId;

if (tabId && !IS_FIREFOX) {
// equivalent logic for Firefox is in prepareInjection.js
chrome.scripting.executeScript({
target: {tabId},
files: ['/build/backendManager.js'],
world: chrome.scripting.ExecutionWorld.MAIN,
});
case 'react-devtools-fetch-resource-content-script': {
handleFetchResourceContentScriptMessage(message);
break;
}
case 'react-devtools-backend-manager': {
handleBackendManagerMessage(message, sender);
break;
}
case 'react-devtools-hook': {
handleReactDevToolsHookMessage(message, sender);
}
}
});
Expand Down
12 changes: 0 additions & 12 deletions packages/react-devtools-extensions/src/background/injectProxy.js

This file was deleted.

103 changes: 103 additions & 0 deletions packages/react-devtools-extensions/src/background/messageHandlers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* global chrome */

import setExtensionIconAndPopup from './setExtensionIconAndPopup';
import {executeScriptInMainWorld} from './executeScript';

import {EXTENSION_CONTAINED_VERSIONS} from '../utils';

export function handleReactDevToolsHookMessage(message, sender) {
const {payload} = message;

switch (payload?.type) {
case 'react-renderer-attached': {
setExtensionIconAndPopup(payload.reactBuildType, sender.tab.id);

break;
}
}
}

export function handleBackendManagerMessage(message, sender) {
const {payload} = message;

switch (payload?.type) {
case 'require-backends': {
payload.versions.forEach(version => {
if (EXTENSION_CONTAINED_VERSIONS.includes(version)) {
executeScriptInMainWorld({
target: {tabId: sender.tab.id},
files: [`/build/react_devtools_backend_${version}.js`],
});
}
});

break;
}
}
}

export function handleDevToolsPageMessage(message) {
const {payload} = message;

switch (payload?.type) {
// Proxy this message from DevTools page to content script via chrome.tabs.sendMessage
case 'fetch-file-with-cache': {
const {
payload: {tabId, url},
} = message;

if (!tabId) {
throw new Error("Couldn't fetch file sources: tabId not specified");
}

if (!url) {
throw new Error("Couldn't fetch file sources: url not specified");
}

chrome.tabs.sendMessage(tabId, {
source: 'devtools-page',
payload: {
type: 'fetch-file-with-cache',
url,
},
});

break;
}

case 'inject-backend-manager': {
const {
payload: {tabId},
} = message;

if (!tabId) {
throw new Error("Couldn't inject backend manager: tabId not specified");
}

executeScriptInMainWorld({
target: {tabId},
files: ['/build/backendManager.js'],
});

break;
}
}
}

export function handleFetchResourceContentScriptMessage(message) {
const {payload} = message;

switch (payload?.type) {
case 'fetch-file-with-cache-complete':
case 'fetch-file-with-cache-error':
// Forward the result of fetch-in-page requests back to the DevTools page.
// We switch the source here because of inconsistency between Firefox and Chrome
// In Chromium this message will be propagated from content script to DevTools page
// For Firefox, only background script will get this message, so we need to forward it to DevTools page
chrome.runtime.sendMessage({
source: 'react-devtools-background',
payload,
});
break;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ function updateRequiredBackends() {
{
source: 'react-devtools-backend-manager',
payload: {
type: 'react-devtools-required-backends',
type: 'require-backends',
versions: Array.from(requiredBackends),
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/* global chrome */

function fetchResource(url) {
const reject = value => {
chrome.runtime.sendMessage({
source: 'react-devtools-fetch-resource-content-script',
payload: {
type: 'fetch-file-with-cache-error',
url,
value,
},
});
};

const resolve = value => {
chrome.runtime.sendMessage({
source: 'react-devtools-fetch-resource-content-script',
payload: {
type: 'fetch-file-with-cache-complete',
url,
value,
},
});
};

fetch(url, {cache: 'force-cache'}).then(
response => {
if (response.ok) {
response
.text()
.then(text => resolve(text))
.catch(error => reject(null));
} else {
reject(null);
}
},
error => reject(null),
);
}

chrome.runtime.onMessage.addListener(message => {
if (
message?.source === 'devtools-page' &&
message?.payload?.type === 'fetch-file-with-cache'
) {
fetchResource(message.payload.url);
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ if (!window.hasOwnProperty('__REACT_DEVTOOLS_GLOBAL_HOOK__')) {
function ({reactBuildType}) {
window.postMessage(
{
source: 'react-devtools-detector',
reactBuildType,
source: 'react-devtools-hook',
payload: {
type: 'react-renderer-attached',
reactBuildType,
},
},
'*',
);
Expand Down
Loading

0 comments on commit 09285d5

Please sign in to comment.