feat: add support for WebHID (#30213)

* feat: add support for WebHID

* Apply suggestions from code review

Co-authored-by: Jeremy Rose <jeremya@chromium.org>

* Address review feedback

* Address review feedback

* chore: clear granted_devices on navigation

Also added test to verify devices get cleared

* fixup testing for device clear

* make sure navigator.hid.getDevices is run on correct frame

* clear granted devices on RenderFrameHost deletion/change

* manage device permissions per RenderFrameHost

This change makes sure we don't clear device permission prematurely due to child frame navigation

* Update shell/browser/api/electron_api_web_contents.cc

Co-authored-by: Jeremy Rose <jeremya@chromium.org>

* apply review feedback from @zcbenz

* Match upstream ObjectMap

This change matches what ObjectPermissionContextBase uses to cache object permissions: https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/object_permission_context_base.h;l=52;drc=8f95b5eab2797a3e26bba299f3b0df85bfc98bf5;bpv=1;bpt=0

The main reason for this was to resolve this crash on Win x64:
ok 2 WebContentsView doesn't crash when GCed during allocation
Received fatal exception EXCEPTION_ACCESS_VIOLATION
Backtrace:
        gin::WrappableBase::SecondWeakCallback [0x00007FF6F2AFA005+133] (o:\gin\wrappable.cc:53)
        v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacks [0x00007FF6F028F9AB+171] (o:\v8\src\handles\global-handles.cc:1400)
        v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacksFromTask [0x00007FF6F028F867+391] (o:\v8\src\handles\global-handles.cc:1387)
        node::PerIsolatePlatformData::RunForegroundTask [0x00007FF6F3B4D065+317] (o:\third_party\electron_node\src\node_platform.cc:415)
        node::PerIsolatePlatformData::FlushForegroundTasksInternal [0x00007FF6F3B4C424+776] (o:\third_party\electron_node\src\node_platform.cc:479)
        uv_run [0x00007FF6F2DDD07C+492] (o:\third_party\electron_node\deps\uv\src\win\core.c:609)
        electron::NodeBindings::UvRunOnce [0x00007FF6EEE1E036+294] (o:\electron\shell\common\node_bindings.cc:631)
        base::TaskAnnotator::RunTask [0x00007FF6F2318A19+457] (o:\base\task\common\task_annotator.cc:178)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl [0x00007FF6F2E6F553+963] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:361)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork [0x00007FF6F2E6EC69+137] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:266)
        base::MessagePumpForUI::DoRunLoop [0x00007FF6F235AA58+216] (o:\base\message_loop\message_pump_win.cc:221)
        base::MessagePumpWin::Run [0x00007FF6F235A01A+106] (o:\base\message_loop\message_pump_win.cc:79)
        base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run [0x00007FF6F2E702DA+682] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:470)
        base::RunLoop::Run [0x00007FF6F22F95BA+842] (o:\base\run_loop.cc:136)
        content::BrowserMainLoop::RunMainMessageLoop [0x00007FF6F14423CC+208] (o:\content\browser\browser_main_loop.cc:990)
        content::BrowserMainRunnerImpl::Run [0x00007FF6F144402F+143] (o:\content\browser\browser_main_runner_impl.cc:153)
        content::BrowserMain [0x00007FF6F143F911+257] (o:\content\browser\browser_main.cc:49)
        content::RunBrowserProcessMain [0x00007FF6EFFA7D18+112] (o:\content\app\content_main_runner_impl.cc:608)
        content::ContentMainRunnerImpl::RunBrowser [0x00007FF6EFFA8CF4+1220] (o:\content\app\content_main_runner_impl.cc:1104)
        content::ContentMainRunnerImpl::Run [0x00007FF6EFFA87C9+393] (o:\content\app\content_main_runner_impl.cc:971)
        content::RunContentProcess [0x00007FF6EFFA73BD+733] (o:\content\app\content_main.cc:394)
        content::ContentMain [0x00007FF6EFFA79E1+54] (o:\content\app\content_main.cc:422)
        wWinMain [0x00007FF6EECA1535+889] (o:\electron\shell\app\electron_main.cc:291)
        __scrt_common_main_seh [0x00007FF6F6F88482+262] (d:\A01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288)
        BaseThreadInitThunk [0x00007FFEC0087034+20]
        RtlUserThreadStart [0x00007FFEC1F02651+33]
✗ Electron tests failed with code 0xc0000005.

Co-authored-by: Jeremy Rose <jeremya@chromium.org>
This commit is contained in:
John Kleinschmidt 2021-09-23 07:00:11 -04:00 committed by GitHub
parent 77579614e0
commit 6aece4a83d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 2142 additions and 4 deletions

View file

@ -379,6 +379,7 @@ source_set("electron_lib") {
"//ppapi/shared_impl",
"//printing/buildflags",
"//services/device/public/cpp/geolocation",
"//services/device/public/cpp/hid",
"//services/device/public/mojom",
"//services/proxy_resolver:lib",
"//services/video_capture/public/mojom:constants",

View file

@ -180,6 +180,96 @@ Emitted when a hunspell dictionary file download fails. For details
on the failure you should collect a netlog and inspect the download
request.
#### Event: 'select-hid-device'
Returns:
* `event` Event
* `details` Object
* `deviceList` [HIDDevice[]](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
* `callback` Function
* `deviceId` String | null (optional)
Emitted when a HID device needs to be selected when a call to
`navigator.hid.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.hid` 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 === 'hid') {
// Add logic here to determine if permission should be given to allow HID selection
return true
}
return false
})
// Optionally, retrieve previously persisted devices from a persistent store
const grantedDevices = fetchGrantedDevices()
win.webContents.session.setDevicePermissionHandler((details) => {
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
if (details.device.vendorId === 123 && details.device.productId === 345) {
// Always allow this type of device (this allows skipping the call to `navigator.hid.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-hid-device', (event, details, callback) => {
event.preventDefault()
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
callback(selectedPort?.deviceId)
})
})
```
#### Event: 'hid-device-added'
Returns:
* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
Emitted when a new HID device becomes available. For example, when a new USB device is plugged in.
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
#### Event: 'hid-device-removed'
Returns:
* `event` Event
* `details` Object
* `device` [HIDDevice[]](structures/hid-device.md)
* `frame` [WebFrameMain](web-frame-main.md)
Emitted when a HID device has been removed. For example, this event will fire when a USB device is unplugged.
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
#### Event: 'select-serial-port'
Returns:
@ -525,7 +615,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`, or `serial`.
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
* `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.
@ -553,6 +643,71 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
})
```
#### `ses.setDevicePermissionHandler(handler)`
* `handler` Function\<Boolean> | null
* `details` Object
* `deviceType` String - The type of device that permission is being requested on, can be `hid`.
* `origin` String - The origin URL of the device permission check.
* `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for.
* `frame` [WebFrameMain](web-frame-main.md) - WebFrameMain checking the device permission.
Sets the handler which can be used to respond to device permission checks for the `session`.
Returning `true` will allow the device to be permitted and `false` will reject it.
To clear the handler, call `setDevicePermissionHandler(null)`.
This handler can be used to provide default permissioning to devices without first calling for permission
to devices (eg via `navigator.hid.requestDevice`). If this handler is not defined, the default device
permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used.
Additionally, the default behavior of Electron is to store granted device permision 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-hid-device` event) and then read from that storage with `setDevicePermissionHandler`.
```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 === 'hid') {
// Add logic here to determine if permission should be given to allow HID selection
return true
}
return false
})
// Optionally, retrieve previously persisted devices from a persistent store
const grantedDevices = fetchGrantedDevices()
win.webContents.session.setDevicePermissionHandler((details) => {
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
if (details.device.vendorId === 123 && details.device.productId === 345) {
// Always allow this type of device (this allows skipping the call to `navigator.hid.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-hid-device', (event, details, callback) => {
event.preventDefault()
const selectedDevice = details.deviceList.find((device) => {
return device.vendorId === '9025' && device.productId === '67'
})
callback(selectedPort?.deviceId)
})
})
```
#### `ses.clearHostResolverCache()`
Returns `Promise<void>` - Resolves when the operation is complete.

View file

@ -0,0 +1,8 @@
# HIDDevice Object
* `deviceId` String - Unique identifier for the device.
* `name` String - Name of the device.
* `vendorId` Integer - The USB vendor ID.
* `productId` Integer - The USB product ID.
* `serialNumber` String (optional) - The USB device serial number.
* `guid` String (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces.

View file

@ -0,0 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Web Bluetooth API</title>
</head>
<body>
<h1>Web Bluetooth API</h1>
<button id="clickme">Test Bluetooth</button>
<p>Currently selected bluetooth device: <strong id="device-name""></strong></p>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -0,0 +1,30 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})
mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
event.preventDefault()
if (deviceList && deviceList.length > 0) {
callback(deviceList[0].deviceId)
}
})
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,8 @@
async function testIt() {
const device = await navigator.bluetooth.requestDevice({
acceptAllDevices: true
})
document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
}
document.getElementById('clickme').addEventListener('click',testIt)

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>WebHID API</title>
</head>
<body>
<h1>WebHID API</h1>
<button id="clickme">Test WebHID</button>
<h3>HID devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
<div id="granted-devices"></div>
<h3>HID devices automatically granted access via <i>select-hid-device</i></h3>
<div id="granted-devices2"></div>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -0,0 +1,50 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})
mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => {
event.preventDefault()
if (details.deviceList && details.deviceList.length > 0) {
callback(details.deviceList[0].deviceId)
}
})
mainWindow.webContents.session.on('hid-device-added', (event, device) => {
console.log('hid-device-added FIRED WITH', device)
})
mainWindow.webContents.session.on('hid-device-removed', (event, device) => {
console.log('hid-device-removed FIRED WITH', device)
})
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'hid' && details.securityOrigin === 'file:///') {
return true
}
})
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
if (details.deviceType === 'hid' && details.origin === 'file://') {
return true
}
})
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,19 @@
async function testIt() {
const grantedDevices = await navigator.hid.getDevices()
let grantedDeviceList = ''
grantedDevices.forEach(device => {
grantedDeviceList += `<hr>${device.productName}</hr>`
})
document.getElementById('granted-devices').innerHTML = grantedDeviceList
const grantedDevices2 = await navigator.hid.requestDevice({
filters: []
})
grantedDeviceList = ''
grantedDevices2.forEach(device => {
grantedDeviceList += `<hr>${device.productName}</hr>`
})
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
}
document.getElementById('clickme').addEventListener('click',testIt)

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
<title>Web Serial API</title>
<body>
<h1>Web Serial API</h1>
<button id="clickme">Test Web Serial API</button>
<p>Matching Arduino Uno device: <strong id="device-name""></strong></p>
<script src="./renderer.js"></script>
</body>
</html>

View file

@ -0,0 +1,54 @@
const {app, BrowserWindow} = require('electron')
const path = require('path')
function createWindow () {
const mainWindow = new BrowserWindow({
width: 800,
height: 600
})
mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
event.preventDefault()
if (portList && portList.length > 0) {
callback(portList[0].portId)
} else {
callback('') //Could not find any matching devices
}
})
mainWindow.webContents.session.on('serial-port-added', (event, port) => {
console.log('serial-port-added FIRED WITH', port)
})
mainWindow.webContents.session.on('serial-port-removed', (event, port) => {
console.log('serial-port-removed FIRED WITH', port)
})
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
if (permission === 'serial' && details.securityOrigin === 'file:///') {
return true
}
})
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
if (details.deviceType === 'serial' && details.origin === 'file://') {
return true
}
})
mainWindow.loadFile('index.html')
mainWindow.webContents.openDevTools()
}
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,19 @@
async function testIt() {
const filters = [
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
];
try {
const port = await navigator.serial.requestPort({filters});
const portInfo = port.getInfo();
document.getElementById('device-name').innerHTML = `vendorId: ${portInfo.usbVendorId} | productId: ${portInfo.usbProductId} `
} catch (ex) {
if (ex.name === 'NotFoundError') {
document.getElementById('device-name').innerHTML = 'Device NOT found'
} else {
document.getElementById('device-name').innerHTML = ex
}
}
}
document.getElementById('clickme').addEventListener('click',testIt)

99
docs/tutorial/devices.md Normal file
View file

@ -0,0 +1,99 @@
# Device Access
Like Chromium based browsers, Electron provides access to device hardware
through web APIs. For the most part these APIs work like they do in a browser,
but there are some differences that need to be taken into account. The primary
difference between Electron and browsers is what happens when device access is
requested. In a browser, users are presented with a popup where they can grant
access to an individual device. In Electron APIs are provided which can be
used by a developer to either automatically pick a device or prompt users to
pick a device via a developer created interface.
## Web Bluetooth API
The [Web Bluetooth API](https://web.dev/bluetooth/) can be used to communicate
with bluetooth devices. In order to use this API in Electron, developers will
need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
associated with the device request.
### Example
This example demonstrates an Electron application that automatically selects
the first available bluetooth device when the `Test Bluetooth` button is
clicked.
```javascript fiddle='docs/fiddles/features/web-bluetooth'
```
## WebHID API
The [WebHID API](https://web.dev/hid/) can be used to access HID devices such
as keyboards and gamepads. Electron provides several APIs for working with
the WebHID API:
* The [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
can be used to select a HID device when a call to
`navigator.hid.requestDevice` is made. Additionally the [`hid-device-added`](../api/session.md#event-hid-device-added)
and [`hid-device-removed`](../api/session.md#event-hid-device-removed) events
on the Session can be used to handle devices being plugged in or unplugged during the
`navigator.hid.requestDevice` process.
* [`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.hid.requestDevice`. Additionally,
the default behavior of Electron is to store granted device permision 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-hid-device` event) and then read from that storage with
`setDevicePermissionHandler`.
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
can be used to disable HID access for specific origins.
### Blocklist
By default Electron employs the same [blocklist](https://github.com/WICG/webhid/blob/main/blocklist.txt)
used by Chromium. If you wish to override this behavior, you can do so by
setting the `disable-hid-blocklist` flag:
```javascript
app.commandLine.appendSwitch('disable-hid-blocklist')
```
### Example
This example demonstrates an Electron application that automatically selects
HID devices through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
and through [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
when the `Test WebHID` button is clicked.
```javascript fiddle='docs/fiddles/features/web-hid'
```
## Web Serial API
The [Web Serial API](https://web.dev/serial/) can be used to access serial
devices that are connected via serial port, USB, or Bluetooth. In order to use
this API in Electron, developers will need to handle the
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
associated with the serial port request.
There are several additional APIs for working with the Web Serial API:
* The [`serial-port-added`](../api/session.md#event-serial-port-added)
and [`serial-port-removed`](../api/session.md#event-serial-port-removed) events
on the Session can be used to handle devices being plugged in or unplugged during the
`navigator.serial.requestPort` process.
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
can be used to disable serial access for specific origins.
### Example
This example demonstrates an Electron application that automatically selects
the first available Arduino Uno serial device (if connected) through
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
when the `Test Web Serial` button is clicked.
```javascript fiddle='docs/fiddles/features/web-serial'
```

View file

@ -125,5 +125,7 @@
</message>
<message name="IDS_BADGE_UNREAD_NOTIFICATIONS" desc="The accessibility text which will be read by a screen reader when there are notifcatications">
{UNREAD_NOTIFICATIONS, plural, =1 {1 Unread Notification} other {# Unread Notifications}}
</message>
</message>
<message name="IDS_HID_CHOOSER_ITEM_WITHOUT_NAME" desc="User option displaying the device IDs for a Human Interface Device (HID) without a device name.">
Unknown Device (<ph name="DEVICE_ID">$1<ex>1234:abcd</ex></ph>) </message>
</grit-part>

View file

@ -84,6 +84,7 @@ auto_filenames = {
"docs/api/structures/file-filter.md",
"docs/api/structures/file-path-with-headers.md",
"docs/api/structures/gpu-feature-status.md",
"docs/api/structures/hid-device.md",
"docs/api/structures/input-event.md",
"docs/api/structures/io-counters.md",
"docs/api/structures/ipc-main-event.md",

View file

@ -387,6 +387,14 @@ filenames = {
"shell/browser/font/electron_font_access_delegate.h",
"shell/browser/font_defaults.cc",
"shell/browser/font_defaults.h",
"shell/browser/hid/electron_hid_delegate.cc",
"shell/browser/hid/electron_hid_delegate.h",
"shell/browser/hid/hid_chooser_context.cc",
"shell/browser/hid/hid_chooser_context.h",
"shell/browser/hid/hid_chooser_context_factory.cc",
"shell/browser/hid/hid_chooser_context_factory.h",
"shell/browser/hid/hid_chooser_controller.cc",
"shell/browser/hid/hid_chooser_controller.h",
"shell/browser/javascript_environment.cc",
"shell/browser/javascript_environment.h",
"shell/browser/lib/bluetooth_chooser.cc",

View file

@ -644,6 +644,18 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
permission_manager->SetPermissionCheckHandler(handler);
}
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
gin::Arguments* args) {
ElectronPermissionManager::DeviceCheckHandler handler;
if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
args->ThrowTypeError("Must pass null or function");
return;
}
auto* permission_manager = static_cast<ElectronPermissionManager*>(
browser_context()->GetPermissionControllerDelegate());
permission_manager->SetDevicePermissionHandler(handler);
}
v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
v8::Isolate* isolate = args->isolate();
gin_helper::Promise<void> promise(isolate);
@ -1148,6 +1160,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
&Session::SetPermissionRequestHandler)
.SetMethod("setPermissionCheckHandler",
&Session::SetPermissionCheckHandler)
.SetMethod("setDevicePermissionHandler",
&Session::SetDevicePermissionHandler)
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
.SetMethod("clearAuthCache", &Session::ClearAuthCache)
.SetMethod("allowNTLMCredentialsForDomains",

View file

@ -104,6 +104,8 @@ class Session : public gin::Wrappable<Session>,
gin::Arguments* args);
void SetPermissionCheckHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
void SetDevicePermissionHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
v8::Local<v8::Promise> ClearAuthCache();
void AllowNTLMCredentialsForDomains(const std::string& domains);

View file

@ -918,6 +918,12 @@ void WebContents::InitWithWebContents(
}
WebContents::~WebContents() {
// clear out objects that have been granted permissions so that when
// WebContents::RenderFrameDeleted is called as a result of WebContents
// destruction it doesn't try to clear out a granted_devices_
// on a destructed object.
granted_devices_.clear();
if (!inspectable_web_contents_) {
WebContentsDestroyed();
return;
@ -1427,6 +1433,12 @@ void WebContents::RenderFrameDeleted(
// - Cross-origin navigation creates a new RFH in a separate process which
// is swapped by content::RenderFrameHostManager.
//
// clear out objects that have been granted permissions
if (!granted_devices_.empty()) {
granted_devices_.erase(render_frame_host->GetFrameTreeNodeId());
}
// WebFrameMain::FromRenderFrameHost(rfh) will use the RFH's FrameTreeNode ID
// to find an existing instance of WebFrameMain. During a cross-origin
// navigation, the deleted RFH will be the old host which was swapped out. In
@ -3237,6 +3249,42 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
return handle;
}
void WebContents::GrantDevicePermission(
const url::Origin& origin,
const base::Value* device,
content::PermissionType permissionType,
content::RenderFrameHost* render_frame_host) {
granted_devices_[render_frame_host->GetFrameTreeNodeId()][permissionType]
[origin]
.push_back(
std::make_unique<base::Value>(device->Clone()));
}
std::vector<base::Value> WebContents::GetGrantedDevices(
const url::Origin& origin,
content::PermissionType permissionType,
content::RenderFrameHost* render_frame_host) {
const auto& devices_for_frame_host_it =
granted_devices_.find(render_frame_host->GetFrameTreeNodeId());
if (devices_for_frame_host_it == granted_devices_.end())
return {};
const auto& current_devices_it =
devices_for_frame_host_it->second.find(permissionType);
if (current_devices_it == devices_for_frame_host_it->second.end())
return {};
const auto& origin_devices_it = current_devices_it->second.find(origin);
if (origin_devices_it == current_devices_it->second.end())
return {};
std::vector<base::Value> results;
for (const auto& object : origin_devices_it->second)
results.push_back(object->Clone());
return results;
}
void WebContents::UpdatePreferredSize(content::WebContents* web_contents,
const gfx::Size& pref_size) {
Emit("preferred-size-changed", pref_size);

View file

@ -20,6 +20,7 @@
#include "content/common/frame.mojom.h"
#include "content/public/browser/devtools_agent_host.h"
#include "content/public/browser/keyboard_event_processing_result.h"
#include "content/public/browser/permission_type.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_delegate.h"
@ -91,6 +92,11 @@ class OffScreenWebContentsView;
namespace api {
using DevicePermissionMap = std::map<
int,
std::map<content::PermissionType,
std::map<url::Origin, std::vector<std::unique_ptr<base::Value>>>>>;
// Wrapper around the content::WebContents.
class WebContents : public gin::Wrappable<WebContents>,
public gin_helper::EventEmitterMixin<WebContents>,
@ -419,6 +425,21 @@ class WebContents : public gin::Wrappable<WebContents>,
electron::mojom::ElectronBrowser::DoGetZoomLevelCallback callback);
void SetImageAnimationPolicy(const std::string& new_policy);
// Grants |origin| access to |device|.
// To be used in place of ObjectPermissionContextBase::GrantObjectPermission.
void GrantDevicePermission(const url::Origin& origin,
const base::Value* device,
content::PermissionType permissionType,
content::RenderFrameHost* render_frame_host);
// Returns the list of devices that |origin| has been granted permission to
// access. To be used in place of
// ObjectPermissionContextBase::GetGrantedObjects.
std::vector<base::Value> GetGrantedDevices(
const url::Origin& origin,
content::PermissionType permissionType,
content::RenderFrameHost* render_frame_host);
private:
// Does not manage lifetime of |web_contents|.
WebContents(v8::Isolate* isolate, content::WebContents* web_contents);
@ -762,6 +783,9 @@ class WebContents : public gin::Wrappable<WebContents>,
// Stores the frame thats currently in fullscreen, nullptr if there is none.
content::RenderFrameHost* fullscreen_frame_ = nullptr;
// In-memory cache that holds objects that have been granted permissions.
DevicePermissionMap granted_devices_;
base::WeakPtrFactory<WebContents> weak_factory_{this};
DISALLOW_COPY_AND_ASSIGN(WebContents);

View file

@ -1705,4 +1705,10 @@ device::GeolocationManager* ElectronBrowserClient::GetGeolocationManager() {
#endif
}
content::HidDelegate* ElectronBrowserClient::GetHidDelegate() {
if (!hid_delegate_)
hid_delegate_ = std::make_unique<ElectronHidDelegate>();
return hid_delegate_.get();
}
} // namespace electron

View file

@ -21,6 +21,7 @@
#include "services/metrics/public/cpp/ukm_source_id.h"
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
#include "shell/browser/font/electron_font_access_delegate.h"
#include "shell/browser/hid/electron_hid_delegate.h"
#include "shell/browser/serial/electron_serial_delegate.h"
#include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"
@ -96,6 +97,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
content::BluetoothDelegate* GetBluetoothDelegate() override;
content::HidDelegate* GetHidDelegate() override;
device::GeolocationManager* GetGeolocationManager() override;
protected:
@ -309,6 +312,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
std::unique_ptr<ElectronFontAccessDelegate> font_access_delegate_;
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
#if defined(OS_MAC)
ElectronBrowserMainParts* browser_main_parts_ = nullptr;

View file

@ -17,9 +17,17 @@
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/web_contents.h"
#include "gin/data_object_builder.h"
#include "shell/browser/api/electron_api_web_contents.h"
#include "shell/browser/electron_browser_client.h"
#include "shell/browser/electron_browser_main_parts.h"
#include "shell/browser/hid/hid_chooser_context.h"
#include "shell/browser/web_contents_permission_helper.h"
#include "shell/browser/web_contents_preferences.h"
#include "shell/common/gin_converters/content_converter.h"
#include "shell/common/gin_converters/frame_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
namespace electron {
@ -117,6 +125,11 @@ void ElectronPermissionManager::SetPermissionCheckHandler(
check_handler_ = handler;
}
void ElectronPermissionManager::SetDevicePermissionHandler(
const DeviceCheckHandler& handler) {
device_permission_handler_ = handler;
}
void ElectronPermissionManager::RequestPermission(
content::PermissionType permission,
content::RenderFrameHost* render_frame_host,
@ -282,6 +295,71 @@ bool ElectronPermissionManager::CheckPermissionWithDetails(
mutable_details);
}
bool ElectronPermissionManager::CheckDevicePermission(
content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
if (device_permission_handler_.is_null()) {
if (api_web_contents) {
std::vector<base::Value> granted_devices =
api_web_contents->GetGrantedDevices(origin, permission,
render_frame_host);
for (const auto& granted_device : granted_devices) {
if (permission ==
static_cast<content::PermissionType>(
WebContentsPermissionHelper::PermissionType::HID)) {
if (device->FindIntKey(kHidVendorIdKey) !=
granted_device.FindIntKey(kHidVendorIdKey) ||
device->FindIntKey(kHidProductIdKey) !=
granted_device.FindIntKey(kHidProductIdKey)) {
continue;
}
const auto* serial_number =
granted_device.FindStringKey(kHidSerialNumberKey);
const auto* device_serial_number =
device->FindStringKey(kHidSerialNumberKey);
if (serial_number && device_serial_number &&
*device_serial_number == *serial_number)
return true;
}
}
}
return false;
} else {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
.Set("deviceType", permission)
.Set("origin", origin.Serialize())
.Set("device", device->Clone())
.Set("frame", render_frame_host)
.Build();
return device_permission_handler_.Run(details);
}
}
void ElectronPermissionManager::GrantDevicePermission(
content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const {
if (device_permission_handler_.is_null()) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
if (api_web_contents)
api_web_contents->GrantDevicePermission(origin, device, permission,
render_frame_host);
}
}
blink::mojom::PermissionStatus
ElectronPermissionManager::GetPermissionStatusForFrame(
content::PermissionType permission,

View file

@ -11,6 +11,7 @@
#include "base/callback.h"
#include "base/containers/id_map.h"
#include "content/public/browser/permission_controller_delegate.h"
#include "gin/dictionary.h"
namespace base {
class DictionaryValue;
@ -42,9 +43,13 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
const GURL& requesting_origin,
const base::Value&)>;
using DeviceCheckHandler =
base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
// Handler to dispatch permission requests in JS.
void SetPermissionRequestHandler(const RequestHandler& handler);
void SetPermissionCheckHandler(const CheckHandler& handler);
void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
// content::PermissionControllerDelegate:
void RequestPermission(content::PermissionType permission,
@ -81,6 +86,16 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
const GURL& requesting_origin,
const base::DictionaryValue* details) const;
bool CheckDevicePermission(content::PermissionType permission,
const url::Origin& origin,
const base::Value* object,
content::RenderFrameHost* render_frame_host) const;
void GrantDevicePermission(content::PermissionType permission,
const url::Origin& origin,
const base::Value* object,
content::RenderFrameHost* render_frame_host) const;
protected:
void OnPermissionResponse(int request_id,
int permission_id,
@ -108,6 +123,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
RequestHandler request_handler_;
CheckHandler check_handler_;
DeviceCheckHandler device_permission_handler_;
PendingRequestsMap pending_requests_;

View file

@ -0,0 +1,163 @@
// Copyright (c) 2021 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/hid/electron_hid_delegate.h"
#include <string>
#include <utility>
#include "content/public/browser/web_contents.h"
#include "shell/browser/hid/hid_chooser_context.h"
#include "shell/browser/hid/hid_chooser_context_factory.h"
#include "shell/browser/hid/hid_chooser_controller.h"
#include "shell/browser/web_contents_permission_helper.h"
namespace {
electron::HidChooserContext* GetChooserContext(
content::RenderFrameHost* frame) {
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
auto* browser_context = web_contents->GetBrowserContext();
return electron::HidChooserContextFactory::GetForBrowserContext(
browser_context);
}
} // namespace
namespace electron {
ElectronHidDelegate::ElectronHidDelegate() = default;
ElectronHidDelegate::~ElectronHidDelegate() = default;
std::unique_ptr<content::HidChooser> ElectronHidDelegate::RunChooser(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback) {
electron::HidChooserContext* chooser_context =
GetChooserContext(render_frame_host);
if (!device_observation_.IsObserving())
device_observation_.Observe(chooser_context);
HidChooserController* controller = ControllerForFrame(render_frame_host);
if (controller) {
DeleteControllerForFrame(render_frame_host);
}
AddControllerForFrame(render_frame_host, std::move(filters),
std::move(callback));
// Return a nullptr because the return value isn't used for anything, eg
// there is no mechanism to cancel navigator.hid.requestDevice(). The return
// value is simply used in Chromium to cleanup the chooser UI once the serial
// service is destroyed.
return nullptr;
}
bool ElectronHidDelegate::CanRequestDevicePermission(
content::RenderFrameHost* render_frame_host) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckHIDAccessPermission(
web_contents->GetMainFrame()->GetLastCommittedOrigin());
}
bool ElectronHidDelegate::HasDevicePermission(
content::RenderFrameHost* render_frame_host,
const device::mojom::HidDeviceInfo& device) {
auto* chooser_context = GetChooserContext(render_frame_host);
const auto& origin =
render_frame_host->GetMainFrame()->GetLastCommittedOrigin();
return chooser_context->HasDevicePermission(origin, device,
render_frame_host);
}
device::mojom::HidManager* ElectronHidDelegate::GetHidManager(
content::RenderFrameHost* render_frame_host) {
auto* chooser_context = GetChooserContext(render_frame_host);
return chooser_context->GetHidManager();
}
void ElectronHidDelegate::AddObserver(
content::RenderFrameHost* render_frame_host,
Observer* observer) {
observer_list_.AddObserver(observer);
auto* chooser_context = GetChooserContext(render_frame_host);
if (!device_observation_.IsObserving())
device_observation_.Observe(chooser_context);
}
void ElectronHidDelegate::RemoveObserver(
content::RenderFrameHost* render_frame_host,
content::HidDelegate::Observer* observer) {
observer_list_.RemoveObserver(observer);
}
const device::mojom::HidDeviceInfo* ElectronHidDelegate::GetDeviceInfo(
content::RenderFrameHost* render_frame_host,
const std::string& guid) {
auto* chooser_context = GetChooserContext(render_frame_host);
return chooser_context->GetDeviceInfo(guid);
}
bool ElectronHidDelegate::IsFidoAllowedForOrigin(const url::Origin& origin) {
return false;
}
void ElectronHidDelegate::OnDeviceAdded(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceAdded(device_info);
}
void ElectronHidDelegate::OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceRemoved(device_info);
}
void ElectronHidDelegate::OnDeviceChanged(
const device::mojom::HidDeviceInfo& device_info) {
for (auto& observer : observer_list_)
observer.OnDeviceChanged(device_info);
}
void ElectronHidDelegate::OnHidManagerConnectionError() {
device_observation_.Reset();
for (auto& observer : observer_list_)
observer.OnHidManagerConnectionError();
}
void ElectronHidDelegate::OnHidChooserContextShutdown() {
device_observation_.Reset();
}
HidChooserController* ElectronHidDelegate::ControllerForFrame(
content::RenderFrameHost* render_frame_host) {
auto mapping = controller_map_.find(render_frame_host);
return mapping == controller_map_.end() ? nullptr : mapping->second.get();
}
HidChooserController* ElectronHidDelegate::AddControllerForFrame(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto controller = std::make_unique<HidChooserController>(
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 ElectronHidDelegate::DeleteControllerForFrame(
content::RenderFrameHost* render_frame_host) {
controller_map_.erase(render_frame_host);
}
} // namespace electron

View file

@ -0,0 +1,84 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
#define SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "base/observer_list.h"
#include "base/scoped_observation.h"
#include "content/public/browser/hid_delegate.h"
#include "shell/browser/hid/hid_chooser_context.h"
namespace electron {
class HidChooserController;
class ElectronHidDelegate : public content::HidDelegate,
public HidChooserContext::DeviceObserver {
public:
ElectronHidDelegate();
ElectronHidDelegate(ElectronHidDelegate&) = delete;
ElectronHidDelegate& operator=(ElectronHidDelegate&) = delete;
~ElectronHidDelegate() override;
// content::HidDelegate:
std::unique_ptr<content::HidChooser> RunChooser(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback) override;
bool CanRequestDevicePermission(
content::RenderFrameHost* render_frame_host) override;
bool HasDevicePermission(content::RenderFrameHost* render_frame_host,
const device::mojom::HidDeviceInfo& device) override;
device::mojom::HidManager* GetHidManager(
content::RenderFrameHost* render_frame_host) override;
void AddObserver(content::RenderFrameHost* render_frame_host,
content::HidDelegate::Observer* observer) override;
void RemoveObserver(content::RenderFrameHost* render_frame_host,
content::HidDelegate::Observer* observer) override;
const device::mojom::HidDeviceInfo* GetDeviceInfo(
content::RenderFrameHost* render_frame_host,
const std::string& guid) override;
bool IsFidoAllowedForOrigin(const url::Origin& origin) override;
// HidChooserContext::DeviceObserver:
void OnDeviceAdded(const device::mojom::HidDeviceInfo&) override;
void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) override;
void OnDeviceChanged(const device::mojom::HidDeviceInfo&) override;
void OnHidManagerConnectionError() override;
void OnHidChooserContextShutdown() override;
void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
private:
HidChooserController* ControllerForFrame(
content::RenderFrameHost* render_frame_host);
HidChooserController* AddControllerForFrame(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback);
base::ScopedObservation<HidChooserContext,
HidChooserContext::DeviceObserver,
&HidChooserContext::AddDeviceObserver,
&HidChooserContext::RemoveDeviceObserver>
device_observation_{this};
base::ObserverList<content::HidDelegate::Observer> observer_list_;
std::unordered_map<content::RenderFrameHost*,
std::unique_ptr<HidChooserController>>
controller_map_;
base::WeakPtrFactory<ElectronHidDelegate> weak_factory_{this};
};
} // namespace electron
#endif // SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_

View file

@ -0,0 +1,272 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shell/browser/hid/hid_chooser_context.h"
#include <utility>
#include "base/command_line.h"
#include "base/containers/contains.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/values.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/device_service.h"
#include "electron/grit/electron_resources.h"
#include "services/device/public/cpp/hid/hid_blocklist.h"
#include "services/device/public/cpp/hid/hid_switches.h"
#include "shell/browser/web_contents_permission_helper.h"
#include "ui/base/l10n/l10n_util.h"
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) {}
HidChooserContext::~HidChooserContext() {
// Notify observers that the chooser context is about to be destroyed.
// Observers must remove themselves from the observer lists.
for (auto& observer : device_observer_list_) {
observer.OnHidChooserContextShutdown();
DCHECK(!device_observer_list_.HasObserver(&observer));
}
}
// static
std::u16string HidChooserContext::DisplayNameFromDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
if (device.product_name.empty()) {
auto device_id_string = base::ASCIIToUTF16(
base::StringPrintf("%04X:%04X", device.vendor_id, device.product_id));
return l10n_util::GetStringFUTF16(IDS_HID_CHOOSER_ITEM_WITHOUT_NAME,
device_id_string);
}
return base::UTF8ToUTF16(device.product_name);
}
// static
bool HidChooserContext::CanStorePersistentEntry(
const device::mojom::HidDeviceInfo& device) {
return !device.serial_number.empty() && !device.product_name.empty();
}
// static
base::Value HidChooserContext::DeviceInfoToValue(
const device::mojom::HidDeviceInfo& device) {
base::Value value(base::Value::Type::DICTIONARY);
value.SetStringKey(
kHidDeviceNameKey,
base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
value.SetIntKey(kHidVendorIdKey, device.vendor_id);
value.SetIntKey(kHidProductIdKey, 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);
} 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
// and must be granted again each time the device is connected.
value.SetStringKey(kHidGuidKey, device.guid);
}
return value;
}
void HidChooserContext::GrantDevicePermission(
const url::Origin& origin,
const device::mojom::HidDeviceInfo& device,
content::RenderFrameHost* render_frame_host) {
DCHECK(base::Contains(devices_, device.guid));
if (CanStorePersistentEntry(device)) {
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
permission_helper->GrantHIDDevicePermission(
origin, DeviceInfoToValue(device), render_frame_host);
} else {
ephemeral_devices_[origin].insert(device.guid);
}
}
bool HidChooserContext::HasDevicePermission(
const url::Origin& origin,
const device::mojom::HidDeviceInfo& device,
content::RenderFrameHost* render_frame_host) {
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableHidBlocklist) &&
device::HidBlocklist::IsDeviceExcluded(device))
return false;
auto it = ephemeral_devices_.find(origin);
if (it != ephemeral_devices_.end() &&
base::Contains(it->second, device.guid)) {
return true;
}
auto* web_contents =
content::WebContents::FromRenderFrameHost(render_frame_host);
auto* permission_helper =
WebContentsPermissionHelper::FromWebContents(web_contents);
return permission_helper->CheckHIDDevicePermission(
origin, DeviceInfoToValue(device), render_frame_host);
}
void HidChooserContext::AddDeviceObserver(DeviceObserver* observer) {
EnsureHidManagerConnection();
device_observer_list_.AddObserver(observer);
}
void HidChooserContext::RemoveDeviceObserver(DeviceObserver* observer) {
device_observer_list_.RemoveObserver(observer);
}
void HidChooserContext::GetDevices(
device::mojom::HidManager::GetDevicesCallback callback) {
if (!is_initialized_) {
EnsureHidManagerConnection();
pending_get_devices_requests_.push(std::move(callback));
return;
}
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
device_list.reserve(devices_.size());
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)));
}
const device::mojom::HidDeviceInfo* HidChooserContext::GetDeviceInfo(
const std::string& guid) {
DCHECK(is_initialized_);
auto it = devices_.find(guid);
return it == devices_.end() ? nullptr : it->second.get();
}
device::mojom::HidManager* HidChooserContext::GetHidManager() {
EnsureHidManagerConnection();
return hid_manager_.get();
}
base::WeakPtr<HidChooserContext> HidChooserContext::AsWeakPtr() {
return weak_factory_.GetWeakPtr();
}
void HidChooserContext::DeviceAdded(device::mojom::HidDeviceInfoPtr device) {
DCHECK(device);
// Update the device list.
if (!base::Contains(devices_, device->guid))
devices_.insert({device->guid, device->Clone()});
// Notify all observers.
for (auto& observer : device_observer_list_)
observer.OnDeviceAdded(*device);
}
void HidChooserContext::DeviceRemoved(device::mojom::HidDeviceInfoPtr device) {
DCHECK(device);
DCHECK(base::Contains(devices_, device->guid));
// Update the device list.
devices_.erase(device->guid);
// Notify all device observers.
for (auto& observer : device_observer_list_)
observer.OnDeviceRemoved(*device);
// Next we'll notify observers for revoked permissions. If the device does not
// support persistent permissions then device permissions are revoked on
// disconnect.
if (CanStorePersistentEntry(*device))
return;
std::vector<url::Origin> revoked_origins;
for (auto& map_entry : ephemeral_devices_) {
if (map_entry.second.erase(device->guid) > 0)
revoked_origins.push_back(map_entry.first);
}
if (revoked_origins.empty())
return;
}
void HidChooserContext::DeviceChanged(device::mojom::HidDeviceInfoPtr device) {
DCHECK(device);
DCHECK(base::Contains(devices_, device->guid));
// Update the device list.
devices_[device->guid] = device->Clone();
// Notify all observers.
for (auto& observer : device_observer_list_)
observer.OnDeviceChanged(*device);
}
void HidChooserContext::EnsureHidManagerConnection() {
if (hid_manager_)
return;
mojo::PendingRemote<device::mojom::HidManager> manager;
content::GetDeviceService().BindHidManager(
manager.InitWithNewPipeAndPassReceiver());
SetUpHidManagerConnection(std::move(manager));
}
void HidChooserContext::SetUpHidManagerConnection(
mojo::PendingRemote<device::mojom::HidManager> manager) {
hid_manager_.Bind(std::move(manager));
hid_manager_.set_disconnect_handler(base::BindOnce(
&HidChooserContext::OnHidManagerConnectionError, base::Unretained(this)));
hid_manager_->GetDevicesAndSetClient(
client_receiver_.BindNewEndpointAndPassRemote(),
base::BindOnce(&HidChooserContext::InitDeviceList,
weak_factory_.GetWeakPtr()));
}
void HidChooserContext::InitDeviceList(
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
for (auto& device : devices)
devices_.insert({device->guid, std::move(device)});
is_initialized_ = true;
while (!pending_get_devices_requests_.empty()) {
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
device_list.reserve(devices.size());
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 HidChooserContext::OnHidManagerConnectionError() {
hid_manager_.reset();
client_receiver_.reset();
devices_.clear();
std::vector<url::Origin> revoked_origins;
revoked_origins.reserve(ephemeral_devices_.size());
for (const auto& map_entry : ephemeral_devices_)
revoked_origins.push_back(map_entry.first);
ephemeral_devices_.clear();
// Notify all device observers.
for (auto& observer : device_observer_list_)
observer.OnHidManagerConnectionError();
}
} // namespace electron

View file

@ -0,0 +1,136 @@
// Copyright (c) 2021 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/containers/queue.h"
#include "base/memory/weak_ptr.h"
#include "base/observer_list.h"
#include "base/unguessable_token.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/device/public/mojom/hid.mojom.h"
#include "shell/browser/electron_browser_context.h"
#include "url/origin.h"
namespace base {
class Value;
}
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.
class HidChooserContext : public KeyedService,
public device::mojom::HidManagerClient {
public:
// This observer can be used to be notified when HID devices are connected or
// disconnected.
class DeviceObserver : public base::CheckedObserver {
public:
virtual void OnDeviceAdded(const device::mojom::HidDeviceInfo&) = 0;
virtual void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) = 0;
virtual void OnDeviceChanged(const device::mojom::HidDeviceInfo&) = 0;
virtual void OnHidManagerConnectionError() = 0;
// Called when the HidChooserContext is shutting down. Observers must remove
// themselves before returning.
virtual void OnHidChooserContextShutdown() = 0;
};
explicit HidChooserContext(ElectronBrowserContext* context);
HidChooserContext(const HidChooserContext&) = delete;
HidChooserContext& operator=(const HidChooserContext&) = delete;
~HidChooserContext() override;
// Returns a human-readable string identifier for |device|.
static std::u16string DisplayNameFromDeviceInfo(
const device::mojom::HidDeviceInfo& device);
// Returns true if a persistent permission can be granted for |device|.
static bool CanStorePersistentEntry(
const device::mojom::HidDeviceInfo& device);
static base::Value DeviceInfoToValue(
const device::mojom::HidDeviceInfo& device);
// HID-specific interface for granting and checking permissions.
void GrantDevicePermission(const url::Origin& origin,
const device::mojom::HidDeviceInfo& device,
content::RenderFrameHost* render_frame_host);
bool HasDevicePermission(const url::Origin& origin,
const device::mojom::HidDeviceInfo& device,
content::RenderFrameHost* render_frame_host);
// For ScopedObserver.
void AddDeviceObserver(DeviceObserver* observer);
void RemoveDeviceObserver(DeviceObserver* observer);
// Forward HidManager::GetDevices.
void GetDevices(device::mojom::HidManager::GetDevicesCallback callback);
// Only call this if you're sure |devices_| has been initialized before-hand.
// The returned raw pointer is owned by |devices_| and will be destroyed when
// the device is removed.
const device::mojom::HidDeviceInfo* GetDeviceInfo(const std::string& guid);
device::mojom::HidManager* GetHidManager();
base::WeakPtr<HidChooserContext> AsWeakPtr();
private:
// device::mojom::HidManagerClient implementation:
void DeviceAdded(device::mojom::HidDeviceInfoPtr device_info) override;
void DeviceRemoved(device::mojom::HidDeviceInfoPtr device_info) override;
void DeviceChanged(device::mojom::HidDeviceInfoPtr device_info) override;
void EnsureHidManagerConnection();
void SetUpHidManagerConnection(
mojo::PendingRemote<device::mojom::HidManager> manager);
void InitDeviceList(std::vector<device::mojom::HidDeviceInfoPtr> devices);
void OnHidManagerInitializedForTesting(
device::mojom::HidManager::GetDevicesCallback callback,
std::vector<device::mojom::HidDeviceInfoPtr> devices);
void OnHidManagerConnectionError();
ElectronBrowserContext* browser_context_;
bool is_initialized_ = false;
base::queue<device::mojom::HidManager::GetDevicesCallback>
pending_get_devices_requests_;
// Tracks the set of devices to which an origin has access to.
std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
// Map from device GUID to device info.
std::map<std::string, device::mojom::HidDeviceInfoPtr> devices_;
mojo::Remote<device::mojom::HidManager> hid_manager_;
mojo::AssociatedReceiver<device::mojom::HidManagerClient> client_receiver_{
this};
base::ObserverList<DeviceObserver> device_observer_list_;
base::WeakPtrFactory<HidChooserContext> weak_factory_{this};
};
} // namespace electron
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_

