diff --git a/default_app/index.html b/default_app/index.html index 68edcf8180618..40a9148af0006 100644 --- a/default_app/index.html +++ b/default_app/index.html @@ -2,10 +2,9 @@ Electron - + - @@ -84,6 +83,9 @@

Forge

+ \ No newline at end of file diff --git a/default_app/index.ts b/default_app/index.ts deleted file mode 100644 index 6b3fdd084f0b4..0000000000000 --- a/default_app/index.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 - } - } - } -} - -for (const element of document.querySelectorAll('.octicon')) { - loadSVG(element) -} diff --git a/default_app/preload.ts b/default_app/preload.ts index a5546da8f9a32..855aa26ca3ba9 100644 --- a/default_app/preload.ts +++ b/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') @@ -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('.octicon')) { + loadSVG(element) + } } -document.addEventListener('DOMContentLoaded', initialize) +contextBridge.exposeInMainWorld('electronDefaultApp', { + initialize +}) diff --git a/docs/api/context-bridge.md b/docs/api/context-bridge.md new file mode 100644 index 0000000000000..a9932e059d536 --- /dev/null +++ b/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 - 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. diff --git a/filenames.auto.gni b/filenames.auto.gni index ecbf266473604..3c557aecc856f 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -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", @@ -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", @@ -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", @@ -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", diff --git a/filenames.gni b/filenames.gni index 897fb4f626caa..6306fb792d450 100644 --- a/filenames.gni +++ b/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", ] @@ -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", diff --git a/lib/renderer/api/context-bridge.ts b/lib/renderer/api/context-bridge.ts new file mode 100644 index 0000000000000..b435be60d7be1 --- /dev/null +++ b/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) => { + checkContextIsolationEnabled() + return binding.exposeAPIInMainWorld(key, api) + }, + debugGC: () => binding._debugGCMaps({}) +} + +if (!binding._debugGCMaps) delete contextBridge.debugGC + +export default contextBridge diff --git a/lib/renderer/api/module-list.js b/lib/renderer/api/module-list.js index 27406d1f63540..fe492e806a17e 100644 --- a/lib/renderer/api/module-list.js +++ b/lib/renderer/api/module-list.js @@ -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') } diff --git a/lib/sandboxed_renderer/api/module-list.js b/lib/sandboxed_renderer/api/module-list.js index 5afffe694e931..0fb24c3077251 100644 --- a/lib/sandboxed_renderer/api/module-list.js +++ b/lib/sandboxed_renderer/api/module-list.js @@ -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') diff --git a/native_mate/native_mate/arguments.h b/native_mate/native_mate/arguments.h index 3a8a07fdf3158..41b0fdc7e19a9 100644 --- a/native_mate/native_mate/arguments.h +++ b/native_mate/native_mate/arguments.h @@ -27,7 +27,7 @@ class Arguments { template bool GetHolder(T* out) { - return ConvertFromV8(isolate_, info_->Holder(), out); + return mate::ConvertFromV8(isolate_, info_->Holder(), out); } template @@ -53,7 +53,7 @@ class Arguments { return false; } v8::Local val = (*info_)[next_]; - bool success = ConvertFromV8(isolate_, val, out); + bool success = mate::ConvertFromV8(isolate_, val, out); if (success) next_++; return success; diff --git a/native_mate/native_mate/dictionary.h b/native_mate/native_mate/dictionary.h index 2c2a275fb5c7d..b7040652dd905 100644 --- a/native_mate/native_mate/dictionary.h +++ b/native_mate/native_mate/dictionary.h @@ -40,6 +40,12 @@ class Dictionary { static Dictionary CreateEmpty(v8::Isolate* isolate); + bool Has(base::StringPiece key) const { + v8::Local context = isolate_->GetCurrentContext(); + v8::Local v8_key = StringToV8(isolate_, key); + return internal::IsTrue(GetHandle()->Has(context, v8_key)); + } + template bool Get(base::StringPiece key, T* out) const { // Check for existence before getting, otherwise this method will always @@ -102,6 +108,17 @@ class Dictionary { return !result.IsNothing() && result.FromJust(); } + template + bool SetReadOnlyNonConfigurable(base::StringPiece key, T val) { + v8::Local v8_value; + if (!TryConvertToV8(isolate_, val, &v8_value)) + return false; + v8::Maybe result = GetHandle()->DefineOwnProperty( + isolate_->GetCurrentContext(), StringToV8(isolate_, key), v8_value, + static_cast(v8::ReadOnly | v8::DontDelete)); + return !result.IsNothing() && result.FromJust(); + } + template bool SetMethod(base::StringPiece key, const T& callback) { return GetHandle() diff --git a/shell/common/native_mate_converters/blink_converter.cc b/shell/common/native_mate_converters/blink_converter.cc index 8b866b252b283..436620f6ebccd 100644 --- a/shell/common/native_mate_converters/blink_converter.cc +++ b/shell/common/native_mate_converters/blink_converter.cc @@ -6,14 +6,18 @@ #include #include +#include #include #include "base/strings/string_util.h" #include "base/strings/utf_string_conversions.h" #include "content/public/browser/native_web_keyboard_event.h" #include "gin/converter.h" +#include "mojo/public/cpp/base/values_mojom_traits.h" +#include "mojo/public/mojom/base/values.mojom.h" #include "native_mate/dictionary.h" #include "shell/common/keyboard_util.h" +#include "shell/common/native_mate_converters/value_converter.h" #include "third_party/blink/public/platform/web_input_event.h" #include "third_party/blink/public/platform/web_mouse_event.h" #include "third_party/blink/public/platform/web_mouse_wheel_event.h" @@ -527,4 +531,175 @@ bool Converter::FromV8( return true; } +namespace { +constexpr uint8_t kNewSerializationTag = 0; +constexpr uint8_t kOldSerializationTag = 1; + +class V8Serializer : public v8::ValueSerializer::Delegate { + public: + explicit V8Serializer(v8::Isolate* isolate, + bool use_old_serialization = false) + : isolate_(isolate), + serializer_(isolate, this), + use_old_serialization_(use_old_serialization) {} + ~V8Serializer() override = default; + + bool Serialize(v8::Local value, blink::CloneableMessage* out) { + serializer_.WriteHeader(); + if (use_old_serialization_) { + WriteTag(kOldSerializationTag); + if (!WriteBaseValue(value)) { + isolate_->ThrowException( + mate::StringToV8(isolate_, "An object could not be cloned.")); + return false; + } + } else { + WriteTag(kNewSerializationTag); + bool wrote_value; + v8::TryCatch try_catch(isolate_); + if (!serializer_.WriteValue(isolate_->GetCurrentContext(), value) + .To(&wrote_value)) { + try_catch.Reset(); + if (!V8Serializer(isolate_, true).Serialize(value, out)) { + try_catch.ReThrow(); + return false; + } + return true; + } + DCHECK(wrote_value); + } + + std::pair buffer = serializer_.Release(); + DCHECK_EQ(buffer.first, data_.data()); + out->encoded_message = base::make_span(buffer.first, buffer.second); + out->owned_encoded_message = std::move(data_); + + return true; + } + + bool WriteBaseValue(v8::Local object) { + base::Value value; + if (!ConvertFromV8(isolate_, object, &value)) { + return false; + } + mojo::Message message = mojo_base::mojom::Value::SerializeAsMessage(&value); + + serializer_.WriteUint32(message.data_num_bytes()); + serializer_.WriteRawBytes(message.data(), message.data_num_bytes()); + return true; + } + + void WriteTag(uint8_t tag) { serializer_.WriteRawBytes(&tag, 1); } + + // v8::ValueSerializer::Delegate + void* ReallocateBufferMemory(void* old_buffer, + size_t size, + size_t* actual_size) override { + DCHECK_EQ(old_buffer, data_.data()); + data_.resize(size); + *actual_size = data_.capacity(); + return data_.data(); + } + + void FreeBufferMemory(void* buffer) override { + DCHECK_EQ(buffer, data_.data()); + data_ = {}; + } + + void ThrowDataCloneError(v8::Local message) override { + isolate_->ThrowException(v8::Exception::Error(message)); + } + + private: + v8::Isolate* isolate_; + std::vector data_; + v8::ValueSerializer serializer_; + bool use_old_serialization_; +}; + +class V8Deserializer : public v8::ValueDeserializer::Delegate { + public: + V8Deserializer(v8::Isolate* isolate, const blink::CloneableMessage& message) + : isolate_(isolate), + deserializer_(isolate, + message.encoded_message.data(), + message.encoded_message.size(), + this) {} + + v8::Local Deserialize() { + v8::EscapableHandleScope scope(isolate_); + auto context = isolate_->GetCurrentContext(); + bool read_header; + if (!deserializer_.ReadHeader(context).To(&read_header)) + return v8::Null(isolate_); + DCHECK(read_header); + uint8_t tag; + if (!ReadTag(&tag)) + return v8::Null(isolate_); + switch (tag) { + case kNewSerializationTag: { + v8::Local value; + if (!deserializer_.ReadValue(context).ToLocal(&value)) { + return v8::Null(isolate_); + } + return scope.Escape(value); + } + case kOldSerializationTag: { + v8::Local value; + if (!ReadBaseValue(&value)) { + return v8::Null(isolate_); + } + return scope.Escape(value); + } + default: + NOTREACHED() << "Invalid tag: " << tag; + return v8::Null(isolate_); + } + } + + bool ReadTag(uint8_t* tag) { + const void* tag_bytes; + if (!deserializer_.ReadRawBytes(1, &tag_bytes)) + return false; + *tag = *reinterpret_cast(tag_bytes); + return true; + } + + bool ReadBaseValue(v8::Local* value) { + uint32_t length; + const void* data; + if (!deserializer_.ReadUint32(&length) || + !deserializer_.ReadRawBytes(length, &data)) { + return false; + } + mojo::Message message( + base::make_span(reinterpret_cast(data), length), {}); + base::Value out; + if (!mojo_base::mojom::Value::DeserializeFromMessage(std::move(message), + &out)) { + return false; + } + *value = ConvertToV8(isolate_, out); + return true; + } + + private: + v8::Isolate* isolate_; + v8::ValueDeserializer deserializer_; +}; + +} // namespace + +v8::Local Converter::ToV8( + v8::Isolate* isolate, + const blink::CloneableMessage& in) { + return V8Deserializer(isolate, in).Deserialize(); +} + +bool Converter::FromV8(v8::Isolate* isolate, + v8::Handle val, + blink::CloneableMessage* out) { + return V8Serializer(isolate).Serialize(val, out); +} + } // namespace mate diff --git a/shell/common/native_mate_converters/blink_converter.h b/shell/common/native_mate_converters/blink_converter.h index 00b4c513f3aff..6613230bab997 100644 --- a/shell/common/native_mate_converters/blink_converter.h +++ b/shell/common/native_mate_converters/blink_converter.h @@ -6,6 +6,7 @@ #define SHELL_COMMON_NATIVE_MATE_CONVERTERS_BLINK_CONVERTER_H_ #include "native_mate/converter.h" +#include "third_party/blink/public/common/messaging/cloneable_message.h" #include "third_party/blink/public/platform/web_cache.h" #include "third_party/blink/public/platform/web_input_event.h" #include "third_party/blink/public/web/web_context_menu_data.h" @@ -131,6 +132,15 @@ struct Converter { network::mojom::ReferrerPolicy* out); }; +template <> +struct Converter { + static v8::Local ToV8(v8::Isolate* isolate, + const blink::CloneableMessage& in); + static bool FromV8(v8::Isolate* isolate, + v8::Local val, + blink::CloneableMessage* out); +}; + v8::Local EditFlagsToV8(v8::Isolate* isolate, int editFlags); v8::Local MediaFlagsToV8(v8::Isolate* isolate, int mediaFlags); diff --git a/shell/common/node_bindings.cc b/shell/common/node_bindings.cc index 655fac20eceea..892892d1020ef 100644 --- a/shell/common/node_bindings.cc +++ b/shell/common/node_bindings.cc @@ -66,6 +66,7 @@ V(atom_common_screen) \ V(atom_common_shell) \ V(atom_common_v8_util) \ + V(atom_renderer_context_bridge) \ V(atom_renderer_ipc) \ V(atom_renderer_web_frame) diff --git a/shell/common/promise_util.h b/shell/common/promise_util.h index ec43ab02e212b..ae0f35ab6e517 100644 --- a/shell/common/promise_util.h +++ b/shell/common/promise_util.h @@ -119,6 +119,16 @@ class Promise { return GetInner()->Reject(GetContext(), v8::Undefined(isolate())); } + v8::Maybe Reject(v8::Local exception) { + v8::HandleScope handle_scope(isolate()); + v8::MicrotasksScope script_scope(isolate(), + v8::MicrotasksScope::kRunMicrotasks); + v8::Context::Scope context_scope( + v8::Local::New(isolate(), GetContext())); + + return GetInner()->Reject(GetContext(), exception); + } + // Please note that using Then is effectively the same as calling .then // in javascript. This means (a) it is not type safe and (b) please note // it is NOT type safe. diff --git a/shell/renderer/api/atom_api_context_bridge.cc b/shell/renderer/api/atom_api_context_bridge.cc new file mode 100644 index 0000000000000..eab92d8309e2e --- /dev/null +++ b/shell/renderer/api/atom_api_context_bridge.cc @@ -0,0 +1,513 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/renderer/api/atom_api_context_bridge.h" + +#include +#include +#include + +#include "base/no_destructor.h" +#include "base/strings/string_number_conversions.h" +#include "shell/common/api/object_life_monitor.h" +#include "shell/common/native_mate_converters/blink_converter.h" +#include "shell/common/native_mate_converters/callback.h" +#include "shell/common/native_mate_converters/once_callback.h" +#include "shell/common/promise_util.h" + +namespace electron { + +namespace api { + +namespace { + +static int kMaxRecursion = 1000; + +content::RenderFrame* GetRenderFrame(const v8::Local& value) { + v8::Local context = value->CreationContext(); + if (context.IsEmpty()) + return nullptr; + blink::WebLocalFrame* frame = blink::WebLocalFrame::FrameForContext(context); + if (!frame) + return nullptr; + return content::RenderFrame::FromWebFrame(frame); +} + +std::map& +GetStoreMap() { + static base::NoDestructor> + store_map; + return *store_map; +} + +context_bridge::RenderFramePersistenceStore* GetOrCreateStore( + content::RenderFrame* render_frame) { + auto it = GetStoreMap().find(render_frame); + if (it == GetStoreMap().end()) { + auto* store = new context_bridge::RenderFramePersistenceStore(render_frame); + GetStoreMap().emplace(render_frame, store); + return store; + } + return it->second; +} + +// Sourced from "extensions/renderer/v8_schema_registry.cc" +// Recursively freezes every v8 object on |object|. +bool DeepFreeze(const v8::Local& object, + const v8::Local& context, + std::set frozen = std::set()) { + int hash = object->GetIdentityHash(); + if (frozen.find(hash) != frozen.end()) + return true; + frozen.insert(hash); + + v8::Local property_names = + object->GetOwnPropertyNames(context).ToLocalChecked(); + for (uint32_t i = 0; i < property_names->Length(); ++i) { + v8::Local child = + object->Get(context, property_names->Get(context, i).ToLocalChecked()) + .ToLocalChecked(); + if (child->IsObject() && !child->IsTypedArray()) { + if (!DeepFreeze(v8::Local::Cast(child), context, frozen)) + return false; + } + } + return mate::internal::IsTrue( + object->SetIntegrityLevel(context, v8::IntegrityLevel::kFrozen)); +} + +bool IsPlainObject(const v8::Local& object) { + if (!object->IsObject()) + return false; + + return !(object->IsNullOrUndefined() || object->IsDate() || + object->IsArgumentsObject() || object->IsBigIntObject() || + object->IsBooleanObject() || object->IsNumberObject() || + object->IsStringObject() || object->IsSymbolObject() || + object->IsNativeError() || object->IsRegExp() || + object->IsPromise() || object->IsMap() || object->IsSet() || + object->IsMapIterator() || object->IsSetIterator() || + object->IsWeakMap() || object->IsWeakSet() || + object->IsArrayBuffer() || object->IsArrayBufferView() || + object->IsArray() || object->IsDataView() || + object->IsSharedArrayBuffer() || object->IsProxy() || + object->IsWebAssemblyCompiledModule() || + object->IsModuleNamespaceObject()); +} + +bool IsPlainArray(const v8::Local& arr) { + if (!arr->IsArray()) + return false; + + return !arr->IsTypedArray(); +} + +class FunctionLifeMonitor final : public ObjectLifeMonitor { + public: + static void BindTo(v8::Isolate* isolate, + v8::Local target, + context_bridge::RenderFramePersistenceStore* store, + size_t func_id) { + new FunctionLifeMonitor(isolate, target, store, func_id); + } + + protected: + FunctionLifeMonitor(v8::Isolate* isolate, + v8::Local target, + context_bridge::RenderFramePersistenceStore* store, + size_t func_id) + : ObjectLifeMonitor(isolate, target), store_(store), func_id_(func_id) {} + ~FunctionLifeMonitor() override = default; + + void RunDestructor() override { store_->functions().erase(func_id_); } + + private: + context_bridge::RenderFramePersistenceStore* store_; + size_t func_id_; +}; + +} // namespace + +template +v8::Local BindRepeatingFunctionToV8( + v8::Isolate* isolate, + const base::RepeatingCallback& val) { + auto translater = + base::BindRepeating(&mate::internal::NativeFunctionInvoker::Go, val); + return mate::internal::CreateFunctionFromTranslater(isolate, translater, + false); +} + +v8::MaybeLocal PassValueToOtherContext( + v8::Local source_context, + v8::Local destination_context, + v8::Local value, + context_bridge::RenderFramePersistenceStore* store, + int recursion_depth) { + if (recursion_depth >= kMaxRecursion) { + v8::Context::Scope source_scope(source_context); + { + source_context->GetIsolate()->ThrowException(v8::Exception::TypeError( + mate::StringToV8(source_context->GetIsolate(), + "Electron contextBridge recursion depth exceeded. " + "Nested objects " + "deeper than 1000 are not supported."))); + return v8::MaybeLocal(); + } + } + // Check Cache + auto cached_value = store->GetCachedProxiedObject(value); + if (!cached_value.IsEmpty()) { + return cached_value; + } + + // Proxy functions and monitor the lifetime in the new context to release + // the global handle at the right time. + if (value->IsFunction()) { + auto func = v8::Local::Cast(value); + v8::Global global_func(source_context->GetIsolate(), func); + v8::Global global_source(source_context->GetIsolate(), + source_context); + + size_t func_id = store->take_func_id(); + store->functions()[func_id] = + std::make_tuple(std::move(global_func), std::move(global_source)); + v8::Context::Scope destination_scope(destination_context); + { + v8::Local proxy_func = BindRepeatingFunctionToV8( + destination_context->GetIsolate(), + base::BindRepeating(&ProxyFunctionWrapper, store, func_id)); + FunctionLifeMonitor::BindTo(destination_context->GetIsolate(), + v8::Local::Cast(proxy_func), + store, func_id); + store->CacheProxiedObject(value, proxy_func); + return v8::MaybeLocal(proxy_func); + } + } + + // Proxy promises as they have a safe and guaranteed memory lifecycle + if (value->IsPromise()) { + v8::Context::Scope destination_scope(destination_context); + { + auto source_promise = v8::Local::Cast(value); + auto* proxied_promise = + new util::Promise(destination_context->GetIsolate()); + v8::Local proxied_promise_handle = + proxied_promise->GetHandle(); + + auto then_cb = base::BindOnce( + [](util::Promise* proxied_promise, v8::Isolate* isolate, + v8::Global global_source_context, + v8::Global global_destination_context, + context_bridge::RenderFramePersistenceStore* store, + v8::Local result) { + auto val = PassValueToOtherContext( + global_source_context.Get(isolate), + global_destination_context.Get(isolate), result, store, 0); + if (!val.IsEmpty()) + proxied_promise->Resolve(val.ToLocalChecked()); + delete proxied_promise; + }, + proxied_promise, destination_context->GetIsolate(), + v8::Global(source_context->GetIsolate(), source_context), + v8::Global(destination_context->GetIsolate(), + destination_context), + store); + auto catch_cb = base::BindOnce( + [](util::Promise* proxied_promise, v8::Isolate* isolate, + v8::Global global_source_context, + v8::Global global_destination_context, + context_bridge::RenderFramePersistenceStore* store, + v8::Local result) { + auto val = PassValueToOtherContext( + global_source_context.Get(isolate), + global_destination_context.Get(isolate), result, store, 0); + if (!val.IsEmpty()) + proxied_promise->Reject(val.ToLocalChecked()); + delete proxied_promise; + }, + proxied_promise, destination_context->GetIsolate(), + v8::Global(source_context->GetIsolate(), source_context), + v8::Global(destination_context->GetIsolate(), + destination_context), + store); + + ignore_result(source_promise->Then( + source_context, + v8::Local::Cast( + mate::ConvertToV8(destination_context->GetIsolate(), then_cb)), + v8::Local::Cast( + mate::ConvertToV8(destination_context->GetIsolate(), catch_cb)))); + + store->CacheProxiedObject(value, proxied_promise_handle); + return v8::MaybeLocal(proxied_promise_handle); + } + } + + // Errors aren't serializable currently, we need to pull the message out and + // re-construct in the destination context + if (value->IsNativeError()) { + v8::Context::Scope destination_context_scope(destination_context); + return v8::MaybeLocal(v8::Exception::Error( + v8::Exception::CreateMessage(destination_context->GetIsolate(), value) + ->Get())); + } + + // Manually go through the array and pass each value individually into a new + // array so that functions deep inside arrays get proxied or arrays of + // promises are proxied correctly. + if (IsPlainArray(value)) { + v8::Context::Scope destination_context_scope(destination_context); + { + v8::Local arr = v8::Local::Cast(value); + size_t length = arr->Length(); + v8::Local cloned_arr = + v8::Array::New(destination_context->GetIsolate(), length); + for (size_t i = 0; i < length; i++) { + auto value_for_array = PassValueToOtherContext( + source_context, destination_context, + arr->Get(source_context, i).ToLocalChecked(), store, + recursion_depth + 1); + if (value_for_array.IsEmpty()) + return v8::MaybeLocal(); + + if (!mate::internal::IsTrue( + cloned_arr->Set(destination_context, static_cast(i), + value_for_array.ToLocalChecked()))) { + return v8::MaybeLocal(); + } + } + store->CacheProxiedObject(value, cloned_arr); + return v8::MaybeLocal(cloned_arr); + } + } + + // Proxy all objects + if (IsPlainObject(value)) { + auto object_value = v8::Local::Cast(value); + auto passed_value = + CreateProxyForAPI(object_value, source_context, destination_context, + store, recursion_depth + 1); + if (passed_value.IsEmpty()) + return v8::MaybeLocal(); + return v8::MaybeLocal(passed_value.ToLocalChecked()); + } + + // Serializable objects + blink::CloneableMessage ret; + { + v8::Context::Scope source_context_scope(source_context); + { + // V8 serializer will throw an error if required + if (!mate::ConvertFromV8(source_context->GetIsolate(), value, &ret)) + return v8::MaybeLocal(); + } + } + + v8::Context::Scope destination_context_scope(destination_context); + { + v8::Local cloned_value = + mate::ConvertToV8(destination_context->GetIsolate(), ret); + store->CacheProxiedObject(value, cloned_value); + return v8::MaybeLocal(cloned_value); + } +} + +v8::Local ProxyFunctionWrapper( + context_bridge::RenderFramePersistenceStore* store, + size_t func_id, + mate::Arguments* args) { + // Context the proxy function was called from + v8::Local calling_context = args->isolate()->GetCurrentContext(); + // Context the function was created in + v8::Local func_owning_context = + std::get<1>(store->functions()[func_id]).Get(args->isolate()); + + v8::Context::Scope func_owning_context_scope(func_owning_context); + { + v8::Local func = + (std::get<0>(store->functions()[func_id])).Get(args->isolate()); + + std::vector> original_args; + std::vector> proxied_args; + args->GetRemaining(&original_args); + + for (auto value : original_args) { + auto arg = PassValueToOtherContext(calling_context, func_owning_context, + value, store, 0); + if (arg.IsEmpty()) + return v8::Undefined(args->isolate()); + proxied_args.push_back(arg.ToLocalChecked()); + } + + v8::MaybeLocal maybe_return_value; + bool did_error = false; + std::string error_message; + { + v8::TryCatch try_catch(args->isolate()); + maybe_return_value = func->Call(func_owning_context, func, + proxied_args.size(), proxied_args.data()); + if (try_catch.HasCaught()) { + did_error = true; + auto message = try_catch.Message(); + + if (message.IsEmpty() || + !mate::ConvertFromV8(args->isolate(), message->Get(), + &error_message)) { + error_message = + "An unknown exception occurred in the isolated context, an error " + "occurred but a valid exception was not thrown."; + } + } + } + + if (did_error) { + v8::Context::Scope calling_context_scope(calling_context); + { + args->ThrowError(error_message); + return v8::Local(); + } + } + + if (maybe_return_value.IsEmpty()) + return v8::Undefined(args->isolate()); + + auto ret = + PassValueToOtherContext(func_owning_context, calling_context, + maybe_return_value.ToLocalChecked(), store, 0); + if (ret.IsEmpty()) + return v8::Undefined(args->isolate()); + return ret.ToLocalChecked(); + } +} + +v8::MaybeLocal CreateProxyForAPI( + const v8::Local& api_object, + const v8::Local& source_context, + const v8::Local& destination_context, + context_bridge::RenderFramePersistenceStore* store, + int recursion_depth) { + mate::Dictionary api(source_context->GetIsolate(), api_object); + mate::Dictionary proxy = + mate::Dictionary::CreateEmpty(destination_context->GetIsolate()); + store->CacheProxiedObject(api.GetHandle(), proxy.GetHandle()); + auto maybe_keys = api.GetHandle()->GetOwnPropertyNames( + source_context, + static_cast(v8::ONLY_ENUMERABLE | v8::SKIP_SYMBOLS), + v8::KeyConversionMode::kConvertToString); + if (maybe_keys.IsEmpty()) + return v8::MaybeLocal(proxy.GetHandle()); + auto keys = maybe_keys.ToLocalChecked(); + + v8::Context::Scope destination_context_scope(destination_context); + { + uint32_t length = keys->Length(); + std::string key_str; + for (uint32_t i = 0; i < length; i++) { + v8::Local key = + keys->Get(destination_context, i).ToLocalChecked(); + // Try get the key as a string + if (!mate::ConvertFromV8(api.isolate(), key, &key_str)) { + continue; + } + v8::Local value; + if (!api.Get(key_str, &value)) + continue; + + auto passed_value = + PassValueToOtherContext(source_context, destination_context, value, + store, recursion_depth + 1); + if (passed_value.IsEmpty()) + return v8::MaybeLocal(); + proxy.Set(key_str, passed_value.ToLocalChecked()); + } + + return proxy.GetHandle(); + } +} + +#ifdef DCHECK_IS_ON +mate::Dictionary DebugGC(mate::Dictionary empty) { + auto* render_frame = GetRenderFrame(empty.GetHandle()); + auto* store = GetOrCreateStore(render_frame); + mate::Dictionary ret = mate::Dictionary::CreateEmpty(empty.isolate()); + ret.Set("functionCount", store->functions().size()); + auto* proxy_map = store->proxy_map(); + ret.Set("objectCount", proxy_map->size() * 2); + int live_from = 0; + int live_proxy = 0; + for (auto iter = proxy_map->begin(); iter != proxy_map->end(); iter++) { + auto* node = iter->second; + while (node) { + if (!std::get<0>(node->pair).IsEmpty()) + live_from++; + if (!std::get<1>(node->pair).IsEmpty()) + live_proxy++; + node = node->next; + } + } + ret.Set("liveFromValues", live_from); + ret.Set("liveProxyValues", live_proxy); + return ret; +} +#endif + +void ExposeAPIInMainWorld(const std::string& key, + v8::Local api_object, + mate::Arguments* args) { + auto* render_frame = GetRenderFrame(api_object); + CHECK(render_frame); + context_bridge::RenderFramePersistenceStore* store = + GetOrCreateStore(render_frame); + auto* frame = render_frame->GetWebFrame(); + CHECK(frame); + v8::Local main_context = frame->MainWorldScriptContext(); + mate::Dictionary global(main_context->GetIsolate(), main_context->Global()); + + if (global.Has(key)) { + args->ThrowError( + "Cannot bind an API on top of an existing property on the window " + "object"); + return; + } + + v8::Local isolated_context = frame->WorldScriptContext( + args->isolate(), electron::World::ISOLATED_WORLD); + + v8::Context::Scope main_context_scope(main_context); + { + v8::MaybeLocal maybe_proxy = + CreateProxyForAPI(api_object, isolated_context, main_context, store, 0); + if (maybe_proxy.IsEmpty()) + return; + auto proxy = maybe_proxy.ToLocalChecked(); + if (!DeepFreeze(proxy, main_context)) + return; + + global.SetReadOnlyNonConfigurable(key, proxy); + } +} + +} // namespace api + +} // namespace electron + +namespace { + +void Initialize(v8::Local exports, + v8::Local unused, + v8::Local context, + void* priv) { + v8::Isolate* isolate = context->GetIsolate(); + mate::Dictionary dict(isolate, exports); + dict.SetMethod("exposeAPIInMainWorld", &electron::api::ExposeAPIInMainWorld); +#ifdef DCHECK_IS_ON + dict.SetMethod("_debugGCMaps", &electron::api::DebugGC); +#endif +} + +} // namespace + +NODE_LINKED_MODULE_CONTEXT_AWARE(atom_renderer_context_bridge, Initialize) diff --git a/shell/renderer/api/atom_api_context_bridge.h b/shell/renderer/api/atom_api_context_bridge.h new file mode 100644 index 0000000000000..a855e68f6eede --- /dev/null +++ b/shell/renderer/api/atom_api_context_bridge.h @@ -0,0 +1,41 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_ +#define SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_ + +#include +#include +#include + +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "native_mate/converter.h" +#include "native_mate/dictionary.h" +#include "shell/common/node_includes.h" +#include "shell/renderer/api/context_bridge/render_frame_context_bridge_store.h" +#include "shell/renderer/atom_render_frame_observer.h" +#include "third_party/blink/public/web/web_local_frame.h" + +namespace electron { + +namespace api { + +v8::Local ProxyFunctionWrapper( + context_bridge::RenderFramePersistenceStore* store, + size_t func_id, + mate::Arguments* args); + +v8::MaybeLocal CreateProxyForAPI( + const v8::Local& api_object, + const v8::Local& source_context, + const v8::Local& target_context, + context_bridge::RenderFramePersistenceStore* store, + int recursion_depth); + +} // namespace api + +} // namespace electron + +#endif // SHELL_RENDERER_API_ATOM_API_CONTEXT_BRIDGE_H_ diff --git a/shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc b/shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc new file mode 100644 index 0000000000000..2ce094947c90c --- /dev/null +++ b/shell/renderer/api/context_bridge/render_frame_context_bridge_store.cc @@ -0,0 +1,145 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/renderer/api/context_bridge/render_frame_context_bridge_store.h" + +#include + +#include "shell/common/api/object_life_monitor.h" + +namespace electron { + +namespace api { + +namespace context_bridge { + +namespace { + +class CachedProxyLifeMonitor final : public ObjectLifeMonitor { + public: + static void BindTo(v8::Isolate* isolate, + v8::Local target, + RenderFramePersistenceStore* store, + WeakGlobalPairNode* node, + int hash) { + new CachedProxyLifeMonitor(isolate, target, store, node, hash); + } + + protected: + CachedProxyLifeMonitor(v8::Isolate* isolate, + v8::Local target, + RenderFramePersistenceStore* store, + WeakGlobalPairNode* node, + int hash) + : ObjectLifeMonitor(isolate, target), + store_(store), + node_(node), + hash_(hash) {} + + void RunDestructor() override { + if (node_->detached) { + delete node_; + } + if (node_->prev) { + node_->prev->next = node_->next; + } + if (node_->next) { + node_->next->prev = node_->prev; + } + if (!node_->prev && !node_->next) { + // Must be a single length linked list + store_->proxy_map()->erase(hash_); + } + node_->detached = true; + } + + private: + RenderFramePersistenceStore* store_; + WeakGlobalPairNode* node_; + int hash_; +}; + +} // namespace + +WeakGlobalPairNode::WeakGlobalPairNode(WeakGlobalPair pair) { + this->pair = std::move(pair); +} + +WeakGlobalPairNode::~WeakGlobalPairNode() { + if (next) { + delete next; + } +} + +RenderFramePersistenceStore::RenderFramePersistenceStore( + content::RenderFrame* render_frame) + : content::RenderFrameObserver(render_frame) {} + +RenderFramePersistenceStore::~RenderFramePersistenceStore() = default; + +void RenderFramePersistenceStore::OnDestruct() { + delete this; +} + +void RenderFramePersistenceStore::CacheProxiedObject( + v8::Local from, + v8::Local proxy_value) { + if (from->IsObject() && !from->IsNullOrUndefined()) { + auto obj = v8::Local::Cast(from); + int hash = obj->GetIdentityHash(); + auto global_from = v8::Global(v8::Isolate::GetCurrent(), from); + auto global_proxy = + v8::Global(v8::Isolate::GetCurrent(), proxy_value); + // Do not retain + global_from.SetWeak(); + global_proxy.SetWeak(); + auto iter = proxy_map_.find(hash); + auto* node = new WeakGlobalPairNode( + std::make_tuple(std::move(global_from), std::move(global_proxy))); + CachedProxyLifeMonitor::BindTo(v8::Isolate::GetCurrent(), obj, this, node, + hash); + CachedProxyLifeMonitor::BindTo(v8::Isolate::GetCurrent(), + v8::Local::Cast(proxy_value), + this, node, hash); + if (iter == proxy_map_.end()) { + proxy_map_.emplace(hash, node); + } else { + WeakGlobalPairNode* target = iter->second; + while (target->next) { + target = target->next; + } + target->next = node; + node->prev = target; + } + } +} + +v8::MaybeLocal RenderFramePersistenceStore::GetCachedProxiedObject( + v8::Local from) { + if (!from->IsObject() || from->IsNullOrUndefined()) + return v8::MaybeLocal(); + + auto obj = v8::Local::Cast(from); + int hash = obj->GetIdentityHash(); + auto iter = proxy_map_.find(hash); + if (iter == proxy_map_.end()) + return v8::MaybeLocal(); + WeakGlobalPairNode* target = iter->second; + while (target) { + auto from_cmp = std::get<0>(target->pair).Get(v8::Isolate::GetCurrent()); + if (from_cmp == from) { + if (std::get<1>(target->pair).IsEmpty()) + return v8::MaybeLocal(); + return std::get<1>(target->pair).Get(v8::Isolate::GetCurrent()); + } + target = target->next; + } + return v8::MaybeLocal(); +} + +} // namespace context_bridge + +} // namespace api + +} // namespace electron diff --git a/shell/renderer/api/context_bridge/render_frame_context_bridge_store.h b/shell/renderer/api/context_bridge/render_frame_context_bridge_store.h new file mode 100644 index 0000000000000..892837b6d83a3 --- /dev/null +++ b/shell/renderer/api/context_bridge/render_frame_context_bridge_store.h @@ -0,0 +1,71 @@ +// Copyright (c) 2019 Slack Technologies, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_ +#define SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_ + +#include +#include + +#include "content/public/renderer/render_frame.h" +#include "content/public/renderer/render_frame_observer.h" +#include "shell/renderer/atom_render_frame_observer.h" +#include "third_party/blink/public/web/web_local_frame.h" + +namespace electron { + +namespace api { + +namespace context_bridge { + +using FunctionContextPair = + std::tuple, v8::Global>; + +using WeakGlobalPair = std::tuple, v8::Global>; + +struct WeakGlobalPairNode { + explicit WeakGlobalPairNode(WeakGlobalPair pair_); + ~WeakGlobalPairNode(); + WeakGlobalPair pair; + bool detached = false; + struct WeakGlobalPairNode* prev = nullptr; + struct WeakGlobalPairNode* next = nullptr; +}; + +class RenderFramePersistenceStore final : public content::RenderFrameObserver { + public: + explicit RenderFramePersistenceStore(content::RenderFrame* render_frame); + ~RenderFramePersistenceStore() override; + + // RenderFrameObserver implementation. + void OnDestruct() override; + + size_t take_func_id() { return next_func_id_++; } + + std::map& functions() { return functions_; } + std::map* proxy_map() { return &proxy_map_; } + + void CacheProxiedObject(v8::Local from, + v8::Local proxy_value); + v8::MaybeLocal GetCachedProxiedObject(v8::Local from); + + private: + // func_id ==> { function, owning_context } + std::map functions_; + size_t next_func_id_ = 1; + + // proxy maps are weak globals, i.e. these are not retained beyond + // there normal JS lifetime. You must check IsEmpty() + + // object_identity ==> [from_value, proxy_value] + std::map proxy_map_; +}; + +} // namespace context_bridge + +} // namespace api + +} // namespace electron + +#endif // SHELL_RENDERER_API_CONTEXT_BRIDGE_RENDER_FRAME_CONTEXT_BRIDGE_STORE_H_ diff --git a/spec-main/api-context-bridge-spec.ts b/spec-main/api-context-bridge-spec.ts new file mode 100644 index 0000000000000..1a424331444b8 --- /dev/null +++ b/spec-main/api-context-bridge-spec.ts @@ -0,0 +1,680 @@ +import { contextBridge, BrowserWindow, ipcMain } from 'electron' +import { expect } from 'chai' +import * as fs from 'fs-extra' +import * as os from 'os' +import * as path from 'path' + +import { closeWindow } from './window-helpers' +import { emittedOnce } from './events-helpers' + +const fixturesPath = path.resolve(__dirname, 'fixtures', 'api', 'context-bridge') + +describe('contextBridge', () => { + let w: BrowserWindow + let dir: string + + afterEach(async () => { + await closeWindow(w) + if (dir) await fs.remove(dir) + }) + + it('should not be accessible when contextIsolation is disabled', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: false, + preload: path.resolve(fixturesPath, 'can-bind-preload.js') + } + }) + const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html'))) + expect(bound).to.equal(false) + }) + + it('should be accessible when contextIsolation is enabled', async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + preload: path.resolve(fixturesPath, 'can-bind-preload.js') + } + }) + const [,bound] = await emittedOnce(ipcMain, 'context-bridge-bound', () => w.loadFile(path.resolve(fixturesPath, 'empty.html'))) + expect(bound).to.equal(true) + }) + + const generateTests = (useSandbox: boolean) => { + describe(`with sandbox=${useSandbox}`, () => { + const makeBindingWindow = async (bindingCreator: Function) => { + const preloadContent = `const electron_1 = require('electron'); + ${useSandbox ? '' : `require('v8').setFlagsFromString('--expose_gc'); + const gc=require('vm').runInNewContext('gc'); + electron_1.contextBridge.exposeInMainWorld('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) + w = new BrowserWindow({ + show: false, + webPreferences: { + contextIsolation: true, + nodeIntegration: true, + sandbox: useSandbox, + preload: path.resolve(tmpDir, 'preload.js') + } + }) + await w.loadFile(path.resolve(fixturesPath, 'empty.html')) + } + + const callWithBindings = async (fn: Function) => { + return await w.webContents.executeJavaScript(`(${fn.toString()})(window)`) + } + + const getGCInfo = async (): Promise<{ + functionCount: number + objectCount: number + liveFromValues: number + liveProxyValues: number + }> => { + const [,info] = await emittedOnce(ipcMain, 'gc-info', () => w.webContents.send('get-gc-info')) + return info + } + + it('should proxy numbers', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myNumber: 123, + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.myNumber + }) + expect(result).to.equal(123) + }) + + it('should make properties unwriteable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myNumber: 123, + }) + }) + const result = await callWithBindings((root: any) => { + root.example.myNumber = 456 + return root.example.myNumber + }) + expect(result).to.equal(123) + }) + + it('should proxy strings', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myString: 'my-words', + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.myString + }) + expect(result).to.equal('my-words') + }) + + it('should proxy arrays', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myArr: [123, 'my-words'], + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.myArr + }) + expect(result).to.deep.equal([123, 'my-words']) + }) + + it('should make arrays immutable', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myArr: [123, 'my-words'], + }) + }) + const immutable = await callWithBindings((root: any) => { + try { + root.example.myArr.push(456) + return false + } catch { + return true + } + }) + expect(immutable).to.equal(true) + }) + + it('should proxy booleans', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myBool: true, + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.myBool + }) + expect(result).to.equal(true) + }) + + it('should proxy promises and resolve with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: Promise.resolve('i-resolved'), + }) + }) + const result = await callWithBindings(async (root: any) => { + return await root.example.myPromise + }) + expect(result).to.equal('i-resolved') + }) + + it('should proxy promises and reject with the correct value', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: Promise.reject('i-rejected'), + }) + }) + const result = await callWithBindings(async (root: any) => { + try { + await root.example.myPromise + return null + } catch (err) { + return err + } + }) + expect(result).to.equal('i-rejected') + }) + + it('should proxy promises and resolve with the correct value if it resolves later', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: () => new Promise(r => setTimeout(() => r('delayed'), 20)), + }) + }) + const result = await callWithBindings(async (root: any) => { + return await root.example.myPromise() + }) + expect(result).to.equal('delayed') + }) + + it('should proxy nested promises correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + myPromise: () => new Promise(r => setTimeout(() => r(Promise.resolve(123)), 20)), + }) + }) + const result = await callWithBindings(async (root: any) => { + return await root.example.myPromise() + }) + expect(result).to.equal(123) + }) + + it('should proxy methods', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getNumber: () => 123, + getString: () => 'help', + getBoolean: () => false, + getPromise: async () => 'promise' + }) + }) + const result = await callWithBindings(async (root: any) => { + return [root.example.getNumber(), root.example.getString(), root.example.getBoolean(), await root.example.getPromise()] + }) + expect(result).to.deep.equal([123, 'help', false, 'promise']) + }) + + it('should proxy methods that are callable multiple times', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + doThing: () => 123 + }) + }) + const result = await callWithBindings(async (root: any) => { + return [root.example.doThing(), root.example.doThing(), root.example.doThing()] + }) + expect(result).to.deep.equal([123, 123, 123]) + }) + + it('should proxy methods in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + callWithNumber: (fn: any) => fn(123), + }) + }) + const result = await callWithBindings(async (root: any) => { + return root.example.callWithNumber((n: number) => n + 1) + }) + expect(result).to.equal(124) + }) + + it('should proxy promises in the reverse direction', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + getPromiseValue: async (p: Promise) => await p, + }) + }) + const result = await callWithBindings(async (root: any) => { + return await root.example.getPromiseValue(Promise.resolve('my-proxied-value')) + }) + expect(result).to.equal('my-proxied-value') + }) + + it('should proxy objects with number keys', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + [1]: 123, + [2]: 456, + '3': 789 + }) + }) + const result = await callWithBindings(async (root: any) => { + return [root.example[1], root.example[2], root.example[3], Array.isArray(root.example)] + }) + expect(result).to.deep.equal([123, 456, 789, false]) + }) + + it('it should proxy null and undefined correctly', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + values: [null, undefined] + }) + }) + const result = await callWithBindings((root: any) => { + // Convert to strings as although the context bridge keeps the right value + // IPC does not + return root.example.values.map((val: any) => `${val}`) + }) + expect(result).to.deep.equal(['null', 'undefined']) + }) + + it('should proxy typed arrays and regexps through the serializer', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + arr: new Uint8Array(100), + regexp: /a/g + }) + }) + const result = await callWithBindings((root: any) => { + return [root.example.arr.__proto__ === Uint8Array.prototype, root.example.regexp.__proto__ === RegExp.prototype] + }) + expect(result).to.deep.equal([true, true]) + }) + + it('it should handle recursive objects', async () => { + await makeBindingWindow(() => { + const o: any = { value: 135 } + o.o = o + contextBridge.exposeInMainWorld('example', { + o, + }) + }) + const result = await callWithBindings((root: any) => { + return [root.example.o.value, root.example.o.o.value, root.example.o.o.o.value] + }) + expect(result).to.deep.equal([135, 135, 135]) + }) + + it('it should follow expected simple rules of object identity', async () => { + await makeBindingWindow(() => { + const o: any = { value: 135 } + const sub = { thing: 7 } + o.a = sub + o.b = sub + contextBridge.exposeInMainWorld('example', { + o, + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.a === root.example.b + }) + expect(result).to.equal(true) + }) + + it('it should follow expected complex rules of object identity', async () => { + await makeBindingWindow(() => { + let first: any = null + contextBridge.exposeInMainWorld('example', { + check: (arg: any) => { + if (first === null) { + first = arg + } else { + return first === arg + } + }, + }) + }) + const result = await callWithBindings((root: any) => { + const o = { thing: 123 } + root.example.check(o) + return root.example.check(o) + }) + expect(result).to.equal(true) + }) + + // Can only run tests which use the GCRunner in non-sandboxed environments + if (!useSandbox) { + it('should release the global hold on methods sent across contexts', async () => { + await makeBindingWindow(() => { + require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC())) + contextBridge.exposeInMainWorld('example', { + getFunction: () => () => 123 + }) + }) + expect((await getGCInfo()).functionCount).to.equal(2) + await callWithBindings(async (root: any) => { + root.x = [root.example.getFunction()] + }) + expect((await getGCInfo()).functionCount).to.equal(3) + await callWithBindings(async (root: any) => { + root.x = [] + root.GCRunner.run() + }) + expect((await getGCInfo()).functionCount).to.equal(2) + }) + + it('should release the global hold on objects sent across contexts when the object proxy is de-reffed', async () => { + await makeBindingWindow(() => { + require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC())) + let myObj: any + contextBridge.exposeInMainWorld('example', { + setObj: (o: any) => { + myObj = o + }, + getObj: () => myObj + }) + }) + await callWithBindings(async (root: any) => { + root.GCRunner.run() + }) + // Initial Setup + let info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + + // Create Reference + await callWithBindings(async (root: any) => { + root.x = { value: 123 } + root.example.setObj(root.x) + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(4) + expect(info.liveProxyValues).to.equal(4) + expect(info.objectCount).to.equal(8) + + // Release Reference + await callWithBindings(async (root: any) => { + root.example.setObj(null) + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + }) + + it('should release the global hold on objects sent across contexts when the object source is de-reffed', async () => { + await makeBindingWindow(() => { + require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC())) + let myObj: any; + contextBridge.exposeInMainWorld('example', { + setObj: (o: any) => { + myObj = o + }, + getObj: () => myObj + }) + }) + await callWithBindings(async (root: any) => { + root.GCRunner.run() + }) + // Initial Setup + let info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + + // Create Reference + await callWithBindings(async (root: any) => { + root.x = { value: 123 } + root.example.setObj(root.x) + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(4) + expect(info.liveProxyValues).to.equal(4) + expect(info.objectCount).to.equal(8) + + // Release Reference + await callWithBindings(async (root: any) => { + delete root.x + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + }) + + it('should not crash when the object source is de-reffed AND the object proxy is de-reffed', async () => { + await makeBindingWindow(() => { + require('electron').ipcRenderer.on('get-gc-info', e => e.sender.send('gc-info', (contextBridge as any).debugGC())) + let myObj: any; + contextBridge.exposeInMainWorld('example', { + setObj: (o: any) => { + myObj = o + }, + getObj: () => myObj + }) + }) + await callWithBindings(async (root: any) => { + root.GCRunner.run() + }) + // Initial Setup + let info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + + // Create Reference + await callWithBindings(async (root: any) => { + root.x = { value: 123 } + root.example.setObj(root.x) + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(4) + expect(info.liveProxyValues).to.equal(4) + expect(info.objectCount).to.equal(8) + + // Release Reference + await callWithBindings(async (root: any) => { + delete root.x + root.example.setObj(null) + root.GCRunner.run() + }) + info = await getGCInfo() + expect(info.liveFromValues).to.equal(3) + expect(info.liveProxyValues).to.equal(3) + expect(info.objectCount).to.equal(6) + }) + } + + it('it should not let you overwrite existing exposed things', async () => { + await makeBindingWindow(() => { + let threw = false + contextBridge.exposeInMainWorld('example', { + attempt: 1, + getThrew: () => threw + }) + try { + contextBridge.exposeInMainWorld('example', { + attempt: 2, + getThrew: () => threw + }) + } catch { + threw = true + } + }) + const result = await callWithBindings((root: any) => { + return [root.example.attempt, root.example.getThrew()] + }) + expect(result).to.deep.equal([1, true]) + }) + + it('should work with complex nested methods and promises', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + first: (second: Function) => second(async (fourth: Function) => { + return await fourth() + }) + }) + }) + const result = await callWithBindings((root: any) => { + return root.example.first((third: Function) => { + return third(() => Promise.resolve('final value')) + }) + }) + expect(result).to.equal('final value') + }) + + it('should throw an error when recursion depth is exceeded', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + doThing: (a: any) => console.log(a) + }) + }) + let threw = await callWithBindings((root: any) => { + try { + let a: any = [] + for (let i = 0; i < 999; i++) { + a = [ a ] + } + root.example.doThing(a) + return false + } catch { + return true + } + }) + expect(threw).to.equal(false) + threw = await callWithBindings((root: any) => { + try { + let a: any = [] + for (let i = 0; i < 1000; i++) { + a = [ a ] + } + root.example.doThing(a) + return false + } catch { + return true + } + }) + expect(threw).to.equal(true) + }) + + it('should not leak prototypes', async () => { + await makeBindingWindow(() => { + contextBridge.exposeInMainWorld('example', { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getNumber: () => 123, + getString: () => 'string', + getBoolean: () => true, + getArr: () => [123, 'string', true, ['foo']], + getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}), + getFunctionFromFunction: async () => () => null, + object: { + number: 123, + string: 'string', + boolean: true, + arr: [123, 'string', true, ['foo']], + getPromise: async () => ({ number: 123, string: 'string', boolean: true, fn: () => 'string', arr: [123, 'string', true, ['foo']]}), + }, + receiveArguments: (fn: any) => fn({ key: 'value' }) + }) + }) + const result = await callWithBindings(async (root: any) => { + const { example } = root + let arg: any + example.receiveArguments((o: any) => { arg = o }) + const protoChecks = [ + [example, Object], + [example.number, Number], + [example.string, String], + [example.boolean, Boolean], + [example.arr, Array], + [example.arr[0], Number], + [example.arr[1], String], + [example.arr[2], Boolean], + [example.arr[3], Array], + [example.arr[3][0], String], + [example.getNumber, Function], + [example.getNumber(), Number], + [example.getString(), String], + [example.getBoolean(), Boolean], + [example.getArr(), Array], + [example.getArr()[0], Number], + [example.getArr()[1], String], + [example.getArr()[2], Boolean], + [example.getArr()[3], Array], + [example.getArr()[3][0], String], + [example.getFunctionFromFunction, Function], + [example.getFunctionFromFunction(), Promise], + [await example.getFunctionFromFunction(), Function], + [example.getPromise(), Promise], + [await example.getPromise(), Object], + [(await example.getPromise()).number, Number], + [(await example.getPromise()).string, String], + [(await example.getPromise()).boolean, Boolean], + [(await example.getPromise()).fn, Function], + [(await example.getPromise()).fn(), String], + [(await example.getPromise()).arr, Array], + [(await example.getPromise()).arr[0], Number], + [(await example.getPromise()).arr[1], String], + [(await example.getPromise()).arr[2], Boolean], + [(await example.getPromise()).arr[3], Array], + [(await example.getPromise()).arr[3][0], String], + [example.object, Object], + [example.object.number, Number], + [example.object.string, String], + [example.object.boolean, Boolean], + [example.object.arr, Array], + [example.object.arr[0], Number], + [example.object.arr[1], String], + [example.object.arr[2], Boolean], + [example.object.arr[3], Array], + [example.object.arr[3][0], String], + [await example.object.getPromise(), Object], + [(await example.object.getPromise()).number, Number], + [(await example.object.getPromise()).string, String], + [(await example.object.getPromise()).boolean, Boolean], + [(await example.object.getPromise()).fn, Function], + [(await example.object.getPromise()).fn(), String], + [(await example.object.getPromise()).arr, Array], + [(await example.object.getPromise()).arr[0], Number], + [(await example.object.getPromise()).arr[1], String], + [(await example.object.getPromise()).arr[2], Boolean], + [(await example.object.getPromise()).arr[3], Array], + [(await example.object.getPromise()).arr[3][0], String], + [arg, Object], + [arg.key, String] + ] + return { + protoMatches: protoChecks.map(([a, Constructor]) => a.__proto__ === Constructor.prototype) + } + }) + // Every protomatch should be true + expect(result.protoMatches).to.deep.equal(result.protoMatches.map(() => true)) + }) + }) + } + + generateTests(true) + generateTests(false) +}) diff --git a/spec-main/fixtures/api/context-bridge/can-bind-preload.js b/spec-main/fixtures/api/context-bridge/can-bind-preload.js new file mode 100644 index 0000000000000..cc0204037bdb2 --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/can-bind-preload.js @@ -0,0 +1,13 @@ +const { contextBridge, ipcRenderer } = require('electron') + +console.info(contextBridge) + +let bound = false +try { + contextBridge.exposeInMainWorld('test', {}) + bound = true +} catch { + // Ignore +} + +ipcRenderer.send('context-bridge-bound', bound) diff --git a/spec-main/fixtures/api/context-bridge/empty.html b/spec-main/fixtures/api/context-bridge/empty.html new file mode 100644 index 0000000000000..6c70bcfe4d48d --- /dev/null +++ b/spec-main/fixtures/api/context-bridge/empty.html @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/spec/asar-spec.js b/spec/asar-spec.js index 5b6daab470fe4..53b0bc47fd0fc 100644 --- a/spec/asar-spec.js +++ b/spec/asar-spec.js @@ -43,7 +43,7 @@ describe('asar package', function () { it('does not leak fd', function () { let readCalls = 1 while (readCalls <= 10000) { - fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'index.js')) + fs.readFileSync(path.join(process.resourcesPath, 'default_app.asar', 'main.js')) readCalls++ } })