blob: 45348f58e95d398d1c81573f0497946ab5e1d8fb [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "base/bind.h"
#include "base/files/file_path.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/post_task.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "content/browser/frame_host/navigation_handle_impl.h"
#include "content/browser/loader/prefetch_url_loader_service.h"
#include "content/browser/storage_partition_impl.h"
#include "content/browser/web_package/signed_exchange_handler.h"
#include "content/browser/web_package/signed_exchange_utils.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/download_manager.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/ssl_status.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_paths.h"
#include "content/public/common/page_type.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test_utils.h"
#include "content/public/test/content_cert_verifier_browser_test.h"
#include "content/public/test/signed_exchange_browser_test_helper.h"
#include "content/public/test/test_navigation_throttle.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/shell/browser/shell.h"
#include "content/shell/browser/shell_download_manager_delegate.h"
#include "net/cert/cert_verify_result.h"
#include "net/cert/mock_cert_verifier.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/cert_test_util.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "net/test/embedded_test_server/http_request.h"
#include "net/test/embedded_test_server/http_response.h"
#include "net/test/test_data_directory.h"
#include "net/test/url_request/url_request_mock_http_job.h"
#include "net/url_request/url_request_filter.h"
#include "net/url_request/url_request_interceptor.h"
#include "services/network/loader_util.h"
#include "services/network/public/cpp/features.h"
#include "testing/gmock/include/gmock/gmock-matchers.h"
#include "third_party/blink/public/common/features.h"
namespace content {
namespace {
constexpr char kExpectedSXGEnabledAcceptHeaderForPrefetch[] =
"application/signed-exchange;v=b3;q=0.9,*/*;q=0.8";
constexpr char kLoadResultHistogram[] = "SignedExchange.LoadResult2";
constexpr char kPrefetchResultHistogram[] =
"SignedExchange.Prefetch.LoadResult2";
class RedirectObserver : public WebContentsObserver {
public:
explicit RedirectObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
~RedirectObserver() override = default;
void DidRedirectNavigation(NavigationHandle* handle) override {
const net::HttpResponseHeaders* response = handle->GetResponseHeaders();
if (response)
response_code_ = response->response_code();
}
const base::Optional<int>& response_code() const { return response_code_; }
private:
base::Optional<int> response_code_;
DISALLOW_COPY_AND_ASSIGN(RedirectObserver);
};
class AssertNavigationHandleFlagObserver : public WebContentsObserver {
public:
explicit AssertNavigationHandleFlagObserver(WebContents* web_contents)
: WebContentsObserver(web_contents) {}
~AssertNavigationHandleFlagObserver() override = default;
void DidFinishNavigation(NavigationHandle* handle) override {
EXPECT_TRUE(static_cast<NavigationHandleImpl*>(handle)->IsSignedExchangeInnerResponse());
}
private:
DISALLOW_COPY_AND_ASSIGN(AssertNavigationHandleFlagObserver);
};
class MockContentBrowserClient final : public ContentBrowserClient {
public:
std::string GetAcceptLangs(BrowserContext* context) override {
return accept_langs_;
}
void SetAcceptLangs(const std::string langs) { accept_langs_ = langs; }
private:
std::string accept_langs_ = "en";
};
} // namespace
class SignedExchangeRequestHandlerBrowserTestBase
: public CertVerifierBrowserTest {
public:
SignedExchangeRequestHandlerBrowserTestBase() {
// This installs "root_ca_cert.pem" from which our test certificates are
// created. (Needed for the tests that use real certificate, i.e.
// RealCertVerifier)
net::EmbeddedTestServer::RegisterTestCerts();
}
void SetUp() override {
sxg_test_helper_.SetUp();
feature_list_.InitWithFeatures({features::kSignedHTTPExchange}, {});
CertVerifierBrowserTest::SetUp();
}
void SetUpOnMainThread() override {
CertVerifierBrowserTest::SetUpOnMainThread();
#if defined(OS_ANDROID)
// TODO(crbug.com/864403): It seems that we call unsupported Android APIs on
// KitKat when we set a ContentBrowserClient. Don't call such APIs and make
// this test available on KitKat.
int32_t major_version = 0, minor_version = 0, bugfix_version = 0;
base::SysInfo::OperatingSystemVersionNumbers(&major_version, &minor_version,
&bugfix_version);
if (major_version < 5)
return;
#endif
original_client_ = SetBrowserClientForTesting(&client_);
}
void TearDownOnMainThread() override {
sxg_test_helper_.TearDownOnMainThread();
if (original_client_)
SetBrowserClientForTesting(original_client_);
}
protected:
void InstallUrlInterceptor(const GURL& url, const std::string& data_path) {
sxg_test_helper_.InstallUrlInterceptor(url, data_path);
}
void InstallMockCert() {
sxg_test_helper_.InstallMockCert(mock_cert_verifier());
}
void InstallMockCertChainInterceptor() {
sxg_test_helper_.InstallMockCertChainInterceptor();
}
void TriggerPrefetch(const GURL& url, bool expect_success) {
const GURL prefetch_html_url = embedded_test_server()->GetURL(
std::string("/sxg/prefetch.html#") + url.spec());
base::string16 expected_title =
base::ASCIIToUTF16(expect_success ? "OK" : "FAIL");
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
NavigateToURL(shell(), prefetch_html_url);
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
}
// Returns false if we cannot override accept languages. It happens only on
// Android Kitkat or older systems.
bool SetAcceptLangs(const std::string langs) {
if (!original_client_)
return false;
client_.SetAcceptLangs(langs);
StoragePartitionImpl* partition = static_cast<StoragePartitionImpl*>(
BrowserContext::GetDefaultStoragePartition(
shell()->web_contents()->GetBrowserContext()));
partition->GetPrefetchURLLoaderService()->SetAcceptLanguages(langs);
return true;
}
const base::HistogramTester histogram_tester_;
private:
ContentBrowserClient* original_client_ = nullptr;
MockContentBrowserClient client_;
base::test::ScopedFeatureList feature_list_;
SignedExchangeBrowserTestHelper sxg_test_helper_;
DISALLOW_COPY_AND_ASSIGN(SignedExchangeRequestHandlerBrowserTestBase);
};
enum class SignedExchangeRequestHandlerBrowserTestPrefetchParam {
kPrefetchDisabled,
kPrefetchEnabled
};
class SignedExchangeRequestHandlerBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase,
public testing::WithParamInterface<
SignedExchangeRequestHandlerBrowserTestPrefetchParam> {
public:
SignedExchangeRequestHandlerBrowserTest() = default;
protected:
bool PrefetchIsEnabled() {
return GetParam() == SignedExchangeRequestHandlerBrowserTestPrefetchParam::
kPrefetchEnabled;
}
private:
DISALLOW_COPY_AND_ASSIGN(SignedExchangeRequestHandlerBrowserTest);
};
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Simple) {
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, true);
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
AssertNavigationHandleFlagObserver assert_navigation_handle_flag_observer(
shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
NavigationEntry* entry =
shell()->web_contents()->GetController().GetVisibleEntry();
EXPECT_TRUE(entry->GetSSL().initialized);
EXPECT_FALSE(!!(entry->GetSSL().content_status &
SSLStatus::DISPLAYED_INSECURE_CONTENT));
ASSERT_TRUE(entry->GetSSL().certificate);
// "test.example.org.public.pem.cbor" is generated from
// "prime256v1-sha256.public.pem". So the SHA256 of the certificates must
// match.
const net::SHA256HashValue fingerprint =
net::X509Certificate::CalculateFingerprint256(
entry->GetSSL().certificate->cert_buffer());
scoped_refptr<net::X509Certificate> original_cert =
SignedExchangeBrowserTestHelper::LoadCertificate();
const net::SHA256HashValue original_fingerprint =
net::X509Certificate::CalculateFingerprint256(
original_cert->cert_buffer());
EXPECT_EQ(original_fingerprint, fingerprint);
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess,
PrefetchIsEnabled() ? 2 : 1);
histogram_tester_.ExpectTotalCount(
"SignedExchange.Time.CertificateFetch.Success",
PrefetchIsEnabled() ? 2 : 1);
if (PrefetchIsEnabled()) {
histogram_tester_.ExpectUniqueSample(kPrefetchResultHistogram,
SignedExchangeLoadResult::kSuccess, 1);
histogram_tester_.ExpectUniqueSample(
"SignedExchange.Prefetch.Recall.30Seconds", true, 1);
histogram_tester_.ExpectUniqueSample(
"SignedExchange.Prefetch.Precision.30Seconds", true, 1);
} else {
histogram_tester_.ExpectUniqueSample(
"SignedExchange.Prefetch.Recall.30Seconds", false, 1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, VariantMatch) {
if (!SetAcceptLangs("en-US,fr"))
return;
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, true);
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kSuccess,
PrefetchIsEnabled() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
VariantMismatch) {
if (!SetAcceptLangs("en-US,ja"))
return;
InstallUrlInterceptor(
GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/test.example.org.public.pem.cbor");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_fr_variant.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kVariantMismatch,
PrefetchIsEnabled() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
MissingNosniff) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_missing_nosniff.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kSXGServedWithoutNosniff,
PrefetchIsEnabled() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
InvalidContentType) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_invalid_content_type.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch,
PrefetchIsEnabled() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, Expired) {
SignedExchangeHandler::SetVerificationTimeForTesting(
base::Time::UnixEpoch() +
base::TimeDelta::FromSeconds(
SignedExchangeBrowserTestHelper::kSignatureHeaderExpires + 1));
InstallMockCertChainInterceptor();
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
RedirectObserver redirect_observer(shell()->web_contents());
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
EXPECT_EQ(303, redirect_observer.response_code());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram,
SignedExchangeLoadResult::kSignatureVerificationError, 1);
histogram_tester_.ExpectUniqueSample(
"SignedExchange.SignatureVerificationResult",
SignedExchangeSignatureVerifier::Result::kErrExpired, 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
RedirectBrokenSignedExchanges) {
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
constexpr const char* kBrokenExchanges[] = {
"/sxg/test.example.org_test_invalid_magic_string.sxg",
"/sxg/test.example.org_test_invalid_cbor_header.sxg",
};
for (const auto* broken_exchange : kBrokenExchanges) {
SCOPED_TRACE(testing::Message()
<< "testing broken exchange: " << broken_exchange);
GURL url = embedded_test_server()->GetURL(broken_exchange);
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
PrefetchIsEnabled() ? 4 : 2);
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kVersionMismatch,
PrefetchIsEnabled() ? 2 : 1);
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kHeaderParseError,
PrefetchIsEnabled() ? 2 : 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, BadMICE) {
InstallMockCertChainInterceptor();
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url =
embedded_test_server()->GetURL("/sxg/test.example.org_test_bad_mice.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
const base::string16 title_good = base::ASCIIToUTF16("Reached End: false");
const base::string16 title_bad = base::ASCIIToUTF16("Reached End: true");
TitleWatcher title_watcher(shell()->web_contents(), title_good);
title_watcher.AlsoWaitForTitle(title_bad);
NavigateToURL(shell(), url);
EXPECT_EQ(title_good, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
PrefetchIsEnabled() ? 2 : 1);
{
SCOPED_TRACE(testing::Message()
<< "testing SignedExchangeLoadResult::kMerkleIntegrityError");
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError,
PrefetchIsEnabled() ? 2 : 1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, BadMICESmall) {
InstallMockCertChainInterceptor();
InstallMockCert();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL(
"/sxg/test.example.org_test_bad_mice_small.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
// Note: TitleWatcher is not needed. NavigateToURL waits until navigation
// complete.
NavigateToURL(shell(), url);
histogram_tester_.ExpectTotalCount(kLoadResultHistogram,
PrefetchIsEnabled() ? 2 : 1);
{
SCOPED_TRACE(testing::Message()
<< "testing SignedExchangeLoadResult::kMerkleIntegrityError");
histogram_tester_.ExpectBucketCount(
kLoadResultHistogram, SignedExchangeLoadResult::kMerkleIntegrityError,
PrefetchIsEnabled() ? 2 : 1);
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest, CertNotFound) {
InstallUrlInterceptor(GURL("https://cert.example.org/cert.msg"),
"content/test/data/sxg/404.msg");
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
if (PrefetchIsEnabled())
TriggerPrefetch(url, false);
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
histogram_tester_.ExpectUniqueSample(
kLoadResultHistogram, SignedExchangeLoadResult::kCertFetchError,
PrefetchIsEnabled() ? 2 : 1);
histogram_tester_.ExpectTotalCount(
"SignedExchange.Time.CertificateFetch.Failure",
PrefetchIsEnabled() ? 2 : 1);
}
INSTANTIATE_TEST_SUITE_P(
SignedExchangeRequestHandlerBrowserTest,
SignedExchangeRequestHandlerBrowserTest,
testing::Values(
SignedExchangeRequestHandlerBrowserTestPrefetchParam::kPrefetchDisabled,
SignedExchangeRequestHandlerBrowserTestPrefetchParam::
kPrefetchEnabled));
class SignedExchangeRequestHandlerDownloadBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRequestHandlerDownloadBrowserTest() = default;
~SignedExchangeRequestHandlerDownloadBrowserTest() override = default;
protected:
class DownloadObserver : public DownloadManager::Observer {
public:
DownloadObserver(DownloadManager* manager) : manager_(manager) {
manager_->AddObserver(this);
}
~DownloadObserver() override { manager_->RemoveObserver(this); }
void WaitUntilDownloadCreated() { run_loop_.Run(); }
const GURL& observed_url() const { return url_; }
const std::string& observed_content_disposition() const {
return content_disposition_;
}
// content::DownloadManager::Observer implementation.
void OnDownloadCreated(content::DownloadManager* manager,
download::DownloadItem* item) override {
url_ = item->GetURL();
content_disposition_ = item->GetContentDisposition();
run_loop_.Quit();
}
private:
DownloadManager* manager_;
base::RunLoop run_loop_;
GURL url_;
std::string content_disposition_;
};
void SetUpOnMainThread() override {
SignedExchangeRequestHandlerBrowserTestBase::SetUpOnMainThread();
ASSERT_TRUE(downloads_directory_.CreateUniqueTempDir());
ShellDownloadManagerDelegate* delegate =
static_cast<ShellDownloadManagerDelegate*>(
shell()
->web_contents()
->GetBrowserContext()
->GetDownloadManagerDelegate());
delegate->SetDownloadBehaviorForTesting(downloads_directory_.GetPath());
}
private:
base::ScopedTempDir downloads_directory_;
};
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest,
Download) {
std::unique_ptr<DownloadObserver> observer =
std::make_unique<DownloadObserver>(BrowserContext::GetDownloadManager(
shell()->web_contents()->GetBrowserContext()));
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"));
const std::string load_sxg =
"const iframe = document.createElement('iframe');"
"iframe.src = './sxg/test.example.org_test_download.sxg';"
"document.body.appendChild(iframe);";
EXPECT_TRUE(ExecuteScript(shell()->web_contents(), load_sxg));
observer->WaitUntilDownloadCreated();
EXPECT_EQ(
embedded_test_server()->GetURL("/sxg/test.example.org_test_download.sxg"),
observer->observed_url());
EXPECT_EQ("attachment; filename=test.sxg",
observer->observed_content_disposition());
}
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerDownloadBrowserTest,
DataURLDownload) {
const GURL sxg_url = GURL("data:application/signed-exchange,");
std::unique_ptr<DownloadObserver> observer =
std::make_unique<DownloadObserver>(BrowserContext::GetDownloadManager(
shell()->web_contents()->GetBrowserContext()));
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
NavigateToURL(shell(), embedded_test_server()->GetURL("/empty.html"));
const std::string load_sxg = base::StringPrintf(
"const iframe = document.createElement('iframe');"
"iframe.src = '%s';"
"document.body.appendChild(iframe);",
sxg_url.spec().c_str());
EXPECT_TRUE(ExecuteScript(shell()->web_contents(), load_sxg));
observer->WaitUntilDownloadCreated();
EXPECT_EQ(sxg_url, observer->observed_url());
}
class SignedExchangeRequestHandlerRealCertVerifierBrowserTest
: public SignedExchangeRequestHandlerBrowserTestBase {
public:
SignedExchangeRequestHandlerRealCertVerifierBrowserTest() {
// Use "real" CertVerifier.
disable_mock_cert_verifier();
}
};
IN_PROC_BROWSER_TEST_F(SignedExchangeRequestHandlerRealCertVerifierBrowserTest,
Basic) {
InstallMockCertChainInterceptor();
InstallUrlInterceptor(GURL("https://test.example.org/test/"),
"content/test/data/sxg/fallback.html");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
// "test.example.org_test.sxg" should pass CertVerifier::Verify() and then
// fail at SignedExchangeHandler::CheckOCSPStatus() because of the dummy OCSP
// response.
// TODO(https://crbug.com/815024): Make this test pass the OCSP check. We'll
// need to either generate an OCSP response on the fly, or override the OCSP
// verification time.
base::string16 title = base::ASCIIToUTF16("Fallback URL response");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// Verify that it failed at the OCSP check step.
histogram_tester_.ExpectUniqueSample(kLoadResultHistogram,
SignedExchangeLoadResult::kOCSPError, 1);
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
LogicalUrlInServiceWorkerScope) {
// SW-scope: https://test.example.org/test/
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
const GURL install_sw_url =
GURL("https://test.example.org/test/publisher-service-worker.html");
InstallUrlInterceptor(install_sw_url,
"content/test/data/sxg/publisher-service-worker.html");
InstallUrlInterceptor(
GURL("https://test.example.org/test/publisher-service-worker.js"),
"content/test/data/sxg/publisher-service-worker.js");
{
base::string16 title = base::ASCIIToUTF16("Done");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), install_sw_url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
title_watcher.AlsoWaitForTitle(base::ASCIIToUTF16("Generated"));
NavigateToURL(shell(), url);
// The page content shoud be served from the signed exchange.
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
NotControlledByDistributorsSW) {
// SW-scope: http://127.0.0.1:PORT/sxg/
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
const GURL install_sw_url = embedded_test_server()->GetURL(
"/sxg/no-respond-with-service-worker.html");
{
base::string16 title = base::ASCIIToUTF16("Done");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), install_sw_url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), embedded_test_server()->GetURL(
"/sxg/test.example.org_test.sxg"));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// The page must not be controlled by the service worker of the physical URL.
EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller"));
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
NotControlledBySameOriginDistributorsSW) {
// SW-scope: https://test.example.org/scope/
// SXG physical URL: https://test.example.org/scope/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
InstallUrlInterceptor(GURL("https://test.example.org/scope/test.sxg"),
"content/test/data/sxg/test.example.org_test.sxg");
const GURL install_sw_url = GURL(
"https://test.example.org/scope/no-respond-with-service-worker.html");
InstallUrlInterceptor(
install_sw_url,
"content/test/data/sxg/no-respond-with-service-worker.html");
InstallUrlInterceptor(
GURL("https://test.example.org/scope/no-respond-with-service-worker.js"),
"content/test/data/sxg/no-respond-with-service-worker.js");
{
base::string16 title = base::ASCIIToUTF16("Done");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), install_sw_url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), GURL("https://test.example.org/scope/test.sxg"));
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
// The page must not be controlled by the service worker of the physical URL.
EXPECT_EQ(false, EvalJs(shell(), "!!navigator.serviceWorker.controller"));
}
IN_PROC_BROWSER_TEST_P(SignedExchangeRequestHandlerBrowserTest,
RegisterServiceWorkerFromSignedExchange) {
// SXG physical URL: http://127.0.0.1:PORT/sxg/test.example.org_test.sxg
// SXG logical URL: https://test.example.org/test/
InstallMockCert();
InstallMockCertChainInterceptor();
InstallUrlInterceptor(
GURL("https://test.example.org/test/publisher-service-worker.js"),
"content/test/data/sxg/publisher-service-worker.js");
embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
ASSERT_TRUE(embedded_test_server()->Start());
GURL url = embedded_test_server()->GetURL("/sxg/test.example.org_test.sxg");
{
base::string16 title = base::ASCIIToUTF16("https://test.example.org/test/");
TitleWatcher title_watcher(shell()->web_contents(), title);
NavigateToURL(shell(), url);
EXPECT_EQ(title, title_watcher.WaitAndGetTitle());
}
const std::string register_sw_script =
"(async function() {"
" try {"
" const registration = await navigator.serviceWorker.register("
" 'publisher-service-worker.js', {scope: './'});"
" window.domAutomationController.send(true);"
" } catch (e) {"
" window.domAutomationController.send(false);"
" }"
"})();";
bool result = false;
EXPECT_TRUE(ExecuteScriptAndExtractBool(shell()->web_contents(),
register_sw_script, &result));
// serviceWorker.register() fails because the document URL of
// ServiceWorkerProviderHost is empty.
EXPECT_FALSE(result);
}
class SignedExchangeAcceptHeaderBrowserTest
: public ContentBrowserTest,
public testing::WithParamInterface<bool> {
public:
using self = SignedExchangeAcceptHeaderBrowserTest;
SignedExchangeAcceptHeaderBrowserTest()
: https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
~SignedExchangeAcceptHeaderBrowserTest() override = default;
protected:
void SetUp() override {
if (GetParam()) {
feature_list_.InitAndEnableFeature(features::kSignedHTTPExchange);
} else {
feature_list_.InitAndDisableFeature(features::kSignedHTTPExchange);
}
https_server_.ServeFilesFromSourceDirectory("content/test/data");
https_server_.RegisterRequestHandler(
base::BindRepeating(&self::RedirectResponseHandler));
https_server_.RegisterRequestHandler(
base::BindRepeating(&self::FallbackSxgResponseHandler));
https_server_.RegisterRequestMonitor(
base::BindRepeating(&self::MonitorRequest, base::Unretained(this)));
ASSERT_TRUE(https_server_.Start());
ContentBrowserTest::SetUp();
}
void NavigateAndWaitForTitle(const GURL& url, const std::string title) {
base::string16 expected_title = base::ASCIIToUTF16(title);
TitleWatcher title_watcher(shell()->web_contents(), expected_title);
NavigateToURL(shell(), url);
EXPECT_EQ(expected_title, title_watcher.WaitAndGetTitle());
}
bool IsSignedExchangeEnabled() const { return GetParam(); }
void CheckAcceptHeader(const GURL& url,
bool is_navigation,
bool is_fallback) {
const auto accept_header = GetInterceptedAcceptHeader(url);
ASSERT_TRUE(accept_header);
EXPECT_EQ(
*accept_header,
IsSignedExchangeEnabled() && !is_fallback
? (is_navigation
? std::string(network::kFrameAcceptHeader) +
std::string(kAcceptHeaderSignedExchangeSuffix)
: std::string(kExpectedSXGEnabledAcceptHeaderForPrefetch))
: (is_navigation ? std::string(network::kFrameAcceptHeader)
: std::string(network::kDefaultAcceptHeader)));
}
void CheckNavigationAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, true /* is_navigation */, false /* is_fallback */);
}
}
void CheckPrefetchAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, false /* is_navigation */,
false /* is_fallback */);
}
}
void CheckFallbackAcceptHeader(const std::vector<GURL>& urls) {
for (const auto& url : urls) {
SCOPED_TRACE(url);
CheckAcceptHeader(url, true /* is_navigation */, true /* is_fallback */);
}
}
base::Optional<std::string> GetInterceptedAcceptHeader(
const GURL& url) const {
const auto it = url_accept_header_map_.find(url);
if (it == url_accept_header_map_.end())
return base::nullopt;
return it->second;
}
void ClearInterceptedAcceptHeaders() { url_accept_header_map_.clear(); }
net::EmbeddedTestServer https_server_;
private:
static std::unique_ptr<net::test_server::HttpResponse>
RedirectResponseHandler(const net::test_server::HttpRequest& request) {
if (!base::StartsWith(request.relative_url, "/r?",
base::CompareCase::SENSITIVE)) {
return std::unique_ptr<net::test_server::HttpResponse>();
}
std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
new net::test_server::BasicHttpResponse);
http_response->set_code(net::HTTP_MOVED_PERMANENTLY);
http_response->AddCustomHeader("Location", request.relative_url.substr(3));
http_response->AddCustomHeader("Cache-Control", "no-cache");
return std::move(http_response);
}
// Responds with a prologue-only signed exchange that triggers a fallback
// redirect.
static std::unique_ptr<net::test_server::HttpResponse>
FallbackSxgResponseHandler(const net::test_server::HttpRequest& request) {
const std::string prefix = "/fallback_sxg?";
if (!base::StartsWith(request.relative_url, prefix,
base::CompareCase::SENSITIVE)) {
return std::unique_ptr<net::test_server::HttpResponse>();
}
std::string fallback_url(request.relative_url.substr(prefix.length()));
std::unique_ptr<net::test_server::BasicHttpResponse> http_response(
new net::test_server::BasicHttpResponse);
http_response->set_code(net::HTTP_OK);
http_response->set_content_type("application/signed-exchange;v=b3");
std::string sxg("sxg1-b3", 8);
sxg.push_back(fallback_url.length() >> 8);
sxg.push_back(fallback_url.length() & 0xff);
sxg += fallback_url;
// FallbackUrlAndAfter() requires 6 more bytes for sizes of next fields.
sxg.resize(sxg.length() + 6);
http_response->set_content(sxg);
return std::move(http_response);
}
void MonitorRequest(const net::test_server::HttpRequest& request) {
const auto it = request.headers.find(std::string(network::kAcceptHeader));
if (it == request.headers.end())
return;
url_accept_header_map_[request.base_url.Resolve(request.relative_url)] =
it->second;
}
base::test::ScopedFeatureList feature_list_;
base::test::ScopedFeatureList feature_list_for_accept_header_;
std::map<GURL, std::string> url_accept_header_map_;
};
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, Simple) {
const GURL test_url = https_server_.GetURL("/sxg/test.html");
NavigateAndWaitForTitle(test_url, test_url.spec());
CheckNavigationAcceptHeader({test_url});
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, Redirect) {
const GURL test_url = https_server_.GetURL("/sxg/test.html");
const GURL redirect_url = https_server_.GetURL("/r?" + test_url.spec());
const GURL redirect_redirect_url =
https_server_.GetURL("/r?" + redirect_url.spec());
NavigateAndWaitForTitle(redirect_redirect_url, test_url.spec());
CheckNavigationAcceptHeader({redirect_redirect_url, redirect_url, test_url});
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest,
FallbackRedirect) {
if (!IsSignedExchangeEnabled())
return;
const GURL fallback_url = https_server_.GetURL("/sxg/test.html");
const GURL test_url =
https_server_.GetURL("/fallback_sxg?" + fallback_url.spec());
NavigateAndWaitForTitle(test_url, fallback_url.spec());
CheckNavigationAcceptHeader({test_url});
CheckFallbackAcceptHeader({fallback_url});
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest,
PrefetchEnabledPageEnabledTarget) {
const GURL target = https_server_.GetURL("/sxg/hello.txt");
const GURL page_url =
https_server_.GetURL(std::string("/sxg/prefetch.html#") + target.spec());
NavigateAndWaitForTitle(page_url, "OK");
CheckPrefetchAcceptHeader({target});
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest,
PrefetchRedirect) {
const GURL target = https_server_.GetURL("/sxg/hello.txt");
const GURL redirect_url = https_server_.GetURL("/r?" + target.spec());
const GURL redirect_redirect_url =
https_server_.GetURL("/r?" + redirect_url.spec());
const GURL page_url = https_server_.GetURL(
std::string("/sxg/prefetch.html#") + redirect_redirect_url.spec());
NavigateAndWaitForTitle(page_url, "OK");
CheckPrefetchAcceptHeader({redirect_redirect_url, redirect_url, target});
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest, ServiceWorker) {
NavigateAndWaitForTitle(https_server_.GetURL("/sxg/service-worker.html"),
"Done");
const std::string frame_accept = std::string(network::kFrameAcceptHeader);
const std::string frame_accept_with_sxg =
frame_accept + std::string(kAcceptHeaderSignedExchangeSuffix);
const std::vector<std::string> scopes = {"/sxg/sw-scope-generated/",
"/sxg/sw-scope-navigation-preload/",
"/sxg/sw-scope-no-respond-with/"};
for (const auto& scope : scopes) {
SCOPED_TRACE(scope);
const bool is_generated_scope =
scope == std::string("/sxg/sw-scope-generated/");
const GURL target_url = https_server_.GetURL(scope + "test.html");
const GURL redirect_target_url =
https_server_.GetURL("/r?" + target_url.spec());
const GURL redirect_redirect_target_url =
https_server_.GetURL("/r?" + redirect_target_url.spec());
const std::string expected_title =
is_generated_scope
? (IsSignedExchangeEnabled() ? frame_accept_with_sxg : frame_accept)
: "Done";
const base::Optional<std::string> expected_target_accept_header =
is_generated_scope
? base::nullopt
: base::Optional<std::string>(IsSignedExchangeEnabled()
? frame_accept_with_sxg
: frame_accept);
NavigateAndWaitForTitle(target_url, expected_title);
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
NavigateAndWaitForTitle(redirect_target_url, expected_title);
CheckNavigationAcceptHeader({redirect_target_url});
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
NavigateAndWaitForTitle(redirect_redirect_target_url, expected_title);
CheckNavigationAcceptHeader(
{redirect_redirect_target_url, redirect_target_url});
EXPECT_EQ(expected_target_accept_header,
GetInterceptedAcceptHeader(target_url));
ClearInterceptedAcceptHeaders();
}
}
IN_PROC_BROWSER_TEST_P(SignedExchangeAcceptHeaderBrowserTest,
ServiceWorkerPrefetch) {
NavigateAndWaitForTitle(
https_server_.GetURL("/sxg/service-worker-prefetch.html"), "Done");
const std::string scope = "/sxg/sw-prefetch-scope/";
const GURL target_url = https_server_.GetURL(scope + "test.html");
const GURL prefetch_target =
https_server_.GetURL(std::string("/sxg/hello.txt"));
const std::string load_prefetch_script = base::StringPrintf(
"(function loadPrefetch(urls) {"
" for (let url of urls) {"
" let link = document.createElement('link');"
" link.rel = 'prefetch';"
" link.href = url;"
" document.body.appendChild(link);"
" }"
" function check() {"
" const entries = performance.getEntriesByType('resource');"
" const url_set = new Set(urls);"
" for (let entry of entries) {"
" url_set.delete(entry.name);"
" }"
" if (!url_set.size) {"
" window.domAutomationController.send(true);"
" } else {"
" setTimeout(check, 100);"
" }"
" }"
" check();"
"})(['%s'])",
prefetch_target.spec().c_str());
bool unused = false;
NavigateAndWaitForTitle(target_url, "Done");
EXPECT_TRUE(ExecuteScriptAndExtractBool(shell()->web_contents(),
load_prefetch_script, &unused));
CheckPrefetchAcceptHeader({prefetch_target});
ClearInterceptedAcceptHeaders();
}
INSTANTIATE_TEST_SUITE_P(SignedExchangeAcceptHeaderBrowserTest,
SignedExchangeAcceptHeaderBrowserTest,
testing::Bool());
} // namespace content