| // Copyright 2018 The Chromium Authors | |
| // Use of this source code is governed by a BSD-style license that can be | |
| // found in the LICENSE file. | |
| #include <stddef.h> | |
| #include <cstdint> | |
| #include <vector> | |
| #include "base/containers/span.h" | |
| #include "base/functional/bind.h" | |
| #include "base/memory/raw_ptr.h" | |
| #include "base/memory/scoped_refptr.h" | |
| #include "base/strings/strcat.h" | |
| #include "base/strings/string_number_conversions.h" | |
| #include "base/synchronization/lock.h" | |
| #include "base/test/run_until.h" | |
| #include "base/test/scoped_feature_list.h" | |
| #include "base/test/test_future.h" | |
| #include "base/thread_annotations.h" | |
| #include "net/base/features.h" | |
| #include "net/base/host_port_pair.h" | |
| #include "net/base/ip_address.h" | |
| #include "net/base/ip_endpoint.h" | |
| #include "net/base/isolation_info.h" | |
| #include "net/base/network_change_notifier.h" | |
| #include "net/base/privacy_mode.h" | |
| #include "net/base/proxy_server.h" | |
| #include "net/cert/mock_cert_verifier.h" | |
| #include "net/cookies/site_for_cookies.h" | |
| #include "net/dns/context_host_resolver.h" | |
| #include "net/dns/dns_client.h" | |
| #include "net/dns/dns_config.h" | |
| #include "net/dns/dns_query.h" | |
| #include "net/dns/dns_test_util.h" | |
| #include "net/dns/dns_transaction.h" | |
| #include "net/dns/host_resolver.h" | |
| #include "net/dns/host_resolver_manager.h" | |
| #include "net/dns/host_resolver_proc.h" | |
| #include "net/dns/public/dns_config_overrides.h" | |
| #include "net/dns/public/dns_over_https_config.h" | |
| #include "net/dns/public/secure_dns_mode.h" | |
| #include "net/dns/public/secure_dns_policy.h" | |
| #include "net/dns/public/util.h" | |
| #include "net/http/http_stream_factory_test_util.h" | |
| #include "net/http/http_stream_pool.h" | |
| #include "net/http/http_stream_pool_group.h" | |
| #include "net/http/http_stream_pool_test_util.h" | |
| #include "net/log/net_log.h" | |
| #include "net/reporting/reporting_cache.h" | |
| #include "net/reporting/reporting_cache_observer.h" | |
| #include "net/reporting/reporting_context.h" | |
| #include "net/reporting/reporting_policy.h" | |
| #include "net/reporting/reporting_service.h" | |
| #include "net/reporting/reporting_target_type.h" | |
| #include "net/socket/transport_client_socket_pool.h" | |
| #include "net/ssl/ssl_config_service.h" | |
| #include "net/ssl/test_ssl_config_service.h" | |
| #include "net/ssl/test_static_ech_mode_getter.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/gtest_util.h" | |
| #include "net/test/ssl_test_util.h" | |
| #include "net/test/test_doh_server.h" | |
| #include "net/test/test_with_task_environment.h" | |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" | |
| #include "net/url_request/url_request.h" | |
| #include "net/url_request/url_request_context.h" | |
| #include "net/url_request/url_request_context_builder.h" | |
| #include "net/url_request/url_request_test_util.h" | |
| #include "testing/gtest/include/gtest/gtest.h" | |
| #include "testing/platform_test.h" | |
| #include "third_party/boringssl/src/include/openssl/ssl.h" | |
| #include "url/gurl.h" | |
| #include "url/origin.h" | |
| #include "url/scheme_host_port.h" | |
| #include "url/url_constants.h" | |
| namespace net { | |
| namespace { | |
| using net::test::IsError; | |
| using net::test::IsOk; | |
| const char kDohHostname[] = "doh-server.example"; | |
| const char kHostname[] = "bar.example.com"; | |
| const char kTestBody[] = "<html><body>TEST RESPONSE</body></html>"; | |
| class TestHostResolverProc : public HostResolverProc { | |
| public: | |
| TestHostResolverProc() : HostResolverProc(nullptr) {} | |
| int Resolve(const std::string& hostname, | |
| AddressFamily address_family, | |
| HostResolverFlags host_resolver_flags, | |
| AddressList* addrlist, | |
| int* os_error) override { | |
| insecure_queries_served_++; | |
| *addrlist = AddressList::CreateFromIPAddress(IPAddress(127, 0, 0, 1), 0); | |
| return OK; | |
| } | |
| uint32_t insecure_queries_served() { return insecure_queries_served_; } | |
| private: | |
| ~TestHostResolverProc() override = default; | |
| uint32_t insecure_queries_served_ = 0; | |
| }; | |
| // Runs and waits for the DoH probe to complete in automatic mode. The resolver | |
| // must have a single DoH server, and the DoH server must serve addresses for | |
| // `kDohProbeHostname`. | |
| class DohProber : public NetworkChangeNotifier::DNSObserver { | |
| public: | |
| explicit DohProber(ContextHostResolver* resolver) : resolver_(resolver) {} | |
| void ProbeAndWaitForCompletion() { | |
| std::unique_ptr<HostResolver::ProbeRequest> probe_request = | |
| resolver_->CreateDohProbeRequest(); | |
| EXPECT_THAT(probe_request->Start(), IsError(ERR_IO_PENDING)); | |
| if (NumAvailableDohServers() == 0) { | |
| NetworkChangeNotifier::AddDNSObserver(this); | |
| loop_.Run(); | |
| NetworkChangeNotifier::RemoveDNSObserver(this); | |
| } | |
| EXPECT_GT(NumAvailableDohServers(), 0u); | |
| } | |
| void OnDNSChanged() override { | |
| if (NumAvailableDohServers() > 0) { | |
| loop_.Quit(); | |
| } | |
| } | |
| private: | |
| size_t NumAvailableDohServers() { | |
| ResolveContext* context = resolver_->resolve_context_for_testing(); | |
| return context->NumAvailableDohServers( | |
| context->current_session_for_testing()); | |
| } | |
| raw_ptr<ContextHostResolver> resolver_; | |
| base::RunLoop loop_; | |
| }; | |
| // A test fixture that creates a DoH server with a `URLRequestContext` | |
| // configured to use it. | |
| class DnsOverHttpsIntegrationTest : public TestWithTaskEnvironment { | |
| public: | |
| DnsOverHttpsIntegrationTest() | |
| : DnsOverHttpsIntegrationTest(/*start_server=*/true) {} | |
| URLRequestContext* context() { return request_context_.get(); } | |
| void ResetContext(SecureDnsMode mode = SecureDnsMode::kSecure, | |
| bool require_network_anonymization_key = false, | |
| bool use_post_only_template = true, | |
| bool enable_reporting = false) { | |
| // TODO(crbug.com/40198637): Simplify this. | |
| HostResolver::ManagerOptions manager_options; | |
| // Without a DnsConfig, HostResolverManager will not use DoH, even in | |
| // kSecure mode. See https://crbug.com/1251715. However, | |
| // DnsClient::BuildEffectiveConfig special-cases overrides that override | |
| // everything, so that gets around it. Ideally, we would instead mock out a | |
| // system DnsConfig via the usual pathway. | |
| manager_options.dns_config_overrides = | |
| DnsConfigOverrides::CreateOverridingEverythingWithDefaults(); | |
| manager_options.dns_config_overrides.secure_dns_mode = mode; | |
| manager_options.dns_config_overrides.dns_over_https_config = | |
| *DnsOverHttpsConfig::FromString(use_post_only_template | |
| ? doh_server_.GetPostOnlyTemplate() | |
| : doh_server_.GetTemplate()); | |
| manager_options.dns_config_overrides.use_local_ipv6 = true; | |
| auto resolver = HostResolver::CreateStandaloneContextResolver( | |
| NetLog::Get(), manager_options); | |
| // Configure `resolver_` to use `host_resolver_proc_` to resolve | |
| // `doh_server_` itself. Additionally, without an explicit HostResolverProc, | |
| // HostResolverManager::HaveTestProcOverride disables the built-in DNS | |
| // client. | |
| auto* resolver_raw = resolver.get(); | |
| resolver->SetHostResolverSystemParamsForTest( | |
| HostResolverSystemTask::Params(host_resolver_proc_, 1)); | |
| auto context_builder = CreateTestURLRequestContextBuilder(); | |
| context_builder->set_require_network_anonymization_key( | |
| require_network_anonymization_key); | |
| context_builder->set_host_resolver(std::move(resolver)); | |
| #if BUILDFLAG(ENABLE_REPORTING) | |
| if (enable_reporting) { | |
| context_builder->set_network_error_logging_enabled(true); | |
| auto cert_verifier = std::make_unique<MockCertVerifier>(); | |
| cert_verifier->set_default_result(OK); | |
| context_builder->SetCertVerifier(std::move(cert_verifier)); | |
| auto reporting_policy = std::make_unique<ReportingPolicy>(); | |
| reporting_policy->delivery_interval = base::Milliseconds(1); | |
| context_builder->set_reporting_policy(std::move(reporting_policy)); | |
| } | |
| #else | |
| CHECK(!enable_reporting); | |
| #endif | |
| auto ssl_config_service = | |
| std::make_unique<TestSSLConfigService>(SSLContextConfig()); | |
| ssl_config_service_ = ssl_config_service.get(); | |
| context_builder->set_ssl_config_service(std::move(ssl_config_service)); | |
| request_context_ = context_builder->Build(); | |
| if (mode == SecureDnsMode::kAutomatic) { | |
| DohProber prober(resolver_raw); | |
| prober.ProbeAndWaitForCompletion(); | |
| } | |
| } | |
| void AddHostWithEch(const url::SchemeHostPort& host, | |
| const IPAddress& address, | |
| base::span<const uint8_t> ech_config_list) { | |
| doh_server_.AddAddressRecord(host.host(), address); | |
| doh_server_.AddRecord(BuildTestHttpsServiceRecord( | |
| dns_util::GetNameForHttpsQuery(host), | |
| /*priority=*/1, /*service_name=*/host.host(), | |
| {BuildTestHttpsServiceEchConfigParam(ech_config_list)})); | |
| } | |
| protected: | |
| explicit DnsOverHttpsIntegrationTest(bool start_server) | |
| : host_resolver_proc_(base::MakeRefCounted<TestHostResolverProc>()) { | |
| if (start_server) { | |
| doh_server_.SetHostname(kDohHostname); | |
| EXPECT_TRUE(doh_server_.Start()); | |
| // In `kAutomatic` mode, DoH support depends on a probe for | |
| // `kDohProbeHostname`. | |
| doh_server_.AddAddressRecord(kDohProbeHostname, | |
| IPAddress::IPv4Localhost()); | |
| ResetContext(); | |
| } | |
| } | |
| TestDohServer doh_server_; | |
| scoped_refptr<net::TestHostResolverProc> host_resolver_proc_; | |
| std::unique_ptr<URLRequestContext> request_context_; | |
| raw_ptr<TestSSLConfigService> ssl_config_service_; | |
| }; | |
| // A convenience wrapper over `DnsOverHttpsIntegrationTest` that also starts an | |
| // HTTPS server. | |
| class HttpsWithDnsOverHttpsTest : public DnsOverHttpsIntegrationTest { | |
| public: | |
| HttpsWithDnsOverHttpsTest() { | |
| EmbeddedTestServer::ServerCertificateConfig cert_config; | |
| cert_config.dns_names = {kHostname}; | |
| https_server_.SetSSLConfig(cert_config); | |
| https_server_.RegisterRequestHandler( | |
| base::BindRepeating(&HttpsWithDnsOverHttpsTest::HandleDefaultRequest, | |
| base::Unretained(this))); | |
| EXPECT_TRUE(https_server_.Start()); | |
| doh_server_.AddAddressRecord(kHostname, IPAddress(127, 0, 0, 1)); | |
| } | |
| std::unique_ptr<test_server::HttpResponse> HandleDefaultRequest( | |
| const test_server::HttpRequest& request) { | |
| auto http_response = std::make_unique<test_server::BasicHttpResponse>(); | |
| test_https_requests_served_++; | |
| http_response->set_content(kTestBody); | |
| http_response->set_content_type("text/html"); | |
| return std::move(http_response); | |
| } | |
| protected: | |
| EmbeddedTestServer https_server_{EmbeddedTestServer::Type::TYPE_HTTPS}; | |
| uint32_t test_https_requests_served_ = 0; | |
| }; | |
| class TestHttpDelegate : public HttpStreamRequest::Delegate { | |
| public: | |
| explicit TestHttpDelegate(HttpNetworkSession* session) : session_(session) {} | |
| ~TestHttpDelegate() override = default; | |
| void WaitForCompletion(std::unique_ptr<HttpStreamRequest> request) { | |
| request_ = std::move(request); | |
| loop_.Run(); | |
| } | |
| void OnStreamReady(const ProxyInfo& used_proxy_info, | |
| std::unique_ptr<HttpStream> stream) override { | |
| stream->Close(false); | |
| loop_.Quit(); | |
| } | |
| void OnWebSocketHandshakeStreamReady( | |
| const ProxyInfo& used_proxy_info, | |
| std::unique_ptr<WebSocketHandshakeStreamBase> stream) override {} | |
| void OnBidirectionalStreamImplReady( | |
| const ProxyInfo& used_proxy_info, | |
| std::unique_ptr<BidirectionalStreamImpl> stream) override {} | |
| void OnStreamFailed(int status, | |
| const NetErrorDetails& net_error_details, | |
| const ProxyInfo& used_proxy_info, | |
| ResolveErrorInfo resolve_eror_info) override {} | |
| void OnCertificateError(int status, const SSLInfo& ssl_info) override {} | |
| void OnNeedsProxyAuth(const HttpResponseInfo& proxy_response, | |
| const ProxyInfo& used_proxy_info, | |
| HttpAuthController* auth_controller) override {} | |
| void OnNeedsClientAuth(SSLCertRequestInfo* cert_info) override {} | |
| void OnQuicBroken() override {} | |
| private: | |
| raw_ptr<HttpNetworkSession> session_; | |
| base::RunLoop loop_; | |
| std::unique_ptr<HttpStreamRequest> request_; | |
| }; | |
| // This test sets up a request which will reenter the connection pools by | |
| // triggering a DNS over HTTPS request. It also sets up an idle socket | |
| // which was a precondition for the crash we saw in https://crbug.com/830917. | |
| TEST_F(HttpsWithDnsOverHttpsTest, EndToEnd) { | |
| // Create and start http server. | |
| EmbeddedTestServer http_server(EmbeddedTestServer::Type::TYPE_HTTP); | |
| http_server.RegisterRequestHandler( | |
| base::BindRepeating(&HttpsWithDnsOverHttpsTest::HandleDefaultRequest, | |
| base::Unretained(this))); | |
| EXPECT_TRUE(http_server.Start()); | |
| // Set up an idle socket. | |
| HttpTransactionFactory* transaction_factory = | |
| request_context_->http_transaction_factory(); | |
| HttpStreamFactory::JobFactory default_job_factory; | |
| HttpNetworkSession* network_session = transaction_factory->GetSession(); | |
| TestHttpDelegate request_delegate(network_session); | |
| HttpStreamFactory* factory = network_session->http_stream_factory(); | |
| HttpRequestInfo request_info; | |
| request_info.method = "GET"; | |
| request_info.url = http_server.GetURL("localhost", "/preconnect"); | |
| std::unique_ptr<HttpStreamRequest> request(factory->RequestStream( | |
| request_info, DEFAULT_PRIORITY, /*allowed_bad_certs=*/{}, | |
| &request_delegate, false, false, NetLogWithSource())); | |
| request_delegate.WaitForCompletion(std::move(request)); | |
| size_t idle_socket_count = 0; | |
| ClientSocketPool::GroupId group_id( | |
| url::SchemeHostPort(request_info.url), PrivacyMode::PRIVACY_MODE_DISABLED, | |
| NetworkAnonymizationKey(), SecureDnsPolicy::kAllow, | |
| /*disable_cert_network_fetches=*/false, handles::kInvalidNetworkHandle); | |
| if (base::FeatureList::IsEnabled(features::kHappyEyeballsV3)) { | |
| idle_socket_count = | |
| network_session->http_stream_pool() | |
| ->GetOrCreateGroupForTesting(GroupIdToHttpStreamKey(group_id)) | |
| .IdleStreamSocketCount(); | |
| } else { | |
| idle_socket_count = | |
| network_session | |
| ->GetSocketPool(HttpNetworkSession::SocketPoolType::kNormal, | |
| ProxyChain::Direct()) | |
| ->IdleSocketCountInGroup(group_id); | |
| } | |
| EXPECT_EQ(idle_socket_count, 1u); | |
| // The domain "localhost" is resolved locally, so no DNS lookups should have | |
| // occurred. | |
| EXPECT_EQ(doh_server_.QueriesServed(), 0); | |
| EXPECT_EQ(host_resolver_proc_->insecure_queries_served(), 0u); | |
| // A stream was established, but no HTTPS request has been made yet. | |
| EXPECT_EQ(test_https_requests_served_, 0u); | |
| // Make a request that will trigger a DoH query as well. | |
| TestDelegate d; | |
| GURL main_url = https_server_.GetURL(kHostname, "/test"); | |
| std::unique_ptr<URLRequest> req(context()->CreateRequest( | |
| main_url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle)); | |
| req->Start(); | |
| d.RunUntilComplete(); | |
| EXPECT_TRUE(https_server_.ShutdownAndWaitUntilComplete()); | |
| EXPECT_TRUE(http_server.ShutdownAndWaitUntilComplete()); | |
| EXPECT_TRUE(doh_server_.ShutdownAndWaitUntilComplete()); | |
| // There should be three DoH lookups for kHostname (A, AAAA, and HTTPS). | |
| EXPECT_EQ(doh_server_.QueriesServed(), 3); | |
| // The requests to the DoH server are pooled, so there should only be one | |
| // insecure lookup for the DoH server hostname. | |
| EXPECT_EQ(host_resolver_proc_->insecure_queries_served(), 1u); | |
| // There should be one non-DoH HTTPS request for the connection to kHostname. | |
| EXPECT_EQ(test_https_requests_served_, 1u); | |
| EXPECT_TRUE(d.response_completed()); | |
| EXPECT_EQ(d.request_status(), 0); | |
| EXPECT_EQ(d.data_received(), kTestBody); | |
| } | |
| TEST_F(HttpsWithDnsOverHttpsTest, EndToEndWithIsolationInfo) { | |
| // Reset context with require_network_anonymization_key = true. | |
| ResetContext(SecureDnsMode::kSecure, | |
| /*require_network_anonymization_key=*/true); | |
| // Make a request with a valid IsolationInfo. | |
| TestDelegate d; | |
| GURL main_url = https_server_.GetURL(kHostname, "/test"); | |
| std::unique_ptr<URLRequest> req(context()->CreateRequest( | |
| main_url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle)); | |
| url::Origin origin = url::Origin::Create(main_url); | |
| req->set_isolation_info( | |
| IsolationInfo::Create(IsolationInfo::RequestType::kMainFrame, origin, | |
| origin, SiteForCookies::FromOrigin(origin))); | |
| req->Start(); | |
| d.RunUntilComplete(); | |
| EXPECT_TRUE(https_server_.ShutdownAndWaitUntilComplete()); | |
| EXPECT_TRUE(doh_server_.ShutdownAndWaitUntilComplete()); | |
| // There should be three DoH lookups for kHostname (A, AAAA, and HTTPS). | |
| EXPECT_EQ(doh_server_.QueriesServed(), 3); | |
| // We are also implicitly checking that the DoH requests bypassed the | |
| // IsolationInfo-is-not-empty check in `URLRequest::Start` that occurs when | |
| // `require_network_anonymization_key` is set since DoH requests use an empty | |
| // IsolationInfo with a custom partition (which we exempt from the check). | |
| } | |
| TEST_F(HttpsWithDnsOverHttpsTest, EndToEndFail) { | |
| // Fail all DoH requests. | |
| doh_server_.SetFailRequests(true); | |
| // Make a request that will trigger a DoH query. | |
| TestDelegate d; | |
| GURL main_url = https_server_.GetURL(kHostname, "/test"); | |
| std::unique_ptr<URLRequest> req(context()->CreateRequest( | |
| main_url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle)); | |
| req->Start(); | |
| d.RunUntilComplete(); | |
| EXPECT_TRUE(https_server_.ShutdownAndWaitUntilComplete()); | |
| EXPECT_TRUE(doh_server_.ShutdownAndWaitUntilComplete()); | |
| // No HTTPS connection to the test server will be attempted due to the | |
| // host resolution error. | |
| EXPECT_EQ(test_https_requests_served_, 0u); | |
| EXPECT_TRUE(d.response_completed()); | |
| EXPECT_EQ(d.request_status(), net::ERR_NAME_NOT_RESOLVED); | |
| const auto& resolve_error_info = req->response_info().resolve_error_info; | |
| EXPECT_TRUE(resolve_error_info.is_secure_network_error); | |
| EXPECT_EQ(resolve_error_info.error, net::ERR_DNS_MALFORMED_RESPONSE); | |
| } | |
| // An end-to-end test of the HTTPS upgrade behavior. | |
| TEST_F(HttpsWithDnsOverHttpsTest, HttpsUpgrade) { | |
| AddScopedFeatureList().InitAndEnableFeatureWithParameters( | |
| features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}); | |
| ResetContext(); | |
| GURL https_url = https_server_.GetURL(kHostname, "/test"); | |
| EXPECT_TRUE(https_url.SchemeIs(url::kHttpsScheme)); | |
| GURL::Replacements replacements; | |
| replacements.SetSchemeStr(url::kHttpScheme); | |
| GURL http_url = https_url.ReplaceComponents(replacements); | |
| // `service_name` is `kHostname` rather than "." because "." specifies the | |
| // query name. For non-defaults ports, the query name uses port prefix naming | |
| // and does not match the A/AAAA records. | |
| doh_server_.AddRecord(BuildTestHttpsServiceRecord( | |
| dns_util::GetNameForHttpsQuery(url::SchemeHostPort(https_url)), | |
| /*priority=*/1, /*service_name=*/kHostname, /*params=*/{})); | |
| for (auto mode : {SecureDnsMode::kSecure, SecureDnsMode::kAutomatic}) { | |
| SCOPED_TRACE(kSecureDnsModes.at(mode)); | |
| ResetContext(mode); | |
| // Fetch the http URL. | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> req(context()->CreateRequest( | |
| http_url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle)); | |
| req->Start(); | |
| d.RunUntilComplete(); | |
| ASSERT_THAT(d.request_status(), IsOk()); | |
| // The request should have been redirected to https. | |
| EXPECT_EQ(d.received_redirect_count(), 1); | |
| EXPECT_EQ(req->url(), https_url); | |
| EXPECT_TRUE(d.response_completed()); | |
| EXPECT_EQ(d.request_status(), 0); | |
| EXPECT_EQ(d.data_received(), kTestBody); | |
| } | |
| } | |
| // An end-to-end test for requesting a domain with a basic HTTPS record. Expect | |
| // this to exercise connection logic for extra HostResolver results with | |
| // metadata. | |
| TEST_F(HttpsWithDnsOverHttpsTest, HttpsMetadata) { | |
| AddScopedFeatureList().InitAndEnableFeatureWithParameters( | |
| features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}); | |
| ResetContext(); | |
| GURL main_url = https_server_.GetURL(kHostname, "/test"); | |
| EXPECT_TRUE(main_url.SchemeIs(url::kHttpsScheme)); | |
| doh_server_.AddRecord(BuildTestHttpsServiceRecord( | |
| dns_util::GetNameForHttpsQuery(url::SchemeHostPort(main_url)), | |
| /*priority=*/1, /*service_name=*/kHostname, /*params=*/{})); | |
| // Fetch the http URL. | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> req(context()->CreateRequest( | |
| main_url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle)); | |
| req->Start(); | |
| d.RunUntilComplete(); | |
| ASSERT_THAT(d.request_status(), IsOk()); | |
| // There should be three DoH lookups for kHostname (A, AAAA, and HTTPS). | |
| EXPECT_EQ(doh_server_.QueriesServed(), 3); | |
| EXPECT_TRUE(d.response_completed()); | |
| EXPECT_EQ(d.request_status(), 0); | |
| EXPECT_EQ(d.data_received(), kTestBody); | |
| } | |
| TEST_F(DnsOverHttpsIntegrationTest, EncryptedClientHello) { | |
| AddScopedFeatureList().InitWithFeaturesAndParameters( | |
| /*enabled_features=*/{{features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}}}, | |
| /*disabled_features=*/{}); | |
| // Configure a test server that speaks ECH. | |
| static constexpr char kRealName[] = "secret.example"; | |
| static constexpr char kPublicName[] = "public.example"; | |
| EmbeddedTestServer::ServerCertificateConfig server_cert_config; | |
| server_cert_config.dns_names = {kRealName}; | |
| SSLServerConfig ssl_server_config; | |
| std::vector<uint8_t> ech_config_list; | |
| ssl_server_config.ech_keys = | |
| MakeTestEchKeys(kPublicName, /*max_name_len=*/128, &ech_config_list); | |
| ASSERT_TRUE(ssl_server_config.ech_keys); | |
| EmbeddedTestServer test_server(EmbeddedTestServer::TYPE_HTTPS); | |
| test_server.SetSSLConfig(server_cert_config, ssl_server_config); | |
| RegisterDefaultHandlers(&test_server); | |
| ASSERT_TRUE(test_server.Start()); | |
| AddressList addr; | |
| ASSERT_TRUE(test_server.GetAddressList(&addr)); | |
| GURL url = test_server.GetURL(kRealName, "/defaultresponse"); | |
| AddHostWithEch(url::SchemeHostPort(url), addr.front().address(), | |
| ech_config_list); | |
| for (bool ech_enabled : {true, false}) { | |
| SCOPED_TRACE(ech_enabled); | |
| // Create a new `URLRequestContext`, to ensure there are no cached | |
| // sockets, etc., from the previous loop iteration. | |
| ResetContext(); | |
| ssl_config_service_->SetEchModeGetter( | |
| std::make_unique<TestStaticEchModeGetter>( | |
| ech_enabled ? EchMode::kOpportunistic : EchMode::kDisabled)); | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), IsOk()); | |
| EXPECT_EQ(1, d.response_started_count()); | |
| EXPECT_FALSE(d.received_data_before_response()); | |
| EXPECT_NE(0, d.bytes_received()); | |
| EXPECT_EQ(ech_enabled, r->ssl_info().encrypted_client_hello); | |
| } | |
| } | |
| // Test that, if the DNS returns a stale ECHConfigList (or other key mismatch), | |
| // the client can recover and connect to the server, provided the server can | |
| // handshake as the public name. | |
| TEST_F(DnsOverHttpsIntegrationTest, EncryptedClientHelloStaleKey) { | |
| AddScopedFeatureList().InitWithFeaturesAndParameters( | |
| /*enabled_features=*/{{features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}}}, | |
| /*disabled_features=*/{}); | |
| ResetContext(); | |
| static constexpr char kRealNameStale[] = "secret1.example"; | |
| static constexpr char kRealNameWrongPublicName[] = "secret2.example"; | |
| static constexpr char kPublicName[] = "public.example"; | |
| static constexpr char kWrongPublicName[] = "wrong-public.example"; | |
| std::vector<uint8_t> ech_config_list, ech_config_list_stale, | |
| ech_config_list_wrong_public_name; | |
| bssl::UniquePtr<SSL_ECH_KEYS> ech_keys = | |
| MakeTestEchKeys(kPublicName, /*max_name_len=*/128, &ech_config_list); | |
| ASSERT_TRUE(ech_keys); | |
| ASSERT_TRUE(MakeTestEchKeys(kPublicName, /*max_name_len=*/128, | |
| &ech_config_list_stale)); | |
| ASSERT_TRUE(MakeTestEchKeys(kWrongPublicName, /*max_name_len=*/128, | |
| &ech_config_list_wrong_public_name)); | |
| // Configure an ECH-supporting server that can speak for all names except | |
| // `kWrongPublicName`. | |
| EmbeddedTestServer::ServerCertificateConfig server_cert_config; | |
| server_cert_config.dns_names = {kRealNameStale, kRealNameWrongPublicName, | |
| kPublicName}; | |
| SSLServerConfig ssl_server_config; | |
| ssl_server_config.ech_keys = std::move(ech_keys); | |
| EmbeddedTestServer test_server(EmbeddedTestServer::TYPE_HTTPS); | |
| test_server.SetSSLConfig(server_cert_config, ssl_server_config); | |
| RegisterDefaultHandlers(&test_server); | |
| ASSERT_TRUE(test_server.Start()); | |
| AddressList addr; | |
| ASSERT_TRUE(test_server.GetAddressList(&addr)); | |
| GURL url_stale = test_server.GetURL(kRealNameStale, "/defaultresponse"); | |
| GURL url_wrong_public_name = | |
| test_server.GetURL(kRealNameWrongPublicName, "/defaultresponse"); | |
| AddHostWithEch(url::SchemeHostPort(url_stale), addr.front().address(), | |
| ech_config_list_stale); | |
| AddHostWithEch(url::SchemeHostPort(url_wrong_public_name), | |
| addr.front().address(), ech_config_list_wrong_public_name); | |
| // Connecting to `url_stale` should succeed. Although the server will not | |
| // decrypt the ClientHello, it can handshake as `kPublicName` and provide new | |
| // keys for the client to use. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_stale, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), IsOk()); | |
| EXPECT_EQ(1, d.response_started_count()); | |
| EXPECT_FALSE(d.received_data_before_response()); | |
| EXPECT_NE(0, d.bytes_received()); | |
| EXPECT_TRUE(r->ssl_info().encrypted_client_hello); | |
| } | |
| // Connecting to `url_wrong_public_name` should fail. The server can neither | |
| // decrypt the ClientHello, nor handshake as `kWrongPublicName`. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_wrong_public_name, DEFAULT_PRIORITY, &d, | |
| TRAFFIC_ANNOTATION_FOR_TESTS, net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), | |
| IsError(ERR_ECH_FALLBACK_CERTIFICATE_INVALID)); | |
| } | |
| } | |
| TEST_F(DnsOverHttpsIntegrationTest, EncryptedClientHelloFallback) { | |
| AddScopedFeatureList().InitWithFeaturesAndParameters( | |
| /*enabled_features=*/{{features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}}}, | |
| /*disabled_features=*/{}); | |
| ResetContext(); | |
| static constexpr char kRealNameStale[] = "secret1.example"; | |
| static constexpr char kRealNameWrongPublicName[] = "secret2.example"; | |
| static constexpr char kPublicName[] = "public.example"; | |
| static constexpr char kWrongPublicName[] = "wrong-public.example"; | |
| std::vector<uint8_t> ech_config_list_stale, ech_config_list_wrong_public_name; | |
| ASSERT_TRUE(MakeTestEchKeys(kPublicName, /*max_name_len=*/128, | |
| &ech_config_list_stale)); | |
| ASSERT_TRUE(MakeTestEchKeys(kWrongPublicName, /*max_name_len=*/128, | |
| &ech_config_list_wrong_public_name)); | |
| // Configure a server, without ECH, that can speak for all names except | |
| // `kWrongPublicName`. | |
| EmbeddedTestServer::ServerCertificateConfig server_cert_config; | |
| server_cert_config.dns_names = {kRealNameStale, kRealNameWrongPublicName, | |
| kPublicName}; | |
| EmbeddedTestServer test_server(EmbeddedTestServer::TYPE_HTTPS); | |
| test_server.SetSSLConfig(server_cert_config); | |
| RegisterDefaultHandlers(&test_server); | |
| ASSERT_TRUE(test_server.Start()); | |
| AddressList addr; | |
| ASSERT_TRUE(test_server.GetAddressList(&addr)); | |
| GURL url_stale = test_server.GetURL(kRealNameStale, "/defaultresponse"); | |
| GURL url_wrong_public_name = | |
| test_server.GetURL(kRealNameWrongPublicName, "/defaultresponse"); | |
| AddHostWithEch(url::SchemeHostPort(url_stale), addr.front().address(), | |
| ech_config_list_stale); | |
| AddHostWithEch(url::SchemeHostPort(url_wrong_public_name), | |
| addr.front().address(), ech_config_list_wrong_public_name); | |
| // Connecting to `url_stale` should succeed. Although the server will not | |
| // decrypt the ClientHello, it can handshake as `kPublicName` and trigger an | |
| // authenticated fallback. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_stale, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), IsOk()); | |
| EXPECT_EQ(1, d.response_started_count()); | |
| EXPECT_FALSE(d.received_data_before_response()); | |
| EXPECT_NE(0, d.bytes_received()); | |
| EXPECT_FALSE(r->ssl_info().encrypted_client_hello); | |
| } | |
| // Connecting to `url_wrong_public_name` should fail. The server can neither | |
| // decrypt the ClientHello, nor handshake as `kWrongPublicName`. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_wrong_public_name, DEFAULT_PRIORITY, &d, | |
| TRAFFIC_ANNOTATION_FOR_TESTS, net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), | |
| IsError(ERR_ECH_FALLBACK_CERTIFICATE_INVALID)); | |
| } | |
| } | |
| TEST_F(DnsOverHttpsIntegrationTest, EncryptedClientHelloFallbackTLS12) { | |
| AddScopedFeatureList().InitWithFeaturesAndParameters( | |
| /*enabled_features=*/{{features::kUseDnsHttpsSvcb, | |
| {// Disable timeouts. | |
| {"UseDnsHttpsSvcbSecureExtraTimeMax", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimePercent", "0"}, | |
| {"UseDnsHttpsSvcbSecureExtraTimeMin", "0"}}}}, | |
| /*disabled_features=*/{}); | |
| ResetContext(); | |
| static constexpr char kRealNameStale[] = "secret1.example"; | |
| static constexpr char kRealNameWrongPublicName[] = "secret2.example"; | |
| static constexpr char kPublicName[] = "public.example"; | |
| static constexpr char kWrongPublicName[] = "wrong-public.example"; | |
| std::vector<uint8_t> ech_config_list_stale, ech_config_list_wrong_public_name; | |
| ASSERT_TRUE(MakeTestEchKeys(kPublicName, /*max_name_len=*/128, | |
| &ech_config_list_stale)); | |
| ASSERT_TRUE(MakeTestEchKeys(kWrongPublicName, /*max_name_len=*/128, | |
| &ech_config_list_wrong_public_name)); | |
| // Configure a server, without ECH or TLS 1.3, that can speak for all names | |
| // except `kWrongPublicName`. | |
| EmbeddedTestServer::ServerCertificateConfig server_cert_config; | |
| server_cert_config.dns_names = {kRealNameStale, kRealNameWrongPublicName, | |
| kPublicName}; | |
| SSLServerConfig ssl_server_config; | |
| ssl_server_config.version_max = SSL_PROTOCOL_VERSION_TLS1_2; | |
| EmbeddedTestServer test_server(EmbeddedTestServer::TYPE_HTTPS); | |
| test_server.SetSSLConfig(server_cert_config, ssl_server_config); | |
| RegisterDefaultHandlers(&test_server); | |
| ASSERT_TRUE(test_server.Start()); | |
| AddressList addr; | |
| ASSERT_TRUE(test_server.GetAddressList(&addr)); | |
| GURL url_stale = test_server.GetURL(kRealNameStale, "/defaultresponse"); | |
| GURL url_wrong_public_name = | |
| test_server.GetURL(kRealNameWrongPublicName, "/defaultresponse"); | |
| AddHostWithEch(url::SchemeHostPort(url_stale), addr.front().address(), | |
| ech_config_list_stale); | |
| AddHostWithEch(url::SchemeHostPort(url_wrong_public_name), | |
| addr.front().address(), ech_config_list_wrong_public_name); | |
| // Connecting to `url_stale` should succeed. Although the server will not | |
| // decrypt the ClientHello, it can handshake as `kPublicName` and trigger an | |
| // authenticated fallback. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_stale, DEFAULT_PRIORITY, &d, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), IsOk()); | |
| EXPECT_EQ(1, d.response_started_count()); | |
| EXPECT_FALSE(d.received_data_before_response()); | |
| EXPECT_NE(0, d.bytes_received()); | |
| EXPECT_FALSE(r->ssl_info().encrypted_client_hello); | |
| } | |
| // Connecting to `url_wrong_public_name` should fail. The server can neither | |
| // decrypt the ClientHello, nor handshake as `kWrongPublicName`. | |
| { | |
| TestDelegate d; | |
| std::unique_ptr<URLRequest> r = context()->CreateRequest( | |
| url_wrong_public_name, DEFAULT_PRIORITY, &d, | |
| TRAFFIC_ANNOTATION_FOR_TESTS, net::handles::kInvalidNetworkHandle); | |
| r->Start(); | |
| EXPECT_TRUE(r->is_pending()); | |
| d.RunUntilComplete(); | |
| EXPECT_THAT(d.request_status(), | |
| IsError(ERR_ECH_FALLBACK_CERTIFICATE_INVALID)); | |
| } | |
| } | |
| class DnsOverHttpsReportingTest : public DnsOverHttpsIntegrationTest { | |
| public: | |
| DnsOverHttpsReportingTest() | |
| : DnsOverHttpsIntegrationTest(/*start_server=*/false) { | |
| AddScopedFeatureList().InitWithFeatures( | |
| {features::kPartitionConnectionsByNetworkIsolationKey}, | |
| // Disable HTTPS record lookups to simplify what requests we expect. | |
| {features::kUseDnsHttpsSvcb}); | |
| EmbeddedTestServer::ServerCertificateConfig cert_config; | |
| cert_config.dns_names = {kHostname}; | |
| cert_config.ip_addresses = {IPAddress::IPv4Localhost()}; | |
| https_server_.SetSSLConfig(cert_config); | |
| https_server_.RegisterRequestHandler( | |
| base::BindRepeating(&DnsOverHttpsReportingTest::HandleReportingRequest, | |
| base::Unretained(this))); | |
| https_server_.RegisterRequestHandler( | |
| base::BindRepeating(&DnsOverHttpsReportingTest::HandleDefaultRequest)); | |
| EXPECT_TRUE(https_server_.Start()); | |
| doh_server_.SetHostname(kDohHostname); | |
| doh_server_.AddAddressRecord(kHostname, IPAddress::IPv4Localhost()); | |
| // Use the HTTPS server IP address as the report destination so we don't | |
| // have to worry about DNS resolution while the test DoH server is | |
| // potentially configured to respond with errors instead of addresses. | |
| std::string report_to = | |
| "{\"group\":\"group\",\"max_age\":86400,\"endpoints\":[{\"url\":\"" + | |
| https_server_.GetURL("/upload").spec() + "\"}]}"; | |
| doh_server_.AddCustomResponseHeader("Report-To", report_to); | |
| doh_server_.AddCustomResponseHeader( | |
| "NEL", | |
| "{\"report_to\":\"group\",\"max_age\":86400, " | |
| "\"success_fraction\":0.0, " | |
| "\"failure_fraction\":1.0}"); | |
| EXPECT_TRUE(doh_server_.Start()); | |
| ResetContext(SecureDnsMode::kSecure, | |
| /*require_network_anonymization_key=*/false, | |
| /*use_post_only_template=*/false, | |
| /*enable_reporting=*/true); | |
| } | |
| ~DnsOverHttpsReportingTest() override { | |
| EXPECT_TRUE(https_server_.ShutdownAndWaitUntilComplete()); | |
| } | |
| std::unique_ptr<test_server::HttpResponse> HandleReportingRequest( | |
| const test_server::HttpRequest& request) { | |
| // Handle the preflight that occurs to check whether sending reports is | |
| // allowed. | |
| if (request.method == test_server::METHOD_OPTIONS) { | |
| auto http_response = std::make_unique<test_server::BasicHttpResponse>(); | |
| http_response->set_code(HTTP_OK); | |
| http_response->AddCustomHeader("Access-Control-Allow-Origin", "*"); | |
| http_response->AddCustomHeader("Access-Control-Allow-Headers", | |
| "content-type"); | |
| return http_response; | |
| } | |
| // Handle reports being sent. | |
| if (request.method == test_server::METHOD_POST && | |
| request.relative_url == "/upload") { | |
| auto http_response = std::make_unique<test_server::BasicHttpResponse>(); | |
| http_response->set_code(HTTP_OK); | |
| // Note that failing DoH queries can get retried and possibly cause | |
| // multiple reports to be generated, but for our purposes we only care | |
| // whether at least one report was uploaded. Thus, we won't make any | |
| // assumptions here about the previous value of `report_uploaded_`. | |
| set_report_uploaded(true); | |
| return http_response; | |
| } | |
| return nullptr; | |
| } | |
| static std::unique_ptr<test_server::HttpResponse> HandleDefaultRequest( | |
| const test_server::HttpRequest& request) { | |
| auto http_response = std::make_unique<test_server::BasicHttpResponse>(); | |
| http_response->set_code(HTTP_OK); | |
| http_response->set_content(kTestBody); | |
| http_response->set_content_type("text/html"); | |
| return http_response; | |
| } | |
| bool report_uploaded() { | |
| base::AutoLock lock(report_uploaded_lock_); | |
| return report_uploaded_; | |
| } | |
| void set_report_uploaded(bool report_uploaded) { | |
| base::AutoLock lock(report_uploaded_lock_); | |
| report_uploaded_ = report_uploaded; | |
| } | |
| protected: | |
| EmbeddedTestServer https_server_{EmbeddedTestServer::Type::TYPE_HTTPS}; | |
| base::Lock report_uploaded_lock_; | |
| bool report_uploaded_ GUARDED_BY(report_uploaded_lock_) = false; | |
| }; | |
| #if BUILDFLAG(ENABLE_REPORTING) | |
| // Regression test for crbug.com/500671397. Ensures that if a DoH query | |
| // results in a reporting API upload it doesn't trigger a CHECK. | |
| TEST_F(DnsOverHttpsReportingTest, ReportingApi) { | |
| class TestObserver : public ReportingCacheObserver { | |
| public: | |
| void OnReportAdded(const ReportingReport* report) override { | |
| EXPECT_EQ( | |
| report->network_anonymization_key, | |
| DnsHTTPAttempt::GetDohIsolationInfo().network_anonymization_key()); | |
| // Note that failing DoH queries can get retried and possibly cause | |
| // multiple reports to be generated, but for our purposes we only care | |
| // whether at least one report was added. Thus, we won't make any | |
| // assumptions here about the previous value of `report_added_`. | |
| report_added_ = true; | |
| } | |
| bool report_added() const { return report_added_; } | |
| private: | |
| bool report_added_ = false; | |
| }; | |
| TestObserver observer; | |
| context()->reporting_service()->GetContextForTesting()->AddCacheObserver( | |
| &observer); | |
| // 1. Perform a DoH query to get a NEL policy. | |
| { | |
| GURL url = https_server_.GetURL(kHostname, "/foo"); | |
| TestDelegate delegate; | |
| std::unique_ptr<URLRequest> request = context()->CreateRequest( | |
| url, DEFAULT_PRIORITY, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| request->Start(); | |
| delegate.RunUntilComplete(); | |
| ASSERT_THAT(delegate.request_status(), IsOk()); | |
| } | |
| // 2. Perform a failing DoH query to trigger one or more NEL reports (the DoH | |
| // query may be retried several times, resulting in multiple reports possibly | |
| // being generated). | |
| doh_server_.SetFailRequests(true); | |
| { | |
| GURL url = https_server_.GetURL("fail.com", "/foo"); | |
| TestDelegate delegate; | |
| std::unique_ptr<URLRequest> request = context()->CreateRequest( | |
| url, DEFAULT_PRIORITY, &delegate, TRAFFIC_ANNOTATION_FOR_TESTS, | |
| net::handles::kInvalidNetworkHandle); | |
| request->Start(); | |
| delegate.RunUntilComplete(); | |
| EXPECT_THAT(delegate.request_status(), IsError(ERR_NAME_NOT_RESOLVED)); | |
| } | |
| ReportingService* reporting_service = context()->reporting_service(); | |
| ReportingCache* cache = reporting_service->GetContextForTesting()->cache(); | |
| // 3. Wait for the report to be queued. | |
| EXPECT_TRUE(base::test::RunUntil([&]() { return observer.report_added(); })); | |
| // 4. Wait for the report to be delivered (and thus removed from the cache). | |
| EXPECT_TRUE(base::test::RunUntil( | |
| [&]() { return cache->GetFullReportCountForTesting() == 0; })); | |
| EXPECT_TRUE(report_uploaded()); | |
| // 5. The test passes if no CHECKs were encountered at this point. | |
| context()->reporting_service()->GetContextForTesting()->RemoveCacheObserver( | |
| &observer); | |
| } | |
| #endif | |
| } // namespace | |
| } // namespace net |