Skip to content

Commit

Permalink
feat: add exposeInIsolatedWorld(worldId, key, api) to contextBridge
Browse files Browse the repository at this point in the history
  • Loading branch information
akshaydeo committed Aug 26, 2022
1 parent f1746c8 commit 1f42929
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 22 deletions.
34 changes: 33 additions & 1 deletion docs/api/context-bridge.md
Expand Up @@ -46,11 +46,17 @@ The `contextBridge` module has the following methods:
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
* `api` any - Your API, more information on what this API can be and how it works is available below.

### `contextBridge.exposeInIsolatedWorld(worldId, apiKey, api)`

* `worldId` Integer - The ID of the world to inject the API into. This has to be an existing world. 0 is the default world, 999 is the world used by Electrons contextIsolation feature. Chrome extensions reserve the range of IDs in [1 << 20, 1 << 29). You can provide any integer here.
* `apiKey` string - The key to inject the API onto `window` with. The API will be accessible on `window[apiKey]`.
* `api` any - Your API, more information on what this API can be and how it works is available below.

## Usage

### API

The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or an object
The `api` provided to [`exposeInMainWorld`](#contextbridgeexposeinmainworldapikey-api) and [`exposeInIsolatedWorld`](#contextbridgeexposeinisolatedworldworldid-apikey-api) must be a `Function`, `string`, `number`, `Array`, `boolean`, or 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**. Any data / primitives sent in
Expand Down Expand Up @@ -84,6 +90,32 @@ contextBridge.exposeInMainWorld(
)
```

An example of API exposed in an isolated world is shown below:

```javascript

const { contextBridge, webFrame } = require('electron')

webFrame.setIsolatedWorldInfo(1005, {
name: 'Isolated World'
})

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

// To call this API in isolated world, you can use `executeJavaScriptInIsolatedWorld`
webFrame.executeJavaScriptInIsolatedWorld(1005, [
{
code: 'window.electron.doThing()'
}
])
```

### API Functions

`Function` values that you bind through the `contextBridge` are proxied through Electron to ensure that contexts remain isolated. This
Expand Down
6 changes: 5 additions & 1 deletion lib/renderer/api/context-bridge.ts
Expand Up @@ -7,7 +7,11 @@ const checkContextIsolationEnabled = () => {
const contextBridge: Electron.ContextBridge = {
exposeInMainWorld: (key: string, api: any) => {
checkContextIsolationEnabled();
return binding.exposeAPIInMainWorld(key, api);
return binding.exposeAPIInWorld(0, key, api);
},
exposeInIsolatedWorld: (worldId: number, key: string, api: any) => {
checkContextIsolationEnabled();
return binding.exposeAPIInWorld(worldId, key, api);
}
};

Expand Down
37 changes: 22 additions & 15 deletions shell/renderer/api/electron_api_context_bridge.cc
Expand Up @@ -560,50 +560,57 @@ v8::MaybeLocal<v8::Object> CreateProxyForAPI(
}
}

void ExposeAPIInMainWorld(v8::Isolate* isolate,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT1("electron", "ContextBridge::ExposeAPIInMainWorld", "key", key);
void ExposeAPIInWorld(v8::Isolate* isolate,
const int world_id,
const std::string& key,
v8::Local<v8::Value> api,
gin_helper::Arguments* args) {
TRACE_EVENT2("electron", "ContextBridge::ExposeAPIInWorld", "key", key,
"worldId", world_id);

auto* render_frame = GetRenderFrame(isolate->GetCurrentContext()->Global());
CHECK(render_frame);
auto* frame = render_frame->GetWebFrame();
CHECK(frame);

v8::Local<v8::Context> main_context = frame->MainWorldScriptContext();
gin_helper::Dictionary global(main_context->GetIsolate(),
main_context->Global());
v8::Local<v8::Context> context =
world_id == 0 ? main_context
: frame->GetScriptContextFromWorldId(isolate, world_id);

gin_helper::Dictionary properties(context->GetIsolate(), context->Global());

if (global.Has(key)) {
if (properties.Has(key)) {
args->ThrowError(
"Cannot bind an API on top of an existing property on the window "
"object");
return;
}

v8::Local<v8::Context> isolated_context = frame->GetScriptContextFromWorldId(
args->isolate(), WorldIDs::ISOLATED_WORLD_ID);
v8::Local<v8::Context> electron_isolated_context =
frame->GetScriptContextFromWorldId(args->isolate(),
WorldIDs::ISOLATED_WORLD_ID);

{
context_bridge::ObjectCache object_cache;
v8::Context::Scope main_context_scope(main_context);

v8::MaybeLocal<v8::Value> maybe_proxy = PassValueToOtherContext(
isolated_context, main_context, api, &object_cache, false, 0);
electron_isolated_context, context, api, &object_cache, false, 0);
if (maybe_proxy.IsEmpty())
return;
auto proxy = maybe_proxy.ToLocalChecked();

if (base::FeatureList::IsEnabled(features::kContextBridgeMutability)) {
global.Set(key, proxy);
isolated.Set(key, proxy);
return;
}

if (proxy->IsObject() && !proxy->IsTypedArray() &&
!DeepFreeze(proxy.As<v8::Object>(), main_context))
!DeepFreeze(proxy.As<v8::Object>(), electron_isolated_context))
return;

global.SetReadOnlyNonConfigurable(key, proxy);
isolated.SetReadOnlyNonConfigurable(key, proxy);
}
}

Expand Down Expand Up @@ -716,7 +723,7 @@ void Initialize(v8::Local<v8::Object> exports,
void* priv) {
v8::Isolate* isolate = context->GetIsolate();
gin_helper::Dictionary dict(isolate, exports);
dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld);
dict.SetMethod("exposeAPIInWorld", &electron::api::ExposeAPIInWorld);
dict.SetMethod("_overrideGlobalValueFromIsolatedWorld",
&electron::api::OverrideGlobalValueFromIsolatedWorld);
dict.SetMethod("_overrideGlobalPropertyFromIsolatedWorld",
Expand Down
56 changes: 51 additions & 5 deletions spec-main/api-context-bridge-spec.ts
Expand Up @@ -62,17 +62,27 @@ describe('contextBridge', () => {

const generateTests = (useSandbox: boolean) => {
describe(`with sandbox=${useSandbox}`, () => {
const makeBindingWindow = async (bindingCreator: Function) => {
const preloadContent = `const renderer_1 = require('electron');
const makeBindingWindow = async (bindingCreator: Function, worldId: number = 0) => {
const preloadContentForMainContext = `const renderer_1 = require('electron');
${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
const gc=require('vm').runInNewContext('gc');
renderer_1.contextBridge.exposeInMainWorld('GCRunner', {
run: () => gc()
});`}
(${bindingCreator.toString()})();`;

const preloadContentForIsolatedContext = `const renderer_1 = require('electron');
${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc');
renderer_1.webFrame.setIsolatedWorldInfo(${worldId}, {
name: "Isolated World"
});
renderer_1.contextBridge.exposeInIsolatedWorld(1004, 'GCRunner', {
run: () => gc()
});`}
(${bindingCreator.toString()})();`;
const tmpDir = await fs.mkdtemp(path.resolve(os.tmpdir(), 'electron-spec-preload-'));
dir = tmpDir;
await fs.writeFile(path.resolve(tmpDir, 'preload.js'), preloadContent);
await fs.writeFile(path.resolve(tmpDir, 'preload.js'), worldId === 0 ? preloadContentForMainContext : preloadContentForIsolatedContext);
w = new BrowserWindow({
show: false,
webPreferences: {
Expand All @@ -86,8 +96,9 @@ describe('contextBridge', () => {
await w.loadURL(`http://127.0.0.1:${(server.address() as AddressInfo).port}`);
};

const callWithBindings = (fn: Function) =>
w.webContents.executeJavaScript(`(${fn.toString()})(window)`);
const callWithBindings = (fn: Function, worldId: number = 0) =>
worldId === 0 ? w.webContents.executeJavaScript(`(${fn.toString()})(window)`)
: w.webContents.executeJavaScriptInIsolatedWorld(worldId, [{ code: `(${fn.toString()})(window)` }]); ;

const getGCInfo = async (): Promise<{
trackedValues: number;
Expand All @@ -114,6 +125,16 @@ describe('contextBridge', () => {
expect(result).to.equal(123);
});

it('should proxy numbers', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInIsolatedWorld(1004, 'example', 123);
}, 1004);
const result = await callWithBindings((root: any) => {
return root.example;
}, 1004);
expect(result).to.equal(123);
});

it('should make global properties read-only', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', 123);
Expand Down Expand Up @@ -150,6 +171,19 @@ describe('contextBridge', () => {
expect(result).to.equal(123);
});

it('should make properties unwriteable', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInIsolatedWorld(1004, 'example', {
myNumber: 123
});
}, 1004);
const result = await callWithBindings((root: any) => {
root.example.myNumber = 456;
return root.example.myNumber;
}, 1004);
expect(result).to.equal(123);
});

it('should proxy strings', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example', 'my-words');
Expand Down Expand Up @@ -248,6 +282,18 @@ describe('contextBridge', () => {
expect(result).to.equal(true);
});

it('should proxy nested booleans', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInIsolatedWorld(1004, 'example', {
myBool: true
});
}, 1004);
const result = await callWithBindings((root: any) => {
return root.example.myBool;
}, 1004);
expect(result).to.equal(true);
});

it('should proxy promises and resolve with the correct value', async () => {
await makeBindingWindow(() => {
contextBridge.exposeInMainWorld('example',
Expand Down

0 comments on commit 1f42929

Please sign in to comment.