feat: add USB protected classes handler (#38263)

* feat: add USB protected classes handler

* chore: apply review suggestions

Co-authored-by: Charles Kerr <charles@charleskerr.com>

* chore: update docs

* chore: apply review suggestions

* update doc per suggestion

---------

Co-authored-by: Charles Kerr <charles@charleskerr.com>
This commit is contained in:
John Kleinschmidt 2023-05-30 12:35:25 -04:00 коммит произвёл GitHub
Родитель 71fb19ea14
Коммит b4ec363b3d
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
9 изменённых файлов: 176 добавлений и 31 удалений

Просмотреть файл

@ -1091,6 +1091,55 @@ app.whenReady().then(() => {
})
```
#### `ses.setUSBProtectedClassesHandler(handler)`
* `handler` Function\<string[]> | null
* `details` Object
* `protectedClasses` string[] - The current list of protected USB classes. Possible class values are:
* `audio`
* `audio-video`
* `hid`
* `mass-storage`
* `smart-card`
* `video`
* `wireless`
Sets the handler which can be used to override which [USB classes are protected](https://wicg.github.io/webusb/#usbinterface-interface).
The return value for the handler is a string array of USB classes which should be considered protected (eg not available in the renderer). Valid values for the array are:
* `audio`
* `audio-video`
* `hid`
* `mass-storage`
* `smart-card`
* `video`
* `wireless`
Returning an empty string array from the handler will allow all USB classes; returning the passed in array will maintain the default list of protected USB classes (this is also the default behavior if a handler is not defined).
To clear the handler, call `setUSBProtectedClassesHandler(null)`.
```javascript
const { app, BrowserWindow } = require('electron')
let win = null
app.whenReady().then(() => {
win = new BrowserWindow()
win.webContents.session.setUSBProtectedClassesHandler((details) => {
// Allow all classes:
// return []
// Keep the current set of protected classes:
// return details.protectedClasses
// Selectively remove classes:
return details.protectedClasses.filter((usbClass) => {
// Exclude classes except for audio classes
return usbClass.indexOf('audio') === -1
})
})
})
```
#### `ses.setBluetoothPairingHandler(handler)` _Windows_ _Linux_
* `handler` Function | null

Просмотреть файл

@ -51,6 +51,13 @@ function createWindow () {
}
})
mainWindow.webContents.session.setUSBProtectedClassesHandler((details) => {
return details.protectedClasses.filter((usbClass) => {
// Exclude classes except for audio classes
return usbClass.indexOf('audio') === -1
})
})
mainWindow.loadFile('index.html')
}

Просмотреть файл

@ -142,6 +142,8 @@ Electron provides several APIs for working with the WebUSB API:
`setDevicePermissionHandler`.
* [`ses.setPermissionCheckHandler(handler)`](../api/session.md#sessetpermissioncheckhandlerhandler)
can be used to disable USB access for specific origins.
* [`ses.setUSBProtectedClassesHandler](../api/session.md#sessetusbprotectedclasseshandlerhandler)
can be used to allow usage of [protected USB classes](https://wicg.github.io/webusb/#usbinterface-interface) that are not available by default.
### Example

Просмотреть файл

@ -74,6 +74,7 @@
#include "shell/common/gin_converters/gurl_converter.h"
#include "shell/common/gin_converters/media_converter.h"
#include "shell/common/gin_converters/net_converter.h"
#include "shell/common/gin_converters/usb_protected_classes_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/dictionary.h"
#include "shell/common/gin_helper/object_template_builder.h"
@ -699,6 +700,18 @@ void Session::SetDevicePermissionHandler(v8::Local<v8::Value> val,
permission_manager->SetDevicePermissionHandler(handler);
}
void Session::SetUSBProtectedClassesHandler(v8::Local<v8::Value> val,
gin::Arguments* args) {
ElectronPermissionManager::ProtectedUSBHandler 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->SetProtectedUSBHandler(handler);
}
void Session::SetBluetoothPairingHandler(v8::Local<v8::Value> val,
gin::Arguments* args) {
ElectronPermissionManager::BluetoothPairingHandler handler;
@ -1294,6 +1307,8 @@ void Session::FillObjectTemplate(v8::Isolate* isolate,
&Session::SetDisplayMediaRequestHandler)
.SetMethod("setDevicePermissionHandler",
&Session::SetDevicePermissionHandler)
.SetMethod("setUSBProtectedClassesHandler",
&Session::SetUSBProtectedClassesHandler)
.SetMethod("setBluetoothPairingHandler",
&Session::SetBluetoothPairingHandler)
.SetMethod("clearHostResolverCache", &Session::ClearHostResolverCache)

Просмотреть файл

@ -118,6 +118,8 @@ class Session : public gin::Wrappable<Session>,
gin::Arguments* args);
void SetDevicePermissionHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
void SetUSBProtectedClassesHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
void SetBluetoothPairingHandler(v8::Local<v8::Value> val,
gin::Arguments* args);
v8::Local<v8::Promise> ClearHostResolverCache(gin::Arguments* args);

Просмотреть файл

@ -25,6 +25,7 @@
#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/usb_protected_classes_converter.h"
#include "shell/common/gin_converters/value_converter.h"
#include "shell/common/gin_helper/event_emitter_caller.h"
#include "third_party/blink/public/common/permissions/permission_utils.h"
@ -130,6 +131,11 @@ void ElectronPermissionManager::SetDevicePermissionHandler(
device_permission_handler_ = handler;
}
void ElectronPermissionManager::SetProtectedUSBHandler(
const ProtectedUSBHandler& handler) {
protected_usb_handler_ = handler;
}
void ElectronPermissionManager::SetBluetoothPairingHandler(
const BluetoothPairingHandler& handler) {
bluetooth_pairing_handler_ = handler;
@ -362,6 +368,21 @@ void ElectronPermissionManager::RevokeDevicePermission(
browser_context->RevokeDevicePermission(origin, device, permission);
}
ElectronPermissionManager::USBProtectedClasses
ElectronPermissionManager::CheckProtectedUSBClasses(
const USBProtectedClasses& classes) const {
if (protected_usb_handler_.is_null()) {
return classes;
} else {
v8::Isolate* isolate = JavascriptEnvironment::GetIsolate();
v8::HandleScope scope(isolate);
v8::Local<v8::Object> details = gin::DataObjectBuilder(isolate)
.Set("protectedClasses", classes)
.Build();
return protected_usb_handler_.Run(details);
}
}
blink::mojom::PermissionStatus
ElectronPermissionManager::GetPermissionStatusForCurrentDocument(
blink::PermissionType permission,

Просмотреть файл

@ -35,6 +35,8 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
ElectronPermissionManager& operator=(const ElectronPermissionManager&) =
delete;
using USBProtectedClasses = std::vector<uint8_t>;
using StatusCallback =
base::OnceCallback<void(blink::mojom::PermissionStatus)>;
using StatusesCallback = base::OnceCallback<void(
@ -52,6 +54,10 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
using DeviceCheckHandler =
base::RepeatingCallback<bool(const v8::Local<v8::Object>&)>;
using ProtectedUSBHandler = base::RepeatingCallback<USBProtectedClasses(
const v8::Local<v8::Object>&)>;
using BluetoothPairingHandler =
base::RepeatingCallback<void(gin_helper::Dictionary, PairCallback)>;
@ -59,6 +65,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
void SetPermissionRequestHandler(const RequestHandler& handler);
void SetPermissionCheckHandler(const CheckHandler& handler);
void SetDevicePermissionHandler(const DeviceCheckHandler& handler);
void SetProtectedUSBHandler(const ProtectedUSBHandler& handler);
void SetBluetoothPairingHandler(const BluetoothPairingHandler& handler);
// content::PermissionControllerDelegate:
@ -109,6 +116,9 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
const base::Value& object,
ElectronBrowserContext* browser_context) const;
USBProtectedClasses CheckProtectedUSBClasses(
const USBProtectedClasses& classes) const;
protected:
void OnPermissionResponse(int request_id,
int permission_id,
@ -159,6 +169,7 @@ class ElectronPermissionManager : public content::PermissionControllerDelegate {
RequestHandler request_handler_;
CheckHandler check_handler_;
DeviceCheckHandler device_permission_handler_;
ProtectedUSBHandler protected_usb_handler_;
BluetoothPairingHandler bluetooth_pairing_handler_;
PendingRequestsMap pending_requests_;

Просмотреть файл

@ -152,37 +152,9 @@ void ElectronUsbDelegate::AdjustProtectedInterfaceClasses(
const url::Origin& origin,
content::RenderFrameHost* frame,
std::vector<uint8_t>& classes) {
// Isolated Apps have unrestricted access to any USB interface class.
if (frame &&
frame->GetWebExposedIsolationLevel() >=
content::WebExposedIsolationLevel::kMaybeIsolatedApplication) {
// TODO(https://crbug.com/1236706): Should the list of interface classes the
// app expects to claim be encoded in the Web App Manifest?
classes.clear();
return;
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Don't enforce protected interface classes for Chrome Apps since the
// chrome.usb API has no such restriction.
if (origin.scheme() == extensions::kExtensionScheme) {
auto* extension_registry =
extensions::ExtensionRegistry::Get(browser_context);
if (extension_registry) {
const extensions::Extension* extension =
extension_registry->enabled_extensions().GetByID(origin.host());
if (extension && extension->is_platform_app()) {
classes.clear();
return;
}
}
}
if (origin.scheme() == extensions::kExtensionScheme &&
base::Contains(kSmartCardPrivilegedExtensionIds, origin.host())) {
base::Erase(classes, device::mojom::kUsbSmartCardClass);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
auto* permission_manager = static_cast<ElectronPermissionManager*>(
browser_context->GetPermissionControllerDelegate());
classes = permission_manager->CheckProtectedUSBClasses(classes);
}
std::unique_ptr<UsbChooser> ElectronUsbDelegate::RunChooser(

Просмотреть файл

@ -0,0 +1,66 @@
// Copyright (c) 2022 Microsoft, Inc.
// Use of this source code is governed by the MIT license that can be
// found in the LICENSE file.
#ifndef ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_
#define ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_
#include <string>
#include <string_view>
#include <utility>
#include <vector>
#include "gin/converter.h"
#include "services/device/public/mojom/usb_device.mojom-forward.h"
#include "shell/browser/electron_permission_manager.h"
namespace gin {
static auto constexpr ClassMapping =
std::array<std::pair<uint8_t, std::string_view>, 7>{
{{device::mojom::kUsbAudioClass, "audio"},
{device::mojom::kUsbHidClass, "hid"},
{device::mojom::kUsbMassStorageClass, "mass-storage"},
{device::mojom::kUsbSmartCardClass, "smart-card"},
{device::mojom::kUsbVideoClass, "video"},
{device::mojom::kUsbAudioVideoClass, "audio-video"},
{device::mojom::kUsbWirelessClass, "wireless"}}};
template <>
struct Converter<electron::ElectronPermissionManager::USBProtectedClasses> {
static v8::Local<v8::Value> ToV8(
v8::Isolate* isolate,
const electron::ElectronPermissionManager::USBProtectedClasses& classes) {
std::vector<std::string> class_strings;
class_strings.reserve(std::size(classes));
for (const auto& itr : classes) {
for (const auto& [usb_class, name] : ClassMapping) {
if (usb_class == itr)
class_strings.emplace_back(name);
}
}
return gin::ConvertToV8(isolate, class_strings);
}
static bool FromV8(
v8::Isolate* isolate,
v8::Local<v8::Value> val,
electron::ElectronPermissionManager::USBProtectedClasses* out) {
std::vector<std::string> class_strings;
if (ConvertFromV8(isolate, val, &class_strings)) {
out->reserve(std::size(class_strings));
for (const auto& itr : class_strings) {
for (const auto& [usb_class, name] : ClassMapping) {
if (name == itr)
out->emplace_back(usb_class);
}
}
return true;
}
return false;
}
};
} // namespace gin
#endif // ELECTRON_SHELL_COMMON_GIN_CONVERTERS_USB_PROTECTED_CLASSES_CONVERTER_H_