| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/public/platform/web_resource_request_sender.h" |
| |
| #include <stddef.h> |
| #include <stdint.h> |
| |
| #include <memory> |
| #include <string> |
| #include <tuple> |
| #include <utility> |
| #include <vector> |
| |
| #include "base/feature_list.h" |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/test/task_environment.h" |
| #include "mojo/public/cpp/bindings/pending_receiver.h" |
| #include "mojo/public/cpp/bindings/pending_remote.h" |
| #include "mojo/public/cpp/bindings/remote.h" |
| #include "net/base/net_errors.h" |
| #include "net/base/request_priority.h" |
| #include "net/http/http_response_headers.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/resource_request.h" |
| #include "services/network/public/cpp/url_loader_completion_status.h" |
| #include "services/network/public/cpp/weak_wrapper_shared_url_loader_factory.h" |
| #include "services/network/public/mojom/url_response_head.mojom.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/common/loader/referrer_utils.h" |
| #include "third_party/blink/public/platform/resource_load_info_notifier_wrapper.h" |
| #include "third_party/blink/public/platform/scheduler/test/renderer_scheduler_test_support.h" |
| #include "third_party/blink/public/platform/web_back_forward_cache_loader_helper.h" |
| #include "third_party/blink/public/platform/web_request_peer.h" |
| #include "third_party/blink/public/platform/web_resource_request_sender_delegate.h" |
| #include "third_party/blink/public/platform/web_url_request_extra_data.h" |
| #include "third_party/blink/renderer/platform/testing/testing_platform_support.h" |
| #include "url/gurl.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| static constexpr char kTestPageUrl[] = "http://www.google.com/"; |
| static constexpr char kTestPageHeaders[] = |
| "HTTP/1.1 200 OK\nContent-Type:text/html\n\n"; |
| static constexpr char kTestPageMimeType[] = "text/html"; |
| static constexpr char kTestPageCharset[] = ""; |
| static constexpr char kTestPageContents[] = |
| "<html><head><title>Google</title></head><body><h1>Google</h1></body></" |
| "html>"; |
| |
| constexpr size_t kDataPipeCapacity = 4096; |
| |
| std::string ReadOneChunk(mojo::ScopedDataPipeConsumerHandle* handle) { |
| char buffer[kDataPipeCapacity]; |
| uint32_t read_bytes = kDataPipeCapacity; |
| MojoResult result = |
| (*handle)->ReadData(buffer, &read_bytes, MOJO_READ_DATA_FLAG_NONE); |
| if (result != MOJO_RESULT_OK) |
| return ""; |
| return std::string(buffer, read_bytes); |
| } |
| |
| // Returns a fake TimeTicks based on the given microsecond offset. |
| base::TimeTicks TicksFromMicroseconds(int64_t micros) { |
| return base::TimeTicks() + base::TimeDelta::FromMicroseconds(micros); |
| } |
| |
| } // namespace |
| |
| class TestResourceRequestSenderDelegate |
| : public WebResourceRequestSenderDelegate { |
| public: |
| TestResourceRequestSenderDelegate() = default; |
| ~TestResourceRequestSenderDelegate() override = default; |
| |
| void OnRequestComplete() override {} |
| |
| scoped_refptr<WebRequestPeer> OnReceivedResponse( |
| scoped_refptr<WebRequestPeer> current_peer, |
| const WebString& mime_type, |
| const WebURL& url) override { |
| return base::MakeRefCounted<WrapperPeer>(std::move(current_peer)); |
| } |
| |
| class WrapperPeer : public WebRequestPeer { |
| public: |
| explicit WrapperPeer(scoped_refptr<WebRequestPeer> original_peer) |
| : original_peer_(std::move(original_peer)) {} |
| |
| // WebRequestPeer overrides: |
| void OnUploadProgress(uint64_t position, uint64_t size) override {} |
| bool OnReceivedRedirect(const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head, |
| std::vector<std::string>*) override { |
| return false; |
| } |
| void OnReceivedResponse(network::mojom::URLResponseHeadPtr head) override { |
| response_head_ = std::move(head); |
| } |
| void OnStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle body) override { |
| body_handle_ = std::move(body); |
| } |
| void OnTransferSizeUpdated(int transfer_size_diff) override {} |
| void OnCompletedRequest( |
| const network::URLLoaderCompletionStatus& status) override { |
| original_peer_->OnReceivedResponse(std::move(response_head_)); |
| original_peer_->OnStartLoadingResponseBody(std::move(body_handle_)); |
| original_peer_->OnCompletedRequest(status); |
| } |
| |
| private: |
| scoped_refptr<WebRequestPeer> original_peer_; |
| network::mojom::URLResponseHeadPtr response_head_; |
| mojo::ScopedDataPipeConsumerHandle body_handle_; |
| |
| DISALLOW_COPY_AND_ASSIGN(WrapperPeer); |
| }; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(TestResourceRequestSenderDelegate); |
| }; |
| |
| // A mock WebRequestPeer to receive messages from the WebResourceRequestSender. |
| class MockRequestPeer : public WebRequestPeer { |
| public: |
| explicit MockRequestPeer(WebResourceRequestSender* resource_request_sender) |
| : resource_request_sender_(resource_request_sender) {} |
| |
| // WebRequestPeer overrides: |
| void OnUploadProgress(uint64_t position, uint64_t size) override {} |
| bool OnReceivedRedirect(const net::RedirectInfo& redirect_info, |
| network::mojom::URLResponseHeadPtr head, |
| std::vector<std::string>* removed_headers) override { |
| last_load_timing_ = head->load_timing; |
| return true; |
| } |
| void OnReceivedResponse(network::mojom::URLResponseHeadPtr head) override { |
| last_load_timing_ = head->load_timing; |
| received_response_ = true; |
| if (cancel_on_receive_response_) { |
| resource_request_sender_->Cancel( |
| scheduler::GetSingleThreadTaskRunnerForTesting()); |
| } |
| } |
| void OnStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle body) override { |
| if (cancel_on_receive_response_) |
| return; |
| if (body) { |
| data_ += ReadOneChunk(&body); |
| } |
| } |
| void OnTransferSizeUpdated(int transfer_size_diff) override {} |
| void OnReceivedCachedMetadata(mojo_base::BigBuffer data) override {} |
| void OnCompletedRequest( |
| const network::URLLoaderCompletionStatus& status) override { |
| if (cancel_on_receive_response_) |
| return; |
| completion_status_ = status; |
| complete_ = true; |
| } |
| |
| std::string data() { return data_; } |
| bool received_response() { return received_response_; } |
| bool complete() { return complete_; } |
| net::LoadTimingInfo last_load_timing() { return last_load_timing_; } |
| network::URLLoaderCompletionStatus completion_status() { |
| return completion_status_; |
| } |
| |
| void SetCancelOnReceiveResponse(bool cancel_on_receive_response) { |
| cancel_on_receive_response_ = cancel_on_receive_response; |
| } |
| |
| private: |
| // Data received. If downloading to file, remains empty. |
| std::string data_; |
| |
| bool received_response_ = false; |
| bool complete_ = false; |
| bool cancel_on_receive_response_ = false; |
| net::LoadTimingInfo last_load_timing_; |
| network::URLLoaderCompletionStatus completion_status_; |
| WebResourceRequestSender* resource_request_sender_ = nullptr; |
| }; // namespace blink |
| |
| // Sets up the message sender override for the unit test. |
| class WebResourceRequestSenderTest : public testing::Test, |
| public network::mojom::URLLoaderFactory { |
| public: |
| explicit WebResourceRequestSenderTest() |
| : platform_(&delegate_), |
| resource_request_sender_(new WebResourceRequestSender()) {} |
| |
| ~WebResourceRequestSenderTest() override { |
| resource_request_sender_.reset(); |
| base::RunLoop().RunUntilIdle(); |
| } |
| |
| void CreateLoaderAndStart( |
| mojo::PendingReceiver<network::mojom::URLLoader> receiver, |
| int32_t routing_id, |
| int32_t request_id, |
| uint32_t options, |
| const network::ResourceRequest& url_request, |
| mojo::PendingRemote<network::mojom::URLLoaderClient> client, |
| const net::MutableNetworkTrafficAnnotationTag& annotation) override { |
| loader_and_clients_.emplace_back(std::move(receiver), std::move(client)); |
| } |
| |
| void Clone(mojo::PendingReceiver<network::mojom::URLLoaderFactory> receiver) |
| override { |
| NOTREACHED(); |
| } |
| |
| void CallOnReceiveResponse(network::mojom::URLLoaderClient* client) { |
| auto head = network::mojom::URLResponseHead::New(); |
| std::string raw_headers(kTestPageHeaders); |
| std::replace(raw_headers.begin(), raw_headers.end(), '\n', '\0'); |
| head->headers = new net::HttpResponseHeaders(raw_headers); |
| head->mime_type = kTestPageMimeType; |
| head->charset = kTestPageCharset; |
| client->OnReceiveResponse(std::move(head)); |
| } |
| |
| std::unique_ptr<network::ResourceRequest> CreateResourceRequest() { |
| std::unique_ptr<network::ResourceRequest> request( |
| new network::ResourceRequest()); |
| |
| request->method = "GET"; |
| request->url = GURL(kTestPageUrl); |
| request->site_for_cookies = |
| net::SiteForCookies::FromUrl(GURL(kTestPageUrl)); |
| request->referrer_policy = ReferrerUtils::GetDefaultNetReferrerPolicy(); |
| request->resource_type = |
| static_cast<int>(mojom::ResourceType::kSubResource); |
| request->priority = net::LOW; |
| request->mode = network::mojom::RequestMode::kNoCors; |
| |
| auto url_request_extra_data = |
| base::MakeRefCounted<WebURLRequestExtraData>(); |
| url_request_extra_data->CopyToResourceRequest(request.get()); |
| |
| return request; |
| } |
| |
| WebResourceRequestSender* sender() { return resource_request_sender_.get(); } |
| |
| void StartAsync(std::unique_ptr<network::ResourceRequest> request, |
| scoped_refptr<WebRequestPeer> peer) { |
| sender()->SendAsync( |
| std::move(request), 0, scheduler::GetSingleThreadTaskRunnerForTesting(), |
| TRAFFIC_ANNOTATION_FOR_TESTS, false, WebVector<WebString>(), |
| std::move(peer), |
| base::MakeRefCounted<network::WeakWrapperSharedURLLoaderFactory>(this), |
| std::vector<std::unique_ptr<URLLoaderThrottle>>(), |
| std::make_unique<ResourceLoadInfoNotifierWrapper>( |
| /*resource_load_info_notifier=*/nullptr), |
| WebBackForwardCacheLoaderHelper()); |
| } |
| |
| static MojoCreateDataPipeOptions DataPipeOptions() { |
| MojoCreateDataPipeOptions options; |
| options.struct_size = sizeof(MojoCreateDataPipeOptions); |
| options.flags = MOJO_CREATE_DATA_PIPE_FLAG_NONE; |
| options.element_num_bytes = 1; |
| options.capacity_num_bytes = kDataPipeCapacity; |
| return options; |
| } |
| |
| class TestPlatform final : public TestingPlatformSupport { |
| public: |
| explicit TestPlatform(WebResourceRequestSenderDelegate* delegate) |
| : delegate_(delegate) {} |
| WebResourceRequestSenderDelegate* GetResourceRequestSenderDelegate() |
| override { |
| return delegate_; |
| } |
| |
| private: |
| WebResourceRequestSenderDelegate* delegate_; |
| }; |
| |
| protected: |
| std::vector<std::pair<mojo::PendingReceiver<network::mojom::URLLoader>, |
| mojo::PendingRemote<network::mojom::URLLoaderClient>>> |
| loader_and_clients_; |
| TestResourceRequestSenderDelegate delegate_; |
| base::test::SingleThreadTaskEnvironment task_environment_; |
| ScopedTestingPlatformSupport<TestPlatform, WebResourceRequestSenderDelegate*> |
| platform_; |
| std::unique_ptr<WebResourceRequestSender> resource_request_sender_; |
| |
| scoped_refptr<MockRequestPeer> mock_peer_; |
| }; |
| |
| // Tests the generation of unique request ids. |
| TEST_F(WebResourceRequestSenderTest, MakeRequestID) { |
| int first_id = WebResourceRequestSender::MakeRequestID(); |
| int second_id = WebResourceRequestSender::MakeRequestID(); |
| |
| // Child process ids are unique (per process) and counting from 0 upwards: |
| EXPECT_GT(second_id, first_id); |
| EXPECT_GE(first_id, 0); |
| } |
| |
| TEST_F(WebResourceRequestSenderTest, DelegateTest) { |
| std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest()); |
| mock_peer_ = |
| base::MakeRefCounted<MockRequestPeer>(resource_request_sender_.get()); |
| StartAsync(std::move(request), mock_peer_); |
| |
| ASSERT_EQ(1u, loader_and_clients_.size()); |
| mojo::Remote<network::mojom::URLLoaderClient> client( |
| std::move(loader_and_clients_[0].second)); |
| loader_and_clients_.clear(); |
| |
| // The wrapper eats all messages until RequestComplete message is sent. |
| CallOnReceiveResponse(client.get()); |
| |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| auto options = DataPipeOptions(); |
| ASSERT_EQ(mojo::CreateDataPipe(&options, producer_handle, consumer_handle), |
| MOJO_RESULT_OK); |
| client->OnStartLoadingResponseBody(std::move(consumer_handle)); |
| |
| uint32_t size = strlen(kTestPageContents); |
| auto result = producer_handle->WriteData(kTestPageContents, &size, |
| MOJO_WRITE_DATA_FLAG_NONE); |
| ASSERT_EQ(result, MOJO_RESULT_OK); |
| ASSERT_EQ(size, strlen(kTestPageContents)); |
| |
| producer_handle.reset(); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(mock_peer_->received_response()); |
| |
| // This lets the wrapper peer pass all the messages to the original |
| // peer at once. |
| network::URLLoaderCompletionStatus status; |
| status.error_code = net::OK; |
| status.exists_in_cache = false; |
| status.encoded_data_length = strlen(kTestPageContents); |
| client->OnComplete(status); |
| |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_TRUE(mock_peer_->received_response()); |
| EXPECT_EQ(kTestPageContents, mock_peer_->data()); |
| EXPECT_TRUE(mock_peer_->complete()); |
| } |
| |
| TEST_F(WebResourceRequestSenderTest, CancelDuringCallbackWithWrapperPeer) { |
| std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest()); |
| mock_peer_ = |
| base::MakeRefCounted<MockRequestPeer>(resource_request_sender_.get()); |
| mock_peer_->SetCancelOnReceiveResponse(true); |
| StartAsync(std::move(request), mock_peer_); |
| |
| ASSERT_EQ(1u, loader_and_clients_.size()); |
| mojo::Remote<network::mojom::URLLoaderClient> client( |
| std::move(loader_and_clients_[0].second)); |
| loader_and_clients_.clear(); |
| |
| CallOnReceiveResponse(client.get()); |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| auto options = DataPipeOptions(); |
| ASSERT_EQ(mojo::CreateDataPipe(&options, producer_handle, consumer_handle), |
| MOJO_RESULT_OK); |
| client->OnStartLoadingResponseBody(std::move(consumer_handle)); |
| uint32_t size = strlen(kTestPageContents); |
| auto result = producer_handle->WriteData(kTestPageContents, &size, |
| MOJO_WRITE_DATA_FLAG_NONE); |
| ASSERT_EQ(result, MOJO_RESULT_OK); |
| ASSERT_EQ(size, strlen(kTestPageContents)); |
| producer_handle.reset(); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_FALSE(mock_peer_->received_response()); |
| |
| // This lets the wrapper peer pass all the messages to the original |
| // peer at once, but the original peer cancels right after it receives |
| // the response. (This will remove pending request info from |
| // WebResourceRequestSender while the wrapper peer is still running |
| // OnCompletedRequest, but it should not lead to crashes.) |
| network::URLLoaderCompletionStatus status; |
| status.error_code = net::OK; |
| status.exists_in_cache = false; |
| status.encoded_data_length = strlen(kTestPageContents); |
| client->OnComplete(status); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_TRUE(mock_peer_->received_response()); |
| // Request should have been cancelled with no additional messages. |
| // EXPECT_TRUE(peer_context.cancelled); |
| EXPECT_EQ("", mock_peer_->data()); |
| EXPECT_FALSE(mock_peer_->complete()); |
| } |
| |
| class TimeConversionTest : public WebResourceRequestSenderTest { |
| public: |
| void PerformTest(network::mojom::URLResponseHeadPtr response_head) { |
| std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest()); |
| StartAsync(std::move(request), mock_peer_); |
| |
| ASSERT_EQ(1u, loader_and_clients_.size()); |
| mojo::Remote<network::mojom::URLLoaderClient> client( |
| std::move(loader_and_clients_[0].second)); |
| loader_and_clients_.clear(); |
| client->OnReceiveResponse(std::move(response_head)); |
| } |
| |
| const network::mojom::URLResponseHead& response_info() const { |
| return *response_info_; |
| } |
| |
| private: |
| network::mojom::URLResponseHeadPtr response_info_ = |
| network::mojom::URLResponseHead::New(); |
| }; |
| |
| // TODO(simonjam): Enable this when 10829031 lands. |
| TEST_F(TimeConversionTest, DISABLED_ProperlyInitialized) { |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->request_start = TicksFromMicroseconds(5); |
| response_head->response_start = TicksFromMicroseconds(15); |
| response_head->load_timing.request_start_time = base::Time::Now(); |
| response_head->load_timing.request_start = TicksFromMicroseconds(10); |
| response_head->load_timing.connect_timing.connect_start = |
| TicksFromMicroseconds(13); |
| |
| auto request_start = response_head->load_timing.request_start; |
| PerformTest(std::move(response_head)); |
| |
| EXPECT_LT(base::TimeTicks(), response_info().load_timing.request_start); |
| EXPECT_EQ(base::TimeTicks(), |
| response_info().load_timing.connect_timing.dns_start); |
| EXPECT_LE(request_start, |
| response_info().load_timing.connect_timing.connect_start); |
| } |
| |
| TEST_F(TimeConversionTest, PartiallyInitialized) { |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->request_start = TicksFromMicroseconds(5); |
| response_head->response_start = TicksFromMicroseconds(15); |
| |
| PerformTest(std::move(response_head)); |
| |
| EXPECT_EQ(base::TimeTicks(), response_info().load_timing.request_start); |
| EXPECT_EQ(base::TimeTicks(), |
| response_info().load_timing.connect_timing.dns_start); |
| } |
| |
| TEST_F(TimeConversionTest, NotInitialized) { |
| auto response_head = network::mojom::URLResponseHead::New(); |
| |
| PerformTest(std::move(response_head)); |
| |
| EXPECT_EQ(base::TimeTicks(), response_info().load_timing.request_start); |
| EXPECT_EQ(base::TimeTicks(), |
| response_info().load_timing.connect_timing.dns_start); |
| } |
| |
| class CompletionTimeConversionTest : public WebResourceRequestSenderTest { |
| public: |
| void PerformTest(base::TimeTicks remote_request_start, |
| base::TimeTicks completion_time, |
| base::TimeDelta delay) { |
| std::unique_ptr<network::ResourceRequest> request(CreateResourceRequest()); |
| mock_peer_ = |
| base::MakeRefCounted<MockRequestPeer>(resource_request_sender_.get()); |
| StartAsync(std::move(request), mock_peer_); |
| |
| ASSERT_EQ(1u, loader_and_clients_.size()); |
| mojo::Remote<network::mojom::URLLoaderClient> client( |
| std::move(loader_and_clients_[0].second)); |
| auto response_head = network::mojom::URLResponseHead::New(); |
| response_head->request_start = remote_request_start; |
| response_head->load_timing.request_start = remote_request_start; |
| response_head->load_timing.receive_headers_end = remote_request_start; |
| // We need to put something non-null time, otherwise no values will be |
| // copied. |
| response_head->load_timing.request_start_time = |
| base::Time() + base::TimeDelta::FromSeconds(99); |
| client->OnReceiveResponse(std::move(response_head)); |
| |
| mojo::ScopedDataPipeProducerHandle producer_handle; |
| mojo::ScopedDataPipeConsumerHandle consumer_handle; |
| ASSERT_EQ(mojo::CreateDataPipe(nullptr, producer_handle, consumer_handle), |
| MOJO_RESULT_OK); |
| client->OnStartLoadingResponseBody(std::move(consumer_handle)); |
| producer_handle.reset(); // The response is empty. |
| |
| network::URLLoaderCompletionStatus status; |
| status.completion_time = completion_time; |
| |
| client->OnComplete(status); |
| |
| const base::TimeTicks until = base::TimeTicks::Now() + delay; |
| while (base::TimeTicks::Now() < until) |
| base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(1)); |
| base::RunLoop().RunUntilIdle(); |
| loader_and_clients_.clear(); |
| } |
| |
| base::TimeTicks request_start() const { |
| EXPECT_TRUE(mock_peer_->received_response()); |
| return mock_peer_->last_load_timing().request_start; |
| } |
| base::TimeTicks completion_time() const { |
| EXPECT_TRUE(mock_peer_->complete()); |
| return mock_peer_->completion_status().completion_time; |
| } |
| }; |
| |
| TEST_F(CompletionTimeConversionTest, NullCompletionTimestamp) { |
| const auto remote_request_start = |
| base::TimeTicks() + base::TimeDelta::FromMilliseconds(4); |
| |
| PerformTest(remote_request_start, base::TimeTicks(), base::TimeDelta()); |
| |
| EXPECT_EQ(base::TimeTicks(), completion_time()); |
| } |
| |
| TEST_F(CompletionTimeConversionTest, RemoteRequestStartIsUnavailable) { |
| base::TimeTicks begin = base::TimeTicks::Now(); |
| |
| const auto remote_completion_time = |
| base::TimeTicks() + base::TimeDelta::FromMilliseconds(8); |
| |
| PerformTest(base::TimeTicks(), remote_completion_time, base::TimeDelta()); |
| |
| base::TimeTicks end = base::TimeTicks::Now(); |
| EXPECT_LE(begin, completion_time()); |
| EXPECT_LE(completion_time(), end); |
| } |
| |
| TEST_F(CompletionTimeConversionTest, Convert) { |
| const auto remote_request_start = |
| base::TimeTicks() + base::TimeDelta::FromMilliseconds(4); |
| |
| const auto remote_completion_time = |
| remote_request_start + base::TimeDelta::FromMilliseconds(3); |
| |
| PerformTest(remote_request_start, remote_completion_time, |
| base::TimeDelta::FromMilliseconds(15)); |
| |
| EXPECT_EQ(completion_time(), |
| request_start() + base::TimeDelta::FromMilliseconds(3)); |
| } |
| |
| } // namespace blink |