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:
parent
2751c2b07f
commit
629c54ba36
29 changed files with 1772 additions and 23 deletions
|
@ -239,7 +239,7 @@ app.whenReady().then(() => {
|
||||||
const selectedDevice = details.deviceList.find((device) => {
|
const selectedDevice = details.deviceList.find((device) => {
|
||||||
return device.vendorId === '9025' && device.productId === '67'
|
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
|
### Instance Methods
|
||||||
|
|
||||||
The following methods are available on instances of `Session`:
|
The following methods are available on instances of `Session`:
|
||||||
|
@ -714,7 +826,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
|
||||||
|
|
||||||
* `handler` Function\<boolean> | null
|
* `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.
|
* `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
|
* `requestingOrigin` string - The origin URL of the permission check
|
||||||
* `details` Object - Some properties are only available on certain permission types.
|
* `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.
|
* `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
|
* `handler` Function\<boolean> | null
|
||||||
* `details` Object
|
* `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.
|
* `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.
|
* `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
|
return true
|
||||||
} else if (permission === 'serial') {
|
} else if (permission === 'serial') {
|
||||||
// Add logic here to determine if permission should be given to allow serial port selection
|
// 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
|
return false
|
||||||
})
|
})
|
||||||
|
|
17
docs/api/structures/usb-device.md
Normal file
17
docs/api/structures/usb-device.md
Normal 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.
|
21
docs/fiddles/features/web-usb/index.html
Normal file
21
docs/fiddles/features/web-usb/index.html
Normal 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>
|
72
docs/fiddles/features/web-usb/main.js
Normal file
72
docs/fiddles/features/web-usb/main.js
Normal 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()
|
||||||
|
})
|
33
docs/fiddles/features/web-usb/renderer.js
Normal file
33
docs/fiddles/features/web-usb/renderer.js
Normal 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)
|
|
@ -115,3 +115,41 @@ when the `Test Web Serial` button is clicked.
|
||||||
```javascript fiddle='docs/fiddles/features/web-serial'
|
```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'
|
||||||
|
|
||||||
|
```
|
||||||
|
|
|
@ -132,6 +132,7 @@ auto_filenames = {
|
||||||
"docs/api/structures/upload-data.md",
|
"docs/api/structures/upload-data.md",
|
||||||
"docs/api/structures/upload-file.md",
|
"docs/api/structures/upload-file.md",
|
||||||
"docs/api/structures/upload-raw-data.md",
|
"docs/api/structures/upload-raw-data.md",
|
||||||
|
"docs/api/structures/usb-device.md",
|
||||||
"docs/api/structures/user-default-types.md",
|
"docs/api/structures/user-default-types.md",
|
||||||
"docs/api/structures/web-request-filter.md",
|
"docs/api/structures/web-request-filter.md",
|
||||||
"docs/api/structures/web-source.md",
|
"docs/api/structures/web-source.md",
|
||||||
|
|
|
@ -505,6 +505,14 @@ filenames = {
|
||||||
"shell/browser/ui/tray_icon_observer.h",
|
"shell/browser/ui/tray_icon_observer.h",
|
||||||
"shell/browser/ui/webui/accessibility_ui.cc",
|
"shell/browser/ui/webui/accessibility_ui.cc",
|
||||||
"shell/browser/ui/webui/accessibility_ui.h",
|
"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.cc",
|
||||||
"shell/browser/web_contents_permission_helper.h",
|
"shell/browser/web_contents_permission_helper.h",
|
||||||
"shell/browser/web_contents_preferences.cc",
|
"shell/browser/web_contents_preferences.cc",
|
||||||
|
@ -586,6 +594,7 @@ filenames = {
|
||||||
"shell/common/gin_converters/std_converter.h",
|
"shell/common/gin_converters/std_converter.h",
|
||||||
"shell/common/gin_converters/time_converter.cc",
|
"shell/common/gin_converters/time_converter.cc",
|
||||||
"shell/common/gin_converters/time_converter.h",
|
"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.cc",
|
||||||
"shell/common/gin_converters/value_converter.h",
|
"shell/common/gin_converters/value_converter.h",
|
||||||
"shell/common/gin_helper/arguments.cc",
|
"shell/common/gin_helper/arguments.cc",
|
||||||
|
|
|
@ -1715,6 +1715,12 @@ content::BluetoothDelegate* ElectronBrowserClient::GetBluetoothDelegate() {
|
||||||
return bluetooth_delegate_.get();
|
return bluetooth_delegate_.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
content::UsbDelegate* ElectronBrowserClient::GetUsbDelegate() {
|
||||||
|
if (!usb_delegate_)
|
||||||
|
usb_delegate_ = std::make_unique<ElectronUsbDelegate>();
|
||||||
|
return usb_delegate_.get();
|
||||||
|
}
|
||||||
|
|
||||||
void BindBadgeServiceForServiceWorker(
|
void BindBadgeServiceForServiceWorker(
|
||||||
const content::ServiceWorkerVersionBaseInfo& info,
|
const content::ServiceWorkerVersionBaseInfo& info,
|
||||||
mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
|
mojo::PendingReceiver<blink::mojom::BadgeService> receiver) {
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
|
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
|
||||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||||
#include "shell/browser/serial/electron_serial_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"
|
#include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"
|
||||||
|
|
||||||
namespace content {
|
namespace content {
|
||||||
|
@ -103,6 +104,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||||
content::BluetoothDelegate* GetBluetoothDelegate() override;
|
content::BluetoothDelegate* GetBluetoothDelegate() override;
|
||||||
|
|
||||||
content::HidDelegate* GetHidDelegate() override;
|
content::HidDelegate* GetHidDelegate() override;
|
||||||
|
content::UsbDelegate* GetUsbDelegate() override;
|
||||||
|
|
||||||
content::WebAuthenticationDelegate* GetWebAuthenticationDelegate() override;
|
content::WebAuthenticationDelegate* GetWebAuthenticationDelegate() override;
|
||||||
|
|
||||||
|
@ -326,6 +328,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
||||||
|
|
||||||
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
|
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
|
||||||
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
|
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
|
||||||
|
std::unique_ptr<ElectronUsbDelegate> usb_delegate_;
|
||||||
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
|
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
|
||||||
std::unique_ptr<ElectronWebAuthenticationDelegate>
|
std::unique_ptr<ElectronWebAuthenticationDelegate>
|
||||||
web_authentication_delegate_;
|
web_authentication_delegate_;
|
||||||
|
|
|
@ -51,6 +51,7 @@
|
||||||
#include "shell/browser/web_view_manager.h"
|
#include "shell/browser/web_view_manager.h"
|
||||||
#include "shell/browser/zoom_level_delegate.h"
|
#include "shell/browser/zoom_level_delegate.h"
|
||||||
#include "shell/common/application_info.h"
|
#include "shell/common/application_info.h"
|
||||||
|
#include "shell/common/electron_constants.h"
|
||||||
#include "shell/common/electron_paths.h"
|
#include "shell/common/electron_paths.h"
|
||||||
#include "shell/common/gin_converters/frame_converter.h"
|
#include "shell/common/gin_converters/frame_converter.h"
|
||||||
#include "shell/common/gin_helper/error_thrower.h"
|
#include "shell/common/gin_helper/error_thrower.h"
|
||||||
|
@ -583,19 +584,22 @@ bool ElectronBrowserContext::DoesDeviceMatch(
|
||||||
const base::Value* device_to_compare,
|
const base::Value* device_to_compare,
|
||||||
blink::PermissionType permission_type) {
|
blink::PermissionType permission_type) {
|
||||||
if (permission_type ==
|
if (permission_type ==
|
||||||
static_cast<blink::PermissionType>(
|
static_cast<blink::PermissionType>(
|
||||||
WebContentsPermissionHelper::PermissionType::HID)) {
|
WebContentsPermissionHelper::PermissionType::HID) ||
|
||||||
if (device.GetDict().FindInt(kHidVendorIdKey) !=
|
permission_type ==
|
||||||
device_to_compare->GetDict().FindInt(kHidVendorIdKey) ||
|
static_cast<blink::PermissionType>(
|
||||||
device.GetDict().FindInt(kHidProductIdKey) !=
|
WebContentsPermissionHelper::PermissionType::USB)) {
|
||||||
device_to_compare->GetDict().FindInt(kHidProductIdKey)) {
|
if (device.GetDict().FindInt(kDeviceVendorIdKey) !=
|
||||||
|
device_to_compare->GetDict().FindInt(kDeviceVendorIdKey) ||
|
||||||
|
device.GetDict().FindInt(kDeviceProductIdKey) !=
|
||||||
|
device_to_compare->GetDict().FindInt(kDeviceProductIdKey)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const auto* serial_number =
|
const auto* serial_number =
|
||||||
device_to_compare->GetDict().FindString(kHidSerialNumberKey);
|
device_to_compare->GetDict().FindString(kDeviceSerialNumberKey);
|
||||||
const auto* device_serial_number =
|
const auto* device_serial_number =
|
||||||
device.GetDict().FindString(kHidSerialNumberKey);
|
device.GetDict().FindString(kDeviceSerialNumberKey);
|
||||||
|
|
||||||
if (serial_number && device_serial_number &&
|
if (serial_number && device_serial_number &&
|
||||||
*device_serial_number == *serial_number)
|
*device_serial_number == *serial_number)
|
||||||
|
|
|
@ -17,6 +17,10 @@
|
||||||
#include "net/base/features.h"
|
#include "net/base/features.h"
|
||||||
#include "services/network/public/cpp/features.h"
|
#include "services/network/public/cpp/features.h"
|
||||||
|
|
||||||
|
#if BUILDFLAG(IS_MAC)
|
||||||
|
#include "device/base/features.h" // nogncheck
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace electron {
|
namespace electron {
|
||||||
|
|
||||||
void InitializeFeatureList() {
|
void InitializeFeatureList() {
|
||||||
|
@ -32,6 +36,11 @@ void InitializeFeatureList() {
|
||||||
disable_features +=
|
disable_features +=
|
||||||
std::string(",") + features::kSpareRendererForSitePerProcess.name;
|
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)
|
#if !BUILDFLAG(ENABLE_PICTURE_IN_PICTURE)
|
||||||
disable_features += std::string(",") + media::kPictureInPicture.name;
|
disable_features += std::string(",") + media::kPictureInPicture.name;
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -22,6 +22,7 @@
|
||||||
#include "shell/browser/api/electron_api_session.h"
|
#include "shell/browser/api/electron_api_session.h"
|
||||||
#include "shell/browser/electron_permission_manager.h"
|
#include "shell/browser/electron_permission_manager.h"
|
||||||
#include "shell/browser/web_contents_permission_helper.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/content_converter.h"
|
||||||
#include "shell/common/gin_converters/frame_converter.h"
|
#include "shell/common/gin_converters/frame_converter.h"
|
||||||
#include "shell/common/gin_converters/hid_device_info_converter.h"
|
#include "shell/common/gin_converters/hid_device_info_converter.h"
|
||||||
|
@ -35,9 +36,6 @@ namespace electron {
|
||||||
|
|
||||||
const char kHidDeviceNameKey[] = "name";
|
const char kHidDeviceNameKey[] = "name";
|
||||||
const char kHidGuidKey[] = "guid";
|
const char kHidGuidKey[] = "guid";
|
||||||
const char kHidVendorIdKey[] = "vendorId";
|
|
||||||
const char kHidProductIdKey[] = "productId";
|
|
||||||
const char kHidSerialNumberKey[] = "serialNumber";
|
|
||||||
|
|
||||||
HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
|
HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
|
||||||
: browser_context_(context) {}
|
: browser_context_(context) {}
|
||||||
|
@ -76,12 +74,12 @@ base::Value HidChooserContext::DeviceInfoToValue(
|
||||||
value.SetStringKey(
|
value.SetStringKey(
|
||||||
kHidDeviceNameKey,
|
kHidDeviceNameKey,
|
||||||
base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
|
base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
|
||||||
value.SetIntKey(kHidVendorIdKey, device.vendor_id);
|
value.SetIntKey(kDeviceVendorIdKey, device.vendor_id);
|
||||||
value.SetIntKey(kHidProductIdKey, device.product_id);
|
value.SetIntKey(kDeviceProductIdKey, device.product_id);
|
||||||
if (HidChooserContext::CanStorePersistentEntry(device)) {
|
if (HidChooserContext::CanStorePersistentEntry(device)) {
|
||||||
// Use the USB serial number as a persistent identifier. If it is
|
// Use the USB serial number as a persistent identifier. If it is
|
||||||
// unavailable, only ephemeral permissions may be granted.
|
// unavailable, only ephemeral permissions may be granted.
|
||||||
value.SetStringKey(kHidSerialNumberKey, device.serial_number);
|
value.SetStringKey(kDeviceSerialNumberKey, device.serial_number);
|
||||||
} else {
|
} else {
|
||||||
// The GUID is a temporary ID created on connection that remains valid until
|
// The GUID is a temporary ID created on connection that remains valid until
|
||||||
// the device is disconnected. Ephemeral permissions are keyed by this ID
|
// the device is disconnected. Ephemeral permissions are keyed by this ID
|
||||||
|
|
|
@ -33,9 +33,7 @@ namespace electron {
|
||||||
|
|
||||||
extern const char kHidDeviceNameKey[];
|
extern const char kHidDeviceNameKey[];
|
||||||
extern const char kHidGuidKey[];
|
extern const char kHidGuidKey[];
|
||||||
extern const char kHidVendorIdKey[];
|
|
||||||
extern const char kHidProductIdKey[];
|
extern const char kHidProductIdKey[];
|
||||||
extern const char kHidSerialNumberKey[];
|
|
||||||
|
|
||||||
// Manages the internal state and connection to the device service for the
|
// Manages the internal state and connection to the device service for the
|
||||||
// Human Interface Device (HID) chooser UI.
|
// Human Interface Device (HID) chooser UI.
|
||||||
|
|
|
@ -29,9 +29,6 @@ class Value;
|
||||||
|
|
||||||
namespace electron {
|
namespace electron {
|
||||||
|
|
||||||
extern const char kHidVendorIdKey[];
|
|
||||||
extern const char kHidProductIdKey[];
|
|
||||||
|
|
||||||
#if BUILDFLAG(IS_WIN)
|
#if BUILDFLAG(IS_WIN)
|
||||||
extern const char kDeviceInstanceIdKey[];
|
extern const char kDeviceInstanceIdKey[];
|
||||||
#else
|
#else
|
||||||
|
|
317
shell/browser/usb/electron_usb_delegate.cc
Normal file
317
shell/browser/usb/electron_usb_delegate.cc
Normal 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
|
106
shell/browser/usb/electron_usb_delegate.h
Normal file
106
shell/browser/usb/electron_usb_delegate.h
Normal 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_
|
354
shell/browser/usb/usb_chooser_context.cc
Normal file
354
shell/browser/usb/usb_chooser_context.cc
Normal 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
|
122
shell/browser/usb/usb_chooser_context.h
Normal file
122
shell/browser/usb/usb_chooser_context.h
Normal 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_
|
45
shell/browser/usb/usb_chooser_context_factory.cc
Normal file
45
shell/browser/usb/usb_chooser_context_factory.cc
Normal 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
|
39
shell/browser/usb/usb_chooser_context_factory.h
Normal file
39
shell/browser/usb/usb_chooser_context_factory.h
Normal 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_
|
165
shell/browser/usb/usb_chooser_controller.cc
Normal file
165
shell/browser/usb/usb_chooser_controller.cc
Normal 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
|
81
shell/browser/usb/usb_chooser_controller.h
Normal file
81
shell/browser/usb/usb_chooser_controller.h
Normal 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_
|
|
@ -29,7 +29,8 @@ class WebContentsPermissionHelper
|
||||||
FULLSCREEN,
|
FULLSCREEN,
|
||||||
OPEN_EXTERNAL,
|
OPEN_EXTERNAL,
|
||||||
SERIAL,
|
SERIAL,
|
||||||
HID
|
HID,
|
||||||
|
USB
|
||||||
};
|
};
|
||||||
|
|
||||||
// Asynchronous Requests
|
// Asynchronous Requests
|
||||||
|
|
|
@ -25,6 +25,10 @@ const char kSecureProtocolDescription[] =
|
||||||
"The connection to this site is using a strong protocol version "
|
"The connection to this site is using a strong protocol version "
|
||||||
"and cipher suite.";
|
"and cipher suite.";
|
||||||
|
|
||||||
|
const char kDeviceVendorIdKey[] = "vendorId";
|
||||||
|
const char kDeviceProductIdKey[] = "productId";
|
||||||
|
const char kDeviceSerialNumberKey[] = "serialNumber";
|
||||||
|
|
||||||
#if BUILDFLAG(ENABLE_RUN_AS_NODE)
|
#if BUILDFLAG(ENABLE_RUN_AS_NODE)
|
||||||
const char kRunAsNode[] = "ELECTRON_RUN_AS_NODE";
|
const char kRunAsNode[] = "ELECTRON_RUN_AS_NODE";
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -25,6 +25,11 @@ extern const char kValidCertificateDescription[];
|
||||||
extern const char kSecureProtocol[];
|
extern const char kSecureProtocol[];
|
||||||
extern const char kSecureProtocolDescription[];
|
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)
|
#if BUILDFLAG(ENABLE_RUN_AS_NODE)
|
||||||
extern const char kRunAsNode[];
|
extern const char kRunAsNode[];
|
||||||
#endif
|
#endif
|
||||||
|
|
|
@ -209,6 +209,8 @@ v8::Local<v8::Value> Converter<blink::PermissionType>::ToV8(
|
||||||
return StringToV8(isolate, "serial");
|
return StringToV8(isolate, "serial");
|
||||||
case PermissionType::HID:
|
case PermissionType::HID:
|
||||||
return StringToV8(isolate, "hid");
|
return StringToV8(isolate, "hid");
|
||||||
|
case PermissionType::USB:
|
||||||
|
return StringToV8(isolate, "usb");
|
||||||
default:
|
default:
|
||||||
return StringToV8(isolate, "unknown");
|
return StringToV8(isolate, "unknown");
|
||||||
}
|
}
|
||||||
|
|
27
shell/common/gin_converters/usb_device_info_converter.h
Normal file
27
shell/common/gin_converters/usb_device_info_converter.h
Normal 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_
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue