| // Copyright 2014 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/api/hid/hid_device_manager.h" |
| |
| #include <stdint.h> |
| |
| #include <functional> |
| #include <limits> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/bind.h" |
| #include "base/containers/contains.h" |
| #include "base/lazy_instance.h" |
| #include "base/location.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/no_destructor.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/device_service.h" |
| #include "extensions/browser/api/device_permissions_manager.h" |
| #include "extensions/common/mojom/event_dispatcher.mojom-forward.h" |
| #include "extensions/common/permissions/permissions_data.h" |
| #include "extensions/common/permissions/usb_device_permission.h" |
| #include "mojo/public/cpp/bindings/callback_helpers.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "services/device/public/cpp/hid/hid_device_filter.h" |
| #include "services/device/public/cpp/hid/hid_usage_and_page.h" |
| #include "services/device/public/mojom/hid.mojom.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| |
| namespace hid = extensions::api::hid; |
| |
| using device::HidDeviceFilter; |
| |
| namespace extensions { |
| |
| namespace { |
| |
| void PopulateHidDeviceInfo(hid::HidDeviceInfo* output, |
| const device::mojom::HidDeviceInfo& input) { |
| output->vendor_id = input.vendor_id; |
| output->product_id = input.product_id; |
| output->product_name = input.product_name; |
| output->serial_number = input.serial_number; |
| output->max_input_report_size = input.max_input_report_size; |
| output->max_output_report_size = input.max_output_report_size; |
| output->max_feature_report_size = input.max_feature_report_size; |
| |
| for (const auto& collection : input.collections) { |
| // Don't expose sensitive data. |
| if (device::IsAlwaysProtected(*collection->usage)) { |
| continue; |
| } |
| |
| hid::HidCollectionInfo api_collection; |
| api_collection.usage_page = collection->usage->usage_page; |
| api_collection.usage = collection->usage->usage; |
| |
| api_collection.report_ids.insert(api_collection.report_ids.begin(), |
| collection->report_ids.begin(), |
| collection->report_ids.end()); |
| |
| output->collections.push_back(std::move(api_collection)); |
| } |
| |
| const std::vector<uint8_t>& report_descriptor = input.report_descriptor; |
| if (report_descriptor.size() > 0) { |
| output->report_descriptor.assign(report_descriptor.begin(), |
| report_descriptor.end()); |
| } |
| } |
| |
| bool WillDispatchDeviceEvent( |
| base::WeakPtr<HidDeviceManager> device_manager, |
| const device::mojom::HidDeviceInfo& device_info, |
| content::BrowserContext* browser_context, |
| Feature::Context target_context, |
| const Extension* extension, |
| const base::Value::Dict* listener_filter, |
| absl::optional<base::Value::List>& event_args_out, |
| mojom::EventFilteringInfoPtr& event_filtering_info_out) { |
| if (device_manager && extension) { |
| return device_manager->HasPermission(extension, device_info, false); |
| } |
| return false; |
| } |
| |
| HidDeviceManager::HidManagerBinder& GetHidManagerBinderOverride() { |
| static base::NoDestructor<HidDeviceManager::HidManagerBinder> binder; |
| return *binder; |
| } |
| |
| } // namespace |
| |
| struct HidDeviceManager::GetApiDevicesParams { |
| public: |
| GetApiDevicesParams(const Extension* extension, |
| const std::vector<HidDeviceFilter>& filters, |
| GetApiDevicesCallback callback) |
| : extension(extension), filters(filters), callback(std::move(callback)) {} |
| ~GetApiDevicesParams() {} |
| |
| raw_ptr<const Extension> extension; |
| std::vector<HidDeviceFilter> filters; |
| GetApiDevicesCallback callback; |
| }; |
| |
| HidDeviceManager::HidDeviceManager(content::BrowserContext* context) |
| : browser_context_(context) { |
| event_router_ = EventRouter::Get(context); |
| if (event_router_) { |
| event_router_->RegisterObserver(this, hid::OnDeviceAdded::kEventName); |
| event_router_->RegisterObserver(this, hid::OnDeviceRemoved::kEventName); |
| } |
| } |
| |
| HidDeviceManager::~HidDeviceManager() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| } |
| |
| // static |
| BrowserContextKeyedAPIFactory<HidDeviceManager>* |
| HidDeviceManager::GetFactoryInstance() { |
| static base::LazyInstance<BrowserContextKeyedAPIFactory<HidDeviceManager>>:: |
| DestructorAtExit factory = LAZY_INSTANCE_INITIALIZER; |
| return &factory.Get(); |
| } |
| |
| void HidDeviceManager::GetApiDevices( |
| const Extension* extension, |
| const std::vector<HidDeviceFilter>& filters, |
| GetApiDevicesCallback callback) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| LazyInitialize(); |
| |
| if (enumeration_ready_) { |
| base::Value::List devices = CreateApiDeviceList(extension, filters); |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), std::move(devices))); |
| } else { |
| pending_enumerations_.push_back(std::make_unique<GetApiDevicesParams>( |
| extension, filters, std::move(callback))); |
| } |
| } |
| |
| const device::mojom::HidDeviceInfo* HidDeviceManager::GetDeviceInfo( |
| int resource_id) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| ResourceIdToDeviceInfoMap::const_iterator device_iter = |
| devices_.find(resource_id); |
| if (device_iter == devices_.end()) { |
| return nullptr; |
| } |
| |
| return device_iter->second.get(); |
| } |
| |
| void HidDeviceManager::Connect(const std::string& device_guid, |
| ConnectCallback callback) { |
| DCHECK(initialized_); |
| |
| hid_manager_->Connect(device_guid, /*connection_client=*/mojo::NullRemote(), |
| /*watcher=*/mojo::NullRemote(), |
| /*allow_protected_reports=*/true, |
| /*allow_fido_reports=*/true, |
| mojo::WrapCallbackWithDefaultInvokeIfNotRun( |
| std::move(callback), mojo::NullRemote())); |
| } |
| |
| bool HidDeviceManager::HasPermission( |
| const Extension* extension, |
| const device::mojom::HidDeviceInfo& device_info, |
| bool update_last_used) { |
| DevicePermissionsManager* permissions_manager = |
| DevicePermissionsManager::Get(browser_context_); |
| CHECK(permissions_manager); |
| DevicePermissions* device_permissions = |
| permissions_manager->GetForExtension(extension->id()); |
| DCHECK(device_permissions); |
| scoped_refptr<DevicePermissionEntry> permission_entry = |
| device_permissions->FindHidDeviceEntry(device_info); |
| if (permission_entry) { |
| if (update_last_used) { |
| permissions_manager->UpdateLastUsed(extension->id(), permission_entry); |
| } |
| return true; |
| } |
| |
| std::unique_ptr<UsbDevicePermission::CheckParam> usb_param = |
| UsbDevicePermission::CheckParam::ForHidDevice( |
| extension, device_info.vendor_id, device_info.product_id); |
| if (extension->permissions_data()->CheckAPIPermissionWithParam( |
| mojom::APIPermissionID::kUsbDevice, usb_param.get())) { |
| return true; |
| } |
| |
| if (extension->permissions_data()->HasAPIPermission( |
| mojom::APIPermissionID::kU2fDevices)) { |
| HidDeviceFilter u2f_filter; |
| u2f_filter.SetUsagePage(0xF1D0); |
| if (u2f_filter.Matches(device_info)) { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| void HidDeviceManager::Shutdown() { |
| if (event_router_) { |
| event_router_->UnregisterObserver(this); |
| } |
| } |
| |
| void HidDeviceManager::OnListenerAdded(const EventListenerInfo& details) { |
| LazyInitialize(); |
| } |
| void HidDeviceManager::DeviceAdded(device::mojom::HidDeviceInfoPtr device) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| DCHECK_LT(next_resource_id_, std::numeric_limits<int>::max()); |
| int new_id = next_resource_id_++; |
| DCHECK(!base::Contains(resource_ids_, device->guid)); |
| resource_ids_[device->guid] = new_id; |
| devices_[new_id] = std::move(device); |
| |
| // Don't generate events during the initial enumeration. |
| if (enumeration_ready_ && event_router_) { |
| api::hid::HidDeviceInfo api_device_info; |
| api_device_info.device_id = new_id; |
| |
| PopulateHidDeviceInfo(&api_device_info, *devices_[new_id]); |
| |
| if (api_device_info.collections.size() > 0) { |
| auto args(hid::OnDeviceAdded::Create(api_device_info)); |
| DispatchEvent(events::HID_ON_DEVICE_ADDED, hid::OnDeviceAdded::kEventName, |
| std::move(args), *devices_[new_id]); |
| } |
| } |
| } |
| |
| void HidDeviceManager::DeviceRemoved(device::mojom::HidDeviceInfoPtr device) { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| const auto& resource_entry = resource_ids_.find(device->guid); |
| DCHECK(resource_entry != resource_ids_.end()); |
| int resource_id = resource_entry->second; |
| const auto& device_entry = devices_.find(resource_id); |
| DCHECK(device_entry != devices_.end()); |
| resource_ids_.erase(resource_entry); |
| devices_.erase(device_entry); |
| |
| if (event_router_) { |
| DCHECK(enumeration_ready_); |
| auto args(hid::OnDeviceRemoved::Create(resource_id)); |
| DispatchEvent(events::HID_ON_DEVICE_REMOVED, |
| hid::OnDeviceRemoved::kEventName, std::move(args), *device); |
| } |
| |
| // Remove permission entry for ephemeral hid device. |
| DevicePermissionsManager* permissions_manager = |
| DevicePermissionsManager::Get(browser_context_); |
| DCHECK(permissions_manager); |
| permissions_manager->RemoveEntryByDeviceGUID(DevicePermissionEntry::Type::HID, |
| device->guid); |
| } |
| |
| void HidDeviceManager::DeviceChanged(device::mojom::HidDeviceInfoPtr device) { |
| // Find |device| in |devices_|. |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| const auto& resource_entry = resource_ids_.find(device->guid); |
| DCHECK(resource_entry != resource_ids_.end()); |
| int resource_id = resource_entry->second; |
| DCHECK(base::Contains(devices_, resource_id)); |
| |
| // Update the device information. |
| devices_[resource_id] = std::move(device); |
| } |
| |
| void HidDeviceManager::LazyInitialize() { |
| DCHECK(thread_checker_.CalledOnValidThread()); |
| |
| if (initialized_) { |
| return; |
| } |
| // |hid_manager_| may already be initialized in tests. |
| if (!hid_manager_) { |
| // |hid_manager_| is initialized and safe to use whether or not the |
| // connection is successful. |
| |
| DCHECK_CURRENTLY_ON(content::BrowserThread::UI); |
| auto receiver = hid_manager_.BindNewPipeAndPassReceiver(); |
| const auto& binder = GetHidManagerBinderOverride(); |
| if (binder) |
| binder.Run(std::move(receiver)); |
| else |
| content::GetDeviceService().BindHidManager(std::move(receiver)); |
| } |
| // Enumerate HID devices and set client. |
| std::vector<device::mojom::HidDeviceInfoPtr> empty_devices; |
| hid_manager_->GetDevicesAndSetClient( |
| receiver_.BindNewEndpointAndPassRemote(), |
| mojo::WrapCallbackWithDefaultInvokeIfNotRun( |
| base::BindOnce(&HidDeviceManager::OnEnumerationComplete, |
| weak_factory_.GetWeakPtr()), |
| std::move(empty_devices))); |
| |
| initialized_ = true; |
| } |
| |
| // static |
| void HidDeviceManager::OverrideHidManagerBinderForTesting( |
| HidManagerBinder binder) { |
| GetHidManagerBinderOverride() = std::move(binder); |
| } |
| |
| base::Value::List HidDeviceManager::CreateApiDeviceList( |
| const Extension* extension, |
| const std::vector<HidDeviceFilter>& filters) { |
| base::Value::List api_devices; |
| for (const ResourceIdToDeviceInfoMap::value_type& map_entry : devices_) { |
| int resource_id = map_entry.first; |
| auto& device_info = map_entry.second; |
| |
| if (!filters.empty() && |
| !HidDeviceFilter::MatchesAny(*device_info, filters)) { |
| continue; |
| } |
| |
| if (!HasPermission(extension, *device_info, false)) { |
| continue; |
| } |
| |
| hid::HidDeviceInfo api_device_info; |
| api_device_info.device_id = resource_id; |
| PopulateHidDeviceInfo(&api_device_info, *device_info); |
| |
| // Expose devices with which user can communicate. |
| if (api_device_info.collections.size() > 0) { |
| api_devices.Append(api_device_info.ToValue()); |
| } |
| } |
| |
| return api_devices; |
| } |
| |
| void HidDeviceManager::OnEnumerationComplete( |
| std::vector<device::mojom::HidDeviceInfoPtr> devices) { |
| DCHECK(resource_ids_.empty()); |
| DCHECK(devices_.empty()); |
| |
| for (auto& device_info : devices) { |
| DeviceAdded(std::move(device_info)); |
| } |
| enumeration_ready_ = true; |
| |
| for (const auto& params : pending_enumerations_) { |
| base::Value::List devices_list = |
| CreateApiDeviceList(params->extension, params->filters); |
| std::move(params->callback).Run(std::move(devices_list)); |
| } |
| pending_enumerations_.clear(); |
| } |
| |
| void HidDeviceManager::DispatchEvent( |
| events::HistogramValue histogram_value, |
| const std::string& event_name, |
| base::Value::List event_args, |
| const device::mojom::HidDeviceInfo& device_info) { |
| std::unique_ptr<Event> event( |
| new Event(histogram_value, event_name, std::move(event_args))); |
| // The |event->will_dispatch_callback| will be called synchronously, it is |
| // safe to pass |device_info| by reference. |
| event->will_dispatch_callback = |
| base::BindRepeating(&WillDispatchDeviceEvent, weak_factory_.GetWeakPtr(), |
| std::cref(device_info)); |
| event_router_->BroadcastEvent(std::move(event)); |
| } |
| |
| } // namespace extensions |