blob: 3e563d1ae00598ceb9095dad0d12a83749dc5060 [file] [log] [blame]
// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "content/browser/hid/hid_service.h"
#include <map>
#include <memory>
#include <utility>
#include "base/containers/contains.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "content/browser/service_worker/service_worker_context_core.h"
#include "content/browser/service_worker/service_worker_hid_delegate_observer.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/document_service.h"
#include "content/public/browser/hid_chooser.h"
#include "content/public/browser/hid_delegate.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/common/content_client.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/self_owned_receiver.h"
#include "services/device/public/cpp/device_features.h"
#include "services/network/public/mojom/permissions_policy/permissions_policy_feature.mojom.h"
namespace content {
// Deletes the HidService when the connected document is destroyed.
class DocumentHelper
: public content::DocumentService<blink::mojom::HidService> {
public:
DocumentHelper(std::unique_ptr<HidService> parent,
RenderFrameHost& render_frame_host,
mojo::PendingReceiver<blink::mojom::HidService> receiver)
: DocumentService(render_frame_host, std::move(receiver)),
parent_(std::move(parent)) {
DCHECK(parent_);
}
~DocumentHelper() override = default;
// blink::mojom::HidService:
void RegisterClient(
mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> client)
override {
parent_->RegisterClient(std::move(client));
}
void GetDevices(GetDevicesCallback callback) override {
parent_->GetDevices(std::move(callback));
}
void RequestDevice(
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters,
RequestDeviceCallback callback) override {
parent_->RequestDevice(std::move(filters), std::move(exclusion_filters),
std::move(callback));
}
void Connect(const std::string& device_guid,
mojo::PendingRemote<device::mojom::HidConnectionClient> client,
ConnectCallback callback) override {
parent_->Connect(device_guid, std::move(client), std::move(callback));
}
void Forget(device::mojom::HidDeviceInfoPtr device_info,
ForgetCallback callback) override {
parent_->Forget(std::move(device_info), std::move(callback));
}
private:
const std::unique_ptr<HidService> parent_;
};
HidService::HidService(
RenderFrameHostImpl* render_frame_host,
base::WeakPtr<ServiceWorkerVersion> service_worker_version,
const url::Origin& origin)
: render_frame_host_(render_frame_host),
service_worker_version_(std::move(service_worker_version)),
origin_(origin) {
if (render_frame_host &&
base::FeatureList::IsEnabled(
features::kWebHidAttributeAllowsBackForwardCache)) {
// Prevent `render_frame_host` from entering the back forward cache once the
// HidService is created.
// TODO(crbug.com/40232335): Remove after WebHID API has been updated to
// handle system and device state changes that occur while the frame is
// in the back forward cache.
back_forward_cache_feature_handle_ =
render_frame_host->RegisterBackForwardCacheDisablingNonStickyFeature(
blink::scheduler::WebSchedulerTrackedFeature::kWebHID);
}
watchers_.set_disconnect_handler(base::BindRepeating(
&HidService::OnWatcherRemoved, base::Unretained(this),
/* cleanup_watcher_ids=*/true, /*watchers_removed=*/1));
HidDelegate* delegate = GetContentClient()->browser()->GetHidDelegate();
if (delegate && render_frame_host_) {
delegate->AddObserver(GetBrowserContext(), this);
} else if (service_worker_version_) {
// For service worker case, it relies on ServiceWorkerHidDelegateObserver to
// be the broker between HidDelegate and HidService.
auto context = service_worker_version_->context();
if (context) {
context->hid_delegate_observer()->RegisterHidService(
service_worker_version_->registration_id(),
weak_factory_.GetWeakPtr());
}
}
}
HidService::HidService(RenderFrameHostImpl* render_frame_host)
: HidService(render_frame_host,
/*service_worker_version=*/nullptr,
render_frame_host->GetMainFrame()->GetLastCommittedOrigin()) {}
HidService::HidService(
base::WeakPtr<ServiceWorkerVersion> service_worker_version,
const url::Origin& origin)
: HidService(/*render_frame_host=*/nullptr,
std::move(service_worker_version),
origin) {}
HidService::~HidService() {
HidDelegate* delegate = GetContentClient()->browser()->GetHidDelegate();
if (delegate && render_frame_host_) {
delegate->RemoveObserver(GetBrowserContext(), this);
}
// Update connection count and active frame count tracking as remaining
// watchers will be closed from this end.
if (!watchers_.empty())
DecrementActivityCount();
for (size_t i = 0; i < watchers_.size(); i++) {
delegate->DecrementConnectionCount(GetBrowserContext(), origin_);
}
}
// static
void HidService::Create(
RenderFrameHostImpl* render_frame_host,
mojo::PendingReceiver<blink::mojom::HidService> receiver) {
CHECK(render_frame_host);
if (!render_frame_host->IsFeatureEnabled(
network::mojom::PermissionsPolicyFeature::kHid)) {
mojo::ReportBadMessage("Permissions policy blocks access to HID.");
return;
}
// Avoid creating the HidService if there is no HID delegate to provide the
// implementation.
if (!GetContentClient()->browser()->GetHidDelegate())
return;
if (render_frame_host->IsNestedWithinFencedFrame()) {
// The renderer is supposed to disallow the use of hid services when inside
// a fenced frame. Anything getting past the renderer checks must be marked
// as a bad request.
mojo::ReportBadMessage("WebHID is not allowed in a fenced frame tree.");
return;
}
if (render_frame_host->GetOutermostMainFrame()
->GetLastCommittedOrigin()
.opaque()) {
mojo::ReportBadMessage("WebHID is not allowed from an opaque origin.");
return;
}
// DocumentHelper observes the lifetime of the document connected to
// `render_frame_host` and destroys the HidService when the Mojo connection is
// disconnected, RenderFrameHost is deleted, or the RenderFrameHost commits a
// cross-document navigation. It forwards its Mojo interface to HidService.
new DocumentHelper(std::make_unique<HidService>(render_frame_host),
*render_frame_host, std::move(receiver));
}
// static
void HidService::Create(
base::WeakPtr<ServiceWorkerVersion> service_worker_version,
const url::Origin& origin,
mojo::PendingReceiver<blink::mojom::HidService> receiver) {
DCHECK(service_worker_version);
if (origin.opaque()) {
// Service worker should not be available to a window/worker client which
// origin is opaque according to Service Worker specification.
mojo::ReportBadMessage("WebHID is blocked in an opaque origin.");
return;
}
// Avoid creating the HidService if there is no HID delegate to provide
// the implementation.
if (!GetContentClient()->browser()->GetHidDelegate())
return;
// This makes HidService a self-owned receiver so it will self-destruct when a
// mojo interface error occurs.
mojo::MakeSelfOwnedReceiver(
std::make_unique<HidService>(std::move(service_worker_version), origin),
std::move(receiver));
}
// static
void HidService::RemoveProtectedReports(device::mojom::HidDeviceInfo& device,
bool is_known_security_key,
bool is_fido_allowed) {
// If the origin is allowed to access FIDO and `device` is a known FIDO U2F
// security key, do not remove any reports.
if (base::FeatureList::IsEnabled(
features::kSecurityKeyHidInterfacesAreFido) &&
is_known_security_key && is_fido_allowed) {
return;
}
std::vector<device::mojom::HidCollectionInfoPtr> collections;
for (auto& collection : device.collections) {
const bool is_fido =
collection->usage->usage_page == device::mojom::kPageFido;
std::vector<device::mojom::HidReportDescriptionPtr> input_reports;
for (auto& report : collection->input_reports) {
if ((is_fido && is_fido_allowed) ||
!device.protected_input_report_ids.has_value() ||
!base::Contains(*device.protected_input_report_ids,
report->report_id)) {
input_reports.push_back(std::move(report));
}
}
std::vector<device::mojom::HidReportDescriptionPtr> output_reports;
for (auto& report : collection->output_reports) {
if ((is_fido && is_fido_allowed) ||
!device.protected_output_report_ids.has_value() ||
!base::Contains(*device.protected_output_report_ids,
report->report_id)) {
output_reports.push_back(std::move(report));
}
}
std::vector<device::mojom::HidReportDescriptionPtr> feature_reports;
for (auto& report : collection->feature_reports) {
if ((is_fido && is_fido_allowed) ||
!device.protected_feature_report_ids.has_value() ||
!base::Contains(*device.protected_feature_report_ids,
report->report_id)) {
feature_reports.push_back(std::move(report));
}
}
// Only keep the collection if it has at least one report.
if (!input_reports.empty() || !output_reports.empty() ||
!feature_reports.empty()) {
collection->input_reports = std::move(input_reports);
collection->output_reports = std::move(output_reports);
collection->feature_reports = std::move(feature_reports);
collections.push_back(std::move(collection));
}
}
device.collections = std::move(collections);
}
void HidService::RegisterClient(
mojo::PendingAssociatedRemote<device::mojom::HidManagerClient> client) {
clients_.Add(std::move(client));
if (service_worker_version_ && service_worker_version_->context()) {
// HidService is expected to have only one HidManagerClient when it is for a
// service worker. One renderer side of a service worker has its own
// associated HidService.
CHECK_EQ(1u, clients_.size());
// When a service worker is woken up by a device connection event, the
// client might not have yet registered with the HidService or the
// HidService hasn't been created yet when service worker is in running
// state. This is because service worker is set to running state after
// script evaluation but inter-processes request triggered from the script
// evaluation that creates HidService or registers a client might not be
// done in the browser process. To handle this situation, pending callbacks
// are stored and to be processed when registering the client.
service_worker_version_->context()
->hid_delegate_observer()
->ProcessPendingCallbacks(service_worker_version_.get());
}
}
void HidService::GetDevices(GetDevicesCallback callback) {
auto* browser_context = GetBrowserContext();
if (!browser_context) {
std::move(callback).Run({});
return;
}
GetContentClient()
->browser()
->GetHidDelegate()
->GetHidManager(browser_context)
->GetDevices(base::BindOnce(&HidService::FinishGetDevices,
weak_factory_.GetWeakPtr(),
std::move(callback)));
}
void HidService::RequestDevice(
std::vector<blink::mojom::HidDeviceFilterPtr> filters,
std::vector<blink::mojom::HidDeviceFilterPtr> exclusion_filters,
RequestDeviceCallback callback) {
HidDelegate* delegate = GetContentClient()->browser()->GetHidDelegate();
if (!render_frame_host_ ||
!delegate->CanRequestDevicePermission(GetBrowserContext(), origin_)) {
std::move(callback).Run(std::vector<device::mojom::HidDeviceInfoPtr>());
return;
}
chooser_ = GetContentClient()->browser()->GetHidDelegate()->RunChooser(
render_frame_host_, std::move(filters), std::move(exclusion_filters),
base::BindOnce(&HidService::FinishRequestDevice,
weak_factory_.GetWeakPtr(), std::move(callback)));
}
void HidService::Connect(
const std::string& device_guid,
mojo::PendingRemote<device::mojom::HidConnectionClient> client,
ConnectCallback callback) {
auto* browser_context = GetBrowserContext();
if (!browser_context) {
std::move(callback).Run(mojo::NullRemote());
return;
}
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
if (!delegate) {
std::move(callback).Run(mojo::NullRemote());
return;
}
auto* device_info = delegate->GetDeviceInfo(browser_context, device_guid);
if (!device_info ||
!delegate->HasDevicePermission(browser_context, render_frame_host_,
origin_, *device_info)) {
std::move(callback).Run(mojo::NullRemote());
return;
}
if (watchers_.empty()) {
IncrementActivityCount();
}
delegate->IncrementConnectionCount(browser_context, origin_);
mojo::PendingRemote<device::mojom::HidConnectionWatcher> watcher;
mojo::ReceiverId receiver_id =
watchers_.Add(this, watcher.InitWithNewPipeAndPassReceiver());
watcher_ids_.insert({device_guid, receiver_id});
delegate->GetHidManager(browser_context)
->Connect(
device_guid, std::move(client), std::move(watcher),
/*allow_protected_reports=*/false,
delegate->IsFidoAllowedForOrigin(browser_context, origin_),
base::BindOnce(&HidService::FinishConnect, weak_factory_.GetWeakPtr(),
std::move(callback)));
}
void HidService::Forget(device::mojom::HidDeviceInfoPtr device_info,
ForgetCallback callback) {
auto* browser_context = GetBrowserContext();
if (browser_context) {
GetContentClient()->browser()->GetHidDelegate()->RevokeDevicePermission(
browser_context, render_frame_host_, origin_, *device_info);
}
std::move(callback).Run();
}
void HidService::OnWatcherRemoved(bool cleanup_watcher_ids,
size_t watchers_removed) {
if (watchers_.empty())
DecrementActivityCount();
// When |cleanup_watcher_ids| is true, it is the case like watcher disconnect
// handler where the entry in |watchers_| is removed but |watcher_ids_| isn't
// yet, so the entry in |watcher_ids_| needs to be removed.
if (cleanup_watcher_ids) {
// Clean up any associated |watchers_ids_| entries.
std::erase_if(watcher_ids_, [&](const auto& watcher_entry) {
return watcher_entry.second == watchers_.current_receiver();
});
}
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
for (size_t i = 0; i < watchers_removed; i++) {
delegate->DecrementConnectionCount(GetBrowserContext(), origin_);
}
}
void HidService::IncrementActivityCount() {
if (render_frame_host_) {
auto* web_contents_impl =
WebContentsImpl::FromRenderFrameHostImpl(render_frame_host_);
web_contents_impl->IncrementHidActiveFrameCount();
} else if (service_worker_version_) {
CHECK(!service_worker_activity_request_uuid_);
service_worker_activity_request_uuid_ = base::Uuid::GenerateRandomV4();
service_worker_version_->StartExternalRequest(
*service_worker_activity_request_uuid_,
ServiceWorkerExternalRequestTimeoutType::kDoesNotTimeout);
}
}
void HidService::DecrementActivityCount() {
if (render_frame_host_) {
auto* web_contents_impl =
WebContentsImpl::FromRenderFrameHostImpl(render_frame_host_);
web_contents_impl->DecrementHidActiveFrameCount();
} else if (service_worker_version_) {
CHECK(service_worker_activity_request_uuid_);
service_worker_version_->FinishExternalRequest(
*service_worker_activity_request_uuid_);
service_worker_activity_request_uuid_.reset();
}
}
void HidService::OnDeviceAdded(
const device::mojom::HidDeviceInfo& device_info) {
auto* browser_context = GetBrowserContext();
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
if (!delegate->HasDevicePermission(browser_context, render_frame_host_,
origin_, device_info)) {
return;
}
auto filtered_device_info = device_info.Clone();
RemoveProtectedReports(
*filtered_device_info,
delegate->IsKnownSecurityKey(browser_context, device_info),
delegate->IsFidoAllowedForOrigin(browser_context, origin_));
if (filtered_device_info->collections.empty())
return;
for (auto& client : clients_)
client->DeviceAdded(filtered_device_info->Clone());
}
void HidService::OnDeviceRemoved(
const device::mojom::HidDeviceInfo& device_info) {
size_t watchers_removed =
std::erase_if(watcher_ids_, [&](const auto& watcher_entry) {
if (watcher_entry.first != device_info.guid)
return false;
watchers_.Remove(watcher_entry.second);
return true;
});
// If needed, decrement the active frame count.
if (watchers_removed > 0)
OnWatcherRemoved(/*cleanup_watcher_ids=*/false, watchers_removed);
auto* browser_context = GetBrowserContext();
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
if (!delegate->HasDevicePermission(browser_context, render_frame_host_,
origin_, device_info)) {
return;
}
auto filtered_device_info = device_info.Clone();
RemoveProtectedReports(
*filtered_device_info,
delegate->IsKnownSecurityKey(browser_context, device_info),
delegate->IsFidoAllowedForOrigin(browser_context, origin_));
if (filtered_device_info->collections.empty())
return;
for (auto& client : clients_)
client->DeviceRemoved(filtered_device_info->Clone());
}
void HidService::OnDeviceChanged(
const device::mojom::HidDeviceInfo& device_info) {
auto* browser_context = GetBrowserContext();
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
const bool has_device_permission = delegate->HasDevicePermission(
browser_context, render_frame_host_, origin_, device_info);
device::mojom::HidDeviceInfoPtr filtered_device_info;
if (has_device_permission) {
filtered_device_info = device_info.Clone();
RemoveProtectedReports(
*filtered_device_info,
delegate->IsKnownSecurityKey(browser_context, device_info),
delegate->IsFidoAllowedForOrigin(browser_context, origin_));
}
if (!has_device_permission || filtered_device_info->collections.empty()) {
// Changing the device information has caused permissions to be revoked.
size_t watchers_removed =
std::erase_if(watcher_ids_, [&](const auto& watcher_entry) {
if (watcher_entry.first != device_info.guid)
return false;
watchers_.Remove(watcher_entry.second);
return true;
});
// If needed, decrement the active frame count.
if (watchers_removed > 0)
OnWatcherRemoved(/*cleanup_watcher_ids=*/false, watchers_removed);
return;
}
for (auto& client : clients_)
client->DeviceChanged(filtered_device_info->Clone());
}
void HidService::OnHidManagerConnectionError() {
// Close the connection with Blink.
clients_.Clear();
}
void HidService::OnPermissionRevoked(const url::Origin& origin) {
if (origin_ != origin) {
return;
}
auto* browser_context = GetBrowserContext();
HidDelegate* delegate = GetContentClient()->browser()->GetHidDelegate();
size_t watchers_removed =
std::erase_if(watcher_ids_, [&](const auto& watcher_entry) {
const auto* device_info =
delegate->GetDeviceInfo(browser_context, watcher_entry.first);
if (!device_info)
return true;
if (delegate->HasDevicePermission(browser_context, render_frame_host_,
origin_, *device_info)) {
return false;
}
watchers_.Remove(watcher_entry.second);
return true;
});
// If needed decrement the active frame count.
if (watchers_removed > 0)
OnWatcherRemoved(/*cleanup_watcher_ids=*/false, watchers_removed);
}
void HidService::FinishGetDevices(
GetDevicesCallback callback,
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
auto* browser_context = GetBrowserContext();
auto* delegate = GetContentClient()->browser()->GetHidDelegate();
bool is_fido_allowed =
delegate->IsFidoAllowedForOrigin(browser_context, origin_);
std::vector<device::mojom::HidDeviceInfoPtr> result;
for (auto& device : devices) {
RemoveProtectedReports(
*device, delegate->IsKnownSecurityKey(browser_context, *device),
is_fido_allowed);
if (device->collections.empty())
continue;
if (delegate->HasDevicePermission(browser_context, render_frame_host_,
origin_, *device)) {
result.push_back(std::move(device));
}
}
std::move(callback).Run(std::move(result));
}
void HidService::FinishRequestDevice(
RequestDeviceCallback callback,
std::vector<device::mojom::HidDeviceInfoPtr> devices) {
std::move(callback).Run(std::move(devices));
}
void HidService::FinishConnect(
ConnectCallback callback,
mojo::PendingRemote<device::mojom::HidConnection> connection) {
if (!connection) {
std::move(callback).Run(mojo::NullRemote());
return;
}
std::move(callback).Run(std::move(connection));
}
BrowserContext* HidService::GetBrowserContext() {
if (render_frame_host_) {
return render_frame_host_->GetBrowserContext();
}
if (service_worker_version_ && service_worker_version_->context()) {
return service_worker_version_->context()->wrapper()->browser_context();
}
return nullptr;
}
} // namespace content