blob: ccbbdd5329f4d0307e61541ea0cd39cfbe2d7d05 [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 <vector>
#include "base/base_switches.h"
#include "base/test/scoped_feature_list.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/smart_card_delegate.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/content_browser_test.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_mock_cert_verifier.h"
#include "content/public/test/test_utils.h"
#include "content/shell/browser/shell.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/device/public/mojom/smart_card.mojom.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"
using device::mojom::SmartCardReaderInfo;
using device::mojom::SmartCardReaderInfoPtr;
using device::mojom::SmartCardReaderState;
namespace content {
namespace {
class FakeSmartCardDelegate : public SmartCardDelegate {
public:
void GetReaders(GetReadersCallback) override;
bool SupportsReaderAddedRemovedNotifications() const override { return true; }
bool AddReader(SmartCardReaderInfoPtr reader_info);
bool RemoveReader(const std::string& name);
private:
std::unordered_map<std::string, SmartCardReaderInfoPtr> reader_infos_;
};
class SmartCardTestContentBrowserClient : public ContentBrowserClient {
public:
SmartCardTestContentBrowserClient();
SmartCardTestContentBrowserClient(SmartCardTestContentBrowserClient&) =
delete;
SmartCardTestContentBrowserClient& operator=(
SmartCardTestContentBrowserClient&) = delete;
~SmartCardTestContentBrowserClient() override;
FakeSmartCardDelegate& delegate() { return delegate_; }
// ContentBrowserClient:
SmartCardDelegate* GetSmartCardDelegate(
content::BrowserContext* browser_context) override;
bool ShouldUrlUseApplicationIsolationLevel(BrowserContext* browser_context,
const GURL& url,
bool origin_matches_flag) override;
absl::optional<blink::ParsedPermissionsPolicy>
GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) override;
private:
FakeSmartCardDelegate delegate_;
};
class SmartCardTest : public ContentBrowserTest {
public:
FakeSmartCardDelegate& delegate() { return test_client_.delegate(); }
GURL GetIsolatedContextUrl() {
return https_server_.GetURL(
"a.com",
"/set-header?Cross-Origin-Opener-Policy: same-origin&"
"Cross-Origin-Embedder-Policy: require-corp&"
"Permissions-Policy: smart-card%3D(self)");
}
bool AddReader(const std::string& name) {
std::vector<uint8_t> atr = {1, 2, 3, 4};
SmartCardReaderInfoPtr reader =
SmartCardReaderInfo::New(name, SmartCardReaderState::kEmpty, atr);
return delegate().AddReader(std::move(reader));
}
bool RemoveReader(const std::string& name) {
return delegate().RemoveReader(name);
}
private:
void SetUpCommandLine(base::CommandLine* command_line) override {
ContentBrowserTest::SetUpCommandLine(command_line);
mock_cert_verifier_.SetUpCommandLine(command_line);
}
void SetUpOnMainThread() override {
ContentBrowserTest::SetUpOnMainThread();
scoped_setting_ =
std::make_unique<content::ScopedContentBrowserClientSetting>(
&test_client_);
mock_cert_verifier_.mock_cert_verifier()->set_default_result(net::OK);
// Serve a.com (and any other domain).
host_resolver()->AddRule("*", "127.0.0.1");
// Add a handler for the "/set-header" page (among others)
https_server_.AddDefaultHandlers(GetTestDataFilePath());
ASSERT_TRUE(https_server_.Start());
}
void SetUpInProcessBrowserTestFixture() override {
ContentBrowserTest::SetUpInProcessBrowserTestFixture();
mock_cert_verifier_.SetUpInProcessBrowserTestFixture();
}
void TearDownInProcessBrowserTestFixture() override {
ContentBrowserTest::TearDownInProcessBrowserTestFixture();
mock_cert_verifier_.TearDownInProcessBrowserTestFixture();
}
void TearDown() override {
ASSERT_TRUE(https_server_.ShutdownAndWaitUntilComplete());
ContentBrowserTest::TearDown();
}
SmartCardTestContentBrowserClient test_client_;
std::unique_ptr<ScopedContentBrowserClientSetting> scoped_setting_;
// Need a mock CertVerifier for HTTPS connections to succeed with the test
// server.
ContentMockCertVerifier mock_cert_verifier_;
net::EmbeddedTestServer https_server_{net::EmbeddedTestServer::TYPE_HTTPS};
base::test::ScopedFeatureList scoped_feature_list_{
blink::features::kSmartCard};
};
} // namespace
SmartCardTestContentBrowserClient::SmartCardTestContentBrowserClient() =
default;
SmartCardTestContentBrowserClient::~SmartCardTestContentBrowserClient() =
default;
SmartCardDelegate* SmartCardTestContentBrowserClient::GetSmartCardDelegate(
content::BrowserContext* browser_context) {
return &delegate_;
}
bool SmartCardTestContentBrowserClient::ShouldUrlUseApplicationIsolationLevel(
BrowserContext* browser_context,
const GURL& url,
bool origin_matches_flag) {
return true;
}
absl::optional<blink::ParsedPermissionsPolicy>
SmartCardTestContentBrowserClient::GetPermissionsPolicyForIsolatedWebApp(
content::BrowserContext* browser_context,
const url::Origin& app_origin) {
blink::ParsedPermissionsPolicy out;
blink::ParsedPermissionsPolicyDeclaration decl(
blink::mojom::PermissionsPolicyFeature::kSmartCard,
/*allowed_origins=*/
{blink::OriginWithPossibleWildcards(app_origin,
/*has_subdomain_wildcard=*/false)},
/*matches_all_origins=*/false, /*matches_opaque_src=*/false);
out.push_back(decl);
return out;
}
void FakeSmartCardDelegate::GetReaders(GetReadersCallback callback) {
std::vector<SmartCardReaderInfoPtr> readers;
readers.reserve(reader_infos_.size());
for (auto& reader : reader_infos_) {
readers.push_back(reader.second->Clone());
}
std::move(callback).Run(std::move(readers));
}
bool FakeSmartCardDelegate::AddReader(SmartCardReaderInfoPtr reader_info) {
if (reader_infos_.count(reader_info->name) > 0) {
return false;
}
for (auto& observer : observer_list_) {
observer.OnReaderAdded(*reader_info);
}
std::string name = reader_info->name;
reader_infos_[name] = std::move(reader_info);
return true;
}
bool FakeSmartCardDelegate::RemoveReader(const std::string& name) {
auto node_handle = reader_infos_.extract(name);
if (node_handle.empty()) {
return false;
}
for (auto& observer : observer_list_) {
observer.OnReaderRemoved(*node_handle.mapped());
}
return true;
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, GetReaders) {
ASSERT_TRUE(AddReader("Fake Reader"));
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_EQ(true, EvalJs(shell(),
R"((async () => {
let readers = await navigator.smartCard.getReaders();
return readers.length == 1 && readers[0].name == "Fake Reader";
})())"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ReaderAdd) {
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_TRUE(ExecJs(shell(), R"((async () => {
let observer = await navigator.smartCard.watchForReaders();
window.promise = new Promise((resolve) => {
observer.addEventListener('readeradd', (e) => {
resolve(e.reader.name);
}, { once: true });
});
})())"));
ASSERT_TRUE(AddReader("New Fake Reader"));
EXPECT_EQ("New Fake Reader", EvalJs(shell(), "window.promise"));
}
IN_PROC_BROWSER_TEST_F(SmartCardTest, ReaderRemove) {
const std::string reader_name = "Fake Reader";
ASSERT_TRUE(AddReader(reader_name));
ASSERT_TRUE(NavigateToURL(shell(), GetIsolatedContextUrl()));
EXPECT_TRUE(ExecJs(shell(), R"((async () => {
let observer = await navigator.smartCard.watchForReaders();
window.promise = new Promise((resolve) => {
observer.addEventListener('readerremove', (e) => {
resolve(e.reader.name);
}, { once: true });
});
})())"));
ASSERT_TRUE(RemoveReader(reader_name));
EXPECT_EQ(reader_name, EvalJs(shell(), "window.promise"));
}
} // namespace content