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 <charles@charleskerr.com>

* 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 <charles@charleskerr.com>
This commit is contained in:
John Kleinschmidt 2022-11-22 16:50:32 -05:00 committed by GitHub
parent 2751c2b07f
commit 629c54ba36
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 1772 additions and 23 deletions

View file

@ -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\<boolean> | 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\<boolean> | 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
})

View file

@ -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.

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>WebUSB API</title>
</head>
<body>
<h1>WebUSB API</h1>
<button id="clickme">Test WebUSB</button>
<h3>USB devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
<div id="granted-devices"></div>
<h3>USB devices automatically granted access via <i>select-usb-device</i></h3>
<div id="granted-devices2"></div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -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()
})

View file

@ -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 += `<hr>${getDeviceDetails(device)}</hr>`
})
} else {
grantedDeviceList = noDevicesFoundMsg
}
document.getElementById('granted-devices').innerHTML = grantedDeviceList
grantedDeviceList = ''
try {
const grantedDevice = await navigator.usb.requestDevice({
filters: []
})
grantedDeviceList += `<hr>${getDeviceDetails(device)}</hr>`
} catch (ex) {
if (ex.name === 'NotFoundError') {
grantedDeviceList = noDevicesFoundMsg
}
}
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
}
document.getElementById('clickme').addEventListener('click',testIt)

View file

