| // Copyright 2018 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "content/common/mime_sniffing_throttle.h" |
| |
| #include <memory> |
| |
| #include "base/bind.h" |
| #include "base/run_loop.h" |
| #include "base/test/scoped_task_environment.h" |
| #include "content/common/mime_sniffing_url_loader.h" |
| #include "content/public/common/url_loader_throttle.h" |
| #include "mojo/public/cpp/system/data_pipe_utils.h" |
| #include "services/network/test/test_url_loader_client.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "url/gurl.h" |
| |
| namespace content { |
| |
| namespace { |
| |
| class MojoDataPipeSender { |
| public: |
| MojoDataPipeSender(mojo::ScopedDataPipeProducerHandle handle) |
| : handle_(std::move(handle)), |
| watcher_(FROM_HERE, mojo::SimpleWatcher::ArmingPolicy::AUTOMATIC) {} |
| |
| void Start(std::string data, base::OnceClosure done_callback) { |
| data_ = std::move(data); |
| done_callback_ = std::move(done_callback); |
| watcher_.Watch(handle_.get(), |
| MOJO_HANDLE_SIGNAL_WRITABLE | MOJO_HANDLE_SIGNAL_PEER_CLOSED, |
| base::BindRepeating(&MojoDataPipeSender::OnWritable, |
| base::Unretained(this))); |
| } |
| |
| void OnWritable(MojoResult) { |
| uint32_t sending_bytes = data_.size() - sent_bytes_; |
| MojoResult result = handle_->WriteData( |
| data_.c_str() + sent_bytes_, &sending_bytes, MOJO_WRITE_DATA_FLAG_NONE); |
| switch (result) { |
| case MOJO_RESULT_OK: |
| break; |
| case MOJO_RESULT_FAILED_PRECONDITION: |
| // Finished unexpectedly. |
| std::move(done_callback_).Run(); |
| return; |
| case MOJO_RESULT_SHOULD_WAIT: |
| // Just wait until OnWritable() is called by the watcher. |
| return; |
| default: |
| NOTREACHED(); |
| return; |
| } |
| sent_bytes_ += sending_bytes; |
| if (data_.size() == sent_bytes_) |
| std::move(done_callback_).Run(); |
| } |
| |
| mojo::ScopedDataPipeProducerHandle ReleaseHandle() { |
| return std::move(handle_); |
| } |
| |
| bool has_succeeded() const { return data_.size() == sent_bytes_; } |
| |
| private: |
| mojo::ScopedDataPipeProducerHandle handle_; |
| mojo::SimpleWatcher watcher_; |
| base::OnceClosure done_callback_; |
| std::string data_; |
| uint32_t sent_bytes_ = 0; |
| }; |
| |
| class MockDelegate : public URLLoaderThrottle::Delegate { |
| public: |
| // Implements URLLoaderThrottle::Delegate. |
| void CancelWithError(int error_code, |
| base::StringPiece custom_reason) override { |
| NOTIMPLEMENTED(); |
| } |
| void Resume() override { |
| is_resumed_ = true; |
| // Resume from OnReceiveResponse() with a customized response header. |
| destination_loader_client()->OnReceiveResponse( |
| updated_response_head().value()); |
| } |
| |
| void SetPriority(net::RequestPriority priority) override { NOTIMPLEMENTED(); } |
| void UpdateDeferredResponseHead( |
| const network::ResourceResponseHead& new_response_head) override { |
| updated_response_head_ = new_response_head; |
| } |
| void PauseReadingBodyFromNet() override { NOTIMPLEMENTED(); } |
| void ResumeReadingBodyFromNet() override { NOTIMPLEMENTED(); } |
| void InterceptResponse( |
| network::mojom::URLLoaderPtr new_loader, |
| network::mojom::URLLoaderClientRequest new_client_request, |
| network::mojom::URLLoaderPtr* original_loader, |
| network::mojom::URLLoaderClientRequest* original_client_request) |
| override { |
| is_intercepted_ = true; |
| |
| destination_loader_ptr_ = std::move(new_loader); |
| ASSERT_TRUE(mojo::FuseInterface( |
| std::move(new_client_request), |
| destination_loader_client_.CreateInterfacePtr().PassInterface())); |
| source_loader_request_ = mojo::MakeRequest(original_loader); |
| *original_client_request = mojo::MakeRequest(&source_loader_client_ptr_); |
| } |
| |
| void LoadResponseBody(const std::string& body) { |
| if (!source_body_handle_.is_valid()) { |
| // Send OnStartLoadingResponseBody() if it's the first call. |
| mojo::ScopedDataPipeConsumerHandle consumer; |
| EXPECT_EQ(MOJO_RESULT_OK, |
| mojo::CreateDataPipe(nullptr, &source_body_handle_, &consumer)); |
| source_loader_client()->OnStartLoadingResponseBody(std::move(consumer)); |
| } |
| |
| MojoDataPipeSender sender(std::move(source_body_handle_)); |
| base::RunLoop loop; |
| sender.Start(body, loop.QuitClosure()); |
| loop.Run(); |
| |
| EXPECT_TRUE(sender.has_succeeded()); |
| source_body_handle_ = sender.ReleaseHandle(); |
| } |
| |
| void CompleteResponse() { |
| source_loader_client()->OnComplete(network::URLLoaderCompletionStatus()); |
| source_body_handle_.reset(); |
| } |
| |
| uint32_t ReadResponseBody(uint32_t size) { |
| std::vector<uint8_t> buffer(size); |
| MojoResult result = destination_loader_client_.response_body().ReadData( |
| buffer.data(), &size, MOJO_READ_DATA_FLAG_NONE); |
| switch (result) { |
| case MOJO_RESULT_OK: |
| return size; |
| case MOJO_RESULT_FAILED_PRECONDITION: |
| return 0; |
| case MOJO_RESULT_SHOULD_WAIT: |
| return 0; |
| default: |
| NOTREACHED(); |
| } |
| return 0; |
| } |
| |
| bool is_intercepted() const { return is_intercepted_; } |
| bool is_resumed() const { return is_resumed_; } |
| |
| const base::Optional<network::ResourceResponseHead>& updated_response_head() |
| const { |
| return updated_response_head_; |
| } |
| |
| network::TestURLLoaderClient* destination_loader_client() { |
| return &destination_loader_client_; |
| } |
| |
| network::mojom::URLLoaderClient* source_loader_client() { |
| return source_loader_client_ptr_.get(); |
| } |
| |
| private: |
| bool is_intercepted_ = false; |
| bool is_resumed_ = false; |
| base::Optional<network::ResourceResponseHead> updated_response_head_; |
| |
| // A pair of a loader and a loader client for destination of the response. |
| network::mojom::URLLoaderPtr destination_loader_ptr_; |
| network::TestURLLoaderClient destination_loader_client_; |
| |
| // A pair of a loader and a loader client for source of the response. |
| network::mojom::URLLoaderClientPtr source_loader_client_ptr_; |
| network::mojom::URLLoaderRequest source_loader_request_; |
| |
| mojo::ScopedDataPipeProducerHandle source_body_handle_; |
| }; |
| |
| } // namespace |
| |
| class MimeSniffingThrottleTest : public testing::Test { |
| protected: |
| // Be the first member so it is destroyed last. |
| base::test::ScopedTaskEnvironment scoped_task_environment_; |
| }; |
| |
| TEST_F(MimeSniffingThrottleTest, NoMimeTypeWithSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("https://example.com"), &response_head, |
| &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, SniffableMimeTypeWithSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| response_head.mime_type = "text/plain"; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("https://example.com"), &response_head, |
| &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, NotSniffableMimeTypeWithSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| response_head.mime_type = "text/javascript"; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("https://example.com"), &response_head, |
| &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, NoMimeTypeWithNotSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("wss://example.com"), &response_head, |
| &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, SniffableMimeTypeWithNotSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| response_head.mime_type = "text/plain"; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("wss://example.com"), &response_head, |
| &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, NotSniffableMimeTypeWithNotSniffableScheme) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| response_head.mime_type = "text/javascript"; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("wss://example.com"), &response_head, |
| &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, SniffableButAlreadySniffed) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| network::ResourceResponseHead response_head; |
| response_head.mime_type = "text/plain"; |
| response_head.did_mime_sniff = true; |
| bool defer = false; |
| throttle->WillProcessResponse(GURL("https://example.com"), &response_head, |
| &defer); |
| EXPECT_FALSE(defer); |
| EXPECT_FALSE(delegate->is_intercepted()); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, NoBody) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Call OnComplete() without sending body. |
| delegate->source_loader_client()->OnComplete( |
| network::URLLoaderCompletionStatus(net::ERR_FAILED)); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // The mime type should be updated to the default mime type ("text/plain"). |
| EXPECT_TRUE(delegate->destination_loader_client()->has_received_response()); |
| EXPECT_EQ("text/plain", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, EmptyBody) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| mojo::DataPipe pipe; |
| delegate->source_loader_client()->OnStartLoadingResponseBody( |
| std::move(pipe.consumer_handle)); |
| pipe.producer_handle.reset(); // The pipe is empty. |
| |
| delegate->source_loader_client()->OnComplete( |
| network::URLLoaderCompletionStatus()); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // The mime type should be updated to the default mime type ("text/plain"). |
| EXPECT_TRUE(delegate->destination_loader_client()->has_received_response()); |
| EXPECT_EQ("text/plain", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, Body_PlainText) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Send the body and complete the response. |
| delegate->LoadResponseBody("This is a text."); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // The mime type should be updated. |
| EXPECT_TRUE(delegate->is_resumed()); |
| EXPECT_EQ("text/plain", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, Body_Docx) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com/hogehoge.docx"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Send the body and complete the response. |
| delegate->LoadResponseBody("\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // The mime type should be updated. |
| EXPECT_TRUE(delegate->is_resumed()); |
| EXPECT_EQ("application/msword", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, Body_PNG) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com/hogehoge.docx"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Send the body and complete the response. |
| delegate->LoadResponseBody("\x89PNG\x0D\x0A\x1A\x0A"); |
| delegate->CompleteResponse(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // The mime type should be updated. |
| EXPECT_TRUE(delegate->is_resumed()); |
| EXPECT_EQ("image/png", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, Body_LongPlainText) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // 64KiB is coming from the default value used in |
| // mojo::core::Core::CreateDataPipe(). |
| const uint32_t kDefaultDataPipeBufferSize = 64 * 1024; |
| std::string long_body(kDefaultDataPipeBufferSize * 2, 'x'); |
| |
| // Send the data to the MimeSniffingURLLoader. |
| // |delegate|'s MojoDataPipeSender sends the first |
| // |kDefaultDataPipeBufferSize| bytes to MimeSniffingURLLoader and |
| // MimeSniffingURLLoader will read the first |kDefaultDataPipeBufferSize| |
| // bytes of the body, so the MojoDataPipeSender can push the rest of |
| // |kDefaultDataPipeBufferSize| of the body soon and finishes sending the |
| // body. After this, MimeSniffingURLLoader is waiting to push the body to the |
| // destination data pipe since the pipe should be full until it's read. |
| delegate->LoadResponseBody(long_body); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| // Send OnComplete() to the MimeSniffingURLLoader. |
| delegate->CompleteResponse(); |
| scoped_task_environment_.RunUntilIdle(); |
| // MimeSniffingURLLoader should not send OnComplete() to the destination |
| // client until it finished writing all the data. |
| EXPECT_FALSE( |
| delegate->destination_loader_client()->has_received_completion()); |
| |
| // Read the half of the body. This unblocks MimeSniffingURLLoader to push the |
| // rest of the body to the data pipe. |
| uint32_t read_bytes = delegate->ReadResponseBody(long_body.size() / 2); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| // Read the rest of the body. |
| read_bytes += delegate->ReadResponseBody(long_body.size() / 2); |
| scoped_task_environment_.RunUntilIdle(); |
| delegate->destination_loader_client()->RunUntilComplete(); |
| |
| // Check if all data has been read. |
| EXPECT_EQ(long_body.size(), read_bytes); |
| |
| // The mime type should be updated. |
| EXPECT_TRUE(delegate->is_resumed()); |
| EXPECT_EQ("text/plain", |
| delegate->destination_loader_client()->response_head().mime_type); |
| } |
| |
| TEST_F(MimeSniffingThrottleTest, Abort_NoBodyPipe) { |
| auto throttle = std::make_unique<MimeSniffingThrottle>( |
| scoped_task_environment_.GetMainThreadTaskRunner()); |
| auto delegate = std::make_unique<MockDelegate>(); |
| throttle->set_delegate(delegate.get()); |
| |
| GURL response_url("https://example.com"); |
| network::ResourceResponseHead response_head; |
| bool defer = false; |
| throttle->WillProcessResponse(response_url, &response_head, &defer); |
| EXPECT_TRUE(defer); |
| EXPECT_TRUE(delegate->is_intercepted()); |
| |
| // Send the body |
| std::string body = "This should be long enough to complete sniffing."; |
| body.resize(1024, 'a'); |
| delegate->LoadResponseBody(body); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| // Release a pipe for the body on the receiver side. |
| delegate->destination_loader_client()->response_body_release(); |
| scoped_task_environment_.RunUntilIdle(); |
| |
| // Send the body after the pipe is closed. The the loader aborts. |
| delegate->LoadResponseBody("This is a text."); |
| |
| // Calling OnComplete should not crash. |
| delegate->CompleteResponse(); |
| scoped_task_environment_.RunUntilIdle(); |
| } |
| |
| } // namespace content |