blob: a8ac246a016c2ec6da4c977158cda4803a24fe4d [file] [log] [blame]
// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <string>
#include <string_view>
#include <utility>
#include "base/functional/bind.h"
#include "base/memory/ref_counted.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/gmock_expected_support.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/chrome_content_browser_client.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/device_notifications/device_pinned_notification_renderer.h"
#include "chrome/browser/device_notifications/device_status_icon_renderer.h"
#include "chrome/browser/notifications/notification_display_service_tester.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_dialogs.h"
#include "chrome/browser/ui/chooser_bubble_testapi.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/usb/chrome_usb_delegate.h"
#include "chrome/browser/usb/usb_browser_test_utils.h"
#include "chrome/browser/usb/usb_chooser_context.h"
#include "chrome/browser/usb/usb_chooser_context_factory.h"
#include "chrome/browser/usb/usb_chooser_controller.h"
#include "chrome/browser/usb/usb_pinned_notification.h"
#include "chrome/browser/usb/usb_status_icon.h"
#include "chrome/browser/usb/web_usb_chooser.h"
#include "chrome/browser/usb/web_usb_histograms.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/mixin_based_in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/console_message.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/service_worker_context.h"
#include "content/public/browser/service_worker_context_observer.h"
#include "content/public/browser/service_worker_running_info.h"
#include "content/public/browser/storage_partition.h"
#include "content/public/browser/usb_chooser.h"
#include "content/public/common/content_client.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/service_worker_test_helpers.h"
#include "content/public/test/test_navigation_observer.h"
#include "extensions/buildflags/buildflags.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.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/service_manager/public/cpp/binder_registry.h"
#include "third_party/blink/public/mojom/usb/web_usb_service.mojom.h"
#if BUILDFLAG(ENABLE_EXTENSIONS)
#include "base/test/test_future.h"
#include "base/test/values_test_util.h"
#include "chrome/browser/extensions/extension_browsertest.h"
#include "chrome/common/chrome_features.h"
#include "components/content_settings/core/common/pref_names.h"
#include "extensions/common/extension.h"
#include "extensions/test/extension_test_message_listener.h"
#include "extensions/test/result_catcher.h"
#include "extensions/test/test_extension_dir.h"
#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/test/regular_logged_in_browser_test_mixin.h"
#include "components/account_id/account_id.h"
#include "components/account_id/account_id_literal.h" // nogncheck
#include "components/user_manager/user_manager.h"
#endif // BUILDFLAG(IS_CHROMEOS)
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
namespace {
using ::base::test::TestFuture;
using ::content::JsReplace;
using ::extensions::Extension;
using ::extensions::ExtensionId;
using ::extensions::TestExtensionDir;
using ::testing::Return;
const char kTestExtensionId[] = "iegclhlplifhodhkoafiokenjoapiobj";
// Key for extension id `kTestExtensionId`.
constexpr const char kTestExtensionKey[] =
"MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAjzv7dI7Ygyh67VHE1DdidudpYf8P"
"Ffv8iucWvzO+3xpF/Dm5xNo7aQhPNiEaNfHwJQ7lsp4gc+C+4bbaVewBFspTruoSJhZc5uEf"
"qxwovJwN+v1/SUFXTXQmQBv6gs0qZB4gBbl4caNQBlqrFwAMNisnu1V6UROna8rOJQ90D7Nv"
"7TCwoVPKBfVshpFjdDOTeBg4iLctO3S/06QYqaTDrwVceSyHkVkvzBY6tc6mnYX0RZu78J9i"
"L8bdqwfllOhs69cqoHHgrLdI6JdOyiuh6pBP6vxMlzSKWJ3YTNjaQTPwfOYaLMuzdl0v+Ydz"
"afIzV9zwe4Xiskk+5JNGt8b2rQIDAQAB";
constexpr uint8_t kUsbPrinterClass = 7;
constexpr char kNonAppHost[] = "nonapp.com";
constexpr char kNonAppHost2[] = "nonapp2.com";
constexpr char OpenAndClaimDeviceScript[] = R"((async () => {
try {
const devices = await navigator.usb.getDevices();
const device = devices[0];
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
return "Success";
} catch (e) {
return e.message;
}
})();)";
auto FailedWithSubstr(std::string_view substr) {
return content::EvalJsResult::ErrorIs(testing::HasSubstr(substr));
}
#if BUILDFLAG(ENABLE_EXTENSIONS) && BUILDFLAG(IS_CHROMEOS)
constexpr auto kManagedUserAccountId =
AccountId::Literal::FromUserEmailGaiaId("example@example.com",
GaiaId::Literal("12345"));
#endif // BUILDFLAG(ENABLE_EXTENSIONS) && BUILDFLAG(IS_CHROMEOS)
// Observer for an extension service worker events like start, activated, and
// stop.
class TestServiceWorkerContextObserver
: public content::ServiceWorkerContextObserver {
public:
TestServiceWorkerContextObserver(content::ServiceWorkerContext* context,
const ExtensionId& extension_id)
: extension_url_(Extension::GetBaseURLFromExtensionId(extension_id)) {
scoped_observation_.Observe(context);
}
TestServiceWorkerContextObserver(const TestServiceWorkerContextObserver&) =
delete;
TestServiceWorkerContextObserver& operator=(
const TestServiceWorkerContextObserver&) = delete;
~TestServiceWorkerContextObserver() override = default;
void WaitForWorkerStart() {
started_run_loop_.Run();
EXPECT_TRUE(running_version_id_.has_value());
}
void WaitForWorkerActivated() {
activated_run_loop_.Run();
EXPECT_TRUE(running_version_id_.has_value());
}
void WaitForWorkerStop() {
stopped_run_loop_.Run();
EXPECT_EQ(running_version_id_, std::nullopt);
}
int64_t GetServiceWorkerVersionId() { return running_version_id_.value(); }
private:
// ServiceWorkerContextObserver:
void OnVersionStartedRunning(
int64_t version_id,
const content::ServiceWorkerRunningInfo& running_info) override {
if (running_info.scope != extension_url_) {
return;
}
running_version_id_ = version_id;
started_run_loop_.Quit();
}
void OnVersionActivated(int64_t version_id, const GURL& scope) override {
if (running_version_id_ != version_id) {
return;
}
activated_run_loop_.Quit();
}
void OnVersionStoppedRunning(int64_t version_id) override {
if (running_version_id_ != version_id) {
return;
}
stopped_run_loop_.Quit();
running_version_id_ = std::nullopt;
}
void OnDestruct(content::ServiceWorkerContext* context) override {
ASSERT_TRUE(scoped_observation_.IsObserving());
scoped_observation_.Reset();
}
base::RunLoop started_run_loop_;
base::RunLoop activated_run_loop_;
base::RunLoop stopped_run_loop_;
std::optional<int64_t> running_version_id_;
base::ScopedObservation<content::ServiceWorkerContext,
content::ServiceWorkerContextObserver>
scoped_observation_{this};
GURL extension_url_;
};
class TestServiceWorkerConsoleObserver
: public content::ServiceWorkerContextObserver {
public:
explicit TestServiceWorkerConsoleObserver(
content::BrowserContext* browser_context) {
content::StoragePartition* partition =
browser_context->GetDefaultStoragePartition();
scoped_observation_.Observe(partition->GetServiceWorkerContext());
}
~TestServiceWorkerConsoleObserver() override = default;
TestServiceWorkerConsoleObserver(const TestServiceWorkerConsoleObserver&) =
delete;
TestServiceWorkerConsoleObserver& operator=(
const TestServiceWorkerConsoleObserver&) = delete;
using Message = content::ConsoleMessage;
const std::vector<Message>& messages() const { return messages_; }
void WaitForMessages() { run_loop_.Run(); }
private:
// ServiceWorkerContextObserver:
void OnReportConsoleMessage(int64_t version_id,
const GURL& scope,
const Message& message) override {
messages_.push_back(message);
run_loop_.Quit();
}
base::RunLoop run_loop_;
std::vector<Message> messages_;
base::ScopedObservation<content::ServiceWorkerContext,
content::ServiceWorkerContextObserver>
scoped_observation_{this};
};
class ChromeWebUsbTest : public InProcessBrowserTest {
public:
void SetUpOnMainThread() override {
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
AddFakeDevice("123456");
// Connect with the FakeUsbDeviceManager.
mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager;
device_manager_.AddReceiver(
device_manager.InitWithNewPipeAndPassReceiver());
UsbChooserContextFactory::GetForProfile(browser()->profile())
->SetDeviceManagerForTesting(std::move(device_manager));
test_content_browser_client_.SetAsBrowserClient();
GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html");
EXPECT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
origin_ = url.DeprecatedGetOriginAsURL();
content::RenderFrameHost* render_frame_host = browser()
->tab_strip_model()
->GetActiveWebContents()
->GetPrimaryMainFrame();
EXPECT_EQ(origin_, render_frame_host->GetLastCommittedOrigin().GetURL());
}
void TearDownOnMainThread() override {
test_content_browser_client_.UnsetAsBrowserClient();
}
void AddFakeDevice(const std::string& serial_number) {
ASSERT_TRUE(!fake_device_info_);
fake_device_info_ = device_manager_.CreateAndAddDevice(
0, 0, "Test Manufacturer", "Test Device", serial_number);
}
void RemoveFakeDevice() {
ASSERT_TRUE(fake_device_info_);
device_manager_.RemoveDevice(fake_device_info_->guid);
fake_device_info_ = nullptr;
}
const GURL& origin() { return origin_; }
void UseFakeChooser() {
test_content_browser_client_.delegate().UseFakeChooser();
}
UsbChooserContext* GetChooserContext() {
return UsbChooserContextFactory::GetForProfile(browser()->profile());
}
private:
device::FakeUsbDeviceManager device_manager_;
device::mojom::UsbDeviceInfoPtr fake_device_info_;
TestUsbContentBrowserClient test_content_browser_client_;
GURL origin_;
};
scoped_refptr<device::FakeUsbDeviceInfo> CreateUsbDevice(
uint8_t class_code,
uint16_t product_id = 0x8765) {
auto alternate_setting = device::mojom::UsbAlternateInterfaceInfo::New();
alternate_setting->alternate_setting = 0;
alternate_setting->class_code = class_code;
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, product_id, "ACME", "Frobinator", "ABCDEF", std::move(configs));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, RequestAndGetDevices) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
// Call getDevices with no device permissions.
EXPECT_EQ(content::ListValueOf(), EvalJs(web_contents,
R"((async () => {
let devices = await navigator.usb.getDevices();
return devices.map(device => device.serialNumber);
})())"));
// Request permission to access a device. The chooser will automatically
// select the item representing the fake device, granting the permission.
UseFakeChooser();
EXPECT_EQ("123456", EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
// Call getDevices again. This time the fake device is included.
EXPECT_EQ(content::ListValueOf("123456"), EvalJs(web_contents,
R"((async () => {
let devices = await navigator.usb.getDevices();
return devices.map(device => device.serialNumber);
})())"));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, RequestDeviceWithGuardBlocked) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
auto* map =
HostContentSettingsMapFactory::GetForProfile(browser()->profile());
map->SetContentSettingDefaultScope(origin(), origin(),
ContentSettingsType::USB_GUARD,
CONTENT_SETTING_BLOCK);
UseFakeChooser();
EXPECT_EQ(
"NotFoundError: Failed to execute 'requestDevice' on 'USB': No device "
"selected.",
content::EvalJs(web_contents,
R"((async () => {
try {
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return "Expected error, got success.";
} catch (e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, AddRemoveDevice) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
UseFakeChooser();
EXPECT_EQ("123456", content::EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var removedPromise = new Promise(resolve => {
navigator.usb.addEventListener('disconnect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
RemoveFakeDevice();
EXPECT_EQ("123456", content::EvalJs(web_contents, "removedPromise"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var addedPromise = new Promise(resolve => {
navigator.usb.addEventListener('connect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
// The device is eligible for perisistent permissions, so the permission is
// remembered and a connect event is fired when the device is reconnected.
AddFakeDevice("123456");
EXPECT_EQ("123456", content::EvalJs(web_contents, "addedPromise"));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, AddRemoveDeviceEphemeral) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
// Replace the default mock device with one that has no serial number.
RemoveFakeDevice();
AddFakeDevice("");
UseFakeChooser();
EXPECT_EQ("", content::EvalJs(web_contents,
R"((async () => {
let device =
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
return device.serialNumber;
})())"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var removedPromise = new Promise(resolve => {
navigator.usb.addEventListener('disconnect', e => {
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
RemoveFakeDevice();
EXPECT_EQ("", content::EvalJs(web_contents, "removedPromise"));
EXPECT_TRUE(content::ExecJs(web_contents,
R"(
var sawConnectEvent = false;
var addedPromise = new Promise(resolve => {
navigator.usb.addEventListener('connect', e => {
sawConnectEvent = true;
resolve(e.device.serialNumber);
}, { once: true });
});
)"));
// The ephemeral device permission is not persisted after disconnection, so
// the connect event should not be fired when the device is reconnected.
AddFakeDevice("");
// Call getDevices and wait for it to return to force synchronization before
// checking `sawConnectEvent`.
EXPECT_EQ(false, content::EvalJs(web_contents, R"((async () => {
await navigator.usb.getDevices();
return sawConnectEvent;
})())"));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, NavigateWithChooserCrossOrigin) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
content::TestNavigationObserver observer(
web_contents, 1 /* number_of_navigations */,
content::MessageLoopRunner::QuitMode::DEFERRED);
auto waiter = test::ChooserBubbleUiWaiter::Create();
EXPECT_TRUE(content::ExecJs(web_contents,
"navigator.usb.requestDevice({ filters: [] })",
content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
// Wait for the chooser to be displayed before navigating to avoid a race
// between the two IPCs.
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_shown());
EXPECT_TRUE(content::ExecJs(web_contents,
"document.location.href = 'https://google.com'"));
observer.Wait();
waiter->WaitForChange();
EXPECT_TRUE(waiter->has_closed());
EXPECT_EQ(GURL("https://google.com"), web_contents->GetLastCommittedURL());
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, ShowChooserInBackgroundTab) {
// Create a new foreground tab that covers `background_web_contents`.
content::WebContents* background_web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
GURL url = embedded_test_server()->GetURL("localhost", "/simple_page.html");
ui_test_utils::NavigateToURLWithDisposition(
browser(), url, WindowOpenDisposition::NEW_FOREGROUND_TAB,
ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
// Try to show the chooser in the background tab.
EXPECT_EQ(
"NotFoundError: Failed to execute 'requestDevice' on 'USB': No device "
"selected.",
content::EvalJs(background_web_contents,
R"((async () => {
try {
await navigator.usb.requestDevice({ filters: [] });
return "Expected error, got success.";
} catch (e) {
return `${e.name}: ${e.message}`;
}
})())"));
}
IN_PROC_BROWSER_TEST_F(ChromeWebUsbTest, ForgetDevice) {
content::WebContents* web_contents =
browser()->tab_strip_model()->GetActiveWebContents();
base::HistogramTester histogram_tester;
UseFakeChooser();
EXPECT_EQ(1, content::EvalJs(web_contents,
R"((async () => {
await navigator.usb.requestDevice({ filters: [{ vendorId: 0 }] });
const devices = await navigator.usb.getDevices();
return devices.length;
})())"));
EXPECT_EQ(0, content::EvalJs(web_contents,
R"((async () => {
const [device] = await navigator.usb.getDevices();
await device.forget();
const devices = await navigator.usb.getDevices();
return devices.length;
})())"));
histogram_tester.ExpectUniqueSample("WebUsb.PermissionRevoked",
WEBUSB_PERMISSION_REVOKED_BY_WEBSITE, 1);
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
class ChromeWebUsbAppTest : public extensions::ExtensionBrowserTest {
public:
void SetUpOnMainThread() override {
ExtensionBrowserTest::SetUpOnMainThread();
mojo::PendingRemote<device::mojom::UsbDeviceManager> remote;
device_manager_.AddReceiver(remote.InitWithNewPipeAndPassReceiver());
GetChooserContext()->SetDeviceManagerForTesting(std::move(remote));
}
protected:
UsbChooserContext* GetChooserContext() {
return UsbChooserContextFactory::GetForProfile(browser()->profile());
}
device::FakeUsbDeviceManager& device_manager() { return device_manager_; }
private:
device::FakeUsbDeviceManager device_manager_;
};
IN_PROC_BROWSER_TEST_F(ChromeWebUsbAppTest, AllowProtectedInterfaces) {
extensions::TestExtensionDir dir;
dir.WriteManifest(R"(
{
"name": "WebUsbTest App",
"version": "1.0",
"manifest_version": 2,
"app": {
"background": {
"scripts": ["background_script.js"]
}
}
}
)");
dir.WriteFile(FILE_PATH_LITERAL("background_script.js"), R"(
chrome.test.sendMessage("ready", async () => {
try {
const devices = await navigator.usb.getDevices();
const device = devices[0];
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)");
// Launch the test app.
ExtensionTestMessageListener ready_listener("ready",
ReplyBehavior::kWillReply);
extensions::ResultCatcher result_catcher;
scoped_refptr<const extensions::Extension> extension =
LoadExtension(dir.UnpackedPath());
// Configure the test device.
auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass);
auto device_info = device_manager().AddDevice(fake_device_info);
GetChooserContext()->GrantDevicePermission(extension->origin(), *device_info);
// Run the test.
EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
ready_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
class IsolatedWebAppUsbBrowserTest
: public web_app::IsolatedWebAppBrowserTestHarness {
public:
~IsolatedWebAppUsbBrowserTest() override = default;
void SetUpOnMainThread() override {
IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
mojo::PendingRemote<device::mojom::UsbDeviceManager> remote;
device_manager_.AddReceiver(remote.InitWithNewPipeAndPassReceiver());
chooser_context()->SetDeviceManagerForTesting(std::move(remote));
}
protected:
UsbChooserContext* chooser_context() {
return UsbChooserContextFactory::GetForProfile(profile());
}
device::FakeUsbDeviceManager& device_manager() { return device_manager_; }
private:
device::FakeUsbDeviceManager device_manager_;
};
IN_PROC_BROWSER_TEST_F(IsolatedWebAppUsbBrowserTest, ClaimInterface) {
// Verifies that non-IWA main frames and cross-origin iframes in an IWA can
// access normal USB devices, but not devices from a protected class. IWA
// frames without usb-unrestricted permission can only access non-protected
// class too.
GURL frame_url = https_server()->GetURL("/banners/isolated/simple.html");
auto* non_app_main_frame = ui_test_utils::NavigateToURL(browser(), frame_url);
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
web_app::CreateIframe(app_frame, "child", frame_url,
/*permissions_policy=*/"usb *");
auto* delegated_non_app_iframe = ChildFrameAt(app_frame, 0);
const uint16_t kSmartCardProductId = 0x8765;
auto fake_smart_card_device_info =
CreateUsbDevice(device::mojom::kUsbSmartCardClass, kSmartCardProductId);
auto smart_card_device_info =
device_manager().AddDevice(std::move(fake_smart_card_device_info));
chooser_context()->GrantDevicePermission(
non_app_main_frame->GetLastCommittedOrigin(), *smart_card_device_info);
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*smart_card_device_info);
const uint16_t kPrinterProductId = 0x5678;
auto fake_printer_device_info =
CreateUsbDevice(kUsbPrinterClass, kPrinterProductId);
auto printer_device_info =
device_manager().AddDevice(std::move(fake_printer_device_info));
chooser_context()->GrantDevicePermission(
non_app_main_frame->GetLastCommittedOrigin(), *printer_device_info);
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*printer_device_info);
constexpr char kClaimInterface[] = R"((async () => {
const devices = await navigator.usb.getDevices();
const device = devices.filter((device) => device.productId === $1)[0];
await device.open();
await device.selectConfiguration(1);
await device.claimInterface(0);
return "Success";
})())";
EXPECT_EQ("Success",
EvalJs(app_frame, JsReplace(kClaimInterface, kPrinterProductId)));
EXPECT_THAT(
EvalJs(app_frame, JsReplace(kClaimInterface, kSmartCardProductId)),
FailedWithSubstr("requested interface implements a protected class"));
EXPECT_EQ("Success", EvalJs(non_app_main_frame,
JsReplace(kClaimInterface, kPrinterProductId)));
EXPECT_THAT(
EvalJs(non_app_main_frame,
JsReplace(kClaimInterface, kSmartCardProductId)),
FailedWithSubstr("requested interface implements a protected class"));
EXPECT_EQ("Success", EvalJs(delegated_non_app_iframe,
JsReplace(kClaimInterface, kPrinterProductId)));
EXPECT_THAT(
EvalJs(delegated_non_app_iframe,
JsReplace(kClaimInterface, kSmartCardProductId)),
FailedWithSubstr("requested interface implements a protected class"));
}
class IsolatedWebAppPermissionsPolicyBrowserTest
: public IsolatedWebAppUsbBrowserTest {
public:
void SetUpCommandLine(base::CommandLine* command_line) override {
IsolatedWebAppUsbBrowserTest::SetUpCommandLine(command_line);
command_line->AppendSwitch(
switches::kEnableExperimentalWebPlatformFeatures);
}
};
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Iframe_NoAllowAttribute) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Create a same-origin iframe on the page that does not specify an
// allow attribute, and expect that usb is accessible on the main frame, as
// well as in the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
const std::string permissions_policy = "";
web_app::CreateIframe(app_frame, "child", GURL("/"), permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
// Create a cross-origin iframe and expect usb to be disabled in that context
// since it does not specify usb in the allowlist.
GURL non_app_url =
https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html");
web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy);
iframe = ChildFrameAt(app_frame, 1);
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Iframe_Self) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Create a same-origin iframe on the page that specifies an allow
// attribute allowing usb for 'self', and expect that usb is accessible on the
// main frame, as well as in the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
const std::string permissions_policy = "usb 'self'";
web_app::CreateIframe(app_frame, "child", GURL("/"), permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
// Perform a cross-origin navigation in the iframe, which should no longer
// match the 'self' permissions policy token, and verify the permissions
// policy blocks access to usb.
GURL non_app_url =
https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html");
EXPECT_TRUE(content::NavigateToURLFromRenderer(iframe, non_app_url));
iframe = ChildFrameAt(app_frame, 0);
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
// TODO(crbug.com/398292721): Flaky on Mac and Linux builds.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_PermissionsPolicy_Iframe_Src DISABLED_PermissionsPolicy_Iframe_Src
#else
#define MAYBE_PermissionsPolicy_Iframe_Src PermissionsPolicy_Iframe_Src
#endif
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
MAYBE_PermissionsPolicy_Iframe_Src) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Create a cross-origin iframe on the page that specifies an allow
// attribute allowing usb for 'src', and expect that usb is accessible on the
// main frame, as well as in the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
GURL non_app_url =
https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html");
const std::string permissions_policy = "usb 'src'";
web_app::CreateIframe(app_frame, "child", non_app_url, permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
// Perform a navigation in the iframe to a cross-origin resource (a distinct
// origin from that which the iframe originally loaded, as well as the main
// frame), which should no longer match the 'src' permissions policy token,
// and verify the permissions policy blocks access to usb.
GURL non_app_url_2 =
https_server()->GetURL(kNonAppHost2, "/banners/isolated/simple.html");
EXPECT_TRUE(content::NavigateToURLFromRenderer(iframe, non_app_url_2));
iframe = ChildFrameAt(app_frame, 0);
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Iframe_None) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Create a cross-origin iframe on the page that specifies an allow
// attribute allowing usb with the 'none' token, and expect that usb is
// accessible on the main frame, but is blocked by permissions policy in the
// iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
const std::string permissions_policy = "usb 'none'";
web_app::CreateIframe(app_frame, "child", GURL("/index.html"),
permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Iframe_CrossOrigin) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Create a cross-origin iframe on the page that specifies an allow
// attribute allowing usb for the iframe by explicitly listing the iframe
// origin in the allowlist, and expect that usb is accessible on the main
// frame as well as in the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
GURL non_app_url =
https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html");
const std::string permissions_policy = base::StringPrintf(
"usb %s", https_server()->GetURL(kNonAppHost, "/").spec().c_str());
web_app::CreateIframe(app_frame, "child", non_app_url, permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Headers_None) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Load a page in the app window that has a Permissions Policy
// header which does not allow usb on any origin (using an empty allowlist).
// Create a same-origin iframe on the page that does not specify an allow
// attribute, and expect that usb is not accessible on the main frame or in
// the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.AddFileFromDisk("/usb_none.html",
"web_apps/simple_isolated_app/usb_none.html")
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
GURL app_url = url_info.origin().GetURL().Resolve("/usb_none.html");
auto* app_browser = GetBrowserFromFrame(app_frame);
app_frame = ui_test_utils::NavigateToURL(app_browser, app_url);
const std::string permissions_policy = "";
web_app::CreateIframe(app_frame, "child", app_url, permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_THAT(EvalJs(app_frame, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
// Create a cross-origin iframe and expect usb to be disabled in that context.
GURL non_app_url = https_server()->GetURL(
kNonAppHost, "/web_apps/simple_isolated_app/usb_none.html");
web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy);
iframe = ChildFrameAt(app_frame, 1);
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Headers_Self) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Load a page in the app window that has a Permissions Policy
// header which allows usb on the same origin using the 'self' token. Create a
// same-origin iframe on the page that does not specify an allow attribute,
// and expect that usb is accessible on the main frame, as well as in the
// iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.AddFileFromDisk("/usb_self.html",
"web_apps/simple_isolated_app/usb_self.html")
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
GURL app_url = url_info.origin().GetURL().Resolve("/usb_self.html");
auto* app_browser = GetBrowserFromFrame(app_frame);
app_frame = ui_test_utils::NavigateToURL(app_browser, app_url);
const std::string permissions_policy = "";
web_app::CreateIframe(app_frame, "child", app_url, permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
// Create a cross-origin iframe and expect usb to be disabled in that context.
GURL non_app_url = https_server()->GetURL(
kNonAppHost, "/web_apps/simple_isolated_app/usb_self.html");
web_app::CreateIframe(app_frame, "child2", non_app_url, permissions_policy);
iframe = ChildFrameAt(app_frame, 1);
EXPECT_THAT(EvalJs(iframe, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Headers_All) {
// Install an Isolated Web App that has usb turned on for all origins in its
// manifest. Load a page in the app window that has a Permissions Policy
// header which allows usb on any origin. Create a same-origin iframe on the
// page that does not specify an allow attribute, and expect that usb is
// accessible on the main frame, as well as in the iframe.
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb))
.AddFileFromDisk("/usb_all.html",
"web_apps/simple_isolated_app/usb_all.html")
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
GURL app_url = url_info.origin().GetURL().Resolve("/usb_all.html");
auto* app_browser = GetBrowserFromFrame(app_frame);
app_frame = ui_test_utils::NavigateToURL(app_browser, app_url);
const std::string permissions_policy = "";
web_app::CreateIframe(app_frame, "child", app_url, permissions_policy);
auto* iframe = ChildFrameAt(app_frame, 0);
auto fake_device_info = CreateUsbDevice(kUsbPrinterClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
// Create a cross-origin iframe with "usb" in the allow attribute and expect
// usb to be enabled in that context.
GURL non_app_url = https_server()->GetURL(
kNonAppHost, "/web_apps/simple_isolated_app/usb_all.html");
web_app::CreateIframe(app_frame, "child2", non_app_url, "usb");
iframe = ChildFrameAt(app_frame, 1);
EXPECT_EQ("Success", EvalJs(iframe, OpenAndClaimDeviceScript));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Usb_Unrestricted_CrossOrigin_Iframe) {
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder()
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb)
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsbUnrestricted)
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::
kCrossOriginIsolated))
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
// Create a fake device with protected class and grant permission.
auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
// With "usb-unrestricted" permission, when main frame claims protected class
// device it should succeed.
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
GURL cross_origin_iframe_url =
https_server()->GetURL(kNonAppHost, "/banners/isolated/simple.html");
// Create a cross-origin Iframe without any permission and request to
// protected class device should be denied due to "usb" feature is not
// enabled on iframe's document.
web_app::CreateIframe(app_frame, "child0", cross_origin_iframe_url, "");
auto* cross_origin_iframe0 = ChildFrameAt(app_frame, 0);
EXPECT_THAT(
EvalJs(cross_origin_iframe0, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
// Create a cross-origin Iframe with only "usb-unrestricted" permission,
// request to protected class device should be denied due to "usb" feature is
// not enabled on iframe's document.
web_app::CreateIframe(app_frame, "child1", cross_origin_iframe_url,
"usb-unrestricted");
auto* cross_origin_iframe1 = ChildFrameAt(app_frame, 1);
EXPECT_THAT(
EvalJs(cross_origin_iframe1, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("permissions policy."));
// Create a cross-origin Iframe with only "usb" permission, request to
// protected class device should be denied due to "usb-unrestricted" is not
// enabled.
web_app::CreateIframe(app_frame, "child2", cross_origin_iframe_url, "usb");
auto* cross_origin_iframe2 = ChildFrameAt(app_frame, 2);
EXPECT_THAT(
EvalJs(cross_origin_iframe2, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("requested interface implements a protected class."));
// Create a cross-origin Iframe with "usb + usb-unrestricted" and request to
// protected class device should be denied due to iframe's isolation level =
// 0.
web_app::CreateIframe(app_frame, "child3", cross_origin_iframe_url,
"usb; usb-unrestricted");
auto* cross_origin_iframe3 = ChildFrameAt(app_frame, 3);
EXPECT_THAT(
EvalJs(cross_origin_iframe3, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("requested interface implements a protected class."));
}
IN_PROC_BROWSER_TEST_F(IsolatedWebAppPermissionsPolicyBrowserTest,
PermissionsPolicy_Usb_Unrestricted_Iframe) {
std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
web_app::IsolatedWebAppBuilder(
web_app::ManifestBuilder()
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsb)
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::kUsbUnrestricted)
.AddPermissionsPolicyWildcard(
network::mojom::PermissionsPolicyFeature::
kCrossOriginIsolated))
.AddHtml("/empty.html", "Empty Page")
.BuildBundle();
ASSERT_OK_AND_ASSIGN(web_app::IsolatedWebAppUrlInfo url_info,
app->Install(profile()));
content::RenderFrameHost* app_frame = OpenApp(url_info.app_id());
// Create a fake device with protected class and grant permission.
auto fake_device_info = CreateUsbDevice(device::mojom::kUsbSmartCardClass);
auto device_info = device_manager().AddDevice(std::move(fake_device_info));
chooser_context()->GrantDevicePermission(app_frame->GetLastCommittedOrigin(),
*device_info);
// With "usb + usb-unrestricted" permission, when main frame claims protected
// class device it should succeed.
EXPECT_EQ("Success", EvalJs(app_frame, OpenAndClaimDeviceScript));
// Create a same-origin iframe without any permissions in attribute, request
// to protected class device should still succeed due to "usb +
// usb-unrestricted" feature is inherited from main frame and same-origin
// iframe is still isolated.
web_app::CreateIframe(app_frame, "child0", GURL("empty.html"), "");
auto* iframe0 = ChildFrameAt(app_frame, 0);
EXPECT_EQ("Success", EvalJs(iframe0, OpenAndClaimDeviceScript));
// Create a same-origin iframe with "usb-unrestricted" permissions disabled,
// request to protected class device should fail.
web_app::CreateIframe(app_frame, "child1", GURL("empty.html"),
"usb-unrestricted 'none'");
auto* iframe1 = ChildFrameAt(app_frame, 1);
EXPECT_THAT(
EvalJs(iframe1, OpenAndClaimDeviceScript).ExtractString(),
testing::EndsWith("requested interface implements a protected class."));
}
#if BUILDFLAG(ENABLE_EXTENSIONS)
class WebUsbExtensionBrowserTest : public InProcessBrowserTestMixinHostSupport<
extensions::ExtensionBrowserTest> {
public:
WebUsbExtensionBrowserTest() {
#if BUILDFLAG(IS_CHROMEOS)
// The user is created via RegularLoggedInBrowserTestMixin.
set_chromeos_user_ = false;
#endif // BUILDFLAG(IS_CHROMEOS)
}
void SetUpOnMainThread() override {
InProcessBrowserTestMixinHostSupport<
extensions::ExtensionBrowserTest>::SetUpOnMainThread();
#if BUILDFLAG(IS_CHROMEOS)
user_manager::UserManager::Get()->SetUserPolicyStatus(
kManagedUserAccountId, /*is_managed=*/true, /*is_affiliated=*/true);
display_service_for_system_notification_ =
std::make_unique<NotificationDisplayServiceTester>(
/*profile=*/nullptr);
#endif // BUILDFLAG(IS_CHROMEOS)
mojo::PendingRemote<device::mojom::UsbDeviceManager> device_manager;
device_manager_.AddReceiver(
device_manager.InitWithNewPipeAndPassReceiver());
// Connect the UsbDeviceManager and ensure we've received the initial
// enumeration before continuing.
auto* chooser_context = UsbChooserContextFactory::GetForProfile(profile());
chooser_context->SetDeviceManagerForTesting(std::move(device_manager));
TestFuture<std::vector<device::mojom::UsbDeviceInfoPtr>> devices_future;
chooser_context->GetDevices(devices_future.GetCallback());
ASSERT_TRUE(devices_future.Get().empty());
}
void SetUpPolicy(const extensions::Extension* extension) {
// Define a policy to automatically grant permission to access the device
// created by AddFakeDevice.
constexpr char kPolicyTemplate[] = R"(
[
{
"devices": [{ "vendor_id": 1234, "product_id": 5678 }],
"urls": ["%s"]
}
])";
profile()->GetPrefs()->Set(
prefs::kManagedWebUsbAllowDevicesForUrls,
base::test::ParseJson(base::StringPrintf(
kPolicyTemplate, extension->url().spec().c_str())));
}
void SetUpTestDir(extensions::TestExtensionDir& test_dir,
std::string_view background_js) {
test_dir.WriteManifest(base::StringPrintf(
R"({
"name": "Test Extension",
"version": "0.1",
"key": "%s",
"manifest_version": 3,
"background": {
"service_worker": "background.js"
}
})",
kTestExtensionKey));
test_dir.WriteFile(FILE_PATH_LITERAL("background.js"), background_js);
}
const Extension* LoadExtensionAndRunTest(std::string_view background_js) {
extensions::TestExtensionDir test_dir;
SetUpTestDir(test_dir, background_js);
// Launch the test app.
ExtensionTestMessageListener ready_listener("ready",
ReplyBehavior::kWillReply);
extensions::ResultCatcher result_catcher;
const extensions::Extension* extension =
LoadExtension(test_dir.UnpackedPath());
CHECK(extension);
CHECK_EQ(extension->id(), kTestExtensionId);
// TODO(crbug.com/40847683): Grant permission using requestDevice().
// Run the test.
SetUpPolicy(extension);
EXPECT_TRUE(ready_listener.WaitUntilSatisfied());
ready_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
return extension;
}
device::FakeUsbDeviceManager& device_manager() { return device_manager_; }
void AddFakeDevice() {
DCHECK(!fake_device_info_);
fake_device_info_ = device_manager_.CreateAndAddDevice(
1234, 5678, "Test Manufacturer", "Test Device", "123456");
}
void RemoveFakeDevice() {
DCHECK(fake_device_info_);
device_manager_.RemoveDevice(fake_device_info_->guid);
fake_device_info_ = nullptr;
}
void SimulateClickOnSystemTrayIconButton(Browser* browser,
const Extension* extension) {
#if BUILDFLAG(IS_CHROMEOS)
auto* usb_pinned_notification = static_cast<UsbPinnedNotification*>(
g_browser_process->usb_system_tray_icon());
auto* device_pinned_notification_renderer =
static_cast<DevicePinnedNotificationRenderer*>(
usb_pinned_notification->GetIconRendererForTesting());
auto expected_pinned_notification_id =
device_pinned_notification_renderer->GetNotificationId(
browser->profile());
auto maybe_indicator_notification =
display_service_for_system_notification_->GetNotification(
expected_pinned_notification_id);
ASSERT_TRUE(maybe_indicator_notification);
EXPECT_TRUE(maybe_indicator_notification->pinned());
display_service_for_system_notification_->SimulateClick(
NotificationHandler::Type::TRANSIENT, expected_pinned_notification_id,
/*action_index=*/0, /*reply=*/std::nullopt);
auto* web_contents = browser->tab_strip_model()->GetActiveWebContents();
EXPECT_EQ(web_contents->GetURL(), "chrome://settings/content/usbDevices");
#else
// On non-ChromeOS platforms, as they use status icon and there isn't good
// test infra to simulate click on the status icon button, so simulate the
// click event by invoking ExecuteCommand of UsbConnectionTracker directly.
auto* usb_status_icon =
static_cast<UsbStatusIcon*>(g_browser_process->usb_system_tray_icon());
auto* status_icon_renderer = static_cast<DeviceStatusIconRenderer*>(
usb_status_icon->GetIconRendererForTesting());
status_icon_renderer->ExecuteCommandForTesting(
IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST, 0);
EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
"https://support.google.com/chrome?p=webusb");
status_icon_renderer->ExecuteCommandForTesting(
IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST + 1, 0);
EXPECT_EQ(browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
"chrome://settings/content/usbDevices");
status_icon_renderer->ExecuteCommandForTesting(
IDC_DEVICE_SYSTEM_TRAY_ICON_FIRST + 2, 0);
EXPECT_EQ(
browser->tab_strip_model()->GetActiveWebContents()->GetURL(),
"chrome://settings/content/siteDetails?site=chrome-extension%3A%2F%2F" +
extension->id());
#endif
}
private:
device::FakeUsbDeviceManager device_manager_;
device::mojom::UsbDeviceInfoPtr fake_device_info_;
#if BUILDFLAG(IS_CHROMEOS)
ash::RegularLoggedInBrowserTestMixin logged_in_mixin_{&mixin_host_,
kManagedUserAccountId};
std::unique_ptr<NotificationDisplayServiceTester>
display_service_for_system_notification_;
#endif // BUILDFLAG(IS_CHROMEOS)
};
// TODO(crbug.com/41494522): Flaky on non-Mac release builds.
#if !BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_GetDevices DISABLED_GetDevices
#else
#define MAYBE_GetDevices GetDevices
#endif
IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_GetDevices) {
constexpr std::string_view kBackgroundJs = R"(
chrome.test.sendMessage("ready", async () => {
try {
const devices = await navigator.usb.getDevices();
chrome.test.assertEq(1, devices.length);
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)";
AddFakeDevice();
LoadExtensionAndRunTest(kBackgroundJs);
}
// TODO(crbug.com/41494522): Flaky on non-Mac release builds.
#if !BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_RequestDevice DISABLED_RequestDevice
#else
#define MAYBE_RequestDevice RequestDevice
#endif
IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_RequestDevice) {
constexpr std::string_view kBackgroundJs = R"(
chrome.test.sendMessage("ready", async () => {
try {
chrome.test.assertEq(navigator.usb.requestDevice, undefined);
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)";
LoadExtensionAndRunTest(kBackgroundJs);
}
// TODO(crbug.com/41494522): Flaky on non-Mac release builds.
#if !BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_UsbConnectionTracker DISABLED_UsbConnectionTracker
#else
#define MAYBE_UsbConnectionTracker UsbConnectionTracker
#endif
IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest, MAYBE_UsbConnectionTracker) {
constexpr char kBackgroundJs[] = R"(
// |device| is a global variable to store UsbDevice object being tested in
// case the local one is garbage collected, which can close the connection.
var device;
chrome.test.sendMessage("ready", async () => {
try {
const devices = await navigator.usb.getDevices();
device = devices[0];
chrome.test.assertEq(1, devices.length);
// Bounce device a few times to make sure nothing unexpected happens.
await device.open();
await device.close();
await device.open();
await device.close();
await device.open();
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
)";
AddFakeDevice();
const auto* extension = LoadExtensionAndRunTest(kBackgroundJs);
SimulateClickOnSystemTrayIconButton(browser(), extension);
}
// Test the scenario of waking up the service worker upon device events and
// the service worker being kept alive with active device session.
// TODO(crbug.com/41494522): Flaky on non-Mac release builds and ChromeOS
// builds.
#if (!BUILDFLAG(IS_MAC) && defined(NDEBUG)) || BUILDFLAG(IS_CHROMEOS)
#define MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped \
DISABLED_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped
#else
#define MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped \
DeviceConnectAndOpenDeviceWhenServiceWorkerStopped
#endif
IN_PROC_BROWSER_TEST_F(
WebUsbExtensionBrowserTest,
MAYBE_DeviceConnectAndOpenDeviceWhenServiceWorkerStopped) {
content::ServiceWorkerContext* context = browser()
->profile()
->GetDefaultStoragePartition()
->GetServiceWorkerContext();
// Set up an observer for service worker events.
TestServiceWorkerContextObserver sw_observer(context, kTestExtensionId);
TestExtensionDir test_dir;
constexpr char kBackgroundJs[] = R"(
navigator.usb.onconnect = async (e) => {
chrome.test.sendMessage("connect", async () => {
try {
let device = e.device;
// Bounce device a few times to make sure nothing unexpected
// happens.
await device.open();
await device.close();
await device.open();
await device.close();
await device.open();
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
}
navigator.usb.ondisconnect = async (e) => {
chrome.test.sendMessage("disconnect", async () => {
try {
chrome.test.notifyPass();
} catch (e) {
chrome.test.fail(e.name + ':' + e.message);
}
});
}
)";
SetUpTestDir(test_dir, kBackgroundJs);
// Launch the test app.
ExtensionTestMessageListener connect_listener("connect",
ReplyBehavior::kWillReply);
extensions::ResultCatcher result_catcher;
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
// TODO(crbug.com/40847683): Grant permission using requestDevice().
// Run the test.
SetUpPolicy(extension);
ASSERT_TRUE(extension);
ASSERT_EQ(extension->id(), kTestExtensionId);
sw_observer.WaitForWorkerStart();
sw_observer.WaitForWorkerActivated();
// The device event is handled right after the service worker is activated.
int64_t service_worker_version_id = sw_observer.GetServiceWorkerVersionId();
base::SimpleTestTickClock tick_clock;
AddFakeDevice();
EXPECT_TRUE(connect_listener.WaitUntilSatisfied());
connect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and the service worker is still alive due to active device
// session.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
// Since we have active USB device session at this point, click the USB system
// tray icon and check right links are opened by the browser.
SimulateClickOnSystemTrayIconButton(browser(), extension);
// Remove device will close the device session, and worker will stop running
// when it times out.
ExtensionTestMessageListener disconnect_listener("disconnect",
ReplyBehavior::kWillReply);
RemoveFakeDevice();
EXPECT_TRUE(disconnect_listener.WaitUntilSatisfied());
disconnect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and check that the receiver service worker stopped.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_FALSE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
sw_observer.WaitForWorkerStop();
// Another device event wakes up the inactive worker.
connect_listener.Reset();
AddFakeDevice();
EXPECT_TRUE(connect_listener.WaitUntilSatisfied());
connect_listener.Reply("ok");
EXPECT_TRUE(result_catcher.GetNextResult()) << result_catcher.message();
// Advance clock and the service worker is still alive due to active device
// session.
content::AdvanceClockAfterRequestTimeout(context, service_worker_version_id,
&tick_clock);
EXPECT_TRUE(content::TriggerTimeoutAndCheckRunningState(
context, service_worker_version_id));
// Since we have active USB device session at this point, click the USB system
// tray icon and check right links are opened by the browser.
SimulateClickOnSystemTrayIconButton(browser(), extension);
}
// TODO(crbug.com/41494522): Flaky on non-Mac release builds.
#if !BUILDFLAG(IS_MAC) && defined(NDEBUG)
#define MAYBE_EventListenerAddedAfterServiceWorkerIsActivated \
DISABLED_EventListenerAddedAfterServiceWorkerIsActivated
#else
#define MAYBE_EventListenerAddedAfterServiceWorkerIsActivated \
EventListenerAddedAfterServiceWorkerIsActivated
#endif
IN_PROC_BROWSER_TEST_F(WebUsbExtensionBrowserTest,
MAYBE_EventListenerAddedAfterServiceWorkerIsActivated) {
const char kWarningMessage[] =
"Event handler of '%s' event must be added on the initial evaluation "
"of worker script. More info: "
"https://developer.chrome.com/docs/extensions/mv3/service_workers/"
"events/";
content::ServiceWorkerContext* context = browser()
->profile()
->GetDefaultStoragePartition()
->GetServiceWorkerContext();
// Set up an observer for service worker events.
TestServiceWorkerContextObserver sw_observer(context, kTestExtensionId);
// Set up an observer for console messages reported by service worker
TestServiceWorkerConsoleObserver console_observer(browser()
->tab_strip_model()
->GetActiveWebContents()
->GetBrowserContext());
TestExtensionDir test_dir;
constexpr char kBackgroundJs[] = R"(
chrome.test.sendMessage("ready", function() {
navigator.usb.addEventListener("connect", () => {});
});
)";
SetUpTestDir(test_dir, kBackgroundJs);
// Launch the test app.
extensions::ResultCatcher result_catcher;
const Extension* extension = LoadExtension(test_dir.UnpackedPath());
// TODO(crbug.com/40847683): Grant permission using requestDevice().
// Run the test.
SetUpPolicy(extension);
ASSERT_TRUE(extension);
ASSERT_EQ(extension->id(), kTestExtensionId);
sw_observer.WaitForWorkerStart();
sw_observer.WaitForWorkerActivated();
AddFakeDevice();
// Warning message will be displayed when event listener is nested inside a
// function
console_observer.WaitForMessages();
EXPECT_EQ(console_observer.messages().size(), 1u);
EXPECT_EQ(console_observer.messages().begin()->message_level,
blink::mojom::ConsoleMessageLevel::kWarning);
EXPECT_EQ(console_observer.messages().begin()->message,
base::UTF8ToUTF16(base::StringPrintf(kWarningMessage, "connect")));
}
#endif // BUILDFLAG(ENABLE_EXTENSIONS)
} // namespace