X Tutup
Skip to content

Commit 0090616

Browse files
feat: add a new contextBridge module (electron#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
1 parent 8099e61 commit 0090616

File tree

21 files changed

+1680
-38
lines changed

21 files changed

+1680
-38
lines changed

default_app/index.html

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
<head>
44
<title>Electron</title>
5-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self'; connect-src 'self'" />
5+
<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'" />
66
<link href="./styles.css" type="text/css" rel="stylesheet" />
77
<link href="./octicon/build.css" type="text/css" rel="stylesheet" />
8-
<script defer src="./index.js"></script>
98
</head>
109

1110
<body>
@@ -84,6 +83,9 @@ <h4>Forge</h4>
8483
</div>
8584
</div>
8685
</nav>
86+
<script>
87+
window.electronDefaultApp.initialize()
88+
</script>
8789
</body>
8890

8991
</html>

default_app/index.ts

Lines changed: 0 additions & 30 deletions
This file was deleted.

default_app/preload.ts

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,31 @@
1-
import { ipcRenderer } from 'electron'
1+
import { ipcRenderer, contextBridge } from 'electron'
2+
3+
async function getOcticonSvg (name: string) {
4+
try {
5+
const response = await fetch(`octicon/${name}.svg`)
6+
const div = document.createElement('div')
7+
div.innerHTML = await response.text()
8+
return div
9+
} catch {
10+
return null
11+
}
12+
}
13+
14+
async function loadSVG (element: HTMLSpanElement) {
15+
for (const cssClass of element.classList) {
16+
if (cssClass.startsWith('octicon-')) {
17+
const icon = await getOcticonSvg(cssClass.substr(8))
18+
if (icon) {
19+
for (const elemClass of element.classList) {
20+
icon.classList.add(elemClass)
21+
}
22+
element.before(icon)
23+
element.remove()
24+
break
25+
}
26+
}
27+
}
28+
}
229

330
async function initialize () {
431
const electronPath = await ipcRenderer.invoke('bootstrap')
@@ -15,6 +42,12 @@ async function initialize () {
1542
replaceText('.node-version', `Node v${process.versions.node}`)
1643
replaceText('.v8-version', `v8 v${process.versions.v8}`)
1744
replaceText('.command-example', `${electronPath} path-to-app`)
45+
46+
for (const element of document.querySelectorAll<HTMLSpanElement>('.octicon')) {
47+
loadSVG(element)
48+
}
1849
}
1950

20-
document.addEventListener('DOMContentLoaded', initialize)
51+
contextBridge.exposeInMainWorld('electronDefaultApp', {
52+
initialize
53+
})

docs/api/context-bridge.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# contextBridge
2+
3+
> Create a safe, bi-directional, synchronous bridge across isolated contexts
4+
5+
Process: [Renderer](../glossary.md#renderer-process)
6+
7+
An example of exposing an API to a renderer from an isolated preload script is given below:
8+
9+
```javascript
10+
// Preload (Isolated World)
11+
const { contextBridge, ipcRenderer } = require('electron')
12+
13+
contextBridge.exposeInMainWorld(
14+
'electron',
15+
{
16+
doThing: () => ipcRenderer.send('do-a-thing')
17+
}
18+
)
19+
```
20+
21+
```javascript
22+
// Renderer (Main World)
23+
24+
window.electron.doThing()
25+
```
26+
27+
## Glossary
28+
29+
### Main World
30+
31+
The "Main World" is the javascript context that your main renderer code runs in. By default the page you load in your renderer
32+
executes code in this world.
33+
34+
### Isolated World
35+
36+
When `contextIsolation` is enabled in your `webPreferences` your `preload` scripts run in an "Isolated World". You can read more about
37+
context isolation and what it affects in the [BrowserWindow](browser-window.md) docs.
38+
39+
## Methods
40+
41+
The `contextBridge` module has the following methods:
42+
43+
### `contextBridge.exposeInMainWorld(apiKey, api)`
44+
45+
* `apiKey` String - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
46+
* `api` Record<String, any> - Your API object, more information on what this API can be and how it works is available below.
47+
48+
## Usage
49+
50+
### API Objects
51+
52+
The `api` object provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be an object
53+
whose keys are strings and values are a `Function`, `String`, `Number`, `Array`, `Boolean` or another nested object that meets the same conditions.
54+
55+
`Function` values are proxied to the other context and all other values are **copied** and **frozen**. I.e. Any data / primitives sent in
56+
the API object become immutable and updates on either side of the bridge do not result in an update on the other side.
57+
58+
An example of a complex API object is shown below.
59+
60+
```javascript
61+
const { contextBridge } = require('electron')
62+
63+
contextBridge.exposeInMainWorld(
64+
'electron',
65+
{
66+
doThing: () => ipcRenderer.send('do-a-thing'),
67+
myPromises: [Promise.resolve(), Promise.reject(new Error('whoops'))],
68+
anAsyncFunction: async () => 123,
69+
data: {
70+
myFlags: ['a', 'b', 'c'],
71+
bootTime: 1234
72+
},
73+
nestedAPI: {
74+
evenDeeper: {
75+
youCanDoThisAsMuchAsYouWant: {
76+
fn: () => ({
77+
returnData: 123
78+
})
79+
}
80+
}
81+
}
82+
}
83+
)
84+
```
85+
86+
### API Functions
87+
88+
`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
89+
results in some key limitations that we've outlined below.
90+
91+
#### Parameter / Error / Return Type support
92+
93+
Because parameters, errors and return values are **copied** when they are sent over the bridge there are only certain types that can be used.
94+
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
95+
has been included below for completeness.
96+
97+
| Type | Complexity | Parameter Support | Return Value Support | Limitations |
98+
| ---- | ---------- | ----------------- | -------------------- | ----------- |
99+
| `String` | Simple ||| N/A |
100+
| `Number` | Simple ||| N/A |
101+
| `Boolean` | Simple ||| N/A |
102+
| `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. |
103+
| `Array` | Complex ||| Same limitations as the `Object` type |
104+
| `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 |
105+
| `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. |
106+
| `Function` | Complex ||| Prototype modifications are dropped. Sending classes or constructors will not work. |
107+
| [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 |
108+
| `Symbol` | N/A ||| Symbols cannot be copied across contexts so they are dropped |
109+
110+
111+
If the type you care about is not in the above table it is probably not supported.

filenames.auto.gni

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ auto_filenames = {
1414
"docs/api/command-line-switches.md",
1515
"docs/api/command-line.md",
1616
"docs/api/content-tracing.md",
17+
"docs/api/context-bridge.md",
1718
"docs/api/cookies.md",
1819
"docs/api/crash-reporter.md",
1920
"docs/api/debugger.md",
@@ -140,6 +141,7 @@ auto_filenames = {
140141
"lib/common/electron-binding-setup.ts",
141142
"lib/common/remote/type-utils.ts",
142143
"lib/common/web-view-methods.ts",
144+
"lib/renderer/api/context-bridge.ts",
143145
"lib/renderer/api/crash-reporter.js",
144146
"lib/renderer/api/desktop-capturer.ts",
145147
"lib/renderer/api/ipc-renderer.ts",
@@ -295,6 +297,7 @@ auto_filenames = {
295297
"lib/common/remote/type-utils.ts",
296298
"lib/common/reset-search-paths.ts",
297299
"lib/common/web-view-methods.ts",
300+
"lib/renderer/api/context-bridge.ts",
298301
"lib/renderer/api/crash-reporter.js",
299302
"lib/renderer/api/desktop-capturer.ts",
300303
"lib/renderer/api/exports/electron.ts",
@@ -342,6 +345,7 @@ auto_filenames = {
342345
"lib/common/init.ts",
343346
"lib/common/remote/type-utils.ts",
344347
"lib/common/reset-search-paths.ts",
348+
"lib/renderer/api/context-bridge.ts",
345349
"lib/renderer/api/crash-reporter.js",
346350
"lib/renderer/api/desktop-capturer.ts",
347351
"lib/renderer/api/exports/electron.ts",

filenames.gni

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
filenames = {
22
default_app_ts_sources = [
33
"default_app/default_app.ts",
4-
"default_app/index.ts",
54
"default_app/main.ts",
65
"default_app/preload.ts",
76
]
@@ -554,6 +553,10 @@ filenames = {
554553
"shell/common/promise_util.cc",
555554
"shell/common/skia_util.h",
556555
"shell/common/skia_util.cc",
556+
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc",
557+
"shell/renderer/api/context_bridge/render_frame_context_bridge_store.h",
558+
"shell/renderer/api/atom_api_context_bridge.cc",
559+
"shell/renderer/api/atom_api_context_bridge.h",
557560
"shell/renderer/api/atom_api_renderer_ipc.cc",
558561
"shell/renderer/api/atom_api_spell_check_client.cc",
559562
"shell/renderer/api/atom_api_spell_check_client.h",

lib/renderer/api/context-bridge.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
const { hasSwitch } = process.electronBinding('command_line')
2+
const binding = process.electronBinding('context_bridge')
3+
4+
const contextIsolationEnabled = hasSwitch('context-isolation')
5+
6+
const checkContextIsolationEnabled = () => {
7+
if (!contextIsolationEnabled) throw new Error('contextBridge API can only be used when contextIsolation is enabled')
8+
}
9+
10+
const contextBridge = {
11+
exposeInMainWorld: (key: string, api: Record<string, any>) => {
12+
checkContextIsolationEnabled()
13+
return binding.exposeAPIInMainWorld(key, api)
14+
},
15+
debugGC: () => binding._debugGCMaps({})
16+
}
17+
18+
if (!binding._debugGCMaps) delete contextBridge.debugGC
19+
20+
export default contextBridge

lib/renderer/api/module-list.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const enableRemoteModule = v8Util.getHiddenValue<boolean>(global, 'enableRemoteM
55

66
// Renderer side modules, please sort alphabetically.
77
export const rendererModuleList: ElectronInternal.ModuleEntry[] = [
8+
{ name: 'contextBridge', loader: () => require('./context-bridge') },
89
{ name: 'crashReporter', loader: () => require('./crash-reporter') },
910
{ name: 'ipcRenderer', loader: () => require('./ipc-renderer') },
1011
{ name: 'webFrame', loader: () => require('./web-frame') }

lib/sandboxed_renderer/api/module-list.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
const features = process.electronBinding('features')
22

33
export const moduleList: ElectronInternal.ModuleEntry[] = [
4+
{
5+
name: 'contextBridge',
6+
loader: () => require('@electron/internal/renderer/api/context-bridge')
7+
},
48
{
59
name: 'crashReporter',
610
loader: () => require('@electron/internal/renderer/api/crash-reporter')

native_mate/native_mate/arguments.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Arguments {
3131

3232
template <typename T>
3333
bool GetHolder(T* out) {
34-
return ConvertFromV8(isolate_, info_->Holder(), out);
34+
return mate::ConvertFromV8(isolate_, info_->Holder(), out);
3535
}
3636

3737
template <typename T>
@@ -57,7 +57,7 @@ class Arguments {
5757
return false;
5858
}
5959
v8::Local<v8::Value> val = (*info_)[next_];
60-
bool success = ConvertFromV8(isolate_, val, out);
60+
bool success = mate::ConvertFromV8(isolate_, val, out);
6161
if (success)
6262
next_++;
6363
return success;

0 commit comments

Comments
 (0)
X Tutup