@ -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'
```

View file

@ -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",

View file

@ -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",

View file

@ -1715,6 +1715,12 @@ content::BluetoothDelegate* ElectronBrowserClient::GetBluetoothDelegate() {
return bluetooth_delegate_.get();
}
content::UsbDelegate* ElectronBrowserClient::GetUsbDelegate() {
if (!usb_delegate_)
usb_delegate_ = std::make_unique<ElectronUsbDelegate>();
return usb_delegate_.get();
}
void BindBadgeServiceForServiceWorker(
const content::ServiceWorkerVersionBaseInfo& info,
mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {

View file

@ -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<ElectronSerialDelegate> serial_delegate_;
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
std::unique_ptr<ElectronUsbDelegate> usb_delegate_;
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
std::unique_ptr<ElectronWebAuthenticationDelegate>
web_authentication_delegate_;

View file

@ -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"
@ -584,18 +585,21 @@ bool ElectronBrowserContext::DoesDeviceMatch(
blink::PermissionType permission_type) {
if (permission_type ==
static_cast<blink::PermissionType>(
WebContentsPermissionHelper::PermissionType::HID)) {
if (device.GetDict().FindInt(kHidVendorIdKey) !=
device_to_compare->GetDict().FindInt(kHidVendorIdKey) ||
device.GetDict().FindInt(kHidProductIdKey) !=
device_to_compare->GetDict().FindInt(kHidProductIdKey)) {
WebContentsPermissionHelper::PermissionType::HID) ||
permission_type ==
static_cast<blink::PermissionType>(
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)

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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 <utility>
#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<base::StringPiece>({
// 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<ElectronUsbDelegate> parent_;
// Safe because `this` is destroyed when the context is lost.
const raw_ptr<content::BrowserContext> browser_context_;
base::ScopedObservation<UsbChooserContext, UsbChooserContext::DeviceObserver>
device_observation_{this};
base::ObserverList<content::UsbDelegate::Observer> observer_list_;
};
ElectronUsbDelegate::ElectronUsbDelegate() = default;
ElectronUsbDelegate::~ElectronUsbDelegate() = default;
void ElectronUsbDelegate::AdjustProtectedInterfaceClasses(
content::BrowserContext* browser_context,
const url::Origin& origin,
content::RenderFrameHost* frame,
std::vector<uint8_t>& 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<UsbChooser> ElectronUsbDelegate::RunChooser(
content::RenderFrameHost& frame,
std::vector<device::mojom::UsbDeviceFilterPtr> 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<ElectronPermissionManager*>(
browser_context->GetPermissionControllerDelegate());
return permission_manager->CheckPermissionWithDetails(
static_cast<blink::PermissionType>(
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<const uint8_t> blocked_interface_classes,
mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
mojo::PendingRemote<device::mojom::UsbDeviceClient> 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<ContextObservation>(
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<device::mojom::UsbDeviceFilterPtr> filters,
blink::mojom::WebUsbService::GetPermissionCallback callback) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto controller = std::make_unique<UsbChooserController>(
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

View file

@ -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 <memory>
#include <string>
#include <unordered_map>
#include <vector>
#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<uint8_t>& classes) override;
std::unique_ptr<content::UsbChooser> RunChooser(
content::RenderFrameHost& frame,
std::vector<device::mojom::UsbDeviceFilterPtr> 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<const uint8_t> blocked_interface_classes,
mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
mojo::PendingRemote<device::mojom::UsbDeviceClient> 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<device::mojom::UsbDeviceFilterPtr> filters,
blink::mojom::WebUsbService::GetPermissionCallback callback);
class ContextObservation;
ContextObservation* GetContextObserver(
content::BrowserContext* browser_context);
base::flat_map<content::BrowserContext*, std::unique_ptr<ContextObservation>>
observations_;
std::unordered_map<content::RenderFrameHost*,
std::unique_ptr<UsbChooserController>>
controller_map_;
base::WeakPtrFactory<ElectronUsbDelegate> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_USB_ELECTRON_USB_DELEGATE_H_

View file

@ -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 <memory>
#include <utility>
#include <vector>
#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<device::mojom::UsbDeviceInfoPtr> 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::mojom::UsbDeviceInfoPtr> 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<UsbDeviceManager> 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<ElectronPermissionManager*>(
browser_context_->GetPermissionControllerDelegate());
permission_manager->RevokeDevicePermission(
static_cast<blink::PermissionType>(
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<ElectronPermissionManager*>(
browser_context_->GetPermissionControllerDelegate());
permission_manager->GrantDevicePermission(
static_cast<blink::PermissionType>(
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<ElectronPermissionManager*>(
browser_context_->GetPermissionControllerDelegate());
return permission_manager->CheckDevicePermission(
static_cast<blink::PermissionType>(
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::mojom::UsbDeviceInfoPtr> 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<const uint8_t> blocked_interface_classes,
mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
mojo::PendingRemote<device::mojom::UsbDeviceClient> device_client) {
EnsureConnectionWithDeviceManager();
device_manager_->GetDevice(
guid,
std::vector<uint8_t>(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> 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

View file

@ -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 <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#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<const uint8_t> blocked_interface_classes,
mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
mojo::PendingRemote<device::mojom::UsbDeviceClient> 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<UsbChooserContext> 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<device::mojom::UsbDeviceManager::GetDevicesCallback>
pending_get_devices_requests_;
std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
std::map<std::string, device::mojom::UsbDeviceInfoPtr> devices_;
// Connection to |device_manager_instance_|.
mojo::Remote<device::mojom::UsbDeviceManager> device_manager_;
mojo::AssociatedReceiver<device::mojom::UsbDeviceManagerClient>
client_receiver_{this};
base::ObserverList<DeviceObserver> device_observer_list_;
ElectronBrowserContext* browser_context_;
base::WeakPtrFactory<UsbChooserContext> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_H_

View file

@ -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<electron::ElectronBrowserContext*>(context);
return new UsbChooserContext(browser_context);
}
// static
UsbChooserContextFactory* UsbChooserContextFactory::GetInstance() {
return base::Singleton<UsbChooserContextFactory>::get();
}
// static
UsbChooserContext* UsbChooserContextFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<UsbChooserContext*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/true));
}
UsbChooserContext* UsbChooserContextFactory::GetForBrowserContextIfExists(
content::BrowserContext* context) {
return static_cast<UsbChooserContext*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
}
} // namespace electron

View file

@ -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();
~UsbChooserContextFactory() override;
// BrowserContextKeyedServiceFactory methods:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* profile) const override;
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTEXT_FACTORY_H_

View file

@ -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 <stddef.h>
#include <utility>
#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::mojom::UsbDeviceFilterPtr> device_filters,
blink::mojom::WebUsbService::GetPermissionCallback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronUsbDelegate> 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<v8::Object> 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

View file

@ -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 <string>
#include <unordered_map>
#include <utility>
#include <vector>
#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::mojom::UsbDeviceFilterPtr> device_filters,
blink::mojom::WebUsbService::GetPermissionCallback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronUsbDelegate> 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<device::mojom::UsbDeviceInfoPtr> devices);
bool DisplayDevice(const device::mojom::UsbDeviceInfo& device) const;
void RunCallback(device::mojom::UsbDeviceInfoPtr device_info);
void OnDeviceChosen(gin::Arguments* args);
std::vector<device::mojom::UsbDeviceFilterPtr> filters_;
blink::mojom::WebUsbService::GetPermissionCallback callback_;
url::Origin origin_;
base::WeakPtr<UsbChooserContext> chooser_context_;
base::ScopedObservation<UsbChooserContext, UsbChooserContext::DeviceObserver>
observation_{this};
base::WeakPtr<ElectronUsbDelegate> usb_delegate_;
content::GlobalRenderFrameHostId render_frame_host_id_;
base::WeakPtrFactory<UsbChooserController> weak_factory_{this};
};
} // namespace electron
#endif // ELECTRON_SHELL_BROWSER_USB_USB_CHOOSER_CONTROLLER_H_

View file

@ -29,7 +29,8 @@ class WebContentsPermissionHelper
FULLSCREEN,
OPEN_EXTERNAL,
SERIAL,
HID
HID,
USB
};
// Asynchronous Requests

View file

@ -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

View file

@ -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

View file

@ -209,6 +209,8 @@ v8::Local<v8::Value> Converter<blink::PermissionType>::ToV8(
return StringToV8(isolate, "serial");
case PermissionType::HID:
return StringToV8(isolate, "hid");
case PermissionType::USB:
return StringToV8(isolate, "usb");
default:
return StringToV8(isolate, "unknown");
}

View file

@ -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<device::mojom::UsbDeviceInfoPtr> {
static v8::Local<v8::Value> 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_

View file

@ -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('<body>');
});
await new Promise<void>(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);
}
}
}
});
});