Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add preloadInWorker API #28923

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions docs/api/browser-window.md
Original file line number Diff line number Diff line change
Expand Up @@ -268,6 +268,12 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
When node integration is turned off, the preload script can reintroduce
Node global symbols back to the global scope. See example
[here](context-bridge.md#exposing-node-global-symbols).
* `preloadInWorker` String (optional) - Specifies a script that will be loaded before other
scripts run in worker. This script will always have access to node APIs
no matter whether node integration in worker is turned on or off. The value should
be the absolute file path to the script.
When node integration is turned off, the preload script can reintroduce
Node global symbols back to the global scope.
* `sandbox` Boolean (optional) - If set, this will sandbox the renderer
associated with the window, making it compatible with the Chromium
OS-level sandbox and disabling the Node.js engine. This is not the same as
Expand Down
75 changes: 56 additions & 19 deletions lib/worker/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,61 @@ require('../common/reset-search-paths');
// Import common settings.
require('@electron/internal/common/init');

// Export node bindings to global.
const { makeRequireFunction } = __non_webpack_require__('internal/modules/cjs/helpers') // eslint-disable-line
global.module = new Module('electron/js2c/worker_init');
global.require = makeRequireFunction(global.module);

// Set the __filename to the path of html file if it is file: protocol.
if (self.location.protocol === 'file:') {
const pathname = process.platform === 'win32' && self.location.pathname[0] === '/' ? self.location.pathname.substr(1) : self.location.pathname;
global.__filename = path.normalize(decodeURIComponent(pathname));
global.__dirname = path.dirname(global.__filename);

// Set module's filename so relative require can work as expected.
global.module.filename = global.__filename;

// Also search for module under the html file.
global.module.paths = Module._nodeModulePaths(global.__dirname);
// Process command line arguments.
const { hasSwitch, getSwitchValue } = process._linkedBinding('electron_common_command_line');

const parseOption = function (name: string) {
return hasSwitch(name) ? getSwitchValue(name) : null;
};

const nodeIntegration = hasSwitch('node-integration-in-worker');
const preloadScript = parseOption('preload-in-worker');

if (nodeIntegration) {
// Export node bindings to global.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file shares lots of code with lib/renderer/init.ts, can you move the shared code into a utility function? Like what windowSetup and webViewInit do.

const { makeRequireFunction } = __non_webpack_require__('internal/modules/cjs/helpers') // eslint-disable-line
global.module = new Module('electron/js2c/worker_init');
global.require = makeRequireFunction(global.module);

// Set the __filename to the path of html file if it is file: protocol.
if (self.location.protocol === 'file:') {
const pathname = process.platform === 'win32' && self.location.pathname[0] === '/' ? self.location.pathname.substr(1) : self.location.pathname;
global.__filename = path.normalize(decodeURIComponent(pathname));
global.__dirname = path.dirname(global.__filename);

// Set module's filename so relative require can work as expected.
global.module.filename = global.__filename;

// Also search for module under the html file.
global.module.paths = Module._nodeModulePaths(global.__dirname);
} else {
// For backwards compatibility we fake these two paths here
global.__filename = path.join(process.resourcesPath, 'electron.asar', 'worker', 'init.js');
global.__dirname = path.join(process.resourcesPath, 'electron.asar', 'worker');
}
} else {
// For backwards compatibility we fake these two paths here
global.__filename = path.join(process.resourcesPath, 'electron.asar', 'worker', 'init.js');
global.__dirname = path.join(process.resourcesPath, 'electron.asar', 'worker');
// Delete Node's symbols after the Environment has been loaded.
process.once('loaded', function () {
delete (global as any).process;
delete (global as any).Buffer;
delete (global as any).setImmediate;
delete (global as any).clearImmediate;
delete (global as any).global;
delete (global as any).root;
delete (global as any).GLOBAL;
});
}

const preloadScripts = [];
if (preloadScript) {
preloadScripts.push(preloadScript);
}

for (const preloadScript of preloadScripts) {
try {
Module._load(preloadScript);
} catch (error) {
console.error(`Unable to load preload script: ${preloadScript}`);
console.error(error);
}
}
22 changes: 22 additions & 0 deletions shell/browser/web_contents_preferences.cc
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,21 @@ bool WebContentsPreferences::GetPreloadPath(base::FilePath* path) const {
return false;
}

bool WebContentsPreferences::GetPreloadInWorkerPath(
base::FilePath::StringType* path) const {
DCHECK(path);
base::FilePath::StringType preload;
if (GetAsString(&preference_, options::kPreloadScriptInWorker, &preload)) {
if (base::FilePath(preload).IsAbsolute()) {
*path = std::move(preload);
return true;
} else {
LOG(ERROR) << "preloadInWorker script must have absolute path.";
}
}
return false;
}

// static
content::WebContents* WebContentsPreferences::GetWebContentsFromProcessID(
int process_id) {
Expand Down Expand Up @@ -363,6 +378,13 @@ void WebContentsPreferences::AppendCommandLineSwitches(
if (IsEnabled(options::kNodeIntegrationInWorker))
command_line->AppendSwitch(switches::kNodeIntegrationInWorker);

// The preload in worker script.
base::FilePath::StringType preloadInWorker;
if (GetPreloadInWorkerPath(&preloadInWorker)) {
command_line->AppendSwitchNative(switches::kPreloadScriptInWorker,
preloadInWorker);
}

// We are appending args to a webContents so let's save the current state
// of our preferences object so that during the lifetime of the WebContents
// we can fetch the options used to initally configure the WebContents
Expand Down
3 changes: 3 additions & 0 deletions shell/browser/web_contents_preferences.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ class WebContentsPreferences
// Returns the preload script path.
bool GetPreloadPath(base::FilePath* path) const;

// Returns the preload script path to be used in worker.
bool GetPreloadInWorkerPath(base::FilePath::StringType* path) const;

// Returns the web preferences.
base::Value* preference() { return &preference_; }
base::Value* last_preference() { return &last_preference_; }
Expand Down
6 changes: 6 additions & 0 deletions shell/common/options_switches.cc
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ const char kPreloadScripts[] = "preloadScripts";
// Like --preload, but the passed argument is an URL.
const char kPreloadURL[] = "preloadURL";

// Script that will be loaded by guest WebWorker before other scripts.
const char kPreloadScriptInWorker[] = "preloadInWorker";

// Enable the node integration.
const char kNodeIntegration[] = "nodeIntegration";

Expand Down Expand Up @@ -245,6 +248,9 @@ const char kScrollBounce[] = "scroll-bounce";
// Command switch passed to renderer process to control nodeIntegration.
const char kNodeIntegrationInWorker[] = "node-integration-in-worker";

// Command switch passed to renderer process to define preload script.
const char kPreloadScriptInWorker[] = "preload-in-worker";

// Widevine options
// Path to Widevine CDM binaries.
const char kWidevineCdmPath[] = "widevine-cdm-path";
Expand Down
2 changes: 2 additions & 0 deletions shell/common/options_switches.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ extern const char kZoomFactor[];
extern const char kPreloadScript[];
extern const char kPreloadScripts[];
extern const char kPreloadURL[];
extern const char kPreloadScriptInWorker[];
extern const char kNodeIntegration[];
extern const char kContextIsolation[];
extern const char kGuestInstanceID[];
Expand Down Expand Up @@ -121,6 +122,7 @@ extern const char kEnableApiFilteringLogging[];

extern const char kScrollBounce[];
extern const char kNodeIntegrationInWorker[];
extern const char kPreloadScriptInWorker[];

extern const char kWidevineCdmPath[];
extern const char kWidevineCdmVersion[];
Expand Down
20 changes: 16 additions & 4 deletions shell/renderer/electron_renderer_client.cc
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@ bool IsDevToolsExtension(content::RenderFrame* render_frame) {
.SchemeIs("chrome-extension");
}

bool WorkerHasNodeIntegrationOrPreloadScript() {
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInWorker)) {
return true;
}

if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kPreloadScriptInWorker)) {
return true;
}

return false;
}

} // namespace

