From fd63510ca94af9e105dd61cae1ffa56acd110e1f Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Mon, 28 Sep 2020 12:22:03 -0400 Subject: [PATCH] feat: add serial api support (#25237) * feat: add serial api support resolves #22478 * Put serial port support behind a flag and mark as experimental * Update docs/api/session.md Co-authored-by: Jeremy Rose * Use enable-blink-features=Serial instead of enable-experimental-web-platform-features * Set enableBlinkFeatures on webPreferences instead of commandline Co-authored-by: Jeremy Rose --- docs/api/session.md | 72 ++++++- docs/api/structures/serial-port.md | 8 + filenames.auto.gni | 1 + filenames.gni | 8 + shell/browser/electron_browser_client.cc | 7 + shell/browser/electron_browser_client.h | 4 + .../serial/electron_serial_delegate.cc | 121 ++++++++++++ .../browser/serial/electron_serial_delegate.h | 60 ++++++ .../browser/serial/serial_chooser_context.cc | 162 ++++++++++++++++ shell/browser/serial/serial_chooser_context.h | 92 +++++++++ .../serial/serial_chooser_context_factory.cc | 41 ++++ .../serial/serial_chooser_context_factory.h | 40 ++++ .../serial/serial_chooser_controller.cc | 181 ++++++++++++++++++ .../serial/serial_chooser_controller.h | 79 ++++++++ .../browser/web_contents_permission_helper.cc | 8 + .../browser/web_contents_permission_helper.h | 2 + .../gin_converters/content_converter.cc | 2 + spec-main/chromium-spec.ts | 48 +++++ spec-main/index.js | 1 + 19 files changed, 936 insertions(+), 1 deletion(-) create mode 100644 docs/api/structures/serial-port.md create mode 100644 shell/browser/serial/electron_serial_delegate.cc create mode 100644 shell/browser/serial/electron_serial_delegate.h create mode 100644 shell/browser/serial/serial_chooser_context.cc create mode 100644 shell/browser/serial/serial_chooser_context.h create mode 100644 shell/browser/serial/serial_chooser_context_factory.cc create mode 100644 shell/browser/serial/serial_chooser_context_factory.h create mode 100644 shell/browser/serial/serial_chooser_controller.cc create mode 100644 shell/browser/serial/serial_chooser_controller.h diff --git a/docs/api/session.md b/docs/api/session.md index 0e77b2e754a6..390d750a5445 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -178,6 +178,76 @@ Emitted when a hunspell dictionary file download fails. For details on the failure you should collect a netlog and inspect the download request. +#### Event: 'select-serial-port' _Experimental_ + +Returns: + +* `event` Event +* `portList` [SerialPort[]](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) +* `callback` Function + * `portId` String + +Emitted when a serial port needs to be selected when a call to +`navigator.serial.requestPort` is made. `callback` should be called with +`portId` to be selected, passing an empty string to `callback` will +cancel the request. Additionally, permissioning on `navigator.serial` can +be managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler) +with the `serial` permission. + +Because this is an experimental feature it is disabled by default. To enable this feature, you +will need to use the `--enable-features=ElectronSerialChooser` command line switch. Additionally +because this is an experimental Chromium feature you will need to set `enableBlinkFeatures: 'Serial'` +on the `webPreferences` property when opening a BrowserWindow. + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null +app.commandLine.appendSwitch('enable-features', 'ElectronSerialChooser') + +app.whenReady().then(() => { + win = new BrowserWindow({ + width: 800, + height: 600, + webPreferences: { + enableBlinkFeatures: 'Serial' + } + }) + win.webContents.session.on('select-serial-port', (event, portList, callback) => { + event.preventDefault() + const selectedPort = portList.find((device) => { + return device.vendorId === 0x2341 && device.productId === 0x0043 + }) + if (!selectedPort) { + callback('') + } else { + callback(result1.portId) + } + }) +}) +``` + +#### Event: 'serial-port-added' _Experimental_ + +Returns: + +* `event` Event +* `port` [SerialPort](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) + +Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a new serial port becomes available. For example, this event will fire when a new USB device is plugged in. + +#### Event: 'serial-port-removed' _Experimental_ + +Returns: + +* `event` Event +* `port` [SerialPort](structures/serial-port.md) +* `webContents` [WebContents](web-contents.md) + +Emitted after `navigator.serial.requestPort` has been called and `select-serial-port` has fired if a serial port has been removed. For example, this event will fire when a USB device is unplugged. + ### Instance Methods The following methods are available on instances of `Session`: @@ -420,7 +490,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents * `handler` Function | null * `webContents` [WebContents](web-contents.md) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. - * `permission` String - Enum of 'media'. + * `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`. * `requestingOrigin` String - The origin URL of the permission check * `details` Object - Some properties are only available on certain permission types. * `securityOrigin` String - The security origin of the `media` check. diff --git a/docs/api/structures/serial-port.md b/docs/api/structures/serial-port.md new file mode 100644 index 000000000000..a6b0e129fd59 --- /dev/null +++ b/docs/api/structures/serial-port.md @@ -0,0 +1,8 @@ +# SerialPort Object + +* `portId` String - Unique identifier for the port +* `portName` String - Name of the port +* `displayName` String - Addtional information for the port +* `persistentId` String - This platform-specific identifier, if present, can be used to identify the device across restarts of the application and operating system. +* `vendorId` String - Optional USB vendor ID +* `productId` String - Optional USB product ID diff --git a/filenames.auto.gni b/filenames.auto.gni index 905fcb9e8a4c..73aa6408e933 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -115,6 +115,7 @@ auto_filenames = { "docs/api/structures/referrer.md", "docs/api/structures/scrubber-item.md", "docs/api/structures/segmented-control-segment.md", + "docs/api/structures/serial-port.md", "docs/api/structures/service-worker-info.md", "docs/api/structures/shared-worker-info.md", "docs/api/structures/shortcut-details.md", diff --git a/filenames.gni b/filenames.gni index 65029260dbd5..0fdc527744c6 100644 --- a/filenames.gni +++ b/filenames.gni @@ -299,6 +299,14 @@ filenames = { "shell/browser/relauncher_linux.cc", "shell/browser/relauncher_mac.cc", "shell/browser/relauncher_win.cc", + "shell/browser/serial/electron_serial_delegate.cc", + "shell/browser/serial/electron_serial_delegate.h", + "shell/browser/serial/serial_chooser_context.cc", + "shell/browser/serial/serial_chooser_context.h", + "shell/browser/serial/serial_chooser_context_factory.cc", + "shell/browser/serial/serial_chooser_context_factory.h", + "shell/browser/serial/serial_chooser_controller.cc", + "shell/browser/serial/serial_chooser_controller.h", "shell/browser/session_preferences.cc", "shell/browser/session_preferences.h", "shell/browser/special_storage_policy.cc", diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 2dc6c3a03bb8..890f551a3500 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -86,6 +86,7 @@ #include "shell/browser/notifications/notification_presenter.h" #include "shell/browser/notifications/platform_notification_service.h" #include "shell/browser/protocol_registry.h" +#include "shell/browser/serial/electron_serial_delegate.h" #include "shell/browser/session_preferences.h" #include "shell/browser/ui/devtools_manager_delegate.h" #include "shell/browser/web_contents_permission_helper.h" @@ -1747,4 +1748,10 @@ ElectronBrowserClient::GetPluginMimeTypesWithExternalHandlers( return mime_types; } +content::SerialDelegate* ElectronBrowserClient::GetSerialDelegate() { + if (!serial_delegate_) + serial_delegate_ = std::make_unique(); + return serial_delegate_.get(); +} + } // namespace electron diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h index 9b02d4b817e0..b9d4ea983143 100644 --- a/shell/browser/electron_browser_client.h +++ b/shell/browser/electron_browser_client.h @@ -18,6 +18,7 @@ #include "content/public/browser/web_contents.h" #include "electron/buildflags/buildflags.h" #include "net/ssl/client_cert_identity.h" +#include "shell/browser/serial/electron_serial_delegate.h" namespace content { class ClientCertificateDelegate; @@ -82,6 +83,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient, void SetCanUseCustomSiteInstance(bool should_disable); bool CanUseCustomSiteInstance() override; + content::SerialDelegate* GetSerialDelegate() override; protected: void RenderProcessWillLaunch(content::RenderProcessHost* host) override; @@ -335,6 +337,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient, // ProxyingWebSocket classes. uint64_t next_id_ = 0; + std::unique_ptr serial_delegate_; + DISALLOW_COPY_AND_ASSIGN(ElectronBrowserClient); }; diff --git a/shell/browser/serial/electron_serial_delegate.cc b/shell/browser/serial/electron_serial_delegate.cc new file mode 100644 index 000000000000..468148a07d28 --- /dev/null +++ b/shell/browser/serial/electron_serial_delegate.cc @@ -0,0 +1,121 @@ +// Copyright (c) 2020 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/serial/electron_serial_delegate.h" + +#include + +#include "base/feature_list.h" +#include "content/public/browser/web_contents.h" +#include "shell/browser/api/electron_api_web_contents.h" +#include "shell/browser/serial/serial_chooser_context.h" +#include "shell/browser/serial/serial_chooser_context_factory.h" +#include "shell/browser/serial/serial_chooser_controller.h" +#include "shell/browser/web_contents_permission_helper.h" + +namespace features { + +const base::Feature kElectronSerialChooser{"ElectronSerialChooser", + base::FEATURE_DISABLED_BY_DEFAULT}; +} + +namespace electron { + +SerialChooserContext* GetChooserContext(content::RenderFrameHost* frame) { + auto* web_contents = content::WebContents::FromRenderFrameHost(frame); + auto* browser_context = web_contents->GetBrowserContext(); + return SerialChooserContextFactory::GetForBrowserContext(browser_context); +} + +ElectronSerialDelegate::ElectronSerialDelegate() = default; + +ElectronSerialDelegate::~ElectronSerialDelegate() = default; + +std::unique_ptr ElectronSerialDelegate::RunChooser( + content::RenderFrameHost* frame, + std::vector filters, + content::SerialChooser::Callback callback) { + if (base::FeatureList::IsEnabled(features::kElectronSerialChooser)) { + SerialChooserController* controller = ControllerForFrame(frame); + if (controller) { + DeleteControllerForFrame(frame); + } + AddControllerForFrame(frame, std::move(filters), std::move(callback)); + } else { + // If feature is disabled, immediately return back with no port selected. + std::move(callback).Run(nullptr); + } + + // Return a nullptr because the return value isn't used for anything, eg + // there is no mechanism to cancel navigator.serial.requestPort(). The return + // value is simply used in Chromium to cleanup the chooser UI once the serial + // service is destroyed. + return nullptr; +} + +bool ElectronSerialDelegate::CanRequestPortPermission( + content::RenderFrameHost* frame) { + auto* web_contents = content::WebContents::FromRenderFrameHost(frame); + auto* permission_helper = + WebContentsPermissionHelper::FromWebContents(web_contents); + return permission_helper->CheckSerialAccessPermission( + web_contents->GetMainFrame()->GetLastCommittedOrigin()); +} + +bool ElectronSerialDelegate::HasPortPermission( + content::RenderFrameHost* frame, + const device::mojom::SerialPortInfo& port) { + auto* web_contents = content::WebContents::FromRenderFrameHost(frame); + auto* browser_context = web_contents->GetBrowserContext(); + auto* chooser_context = + SerialChooserContextFactory::GetForBrowserContext(browser_context); + return chooser_context->HasPortPermission( + frame->GetLastCommittedOrigin(), + web_contents->GetMainFrame()->GetLastCommittedOrigin(), port); +} + +device::mojom::SerialPortManager* ElectronSerialDelegate::GetPortManager( + content::RenderFrameHost* frame) { + return GetChooserContext(frame)->GetPortManager(); +} + +void ElectronSerialDelegate::AddObserver(content::RenderFrameHost* frame, + Observer* observer) { + return GetChooserContext(frame)->AddPortObserver(observer); +} + +void ElectronSerialDelegate::RemoveObserver(content::RenderFrameHost* frame, + Observer* observer) { + SerialChooserContext* serial_chooser_context = GetChooserContext(frame); + if (serial_chooser_context) { + return serial_chooser_context->RemovePortObserver(observer); + } +} + +SerialChooserController* ElectronSerialDelegate::ControllerForFrame( + content::RenderFrameHost* render_frame_host) { + auto mapping = controller_map_.find(render_frame_host); + return mapping == controller_map_.end() ? nullptr : mapping->second.get(); +} + +SerialChooserController* ElectronSerialDelegate::AddControllerForFrame( + content::RenderFrameHost* render_frame_host, + std::vector filters, + content::SerialChooser::Callback callback) { + auto* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + auto controller = std::make_unique( + render_frame_host, std::move(filters), std::move(callback), web_contents, + weak_factory_.GetWeakPtr()); + controller_map_.insert( + std::make_pair(render_frame_host, std::move(controller))); + return ControllerForFrame(render_frame_host); +} + +void ElectronSerialDelegate::DeleteControllerForFrame( + content::RenderFrameHost* render_frame_host) { + controller_map_.erase(render_frame_host); +} + +} // namespace electron diff --git a/shell/browser/serial/electron_serial_delegate.h b/shell/browser/serial/electron_serial_delegate.h new file mode 100644 index 000000000000..5b73100cb1f2 --- /dev/null +++ b/shell/browser/serial/electron_serial_delegate.h @@ -0,0 +1,60 @@ +// Copyright (c) 2020 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_ +#define SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_ + +#include +#include +#include + +#include "base/memory/weak_ptr.h" +#include "content/public/browser/serial_delegate.h" +#include "shell/browser/serial/serial_chooser_controller.h" + +namespace electron { + +class SerialChooserController; + +class ElectronSerialDelegate : public content::SerialDelegate { + public: + ElectronSerialDelegate(); + ~ElectronSerialDelegate() override; + + std::unique_ptr RunChooser( + content::RenderFrameHost* frame, + std::vector filters, + content::SerialChooser::Callback callback) override; + bool CanRequestPortPermission(content::RenderFrameHost* frame) override; + bool HasPortPermission(content::RenderFrameHost* frame, + const device::mojom::SerialPortInfo& port) override; + device::mojom::SerialPortManager* GetPortManager( + content::RenderFrameHost* frame) override; + void AddObserver(content::RenderFrameHost* frame, + Observer* observer) override; + void RemoveObserver(content::RenderFrameHost* frame, + Observer* observer) override; + + void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host); + + private: + SerialChooserController* ControllerForFrame( + content::RenderFrameHost* render_frame_host); + SerialChooserController* AddControllerForFrame( + content::RenderFrameHost* render_frame_host, + std::vector filters, + content::SerialChooser::Callback callback); + + std::unordered_map> + controller_map_; + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(ElectronSerialDelegate); +}; + +} // namespace electron + +#endif // SHELL_BROWSER_SERIAL_ELECTRON_SERIAL_DELEGATE_H_ diff --git a/shell/browser/serial/serial_chooser_context.cc b/shell/browser/serial/serial_chooser_context.cc new file mode 100644 index 000000000000..a3f66a78a20e --- /dev/null +++ b/shell/browser/serial/serial_chooser_context.cc @@ -0,0 +1,162 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "shell/browser/serial/serial_chooser_context.h" + +#include + +#include "base/base64.h" +#include "base/strings/utf_string_conversions.h" +#include "base/values.h" +#include "content/public/browser/device_service.h" +#include "mojo/public/cpp/bindings/pending_remote.h" + +namespace electron { + +constexpr char kPortNameKey[] = "name"; +constexpr char kPersistentIdKey[] = "persistent_id"; +constexpr char kTokenKey[] = "token"; + +std::string EncodeToken(const base::UnguessableToken& token) { + const uint64_t data[2] = {token.GetHighForSerialization(), + token.GetLowForSerialization()}; + std::string buffer; + base::Base64Encode( + base::StringPiece(reinterpret_cast(&data[0]), sizeof(data)), + &buffer); + return buffer; +} + +base::UnguessableToken DecodeToken(base::StringPiece input) { + std::string buffer; + if (!base::Base64Decode(input, &buffer) || + buffer.length() != sizeof(uint64_t) * 2) { + return base::UnguessableToken(); + } + + const uint64_t* data = reinterpret_cast(buffer.data()); + return base::UnguessableToken::Deserialize(data[0], data[1]); +} + +base::Value PortInfoToValue(const device::mojom::SerialPortInfo& port) { + base::Value value(base::Value::Type::DICTIONARY); + if (port.display_name && !port.display_name->empty()) + value.SetStringKey(kPortNameKey, *port.display_name); + else + value.SetStringKey(kPortNameKey, port.path.LossyDisplayName()); + if (SerialChooserContext::CanStorePersistentEntry(port)) + value.SetStringKey(kPersistentIdKey, port.persistent_id.value()); + else + value.SetStringKey(kTokenKey, EncodeToken(port.token)); + return value; +} + +SerialChooserContext::SerialChooserContext() = default; +SerialChooserContext::~SerialChooserContext() = default; + +void SerialChooserContext::GrantPortPermission( + const url::Origin& requesting_origin, + const url::Origin& embedding_origin, + const device::mojom::SerialPortInfo& port) { + base::Value value = PortInfoToValue(port); + port_info_.insert({port.token, value.Clone()}); + + ephemeral_ports_[{requesting_origin, embedding_origin}].insert(port.token); +} + +bool SerialChooserContext::HasPortPermission( + const url::Origin& requesting_origin, + const url::Origin& embedding_origin, + const device::mojom::SerialPortInfo& port) { + auto it = ephemeral_ports_.find({requesting_origin, embedding_origin}); + if (it != ephemeral_ports_.end()) { + const std::set ports = it->second; + if (base::Contains(ports, port.token)) + return true; + } + + return false; +} + +// static +bool SerialChooserContext::CanStorePersistentEntry( + const device::mojom::SerialPortInfo& port) { + // If there is no display name then the path name will be used instead. The + // path name is not guaranteed to be stable. For example, on Linux the name + // "ttyUSB0" is reused for any USB serial device. A name like that would be + // confusing to show in settings when the device is disconnected. + if (!port.display_name || port.display_name->empty()) + return false; + + return port.persistent_id && !port.persistent_id->empty(); +} + +device::mojom::SerialPortManager* SerialChooserContext::GetPortManager() { + EnsurePortManagerConnection(); + return port_manager_.get(); +} + +void SerialChooserContext::AddPortObserver(PortObserver* observer) { + port_observer_list_.AddObserver(observer); +} + +void SerialChooserContext::RemovePortObserver(PortObserver* observer) { + port_observer_list_.RemoveObserver(observer); +} + +base::WeakPtr SerialChooserContext::AsWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +void SerialChooserContext::OnPortAdded(device::mojom::SerialPortInfoPtr port) { + for (auto& observer : port_observer_list_) + observer.OnPortAdded(*port); +} + +void SerialChooserContext::OnPortRemoved( + device::mojom::SerialPortInfoPtr port) { + for (auto& observer : port_observer_list_) + observer.OnPortRemoved(*port); + + std::vector> revoked_url_pairs; + for (auto& map_entry : ephemeral_ports_) { + std::set& ports = map_entry.second; + if (ports.erase(port->token) > 0) { + revoked_url_pairs.push_back(map_entry.first); + } + } + + port_info_.erase(port->token); +} + +void SerialChooserContext::EnsurePortManagerConnection() { + if (port_manager_) + return; + + mojo::PendingRemote manager; + content::GetDeviceService().BindSerialPortManager( + manager.InitWithNewPipeAndPassReceiver()); + SetUpPortManagerConnection(std::move(manager)); +} + +void SerialChooserContext::SetUpPortManagerConnection( + mojo::PendingRemote manager) { + port_manager_.Bind(std::move(manager)); + port_manager_.set_disconnect_handler( + base::BindOnce(&SerialChooserContext::OnPortManagerConnectionError, + base::Unretained(this))); + + port_manager_->SetClient(client_receiver_.BindNewPipeAndPassRemote()); +} + +void SerialChooserContext::OnPortManagerConnectionError() { + port_manager_.reset(); + client_receiver_.reset(); + + port_info_.clear(); + + ephemeral_ports_.clear(); +} + +} // namespace electron diff --git a/shell/browser/serial/serial_chooser_context.h b/shell/browser/serial/serial_chooser_context.h new file mode 100644 index 000000000000..258307d9ff3c --- /dev/null +++ b/shell/browser/serial/serial_chooser_context.h @@ -0,0 +1,92 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_ +#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_ + +#include +#include +#include +#include +#include +#include + +#include "base/memory/weak_ptr.h" +#include "base/observer_list.h" +#include "base/unguessable_token.h" +#include "components/keyed_service/core/keyed_service.h" +#include "content/public/browser/serial_delegate.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/device/public/mojom/serial.mojom-forward.h" +#include "third_party/blink/public/mojom/serial/serial.mojom.h" +#include "url/gurl.h" +#include "url/origin.h" + +namespace base { +class Value; +} + +namespace electron { + +class SerialChooserContext : public KeyedService, + public device::mojom::SerialPortManagerClient { + public: + using PortObserver = content::SerialDelegate::Observer; + + SerialChooserContext(); + ~SerialChooserContext() override; + + // Serial-specific interface for granting and checking permissions. + void GrantPortPermission(const url::Origin& requesting_origin, + const url::Origin& embedding_origin, + const device::mojom::SerialPortInfo& port); + bool HasPortPermission(const url::Origin& requesting_origin, + const url::Origin& embedding_origin, + const device::mojom::SerialPortInfo& port); + static bool CanStorePersistentEntry( + const device::mojom::SerialPortInfo& port); + + device::mojom::SerialPortManager* GetPortManager(); + + void AddPortObserver(PortObserver* observer); + void RemovePortObserver(PortObserver* observer); + + base::WeakPtr AsWeakPtr(); + + // SerialPortManagerClient implementation. + void OnPortAdded(device::mojom::SerialPortInfoPtr port) override; + void OnPortRemoved(device::mojom::SerialPortInfoPtr port) override; + + private: + void EnsurePortManagerConnection(); + void SetUpPortManagerConnection( + mojo::PendingRemote manager); + void OnPortManagerConnectionError(); + void OnGetPorts(const url::Origin& requesting_origin, + const url::Origin& embedding_origin, + blink::mojom::SerialService::GetPortsCallback callback, + std::vector ports); + + // Tracks the set of ports to which an origin (potentially embedded in another + // origin) has access to. Key is (requesting_origin, embedding_origin). + std::map, + std::set> + ephemeral_ports_; + + // Holds information about ports in |ephemeral_ports_|. + std::map port_info_; + + mojo::Remote port_manager_; + mojo::Receiver client_receiver_{this}; + base::ObserverList port_observer_list_; + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(SerialChooserContext); +}; + +} // namespace electron + +#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_H_ diff --git a/shell/browser/serial/serial_chooser_context_factory.cc b/shell/browser/serial/serial_chooser_context_factory.cc new file mode 100644 index 000000000000..58cd1b0e31c5 --- /dev/null +++ b/shell/browser/serial/serial_chooser_context_factory.cc @@ -0,0 +1,41 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "shell/browser/serial/serial_chooser_context_factory.h" + +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "shell/browser/serial/serial_chooser_context.h" + +namespace electron { + +SerialChooserContextFactory::SerialChooserContextFactory() + : BrowserContextKeyedServiceFactory( + "SerialChooserContext", + BrowserContextDependencyManager::GetInstance()) {} + +SerialChooserContextFactory::~SerialChooserContextFactory() {} + +KeyedService* SerialChooserContextFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + return new SerialChooserContext(); +} + +// static +SerialChooserContextFactory* SerialChooserContextFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +SerialChooserContext* SerialChooserContextFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, true)); +} + +content::BrowserContext* SerialChooserContextFactory::GetBrowserContextToUse( + content::BrowserContext* context) const { + return context; +} + +} // namespace electron diff --git a/shell/browser/serial/serial_chooser_context_factory.h b/shell/browser/serial/serial_chooser_context_factory.h new file mode 100644 index 000000000000..09611d702beb --- /dev/null +++ b/shell/browser/serial/serial_chooser_context_factory.h @@ -0,0 +1,40 @@ +// Copyright 2019 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_ +#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_ + +#include "base/macros.h" +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" +#include "shell/browser/serial/serial_chooser_context.h" + +namespace electron { + +class SerialChooserContext; + +class SerialChooserContextFactory : public BrowserContextKeyedServiceFactory { + public: + static SerialChooserContext* GetForBrowserContext( + content::BrowserContext* context); + static SerialChooserContextFactory* GetInstance(); + + private: + friend struct base::DefaultSingletonTraits; + + SerialChooserContextFactory(); + ~SerialChooserContextFactory() override; + + // BrowserContextKeyedServiceFactory methods: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* context) const override; + content::BrowserContext* GetBrowserContextToUse( + content::BrowserContext* context) const override; + + DISALLOW_COPY_AND_ASSIGN(SerialChooserContextFactory); +}; + +} // namespace electron + +#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTEXT_FACTORY_H_ diff --git a/shell/browser/serial/serial_chooser_controller.cc b/shell/browser/serial/serial_chooser_controller.cc new file mode 100644 index 000000000000..b1f7b7fe0fd8 --- /dev/null +++ b/shell/browser/serial/serial_chooser_controller.cc @@ -0,0 +1,181 @@ +// Copyright (c) 2020 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/serial/serial_chooser_controller.h" + +#include +#include + +#include "base/bind.h" +#include "base/files/file_path.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "shell/browser/api/electron_api_session.h" +#include "shell/browser/serial/serial_chooser_context.h" +#include "shell/browser/serial/serial_chooser_context_factory.h" +#include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_converters/content_converter.h" +#include "shell/common/gin_helper/dictionary.h" +#include "ui/base/l10n/l10n_util.h" + +namespace gin { + +template <> +struct Converter { + static v8::Local ToV8( + v8::Isolate* isolate, + const device::mojom::SerialPortInfoPtr& port) { + gin_helper::Dictionary dict = gin::Dictionary::CreateEmpty(isolate); + dict.Set("portId", port->token.ToString()); + dict.Set("portName", port->path.BaseName().LossyDisplayName()); + if (port->display_name && !port->display_name->empty()) { + dict.Set("displayName", *port->display_name); + } + if (port->persistent_id && !port->persistent_id->empty()) { + dict.Set("persistentId", *port->persistent_id); + } + if (port->has_vendor_id) { + dict.Set("vendorId", base::StringPrintf("%u", port->vendor_id)); + } + if (port->has_product_id) { + dict.Set("productId", base::StringPrintf("%u", port->product_id)); + } + + return gin::ConvertToV8(isolate, dict); + } +}; + +} // namespace gin + +namespace electron { + +SerialChooserController::SerialChooserController( + content::RenderFrameHost* render_frame_host, + std::vector filters, + content::SerialChooser::Callback callback, + content::WebContents* web_contents, + base::WeakPtr serial_delegate) + : WebContentsObserver(web_contents), + filters_(std::move(filters)), + callback_(std::move(callback)), + serial_delegate_(serial_delegate) { + requesting_origin_ = render_frame_host->GetLastCommittedOrigin(); + embedding_origin_ = web_contents->GetMainFrame()->GetLastCommittedOrigin(); + + chooser_context_ = SerialChooserContextFactory::GetForBrowserContext( + web_contents->GetBrowserContext()) + ->AsWeakPtr(); + DCHECK(chooser_context_); + chooser_context_->GetPortManager()->GetDevices(base::BindOnce( + &SerialChooserController::OnGetDevices, weak_factory_.GetWeakPtr())); + observer_.Add(chooser_context_.get()); +} + +SerialChooserController::~SerialChooserController() { + RunCallback(/*port=*/nullptr); +} + +api::Session* SerialChooserController::GetSession() { + if (!web_contents()) { + return nullptr; + } + return api::Session::FromBrowserContext(web_contents()->GetBrowserContext()); +} + +void SerialChooserController::OnPortAdded( + const device::mojom::SerialPortInfo& port) { + ports_.push_back(port.Clone()); + api::Session* session = GetSession(); + if (session) { + session->Emit("serial-port-added", port.Clone(), web_contents()); + } +} + +void SerialChooserController::OnPortRemoved( + const device::mojom::SerialPortInfo& port) { + const auto it = std::find_if( + ports_.begin(), ports_.end(), + [&port](const auto& ptr) { return ptr->token == port.token; }); + if (it != ports_.end()) { + api::Session* session = GetSession(); + if (session) { + session->Emit("serial-port-removed", port.Clone(), web_contents()); + } + ports_.erase(it); + } +} + +void SerialChooserController::OnPortManagerConnectionError() { + observer_.RemoveAll(); +} + +void SerialChooserController::OnDeviceChosen(const std::string& port_id) { + if (port_id.empty()) { + RunCallback(/*port=*/nullptr); + } else { + const auto it = + std::find_if(ports_.begin(), ports_.end(), [&port_id](const auto& ptr) { + return ptr->token.ToString() == port_id; + }); + chooser_context_->GrantPortPermission(requesting_origin_, embedding_origin_, + *it->get()); + RunCallback(it->Clone()); + } +} + +void SerialChooserController::OnGetDevices( + std::vector ports) { + // Sort ports by file paths. + std::sort(ports.begin(), ports.end(), + [](const auto& port1, const auto& port2) { + return port1->path.BaseName() < port2->path.BaseName(); + }); + + for (auto& port : ports) { + if (FilterMatchesAny(*port)) + ports_.push_back(std::move(port)); + } + + bool prevent_default = false; + api::Session* session = GetSession(); + if (session) { + prevent_default = + session->Emit("select-serial-port", ports_, web_contents(), + base::AdaptCallbackForRepeating(base::BindOnce( + &SerialChooserController::OnDeviceChosen, + weak_factory_.GetWeakPtr()))); + } + if (!prevent_default) { + RunCallback(/*port=*/nullptr); + } +} + +bool SerialChooserController::FilterMatchesAny( + const device::mojom::SerialPortInfo& port) const { + if (filters_.empty()) + return true; + + for (const auto& filter : filters_) { + if (filter->has_vendor_id && + (!port.has_vendor_id || filter->vendor_id != port.vendor_id)) { + continue; + } + if (filter->has_product_id && + (!port.has_product_id || filter->product_id != port.product_id)) { + continue; + } + return true; + } + + return false; +} + +void SerialChooserController::RunCallback( + device::mojom::SerialPortInfoPtr port) { + if (callback_) { + std::move(callback_).Run(std::move(port)); + } +} + +} // namespace electron diff --git a/shell/browser/serial/serial_chooser_controller.h b/shell/browser/serial/serial_chooser_controller.h new file mode 100644 index 000000000000..1556370428a6 --- /dev/null +++ b/shell/browser/serial/serial_chooser_controller.h @@ -0,0 +1,79 @@ +// Copyright (c) 2020 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_ +#define SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_ + +#include +#include + +#include "base/macros.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observer.h" +#include "base/strings/string16.h" +#include "content/public/browser/serial_chooser.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_observer.h" +#include "services/device/public/mojom/serial.mojom-forward.h" +#include "shell/browser/api/electron_api_session.h" +#include "shell/browser/serial/electron_serial_delegate.h" +#include "shell/browser/serial/serial_chooser_context.h" +#include "third_party/blink/public/mojom/serial/serial.mojom.h" + +namespace content { +class RenderFrameHost; +} // namespace content + +namespace electron { + +class ElectronSerialDelegate; + +// SerialChooserController provides data for the Serial API permission prompt. +class SerialChooserController final : public SerialChooserContext::PortObserver, + public content::WebContentsObserver { + public: + SerialChooserController( + content::RenderFrameHost* render_frame_host, + std::vector filters, + content::SerialChooser::Callback callback, + content::WebContents* web_contents, + base::WeakPtr serial_delegate); + ~SerialChooserController() override; + + // SerialChooserContext::PortObserver: + void OnPortAdded(const device::mojom::SerialPortInfo& port) override; + void OnPortRemoved(const device::mojom::SerialPortInfo& port) override; + void OnPortManagerConnectionError() override; + + private: + api::Session* GetSession(); + void OnGetDevices(std::vector ports); + bool FilterMatchesAny(const device::mojom::SerialPortInfo& port) const; + void RunCallback(device::mojom::SerialPortInfoPtr port); + void OnDeviceChosen(const std::string& port_id); + + std::vector filters_; + content::SerialChooser::Callback callback_; + url::Origin requesting_origin_; + url::Origin embedding_origin_; + + base::WeakPtr chooser_context_; + ScopedObserver + observer_{this}; + + std::vector ports_; + + base::WeakPtr serial_delegate_; + + base::WeakPtrFactory weak_factory_{this}; + + DISALLOW_COPY_AND_ASSIGN(SerialChooserController); +}; + +} // namespace electron + +#endif // SHELL_BROWSER_SERIAL_SERIAL_CHOOSER_CONTROLLER_H_ diff --git a/shell/browser/web_contents_permission_helper.cc b/shell/browser/web_contents_permission_helper.cc index 324e1893a036..82273f6adc73 100644 --- a/shell/browser/web_contents_permission_helper.cc +++ b/shell/browser/web_contents_permission_helper.cc @@ -160,6 +160,14 @@ bool WebContentsPermissionHelper::CheckMediaAccessPermission( return CheckPermission(content::PermissionType::AUDIO_CAPTURE, &details); } +bool WebContentsPermissionHelper::CheckSerialAccessPermission( + const url::Origin& embedding_origin) const { + base::DictionaryValue details; + details.SetString("securityOrigin", embedding_origin.GetURL().spec()); + return CheckPermission( + static_cast(PermissionType::SERIAL), &details); +} + WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper) } // namespace electron diff --git a/shell/browser/web_contents_permission_helper.h b/shell/browser/web_contents_permission_helper.h index 9469bc7a1a97..d0732941ffef 100644 --- a/shell/browser/web_contents_permission_helper.h +++ b/shell/browser/web_contents_permission_helper.h @@ -22,6 +22,7 @@ class WebContentsPermissionHelper POINTER_LOCK = static_cast(content::PermissionType::NUM) + 1, FULLSCREEN, OPEN_EXTERNAL, + SERIAL }; // Asynchronous Requests @@ -38,6 +39,7 @@ class WebContentsPermissionHelper // Synchronous Checks bool CheckMediaAccessPermission(const GURL& security_origin, blink::mojom::MediaStreamType type) const; + bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const; private: explicit WebContentsPermissionHelper(content::WebContents* web_contents); diff --git a/shell/common/gin_converters/content_converter.cc b/shell/common/gin_converters/content_converter.cc index e580381badb8..6ba67ea7ab9b 100644 --- a/shell/common/gin_converters/content_converter.cc +++ b/shell/common/gin_converters/content_converter.cc @@ -199,6 +199,8 @@ v8::Local Converter::ToV8( return StringToV8(isolate, "fullscreen"); case PermissionType::OPEN_EXTERNAL: return StringToV8(isolate, "openExternal"); + case PermissionType::SERIAL: + return StringToV8(isolate, "serial"); default: return StringToV8(isolate, "unknown"); } diff --git a/spec-main/chromium-spec.ts b/spec-main/chromium-spec.ts index c41b45d4b07a..be1808180da6 100644 --- a/spec-main/chromium-spec.ts +++ b/spec-main/chromium-spec.ts @@ -1455,3 +1455,51 @@ describe('iframe using HTML fullscreen API while window is OS-fullscreened', () expect(width).to.equal(0); }); }); + +describe('navigator.serial', () => { + let w: BrowserWindow; + before(async () => { + w = new BrowserWindow({ + show: false, + webPreferences: { + enableBlinkFeatures: 'Serial' + } + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + }); + + const getPorts: any = () => { + return w.webContents.executeJavaScript(` + navigator.serial.requestPort().then(port => port.toString()).catch(err => err.toString()); + `, true); + }; + + after(closeAllWindows); + afterEach(() => { + session.defaultSession.setPermissionCheckHandler(null); + session.defaultSession.removeAllListeners('select-serial-port'); + }); + + it('does not return a port if select-serial-port event is not defined', async () => { + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const port = await getPorts(); + expect(port).to.equal('NotFoundError: No port selected by the user.'); + }); + + it('does not return a port when permission denied', async () => { + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + callback(portList[0].portId); + }); + session.defaultSession.setPermissionCheckHandler(() => false); + const port = await getPorts(); + expect(port).to.equal('NotFoundError: No port selected by the user.'); + }); + + it('returns a port when select-serial-port event is defined', async () => { + w.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => { + callback(portList[0].portId); + }); + const port = await getPorts(); + expect(port).to.equal('[object SerialPort]'); + }); +}); diff --git a/spec-main/index.js b/spec-main/index.js index a2a80580b8c8..c3adaf8349bb 100644 --- a/spec-main/index.js +++ b/spec-main/index.js @@ -18,6 +18,7 @@ const { app, protocol } = require('electron'); v8.setFlagsFromString('--expose_gc'); app.commandLine.appendSwitch('js-flags', '--expose_gc'); +app.commandLine.appendSwitch('enable-features', 'ElectronSerialChooser'); // Prevent the spec runner quiting when the first window closes app.on('window-all-closed', () => null);