diff --git a/docs/api/session.md b/docs/api/session.md index 738af3f373..11f9a615d2 100644 --- a/docs/api/session.md +++ b/docs/api/session.md @@ -297,6 +297,35 @@ app.whenReady().then(() => { width: 800, height: 600 }) + + win.webContents.session.setPermissionCheckHandler((webContents, permission, requestingOrigin, details) => { + if (permission === 'serial') { + // Add logic here to determine if permission should be given to allow serial 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 === 'serial') { + if (details.device.vendorId === 123 && details.device.productId === 345) { + // Always allow this type of device (this allows skipping the call to `navigator.serial.requestPort` 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-serial-port', (event, portList, webContents, callback) => { event.preventDefault() const selectedPort = portList.find((device) => { @@ -647,9 +676,9 @@ session.fromPartition('some-partition').setPermissionCheckHandler((webContents, * `handler` Function\ | null * `details` Object - * `deviceType` String - The type of device that permission is being requested on, can be `hid`. + * `deviceType` String - The type of device that permission is being requested on, can be `hid` or `serial`. * `origin` String - The origin URL of the device permission check. - * `device` [HIDDevice](structures/hid-device.md) - the device that permission is being requested for. + * `device` [HIDDevice](structures/hid-device.md) | [SerialPort](structures/serial-port.md)- the device that permission is being requested for. * `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`. @@ -674,6 +703,8 @@ app.whenReady().then(() => { if (permission === 'hid') { // Add logic here to determine if permission should be given to allow HID selection return true + } else if (permission === 'serial') { + // Add logic here to determine if permission should be given to allow serial port selection } return false }) @@ -694,6 +725,11 @@ app.whenReady().then(() => { grantedDevice.productId === details.device.productId && grantedDevice.serialNumber && grantedDevice.serialNumber === details.device.serialNumber }) + } else if (details.deviceType === 'serial') { + 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 + } } return false }) diff --git a/docs/tutorial/devices.md b/docs/tutorial/devices.md index 02194a79ca..c5a81fc994 100644 --- a/docs/tutorial/devices.md +++ b/docs/tutorial/devices.md @@ -84,13 +84,22 @@ There are several additional APIs for working with the Web Serial API: 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.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler) + can be used to provide default permissioning to devices without first calling + for permission to devices via `navigator.serial.requestPort`. 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-serial-port` event) and then read from that storage with + `setDevicePermissionHandler`. * [`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 +serial devices through [`ses.setDevicePermissionHandler(handler)`](../api/session.md#sessetdevicepermissionhandlerhandler) +as well as demonstrating selecting 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. diff --git a/shell/browser/electron_browser_context.cc b/shell/browser/electron_browser_context.cc index 502858f117..9004709c61 100644 --- a/shell/browser/electron_browser_context.cc +++ b/shell/browser/electron_browser_context.cc @@ -8,8 +8,6 @@ #include -#include - #include "base/barrier_closure.h" #include "base/base_paths.h" #include "base/command_line.h" @@ -95,9 +93,6 @@ std::string MakePartitionName(const std::string& input) { } // namespace -const char kSerialGrantedDevicesPref[] = - "profile.content_settings.exceptions.serial-chooser-data"; - // static ElectronBrowserContext::BrowserContextMap& ElectronBrowserContext::browser_context_map() { @@ -194,7 +189,6 @@ void ElectronBrowserContext::InitPrefs() { registry->RegisterFilePathPref(prefs::kDownloadDefaultDirectory, download_dir); registry->RegisterDictionaryPref(prefs::kDevToolsFileSystemPaths); - registry->RegisterDictionaryPref(kSerialGrantedDevicesPref); InspectableWebContents::RegisterPrefs(registry.get()); MediaDeviceIDSalt::RegisterPrefs(registry.get()); ZoomLevelDelegate::RegisterPrefs(registry.get()); @@ -420,54 +414,6 @@ void ElectronBrowserContext::SetSSLConfigClient( ssl_config_client_ = std::move(client); } -void ElectronBrowserContext::GrantObjectPermission( - const url::Origin& origin, - base::Value object, - const std::string& pref_key) { - std::string origin_string = origin.Serialize(); - DictionaryPrefUpdate update(prefs(), pref_key); - base::Value* const current_objects = update.Get(); - if (!current_objects || !current_objects->is_dict()) { - base::ListValue objects_for_origin; - objects_for_origin.Append(std::move(object)); - base::DictionaryValue objects_by_origin; - objects_by_origin.SetPath(origin_string, std::move(objects_for_origin)); - prefs()->Set(pref_key, std::move(objects_by_origin)); - } else { - base::Value* const objects_mutable = - current_objects->FindListKey(origin_string); - if (objects_mutable) { - base::Value::ListStorage objects = std::move(*objects_mutable).TakeList(); - objects.push_back(std::move(object)); - *objects_mutable = base::Value(std::move(objects)); - } else { - base::Value new_objects(base::Value::Type::LIST); - new_objects.Append(std::move(object)); - current_objects->SetKey(origin_string, std::move(new_objects)); - } - } -} - -std::vector> -ElectronBrowserContext::GetGrantedObjects(const url::Origin& origin, - const std::string& pref_key) { - auto* current_objects = prefs()->Get(pref_key); - if (!current_objects || !current_objects->is_dict()) { - return {}; - } - - const base::Value* objects_for_origin = - current_objects->FindPath(origin.Serialize()); - if (!objects_for_origin) - return {}; - - std::vector> results; - for (const auto& object : objects_for_origin->GetList()) - results.push_back(std::make_unique(object.Clone())); - - return results; -} - // static ElectronBrowserContext* ElectronBrowserContext::From( const std::string& partition, diff --git a/shell/browser/electron_browser_context.h b/shell/browser/electron_browser_context.h index 86dc1f56de..c7cf25eb75 100644 --- a/shell/browser/electron_browser_context.h +++ b/shell/browser/electron_browser_context.h @@ -8,7 +8,6 @@ #include #include #include -#include #include "base/memory/weak_ptr.h" #include "chrome/browser/predictors/preconnect_manager.h" @@ -47,9 +46,6 @@ class ResolveProxyHelper; class WebViewManager; class ProtocolRegistry; -// Preference keys for device apis -extern const char kSerialGrantedDevicesPref[]; - class ElectronBrowserContext : public content::BrowserContext { public: // partition_id => browser_context @@ -146,19 +142,6 @@ class ElectronBrowserContext : public content::BrowserContext { network::mojom::SSLConfigPtr GetSSLConfig(); void SetSSLConfigClient(mojo::Remote client); - // Grants |origin| access to |object| by writing it into the browser context. - // To be used in place of ObjectPermissionContextBase::GrantObjectPermission. - void GrantObjectPermission(const url::Origin& origin, - base::Value object, - const std::string& pref_key); - - // Returns the list of objects that |origin| has been granted permission to - // access. To be used in place of - // ObjectPermissionContextBase::GetGrantedObjects. - std::vector> GetGrantedObjects( - const url::Origin& origin, - const std::string& pref_key); - ~ElectronBrowserContext() override; private: diff --git a/shell/browser/electron_permission_manager.cc b/shell/browser/electron_permission_manager.cc index cc38879d1d..a7e832aa1b 100644 --- a/shell/browser/electron_permission_manager.cc +++ b/shell/browser/electron_permission_manager.cc @@ -22,6 +22,7 @@ #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/serial/serial_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" @@ -328,6 +329,31 @@ bool ElectronPermissionManager::CheckDevicePermission( if (serial_number && device_serial_number && *device_serial_number == *serial_number) return true; + } else if (permission == + static_cast( + WebContentsPermissionHelper::PermissionType::SERIAL)) { +#if defined(OS_WIN) + if (device->FindStringKey(kDeviceInstanceIdKey) == + granted_device.FindStringKey(kDeviceInstanceIdKey)) + return true; +#else + if (device->FindIntKey(kVendorIdKey) != + granted_device.FindIntKey(kVendorIdKey) || + device->FindIntKey(kProductIdKey) != + granted_device.FindIntKey(kProductIdKey) || + *device->FindStringKey(kSerialNumberKey) != + *granted_device.FindStringKey(kSerialNumberKey)) { + continue; + } + +#if defined(OS_MAC) + if (*device->FindStringKey(kUsbDriverKey) != + *granted_device.FindStringKey(kUsbDriverKey)) { + continue; + } +#endif // defined(OS_MAC) + return true; +#endif // defined(OS_WIN) } } } diff --git a/shell/browser/serial/electron_serial_delegate.cc b/shell/browser/serial/electron_serial_delegate.cc index ad21fbc7f5..c2b6df4504 100644 --- a/shell/browser/serial/electron_serial_delegate.cc +++ b/shell/browser/serial/electron_serial_delegate.cc @@ -60,7 +60,7 @@ bool ElectronSerialDelegate::HasPortPermission( auto* chooser_context = SerialChooserContextFactory::GetForBrowserContext(browser_context); return chooser_context->HasPortPermission( - web_contents->GetMainFrame()->GetLastCommittedOrigin(), port); + web_contents->GetMainFrame()->GetLastCommittedOrigin(), port, frame); } device::mojom::SerialPortManager* ElectronSerialDelegate::GetPortManager( diff --git a/shell/browser/serial/serial_chooser_context.cc b/shell/browser/serial/serial_chooser_context.cc index f90920e985..594c9b4169 100644 --- a/shell/browser/serial/serial_chooser_context.cc +++ b/shell/browser/serial/serial_chooser_context.cc @@ -13,22 +13,25 @@ #include "base/strings/utf_string_conversions.h" #include "base/values.h" #include "content/public/browser/device_service.h" +#include "content/public/browser/web_contents.h" #include "mojo/public/cpp/bindings/pending_remote.h" +#include "shell/browser/web_contents_permission_helper.h" namespace electron { constexpr char kPortNameKey[] = "name"; constexpr char kTokenKey[] = "token"; + #if defined(OS_WIN) -constexpr char kDeviceInstanceIdKey[] = "device_instance_id"; +const char kDeviceInstanceIdKey[] = "device_instance_id"; #else -constexpr char kVendorIdKey[] = "vendor_id"; -constexpr char kProductIdKey[] = "product_id"; -constexpr char kSerialNumberKey[] = "serial_number"; +const char kVendorIdKey[] = "vendor_id"; +const char kProductIdKey[] = "product_id"; +const char kSerialNumberKey[] = "serial_number"; #if defined(OS_MAC) -constexpr char kUsbDriverKey[] = "usb_driver"; +const char kUsbDriverKey[] = "usb_driver"; #endif // defined(OS_MAC) -#endif // defined(OS_WIN) +#endif // defined(OS_WIN std::string EncodeToken(const base::UnguessableToken& token) { const uint64_t data[2] = {token.GetHighForSerialization(), @@ -83,21 +86,24 @@ base::Value PortInfoToValue(const device::mojom::SerialPortInfo& port) { return value; } -SerialChooserContext::SerialChooserContext( - ElectronBrowserContext* browser_context) - : browser_context_(browser_context) {} +SerialChooserContext::SerialChooserContext() = default; SerialChooserContext::~SerialChooserContext() = default; void SerialChooserContext::GrantPortPermission( const url::Origin& origin, - const device::mojom::SerialPortInfo& port) { + const device::mojom::SerialPortInfo& port, + content::RenderFrameHost* render_frame_host) { base::Value value = PortInfoToValue(port); port_info_.insert({port.token, value.Clone()}); if (CanStorePersistentEntry(port)) { - browser_context_->GrantObjectPermission(origin, std::move(value), - kSerialGrantedDevicesPref); + auto* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + auto* permission_helper = + WebContentsPermissionHelper::FromWebContents(web_contents); + permission_helper->GrantSerialPortPermission(origin, std::move(value), + render_frame_host); return; } @@ -106,7 +112,8 @@ void SerialChooserContext::GrantPortPermission( bool SerialChooserContext::HasPortPermission( const url::Origin& origin, - const device::mojom::SerialPortInfo& port) { + const device::mojom::SerialPortInfo& port, + content::RenderFrameHost* render_frame_host) { auto it = ephemeral_ports_.find(origin); if (it != ephemeral_ports_.end()) { const std::set ports = it->second; @@ -118,39 +125,13 @@ bool SerialChooserContext::HasPortPermission( return false; } - std::vector> object_list = - browser_context_->GetGrantedObjects(origin, kSerialGrantedDevicesPref); - for (const auto& device : object_list) { -#if defined(OS_WIN) - const std::string& device_instance_id = - *device->FindStringKey(kDeviceInstanceIdKey); - if (port.device_instance_id == device_instance_id) - return true; -#else - const int vendor_id = *device->FindIntKey(kVendorIdKey); - const int product_id = *device->FindIntKey(kProductIdKey); - const std::string& serial_number = *device->FindStringKey(kSerialNumberKey); - - // Guaranteed by the CanStorePersistentEntry) check above. - DCHECK(port.has_vendor_id); - DCHECK(port.has_product_id); - DCHECK(port.serial_number && !port.serial_number->empty()); - if (port.vendor_id != vendor_id || port.product_id != product_id || - port.serial_number != serial_number) { - continue; - } - -#if defined(OS_MAC) - const std::string& usb_driver_name = *device->FindStringKey(kUsbDriverKey); - if (port.usb_driver_name != usb_driver_name) { - continue; - } -#endif // defined(OS_MAC) - - return true; -#endif // defined(OS_WIN) - } - return false; + auto* web_contents = + content::WebContents::FromRenderFrameHost(render_frame_host); + auto* permission_helper = + WebContentsPermissionHelper::FromWebContents(web_contents); + base::Value value = PortInfoToValue(port); + return permission_helper->CheckSerialPortPermission(origin, std::move(value), + render_frame_host); } // static diff --git a/shell/browser/serial/serial_chooser_context.h b/shell/browser/serial/serial_chooser_context.h index 490051e484..289b716aef 100644 --- a/shell/browser/serial/serial_chooser_context.h +++ b/shell/browser/serial/serial_chooser_context.h @@ -29,19 +29,35 @@ class Value; namespace electron { +extern const char kHidVendorIdKey[]; +extern const char kHidProductIdKey[]; + +#if defined(OS_WIN) +extern const char kDeviceInstanceIdKey[]; +#else +extern const char kVendorIdKey[]; +extern const char kProductIdKey[]; +extern const char kSerialNumberKey[]; +#if defined(OS_MAC) +extern const char kUsbDriverKey[]; +#endif // defined(OS_MAC) +#endif // defined(OS_WIN) + class SerialChooserContext : public KeyedService, public device::mojom::SerialPortManagerClient { public: using PortObserver = content::SerialDelegate::Observer; - explicit SerialChooserContext(ElectronBrowserContext* browser_context); + SerialChooserContext(); ~SerialChooserContext() override; // Serial-specific interface for granting and checking permissions. void GrantPortPermission(const url::Origin& origin, - const device::mojom::SerialPortInfo& port); + const device::mojom::SerialPortInfo& port, + content::RenderFrameHost* render_frame_host); bool HasPortPermission(const url::Origin& origin, - const device::mojom::SerialPortInfo& port); + const device::mojom::SerialPortInfo& port, + content::RenderFrameHost* render_frame_host); static bool CanStorePersistentEntry( const device::mojom::SerialPortInfo& port); @@ -62,8 +78,6 @@ class SerialChooserContext : public KeyedService, mojo::PendingRemote manager); void OnPortManagerConnectionError(); - ElectronBrowserContext* browser_context_; - // Tracks the set of ports to which an origin has access to. std::map> ephemeral_ports_; diff --git a/shell/browser/serial/serial_chooser_context_factory.cc b/shell/browser/serial/serial_chooser_context_factory.cc index c3f18c856b..914bdd4999 100644 --- a/shell/browser/serial/serial_chooser_context_factory.cc +++ b/shell/browser/serial/serial_chooser_context_factory.cc @@ -19,9 +19,7 @@ SerialChooserContextFactory::~SerialChooserContextFactory() = default; KeyedService* SerialChooserContextFactory::BuildServiceInstanceFor( content::BrowserContext* context) const { - auto* browser_context = - static_cast(context); - return new SerialChooserContext(browser_context); + return new SerialChooserContext(); } // static diff --git a/shell/browser/serial/serial_chooser_controller.cc b/shell/browser/serial/serial_chooser_controller.cc index 5f35ac5a34..94c7355188 100644 --- a/shell/browser/serial/serial_chooser_controller.cc +++ b/shell/browser/serial/serial_chooser_controller.cc @@ -67,7 +67,8 @@ SerialChooserController::SerialChooserController( : WebContentsObserver(web_contents), filters_(std::move(filters)), callback_(std::move(callback)), - serial_delegate_(serial_delegate) { + serial_delegate_(serial_delegate), + render_frame_host_id_(render_frame_host->GetGlobalId()) { origin_ = web_contents->GetMainFrame()->GetLastCommittedOrigin(); chooser_context_ = SerialChooserContextFactory::GetForBrowserContext( @@ -124,7 +125,8 @@ void SerialChooserController::OnDeviceChosen(const std::string& port_id) { return ptr->token.ToString() == port_id; }); if (it != ports_.end()) { - chooser_context_->GrantPortPermission(origin_, *it->get()); + auto* rfh = content::RenderFrameHost::FromID(render_frame_host_id_); + chooser_context_->GrantPortPermission(origin_, *it->get(), rfh); RunCallback(it->Clone()); } else { RunCallback(/*port=*/nullptr); diff --git a/shell/browser/serial/serial_chooser_controller.h b/shell/browser/serial/serial_chooser_controller.h index 6930193766..7ac8f6f711 100644 --- a/shell/browser/serial/serial_chooser_controller.h +++ b/shell/browser/serial/serial_chooser_controller.h @@ -10,6 +10,7 @@ #include "base/macros.h" #include "base/memory/weak_ptr.h" +#include "content/public/browser/global_routing_id.h" #include "content/public/browser/serial_chooser.h" #include "content/public/browser/web_contents.h" #include "content/public/browser/web_contents_observer.h" @@ -61,6 +62,8 @@ class SerialChooserController final : public SerialChooserContext::PortObserver, base::WeakPtr serial_delegate_; + content::GlobalRenderFrameHostId render_frame_host_id_; + base::WeakPtrFactory weak_factory_{this}; DISALLOW_COPY_AND_ASSIGN(SerialChooserController); diff --git a/shell/browser/web_contents_permission_helper.cc b/shell/browser/web_contents_permission_helper.cc index fb1c81852d..cf4bc93870 100644 --- a/shell/browser/web_contents_permission_helper.cc +++ b/shell/browser/web_contents_permission_helper.cc @@ -190,6 +190,24 @@ bool WebContentsPermissionHelper::CheckSerialAccessPermission( static_cast(PermissionType::SERIAL), &details); } +bool WebContentsPermissionHelper::CheckSerialPortPermission( + const url::Origin& origin, + base::Value device, + content::RenderFrameHost* render_frame_host) const { + return CheckDevicePermission( + static_cast(PermissionType::SERIAL), origin, + &device, render_frame_host); +} + +void WebContentsPermissionHelper::GrantSerialPortPermission( + const url::Origin& origin, + base::Value device, + content::RenderFrameHost* render_frame_host) const { + return GrantDevicePermission( + static_cast(PermissionType::SERIAL), origin, + &device, render_frame_host); +} + bool WebContentsPermissionHelper::CheckHIDAccessPermission( const url::Origin& embedding_origin) const { base::DictionaryValue details; diff --git a/shell/browser/web_contents_permission_helper.h b/shell/browser/web_contents_permission_helper.h index 9f7d7aa75e..1aca12c7db 100644 --- a/shell/browser/web_contents_permission_helper.h +++ b/shell/browser/web_contents_permission_helper.h @@ -42,6 +42,14 @@ class WebContentsPermissionHelper bool CheckMediaAccessPermission(const GURL& security_origin, blink::mojom::MediaStreamType type) const; bool CheckSerialAccessPermission(const url::Origin& embedding_origin) const; + bool CheckSerialPortPermission( + const url::Origin& origin, + base::Value device, + content::RenderFrameHost* render_frame_host) const; + void GrantSerialPortPermission( + const url::Origin& origin, + base::Value device, + content::RenderFrameHost* render_frame_host) const; bool CheckHIDAccessPermission(const url::Origin& embedding_origin) const; bool CheckHIDDevicePermission( const url::Origin& origin,