View file

@ -0,0 +1,55 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "shell/browser/hid/hid_chooser_context_factory.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "shell/browser/electron_browser_context.h"
#include "shell/browser/hid/hid_chooser_context.h"
namespace electron {
// static
HidChooserContextFactory* HidChooserContextFactory::GetInstance() {
static base::NoDestructor<HidChooserContextFactory> factory;
return factory.get();
}
// static
HidChooserContext* HidChooserContextFactory::GetForBrowserContext(
content::BrowserContext* context) {
return static_cast<HidChooserContext*>(
GetInstance()->GetServiceForBrowserContext(context, true));
}
// static
HidChooserContext* HidChooserContextFactory::GetForBrowserContextIfExists(
content::BrowserContext* context) {
return static_cast<HidChooserContext*>(
GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
}
HidChooserContextFactory::HidChooserContextFactory()
: BrowserContextKeyedServiceFactory(
"HidChooserContext",
BrowserContextDependencyManager::GetInstance()) {}
HidChooserContextFactory::~HidChooserContextFactory() = default;
KeyedService* HidChooserContextFactory::BuildServiceInstanceFor(
content::BrowserContext* context) const {
auto* browser_context =
static_cast<electron::ElectronBrowserContext*>(context);
return new HidChooserContext(browser_context);
}
content::BrowserContext* HidChooserContextFactory::GetBrowserContextToUse(
content::BrowserContext* context) const {
return context;
}
void HidChooserContextFactory::BrowserContextShutdown(
content::BrowserContext* context) {}
} // namespace electron

View file

@ -0,0 +1,42 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
#include "base/macros.h"
#include "base/no_destructor.h"
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
namespace electron {
class HidChooserContext;
class HidChooserContextFactory : public BrowserContextKeyedServiceFactory {
public:
static HidChooserContext* GetForBrowserContext(
content::BrowserContext* context);
static HidChooserContext* GetForBrowserContextIfExists(
content::BrowserContext* context);
static HidChooserContextFactory* GetInstance();
private:
friend base::NoDestructor<HidChooserContextFactory>;
HidChooserContextFactory();
~HidChooserContextFactory() override;
// BrowserContextKeyedBaseFactory:
KeyedService* BuildServiceInstanceFor(
content::BrowserContext* profile) const override;
content::BrowserContext* GetBrowserContextToUse(
content::BrowserContext* context) const override;
void BrowserContextShutdown(content::BrowserContext* context) override;
DISALLOW_COPY_AND_ASSIGN(HidChooserContextFactory);
};
} // namespace electron
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_

View file

@ -0,0 +1,366 @@
// Copyright (c) 2021 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#include "shell/browser/hid/hid_chooser_controller.h"
#include <utility>
#include "base/bind.h"
#include "base/command_line.h"
#include "base/containers/contains.h"
#include "base/ranges/algorithm.h"
#include "base/stl_util.h"
#include "gin/data_object_builder.h"
#include "services/device/public/cpp/hid/hid_blocklist.h"
#include "services/device/public/cpp/hid/hid_switches.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/hid/hid_chooser_context.h"
#include "shell/browser/hid/hid_chooser_context_factory.h"
#include "shell/browser/javascript_environment.h"
#include "shell/common/gin_converters/callback_converter.h"
#include "shell/common/gin_converters/content_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/node_includes.h"
#include "shell/common/process_util.h"
#include "ui/base/l10n/l10n_util.h"
namespace {
std::string PhysicalDeviceIdFromDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
// A single physical device may expose multiple HID interfaces, each
// represented by a HidDeviceInfo object. When a device exposes multiple
// HID interfaces, the HidDeviceInfo objects will share a common
// |physical_device_id|. Group these devices so that a single chooser item
// is shown for each physical device. If a device's physical device ID is
// empty, use its GUID instead.
return device.physical_device_id.empty() ? device.guid
: device.physical_device_id;
}
} // namespace
namespace gin {
template <>
struct Converter<device::mojom::HidDeviceInfoPtr> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
const device::mojom::HidDeviceInfoPtr& device) {
base::Value value = electron::HidChooserContext::DeviceInfoToValue(*device);
value.SetStringKey("deviceId", PhysicalDeviceIdFromDeviceInfo(*device));
return gin::ConvertToV8(isolate, value);
}
};
} // namespace gin
namespace electron {
HidChooserController::HidChooserController(
content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronHidDelegate> hid_delegate)
: WebContentsObserver(web_contents),
filters_(std::move(filters)),
callback_(std::move(callback)),
origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
->GetMainFrame()
->GetLastCommittedOrigin()),
frame_tree_node_id_(render_frame_host->GetFrameTreeNodeId()),
hid_delegate_(hid_delegate),
render_frame_host_id_(render_frame_host->GetGlobalId()) {
chooser_context_ = HidChooserContextFactory::GetForBrowserContext(
web_contents->GetBrowserContext())
->AsWeakPtr();
DCHECK(chooser_context_);
chooser_context_->GetHidManager()->GetDevices(base::BindOnce(
&HidChooserController::OnGotDevices, weak_factory_.GetWeakPtr()));
}
HidChooserController::~HidChooserController() {
if (callback_)
std::move(callback_).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
}
api::Session* HidChooserController::GetSession() {
if (!web_contents()) {
return nullptr;
}
return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
}
void HidChooserController::OnDeviceAdded(
const device::mojom::HidDeviceInfo& device) {
if (!DisplayDevice(device))
return;
if (AddDeviceInfo(device)) {
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("device", device.Clone())
.Set("frame", rfh)
.Build();
session->Emit("hid-device-added", details);
}
}
return;
}
void HidChooserController::OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto items_it = std::find(items_.begin(), items_.end(), id);
if (items_it == items_.end())
return;
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("device", device.Clone())
.Set("frame", rfh)
.Build();
session->Emit("hid-device-removed", details);
}
RemoveDeviceInfo(device);
}
void HidChooserController::OnDeviceChanged(
const device::mojom::HidDeviceInfo& device) {
bool has_chooser_item =
base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device));
if (!DisplayDevice(device)) {
if (has_chooser_item)
OnDeviceRemoved(device);
return;
}
if (!has_chooser_item) {
OnDeviceAdded(device);
return;
}
// Update the item to replace the old device info with |device|.
UpdateDeviceInfo(device);
}
void HidChooserController::OnDeviceChosen(gin::Arguments* args) {
std::string device_id;
if (!args->GetNext(&device_id) || device_id.empty()) {
RunCallback({});
} else {
auto find_it = device_map_.find(device_id);
if (find_it != device_map_.end()) {
auto& device_infos = find_it->second;
std::vector<device::mojom::HidDeviceInfoPtr> devices;
devices.reserve(device_infos.size());
for (auto& device : device_infos) {
chooser_context_->GrantDevicePermission(origin_, *device,
web_contents()->GetMainFrame());
devices.push_back(device->Clone());
}
RunCallback(std::move(devices));
} else {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
node::Environment* env = node::Environment::GetCurrent(isolate);
EmitWarning(env, "The device id " + device_id + " was not found.",
"UnknownHIDDeviceId");
RunCallback({});
}
}
}
void HidChooserController::OnHidManagerConnectionError() {
observation_.Reset();
}
void HidChooserController::OnHidChooserContextShutdown() {
observation_.Reset();
}
void HidChooserController::OnGotDevices(
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
std::vector<device::mojom::HidDeviceInfoPtr> devicesToDisplay;
devicesToDisplay.reserve(devices.size());
for (auto& device : devices) {
if (DisplayDevice(*device)) {
if (AddDeviceInfo(*device)) {
devicesToDisplay.push_back(device->Clone());
}
}
}
// Listen to HidChooserContext 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", devicesToDisplay)
.Set("frame", rfh)
.Build();
prevent_default =
session->Emit("select-hid-device", details,
base::AdaptCallbackForRepeating(
base::BindOnce(&HidChooserController::OnDeviceChosen,
weak_factory_.GetWeakPtr())));
}
if (!prevent_default) {
RunCallback({});
}
}
bool HidChooserController::DisplayDevice(
const device::mojom::HidDeviceInfo& device) const {
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
switches::kDisableHidBlocklist)) {
// Do not pass the device to the chooser if it is excluded by the blocklist.
if (device::HidBlocklist::IsDeviceExcluded(device))
return false;
// Do not pass the device to the chooser if it has a top-level collection
// with the FIDO usage page.
//
// Note: The HID blocklist also blocks top-level collections with the FIDO
// usage page, but will not block the device if it has other (non-FIDO)
// collections. The check below will exclude the device from the chooser
// if it has any top-level FIDO collection.
auto find_it =
std::find_if(device.collections.begin(), device.collections.end(),
[](const device::mojom::HidCollectionInfoPtr& c) {
return c->usage->usage_page == device::mojom::kPageFido;
});
if (find_it != device.collections.end())
return false;
}
return FilterMatchesAny(device);
}
bool HidChooserController::FilterMatchesAny(
const device::mojom::HidDeviceInfo& device) const {
if (filters_.empty())
return true;
for (const auto& filter : filters_) {
if (filter->device_ids) {
if (filter->device_ids->is_vendor()) {
if (filter->device_ids->get_vendor() != device.vendor_id)
continue;
} else if (filter->device_ids->is_vendor_and_product()) {
const auto& vendor_and_product =
filter->device_ids->get_vendor_and_product();
if (vendor_and_product->vendor != device.vendor_id)
continue;
if (vendor_and_product->product != device.product_id)
continue;
}
}
if (filter->usage) {
if (filter->usage->is_page()) {
const uint16_t usage_page = filter->usage->get_page();
auto find_it =
std::find_if(device.collections.begin(), device.collections.end(),
[=](const device::mojom::HidCollectionInfoPtr& c) {
return usage_page == c->usage->usage_page;
});
if (find_it == device.collections.end())
continue;
} else if (filter->usage->is_usage_and_page()) {
const auto& usage_and_page = filter->usage->get_usage_and_page();
auto find_it = std::find_if(
device.collections.begin(), device.collections.end(),
[&usage_and_page](const device::mojom::HidCollectionInfoPtr& c) {
return usage_and_page->usage_page == c->usage->usage_page &&
usage_and_page->usage == c->usage->usage;
});
if (find_it == device.collections.end())
continue;
}
}
return true;
}
return false;
}
bool HidChooserController::AddDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto find_it = device_map_.find(id);
if (find_it != device_map_.end()) {
find_it->second.push_back(device.Clone());
return false;
}
// A new device was connected. Append it to the end of the chooser list.
device_map_[id].push_back(device.Clone());
items_.push_back(id);
return true;
}
bool HidChooserController::RemoveDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto find_it = device_map_.find(id);
DCHECK(find_it != device_map_.end());
auto& device_infos = find_it->second;
base::EraseIf(device_infos,
[&device](const device::mojom::HidDeviceInfoPtr& d) {
return d->guid == device.guid;
});
if (!device_infos.empty())
return false;
// A device was disconnected. Remove it from the chooser list.
device_map_.erase(find_it);
base::Erase(items_, id);
return true;
}
void HidChooserController::UpdateDeviceInfo(
const device::mojom::HidDeviceInfo& device) {
auto id = PhysicalDeviceIdFromDeviceInfo(device);
auto physical_device_it = device_map_.find(id);
DCHECK(physical_device_it != device_map_.end());
auto& device_infos = physical_device_it->second;
auto device_it = base::ranges::find_if(
device_infos, [&device](const device::mojom::HidDeviceInfoPtr& d) {
return d->guid == device.guid;
});
DCHECK(device_it != device_infos.end());
*device_it = device.Clone();
}
void HidChooserController::RunCallback(
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
if (callback_) {
std::move(callback_).Run(std::move(devices));
}
}
void HidChooserController::RenderFrameDeleted(
content::RenderFrameHost* render_frame_host) {
if (hid_delegate_) {
hid_delegate_->DeleteControllerForFrame(render_frame_host);
}
}
} // namespace electron

