Skip to content

Commit

Permalink
feat: preloads and nodeIntegration in iframes (#16425)
Browse files Browse the repository at this point in the history
* feat: add support for node / preloads in subframes

This feature has delibrately been built / implemented in such a way
that it has minimum impact on existing apps / code-paths.
Without enabling the new "nodeSupportInSubFrames" option basically none of this
new code will be hit.

The things that I believe need extra scrutiny are:

* Introduction of `event.reply` for IPC events and usage of `event.reply` instead of `event.sender.send()`
* Usage of `node::FreeEnvironment(env)` when the new option is enabled in order to avoid memory leaks.  I have tested this quite a bit and haven't managed to cause a crash but it is still feature flagged behind the "nodeSupportInSubFrames" flag to avoid potential impact.

Closes #10569
Closes #10401
Closes #11868
Closes #12505
Closes #14035

* feat: add support preloads in subframes for sandboxed renderers

* spec: add tests for new nodeSupportInSubFrames option

* spec: fix specs for .reply and ._replyInternal for internal messages

* chore: revert change to use flag instead of environment set size

* chore: clean up subframe impl

* chore: apply suggestions from code review

Co-Authored-By: MarshallOfSound <samuel.r.attard@gmail.com>

* chore: clean up reply usage

* chore: fix TS docs generation

* chore: cleanup after rebase

* chore: rename wrap to add in event fns
  • Loading branch information
MarshallOfSound committed Jan 22, 2019
1 parent 92b9525 commit 58a6fe1
Show file tree
Hide file tree
Showing 26 changed files with 332 additions and 49 deletions.
5 changes: 3 additions & 2 deletions atom/browser/api/event_emitter.cc
Expand Up @@ -57,9 +57,10 @@ v8::Local<v8::Object> CreateJSEvent(v8::Isolate* isolate,
} else {
event = CreateEventObject(isolate);
}
mate::Dictionary(isolate, event).Set("sender", object);
mate::Dictionary dict(isolate, event);
dict.Set("sender", object);
if (sender)
mate::Dictionary(isolate, event).Set("frameId", sender->GetRoutingID());
dict.Set("frameId", sender->GetRoutingID());
return event;
}

Expand Down
4 changes: 4 additions & 0 deletions atom/browser/web_contents_preferences.cc
Expand Up @@ -123,6 +123,7 @@ WebContentsPreferences::WebContentsPreferences(
SetDefaultBoolIfUndefined(options::kPlugins, false);
SetDefaultBoolIfUndefined(options::kExperimentalFeatures, false);
SetDefaultBoolIfUndefined(options::kNodeIntegration, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInSubFrames, false);
SetDefaultBoolIfUndefined(options::kNodeIntegrationInWorker, false);
SetDefaultBoolIfUndefined(options::kWebviewTag, false);
SetDefaultBoolIfUndefined(options::kSandbox, false);
Expand Down Expand Up @@ -369,6 +370,9 @@ void WebContentsPreferences::AppendCommandLineSwitches(
}
}

if (IsEnabled(options::kNodeIntegrationInSubFrames))
command_line->AppendSwitch(switches::kNodeIntegrationInSubFrames);

// 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
6 changes: 6 additions & 0 deletions atom/common/options_switches.cc
Expand Up @@ -154,6 +154,8 @@ const char kAllowRunningInsecureContent[] = "allowRunningInsecureContent";

const char kOffscreen[] = "offscreen";

const char kNodeIntegrationInSubFrames[] = "nodeIntegrationInSubFrames";

} // namespace options

namespace switches {
Expand Down Expand Up @@ -205,6 +207,10 @@ const char kWebviewTag[] = "webview-tag";
// Command switch passed to renderer process to control nodeIntegration.
const char kNodeIntegrationInWorker[] = "node-integration-in-worker";

// Command switch passed to renderer process to control whether node
// environments will be created in sub-frames.
const char kNodeIntegrationInSubFrames[] = "node-integration-in-subframes";

// Widevine options
// Path to Widevine CDM binaries.
const char kWidevineCdmPath[] = "widevine-cdm-path";
Expand Down
2 changes: 2 additions & 0 deletions atom/common/options_switches.h
Expand Up @@ -75,6 +75,7 @@ extern const char kSandbox[];
extern const char kWebSecurity[];
extern const char kAllowRunningInsecureContent[];
extern const char kOffscreen[];
extern const char kNodeIntegrationInSubFrames[];

} // namespace options