// static
Expand Down Expand Up @@ -209,8 +223,7 @@ void ElectronRendererClient::WorkerScriptReadyForEvaluationOnWorkerThread(
v8::Local<v8::Context> context) {
// TODO(loc): Note that this will not be correct for in-process child windows
// with webPreferences that have a different value for nodeIntegrationInWorker
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInWorker)) {
if (WorkerHasNodeIntegrationOrPreloadScript()) {
WebWorkerObserver::GetCurrent()->WorkerScriptReadyForEvaluation(context);
}
}
Expand All @@ -219,8 +232,7 @@ void ElectronRendererClient::WillDestroyWorkerContextOnWorkerThread(
v8::Local<v8::Context> context) {
// TODO(loc): Note that this will not be correct for in-process child windows
// with webPreferences that have a different value for nodeIntegrationInWorker
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInWorker)) {
if (WorkerHasNodeIntegrationOrPreloadScript()) {
WebWorkerObserver::GetCurrent()->ContextWillDestroy(context);
}
}
Expand Down
34 changes: 34 additions & 0 deletions spec-main/api-browser-window-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2735,6 +2735,40 @@ describe('BrowserWindow module', () => {
expect(w.getSize()).to.deep.equal(size);
});
});

describe('"preloadInWorker" option', () => {
const getResult = async (webPrefs: { nodeIntegrationInWorker: boolean}) => {
const w = new BrowserWindow({
webPreferences: {
...webPrefs,
contextIsolation: false,
nodeIntegration: true,
preloadInWorker: path.join(fixtures, 'module', 'preload-worker.js')
},
show: false
});
w.loadFile(path.join(fixtures, 'api', 'no-leak-worker.html'));
const [, result] = await emittedOnce(ipcMain, 'var');
w.destroy();
return result;
};

it('does not leak "require", "process" and "global" with disabled node integration', async () => {
const result = await getResult({ nodeIntegrationInWorker: false });
expect(result).to.have.property('require', 'undefined');
expect(result).to.have.property('process', 'undefined');
expect(result).to.have.property('global', 'undefined');
expect(result).to.have.property('foo', 'string');
});

it('does not hide "require", "process" and "global" with enabled node integration', async () => {
const result = await getResult({ nodeIntegrationInWorker: true });
expect(result).to.have.property('require', 'function');
expect(result).to.have.property('process', 'object');
expect(result).to.have.property('global', 'object');
expect(result).to.have.property('foo', 'string');
});
});
});

describe('nativeWindowOpen + contextIsolation options', () => {
Expand Down
16 changes: 16 additions & 0 deletions spec/fixtures/api/no-leak-worker.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<script type="text/javascript" charset="utf-8">
const {ipcRenderer} = require('electron')
let worker = new Worker(`../workers/worker_var.js`)
worker.onmessage = function (event) {
ipcRenderer.send('var', event.data)
worker.terminate()
}
</script>
</body>
</html>
1 change: 1 addition & 0 deletions spec/fixtures/module/preload-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
self.foo = 'bar';
6 changes: 6 additions & 0 deletions spec/fixtures/workers/worker_var.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
self.postMessage({
require: typeof require,
process: typeof process,
global: typeof global,
foo: typeof foo
});