View file

@ -0,0 +1,126 @@
// Copyright (c) 2021 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
#include <map>
#include <string>
#include <vector>
#include "base/macros.h"
#include "base/memory/weak_ptr.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/hid_chooser.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "services/device/public/mojom/hid.mojom-forward.h"
#include "shell/browser/api/electron_api_session.h"
#include "shell/browser/hid/electron_hid_delegate.h"
#include "shell/browser/hid/hid_chooser_context.h"
#include "shell/common/gin_converters/frame_converter.h"
#include "third_party/blink/public/mojom/hid/hid.mojom.h"
namespace content {
class RenderFrameHost;
} // namespace content
namespace electron {
class ElectronHidDelegate;
class HidChooserContext;
// HidChooserController provides data for the WebHID API permission prompt.
class HidChooserController
: public content::WebContentsObserver,
public electron::HidChooserContext::DeviceObserver {
public:
// Construct a chooser controller for Human Interface Devices (HID).
// |render_frame_host| is used to initialize the chooser strings and to access
// the requesting and embedding origins. |callback| is called when the chooser
// is closed, either by selecting an item or by dismissing the chooser dialog.
// The callback is called with the selected device, or nullptr if no device is
// selected.
HidChooserController(content::RenderFrameHost* render_frame_host,
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
content::HidChooser::Callback callback,
content::WebContents* web_contents,
base::WeakPtr<ElectronHidDelegate> hid_delegate);
HidChooserController(HidChooserController&) = delete;
HidChooserController& operator=(HidChooserController&) = delete;
~HidChooserController() override;
// HidChooserContext::DeviceObserver:
void OnDeviceAdded(const device::mojom::HidDeviceInfo& device_info) override;
void OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device_info) override;
void OnDeviceChanged(
const device::mojom::HidDeviceInfo& device_info) override;
void OnHidManagerConnectionError() override;
void OnHidChooserContextShutdown() override;
// content::WebContentsObserver:
void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
private:
api::Session* GetSession();
void OnGotDevices(std::vector<device::mojom::HidDeviceInfoPtr> devices);
bool DisplayDevice(const device::mojom::HidDeviceInfo& device) const;
bool FilterMatchesAny(const device::mojom::HidDeviceInfo& device) const;
// Add |device_info| to |device_map_|. The device is added to the chooser item
// representing the physical device. If the chooser item does not yet exist, a
// new item is appended. Returns true if an item was appended.
bool AddDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
// Remove |device_info| from |device_map_|. The device info is removed from
// the chooser item representing the physical device. If this would cause the
// item to be empty, the chooser item is removed. Does nothing if the device
// is not in the chooser item. Returns true if an item was removed.
bool RemoveDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
// Update the information for the device described by |device_info| in the
// |device_map_|.
void UpdateDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
void RunCallback(std::vector<device::mojom::HidDeviceInfoPtr> devices);
void OnDeviceChosen(gin::Arguments* args);
std::vector<blink::mojom::HidDeviceFilterPtr> filters_;
content::HidChooser::Callback callback_;
const url::Origin origin_;
const int frame_tree_node_id_;
// The lifetime of the chooser context is tied to the browser context used to
// create it, and may be destroyed while the chooser is still active.
base::WeakPtr<HidChooserContext> chooser_context_;
// Information about connected devices and their HID interfaces. A single
// physical device may expose multiple HID interfaces. Keys are physical
// device IDs, values are collections of HidDeviceInfo objects representing
// the HID interfaces hosted by the physical device.
std::map<std::string, std::vector<device::mojom::HidDeviceInfoPtr>>
device_map_;
// An ordered list of physical device IDs that determines the order of items
// in the chooser.
std::vector<std::string> items_;
base::ScopedObservation<HidChooserContext,
HidChooserContext::DeviceObserver,
&HidChooserContext::AddDeviceObserver,
&HidChooserContext::RemoveDeviceObserver>
observation_{this};
base::WeakPtr<ElectronHidDelegate> hid_delegate_;
content::GlobalRenderFrameHostId render_frame_host_id_;
base::WeakPtrFactory<HidChooserController> weak_factory_{this};
};
} // namespace electron
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_

