Skip to content

Commit

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

* 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

* docs: mark contextBridge as experimental (#20638)

* docs: mark contextBridge as experimental

This commit didn't make it to the original PR, quick addition here

* Update context-bridge.md

* chore: update for 7-0-x differences

* chore: update callback header

* chore: add v8 serializer converter, cherry picked from 2fad53e

* chore: update for 7-0-x differences
  • Loading branch information
MarshallOfSound committed Nov 4, 2019
1 parent ef548b6 commit 976c7d1
Show file tree
Hide file tree
Showing 23 changed files with 1,863 additions and 38 deletions.
6 changes: 4 additions & 2 deletions default_app/index.html
Expand Up @@ -2,10 +2,9 @@

<head>
<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="./octicon/build.css" type="text/css" rel="stylesheet" />
<script defer src="./index.js"></script>
</head>

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

</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
@@ -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 () {
const electronPath = await ipcRenderer.invoke('bootstrap')
Expand All @@ -15,6 +42,12 @@ async function initialize () {
replaceText('.node-version', `Node v${process.versions.node}`)
replaceText('.v8-version', `v8 v${process.versions.v8}`)
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
@@ -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)` _Experimental_

* `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-experimental) 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
Expand Up @@ -14,6 +14,7 @@ auto_filenames = {
"docs/api/clipboard.md",
"docs/api/command-line.md",
"docs/api/content-tracing.md",
"docs/api/context-bridge.md",
"docs/api/cookies.md",
"docs/api/crash-reporter.md",
"docs/api/debugger.md",
Expand Down Expand Up @@ -140,6 +141,7 @@ auto_filenames = {
"lib/common/error-utils.ts",
"lib/common/is-promise.ts",
"lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/ipc-renderer.js",
Expand Down Expand Up @@ -299,6 +301,7 @@ auto_filenames = {
"lib/common/is-promise.ts",
"lib/common/reset-search-paths.ts",
"lib/common/web-view-methods.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.js",
Expand Down Expand Up @@ -348,6 +351,7 @@ auto_filenames = {
"lib/common/init.ts",
"lib/common/is-promise.ts",
"lib/common/reset-search-paths.ts",
"lib/renderer/api/context-bridge.ts",
"lib/renderer/api/crash-reporter.js",
"lib/renderer/api/desktop-capturer.ts",
"lib/renderer/api/exports/electron.js",
Expand Down
5 changes: 4 additions & 1 deletion filenames.gni
@@ -1,7 +1,6 @@
filenames = {
default_app_ts_sources = [
"default_app/default_app.ts",
"default_app/index.ts",
"default_app/main.ts",
"default_app/preload.ts",
]
Expand Down Expand Up @@ -537,6 +536,10 @@ filenames = {
"shell/common/promise_util.cc",
"shell/common/skia_util.h",
"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_spell_check_client.cc",
"shell/renderer/api/atom_api_spell_check_client.h",
Expand Down
20 changes: 20 additions & 0 deletions lib/renderer/api/context-bridge.ts
@@ -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.js
Expand Up @@ -8,6 +8,7 @@ const enableRemoteModule = v8Util.getHiddenValue(global, 'enableRemoteModule')
// Renderer side modules, please sort alphabetically.
// A module is `enabled` if there is no explicit condition defined.
module.exports = [
{ name: 'contextBridge', loader: () => require('./context-bridge') },
{ name: 'crashReporter', loader: () => require('./crash-reporter') },
{ name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
{ name: 'webFrame', loader: () => require('./web-frame') }
Expand Down
4 changes: 4 additions & 0 deletions lib/sandboxed_renderer/api/module-list.js
Expand Up @@ -3,6 +3,10 @@
const features = process.electronBinding('features')

module.exports = [
{
name: 'contextBridge',
load: () => require('@electron/internal/renderer/api/context-bridge')
},
{
name: 'crashReporter',
load: () => require('@electron/internal/renderer/api/crash-reporter')
Expand Down
4 changes: 2 additions & 2 deletions native_mate/native_mate/arguments.h
Expand Up @@ -27,7 +27,7 @@ class Arguments {

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

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

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>
bool Get(base::StringPiece key, T* out) const {
// Check for existence before getting, otherwise this method will always
Expand Down Expand Up @@ -102,6 +108,17 @@ class Dictionary {
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>
bool SetMethod(base::StringPiece key, const T& callback) {
return GetHandle()
Expand Down

0 comments on commit 976c7d1

Please sign in to comment.