blob: 76fe604c5255fe97450df6a615a28a59914a8822 [file] [log] [blame]
// Copyright 2018 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/usb/web_usb_service_impl.h"
#include <memory>
#include <set>
#include <string>
#include <utility>
#include <vector>
#include "base/barrier_closure.h"
#include "base/bind.h"
#include "base/run_loop.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/test_future.h"
#include "content/browser/usb/usb_test_utils.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/back_forward_cache_util.h"
#include "content/public/test/test_renderer_host.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/associated_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/remote.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/mojom/usb_device.mojom.h"
#include "services/device/public/mojom/usb_enumeration_options.mojom.h"
#include "services/device/public/mojom/usb_manager_client.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "url/gurl.h"
namespace content {
namespace {
using ::base::test::RunOnceCallback;
using ::base::test::RunOnceClosure;
using ::base::test::TestFuture;
using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
constexpr base::StringPiece kDefaultTestUrl{"https://www.google.com/"};
constexpr base::StringPiece kCrossOriginTestUrl{"https://www.chromium.org"};
class MockWebContentsObserver : public WebContentsObserver {
public:
explicit MockWebContentsObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
MockWebContentsObserver(const MockWebContentsObserver&) = delete;
MockWebContentsObserver& operator=(const MockWebContentsObserver&) = delete;
~MockWebContentsObserver() override = default;
MOCK_METHOD1(OnIsConnectedToUsbDeviceChanged, void(bool));
};
// Test fixture for WebUsbServiceImpl unit tests.
class WebUsbServiceImplTest : public RenderViewHostImplTestHarness {
public:
WebUsbServiceImplTest() = default;
WebUsbServiceImplTest(const WebUsbServiceImplTest&) = delete;
WebUsbServiceImplTest& operator=(const WebUsbServiceImplTest&) = delete;
void SetUp() override {
original_client_ = SetBrowserClientForTesting(&test_client_);
RenderViewHostTestHarness::SetUp();
NavigateAndCommit(GURL(kDefaultTestUrl));
}
void TearDown() override {
RenderViewHostTestHarness::TearDown();
SetBrowserClientForTesting(original_client_);
}
protected:
void SimulateDeviceServiceCrash() { device_manager()->CloseAllBindings(); }
void ConnectToService(
mojo::PendingReceiver<blink::mojom::WebUsbService> receiver) {
contents()->GetPrimaryMainFrame()->CreateWebUsbService(std::move(receiver));
// Set the fake device manager.
if (!device_manager()->IsBound()) {
mojo::PendingRemote<device::mojom::UsbDeviceManager>
pending_device_manager;
device_manager()->AddReceiver(
pending_device_manager.InitWithNewPipeAndPassReceiver());
}
// For tests, all devices are permitted by default.
ON_CALL(delegate(), HasDevicePermission).WillByDefault(Return(true));
// Forward calls to the fake device manager.
ON_CALL(delegate(), GetDevices)
.WillByDefault(
[&](auto& frame,
device::mojom::UsbDeviceManager::GetDevicesCallback callback) {
device_manager()->GetDevices(nullptr, std::move(callback));
});
ON_CALL(delegate(), GetDevice)
.WillByDefault(
[&](auto& frame, const std::string& guid,
base::span<const uint8_t> blocked_interface_classes,
mojo::PendingReceiver<device::mojom::UsbDevice> device_receiver,
mojo::PendingRemote<device::mojom::UsbDeviceClient>
device_client) {
device_manager()->GetDevice(
guid,
std::vector<uint8_t>(blocked_interface_classes.begin(),
blocked_interface_classes.end()),
std::move(device_receiver), std::move(device_client));
});
ON_CALL(delegate(), GetDeviceInfo)
.WillByDefault([&](auto& frame, const std::string& guid) {
return device_manager()->GetDeviceInfo(guid);
});
}
device::FakeUsbDeviceManager* device_manager() {
if (!device_manager_) {
device_manager_ = std::make_unique<device::FakeUsbDeviceManager>();
}
return device_manager_.get();
}
MockUsbDelegate& delegate() { return test_client_.delegate(); }
private:
UsbTestContentBrowserClient test_client_;
raw_ptr<ContentBrowserClient> original_client_;
std::unique_ptr<device::FakeUsbDeviceManager> device_manager_;
};
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());
ASSERT_TRUE(get_devices_future.Wait());
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);
}
void OpenDeviceBlocking(device::mojom::UsbDevice* device) {
TestFuture<device::mojom::UsbOpenDeviceError> open_future;
device->Open(open_future.GetCallback());
EXPECT_EQ(open_future.Get(), device::mojom::UsbOpenDeviceError::OK);
}
} // namespace
TEST_F(WebUsbServiceImplTest, OpenAndCloseDevice) {
const auto origin = url::Origin::Create(GURL(kDefaultTestUrl));
MockWebContentsObserver web_contents_observer(contents());
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
auto device_info = device_manager()->CreateAndAddDevice(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
device::MockUsbMojoDevice mock_device;
device_manager()->SetMockForDevice(device_info->guid, &mock_device);
GetDevicesBlocking(service.get(), {device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(true));
EXPECT_CALL(mock_device, Open)
.WillOnce(RunOnceCallback<0>(device::mojom::UsbOpenDeviceError::OK));
OpenDeviceBlocking(device.get());
EXPECT_TRUE(contents()->IsConnectedToUsbDevice());
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(false));
EXPECT_CALL(mock_device, Close).WillOnce(RunOnceClosure<0>());
{
base::RunLoop run_loop;
device->Close(run_loop.QuitClosure());
run_loop.Run();
}
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
}
TEST_F(WebUsbServiceImplTest, OpenAndDisconnectDevice) {
const auto origin = url::Origin::Create(GURL(kDefaultTestUrl));
MockWebContentsObserver web_contents_observer(contents());
auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto device_info = device_manager()->AddDevice(fake_device);
device::MockUsbMojoDevice mock_device;
device_manager()->SetMockForDevice(device_info->guid, &mock_device);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
GetDevicesBlocking(service.get(), {device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(true));
EXPECT_CALL(mock_device, Open)
.WillOnce(RunOnceCallback<0>(device::mojom::UsbOpenDeviceError::OK));
OpenDeviceBlocking(device.get());
EXPECT_TRUE(contents()->IsConnectedToUsbDevice());
base::RunLoop loop;
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(false))
.WillOnce([&loop](bool is_connected_to_usb_device) { loop.Quit(); });
device_manager()->RemoveDevice(fake_device);
loop.Run();
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
}
TEST_F(WebUsbServiceImplTest, OpenAndNavigateCrossOrigin) {
const auto origin = url::Origin::Create(GURL(kDefaultTestUrl));
MockWebContentsObserver web_contents_observer(contents());
auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF");
auto device_info = device_manager()->AddDevice(fake_device);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
GetDevicesBlocking(service.get(), {device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(true));
OpenDeviceBlocking(device.get());
EXPECT_TRUE(contents()->IsConnectedToUsbDevice());
base::RunLoop loop;
EXPECT_CALL(web_contents_observer, OnIsConnectedToUsbDeviceChanged(false))
.WillOnce([&loop](bool is_connected_to_usb_device) { loop.Quit(); });
NavigateAndCommit(GURL(kCrossOriginTestUrl));
loop.Run();
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
}
class WebUsbServiceImplProtectedInterfaceTest
: public WebUsbServiceImplTest,
public testing::WithParamInterface<uint8_t> {};
TEST_P(WebUsbServiceImplProtectedInterfaceTest, BlockProtectedInterface) {
const auto kOrigin = url::Origin::Create(GURL(kDefaultTestUrl));
auto blocked_interface_alt = device::mojom::UsbAlternateInterfaceInfo::New();
blocked_interface_alt->alternate_setting = 0;
blocked_interface_alt->class_code = GetParam();
auto blocked_interface = device::mojom::UsbInterfaceInfo::New();
blocked_interface->interface_number = 0;
blocked_interface->alternates.push_back(std::move(blocked_interface_alt));
auto unblocked_interface_alt =
device::mojom::UsbAlternateInterfaceInfo::New();
unblocked_interface_alt->alternate_setting = 0;
unblocked_interface_alt->class_code = 0xff; // Vendor specific interface.
auto unblocked_interface = device::mojom::UsbInterfaceInfo::New();
unblocked_interface->interface_number = 1;
unblocked_interface->alternates.push_back(std::move(unblocked_interface_alt));
auto config = device::mojom::UsbConfigurationInfo::New();
config->configuration_value = 1;
config->interfaces.push_back(std::move(blocked_interface));
config->interfaces.push_back(std::move(unblocked_interface));
std::vector<device::mojom::UsbConfigurationInfoPtr> configs;
configs.push_back(std::move(config));
auto fake_device = base::MakeRefCounted<device::FakeUsbDeviceInfo>(
0x1234, 0x5678, "ACME", "Frobinator", "ABCDEF", std::move(configs));
auto device_info = device_manager()->AddDevice(fake_device);
mojo::Remote<blink::mojom::WebUsbService> service;
ConnectToService(service.BindNewPipeAndPassReceiver());
GetDevicesBlocking(service.get(), {device_info->guid});
mojo::Remote<device::mojom::UsbDevice> device;
service->GetDevice(device_info->guid, device.BindNewPipeAndPassReceiver());
EXPECT_FALSE(contents()->IsConnectedToUsbDevice());
OpenDeviceBlocking(device.get());
TestFuture<bool> set_configuration_future;
device->SetConfiguration(1, set_configuration_future.GetCallback());
EXPECT_TRUE(set_configuration_future.Get());
TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface0_future;
device->ClaimInterface(0, claim_interface0_future.GetCallback());
EXPECT_EQ(claim_interface0_future.Get(),
device::mojom::UsbClaimInterfaceResult::kProtectedClass);
TestFuture<device::mojom::UsbClaimInterfaceResult> claim_interface1_future;
device->ClaimInterface(1, claim_interface1_future.GetCallback());
EXPECT_EQ(claim_interface1_future.Get(),
device::mojom::UsbClaimInterfaceResult::kSuccess);
}
INSTANTIATE_TEST_SUITE_P(WebUsbServiceImplProtectedInterfaceTests,
WebUsbServiceImplProtectedInterfaceTest,
testing::Values(device::mojom::kUsbAudioClass,
device::mojom::kUsbHidClass,
device::mojom::kUsbMassStorageClass,
device::mojom::kUsbSmartCardClass,
device::mojom::kUsbVideoClass,
device::mojom::kUsbAudioVideoClass,
device::mojom::kUsbWirelessClass));
} // namespace content