View file

@ -94,6 +94,28 @@ bool WebContentsPermissionHelper::CheckPermission(
details);
}
bool WebContentsPermissionHelper::CheckDevicePermission(
content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const {
auto* permission_manager = static_cast<ElectronPermissionManager*>(
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
return permission_manager->CheckDevicePermission(permission, origin, device,
render_frame_host);
}
void WebContentsPermissionHelper::GrantDevicePermission(
content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const {
auto* permission_manager = static_cast<ElectronPermissionManager*>(
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
permission_manager->GrantDevicePermission(permission, origin, device,
render_frame_host);
}
void WebContentsPermissionHelper::RequestFullscreenPermission(
base::OnceCallback<void(bool)> callback) {
RequestPermission(
@ -168,6 +190,32 @@ bool WebContentsPermissionHelper::CheckSerialAccessPermission(
static_cast<content::PermissionType>(PermissionType::SERIAL), &details);
}
bool WebContentsPermissionHelper::CheckHIDAccessPermission(
const url::Origin& embedding_origin) const {
base::DictionaryValue details;
details.SetString("securityOrigin", embedding_origin.GetURL().spec());
return CheckPermission(
static_cast<content::PermissionType>(PermissionType::HID), &details);
}
bool WebContentsPermissionHelper::CheckHIDDevicePermission(
const url::Origin& origin,
base::Value device,
content::RenderFrameHost* render_frame_host) const {
return CheckDevicePermission(
static_cast<content::PermissionType>(PermissionType::HID), origin,
&device, render_frame_host);
}
void WebContentsPermissionHelper::GrantHIDDevicePermission(
const url::Origin& origin,
base::Value device,
content::RenderFrameHost* render_frame_host) const {
return GrantDevicePermission(
static_cast<content::PermissionType>(PermissionType::HID), origin,
&device, render_frame_host);
}
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper)
} // namespace electron

View file

@ -23,7 +23,8 @@ class WebContentsPermissionHelper
POINTER_LOCK = static_cast<int>(content::PermissionType::NUM) + 1,
FULLSCREEN,
OPEN_EXTERNAL,
SERIAL
SERIAL,
HID
};
// Asynchronous Requests
@ -41,6 +42,15 @@ class WebContentsPermissionHelper
bool CheckMediaAccessPermission(const GURL& security_origin,
blink::mojom::MediaStreamType type) const;
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
bool CheckHIDAccessPermission(const url::Origin& embedding_origin) const;
bool CheckHIDDevicePermission(
const url::Origin& origin,
base::Value device,
content::RenderFrameHost* render_frame_host) const;
void GrantHIDDevicePermission(
const url::Origin& origin,
base::Value device,
content::RenderFrameHost* render_frame_host) const;
private:
explicit WebContentsPermissionHelper(content::WebContents* web_contents);
@ -54,6 +64,16 @@ class WebContentsPermissionHelper
bool CheckPermission(content::PermissionType permission,
const base::DictionaryValue* details) const;
bool CheckDevicePermission(content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const;
void GrantDevicePermission(content::PermissionType permission,
const url::Origin& origin,
const base::Value* device,
content::RenderFrameHost* render_frame_host) const;
content::WebContents* web_contents_;
WEB_CONTENTS_USER_DATA_KEY_DECL();

View file

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

View file

@ -1,5 +1,5 @@
import { expect } from 'chai';
import { BrowserWindow, WebContents, session, ipcMain, app, protocol, webContents } from 'electron/main';
import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
import { emittedOnce } from './events-helpers';
import { closeAllWindows } from './window-helpers';
import * as https from 'https';
@ -1878,3 +1878,127 @@ describe('navigator.bluetooth', () => {
expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
});
});
describe('navigator.hid', () => {
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 getDevices: any = () => {
return w.webContents.executeJavaScript(`
navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
`, true);
};
after(() => {
server.close();
closeAllWindows();
});
afterEach(() => {
session.defaultSession.setPermissionCheckHandler(null);
session.defaultSession.setDevicePermissionHandler(null);
session.defaultSession.removeAllListeners('select-hid-device');
});
it('does not return a device if select-hid-device event is not defined', async () => {
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
const device = await getDevices();
expect(device).to.equal('');
});
it('does not return a device when permission denied', async () => {
let selectFired = false;
w.webContents.session.on('select-hid-device', (event, details, callback) => {
selectFired = true;
callback();
});
session.defaultSession.setPermissionCheckHandler(() => false);
const device = await getDevices();
expect(selectFired).to.be.false();
expect(device).to.equal('');
});
it('returns a device when select-hid-device event is defined', async () => {
let haveDevices = false;
let selectFired = false;
w.webContents.session.on('select-hid-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 getDevices();
expect(selectFired).to.be.true();
if (haveDevices) {
expect(device).to.contain('[object HIDDevice]');
} else {
expect(device).to.equal('');
}
if (process.arch === 'arm64' || process.arch === 'arm') {
// arm CI returns HID devices - this block may need to change if CI hardware changes.
expect(haveDevices).to.be.true();
// Verify that navigation will clear device permissions
const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.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.hid.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-hid-device', (event, details, callback) => {
selectFired = true;
if (details.deviceList.length > 0) {
const foundDevice = details.deviceList.find((device) => {
if (device.name && device.name !== '' && 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.hid.getDevices();', true);
const device = await getDevices();
expect(selectFired).to.be.true();
if (haveDevices) {
expect(device).to.contain('[object HIDDevice]');
expect(gotDevicePerms).to.be.true();
} else {
expect(device).to.equal('');
}
});
});