From 629c54ba36fcca787cd3ac69924a2527ba131bfc Mon Sep 17 00:00:00 2001 From: John Kleinschmidt Date: Tue, 22 Nov 2022 16:50:32 -0500 Subject: [PATCH] feat: add support for WebUSB (#36289) * feat: add support for WebUSB * fixup for gn check * fixup gn check on Windows * Apply review feedback Co-authored-by: Charles Kerr * chore: address review feedback * chore: removed unneeded code * Migrate non-default ScopedObservation<> instantiations to ScopedObservationTraits<> in chrome/browser/ https://chromium-review.googlesource.com/c/chromium/src/+/4016595 Co-authored-by: Charles Kerr --- docs/api/session.md | 120 +++++- docs/api/structures/usb-device.md | 17 + docs/fiddles/features/web-usb/index.html | 21 ++ docs/fiddles/features/web-usb/main.js | 72 ++++ docs/fiddles/features/web-usb/renderer.js | 33 ++ docs/tutorial/devices.md | 38 ++ filenames.auto.gni | 1 + filenames.gni | 9 + shell/browser/electron_browser_client.cc | 6 + shell/browser/electron_browser_client.h | 3 + shell/browser/electron_browser_context.cc | 20 +- shell/browser/feature_list.cc | 9 + shell/browser/hid/hid_chooser_context.cc | 10 +- shell/browser/hid/hid_chooser_context.h | 2 - shell/browser/serial/serial_chooser_context.h | 3 - shell/browser/usb/electron_usb_delegate.cc | 317 ++++++++++++++++ shell/browser/usb/electron_usb_delegate.h | 106 ++++++ shell/browser/usb/usb_chooser_context.cc | 354 ++++++++++++++++++ shell/browser/usb/usb_chooser_context.h | 122 ++++++ .../usb/usb_chooser_context_factory.cc | 45 +++ .../browser/usb/usb_chooser_context_factory.h | 39 ++ shell/browser/usb/usb_chooser_controller.cc | 165 ++++++++ shell/browser/usb/usb_chooser_controller.h | 81 ++++ .../browser/web_contents_permission_helper.h | 3 +- shell/common/electron_constants.cc | 4 + shell/common/electron_constants.h | 5 + .../gin_converters/content_converter.cc | 2 + .../usb_device_info_converter.h | 27 ++ spec/chromium-spec.ts | 161 ++++++++ 29 files changed, 1772 insertions(+), 23 deletions(-) create mode 100644 docs/api/structures/usb-device.md create mode 100644 docs/fiddles/features/web-usb/index.html create mode 100644 docs/fiddles/features/web-usb/main.js create mode 100644 docs/fiddles/features/web-usb/renderer.js create mode 100644 shell/browser/usb/electron_usb_delegate.cc create mode 100644 shell/browser/usb/electron_usb_delegate.h create mode 100644 shell/browser/usb/usb_chooser_context.cc create mode 100644 shell/browser/usb/usb_chooser_context.h create mode 100644 shell/browser/usb/usb_chooser_context_factory.cc create mode 100644 shell/browser/usb/usb_chooser_context_factory.h create mode 100644 shell/browser/usb/usb_chooser_controller.cc create mode 100644 shell/browser/usb/usb_chooser_controller.h create mode 100644 shell/common/gin_converters/usb_device_info_converter.h diff --git a/docs/api/session.md b/docs/api/session.md index 764243c7b14d..bc86c21713bf 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -239,7 +239,7 @@ app.whenReady().then(() => { const selectedDevice = details.deviceList.find((device) => { return device.vendorId === '9025' && device.productId === '67' }) - callback(selectedPort?.deviceId) + callback(selectedDevice?.deviceId) }) }) ``` @@ -429,6 +429,118 @@ const portConnect = async () => { } ``` +#### Event: 'select-usb-device' + +Returns: + +* `event` Event +* `details` Object + * `deviceList` [USBDevice[]](structures/usb-device.md) + * `frame` [WebFrameMain](web-frame-main.md) +* `callback` Function + * `deviceId` string (optional) + +Emitted when a USB device needs to be selected when a call to +`navigator.usb.requestDevice` is made. `callback` should be called with +`deviceId` to be selected; passing no arguments to `callback` will +cancel the request. Additionally, permissioning on `navigator.usb` can +be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler) +and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler). + +```javascript +const { app, BrowserWindow } = require('electron') + +let win = null + +app.whenReady().then(() => { + win = new BrowserWindow() + + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'usb') { + // Add logic here to determine if permission should be given to allow USB selection + return true + } + return false + }) + + // Optionally, retrieve previously persisted devices from a persistent store (fetchGrantedDevices needs to be implemented by developer to fetch persisted permissions) + const grantedDevices = fetchGrantedDevices() + + win.webContents.session.setDevicePermissionHandler((details) => { + if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'usb') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.usb.requestDevice` first) + return true + } + + // Search through the list of devices that have previously been granted permission + return grantedDevices.some((grantedDevice) => { + return grantedDevice.vendorId === details.device.vendorId && + grantedDevice.productId === details.device.productId && + grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber + }) + } + return false + }) + + win.webContents.session.on('select-usb-device', (event, details, callback) => { + event.preventDefault() + const selectedDevice = details.deviceList.find((device) => { + return device.vendorId === '9025' && device.productId === '67' + }) + if (selectedDevice) { + // Optionally, add this to the persisted devices (updateGrantedDevices needs to be implemented by developer to persist permissions) + grantedDevices.push(selectedDevice) + updateGrantedDevices(grantedDevices) + } + callback(selectedDevice?.deviceId) + }) +}) +``` + +#### Event: 'usb-device-added' + +Returns: + +* `event` Event +* `details` Object + * `device` [USBDevice](structures/usb-device.md) + * `frame` [WebFrameMain](web-frame-main.md) + +Emitted after `navigator.usb.requestDevice` has been called and +`select-usb-device` has fired if a new device becomes available before +the callback from `select-usb-device` is called. This event is intended for +use when using a UI to ask users to pick a device so that the UI can be updated +with the newly added device. + +#### Event: 'usb-device-removed' + +Returns: + +* `event` Event +* `details` Object + * `device` [USBDevice](structures/usb-device.md) + * `frame` [WebFrameMain](web-frame-main.md) + +Emitted after `navigator.usb.requestDevice` has been called and +`select-usb-device` has fired if a device has been removed before the callback +from `select-usb-device` is called. This event is intended for use when using +a UI to ask users to pick a device so that the UI can be updated to remove the +specified device. + +#### Event: 'usb-device-revoked' + +Returns: + +* `event` Event +* `details` Object + * `device` [USBDevice[]](structures/usb-device.md) + * `origin` string (optional) - The origin that the device has been revoked from. + +Emitted after `USBDevice.forget()` has been called. This event can be used +to help maintain persistent storage of permissions when +`setDevicePermissionHandler` is used. + ### Instance Methods The following methods are available on instances of `Session`: @@ -714,7 +826,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents * `handler` Function\ | null * `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively. - * `permission` string - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`. + * `permission` string - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, `serial`, or `usb`. * `requestingOrigin` string - The origin URL of the permission check * `details` Object - Some properties are only available on certain permission types. * `embeddingOrigin` string (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks. @@ -800,7 +912,7 @@ Passing `null` instead of a function resets the handler to its default state. * `handler` Function\ | null * `details` Object - * `deviceType` string - The type of device that permission is being requested on, can be `hid` or `serial`. + * `deviceType` string - The type of device that permission is being requested on, can be `hid`, `serial`, or `usb`. * `origin` string - The origin URL of the device permission check. * `device` [HIDDevice](structures/hid-device.md) | [SerialPort](structures/serial-port.md)- the device that permission is being requested for. @@ -828,6 +940,8 @@ app.whenReady().then(() => { return true } else if (permission === 'serial') { // Add logic here to determine if permission should be given to allow serial port selection + } else if (permission === 'usb') { + // Add logic here to determine if permission should be given to allow USB device selection } return false }) diff --git a/docs/api/structures/usb-device.md b/docs/api/structures/usb-device.md new file mode 100644 index 000000000000..e1f427fb4edb --- /dev/null +++ b/docs/api/structures/usb-device.md @@ -0,0 +1,17 @@ +# USBDevice Object + +* `deviceId` string - Unique identifier for the device. +* `vendorId` Integer - The USB vendor ID. +* `productId` Integer - The USB product ID. +* `productName` string (optional) - Name of the device. +* `serialNumber` string (optional) - The USB device serial number. +* `manufacturerName` string (optional) - The manufacturer name of the device. +* `usbVersionMajor` Integer - The USB protocol major version supported by the device +* `usbVersionMinor` Integer - The USB protocol minor version supported by the device +* `usbVersionSubminor` Integer - The USB protocol subminor version supported by the device +* `deviceClass` Integer - The device class for the communication interface supported by the device +* `deviceSubclass` Integer - The device subclass for the communication interface supported by the device +* `deviceProtocol` Integer - The device protocol for the communication interface supported by the device +* `deviceVersionMajor` Integer - The major version number of the device as defined by the device manufacturer. +* `deviceVersionMinor` Integer - The minor version number of the device as defined by the device manufacturer. +* `deviceVersionSubminor` Integer - The subminor version number of the device as defined by the device manufacturer. diff --git a/docs/fiddles/features/web-usb/index.html b/docs/fiddles/features/web-usb/index.html new file mode 100644 index 000000000000..95541d9a1fee --- /dev/null +++ b/docs/fiddles/features/web-usb/index.html @@ -0,0 +1,21 @@ + + + + + + WebUSB API + + +

WebUSB API

+ + + +

USB devices automatically granted access via setDevicePermissionHandler

+
+ +

USB devices automatically granted access via select-usb-device

+
+ + + + diff --git a/docs/fiddles/features/web-usb/main.js b/docs/fiddles/features/web-usb/main.js new file mode 100644 index 000000000000..316a0dbdf86a --- /dev/null +++ b/docs/fiddles/features/web-usb/main.js @@ -0,0 +1,72 @@ +const {app, BrowserWindow} = require('electron') +const e = require('express') +const path = require('path') + +function createWindow () { + const mainWindow = new BrowserWindow({ + width: 800, + height: 600 + }) + + let grantedDeviceThroughPermHandler + + mainWindow.webContents.session.on('select-usb-device', (event, details, callback) => { + //Add events to handle devices being added or removed before the callback on + //`select-usb-device` is called. + mainWindow.webContents.session.on('usb-device-added', (event, device) => { + console.log('usb-device-added FIRED WITH', device) + //Optionally update details.deviceList + }) + + mainWindow.webContents.session.on('usb-device-removed', (event, device) => { + console.log('usb-device-removed FIRED WITH', device) + //Optionally update details.deviceList + }) + + event.preventDefault() + if (details.deviceList && details.deviceList.length > 0) { + const deviceToReturn = details.deviceList.find((device) => { + if (!grantedDeviceThroughPermHandler || (device.deviceId != grantedDeviceThroughPermHandler.deviceId)) { + return true + } + }) + if (deviceToReturn) { + callback(deviceToReturn.deviceId) + } else { + callback() + } + } + }) + + mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'usb' && details.securityOrigin === 'file:///') { + return true + } + }) + + + mainWindow.webContents.session.setDevicePermissionHandler((details) => { + if (details.deviceType === 'usb' && details.origin === 'file://') { + if (!grantedDeviceThroughPermHandler) { + grantedDeviceThroughPermHandler = details.device + return true + } else { + return false + } + } + }) + + mainWindow.loadFile('index.html') +} + +app.whenReady().then(() => { + createWindow() + + app.on('activate', function () { + if (BrowserWindow.getAllWindows().length === 0) createWindow() + }) +}) + +app.on('window-all-closed', function () { + if (process.platform !== 'darwin') app.quit() +}) diff --git a/docs/fiddles/features/web-usb/renderer.js b/docs/fiddles/features/web-usb/renderer.js new file mode 100644 index 000000000000..f1aa3be30b46 --- /dev/null +++ b/docs/fiddles/features/web-usb/renderer.js @@ -0,0 +1,33 @@ +function getDeviceDetails(device) { + return grantedDevice.productName || `Unknown device ${grantedDevice.deviceId}` +} + +async function testIt() { + const noDevicesFoundMsg = 'No devices found' + const grantedDevices = await navigator.usb.getDevices() + let grantedDeviceList = '' + if (grantedDevices.length > 0) { + grantedDevices.forEach(device => { + grantedDeviceList += `
${getDeviceDetails(device)}` + }) + } else { + grantedDeviceList = noDevicesFoundMsg + } + document.getElementById('granted-devices').innerHTML = grantedDeviceList + + grantedDeviceList = '' + try { + const grantedDevice = await navigator.usb.requestDevice({ + filters: [] + }) + grantedDeviceList += `
${getDeviceDetails(device)}` + + } catch (ex) { + if (ex.name === 'NotFoundError') { + grantedDeviceList = noDevicesFoundMsg + } + } + document.getElementById('granted-devices2').innerHTML = grantedDeviceList +} + +document.getElementById('clickme').addEventListener('click',testIt) diff --git a/docs/tutorial/devices.md b/docs/tutorial/devices.md index 6f11fec76020..fc988c522424 100644 --- a/docs/tutorial/devices.md +++ b/docs/tutorial/devices.md @@ -115,3 +115,41 @@ when the `Test Web Serial` button is clicked. ```javascript fiddle='docs/fiddles/features/web-serial' ``` + +## WebUSB API + +The [WebUSB API](https://web.dev/usb/) can be used to access USB devices. +Electron provides several APIs for working with the WebUSB API: + +* The [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device) + can be used to select a USB device when a call to + `navigator.usb.requestDevice` is made. Additionally the [`usb-device-added`](../api/session.md#event-usb-device-added) + and [`usb-device-removed`](../api/session.md#event-usb-device-removed) events + on the Session can be used to handle devices being plugged in or unplugged + when handling the `select-usb-device` event. + **Note:** These two events only fire until the callback from `select-usb-device` + is called. They are not intended to be used as a generic usb device listener. +* The [`usb-device-revoked' event on the Session](../api/session.md#event-usb-device-revoked) can + be used to respond when [device.forget()](https://developer.chrome.com/articles/usb/#revoke-access) + is called on a USB device. +* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler) + can be used to provide default permissioning to devices without first calling + for permission to devices via `navigator.usb.requestDevice`. Additionally, + the default behavior of Electron is to store granted device permission through + the lifetime of the corresponding WebContents. If longer term storage is + needed, a developer can store granted device permissions (eg when handling + the `select-usb-device` event) and then read from that storage with + `setDevicePermissionHandler`. +* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler) + can be used to disable USB access for specific origins. + +### Example + +This example demonstrates an Electron application that automatically selects +USB devices (if they are attached) through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler) +and through [`select-usb-device` event on the Session](../api/session.md#event-select-usb-device) +when the `Test WebUSB` button is clicked. + +```javascript fiddle='docs/fiddles/features/web-usb' + +``` diff --git a/filenames.auto.gni b/filenames.auto.gni index f34e4c88ccd0..274e7270a1a4 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -132,6 +132,7 @@ auto_filenames = { "docs/api/structures/upload-data.md", "docs/api/structures/upload-file.md", "docs/api/structures/upload-raw-data.md", + "docs/api/structures/usb-device.md", "docs/api/structures/user-default-types.md", "docs/api/structures/web-request-filter.md", "docs/api/structures/web-source.md", diff --git a/filenames.gni b/filenames.gni index c87fa8744e0e..d68a04943b43 100644 --- a/filenames.gni +++ b/filenames.gni @@ -505,6 +505,14 @@ filenames = { "shell/browser/ui/tray_icon_observer.h", "shell/browser/ui/webui/accessibility_ui.cc", "shell/browser/ui/webui/accessibility_ui.h", + "shell/browser/usb/electron_usb_delegate.cc", + "shell/browser/usb/electron_usb_delegate.h", + "shell/browser/usb/usb_chooser_context.cc", + "shell/browser/usb/usb_chooser_context.h", + "shell/browser/usb/usb_chooser_context_factory.cc", + "shell/browser/usb/usb_chooser_context_factory.h", + "shell/browser/usb/usb_chooser_controller.cc", + "shell/browser/usb/usb_chooser_controller.h", "shell/browser/web_contents_permission_helper.cc", "shell/browser/web_contents_permission_helper.h", "shell/browser/web_contents_preferences.cc", @@ -586,6 +594,7 @@ filenames = { "shell/common/gin_converters/std_converter.h", "shell/common/gin_converters/time_converter.cc", "shell/common/gin_converters/time_converter.h", + "shell/common/gin_converters/usb_device_info_converter.h", "shell/common/gin_converters/value_converter.cc", "shell/common/gin_converters/value_converter.h", "shell/common/gin_helper/arguments.cc", diff --git a/shell/browser/electron_browser_client.cc b/shell/browser/electron_browser_client.cc index 51faa6ae8ab1..89e3603fd11a 100644 --- a/shell/browser/electron_browser_client.cc +++ b/shell/browser/electron_browser_client.cc @@ -1715,6 +1715,12 @@ content::BluetoothDelegate* ElectronBrowserClient::GetBluetoothDelegate() { return bluetooth_delegate_.get(); } +content::UsbDelegate* ElectronBrowserClient::GetUsbDelegate() { + if (!usb_delegate_) + usb_delegate_ = std::make_unique(); + return usb_delegate_.get(); +} + void BindBadgeServiceForServiceWorker( const content::ServiceWorkerVersionBaseInfo& info, mojo::PendingReceiver receiver) { diff --git a/shell/browser/electron_browser_client.h b/shell/browser/electron_browser_client.h index 840ff7b31e64..a410ef9781f9 100644 --- a/shell/browser/electron_browser_client.h +++ b/shell/browser/electron_browser_client.h @@ -22,6 +22,7 @@ #include "shell/browser/bluetooth/electron_bluetooth_delegate.h" #include "shell/browser/hid/electron_hid_delegate.h" #include "shell/browser/serial/electron_serial_delegate.h" +#include "shell/browser/usb/electron_usb_delegate.h" #include "third_party/blink/public/mojom/badging/badging.mojom-forward.h" namespace content { @@ -103,6 +104,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient, content::BluetoothDelegate* GetBluetoothDelegate() override; content::HidDelegate* GetHidDelegate() override; + content::UsbDelegate* GetUsbDelegate() override; content::WebAuthenticationDelegate* GetWebAuthenticationDelegate() override; @@ -326,6 +328,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient, std::unique_ptr serial_delegate_; std::unique_ptr bluetooth_delegate_; + std::unique_ptr usb_delegate_; std::unique_ptr hid_delegate_; std::unique_ptr web_authentication_delegate_; diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc index 14e4e1514984..3e1706747ce7 100644 --- a/shell/browser/electron_browser_context.cc +++ b/shell/browser/electron_browser_context.cc @@ -51,6 +51,7 @@ #include "shell/browser/web_view_manager.h" #include "shell/browser/zoom_level_delegate.h" #include "shell/common/application_info.h" +#include "shell/common/electron_constants.h" #include "shell/common/electron_paths.h" #include "shell/common/gin_converters/frame_converter.h" #include "shell/common/gin_helper/error_thrower.h" @@ -583,19 +584,22 @@ bool ElectronBrowserContext::DoesDeviceMatch( const base::Value* device_to_compare, blink::PermissionType permission_type) { if (permission_type == - static_cast( - WebContentsPermissionHelper::PermissionType::HID)) { - if (device.GetDict().FindInt(kHidVendorIdKey) != - device_to_compare->GetDict().FindInt(kHidVendorIdKey) || - device.GetDict().FindInt(kHidProductIdKey) != - device_to_compare->GetDict().FindInt(kHidProductIdKey)) { + static_cast( + WebContentsPermissionHelper::PermissionType::HID) || + permission_type == + static_cast( + WebContentsPermissionHelper::PermissionType::USB)) { + if (device.GetDict().FindInt(kDeviceVendorIdKey) != + device_to_compare->GetDict().FindInt(kDeviceVendorIdKey) || + device.GetDict().FindInt(kDeviceProductIdKey) != + device_to_compare->GetDict().FindInt(kDeviceProductIdKey)) { return false; } const auto* serial_number = - device_to_compare->GetDict().FindString(kHidSerialNumberKey); + device_to_compare->GetDict().FindString(kDeviceSerialNumberKey); const auto* device_serial_number = - device.GetDict().FindString(kHidSerialNumberKey); + device.GetDict().FindString(kDeviceSerialNumberKey); if (serial_number && device_serial_number && *device_serial_number == *serial_number) diff --git a/shell/browser/feature_list.cc b/shell/browser/feature_list.cc index 47c057fdae4b..0cf9374876bc 100644 --- a/shell/browser/feature_list.cc +++ b/shell/browser/feature_list.cc @@ -17,6 +17,10 @@ #include "net/base/features.h" #include "services/network/public/cpp/features.h" +#if BUILDFLAG(IS_MAC) +#include "device/base/features.h" // nogncheck +#endif + namespace electron { void InitializeFeatureList() { @@ -32,6 +36,11 @@ void InitializeFeatureList() { disable_features += std::string(",") + features::kSpareRendererForSitePerProcess.name; +#if BUILDFLAG(IS_MAC) + // Needed for WebUSB implementation + enable_features += std::string(",") + device::kNewUsbBackend.name; +#endif + #if !BUILDFLAG(ENABLE_PICTURE_IN_PICTURE) disable_features += std::string(",") + media::kPictureInPicture.name; #endif diff --git a/shell/browser/hid/hid_chooser_context.cc b/shell/browser/hid/hid_chooser_context.cc index 108ecb28f49f..4445a2b7b142 100644 --- a/shell/browser/hid/hid_chooser_context.cc +++ b/shell/browser/hid/hid_chooser_context.cc @@ -22,6 +22,7 @@ #include "shell/browser/api/electron_api_session.h" #include "shell/browser/electron_permission_manager.h" #include "shell/browser/web_contents_permission_helper.h" +#include "shell/common/electron_constants.h" #include "shell/common/gin_converters/content_converter.h" #include "shell/common/gin_converters/frame_converter.h" #include "shell/common/gin_converters/hid_device_info_converter.h" @@ -35,9 +36,6 @@ namespace electron { const char kHidDeviceNameKey[] = "name"; const char kHidGuidKey[] = "guid"; -const char kHidVendorIdKey[] = "vendorId"; -const char kHidProductIdKey[] = "productId"; -const char kHidSerialNumberKey[] = "serialNumber"; HidChooserContext::HidChooserContext(ElectronBrowserContext* context) : browser_context_(context) {} @@ -76,12 +74,12 @@ base::Value HidChooserContext::DeviceInfoToValue( value.SetStringKey( kHidDeviceNameKey, base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device))); - value.SetIntKey(kHidVendorIdKey, device.vendor_id); - value.SetIntKey(kHidProductIdKey, device.product_id); + value.SetIntKey(kDeviceVendorIdKey, device.vendor_id); + value.SetIntKey(kDeviceProductIdKey, device.product_id); if (HidChooserContext::CanStorePersistentEntry(device)) { // Use the USB serial number as a persistent identifier. If it is // unavailable, only ephemeral permissions may be granted. - value.SetStringKey(kHidSerialNumberKey, device.serial_number); + value.SetStringKey(kDeviceSerialNumberKey, device.serial_number); } else { // The GUID is a temporary ID created on connection that remains valid until // the device is disconnected. Ephemeral permissions are keyed by this ID diff --git a/shell/browser/hid/hid_chooser_context.h b/shell/browser/hid/hid_chooser_context.h index e748eeb39615..6c174427f452 100644 --- a/shell/browser/hid/hid_chooser_context.h +++ b/shell/browser/hid/hid_chooser_context.h @@ -33,9 +33,7 @@ namespace electron { extern const char kHidDeviceNameKey[]; extern const char kHidGuidKey[]; -extern const char kHidVendorIdKey[]; extern const char kHidProductIdKey[]; -extern const char kHidSerialNumberKey[]; // Manages the internal state and connection to the device service for the // Human Interface Device (HID) chooser UI. diff --git a/shell/browser/serial/serial_chooser_context.h b/shell/browser/serial/serial_chooser_context.h index 1c3e2d7641dc..f64597b6677d 100644 --- a/shell/browser/serial/serial_chooser_context.h +++ b/shell/browser/serial/serial_chooser_context.h @@ -29,9 +29,6 @@ class Value; namespace electron { -extern const char kHidVendorIdKey[]; -extern const char kHidProductIdKey[]; - #if BUILDFLAG(IS_WIN) extern const char kDeviceInstanceIdKey[]; #else diff --git a/shell/browser/usb/electron_usb_delegate.cc b/shell/browser/usb/electron_usb_delegate.cc new file mode 100644 index 000000000000..a5bb4bc0e375 --- /dev/null +++ b/shell/browser/usb/electron_usb_delegate.cc @@ -0,0 +1,317 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/usb/electron_usb_delegate.h" + +#include + +#include "base/containers/contains.h" +#include "base/containers/cxx20_erase.h" +#include "base/observer_list.h" +#include "base/observer_list_types.h" +#include "base/scoped_observation.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "extensions/buildflags/buildflags.h" +#include "services/device/public/mojom/usb_enumeration_options.mojom.h" +#include "shell/browser/electron_permission_manager.h" +#include "shell/browser/usb/usb_chooser_context.h" +#include "shell/browser/usb/usb_chooser_context_factory.h" +#include "shell/browser/usb/usb_chooser_controller.h" +#include "shell/browser/web_contents_permission_helper.h" + +#if BUILDFLAG(ENABLE_EXTENSIONS) +#include "base/containers/fixed_flat_set.h" +#include "chrome/common/chrome_features.h" +#include "extensions/browser/extension_registry.h" +#include "extensions/common/constants.h" +#include "extensions/common/extension.h" +#include "services/device/public/mojom/usb_device.mojom.h" +#endif + +namespace { + +using ::content::UsbChooser; + +electron::UsbChooserContext* GetChooserContext( + content::BrowserContext* browser_context) { + return electron::UsbChooserContextFactory::GetForBrowserContext( + browser_context); +} + +#if BUILDFLAG(ENABLE_EXTENSIONS) +// These extensions can claim the smart card USB class and automatically gain +// permissions for devices that have an interface with this class. +constexpr auto kSmartCardPrivilegedExtensionIds = + base::MakeFixedFlatSet({ + // Smart Card Connector Extension and its Beta version, see + // crbug.com/1233881. + "khpfeaanjngmcnplbdlpegiifgpfgdco", + "mockcojkppdndnhgonljagclgpkjbkek", + }); + +bool DeviceHasInterfaceWithClass( + const device::mojom::UsbDeviceInfo& device_info, + uint8_t interface_class) { + for (const auto& configuration : device_info.configurations) { + for (const auto& interface : configuration->interfaces) { + for (const auto& alternate : interface->alternates) { + if (alternate->class_code == interface_class) + return true; + } + } + } + return false; +} +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + +bool IsDevicePermissionAutoGranted( + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device_info) { +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Note: The `DeviceHasInterfaceWithClass()` call is made after checking the + // origin, since that method call is expensive. + if (origin.scheme() == extensions::kExtensionScheme && + base::Contains(kSmartCardPrivilegedExtensionIds, origin.host()) && + DeviceHasInterfaceWithClass(device_info, + device::mojom::kUsbSmartCardClass)) { + return true; + } +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + + return false; +} + +} // namespace + +namespace electron { + +// Manages the UsbDelegate observers for a single browser context. +class ElectronUsbDelegate::ContextObservation + : public UsbChooserContext::DeviceObserver { + public: + ContextObservation(ElectronUsbDelegate* parent, + content::BrowserContext* browser_context) + : parent_(parent), browser_context_(browser_context) { + auto* chooser_context = GetChooserContext(browser_context_); + device_observation_.Observe(chooser_context); + } + ContextObservation(ContextObservation&) = delete; + ContextObservation& operator=(ContextObservation&) = delete; + ~ContextObservation() override = default; + + // UsbChooserContext::DeviceObserver: + void OnDeviceAdded(const device::mojom::UsbDeviceInfo& device_info) override { + for (auto& observer : observer_list_) + observer.OnDeviceAdded(device_info); + } + + void OnDeviceRemoved( + const device::mojom::UsbDeviceInfo& device_info) override { + for (auto& observer : observer_list_) + observer.OnDeviceRemoved(device_info); + } + + void OnDeviceManagerConnectionError() override { + for (auto& observer : observer_list_) + observer.OnDeviceManagerConnectionError(); + } + + void OnBrowserContextShutdown() override { + parent_->observations_.erase(browser_context_); + // Return since `this` is now deleted. + } + + void AddObserver(content::UsbDelegate::Observer* observer) { + observer_list_.AddObserver(observer); + } + + void RemoveObserver(content::UsbDelegate::Observer* observer) { + observer_list_.RemoveObserver(observer); + } + + private: + // Safe because `parent_` owns `this`. + const raw_ptr parent_; + + // Safe because `this` is destroyed when the context is lost. + const raw_ptr browser_context_; + + base::ScopedObservation + device_observation_{this}; + base::ObserverList observer_list_; +}; + +ElectronUsbDelegate::ElectronUsbDelegate() = default; + +ElectronUsbDelegate::~ElectronUsbDelegate() = default; + +void ElectronUsbDelegate::AdjustProtectedInterfaceClasses( + content::BrowserContext* browser_context, + const url::Origin& origin, + content::RenderFrameHost* frame, + std::vector& classes) { + // Isolated Apps have unrestricted access to any USB interface class. + if (frame && frame->GetWebExposedIsolationLevel() >= + content::RenderFrameHost::WebExposedIsolationLevel:: + kMaybeIsolatedApplication) { + // TODO(https://crbug.com/1236706): Should the list of interface classes the + // app expects to claim be encoded in the Web App Manifest? + classes.clear(); + return; + } + +#if BUILDFLAG(ENABLE_EXTENSIONS) + // Don't enforce protected interface classes for Chrome Apps since the + // chrome.usb API has no such restriction. + if (origin.scheme() == extensions::kExtensionScheme) { + auto* extension_registry = + extensions::ExtensionRegistry::Get(browser_context); + if (extension_registry) { + const extensions::Extension* extension = + extension_registry->enabled_extensions().GetByID(origin.host()); + if (extension && extension->is_platform_app()) { + classes.clear(); + return; + } + } + } + + if (origin.scheme() == extensions::kExtensionScheme && + base::Contains(kSmartCardPrivilegedExtensionIds, origin.host())) { + base::Erase(classes, device::mojom::kUsbSmartCardClass); + } +#endif // BUILDFLAG(ENABLE_EXTENSIONS) +} + +std::unique_ptr ElectronUsbDelegate::RunChooser( + content::RenderFrameHost& frame, + std::vector filters, + blink::mojom::WebUsbService::GetPermissionCallback callback) { + UsbChooserController* controller = ControllerForFrame(&frame); + if (controller) { + DeleteControllerForFrame(&frame); + } + AddControllerForFrame(&frame, std::move(filters), std::move(callback)); + // Return a nullptr because the return value isn't used for anything. The + // return value is simply used in Chromium to cleanup the chooser UI once the + // usb service is destroyed. + return nullptr; +} + +bool ElectronUsbDelegate::CanRequestDevicePermission( + content::BrowserContext* browser_context, + const url::Origin& origin) { + base::Value::Dict details; + details.Set("securityOrigin", origin.GetURL().spec()); + auto* permission_manager = static_cast( + browser_context->GetPermissionControllerDelegate()); + return permission_manager->CheckPermissionWithDetails( + static_cast( + WebContentsPermissionHelper::PermissionType::USB), + nullptr, origin.GetURL(), std::move(details)); +} + +void ElectronUsbDelegate::RevokeDevicePermissionWebInitiated( + content::BrowserContext* browser_context, + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device) { + GetChooserContext(browser_context) + ->RevokeDevicePermissionWebInitiated(origin, device); +} + +const device::mojom::UsbDeviceInfo* ElectronUsbDelegate::GetDeviceInfo( + content::BrowserContext* browser_context, + const std::string& guid) { + return GetChooserContext(browser_context)->GetDeviceInfo(guid); +} + +bool ElectronUsbDelegate::HasDevicePermission( + content::BrowserContext* browser_context, + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device) { + if (IsDevicePermissionAutoGranted(origin, device)) + return true; + + return GetChooserContext(browser_context) + ->HasDevicePermission(origin, device); +} + +void ElectronUsbDelegate::GetDevices( + content::BrowserContext* browser_context, + blink::mojom::WebUsbService::GetDevicesCallback callback) { + GetChooserContext(browser_context)->GetDevices(std::move(callback)); +} + +void ElectronUsbDelegate::GetDevice( + content::BrowserContext* browser_context, + const std::string& guid, + base::span blocked_interface_classes, + mojo::PendingReceiver device_receiver, + mojo::PendingRemote device_client) { + GetChooserContext(browser_context) + ->GetDevice(guid, blocked_interface_classes, std::move(device_receiver), + std::move(device_client)); +} + +void ElectronUsbDelegate::AddObserver(content::BrowserContext* browser_context, + Observer* observer) { + GetContextObserver(browser_context)->AddObserver(observer); +} + +void ElectronUsbDelegate::RemoveObserver( + content::BrowserContext* browser_context, + Observer* observer) { + GetContextObserver(browser_context)->RemoveObserver(observer); +} + +ElectronUsbDelegate::ContextObservation* +ElectronUsbDelegate::GetContextObserver( + content::BrowserContext* browser_context) { + if (!base::Contains(observations_, browser_context)) { + observations_.emplace(browser_context, std::make_unique( + this, browser_context)); + } + return observations_[browser_context].get(); +} + +bool ElectronUsbDelegate::IsServiceWorkerAllowedForOrigin( + const url::Origin& origin) { +#if BUILDFLAG(ENABLE_EXTENSIONS) + // WebUSB is only available on extension service workers for now. + if (base::FeatureList::IsEnabled( + features::kEnableWebUsbOnExtensionServiceWorker) && + origin.scheme() == extensions::kExtensionScheme) { + return true; + } +#endif // BUILDFLAG(ENABLE_EXTENSIONS) + return false; +} + +UsbChooserController* ElectronUsbDelegate::ControllerForFrame( + content::RenderFrameHost* render_frame_host) { + auto mapping = controller_map_.find(render_frame_host); + return mapping == controller_map_.end() ? nullptr : mapping->second.get(); +} + +UsbChooserController* ElectronUsbDelegate::AddControllerForFrame( + content::RenderFrameHost* render_frame_host, + std::vector filters, + blink::mojom::WebUsbService::GetPermissionCallback 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 ElectronUsbDelegate::DeleteControllerForFrame( + content::RenderFrameHost* render_frame_host) { + controller_map_.erase(render_frame_host); +} + +} // namespace electron diff --git a/shell/browser/usb/electron_usb_delegate.h b/shell/browser/usb/electron_usb_delegate.h new file mode 100644 index 000000000000..943921bfb70b --- /dev/null +++ b/shell/browser/usb/electron_usb_delegate.h @@ -0,0 +1,106 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_ +#define ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_ + +#include +#include +#include +#include + +#include "base/containers/span.h" +#include "base/memory/weak_ptr.h" +#include "content/public/browser/usb_chooser.h" +#include "content/public/browser/usb_delegate.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "services/device/public/mojom/usb_device.mojom-forward.h" +#include "services/device/public/mojom/usb_enumeration_options.mojom-forward.h" +#include "services/device/public/mojom/usb_manager.mojom-forward.h" +#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h" +#include "url/origin.h" + +namespace content { +class BrowserContext; +class RenderFrameHost; +} // namespace content + +namespace electron { + +class UsbChooserController; + +class ElectronUsbDelegate : public content::UsbDelegate { + public: + ElectronUsbDelegate(); + ElectronUsbDelegate(ElectronUsbDelegate&&) = delete; + ElectronUsbDelegate& operator=(ElectronUsbDelegate&) = delete; + ~ElectronUsbDelegate() override; + + // content::UsbDelegate: + void AdjustProtectedInterfaceClasses(content::BrowserContext* browser_context, + const url::Origin& origin, + content::RenderFrameHost* frame, + std::vector& classes) override; + std::unique_ptr RunChooser( + content::RenderFrameHost& frame, + std::vector filters, + blink::mojom::WebUsbService::GetPermissionCallback callback) override; + bool CanRequestDevicePermission(content::BrowserContext* browser_context, + const url::Origin& origin) override; + void RevokeDevicePermissionWebInitiated( + content::BrowserContext* browser_context, + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device) override; + const device::mojom::UsbDeviceInfo* GetDeviceInfo( + content::BrowserContext* browser_context, + const std::string& guid) override; + bool HasDevicePermission(content::BrowserContext* browser_context, + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device) override; + void GetDevices( + content::BrowserContext* browser_context, + blink::mojom::WebUsbService::GetDevicesCallback callback) override; + void GetDevice( + content::BrowserContext* browser_context, + const std::string& guid, + base::span blocked_interface_classes, + mojo::PendingReceiver device_receiver, + mojo::PendingRemote device_client) + override; + void AddObserver(content::BrowserContext* browser_context, + Observer* observer) override; + void RemoveObserver(content::BrowserContext* browser_context, + Observer* observer) override; + bool IsServiceWorkerAllowedForOrigin(const url::Origin& origin) override; + + void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host); + + private: + UsbChooserController* ControllerForFrame( + content::RenderFrameHost* render_frame_host); + + UsbChooserController* AddControllerForFrame( + content::RenderFrameHost* render_frame_host, + std::vector filters, + blink::mojom::WebUsbService::GetPermissionCallback callback); + + class ContextObservation; + + ContextObservation* GetContextObserver( + content::BrowserContext* browser_context); + + base::flat_map> + observations_; + + std::unordered_map> + controller_map_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_ diff --git a/shell/browser/usb/usb_chooser_context.cc b/shell/browser/usb/usb_chooser_context.cc new file mode 100644 index 000000000000..79f101d8c0ce --- /dev/null +++ b/shell/browser/usb/usb_chooser_context.cc @@ -0,0 +1,354 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/usb/usb_chooser_context.h" + +#include +#include +#include + +#include "base/bind.h" +#include "base/containers/contains.h" +#include "base/observer_list.h" +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "base/threading/sequenced_task_runner_handle.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/content_settings/core/common/content_settings.h" +#include "content/public/browser/device_service.h" +#include "services/device/public/cpp/usb/usb_ids.h" +#include "services/device/public/mojom/usb_device.mojom.h" +#include "shell/browser/api/electron_api_session.h" +#include "shell/browser/electron_permission_manager.h" +#include "shell/browser/web_contents_permission_helper.h" +#include "shell/common/electron_constants.h" +#include "shell/common/gin_converters/usb_device_info_converter.h" +#include "shell/common/node_includes.h" +#include "ui/base/l10n/l10n_util.h" + +namespace { + +constexpr char kDeviceNameKey[] = "productName"; +constexpr char kDeviceIdKey[] = "deviceId"; +constexpr int kUsbClassMassStorage = 0x08; + +bool CanStorePersistentEntry(const device::mojom::UsbDeviceInfo& device_info) { + return device_info.serial_number && !device_info.serial_number->empty(); +} + +bool IsMassStorageInterface(const device::mojom::UsbInterfaceInfo& interface) { + for (auto& alternate : interface.alternates) { + if (alternate->class_code == kUsbClassMassStorage) + return true; + } + return false; +} + +bool ShouldExposeDevice(const device::mojom::UsbDeviceInfo& device_info) { + // blink::USBDevice::claimInterface() disallows claiming mass storage + // interfaces, but explicitly prevent access in the browser process as + // ChromeOS would allow these interfaces to be claimed. + for (auto& configuration : device_info.configurations) { + if (configuration->interfaces.size() == 0) { + return true; + } + for (auto& interface : configuration->interfaces) { + if (!IsMassStorageInterface(*interface)) + return true; + } + } + return false; +} + +} // namespace + +namespace electron { + +void UsbChooserContext::DeviceObserver::OnDeviceAdded( + const device::mojom::UsbDeviceInfo& device_info) {} + +void UsbChooserContext::DeviceObserver::OnDeviceRemoved( + const device::mojom::UsbDeviceInfo& device_info) {} + +void UsbChooserContext::DeviceObserver::OnDeviceManagerConnectionError() {} + +UsbChooserContext::UsbChooserContext(ElectronBrowserContext* context) + : browser_context_(context) {} + +// static +base::Value UsbChooserContext::DeviceInfoToValue( + const device::mojom::UsbDeviceInfo& device_info) { + base::Value device_value(base::Value::Type::DICTIONARY); + device_value.SetStringKey(kDeviceNameKey, device_info.product_name + ? *device_info.product_name + : base::StringPiece16()); + device_value.SetIntKey(kDeviceVendorIdKey, device_info.vendor_id); + device_value.SetIntKey(kDeviceProductIdKey, device_info.product_id); + + if (device_info.manufacturer_name) { + device_value.SetStringKey("manufacturerName", + *device_info.manufacturer_name); + } + + // CanStorePersistentEntry checks if |device_info.serial_number| is not empty. + if (CanStorePersistentEntry(device_info)) { + device_value.SetStringKey(kDeviceSerialNumberKey, + *device_info.serial_number); + } + + device_value.SetStringKey(kDeviceIdKey, device_info.guid); + + device_value.SetIntKey("usbVersionMajor", device_info.usb_version_major); + device_value.SetIntKey("usbVersionMinor", device_info.usb_version_minor); + device_value.SetIntKey("usbVersionSubminor", + device_info.usb_version_subminor); + device_value.SetIntKey("deviceClass", device_info.class_code); + device_value.SetIntKey("deviceSubclass", device_info.subclass_code); + device_value.SetIntKey("deviceProtocol", device_info.protocol_code); + device_value.SetIntKey("deviceVersionMajor", + device_info.device_version_major); + device_value.SetIntKey("deviceVersionMinor", + device_info.device_version_minor); + device_value.SetIntKey("deviceVersionSubminor", + device_info.device_version_subminor); + return device_value; +} + +void UsbChooserContext::InitDeviceList( + std::vector devices) { + for (auto& device_info : devices) { + DCHECK(device_info); + if (ShouldExposeDevice(*device_info)) { + devices_.insert( + std::make_pair(device_info->guid, std::move(device_info))); + } + } + is_initialized_ = true; + + while (!pending_get_devices_requests_.empty()) { + std::vector device_list; + for (const auto& entry : devices_) { + device_list.push_back(entry.second->Clone()); + } + std::move(pending_get_devices_requests_.front()) + .Run(std::move(device_list)); + pending_get_devices_requests_.pop(); + } +} + +void UsbChooserContext::EnsureConnectionWithDeviceManager() { + if (device_manager_) + return; + + // Receive mojo::Remote from DeviceService. + content::GetDeviceService().BindUsbDeviceManager( + device_manager_.BindNewPipeAndPassReceiver()); + + SetUpDeviceManagerConnection(); +} + +void UsbChooserContext::SetUpDeviceManagerConnection() { + DCHECK(device_manager_); + device_manager_.set_disconnect_handler( + base::BindOnce(&UsbChooserContext::OnDeviceManagerConnectionError, + base::Unretained(this))); + + // Listen for added/removed device events. + DCHECK(!client_receiver_.is_bound()); + device_manager_->EnumerateDevicesAndSetClient( + client_receiver_.BindNewEndpointAndPassRemote(), + base::BindOnce(&UsbChooserContext::InitDeviceList, + weak_factory_.GetWeakPtr())); +} + +UsbChooserContext::~UsbChooserContext() { + OnDeviceManagerConnectionError(); + for (auto& observer : device_observer_list_) { + observer.OnBrowserContextShutdown(); + DCHECK(!device_observer_list_.HasObserver(&observer)); + } +} + +void UsbChooserContext::RevokeDevicePermissionWebInitiated( + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device) { + DCHECK(base::Contains(devices_, device.guid)); + RevokeObjectPermissionInternal(origin, DeviceInfoToValue(device), + /*revoked_by_website=*/true); +} + +void UsbChooserContext::RevokeObjectPermissionInternal( + const url::Origin& origin, + const base::Value& object, + bool revoked_by_website = false) { + if (object.FindStringKey(kDeviceSerialNumberKey)) { + auto* permission_manager = static_cast( + browser_context_->GetPermissionControllerDelegate()); + permission_manager->RevokeDevicePermission( + static_cast( + WebContentsPermissionHelper::PermissionType::USB), + origin, object, browser_context_); + } else { + const std::string* guid = object.FindStringKey(kDeviceIdKey); + auto it = ephemeral_devices_.find(origin); + if (it != ephemeral_devices_.end()) { + it->second.erase(*guid); + if (it->second.empty()) + ephemeral_devices_.erase(it); + } + } + + api::Session* session = api::Session::FromBrowserContext(browser_context_); + if (session) { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + gin_helper::Dictionary details = + gin_helper::Dictionary::CreateEmpty(isolate); + details.Set("device", object.Clone()); + details.Set("origin", origin.Serialize()); + session->Emit("usb-device-revoked", details); + } +} + +void UsbChooserContext::GrantDevicePermission( + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device_info) { + if (CanStorePersistentEntry(device_info)) { + auto* permission_manager = static_cast( + browser_context_->GetPermissionControllerDelegate()); + permission_manager->GrantDevicePermission( + static_cast( + WebContentsPermissionHelper::PermissionType::USB), + origin, DeviceInfoToValue(device_info), browser_context_); + } else { + ephemeral_devices_[origin].insert(device_info.guid); + } +} + +bool UsbChooserContext::HasDevicePermission( + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device_info) { + auto it = ephemeral_devices_.find(origin); + if (it != ephemeral_devices_.end() && + base::Contains(it->second, device_info.guid)) { + return true; + } + + auto* permission_manager = static_cast( + browser_context_->GetPermissionControllerDelegate()); + + return permission_manager->CheckDevicePermission( + static_cast( + WebContentsPermissionHelper::PermissionType::USB), + origin, DeviceInfoToValue(device_info), browser_context_); +} + +void UsbChooserContext::GetDevices( + device::mojom::UsbDeviceManager::GetDevicesCallback callback) { + if (!is_initialized_) { + EnsureConnectionWithDeviceManager(); + pending_get_devices_requests_.push(std::move(callback)); + return; + } + + std::vector device_list; + for (const auto& pair : devices_) { + device_list.push_back(pair.second->Clone()); + } + base::SequencedTaskRunnerHandle::Get()->PostTask( + FROM_HERE, base::BindOnce(std::move(callback), std::move(device_list))); +} + +void UsbChooserContext::GetDevice( + const std::string& guid, + base::span blocked_interface_classes, + mojo::PendingReceiver device_receiver, + mojo::PendingRemote device_client) { + EnsureConnectionWithDeviceManager(); + device_manager_->GetDevice( + guid, + std::vector(blocked_interface_classes.begin(), + blocked_interface_classes.end()), + std::move(device_receiver), std::move(device_client)); +} + +const device::mojom::UsbDeviceInfo* UsbChooserContext::GetDeviceInfo( + const std::string& guid) { + DCHECK(is_initialized_); + auto it = devices_.find(guid); + return it == devices_.end() ? nullptr : it->second.get(); +} + +void UsbChooserContext::AddObserver(DeviceObserver* observer) { + EnsureConnectionWithDeviceManager(); + device_observer_list_.AddObserver(observer); +} + +void UsbChooserContext::RemoveObserver(DeviceObserver* observer) { + device_observer_list_.RemoveObserver(observer); +} + +base::WeakPtr UsbChooserContext::AsWeakPtr() { + return weak_factory_.GetWeakPtr(); +} + +void UsbChooserContext::OnDeviceAdded( + device::mojom::UsbDeviceInfoPtr device_info) { + DCHECK(device_info); + // Update the device list. + DCHECK(!base::Contains(devices_, device_info->guid)); + if (!ShouldExposeDevice(*device_info)) + return; + devices_.insert(std::make_pair(device_info->guid, device_info->Clone())); + + // Notify all observers. + for (auto& observer : device_observer_list_) + observer.OnDeviceAdded(*device_info); +} + +void UsbChooserContext::OnDeviceRemoved( + device::mojom::UsbDeviceInfoPtr device_info) { + DCHECK(device_info); + + if (!ShouldExposeDevice(*device_info)) { + DCHECK(!base::Contains(devices_, device_info->guid)); + return; + } + + // Update the device list. + DCHECK(base::Contains(devices_, device_info->guid)); + devices_.erase(device_info->guid); + + // Notify all device observers. + for (auto& observer : device_observer_list_) + observer.OnDeviceRemoved(*device_info); + + // If the device was persistent, return. Otherwise, notify all permission + // observers that its permissions were revoked. + if (device_info->serial_number && + !device_info->serial_number.value().empty()) { + return; + } + for (auto& map_entry : ephemeral_devices_) { + map_entry.second.erase(device_info->guid); + } +} + +void UsbChooserContext::OnDeviceManagerConnectionError() { + device_manager_.reset(); + client_receiver_.reset(); + devices_.clear(); + is_initialized_ = false; + + ephemeral_devices_.clear(); + + // Notify all device observers. + for (auto& observer : device_observer_list_) + observer.OnDeviceManagerConnectionError(); +} + +} // namespace electron diff --git a/shell/browser/usb/usb_chooser_context.h b/shell/browser/usb/usb_chooser_context.h new file mode 100644 index 000000000000..76c55b0dd16c --- /dev/null +++ b/shell/browser/usb/usb_chooser_context.h @@ -0,0 +1,122 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_ +#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_ + +#include +#include +#include +#include +#include +#include + +#include "base/containers/queue.h" +#include "base/observer_list.h" +#include "base/values.h" +#include "build/build_config.h" +#include "components/keyed_service/core/keyed_service.h" +#include "mojo/public/cpp/bindings/associated_receiver.h" +#include "mojo/public/cpp/bindings/pending_receiver.h" +#include "mojo/public/cpp/bindings/pending_remote.h" +#include "mojo/public/cpp/bindings/remote.h" +#include "services/device/public/mojom/usb_manager.mojom.h" +#include "services/device/public/mojom/usb_manager_client.mojom.h" +#include "shell/browser/electron_browser_context.h" +#include "url/origin.h" + +namespace electron { + +class UsbChooserContext : public KeyedService, + public device::mojom::UsbDeviceManagerClient { + public: + explicit UsbChooserContext(ElectronBrowserContext* context); + + UsbChooserContext(const UsbChooserContext&) = delete; + UsbChooserContext& operator=(const UsbChooserContext&) = delete; + + ~UsbChooserContext() override; + + // This observer can be used to be notified of changes to USB devices that are + // connected. + class DeviceObserver : public base::CheckedObserver { + public: + virtual void OnDeviceAdded(const device::mojom::UsbDeviceInfo&); + virtual void OnDeviceRemoved(const device::mojom::UsbDeviceInfo&); + virtual void OnDeviceManagerConnectionError(); + + // Called when the BrowserContext is shutting down. Observers must remove + // themselves before returning. + virtual void OnBrowserContextShutdown() = 0; + }; + + static base::Value DeviceInfoToValue( + const device::mojom::UsbDeviceInfo& device_info); + + // Grants |origin| access to the USB device. + void GrantDevicePermission(const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device_info); + + // Checks if |origin| has access to a device with |device_info|. + bool HasDevicePermission(const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device_info); + + // Revokes |origin| access to the USB device ordered by website. + void RevokeDevicePermissionWebInitiated( + const url::Origin& origin, + const device::mojom::UsbDeviceInfo& device); + + void AddObserver(DeviceObserver* observer); + void RemoveObserver(DeviceObserver* observer); + + // Forward UsbDeviceManager methods. + void GetDevices(device::mojom::UsbDeviceManager::GetDevicesCallback callback); + void GetDevice( + const std::string& guid, + base::span blocked_interface_classes, + mojo::PendingReceiver device_receiver, + mojo::PendingRemote device_client); + + // This method should only be called when you are sure that |devices_| has + // been initialized. It will return nullptr if the guid cannot be found. + const device::mojom::UsbDeviceInfo* GetDeviceInfo(const std::string& guid); + + base::WeakPtr AsWeakPtr(); + + void InitDeviceList(std::vector<::device::mojom::UsbDeviceInfoPtr> devices); + + private: + // device::mojom::UsbDeviceManagerClient implementation. + void OnDeviceAdded(device::mojom::UsbDeviceInfoPtr device_info) override; + void OnDeviceRemoved(device::mojom::UsbDeviceInfoPtr device_info) override; + + void RevokeObjectPermissionInternal(const url::Origin& origin, + const base::Value& object, + bool revoked_by_website); + + void OnDeviceManagerConnectionError(); + void EnsureConnectionWithDeviceManager(); + void SetUpDeviceManagerConnection(); + + bool is_initialized_ = false; + base::queue + pending_get_devices_requests_; + + std::map> ephemeral_devices_; + std::map devices_; + + // Connection to |device_manager_instance_|. + mojo::Remote device_manager_; + mojo::AssociatedReceiver + client_receiver_{this}; + base::ObserverList device_observer_list_; + + ElectronBrowserContext* browser_context_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_ diff --git a/shell/browser/usb/usb_chooser_context_factory.cc b/shell/browser/usb/usb_chooser_context_factory.cc new file mode 100644 index 000000000000..28fa16f5d10f --- /dev/null +++ b/shell/browser/usb/usb_chooser_context_factory.cc @@ -0,0 +1,45 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/usb/usb_chooser_context_factory.h" + +#include "components/keyed_service/content/browser_context_dependency_manager.h" +#include "shell/browser/electron_browser_context.h" +#include "shell/browser/usb/usb_chooser_context.h" + +namespace electron { + +UsbChooserContextFactory::UsbChooserContextFactory() + : BrowserContextKeyedServiceFactory( + "UsbChooserContext", + BrowserContextDependencyManager::GetInstance()) {} + +UsbChooserContextFactory::~UsbChooserContextFactory() {} + +KeyedService* UsbChooserContextFactory::BuildServiceInstanceFor( + content::BrowserContext* context) const { + auto* browser_context = + static_cast(context); + return new UsbChooserContext(browser_context); +} + +// static +UsbChooserContextFactory* UsbChooserContextFactory::GetInstance() { + return base::Singleton::get(); +} + +// static +UsbChooserContext* UsbChooserContextFactory::GetForBrowserContext( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, /*create=*/true)); +} + +UsbChooserContext* UsbChooserContextFactory::GetForBrowserContextIfExists( + content::BrowserContext* context) { + return static_cast( + GetInstance()->GetServiceForBrowserContext(context, /*create=*/false)); +} + +} // namespace electron diff --git a/shell/browser/usb/usb_chooser_context_factory.h b/shell/browser/usb/usb_chooser_context_factory.h new file mode 100644 index 000000000000..14e18df2e831 --- /dev/null +++ b/shell/browser/usb/usb_chooser_context_factory.h @@ -0,0 +1,39 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_ +#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_ + +#include "base/memory/singleton.h" +#include "components/keyed_service/content/browser_context_keyed_service_factory.h" + +namespace electron { + +class UsbChooserContext; + +class UsbChooserContextFactory : public BrowserContextKeyedServiceFactory { + public: + static UsbChooserContext* GetForBrowserContext( + content::BrowserContext* context); + static UsbChooserContext* GetForBrowserContextIfExists( + content::BrowserContext* context); + static UsbChooserContextFactory* GetInstance(); + + UsbChooserContextFactory(const UsbChooserContextFactory&) = delete; + UsbChooserContextFactory& operator=(const UsbChooserContextFactory&) = delete; + + private: + friend struct base::DefaultSingletonTraits; + + UsbChooserContextFactory(); + ~UsbChooserContextFactory() override; + + // BrowserContextKeyedServiceFactory methods: + KeyedService* BuildServiceInstanceFor( + content::BrowserContext* profile) const override; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_ diff --git a/shell/browser/usb/usb_chooser_controller.cc b/shell/browser/usb/usb_chooser_controller.cc new file mode 100644 index 000000000000..87e5457884a2 --- /dev/null +++ b/shell/browser/usb/usb_chooser_controller.cc @@ -0,0 +1,165 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#include "shell/browser/usb/usb_chooser_controller.h" + +#include +#include + +#include "base/bind.h" +#include "base/strings/stringprintf.h" +#include "base/strings/utf_string_conversions.h" +#include "build/build_config.h" +#include "components/strings/grit/components_strings.h" +#include "content/public/browser/render_frame_host.h" +#include "content/public/browser/web_contents.h" +#include "gin/data_object_builder.h" +#include "services/device/public/cpp/usb/usb_utils.h" +#include "services/device/public/mojom/usb_enumeration_options.mojom.h" +#include "shell/browser/javascript_environment.h" +#include "shell/browser/usb/usb_chooser_context_factory.h" +#include "shell/common/gin_converters/callback_converter.h" +#include "shell/common/gin_converters/content_converter.h" +#include "shell/common/gin_converters/frame_converter.h" +#include "shell/common/gin_converters/usb_device_info_converter.h" +#include "shell/common/node_includes.h" +#include "shell/common/process_util.h" +#include "ui/base/l10n/l10n_util.h" +#include "url/gurl.h" + +using content::RenderFrameHost; +using content::WebContents; + +namespace electron { + +UsbChooserController::UsbChooserController( + RenderFrameHost* render_frame_host, + std::vector device_filters, + blink::mojom::WebUsbService::GetPermissionCallback callback, + content::WebContents* web_contents, + base::WeakPtr usb_delegate) + : WebContentsObserver(web_contents), + filters_(std::move(device_filters)), + callback_(std::move(callback)), + origin_(render_frame_host->GetMainFrame()->GetLastCommittedOrigin()), + usb_delegate_(usb_delegate), + render_frame_host_id_(render_frame_host->GetGlobalId()) { + chooser_context_ = UsbChooserContextFactory::GetForBrowserContext( + web_contents->GetBrowserContext()) + ->AsWeakPtr(); + DCHECK(chooser_context_); + chooser_context_->GetDevices(base::BindOnce( + &UsbChooserController::GotUsbDeviceList, weak_factory_.GetWeakPtr())); +} + +UsbChooserController::~UsbChooserController() { + RunCallback(/*device=*/nullptr); +} + +api::Session* UsbChooserController::GetSession() { + if (!web_contents()) { + return nullptr; + } + return api::Session::FromBrowserContext(web_contents()->GetBrowserContext()); +} + +void UsbChooserController::OnDeviceAdded( + const device::mojom::UsbDeviceInfo& device_info) { + if (DisplayDevice(device_info)) { + api::Session* session = GetSession(); + if (session) { + session->Emit("usb-device-added", device_info.Clone(), web_contents()); + } + } +} + +void UsbChooserController::OnDeviceRemoved( + const device::mojom::UsbDeviceInfo& device_info) { + api::Session* session = GetSession(); + if (session) { + session->Emit("usb-device-removed", device_info.Clone(), web_contents()); + } +} + +void UsbChooserController::OnDeviceChosen(gin::Arguments* args) { + std::string device_id; + if (!args->GetNext(&device_id) || device_id.empty()) { + RunCallback(/*device=*/nullptr); + } else { + auto* device_info = chooser_context_->GetDeviceInfo(device_id); + if (device_info) { + RunCallback(device_info->Clone()); + } else { + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + node::Environment* env = node::Environment::GetCurrent(isolate); + EmitWarning(env, "The device id " + device_id + " was not found.", + "UnknownUsbDeviceId"); + RunCallback(/*device=*/nullptr); + } + } +} + +void UsbChooserController::OnBrowserContextShutdown() { + observation_.Reset(); +} + +// Get a list of devices that can be shown in the chooser bubble UI for +// user to grant permsssion. +void UsbChooserController::GotUsbDeviceList( + std::vector<::device::mojom::UsbDeviceInfoPtr> devices) { + // Listen to UsbChooserContext for OnDeviceAdded/Removed events after the + // enumeration. + if (chooser_context_) + observation_.Observe(chooser_context_.get()); + + bool prevent_default = false; + api::Session* session = GetSession(); + if (session) { + auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_); + v8::Isolate* isolate = JavascriptEnvironment::GetIsolate(); + v8::HandleScope scope(isolate); + v8::Local details = gin::DataObjectBuilder(isolate) + .Set("deviceList", devices) + .Set("frame", rfh) + .Build(); + + prevent_default = + session->Emit("select-usb-device", details, + base::AdaptCallbackForRepeating( + base::BindOnce(&UsbChooserController::OnDeviceChosen, + weak_factory_.GetWeakPtr()))); + } + if (!prevent_default) { + RunCallback(/*port=*/nullptr); + } +} + +bool UsbChooserController::DisplayDevice( + const device::mojom::UsbDeviceInfo& device_info) const { + if (!device::UsbDeviceFilterMatchesAny(filters_, device_info)) + return false; + + return true; +} + +void UsbChooserController::RenderFrameDeleted( + content::RenderFrameHost* render_frame_host) { + if (usb_delegate_) { + usb_delegate_->DeleteControllerForFrame(render_frame_host); + } +} + +void UsbChooserController::RunCallback( + device::mojom::UsbDeviceInfoPtr device_info) { + if (callback_) { + if (!chooser_context_ || !device_info) { + std::move(callback_).Run(nullptr); + } else { + chooser_context_->GrantDevicePermission(origin_, *device_info); + std::move(callback_).Run(std::move(device_info)); + } + } +} + +} // namespace electron diff --git a/shell/browser/usb/usb_chooser_controller.h b/shell/browser/usb/usb_chooser_controller.h new file mode 100644 index 000000000000..4e71bc17c110 --- /dev/null +++ b/shell/browser/usb/usb_chooser_controller.h @@ -0,0 +1,81 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_ +#define ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_ + +#include +#include +#include +#include + +#include "base/memory/raw_ptr.h" +#include "base/memory/ref_counted.h" +#include "base/memory/weak_ptr.h" +#include "base/scoped_observation.h" +#include "content/public/browser/web_contents.h" +#include "content/public/browser/web_contents_observer.h" +#include "services/device/public/mojom/usb_device.mojom.h" +#include "shell/browser/api/electron_api_session.h" +#include "shell/browser/usb/electron_usb_delegate.h" +#include "shell/browser/usb/usb_chooser_context.h" +#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h" +#include "url/origin.h" + +namespace content { +class RenderFrameHost; +} + +namespace electron { + +// UsbChooserController creates a chooser for WebUSB. +class UsbChooserController final : public UsbChooserContext::DeviceObserver, + public content::WebContentsObserver { + public: + UsbChooserController( + content::RenderFrameHost* render_frame_host, + std::vector device_filters, + blink::mojom::WebUsbService::GetPermissionCallback callback, + content::WebContents* web_contents, + base::WeakPtr usb_delegate); + + UsbChooserController(const UsbChooserController&) = delete; + UsbChooserController& operator=(const UsbChooserController&) = delete; + + ~UsbChooserController() override; + + // UsbChooserContext::DeviceObserver implementation: + void OnDeviceAdded(const device::mojom::UsbDeviceInfo& device_info) override; + void OnDeviceRemoved( + const device::mojom::UsbDeviceInfo& device_info) override; + void OnBrowserContextShutdown() override; + + // content::WebContentsObserver: + void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override; + + private: + api::Session* GetSession(); + void GotUsbDeviceList(std::vector devices); + bool DisplayDevice(const device::mojom::UsbDeviceInfo& device) const; + void RunCallback(device::mojom::UsbDeviceInfoPtr device_info); + void OnDeviceChosen(gin::Arguments* args); + + std::vector filters_; + blink::mojom::WebUsbService::GetPermissionCallback callback_; + url::Origin origin_; + + base::WeakPtr chooser_context_; + base::ScopedObservation + observation_{this}; + + base::WeakPtr usb_delegate_; + + content::GlobalRenderFrameHostId render_frame_host_id_; + + base::WeakPtrFactory weak_factory_{this}; +}; + +} // namespace electron + +#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_ diff --git a/shell/browser/web_contents_permission_helper.h b/shell/browser/web_contents_permission_helper.h index 43a5ca63ec30..972675c19f9b 100644 --- a/shell/browser/web_contents_permission_helper.h +++ b/shell/browser/web_contents_permission_helper.h @@ -29,7 +29,8 @@ class WebContentsPermissionHelper FULLSCREEN, OPEN_EXTERNAL, SERIAL, - HID + HID, + USB }; // Asynchronous Requests diff --git a/shell/common/electron_constants.cc b/shell/common/electron_constants.cc index 3d6eda0e9eea..763c2e18e5c5 100644 --- a/shell/common/electron_constants.cc +++ b/shell/common/electron_constants.cc @@ -25,6 +25,10 @@ const char kSecureProtocolDescription[] = "The connection to this site is using a strong protocol version " "and cipher suite."; +const char kDeviceVendorIdKey[] = "vendorId"; +const char kDeviceProductIdKey[] = "productId"; +const char kDeviceSerialNumberKey[] = "serialNumber"; + #if BUILDFLAG(ENABLE_RUN_AS_NODE) const char kRunAsNode[] = "ELECTRON_RUN_AS_NODE"; #endif diff --git a/shell/common/electron_constants.h b/shell/common/electron_constants.h index 4218b3e47ffc..6927411006a5 100644 --- a/shell/common/electron_constants.h +++ b/shell/common/electron_constants.h @@ -25,6 +25,11 @@ extern const char kValidCertificateDescription[]; extern const char kSecureProtocol[]; extern const char kSecureProtocolDescription[]; +// Keys for Device APIs +extern const char kDeviceVendorIdKey[]; +extern const char kDeviceProductIdKey[]; +extern const char kDeviceSerialNumberKey[]; + #if BUILDFLAG(ENABLE_RUN_AS_NODE) extern const char kRunAsNode[]; #endif diff --git a/shell/common/gin_converters/content_converter.cc b/shell/common/gin_converters/content_converter.cc index 71c2e9bb247b..2c1bf9c52a57 100644 --- a/shell/common/gin_converters/content_converter.cc +++ b/shell/common/gin_converters/content_converter.cc @@ -209,6 +209,8 @@ v8::Local Converter::ToV8( return StringToV8(isolate, "serial"); case PermissionType::HID: return StringToV8(isolate, "hid"); + case PermissionType::USB: + return StringToV8(isolate, "usb"); default: return StringToV8(isolate, "unknown"); } diff --git a/shell/common/gin_converters/usb_device_info_converter.h b/shell/common/gin_converters/usb_device_info_converter.h new file mode 100644 index 000000000000..d8ffda006b2d --- /dev/null +++ b/shell/common/gin_converters/usb_device_info_converter.h @@ -0,0 +1,27 @@ +// Copyright (c) 2022 Microsoft, Inc. +// Use of this source code is governed by the MIT license that can be +// found in the LICENSE file. + +#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_ +#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_ + +#include "gin/converter.h" +#include "services/device/public/mojom/usb_device.mojom.h" +#include "shell/browser/usb/usb_chooser_context.h" +#include "shell/common/gin_converters/value_converter.h" + +namespace gin { + +template <> +struct Converter { + static v8::Local ToV8( + v8::Isolate* isolate, + const device::mojom::UsbDeviceInfoPtr& device) { + base::Value value = electron::UsbChooserContext::DeviceInfoToValue(*device); + return gin::ConvertToV8(isolate, value); + } +}; + +} // namespace gin + +#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_DEVICE_INFO_CONVERTER_H_ diff --git a/spec/chromium-spec.ts b/spec/chromium-spec.ts index 53281f660447..dc8f8fc86a89 100644 --- a/spec/chromium-spec.ts +++ b/spec/chromium-spec.ts @@ -2857,3 +2857,164 @@ describe('navigator.hid', () => { } }); }); + +describe('navigator.usb', () => { + let w: BrowserWindow; + let server: http.Server; + let serverUrl: string; + before(async () => { + w = new BrowserWindow({ + show: false + }); + await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + server = http.createServer((req, res) => { + res.setHeader('Content-Type', 'text/html'); + res.end(''); + }); + await new Promise(resolve => server.listen(0, '127.0.0.1', resolve)); + serverUrl = `http://localhost:${(server.address() as any).port}`; + }); + + const requestDevices: any = () => { + return w.webContents.executeJavaScript(` + navigator.usb.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString()); + `, true); + }; + + const notFoundError = 'NotFoundError: Failed to execute \'requestDevice\' on \'USB\': No device selected.'; + + after(() => { + server.close(); + closeAllWindows(); + }); + afterEach(() => { + session.defaultSession.setPermissionCheckHandler(null); + session.defaultSession.setDevicePermissionHandler(null); + session.defaultSession.removeAllListeners('select-usb-device'); + }); + + it('does not return a device if select-usb-device event is not defined', async () => { + w.loadFile(path.join(fixturesPath, 'pages', 'blank.html')); + const device = await requestDevices(); + expect(device).to.equal(notFoundError); + }); + + it('does not return a device when permission denied', async () => { + let selectFired = false; + w.webContents.session.on('select-usb-device', (event, details, callback) => { + selectFired = true; + callback(); + }); + session.defaultSession.setPermissionCheckHandler(() => false); + const device = await requestDevices(); + expect(selectFired).to.be.false(); + expect(device).to.equal(notFoundError); + }); + + it('returns a device when select-usb-device event is defined', async () => { + let haveDevices = false; + let selectFired = false; + w.webContents.session.on('select-usb-device', (event, details, callback) => { + expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number'); + selectFired = true; + if (details.deviceList.length > 0) { + haveDevices = true; + callback(details.deviceList[0].deviceId); + } else { + callback(); + } + }); + const device = await requestDevices(); + expect(selectFired).to.be.true(); + if (haveDevices) { + expect(device).to.contain('[object USBDevice]'); + } else { + expect(device).to.equal(notFoundError); + } + if (haveDevices) { + // Verify that navigation will clear device permissions + const grantedDevices = await w.webContents.executeJavaScript('navigator.usb.getDevices()'); + expect(grantedDevices).to.not.be.empty(); + w.loadURL(serverUrl); + const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate'); + const frame = webFrameMain.fromId(frameProcessId, frameRoutingId); + expect(frame).to.not.be.empty(); + if (frame) { + const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.usb.getDevices()'); + expect(grantedDevicesOnNewPage).to.be.empty(); + } + } + }); + + it('returns a device when DevicePermissionHandler is defined', async () => { + let haveDevices = false; + let selectFired = false; + let gotDevicePerms = false; + w.webContents.session.on('select-usb-device', (event, details, callback) => { + selectFired = true; + if (details.deviceList.length > 0) { + const foundDevice = details.deviceList.find((device) => { + if (device.productName && device.productName !== '' && device.serialNumber && device.serialNumber !== '') { + haveDevices = true; + return true; + } + }); + if (foundDevice) { + callback(foundDevice.deviceId); + return; + } + } + callback(); + }); + session.defaultSession.setDevicePermissionHandler(() => { + gotDevicePerms = true; + return true; + }); + await w.webContents.executeJavaScript('navigator.usb.getDevices();', true); + const device = await requestDevices(); + expect(selectFired).to.be.true(); + if (haveDevices) { + expect(device).to.contain('[object USBDevice]'); + expect(gotDevicePerms).to.be.true(); + } else { + expect(device).to.equal(notFoundError); + } + }); + + it('supports device.forget()', async () => { + let deletedDeviceFromEvent; + let haveDevices = false; + w.webContents.session.on('select-usb-device', (event, details, callback) => { + if (details.deviceList.length > 0) { + haveDevices = true; + callback(details.deviceList[0].deviceId); + } else { + callback(); + } + }); + w.webContents.session.on('usb-device-revoked', (event, details) => { + deletedDeviceFromEvent = details.device; + }); + await requestDevices(); + if (haveDevices) { + const grantedDevices = await w.webContents.executeJavaScript('navigator.usb.getDevices()'); + if (grantedDevices.length > 0) { + const deletedDevice = await w.webContents.executeJavaScript(` + navigator.usb.getDevices().then(devices => { + devices[0].forget(); + return { + vendorId: devices[0].vendorId, + productId: devices[0].productId, + productName: devices[0].productName + } + }) + `); + const grantedDevices2 = await w.webContents.executeJavaScript('navigator.usb.getDevices()'); + expect(grantedDevices2.length).to.be.lessThan(grantedDevices.length); + if (deletedDevice.name !== '' && deletedDevice.productId && deletedDevice.vendorId) { + expect(deletedDeviceFromEvent).to.include(deletedDevice); + } + } + } + }); +});