Expand Down Expand Up @@ -106,6 +107,7 @@ extern const char kHiddenPage[];
extern const char kNativeWindowOpen[];
extern const char kNodeIntegrationInWorker[];
extern const char kWebviewTag[];
extern const char kNodeIntegrationInSubFrames[];

extern const char kWidevineCdmPath[];
extern const char kWidevineCdmVersion[];
Expand Down
2 changes: 1 addition & 1 deletion atom/renderer/atom_render_frame_observer.cc
Expand Up @@ -187,7 +187,7 @@ void AtomRenderFrameObserver::OnBrowserMessage(bool internal,
return;

blink::WebLocalFrame* frame = render_frame_->GetWebFrame();
if (!frame || !render_frame_->IsMainFrame())
if (!frame)
return;

EmitIPCEvent(frame, internal, channel, args, sender_id);
Expand Down
42 changes: 24 additions & 18 deletions atom/renderer/atom_renderer_client.cc
Expand Up @@ -79,25 +79,27 @@ void AtomRendererClient::DidCreateScriptContext(
content::RenderFrame* render_frame) {
RendererClientBase::DidCreateScriptContext(context, render_frame);

// Only allow node integration for the main frame of the top window, unless it
// is a devtools extension page. Allowing child frames or child windows to
// have node integration would result in memory leak, since we don't destroy
// node environment when script context is destroyed.
//
// DevTools extensions do not follow this rule because our implementation
// requires node integration in iframes to work. And usually DevTools
// extensions do not dynamically add/remove iframes.
//
// TODO(zcbenz): Do not create Node environment if node integration is not
// enabled.
if (!(render_frame->IsMainFrame() &&
!render_frame->GetWebFrame()->Opener()) &&
!IsDevToolsExtension(render_frame))

// Do not load node if we're aren't a main frame or a devtools extension
// unless node support has been explicitly enabled for sub frames
bool is_main_frame =
render_frame->IsMainFrame() && !render_frame->GetWebFrame()->Opener();
bool is_devtools = IsDevToolsExtension(render_frame);
bool allow_node_in_subframes =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_node =
is_main_frame || is_devtools || allow_node_in_subframes;
if (!should_load_node) {
return;
}

injected_frames_.insert(render_frame);

// Prepare the node bindings.
// If this is the first environment we are creating, prepare the node
// bindings.
if (!node_integration_initialized_) {
node_integration_initialized_ = true;
node_bindings_->Initialize();
Expand All @@ -115,6 +117,8 @@ void AtomRendererClient::DidCreateScriptContext(
// Add Electron extended APIs.
atom_bindings_->BindTo(env->isolate(), env->process_object());
AddRenderBindings(env->isolate(), env->process_object());
mate::Dictionary process_dict(env->isolate(), env->process_object());
process_dict.SetReadOnly("isMainFrame", render_frame->IsMainFrame());

// Load everything.
node_bindings_->LoadEnvironment(env);
Expand Down Expand Up @@ -146,11 +150,13 @@ void AtomRendererClient::WillReleaseScriptContext(
if (env == node_bindings_->uv_env())
node_bindings_->set_uv_env(nullptr);

// Destroy the node environment.
// This is disabled because pending async tasks may still use the environment
// and would cause crashes later. Node does not seem to clear all async tasks
// when the environment is destroyed.
// node::FreeEnvironment(env);
// Destroy the node environment. We only do this if node support has been
// enabled for sub-frames to avoid a change-of-behavior / introduce crashes
// for existing users.
// TODO(MarshallOfSOund): Free the environment regardless of this switch
if (base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
node::FreeEnvironment(env);

// AtomBindings is tracking node environments.
atom_bindings_->EnvironmentDestroyed(env);
Expand Down
23 changes: 18 additions & 5 deletions atom/renderer/atom_sandboxed_renderer_client.cc
Expand Up @@ -139,7 +139,8 @@ AtomSandboxedRendererClient::~AtomSandboxedRendererClient() {}

void AtomSandboxedRendererClient::InitializeBindings(
v8::Local<v8::Object> binding,
v8::Local<v8::Context> context) {
v8::Local<v8::Context> context,
bool is_main_frame) {
auto* isolate = context->GetIsolate();
mate::Dictionary b(isolate, binding);
b.SetMethod("get", GetBinding);
Expand All @@ -154,6 +155,7 @@ void AtomSandboxedRendererClient::InitializeBindings(
process.SetReadOnly("pid", base::GetCurrentProcId());
process.SetReadOnly("sandboxed", true);
process.SetReadOnly("type", "renderer");
process.SetReadOnly("isMainFrame", is_main_frame);

// Pass in CLI flags needed to setup the renderer
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
Expand All @@ -180,15 +182,23 @@ void AtomSandboxedRendererClient::DidCreateScriptContext(

// Only allow preload for the main frame or
// For devtools we still want to run the preload_bundle script
if (!render_frame->IsMainFrame() && !IsDevTools(render_frame) &&
!IsDevToolsExtension(render_frame))
// Or when nodeSupport is explicitly enabled in sub frames
bool is_main_frame = render_frame->IsMainFrame();
bool is_devtools =
IsDevTools(render_frame) || IsDevToolsExtension(render_frame);
bool allow_node_in_sub_frames =
base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames);
bool should_load_preload =
is_main_frame || is_devtools || allow_node_in_sub_frames;
if (!should_load_preload)
return;

// Wrap the bundle into a function that receives the binding object as
// argument.
auto* isolate = context->GetIsolate();
auto binding = v8::Object::New(isolate);
InitializeBindings(binding, context);
InitializeBindings(binding, context, render_frame->IsMainFrame());
AddRenderBindings(isolate, binding);

std::vector<v8::Local<v8::String>> preload_bundle_params = {
Expand Down Expand Up @@ -229,7 +239,10 @@ void AtomSandboxedRendererClient::WillReleaseScriptContext(
v8::Handle<v8::Context> context,
content::RenderFrame* render_frame) {
// Only allow preload for the main frame
if (!render_frame->IsMainFrame())
// Or for sub frames when explicitly enabled
if (!render_frame->IsMainFrame() &&
!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kNodeIntegrationInSubFrames))
return;

auto* isolate = context->GetIsolate();
Expand Down
3 changes: 2 additions & 1 deletion atom/renderer/atom_sandboxed_renderer_client.h
Expand Up @@ -19,7 +19,8 @@ class AtomSandboxedRendererClient : public RendererClientBase {
~AtomSandboxedRendererClient() override;

void InitializeBindings(v8::Local<v8::Object> binding,
v8::Local<v8::Context> context);
v8::Local<v8::Context> context,
bool is_main_frame);
void InvokeIpcCallback(v8::Handle<v8::Context> context,
const std::string& callback_name,
std::vector<v8::Handle<v8::Value>> args);
Expand Down
4 changes: 4 additions & 0 deletions docs/api/browser-window.md
Expand Up @@ -255,6 +255,10 @@ It creates a new `BrowserWindow` with native properties as set by the `options`.
* `nodeIntegrationInWorker` Boolean (optional) - Whether node integration is
enabled in web workers. Default is `false`. More about this can be found
in [Multithreading](../tutorial/multithreading.md).
* `nodeIntegrationInSubFrames` Boolean (optional) - Experimental option for
enabling NodeJS support in sub-frames such as iframes. All your preloads will load for
every iframe, you can use `process.isMainFrame` to determine if you are
in the main frame or not.
* `preload` String (optional) - Specifies a script that will be loaded before other
scripts run in the page. This script will always have access to node APIs
no matter whether node integration is turned on or off. The value should
Expand Down
17 changes: 15 additions & 2 deletions docs/api/ipc-main.md
Expand Up @@ -18,7 +18,9 @@ process, see [webContents.send][web-contents-send] for more information.
* When sending a message, the event name is the `channel`.
* To reply to a synchronous message, you need to set `event.returnValue`.
* To send an asynchronous message back to the sender, you can use
`event.sender.send(...)`.
`event.reply(...)`. This helper method will automatically handle messages
coming from frames that aren't the main frame (e.g. iframes) whereas
`event.sender.send(...)` will always send to the main frame.

An example of sending and handling messages between the render and main
processes:
Expand All @@ -28,7 +30,7 @@ processes:
const { ipcMain } = require('electron')
ipcMain.on('asynchronous-message', (event, arg) => {
console.log(arg) // prints "ping"
event.sender.send('asynchronous-reply', 'pong')
event.reply('asynchronous-reply', 'pong')
})

ipcMain.on('synchronous-message', (event, arg) => {
Expand Down Expand Up @@ -86,6 +88,10 @@ Removes listeners of the specified `channel`.

The `event` object passed to the `callback` has the following methods:

### `event.frameId`

An `Integer` representing the ID of the renderer frame that sent this message.

### `event.returnValue`

Set this to the value to be returned in a synchronous message.
Expand All @@ -97,3 +103,10 @@ Returns the `webContents` that sent the message, you can call
[webContents.send][web-contents-send] for more information.

[web-contents-send]: web-contents.md#contentssendchannel-arg1-arg2-

### `event.reply`

A function that will send an IPC message to the renderer frane that sent
the original message that you are currently handling. You should use this
method to "reply" to the sent message in order to guaruntee the reply will go
to the correct process and frame.
5 changes: 5 additions & 0 deletions docs/api/process.md
Expand Up @@ -59,6 +59,11 @@ process.once('loaded', () => {
A `Boolean`. When app is started by being passed as parameter to the default app, this
property is `true` in the main process, otherwise it is `undefined`.

### `process.isMainFrame`

A `Boolean`, `true` when the current renderer context is the "main" renderer
frame. If you want the ID of the current frame you should use `webFrame.routingId`.

### `process.mas`

A `Boolean`. For Mac App Store build, this property is `true`, for other builds it is
Expand Down
30 changes: 30 additions & 0 deletions docs/api/web-contents.md
Expand Up @@ -1420,6 +1420,36 @@ app.on('ready', () => {
</html>
```

#### `contents.sendToFrame(frameId, channel[, arg1][, arg2][, ...])`

* `frameId` Integer
* `channel` String
* `...args` any[]

Send an asynchronous message to a specific frame in a renderer process via
`channel`. Arguments will be serialized
as JSON internally and as such no functions or prototype chains will be included.

The renderer process can handle the message by listening to `channel` with the
[`ipcRenderer`](ipc-renderer.md) module.

If you want to get the `frameId` of a given renderer context you should use
the `webFrame.routingId` value. E.g.

```js
// In a renderer process
console.log('My frameId is:', require('electron').webFrame.routingId)
```

You can also read `frameId` from all incoming IPC messages in the main process.

```js
// In the main process
ipcMain.on('ping', (event) => {
console.info('Message came from frameId:', event.frameId)
})
```

#### `contents.enableDeviceEmulation(parameters)`

* `parameters` Object
Expand Down
32 changes: 32 additions & 0 deletions lib/browser/api/web-contents.js
Expand Up @@ -143,6 +143,18 @@ WebContents.prototype._sendInternalToAll = function (channel, ...args) {

return this._send(internal, sendToAll, channel, args)
}
WebContents.prototype.sendToFrame = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
} else if (typeof frameId !== 'number') {
throw new Error('Missing required frameId argument')
}

const internal = false
const sendToAll = false

return this._sendToFrame(internal, sendToAll, frameId, channel, args)
}
WebContents.prototype._sendToFrameInternal = function (frameId, channel, ...args) {
if (typeof channel !== 'string') {
throw new Error('Missing required channel argument')
Expand Down Expand Up @@ -330,6 +342,22 @@ WebContents.prototype.loadFile = function (filePath, options = {}) {
}))
}

const addReplyToEvent = (event) => {
event.reply = (...args) => {
event.sender.sendToFrame(event.frameId, ...args)
}
}

const addReplyInternalToEvent = (event) => {
Object.defineProperty(event, '_replyInternal', {
configurable: false,
enumerable: false,
value: (...args) => {
event.sender._sendToFrameInternal(event.frameId, ...args)
}
})
}

// Add JavaScript wrappers for WebContents class.
WebContents.prototype._init = function () {
// The navigation controller.
Expand All @@ -343,6 +371,7 @@ WebContents.prototype._init = function () {

// Dispatch IPC messages to the ipc module.
this.on('-ipc-message', function (event, [channel, ...args]) {
addReplyToEvent(event)
this.emit('ipc-message', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})
Expand All @@ -354,11 +383,13 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyToEvent(event)
this.emit('ipc-message-sync', event, channel, ...args)
ipcMain.emit(channel, event, ...args)
})

this.on('ipc-internal-message', function (event, [channel, ...args]) {
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})

Expand All @@ -369,6 +400,7 @@ WebContents.prototype._init = function () {
},
get: function () {}
})
addReplyInternalToEvent(event)
ipcMainInternal.emit(channel, event, ...args)
})

Expand Down

0 comments on commit 58a6fe1

Please sign in to comment.