Skip to content

Commit

Permalink
feat: add a new contextBridge module (#20307)
Browse files Browse the repository at this point in the history
* feat: add a new contextBridge module

* chore: fix docs linting

* feat: add support for function arguments being proxied

* chore: ensure that contextBridge can only be used when contextIsolation is enabled

* docs: getReverseBinding can be null

* docs: fix broken links in md file

* feat: add support for promises in function parameters

* fix: linting failure for explicit constructor

* Update atom_api_context_bridge.cc

* chore: update docs and API design as per feedback

* refactor: remove reverse bindings and handle GC'able functions across the bridge

* chore: only expose debugGC in testing builds

* fix: do not proxy promises as objects

* spec: add complete spec coverage for contextBridge

* spec: add tests for null/undefined and the anti-overwrite logic

* chore: fix linting

* spec: add complex nested back-and-forth function calling

* fix: expose contextBridge in sandboxed renderers

* refactor: improve security of default_app using the new contextBridge module

* s/bindAPIInMainWorld/exposeInMainWorld

* chore: sorry for this commit, its a big one, I fixed like everything and refactored a lot

* chore: remove PassedValueCache as it is unused now

Values transferred from context A to context B are now cachde in the RenderFramePersistenceStore

* chore: move to anonymous namespace

* refactor: remove PassValueToOtherContextWithCache

* chore: remove commented unused code blocks

* chore: remove .only

* chore: remote commented code

* refactor: extract RenderFramePersistenceStore

* spec: ensure it works with numbered keys

* fix: handle number keys correctly

* fix: sort out the linter

* spec: update default_app asar spec for removed file

* refactor: change signatures to return v8 objects directly rather than the mate dictionary handle

* refactor: use the v8 serializer to support cloneable buffers and other object types

* chore: fix linting

* fix: handle hash collisions with a linked list in the map

* fix: enforce a recursion limit on the context bridge

* chore: fix linting

* chore: remove TODO

* chore: adapt for PR feedback

* chore: remove .only

* chore: clean up docs and clean up the proxy map when objects are released

* chore: ensure we cache object values that are cloned through the V8 serializer
  • Loading branch information
MarshallOfSound committed Oct 18, 2019
1 parent 8099e61 commit 0090616
Show file tree
Hide file tree
Showing 21 changed files with 1,680 additions and 38 deletions.
6 changes: 4 additions & 2 deletions default_app/index.html
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@


<head> <head>
<title>Electron</title> <title>Electron</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'" /> <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'sha256-6PH54BfkNq/EMMhUY7nhHf3c+AxloOwfy7hWyT01CM8='; style-src 'self'; img-src 'self'; connect-src 'self'" />
<link href="./styles.css" type="text/css" rel="stylesheet" /> <link href="./styles.css" type="text/css" rel="stylesheet" />
<link href="./octicon/build.css" type="text/css" rel="stylesheet" /> <link href="./octicon/build.css" type="text/css" rel="stylesheet" />
<script defer src="./index.js"></script>
</head> </head>


<body> <body>
Expand Down Expand Up @@ -84,6 +83,9 @@ <h4>Forge</h4>
</div> </div>
</div> </div>
</nav> </nav>
<script>
window.electronDefaultApp.initialize()
</script>
</body> </body>


</html> </html>
30 changes: 0 additions & 30 deletions default_app/index.ts

This file was deleted.

37 changes: 35 additions & 2 deletions default_app/preload.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,4 +1,31 @@
import { ipcRenderer } from 'electron' import { ipcRenderer, contextBridge } from 'electron'

async function getOcticonSvg (name: string) {
try {
const response = await fetch(`octicon/${name}.svg`)
const div = document.createElement('div')
div.innerHTML = await response.text()
return div
} catch {
return null
}
}

async function loadSVG (element: HTMLSpanElement) {
for (const cssClass of element.classList) {
if (cssClass.startsWith('octicon-')) {
const icon = await getOcticonSvg(cssClass.substr(8))
if (icon) {
for (const elemClass of element.classList) {
icon.classList.add(elemClass)
}
element.before(icon)
element.remove()
break
}
}
}
}


async function initialize () { async function initialize () {
const electronPath = await ipcRenderer.invoke('bootstrap') const electronPath = await ipcRenderer.invoke('bootstrap')
Expand All @@ -15,6 +42,12 @@ async function initialize () {
replaceText('.node-version', `Node v${process.versions.node}`) replaceText('.node-version', `Node v${process.versions.node}`)
replaceText('.v8-version', `v8 v${process.versions.v8}`) replaceText('.v8-version', `v8 v${process.versions.v8}`)
replaceText('.command-example', `${electronPath} path-to-app`) replaceText('.command-example', `${electronPath} path-to-app`)

for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
loadSVG(element)
}
} }


document.addEventListener('DOMContentLoaded', initialize) contextBridge.exposeInMainWorld('electronDefaultApp', {
initialize
})
111 changes: 111 additions & 0 deletions docs/api/context-bridge.md
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,111 @@
# contextBridge

> Create a safe, bi-directional, synchronous bridge across isolated contexts
Process: [Renderer](../glossary.md#renderer-process)

An example of exposing an API to a renderer from an isolated preload script is given below:

```javascript
// Preload (Isolated World)
const { contextBridge, ipcRenderer } = require('electron')

contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing')
}
)
```

```javascript
// Renderer (Main World)

window.electron.doThing()
```

## Glossary

### Main World

The "Main World" is the javascript context that your main renderer code runs in. By default the page you load in your renderer
executes code in this world.

### Isolated World

When `contextIsolation` is enabled in your `webPreferences` your `preload` scripts run in an "Isolated World". You can read more about
context isolation and what it affects in the [BrowserWindow](browser-window.md) docs.

## Methods

The `contextBridge` module has the following methods:

### `contextBridge.exposeInMainWorld(apiKey, api)`

* `apiKey` String - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
* `api` Record<String, any> - Your API object, more information on what this API can be and how it works is available below.

## Usage

### API Objects

The `api` object provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be an object
whose keys are strings and values are a `Function`, `String`, `Number`, `Array`, `Boolean` or another nested object that meets the same conditions.

`Function` values are proxied to the other context and all other values are **copied** and **frozen**. I.e. Any data / primitives sent in
the API object become immutable and updates on either side of the bridge do not result in an update on the other side.

An example of a complex API object is shown below.

```javascript
const { contextBridge } = require('electron')

contextBridge.exposeInMainWorld(
'electron',
{
doThing: () => ipcRenderer.send('do-a-thing'),
myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))],
anAsyncFunction: async () => 123,
data: {
myFlags: ['a', 'b', 'c'],
bootTime: 1234
},
nestedAPI: {
evenDeeper: {
youCanDoThisAsMuchAsYouWant: {
fn: () => ({
returnData: 123
})
}
}
}
}
)
```

### API Functions

`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
results in some key limitations that we've outlined below.

#### Parameter / Error / Return Type support

Because parameters, errors and return values are **copied** when they are sent over the bridge there are only certain types that can be used.
At a high level if the type you want to use can be serialized and un-serialized into the same object it will work. A table of type support
has been included below for completeness.

| Type | Complexity | Parameter Support | Return Value Support | Limitations |
| ---- | ---------- | ----------------- | -------------------- | ----------- |
| `String` | Simple ||| N/A |
| `Number` | Simple ||| N/A |
| `Boolean` | Simple ||| N/A |
| `Object` | Complex ||| Keys must be supported "Simple" types in this table. Values must be supported in this table. Prototype modifications are dropped. Sending custom classes will copy values but not the prototype. |
| `Array` | Complex ||| Same limitations as the `Object` type |
| `Error` | Complex ||| Errors that are thrown are also copied, this can result in the message and stack trace of the error changing slightly due to being thrown in a different context |
| `Promise` | Complex ||| Promises are only proxied if they are a the return value or exact parameter. Promises nested in arrays or obejcts will be dropped. |
| `Function` | Complex ||| Prototype modifications are dropped. Sending classes or constructors will not work. |
| [Cloneable Types](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) | Simple ||| See the linked document on cloneable types |
| `Symbol` | N/A ||| Symbols cannot be copied across contexts so they are dropped |


If the type you care about is not in the above table it is probably not supported.
4 changes: 4 additions & 0 deletions filenames.auto.gni
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ auto_filenames = {
"docs/api/command-line-switches.md", "docs/api/command-line-switches.md",
"docs/api/command-line.md", "docs/api/command-line.md",
"docs/api/content-tracing.md", "docs/api/content-tracing.md",
"docs/api/context-bridge.md",
"docs/api/cookies.md", "docs/api/cookies.md",
"docs/api/crash-reporter.md", "docs/api/crash-reporter.md",
"docs/api/debugger.md", "docs/api/debugger.md",
Expand Down Expand Up @@ -140,6 +141,7 @@ auto_filenames = {
"lib/common/electron-binding-setup.ts", "lib/common/electron-binding-setup.ts",
"lib/common/remote/type-utils.ts", "lib/common/remote/type-utils.ts",
"lib/common/web-view-methods.ts", "lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js", "lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/ipc-renderer.ts", "lib/renderer/api/ipc-renderer.ts",
Expand Down Expand Up @@ -295,6 +297,7 @@ auto_filenames = {
"lib/common/remote/type-utils.ts", "lib/common/remote/type-utils.ts",
"lib/common/reset-search-paths.ts", "lib/common/reset-search-paths.ts",
"lib/common/web-view-methods.ts", "lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js", "lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.ts", "lib/renderer/api/exports/electron.ts",
Expand Down Expand Up @@ -342,6 +345,7 @@ auto_filenames = {
"lib/common/init.ts", "lib/common/init.ts",
"lib/common/remote/type-utils.ts", "lib/common/remote/type-utils.ts",
"lib/common/reset-search-paths.ts", "lib/common/reset-search-paths.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js", "lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts", "lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.ts", "lib/renderer/api/exports/electron.ts",
Expand Down
5 changes: 4 additions & 1 deletion filenames.gni
Original file line number Original file line Diff line number Diff line change
@@ -1,7 +1,6 @@
filenames = { filenames = {
default_app_ts_sources = [ default_app_ts_sources = [
"default_app/default_app.ts", "default_app/default_app.ts",
"default_app/index.ts",
"default_app/main.ts", "default_app/main.ts",
"default_app/preload.ts", "default_app/preload.ts",
] ]
Expand Down Expand Up @@ -554,6 +553,10 @@ filenames = {
"shell/common/promise_util.cc", "shell/common/promise_util.cc",
"shell/common/skia_util.h", "shell/common/skia_util.h",
"shell/common/skia_util.cc", "shell/common/skia_util.cc",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc",
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.h",
"shell/renderer/api/atom_api_context_bridge.cc",
"shell/renderer/api/atom_api_context_bridge.h",
"shell/renderer/api/atom_api_renderer_ipc.cc", "shell/renderer/api/atom_api_renderer_ipc.cc",
"shell/renderer/api/atom_api_spell_check_client.cc", "shell/renderer/api/atom_api_spell_check_client.cc",
"shell/renderer/api/atom_api_spell_check_client.h", "shell/renderer/api/atom_api_spell_check_client.h",
Expand Down
20 changes: 20 additions & 0 deletions lib/renderer/api/context-bridge.ts
Original file line number Original file line Diff line number Diff line change
@@ -0,0 +1,20 @@
const { hasSwitch } = process.electronBinding('command_line')
const binding = process.electronBinding('context_bridge')

const contextIsolationEnabled = hasSwitch('context-isolation')

const checkContextIsolationEnabled = () => {
if (!contextIsolationEnabled) throw new Error('contextBridge API can only be used when contextIsolation is enabled')
}

const contextBridge = {
exposeInMainWorld: (key: string, api: Record<string, any>) => {
checkContextIsolationEnabled()
return binding.exposeAPIInMainWorld(key, api)
},
debugGC: () => binding._debugGCMaps({})
}

if (!binding._debugGCMaps) delete contextBridge.debugGC

export default contextBridge
1 change: 1 addition & 0 deletions lib/renderer/api/module-list.ts
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const enableRemoteModule = v8Util.getHiddenValue<boolean>(global, 'enableRemoteM


// Renderer side modules, please sort alphabetically. // Renderer side modules, please sort alphabetically.
export const rendererModuleList: ElectronInternal.ModuleEntry[] = [ export const rendererModuleList: ElectronInternal.ModuleEntry[] = [
{ name: 'contextBridge', loader: () => require('./context-bridge') },
{ name: 'crashReporter', loader: () => require('./crash-reporter') }, { name: 'crashReporter', loader: () => require('./crash-reporter') },
{ name: 'ipcRenderer', loader: () => require('./ipc-renderer') }, { name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
{ name: 'webFrame', loader: () => require('./web-frame') } { name: 'webFrame', loader: () => require('./web-frame') }
Expand Down
4 changes: 4 additions & 0 deletions lib/sandboxed_renderer/api/module-list.ts
Original file line number Original file line Diff line number Diff line change
@@ -1,6 +1,10 @@
const features = process.electronBinding('features') const features = process.electronBinding('features')


export const moduleList: ElectronInternal.ModuleEntry[] = [ export const moduleList: ElectronInternal.ModuleEntry[] = [
{
name: 'contextBridge',
loader: () => require('@electron/internal/renderer/api/context-bridge')
},
{ {
name: 'crashReporter', name: 'crashReporter',
loader: () => require('@electron/internal/renderer/api/crash-reporter') loader: () => require('@electron/internal/renderer/api/crash-reporter')
Expand Down
4 changes: 2 additions & 2 deletions native_mate/native_mate/arguments.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class Arguments {


template <typename T> template <typename T>
bool GetHolder(T* out) { bool GetHolder(T* out) {
return ConvertFromV8(isolate_, info_->Holder(), out); return mate::ConvertFromV8(isolate_, info_->Holder(), out);
} }


template <typename T> template <typename T>
Expand All @@ -57,7 +57,7 @@ class Arguments {
return false; return false;
} }
v8::Local<v8::Value> val = (*info_)[next_]; v8::Local<v8::Value> val = (*info_)[next_];
bool success = ConvertFromV8(isolate_, val, out); bool success = mate::ConvertFromV8(isolate_, val, out);
if (success) if (success)
next_++; next_++;
return success; return success;
Expand Down
17 changes: 17 additions & 0 deletions native_mate/native_mate/dictionary.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ class Dictionary {


static Dictionary CreateEmpty(v8::Isolate* isolate); static Dictionary CreateEmpty(v8::Isolate* isolate);


bool Has(base::StringPiece key) const {
v8::Local<v8::Context> context = isolate_->GetCurrentContext();
v8::Local<v8::String> v8_key = StringToV8(isolate_, key);
return internal::IsTrue(GetHandle()->Has(context, v8_key));
}

template <typename T> template <typename T>
bool Get(base::StringPiece key, T* out) const { bool Get(base::StringPiece key, T* out) const {
// Check for existence before getting, otherwise this method will always // Check for existence before getting, otherwise this method will always
Expand Down Expand Up @@ -76,6 +82,17 @@ class Dictionary {
return !result.IsNothing() && result.FromJust(); return !result.IsNothing() && result.FromJust();
} }


template <typename T>
bool SetReadOnlyNonConfigurable(base::StringPiece key, T val) {
v8::Local<v8::Value> v8_value;
if (!TryConvertToV8(isolate_, val, &v8_value))
return false;
v8::Maybe<bool> result = GetHandle()->DefineOwnProperty(
isolate_->GetCurrentContext(), StringToV8(isolate_, key), v8_value,
static_cast<v8::PropertyAttribute>(v8::ReadOnly | v8::DontDelete));
return !result.IsNothing() && result.FromJust();
}

template <typename T> template <typename T>
bool SetMethod(base::StringPiece key, const T& callback) { bool SetMethod(base::StringPiece key, const T& callback) {
return GetHandle() return GetHandle()
Expand Down
1 change: 1 addition & 0 deletions shell/common/node_bindings.cc
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
V(atom_common_screen) \ V(atom_common_screen) \
V(atom_common_shell) \ V(atom_common_shell) \
V(atom_common_v8_util) \ V(atom_common_v8_util) \
V(atom_renderer_context_bridge) \
V(atom_renderer_ipc) \ V(atom_renderer_ipc) \
V(atom_renderer_web_frame) V(atom_renderer_web_frame)


Expand Down
10 changes: 10 additions & 0 deletions shell/common/promise_util.h
Original file line number Original file line Diff line number Diff line change
Expand Up @@ -126,6 +126,16 @@ class Promise {
return GetInner()->Reject(GetContext(), v8::Undefined(isolate())); return GetInner()->Reject(GetContext(), v8::Undefined(isolate()));
} }


v8::Maybe<bool> Reject(v8::Local<v8::Value> exception) {
v8::HandleScope handle_scope(isolate());
v8::MicrotasksScope script_scope(isolate(),
v8::MicrotasksScope::kRunMicrotasks);
v8::Context::Scope context_scope(
v8::Local<v8::Context>::New(isolate(), GetContext()));

return GetInner()->Reject(GetContext(), exception);
}

template <typename... ResolveType> template <typename... ResolveType>
v8::MaybeLocal<v8::Promise> Then( v8::MaybeLocal<v8::Promise> Then(
base::OnceCallback<void(ResolveType...)> cb) { base::OnceCallback<void(ResolveType...)> cb) {
Expand Down
Loading

0 comments on commit 0090616

Please sign in to comment.