blob: 29d04b30e62a6bcae8eaa4c1e41bf09e4655ad4c [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/usb/chrome_usb_delegate.h"
#include <optional>
#include <string_view>
#include "base/barrier_closure.h"
#include "base/strings/stringprintf.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/test_future.h"
#include "build/build_config.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/browser/usb/usb_connection_tracker.h"
#include "chrome/browser/usb/usb_connection_tracker_factory.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/embedded_worker_instance_test_harness.h"
#include "content/public/test/test_renderer_host.h"
#include "services/device/public/cpp/test/fake_usb_device_info.h"
#include "services/device/public/cpp/test/fake_usb_device_manager.h"
#include "services/device/public/cpp/test/scoped_usb_device_manager_overrider.h"
#include "services/device/public/mojom/usb_device.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
#include "url/gurl.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/command_line.h"
#include "base/values.h"
#include "chrome/browser/extensions/test_extension_system.h"
#include "extensions/browser/extension_registrar.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace {
using ::base::test::RunClosure;
using ::base::test::RunOnceCallback;
using ::base::test::RunOnceClosure;
using ::base::test::TestFuture;
using ::testing::_;
using ::testing::NiceMock;
constexpr std::string_view kDefaultTestUrl{"https://www.google.com/"};
constexpr std::string_view kCrossOriginTestUrl{"https://www.chromium.org"};
#if BUILDFLAG(ENABLE_EXTENSIONS)
constexpr std::string_view kExtensionId{"ckcendljdlmgnhghiaomidhiiclmapok"};
constexpr char kAllowlistedImprivataExtensionId[] =
"dhodapiemamlmhlhblgcibabhdkohlen";
constexpr char kAllowlistedSmartCardExtensionId[] =
"khpfeaanjngmcnplbdlpegiifgpfgdco";
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
ACTION_P2(ExpectGuidAndThen, expected_guid, callback) {
ASSERT_TRUE(arg0);
EXPECT_EQ(expected_guid, arg0->guid);
if (!callback.is_null())
callback.Run();
}
// Calls GetDevices on `service` and blocks until the GetDevicesCallback is
// invoked. Fails the test if the guids of received UsbDeviceInfos do not
// exactly match `expected_guids`.
void GetDevicesBlocking(blink::mojom::WebUsbService* service,
const std::set<std::string>& expected_guids) {
TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
service->GetDevices(get_devices_future.GetCallback());
EXPECT_EQ(expected_guids.size(), get_devices_future.Get().size());
std::set<std::string> actual_guids;
for (const auto& device : get_devices_future.Get())
actual_guids.insert(device->guid);
EXPECT_EQ(expected_guids, actual_guids);
}
device::mojom::UsbOpenDeviceResultPtr NewUsbOpenDeviceSuccess() {
return device::mojom::UsbOpenDeviceResult::NewSuccess(
device::mojom::UsbOpenDeviceSuccess::OK);
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Creates a FakeUsbDeviceInfo with USB class code.
scoped_refptr<device::FakeUsbDeviceInfo> CreateFakeUsbDeviceInfo() {
auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New();
alternate_setting->alternate_setting = 0;
alternate_setting->class_code = device::mojom::kUsbHidClass;
auto interface = device::mojom::UsbInterfaceInfo::New();
interface->interface_number = 0;
interface->alternates.push_back(std::move(alternate_setting));
auto config = device::mojom::UsbConfigurationInfo::New();
config->configuration_value = 1;
config->interfaces.push_back(std::move(interface));
std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
configs.push_back(std::move(config));
return base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF", std::move(configs));
}
// Creates a FakeUsbDeviceInfo with Smart Card class code.
scoped_refptr<device::FakeUsbDeviceInfo> CreateFakeSmartCardDeviceInfo() {
auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New();
alternate_setting->alternate_setting = 0;
alternate_setting->class_code = device::mojom::kUsbSmartCardClass;
auto interface = device::mojom::UsbInterfaceInfo::New();
interface->interface_number = 0;
interface->alternates.push_back(std::move(alternate_setting));
auto config = device::mojom::UsbConfigurationInfo::New();
config->configuration_value = 1;
config->interfaces.push_back(std::move(interface));
std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
configs.push_back(std::move(config));
return base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x4321, 0x8765, "ACME", "Frobinator", "ABCDEF", std::move(configs));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
// A mock UsbDeviceManagerClient implementation that can be used to listen for
// USB device connection events.
class MockDeviceManagerClient
: public UsbChooserContext::UsbDeviceManagerClient {
public:
MockDeviceManagerClient() = default;
~MockDeviceManagerClient() override = default;
mojo::PendingAssociatedRemote<UsbDeviceManagerClient>
CreateInterfacePtrAndBind() {
auto client = receiver_.BindNewEndpointAndPassRemote();
receiver_.set_disconnect_handler(base::BindOnce(
&MockDeviceManagerClient::OnConnectionError, base::Unretained(this)));
return client;
}
MOCK_METHOD1(OnDeviceAdded, void(device::mojom::UsbDeviceInfoPtr));
MOCK_METHOD1(OnDeviceRemoved, void(device::mojom::UsbDeviceInfoPtr));
MOCK_METHOD0(ConnectionError, void());
void OnConnectionError() {
receiver_.reset();
ConnectionError();
}
private:
mojo::AssociatedReceiver<UsbDeviceManagerClient> receiver_{this};
};
class MockUsbConnectionTracker : public UsbConnectionTracker {
public:
explicit MockUsbConnectionTracker(Profile* profile)
: UsbConnectionTracker(profile) {}
~MockUsbConnectionTracker() override = default;
MOCK_METHOD(void, IncrementConnectionCount, (const url::Origin&), (override));
MOCK_METHOD(void, DecrementConnectionCount, (const url::Origin&), (override));
};
// Tests for embedder-specific behaviors of Chrome's blink::mojom::WebUsbService
// implementation.
class ChromeUsbTestHelper {
public:
#if BUILDFLAG(ENABLE_EXTENSIONS)
// Creates a fake extension with the specified `extension_id` so that it can
// exercise behaviors that are only enabled for privileged extensions.
std::optional<GURL> CreateExtensionWithId(std::string_view extension_id) {
auto manifest = base::Value::Dict()
.Set("name", "Fake extension")
.Set("description", "For testing.")
.Set("version", "0.1")
.Set("manifest_version", 2)
.Set("web_accessible_resources",
base::Value::List().Append("index.html"));
scoped_refptr<const extensions::Extension> extension =
extensions::ExtensionBuilder()
.SetManifest(std::move(manifest))
.SetID(std::string(extension_id))
.Build();
if (!extension) {
return std::nullopt;
}
extensions::TestExtensionSystem* extension_system =
static_cast<extensions::TestExtensionSystem*>(
extensions::ExtensionSystem::Get(profile_));
extension_system->CreateExtensionService(
base::CommandLine::ForCurrentProcess(), base::FilePath(), false);
extensions::ExtensionRegistrar::Get(profile_)->AddExtension(extension);
return extension->GetResourceURL("index.html");
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
void SimulateDeviceServiceCrash() { device_manager()->CloseAllBindings(); }
virtual void ConnectToService(
mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) = 0;
virtual void SetUpOriginUrl() = 0;
void SetUpWebPageOriginUrl() { origin_url_ = GURL(kDefaultTestUrl); }
void SetUpExtensionOriginUrl(std::string_view extension_id) {
auto extension_url = CreateExtensionWithId(extension_id);
ASSERT_TRUE(extension_url);
origin_url_ = *extension_url;
}
void SetUpFakeDeviceManager() {
if (!device_manager()->IsBound()) {
mojo::PendingRemote<device::mojom::UsbDeviceManager>
pending_device_manager;
device_manager()->AddReceiver(
pending_device_manager.InitWithNewPipeAndPassReceiver());
GetChooserContext()->SetDeviceManagerForTesting(
std::move(pending_device_manager));
}
}
UsbChooserContext* GetChooserContext() {
return UsbChooserContextFactory::GetForProfile(profile_);
}
device::FakeUsbDeviceManager* device_manager() {
return usb_device_manager_overrider_.device_manager();
}
BrowserContextKeyedServiceFactory::TestingFactory
GetUsbConnectionTrackerTestingFactory() {
return base::BindRepeating([](content::BrowserContext* browser_context) {
return static_cast<std::unique_ptr<KeyedService>>(
std::make_unique<testing::NiceMock<MockUsbConnectionTracker>>(
Profile::FromBrowserContext(browser_context)));
});
}
void SetUpUsbConnectionTracker() {
// Even MockUsbConnectionTracker can be lazily created in ChromeUsbDelegate,
// we intentionally create it ahead of time so that we can test EXPECT_CALL
// for invoking mock method for the first time.
usb_connection_tracker_ = static_cast<MockUsbConnectionTracker*>(
UsbConnectionTrackerFactory::GetForProfile(profile_, /*create=*/true));
}
MockUsbConnectionTracker& usb_connection_tracker() {
return *usb_connection_tracker_;
}
void TestNoPermissionDevice() {
auto origin = url::Origin::Create(origin_url_);
auto device1 = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto device2 = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5679, "ACME", "Frobinator+", "GHIJKL");
auto no_permission_device1 =
base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0xffff, 0x567b, "ACME", "Frobinator II", "MNOPQR");
auto no_permission_device2 =
base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0xffff, 0x567c, "ACME", "Frobinator Xtreme", "STUVWX");
// Connect two devices and grant permission for one.
auto device_info_1 = device_manager()->AddDevice(device1);
GetChooserContext()->GrantDevicePermission(origin, *device_info_1);
device_manager()->AddDevice(no_permission_device1);
// Create the WebUsbService and register a `mock_client` to receive
// notifications on device connections and disconnections. GetDevices is
// called to ensure the service is started and the client is set.
mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
NiceMock<MockDeviceManagerClient> mock_client;
web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());
GetDevicesBlocking(web_usb_service.get(), {device1->guid()});
// Connect two more devices and grant permission for one.
auto device_info_2 = device_manager()->AddDevice(device2);
GetChooserContext()->GrantDevicePermission(origin, *device_info_2);
device_manager()->AddDevice(no_permission_device2);
// Disconnect all four devices. The `mock_client` should be notified only
// for the devices it has permission to access.
device_manager()->RemoveDevice(device1);
device_manager()->RemoveDevice(device2);
device_manager()->RemoveDevice(no_permission_device1);
device_manager()->RemoveDevice(no_permission_device2);
{
base::RunLoop loop;
base::RepeatingClosure barrier =
base::BarrierClosure(2, loop.QuitClosure());
testing::InSequence s;
EXPECT_CALL(mock_client, OnDeviceRemoved)
.WillOnce(ExpectGuidAndThen(device1->guid(), barrier))
.WillOnce(ExpectGuidAndThen(device2->guid(), barrier));
loop.Run();
}
// Reconnect all four devices. The `mock_client` should be notified only for
// the devices it has permission to access.
device_manager()->AddDevice(device1);
device_manager()->AddDevice(device2);
device_manager()->AddDevice(no_permission_device1);
device_manager()->AddDevice(no_permission_device2);
{
base::RunLoop loop;
base::RepeatingClosure barrier =
base::BarrierClosure(2, loop.QuitClosure());
testing::InSequence s;
EXPECT_CALL(mock_client, OnDeviceAdded)
.WillOnce(ExpectGuidAndThen(device1->guid(), barrier))
.WillOnce(ExpectGuidAndThen(device2->guid(), barrier));
loop.Run();
}
}
void TestReconnectDeviceManager() {
auto origin = url::Origin::Create(origin_url_);
auto* context = GetChooserContext();
auto device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto ephemeral_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0, 0, "ACME", "Frobinator II", "");
// Connect two devices and grant permission for both. The first device is
// eligible for persistent permissions and the second device is only
// eligible for ephemeral permissions.
auto device_info = device_manager()->AddDevice(device);
auto ephemeral_device_info = device_manager()->AddDevice(ephemeral_device);
mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
MockDeviceManagerClient mock_client;
web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());
// GetDevices is called to ensure the service is started, the client is set,
// and the callback from adding device to the device manager is settled.
GetDevicesBlocking(web_usb_service.get(), {});
context->GrantDevicePermission(origin, *ephemeral_device_info);
context->GrantDevicePermission(origin, *device_info);
GetDevicesBlocking(web_usb_service.get(),
{device->guid(), ephemeral_device->guid()});
// Simulate a device service crash. The ephemeral permission should be
// revoked.
SimulateDeviceServiceCrash();
base::RunLoop loop;
EXPECT_CALL(mock_client, ConnectionError()).WillOnce([&]() {
loop.Quit();
});
loop.Run();
EXPECT_TRUE(context->HasDevicePermission(origin, *device_info));
EXPECT_FALSE(context->HasDevicePermission(origin, *ephemeral_device_info));
// Although a new device added, as the Device manager has been destroyed, no
// event will be triggered.
auto another_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5679, "ACME", "Frobinator+", "GHIJKL");
auto another_device_info = device_manager()->AddDevice(another_device);
EXPECT_CALL(mock_client, OnDeviceAdded).Times(0);
base::RunLoop().RunUntilIdle();
// Grant permission to the new device when service is off.
context->GrantDevicePermission(origin, *another_device_info);
device_manager()->RemoveDevice(device);
EXPECT_CALL(mock_client, OnDeviceRemoved).Times(0);
base::RunLoop().RunUntilIdle();
// Reconnect the service.
web_usb_service.reset();
base::RunLoop().RunUntilIdle();
ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
web_usb_service->SetClient(mock_client.CreateInterfacePtrAndBind());
GetDevicesBlocking(web_usb_service.get(), {another_device->guid()});
EXPECT_TRUE(context->HasDevicePermission(origin, *device_info));
EXPECT_TRUE(context->HasDevicePermission(origin, *another_device_info));
EXPECT_FALSE(context->HasDevicePermission(origin, *ephemeral_device_info));
}
void TestRevokeDevicePermission() {
auto origin = url::Origin::Create(origin_url_);
auto* context = GetChooserContext();
// Connect a fake device.
auto device_info = device_manager()->CreateAndAddDevice(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
// Create the WebUsbService and call GetDevices to ensure it is started.
mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
GetDevicesBlocking(web_usb_service.get(), {});
// Grant permission to access the connected device.
context->GrantDevicePermission(origin, *device_info);
auto objects = context->GetGrantedObjects(origin);
ASSERT_EQ(1u, objects.size());
// Connect the UsbDevice.
mojo::Remote<device::mojom::UsbDevice> device;
web_usb_service->GetDevice(device_info->guid,
device.BindNewPipeAndPassReceiver());
ASSERT_TRUE(device);
// Revoke the permission. The UsbDevice should be disconnected.
base::RunLoop disconnect_loop;
device.set_disconnect_handler(disconnect_loop.QuitClosure());
context->RevokeObjectPermission(origin, objects[0]->value);
disconnect_loop.Run();
}
void TestOpenAndCloseDevice(content::WebContents* web_contents) {
auto origin = url::Origin::Create(origin_url_);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
// Connect a device and grant permission to access it.
auto device_info = device_manager()->CreateAndAddDevice(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
GetChooserContext()->GrantDevicePermission(origin, *device_info);
device::MockUsbMojoDevice mock_device;
device_manager()->SetMockForDevice(device_info->guid, &mock_device);
// Call GetDevices and expect the device to be returned.
MockDeviceManagerClient mock_client;
service->SetClient(mock_client.CreateInterfacePtrAndBind());
GetDevicesBlocking(service.get(), {device_info->guid});
// Call GetDevice to get the device. The WebContents should not indicate we
// are connected to a device since the device is not opened.
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Open the device. Now the WebContents should indicate we are connected to
// a USB device.
EXPECT_CALL(mock_device, Open)
.WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
if (supports_usb_connection_tracker_) {
EXPECT_CALL(usb_connection_tracker(), IncrementConnectionCount(origin));
}
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
if (web_contents) {
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Close the device and check that the WebContents no longer indicates we
// are connected.
EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
base::RunLoop loop;
if (supports_usb_connection_tracker_) {
EXPECT_CALL(usb_connection_tracker(), DecrementConnectionCount(origin));
}
device->Close(loop.QuitClosure());
loop.Run();
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
}
void TestOpenAndDisconnectDevice(content::WebContents* web_contents) {
auto origin = url::Origin::Create(origin_url_);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
// Connect a device and grant permission to access it.
auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto device_info = device_manager()->AddDevice(fake_device);
GetChooserContext()->GrantDevicePermission(origin, *device_info);
device::MockUsbMojoDevice mock_device;
device_manager()->SetMockForDevice(device_info->guid, &mock_device);
// Call GetDevices and expect the device to be returned.
MockDeviceManagerClient mock_client;
service->SetClient(mock_client.CreateInterfacePtrAndBind());
GetDevicesBlocking(service.get(), {device_info->guid});
// Call GetDevice to get the device. The WebContents should not indicate we
// are connected to a device since the device is not opened.
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Open the device. Now the WebContents should indicate we are connected to
// a USB device.
EXPECT_CALL(mock_device, Open)
.WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
if (supports_usb_connection_tracker_) {
EXPECT_CALL(usb_connection_tracker(), IncrementConnectionCount(origin));
}
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
if (web_contents) {
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Remove the device and check that the WebContents no longer indicates we
// are connected.
base::RunLoop decrement_connection_count_loop;
if (supports_usb_connection_tracker_) {
EXPECT_CALL(usb_connection_tracker(), DecrementConnectionCount(origin))
.WillOnce(RunClosure(decrement_connection_count_loop.QuitClosure()));
}
EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
device_manager()->RemoveDevice(fake_device);
if (supports_usb_connection_tracker_) {
decrement_connection_count_loop.Run();
} else {
base::RunLoop().RunUntilIdle();
}
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
}
void TestWebUsbServiceNotConnected() {
base::RunLoop run_loop;
mojo::Remote<blink::mojom::WebUsbService> web_usb_service;
ConnectToService(web_usb_service.BindNewPipeAndPassReceiver());
web_usb_service.set_disconnect_handler(run_loop.QuitClosure());
run_loop.Run();
EXPECT_FALSE(web_usb_service.is_connected());
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
void TestAllowlistedImprivataExtension(content::WebContents* web_contents) {
auto imprivata_origin = url::Origin::Create(origin_url_);
auto* context = GetChooserContext();
auto device_info = device_manager()->AddDevice(CreateFakeUsbDeviceInfo());
context->GrantDevicePermission(imprivata_origin, *device_info);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
MockDeviceManagerClient mock_client;
service->SetClient(mock_client.CreateInterfacePtrAndBind());
GetDevicesBlocking(service.get(), {device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
if (web_contents) {
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
TestFuture<bool> set_configuration_future;
device->SetConfiguration(1, set_configuration_future.GetCallback());
EXPECT_TRUE(set_configuration_future.Get());
TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
device->ClaimInterface(0, claim_interface_future.GetCallback());
#if BUILDFLAG(IS_CHROMEOS)
// The allowlist only allows the interface to be claimed on Chrome OS.
EXPECT_EQ(claim_interface_future.Get(),
device::mojom::UsbClaimInterfaceResult::kSuccess);
#else
EXPECT_EQ(claim_interface_future.Get(),
device::mojom::UsbClaimInterfaceResult::kProtectedClass);
#endif // BUILDFLAG(IS_CHROMEOS)
}
void TestAllowlistedSmartCardConnectorExtension(
content::WebContents* web_contents) {
auto extension_origin = url::Origin::Create(origin_url_);
// Add a smart card device. Also add an unrelated device, in order to test
// that access is not automatically granted to it.
auto ccid_device_info =
device_manager()->AddDevice(CreateFakeSmartCardDeviceInfo());
auto usb_device_info =
device_manager()->AddDevice(CreateFakeUsbDeviceInfo());
// No need to grant permission. It is granted automatically for smart
// card device.
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
MockDeviceManagerClient mock_client;
service->SetClient(mock_client.CreateInterfacePtrAndBind());
// Check that the extensions is automatically granted access to the CCID
// device and can claim its interfaces.
{
GetDevicesBlocking(service.get(), {ccid_device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(ccid_device_info->guid,
device.BindNewPipeAndPassReceiver());
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
if (web_contents) {
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
TestFuture<bool> set_configuration_future;
device->SetConfiguration(1, set_configuration_future.GetCallback());
EXPECT_TRUE(set_configuration_future.Get());
TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
device->ClaimInterface(0, claim_interface_future.GetCallback());
EXPECT_EQ(claim_interface_future.Get(),
device::mojom::UsbClaimInterfaceResult::kSuccess);
}
// Check that the extension, if granted permission to a USB device can't
// claim its interfaces.
{
GetChooserContext()->GrantDevicePermission(extension_origin,
*usb_device_info);
GetDevicesBlocking(service.get(),
{ccid_device_info->guid, usb_device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(usb_device_info->guid,
device.BindNewPipeAndPassReceiver());
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
TestFuture<bool> set_configuration_future;
device->SetConfiguration(1, set_configuration_future.GetCallback());
EXPECT_TRUE(set_configuration_future.Get());
TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface_future;
device->ClaimInterface(0, claim_interface_future.GetCallback());
EXPECT_EQ(claim_interface_future.Get(),
device::mojom::UsbClaimInterfaceResult::kProtectedClass);
}
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
protected:
raw_ptr<TestingProfile, DanglingUntriaged> profile_ = nullptr;
GURL origin_url_;
raw_ptr<MockUsbConnectionTracker, DanglingUntriaged> usb_connection_tracker_ =
nullptr;
// This flag is expected to be set to true only for the scenario of extension
// origin.
bool supports_usb_connection_tracker_ = false;
private:
device::ScopedUsbDeviceManagerOverrider usb_device_manager_overrider_;
};
class ChromeUsbDelegateRenderFrameTestBase
: public ChromeRenderViewHostTestHarness,
public ChromeUsbTestHelper {
public:
void SetUp() override {
ChromeRenderViewHostTestHarness::SetUp();
profile_ = profile();
ASSERT_TRUE(profile_);
UsbConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile_, GetUsbConnectionTrackerTestingFactory());
SetUpUsbConnectionTracker();
SetUpOriginUrl();
NavigateAndCommit(origin_url_);
}
// ChromeUsbTestHelper:
void ConnectToService(
mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) override {
SetUpFakeDeviceManager();
content::RenderFrameHostTester::For(main_rfh())
->CreateWebUsbServiceForTesting(std::move(receiver));
}
void TestOpenAndNavigateCrossOrigin(content::WebContents* web_contents) {
auto origin = url::Origin::Create(origin_url_);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
// Connect a device and grant permission to access it.
auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto device_info = device_manager()->AddDevice(fake_device);
GetChooserContext()->GrantDevicePermission(origin, *device_info);
device::MockUsbMojoDevice mock_device;
device_manager()->SetMockForDevice(device_info->guid, &mock_device);
// Call GetDevices and expect the device info to be returned.
GetDevicesBlocking(service.get(), {device_info->guid});
// Call GetDevice to get the device. The WebContents should not indicate we
// are connected to a device since the device is not opened.
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Open the device. Now the WebContents should indicate we are connected to
// a USB device.
EXPECT_CALL(mock_device, Open)
.WillOnce(RunOnceCallback<0>(NewUsbOpenDeviceSuccess()));
TestFuture<device::mojom::UsbOpenDeviceResultPtr> open_future;
device->Open(open_future.GetCallback());
EXPECT_TRUE(open_future.Get()->is_success());
if (web_contents) {
EXPECT_TRUE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
// Perform a cross-origin navigation. The WebContents should indicate we are
// no longer connected.
EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
NavigateAndCommit(GURL(kCrossOriginTestUrl));
base::RunLoop().RunUntilIdle();
if (web_contents) {
EXPECT_FALSE(web_contents->IsCapabilityActive(
content::WebContentsCapabilityType::kUSB));
}
}
};
class ChromeUsbDelegateServiceWorkerTestBase
: public content::EmbeddedWorkerInstanceTestHarness,
public ChromeUsbTestHelper {
public:
void SetUp() override {
content::EmbeddedWorkerInstanceTestHarness::SetUp();
SetUpOriginUrl();
SetUpUsbConnectionTracker();
StartWorker();
}
void TearDown() override {
StopWorker();
content::EmbeddedWorkerInstanceTestHarness::TearDown();
}
// ChromeUsbTestHelper:
void ConnectToService(
mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) override {
SetUpFakeDeviceManager();
BindUsbServiceToWorker(origin_url_, std::move(receiver));
}
// content::EmbeddedWorkerInstanceTestHarness
std::unique_ptr<content::BrowserContext> CreateBrowserContext() override {
auto builder = TestingProfile::Builder();
auto testing_profile = builder.Build();
profile_ = testing_profile.get();
UsbConnectionTrackerFactory::GetInstance()->SetTestingFactory(
profile_, GetUsbConnectionTrackerTestingFactory());
return testing_profile;
}
void StartWorker() {
auto worker_url =
GURL(base::StringPrintf("%s/worker.js", origin_url_.spec().c_str()));
CreateAndStartWorker(origin_url_, worker_url);
// Wait until tasks triggered by ServiceWorkerUsbDelegateObserver settle.
base::RunLoop().RunUntilIdle();
}
void StopWorker() { StopAndResetWorker(); }
};
class ChromeUsbDelegateRenderFrameTest
: public ChromeUsbDelegateRenderFrameTestBase {
public:
void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};
class ChromeUsbDelegateServiceWorkerTest
: public ChromeUsbDelegateServiceWorkerTestBase {
public:
void SetUpOriginUrl() override { SetUpWebPageOriginUrl(); }
};
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ChromeUsbDelegateExtensionRenderFrameTest
: public ChromeUsbDelegateRenderFrameTestBase {
public:
ChromeUsbDelegateExtensionRenderFrameTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override { SetUpExtensionOriginUrl(kExtensionId); }
};
class ChromeUsbDelegateImprivataExtensionRenderFrameTest
: public ChromeUsbDelegateRenderFrameTestBase {
public:
ChromeUsbDelegateImprivataExtensionRenderFrameTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override {
SetUpExtensionOriginUrl(kAllowlistedImprivataExtensionId);
}
};
class ChromeUsbDelegateSmartCardExtensionRenderFrameTest
: public ChromeUsbDelegateRenderFrameTestBase {
public:
ChromeUsbDelegateSmartCardExtensionRenderFrameTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override {
SetUpExtensionOriginUrl(kAllowlistedSmartCardExtensionId);
}
};
class ChromeUsbDelegateExtensionServiceWorkerTest
: public ChromeUsbDelegateServiceWorkerTestBase {
public:
ChromeUsbDelegateExtensionServiceWorkerTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override { SetUpExtensionOriginUrl(kExtensionId); }
};
class ChromeUsbDelegateImprivataExtensionServiceWorkerTest
: public ChromeUsbDelegateServiceWorkerTestBase {
public:
ChromeUsbDelegateImprivataExtensionServiceWorkerTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override {
SetUpExtensionOriginUrl(kAllowlistedImprivataExtensionId);
}
};
class ChromeUsbDelegateSmartCardExtensionServiceWorkerTest
: public ChromeUsbDelegateServiceWorkerTestBase {
public:
ChromeUsbDelegateSmartCardExtensionServiceWorkerTest() {
supports_usb_connection_tracker_ = true;
}
void SetUpOriginUrl() override {
SetUpExtensionOriginUrl(kAllowlistedSmartCardExtensionId);
}
};
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
} // namespace
TEST_F(ChromeUsbDelegateRenderFrameTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeUsbDelegateRenderFrameTest, ReconnectDeviceManager) {
TestReconnectDeviceManager();
}
TEST_F(ChromeUsbDelegateRenderFrameTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndCloseDevice) {
TestOpenAndCloseDevice(web_contents());
}
TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndDisconnectDevice) {
TestOpenAndDisconnectDevice(web_contents());
}
TEST_F(ChromeUsbDelegateRenderFrameTest, OpenAndNavigateCrossOrigin) {
TestOpenAndNavigateCrossOrigin(web_contents());
}
TEST_F(ChromeUsbDelegateServiceWorkerTest, WebUsbServiceNotConnected) {
TestWebUsbServiceNotConnected();
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, ReconnectDeviceManager) {
TestReconnectDeviceManager();
}
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, OpenAndCloseDevice) {
TestOpenAndCloseDevice(web_contents());
}
TEST_F(ChromeUsbDelegateExtensionRenderFrameTest, OpenAndDisconnectDevice) {
TestOpenAndDisconnectDevice(web_contents());
}
TEST_F(ChromeUsbDelegateImprivataExtensionRenderFrameTest,
AllowlistedImprivataExtension) {
TestAllowlistedImprivataExtension(web_contents());
}
TEST_F(ChromeUsbDelegateSmartCardExtensionRenderFrameTest,
AllowlistedSmartCardConnectorExtension) {
TestAllowlistedSmartCardConnectorExtension(web_contents());
}
TEST_F(ChromeUsbDelegateImprivataExtensionServiceWorkerTest,
AllowlistedImprivataExtension) {
TestAllowlistedImprivataExtension(nullptr);
}
TEST_F(ChromeUsbDelegateSmartCardExtensionServiceWorkerTest,
AllowlistedSmartCardConnectorExtension) {
TestAllowlistedSmartCardConnectorExtension(nullptr);
}
TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, NoPermissionDevice) {
TestNoPermissionDevice();
}
TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, ReconnectDeviceManager) {
TestReconnectDeviceManager();
}
TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, RevokeDevicePermission) {
TestRevokeDevicePermission();
}
TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, OpenAndCloseDevice) {
TestOpenAndCloseDevice(/*web_contents=*/nullptr);
}
TEST_F(ChromeUsbDelegateExtensionServiceWorkerTest, OpenAndDisconnectDevice) {
TestOpenAndDisconnectDevice(/*web_contents=*/nullptr);
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
TEST(ChromeUsbDelegateBrowserContextTest, BrowserContextIsNull) {
base::test::SingleThreadTaskEnvironment task_environment;
ChromeUsbDelegate chrome_usb_delegate;
url::Origin origin = url::Origin::Create(GURL(kDefaultTestUrl));
EXPECT_FALSE(chrome_usb_delegate.CanRequestDevicePermission(
/*browser_context=*/nullptr, origin));
EXPECT_FALSE(chrome_usb_delegate.HasDevicePermission(
/*browser_context=*/nullptr, /*frame=*/nullptr, origin,
device::mojom::UsbDeviceInfo()));
EXPECT_EQ(nullptr, chrome_usb_delegate.GetDeviceInfo(
/*browser_context=*/nullptr,
base::Uuid::GenerateRandomV4().AsLowercaseString()));
TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> get_devices_future;
chrome_usb_delegate.GetDevices(/*browser_context=*/nullptr,
get_devices_future.GetCallback());
EXPECT_TRUE(get_devices_future.Get().empty());
// TODO(crbug.com/40217296): Test GetDevice with null browser_context.
}