feat: add support for WebHID (#30213)
* feat: add support for WebHID * Apply suggestions from code review Co-authored-by: Jeremy Rose <jeremya@chromium.org> * Address review feedback * Address review feedback * chore: clear granted_devices on navigation Also added test to verify devices get cleared * fixup testing for device clear * make sure navigator.hid.getDevices is run on correct frame * clear granted devices on RenderFrameHost deletion/change * manage device permissions per RenderFrameHost This change makes sure we don't clear device permission prematurely due to child frame navigation * Update shell/browser/api/electron_api_web_contents.cc Co-authored-by: Jeremy Rose <jeremya@chromium.org> * apply review feedback from @zcbenz * Match upstream ObjectMap This change matches what ObjectPermissionContextBase uses to cache object permissions: https://source.chromium.org/chromium/chromium/src/+/main:components/permissions/object_permission_context_base.h;l=52;drc=8f95b5eab2797a3e26bba299f3b0df85bfc98bf5;bpv=1;bpt=0 The main reason for this was to resolve this crash on Win x64: ok 2 WebContentsView doesn't crash when GCed during allocation Received fatal exception EXCEPTION_ACCESS_VIOLATION Backtrace: gin::WrappableBase::SecondWeakCallback [0x00007FF6F2AFA005+133] (o:\gin\wrappable.cc:53) v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacks [0x00007FF6F028F9AB+171] (o:\v8\src\handles\global-handles.cc:1400) v8::internal::GlobalHandles::InvokeSecondPassPhantomCallbacksFromTask [0x00007FF6F028F867+391] (o:\v8\src\handles\global-handles.cc:1387) node::PerIsolatePlatformData::RunForegroundTask [0x00007FF6F3B4D065+317] (o:\third_party\electron_node\src\node_platform.cc:415) node::PerIsolatePlatformData::FlushForegroundTasksInternal [0x00007FF6F3B4C424+776] (o:\third_party\electron_node\src\node_platform.cc:479) uv_run [0x00007FF6F2DDD07C+492] (o:\third_party\electron_node\deps\uv\src\win\core.c:609) electron::NodeBindings::UvRunOnce [0x00007FF6EEE1E036+294] (o:\electron\shell\common\node_bindings.cc:631) base::TaskAnnotator::RunTask [0x00007FF6F2318A19+457] (o:\base\task\common\task_annotator.cc:178) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl [0x00007FF6F2E6F553+963] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:361) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork [0x00007FF6F2E6EC69+137] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:266) base::MessagePumpForUI::DoRunLoop [0x00007FF6F235AA58+216] (o:\base\message_loop\message_pump_win.cc:221) base::MessagePumpWin::Run [0x00007FF6F235A01A+106] (o:\base\message_loop\message_pump_win.cc:79) base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run [0x00007FF6F2E702DA+682] (o:\base\task\sequence_manager\thread_controller_with_message_pump_impl.cc:470) base::RunLoop::Run [0x00007FF6F22F95BA+842] (o:\base\run_loop.cc:136) content::BrowserMainLoop::RunMainMessageLoop [0x00007FF6F14423CC+208] (o:\content\browser\browser_main_loop.cc:990) content::BrowserMainRunnerImpl::Run [0x00007FF6F144402F+143] (o:\content\browser\browser_main_runner_impl.cc:153) content::BrowserMain [0x00007FF6F143F911+257] (o:\content\browser\browser_main.cc:49) content::RunBrowserProcessMain [0x00007FF6EFFA7D18+112] (o:\content\app\content_main_runner_impl.cc:608) content::ContentMainRunnerImpl::RunBrowser [0x00007FF6EFFA8CF4+1220] (o:\content\app\content_main_runner_impl.cc:1104) content::ContentMainRunnerImpl::Run [0x00007FF6EFFA87C9+393] (o:\content\app\content_main_runner_impl.cc:971) content::RunContentProcess [0x00007FF6EFFA73BD+733] (o:\content\app\content_main.cc:394) content::ContentMain [0x00007FF6EFFA79E1+54] (o:\content\app\content_main.cc:422) wWinMain [0x00007FF6EECA1535+889] (o:\electron\shell\app\electron_main.cc:291) __scrt_common_main_seh [0x00007FF6F6F88482+262] (d:\A01\_work\6\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288) BaseThreadInitThunk [0x00007FFEC0087034+20] RtlUserThreadStart [0x00007FFEC1F02651+33] ✗ Electron tests failed with code 0xc0000005. Co-authored-by: Jeremy Rose <jeremya@chromium.org>
This commit is contained in:
parent
77579614e0
commit
6aece4a83d
36 changed files with 2142 additions and 4 deletions
1
BUILD.gn
1
BUILD.gn
|
@ -379,6 +379,7 @@ source_set("electron_lib") {
|
|||
"//ppapi/shared_impl",
|
||||
"//printing/buildflags",
|
||||
"//services/device/public/cpp/geolocation",
|
||||
"//services/device/public/cpp/hid",
|
||||
"//services/device/public/mojom",
|
||||
"//services/proxy_resolver:lib",
|
||||
"//services/video_capture/public/mojom:constants",
|
||||
|
|
|
@ -180,6 +180,96 @@ Emitted when a hunspell dictionary file download fails. For details
|
|||
on the failure you should collect a netlog and inspect the download
|
||||
request.
|
||||
|
||||
#### Event: 'select-hid-device'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `deviceList` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
* `callback` Function
|
||||
* `deviceId` String | null (optional)
|
||||
|
||||
Emitted when a HID device needs to be selected when a call to
|
||||
`navigator.hid.requestDevice` is made. `callback` should be called with
|
||||
`deviceId` to be selected; passing no arguments to `callback` will
|
||||
cancel the request. Additionally, permissioning on `navigator.hid` can
|
||||
be further managed by using [ses.setPermissionCheckHandler(handler)](#sessetpermissioncheckhandlerhandler)
|
||||
and [ses.setDevicePermissionHandler(handler)`](#sessetdevicepermissionhandlerhandler).
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow()
|
||||
|
||||
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid') {
|
||||
// Add logic here to determine if permission should be given to allow HID selection
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Optionally, retrieve previously persisted devices from a persistent store
|
||||
const grantedDevices = fetchGrantedDevices()
|
||||
|
||||
win.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
|
||||
if (details.device.vendorId === 123 && details.device.productId === 345) {
|
||||
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
|
||||
return true
|
||||
}
|
||||
|
||||
// Search through the list of devices that have previously been granted permission
|
||||
return grantedDevices.some((grantedDevice) => {
|
||||
return grantedDevice.vendorId === details.device.vendorId &&
|
||||
grantedDevice.productId === details.device.productId &&
|
||||
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
|
||||
})
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
win.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
const selectedDevice = details.deviceList.find((device) => {
|
||||
return device.vendorId === '9025' && device.productId === '67'
|
||||
})
|
||||
callback(selectedPort?.deviceId)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### Event: 'hid-device-added'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `device` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
|
||||
Emitted when a new HID device becomes available. For example, when a new USB device is plugged in.
|
||||
|
||||
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
|
||||
|
||||
#### Event: 'hid-device-removed'
|
||||
|
||||
Returns:
|
||||
|
||||
* `event` Event
|
||||
* `details` Object
|
||||
* `device` [HIDDevice[]](structures/hid-device.md)
|
||||
* `frame` [WebFrameMain](web-frame-main.md)
|
||||
|
||||
Emitted when a HID device has been removed. For example, this event will fire when a USB device is unplugged.
|
||||
|
||||
This event will only be emitted after `navigator.hid.requestDevice` has been called and `select-hid-device` has fired.
|
||||
|
||||
#### Event: 'select-serial-port'
|
||||
|
||||
Returns:
|
||||
|
@ -525,7 +615,7 @@ session.fromPartition('some-partition').setPermissionRequestHandler((webContents
|
|||
|
||||
* `handler` Function\<Boolean> | null
|
||||
* `webContents` ([WebContents](web-contents.md) | null) - WebContents checking the permission. Please note that if the request comes from a subframe you should use `requestingUrl` to check the request origin. All cross origin sub frames making permission checks will pass a `null` webContents to this handler, while certain other permission checks such as `notifications` checks will always pass `null`. You should use `embeddingOrigin` and `requestingOrigin` to determine what origin the owning frame and the requesting frame are on respectively.
|
||||
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, or `serial`.
|
||||
* `permission` String - Type of permission check. Valid values are `midiSysex`, `notifications`, `geolocation`, `media`,`mediaKeySystem`,`midi`, `pointerLock`, `fullscreen`, `openExternal`, `hid`, or `serial`.
|
||||
* `requestingOrigin` String - The origin URL of the permission check
|
||||
* `details` Object - Some properties are only available on certain permission types.
|
||||
* `embeddingOrigin` String (optional) - The origin of the frame embedding the frame that made the permission check. Only set for cross-origin sub frames making permission checks.
|
||||
|
@ -553,6 +643,71 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents,
|
|||
})
|
||||
```
|
||||
|
||||
#### `ses.setDevicePermissionHandler(handler)`
|
||||
|
||||
* `handler` Function\<Boolean> | null
|
||||
* `details` Object
|
||||
* `deviceType` String - The type of device that permission is being requested on, can be `hid`.
|
||||
* `origin` String - The origin URL of the device permission check.
|
||||
* `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for.
|
||||
* `frame` [WebFrameMain](web-frame-main.md) - WebFrameMain checking the device permission.
|
||||
|
||||
Sets the handler which can be used to respond to device permission checks for the `session`.
|
||||
Returning `true` will allow the device to be permitted and `false` will reject it.
|
||||
To clear the handler, call `setDevicePermissionHandler(null)`.
|
||||
This handler can be used to provide default permissioning to devices without first calling for permission
|
||||
to devices (eg via `navigator.hid.requestDevice`). If this handler is not defined, the default device
|
||||
permissions as granted through device selection (eg via `navigator.hid.requestDevice`) will be used.
|
||||
Additionally, the default behavior of Electron is to store granted device permision through the lifetime
|
||||
of the corresponding WebContents. If longer term storage is needed, a developer can store granted device
|
||||
permissions (eg when handling the `select-hid-device` event) and then read from that storage with `setDevicePermissionHandler`.
|
||||
|
||||
```javascript
|
||||
const { app, BrowserWindow } = require('electron')
|
||||
|
||||
let win = null
|
||||
|
||||
app.whenReady().then(() => {
|
||||
win = new BrowserWindow()
|
||||
|
||||
win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid') {
|
||||
// Add logic here to determine if permission should be given to allow HID selection
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
// Optionally, retrieve previously persisted devices from a persistent store
|
||||
const grantedDevices = fetchGrantedDevices()
|
||||
|
||||
win.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (new URL(details.origin).hostname === 'some-host' && details.deviceType === 'hid') {
|
||||
if (details.device.vendorId === 123 && details.device.productId === 345) {
|
||||
// Always allow this type of device (this allows skipping the call to `navigator.hid.requestDevice` first)
|
||||
return true
|
||||
}
|
||||
|
||||
// Search through the list of devices that have previously been granted permission
|
||||
return grantedDevices.some((grantedDevice) => {
|
||||
return grantedDevice.vendorId === details.device.vendorId &&
|
||||
grantedDevice.productId === details.device.productId &&
|
||||
grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber
|
||||
})
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
win.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
const selectedDevice = details.deviceList.find((device) => {
|
||||
return device.vendorId === '9025' && device.productId === '67'
|
||||
})
|
||||
callback(selectedPort?.deviceId)
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
#### `ses.clearHostResolverCache()`
|
||||
|
||||
Returns `Promise<void>` - Resolves when the operation is complete.
|
||||
|
|
8
docs/api/structures/hid-device.md
Normal file
8
docs/api/structures/hid-device.md
Normal file
|
@ -0,0 +1,8 @@
|
|||
# HIDDevice Object
|
||||
|
||||
* `deviceId` String - Unique identifier for the device.
|
||||
* `name` String - Name of the device.
|
||||
* `vendorId` Integer - The USB vendor ID.
|
||||
* `productId` Integer - The USB product ID.
|
||||
* `serialNumber` String (optional) - The USB device serial number.
|
||||
* `guid` String (optional) - Unique identifier for the HID interface. A device may have multiple HID interfaces.
|
17
docs/fiddles/features/web-bluetooth/index.html
Normal file
17
docs/fiddles/features/web-bluetooth/index.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
||||
<title>Web Bluetooth API</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Web Bluetooth API</h1>
|
||||
|
||||
<button id="clickme">Test Bluetooth</button>
|
||||
|
||||
<p>Currently selected bluetooth device: <strong id="device-name""></strong></p>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
30
docs/fiddles/features/web-bluetooth/main.js
Normal file
30
docs/fiddles/features/web-bluetooth/main.js
Normal file
|
@ -0,0 +1,30 @@
|
|||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.on('select-bluetooth-device', (event, deviceList, callback) => {
|
||||
event.preventDefault()
|
||||
if (deviceList && deviceList.length > 0) {
|
||||
callback(deviceList[0].deviceId)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
8
docs/fiddles/features/web-bluetooth/renderer.js
Normal file
8
docs/fiddles/features/web-bluetooth/renderer.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
async function testIt() {
|
||||
const device = await navigator.bluetooth.requestDevice({
|
||||
acceptAllDevices: true
|
||||
})
|
||||
document.getElementById('device-name').innerHTML = device.name || `ID: ${device.id}`
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
21
docs/fiddles/features/web-hid/index.html
Normal file
21
docs/fiddles/features/web-hid/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>WebHID API</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>WebHID API</h1>
|
||||
|
||||
<button id="clickme">Test WebHID</button>
|
||||
|
||||
<h3>HID devices automatically granted access via <i>setDevicePermissionHandler</i></h3>
|
||||
<div id="granted-devices"></div>
|
||||
|
||||
<h3>HID devices automatically granted access via <i>select-hid-device</i></h3>
|
||||
<div id="granted-devices2"></div>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
50
docs/fiddles/features/web-hid/main.js
Normal file
50
docs/fiddles/features/web-hid/main.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
event.preventDefault()
|
||||
if (details.deviceList && details.deviceList.length > 0) {
|
||||
callback(details.deviceList[0].deviceId)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('hid-device-added', (event, device) => {
|
||||
console.log('hid-device-added FIRED WITH', device)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('hid-device-removed', (event, device) => {
|
||||
console.log('hid-device-removed FIRED WITH', device)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'hid' && details.securityOrigin === 'file:///') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (details.deviceType === 'hid' && details.origin === 'file://') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
19
docs/fiddles/features/web-hid/renderer.js
Normal file
19
docs/fiddles/features/web-hid/renderer.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
async function testIt() {
|
||||
const grantedDevices = await navigator.hid.getDevices()
|
||||
let grantedDeviceList = ''
|
||||
grantedDevices.forEach(device => {
|
||||
grantedDeviceList += `<hr>${device.productName}</hr>`
|
||||
})
|
||||
document.getElementById('granted-devices').innerHTML = grantedDeviceList
|
||||
const grantedDevices2 = await navigator.hid.requestDevice({
|
||||
filters: []
|
||||
})
|
||||
|
||||
grantedDeviceList = ''
|
||||
grantedDevices2.forEach(device => {
|
||||
grantedDeviceList += `<hr>${device.productName}</hr>`
|
||||
})
|
||||
document.getElementById('granted-devices2').innerHTML = grantedDeviceList
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
16
docs/fiddles/features/web-serial/index.html
Normal file
16
docs/fiddles/features/web-serial/index.html
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
|
||||
<title>Web Serial API</title>
|
||||
<body>
|
||||
<h1>Web Serial API</h1>
|
||||
|
||||
<button id="clickme">Test Web Serial API</button>
|
||||
|
||||
<p>Matching Arduino Uno device: <strong id="device-name""></strong></p>
|
||||
|
||||
<script src="./renderer.js"></script>
|
||||
</body>
|
||||
</html>
|
54
docs/fiddles/features/web-serial/main.js
Normal file
54
docs/fiddles/features/web-serial/main.js
Normal file
|
@ -0,0 +1,54 @@
|
|||
const {app, BrowserWindow} = require('electron')
|
||||
const path = require('path')
|
||||
|
||||
function createWindow () {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 800,
|
||||
height: 600
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('select-serial-port', (event, portList, webContents, callback) => {
|
||||
event.preventDefault()
|
||||
if (portList && portList.length > 0) {
|
||||
callback(portList[0].portId)
|
||||
} else {
|
||||
callback('') //Could not find any matching devices
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('serial-port-added', (event, port) => {
|
||||
console.log('serial-port-added FIRED WITH', port)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.on('serial-port-removed', (event, port) => {
|
||||
console.log('serial-port-removed FIRED WITH', port)
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => {
|
||||
if (permission === 'serial' && details.securityOrigin === 'file:///') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.webContents.session.setDevicePermissionHandler((details) => {
|
||||
if (details.deviceType === 'serial' && details.origin === 'file://') {
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.loadFile('index.html')
|
||||
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow()
|
||||
|
||||
app.on('activate', function () {
|
||||
if (BrowserWindow.getAllWindows().length === 0) createWindow()
|
||||
})
|
||||
})
|
||||
|
||||
app.on('window-all-closed', function () {
|
||||
if (process.platform !== 'darwin') app.quit()
|
||||
})
|
19
docs/fiddles/features/web-serial/renderer.js
Normal file
19
docs/fiddles/features/web-serial/renderer.js
Normal file
|
@ -0,0 +1,19 @@
|
|||
async function testIt() {
|
||||
const filters = [
|
||||
{ usbVendorId: 0x2341, usbProductId: 0x0043 },
|
||||
{ usbVendorId: 0x2341, usbProductId: 0x0001 }
|
||||
];
|
||||
try {
|
||||
const port = await navigator.serial.requestPort({filters});
|
||||
const portInfo = port.getInfo();
|
||||
document.getElementById('device-name').innerHTML = `vendorId: ${portInfo.usbVendorId} | productId: ${portInfo.usbProductId} `
|
||||
} catch (ex) {
|
||||
if (ex.name === 'NotFoundError') {
|
||||
document.getElementById('device-name').innerHTML = 'Device NOT found'
|
||||
} else {
|
||||
document.getElementById('device-name').innerHTML = ex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('clickme').addEventListener('click',testIt)
|
99
docs/tutorial/devices.md
Normal file
99
docs/tutorial/devices.md
Normal file
|
@ -0,0 +1,99 @@
|
|||
# Device Access
|
||||
|
||||
Like Chromium based browsers, Electron provides access to device hardware
|
||||
through web APIs. For the most part these APIs work like they do in a browser,
|
||||
but there are some differences that need to be taken into account. The primary
|
||||
difference between Electron and browsers is what happens when device access is
|
||||
requested. In a browser, users are presented with a popup where they can grant
|
||||
access to an individual device. In Electron APIs are provided which can be
|
||||
used by a developer to either automatically pick a device or prompt users to
|
||||
pick a device via a developer created interface.
|
||||
|
||||
## Web Bluetooth API
|
||||
|
||||
The [Web Bluetooth API](https://web.dev/bluetooth/) can be used to communicate
|
||||
with bluetooth devices. In order to use this API in Electron, developers will
|
||||
need to handle the [`select-bluetooth-device` event on the webContents](../api/web-contents.md#event-select-bluetooth-device)
|
||||
associated with the device request.
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
the first available bluetooth device when the `Test Bluetooth` button is
|
||||
clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-bluetooth'
|
||||
|
||||
```
|
||||
|
||||
## WebHID API
|
||||
|
||||
The [WebHID API](https://web.dev/hid/) can be used to access HID devices such
|
||||
as keyboards and gamepads. Electron provides several APIs for working with
|
||||
the WebHID API:
|
||||
|
||||
* The [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
|
||||
can be used to select a HID device when a call to
|
||||
`navigator.hid.requestDevice` is made. Additionally the [`hid-device-added`](../api/session.md#event-hid-device-added)
|
||||
and [`hid-device-removed`](../api/session.md#event-hid-device-removed) events
|
||||
on the Session can be used to handle devices being plugged in or unplugged during the
|
||||
`navigator.hid.requestDevice` process.
|
||||
* [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
|
||||
can be used to provide default permissioning to devices without first calling
|
||||
for permission to devices via `navigator.hid.requestDevice`. Additionally,
|
||||
the default behavior of Electron is to store granted device permision through
|
||||
the lifetime of the corresponding WebContents. If longer term storage is
|
||||
needed, a developer can store granted device permissions (eg when handling
|
||||
the `select-hid-device` event) and then read from that storage with
|
||||
`setDevicePermissionHandler`.
|
||||
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
|
||||
can be used to disable HID access for specific origins.
|
||||
|
||||
### Blocklist
|
||||
|
||||
By default Electron employs the same [blocklist](https://github.com/WICG/webhid/blob/main/blocklist.txt)
|
||||
used by Chromium. If you wish to override this behavior, you can do so by
|
||||
setting the `disable-hid-blocklist` flag:
|
||||
|
||||
```javascript
|
||||
app.commandLine.appendSwitch('disable-hid-blocklist')
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
HID devices through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler)
|
||||
and through [`select-hid-device` event on the Session](../api/session.md#event-select-hid-device)
|
||||
when the `Test WebHID` button is clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-hid'
|
||||
|
||||
```
|
||||
|
||||
## Web Serial API
|
||||
|
||||
The [Web Serial API](https://web.dev/serial/) can be used to access serial
|
||||
devices that are connected via serial port, USB, or Bluetooth. In order to use
|
||||
this API in Electron, developers will need to handle the
|
||||
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
|
||||
associated with the serial port request.
|
||||
|
||||
There are several additional APIs for working with the Web Serial API:
|
||||
|
||||
* The [`serial-port-added`](../api/session.md#event-serial-port-added)
|
||||
and [`serial-port-removed`](../api/session.md#event-serial-port-removed) events
|
||||
on the Session can be used to handle devices being plugged in or unplugged during the
|
||||
`navigator.serial.requestPort` process.
|
||||
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
|
||||
can be used to disable serial access for specific origins.
|
||||
|
||||
### Example
|
||||
|
||||
This example demonstrates an Electron application that automatically selects
|
||||
the first available Arduino Uno serial device (if connected) through
|
||||
[`select-serial-port` event on the Session](../api/session.md#event-select-serial-port)
|
||||
when the `Test Web Serial` button is clicked.
|
||||
|
||||
```javascript fiddle='docs/fiddles/features/web-serial'
|
||||
|
||||
```
|
|
@ -125,5 +125,7 @@
|
|||
</message>
|
||||
<message name="IDS_BADGE_UNREAD_NOTIFICATIONS" desc="The accessibility text which will be read by a screen reader when there are notifcatications">
|
||||
{UNREAD_NOTIFICATIONS, plural, =1 {1 Unread Notification} other {# Unread Notifications}}
|
||||
</message>
|
||||
</message>
|
||||
<message name="IDS_HID_CHOOSER_ITEM_WITHOUT_NAME" desc="User option displaying the device IDs for a Human Interface Device (HID) without a device name.">
|
||||
Unknown Device (<ph name="DEVICE_ID">$1<ex>1234:abcd</ex></ph>) </message>
|
||||
</grit-part>
|
||||
|
|
|
@ -84,6 +84,7 @@ auto_filenames = {
|
|||
"docs/api/structures/file-filter.md",
|
||||
"docs/api/structures/file-path-with-headers.md",
|
||||
"docs/api/structures/gpu-feature-status.md",
|
||||
"docs/api/structures/hid-device.md",
|
||||
"docs/api/structures/input-event.md",
|
||||
"docs/api/structures/io-counters.md",
|
||||
"docs/api/structures/ipc-main-event.md",
|
||||
|
|
|
@ -387,6 +387,14 @@ filenames = {
|
|||
"shell/browser/font/electron_font_access_delegate.h",
|
||||
"shell/browser/font_defaults.cc",
|
||||
"shell/browser/font_defaults.h",
|
||||
"shell/browser/hid/electron_hid_delegate.cc",
|
||||
"shell/browser/hid/electron_hid_delegate.h",
|
||||
"shell/browser/hid/hid_chooser_context.cc",
|
||||
"shell/browser/hid/hid_chooser_context.h",
|
||||
"shell/browser/hid/hid_chooser_context_factory.cc",
|
||||
"shell/browser/hid/hid_chooser_context_factory.h",
|
||||
"shell/browser/hid/hid_chooser_controller.cc",
|
||||
"shell/browser/hid/hid_chooser_controller.h",
|
||||
"shell/browser/javascript_environment.cc",
|
||||
"shell/browser/javascript_environment.h",
|
||||
"shell/browser/lib/bluetooth_chooser.cc",
|
||||
|
|
|
@ -644,6 +644,18 @@ void Session::SetPermissionCheckHandler(v8::Local<v8::Value> val,
|
|||
permission_manager->SetPermissionCheckHandler(handler);
|
||||
}
|
||||
|
||||
void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args) {
|
||||
ElectronPermissionManager::DeviceCheckHandler handler;
|
||||
if (!(val->IsNull() || gin::ConvertFromV8(args->isolate(), val, &handler))) {
|
||||
args->ThrowTypeError("Must pass null or function");
|
||||
return;
|
||||
}
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
browser_context()->GetPermissionControllerDelegate());
|
||||
permission_manager->SetDevicePermissionHandler(handler);
|
||||
}
|
||||
|
||||
v8::Local<v8::Promise> Session::ClearHostResolverCache(gin::Arguments* args) {
|
||||
v8::Isolate* isolate = args->isolate();
|
||||
gin_helper::Promise<void> promise(isolate);
|
||||
|
@ -1148,6 +1160,8 @@ gin::ObjectTemplateBuilder Session::GetObjectTemplateBuilder(
|
|||
&Session::SetPermissionRequestHandler)
|
||||
.SetMethod("setPermissionCheckHandler",
|
||||
&Session::SetPermissionCheckHandler)
|
||||
.SetMethod("setDevicePermissionHandler",
|
||||
&Session::SetDevicePermissionHandler)
|
||||
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)
|
||||
.SetMethod("clearAuthCache", &Session::ClearAuthCache)
|
||||
.SetMethod("allowNTLMCredentialsForDomains",
|
||||
|
|
|
@ -104,6 +104,8 @@ class Session : public gin::Wrappable<Session>,
|
|||
gin::Arguments* args);
|
||||
void SetPermissionCheckHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args);
|
||||
void SetDevicePermissionHandler(v8::Local<v8::Value> val,
|
||||
gin::Arguments* args);
|
||||
v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);
|
||||
v8::Local<v8::Promise> ClearAuthCache();
|
||||
void AllowNTLMCredentialsForDomains(const std::string& domains);
|
||||
|
|
|
@ -918,6 +918,12 @@ void WebContents::InitWithWebContents(
|
|||
}
|
||||
|
||||
WebContents::~WebContents() {
|
||||
// clear out objects that have been granted permissions so that when
|
||||
// WebContents::RenderFrameDeleted is called as a result of WebContents
|
||||
// destruction it doesn't try to clear out a granted_devices_
|
||||
// on a destructed object.
|
||||
granted_devices_.clear();
|
||||
|
||||
if (!inspectable_web_contents_) {
|
||||
WebContentsDestroyed();
|
||||
return;
|
||||
|
@ -1427,6 +1433,12 @@ void WebContents::RenderFrameDeleted(
|
|||
// - Cross-origin navigation creates a new RFH in a separate process which
|
||||
// is swapped by content::RenderFrameHostManager.
|
||||
//
|
||||
|
||||
// clear out objects that have been granted permissions
|
||||
if (!granted_devices_.empty()) {
|
||||
granted_devices_.erase(render_frame_host->GetFrameTreeNodeId());
|
||||
}
|
||||
|
||||
// WebFrameMain::FromRenderFrameHost(rfh) will use the RFH's FrameTreeNode ID
|
||||
// to find an existing instance of WebFrameMain. During a cross-origin
|
||||
// navigation, the deleted RFH will be the old host which was swapped out. In
|
||||
|
@ -3237,6 +3249,42 @@ v8::Local<v8::Promise> WebContents::TakeHeapSnapshot(
|
|||
return handle;
|
||||
}
|
||||
|
||||
void WebContents::GrantDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
granted_devices_[render_frame_host->GetFrameTreeNodeId()][permissionType]
|
||||
[origin]
|
||||
.push_back(
|
||||
std::make_unique<base::Value>(device->Clone()));
|
||||
}
|
||||
|
||||
std::vector<base::Value> WebContents::GetGrantedDevices(
|
||||
const url::Origin& origin,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
const auto& devices_for_frame_host_it =
|
||||
granted_devices_.find(render_frame_host->GetFrameTreeNodeId());
|
||||
if (devices_for_frame_host_it == granted_devices_.end())
|
||||
return {};
|
||||
|
||||
const auto& current_devices_it =
|
||||
devices_for_frame_host_it->second.find(permissionType);
|
||||
if (current_devices_it == devices_for_frame_host_it->second.end())
|
||||
return {};
|
||||
|
||||
const auto& origin_devices_it = current_devices_it->second.find(origin);
|
||||
if (origin_devices_it == current_devices_it->second.end())
|
||||
return {};
|
||||
|
||||
std::vector<base::Value> results;
|
||||
for (const auto& object : origin_devices_it->second)
|
||||
results.push_back(object->Clone());
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
void WebContents::UpdatePreferredSize(content::WebContents* web_contents,
|
||||
const gfx::Size& pref_size) {
|
||||
Emit("preferred-size-changed", pref_size);
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
#include "content/common/frame.mojom.h"
|
||||
#include "content/public/browser/devtools_agent_host.h"
|
||||
#include "content/public/browser/keyboard_event_processing_result.h"
|
||||
#include "content/public/browser/permission_type.h"
|
||||
#include "content/public/browser/render_widget_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "content/public/browser/web_contents_delegate.h"
|
||||
|
@ -91,6 +92,11 @@ class OffScreenWebContentsView;
|
|||
|
||||
namespace api {
|
||||
|
||||
using DevicePermissionMap = std::map<
|
||||
int,
|
||||
std::map<content::PermissionType,
|
||||
std::map<url::Origin, std::vector<std::unique_ptr<base::Value>>>>>;
|
||||
|
||||
// Wrapper around the content::WebContents.
|
||||
class WebContents : public gin::Wrappable<WebContents>,
|
||||
public gin_helper::EventEmitterMixin<WebContents>,
|
||||
|
@ -419,6 +425,21 @@ class WebContents : public gin::Wrappable<WebContents>,
|
|||
electron::mojom::ElectronBrowser::DoGetZoomLevelCallback callback);
|
||||
void SetImageAnimationPolicy(const std::string& new_policy);
|
||||
|
||||
// Grants |origin| access to |device|.
|
||||
// To be used in place of ObjectPermissionContextBase::GrantObjectPermission.
|
||||
void GrantDevicePermission(const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
// Returns the list of devices that |origin| has been granted permission to
|
||||
// access. To be used in place of
|
||||
// ObjectPermissionContextBase::GetGrantedObjects.
|
||||
std::vector<base::Value> GetGrantedDevices(
|
||||
const url::Origin& origin,
|
||||
content::PermissionType permissionType,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
private:
|
||||
// Does not manage lifetime of |web_contents|.
|
||||
WebContents(v8::Isolate* isolate, content::WebContents* web_contents);
|
||||
|
@ -762,6 +783,9 @@ class WebContents : public gin::Wrappable<WebContents>,
|
|||
// Stores the frame thats currently in fullscreen, nullptr if there is none.
|
||||
content::RenderFrameHost* fullscreen_frame_ = nullptr;
|
||||
|
||||
// In-memory cache that holds objects that have been granted permissions.
|
||||
DevicePermissionMap granted_devices_;
|
||||
|
||||
base::WeakPtrFactory<WebContents> weak_factory_{this};
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(WebContents);
|
||||
|
|
|
@ -1705,4 +1705,10 @@ device::GeolocationManager* ElectronBrowserClient::GetGeolocationManager() {
|
|||
#endif
|
||||
}
|
||||
|
||||
content::HidDelegate* ElectronBrowserClient::GetHidDelegate() {
|
||||
if (!hid_delegate_)
|
||||
hid_delegate_ = std::make_unique<ElectronHidDelegate>();
|
||||
return hid_delegate_.get();
|
||||
}
|
||||
|
||||
} // namespace electron
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
#include "services/metrics/public/cpp/ukm_source_id.h"
|
||||
#include "shell/browser/bluetooth/electron_bluetooth_delegate.h"
|
||||
#include "shell/browser/font/electron_font_access_delegate.h"
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
#include "shell/browser/serial/electron_serial_delegate.h"
|
||||
#include "third_party/blink/public/mojom/badging/badging.mojom-forward.h"
|
||||
|
||||
|
@ -96,6 +97,8 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
|||
|
||||
content::BluetoothDelegate* GetBluetoothDelegate() override;
|
||||
|
||||
content::HidDelegate* GetHidDelegate() override;
|
||||
|
||||
device::GeolocationManager* GetGeolocationManager() override;
|
||||
|
||||
protected:
|
||||
|
@ -309,6 +312,7 @@ class ElectronBrowserClient : public content::ContentBrowserClient,
|
|||
std::unique_ptr<ElectronSerialDelegate> serial_delegate_;
|
||||
std::unique_ptr<ElectronBluetoothDelegate> bluetooth_delegate_;
|
||||
std::unique_ptr<ElectronFontAccessDelegate> font_access_delegate_;
|
||||
std::unique_ptr<ElectronHidDelegate> hid_delegate_;
|
||||
|
||||
#if defined(OS_MAC)
|
||||
ElectronBrowserMainParts* browser_main_parts_ = nullptr;
|
||||
|
|
|
@ -17,9 +17,17 @@
|
|||
#include "content/public/browser/render_process_host.h"
|
||||
#include "content/public/browser/render_view_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "gin/data_object_builder.h"
|
||||
#include "shell/browser/api/electron_api_web_contents.h"
|
||||
#include "shell/browser/electron_browser_client.h"
|
||||
#include "shell/browser/electron_browser_main_parts.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
#include "shell/browser/web_contents_preferences.h"
|
||||
#include "shell/common/gin_converters/content_converter.h"
|
||||
#include "shell/common/gin_converters/frame_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/event_emitter_caller.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
|
@ -117,6 +125,11 @@ void ElectronPermissionManager::SetPermissionCheckHandler(
|
|||
check_handler_ = handler;
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::SetDevicePermissionHandler(
|
||||
const DeviceCheckHandler& handler) {
|
||||
device_permission_handler_ = handler;
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::RequestPermission(
|
||||
content::PermissionType permission,
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
|
@ -282,6 +295,71 @@ bool ElectronPermissionManager::CheckPermissionWithDetails(
|
|||
mutable_details);
|
||||
}
|
||||
|
||||
bool ElectronPermissionManager::CheckDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
|
||||
if (device_permission_handler_.is_null()) {
|
||||
if (api_web_contents) {
|
||||
std::vector<base::Value> granted_devices =
|
||||
api_web_contents->GetGrantedDevices(origin, permission,
|
||||
render_frame_host);
|
||||
|
||||
for (const auto& granted_device : granted_devices) {
|
||||
if (permission ==
|
||||
static_cast<content::PermissionType>(
|
||||
WebContentsPermissionHelper::PermissionType::HID)) {
|
||||
if (device->FindIntKey(kHidVendorIdKey) !=
|
||||
granted_device.FindIntKey(kHidVendorIdKey) ||
|
||||
device->FindIntKey(kHidProductIdKey) !=
|
||||
granted_device.FindIntKey(kHidProductIdKey)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto* serial_number =
|
||||
granted_device.FindStringKey(kHidSerialNumberKey);
|
||||
const auto* device_serial_number =
|
||||
device->FindStringKey(kHidSerialNumberKey);
|
||||
|
||||
if (serial_number && device_serial_number &&
|
||||
*device_serial_number == *serial_number)
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("deviceType", permission)
|
||||
.Set("origin", origin.Serialize())
|
||||
.Set("device", device->Clone())
|
||||
.Set("frame", render_frame_host)
|
||||
.Build();
|
||||
return device_permission_handler_.Run(details);
|
||||
}
|
||||
}
|
||||
|
||||
void ElectronPermissionManager::GrantDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
if (device_permission_handler_.is_null()) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
api::WebContents* api_web_contents = api::WebContents::From(web_contents);
|
||||
if (api_web_contents)
|
||||
api_web_contents->GrantDevicePermission(origin, device, permission,
|
||||
render_frame_host);
|
||||
}
|
||||
}
|
||||
|
||||
blink::mojom::PermissionStatus
|
||||
ElectronPermissionManager::GetPermissionStatusForFrame(
|
||||
content::PermissionType permission,
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
#include "base/callback.h"
|
||||
#include "base/containers/id_map.h"
|
||||
#include "content/public/browser/permission_controller_delegate.h"
|
||||
#include "gin/dictionary.h"
|
||||
|
||||
namespace base {
|
||||
class DictionaryValue;
|
||||
|
@ -42,9 +43,13 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
|||
const GURL& requesting_origin,
|
||||
const base::Value&)>;
|
||||
|
||||
using DeviceCheckHandler =
|
||||
base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
|
||||
|
||||
// Handler to dispatch permission requests in JS.
|
||||
void SetPermissionRequestHandler(const RequestHandler& handler);
|
||||
void SetPermissionCheckHandler(const CheckHandler& handler);
|
||||
void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
|
||||
|
||||
// content::PermissionControllerDelegate:
|
||||
void RequestPermission(content::PermissionType permission,
|
||||
|
@ -81,6 +86,16 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
|||
const GURL& requesting_origin,
|
||||
const base::DictionaryValue* details) const;
|
||||
|
||||
bool CheckDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* object,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
void GrantDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* object,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
protected:
|
||||
void OnPermissionResponse(int request_id,
|
||||
int permission_id,
|
||||
|
@ -108,6 +123,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
|
|||
|
||||
RequestHandler request_handler_;
|
||||
CheckHandler check_handler_;
|
||||
DeviceCheckHandler device_permission_handler_;
|
||||
|
||||
PendingRequestsMap pending_requests_;
|
||||
|
||||
|
|
163
shell/browser/hid/electron_hid_delegate.cc
Normal file
163
shell/browser/hid/electron_hid_delegate.cc
Normal file
|
@ -0,0 +1,163 @@
|
|||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
#include "shell/browser/hid/hid_chooser_controller.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
|
||||
namespace {
|
||||
|
||||
electron::HidChooserContext* GetChooserContext(
|
||||
content::RenderFrameHost* frame) {
|
||||
auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
|
||||
auto* browser_context = web_contents->GetBrowserContext();
|
||||
return electron::HidChooserContextFactory::GetForBrowserContext(
|
||||
browser_context);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace electron {
|
||||
|
||||
ElectronHidDelegate::ElectronHidDelegate() = default;
|
||||
|
||||
ElectronHidDelegate::~ElectronHidDelegate() = default;
|
||||
|
||||
std::unique_ptr<content::HidChooser> ElectronHidDelegate::RunChooser(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) {
|
||||
electron::HidChooserContext* chooser_context =
|
||||
GetChooserContext(render_frame_host);
|
||||
if (!device_observation_.IsObserving())
|
||||
device_observation_.Observe(chooser_context);
|
||||
|
||||
HidChooserController* controller = ControllerForFrame(render_frame_host);
|
||||
if (controller) {
|
||||
DeleteControllerForFrame(render_frame_host);
|
||||
}
|
||||
AddControllerForFrame(render_frame_host, std::move(filters),
|
||||
std::move(callback));
|
||||
|
||||
// Return a nullptr because the return value isn't used for anything, eg
|
||||
// there is no mechanism to cancel navigator.hid.requestDevice(). The return
|
||||
// value is simply used in Chromium to cleanup the chooser UI once the serial
|
||||
// service is destroyed.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::CanRequestDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckHIDAccessPermission(
|
||||
web_contents->GetMainFrame()->GetLastCommittedOrigin());
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::HasDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
const auto& origin =
|
||||
render_frame_host->GetMainFrame()->GetLastCommittedOrigin();
|
||||
return chooser_context->HasDevicePermission(origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
device::mojom::HidManager* ElectronHidDelegate::GetHidManager(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
return chooser_context->GetHidManager();
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::AddObserver(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
Observer* observer) {
|
||||
observer_list_.AddObserver(observer);
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
if (!device_observation_.IsObserving())
|
||||
device_observation_.Observe(chooser_context);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::RemoveObserver(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) {
|
||||
observer_list_.RemoveObserver(observer);
|
||||
}
|
||||
|
||||
const device::mojom::HidDeviceInfo* ElectronHidDelegate::GetDeviceInfo(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const std::string& guid) {
|
||||
auto* chooser_context = GetChooserContext(render_frame_host);
|
||||
return chooser_context->GetDeviceInfo(guid);
|
||||
}
|
||||
|
||||
bool ElectronHidDelegate::IsFidoAllowedForOrigin(const url::Origin& origin) {
|
||||
return false;
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceAdded(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceAdded(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceRemoved(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device_info) {
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnDeviceChanged(device_info);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnHidManagerConnectionError() {
|
||||
device_observation_.Reset();
|
||||
|
||||
for (auto& observer : observer_list_)
|
||||
observer.OnHidManagerConnectionError();
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::OnHidChooserContextShutdown() {
|
||||
device_observation_.Reset();
|
||||
}
|
||||
|
||||
HidChooserController* ElectronHidDelegate::ControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
auto mapping = controller_map_.find(render_frame_host);
|
||||
return mapping == controller_map_.end() ? nullptr : mapping->second.get();
|
||||
}
|
||||
|
||||
HidChooserController* ElectronHidDelegate::AddControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto controller = std::make_unique<HidChooserController>(
|
||||
render_frame_host, std::move(filters), std::move(callback), web_contents,
|
||||
weak_factory_.GetWeakPtr());
|
||||
controller_map_.insert(
|
||||
std::make_pair(render_frame_host, std::move(controller)));
|
||||
return ControllerForFrame(render_frame_host);
|
||||
}
|
||||
|
||||
void ElectronHidDelegate::DeleteControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
controller_map_.erase(render_frame_host);
|
||||
}
|
||||
|
||||
} // namespace electron
|
84
shell/browser/hid/electron_hid_delegate.h
Normal file
84
shell/browser/hid/electron_hid_delegate.h
Normal file
|
@ -0,0 +1,84 @@
|
|||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
||||
#define SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "base/observer_list.h"
|
||||
#include "base/scoped_observation.h"
|
||||
#include "content/public/browser/hid_delegate.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
class HidChooserController;
|
||||
|
||||
class ElectronHidDelegate : public content::HidDelegate,
|
||||
public HidChooserContext::DeviceObserver {
|
||||
public:
|
||||
ElectronHidDelegate();
|
||||
ElectronHidDelegate(ElectronHidDelegate&) = delete;
|
||||
ElectronHidDelegate& operator=(ElectronHidDelegate&) = delete;
|
||||
~ElectronHidDelegate() override;
|
||||
|
||||
// content::HidDelegate:
|
||||
std::unique_ptr<content::HidChooser> RunChooser(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback) override;
|
||||
bool CanRequestDevicePermission(
|
||||
content::RenderFrameHost* render_frame_host) override;
|
||||
bool HasDevicePermission(content::RenderFrameHost* render_frame_host,
|
||||
const device::mojom::HidDeviceInfo& device) override;
|
||||
device::mojom::HidManager* GetHidManager(
|
||||
content::RenderFrameHost* render_frame_host) override;
|
||||
void AddObserver(content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) override;
|
||||
void RemoveObserver(content::RenderFrameHost* render_frame_host,
|
||||
content::HidDelegate::Observer* observer) override;
|
||||
const device::mojom::HidDeviceInfo* GetDeviceInfo(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
const std::string& guid) override;
|
||||
bool IsFidoAllowedForOrigin(const url::Origin& origin) override;
|
||||
|
||||
// HidChooserContext::DeviceObserver:
|
||||
void OnDeviceAdded(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnDeviceChanged(const device::mojom::HidDeviceInfo&) override;
|
||||
void OnHidManagerConnectionError() override;
|
||||
void OnHidChooserContextShutdown() override;
|
||||
|
||||
void DeleteControllerForFrame(content::RenderFrameHost* render_frame_host);
|
||||
|
||||
private:
|
||||
HidChooserController* ControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
HidChooserController* AddControllerForFrame(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback);
|
||||
|
||||
base::ScopedObservation<HidChooserContext,
|
||||
HidChooserContext::DeviceObserver,
|
||||
&HidChooserContext::AddDeviceObserver,
|
||||
&HidChooserContext::RemoveDeviceObserver>
|
||||
device_observation_{this};
|
||||
base::ObserverList<content::HidDelegate::Observer> observer_list_;
|
||||
|
||||
std::unordered_map<content::RenderFrameHost*,
|
||||
std::unique_ptr<HidChooserController>>
|
||||
controller_map_;
|
||||
|
||||
base::WeakPtrFactory<ElectronHidDelegate> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_ELECTRON_HID_DELEGATE_H_
|
272
shell/browser/hid/hid_chooser_context.cc
Normal file
272
shell/browser/hid/hid_chooser_context.cc
Normal file
|
@ -0,0 +1,272 @@
|
|||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/strings/string_number_conversions.h"
|
||||
#include "base/strings/string_util.h"
|
||||
#include "base/strings/stringprintf.h"
|
||||
#include "base/strings/utf_string_conversions.h"
|
||||
#include "base/values.h"
|
||||
#include "components/content_settings/core/common/content_settings_types.h"
|
||||
#include "components/prefs/pref_service.h"
|
||||
#include "content/public/browser/device_service.h"
|
||||
#include "electron/grit/electron_resources.h"
|
||||
#include "services/device/public/cpp/hid/hid_blocklist.h"
|
||||
#include "services/device/public/cpp/hid/hid_switches.h"
|
||||
#include "shell/browser/web_contents_permission_helper.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
const char kHidDeviceNameKey[] = "name";
|
||||
const char kHidGuidKey[] = "guid";
|
||||
const char kHidVendorIdKey[] = "vendorId";
|
||||
const char kHidProductIdKey[] = "productId";
|
||||
const char kHidSerialNumberKey[] = "serialNumber";
|
||||
|
||||
HidChooserContext::HidChooserContext(ElectronBrowserContext* context)
|
||||
: browser_context_(context) {}
|
||||
|
||||
HidChooserContext::~HidChooserContext() {
|
||||
// Notify observers that the chooser context is about to be destroyed.
|
||||
// Observers must remove themselves from the observer lists.
|
||||
for (auto& observer : device_observer_list_) {
|
||||
observer.OnHidChooserContextShutdown();
|
||||
DCHECK(!device_observer_list_.HasObserver(&observer));
|
||||
}
|
||||
}
|
||||
|
||||
// static
|
||||
std::u16string HidChooserContext::DisplayNameFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
if (device.product_name.empty()) {
|
||||
auto device_id_string = base::ASCIIToUTF16(
|
||||
base::StringPrintf("%04X:%04X", device.vendor_id, device.product_id));
|
||||
return l10n_util::GetStringFUTF16(IDS_HID_CHOOSER_ITEM_WITHOUT_NAME,
|
||||
device_id_string);
|
||||
}
|
||||
return base::UTF8ToUTF16(device.product_name);
|
||||
}
|
||||
|
||||
// static
|
||||
bool HidChooserContext::CanStorePersistentEntry(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
return !device.serial_number.empty() && !device.product_name.empty();
|
||||
}
|
||||
|
||||
// static
|
||||
base::Value HidChooserContext::DeviceInfoToValue(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
base::Value value(base::Value::Type::DICTIONARY);
|
||||
value.SetStringKey(
|
||||
kHidDeviceNameKey,
|
||||
base::UTF16ToUTF8(HidChooserContext::DisplayNameFromDeviceInfo(device)));
|
||||
value.SetIntKey(kHidVendorIdKey, device.vendor_id);
|
||||
value.SetIntKey(kHidProductIdKey, device.product_id);
|
||||
if (HidChooserContext::CanStorePersistentEntry(device)) {
|
||||
// Use the USB serial number as a persistent identifier. If it is
|
||||
// unavailable, only ephemeral permissions may be granted.
|
||||
value.SetStringKey(kHidSerialNumberKey, device.serial_number);
|
||||
} else {
|
||||
// The GUID is a temporary ID created on connection that remains valid until
|
||||
// the device is disconnected. Ephemeral permissions are keyed by this ID
|
||||
// and must be granted again each time the device is connected.
|
||||
value.SetStringKey(kHidGuidKey, device.guid);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
void HidChooserContext::GrantDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
DCHECK(base::Contains(devices_, device.guid));
|
||||
if (CanStorePersistentEntry(device)) {
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
permission_helper->GrantHIDDevicePermission(
|
||||
origin, DeviceInfoToValue(device), render_frame_host);
|
||||
} else {
|
||||
ephemeral_devices_[origin].insert(device.guid);
|
||||
}
|
||||
}
|
||||
|
||||
bool HidChooserContext::HasDevicePermission(
|
||||
const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
|
||||
switches::kDisableHidBlocklist) &&
|
||||
device::HidBlocklist::IsDeviceExcluded(device))
|
||||
return false;
|
||||
|
||||
auto it = ephemeral_devices_.find(origin);
|
||||
if (it != ephemeral_devices_.end() &&
|
||||
base::Contains(it->second, device.guid)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto* web_contents =
|
||||
content::WebContents::FromRenderFrameHost(render_frame_host);
|
||||
auto* permission_helper =
|
||||
WebContentsPermissionHelper::FromWebContents(web_contents);
|
||||
return permission_helper->CheckHIDDevicePermission(
|
||||
origin, DeviceInfoToValue(device), render_frame_host);
|
||||
}
|
||||
|
||||
void HidChooserContext::AddDeviceObserver(DeviceObserver* observer) {
|
||||
EnsureHidManagerConnection();
|
||||
device_observer_list_.AddObserver(observer);
|
||||
}
|
||||
|
||||
void HidChooserContext::RemoveDeviceObserver(DeviceObserver* observer) {
|
||||
device_observer_list_.RemoveObserver(observer);
|
||||
}
|
||||
|
||||
void HidChooserContext::GetDevices(
|
||||
device::mojom::HidManager::GetDevicesCallback callback) {
|
||||
if (!is_initialized_) {
|
||||
EnsureHidManagerConnection();
|
||||
pending_get_devices_requests_.push(std::move(callback));
|
||||
return;
|
||||
}
|
||||
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
|
||||
device_list.reserve(devices_.size());
|
||||
for (const auto& pair : devices_)
|
||||
device_list.push_back(pair.second->Clone());
|
||||
base::SequencedTaskRunnerHandle::Get()->PostTask(
|
||||
FROM_HERE, base::BindOnce(std::move(callback), std::move(device_list)));
|
||||
}
|
||||
|
||||
const device::mojom::HidDeviceInfo* HidChooserContext::GetDeviceInfo(
|
||||
const std::string& guid) {
|
||||
DCHECK(is_initialized_);
|
||||
auto it = devices_.find(guid);
|
||||
return it == devices_.end() ? nullptr : it->second.get();
|
||||
}
|
||||
|
||||
device::mojom::HidManager* HidChooserContext::GetHidManager() {
|
||||
EnsureHidManagerConnection();
|
||||
return hid_manager_.get();
|
||||
}
|
||||
|
||||
base::WeakPtr<HidChooserContext> HidChooserContext::AsWeakPtr() {
|
||||
return weak_factory_.GetWeakPtr();
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceAdded(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
|
||||
// Update the device list.
|
||||
if (!base::Contains(devices_, device->guid))
|
||||
devices_.insert({device->guid, device->Clone()});
|
||||
|
||||
// Notify all observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceAdded(*device);
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceRemoved(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
DCHECK(base::Contains(devices_, device->guid));
|
||||
|
||||
// Update the device list.
|
||||
devices_.erase(device->guid);
|
||||
|
||||
// Notify all device observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceRemoved(*device);
|
||||
|
||||
// Next we'll notify observers for revoked permissions. If the device does not
|
||||
// support persistent permissions then device permissions are revoked on
|
||||
// disconnect.
|
||||
if (CanStorePersistentEntry(*device))
|
||||
return;
|
||||
|
||||
std::vector<url::Origin> revoked_origins;
|
||||
for (auto& map_entry : ephemeral_devices_) {
|
||||
if (map_entry.second.erase(device->guid) > 0)
|
||||
revoked_origins.push_back(map_entry.first);
|
||||
}
|
||||
if (revoked_origins.empty())
|
||||
return;
|
||||
}
|
||||
|
||||
void HidChooserContext::DeviceChanged(device::mojom::HidDeviceInfoPtr device) {
|
||||
DCHECK(device);
|
||||
DCHECK(base::Contains(devices_, device->guid));
|
||||
|
||||
// Update the device list.
|
||||
devices_[device->guid] = device->Clone();
|
||||
|
||||
// Notify all observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnDeviceChanged(*device);
|
||||
}
|
||||
|
||||
void HidChooserContext::EnsureHidManagerConnection() {
|
||||
if (hid_manager_)
|
||||
return;
|
||||
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager;
|
||||
content::GetDeviceService().BindHidManager(
|
||||
manager.InitWithNewPipeAndPassReceiver());
|
||||
SetUpHidManagerConnection(std::move(manager));
|
||||
}
|
||||
|
||||
void HidChooserContext::SetUpHidManagerConnection(
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager) {
|
||||
hid_manager_.Bind(std::move(manager));
|
||||
hid_manager_.set_disconnect_handler(base::BindOnce(
|
||||
&HidChooserContext::OnHidManagerConnectionError, base::Unretained(this)));
|
||||
|
||||
hid_manager_->GetDevicesAndSetClient(
|
||||
client_receiver_.BindNewEndpointAndPassRemote(),
|
||||
base::BindOnce(&HidChooserContext::InitDeviceList,
|
||||
weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
void HidChooserContext::InitDeviceList(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
for (auto& device : devices)
|
||||
devices_.insert({device->guid, std::move(device)});
|
||||
|
||||
is_initialized_ = true;
|
||||
|
||||
while (!pending_get_devices_requests_.empty()) {
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> device_list;
|
||||
device_list.reserve(devices.size());
|
||||
for (const auto& entry : devices_)
|
||||
device_list.push_back(entry.second->Clone());
|
||||
std::move(pending_get_devices_requests_.front())
|
||||
.Run(std::move(device_list));
|
||||
pending_get_devices_requests_.pop();
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserContext::OnHidManagerConnectionError() {
|
||||
hid_manager_.reset();
|
||||
client_receiver_.reset();
|
||||
devices_.clear();
|
||||
|
||||
std::vector<url::Origin> revoked_origins;
|
||||
revoked_origins.reserve(ephemeral_devices_.size());
|
||||
for (const auto& map_entry : ephemeral_devices_)
|
||||
revoked_origins.push_back(map_entry.first);
|
||||
ephemeral_devices_.clear();
|
||||
|
||||
// Notify all device observers.
|
||||
for (auto& observer : device_observer_list_)
|
||||
observer.OnHidManagerConnectionError();
|
||||
}
|
||||
|
||||
} // namespace electron
|
136
shell/browser/hid/hid_chooser_context.h
Normal file
136
shell/browser/hid/hid_chooser_context.h
Normal file
|
@ -0,0 +1,136 @@
|
|||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/containers/queue.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "base/observer_list.h"
|
||||
#include "base/unguessable_token.h"
|
||||
#include "content/public/browser/render_frame_host.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "mojo/public/cpp/bindings/associated_receiver.h"
|
||||
#include "mojo/public/cpp/bindings/pending_remote.h"
|
||||
#include "mojo/public/cpp/bindings/remote.h"
|
||||
#include "services/device/public/mojom/hid.mojom.h"
|
||||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "url/origin.h"
|
||||
|
||||
namespace base {
|
||||
class Value;
|
||||
}
|
||||
|
||||
namespace electron {
|
||||
|
||||
extern const char kHidDeviceNameKey[];
|
||||
extern const char kHidGuidKey[];
|
||||
extern const char kHidVendorIdKey[];
|
||||
extern const char kHidProductIdKey[];
|
||||
extern const char kHidSerialNumberKey[];
|
||||
|
||||
// Manages the internal state and connection to the device service for the
|
||||
// Human Interface Device (HID) chooser UI.
|
||||
class HidChooserContext : public KeyedService,
|
||||
public device::mojom::HidManagerClient {
|
||||
public:
|
||||
// This observer can be used to be notified when HID devices are connected or
|
||||
// disconnected.
|
||||
class DeviceObserver : public base::CheckedObserver {
|
||||
public:
|
||||
virtual void OnDeviceAdded(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnDeviceRemoved(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnDeviceChanged(const device::mojom::HidDeviceInfo&) = 0;
|
||||
virtual void OnHidManagerConnectionError() = 0;
|
||||
|
||||
// Called when the HidChooserContext is shutting down. Observers must remove
|
||||
// themselves before returning.
|
||||
virtual void OnHidChooserContextShutdown() = 0;
|
||||
};
|
||||
|
||||
explicit HidChooserContext(ElectronBrowserContext* context);
|
||||
HidChooserContext(const HidChooserContext&) = delete;
|
||||
HidChooserContext& operator=(const HidChooserContext&) = delete;
|
||||
~HidChooserContext() override;
|
||||
|
||||
// Returns a human-readable string identifier for |device|.
|
||||
static std::u16string DisplayNameFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
// Returns true if a persistent permission can be granted for |device|.
|
||||
static bool CanStorePersistentEntry(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
static base::Value DeviceInfoToValue(
|
||||
const device::mojom::HidDeviceInfo& device);
|
||||
|
||||
// HID-specific interface for granting and checking permissions.
|
||||
void GrantDevicePermission(const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
bool HasDevicePermission(const url::Origin& origin,
|
||||
const device::mojom::HidDeviceInfo& device,
|
||||
content::RenderFrameHost* render_frame_host);
|
||||
|
||||
// For ScopedObserver.
|
||||
void AddDeviceObserver(DeviceObserver* observer);
|
||||
void RemoveDeviceObserver(DeviceObserver* observer);
|
||||
|
||||
// Forward HidManager::GetDevices.
|
||||
void GetDevices(device::mojom::HidManager::GetDevicesCallback callback);
|
||||
|
||||
// Only call this if you're sure |devices_| has been initialized before-hand.
|
||||
// The returned raw pointer is owned by |devices_| and will be destroyed when
|
||||
// the device is removed.
|
||||
const device::mojom::HidDeviceInfo* GetDeviceInfo(const std::string& guid);
|
||||
|
||||
device::mojom::HidManager* GetHidManager();
|
||||
|
||||
base::WeakPtr<HidChooserContext> AsWeakPtr();
|
||||
|
||||
private:
|
||||
// device::mojom::HidManagerClient implementation:
|
||||
void DeviceAdded(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
void DeviceRemoved(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
void DeviceChanged(device::mojom::HidDeviceInfoPtr device_info) override;
|
||||
|
||||
void EnsureHidManagerConnection();
|
||||
void SetUpHidManagerConnection(
|
||||
mojo::PendingRemote<device::mojom::HidManager> manager);
|
||||
void InitDeviceList(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnHidManagerInitializedForTesting(
|
||||
device::mojom::HidManager::GetDevicesCallback callback,
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnHidManagerConnectionError();
|
||||
|
||||
ElectronBrowserContext* browser_context_;
|
||||
|
||||
bool is_initialized_ = false;
|
||||
base::queue<device::mojom::HidManager::GetDevicesCallback>
|
||||
pending_get_devices_requests_;
|
||||
|
||||
// Tracks the set of devices to which an origin has access to.
|
||||
std::map<url::Origin, std::set<std::string>> ephemeral_devices_;
|
||||
|
||||
// Map from device GUID to device info.
|
||||
std::map<std::string, device::mojom::HidDeviceInfoPtr> devices_;
|
||||
|
||||
mojo::Remote<device::mojom::HidManager> hid_manager_;
|
||||
mojo::AssociatedReceiver<device::mojom::HidManagerClient> client_receiver_{
|
||||
this};
|
||||
base::ObserverList<DeviceObserver> device_observer_list_;
|
||||
|
||||
base::WeakPtrFactory<HidChooserContext> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_H_
|
55
shell/browser/hid/hid_chooser_context_factory.cc
Normal file
55
shell/browser/hid/hid_chooser_context_factory.cc
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
|
||||
#include "components/keyed_service/content/browser_context_dependency_manager.h"
|
||||
#include "shell/browser/electron_browser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
// static
|
||||
HidChooserContextFactory* HidChooserContextFactory::GetInstance() {
|
||||
static base::NoDestructor<HidChooserContextFactory> factory;
|
||||
return factory.get();
|
||||
}
|
||||
|
||||
// static
|
||||
HidChooserContext* HidChooserContextFactory::GetForBrowserContext(
|
||||
content::BrowserContext* context) {
|
||||
return static_cast<HidChooserContext*>(
|
||||
GetInstance()->GetServiceForBrowserContext(context, true));
|
||||
}
|
||||
|
||||
// static
|
||||
HidChooserContext* HidChooserContextFactory::GetForBrowserContextIfExists(
|
||||
content::BrowserContext* context) {
|
||||
return static_cast<HidChooserContext*>(
|
||||
GetInstance()->GetServiceForBrowserContext(context, /*create=*/false));
|
||||
}
|
||||
|
||||
HidChooserContextFactory::HidChooserContextFactory()
|
||||
: BrowserContextKeyedServiceFactory(
|
||||
"HidChooserContext",
|
||||
BrowserContextDependencyManager::GetInstance()) {}
|
||||
|
||||
HidChooserContextFactory::~HidChooserContextFactory() = default;
|
||||
|
||||
KeyedService* HidChooserContextFactory::BuildServiceInstanceFor(
|
||||
content::BrowserContext* context) const {
|
||||
auto* browser_context =
|
||||
static_cast<electron::ElectronBrowserContext*>(context);
|
||||
return new HidChooserContext(browser_context);
|
||||
}
|
||||
|
||||
content::BrowserContext* HidChooserContextFactory::GetBrowserContextToUse(
|
||||
content::BrowserContext* context) const {
|
||||
return context;
|
||||
}
|
||||
|
||||
void HidChooserContextFactory::BrowserContextShutdown(
|
||||
content::BrowserContext* context) {}
|
||||
|
||||
} // namespace electron
|
42
shell/browser/hid/hid_chooser_context_factory.h
Normal file
42
shell/browser/hid/hid_chooser_context_factory.h
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright 2019 The Chromium Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/no_destructor.h"
|
||||
#include "components/keyed_service/content/browser_context_keyed_service_factory.h"
|
||||
|
||||
namespace electron {
|
||||
|
||||
class HidChooserContext;
|
||||
|
||||
class HidChooserContextFactory : public BrowserContextKeyedServiceFactory {
|
||||
public:
|
||||
static HidChooserContext* GetForBrowserContext(
|
||||
content::BrowserContext* context);
|
||||
static HidChooserContext* GetForBrowserContextIfExists(
|
||||
content::BrowserContext* context);
|
||||
static HidChooserContextFactory* GetInstance();
|
||||
|
||||
private:
|
||||
friend base::NoDestructor<HidChooserContextFactory>;
|
||||
|
||||
HidChooserContextFactory();
|
||||
~HidChooserContextFactory() override;
|
||||
|
||||
// BrowserContextKeyedBaseFactory:
|
||||
KeyedService* BuildServiceInstanceFor(
|
||||
content::BrowserContext* profile) const override;
|
||||
content::BrowserContext* GetBrowserContextToUse(
|
||||
content::BrowserContext* context) const override;
|
||||
void BrowserContextShutdown(content::BrowserContext* context) override;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(HidChooserContextFactory);
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTEXT_FACTORY_H_
|
366
shell/browser/hid/hid_chooser_controller.cc
Normal file
366
shell/browser/hid/hid_chooser_controller.cc
Normal file
|
@ -0,0 +1,366 @@
|
|||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#include "shell/browser/hid/hid_chooser_controller.h"
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "base/bind.h"
|
||||
#include "base/command_line.h"
|
||||
#include "base/containers/contains.h"
|
||||
#include "base/ranges/algorithm.h"
|
||||
#include "base/stl_util.h"
|
||||
#include "gin/data_object_builder.h"
|
||||
#include "services/device/public/cpp/hid/hid_blocklist.h"
|
||||
#include "services/device/public/cpp/hid/hid_switches.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/browser/hid/hid_chooser_context_factory.h"
|
||||
#include "shell/browser/javascript_environment.h"
|
||||
#include "shell/common/gin_converters/callback_converter.h"
|
||||
#include "shell/common/gin_converters/content_converter.h"
|
||||
#include "shell/common/gin_converters/value_converter.h"
|
||||
#include "shell/common/gin_helper/dictionary.h"
|
||||
#include "shell/common/node_includes.h"
|
||||
#include "shell/common/process_util.h"
|
||||
#include "ui/base/l10n/l10n_util.h"
|
||||
|
||||
namespace {
|
||||
|
||||
std::string PhysicalDeviceIdFromDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
// A single physical device may expose multiple HID interfaces, each
|
||||
// represented by a HidDeviceInfo object. When a device exposes multiple
|
||||
// HID interfaces, the HidDeviceInfo objects will share a common
|
||||
// |physical_device_id|. Group these devices so that a single chooser item
|
||||
// is shown for each physical device. If a device's physical device ID is
|
||||
// empty, use its GUID instead.
|
||||
return device.physical_device_id.empty() ? device.guid
|
||||
: device.physical_device_id;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace gin {
|
||||
|
||||
template <>
|
||||
struct Converter<device::mojom::HidDeviceInfoPtr> {
|
||||
static v8::Local<v8::Value> ToV8(
|
||||
v8::Isolate* isolate,
|
||||
const device::mojom::HidDeviceInfoPtr& device) {
|
||||
base::Value value = electron::HidChooserContext::DeviceInfoToValue(*device);
|
||||
value.SetStringKey("deviceId", PhysicalDeviceIdFromDeviceInfo(*device));
|
||||
return gin::ConvertToV8(isolate, value);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace gin
|
||||
|
||||
namespace electron {
|
||||
|
||||
HidChooserController::HidChooserController(
|
||||
content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback,
|
||||
content::WebContents* web_contents,
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate)
|
||||
: WebContentsObserver(web_contents),
|
||||
filters_(std::move(filters)),
|
||||
callback_(std::move(callback)),
|
||||
origin_(content::WebContents::FromRenderFrameHost(render_frame_host)
|
||||
->GetMainFrame()
|
||||
->GetLastCommittedOrigin()),
|
||||
frame_tree_node_id_(render_frame_host->GetFrameTreeNodeId()),
|
||||
hid_delegate_(hid_delegate),
|
||||
render_frame_host_id_(render_frame_host->GetGlobalId()) {
|
||||
chooser_context_ = HidChooserContextFactory::GetForBrowserContext(
|
||||
web_contents->GetBrowserContext())
|
||||
->AsWeakPtr();
|
||||
DCHECK(chooser_context_);
|
||||
|
||||
chooser_context_->GetHidManager()->GetDevices(base::BindOnce(
|
||||
&HidChooserController::OnGotDevices, weak_factory_.GetWeakPtr()));
|
||||
}
|
||||
|
||||
HidChooserController::~HidChooserController() {
|
||||
if (callback_)
|
||||
std::move(callback_).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
|
||||
}
|
||||
|
||||
api::Session* HidChooserController::GetSession() {
|
||||
if (!web_contents()) {
|
||||
return nullptr;
|
||||
}
|
||||
return api::Session::FromBrowserContext(web_contents()->GetBrowserContext());
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceAdded(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
if (!DisplayDevice(device))
|
||||
return;
|
||||
if (AddDeviceInfo(device)) {
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("device", device.Clone())
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
session->Emit("hid-device-added", details);
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto items_it = std::find(items_.begin(), items_.end(), id);
|
||||
if (items_it == items_.end())
|
||||
return;
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("device", device.Clone())
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
session->Emit("hid-device-removed", details);
|
||||
}
|
||||
RemoveDeviceInfo(device);
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
bool has_chooser_item =
|
||||
base::Contains(items_, PhysicalDeviceIdFromDeviceInfo(device));
|
||||
if (!DisplayDevice(device)) {
|
||||
if (has_chooser_item)
|
||||
OnDeviceRemoved(device);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!has_chooser_item) {
|
||||
OnDeviceAdded(device);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the item to replace the old device info with |device|.
|
||||
UpdateDeviceInfo(device);
|
||||
}
|
||||
|
||||
void HidChooserController::OnDeviceChosen(gin::Arguments* args) {
|
||||
std::string device_id;
|
||||
if (!args->GetNext(&device_id) || device_id.empty()) {
|
||||
RunCallback({});
|
||||
} else {
|
||||
auto find_it = device_map_.find(device_id);
|
||||
if (find_it != device_map_.end()) {
|
||||
auto& device_infos = find_it->second;
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices;
|
||||
devices.reserve(device_infos.size());
|
||||
for (auto& device : device_infos) {
|
||||
chooser_context_->GrantDevicePermission(origin_, *device,
|
||||
web_contents()->GetMainFrame());
|
||||
devices.push_back(device->Clone());
|
||||
}
|
||||
RunCallback(std::move(devices));
|
||||
} else {
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
node::Environment* env = node::Environment::GetCurrent(isolate);
|
||||
EmitWarning(env, "The device id " + device_id + " was not found.",
|
||||
"UnknownHIDDeviceId");
|
||||
RunCallback({});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserController::OnHidManagerConnectionError() {
|
||||
observation_.Reset();
|
||||
}
|
||||
|
||||
void HidChooserController::OnHidChooserContextShutdown() {
|
||||
observation_.Reset();
|
||||
}
|
||||
|
||||
void HidChooserController::OnGotDevices(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devicesToDisplay;
|
||||
devicesToDisplay.reserve(devices.size());
|
||||
|
||||
for (auto& device : devices) {
|
||||
if (DisplayDevice(*device)) {
|
||||
if (AddDeviceInfo(*device)) {
|
||||
devicesToDisplay.push_back(device->Clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to HidChooserContext for OnDeviceAdded/Removed events after the
|
||||
// enumeration.
|
||||
if (chooser_context_)
|
||||
observation_.Observe(chooser_context_.get());
|
||||
bool prevent_default = false;
|
||||
api::Session* session = GetSession();
|
||||
if (session) {
|
||||
auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_);
|
||||
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
|
||||
v8::HandleScope scope(isolate);
|
||||
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
|
||||
.Set("deviceList", devicesToDisplay)
|
||||
.Set("frame", rfh)
|
||||
.Build();
|
||||
prevent_default =
|
||||
session->Emit("select-hid-device", details,
|
||||
base::AdaptCallbackForRepeating(
|
||||
base::BindOnce(&HidChooserController::OnDeviceChosen,
|
||||
weak_factory_.GetWeakPtr())));
|
||||
}
|
||||
if (!prevent_default) {
|
||||
RunCallback({});
|
||||
}
|
||||
}
|
||||
|
||||
bool HidChooserController::DisplayDevice(
|
||||
const device::mojom::HidDeviceInfo& device) const {
|
||||
if (!base::CommandLine::ForCurrentProcess()->HasSwitch(
|
||||
switches::kDisableHidBlocklist)) {
|
||||
// Do not pass the device to the chooser if it is excluded by the blocklist.
|
||||
if (device::HidBlocklist::IsDeviceExcluded(device))
|
||||
return false;
|
||||
|
||||
// Do not pass the device to the chooser if it has a top-level collection
|
||||
// with the FIDO usage page.
|
||||
//
|
||||
// Note: The HID blocklist also blocks top-level collections with the FIDO
|
||||
// usage page, but will not block the device if it has other (non-FIDO)
|
||||
// collections. The check below will exclude the device from the chooser
|
||||
// if it has any top-level FIDO collection.
|
||||
auto find_it =
|
||||
std::find_if(device.collections.begin(), device.collections.end(),
|
||||
[](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return c->usage->usage_page == device::mojom::kPageFido;
|
||||
});
|
||||
if (find_it != device.collections.end())
|
||||
return false;
|
||||
}
|
||||
|
||||
return FilterMatchesAny(device);
|
||||
}
|
||||
|
||||
bool HidChooserController::FilterMatchesAny(
|
||||
const device::mojom::HidDeviceInfo& device) const {
|
||||
if (filters_.empty())
|
||||
return true;
|
||||
|
||||
for (const auto& filter : filters_) {
|
||||
if (filter->device_ids) {
|
||||
if (filter->device_ids->is_vendor()) {
|
||||
if (filter->device_ids->get_vendor() != device.vendor_id)
|
||||
continue;
|
||||
} else if (filter->device_ids->is_vendor_and_product()) {
|
||||
const auto& vendor_and_product =
|
||||
filter->device_ids->get_vendor_and_product();
|
||||
if (vendor_and_product->vendor != device.vendor_id)
|
||||
continue;
|
||||
if (vendor_and_product->product != device.product_id)
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter->usage) {
|
||||
if (filter->usage->is_page()) {
|
||||
const uint16_t usage_page = filter->usage->get_page();
|
||||
auto find_it =
|
||||
std::find_if(device.collections.begin(), device.collections.end(),
|
||||
[=](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return usage_page == c->usage->usage_page;
|
||||
});
|
||||
if (find_it == device.collections.end())
|
||||
continue;
|
||||
} else if (filter->usage->is_usage_and_page()) {
|
||||
const auto& usage_and_page = filter->usage->get_usage_and_page();
|
||||
auto find_it = std::find_if(
|
||||
device.collections.begin(), device.collections.end(),
|
||||
[&usage_and_page](const device::mojom::HidCollectionInfoPtr& c) {
|
||||
return usage_and_page->usage_page == c->usage->usage_page &&
|
||||
usage_and_page->usage == c->usage->usage;
|
||||
});
|
||||
if (find_it == device.collections.end())
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HidChooserController::AddDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto find_it = device_map_.find(id);
|
||||
if (find_it != device_map_.end()) {
|
||||
find_it->second.push_back(device.Clone());
|
||||
return false;
|
||||
}
|
||||
// A new device was connected. Append it to the end of the chooser list.
|
||||
device_map_[id].push_back(device.Clone());
|
||||
items_.push_back(id);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HidChooserController::RemoveDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto find_it = device_map_.find(id);
|
||||
DCHECK(find_it != device_map_.end());
|
||||
auto& device_infos = find_it->second;
|
||||
base::EraseIf(device_infos,
|
||||
[&device](const device::mojom::HidDeviceInfoPtr& d) {
|
||||
return d->guid == device.guid;
|
||||
});
|
||||
if (!device_infos.empty())
|
||||
return false;
|
||||
// A device was disconnected. Remove it from the chooser list.
|
||||
device_map_.erase(find_it);
|
||||
base::Erase(items_, id);
|
||||
return true;
|
||||
}
|
||||
|
||||
void HidChooserController::UpdateDeviceInfo(
|
||||
const device::mojom::HidDeviceInfo& device) {
|
||||
auto id = PhysicalDeviceIdFromDeviceInfo(device);
|
||||
auto physical_device_it = device_map_.find(id);
|
||||
DCHECK(physical_device_it != device_map_.end());
|
||||
auto& device_infos = physical_device_it->second;
|
||||
auto device_it = base::ranges::find_if(
|
||||
device_infos, [&device](const device::mojom::HidDeviceInfoPtr& d) {
|
||||
return d->guid == device.guid;
|
||||
});
|
||||
DCHECK(device_it != device_infos.end());
|
||||
*device_it = device.Clone();
|
||||
}
|
||||
|
||||
void HidChooserController::RunCallback(
|
||||
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
|
||||
if (callback_) {
|
||||
std::move(callback_).Run(std::move(devices));
|
||||
}
|
||||
}
|
||||
|
||||
void HidChooserController::RenderFrameDeleted(
|
||||
content::RenderFrameHost* render_frame_host) {
|
||||
if (hid_delegate_) {
|
||||
hid_delegate_->DeleteControllerForFrame(render_frame_host);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace electron
|
126
shell/browser/hid/hid_chooser_controller.h
Normal file
126
shell/browser/hid/hid_chooser_controller.h
Normal file
|
@ -0,0 +1,126 @@
|
|||
// Copyright (c) 2021 Microsoft, Inc.
|
||||
// Use of this source code is governed by the MIT license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
#ifndef SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
||||
#define SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
||||
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "base/macros.h"
|
||||
#include "base/memory/weak_ptr.h"
|
||||
#include "content/public/browser/global_routing_id.h"
|
||||
#include "content/public/browser/hid_chooser.h"
|
||||
#include "content/public/browser/web_contents.h"
|
||||
#include "content/public/browser/web_contents_observer.h"
|
||||
#include "services/device/public/mojom/hid.mojom-forward.h"
|
||||
#include "shell/browser/api/electron_api_session.h"
|
||||
#include "shell/browser/hid/electron_hid_delegate.h"
|
||||
#include "shell/browser/hid/hid_chooser_context.h"
|
||||
#include "shell/common/gin_converters/frame_converter.h"
|
||||
#include "third_party/blink/public/mojom/hid/hid.mojom.h"
|
||||
|
||||
namespace content {
|
||||
class RenderFrameHost;
|
||||
} // namespace content
|
||||
|
||||
namespace electron {
|
||||
|
||||
class ElectronHidDelegate;
|
||||
|
||||
class HidChooserContext;
|
||||
|
||||
// HidChooserController provides data for the WebHID API permission prompt.
|
||||
class HidChooserController
|
||||
: public content::WebContentsObserver,
|
||||
public electron::HidChooserContext::DeviceObserver {
|
||||
public:
|
||||
// Construct a chooser controller for Human Interface Devices (HID).
|
||||
// |render_frame_host| is used to initialize the chooser strings and to access
|
||||
// the requesting and embedding origins. |callback| is called when the chooser
|
||||
// is closed, either by selecting an item or by dismissing the chooser dialog.
|
||||
// The callback is called with the selected device, or nullptr if no device is
|
||||
// selected.
|
||||
HidChooserController(content::RenderFrameHost* render_frame_host,
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
|
||||
content::HidChooser::Callback callback,
|
||||
content::WebContents* web_contents,
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate);
|
||||
HidChooserController(HidChooserController&) = delete;
|
||||
HidChooserController& operator=(HidChooserController&) = delete;
|
||||
~HidChooserController() override;
|
||||
|
||||
// HidChooserContext::DeviceObserver:
|
||||
void OnDeviceAdded(const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnDeviceRemoved(
|
||||
const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnDeviceChanged(
|
||||
const device::mojom::HidDeviceInfo& device_info) override;
|
||||
void OnHidManagerConnectionError() override;
|
||||
void OnHidChooserContextShutdown() override;
|
||||
|
||||
// content::WebContentsObserver:
|
||||
void RenderFrameDeleted(content::RenderFrameHost* render_frame_host) override;
|
||||
|
||||
private:
|
||||
api::Session* GetSession();
|
||||
void OnGotDevices(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
bool DisplayDevice(const device::mojom::HidDeviceInfo& device) const;
|
||||
bool FilterMatchesAny(const device::mojom::HidDeviceInfo& device) const;
|
||||
|
||||
// Add |device_info| to |device_map_|. The device is added to the chooser item
|
||||
// representing the physical device. If the chooser item does not yet exist, a
|
||||
// new item is appended. Returns true if an item was appended.
|
||||
bool AddDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
// Remove |device_info| from |device_map_|. The device info is removed from
|
||||
// the chooser item representing the physical device. If this would cause the
|
||||
// item to be empty, the chooser item is removed. Does nothing if the device
|
||||
// is not in the chooser item. Returns true if an item was removed.
|
||||
bool RemoveDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
// Update the information for the device described by |device_info| in the
|
||||
// |device_map_|.
|
||||
void UpdateDeviceInfo(const device::mojom::HidDeviceInfo& device_info);
|
||||
|
||||
void RunCallback(std::vector<device::mojom::HidDeviceInfoPtr> devices);
|
||||
void OnDeviceChosen(gin::Arguments* args);
|
||||
|
||||
std::vector<blink::mojom::HidDeviceFilterPtr> filters_;
|
||||
content::HidChooser::Callback callback_;
|
||||
const url::Origin origin_;
|
||||
const int frame_tree_node_id_;
|
||||
|
||||
// The lifetime of the chooser context is tied to the browser context used to
|
||||
// create it, and may be destroyed while the chooser is still active.
|
||||
base::WeakPtr<HidChooserContext> chooser_context_;
|
||||
|
||||
// Information about connected devices and their HID interfaces. A single
|
||||
// physical device may expose multiple HID interfaces. Keys are physical
|
||||
// device IDs, values are collections of HidDeviceInfo objects representing
|
||||
// the HID interfaces hosted by the physical device.
|
||||
std::map<std::string, std::vector<device::mojom::HidDeviceInfoPtr>>
|
||||
device_map_;
|
||||
|
||||
// An ordered list of physical device IDs that determines the order of items
|
||||
// in the chooser.
|
||||
std::vector<std::string> items_;
|
||||
|
||||
base::ScopedObservation<HidChooserContext,
|
||||
HidChooserContext::DeviceObserver,
|
||||
&HidChooserContext::AddDeviceObserver,
|
||||
&HidChooserContext::RemoveDeviceObserver>
|
||||
observation_{this};
|
||||
|
||||
base::WeakPtr<ElectronHidDelegate> hid_delegate_;
|
||||
|
||||
content::GlobalRenderFrameHostId render_frame_host_id_;
|
||||
|
||||
base::WeakPtrFactory<HidChooserController> weak_factory_{this};
|
||||
};
|
||||
|
||||
} // namespace electron
|
||||
|
||||
#endif // SHELL_BROWSER_HID_HID_CHOOSER_CONTROLLER_H_
|
|
@ -94,6 +94,28 @@ bool WebContentsPermissionHelper::CheckPermission(
|
|||
details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
|
||||
return permission_manager->CheckDevicePermission(permission, origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::GrantDevicePermission(
|
||||
content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
auto* permission_manager = static_cast<ElectronPermissionManager*>(
|
||||
web_contents_->GetBrowserContext()->GetPermissionControllerDelegate());
|
||||
permission_manager->GrantDevicePermission(permission, origin, device,
|
||||
render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::RequestFullscreenPermission(
|
||||
base::OnceCallback<void(bool)> callback) {
|
||||
RequestPermission(
|
||||
|
@ -168,6 +190,32 @@ bool WebContentsPermissionHelper::CheckSerialAccessPermission(
|
|||
static_cast<content::PermissionType>(PermissionType::SERIAL), &details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckHIDAccessPermission(
|
||||
const url::Origin& embedding_origin) const {
|
||||
base::DictionaryValue details;
|
||||
details.SetString("securityOrigin", embedding_origin.GetURL().spec());
|
||||
return CheckPermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), &details);
|
||||
}
|
||||
|
||||
bool WebContentsPermissionHelper::CheckHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
return CheckDevicePermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), origin,
|
||||
&device, render_frame_host);
|
||||
}
|
||||
|
||||
void WebContentsPermissionHelper::GrantHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const {
|
||||
return GrantDevicePermission(
|
||||
static_cast<content::PermissionType>(PermissionType::HID), origin,
|
||||
&device, render_frame_host);
|
||||
}
|
||||
|
||||
WEB_CONTENTS_USER_DATA_KEY_IMPL(WebContentsPermissionHelper)
|
||||
|
||||
} // namespace electron
|
||||
|
|
|
@ -23,7 +23,8 @@ class WebContentsPermissionHelper
|
|||
POINTER_LOCK = static_cast<int>(content::PermissionType::NUM) + 1,
|
||||
FULLSCREEN,
|
||||
OPEN_EXTERNAL,
|
||||
SERIAL
|
||||
SERIAL,
|
||||
HID
|
||||
};
|
||||
|
||||
// Asynchronous Requests
|
||||
|
@ -41,6 +42,15 @@ class WebContentsPermissionHelper
|
|||
bool CheckMediaAccessPermission(const GURL& security_origin,
|
||||
blink::mojom::MediaStreamType type) const;
|
||||
bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const;
|
||||
bool CheckHIDAccessPermission(const url::Origin& embedding_origin) const;
|
||||
bool CheckHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
void GrantHIDDevicePermission(
|
||||
const url::Origin& origin,
|
||||
base::Value device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
private:
|
||||
explicit WebContentsPermissionHelper(content::WebContents* web_contents);
|
||||
|
@ -54,6 +64,16 @@ class WebContentsPermissionHelper
|
|||
bool CheckPermission(content::PermissionType permission,
|
||||
const base::DictionaryValue* details) const;
|
||||
|
||||
bool CheckDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
void GrantDevicePermission(content::PermissionType permission,
|
||||
const url::Origin& origin,
|
||||
const base::Value* device,
|
||||
content::RenderFrameHost* render_frame_host) const;
|
||||
|
||||
content::WebContents* web_contents_;
|
||||
|
||||
WEB_CONTENTS_USER_DATA_KEY_DECL();
|
||||
|
|
|
@ -209,6 +209,8 @@ v8::Local<v8::Value> Converter<content::PermissionType>::ToV8(
|
|||
return StringToV8(isolate, "openExternal");
|
||||
case PermissionType::SERIAL:
|
||||
return StringToV8(isolate, "serial");
|
||||
case PermissionType::HID:
|
||||
return StringToV8(isolate, "hid");
|
||||
default:
|
||||
return StringToV8(isolate, "unknown");
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { expect } from 'chai';
|
||||
import { BrowserWindow, WebContents, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { BrowserWindow, WebContents, webFrameMain, session, ipcMain, app, protocol, webContents } from 'electron/main';
|
||||
import { emittedOnce } from './events-helpers';
|
||||
import { closeAllWindows } from './window-helpers';
|
||||
import * as https from 'https';
|
||||
|
@ -1878,3 +1878,127 @@ describe('navigator.bluetooth', () => {
|
|||
expect(bluetooth).to.be.oneOf(['Found a device!', 'Bluetooth adapter not available.', 'User cancelled the requestDevice() chooser.']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('navigator.hid', () => {
|
||||
let w: BrowserWindow;
|
||||
let server: http.Server;
|
||||
let serverUrl: string;
|
||||
before(async () => {
|
||||
w = new BrowserWindow({
|
||||
show: false
|
||||
});
|
||||
await w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
server = http.createServer((req, res) => {
|
||||
res.setHeader('Content-Type', 'text/html');
|
||||
res.end('<body>');
|
||||
});
|
||||
await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
|
||||
serverUrl = `http://localhost:${(server.address() as any).port}`;
|
||||
});
|
||||
|
||||
const getDevices: any = () => {
|
||||
return w.webContents.executeJavaScript(`
|
||||
navigator.hid.requestDevice({filters: []}).then(device => device.toString()).catch(err => err.toString());
|
||||
`, true);
|
||||
};
|
||||
|
||||
after(() => {
|
||||
server.close();
|
||||
closeAllWindows();
|
||||
});
|
||||
afterEach(() => {
|
||||
session.defaultSession.setPermissionCheckHandler(null);
|
||||
session.defaultSession.setDevicePermissionHandler(null);
|
||||
session.defaultSession.removeAllListeners('select-hid-device');
|
||||
});
|
||||
|
||||
it('does not return a device if select-hid-device event is not defined', async () => {
|
||||
w.loadFile(path.join(fixturesPath, 'pages', 'blank.html'));
|
||||
const device = await getDevices();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('does not return a device when permission denied', async () => {
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setPermissionCheckHandler(() => false);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.false();
|
||||
expect(device).to.equal('');
|
||||
});
|
||||
|
||||
it('returns a device when select-hid-device event is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
expect(details.frame).to.have.ownProperty('frameTreeNodeId').that.is.a('number');
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
haveDevices = true;
|
||||
callback(details.deviceList[0].deviceId);
|
||||
} else {
|
||||
callback();
|
||||
}
|
||||
});
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
if (process.arch === 'arm64' || process.arch === 'arm') {
|
||||
// arm CI returns HID devices - this block may need to change if CI hardware changes.
|
||||
expect(haveDevices).to.be.true();
|
||||
// Verify that navigation will clear device permissions
|
||||
const grantedDevices = await w.webContents.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevices).to.not.be.empty();
|
||||
w.loadURL(serverUrl);
|
||||
const [,,,,, frameProcessId, frameRoutingId] = await emittedOnce(w.webContents, 'did-frame-navigate');
|
||||
const frame = webFrameMain.fromId(frameProcessId, frameRoutingId);
|
||||
expect(frame).to.not.be.empty();
|
||||
if (frame) {
|
||||
const grantedDevicesOnNewPage = await frame.executeJavaScript('navigator.hid.getDevices()');
|
||||
expect(grantedDevicesOnNewPage).to.be.empty();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('returns a device when DevicePermissionHandler is defined', async () => {
|
||||
let haveDevices = false;
|
||||
let selectFired = false;
|
||||
let gotDevicePerms = false;
|
||||
w.webContents.session.on('select-hid-device', (event, details, callback) => {
|
||||
selectFired = true;
|
||||
if (details.deviceList.length > 0) {
|
||||
const foundDevice = details.deviceList.find((device) => {
|
||||
if (device.name && device.name !== '' && device.serialNumber && device.serialNumber !== '') {
|
||||
haveDevices = true;
|
||||
return true;
|
||||
}
|
||||
});
|
||||
if (foundDevice) {
|
||||
callback(foundDevice.deviceId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
callback();
|
||||
});
|
||||
session.defaultSession.setDevicePermissionHandler(() => {
|
||||
gotDevicePerms = true;
|
||||
return true;
|
||||
});
|
||||
await w.webContents.executeJavaScript('navigator.hid.getDevices();', true);
|
||||
const device = await getDevices();
|
||||
expect(selectFired).to.be.true();
|
||||
if (haveDevices) {
|
||||
expect(device).to.contain('[object HIDDevice]');
|
||||
expect(gotDevicePerms).to.be.true();
|
||||
} else {
|
||||
expect(device).to.equal('');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue