| // Copyright 2017 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "services/network/url_loader.h" |
| |
| #include <stdint.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <list> |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/base64.h" |
| #include "base/files/file.h" |
| #include "base/files/file_path.h" |
| #include "base/files/file_util.h" |
| #include "base/functional/bind.h" |
| #include "base/functional/callback_helpers.h" |
| #include "base/memory/raw_ptr.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/metrics/field_trial_params.h" |
| #include "base/no_destructor.h" |
| #include "base/numerics/safe_conversions.h" |
| #include "base/path_service.h" |
| #include "base/run_loop.h" |
| #include "base/strings/escape.h" |
| #include "base/strings/strcat.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/string_view_util.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/task/sequenced_task_runner.h" |
| #include "base/task/single_thread_task_runner.h" |
| #include "base/test/bind.h" |
| #include "base/test/gtest_util.h" |
| #include "base/test/insecure_random_generator.h" |
| #include "base/test/metrics/histogram_tester.h" |
| #include "base/test/run_until.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/task_environment.h" |
| #include "base/time/time.h" |
| #include "build/build_config.h" |
| #include "mojo/public/c/system/data_pipe.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "mojo/public/cpp/bindings/self_owned_receiver.h" |
| #include "mojo/public/cpp/system/data_pipe_utils.h" |
| #include "mojo/public/cpp/system/wait.h" |
| #include "net/base/completion_repeating_callback.h" |
| #include "net/base/features.h" |
| #include "net/base/io_buffer.h" |
| #include "net/base/ip_endpoint.h" |
| #include "net/base/isolation_info.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/mime_sniffer.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/transport_info.h" |
| #include "net/cert/test_root_certs.h" |
| #include "net/cookies/cookie_access_result.h" |
| #include "net/cookies/cookie_change_dispatcher.h" |
| #include "net/cookies/cookie_inclusion_status.h" |
| #include "net/cookies/cookie_partition_key.h" |
| #include "net/cookies/cookie_setting_override.h" |
| #include "net/cookies/cookie_util.h" |
| #include "net/dns/mock_host_resolver.h" |
| #include "net/filter/filter_source_stream_test_util.h" |
| #include "net/http/http_network_session.h" |
| #include "net/http/http_response_info.h" |
| #include "net/http/http_status_code.h" |
| #include "net/http/http_transaction_test_util.h" |
| #include "net/log/net_log_event_type.h" |
| #include "net/log/test_net_log.h" |
| #include "net/proxy_resolution/configured_proxy_resolution_service.h" |
| #include "net/socket/socket_test_util.h" |
| #include "net/ssl/client_cert_identity_test_util.h" |
| #include "net/storage_access_api/status.h" |
| #include "net/test/cert_test_util.h" |
| #include "net/test/embedded_test_server/controllable_http_response.h" |
| #include "net/test/embedded_test_server/default_handlers.h" |
| #include "net/test/embedded_test_server/embedded_test_server.h" |
| #include "net/test/embedded_test_server/http_connection.h" |
| #include "net/test/embedded_test_server/http_response.h" |
| #include "net/test/embedded_test_server/register_basic_auth_handler.h" |
| #include "net/test/gtest_util.h" |
| #include "net/test/quic_simple_test_server.h" |
| #include "net/test/test_data_directory.h" |
| #include "net/test/url_request/url_request_failed_job.h" |
| #include "net/third_party/quiche/src/quiche/common/http/http_header_block.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_filter.h" |
| #include "net/url_request/url_request_interceptor.h" |
| #include "net/url_request/url_request_job.h" |
| #include "net/url_request/url_request_test_job.h" |
| #include "net/url_request/url_request_test_util.h" |
| #include "services/network/cookie_settings.h" |
| #include "services/network/devtools_durable_msg_accounting_delegate.h" |
| #include "services/network/devtools_durable_msg_collector.h" |
| #include "services/network/file_opener_for_upload.h" |
| #include "services/network/observer_wrapper.h" |
| #include "services/network/public/cpp/cors/origin_access_list.h" |
| #include "services/network/public/cpp/features.h" |
| #include "services/network/public/cpp/ip_address_space_util.h" |
| #include "services/network/public/cpp/loading_params.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/mojom/cookie_access_observer.mojom.h" |
| #include "services/network/public/mojom/early_hints.mojom.h" |
| #include "services/network/public/mojom/http_raw_headers.mojom.h" |
| #include "services/network/public/mojom/integrity_algorithm.mojom.h" |
| #include "services/network/public/mojom/integrity_metadata.mojom.h" |
| #include "services/network/public/mojom/ip_address_space.mojom.h" |
| #include "services/network/public/mojom/network_context.mojom.h" |
| #include "services/network/public/mojom/network_service.mojom.h" |
| #include "services/network/public/mojom/trust_tokens.mojom-shared.h" |
| #include "services/network/public/mojom/unencoded_digest.mojom.h" |
| #include "services/network/public/mojom/url_loader.mojom.h" |
| #include "services/network/resource_scheduler/resource_scheduler_client.h" |
| #include "services/network/shared_dictionary/shared_dictionary_access_checker.h" |
| #include "services/network/shared_resource_checker.h" |
| #include "services/network/shared_storage/shared_storage_header_utils.h" |
| #include "services/network/shared_storage/shared_storage_request_helper.h" |
| #include "services/network/shared_storage/shared_storage_test_url_loader_network_observer.h" |
| #include "services/network/shared_storage/shared_storage_test_utils.h" |
| #include "services/network/test/mock_devtools_observer.h" |
| #include "services/network/test/test_data_pipe_getter.h" |
| #include "services/network/test/test_network_context_client.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "services/network/test/test_url_loader_network_observer.h" |
| #include "services/network/test/url_loader_context_for_tests.h" |
| #include "services/network/test_chunked_data_pipe_getter.h" |
| #include "services/network/trust_tokens/trust_token_key_commitment_getter.h" |
| #include "services/network/trust_tokens/trust_token_request_helper.h" |
| #include "services/network/trust_tokens/trust_token_request_helper_factory.h" |
| #include "services/network/url_request_context_owner.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| #include "url/scheme_host_port.h" |
| |
| namespace network { |
| |
| namespace { |
| |
| using ::net::test::IsError; |
| using ::net::test::IsOk; |
| using ::testing::Contains; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Key; |
| using ::testing::Not; |
| using ::testing::Optional; |
| using ::testing::Pointee; |
| using ::testing::SizeIs; |
| using ::testing::ValuesIn; |
| |
| // Returns a URLLoader::DeleteCallback that destroys |url_loader| and quits |
| // |run_loop| when invoked. Tests must wait on the RunLoop to ensure nothing is |
| // leaked. Takes a std::unique_ptr<URLLoader>* instead of a unique_ptr because |
| // the callback must be created before the URLLoader is actually created. |
| URLLoader::DeleteCallback DeleteLoaderCallback( |
| base::RunLoop* run_loop, |
| std::unique_ptr<URLLoader>* url_loader) { |
| return base::BindOnce( |
| [](base::RunLoop* run_loop, std::unique_ptr<URLLoader>* url_loader, |
| URLLoader* url_loader_ptr) { |
| DCHECK_EQ(url_loader->get(), url_loader_ptr); |
| url_loader->reset(); |
| run_loop->Quit(); |
| }, |
| run_loop, url_loader); |
| } |
| |
| // Returns a URLLoader::DeleteCallback that does nothing, but calls NOTREACHED. |
| // Tests that use a URLLoader that actually tries to delete itself shouldn't use |
| // this method, as URLLoaders don't expect to be alive after they invoke their |
| // delete callback. |
| URLLoader::DeleteCallback NeverInvokedDeleteLoaderCallback() { |
| return base::BindOnce([](URLLoader* /* loader*/) { NOTREACHED(); }); |
| } |
| |
| constexpr char kTestAuthURL[] = "/auth-basic?password=PASS&realm=REALM"; |
| |
| constexpr char kInsecureHost[] = "othersite.test"; |
| |
| constexpr char kHostnameWithAliases[] = "www.example.test"; |
| |
| constexpr char kHostnameWithoutAliases[] = "www.other.test"; |
| |
| const net::MockConnect kAsyncMockConnect = |
| net::MockConnect(net::ASYNC, net::OK); |
| |
| // MockWrite for requesting "http://origin.test/". |
| const net::MockWrite kOriginTestWrites[] = { |
| net::MockWrite(net::SYNCHRONOUS, |
| 0, |
| "GET / HTTP/1.1\r\n" |
| "Host: origin.test\r\n" |
| "Connection: keep-alive\r\n" |
| "User-Agent: \r\n" |
| "Accept-Encoding: gzip, deflate\r\n\r\n"), |
| }; |
| |
| static ResourceRequest CreateResourceRequest(const char* method, |
| const GURL& url) { |
| ResourceRequest request; |
| request.method = std::string(method); |
| request.url = url; |
| request.site_for_cookies = |
| net::SiteForCookies::FromUrl(url); // bypass third-party cookie blocking |
| url::Origin origin = url::Origin::Create(url); |
| request.request_initiator = origin; // ensure initiator is set |
| request.is_outermost_main_frame = true; |
| request.trusted_params = network::ResourceRequest::TrustedParams(); |
| request.trusted_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(origin); |
| return request; |
| } |
| |
| class URLRequestMultipleWritesJob : public net::URLRequestJob { |
| public: |
| URLRequestMultipleWritesJob(net::URLRequest* request, |
| std::list<std::string> packets, |
| net::Error net_error, |
| bool async_reads) |
| : URLRequestJob(request), |
| packets_(std::move(packets)), |
| net_error_(net_error), |
| async_reads_(async_reads) {} |
| |
| URLRequestMultipleWritesJob(const URLRequestMultipleWritesJob&) = delete; |
| URLRequestMultipleWritesJob& operator=(const URLRequestMultipleWritesJob&) = |
| delete; |
| |
| ~URLRequestMultipleWritesJob() override = default; |
| |
| // net::URLRequestJob implementation: |
| void Start() override { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&URLRequestMultipleWritesJob::StartAsync, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| int ReadRawData(net::IOBuffer* buf, int buf_size) override { |
| int result; |
| if (packets_.empty()) { |
| result = net_error_; |
| } else { |
| std::string packet = packets_.front(); |
| packets_.pop_front(); |
| CHECK_GE(buf_size, static_cast<int>(packet.length())); |
| buf->span().copy_prefix_from(base::as_byte_span(packet)); |
| result = packet.length(); |
| } |
| |
| if (async_reads_) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce(&URLRequestMultipleWritesJob::ReadRawDataComplete, |
| weak_factory_.GetWeakPtr(), result)); |
| return net::ERR_IO_PENDING; |
| } |
| return result; |
| } |
| |
| private: |
| void StartAsync() { NotifyHeadersComplete(); } |
| |
| std::list<std::string> packets_; |
| net::Error net_error_; |
| bool async_reads_; |
| |
| base::WeakPtrFactory<URLRequestMultipleWritesJob> weak_factory_{this}; |
| }; |
| |
| class MultipleWritesInterceptor : public net::URLRequestInterceptor { |
| public: |
| MultipleWritesInterceptor(std::list<std::string> packets, |
| net::Error net_error, |
| bool async_reads) |
| : packets_(std::move(packets)), |
| net_error_(net_error), |
| async_reads_(async_reads) {} |
| |
| MultipleWritesInterceptor(const MultipleWritesInterceptor&) = delete; |
| MultipleWritesInterceptor& operator=(const MultipleWritesInterceptor&) = |
| delete; |
| |
| ~MultipleWritesInterceptor() override {} |
| |
| static GURL GetURL() { return GURL("http://foo"); } |
| |
| // URLRequestInterceptor implementation: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| return std::make_unique<URLRequestMultipleWritesJob>( |
| request, std::move(packets_), net_error_, async_reads_); |
| } |
| |
| private: |
| std::list<std::string> packets_; |
| net::Error net_error_; |
| bool async_reads_; |
| }; |
| |
| // Every read completes synchronously. |
| class URLRequestEternalSyncReadsJob : public net::URLRequestJob { |
| public: |
| // If |fill_entire_buffer| is true, each read fills the entire read buffer at |
| // once. Otherwise, one byte is read at a time. |
| URLRequestEternalSyncReadsJob(net::URLRequest* request, |
| bool fill_entire_buffer) |
| : URLRequestJob(request), fill_entire_buffer_(fill_entire_buffer) {} |
| |
| URLRequestEternalSyncReadsJob(const URLRequestEternalSyncReadsJob&) = delete; |
| URLRequestEternalSyncReadsJob& operator=( |
| const URLRequestEternalSyncReadsJob&) = delete; |
| |
| ~URLRequestEternalSyncReadsJob() override = default; |
| |
| // net::URLRequestJob implementation: |
| void Start() override { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&URLRequestEternalSyncReadsJob::StartAsync, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| int ReadRawData(net::IOBuffer* buf, int buf_size) override { |
| DCHECK_GT(buf_size, 0); |
| if (fill_entire_buffer_) { |
| std::ranges::fill(buf->first(base::checked_cast<size_t>(buf_size)), 'a'); |
| return buf_size; |
| } |
| |
| buf->data()[0] = 'a'; |
| return 1; |
| } |
| |
| private: |
| void StartAsync() { NotifyHeadersComplete(); } |
| |
| const bool fill_entire_buffer_; |
| |
| base::WeakPtrFactory<URLRequestEternalSyncReadsJob> weak_factory_{this}; |
| }; |
| |
| class EternalSyncReadsInterceptor : public net::URLRequestInterceptor { |
| public: |
| EternalSyncReadsInterceptor() {} |
| |
| EternalSyncReadsInterceptor(const EternalSyncReadsInterceptor&) = delete; |
| EternalSyncReadsInterceptor& operator=(const EternalSyncReadsInterceptor&) = |
| delete; |
| |
| ~EternalSyncReadsInterceptor() override {} |
| |
| static std::string GetHostName() { return "eternal"; } |
| |
| static GURL GetSingleByteURL() { return GURL("http://eternal/single-byte"); } |
| static GURL GetFillBufferURL() { return GURL("http://eternal/fill-buffer"); } |
| |
| // URLRequestInterceptor implementation: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| if (request->url() == GetSingleByteURL()) { |
| return std::make_unique<URLRequestEternalSyncReadsJob>( |
| request, false /* fill_entire_buffer */); |
| } |
| if (request->url() == GetFillBufferURL()) { |
| return std::make_unique<URLRequestEternalSyncReadsJob>( |
| request, true /* fill_entire_buffer */); |
| } |
| return nullptr; |
| } |
| }; |
| |
| // Simulates handing over things to the disk to write before returning to the |
| // caller. |
| class URLRequestSimulatedCacheJob : public net::URLRequestJob { |
| public: |
| // If |fill_entire_buffer| is true, each read fills the entire read buffer at |
| // once. Otherwise, one byte is read at a time. |
| URLRequestSimulatedCacheJob( |
| net::URLRequest* request, |
| scoped_refptr<net::IOBuffer>* simulated_cache_dest, |
| bool use_text_plain) |
| : URLRequestJob(request), |
| simulated_cache_dest_(simulated_cache_dest), |
| use_text_plain_(use_text_plain) {} |
| |
| URLRequestSimulatedCacheJob(const URLRequestSimulatedCacheJob&) = delete; |
| URLRequestSimulatedCacheJob& operator=(const URLRequestSimulatedCacheJob&) = |
| delete; |
| |
| ~URLRequestSimulatedCacheJob() override = default; |
| |
| // net::URLRequestJob implementation: |
| void Start() override { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&URLRequestSimulatedCacheJob::StartAsync, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| void GetResponseInfo(net::HttpResponseInfo* info) override { |
| if (!use_text_plain_) { |
| return URLRequestJob::GetResponseInfo(info); |
| } |
| if (!info->headers) { |
| info->headers = net::HttpResponseHeaders::TryToCreate( |
| "HTTP/1.1 200 OK\r\nContent-Type: text/plain"); |
| } |
| } |
| |
| int ReadRawData(net::IOBuffer* buf, int buf_size) override { |
| DCHECK_GT(buf_size, 0); |
| |
| // Pretend this is the entire network stack, which has sent the buffer |
| // to some worker thread to be written to disk. |
| std::ranges::fill(buf->first(base::checked_cast<size_t>(buf_size)), 'a'); |
| *simulated_cache_dest_ = buf; |
| |
| // The network stack will not report the read result until the write |
| // completes. |
| return net::ERR_IO_PENDING; |
| } |
| |
| private: |
| void StartAsync() { NotifyHeadersComplete(); } |
| |
| raw_ptr<scoped_refptr<net::IOBuffer>> simulated_cache_dest_; |
| bool use_text_plain_; |
| base::WeakPtrFactory<URLRequestSimulatedCacheJob> weak_factory_{this}; |
| }; |
| |
| class SimulatedCacheInterceptor : public net::URLRequestInterceptor { |
| public: |
| explicit SimulatedCacheInterceptor( |
| scoped_refptr<net::IOBuffer>* simulated_cache_dest, |
| bool use_text_plain) |
| : simulated_cache_dest_(simulated_cache_dest), |
| use_text_plain_(use_text_plain) {} |
| |
| SimulatedCacheInterceptor(const SimulatedCacheInterceptor&) = delete; |
| SimulatedCacheInterceptor& operator=(const SimulatedCacheInterceptor&) = |
| delete; |
| |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| return std::make_unique<URLRequestSimulatedCacheJob>( |
| request, simulated_cache_dest_, use_text_plain_); |
| } |
| |
| private: |
| raw_ptr<scoped_refptr<net::IOBuffer>> simulated_cache_dest_; |
| bool use_text_plain_; |
| }; |
| |
| // Observes net error results. Thread-compatible. |
| class ResultObserver { |
| public: |
| ResultObserver() = default; |
| |
| ResultObserver(const ResultObserver&) = delete; |
| ResultObserver& operator=(const ResultObserver&) = delete; |
| |
| void Observe(int result) { results_.push_back(result); } |
| |
| const std::vector<int>& results() { return results_; } |
| |
| private: |
| std::vector<int> results_; |
| }; |
| |
| // Fakes the TransportInfo passed to `URLRequest::Delegate::OnConnected()`. |
| class URLRequestFakeTransportInfoJob : public net::URLRequestJob { |
| public: |
| // `transport_info` is subsequently passed to the `OnConnected()` callback |
| // during `Start()`. |
| // `second_transport_info`, if non-nullopt, is passed to the `OnConnected()` |
| // callback during `ReadRawData()`. |
| // `connected_callback_result_observer`, if non-nullptr, is notified of all |
| // the results of calling `OnConnected()`. |
| URLRequestFakeTransportInfoJob( |
| net::URLRequest* request, |
| net::TransportInfo transport_info, |
| std::optional<net::TransportInfo> second_transport_info, |
| ResultObserver* connected_callback_result_observer) |
| : URLRequestJob(request), |
| transport_info_(std::move(transport_info)), |
| second_transport_info_(std::move(second_transport_info)), |
| connected_callback_result_observer_( |
| connected_callback_result_observer) {} |
| |
| URLRequestFakeTransportInfoJob(const URLRequestFakeTransportInfoJob&) = |
| delete; |
| URLRequestFakeTransportInfoJob& operator=( |
| const URLRequestFakeTransportInfoJob&) = delete; |
| |
| ~URLRequestFakeTransportInfoJob() override = default; |
| |
| // net::URLRequestJob implementation: |
| void Start() override { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(&URLRequestFakeTransportInfoJob::StartAsync, |
| weak_factory_.GetWeakPtr())); |
| } |
| |
| int ReadRawData(net::IOBuffer* buf, int buf_size) override { |
| if (!second_transport_info_) { |
| return 0; |
| } |
| |
| int result = NotifyConnected( |
| *second_transport_info_, base::BindLambdaForTesting([](int result) { |
| ADD_FAILURE() << "NotifyConnected() callback called with " << result; |
| })); |
| ObserveResult(result); |
| return result; |
| } |
| |
| private: |
| void StartAsync() { |
| // Simulate notifying caller of a connection. Call NotifyStartError() or |
| // NotifyHeadersComplete() synchronously/asynchronously on error/success, |
| // depending on return value and callback invocation. |
| const int result = NotifyConnected( |
| transport_info_, |
| base::BindOnce( |
| &URLRequestFakeTransportInfoJob::ConnectedCallbackComplete, |
| base::Unretained(this))); |
| |
| // Wait for callback to be invoked in async case. |
| if (result == net::ERR_IO_PENDING) { |
| return; |
| } |
| |
| ConnectedCallbackComplete(result); |
| } |
| |
| void ConnectedCallbackComplete(int result) { |
| ObserveResult(result); |
| |
| if (result != net::OK) { |
| NotifyStartError(result); |
| return; |
| } |
| |
| NotifyHeadersComplete(); |
| } |
| |
| void ObserveResult(int result) { |
| if (!connected_callback_result_observer_) { |
| return; |
| } |
| |
| connected_callback_result_observer_->Observe(result); |
| } |
| |
| // The fake transport info we pass to `NotifyConnected()` during `Start()`. |
| const net::TransportInfo transport_info_; |
| |
| // An optional fake transport info we pass to `NotifyConnected()` during |
| // `ReadRawData()`. |
| const std::optional<net::TransportInfo> second_transport_info_; |
| |
| // An observer to be called with each result of calling `NotifyConnected()`. |
| raw_ptr<ResultObserver> connected_callback_result_observer_; |
| |
| base::WeakPtrFactory<URLRequestFakeTransportInfoJob> weak_factory_{this}; |
| }; |
| |
| // Intercepts URLRequestJob creation to a specific URL. All requests to this |
| // URL will report being connected with a fake TransportInfo struct. |
| class FakeTransportInfoInterceptor : public net::URLRequestInterceptor { |
| public: |
| // All intercepted requests will claim to be connected via |transport_info|. |
| explicit FakeTransportInfoInterceptor( |
| const net::TransportInfo& transport_info) |
| : transport_info_(transport_info) {} |
| |
| // Sets a second transport info that will be passed to `OnConnected()` when |
| // the response data is read. |
| void SetSecondTransportInfo(const net::TransportInfo& transport_info) { |
| second_transport_info_ = transport_info; |
| } |
| |
| // Sets up `observer` to be notified of the result of all future calls to |
| // `NotifyConnected` from within `URLRequestFakeTransportInfoJob`. |
| // |
| // `observer` must outlive this instance. |
| void SetConnectedCallbackResultObserver(ResultObserver* observer) { |
| connected_callback_result_observer_ = observer; |
| } |
| |
| ~FakeTransportInfoInterceptor() override = default; |
| |
| FakeTransportInfoInterceptor(const FakeTransportInfoInterceptor&) = delete; |
| FakeTransportInfoInterceptor& operator=(const FakeTransportInfoInterceptor&) = |
| delete; |
| |
| // URLRequestInterceptor implementation: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| return std::make_unique<URLRequestFakeTransportInfoJob>( |
| request, transport_info_, second_transport_info_, |
| connected_callback_result_observer_); |
| } |
| |
| private: |
| const net::TransportInfo transport_info_; |
| std::optional<net::TransportInfo> second_transport_info_; |
| |
| raw_ptr<ResultObserver> connected_callback_result_observer_ = nullptr; |
| }; |
| |
| // Returns a maximally-restrictive security state for use in tests. |
| mojom::ClientSecurityStatePtr NewSecurityState() { |
| auto result = mojom::ClientSecurityState::New(); |
| result->is_web_secure_context = false; |
| result->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kBlock; |
| result->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| return result; |
| } |
| |
| CorsErrorStatus InsecurePrivateNetworkCorsErrorStatus( |
| mojom::IPAddressSpace resource_address_space) { |
| return CorsErrorStatus(mojom::CorsError::kInsecurePrivateNetwork, |
| mojom::IPAddressSpace::kUnknown, |
| resource_address_space); |
| } |
| |
| std::string CookieOrLineToString(const mojom::CookieOrLinePtr& cookie_or_line) { |
| switch (cookie_or_line->which()) { |
| case mojom::CookieOrLine::Tag::kCookie: |
| return base::StrCat({ |
| cookie_or_line->get_cookie().Name(), |
| "=", |
| cookie_or_line->get_cookie().Value(), |
| }); |
| case mojom::CookieOrLine::Tag::kCookieString: |
| return cookie_or_line->get_cookie_string(); |
| } |
| } |
| |
| MATCHER_P2(CookieOrLine, string, type, "") { |
| return type == arg->which() && |
| testing::ExplainMatchResult(string, CookieOrLineToString(arg), |
| result_listener); |
| } |
| |
| // Mock implementation of durable message accounting delegate. |
| class MockDurableMessageAccountingDelegate |
| : public DevtoolsDurableMessageAccountingDelegate { |
| public: |
| void WillAddBytes(DevtoolsDurableMessage& message, |
| int64_t chunk_size) override { |
| size_ += chunk_size; |
| } |
| |
| void WillRemoveBytes(DevtoolsDurableMessage& message) override { |
| size_ -= message.encoded_byte_size(); |
| } |
| |
| int64_t size() { return size_; } |
| |
| private: |
| int64_t size_ = 0; |
| }; |
| |
| // Permits simply constructing a URLLoader object specifying only those options |
| // which are non-default. |
| // Replace: |
| // auto url_loader = std::make_unique<URLLoader>(context(), ..., |
| // /*some_option=*/true); |
| // With: |
| // URLLoaderOptions options; |
| // options.some_option = true; |
| // auto url_loader = options.MakeURLLoader(context(), ...); |
| struct URLLoaderOptions { |
| // Non-optional arguments are passed to MakeURLLoader(). MakeURLLoader() can |
| // only be called once for each URLLoaderOptions object. |
| std::unique_ptr<URLLoader> MakeURLLoader( |
| URLLoaderContext& context, |
| URLLoader::DeleteCallback delete_callback, |
| mojo::PendingReceiver<mojom::URLLoader> url_loader_receiver, |
| const ResourceRequest& request, |
| mojo::PendingRemote<mojom::URLLoaderClient> url_loader_client) { |
| DCHECK(!used); |
| used = true; |
| shared_resource_checker = |
| std::make_unique<SharedResourceChecker>(cookie_settings); |
| return std::make_unique<URLLoader>( |
| context, std::move(delete_callback), std::move(url_loader_receiver), |
| options, request, std::move(url_loader_client), |
| std::move(sync_url_loader_client), traffic_annotation, request_id, |
| keepalive_request_size, std::move(keepalive_statistics_recorder), |
| std::move(trust_token_helper_factory), |
| std::move(shared_dictionary_manager), |
| std::move(shared_dictionary_checker), |
| ObserverWrapper(std::move(cookie_observer)), |
| ObserverWrapper(std::move(trust_token_observer)), |
| ObserverWrapper(std::move(url_loader_network_observer)), |
| ObserverWrapper(std::move(devtools_observer)), |
| ObserverWrapper(std::move(device_bound_session_observer)), |
| std::move(accept_ch_frame_observer), shared_storage_writable_eligible, |
| *shared_resource_checker, std::move(maybe_durable_message)); |
| } |
| |
| int32_t options = mojom::kURLLoadOptionNone; |
| base::WeakPtr<mojom::URLLoaderClient> sync_url_loader_client; |
| net::NetworkTrafficAnnotationTag traffic_annotation = |
| TRAFFIC_ANNOTATION_FOR_TESTS; |
| int32_t request_id = 0; |
| int keepalive_request_size = 0; |
| base::WeakPtr<KeepaliveStatisticsRecorder> keepalive_statistics_recorder; |
| std::unique_ptr<TrustTokenRequestHelperFactory> trust_token_helper_factory; |
| raw_ptr<SharedDictionaryManager> shared_dictionary_manager; |
| std::unique_ptr<SharedDictionaryAccessChecker> shared_dictionary_checker; |
| mojo::PendingRemote<mojom::CookieAccessObserver> cookie_observer = |
| mojo::NullRemote(); |
| mojo::PendingRemote<mojom::TrustTokenAccessObserver> trust_token_observer = |
| mojo::NullRemote(); |
| mojo::PendingRemote<mojom::URLLoaderNetworkServiceObserver> |
| url_loader_network_observer = mojo::NullRemote(); |
| mojo::PendingRemote<mojom::DevToolsObserver> devtools_observer = |
| mojo::NullRemote(); |
| mojo::PendingRemote<mojom::DeviceBoundSessionAccessObserver> |
| device_bound_session_observer = mojo::NullRemote(); |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> accept_ch_frame_observer = |
| mojo::NullRemote(); |
| bool shared_storage_writable_eligible = false; |
| CookieSettings cookie_settings; |
| std::unique_ptr<SharedResourceChecker> shared_resource_checker; |
| base::WeakPtr<DevtoolsDurableMessage> maybe_durable_message; |
| |
| private: |
| bool used = false; |
| }; |
| |
| } // namespace |
| |
| class MockAcceptCHFrameObserver : public mojom::AcceptCHFrameObserver { |
| public: |
| MockAcceptCHFrameObserver() = default; |
| ~MockAcceptCHFrameObserver() override = default; |
| |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> Bind() { |
| mojo::PendingRemote<mojom::AcceptCHFrameObserver> remote; |
| receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| void OnAcceptCHFrameReceived( |
| const url::Origin& origin, |
| const std::vector<network::mojom::WebClientHintsType>& accept_ch_frame, |
| OnAcceptCHFrameReceivedCallback callback) override { |
| called_ = true; |
| accept_ch_frame_ = accept_ch_frame; |
| std::move(callback).Run(net::OK); |
| } |
| void Clone(mojo::PendingReceiver<network::mojom::AcceptCHFrameObserver> |
| listener) override { |
| receivers_.Add(this, std::move(listener)); |
| } |
| |
| bool called() const { return called_; } |
| |
| const std::vector<network::mojom::WebClientHintsType>& accept_ch_frame() |
| const { |
| return accept_ch_frame_; |
| } |
| |
| private: |
| bool called_ = false; |
| std::vector<network::mojom::WebClientHintsType> accept_ch_frame_; |
| mojo::ReceiverSet<mojom::AcceptCHFrameObserver> receivers_; |
| }; |
| |
| class URLLoaderTest : public testing::Test { |
| public: |
| URLLoaderTest() |
| : task_environment_(base::test::TaskEnvironment::MainThreadType::IO) { |
| scoped_refptr<net::X509Certificate> quic_root = net::ImportCertFromFile( |
| net::GetTestCertsDirectory().AppendASCII("quic-root.pem")); |
| if (quic_root) { |
| scoped_test_root_.Reset({quic_root}); |
| } else { |
| ADD_FAILURE(); |
| } |
| |
| net::QuicSimpleTestServer::Start(); |
| net::URLRequestFailedJob::AddUrlHandler(); |
| } |
| ~URLLoaderTest() override { |
| net::URLRequestFilter::GetInstance()->ClearHandlers(); |
| } |
| |
| void SetUp() override { |
| net::HttpNetworkSessionParams params; |
| auto quic_context = std::make_unique<net::QuicContext>(); |
| quic_context->params()->origins_to_force_quic_on.insert( |
| url::SchemeHostPort("https", net::QuicSimpleTestServer::GetHost(), |
| net::QuicSimpleTestServer::GetPort())); |
| params.enable_quic = true; |
| net::URLRequestContextBuilder context_builder; |
| context_builder.set_http_network_session_params(params); |
| context_builder.set_quic_context(std::move(quic_context)); |
| context_builder.set_proxy_resolution_service( |
| net::ConfiguredProxyResolutionService::CreateDirect()); |
| auto test_network_delegate = std::make_unique<net::TestNetworkDelegate>(); |
| unowned_test_network_delegate_ = test_network_delegate.get(); |
| context_builder.set_network_delegate(std::move(test_network_delegate)); |
| context_builder.set_client_socket_factory_for_testing(GetSocketFactory()); |
| url_request_context_ = context_builder.Build(); |
| context().set_url_request_context(url_request_context_.get()); |
| resource_scheduler_client_ = base::MakeRefCounted<ResourceSchedulerClient>( |
| ResourceScheduler::ClientId::Create(), IsBrowserInitiated(false), |
| &resource_scheduler_, |
| url_request_context_->network_quality_estimator()); |
| context().set_resource_scheduler_client(resource_scheduler_client_.get()); |
| |
| test_server_.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| // This Unretained is safe because test_server_ is owned by |this|. |
| test_server_.RegisterRequestMonitor( |
| base::BindRepeating(&URLLoaderTest::Monitor, base::Unretained(this))); |
| RegisterAdditionalHandlers(); |
| ASSERT_TRUE(test_server_.Start()); |
| |
| // Set up a scoped host resolver so that |kInsecureHost| will resolve to |
| // the loopback address and will let us access |test_server_|. |
| scoped_refptr<net::RuleBasedHostResolverProc> mock_resolver_proc = |
| base::MakeRefCounted<net::RuleBasedHostResolverProc>(nullptr); |
| mock_resolver_proc->AddIPLiteralRuleWithDnsAliases( |
| kHostnameWithAliases, "127.0.0.1", {"alias1", "alias2", "host"}); |
| mock_resolver_proc->AddRule("*", "127.0.0.1"); |
| mock_host_resolver_ = std::make_unique<net::ScopedDefaultHostResolverProc>( |
| mock_resolver_proc.get()); |
| } |
| |
| void TearDown() override { |
| context().Detach(); |
| unowned_test_network_delegate_ = nullptr; |
| url_request_context_.reset(); |
| net::QuicSimpleTestServer::Shutdown(); |
| } |
| |
| // For derived classes to register additional handlers for `test_server_` |
| // before the server is started. |
| virtual void RegisterAdditionalHandlers() {} |
| |
| // Attempts to load |url| and returns the resulting error code. |
| [[nodiscard]] int Load(const GURL& url, std::string* body = nullptr) { |
| DCHECK(!ran_); |
| |
| ResourceRequest request = |
| CreateResourceRequest(!request_body_ ? "GET" : "POST", url); |
| |
| if (request_body_) { |
| request.request_body = request_body_; |
| } |
| |
| request.trusted_params->client_security_state.Swap( |
| &request_client_security_state_); |
| |
| request.trusted_params->enabled_client_hints.swap(enabled_client_hints_); |
| |
| request.headers.MergeFrom(additional_headers_); |
| |
| request.target_ip_address_space = target_ip_address_space_; |
| |
| request.client_side_content_decoding_enabled = |
| client_side_content_decoding_enabled_; |
| |
| return LoadRequest(request, body); |
| } |
| |
| // Attempts to load |request| and returns the resulting error code. If |body| |
| // is non-NULL, also attempts to read the response body. The advantage of |
| // using |body| instead of calling ReadBody() after Load is that it will load |
| // the response body before URLLoader is complete, so URLLoader completion |
| // won't block on trying to write the body buffer. |
| [[nodiscard]] int LoadRequest(const ResourceRequest& request, |
| std::string* body = nullptr, |
| bool is_trusted = true) { |
| uint32_t options = mojom::kURLLoadOptionNone; |
| if (send_ssl_with_response_) { |
| options |= mojom::kURLLoadOptionSendSSLInfoWithResponse; |
| } |
| if (sniff_) { |
| options |= mojom::kURLLoadOptionSniffMimeType; |
| } |
| if (send_ssl_for_cert_error_) { |
| options |= mojom::kURLLoadOptionSendSSLInfoForCertificateError; |
| } |
| |
| std::unique_ptr<TestNetworkContextClient> network_context_client; |
| std::unique_ptr<TestURLLoaderNetworkObserver> url_loader_network_observer; |
| if (allow_file_uploads_) { |
| network_context_client = std::make_unique<TestNetworkContextClient>(); |
| network_context_client->set_upload_files_invalid(upload_files_invalid_); |
| network_context_client->set_ignore_last_upload_file( |
| ignore_last_upload_file_); |
| } |
| context().set_network_context_client(network_context_client.get()); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| |
| SetUpContext(request.url, is_trusted); |
| |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = options; |
| url_loader_options.url_loader_network_observer = |
| network_observer_ ? network_observer_->Bind() : mojo::NullRemote(); |
| url_loader_options.devtools_observer = |
| devtools_observer_ ? devtools_observer_->Bind() : mojo::NullRemote(); |
| url_loader_options.accept_ch_frame_observer = |
| accept_ch_frame_observer_ ? accept_ch_frame_observer_->Bind() |
| : mojo::NullRemote(); |
| url_loader_options.maybe_durable_message = std::move(durable_message_); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client_.CreateRemote()); |
| |
| if (partial_decoder_decoding_buffer_size_) { |
| url_loader->set_partial_decoder_decoding_buffer_size_for_testing( |
| *partial_decoder_decoding_buffer_size_); |
| } |
| |
| ran_ = true; |
| devtools_observer_ = nullptr; |
| accept_ch_frame_observer_ = nullptr; |
| network_observer_ = nullptr; |
| |
| if (expect_redirect_) { |
| client_.RunUntilRedirectReceived(); |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| } |
| |
| if (body) { |
| client_.RunUntilResponseBodyArrived(); |
| *body = ReadBody(); |
| } |
| |
| client_.RunUntilComplete(); |
| if (body) { |
| EXPECT_EQ(body->size(), |
| static_cast<size_t>( |
| client()->completion_status().decoded_body_length)); |
| } |
| |
| delete_run_loop.Run(); |
| |
| context().set_network_context_client(nullptr); |
| return client_.completion_status().error_code; |
| } |
| |
| void LoadAndCompareFile(const std::string& path) { |
| base::FilePath file = GetTestFilePath(path); |
| |
| std::string expected; |
| if (!base::ReadFileToString(file, &expected)) { |
| ADD_FAILURE() << "File not found: " << file.value(); |
| return; |
| } |
| |
| std::string body; |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL(std::string("/") + path), &body)); |
| EXPECT_EQ(expected, body); |
| // The file isn't compressed, so both encoded and decoded body lengths |
| // should match the read body length. |
| EXPECT_EQ( |
| expected.size(), |
| static_cast<size_t>(client()->completion_status().decoded_body_length)); |
| EXPECT_EQ( |
| expected.size(), |
| static_cast<size_t>(client()->completion_status().encoded_body_length)); |
| // Over the wire length should include headers, so should be longer. |
| // TODO(mmenke): Worth adding better tests for encoded_data_length? |
| EXPECT_LT( |
| expected.size(), |
| static_cast<size_t>(client()->completion_status().encoded_data_length)); |
| } |
| |
| void SetUpContext(const GURL& url, bool is_trusted) { |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = orb_enabled_; |
| context().mutable_factory_params().client_security_state.Swap( |
| &factory_client_security_state_); |
| context().mutable_factory_params().isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(url::Origin::Create(url)); |
| context().mutable_factory_params().is_trusted = is_trusted; |
| context().mutable_factory_params().cookie_setting_overrides = |
| cookie_setting_overrides_; |
| } |
| |
| // Adds a MultipleWritesInterceptor for MultipleWritesInterceptor::GetURL() |
| // that results in seeing each element of |packets| read individually, and |
| // then a final read that returns |net_error|. The URLRequestInterceptor is |
| // not removed from URLFilter until the test fixture is torn down. |
| // |async_reads| indicates whether all reads (including those for |packets| |
| // and |net_error|) complete asynchronously or not. |
| void AddMultipleWritesInterceptor(std::list<std::string> packets, |
| net::Error net_error, |
| bool async_reads) { |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| MultipleWritesInterceptor::GetURL(), |
| std::unique_ptr<net::URLRequestInterceptor>( |
| new MultipleWritesInterceptor(std::move(packets), net_error, |
| async_reads))); |
| } |
| |
| // Adds an EternalSyncReadsInterceptor for |
| // EternalSyncReadsInterceptor::GetURL(), which creates URLRequestJobs where |
| // all reads return a sync byte that's read synchronously. |
| void AddEternalSyncReadsInterceptor() { |
| net::URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "http", EternalSyncReadsInterceptor::GetHostName(), |
| std::make_unique<EternalSyncReadsInterceptor>()); |
| } |
| |
| // If |second| is empty, then it's ignored. |
| void LoadPacketsAndVerifyContents(const std::string& first, |
| const std::string& second) { |
| EXPECT_FALSE(first.empty()); |
| std::list<std::string> packets; |
| packets.push_back(first); |
| if (!second.empty()) { |
| packets.push_back(second); |
| } |
| AddMultipleWritesInterceptor(std::move(packets), net::OK, |
| false /* async_reads */); |
| |
| std::string expected_body = first + second; |
| std::string actual_body; |
| EXPECT_EQ(net::OK, Load(MultipleWritesInterceptor::GetURL(), &actual_body)); |
| |
| EXPECT_EQ(actual_body, expected_body); |
| } |
| |
| const net::EmbeddedTestServer* test_server() const { return &test_server_; } |
| net::EmbeddedTestServer* test_server() { return &test_server_; } |
| net::URLRequestContext* url_request_context() { |
| return url_request_context_.get(); |
| } |
| URLLoaderContextForTests& context() { return url_loader_context_for_tests_; } |
| TestURLLoaderClient* client() { return &client_; } |
| |
| // Returns the path of the requested file in the test data directory. |
| base::FilePath GetTestFilePath(const std::string& file_name) { |
| base::FilePath file_path; |
| base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &file_path); |
| file_path = file_path.Append(FILE_PATH_LITERAL("services")); |
| file_path = file_path.Append(FILE_PATH_LITERAL("test")); |
| file_path = file_path.Append(FILE_PATH_LITERAL("data")); |
| return file_path.AppendASCII(file_name); |
| } |
| |
| std::string ReadTestFile(const std::string& file_name) { |
| std::string contents; |
| CHECK(base::ReadFileToString(GetTestFilePath(file_name), &contents)); |
| return contents; |
| } |
| |
| base::File OpenFileForUpload(const base::FilePath& file_path) { |
| int open_flags = base::File::FLAG_OPEN | base::File::FLAG_READ; |
| #if BUILDFLAG(IS_WIN) |
| open_flags |= base::File::FLAG_ASYNC; |
| #endif // BUILDFLAG(IS_WIN) |
| base::File file(file_path, open_flags); |
| EXPECT_TRUE(file.IsValid()); |
| return file; |
| } |
| |
| ResourceScheduler* resource_scheduler() { return &resource_scheduler_; } |
| scoped_refptr<ResourceSchedulerClient> resource_scheduler_client() { |
| return resource_scheduler_client_; |
| } |
| |
| // Configure how Load() works. |
| void allow_file_uploads() { |
| DCHECK(!ran_); |
| allow_file_uploads_ = true; |
| } |
| void set_upload_files_invalid(bool upload_files_invalid) { |
| DCHECK(!ran_); |
| upload_files_invalid_ = upload_files_invalid; |
| } |
| void set_ignore_last_upload_file(bool ignore_last_upload_file) { |
| DCHECK(!ran_); |
| ignore_last_upload_file_ = ignore_last_upload_file; |
| } |
| void set_sniff() { |
| DCHECK(!ran_); |
| sniff_ = true; |
| } |
| void set_send_ssl_with_response() { |
| DCHECK(!ran_); |
| send_ssl_with_response_ = true; |
| } |
| void set_send_ssl_for_cert_error() { |
| DCHECK(!ran_); |
| send_ssl_for_cert_error_ = true; |
| } |
| void set_ignore_certificate_errors() { |
| DCHECK(!ran_); |
| ignore_certificate_errors_ = true; |
| } |
| void set_expect_redirect() { |
| DCHECK(!ran_); |
| expect_redirect_ = true; |
| } |
| void set_client_side_content_decoding_enabled() { |
| DCHECK(!ran_); |
| client_side_content_decoding_enabled_ = true; |
| } |
| void set_factory_client_security_state(mojom::ClientSecurityStatePtr state) { |
| factory_client_security_state_ = std::move(state); |
| } |
| void set_request_client_security_state(mojom::ClientSecurityStatePtr state) { |
| request_client_security_state_ = std::move(state); |
| } |
| void set_network_observer_for_next_request( |
| TestURLLoaderNetworkObserver* observer) { |
| network_observer_ = observer; |
| } |
| void set_devtools_observer_for_next_request(MockDevToolsObserver* observer) { |
| devtools_observer_ = observer; |
| } |
| void set_request_body(scoped_refptr<ResourceRequestBody> request_body) { |
| request_body_ = request_body; |
| } |
| void set_additional_headers(const net::HttpRequestHeaders& headers) { |
| additional_headers_ = headers; |
| } |
| void set_target_ip_address_space(mojom::IPAddressSpace address_space) { |
| target_ip_address_space_ = address_space; |
| } |
| void set_accept_ch_frame_observer_for_next_request( |
| MockAcceptCHFrameObserver* observer) { |
| accept_ch_frame_observer_ = observer; |
| } |
| void set_enabled_client_hints_for_next_request( |
| std::optional<network::ResourceRequest::TrustedParams::EnabledClientHints> |
| enabled_client_hints) { |
| enabled_client_hints_.swap(enabled_client_hints); |
| } |
| void set_cookie_setting_overrides( |
| const net::CookieSettingOverrides& overrides) { |
| cookie_setting_overrides_ = overrides; |
| } |
| void set_durable_message( |
| base::WeakPtr<DevtoolsDurableMessage> durable_message) { |
| durable_message_ = std::move(durable_message); |
| } |
| |
| // Convenience methods after calling Load(); |
| std::string mime_type() const { |
| DCHECK(ran_); |
| return client_.response_head()->mime_type; |
| } |
| |
| bool did_mime_sniff() const { |
| DCHECK(ran_); |
| return client_.response_head()->did_mime_sniff; |
| } |
| |
| const std::optional<net::SSLInfo>& ssl_info() const { |
| DCHECK(ran_); |
| return client_.ssl_info(); |
| } |
| |
| // Reads the response body from client()->response_body() until the channel is |
| // closed. Expects client()->response_body() to already be populated, and |
| // non-NULL. |
| std::string ReadBody() { |
| std::string body; |
| while (true) { |
| MojoHandle consumer = client()->response_body().value(); |
| |
| const void* buffer; |
| uint32_t num_bytes; |
| MojoResult rv = MojoBeginReadData(consumer, nullptr, &buffer, &num_bytes); |
| // If no data has been received yet, spin the message loop until it has. |
| if (rv == MOJO_RESULT_SHOULD_WAIT) { |
| mojo::SimpleWatcher watcher( |
| FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC, |
| base::SequencedTaskRunner::GetCurrentDefault()); |
| base::RunLoop run_loop; |
| |
| watcher.Watch( |
| client()->response_body(), |
| MOJO_HANDLE_SIGNAL_READABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| MOJO_WATCH_CONDITION_SATISFIED, |
| base::BindRepeating( |
| [](base::RepeatingClosure quit, MojoResult result, |
| const mojo::HandleSignalsState& state) { quit.Run(); }, |
| run_loop.QuitClosure())); |
| run_loop.Run(); |
| continue; |
| } |
| |
| // The pipe was closed. |
| if (rv == MOJO_RESULT_FAILED_PRECONDITION) { |
| return body; |
| } |
| |
| CHECK_EQ(rv, MOJO_RESULT_OK); |
| |
| body.append(static_cast<const char*>(buffer), num_bytes); |
| MojoEndReadData(consumer, num_bytes, nullptr); |
| } |
| } |
| |
| std::string ReadAvailableBody() { |
| MojoHandle consumer = client()->response_body().value(); |
| |
| uint32_t num_bytes = 0; |
| MojoReadDataOptions options; |
| options.struct_size = sizeof(options); |
| options.flags = MOJO_READ_DATA_FLAG_QUERY; |
| MojoResult result = MojoReadData(consumer, &options, nullptr, &num_bytes); |
| CHECK_EQ(MOJO_RESULT_OK, result); |
| if (num_bytes == 0) { |
| return std::string(); |
| } |
| |
| std::vector<char> buffer(num_bytes); |
| result = MojoReadData(consumer, nullptr, buffer.data(), &num_bytes); |
| CHECK_EQ(MOJO_RESULT_OK, result); |
| CHECK_EQ(num_bytes, buffer.size()); |
| |
| return std::string(buffer.data(), buffer.size()); |
| } |
| |
| const net::test_server::HttpRequest& sent_request() const { |
| return sent_request_; |
| } |
| |
| net::TestNetworkDelegate* test_network_delegate() { |
| return unowned_test_network_delegate_; |
| } |
| |
| void RunUntilIdle() { task_environment_.RunUntilIdle(); } |
| |
| static constexpr int kProcessId = 4; |
| static constexpr int kRouteId = 8; |
| |
| // |OnServerReceivedRequest| allows subclasses to register additional logic to |
| // execute once a request reaches the test server. |
| virtual void OnServerReceivedRequest(const net::test_server::HttpRequest&) {} |
| |
| // Lets subclasses inject a mock ClientSocketFactory. |
| virtual net::ClientSocketFactory* GetSocketFactory() { return nullptr; } |
| |
| ResourceRequest CreateCrossOriginResourceRequest() { |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/empty.html")); |
| request.request_initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| return request; |
| } |
| |
| protected: |
| void Monitor(const net::test_server::HttpRequest& request) { |
| sent_request_ = request; |
| OnServerReceivedRequest(request); |
| } |
| |
| base::test::TaskEnvironment task_environment_; |
| net::ScopedTestRoot scoped_test_root_; |
| net::EmbeddedTestServer test_server_; |
| std::unique_ptr<net::ScopedDefaultHostResolverProc> mock_host_resolver_; |
| raw_ptr<net::TestNetworkDelegate> |
| unowned_test_network_delegate_; // owned by |url_request_context_| |
| std::unique_ptr<net::URLRequestContext> url_request_context_; |
| URLLoaderContextForTests url_loader_context_for_tests_; |
| ResourceScheduler resource_scheduler_; |
| scoped_refptr<ResourceSchedulerClient> resource_scheduler_client_; |
| |
| // Options applied to the created request in Load(). |
| bool allow_file_uploads_ = false; |
| bool upload_files_invalid_ = false; |
| bool ignore_last_upload_file_ = false; |
| bool sniff_ = false; |
| bool send_ssl_with_response_ = false; |
| bool send_ssl_for_cert_error_ = false; |
| bool ignore_certificate_errors_ = false; |
| bool expect_redirect_ = false; |
| bool client_side_content_decoding_enabled_ = false; |
| mojom::ClientSecurityStatePtr factory_client_security_state_; |
| mojom::ClientSecurityStatePtr request_client_security_state_; |
| raw_ptr<TestURLLoaderNetworkObserver> network_observer_ = nullptr; |
| raw_ptr<MockDevToolsObserver> devtools_observer_ = nullptr; |
| scoped_refptr<ResourceRequestBody> request_body_; |
| net::HttpRequestHeaders additional_headers_; |
| mojom::IPAddressSpace target_ip_address_space_ = |
| mojom::IPAddressSpace::kUnknown; |
| net::CookieSettingOverrides cookie_setting_overrides_; |
| std::optional<int> partial_decoder_decoding_buffer_size_; |
| base::WeakPtr<DevtoolsDurableMessage> durable_message_; |
| |
| bool orb_enabled_ = false; |
| |
| // Used to ensure that methods are called either before or after a request is |
| // made, since the test fixture is meant to be used only once. |
| bool ran_ = false; |
| net::test_server::HttpRequest sent_request_; |
| TestURLLoaderClient client_; |
| |
| raw_ptr<MockAcceptCHFrameObserver> accept_ch_frame_observer_ = nullptr; |
| std::optional<network::ResourceRequest::TrustedParams::EnabledClientHints> |
| enabled_client_hints_; |
| }; |
| |
| class URLLoaderMockSocketTest : public URLLoaderTest { |
| public: |
| URLLoaderMockSocketTest() = default; |
| ~URLLoaderMockSocketTest() override = default; |
| |
| // Lets subclasses inject mock ClientSocketFactories. |
| net::ClientSocketFactory* GetSocketFactory() override { |
| return &socket_factory_; |
| } |
| |
| protected: |
| // Generates a sequence of random bytes that are hard to compress, using a |
| // seed. |
| std::vector<uint8_t> CreateRandomBytesWithSeed(uint64_t seed, size_t size) { |
| std::vector<uint8_t> data; |
| data.reserve(size); |
| base::test::InsecureRandomGenerator gen; |
| gen.ReseedForTesting(seed); |
| for (size_t i = 0; i < size; ++i) { |
| data.push_back(gen.RandUint32() & 0xFF); |
| } |
| return data; |
| } |
| |
| net::MockClientSocketFactory socket_factory_; |
| }; |
| |
| constexpr int URLLoaderTest::kProcessId; |
| constexpr int URLLoaderTest::kRouteId; |
| |
| TEST_F(URLLoaderTest, Basic) { |
| LoadAndCompareFile("simple_page.html"); |
| } |
| |
| TEST_F(URLLoaderTest, Empty) { |
| LoadAndCompareFile("empty.html"); |
| } |
| |
| TEST_F(URLLoaderTest, BasicSSL) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.ServeFilesFromSourceDirectory( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(https_server.Start()); |
| |
| GURL url = https_server.GetURL("/simple_page.html"); |
| set_send_ssl_with_response(); |
| EXPECT_EQ(net::OK, Load(url)); |
| ASSERT_TRUE(!!ssl_info()); |
| ASSERT_TRUE(!!ssl_info()->cert); |
| |
| ASSERT_TRUE(https_server.GetCertificate()->EqualsExcludingChain( |
| ssl_info()->cert.get())); |
| } |
| |
| TEST_F(URLLoaderTest, SSLSentOnlyWhenRequested) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.ServeFilesFromSourceDirectory( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(https_server.Start()); |
| |
| GURL url = https_server.GetURL("/simple_page.html"); |
| EXPECT_EQ(net::OK, Load(url)); |
| ASSERT_FALSE(!!ssl_info()); |
| } |
| |
| // This test verifies that when the request is same-origin and the origin is |
| // potentially trustworthy, the request is not blocked. |
| TEST_F(URLLoaderTest, PotentiallyTrustworthySameOriginIsOk) { |
| mojom::ClientSecurityStatePtr client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| GURL url = test_server()->GetURL("/empty.html"); |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.request_initiator = url::Origin::Create(url); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| // This test verifies that when the URLLoaderFactory's parameters are missing |
| // a client security state, requests to local network resources are authorized. |
| TEST_F(URLLoaderTest, MissingClientSecurityStateIsOk) { |
| EXPECT_EQ(net::OK, LoadRequest(CreateCrossOriginResourceRequest())); |
| } |
| |
| // This test verifies that when the request's `target_ip_address_space` matches |
| // the resource's IP address space, then the request is allowed even if it |
| // would otherwise be blocked by policy. |
| TEST_F(URLLoaderTest, MatchingTargetIPAddressSpaceIsOk) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| request.target_ip_address_space = mojom::IPAddressSpace::kLoopback; |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| // This test verifies that when the request's `target_ip_address_space` does not |
| // match the resource's IP address space, and the policy is `kPermissionBlock`, |
| // then the request is blocked. |
| TEST_F(URLLoaderTest, MismatchingTargetIPAddressSpaceIsBlocked) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| request.target_ip_address_space = mojom::IPAddressSpace::kLocal; |
| |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus(mojom::CorsError::kInvalidPrivateNetworkAccess, |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| // This test verifies that when the request's `target_ip_address_space` does not |
| // match the resource's IP address space, and the policy is `kPermissionBlock`, |
| // then `URLLoader::OnConnected()` returns the right error code. This error code |
| // causes any cache entry in use to be invalidated. |
| TEST_F(URLLoaderTest, MismatchingTargetIPAddressSpaceErrorCode) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResultObserver connected_callback_result_observer; |
| |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.endpoint = net::IPEndPoint(net::IPAddress(127, 0, 0, 1), 80); |
| auto interceptor = std::make_unique<FakeTransportInfoInterceptor>(info); |
| |
| interceptor->SetConnectedCallbackResultObserver( |
| &connected_callback_result_observer); |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::move(interceptor)); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.request_initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| request.target_ip_address_space = mojom::IPAddressSpace::kLocal; |
| |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus(mojom::CorsError::kInvalidPrivateNetworkAccess, |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback))); |
| |
| EXPECT_THAT(connected_callback_result_observer.results(), |
| ElementsAre(IsError(net::ERR_INCONSISTENT_IP_ADDRESS_SPACE))); |
| } |
| |
| // This test verifies that when the request calls `URLLoader::OnConnected()` |
| // twice with endpoints belonging to different IP address spaces, the request |
| // fails. In that case `URLLoader::OnConnected()` returns the right error code, |
| // which causes any cache entry in use to be invalidated. |
| TEST_F(URLLoaderTest, InconsistentIPAddressSpaceIsBlocked) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->is_web_secure_context = true; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResultObserver connected_callback_result_observer; |
| |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.endpoint = net::IPEndPoint(net::IPAddress(1, 2, 3, 4), 80); |
| auto interceptor = std::make_unique<FakeTransportInfoInterceptor>(info); |
| |
| info.endpoint = net::IPEndPoint(net::IPAddress(127, 0, 0, 1), 80); |
| interceptor->SetSecondTransportInfo(info); |
| interceptor->SetConnectedCallbackResultObserver( |
| &connected_callback_result_observer); |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::move(interceptor)); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.request_initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus(mojom::CorsError::kInvalidPrivateNetworkAccess, |
| // TODO(crbug.com/40208529): Expect |
| // `kPublic` here instead, for better debugging. |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLoopback))); |
| |
| // The first connection was fine, but the second was inconsistent. |
| EXPECT_THAT(connected_callback_result_observer.results(), |
| ElementsAre(IsError(net::OK), |
| IsError(net::ERR_INCONSISTENT_IP_ADDRESS_SPACE))); |
| } |
| |
| // These tests verify that requests from both secure and non-secure contexts to |
| // an IP in the `kLoopback` address space are only blocked when the policy is |
| // `kBlock` and the initiator's address space is not `kLoopback`. |
| // |
| // NOTE: These tests exercise the same codepath as |
| // URLLoaderFakeTransportInfoTest below, except they use real URLRequestJob and |
| // HttpTransaction implementations for higher confidence in the correctness of |
| // the whole stack. OTOH, using an embedded test server prevents us from mocking |
| // out the endpoint IP address. |
| |
| TEST_F(URLLoaderTest, SecureUnknownToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| EXPECT_THAT(client()->completion_status().cors_error_status, |
| Optional(InsecurePrivateNetworkCorsErrorStatus( |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, SecureUnknownToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureUnknownToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureUnknownToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| EXPECT_THAT(client()->completion_status().cors_error_status, |
| Optional(InsecurePrivateNetworkCorsErrorStatus( |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureUnknownToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureUnknownToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kUnknown; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| EXPECT_THAT(client()->completion_status().cors_error_status, |
| Optional(InsecurePrivateNetworkCorsErrorStatus( |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecurePublicToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| EXPECT_THAT(client()->completion_status().cors_error_status, |
| Optional(InsecurePrivateNetworkCorsErrorStatus( |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecurePublicToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecurePublicToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| |
| TEST_F(URLLoaderTest, SecureLocalToLoopbackDefault) { |
| // This test presumes that LNA enforcement is enabled. |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| // Local and Loopback are currently merged and LNA checks are not |
| // enforced. |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLocalToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLocalToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLocalToLoopbackDefault) { |
| // This test presumes that LNA enforcement is enabled. |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| // Local and Loopback are currently merged and LNA checks are not |
| // enforced. |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLocalToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLocalToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLoopbackToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLoopbackToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLoopbackToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = true; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLoopbackToLoopbackBlock) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLoopbackToLoopbackWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, NonSecureLoopbackToLoopbackAllow) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, AddsNetLogEntryForPrivateNetworkAccessCheckSuccess) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| net::RecordingNetLogObserver net_log_observer; |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| std::ignore = LoadRequest(request); |
| |
| std::vector<net::NetLogEntry> entries = net_log_observer.GetEntriesWithType( |
| net::NetLogEventType::PRIVATE_NETWORK_ACCESS_CHECK); |
| |
| ASSERT_THAT(entries, SizeIs(1)); |
| |
| const base::Value::Dict& params = entries[0].params; |
| |
| EXPECT_THAT(params.FindString("client_address_space"), |
| Pointee(Eq("loopback"))); |
| |
| EXPECT_THAT(params.FindString("resource_address_space"), |
| Pointee(Eq("loopback"))); |
| |
| EXPECT_THAT(params.FindString("result"), |
| Pointee(Eq("allowed-no-less-public"))); |
| } |
| |
| TEST_F(URLLoaderTest, AddsNetLogEntryForPrivateNetworkAccessCheckFailure) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| net::RecordingNetLogObserver net_log_observer; |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| std::ignore = LoadRequest(request); |
| |
| std::vector<net::NetLogEntry> entries = net_log_observer.GetEntriesWithType( |
| net::NetLogEventType::PRIVATE_NETWORK_ACCESS_CHECK); |
| |
| ASSERT_THAT(entries, SizeIs(1)); |
| |
| const base::Value::Dict params = std::move(entries[0].params); |
| |
| EXPECT_THAT(params.FindString("client_address_space"), Pointee(Eq("public"))); |
| |
| EXPECT_THAT(params.FindString("resource_address_space"), |
| Pointee(Eq("loopback"))); |
| |
| EXPECT_THAT(params.FindString("result"), |
| Pointee(Eq("lna-permission-required"))); |
| } |
| |
| TEST_F(URLLoaderTest, AddsNetLogEntryForPrivateNetworkAccessCheckSameOrigin) { |
| mojom::ClientSecurityStatePtr client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| net::RecordingNetLogObserver net_log_observer; |
| |
| GURL url = test_server()->GetURL("/empty.html"); |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.request_initiator = url::Origin::Create(url); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| |
| std::vector<net::NetLogEntry> entries = net_log_observer.GetEntriesWithType( |
| net::NetLogEventType::PRIVATE_NETWORK_ACCESS_CHECK); |
| |
| ASSERT_THAT(entries, SizeIs(1)); |
| |
| const base::Value::Dict& params = entries[0].params; |
| |
| EXPECT_THAT(params.FindString("client_address_space"), Pointee(Eq("public"))); |
| |
| EXPECT_THAT(params.FindString("resource_address_space"), |
| Pointee(Eq("loopback"))); |
| |
| EXPECT_THAT(params.FindString("result"), |
| Pointee(Eq("allowed-potentially-trustworthy-same-origin"))); |
| } |
| |
| class TestLNAPermissionURLLoaderNetworkObserver |
| : public TestURLLoaderNetworkObserver { |
| public: |
| explicit TestLNAPermissionURLLoaderNetworkObserver(bool permission_granted) |
| : granted_(permission_granted) {} |
| void OnLocalNetworkAccessPermissionRequired( |
| OnLocalNetworkAccessPermissionRequiredCallback callback) override { |
| // A TestURLLoaderNetworkObserver is configured for a single request, and |
| // within a single request this event should only be called once. |
| EXPECT_FALSE(called_); |
| called_ = true; |
| std::move(callback).Run(granted_); |
| } |
| |
| private: |
| const bool granted_; |
| // Track whether OnLocalNetworkAccessPermissionRequired() has been called. |
| bool called_ = false; |
| }; |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackPermissionDenied) { |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| // Simulate that the permission request was denied. |
| TestLNAPermissionURLLoaderNetworkObserver observer( |
| /*permission_granted=*/false); |
| set_network_observer_for_next_request(&observer); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus( |
| mojom::CorsError::kLocalNetworkAccessPermissionDenied, |
| mojom::IPAddressSpace::kUnknown, mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackPermissionGranted) { |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| // Simulate that the permission request was granted. |
| TestLNAPermissionURLLoaderNetworkObserver observer( |
| /*permission_granted=*/true); |
| set_network_observer_for_next_request(&observer); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus( |
| mojom::CorsError::kLocalNetworkAccessPermissionDenied, |
| mojom::IPAddressSpace::kUnknown, mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLocalToLoopbackLNAPermissionNotRequired) { |
| base::test::ScopedFeatureList feature_list; |
| base::FieldTrialParams params; |
| params["LocalNetworkAccessChecksWarn"] = "false"; |
| feature_list.InitAndEnableFeatureWithParameters( |
| features::kLocalNetworkAccessChecks, params); |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| // Simulate that the permission request was denied. |
| TestLNAPermissionURLLoaderNetworkObserver observer( |
| /*permission_granted=*/false); |
| set_network_observer_for_next_request(&observer); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| // Request not blocked because private -> loopback is not current an LNA |
| // request. |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLoopbackToLoopbackPermission) { |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLoopback; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecurePublicToLoopbackPermissionWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, SecureLocalToLoopbackPermissionWarn) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kLocal; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionWarn; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| } |
| |
| TEST_F(URLLoaderTest, LocalNetworkAccessRequestWarning) { |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.devtools_request_id = "fake-id"; |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionWarn; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| MockDevToolsObserver devtools_observer; |
| set_devtools_observer_for_next_request(&devtools_observer); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| |
| devtools_observer.WaitUntilPrivateNetworkRequest(); |
| ASSERT_TRUE(devtools_observer.private_network_request_params()); |
| auto& params = *devtools_observer.private_network_request_params(); |
| ASSERT_TRUE(params.client_security_state); |
| auto& state = params.client_security_state; |
| EXPECT_EQ(state->private_network_request_policy, |
| mojom::PrivateNetworkRequestPolicy::kPermissionWarn); |
| EXPECT_EQ(state->is_web_secure_context, false); |
| EXPECT_EQ(state->ip_address_space, mojom::IPAddressSpace::kPublic); |
| EXPECT_EQ(params.resource_address_space, mojom::IPAddressSpace::kLoopback); |
| EXPECT_EQ(params.devtools_request_id, "fake-id"); |
| EXPECT_TRUE(params.is_warning); |
| EXPECT_THAT(params.url.spec(), testing::HasSubstr("simple_page.html")); |
| } |
| |
| // Bundles together the inputs to a parameterized private network request test. |
| struct URLLoaderFakeTransportInfoTestParams { |
| // The address space of the client. |
| mojom::IPAddressSpace client_address_space; |
| |
| // The address space of the endpoint serving the request. |
| mojom::IPAddressSpace endpoint_address_space; |
| |
| // The type of transport to set in `TransportInfo`. |
| net::TransportType transport_type; |
| |
| // The expected request result. |
| int expected_result; |
| }; |
| |
| // For clarity when debugging parameterized test failures. |
| std::ostream& operator<<(std::ostream& out, |
| const URLLoaderFakeTransportInfoTestParams& params) { |
| return out << "{ client_address_space: " << params.client_address_space |
| << ", endpoint_address_space: " << params.endpoint_address_space |
| << ", transport_type: " |
| << net::TransportTypeToString(params.transport_type) |
| << ", expected_result: " |
| << net::ErrorToString(params.expected_result) << " }"; |
| } |
| |
| mojom::IPAddressSpace ResponseAddressSpace( |
| const URLLoaderFakeTransportInfoTestParams& params) { |
| switch (params.transport_type) { |
| case net::TransportType::kDirect: |
| case net::TransportType::kCached: |
| return params.endpoint_address_space; |
| case net::TransportType::kProxied: |
| case net::TransportType::kCachedFromProxy: |
| return mojom::IPAddressSpace::kUnknown; |
| } |
| } |
| |
| class URLLoaderFakeTransportInfoTest |
| : public URLLoaderTest, |
| public testing::WithParamInterface<URLLoaderFakeTransportInfoTestParams> { |
| protected: |
| // Returns an address in the given IP address space. |
| static net::IPAddress FakeAddress(mojom::IPAddressSpace space) { |
| switch (space) { |
| case mojom::IPAddressSpace::kUnknown: |
| return net::IPAddress(); |
| case mojom::IPAddressSpace::kPublic: |
| return net::IPAddress(42, 0, 1, 2); |
| case mojom::IPAddressSpace::kLocal: |
| return net::IPAddress(10, 0, 1, 2); |
| case mojom::IPAddressSpace::kLoopback: |
| return net::IPAddress::IPv4Localhost(); |
| } |
| } |
| |
| // Returns an endpoint in the given IP address space. |
| static net::IPEndPoint FakeEndpoint(mojom::IPAddressSpace space) { |
| return net::IPEndPoint(FakeAddress(space), 80); |
| } |
| |
| // Returns a transport info with an endpoint in the given IP address space. |
| static net::TransportInfo FakeTransportInfo( |
| const URLLoaderFakeTransportInfoTestParams& params) { |
| return net::TransportInfo( |
| params.transport_type, FakeEndpoint(params.endpoint_address_space), |
| /*accept_ch_frame_arg=*/"", |
| /*cert_is_issued_by_known_root=*/false, net::NextProto::kProtoUnknown); |
| } |
| }; |
| |
| // This test verifies that requests made from insecure contexts are handled |
| // appropriately when they go from a less-private address space to a |
| // more-private address space or not. The test is parameterized by |
| // (client address space, server address space, expected result) tuple. |
| TEST_P(URLLoaderFakeTransportInfoTest, LocalNetworkRequestLoadsCorrectly) { |
| // This test presumes that LNA enforcement is enabled. |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| |
| const auto params = GetParam(); |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = params.client_address_space; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>( |
| FakeTransportInfo(params))); |
| |
| // Despite its name, IsError(OK) asserts that the matched value is OK. |
| EXPECT_THAT(Load(url), IsError(params.expected_result)); |
| if (params.expected_result != net::OK) { |
| if (params.expected_result != |
| net:: |
| ERR_CACHED_IP_ADDRESS_SPACE_BLOCKED_BY_LOCAL_NETWORK_ACCESS_POLICY) { |
| // CORS error status shouldn't be set when the cache entry was blocked by |
| // private network access policy because we'll retry fetching from the |
| // network. |
| EXPECT_THAT(client()->completion_status().cors_error_status, |
| Optional(InsecurePrivateNetworkCorsErrorStatus( |
| params.endpoint_address_space))); |
| } |
| return; |
| } |
| |
| // Check that the right address spaces are reported in `URLResponseHead`. |
| ASSERT_FALSE(client()->response_head().is_null()); |
| EXPECT_EQ(client()->response_head()->client_address_space, |
| params.client_address_space); |
| EXPECT_EQ(client()->response_head()->response_address_space, |
| ResponseAddressSpace(params)); |
| } |
| |
| // Test the case where a PrivateNetworkRequestPolicy is set on the request via |
| // TrustedParams, rather than on the factory. the value should still be |
| // respected. |
| TEST_F(URLLoaderTest, PrivateNetworkRequestPolicyOnRequest) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kBlock; |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| request.target_ip_address_space = mojom::IPAddressSpace::kLocal; |
| request.trusted_params.emplace(); |
| request.trusted_params->client_security_state = |
| std::move(client_security_state); |
| |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus(mojom::CorsError::kInvalidPrivateNetworkAccess, |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| // Test the case where a PrivateNetworkRequestPolicy is set on the request via |
| // TrustedParams, and on the URLLoaderFactory via URLLoaderFactoryParams. The |
| // value set on the request should be preferred. |
| TEST_F(URLLoaderTest, PrivateNetworkRequestPolicyOnRequestAndFactory) { |
| auto client_security_state = NewSecurityState(); |
| // The value set on the factory should not block the request. `kAllow` will |
| // actually DCHECK(), so this test may DCHECK instead on regression, instead |
| // of LoadRequest() succeeding. |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(client_security_state->Clone()); |
| |
| // The value set on the request should block the request. |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kBlock; |
| |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| request.target_ip_address_space = mojom::IPAddressSpace::kLocal; |
| request.trusted_params.emplace(); |
| request.trusted_params->client_security_state = |
| std::move(client_security_state); |
| |
| // The request should be blocked, based on the per-request value, rather than |
| // the factory value. |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request)); |
| EXPECT_THAT( |
| client()->completion_status().cors_error_status, |
| Optional(CorsErrorStatus(mojom::CorsError::kInvalidPrivateNetworkAccess, |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback))); |
| } |
| |
| // Lists all combinations we want to test in URLLoaderFakeTransportInfoTest. |
| constexpr URLLoaderFakeTransportInfoTestParams |
| kURLLoaderFakeTransportInfoTestParamsList[] = { |
| // Client: kUnknown |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kUnknown, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kPublic, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kDirect, |
| net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| }, |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kDirect, |
| net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| }, |
| // Client: kPublic |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kUnknown, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kPublic, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kDirect, |
| net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kDirect, |
| net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| }, |
| // Client: kLocal |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kUnknown, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kPublic, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kDirect, |
| // Local and Loopback are currently merged and LNA checks are not |
| // enforced. |
| net::OK, |
| }, |
| // Client: kLoopback |
| { |
| mojom::IPAddressSpace::kLoopback, |
| mojom::IPAddressSpace::kUnknown, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLoopback, |
| mojom::IPAddressSpace::kPublic, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLoopback, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLoopback, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kDirect, |
| net::OK, |
| }, |
| // TransportType: kProxied |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kProxied, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kProxied, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kProxied, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kProxied, |
| net::OK, |
| }, |
| // TransportType: kCachedFromProxy |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCachedFromProxy, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCachedFromProxy, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLocal, |
| net::TransportType::kCachedFromProxy, |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCachedFromProxy, |
| net::OK, |
| }, |
| // TransportType: kCached. We only test a loopback target for brevity. |
| { |
| mojom::IPAddressSpace::kUnknown, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCached, |
| net:: |
| ERR_CACHED_IP_ADDRESS_SPACE_BLOCKED_BY_LOCAL_NETWORK_ACCESS_POLICY, |
| }, |
| { |
| mojom::IPAddressSpace::kPublic, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCached, |
| net:: |
| ERR_CACHED_IP_ADDRESS_SPACE_BLOCKED_BY_LOCAL_NETWORK_ACCESS_POLICY, |
| }, |
| { |
| mojom::IPAddressSpace::kLocal, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCached, |
| // Local and Loopback are currently merged and LNA checks are not |
| // enforced. |
| net::OK, |
| }, |
| { |
| mojom::IPAddressSpace::kLoopback, |
| mojom::IPAddressSpace::kLoopback, |
| net::TransportType::kCached, |
| net::OK, |
| }, |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(Parameterized, |
| URLLoaderFakeTransportInfoTest, |
| ValuesIn(kURLLoaderFakeTransportInfoTestParamsList)); |
| |
| // Tests that auth challenge info is present on the response when a request |
| // receives an authentication challenge. |
| TEST_F(URLLoaderTest, AuthChallengeInfo) { |
| GURL url = test_server()->GetURL("/auth-basic"); |
| EXPECT_EQ(net::OK, Load(url)); |
| ASSERT_TRUE(client()->response_head()->auth_challenge_info.has_value()); |
| EXPECT_FALSE(client()->response_head()->auth_challenge_info->is_proxy); |
| EXPECT_EQ(url::SchemeHostPort(url), |
| client()->response_head()->auth_challenge_info->challenger); |
| EXPECT_EQ("basic", client()->response_head()->auth_challenge_info->scheme); |
| EXPECT_EQ("testrealm", client()->response_head()->auth_challenge_info->realm); |
| EXPECT_EQ("Basic realm=\"testrealm\"", |
| client()->response_head()->auth_challenge_info->challenge); |
| EXPECT_EQ("/auth-basic", |
| client()->response_head()->auth_challenge_info->path); |
| } |
| |
| // Tests that no auth challenge info is present on the response when a request |
| // does not receive an authentication challenge. |
| TEST_F(URLLoaderTest, NoAuthChallengeInfo) { |
| GURL url = test_server()->GetURL("/"); |
| EXPECT_EQ(net::OK, Load(url)); |
| EXPECT_FALSE(client()->response_head()->auth_challenge_info.has_value()); |
| } |
| |
| // Test decoded_body_length / encoded_body_length when they're different. |
| TEST_F(URLLoaderTest, GzipTest) { |
| std::string body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/gzip-body?Body"), &body)); |
| EXPECT_EQ("Body", body); |
| // Deflating a 4-byte string should result in a longer string - main thing to |
| // check here, though, is that the two lengths are of different. |
| EXPECT_LT(client()->completion_status().decoded_body_length, |
| client()->completion_status().encoded_body_length); |
| // Over the wire length should include headers, so should be longer. |
| EXPECT_LT(client()->completion_status().encoded_body_length, |
| client()->completion_status().encoded_data_length); |
| } |
| |
| TEST_F(URLLoaderTest, ErrorBeforeHeaders) { |
| EXPECT_EQ(net::ERR_EMPTY_RESPONSE, |
| Load(test_server()->GetURL("/close-socket"), nullptr)); |
| EXPECT_FALSE(client()->response_body().is_valid()); |
| } |
| |
| TEST_F(URLLoaderTest, SyncErrorWhileReadingBody) { |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, |
| Load(net::URLRequestFailedJob::GetMockHttpUrlWithFailurePhase( |
| net::URLRequestFailedJob::READ_SYNC, net::ERR_FAILED), |
| &body)); |
| EXPECT_EQ("", body); |
| } |
| |
| TEST_F(URLLoaderTest, AsyncErrorWhileReadingBody) { |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, |
| Load(net::URLRequestFailedJob::GetMockHttpUrlWithFailurePhase( |
| net::URLRequestFailedJob::READ_ASYNC, net::ERR_FAILED), |
| &body)); |
| EXPECT_EQ("", body); |
| } |
| |
| TEST_F(URLLoaderTest, SyncErrorWhileReadingBodyAfterBytesReceived) { |
| const std::string kBody("Foo."); |
| |
| std::list<std::string> packets; |
| packets.push_back(kBody); |
| AddMultipleWritesInterceptor(packets, net::ERR_ACCESS_DENIED, |
| false /*async_reads*/); |
| std::string body; |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, |
| Load(MultipleWritesInterceptor::GetURL(), &body)); |
| EXPECT_EQ(kBody, body); |
| } |
| |
| TEST_F(URLLoaderTest, AsyncErrorWhileReadingBodyAfterBytesReceived) { |
| const std::string kBody("Foo."); |
| |
| std::list<std::string> packets; |
| packets.push_back(kBody); |
| AddMultipleWritesInterceptor(packets, net::ERR_ACCESS_DENIED, |
| true /*async_reads*/); |
| std::string body; |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, |
| Load(MultipleWritesInterceptor::GetURL(), &body)); |
| EXPECT_EQ(kBody, body); |
| } |
| |
| TEST_F(URLLoaderTest, DoNotSniffUnlessSpecified) { |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test0.html"))); |
| EXPECT_FALSE(did_mime_sniff()); |
| ASSERT_TRUE(mime_type().empty()); |
| } |
| |
| TEST_F(URLLoaderTest, SniffMimeType) { |
| set_sniff(); |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test0.html"))); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/html"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, RespectNoSniff) { |
| set_sniff(); |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/nosniff-test.html"))); |
| EXPECT_FALSE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, SniffTextPlainDoesNotResultInHTML) { |
| set_sniff(); |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test1.html"))); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, DoNotSniffHTMLFromImageGIF) { |
| set_sniff(); |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test2.html"))); |
| EXPECT_FALSE(did_mime_sniff()); |
| ASSERT_EQ(std::string("image/gif"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, EmptyHtmlIsTextPlain) { |
| set_sniff(); |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test4.html"))); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, EmptyHtmlIsTextPlainWithAsyncResponse) { |
| set_sniff(); |
| |
| const std::string kBody; |
| |
| std::list<std::string> packets; |
| packets.push_back(kBody); |
| AddMultipleWritesInterceptor(packets, net::OK, true /*async_reads*/); |
| |
| std::string body; |
| EXPECT_EQ(net::OK, Load(MultipleWritesInterceptor::GetURL(), &body)); |
| EXPECT_EQ(kBody, body); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| // Tests the case where the first read doesn't have enough data to figure out |
| // the right mime type. The second read would have enough data even though the |
| // total bytes is still smaller than net::kMaxBytesToSniff. |
| TEST_F(URLLoaderTest, FirstReadNotEnoughToSniff1) { |
| set_sniff(); |
| std::string first(500, 'a'); |
| std::string second(std::string(100, 'b')); |
| second[10] = 0; |
| EXPECT_LE(first.size() + second.size(), |
| static_cast<uint32_t>(net::kMaxBytesToSniff)); |
| LoadPacketsAndVerifyContents(first, second); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("application/octet-stream"), mime_type()); |
| } |
| |
| // Like above, except that the total byte count is > kMaxBytesToSniff. |
| TEST_F(URLLoaderTest, FirstReadNotEnoughToSniff2) { |
| set_sniff(); |
| std::string first(500, 'a'); |
| std::string second(std::string(1000, 'b')); |
| second[10] = 0; |
| EXPECT_GE(first.size() + second.size(), |
| static_cast<uint32_t>(net::kMaxBytesToSniff)); |
| LoadPacketsAndVerifyContents(first, second); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("application/octet-stream"), mime_type()); |
| } |
| |
| // Tests that even if the first and only read is smaller than the minimum number |
| // of bytes needed to sniff, the loader works correctly and returns the data. |
| TEST_F(URLLoaderTest, LoneReadNotEnoughToSniff) { |
| set_sniff(); |
| std::string first(net::kMaxBytesToSniff - 100, 'a'); |
| LoadPacketsAndVerifyContents(first, std::string()); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| // Tests the simple case where the first read is enough to sniff. |
| TEST_F(URLLoaderTest, FirstReadIsEnoughToSniff) { |
| set_sniff(); |
| std::string first(net::kMaxBytesToSniff + 100, 'a'); |
| LoadPacketsAndVerifyContents(first, std::string()); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/plain"), mime_type()); |
| } |
| |
| TEST_F(URLLoaderTest, CompressedResponse) { |
| std::string body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/hello.html.gz"), &body)); |
| ASSERT_EQ(std::string("text/html"), mime_type()); |
| EXPECT_EQ(body, ReadTestFile("hello.html")); |
| } |
| |
| TEST_F(URLLoaderTest, CompressedResponseClienteSideDecoding) { |
| set_client_side_content_decoding_enabled(); |
| std::string body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/hello.html.gz"), &body)); |
| ASSERT_EQ(std::string("text/html"), mime_type()); |
| EXPECT_EQ(body, ReadTestFile("hello.html.gz")); |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| } |
| |
| TEST_F(URLLoaderTest, CompressedResponseSniffMime) { |
| set_sniff(); |
| std::string body; |
| EXPECT_EQ( |
| net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test0.html.gz"), &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/html"), mime_type()); |
| EXPECT_EQ(body, |
| base::as_string_view(ReadTestFile("content-sniffer-test0.html"))); |
| } |
| |
| TEST_F(URLLoaderTest, CompressedResponseSniffMimeClienteSideDecoding) { |
| set_sniff(); |
| set_client_side_content_decoding_enabled(); |
| std::string body; |
| EXPECT_EQ( |
| net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test0.html.gz"), &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| ASSERT_EQ(std::string("text/html"), mime_type()); |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_EQ(body, ReadTestFile("content-sniffer-test0.html.gz")); |
| } |
| |
| // Tests that URLLoader writes into given DurableMessage object when configured. |
| TEST_F(URLLoaderTest, WritesToDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| std::string body; |
| constexpr int kGzippedBodyLength = 60; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/hello.html.gz"), &body)); |
| EXPECT_EQ(body, ReadTestFile("hello.html")); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), kGzippedBodyLength); |
| ASSERT_EQ(durable_message.encoded_byte_size(), |
| static_cast<size_t>(kGzippedBodyLength)); |
| ASSERT_EQ(durable_message.byte_size_for_testing(), body.size()); |
| // Retrieve the stored body and verify that it is accurate. |
| EXPECT_EQ(durable_message.Retrieve(), base::as_byte_span(body)); |
| } |
| |
| TEST_F(URLLoaderTest, DurableMessageWorksWithMimeSniffing) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| constexpr int kGzippedBodyLength = 142; |
| set_sniff(); |
| set_client_side_content_decoding_enabled(); |
| std::string encoded_body; |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/content-sniffer-test0.html.gz"), |
| &encoded_body)); |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_THAT(durable_message.GetClientDecodingTypesForTesting(), |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_EQ(encoded_body, ReadTestFile("content-sniffer-test0.html.gz")); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), kGzippedBodyLength); |
| ASSERT_EQ(durable_message.encoded_byte_size(), |
| static_cast<size_t>(kGzippedBodyLength)); |
| ASSERT_EQ(durable_message.byte_size_for_testing(), encoded_body.size()); |
| // Retrieve the stored body and verify that it is accurate and contents are in |
| // decoded form. |
| mojo_base::BigBuffer buffer = durable_message.Retrieve(); |
| auto decoded_body = ReadTestFile("content-sniffer-test0.html"); |
| EXPECT_EQ(buffer.size(), decoded_body.size()); |
| EXPECT_EQ(buffer, base::as_byte_span(decoded_body)); |
| } |
| |
| // Tests a large response body, which would result in multiple asynchronous |
| // chunks being written into the configured durable message. |
| TEST_F(URLLoaderMockSocketTest, DurableMessageWorksWithLotsOfData) { |
| std::string response_body; |
| constexpr int kReadSize = 1024; |
| response_body.reserve(5 * kReadSize * kReadSize); |
| // Using a repeating patter with a length that's prime is more likely to spot |
| // out of order or repeated chunks of data. |
| while (response_body.size() < 5 * kReadSize * kReadSize) { |
| response_body.append("foppity"); |
| } |
| const auto compressed = net::CompressGzip(response_body); |
| int seq = 1; |
| std::vector<net::MockRead> reads; |
| reads.emplace_back(net::ASYNC, /*seq=*/seq++, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"); |
| for (size_t i = 0; i < compressed.size(); i += kReadSize) { |
| reads.emplace_back(net::ASYNC, |
| base::as_string_view(compressed).substr(i, kReadSize), |
| /*result=*/0, /*seq=*/seq++); |
| } |
| reads.emplace_back(net::ASYNC, net::OK, /*seq=*/seq); |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(reads, kOriginTestWrites); |
| socket_data_reads_writes.set_receive_buffer_size(1); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/plain"); |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_TRUE(durable_message.is_complete()); |
| size_t compressed_size = compressed.size(); |
| ASSERT_EQ(accounting_delegate.size(), static_cast<int64_t>(compressed_size)); |
| ASSERT_EQ(durable_message.encoded_byte_size(), compressed_size); |
| ASSERT_EQ(durable_message.byte_size_for_testing(), compressed_size); |
| EXPECT_EQ(durable_message.Retrieve(), base::as_byte_span(response_body)); |
| } |
| |
| // Tests that client side decoding types are written into DurableMessage and the |
| // body is decoded on retrieval. |
| TEST_F(URLLoaderTest, DurableMessagePerformsClientSideDecodingGzip) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| constexpr size_t kGzippedBodyLength = 60; |
| set_sniff(); |
| set_client_side_content_decoding_enabled(); |
| std::string encoded_body; |
| EXPECT_EQ(net::OK, |
| Load(test_server()->GetURL("/hello.html.gz"), &encoded_body)); |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_THAT(durable_message.GetClientDecodingTypesForTesting(), |
| ElementsAre(net::SourceStreamType::kGzip)); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), |
| static_cast<int64_t>(kGzippedBodyLength)); |
| ASSERT_EQ(durable_message.encoded_byte_size(), kGzippedBodyLength); |
| // Retrieve the stored body and verify that it is accurate and contents are in |
| // decoded form. |
| mojo_base::BigBuffer buffer = durable_message.Retrieve(); |
| auto decoded_body = ReadTestFile("hello.html"); |
| EXPECT_EQ(buffer.size(), decoded_body.size()); |
| EXPECT_EQ(buffer, base::as_byte_span(decoded_body)); |
| } |
| |
| // Tests that URLLoader still completes the load without errors/crashing, but |
| // does not write into the DurableMessage object when the original message has |
| // been evicted. |
| TEST_F(URLLoaderTest, DoesNotWriteToDurableMessageWithEvictedMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| std::unique_ptr<DevtoolsDurableMessage> durable_message = |
| std::make_unique<DevtoolsDurableMessage>("test", accounting_delegate); |
| set_durable_message(durable_message->GetWeakPtr()); |
| |
| // Simulate eviction of the durable message. |
| durable_message.reset(); |
| |
| std::string body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/hello.html.gz"), &body)); |
| EXPECT_EQ(body, ReadTestFile("hello.html")); |
| ASSERT_EQ(accounting_delegate.size(), 0); |
| } |
| |
| // Tests Durable Message incomplete read scenarios. |
| |
| TEST_F(URLLoaderTest, ErrorBeforeHeadersWritesIncompleteDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| EXPECT_EQ(net::ERR_EMPTY_RESPONSE, |
| Load(test_server()->GetURL("/close-socket"), nullptr)); |
| EXPECT_FALSE(client()->response_body().is_valid()); |
| EXPECT_FALSE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), 0); |
| } |
| |
| TEST_F(URLLoaderTest, SyncErrorWhileReadingBodyWritesDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, |
| Load(net::URLRequestFailedJob::GetMockHttpUrlWithFailurePhase( |
| net::URLRequestFailedJob::READ_SYNC, net::ERR_FAILED), |
| &body)); |
| EXPECT_EQ("", body); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), 0); |
| } |
| |
| TEST_F(URLLoaderTest, AsyncErrorWhileReadingBodyWritesDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, |
| Load(net::URLRequestFailedJob::GetMockHttpUrlWithFailurePhase( |
| net::URLRequestFailedJob::READ_ASYNC, net::ERR_FAILED), |
| &body)); |
| EXPECT_EQ("", body); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_EQ(accounting_delegate.size(), 0); |
| } |
| |
| TEST_F(URLLoaderTest, |
| SyncErrorWhileReadingBodyAfterBytesReceivedWritesDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| |
| const std::string response_body("Foo."); |
| std::list<std::string> packets = {response_body}; |
| AddMultipleWritesInterceptor(packets, net::ERR_ACCESS_DENIED, |
| false /*async_reads*/); |
| std::string body_readback; |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, |
| Load(MultipleWritesInterceptor::GetURL(), &body_readback)); |
| EXPECT_EQ(response_body, body_readback); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_GT(accounting_delegate.size(), 0); |
| } |
| |
| TEST_F( |
| URLLoaderTest, |
| AsyncErrorWhileReadingBodyAfterBytesReceivedWritesIncompleteDurableMessage) { |
| MockDurableMessageAccountingDelegate accounting_delegate; |
| DevtoolsDurableMessage durable_message("test", accounting_delegate); |
| set_durable_message(durable_message.GetWeakPtr()); |
| const std::string kBody("Foo."); |
| |
| std::list<std::string> packets; |
| packets.push_back(kBody); |
| AddMultipleWritesInterceptor(packets, net::ERR_ACCESS_DENIED, |
| true /*async_reads*/); |
| std::string body; |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, |
| Load(MultipleWritesInterceptor::GetURL(), &body)); |
| EXPECT_EQ(kBody, body); |
| EXPECT_TRUE(durable_message.is_complete()); |
| ASSERT_GT(accounting_delegate.size(), 0); |
| } |
| |
| class NeverFinishedBodyHttpResponse : public net::test_server::HttpResponse { |
| public: |
| NeverFinishedBodyHttpResponse() = default; |
| ~NeverFinishedBodyHttpResponse() override = default; |
| |
| private: |
| // net::test_server::HttpResponse implementation. |
| void SendResponse( |
| base::WeakPtr<net::test_server::HttpResponseDelegate> delegate) override { |
| delegate->SendResponseHeaders(net::HTTP_OK, "OK", |
| {{"Content-Type", "text/plain"}}); |
| delegate->SendContents( |
| "long long ago..." + |
| std::string(static_cast<uint64_t>(1024 * 1024), 'a')); |
| |
| // Never call |done|, so the other side will never see the completion of the |
| // response body. |
| } |
| }; |
| |
| // Check that the URLLoader tears itself down when the URLLoader pipe is closed. |
| TEST_F(URLLoaderTest, DestroyOnURLLoaderPipeClosed) { |
| net::EmbeddedTestServer server; |
| server.RegisterRequestHandler( |
| base::BindRepeating([](const net::test_server::HttpRequest& request) { |
| std::unique_ptr<net::test_server::HttpResponse> response = |
| std::make_unique<NeverFinishedBodyHttpResponse>(); |
| return response; |
| })); |
| ASSERT_TRUE(server.Start()); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", server.GetURL("/hello.html")); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| // Run until the response body pipe arrives, to make sure that a live body |
| // pipe does not result in keeping the loader alive when the URLLoader pipe is |
| // closed. |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| loader.reset(); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| // client()->RunUntilConnectionError(); |
| // EXPECT_FALSE(client()->has_received_completion()); |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::ERR_FAILED, client()->completion_status().error_code); |
| } |
| |
| // Make sure that the URLLoader is destroyed when the data pipe is closed. |
| // The pipe may either be closed in |
| // URLLoader::OnResponseBodyStreamConsumerClosed() or URLLoader::DidRead(), |
| // depending on whether the closed pipe is first noticed when trying to write to |
| // it, or when a mojo close notification is received, so if only one path |
| // breaks, this test may flakily fail. |
| TEST_F(URLLoaderTest, CloseResponseBodyConsumerBeforeProducer) { |
| net::EmbeddedTestServer server; |
| server.RegisterRequestHandler( |
| base::BindRepeating([](const net::test_server::HttpRequest& request) { |
| std::unique_ptr<net::test_server::HttpResponse> response = |
| std::make_unique<NeverFinishedBodyHttpResponse>(); |
| return response; |
| })); |
| ASSERT_TRUE(server.Start()); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", server.GetURL("/hello.html")); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| // Wait for a little amount of time for the response body pipe to be filled. |
| // (Please note that this doesn't guarantee that the pipe is filled to the |
| // point that it is not writable anymore.) |
| base::RunLoop run_loop; |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask( |
| FROM_HERE, run_loop.QuitClosure(), base::Milliseconds(100)); |
| run_loop.Run(); |
| |
| auto response_body = client()->response_body_release(); |
| response_body.reset(); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::ERR_FAILED, client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, UploadBytes) { |
| const std::string kRequestBody = "Request Body"; |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendCopyOfBytes(base::as_byte_span(kRequestBody)); |
| set_request_body(std::move(request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(kRequestBody, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadFile) { |
| allow_file_uploads(); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| std::string expected_body; |
| ASSERT_TRUE(base::ReadFileToString(file_path, &expected_body)) |
| << "File not found: " << file_path.value(); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| set_request_body(std::move(request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(expected_body, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadFileWithRange) { |
| allow_file_uploads(); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| std::string expected_body; |
| ASSERT_TRUE(base::ReadFileToString(file_path, &expected_body)) |
| << "File not found: " << file_path.value(); |
| expected_body = expected_body.substr(1, expected_body.size() - 2); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendFileRange(file_path, 1, expected_body.size(), |
| base::Time()); |
| set_request_body(std::move(request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(expected_body, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadTwoFiles) { |
| allow_file_uploads(); |
| base::FilePath file_path1 = GetTestFilePath("simple_page.html"); |
| base::FilePath file_path2 = GetTestFilePath("hello.html"); |
| |
| std::string expected_body1; |
| std::string expected_body2; |
| ASSERT_TRUE(base::ReadFileToString(file_path1, &expected_body1)) |
| << "File not found: " << file_path1.value(); |
| ASSERT_TRUE(base::ReadFileToString(file_path2, &expected_body2)) |
| << "File not found: " << file_path2.value(); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendFileRange( |
| file_path1, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| request_body->AppendFileRange( |
| file_path2, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| set_request_body(std::move(request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(expected_body1 + expected_body2, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadTwoBatchesOfFiles) { |
| allow_file_uploads(); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| std::string expected_body; |
| size_t num_files = 2 * FileOpenerForUpload::kMaxFileUploadRequestsPerBatch; |
| for (size_t i = 0; i < num_files; ++i) { |
| std::string tmp_expected_body; |
| ASSERT_TRUE(base::ReadFileToString(file_path, &tmp_expected_body)) |
| << "File not found: " << file_path.value(); |
| expected_body += tmp_expected_body; |
| } |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| for (size_t i = 0; i < num_files; ++i) { |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| } |
| set_request_body(std::move(request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(expected_body, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadTwoBatchesOfFilesWithRespondInvalidFile) { |
| allow_file_uploads(); |
| set_upload_files_invalid(true); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| size_t num_files = 2 * FileOpenerForUpload::kMaxFileUploadRequestsPerBatch; |
| for (size_t i = 0; i < num_files; ++i) { |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| } |
| set_request_body(std::move(request_body)); |
| |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| TEST_F(URLLoaderTest, UploadTwoBatchesOfFilesWithRespondDifferentNumOfFiles) { |
| allow_file_uploads(); |
| set_ignore_last_upload_file(true); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| size_t num_files = 2 * FileOpenerForUpload::kMaxFileUploadRequestsPerBatch; |
| for (size_t i = 0; i < num_files; ++i) { |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| } |
| set_request_body(std::move(request_body)); |
| |
| EXPECT_EQ(net::ERR_FAILED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| TEST_F(URLLoaderTest, UploadInvalidFile) { |
| allow_file_uploads(); |
| set_upload_files_invalid(true); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| set_request_body(std::move(request_body)); |
| |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| TEST_F(URLLoaderTest, UploadFileWithoutNetworkServiceClient) { |
| // Don't call allow_file_uploads(); |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| |
| scoped_refptr<ResourceRequestBody> request_body(new ResourceRequestBody()); |
| request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| set_request_body(std::move(request_body)); |
| |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| class CallbackSavingNetworkContextClient : public TestNetworkContextClient { |
| public: |
| void OnFileUploadRequested(int32_t process_id, |
| bool async, |
| const std::vector<base::FilePath>& file_paths, |
| const GURL& destination_url, |
| OnFileUploadRequestedCallback callback) override { |
| file_upload_requested_callback_ = std::move(callback); |
| if (quit_closure_for_on_file_upload_requested_) { |
| std::move(quit_closure_for_on_file_upload_requested_).Run(); |
| } |
| } |
| |
| void RunUntilUploadRequested(OnFileUploadRequestedCallback* callback) { |
| if (!file_upload_requested_callback_) { |
| base::RunLoop run_loop; |
| quit_closure_for_on_file_upload_requested_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| *callback = std::move(file_upload_requested_callback_); |
| } |
| |
| private: |
| base::OnceClosure quit_closure_for_on_file_upload_requested_; |
| OnFileUploadRequestedCallback file_upload_requested_callback_; |
| }; |
| |
| TEST_F(URLLoaderTest, UploadFileCanceled) { |
| base::FilePath file_path = GetTestFilePath("simple_page.html"); |
| std::vector<base::File> opened_file; |
| opened_file.emplace_back(file_path, base::File::FLAG_OPEN | |
| base::File::FLAG_READ | |
| base::File::FLAG_ASYNC); |
| ASSERT_TRUE(opened_file.back().IsValid()); |
| |
| ResourceRequest request = |
| CreateResourceRequest("POST", test_server()->GetURL("/echo")); |
| request.request_body = new ResourceRequestBody(); |
| request.request_body->AppendFileRange( |
| file_path, 0, std::numeric_limits<uint64_t>::max(), base::Time()); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| auto network_context_client = |
| std::make_unique<CallbackSavingNetworkContextClient>(); |
| context().set_network_context_client(network_context_client.get()); |
| std::unique_ptr<URLLoader> url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| mojom::NetworkContextClient::OnFileUploadRequestedCallback callback; |
| network_context_client->RunUntilUploadRequested(&callback); |
| |
| // Check we can call the callback from a deleted URLLoader without crashing. |
| url_loader.reset(); |
| base::RunLoop().RunUntilIdle(); |
| std::move(callback).Run(net::OK, std::move(opened_file)); |
| base::RunLoop().RunUntilIdle(); |
| context().set_network_context_client(nullptr); |
| } |
| |
| // Tests a request body with a data pipe element. |
| TEST_F(URLLoaderTest, UploadDataPipe) { |
| const std::string kRequestBody = "Request Body"; |
| |
| mojo::PendingRemote<mojom::DataPipeGetter> data_pipe_getter_remote; |
| auto data_pipe_getter = std::make_unique<TestDataPipeGetter>( |
| kRequestBody, data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| set_request_body(std::move(resource_request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(kRequestBody, response_body); |
| } |
| |
| // Same as above and tests that the body is sent after a 307 redirect. |
| TEST_F(URLLoaderTest, UploadDataPipe_Redirect307) { |
| const std::string kRequestBody = "Request Body"; |
| |
| mojo::PendingRemote<mojom::DataPipeGetter> data_pipe_getter_remote; |
| auto data_pipe_getter = std::make_unique<TestDataPipeGetter>( |
| kRequestBody, data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| set_request_body(std::move(resource_request_body)); |
| set_expect_redirect(); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/redirect307-to-echo"), |
| &response_body)); |
| EXPECT_EQ(kRequestBody, response_body); |
| } |
| |
| // Tests a large request body, which should result in multiple asynchronous |
| // reads. |
| TEST_F(URLLoaderTest, UploadDataPipeWithLotsOfData) { |
| std::string request_body; |
| request_body.reserve(5 * 1024 * 1024); |
| // Using a repeating patter with a length that's prime is more likely to spot |
| // out of order or repeated chunks of data. |
| while (request_body.size() < 5 * 1024 * 1024) { |
| request_body.append("foppity"); |
| } |
| |
| mojo::PendingRemote<mojom::DataPipeGetter> data_pipe_getter_remote; |
| auto data_pipe_getter = std::make_unique<TestDataPipeGetter>( |
| request_body, data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| set_request_body(std::move(resource_request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::OK, Load(test_server()->GetURL("/echo"), &response_body)); |
| EXPECT_EQ(request_body, response_body); |
| } |
| |
| TEST_F(URLLoaderTest, UploadDataPipeError) { |
| const std::string kRequestBody = "Request Body"; |
| |
| mojo::PendingRemote<mojom::DataPipeGetter> data_pipe_getter_remote; |
| auto data_pipe_getter = std::make_unique<TestDataPipeGetter>( |
| kRequestBody, data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| data_pipe_getter->set_start_error(net::ERR_ACCESS_DENIED); |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| set_request_body(std::move(resource_request_body)); |
| |
| EXPECT_EQ(net::ERR_ACCESS_DENIED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| TEST_F(URLLoaderTest, UploadDataPipeClosedEarly) { |
| const std::string kRequestBody = "Request Body"; |
| |
| mojo::PendingRemote<mojom::DataPipeGetter> data_pipe_getter_remote; |
| auto data_pipe_getter = std::make_unique<TestDataPipeGetter>( |
| kRequestBody, data_pipe_getter_remote.InitWithNewPipeAndPassReceiver()); |
| data_pipe_getter->set_pipe_closed_early(true); |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->AppendDataPipe(std::move(data_pipe_getter_remote)); |
| set_request_body(std::move(resource_request_body)); |
| |
| std::string response_body; |
| EXPECT_EQ(net::ERR_FAILED, Load(test_server()->GetURL("/echo"))); |
| } |
| |
| // Tests a request body with a chunked data pipe element. |
| TEST_F(URLLoaderTest, UploadChunkedDataPipe) { |
| const std::string kRequestBody = "Request Body"; |
| |
| TestChunkedDataPipeGetter data_pipe_getter; |
| |
| ResourceRequest request = |
| CreateResourceRequest("POST", test_server()->GetURL("/echo")); |
| request.request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| request.request_body->SetAllowHTTP1ForStreamingUpload(true); |
| request.request_body->SetToChunkedDataPipe( |
| data_pipe_getter.GetDataPipeGetterRemote(), |
| ResourceRequestBody::ReadOnlyOnce(false)); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| mojom::ChunkedDataPipeGetter::GetSizeCallback get_size_callback = |
| data_pipe_getter.WaitForGetSize(); |
| mojo::BlockingCopyFromString(kRequestBody, |
| data_pipe_getter.WaitForStartReading()); |
| std::move(get_size_callback).Run(net::OK, kRequestBody.size()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(kRequestBody, ReadBody()); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, UploadChunkedDataPipeOverHTTP2) { |
| const std::string kRequestBody = "Request Body"; |
| |
| TestChunkedDataPipeGetter data_pipe_getter; |
| |
| ResourceRequest request = CreateResourceRequest( |
| "POST", net::QuicSimpleTestServer::GetFileURL("/echo")); |
| request.request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| request.request_body->SetToChunkedDataPipe( |
| data_pipe_getter.GetDataPipeGetterRemote(), |
| ResourceRequestBody::ReadOnlyOnce(false)); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| mojom::ChunkedDataPipeGetter::GetSizeCallback get_size_callback = |
| data_pipe_getter.WaitForGetSize(); |
| mojo::BlockingCopyFromString(kRequestBody, |
| data_pipe_getter.WaitForStartReading()); |
| std::move(get_size_callback).Run(net::OK, kRequestBody.size()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(kRequestBody, ReadBody()); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, UploadChunkedDataPipeNotAllowHTTP1) { |
| const std::string kRequestBody = "Request Body"; |
| |
| TestChunkedDataPipeGetter data_pipe_getter; |
| |
| auto resource_request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| resource_request_body->SetToChunkedDataPipe( |
| data_pipe_getter.GetDataPipeGetterRemote(), |
| ResourceRequestBody::ReadOnlyOnce(false)); |
| set_request_body(std::move(resource_request_body)); |
| |
| EXPECT_EQ(net::ERR_ALPN_NEGOTIATION_FAILED, |
| Load(test_server()->GetURL("/echo"))); |
| } |
| |
| // Tests a request body with ReadOnceStream. |
| TEST_F(URLLoaderTest, UploadChunkedDataPipeReadOnceStream) { |
| const std::string kRequestBody = "Request Body"; |
| |
| TestChunkedDataPipeGetter data_pipe_getter; |
| |
| ResourceRequest request = |
| CreateResourceRequest("POST", test_server()->GetURL("/echo")); |
| request.request_body = base::MakeRefCounted<ResourceRequestBody>(); |
| request.request_body->SetAllowHTTP1ForStreamingUpload(true); |
| request.request_body->SetToChunkedDataPipe( |
| data_pipe_getter.GetDataPipeGetterRemote(), |
| ResourceRequestBody::ReadOnlyOnce(true)); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| mojom::ChunkedDataPipeGetter::GetSizeCallback get_size_callback = |
| data_pipe_getter.WaitForGetSize(); |
| mojo::BlockingCopyFromString(kRequestBody, |
| data_pipe_getter.WaitForStartReading()); |
| std::move(get_size_callback).Run(net::OK, kRequestBody.size()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(kRequestBody, ReadBody()); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| } |
| |
| // Tests that SSLInfo is not attached to OnComplete messages or the |
| // URLResponseHead when there is no certificate error. |
| TEST_F(URLLoaderTest, NoSSLInfoWithoutCertificateError) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| ASSERT_TRUE(https_server.Start()); |
| set_send_ssl_for_cert_error(); |
| EXPECT_EQ(net::OK, Load(https_server.GetURL("/"))); |
| EXPECT_FALSE(client()->completion_status().ssl_info.has_value()); |
| EXPECT_FALSE(client()->response_head()->ssl_info.has_value()); |
| } |
| |
| // Tests that SSLInfo is not attached to OnComplete messages when the |
| // corresponding option is not set. |
| TEST_F(URLLoaderTest, NoSSLInfoOnComplete) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); |
| ASSERT_TRUE(https_server.Start()); |
| EXPECT_EQ(net::ERR_CERT_DATE_INVALID, Load(https_server.GetURL("/"))); |
| EXPECT_FALSE(client()->completion_status().ssl_info.has_value()); |
| } |
| |
| // Tests that SSLInfo is attached to OnComplete messages when the corresponding |
| // option is set and the certificate error causes the load to fail. |
| TEST_F(URLLoaderTest, SSLInfoOnComplete) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); |
| ASSERT_TRUE(https_server.Start()); |
| set_send_ssl_for_cert_error(); |
| EXPECT_EQ(net::ERR_CERT_DATE_INVALID, Load(https_server.GetURL("/"))); |
| ASSERT_TRUE(client()->completion_status().ssl_info.has_value()); |
| EXPECT_TRUE(client()->completion_status().ssl_info.value().cert); |
| EXPECT_EQ(net::CERT_STATUS_DATE_INVALID, |
| client()->completion_status().ssl_info.value().cert_status); |
| } |
| |
| // Tests that SSLInfo is attached to OnComplete messages and the URLResponseHead |
| // when the corresponding option is set and the certificate error doesn't cause |
| // the load to fail. |
| TEST_F(URLLoaderTest, SSLInfoOnResponseWithCertificateError) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); |
| ASSERT_TRUE(https_server.Start()); |
| set_send_ssl_for_cert_error(); |
| |
| TestURLLoaderNetworkObserver observer; |
| observer.set_ignore_certificate_errors(true); |
| set_network_observer_for_next_request(&observer); |
| |
| EXPECT_EQ(net::OK, Load(https_server.GetURL("/"))); |
| ASSERT_TRUE(client()->completion_status().ssl_info.has_value()); |
| EXPECT_TRUE(client()->completion_status().ssl_info.value().cert); |
| EXPECT_EQ(net::CERT_STATUS_DATE_INVALID, |
| client()->completion_status().ssl_info.value().cert_status); |
| ASSERT_TRUE(client()->response_head()->ssl_info.has_value()); |
| EXPECT_TRUE(client()->response_head()->ssl_info.value().cert); |
| EXPECT_EQ(net::CERT_STATUS_DATE_INVALID, |
| client()->response_head()->ssl_info.value().cert_status); |
| } |
| |
| // Tests that SSLInfo is attached to the URLResponseHead on redirects when the |
| // corresponding option is set and the certificate error doesn't cause the load |
| // to fail. |
| TEST_F(URLLoaderTest, SSLInfoOnRedirectWithCertificateError) { |
| net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_EXPIRED); |
| https_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(https_server.Start()); |
| |
| TestURLLoaderClient client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", https_server.GetURL("/server-redirect?http://foo.test")); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| auto network_context_client = std::make_unique<TestNetworkContextClient>(); |
| context().set_network_context_client(network_context_client.get()); |
| TestURLLoaderNetworkObserver url_loader_network_observer; |
| url_loader_network_observer.set_ignore_certificate_errors(true); |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = |
| mojom::kURLLoadOptionSendSSLInfoWithResponse | |
| mojom::kURLLoadOptionSendSSLInfoForCertificateError; |
| url_loader_options.url_loader_network_observer = |
| url_loader_network_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client.CreateRemote()); |
| |
| client.RunUntilRedirectReceived(); |
| ASSERT_TRUE(client.response_head()->ssl_info.has_value()); |
| EXPECT_TRUE(client.response_head()->ssl_info.value().cert); |
| EXPECT_EQ(net::CERT_STATUS_DATE_INVALID, |
| client.response_head()->ssl_info.value().cert_status); |
| context().set_network_context_client(nullptr); |
| } |
| |
| // Make sure the client can modify headers during a redirect. |
| TEST_F(URLLoaderTest, RedirectModifiedHeaders) { |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/redirect307-to-echo")); |
| request.headers.SetHeader("Header1", "Value1"); |
| request.headers.SetHeader("Header2", "Value2"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // Initial request should only have initial headers. |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers1.find("Header1")->second); |
| EXPECT_EQ("Value2", request_headers1.find("Header2")->second); |
| EXPECT_EQ(request_headers1.end(), request_headers1.find("Header3")); |
| |
| // Overwrite Header2 and add Header3. |
| net::HttpRequestHeaders redirect_headers; |
| redirect_headers.SetHeader("Header2", ""); |
| redirect_headers.SetHeader("Header3", "Value3"); |
| loader->FollowRedirect({}, redirect_headers, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // Redirected request should also have modified headers. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers2.find("Header1")->second); |
| EXPECT_EQ("", request_headers2.find("Header2")->second); |
| EXPECT_EQ("Value3", request_headers2.find("Header3")->second); |
| } |
| |
| TEST_F(URLLoaderTest, RedirectFailsOnModifyUnsafeHeader) { |
| const char* kUnsafeHeaders[] = { |
| net::HttpRequestHeaders::kContentLength, |
| net::HttpRequestHeaders::kHost, |
| net::HttpRequestHeaders::kProxyConnection, |
| net::HttpRequestHeaders::kProxyAuthorization, |
| "Proxy-Foo", |
| }; |
| |
| for (const auto* unsafe_header : kUnsafeHeaders) { |
| TestURLLoaderClient client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/redirect307-to-echo")); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client.CreateRemote()); |
| |
| client.RunUntilRedirectReceived(); |
| |
| net::HttpRequestHeaders redirect_headers; |
| redirect_headers.SetHeader(unsafe_header, "foo"); |
| loader->FollowRedirect({}, redirect_headers, {}, std::nullopt); |
| |
| client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_TRUE(client.has_received_completion()); |
| EXPECT_EQ(net::ERR_INVALID_ARGUMENT, client.completion_status().error_code); |
| } |
| } |
| |
| // Test the client can remove headers during a redirect. |
| TEST_F(URLLoaderTest, RedirectRemoveHeader) { |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/redirect307-to-echo")); |
| request.headers.SetHeader("Header1", "Value1"); |
| request.headers.SetHeader("Header2", "Value2"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // Initial request should only have initial headers. |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers1.find("Header1")->second); |
| EXPECT_EQ("Value2", request_headers1.find("Header2")->second); |
| |
| // Remove Header1. |
| std::vector<std::string> removed_headers = {"Header1"}; |
| loader->FollowRedirect(removed_headers, {}, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // Redirected request should have the updated headers. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Header1")); |
| EXPECT_EQ("Value2", request_headers2.find("Header2")->second); |
| } |
| |
| // Test the client can remove headers and add headers back during a redirect. |
| TEST_F(URLLoaderTest, RedirectRemoveHeaderAndAddItBack) { |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/redirect307-to-echo")); |
| request.headers.SetHeader("Header1", "Value1"); |
| request.headers.SetHeader("Header2", "Value2"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // Initial request should only have initial headers. |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers1.find("Header1")->second); |
| EXPECT_EQ("Value2", request_headers1.find("Header2")->second); |
| |
| // Remove Header1 and add it back using a different value. |
| std::vector<std::string> removed_headers = {"Header1"}; |
| net::HttpRequestHeaders modified_headers; |
| modified_headers.SetHeader("Header1", "NewValue1"); |
| loader->FollowRedirect(removed_headers, modified_headers, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // Redirected request should have the updated headers. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ("NewValue1", request_headers2.find("Header1")->second); |
| EXPECT_EQ("Value2", request_headers2.find("Header2")->second); |
| } |
| |
| // Validate Sec- prefixed headers are handled properly when redirecting from |
| // insecure => secure urls. The Sec-Fetch-Site header should be re-added on the |
| // secure url. |
| TEST_F(URLLoaderTest, UpgradeAddsSecHeaders) { |
| // Set up a redirect to signal we will go from insecure => secure. |
| GURL url = test_server()->GetURL( |
| kInsecureHost, |
| "/server-redirect?" + test_server()->GetURL("/echo").spec()); |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // The initial request is received when the redirect before it has been |
| // followed. It should have no added Sec- headers as it is not trustworthy. |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ(request_headers1.end(), request_headers1.find("Sec-Fetch-Site")); |
| EXPECT_EQ(request_headers1.end(), request_headers1.find("Sec-Fetch-User")); |
| |
| // Now follow the redirect to the final destination and validate again. |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // The Sec-Fetch-Site header should have been added again since we are now on |
| // a trustworthy url again. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ("cross-site", request_headers2.find("Sec-Fetch-Site")->second); |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-Fetch-User")); |
| } |
| |
| // Validate Sec- prefixed headers are properly handled when redirecting from |
| // secure => insecure urls. All Sec-CH- and Sec-Fetch- prefixed |
| // headers should be removed. |
| TEST_F(URLLoaderTest, DowngradeRemovesSecHeaders) { |
| // Set up a redirect to signal we will go from secure => insecure. |
| GURL url = test_server()->GetURL( |
| "/server-redirect?" + |
| test_server()->GetURL(kInsecureHost, "/echo").spec()); |
| |
| // Add some initial headers to ensure the right ones are removed and |
| // everything else is left alone. |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.headers.SetHeader("Sec-CH-UA", "Value1"); |
| request.headers.SetHeader("Sec-Other-Type", "Value2"); |
| request.headers.SetHeader("Other-Header", "Value3"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // The initial request is received when the redirect before it has been |
| // followed. It should have all the Sec- headers as it is trustworthy. It |
| // should also have added a Sec-Fetch-Site header |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers1.find("Sec-CH-UA")->second); |
| EXPECT_EQ("Value2", request_headers1.find("Sec-Other-Type")->second); |
| EXPECT_EQ("Value3", request_headers1.find("Other-Header")->second); |
| EXPECT_EQ("same-origin", request_headers1.find("Sec-Fetch-Site")->second); |
| EXPECT_EQ(request_headers1.end(), request_headers1.find("Sec-Fetch-User")); |
| |
| // Now follow the redirect to the final destination and validate again. |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // We should have removed our special Sec-CH- and Sec-Fetch- prefixed headers |
| // and left the others. We are now operating on an un-trustworthy context. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-CH-UA")); |
| EXPECT_EQ("Value2", request_headers2.find("Sec-Other-Type")->second); |
| EXPECT_EQ("Value3", request_headers2.find("Other-Header")->second); |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-Fetch-Site")); |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-Fetch-User")); |
| } |
| |
| // Validate Sec- prefixed headers are properly handled when redirecting from |
| // secure => insecure => secure urls.The headers on insecure |
| // urls should be removed and Sec-Fetch-Site should be re-added on secure ones. |
| TEST_F(URLLoaderTest, RedirectChainRemovesAndAddsSecHeaders) { |
| // Set up a redirect to signal we will go from secure => insecure => secure. |
| GURL insecure_upgrade_url = test_server()->GetURL( |
| kInsecureHost, |
| "/server-redirect?" + test_server()->GetURL("/echo").spec()); |
| GURL url = |
| test_server()->GetURL("/server-redirect?" + insecure_upgrade_url.spec()); |
| |
| // Add some initial headers to ensure the right ones are removed and |
| // everything else is left alone. |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.headers.SetHeader("Sec-CH-UA", "Value1"); |
| request.headers.SetHeader("Sec-Other-Type", "Value2"); |
| request.headers.SetHeader("Other-Header", "Value3"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| // The initial request is received when the redirect before it has been |
| // followed. It should have all the Sec- headers as it is trustworthy. It |
| // should also have added a Sec-Fetch-Site header |
| const auto& request_headers1 = sent_request().headers; |
| EXPECT_EQ("Value1", request_headers1.find("Sec-CH-UA")->second); |
| EXPECT_EQ("Value2", request_headers1.find("Sec-Other-Type")->second); |
| EXPECT_EQ("Value3", request_headers1.find("Other-Header")->second); |
| EXPECT_EQ("same-origin", request_headers1.find("Sec-Fetch-Site")->second); |
| EXPECT_EQ(request_headers1.end(), request_headers1.find("Sec-Fetch-User")); |
| |
| // Follow our redirect and then verify again. |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| client()->ClearHasReceivedRedirect(); |
| client()->RunUntilRedirectReceived(); |
| |
| // Special Sec-CH- and Sec-Fetch- prefixed headers should have been removed |
| // and the others left alone. We are now operating on an un-trustworthy |
| // context. |
| const auto& request_headers2 = sent_request().headers; |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-CH-UA")); |
| EXPECT_EQ("Value2", request_headers2.find("Sec-Other-Type")->second); |
| EXPECT_EQ("Value3", request_headers2.find("Other-Header")->second); |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-Fetch-Site")); |
| EXPECT_EQ(request_headers2.end(), request_headers2.find("Sec-Fetch-User")); |
| |
| // Now follow the final redirect back to a trustworthy destination and |
| // re-validate. |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| const auto& request_headers3 = sent_request().headers; |
| EXPECT_EQ(request_headers3.end(), request_headers3.find("Sec-CH-UA")); |
| EXPECT_EQ("Value2", request_headers3.find("Sec-Other-Type")->second); |
| EXPECT_EQ("Value3", request_headers3.find("Other-Header")->second); |
| EXPECT_EQ("cross-site", request_headers3.find("Sec-Fetch-Site")->second); |
| EXPECT_EQ(request_headers3.end(), request_headers3.find("Sec-Fetch-User")); |
| } |
| |
| // Validate Sec-Fetch-User header is properly handled. |
| TEST_F(URLLoaderTest, RedirectSecHeadersUser) { |
| GURL url = test_server()->GetURL("/server-redirect?" + |
| test_server()->GetURL("/echo").spec()); |
| |
| // Add some initial headers to ensure the right ones are removed and |
| // everything else is left alone. |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.trusted_params->has_user_activation = true; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| const auto& request_headers = sent_request().headers; |
| EXPECT_EQ("same-origin", request_headers.find("Sec-Fetch-Site")->second); |
| EXPECT_EQ("?1", request_headers.find("Sec-Fetch-User")->second); |
| } |
| |
| // Validate Sec-Fetch-User header cannot be modified by manually set the value. |
| TEST_F(URLLoaderTest, RedirectDirectlyModifiedSecHeadersUser) { |
| GURL url = test_server()->GetURL("/server-redirect?" + |
| test_server()->GetURL("/echo").spec()); |
| |
| // Try to modify `Sec-Fetch-User` directly. |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.headers.SetHeader("Sec-Fetch-User", "?1"); |
| request.headers.SetHeader("Sec-Fetch-Dest", "embed"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| const auto& request_headers = sent_request().headers; |
| EXPECT_EQ(request_headers.end(), request_headers.find("Sec-Fetch-User")); |
| EXPECT_EQ("empty", request_headers.find("Sec-Fetch-Dest")->second); |
| } |
| |
| // A mock URLRequestJob which simulates an HTTPS request with a certificate |
| // error. |
| class MockHTTPSURLRequestJob : public net::URLRequestTestJob { |
| public: |
| MockHTTPSURLRequestJob(net::URLRequest* request, |
| const std::string& response_headers, |
| const std::string& response_data, |
| bool auto_advance) |
| : net::URLRequestTestJob(request, |
| response_headers, |
| response_data, |
| auto_advance) {} |
| |
| MockHTTPSURLRequestJob(const MockHTTPSURLRequestJob&) = delete; |
| MockHTTPSURLRequestJob& operator=(const MockHTTPSURLRequestJob&) = delete; |
| |
| ~MockHTTPSURLRequestJob() override = default; |
| |
| // net::URLRequestTestJob: |
| void GetResponseInfo(net::HttpResponseInfo* info) override { |
| // Get the original response info, but override the SSL info. |
| net::URLRequestJob::GetResponseInfo(info); |
| info->ssl_info.cert = |
| net::ImportCertFromFile(net::GetTestCertsDirectory(), "ok_cert.pem"); |
| info->ssl_info.cert_status = net::CERT_STATUS_DATE_INVALID; |
| } |
| }; |
| |
| class MockHTTPSJobURLRequestInterceptor : public net::URLRequestInterceptor { |
| public: |
| MockHTTPSJobURLRequestInterceptor() {} |
| ~MockHTTPSJobURLRequestInterceptor() override {} |
| |
| // net::URLRequestInterceptor: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| return std::make_unique<MockHTTPSURLRequestJob>(request, std::string(), |
| "dummy response", true); |
| } |
| }; |
| |
| // Tests that |cert_status| is set on the resource response. |
| TEST_F(URLLoaderTest, CertStatusOnResponse) { |
| net::URLRequestFilter::GetInstance()->ClearHandlers(); |
| net::URLRequestFilter::GetInstance()->AddHostnameInterceptor( |
| "https", "example.test", |
| std::unique_ptr<net::URLRequestInterceptor>( |
| new MockHTTPSJobURLRequestInterceptor())); |
| |
| EXPECT_EQ(net::OK, Load(GURL("https://example.test/"))); |
| EXPECT_EQ(net::CERT_STATUS_DATE_INVALID, |
| client()->response_head()->cert_status); |
| } |
| |
| // Verifies if URLLoader works well with ResourceScheduler. |
| TEST_F(URLLoaderTest, ResourceSchedulerIntegration) { |
| // ResourceScheduler limits the number of connections for the same host |
| // by 6. |
| constexpr int kRepeat = 6; |
| constexpr char kPath[] = "/hello.html"; |
| |
| net::EmbeddedTestServer server; |
| // This is needed to stall all requests to the server. |
| net::test_server::ControllableHttpResponse response_controllers[kRepeat] = { |
| {&server, kPath}, {&server, kPath}, {&server, kPath}, |
| {&server, kPath}, {&server, kPath}, {&server, kPath}, |
| }; |
| |
| ASSERT_TRUE(server.Start()); |
| |
| ResourceRequest request = CreateResourceRequest("GET", server.GetURL(kPath)); |
| request.load_flags = net::LOAD_DISABLE_CACHE; |
| request.priority = net::IDLE; |
| |
| // Fill up the ResourceScheduler with delayable requests. |
| std::vector< |
| std::pair<std::unique_ptr<URLLoader>, mojo::Remote<mojom::URLLoader>>> |
| loaders; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| for (int i = 0; i < kRepeat; ++i) { |
| TestURLLoaderClient client; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| |
| std::unique_ptr<URLLoader> url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), NeverInvokedDeleteLoaderCallback(), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client.CreateRemote()); |
| |
| loaders.emplace_back(std::move(url_loader), std::move(loader_remote)); |
| } |
| |
| ASSERT_TRUE(base::test::RunUntil([&]() { |
| return std::ranges::all_of(loaders, [](const auto& pair) { |
| return pair.first->GetLoadState() == net::LOAD_STATE_WAITING_FOR_RESPONSE; |
| }); |
| })) << "Timeout waiting for all loaders to be in state " |
| "net::LOAD_STATE_WAITING_FOR_RESPONSE"; |
| |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> loader = URLLoaderOptions().MakeURLLoader( |
| context(), NeverInvokedDeleteLoaderCallback(), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Make sure that the ResourceScheduler throttles this request. |
| EXPECT_EQ(net::LOAD_STATE_WAITING_FOR_DELEGATE, loader->GetLoadState()); |
| |
| loader->SetPriority(net::HIGHEST, 0 /* intra_priority_value */); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Make sure that the ResourceScheduler stops throtting. |
| EXPECT_EQ(net::LOAD_STATE_WAITING_FOR_AVAILABLE_SOCKET, |
| loader->GetLoadState()); |
| } |
| |
| // This tests that case where a read pipe is closed while there's a post task to |
| // invoke ReadMore. |
| TEST_F(URLLoaderTest, ReadPipeClosedWhileReadTaskPosted) { |
| AddEternalSyncReadsInterceptor(); |
| |
| ResourceRequest request = CreateResourceRequest( |
| "GET", EternalSyncReadsInterceptor::GetSingleByteURL()); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| client()->response_body_release(); |
| delete_run_loop.Run(); |
| } |
| |
| class FakeSSLPrivateKeyImpl : public network::mojom::SSLPrivateKey { |
| public: |
| explicit FakeSSLPrivateKeyImpl( |
| scoped_refptr<net::SSLPrivateKey> ssl_private_key) |
| : ssl_private_key_(std::move(ssl_private_key)) {} |
| |
| FakeSSLPrivateKeyImpl(const FakeSSLPrivateKeyImpl&) = delete; |
| FakeSSLPrivateKeyImpl& operator=(const FakeSSLPrivateKeyImpl&) = delete; |
| |
| ~FakeSSLPrivateKeyImpl() override {} |
| |
| // network::mojom::SSLPrivateKey: |
| void Sign(uint16_t algorithm, |
| const std::vector<uint8_t>& input, |
| network::mojom::SSLPrivateKey::SignCallback callback) override { |
| base::span<const uint8_t> input_span(input); |
| ssl_private_key_->Sign( |
| algorithm, input_span, |
| base::BindOnce(&FakeSSLPrivateKeyImpl::Callback, base::Unretained(this), |
| std::move(callback))); |
| } |
| |
| private: |
| void Callback(network::mojom::SSLPrivateKey::SignCallback callback, |
| net::Error net_error, |
| const std::vector<uint8_t>& signature) { |
| std::move(callback).Run(static_cast<int32_t>(net_error), signature); |
| } |
| |
| scoped_refptr<net::SSLPrivateKey> ssl_private_key_; |
| }; |
| |
| using CookieAccessType = mojom::CookieAccessDetails::Type; |
| |
| class MockCookieObserver : public network::mojom::CookieAccessObserver { |
| public: |
| explicit MockCookieObserver( |
| std::optional<CookieAccessType> access_type = std::nullopt) |
| : access_type_(access_type) {} |
| ~MockCookieObserver() override = default; |
| |
| struct CookieDetails { |
| CookieDetails(const mojom::CookieAccessDetailsPtr& details, |
| const mojom::CookieOrLineWithAccessResultPtr& cookie) |
| : type(details->type), |
| cookie_or_line(std::move(cookie->cookie_or_line)), |
| is_include(cookie->access_result.status.IsInclude()), |
| url(details->url), |
| status(cookie->access_result.status) {} |
| |
| CookieAccessType type; |
| mojom::CookieOrLinePtr cookie_or_line; |
| bool is_include; |
| |
| // The full details are available for the tests to query manually, but |
| // they are not covered by operator== (and testing::ElementsAre). |
| GURL url; |
| net::CookieInclusionStatus status; |
| }; |
| |
| mojo::PendingRemote<mojom::CookieAccessObserver> GetRemote() { |
| mojo::PendingRemote<mojom::CookieAccessObserver> remote; |
| receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| void OnCookiesAccessed(std::vector<network::mojom::CookieAccessDetailsPtr> |
| details_vector) override { |
| for (auto& details : details_vector) { |
| if (access_type_ && access_type_ != details->type) { |
| continue; |
| } |
| |
| for (const auto& cookie_with_status : details->cookie_list) { |
| observed_cookies_.emplace_back(details, cookie_with_status); |
| } |
| } |
| if (wait_for_cookie_count_ && |
| observed_cookies().size() >= wait_for_cookie_count_) { |
| std::move(wait_for_cookies_quit_closure_).Run(); |
| } |
| } |
| |
| void WaitForCookies(size_t cookie_count) { |
| if (observed_cookies_.size() < cookie_count) { |
| wait_for_cookie_count_ = cookie_count; |
| base::RunLoop run_loop; |
| wait_for_cookies_quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| EXPECT_EQ(observed_cookies_.size(), cookie_count); |
| } |
| |
| void Clone( |
| mojo::PendingReceiver<mojom::CookieAccessObserver> observer) override { |
| receivers_.Add(this, std::move(observer)); |
| } |
| |
| const std::vector<CookieDetails>& observed_cookies() { |
| return observed_cookies_; |
| } |
| |
| private: |
| std::optional<CookieAccessType> access_type_; |
| size_t wait_for_cookie_count_ = 0; |
| base::OnceClosure wait_for_cookies_quit_closure_; |
| std::vector<CookieDetails> observed_cookies_; |
| mojo::ReceiverSet<mojom::CookieAccessObserver> receivers_; |
| }; |
| |
| MATCHER_P3(MatchesCookieDetails, type, cookie_or_line, is_include, "") { |
| return testing::ExplainMatchResult( |
| testing::AllOf( |
| testing::Field(&MockCookieObserver::CookieDetails::type, type), |
| testing::Field(&MockCookieObserver::CookieDetails::cookie_or_line, |
| cookie_or_line), |
| testing::Field(&MockCookieObserver::CookieDetails::is_include, |
| is_include)), |
| arg, result_listener); |
| } |
| |
| class MockTrustTokenObserver : public network::mojom::TrustTokenAccessObserver { |
| public: |
| MockTrustTokenObserver() = default; |
| ~MockTrustTokenObserver() override = default; |
| |
| struct TrustTokenDetails { |
| explicit TrustTokenDetails( |
| const mojom::TrustTokenAccessDetailsPtr& details) { |
| switch (details->which()) { |
| case mojom::TrustTokenAccessDetails::Tag::kIssuance: |
| type = mojom::TrustTokenOperationType::kIssuance; |
| origin = details->get_issuance()->origin; |
| issuer = details->get_issuance()->issuer; |
| blocked = details->get_issuance()->blocked; |
| break; |
| case mojom::TrustTokenAccessDetails::Tag::kRedemption: |
| type = mojom::TrustTokenOperationType::kRedemption; |
| origin = details->get_redemption()->origin; |
| issuer = details->get_redemption()->issuer; |
| blocked = details->get_redemption()->blocked; |
| break; |
| case mojom::TrustTokenAccessDetails::Tag::kSigning: |
| type = mojom::TrustTokenOperationType::kSigning; |
| origin = details->get_signing()->origin; |
| blocked = details->get_signing()->blocked; |
| break; |
| } |
| } |
| |
| url::Origin origin; |
| mojom::TrustTokenOperationType type; |
| std::optional<url::Origin> issuer; |
| bool blocked; |
| }; |
| |
| mojo::PendingRemote<mojom::TrustTokenAccessObserver> GetRemote() { |
| mojo::PendingRemote<mojom::TrustTokenAccessObserver> remote; |
| receivers_.Add(this, remote.InitWithNewPipeAndPassReceiver()); |
| return remote; |
| } |
| |
| void OnTrustTokensAccessed( |
| mojom::TrustTokenAccessDetailsPtr details) override { |
| observed_tokens_.emplace_back(details); |
| if (wait_for_token_count_ && |
| observed_tokens().size() >= wait_for_token_count_) { |
| std::move(wait_for_tokens_quit_closure_).Run(); |
| } |
| } |
| |
| void WaitForTrustTokens(size_t token_count) { |
| if (observed_tokens_.size() < token_count) { |
| wait_for_token_count_ = token_count; |
| base::RunLoop run_loop; |
| wait_for_tokens_quit_closure_ = run_loop.QuitClosure(); |
| run_loop.Run(); |
| } |
| EXPECT_EQ(observed_tokens_.size(), token_count); |
| } |
| |
| void Clone(mojo::PendingReceiver<mojom::TrustTokenAccessObserver> observer) |
| override { |
| receivers_.Add(this, std::move(observer)); |
| } |
| |
| const std::vector<TrustTokenDetails>& observed_tokens() { |
| return observed_tokens_; |
| } |
| |
| private: |
| size_t wait_for_token_count_ = 0; |
| base::OnceClosure wait_for_tokens_quit_closure_; |
| std::vector<TrustTokenDetails> observed_tokens_; |
| mojo::ReceiverSet<mojom::TrustTokenAccessObserver> receivers_; |
| }; |
| |
| MATCHER_P3(MatchesTrustTokenDetails, origin, issuer, blocked, "") { |
| return testing::ExplainMatchResult( |
| testing::AllOf( |
| testing::Field(&MockTrustTokenObserver::TrustTokenDetails::origin, |
| origin), |
| testing::Field(&MockTrustTokenObserver::TrustTokenDetails::issuer, |
| issuer), |
| testing::Field(&MockTrustTokenObserver::TrustTokenDetails::blocked, |
| blocked)), |
| arg, result_listener); |
| } |
| |
| // Responds certificate request with previously set responses and to HTTP auth |
| // challenges as well. |
| class ClientCertAndHttpAuthObserver : public TestURLLoaderNetworkObserver { |
| public: |
| ClientCertAndHttpAuthObserver() = default; |
| ~ClientCertAndHttpAuthObserver() override = default; |
| |
| enum class CertificateResponse { |
| INVALID = -1, |
| URL_LOADER_REQUEST_CANCELLED, |
| CANCEL_CERTIFICATE_SELECTION, |
| NULL_CERTIFICATE, |
| VALID_CERTIFICATE_SIGNATURE, |
| INVALID_CERTIFICATE_SIGNATURE, |
| DESTROY_CLIENT_CERT_RESPONDER, |
| }; |
| |
| enum class CredentialsResponse { |
| NO_CREDENTIALS, |
| CORRECT_CREDENTIALS, |
| INCORRECT_CREDENTIALS_THEN_CORRECT_ONES, |
| CORRECT_PROXY_CREDENTIALS, |
| }; |
| void OnAuthRequired( |
| const std::optional<base::UnguessableToken>& window_id, |
| int32_t request_id, |
| const GURL& url, |
| bool first_auth_attempt, |
| const net::AuthChallengeInfo& auth_info, |
| const scoped_refptr<net::HttpResponseHeaders>& head_headers, |
| mojo::PendingRemote<mojom::AuthChallengeResponder> |
| auth_challenge_responder) override { |
| switch (credentials_response_) { |
| case CredentialsResponse::NO_CREDENTIALS: |
| auth_credentials_ = std::nullopt; |
| break; |
| case CredentialsResponse::CORRECT_CREDENTIALS: |
| ASSERT_FALSE(auth_info.is_proxy); |
| auth_credentials_ = net::AuthCredentials(u"USER", u"PASS"); |
| break; |
| case CredentialsResponse::INCORRECT_CREDENTIALS_THEN_CORRECT_ONES: |
| auth_credentials_ = net::AuthCredentials(u"USER", u"FAIL"); |
| credentials_response_ = CredentialsResponse::CORRECT_CREDENTIALS; |
| break; |
| case CredentialsResponse::CORRECT_PROXY_CREDENTIALS: |
| ASSERT_TRUE(auth_info.is_proxy); |
| auth_credentials_ = net::AuthCredentials(u"PROXY_USER", u"PROXY_PASS"); |
| break; |
| } |
| mojo::Remote<mojom::AuthChallengeResponder> auth_challenge_responder_remote( |
| std::move(auth_challenge_responder)); |
| auth_challenge_responder_remote->OnAuthCredentials(auth_credentials_); |
| ++on_auth_required_call_counter_; |
| last_seen_response_headers_ = head_headers; |
| } |
| |
| void OnCertificateRequested( |
| const std::optional<base::UnguessableToken>& window_id, |
| const scoped_refptr<net::SSLCertRequestInfo>& cert_info, |
| mojo::PendingRemote<mojom::ClientCertificateResponder> |
| client_cert_responder_remote) override { |
| mojo::Remote<mojom::ClientCertificateResponder> client_cert_responder( |
| std::move(client_cert_responder_remote)); |
| switch (certificate_response_) { |
| case CertificateResponse::INVALID: |
| NOTREACHED(); |
| case CertificateResponse::URL_LOADER_REQUEST_CANCELLED: |
| ASSERT_TRUE(url_loader_remote_); |
| url_loader_remote_->reset(); |
| break; |
| case CertificateResponse::CANCEL_CERTIFICATE_SELECTION: |
| client_cert_responder->CancelRequest(); |
| break; |
| case CertificateResponse::NULL_CERTIFICATE: |
| client_cert_responder->ContinueWithoutCertificate(); |
| break; |
| case CertificateResponse::VALID_CERTIFICATE_SIGNATURE: |
| case CertificateResponse::INVALID_CERTIFICATE_SIGNATURE: |
| client_cert_responder->ContinueWithCertificate( |
| std::move(certificate_), provider_name_, algorithm_preferences_, |
| std::move(ssl_private_key_remote_)); |
| break; |
| case CertificateResponse::DESTROY_CLIENT_CERT_RESPONDER: |
| // Send no response and let the local variable be destroyed. |
| break; |
| } |
| ++on_certificate_requested_counter_; |
| } |
| |
| void set_certificate_response(CertificateResponse certificate_response) { |
| certificate_response_ = certificate_response; |
| } |
| |
| void set_private_key(scoped_refptr<net::SSLPrivateKey> ssl_private_key) { |
| ssl_private_key_ = std::move(ssl_private_key); |
| provider_name_ = ssl_private_key_->GetProviderName(); |
| algorithm_preferences_ = ssl_private_key_->GetAlgorithmPreferences(); |
| mojo::MakeSelfOwnedReceiver( |
| std::make_unique<FakeSSLPrivateKeyImpl>(std::move(ssl_private_key_)), |
| ssl_private_key_remote_.InitWithNewPipeAndPassReceiver()); |
| } |
| |
| void set_certificate(scoped_refptr<net::X509Certificate> certificate) { |
| certificate_ = std::move(certificate); |
| } |
| |
| int on_certificate_requested_counter() { |
| return on_certificate_requested_counter_; |
| } |
| |
| void set_url_loader_remote( |
| mojo::Remote<mojom::URLLoader>* url_loader_remote) { |
| url_loader_remote_ = url_loader_remote; |
| } |
| |
| void set_credentials_response(CredentialsResponse credentials_response) { |
| credentials_response_ = credentials_response; |
| } |
| |
| int on_auth_required_call_counter() { return on_auth_required_call_counter_; } |
| |
| net::HttpResponseHeaders* last_seen_response_headers() { |
| return last_seen_response_headers_.get(); |
| } |
| |
| private: |
| CredentialsResponse credentials_response_ = |
| CredentialsResponse::NO_CREDENTIALS; |
| std::optional<net::AuthCredentials> auth_credentials_; |
| int on_auth_required_call_counter_ = 0; |
| scoped_refptr<net::HttpResponseHeaders> last_seen_response_headers_; |
| CertificateResponse certificate_response_ = CertificateResponse::INVALID; |
| scoped_refptr<net::SSLPrivateKey> ssl_private_key_; |
| scoped_refptr<net::X509Certificate> certificate_; |
| mojo::PendingRemote<network::mojom::SSLPrivateKey> ssl_private_key_remote_; |
| std::string provider_name_; |
| std::vector<uint16_t> algorithm_preferences_; |
| int on_certificate_requested_counter_ = 0; |
| raw_ptr<mojo::Remote<mojom::URLLoader>> url_loader_remote_ = nullptr; |
| }; |
| |
| TEST_F(URLLoaderTest, SetAuth) { |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::CORRECT_CREDENTIALS); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kTestAuthURL)); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(200, headers->response_code()); |
| EXPECT_EQ(1, client_auth_observer.on_auth_required_call_counter()); |
| ASSERT_FALSE(url_loader); |
| EXPECT_FALSE(client()->response_head()->auth_challenge_info.has_value()); |
| } |
| |
| TEST_F(URLLoaderTest, CancelAuth) { |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::NO_CREDENTIALS); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kTestAuthURL)); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(401, headers->response_code()); |
| EXPECT_EQ(1, client_auth_observer.on_auth_required_call_counter()); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| TEST_F(URLLoaderTest, TwoChallenges) { |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse:: |
| INCORRECT_CREDENTIALS_THEN_CORRECT_ONES); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kTestAuthURL)); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(200, headers->response_code()); |
| EXPECT_EQ(2, client_auth_observer.on_auth_required_call_counter()); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| TEST_F(URLLoaderTest, NoAuthRequiredForFavicon) { |
| constexpr char kFaviconTestPage[] = "/has_favicon.html"; |
| |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::CORRECT_CREDENTIALS); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kFaviconTestPage)); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(200, headers->response_code()); |
| // No auth required for favicon. |
| EXPECT_EQ(0, client_auth_observer.on_auth_required_call_counter()); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| TEST_F(URLLoaderTest, HttpAuthResponseHeadersAvailable) { |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::CORRECT_CREDENTIALS); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kTestAuthURL)); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| EXPECT_EQ(1, client_auth_observer.on_auth_required_call_counter()); |
| |
| auto* auth_required_headers = |
| client_auth_observer.last_seen_response_headers(); |
| ASSERT_TRUE(auth_required_headers); |
| EXPECT_EQ(auth_required_headers->response_code(), 401); |
| } |
| |
| // Tests that `did_use_server_http_auth` is present on the response when a |
| // request receives and responds to an authentication challenge. |
| TEST_F(URLLoaderTest, ServerHttpAuthFlagSet) { |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::CORRECT_CREDENTIALS); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL(kTestAuthURL)); |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| EXPECT_TRUE(client()->response_head()->did_use_server_http_auth); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(200, headers->response_code()); |
| EXPECT_EQ(1, client_auth_observer.on_auth_required_call_counter()); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| // Tests that `did_use_server_http_auth` is not present on the response when a |
| // request does not use an authentication challenge. |
| TEST_F(URLLoaderTest, ServerHttpAuthFlagNotSet) { |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->has_received_response()); |
| EXPECT_FALSE(client()->has_received_completion()); |
| EXPECT_FALSE(client()->response_head()->did_use_server_http_auth); |
| |
| // Spin the message loop until the delete callback is invoked, and then delete |
| // the URLLoader. |
| delete_run_loop.Run(); |
| |
| client()->RunUntilComplete(); |
| EXPECT_TRUE(client()->has_received_completion()); |
| scoped_refptr<net::HttpResponseHeaders> headers = |
| client()->response_head()->headers; |
| ASSERT_TRUE(headers); |
| EXPECT_EQ(200, headers->response_code()); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| // Tests that `did_use_server_http_auth` is not present on the response when a |
| // request receives and responds to a proxy authentication challenge. |
| TEST_F(URLLoaderTest, ServerHttpAuthFlagNotSetForProxy) { |
| net::EmbeddedTestServer proxy_server(net::EmbeddedTestServer::TYPE_HTTP); |
| net::test_server::RegisterProxyBasicAuthHandler(proxy_server, "PROXY_USER", |
| "PROXY_PASS"); |
| proxy_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(proxy_server.Start()); |
| |
| net::URLRequestContextBuilder context_builder; |
| context_builder.set_proxy_resolution_service( |
| net::ConfiguredProxyResolutionService::CreateFixedFromPacResultForTest( |
| "PROXY " + proxy_server.host_port_pair().ToString(), |
| TRAFFIC_ANNOTATION_FOR_TESTS)); |
| auto test_network_delegate = std::make_unique<net::TestNetworkDelegate>(); |
| unowned_test_network_delegate_ = test_network_delegate.get(); |
| context_builder.set_network_delegate(std::move(test_network_delegate)); |
| context_builder.set_client_socket_factory_for_testing(GetSocketFactory()); |
| context().set_url_request_context(nullptr); |
| url_request_context_ = context_builder.Build(); |
| context().set_url_request_context(url_request_context_.get()); |
| |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse:: |
| CORRECT_PROXY_CREDENTIALS); |
| |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL(kHostnameWithAliases, "/hello.html")); |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // This should be false as we do not set the flag for proxy auth. |
| EXPECT_FALSE(client()->response_head()->did_use_server_http_auth); |
| ASSERT_FALSE(url_loader); |
| } |
| |
| // Make sure the client can't call FollowRedirect if there's no pending |
| // redirect. |
| TEST_F(URLLoaderTest, FollowRedirectTwice) { |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/redirect307-to-echo")); |
| request.headers.SetHeader("Header1", "Value1"); |
| request.headers.SetHeader("Header2", "Value2"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| |
| url_loader->FollowRedirect({}, {}, {}, std::nullopt); |
| EXPECT_NOTREACHED_DEATH(url_loader->FollowRedirect({}, {}, {}, std::nullopt)); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| } |
| |
| class TestSSLPrivateKey : public net::SSLPrivateKey { |
| public: |
| explicit TestSSLPrivateKey(scoped_refptr<net::SSLPrivateKey> key) |
| : key_(std::move(key)) {} |
| |
| TestSSLPrivateKey(const TestSSLPrivateKey&) = delete; |
| TestSSLPrivateKey& operator=(const TestSSLPrivateKey&) = delete; |
| |
| void set_fail_signing(bool fail_signing) { fail_signing_ = fail_signing; } |
| int sign_count() const { return sign_count_; } |
| |
| std::string GetProviderName() override { return key_->GetProviderName(); } |
| std::vector<uint16_t> GetAlgorithmPreferences() override { |
| return key_->GetAlgorithmPreferences(); |
| } |
| void Sign(uint16_t algorithm, |
| base::span<const uint8_t> input, |
| SignCallback callback) override { |
| sign_count_++; |
| if (fail_signing_) { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(callback), |
| net::ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED, |
| std::vector<uint8_t>())); |
| } else { |
| key_->Sign(algorithm, input, std::move(callback)); |
| } |
| } |
| |
| private: |
| ~TestSSLPrivateKey() override = default; |
| |
| scoped_refptr<net::SSLPrivateKey> key_; |
| bool fail_signing_ = false; |
| int sign_count_ = 0; |
| }; |
| |
| #if !BUILDFLAG(IS_IOS) |
| TEST_F(URLLoaderTest, ClientAuthRespondTwice) { |
| // This tests that one URLLoader can handle two client cert requests. |
| |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| |
| net::EmbeddedTestServer test_server_1(net::EmbeddedTestServer::TYPE_HTTPS); |
| test_server_1.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server_1.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server_1.Start()); |
| |
| net::EmbeddedTestServer test_server_2(net::EmbeddedTestServer::TYPE_HTTPS); |
| test_server_2.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server_2.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server_2.Start()); |
| |
| std::unique_ptr<net::FakeClientCertIdentity> identity = |
| net::FakeClientCertIdentity::CreateFromCertAndKeyFiles( |
| net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8"); |
| ASSERT_TRUE(identity); |
| scoped_refptr<TestSSLPrivateKey> private_key = |
| base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| VALID_CERTIFICATE_SIGNATURE); |
| client_cert_observer.set_private_key(private_key); |
| client_cert_observer.set_certificate(identity->certificate()); |
| |
| // Create a request to server_1 that will redirect to server_2 |
| ResourceRequest request = CreateResourceRequest( |
| "GET", |
| test_server_1.GetURL("/server-redirect-307?" + |
| base::EscapeQueryParamValue( |
| test_server_2.GetURL("/echo").spec(), true))); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| EXPECT_EQ(0, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_EQ(0, private_key->sign_count()); |
| |
| client()->RunUntilRedirectReceived(); |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| // MockNetworkServiceClient gives away the private key when it invokes |
| // ContinueWithCertificate, so we have to give it the key again. |
| client_cert_observer.set_private_key(private_key); |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| EXPECT_EQ(2, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_EQ(2, private_key->sign_count()); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthDestroyResponder) { |
| // When URLLoader receives no message from the ClientCertificateResponder and |
| // its connection errors out, we expect the request to be canceled rather than |
| // just hang. |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| DESTROY_CLIENT_CERT_RESPONDER); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| client_cert_observer.set_url_loader_remote(&loader); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED, |
| client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthCancelConnection) { |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| URL_LOADER_REQUEST_CANCELLED); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| client_cert_observer.set_url_loader_remote(&loader); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(net::ERR_FAILED, client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthCancelCertificateSelection) { |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| CANCEL_CERTIFICATE_SELECTION); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_EQ(net::ERR_SSL_CLIENT_AUTH_CERT_NEEDED, |
| client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthNoCertificate) { |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| |
| // TLS 1.3 client auth errors show up post-handshake, resulting in a read |
| // error which on Windows causes the socket to shutdown immediately before the |
| // error is read. |
| // TODO(crbug.com/41427061): Add support for testing this in TLS 1.3. |
| ssl_config.version_max = net::SSL_PROTOCOL_VERSION_TLS1_2; |
| |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse::NULL_CERTIFICATE); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_EQ(net::ERR_BAD_SSL_CLIENT_AUTH_CERT, |
| client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthCertificateWithValidSignature) { |
| std::unique_ptr<net::FakeClientCertIdentity> identity = |
| net::FakeClientCertIdentity::CreateFromCertAndKeyFiles( |
| net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8"); |
| ASSERT_TRUE(identity); |
| scoped_refptr<TestSSLPrivateKey> private_key = |
| base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key()); |
| |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| VALID_CERTIFICATE_SIGNATURE); |
| client_cert_observer.set_private_key(private_key); |
| scoped_refptr<net::X509Certificate> certificate = |
| test_server.GetCertificate(); |
| client_cert_observer.set_certificate(std::move(certificate)); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| // The private key should have been used. |
| EXPECT_EQ(1, private_key->sign_count()); |
| } |
| |
| TEST_F(URLLoaderTest, ClientAuthCertificateWithInvalidSignature) { |
| std::unique_ptr<net::FakeClientCertIdentity> identity = |
| net::FakeClientCertIdentity::CreateFromCertAndKeyFiles( |
| net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8"); |
| ASSERT_TRUE(identity); |
| scoped_refptr<TestSSLPrivateKey> private_key = |
| base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key()); |
| private_key->set_fail_signing(true); |
| |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| VALID_CERTIFICATE_SIGNATURE); |
| client_cert_observer.set_private_key(private_key); |
| scoped_refptr<net::X509Certificate> certificate = |
| test_server.GetCertificate(); |
| client_cert_observer.set_certificate(std::move(certificate)); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/defaultresponse")); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| // The private key should have been used. |
| EXPECT_EQ(1, private_key->sign_count()); |
| EXPECT_EQ(net::ERR_SSL_CLIENT_AUTH_SIGNATURE_FAILED, |
| client()->completion_status().error_code); |
| } |
| |
| TEST_F(URLLoaderTest, BlockAllCookies) { |
| GURL first_party_url("http://www.example.com.test/"); |
| net::SiteForCookies site_for_cookies = |
| net::SiteForCookies::FromUrl(first_party_url); |
| GURL third_party_url("http://www.some.other.origin.test/"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", first_party_url); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = mojom::kURLLoadOptionBlockAllCookies; |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| GURL cookie_url = test_server()->GetURL("/"); |
| auto cc = net::CanonicalCookie::CreateForTesting( |
| cookie_url, "a=b", base::Time::Now(), std::nullopt /* server_time */, |
| net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://toplevelsite.com"))); |
| |
| EXPECT_FALSE(url_loader->AllowCookie(*cc, first_party_url, site_for_cookies)); |
| EXPECT_FALSE(url_loader->AllowFullCookies(first_party_url, site_for_cookies)); |
| EXPECT_FALSE(url_loader->AllowFullCookies(third_party_url, site_for_cookies)); |
| } |
| |
| TEST_F(URLLoaderTest, BlockOnlyThirdPartyCookies) { |
| GURL first_party_url("http://www.example.com.test/"); |
| net::SiteForCookies site_for_cookies = |
| net::SiteForCookies::FromUrl(first_party_url); |
| GURL third_party_url("http://www.some.other.origin.test/"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", first_party_url); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = mojom::kURLLoadOptionBlockThirdPartyCookies; |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| GURL cookie_url = test_server()->GetURL("/"); |
| auto cc = net::CanonicalCookie::CreateForTesting( |
| cookie_url, "a=b", base::Time::Now(), std::nullopt /* server_time */, |
| net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://toplevelsite.com"))); |
| |
| EXPECT_TRUE(url_loader->AllowCookie(*cc, first_party_url, site_for_cookies)); |
| EXPECT_TRUE(url_loader->AllowFullCookies(first_party_url, site_for_cookies)); |
| EXPECT_FALSE(url_loader->AllowFullCookies(third_party_url, site_for_cookies)); |
| } |
| |
| TEST_F(URLLoaderTest, AllowAllCookies) { |
| GURL first_party_url("http://www.example.com.test/"); |
| net::SiteForCookies site_for_cookies = |
| net::SiteForCookies::FromUrl(first_party_url); |
| GURL third_party_url("http://www.some.other.origin.test/"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", first_party_url); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| std::unique_ptr<URLLoader> url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| GURL cookie_url = test_server()->GetURL("/"); |
| auto cc = net::CanonicalCookie::CreateForTesting( |
| cookie_url, "a=b", base::Time::Now(), std::nullopt /* server_time */, |
| net::CookiePartitionKey::FromURLForTesting( |
| GURL("https://toplevelsite.com"))); |
| |
| EXPECT_TRUE(url_loader->AllowCookie(*cc, first_party_url, site_for_cookies)); |
| EXPECT_TRUE(url_loader->AllowFullCookies(first_party_url, site_for_cookies)); |
| EXPECT_TRUE(url_loader->AllowFullCookies(third_party_url, site_for_cookies)); |
| } |
| |
| class StorageAccessHeaderURLLoaderTest : public URLLoaderTest { |
| public: |
| StorageAccessHeaderURLLoaderTest() = default; |
| |
| void RegisterAdditionalHandlers() override { |
| test_server_.RegisterRequestHandler(base::BindRepeating( |
| &StorageAccessHeaderURLLoaderTest::HandleLoadWithStorageAccessRequest, |
| base::Unretained(this))); |
| } |
| |
| protected: |
| static constexpr char kStorageAccessRedirectLoadPath[] = |
| "/redirect-load-with-storage-access"; |
| |
| private: |
| std::unique_ptr<net::test_server::HttpResponse> |
| HandleLoadWithStorageAccessRequest( |
| const net::test_server::HttpRequest& request) { |
| if (!base::StartsWith(request.GetURL().GetPath(), |
| kStorageAccessRedirectLoadPath)) { |
| return nullptr; |
| } |
| std::string destination = |
| base::UnescapeBinaryURLComponent(request.GetURL().query()); |
| auto http_response = |
| std::make_unique<net::test_server::BasicHttpResponse>(); |
| http_response->set_content_type("text/plain"); |
| http_response->AddCustomHeader("Activate-Storage-Access", "load"); |
| http_response->set_code(net::HTTP_PERMANENT_REDIRECT); |
| http_response->AddCustomHeader( |
| "Location", destination.empty() ? "/empty.html" : destination); |
| return http_response; |
| } |
| }; |
| |
| // Validate that Sec-Fetch-Storage-Access header cannot be overridden by |
| // manually setting the value. |
| TEST_F(StorageAccessHeaderURLLoaderTest, |
| DirectlyModifiedSecFetchStorageAccess) { |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/echo")); |
| request.headers.SetHeader("Sec-Fetch-Storage-Access", "active"); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| |
| EXPECT_THAT(sent_request().headers, |
| Not(Contains(Key("Sec-Fetch-Storage-Access")))); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, LoadNoStatus) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL("/set-header?Activate-Storage-Access: load")); |
| |
| test_network_delegate()->set_storage_access_status(std::nullopt); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // TestNetworkDelegate always returns std::nullopt for the |
| // GetStorageAccessStatus call, so the loader's `storage_access_status_` is |
| // std::nullopt. |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, LoadStatusNone) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL("/set-header?Activate-Storage-Access: load")); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kNone); |
| base::HistogramTester histogram_tester; |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| histogram_tester.ExpectUniqueSample( |
| "API.StorageAccessHeader.ActivateStorageAccessLoadOutcome", |
| /*sample=*/ |
| net::cookie_util::ActivateStorageAccessLoadOutcome::kFailureInvalidStatus, |
| /*expected_bucket_count=*/1); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, LoadStatusInactive) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL("/set-header?Activate-Storage-Access: load")); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kInactive); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_TRUE(client()->response_head()->load_with_storage_access); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, LoadStatusActive) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL("/set-header?Activate-Storage-Access: load")); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| base::HistogramTester histogram_tester; |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_TRUE(client()->response_head()->load_with_storage_access); |
| histogram_tester.ExpectUniqueSample( |
| "API.StorageAccessHeader.ActivateStorageAccessLoadOutcome", |
| /*sample=*/net::cookie_util::ActivateStorageAccessLoadOutcome::kSuccess, |
| /*expected_bucket_count=*/1); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, Load_StatusActive_IgnoredParam) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", |
| test_server_.GetURL("/set-header?Activate-Storage-Access: load;foo")); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_TRUE(client()->response_head()->load_with_storage_access); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, Load_StatusActive_IncorrectType) { |
| base::RunLoop delete_run_loop; |
| // This response will be a comma-separated list, rather than a single item, so |
| // it's the wrong type and should be ignored. |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL("/set-header?Activate-Storage-Access: " |
| "load;bar&Activate-Storage-Access: load;foo")); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| } |
| |
| TEST_F(StorageAccessHeaderURLLoaderTest, RedirectWithLoad) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL(kStorageAccessRedirectLoadPath)); |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| url_loader->FollowRedirect({}, {}, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| // The redirect response included the `load` header, but the final response |
| // did not, so the URLLoader should not propagate it. |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| } |
| |
| // `URLLoader::ConfigureRequest` only calls |
| // `net::URLRequest::set_storage_access_status` if the request's credentials |
| // mode is `kInclude` but `URLLoader::ShouldSetLoadWithStorageAccess` calculates |
| // it anyway if the `Activate-Storage-Access: load` header is present. Once with |
| // the initial URL during `OnReceivedRedirect` and once more with the final URL |
| // during `FollowRedirect`. This test makes sure that the final response's |
| // `load_with_storage_access` is set properly when the request's credentials |
| // mode is NOT `kInclude` and a redirect is involved with `load` on both, but |
| // with different storage access status calculated for each request. |
| TEST_F(StorageAccessHeaderURLLoaderTest, |
| OmitCredentials_RedirectWithLoadActive_DestinationLoadNone) { |
| base::RunLoop delete_run_loop; |
| |
| GURL destination_url = |
| test_server_.GetURL("/set-header?Activate-Storage-Access: load"); |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL(base::StrCat( |
| {kStorageAccessRedirectLoadPath, "?", |
| base::EscapeQueryParamValue(destination_url.spec(), true)}))); |
| request.credentials_mode = mojom::CredentialsMode::kOmit; |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| EXPECT_TRUE(client()->response_head()->load_with_storage_access); |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kNone); |
| url_loader->FollowRedirect({}, {}, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| } |
| |
| // `URLLoader::ConfigureRequest` only calls |
| // `net::URLRequest::set_storage_access_status` if the request's credentials |
| // mode is `kInclude` but `URLLoader::ShouldSetLoadWithStorageAccess` calculates |
| // it anyway if the `Activate-Storage-Access: load` header is present. Once with |
| // the initial URL during `OnReceivedRedirect` and once more with the final URL |
| // during `FollowRedirect`. This test makes sure that the final response's |
| // `load_with_storage_access` is set properly when the request's credentials |
| // mode is NOT `kInclude` and a redirect is involved with `load` on both, but |
| // with different storage access status calculated for each request. |
| TEST_F(StorageAccessHeaderURLLoaderTest, |
| OmitCredentials_RedirectWithLoadNone_DestinationLoadActive) { |
| base::RunLoop delete_run_loop; |
| |
| GURL destination_url = |
| test_server_.GetURL("/set-header?Activate-Storage-Access: load"); |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL(base::StrCat( |
| {kStorageAccessRedirectLoadPath, "?", |
| base::EscapeQueryParamValue(destination_url.spec(), true)}))); |
| request.credentials_mode = mojom::CredentialsMode::kOmit; |
| |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kNone); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| EXPECT_FALSE(client()->response_head()->load_with_storage_access); |
| test_network_delegate()->set_storage_access_status( |
| net::cookie_util::StorageAccessStatus::kActive); |
| url_loader->FollowRedirect({}, {}, {}, std::nullopt); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_TRUE(client()->response_head()->load_with_storage_access); |
| } |
| |
| class URLLoaderCookieSettingOverridesTest |
| : public URLLoaderTest, |
| public ::testing::WithParamInterface< |
| std::tuple<bool, bool, net::StorageAccessApiStatus, std::string>> { |
| public: |
| URLLoaderCookieSettingOverridesTest() = default; |
| |
| ~URLLoaderCookieSettingOverridesTest() override = default; |
| |
| void SetUpRequest(ResourceRequest& request) { |
| if (IsCors()) { |
| request.mode = network::mojom::RequestMode::kCors; |
| } else { |
| // Request mode is `no-cors` by default. |
| EXPECT_EQ(request.mode, network::mojom::RequestMode::kNoCors); |
| } |
| request.is_outermost_main_frame = IsOuterMostFrame(); |
| request.storage_access_api_status = StorageAccessApiStatus(); |
| request.request_initiator = Initiator(); |
| } |
| |
| net::CookieSettingOverrides ExpectedCookieSettingOverrides( |
| const ResourceRequest& request) const { |
| net::CookieSettingOverrides overrides; |
| if (IsCors() && IsOuterMostFrame()) { |
| overrides.Put( |
| net::CookieSettingOverride::kTopLevelStorageAccessGrantEligible); |
| } |
| switch (StorageAccessApiStatus()) { |
| case net::StorageAccessApiStatus::kNone: |
| break; |
| case net::StorageAccessApiStatus::kAccessViaAPI: |
| if (Initiator().IsSameOriginWith(request.url)) { |
| overrides.Put( |
| net::CookieSettingOverride::kStorageAccessGrantEligible); |
| } |
| break; |
| } |
| return overrides; |
| } |
| |
| net::CookieSettingOverrides |
| ExpectedCookieSettingOverridesForCrossOriginRedirect( |
| const ResourceRequest& request) const { |
| net::CookieSettingOverrides overrides = |
| ExpectedCookieSettingOverrides(request); |
| overrides.Remove(net::CookieSettingOverride::kStorageAccessGrantEligible); |
| return overrides; |
| } |
| |
| net::CookieSettingOverrides |
| ExpectedCookieSettingOverridesForCrossSiteRedirect( |
| const ResourceRequest& request) const { |
| net::CookieSettingOverrides overrides = |
| ExpectedCookieSettingOverrides(request); |
| overrides.Remove(net::CookieSettingOverride::kStorageAccessGrantEligible); |
| return overrides; |
| } |
| |
| protected: |
| bool IsCors() const { return std::get<0>(GetParam()); } |
| bool IsOuterMostFrame() const { return std::get<1>(GetParam()); } |
| net::StorageAccessApiStatus StorageAccessApiStatus() const { |
| return std::get<2>(GetParam()); |
| } |
| url::Origin Initiator() const { |
| return url::Origin::Create( |
| test_server()->GetURL(std::get<3>(GetParam()), "/")); |
| } |
| |
| private: |
| base::test::ScopedFeatureList features_; |
| }; |
| |
| // This test makes a request to an endpoint which does not redirect. |
| TEST_P(URLLoaderCookieSettingOverridesTest, NoRedirect) { |
| const GURL url = test_server()->GetURL(kHostnameWithAliases, "/"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| SetUpRequest(request); |
| |
| EXPECT_EQ(LoadRequest(request), net::OK); |
| |
| EXPECT_THAT(test_network_delegate()->cookie_setting_overrides_records(), |
| ElementsAre(ExpectedCookieSettingOverrides(request), |
| ExpectedCookieSettingOverrides(request))); |
| } |
| |
| // This test makes a request to an endpoint that redirects to a cross-origin, |
| // same-site endpoint. |
| TEST_P(URLLoaderCookieSettingOverridesTest, CrossOriginSameSiteRedirect) { |
| const GURL url = test_server()->GetURL( |
| kHostnameWithAliases, |
| "/server-redirect?" + |
| test_server()->GetURL("example.test", "/simple_page.html").spec()); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| SetUpRequest(request); |
| |
| set_expect_redirect(); |
| EXPECT_EQ(LoadRequest(request), net::OK); |
| |
| EXPECT_THAT( |
| test_network_delegate()->cookie_setting_overrides_records(), |
| ElementsAre( |
| ExpectedCookieSettingOverrides(request), |
| ExpectedCookieSettingOverrides(request), |
| ExpectedCookieSettingOverridesForCrossOriginRedirect(request), |
| ExpectedCookieSettingOverridesForCrossOriginRedirect(request))); |
| } |
| |
| // This test makes a request to an endpoint which redirects to a cross-site |
| // endpoint. |
| TEST_P(URLLoaderCookieSettingOverridesTest, CrossSiteRedirect) { |
| const GURL url = test_server()->GetURL( |
| kHostnameWithAliases, |
| "/server-redirect?" + test_server()->GetURL("other.test", "/").spec()); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| SetUpRequest(request); |
| |
| set_expect_redirect(); |
| EXPECT_EQ(LoadRequest(request), net::OK); |
| |
| EXPECT_THAT( |
| test_network_delegate()->cookie_setting_overrides_records(), |
| ElementsAre(ExpectedCookieSettingOverrides(request), |
| ExpectedCookieSettingOverrides(request), |
| ExpectedCookieSettingOverridesForCrossSiteRedirect(request), |
| ExpectedCookieSettingOverridesForCrossSiteRedirect(request))); |
| } |
| |
| // This test sends a request to an endpoint that is cross-site from the |
| // initiator, which redirects to an endpoint that is same-site (and same-origin) |
| // to the initiator. The second redirect leg should not have the |
| // `kStorageAccessGrantEligible` override, since that would include cookies on |
| // the request and therefore make a CSRF attack possible via that redirect. |
| TEST_P(URLLoaderCookieSettingOverridesTest, OnCrossSiteToSameSite) { |
| if (!Initiator().DomainIs(kHostnameWithAliases)) { |
| // This test sets its own request initiator below, so it effectively ignores |
| // the `initiator()` test param. WLOG, we can skip all but one of the |
| // instances of that param. |
| GTEST_SKIP(); |
| } |
| |
| const GURL url = test_server()->GetURL( |
| "cross-site.test", |
| "/server-redirect?" + |
| test_server()->GetURL(kHostnameWithAliases, "/empty.html").spec()); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| SetUpRequest(request); |
| request.request_initiator = test_server()->GetOrigin(); |
| |
| set_expect_redirect(); |
| EXPECT_EQ(LoadRequest(request), net::OK); |
| |
| EXPECT_THAT( |
| test_network_delegate()->cookie_setting_overrides_records(), |
| ElementsAre(ExpectedCookieSettingOverridesForCrossSiteRedirect(request), |
| ExpectedCookieSettingOverridesForCrossSiteRedirect(request), |
| ExpectedCookieSettingOverridesForCrossSiteRedirect(request), |
| ExpectedCookieSettingOverridesForCrossSiteRedirect(request))); |
| } |
| |
| TEST_F(URLLoaderTest, DevToolsCookieSettingOverrides_NotApplied) { |
| GURL url("http://www.example.com.test/"); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| |
| context().mutable_factory_params().process_id = kProcessId; |
| // This override should only apply conditionally when devtools is enabled. |
| context().mutable_factory_params().devtools_cookie_setting_overrides = { |
| net::CookieSettingOverride::kForceDisableThirdPartyCookies}; |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| std::unique_ptr<URLLoader> url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| const std::vector<net::CookieSettingOverrides> records = |
| test_network_delegate()->cookie_setting_overrides_records(); |
| EXPECT_THAT(records, ElementsAre(net::CookieSettingOverrides(), |
| net::CookieSettingOverrides())); |
| } |
| |
| TEST_F(URLLoaderTest, DevToolsCookieSettingOverrides_Applied) { |
| GURL url("http://www.example.com.test/"); |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| |
| net::CookieSettingOverrides devtools_overrides = { |
| net::CookieSettingOverride::kForceDisableThirdPartyCookies}; |
| context().mutable_factory_params().process_id = kProcessId; |
| // This override should only apply conditionally when devtools is enabled. |
| context().mutable_factory_params().devtools_cookie_setting_overrides = |
| devtools_overrides; |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| // `devtools_request_id` is present, meaning devtools is enabled for this |
| // request. |
| request.devtools_request_id = "devtools_id"; |
| std::unique_ptr<URLLoader> url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| const std::vector<net::CookieSettingOverrides> records = |
| test_network_delegate()->cookie_setting_overrides_records(); |
| EXPECT_THAT(records, ElementsAre(devtools_overrides, devtools_overrides)); |
| } |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| URLLoaderCookieSettingOverridesTest, |
| testing::Combine( |
| testing::Bool(), |
| testing::Bool(), |
| testing::Values(net::StorageAccessApiStatus::kNone, |
| net::StorageAccessApiStatus::kAccessViaAPI), |
| testing::Values( |
| // Same-origin initiator |
| kHostnameWithAliases, |
| // Same-site cross-origin initiator |
| "example.test", |
| // Cross-site initiator |
| "other-origin.test"))); |
| |
| namespace { |
| |
| enum class TestMode { |
| kCredentialsModeOmit, |
| kCredentialsModeOmitWorkaround, |
| kCredentialsModeOmitWithFeatureFix, |
| }; |
| |
| } // namespace |
| |
| class URLLoaderParameterTest : public URLLoaderTest, |
| public ::testing::WithParamInterface<TestMode> { |
| public: |
| ~URLLoaderParameterTest() override = default; |
| |
| private: |
| void SetUp() override { |
| URLLoaderTest::SetUp(); |
| if (GetParam() == TestMode::kCredentialsModeOmitWithFeatureFix) { |
| scoped_feature_list.InitAndEnableFeature(features::kOmitCorsClientCert); |
| } else { |
| scoped_feature_list.InitAndDisableFeature(features::kOmitCorsClientCert); |
| } |
| } |
| base::test::ScopedFeatureList scoped_feature_list; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| URLLoaderParameterTest, |
| testing::Values(TestMode::kCredentialsModeOmit, |
| TestMode::kCredentialsModeOmitWorkaround, |
| TestMode::kCredentialsModeOmitWithFeatureFix)); |
| |
| TEST_F(URLLoaderTest, AcceptCHFrameNotAllowedHintWithFeature) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| features::kOffloadAcceptCHFrameCheck, |
| {{"AcceptCHFrameOffloadNotAllowedHints", "true"}}); |
| |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| const GURL url("http://accept-ch.test/"); |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver observer; |
| set_accept_ch_frame_observer_for_next_request(&observer); |
| |
| network::ResourceRequest::TrustedParams::EnabledClientHints enabled_hints; |
| enabled_hints.origin = url::Origin::Create(url); |
| enabled_hints.is_outermost_main_frame = true; |
| enabled_hints.hints = {network::mojom::WebClientHintsType::kUAArch}; |
| enabled_hints.not_allowed_hints = { |
| network::mojom::WebClientHintsType::kUAPlatform}; |
| set_enabled_client_hints_for_next_request(std::move(enabled_hints)); |
| |
| EXPECT_THAT(Load(url), IsOk()); |
| EXPECT_FALSE(observer.called()); |
| } |
| |
| TEST_F(URLLoaderTest, AcceptCHFrameNotAllowedHintWithoutFeature) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeatureWithParameters( |
| features::kOffloadAcceptCHFrameCheck, |
| {{"AcceptCHFrameOffloadNotAllowedHints", "false"}}); |
| |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| const GURL url("http://accept-ch.test/"); |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver observer; |
| set_accept_ch_frame_observer_for_next_request(&observer); |
| |
| network::ResourceRequest::TrustedParams::EnabledClientHints enabled_hints; |
| enabled_hints.origin = url::Origin::Create(url); |
| enabled_hints.is_outermost_main_frame = true; |
| enabled_hints.hints = {network::mojom::WebClientHintsType::kUAArch}; |
| enabled_hints.not_allowed_hints = { |
| network::mojom::WebClientHintsType::kUAPlatform}; |
| set_enabled_client_hints_for_next_request(std::move(enabled_hints)); |
| |
| EXPECT_THAT(Load(url), IsOk()); |
| EXPECT_TRUE(observer.called()); |
| EXPECT_THAT(observer.accept_ch_frame(), |
| ElementsAre(network::mojom::WebClientHintsType::kUAPlatform)); |
| } |
| |
| // Tests that a request with CredentialsMode::kOmit still sends client |
| // certificates when features::kOmitCorsClientCert is disabled, and when the |
| // feature is enabled client certificates are not sent. Also test that when |
| // CredentialsMode::kOmitBug_775438_Workaround is used client certificates are |
| // not sent as well. This should be removed when crbug.com/775438 is fixed. |
| TEST_P(URLLoaderParameterTest, CredentialsModeOmitRequireClientCert) { |
| // Set up a server that requires certificates. |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::REQUIRE_CLIENT_CERT; |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| // Make sure the client has valid certificates. |
| std::unique_ptr<net::FakeClientCertIdentity> identity = |
| net::FakeClientCertIdentity::CreateFromCertAndKeyFiles( |
| net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8"); |
| ASSERT_TRUE(identity); |
| scoped_refptr<TestSSLPrivateKey> private_key = |
| base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| VALID_CERTIFICATE_SIGNATURE); |
| client_cert_observer.set_private_key(private_key); |
| client_cert_observer.set_certificate(identity->certificate()); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/simple_page.html")); |
| if (GetParam() == TestMode::kCredentialsModeOmitWorkaround) { |
| request.credentials_mode = |
| mojom::CredentialsMode::kOmitBug_775438_Workaround; |
| } else { |
| request.credentials_mode = mojom::CredentialsMode::kOmit; |
| } |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| if (GetParam() == TestMode::kCredentialsModeOmitWithFeatureFix || |
| GetParam() == TestMode::kCredentialsModeOmitWorkaround) { |
| EXPECT_EQ(0, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_NE(net::OK, client()->completion_status().error_code); |
| } else { |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| } |
| } |
| |
| // Tests that a request with CredentialsMode::kOmitBug_775438_Workaround, and |
| // CredentialsMode::kOmit when features::kOmitCorsClientCert is enabled doesn't |
| // send client certificates with a server that optionally requires certificates. |
| TEST_P(URLLoaderParameterTest, CredentialsModeOmitOptionalClientCert) { |
| // Set up a server that requires certificates. |
| net::SSLServerConfig ssl_config; |
| ssl_config.client_cert_type = |
| net::SSLServerConfig::ClientCertType::OPTIONAL_CLIENT_CERT; |
| net::EmbeddedTestServer test_server(net::EmbeddedTestServer::TYPE_HTTPS); |
| test_server.SetSSLConfig(net::EmbeddedTestServer::CERT_OK, ssl_config); |
| test_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(test_server.Start()); |
| |
| // Make sure the client has valid certificates. |
| std::unique_ptr<net::FakeClientCertIdentity> identity = |
| net::FakeClientCertIdentity::CreateFromCertAndKeyFiles( |
| net::GetTestCertsDirectory(), "client_1.pem", "client_1.pk8"); |
| ASSERT_TRUE(identity); |
| scoped_refptr<TestSSLPrivateKey> private_key = |
| base::MakeRefCounted<TestSSLPrivateKey>(identity->ssl_private_key()); |
| |
| ClientCertAndHttpAuthObserver client_cert_observer; |
| client_cert_observer.set_certificate_response( |
| ClientCertAndHttpAuthObserver::CertificateResponse:: |
| VALID_CERTIFICATE_SIGNATURE); |
| client_cert_observer.set_private_key(private_key); |
| client_cert_observer.set_certificate(identity->certificate()); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server.GetURL("/simple_page.html")); |
| if (GetParam() == TestMode::kCredentialsModeOmitWorkaround) { |
| request.credentials_mode = |
| mojom::CredentialsMode::kOmitBug_775438_Workaround; |
| } else { |
| request.credentials_mode = mojom::CredentialsMode::kOmit; |
| } |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.url_loader_network_observer = client_cert_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| if (GetParam() == TestMode::kCredentialsModeOmitWithFeatureFix || |
| GetParam() == TestMode::kCredentialsModeOmitWorkaround) { |
| EXPECT_EQ(0, client_cert_observer.on_certificate_requested_counter()); |
| } else { |
| EXPECT_EQ(1, client_cert_observer.on_certificate_requested_counter()); |
| } |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| } |
| |
| #endif // !BUILDFLAG(IS_IOS) |
| |
| TEST_F(URLLoaderTest, CookieReporting) { |
| { |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/set-cookie?a=b")); |
| |
| MockCookieObserver cookie_observer; |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT( |
| cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("a=b", mojom::CookieOrLine::Tag::kCookie), true))); |
| } |
| |
| { |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/nocontent")); |
| |
| MockCookieObserver cookie_observer; |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT( |
| cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kRead, |
| CookieOrLine("a=b", mojom::CookieOrLine::Tag::kCookie), true))); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, CookieReportingRedirect) { |
| MockCookieObserver cookie_observer(CookieAccessType::kChange); |
| |
| GURL dest_url = test_server()->GetURL("/nocontent"); |
| GURL redirecting_url = |
| test_server()->GetURL("/server-redirect-with-cookie?" + dest_url.spec()); |
| |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", redirecting_url); |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilRedirectReceived(); |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT(cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("server-redirect=true", |
| mojom::CookieOrLine::Tag::kCookie), |
| true))); |
| // Make sure that this has the pre-redirect URL, not the post-redirect one. |
| EXPECT_EQ(redirecting_url, cookie_observer.observed_cookies()[0].url); |
| } |
| |
| TEST_F(URLLoaderTest, CookieReportingAuth) { |
| for (auto mode : |
| {ClientCertAndHttpAuthObserver::CredentialsResponse::NO_CREDENTIALS, |
| ClientCertAndHttpAuthObserver::CredentialsResponse:: |
| CORRECT_CREDENTIALS}) { |
| MockCookieObserver cookie_observer(CookieAccessType::kChange); |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response(mode); |
| |
| GURL url = test_server()->GetURL( |
| "/auth-basic?set-cookie-if-challenged&password=PASS"); |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| url_loader_options.url_loader_network_observer = |
| client_auth_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT(cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("got_challenged=true", |
| mojom::CookieOrLine::Tag::kCookie), |
| true))); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawRequestCookies) { |
| { |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/echoheader?cookie")); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| GURL cookie_url = test_server()->GetURL("/"); |
| auto cookie = net::CanonicalCookie::CreateForTesting(cookie_url, "a=b", |
| base::Time::Now()); |
| url_request_context()->cookie_store()->SetCanonicalCookieAsync( |
| std::move(cookie), cookie_url, net::CookieOptions::MakeAllInclusive(), |
| base::DoNothing()); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawRequest(1u); |
| EXPECT_EQ(200, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("a", devtools_observer.raw_request_cookies()[0].cookie.Name()); |
| EXPECT_EQ("b", devtools_observer.raw_request_cookies()[0].cookie.Value()); |
| EXPECT_TRUE(devtools_observer.raw_request_cookies()[0] |
| .access_result.status.IsInclude()); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawRequestCookiesFlagged) { |
| { |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/echoheader?cookie")); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| // Set the path to an irrelevant url to block the cookie from sending |
| GURL cookie_url = test_server()->GetURL("/"); |
| auto cookie = net::CanonicalCookie::CreateForTesting( |
| cookie_url, "a=b;Path=/something-else", base::Time::Now()); |
| url_request_context()->cookie_store()->SetCanonicalCookieAsync( |
| std::move(cookie), cookie_url, net::CookieOptions::MakeAllInclusive(), |
| base::DoNothing()); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawRequest(1u); |
| EXPECT_EQ(200, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("a", devtools_observer.raw_request_cookies()[0].cookie.Name()); |
| EXPECT_EQ("b", devtools_observer.raw_request_cookies()[0].cookie.Value()); |
| EXPECT_TRUE(devtools_observer.raw_request_cookies()[0] |
| .access_result.status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_NOT_ON_PATH})); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawResponseCookies) { |
| { |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/set-cookie?a=b")); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(1u); |
| EXPECT_EQ(200, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("a", devtools_observer.raw_response_cookies()[0].cookie->Name()); |
| EXPECT_EQ("b", devtools_observer.raw_response_cookies()[0].cookie->Value()); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.IsInclude()); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| |
| ASSERT_TRUE(devtools_observer.raw_response_headers()); |
| EXPECT_NE(devtools_observer.raw_response_headers()->find("Set-Cookie: a=b"), |
| std::string::npos); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawResponseCookiesInvalid) { |
| { |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("/set-invalid-cookie")); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(1u); |
| EXPECT_EQ(200, devtools_observer.raw_response_http_status_code()); |
| // On these failures the cookie object is not created |
| EXPECT_FALSE(devtools_observer.raw_response_cookies()[0].cookie); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_DISALLOWED_CHARACTER})); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawResponseCookiesRedirect) { |
| // Check a valid cookie |
| { |
| MockDevToolsObserver devtools_observer; |
| GURL dest_url = test_server()->GetURL("a.test", "/nocontent"); |
| GURL redirecting_url = test_server()->GetURL( |
| "a.test", "/server-redirect-with-cookie?" + dest_url.spec()); |
| |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", redirecting_url); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilRedirectReceived(); |
| devtools_observer.WaitUntilRawResponse(1u); |
| |
| ASSERT_TRUE(devtools_observer.raw_response_headers()); |
| EXPECT_NE(devtools_observer.raw_response_headers()->find( |
| "Set-Cookie: server-redirect=true"), |
| std::string::npos); |
| |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(1u); |
| EXPECT_EQ(204, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("server-redirect", |
| devtools_observer.raw_response_cookies()[0].cookie->Name()); |
| EXPECT_EQ("true", |
| devtools_observer.raw_response_cookies()[0].cookie->Value()); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.IsInclude()); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| |
| // Check a flagged cookie (secure cookie over an insecure connection) |
| { |
| GURL dest_url = test_server()->GetURL("a.test", "/nocontent"); |
| GURL redirecting_url = test_server()->GetURL( |
| "a.test", "/server-redirect-with-secure-cookie?" + dest_url.spec()); |
| |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", redirecting_url); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilRedirectReceived(); |
| loader->FollowRedirect({}, {}, {}, std::nullopt); |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(1u); |
| EXPECT_EQ(204, devtools_observer.raw_response_http_status_code()); |
| // On these failures the cookie object is created but not included. |
| EXPECT_TRUE( |
| devtools_observer.raw_response_cookies()[0].cookie->SecureAttribute()); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_SECURE_ONLY})); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawResponseCookiesAuth) { |
| // Check a valid cookie |
| { |
| MockDevToolsObserver devtools_observer; |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::NO_CREDENTIALS); |
| |
| GURL url = test_server()->GetURL( |
| "a.test", "/auth-basic?set-cookie-if-challenged&password=PASS"); |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(1u); |
| EXPECT_EQ(401, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("got_challenged", |
| devtools_observer.raw_response_cookies()[0].cookie->Name()); |
| EXPECT_EQ("true", |
| devtools_observer.raw_response_cookies()[0].cookie->Value()); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.IsInclude()); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| |
| // Check a flagged cookie (secure cookie from insecure connection) |
| { |
| MockDevToolsObserver devtools_observer; |
| ClientCertAndHttpAuthObserver client_auth_observer; |
| client_auth_observer.set_credentials_response( |
| ClientCertAndHttpAuthObserver::CredentialsResponse::NO_CREDENTIALS); |
| |
| GURL url = test_server()->GetURL( |
| "a.test", "/auth-basic?set-secure-cookie-if-challenged&password=PASS"); |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| loader_client.RunUntilComplete(); |
| delete_run_loop.Run(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| EXPECT_EQ(401, devtools_observer.raw_response_http_status_code()); |
| devtools_observer.WaitUntilRawResponse(1u); |
| // On these failures the cookie object is created but not included. |
| EXPECT_TRUE( |
| devtools_observer.raw_response_cookies()[0].cookie->SecureAttribute()); |
| EXPECT_TRUE(devtools_observer.raw_response_cookies()[0] |
| .access_result.status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_SECURE_ONLY})); |
| |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, RawResponseQUIC) { |
| { |
| MockDevToolsObserver devtools_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = |
| CreateResourceRequest("GET", net::QuicSimpleTestServer::GetFileURL("")); |
| |
| // Set the devtools id to trigger the RawResponse call |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| ASSERT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(0u); |
| EXPECT_EQ(404, devtools_observer.raw_response_http_status_code()); |
| EXPECT_EQ("TEST", devtools_observer.devtools_request_id()); |
| |
| // QUIC responses don't have raw header text, so there shouldn't be any here |
| EXPECT_FALSE(devtools_observer.raw_response_headers()); |
| } |
| } |
| |
| TEST_F(URLLoaderTest, EarlyHints) { |
| const std::string kPath = "/hinted"; |
| const std::string kResponseBody = "content with hints"; |
| const std::string kPreloadPath = "/hello.txt"; |
| |
| // Prepare a response with an Early Hints response. |
| quiche::HttpHeaderBlock response_headers; |
| response_headers[":status"] = "200"; |
| response_headers[":path"] = kPath; |
| std::vector<quiche::HttpHeaderBlock> early_hints; |
| quiche::HttpHeaderBlock hints_headers; |
| std::string preload_link = |
| base::StringPrintf("<%s>; rel=preload", kPreloadPath.c_str()); |
| hints_headers["link"] = preload_link; |
| |
| early_hints.push_back(std::move(hints_headers)); |
| net::QuicSimpleTestServer::AddResponseWithEarlyHints( |
| kPath, response_headers, kResponseBody, early_hints); |
| |
| MockDevToolsObserver devtools_observer; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| |
| // Create a loader and a client to request `kPath`. The client should receive |
| // an Early Hints which contains a preload link header. |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", net::QuicSimpleTestServer::GetFileURL(kPath)); |
| request.devtools_request_id = "TEST"; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| ASSERT_EQ(loader_client.completion_status().error_code, net::OK); |
| |
| ASSERT_EQ(loader_client.early_hints().size(), 1UL); |
| const auto& hints = loader_client.early_hints()[0]; |
| ASSERT_EQ(hints->headers->link_headers.size(), 1UL); |
| const auto& link_header = hints->headers->link_headers[0]; |
| EXPECT_EQ(link_header->href, |
| net::QuicSimpleTestServer::GetFileURL(kPreloadPath)); |
| |
| // Test the early hint headers sent to the devtools observer. |
| devtools_observer.WaitUntilEarlyHints(); |
| const std::vector<network::mojom::HttpRawHeaderPairPtr>& early_hint_headers = |
| devtools_observer.early_hint_headers(); |
| EXPECT_EQ(early_hint_headers.size(), 1UL); |
| network::mojom::HttpRawHeaderPair header_content = *early_hint_headers[0]; |
| EXPECT_EQ(header_content.key, "link"); |
| EXPECT_EQ(header_content.value, preload_link); |
| } |
| |
| TEST_F(URLLoaderTest, CookieReportingCategories) { |
| net::test_server::EmbeddedTestServer https_server( |
| net::test_server::EmbeddedTestServer::TYPE_HTTPS); |
| https_server.SetSSLConfig( |
| net::test_server::EmbeddedTestServer::CERT_TEST_NAMES); |
| https_server.AddDefaultHandlers( |
| base::FilePath(FILE_PATH_LITERAL("services/test/data"))); |
| ASSERT_TRUE(https_server.Start()); |
| |
| // SameSite-by-default deprecation warning. |
| { |
| MockCookieObserver cookie_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", https_server.GetURL("a.test", "/set-cookie?a=b;Secure")); |
| // Make this a third-party request. |
| url::Origin third_party_origin = |
| url::Origin::Create(GURL("http://www.example.com")); |
| request.site_for_cookies = |
| net::SiteForCookies::FromOrigin(third_party_origin); |
| request.trusted_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(third_party_origin); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT(cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("a=b", mojom::CookieOrLine::Tag::kCookie), |
| false /* is_included */))); |
| EXPECT_TRUE(cookie_observer.observed_cookies()[0] |
| .status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_SAMESITE_UNSPECIFIED_TREATED_AS_LAX})); |
| EXPECT_TRUE(cookie_observer.observed_cookies()[0].status.HasWarningReason( |
| net::CookieInclusionStatus::WarningReason:: |
| WARN_SAMESITE_UNSPECIFIED_CROSS_SITE_CONTEXT)); |
| } |
| |
| // Blocked. |
| { |
| MockCookieObserver cookie_observer(CookieAccessType::kChange); |
| TestURLLoaderClient loader_client; |
| test_network_delegate()->set_cookie_options( |
| net::TestNetworkDelegate::NO_SET_COOKIE); |
| ResourceRequest request = CreateResourceRequest( |
| "GET", https_server.GetURL("a.test", "/set-cookie?a=b;Secure")); |
| // Make this a third-party request. |
| url::Origin third_party_origin = |
| url::Origin::Create(GURL("http://www.example.com")); |
| request.site_for_cookies = |
| net::SiteForCookies::FromOrigin(third_party_origin); |
| request.trusted_params->isolation_info = |
| net::IsolationInfo::CreateForInternalRequest(third_party_origin); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT( |
| cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("a=b", mojom::CookieOrLine::Tag::kCookie), false))); |
| EXPECT_TRUE(cookie_observer.observed_cookies()[0] |
| .status.HasExactlyExclusionReasonsForTesting( |
| {net::CookieInclusionStatus::ExclusionReason:: |
| EXCLUDE_USER_PREFERENCES})); |
| |
| test_network_delegate()->set_cookie_options(0); |
| } |
| |
| // Not permitted by cookie rules, but not the sort of thing that's reported |
| // to NetworkContextClient. Note: this uses HTTP, not HTTPS, unlike others; |
| // and is in 1st-party context. |
| { |
| MockCookieObserver cookie_observer; |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server()->GetURL("a.test", "/set-cookie?a=b;Secure&d=e")); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.cookie_observer = cookie_observer.GetRemote(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| |
| delete_run_loop.Run(); |
| loader_client.RunUntilComplete(); |
| EXPECT_EQ(net::OK, loader_client.completion_status().error_code); |
| |
| cookie_observer.WaitForCookies(1u); |
| EXPECT_THAT( |
| cookie_observer.observed_cookies(), |
| testing::ElementsAre(MatchesCookieDetails( |
| CookieAccessType::kChange, |
| CookieOrLine("d=e", mojom::CookieOrLine::Tag::kCookie), true))); |
| } |
| } |
| |
| namespace { |
| |
| enum class SyncOrAsync { kSync, kAsync }; |
| |
| class MockTrustTokenRequestHelper : public TrustTokenRequestHelper { |
| public: |
| // |operation_synchrony| denotes whether to complete the |Begin| |
| // and |Finalize| operations synchronously. |
| // |
| // |begin_done_flag|, if provided, will be set to true immediately before the |
| // |Begin| operation returns. |
| MockTrustTokenRequestHelper( |
| std::optional<mojom::TrustTokenOperationStatus> on_begin, |
| std::optional<mojom::TrustTokenOperationStatus> on_finalize, |
| SyncOrAsync operation_synchrony, |
| bool* begin_done_flag = nullptr) |
| : on_begin_(on_begin), |
| on_finalize_(on_finalize), |
| operation_synchrony_(operation_synchrony), |
| begin_done_flag_(begin_done_flag) {} |
| |
| ~MockTrustTokenRequestHelper() override { |
| DCHECK(!on_begin_.has_value()) |
| << "Begin operation was expected but not performed."; |
| DCHECK(!on_finalize_.has_value()) |
| << "Finalize operation was expected but not performed."; |
| } |
| |
| MockTrustTokenRequestHelper(const MockTrustTokenRequestHelper&) = delete; |
| MockTrustTokenRequestHelper& operator=(const MockTrustTokenRequestHelper&) = |
| delete; |
| |
| // TrustTokenRequestHelper: |
| void Begin(const GURL& url, |
| base::OnceCallback<void(std::optional<net::HttpRequestHeaders>, |
| mojom::TrustTokenOperationStatus)> done) |
| override { |
| DCHECK(on_begin_.has_value()); |
| |
| // Clear storage to crash if the method gets called a second time. |
| mojom::TrustTokenOperationStatus result = *on_begin_; |
| on_begin_.reset(); |
| std::optional<net::HttpRequestHeaders> headers; |
| if (result == mojom::TrustTokenOperationStatus::kOk) { |
| headers.emplace(); |
| } |
| |
| switch (operation_synchrony_) { |
| case SyncOrAsync::kSync: { |
| OnDoneBeginning( |
| base::BindOnce(std::move(done), std::move(headers), result)); |
| return; |
| } |
| case SyncOrAsync::kAsync: { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, |
| base::BindOnce( |
| &MockTrustTokenRequestHelper::OnDoneBeginning, |
| base::Unretained(this), |
| base::BindOnce(std::move(done), std::move(headers), result))); |
| return; |
| } |
| } |
| } |
| |
| void Finalize(net::HttpResponseHeaders& response_headers, |
| base::OnceCallback<void(mojom::TrustTokenOperationStatus)> done) |
| override { |
| DCHECK(on_finalize_.has_value()); |
| |
| // Clear storage to crash if the method gets called a second time. |
| mojom::TrustTokenOperationStatus result = *on_finalize_; |
| on_finalize_.reset(); |
| |
| switch (operation_synchrony_) { |
| case SyncOrAsync::kSync: { |
| std::move(done).Run(result); |
| return; |
| } |
| case SyncOrAsync::kAsync: { |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(done), result)); |
| return; |
| } |
| } |
| } |
| |
| mojom::TrustTokenOperationResultPtr CollectOperationResultWithStatus( |
| mojom::TrustTokenOperationStatus status) override { |
| mojom::TrustTokenOperationResultPtr operation_result = |
| mojom::TrustTokenOperationResult::New(); |
| operation_result->status = status; |
| return operation_result; |
| } |
| |
| private: |
| void OnDoneBeginning(base::OnceClosure done) { |
| if (begin_done_flag_) { |
| EXPECT_FALSE(*begin_done_flag_); |
| *begin_done_flag_ = true; |
| } |
| |
| std::move(done).Run(); |
| } |
| |
| // Store mocked function results in Optionals to hit a CHECK failure if a mock |
| // method is called without having had a return value specified. |
| std::optional<mojom::TrustTokenOperationStatus> on_begin_; |
| std::optional<mojom::TrustTokenOperationStatus> on_finalize_; |
| |
| SyncOrAsync operation_synchrony_; |
| |
| raw_ptr<bool> begin_done_flag_; |
| }; |
| |
| class NoopTrustTokenKeyCommitmentGetter : public TrustTokenKeyCommitmentGetter { |
| public: |
| NoopTrustTokenKeyCommitmentGetter() = default; |
| void Get(const url::Origin& origin, |
| base::OnceCallback<void(mojom::TrustTokenKeyCommitmentResultPtr)> |
| on_done) const override {} |
| }; |
| |
| base::NoDestructor<NoopTrustTokenKeyCommitmentGetter> |
| noop_key_commitment_getter{}; |
| |
| mojom::NetworkContextClient* ReturnNullNetworkContextClient() { |
| return nullptr; |
| } |
| |
| class MockTrustTokenRequestHelperFactory |
| : public TrustTokenRequestHelperFactory { |
| public: |
| MockTrustTokenRequestHelperFactory( |
| mojom::TrustTokenOperationStatus creation_failure_error, |
| SyncOrAsync sync_or_async) |
| : TrustTokenRequestHelperFactory( |
| nullptr, |
| noop_key_commitment_getter.get(), |
| base::BindRepeating(&ReturnNullNetworkContextClient), |
| {}), |
| sync_or_async_(sync_or_async), |
| creation_failure_error_(creation_failure_error) {} |
| |
| MockTrustTokenRequestHelperFactory( |
| std::optional<mojom::TrustTokenOperationStatus> on_begin, |
| std::optional<mojom::TrustTokenOperationStatus> on_finalize, |
| SyncOrAsync sync_or_async, |
| bool* begin_done_flag) |
| : TrustTokenRequestHelperFactory( |
| nullptr, |
| noop_key_commitment_getter.get(), |
| base::BindRepeating(&ReturnNullNetworkContextClient), |
| {}), |
| sync_or_async_(sync_or_async), |
| helper_( |
| std::make_unique<MockTrustTokenRequestHelper>(on_begin, |
| on_finalize, |
| sync_or_async, |
| begin_done_flag)) {} |
| |
| void CreateTrustTokenHelperForRequest( |
| const url::Origin& top_frame_origin, |
| const net::HttpRequestHeaders& headers, |
| const mojom::TrustTokenParams& params, |
| const net::NetLogWithSource& net_log, |
| base::OnceCallback<void(TrustTokenStatusOrRequestHelper)> done) override { |
| if (creation_failure_error_) { |
| switch (sync_or_async_) { |
| case SyncOrAsync::kSync: { |
| std::move(done).Run(std::move(*creation_failure_error_)); |
| return; |
| } |
| case SyncOrAsync::kAsync: |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(done), |
| std::move(*creation_failure_error_))); |
| return; |
| } |
| } |
| |
| switch (sync_or_async_) { |
| case SyncOrAsync::kSync: { |
| std::move(done).Run(std::move(helper_)); |
| return; |
| } |
| case SyncOrAsync::kAsync: |
| base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask( |
| FROM_HERE, base::BindOnce(std::move(done), std::move(helper_))); |
| return; |
| } |
| |
| NOTREACHED(); |
| } |
| |
| private: |
| SyncOrAsync sync_or_async_; |
| std::optional<mojom::TrustTokenOperationStatus> creation_failure_error_; |
| std::unique_ptr<TrustTokenRequestHelper> helper_; |
| }; |
| |
| class MockTrustTokenDevToolsObserver : public MockDevToolsObserver { |
| public: |
| MockTrustTokenDevToolsObserver() = default; |
| ~MockTrustTokenDevToolsObserver() override = default; |
| |
| void OnTrustTokenOperationDone( |
| const std::string& devtool_request_id, |
| network::mojom::TrustTokenOperationResultPtr result) override { |
| // Event should be only triggered once. |
| EXPECT_FALSE(trust_token_operation_status_.has_value()); |
| trust_token_operation_status_ = result->status; |
| } |
| |
| const std::optional<mojom::TrustTokenOperationStatus> |
| trust_token_operation_status() const { |
| return trust_token_operation_status_; |
| } |
| |
| private: |
| std::optional<mojom::TrustTokenOperationStatus> |
| trust_token_operation_status_ = std::nullopt; |
| }; |
| |
| class ExpectBypassCacheInterceptor : public net::URLRequestInterceptor { |
| public: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_TRUE(request->load_flags() & net::LOAD_BYPASS_CACHE); |
| return nullptr; |
| } |
| }; |
| |
| class ExpectCookieSettingOverridesURLRequestInterceptor |
| : public net::URLRequestInterceptor { |
| public: |
| explicit ExpectCookieSettingOverridesURLRequestInterceptor( |
| net::CookieSettingOverrides cookie_setting_overrides, |
| bool* was_intercepted) |
| : cookie_setting_overrides_(cookie_setting_overrides), |
| was_intercepted_(was_intercepted) {} |
| |
| // net::URLRequestInterceptor: |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_FALSE(*was_intercepted_); |
| EXPECT_EQ(request->cookie_setting_overrides(), cookie_setting_overrides_); |
| *was_intercepted_ = true; |
| return nullptr; |
| } |
| |
| private: |
| const net::CookieSettingOverrides cookie_setting_overrides_; |
| const raw_ptr<bool> was_intercepted_; |
| }; |
| |
| } // namespace |
| |
| class URLLoaderSyncOrAsyncTrustTokenOperationTest |
| : public URLLoaderTest, |
| public ::testing::WithParamInterface<SyncOrAsync> { |
| public: |
| void OnServerReceivedRequest(const net::test_server::HttpRequest&) override { |
| EXPECT_TRUE(outbound_trust_token_operation_was_successful_); |
| } |
| |
| protected: |
| ResourceRequest CreateTrustTokenResourceRequest() { |
| GURL request_url = test_server()->GetURL("/simple_page.html"); |
| ResourceRequest request = CreateResourceRequest("GET", request_url); |
| request.trust_token_params = |
| OptionalTrustTokenParams(mojom::TrustTokenParams::New()); |
| // Set the devtools id to trigger the OnTrustTokenOperationDone call. |
| request.devtools_request_id = "TEST"; |
| |
| // Any Trust Token URLRequest that makes it to Start() should have the |
| // BYPASS_CACHE flag set. |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| request_url, std::make_unique<ExpectBypassCacheInterceptor>()); |
| return request; |
| } |
| |
| // Maintain a flag, set by the mock trust token request helper, denoting |
| // whether we've successfully executed the outbound Trust Tokens operation. |
| // This is used to make URLLoader does not send its main request before it |
| // has completed the outbound part of its Trust Tokens operation (this |
| // involves checking preconditions and potentially annotating the request with |
| // Trust Tokens-related request headers). |
| bool outbound_trust_token_operation_was_successful_ = false; |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(WithSyncAndAsyncOperations, |
| URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| ::testing::Values(SyncOrAsync::kSync, |
| SyncOrAsync::kAsync)); |
| |
| // An otherwise-successful request with an associated Trust Tokens operation |
| // whose Begin and Finalize steps are both successful should succeed overall. |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenOperationSuccess) { |
| base::HistogramTester histogram_tester; |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus::kOk /* on_begin */, |
| mojom::TrustTokenOperationStatus::kOk /* on_finalize */, GetParam(), |
| &outbound_trust_token_operation_was_successful_); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Net.TrustTokens.OperationOutcome.Issuance", |
| mojom::TrustTokenOperationStatus::kOk, 1); |
| |
| EXPECT_EQ(client()->completion_status().error_code, net::OK); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kOk); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kOk); |
| // The page should still have loaded. |
| base::FilePath file = GetTestFilePath("simple_page.html"); |
| std::string expected; |
| if (!base::ReadFileToString(file, &expected)) { |
| ADD_FAILURE() << "File not found: " << file.value(); |
| return; |
| } |
| EXPECT_EQ(ReadBody(), expected); |
| |
| EXPECT_FALSE(client()->response_head()->headers->raw_headers().empty()); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), false))); |
| } |
| |
| // A request with an associated Trust Tokens operation whose Begin step returns |
| // kAlreadyExists should return a success result immediately, without completing |
| // the load. |
| // |
| // (This is the case exactly when the request is for token redemption, and the |
| // Trust Tokens logic determines that there is already a cached redemption |
| // record stored locally, obviating the need to execute a redemption |
| // operation.) |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenRedemptionRecordCacheHit) { |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus::kAlreadyExists /* on_begin */, |
| std::nullopt /* on_finalize */, GetParam(), |
| &outbound_trust_token_operation_was_successful_); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_EQ(client()->completion_status().error_code, |
| net::ERR_TRUST_TOKEN_OPERATION_SUCCESS_WITHOUT_SENDING_REQUEST); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kAlreadyExists); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kAlreadyExists); |
| |
| EXPECT_FALSE(client()->response_head()); |
| EXPECT_FALSE(client()->response_body().is_valid()); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), false))); |
| } |
| |
| // When a request's associated Trust Tokens operation's Begin step fails, the |
| // request itself should fail immediately. |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenBeginFailure) { |
| base::HistogramTester histogram_tester; |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus::kFailedPrecondition /* on_begin */, |
| std::nullopt /* on_finalize */, GetParam(), |
| &outbound_trust_token_operation_was_successful_); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| histogram_tester.ExpectUniqueSample( |
| "Net.TrustTokens.OperationOutcome.Issuance", |
| mojom::TrustTokenOperationStatus::kFailedPrecondition, 1); |
| |
| EXPECT_EQ(client()->completion_status().error_code, |
| net::ERR_TRUST_TOKEN_OPERATION_FAILED); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kFailedPrecondition); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kFailedPrecondition); |
| |
| EXPECT_FALSE(client()->response_head()); |
| EXPECT_FALSE(client()->response_body().is_valid()); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), false))); |
| } |
| |
| // When a request's associated Trust Tokens operation's Begin step succeeds but |
| // its Finalize step fails, the request itself should fail. |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenFinalizeFailure) { |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus::kOk /* on_begin */, |
| mojom::TrustTokenOperationStatus::kBadResponse /* on_finalize */, |
| GetParam(), &outbound_trust_token_operation_was_successful_); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_EQ(client()->completion_status().error_code, |
| net::ERR_TRUST_TOKEN_OPERATION_FAILED); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kBadResponse); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kBadResponse); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), false))); |
| } |
| |
| // When URLLoader receives a request parameterized to perform a Trust Tokens |
| // operation but fails to create a trust token request helper (because a |
| // universal Trust Tokens precondition is violated, for instance), the request |
| // should fail entirely. |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenRequestHelperCreationFailure) { |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus:: |
| kInternalError /* helper_creation_error */, |
| GetParam()); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_EQ(client()->completion_status().error_code, |
| net::ERR_TRUST_TOKEN_OPERATION_FAILED); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kInternalError); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kInternalError); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), false))); |
| } |
| |
| // When URLLoader receives a request that is blocked by policy, the request |
| // should fail entirely and report a blocked event to the observer. |
| TEST_P(URLLoaderSyncOrAsyncTrustTokenOperationTest, |
| HandlesTrustTokenRequestHelperCreationBlocked) { |
| ResourceRequest request = CreateTrustTokenResourceRequest(); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| MockTrustTokenDevToolsObserver devtools_observer; |
| |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.trust_token_helper_factory = |
| std::make_unique<MockTrustTokenRequestHelperFactory>( |
| mojom::TrustTokenOperationStatus:: |
| kUnauthorized /* helper_creation_error */, |
| GetParam()); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader_remote.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| EXPECT_EQ(client()->completion_status().error_code, |
| net::ERR_TRUST_TOKEN_OPERATION_FAILED); |
| EXPECT_EQ(client()->completion_status().trust_token_operation_status, |
| mojom::TrustTokenOperationStatus::kUnauthorized); |
| // Verify the DevTools event was fired and it has the right status. |
| EXPECT_EQ(devtools_observer.trust_token_operation_status(), |
| mojom::TrustTokenOperationStatus::kUnauthorized); |
| |
| trust_token_observer.WaitForTrustTokens(1u); |
| EXPECT_THAT( |
| trust_token_observer.observed_tokens(), |
| testing::ElementsAre(MatchesTrustTokenDetails( |
| test_server()->GetOrigin(), test_server()->GetOrigin(), true))); |
| } |
| |
| TEST_F(URLLoaderTest, OnRawRequestClientSecurityStateFactory) { |
| MockDevToolsObserver devtools_observer; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.devtools_request_id = "fake-id"; |
| |
| auto client_security_state = mojom::ClientSecurityState::New(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| context().mutable_factory_params().client_security_state = |
| std::move(client_security_state); |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| URLLoaderOptions url_loader_options; |
| MockTrustTokenObserver trust_token_observer; |
| url_loader_options.trust_token_observer = trust_token_observer.GetRemote(); |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawRequest(0); |
| ASSERT_TRUE(devtools_observer.client_security_state()); |
| EXPECT_EQ( |
| devtools_observer.client_security_state()->private_network_request_policy, |
| mojom::PrivateNetworkRequestPolicy::kAllow); |
| EXPECT_EQ(devtools_observer.client_security_state()->is_web_secure_context, |
| false); |
| EXPECT_EQ(devtools_observer.client_security_state()->ip_address_space, |
| mojom::IPAddressSpace::kPublic); |
| } |
| |
| TEST_F(URLLoaderTest, OnRawRequestClientSecurityStateRequest) { |
| MockDevToolsObserver devtools_observer; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.devtools_request_id = "fake-id"; |
| auto client_security_state = mojom::ClientSecurityState::New(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| request.trusted_params->client_security_state = |
| std::move(client_security_state); |
| |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawRequest(0); |
| ASSERT_TRUE(devtools_observer.client_security_state()); |
| EXPECT_EQ( |
| devtools_observer.client_security_state()->private_network_request_policy, |
| mojom::PrivateNetworkRequestPolicy::kAllow); |
| EXPECT_EQ(devtools_observer.client_security_state()->is_web_secure_context, |
| false); |
| EXPECT_EQ(devtools_observer.client_security_state()->ip_address_space, |
| mojom::IPAddressSpace::kPublic); |
| } |
| |
| TEST_F(URLLoaderTest, OnRawRequestClientSecurityStateNotPresent) { |
| MockDevToolsObserver devtools_observer; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.devtools_request_id = "fake-id"; |
| |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawRequest(0); |
| ASSERT_FALSE(devtools_observer.client_security_state()); |
| } |
| |
| TEST_F(URLLoaderTest, OnRawResponseIPAddressSpace) { |
| MockDevToolsObserver devtools_observer; |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.devtools_request_id = "fake-id"; |
| |
| context().mutable_factory_params().process_id = kProcessId; |
| context().mutable_factory_params().is_orb_enabled = false; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| URLLoaderOptions url_loader_options; |
| url_loader_options.devtools_observer = devtools_observer.Bind(); |
| std::unique_ptr<URLLoader> url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| delete_run_loop.Run(); |
| client()->RunUntilComplete(); |
| EXPECT_EQ(net::OK, client()->completion_status().error_code); |
| |
| devtools_observer.WaitUntilRawResponse(0); |
| ASSERT_EQ(devtools_observer.resource_address_space(), |
| mojom::IPAddressSpace::kLoopback); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, OrbDoesNotCloseSocketsWhenResourcesNotBlocked) { |
| orb_enabled_ = true; |
| |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Length: 5\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, 2, "Hello"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kCors; |
| request.request_initiator = initiator; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_EQ(body, "Hello"); |
| |
| // Socket should still be alive, in the socket pool. |
| EXPECT_TRUE(socket_data_reads_writes.socket()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, OrbClosesSocketOnReceivingHeaders) { |
| orb_enabled_ = true; |
| |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Type: text/html\r\n" |
| "X-Content-Type-Options: nosniff\r\n" |
| "Content-Length: 23\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| |
| // Socket should have been destroyed, so it will not be reused. |
| EXPECT_FALSE(socket_data_reads_writes.socket()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| OrbDoesNotCloseSocketsWhenResourcesNotBlockedAfterSniffingMimeType) { |
| orb_enabled_ = true; |
| |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Type: application/json\r\n" |
| "Content-Length: 17\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, 2, "Not actually JSON"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kCors; |
| request.request_initiator = initiator; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_EQ("Not actually JSON", body); |
| |
| // Socket should still be alive, in the socket pool. |
| EXPECT_TRUE(socket_data_reads_writes.socket()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, OrbClosesSocketOnSniffingMimeType) { |
| orb_enabled_ = true; |
| |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Type: application/json\r\n" |
| "Content-Length: 9\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, 2, "{\"x\" : 3}"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| |
| // Socket should have been destroyed, so it will not be reused. |
| EXPECT_FALSE(socket_data_reads_writes.socket()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, CorpClosesSocket) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->cross_origin_embedder_policy.value = |
| mojom::CrossOriginEmbedderPolicyValue::kRequireCorp; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Type: test/plain\r\n" |
| "Content-Length: 23\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, LoadRequest(request)); |
| |
| // Socket should have been destroyed, so it will not be reused. |
| EXPECT_FALSE(socket_data_reads_writes.socket()); |
| } |
| |
| class URLLoaderMockSocketAuctionOnlyTest |
| : public URLLoaderMockSocketTest, |
| public testing::WithParamInterface<std::string> {}; |
| |
| TEST_P(URLLoaderMockSocketAuctionOnlyTest, |
| FetchAuctionOnlySignalsFromRendererClosesSocket) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| const std::string first_read = base::StringPrintf( |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "%s" |
| "Content-Type: text/plain\r\n" |
| "Content-Length: 23\r\n\r\n", |
| GetParam().c_str()); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, first_read), |
| net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = url::Origin::Create(url); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_RESPONSE, LoadRequest(request, /*body=*/nullptr, |
| /*is_trusted=*/false)); |
| |
| // Socket should have been destroyed, so it will not be reused. |
| EXPECT_FALSE(socket_data_reads_writes.socket()); |
| } |
| |
| TEST_P(URLLoaderMockSocketAuctionOnlyTest, |
| FetchAuctionOnlySignalsFromBrowserProcessSucceeds) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| const std::string first_read = base::StringPrintf( |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "%s" |
| "Content-Type: text/plain\r\n" |
| "Content-Length: 23\r\n\r\n", |
| GetParam().c_str()); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, first_read), |
| net::MockRead(net::SYNCHRONOUS, 2, "This should not be read"), |
| }; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = url::Origin::Create(url); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| EXPECT_EQ(net::OK, |
| LoadRequest(request, /*body=*/nullptr, /*is_trusted=*/true)); |
| EXPECT_TRUE(socket_data_reads_writes.socket()); |
| } |
| |
| // TODO(crbug.com/40269364): Remove old names once API users have migrated to |
| // new names. |
| INSTANTIATE_TEST_SUITE_P( |
| All, |
| URLLoaderMockSocketAuctionOnlyTest, |
| testing::Values( |
| "Ad-Auction-Only: true\r\n", |
| "X-FLEDGE-Auction-Only: true\r\n", |
| "Ad-Auction-Only: true\r\nX-FLEDGE-Auction-Only: true\r\n")); |
| |
| TEST_F(URLLoaderMockSocketTest, PrivateNetworkRequestPolicyDoesNotCloseSocket) { |
| auto client_security_state = NewSecurityState(); |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| // No data should be read or written. Trying to do so will assert. |
| net::SequencedSocketData socket_data_no_reads_no_writes; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_no_reads_no_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| |
| // Socket should not be closed, since it can be reused. |
| EXPECT_TRUE(socket_data_no_reads_no_writes.socket()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, SniffMime) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, "HTTP/1.1 200 OK\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, kTestHtml, |
| /*result=*/0, |
| /*seq=*/2), |
| net::MockRead(net::SYNCHRONOUS, net::OK, /*seq=*/3)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| EXPECT_EQ(body, kTestHtml); |
| } |
| |
| // Regression test for https://crbug.com/404165029. Ensures MIME sniffing works |
| // correctly with incremental body reads, addressing a bug where the sniffing |
| // hint was not properly maintained. |
| TEST_F(URLLoaderMockSocketTest, SniffMimeByteByByteRead) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| std::vector<net::MockRead> reads; |
| reads.emplace_back(net::SYNCHRONOUS, /*seq=*/reads.size() + 1, |
| "HTTP/1.1 200 OK\r\n\r\n"); |
| for (size_t i = 0; i < kTestHtml.size(); ++i) { |
| reads.emplace_back(net::SYNCHRONOUS, kTestHtml.substr(i, 1), |
| /*result=*/0, /*seq=*/reads.size() + 1); |
| } |
| reads.emplace_back(net::SYNCHRONOUS, net::OK, /*seq=*/reads.size() + 1); |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(reads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| EXPECT_EQ(body, kTestHtml); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, CompressedResponseSniffMime) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| const auto compressed = net::CompressGzip(kTestHtml); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2), |
| net::MockRead(net::SYNCHRONOUS, net::OK, /*seq=*/3)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| EXPECT_EQ(body, kTestHtml); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeSync) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| const auto compressed = net::CompressGzip(kTestHtml); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2), |
| net::MockRead(net::SYNCHRONOUS, net::OK, /*seq=*/3)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeAsync) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| const auto compressed = net::CompressGzip(kTestHtml); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::ASYNC, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::ASYNC, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2), |
| net::MockRead(net::ASYNC, net::OK, /*seq=*/3)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeAsyncDelaySniff) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| // SniffForHTML() in mime_sniffer.cc sniffs the head 512 bytes. So add spaces |
| // to delay the completion of sniffing by padding the beginning with spaces to |
| // make the total 512 bytes. |
| const std::string large_space_and_html = |
| base::StrCat({std::string(512 - kTestHtml.size(), ' '), kTestHtml}); |
| const auto compressed = net::CompressGzip(large_space_and_html); |
| std::vector<net::MockRead> reads; |
| reads.emplace_back(net::ASYNC, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"); |
| for (size_t i = 0; i < compressed.size(); ++i) { |
| reads.emplace_back(net::ASYNC, |
| base::as_string_view(compressed).substr(i, 1), |
| /*result=*/0, /*seq=*/i + 2); |
| } |
| reads.emplace_back(net::ASYNC, net::OK, /*seq=*/compressed.size() + 2); |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(reads, kOriginTestWrites); |
| socket_data_reads_writes.set_receive_buffer_size(1); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(did_mime_sniff()); |
| EXPECT_EQ(mime_type(), "text/html"); |
| |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| |
| EXPECT_THAT(client()->response_head()->client_side_content_decoding_types, |
| ElementsAre(net::SourceStreamType::kGzip)); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeSyncError) { |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, /*result=*/net::ERR_FAILED, |
| /*seq=*/2)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeAsyncError) { |
| const net::MockRead kReads[] = { |
| net::MockRead(net::ASYNC, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::ASYNC, /*result=*/net::ERR_FAILED, |
| /*seq=*/2)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::ERR_FAILED, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeSyncDecodeError) { |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/2, |
| "this is an invalid gzipped data")}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeAsyncDecodeError) { |
| const net::MockRead kReads[] = { |
| net::MockRead(net::ASYNC, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::ASYNC, /*seq=*/2, "this is an invalid gzipped data")}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::ERR_CONTENT_DECODING_FAILED, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeEmpty) { |
| const net::MockRead kReads[] = { |
| net::MockRead(net::ASYNC, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::ASYNC, /*result=*/net::OK, /*seq=*/2)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| set_sniff(); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingSniffMimeSyncCancelAfterResponse) { |
| const std::string_view kTestHtml = "<html><body>hello world</body></html>"; |
| const auto compressed = net::CompressGzip(kTestHtml); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, /*seq=*/1, |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n\r\n"), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2), |
| net::MockRead(net::SYNCHRONOUS, net::OK, /*seq=*/3)}; |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| ResourceRequest request = |
| CreateResourceRequest("GET", GURL("http://origin.test/")); |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = mojom::kURLLoadOptionSniffMimeType; |
| url_loader_options.sync_url_loader_client = client_.GetSyncClientWeakPtr(); |
| std::unique_ptr<URLLoader> url_loader; |
| mojo::Remote<mojom::URLLoader> loader; |
| base::RunLoop delete_run_loop; |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, client_.CreateRemote()); |
| client_.SetResponseReceivedCallback( |
| base::BindLambdaForTesting([&]() { client_.response_body_release(); })); |
| delete_run_loop.Run(); |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingOrbClosesSocketOnSniffingMimeType) { |
| orb_enabled_ = true; |
| |
| const auto compressed = net::CompressGzip("{\"x\" : 3}"); |
| const std::string first_read = base::StringPrintf( |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Encoding: gzip\r\n" |
| "Content-Type: application/json\r\n" |
| "Content-Length: %zu\r\n\r\n", |
| compressed.size()); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, first_read), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2)}; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| // Set the flag for ClientSideContentDecoding. |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_TRUE(body.empty()); |
| |
| // Socket should have been destroyed, so it will not be reused. |
| EXPECT_FALSE(socket_data_reads_writes.socket()); |
| } |
| |
| // Tests the code path where client-side decompression is enabled and the |
| // underlying compressed data (corresponding to a chunk of data decompressed by |
| // the PartialDecoder) is larger than the Mojo data pipe's capacity. |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingOrbBodyLargerThanMojoPipe) { |
| orb_enabled_ = true; |
| const auto data_pipe_size = GetDataPipeDefaultAllocationSize( |
| DataPipeAllocationSize::kLargerSizeIfPossible); |
| // Configures the PartialDecoder's internal buffer size to be larger than the |
| // Mojo data pipe. This ensures that when the PartialDecoder accumulates |
| // enough decompressed data to exceed the Mojo pipe size, the corresponding |
| // compressed data will also be larger than the pipe, triggering the specific |
| // code path we want to test. |
| partial_decoder_decoding_buffer_size_ = data_pipe_size + 100; |
| |
| const std::vector<uint8_t> raw_data = CreateRandomBytesWithSeed( |
| /*seed=*/1234, /*size=*/data_pipe_size * 2); |
| const auto compressed = net::CompressGzip(base::as_string_view(raw_data)); |
| // Verify that the compressed data is indeed larger than the Mojo pipe's |
| // capacity, which is a prerequisite for the test scenario. |
| EXPECT_GT(compressed.size(), data_pipe_size); |
| const std::string first_read = base::StringPrintf( |
| "HTTP/1.1 200 OK\r\n" |
| "Content-Encoding: gzip\r\n" |
| "Content-Type: application/json\r\n" |
| "Content-Length: %zu\r\n\r\n", |
| compressed.size()); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, first_read), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2)}; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| // The body might be empty if ORB blocks the response. If not blocked, we |
| // expect the compressed content. |
| if (!body.empty()) { |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| } |
| } |
| |
| TEST_F(URLLoaderMockSocketTest, |
| CompressedResponseClienteSideDecodingOrbNotBlocked) { |
| orb_enabled_ = true; |
| const std::string_view kTestData = " "; |
| const auto compressed = net::CompressGzip(kTestData); |
| const std::string first_read = base::StringPrintf( |
| "HTTP/1.1 200 OK\r\n" |
| "Connection: keep-alive\r\n" |
| "Content-Encoding: gzip\r\n" |
| "Content-Type: application/json\r\n" |
| "Content-Length: %zu\r\n\r\n", |
| compressed.size()); |
| const net::MockRead kReads[] = { |
| net::MockRead(net::SYNCHRONOUS, 1, first_read), |
| net::MockRead(net::SYNCHRONOUS, base::as_string_view(compressed), |
| /*result=*/0, |
| /*seq=*/2)}; |
| |
| net::SequencedSocketData socket_data_connect(kAsyncMockConnect, {}, {}); |
| net::SequencedSocketData socket_data_reads_writes(kReads, kOriginTestWrites); |
| socket_factory_.AddSocketDataProvider(&socket_data_connect); |
| socket_factory_.AddSocketDataProvider(&socket_data_reads_writes); |
| |
| GURL url("http://origin.test/"); |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.client_side_content_decoding_enabled = true; |
| std::string body; |
| EXPECT_EQ(net::OK, LoadRequest(request, &body)); |
| EXPECT_THAT(body, base::as_string_view(compressed)); |
| } |
| |
| TEST_F(URLLoaderTest, WithDnsAliases) { |
| GURL url(test_server_.GetURL(kHostnameWithAliases, "/echo")); |
| |
| EXPECT_EQ(net::OK, Load(url)); |
| ASSERT_TRUE(client_.response_head()); |
| EXPECT_THAT(client_.response_head()->dns_aliases, |
| testing::ElementsAre("alias1", "alias2", "host")); |
| } |
| |
| TEST_F(URLLoaderTest, NoAdditionalDnsAliases) { |
| GURL url(test_server_.GetURL(kHostnameWithoutAliases, "/echo")); |
| |
| EXPECT_EQ(net::OK, Load(url)); |
| ASSERT_TRUE(client_.response_head()); |
| EXPECT_THAT(client_.response_head()->dns_aliases, |
| testing::ElementsAre(kHostnameWithoutAliases)); |
| } |
| |
| TEST_F(URLLoaderTest, |
| PrivateNetworkRequestPolicyReportsOnPrivateNetworkRequestWarn) { |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.devtools_request_id = "fake-id"; |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kWarn; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| MockDevToolsObserver devtools_observer; |
| set_devtools_observer_for_next_request(&devtools_observer); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| |
| devtools_observer.WaitUntilPrivateNetworkRequest(); |
| ASSERT_TRUE(devtools_observer.private_network_request_params()); |
| auto& params = *devtools_observer.private_network_request_params(); |
| ASSERT_TRUE(params.client_security_state); |
| auto& state = params.client_security_state; |
| EXPECT_EQ(state->private_network_request_policy, |
| mojom::PrivateNetworkRequestPolicy::kWarn); |
| EXPECT_EQ(state->is_web_secure_context, false); |
| EXPECT_EQ(state->ip_address_space, mojom::IPAddressSpace::kPublic); |
| EXPECT_EQ(params.resource_address_space, mojom::IPAddressSpace::kLoopback); |
| EXPECT_EQ(params.devtools_request_id, "fake-id"); |
| EXPECT_TRUE(params.is_warning); |
| EXPECT_THAT(params.url.spec(), testing::HasSubstr("simple_page.html")); |
| } |
| |
| TEST_F(URLLoaderTest, |
| PrivateNetworkRequestPolicyReportsOnPrivateNetworkRequestBlock) { |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.devtools_request_id = "fake-id"; |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kBlock; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| MockDevToolsObserver devtools_observer; |
| set_devtools_observer_for_next_request(&devtools_observer); |
| |
| EXPECT_EQ(net::ERR_BLOCKED_BY_LOCAL_NETWORK_ACCESS_CHECKS, |
| LoadRequest(request)); |
| |
| devtools_observer.WaitUntilPrivateNetworkRequest(); |
| ASSERT_TRUE(devtools_observer.private_network_request_params()); |
| auto& params = *devtools_observer.private_network_request_params(); |
| ASSERT_TRUE(params.client_security_state); |
| auto& state = params.client_security_state; |
| EXPECT_EQ(state->private_network_request_policy, |
| mojom::PrivateNetworkRequestPolicy::kBlock); |
| EXPECT_EQ(state->is_web_secure_context, false); |
| EXPECT_EQ(state->ip_address_space, mojom::IPAddressSpace::kPublic); |
| EXPECT_EQ(params.resource_address_space, mojom::IPAddressSpace::kLoopback); |
| EXPECT_EQ(params.devtools_request_id, "fake-id"); |
| EXPECT_FALSE(params.is_warning); |
| EXPECT_THAT(params.url.spec(), testing::HasSubstr("simple_page.html")); |
| } |
| |
| TEST_F(URLLoaderTest, |
| PrivateNetworkRequestPolicyReportsOnPrivateNetworkRequestAllow) { |
| url::Origin initiator = |
| url::Origin::Create(GURL("http://other-origin.test/")); |
| |
| ResourceRequest request = |
| CreateResourceRequest("GET", test_server()->GetURL("/simple_page.html")); |
| request.mode = mojom::RequestMode::kNoCors; |
| request.request_initiator = initiator; |
| request.devtools_request_id = "fake-id"; |
| |
| auto client_security_state = NewSecurityState(); |
| client_security_state->is_web_secure_context = false; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kAllow; |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| MockDevToolsObserver devtools_observer; |
| set_devtools_observer_for_next_request(&devtools_observer); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| |
| // Check that OnPrivateNetworkRequest wasn't triggered. |
| devtools_observer.WaitUntilRawResponse(0); |
| EXPECT_FALSE(devtools_observer.private_network_request_params()); |
| } |
| |
| // An empty ACCEPT_CH frame should skip the client call. |
| TEST_F(URLLoaderFakeTransportInfoTest, AcceptCHFrameEmptyString) { |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = ""; |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // Despite its name, IsError(OK) asserts that the matched value is OK. |
| EXPECT_THAT(Load(url), IsError(net::OK)); |
| EXPECT_FALSE(accept_ch_frame_observer.called()); |
| } |
| |
| TEST_F(URLLoaderFakeTransportInfoTest, AcceptCHFrameParseString) { |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // Despite its name, IsError(OK) asserts that the matched value is OK. |
| EXPECT_THAT(Load(url), IsError(net::OK)); |
| EXPECT_THAT( |
| accept_ch_frame_observer.accept_ch_frame(), |
| testing::ElementsAreArray({mojom::WebClientHintsType::kUAPlatform})); |
| } |
| |
| TEST_F(URLLoaderFakeTransportInfoTest, AcceptCHFrameRemoveDuplicates) { |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform, Sec-CH-UA-Model"; |
| |
| net::HttpRequestHeaders headers; |
| headers.SetHeader("Sec-CH-UA-Model", "foo"); |
| set_additional_headers(headers); |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| EXPECT_THAT(Load(url), IsError(net::OK)); |
| // Headers should be de-dupped. |
| EXPECT_THAT( |
| accept_ch_frame_observer.accept_ch_frame(), |
| testing::ElementsAreArray({mojom::WebClientHintsType::kUAPlatform})); |
| } |
| |
| TEST_F(URLLoaderFakeTransportInfoTest, AcceptCHFrameIgnoreMalformed) { |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| // Non-existent hint. |
| info.accept_ch_frame = "Foo"; |
| |
| const GURL url("http://fake-endpoint"); |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // Despite its name, IsError(OK) asserts that the matched value is OK. |
| EXPECT_THAT(Load(url), IsError(net::OK)); |
| EXPECT_FALSE(accept_ch_frame_observer.called()); |
| } |
| |
| // Test that a request that triggers both Local Network Access checks and |
| // ACCEPT_CH frame processing is handled correctly. |
| TEST_F(URLLoaderFakeTransportInfoTest, LocalNetworkAccessAndAcceptCHFrame) { |
| // Set up enforcement of Local Network Access checks. |
| base::test::ScopedFeatureList feature_list( |
| features::kLocalNetworkAccessChecks); |
| auto client_security_state = NewSecurityState(); |
| client_security_state->ip_address_space = mojom::IPAddressSpace::kPublic; |
| client_security_state->private_network_request_policy = |
| mojom::PrivateNetworkRequestPolicy::kPermissionBlock; |
| set_factory_client_security_state(std::move(client_security_state)); |
| |
| // Simulate that the permission request was granted. |
| TestLNAPermissionURLLoaderNetworkObserver observer( |
| /*permission_granted=*/true); |
| set_network_observer_for_next_request(&observer); |
| |
| // Set up ACCEPT_CH frame. |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| // Trigger a cross-origin resource request that goes to a target URL that will |
| // have an ACCEPT_CH frame. |
| ResourceRequest request = CreateCrossOriginResourceRequest(); |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| request.url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // Despite its name, IsError(OK) asserts that the matched value is OK. |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| EXPECT_THAT( |
| accept_ch_frame_observer.accept_ch_frame(), |
| testing::ElementsAreArray({mojom::WebClientHintsType::kUAPlatform})); |
| } |
| |
| TEST_F(URLLoaderTest, CookieSettingOverridesCopiedToURLRequest) { |
| GURL url = test_server()->GetURL("/simple_page.html"); |
| net::CookieSettingOverrides cookie_setting_overrides = |
| net::CookieSettingOverrides::All(); |
| // The overrides are not allowed to start out with the |
| // `kStorageAccessGrantEligible` or `kStorageAccessGrantEligibleViaHeader` |
| // overrides present. |
| cookie_setting_overrides.Remove( |
| net::CookieSettingOverride::kStorageAccessGrantEligible); |
| cookie_setting_overrides.Remove( |
| net::CookieSettingOverride::kStorageAccessGrantEligibleViaHeader); |
| |
| set_cookie_setting_overrides(cookie_setting_overrides); |
| bool was_intercepted = false; |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<ExpectCookieSettingOverridesURLRequestInterceptor>( |
| cookie_setting_overrides, &was_intercepted)); |
| |
| EXPECT_THAT(Load(url), IsOk()); |
| EXPECT_TRUE(was_intercepted); |
| } |
| |
| TEST_F(URLLoaderTest, ReadAndDiscardBody) { |
| const std::string file = "simple_page.html"; |
| const GURL url = test_server()->GetURL("/" + file); |
| std::optional<int64_t> file_size = base::GetFileSize(GetTestFilePath(file)); |
| ASSERT_TRUE(file_size.has_value()); |
| int64_t actual_size = file_size.value(); |
| |
| TestURLLoaderClient loader_client; |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| SetUpContext(url, /*is_trusted=*/true); |
| URLLoaderOptions url_loader_options; |
| url_loader_options.options = mojom::kURLLoadOptionReadAndDiscardBody; |
| std::unique_ptr<URLLoader> url_loader; |
| base::RunLoop delete_run_loop; |
| mojo::Remote<mojom::URLLoader> loader; |
| url_loader = url_loader_options.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.BindNewPipeAndPassReceiver(), request, |
| loader_client.CreateRemote()); |
| loader_client.RunUntilResponseReceived(); |
| |
| // Response body should not be set. |
| EXPECT_FALSE(loader_client.response_body().is_valid()); |
| |
| loader_client.RunUntilComplete(); |
| const auto& completion_status = loader_client.completion_status(); |
| EXPECT_EQ(completion_status.error_code, net::OK); |
| EXPECT_EQ(completion_status.decoded_body_length, actual_size); |
| EXPECT_EQ(completion_status.encoded_body_length, actual_size); |
| |
| delete_run_loop.Run(); |
| } |
| |
| // These tests verify that LoadTimingInternalInfo is only set for trustworthy |
| // loaders. |
| |
| TEST_F(URLLoaderTest, SetLoadTimingInternalInfoForTrustedLoaders) { |
| GURL url = test_server()->GetURL("/hello.html"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| ASSERT_TRUE(request.trusted_params); |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_TRUE(client()->response_head()->load_timing_internal_info); |
| } |
| |
| TEST_F(URLLoaderTest, DoNotSetLoadTimingInternalInfoForUntrustedLoaders) { |
| GURL url = test_server()->GetURL("/hello.html"); |
| |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| // Clear trusted_params. |
| request.trusted_params = std::nullopt; |
| |
| base::RunLoop delete_run_loop; |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| context().mutable_factory_params().process_id = mojom::kBrowserProcessId; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilResponseBodyArrived(); |
| EXPECT_FALSE(client()->response_head()->load_timing_internal_info); |
| } |
| |
| TEST_F(URLLoaderTest, AcceptCHFrameHintsAlreadyEnabledSkipsObserver) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kOffloadAcceptCHFrameCheck); |
| |
| // 1. Prepare TransportInfo with the ACCEPT_CH frame. |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| const GURL url("https://fake-endpoint"); |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| // 2. Set up MockAcceptCHFrameObserver. |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // 3. Set enabled_client_hints. |
| network::ResourceRequest::TrustedParams::EnabledClientHints |
| enabled_client_hints; |
| enabled_client_hints.origin = url::Origin::Create(url); |
| enabled_client_hints.is_outermost_main_frame = true; |
| enabled_client_hints.hints = std::vector<mojom::WebClientHintsType>{ |
| mojom::WebClientHintsType::kUAPlatform}; |
| set_enabled_client_hints_for_next_request(enabled_client_hints); |
| |
| // 4. Load, expecting net::OK. |
| EXPECT_THAT(Load(url), IsOk()); |
| |
| // 5. the observer should not be called because NeedsObserverCheck() returns |
| // false. |
| EXPECT_FALSE(accept_ch_frame_observer.called()); |
| } |
| |
| TEST_F(URLLoaderTest, AcceptCHFrameNewHintsCallsObserver) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kOffloadAcceptCHFrameCheck); |
| |
| net::TransportInfo info = net::DefaultTransportInfo(); |
| info.accept_ch_frame = "Sec-CH-UA-Platform"; |
| |
| const GURL url("https://fake-endpoint"); |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| url, std::make_unique<FakeTransportInfoInterceptor>(info)); |
| |
| MockAcceptCHFrameObserver accept_ch_frame_observer; |
| set_accept_ch_frame_observer_for_next_request(&accept_ch_frame_observer); |
| |
| // Ensure the client hints is an empty, and not hitting the hints. |
| network::ResourceRequest::TrustedParams::EnabledClientHints |
| enabled_client_hints; |
| enabled_client_hints.origin = url::Origin::Create(url); |
| set_enabled_client_hints_for_next_request(enabled_client_hints); |
| |
| EXPECT_THAT(Load(url), IsOk()); |
| |
| // The observer should not be called because `NeedsObserverCheck()` |
| // returns true. |
| EXPECT_TRUE(accept_ch_frame_observer.called()); |
| EXPECT_THAT(accept_ch_frame_observer.accept_ch_frame(), |
| testing::ElementsAre(mojom::WebClientHintsType::kUAPlatform)); |
| } |
| |
| class SharedStorageRequestHelperURLLoaderTest : public URLLoaderTest { |
| public: |
| void RegisterAdditionalHandlers() override { |
| SharedStorageRequestCount::Reset(); |
| test_server_.RegisterRequestHandler( |
| base::BindRepeating(&HandleSharedStorageRequestMultiple, |
| GetSharedStorageWriteHeaderValues())); |
| net::test_server::RegisterDefaultHandlers(&test_server_); |
| } |
| |
| std::vector<std::string> GetSharedStorageWriteHeaderValues() const { |
| return {"clear, set;value=v;key=k", "append;value=a;key=b, delete;key=k"}; |
| } |
| |
| void SetURLLoaderOptionsForSharedStorageRequest( |
| bool shared_storage_writable_eligible) { |
| observer_ = std::make_unique<SharedStorageTestURLLoaderNetworkObserver>(); |
| url_loader_options_.url_loader_network_observer = observer_->Bind(); |
| url_loader_options_.shared_storage_writable_eligible = |
| shared_storage_writable_eligible; |
| } |
| |
| void WaitForHeadersReceived(size_t expected_total) { |
| observer_->FlushReceivers(); |
| observer_->WaitForHeadersReceived(expected_total); |
| } |
| |
| protected: |
| base::RunLoop delete_run_loop_; |
| mojo::PendingRemote<mojom::URLLoader> loader_remote_; |
| std::unique_ptr<URLLoader> url_loader_; |
| URLLoaderOptions url_loader_options_; |
| std::unique_ptr<SharedStorageTestURLLoaderNetworkObserver> observer_; |
| }; |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, SimpleRequest) { |
| const char kHostname[] = "a.test"; |
| const GURL kRequestUrl = |
| test_server_.GetURL(kHostname, MakeSharedStorageTestPath()); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/true); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| WaitForHeadersReceived(1); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| EXPECT_EQ(observer_->headers_received().front().request_origin, kTestOrigin); |
| EXPECT_THAT(observer_->headers_received().front().methods, |
| ElementsAre(SharedStorageMethodWrapper(MojomClearMethod()), |
| SharedStorageMethodWrapper( |
| MojomSetMethod(/*key=*/u"k", /*value=*/u"v", |
| /*ignore_if_present=*/false)))); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, SimpleRedirect) { |
| const char kHostname[] = "a.test"; |
| const GURL kRequestUrl = test_server_.GetURL( |
| kHostname, "/shared_storage/redirect/write.html?destination.html"); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/true); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| WaitForHeadersReceived(1); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| EXPECT_EQ(observer_->headers_received().front().request_origin, kTestOrigin); |
| EXPECT_THAT(observer_->headers_received().front().methods, |
| ElementsAre(SharedStorageMethodWrapper(MojomClearMethod()), |
| SharedStorageMethodWrapper( |
| MojomSetMethod(/*key=*/u"k", /*value=*/u"v", |
| /*ignore_if_present=*/false)))); |
| |
| // Follow redirect is called by the client. Even if the shared storage request |
| // helper updates headers, `FollowRedirect()` could still be called by the |
| // client without headers changes. |
| url_loader_->FollowRedirect(/*removed_headers=*/{}, /*modified_headers=*/{}, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| client()->RunUntilComplete(); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, MultipleRedirects) { |
| const char kHostname[] = "a.test"; |
| const GURL kRequestUrl = |
| test_server_.GetURL(kHostname, |
| "/shared_storage/redirect/write.html?redirect/" |
| "no_writing.html%3Fdestination/write.html"); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/true); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| WaitForHeadersReceived(1); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| EXPECT_EQ(observer_->headers_received().front().request_origin, kTestOrigin); |
| EXPECT_THAT(observer_->headers_received().front().methods, |
| ElementsAre(SharedStorageMethodWrapper(MojomClearMethod()), |
| SharedStorageMethodWrapper( |
| MojomSetMethod(/*key=*/u"k", /*value=*/u"v", |
| /*ignore_if_present=*/false)))); |
| |
| client()->ClearHasReceivedRedirect(); |
| |
| // Follow redirect is called by the client. Even if the shared storage request |
| // helper updates headers, `FollowRedirect()` could still be called by the |
| // client without headers changes. |
| url_loader_->FollowRedirect(/*removed_headers=*/{}, /*modified_headers=*/{}, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| |
| // No new shared storage headers are observed. |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| |
| // Follow redirect is called by the client. Even if the shared storage request |
| // helper updates headers, `FollowRedirect()` could still be called by the |
| // client without headers changes. |
| url_loader_->FollowRedirect(/*removed_headers=*/{}, /*modified_headers=*/{}, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| client()->RunUntilComplete(); |
| WaitForHeadersReceived(2); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 2u); |
| EXPECT_EQ(observer_->headers_received().back().request_origin, kTestOrigin); |
| EXPECT_THAT( |
| observer_->headers_received().back().methods, |
| ElementsAre(SharedStorageMethodWrapper( |
| MojomAppendMethod(/*key=*/u"b", /*value=*/u"a")), |
| SharedStorageMethodWrapper(MojomDeleteMethod(/*key=*/u"k")))); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, CrossSiteRedirect) { |
| const char kHostname[] = "a.test"; |
| const char kCrossOriginHostname[] = "b.test"; |
| const GURL kRequestUrl = test_server_.GetURL( |
| kHostname, |
| base::StrCat({"/cross-site?", std::string(kCrossOriginHostname), |
| "/shared_storage/destination/write.html"})); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| const url::Origin kCrossOrigin = |
| url::Origin::Create(test_server_.GetURL(kCrossOriginHostname, "/")); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/true); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| |
| // No shared storage headers are received yet. |
| EXPECT_TRUE(observer_->headers_received().empty()); |
| |
| // Follow redirect is called by the client. Even if the shared storage request |
| // helper updates headers, `FollowRedirect()` could still be called by the |
| // client without headers changes. |
| url_loader_->FollowRedirect(/*removed_headers=*/{}, /*modified_headers=*/{}, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| client()->RunUntilComplete(); |
| WaitForHeadersReceived(1); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| EXPECT_EQ(observer_->headers_received().front().request_origin, kCrossOrigin); |
| EXPECT_THAT( |
| observer_->headers_received().front().methods, |
| ElementsAre(SharedStorageMethodWrapper(MojomClearMethod()), |
| SharedStorageMethodWrapper(MojomSetMethod( |
| /*key=*/u"k", u"v", /*ignore_if_present=*/false)))); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, RedirectNoLongerEligible) { |
| const char kHostname[] = "a.test"; |
| const GURL kRequestUrl = test_server_.GetURL( |
| kHostname, "/shared_storage/redirect/new?shared_storage/write.html"); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/true); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| |
| // Simulate having permission revoked by the client, the effect of which is |
| // the request header is removed. |
| std::vector<std::string> removed_headers( |
| {std::string(kSecSharedStorageWritableHeader.data(), |
| kSecSharedStorageWritableHeader.size())}); |
| url_loader_->FollowRedirect(removed_headers, |
| /*modified_headers=*/{}, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| |
| // The `SharedStorageRequestHelper` has `shared_storage_writable_eligible_` |
| // now set to false because the request header was removed. |
| EXPECT_FALSE(url_loader_->shared_storage_request_helper() |
| ->shared_storage_writable_eligible()); |
| client()->RunUntilComplete(); |
| |
| // No shared storage headers are received. |
| EXPECT_TRUE(observer_->headers_received().empty()); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| TEST_F(SharedStorageRequestHelperURLLoaderTest, RedirectBecomesEligible) { |
| const char kHostname[] = "a.test"; |
| const GURL kRequestUrl = test_server_.GetURL( |
| kHostname, "/shared_storage/redirect/new?shared_storage/write.html"); |
| const url::Origin kTestOrigin = url::Origin::Create(kRequestUrl); |
| ResourceRequest request = CreateResourceRequest("GET", kRequestUrl); |
| |
| SetURLLoaderOptionsForSharedStorageRequest( |
| /*shared_storage_writable_eligible=*/false); |
| |
| url_loader_ = url_loader_options_.MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop_, &url_loader_), |
| loader_remote_.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilRedirectReceived(); |
| ASSERT_TRUE(client()->has_received_redirect()); |
| |
| // Simulate having permission restored by the client, the effect of which is |
| // the request header is added. |
| net::HttpRequestHeaders modified_headers; |
| modified_headers.SetHeader(kSecSharedStorageWritableHeader, |
| kSecSharedStorageWritableValue); |
| url_loader_->FollowRedirect(/*removed_headers=*/{}, modified_headers, |
| /*modified_cors_exempt_headers=*/{}, |
| std::nullopt); |
| |
| // The `SharedStorageRequestHelper` has `shared_storage_writable_eligible_` |
| // now set to true because the request header was added. |
| EXPECT_TRUE(url_loader_->shared_storage_request_helper() |
| ->shared_storage_writable_eligible()); |
| client()->RunUntilComplete(); |
| |
| WaitForHeadersReceived(1); |
| |
| EXPECT_EQ(observer_->headers_received().size(), 1u); |
| EXPECT_EQ(observer_->headers_received().front().request_origin, kTestOrigin); |
| EXPECT_THAT(observer_->headers_received().front().methods, |
| ElementsAre(SharedStorageMethodWrapper(MojomClearMethod()), |
| SharedStorageMethodWrapper( |
| MojomSetMethod(/*key=*/u"k", /*value=*/u"v", |
| /*ignore_if_present=*/false)))); |
| |
| delete_run_loop_.Run(); |
| } |
| |
| #if BUILDFLAG(IS_ANDROID) |
| TEST_F(URLLoaderTest, SocketTaggingWorks) { |
| if (!net::CanGetTaggedBytes()) { |
| GTEST_SKIP() << "Skipping test - GetTaggedBytes unsupported."; |
| } |
| GURL url = test_server()->GetURL("/empty.html"); |
| ResourceRequest request = CreateResourceRequest("GET", url); |
| request.request_initiator = url::Origin::Create(url); |
| int32_t tag_val = 0x12345678; |
| uint64_t old_traffic = net::GetTaggedBytes(tag_val); |
| request.socket_tag = net::SocketTag(-1, tag_val); |
| |
| EXPECT_EQ(net::OK, LoadRequest(request)); |
| EXPECT_GT(net::GetTaggedBytes(tag_val), old_traffic); |
| } |
| #endif |
| |
| TEST_F(URLLoaderTest, ParseUnencodedDigest) { |
| base::RunLoop delete_run_loop; |
| ResourceRequest request = CreateResourceRequest( |
| "GET", test_server_.GetURL( |
| "/set-header?" + |
| base::EscapeQueryParamValue( |
| "Unencoded-Digest: " |
| "sha-256=:uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek=:", |
| /*use_plus=*/false))); |
| |
| mojo::PendingRemote<mojom::URLLoader> loader; |
| std::unique_ptr<URLLoader> url_loader; |
| url_loader = URLLoaderOptions().MakeURLLoader( |
| context(), DeleteLoaderCallback(&delete_run_loop, &url_loader), |
| loader.InitWithNewPipeAndPassReceiver(), request, |
| client()->CreateRemote()); |
| |
| client()->RunUntilComplete(); |
| delete_run_loop.Run(); |
| |
| ASSERT_TRUE(client()->response_head()->unencoded_digests->issues.empty()); |
| ASSERT_FALSE(client()->response_head()->unencoded_digests->digests.empty()); |
| std::optional<IntegrityMetadata> expected = |
| IntegrityMetadata::CreateFromBase64( |
| network::mojom::IntegrityAlgorithm::kSha256, |
| "uU0nuZNNPgilLlLX2n2r+sSE7+N6U4DukIj3rOLvzek="); |
| ASSERT_TRUE(expected); |
| EXPECT_EQ(expected, client()->response_head()->unencoded_digests->digests[0]); |
| } |
| |
| class ExpectIgnoreUnsafeMethodForSameSiteLax |
| : public net::URLRequestInterceptor { |
| public: |
| explicit ExpectIgnoreUnsafeMethodForSameSiteLax(bool expected_value) |
| : expected_value_(expected_value) {} |
| |
| std::unique_ptr<net::URLRequestJob> MaybeInterceptRequest( |
| net::URLRequest* request) const override { |
| EXPECT_EQ(expected_value_, |
| request->ignore_unsafe_method_for_same_site_lax()); |
| return nullptr; |
| } |
| |
| private: |
| const bool expected_value_; |
| }; |
| |
| TEST_F(URLLoaderTest, IgnoreUnsafeMethodForSameSiteLaxIsTrue) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kSendSameSiteLaxForFedCM); |
| |
| GURL request_url = test_server()->GetURL("/simple_page.html"); |
| ResourceRequest request = CreateResourceRequest("POST", request_url); |
| request.destination = mojom::RequestDestination::kWebIdentity; |
| request.redirect_mode = mojom::RedirectMode::kError; |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| request_url, |
| std::make_unique<ExpectIgnoreUnsafeMethodForSameSiteLax>(true)); |
| EXPECT_THAT(LoadRequest(request), IsOk()); |
| } |
| |
| TEST_F(URLLoaderTest, IgnoreUnsafeMethodForSameSiteLaxIsFalseWithoutFlag) { |
| // Feature flag not enabled. |
| GURL request_url = test_server()->GetURL("/simple_page.html"); |
| ResourceRequest request = CreateResourceRequest("POST", request_url); |
| request.destination = mojom::RequestDestination::kWebIdentity; |
| request.redirect_mode = mojom::RedirectMode::kError; |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| request_url, |
| std::make_unique<ExpectIgnoreUnsafeMethodForSameSiteLax>(false)); |
| EXPECT_THAT(LoadRequest(request), IsOk()); |
| } |
| |
| TEST_F(URLLoaderTest, |
| IgnoreUnsafeMethodForSameSiteLaxIsFalseForNonWebidentity) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndEnableFeature(features::kSendSameSiteLaxForFedCM); |
| |
| GURL request_url = test_server()->GetURL("/simple_page.html"); |
| ResourceRequest request = CreateResourceRequest("POST", request_url); |
| request.redirect_mode = mojom::RedirectMode::kError; |
| |
| net::URLRequestFilter::GetInstance()->AddUrlInterceptor( |
| request_url, |
| std::make_unique<ExpectIgnoreUnsafeMethodForSameSiteLax>(false)); |
| EXPECT_THAT(LoadRequest(request), IsOk()); |
| } |
| |
| } // namespace network |