| // 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 "chrome/browser/signin/chrome_signin_proxying_url_loader_factory.h" |
| |
| #include <algorithm> |
| #include <memory> |
| |
| #include "base/macros.h" |
| #include "base/run_loop.h" |
| #include "base/test/mock_callback.h" |
| #include "chrome/browser/signin/chrome_signin_helper.h" |
| #include "chrome/browser/signin/header_modification_delegate.h" |
| #include "content/public/test/test_browser_thread_bundle.h" |
| #include "net/traffic_annotation/network_traffic_annotation_test_helper.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "services/network/test/test_url_loader_factory.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using testing::Invoke; |
| using testing::_; |
| |
| namespace signin { |
| |
| namespace { |
| |
| class MockDelegate : public HeaderModificationDelegate { |
| public: |
| MockDelegate() : weak_factory_(this) {} |
| ~MockDelegate() override {} |
| |
| MOCK_METHOD1(ShouldInterceptNavigation, |
| bool(content::NavigationUIData* navigation_ui_data)); |
| MOCK_METHOD2(ProcessRequest, |
| void(ChromeRequestAdapter* request_adapter, |
| const GURL& redirect_url)); |
| MOCK_METHOD2(ProcessResponse, |
| void(ResponseAdapter* response_adapter, |
| const GURL& redirect_url)); |
| |
| base::WeakPtr<MockDelegate> GetWeakPtr() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| private: |
| base::WeakPtrFactory<MockDelegate> weak_factory_; |
| |
| DISALLOW_COPY_AND_ASSIGN(MockDelegate); |
| }; |
| |
| content::ResourceRequestInfo::WebContentsGetter NullWebContentsGetter() { |
| return base::BindRepeating([]() -> content::WebContents* { return nullptr; }); |
| } |
| |
| } // namespace |
| |
| class ChromeSigninProxyingURLLoaderFactoryTest : public testing::Test { |
| public: |
| ChromeSigninProxyingURLLoaderFactoryTest() |
| : test_factory_binding_(&test_factory_) {} |
| ~ChromeSigninProxyingURLLoaderFactoryTest() override {} |
| |
| base::WeakPtr<MockDelegate> StartRequest( |
| std::unique_ptr<network::ResourceRequest> request) { |
| loader_ = network::SimpleURLLoader::Create(std::move(request), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| |
| network::mojom::URLLoaderFactoryPtr factory_ptr; |
| auto factory_request = mojo::MakeRequest(&factory_ptr); |
| loader_->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| factory_ptr.get(), |
| base::BindOnce( |
| &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, |
| base::Unretained(this))); |
| |
| network::mojom::URLLoaderFactoryPtrInfo test_factory_ptr_info; |
| test_factory_binding_.Bind(mojo::MakeRequest(&test_factory_ptr_info)); |
| |
| auto delegate = std::make_unique<MockDelegate>(); |
| base::WeakPtr<MockDelegate> delegate_weak = delegate->GetWeakPtr(); |
| |
| proxying_factory_ = std::make_unique<ProxyingURLLoaderFactory>( |
| std::move(delegate), NullWebContentsGetter(), |
| std::move(factory_request), std::move(test_factory_ptr_info), |
| base::BindOnce(&ChromeSigninProxyingURLLoaderFactoryTest::OnDisconnect, |
| base::Unretained(this))); |
| |
| return delegate_weak; |
| } |
| |
| void CloseFactoryBinding() { test_factory_binding_.Close(); } |
| |
| network::TestURLLoaderFactory* factory() { return &test_factory_; } |
| network::SimpleURLLoader* loader() { return loader_.get(); } |
| std::string* response_body() { return response_body_.get(); } |
| |
| void OnDownloadComplete(std::unique_ptr<std::string> body) { |
| response_body_ = std::move(body); |
| } |
| |
| private: |
| void OnDisconnect(ProxyingURLLoaderFactory* factory) { |
| EXPECT_EQ(factory, proxying_factory_.get()); |
| proxying_factory_.reset(); |
| } |
| |
| content::TestBrowserThreadBundle thread_bundle_; |
| std::unique_ptr<network::SimpleURLLoader> loader_; |
| std::unique_ptr<ProxyingURLLoaderFactory> proxying_factory_; |
| network::TestURLLoaderFactory test_factory_; |
| mojo::Binding<network::mojom::URLLoaderFactory> test_factory_binding_; |
| std::unique_ptr<std::string> response_body_; |
| |
| DISALLOW_COPY_AND_ASSIGN(ChromeSigninProxyingURLLoaderFactoryTest); |
| }; |
| |
| TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, NoModification) { |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GURL("https://google.com/"); |
| |
| factory()->AddResponse("https://google.com/", "Hello."); |
| base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); |
| |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(net::OK, loader()->NetError()); |
| ASSERT_TRUE(response_body()); |
| EXPECT_EQ("Hello.", *response_body()); |
| } |
| |
| TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, ModifyHeaders) { |
| const GURL kTestURL("https://google.com/index.html"); |
| const GURL kTestReferrer("https://chrome.com/referrer.html"); |
| const GURL kTestRedirectURL("https://youtube.com/index.html"); |
| |
| // Set up the request. |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = kTestURL; |
| request->referrer = kTestReferrer; |
| request->resource_type = static_cast<int>(content::RESOURCE_TYPE_MAIN_FRAME); |
| request->is_main_frame = true; |
| request->headers.SetHeader("X-Request-1", "Foo"); |
| |
| base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); |
| |
| // The first destruction callback added by ProcessRequest is expected to be |
| // called. The second (added after a redirect) will not be. |
| base::MockCallback<base::OnceClosure> destruction_callback; |
| EXPECT_CALL(destruction_callback, Run()).Times(1); |
| base::MockCallback<base::OnceClosure> ignored_destruction_callback; |
| EXPECT_CALL(ignored_destruction_callback, Run()).Times(0); |
| |
| // The delegate will be called twice to process a request, first when the |
| // request is started and again when the request is redirected. |
| EXPECT_CALL(*delegate, ProcessRequest(_, _)) |
| .WillOnce( |
| Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { |
| EXPECT_EQ(kTestURL, adapter->GetUrl()); |
| EXPECT_TRUE(adapter->IsMainRequestContext(nullptr /* io_data */)); |
| EXPECT_EQ(content::RESOURCE_TYPE_MAIN_FRAME, |
| adapter->GetResourceType()); |
| EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); |
| |
| EXPECT_TRUE(adapter->HasHeader("X-Request-1")); |
| adapter->RemoveRequestHeaderByName("X-Request-1"); |
| EXPECT_FALSE(adapter->HasHeader("X-Request-1")); |
| |
| adapter->SetExtraHeaderByName("X-Request-2", "Bar"); |
| EXPECT_TRUE(adapter->HasHeader("X-Request-2")); |
| |
| EXPECT_EQ(GURL(), redirect_url); |
| |
| adapter->SetDestructionCallback(destruction_callback.Get()); |
| })) |
| .WillOnce( |
| Invoke([&](ChromeRequestAdapter* adapter, const GURL& redirect_url) { |
| EXPECT_TRUE(adapter->IsMainRequestContext(nullptr)); |
| EXPECT_EQ(content::RESOURCE_TYPE_MAIN_FRAME, |
| adapter->GetResourceType()); |
| |
| // Changes to the URL and referrer take effect after the redirect |
| // is followed. |
| EXPECT_EQ(kTestURL, adapter->GetUrl()); |
| EXPECT_EQ(GURL("https://chrome.com"), adapter->GetReferrerOrigin()); |
| |
| // X-Request-1 and X-Request-2 were modified in the previous call to |
| // ProcessRequest(). These changes should still be present. |
| EXPECT_FALSE(adapter->HasHeader("X-Request-1")); |
| EXPECT_TRUE(adapter->HasHeader("X-Request-2")); |
| |
| adapter->RemoveRequestHeaderByName("X-Request-2"); |
| EXPECT_FALSE(adapter->HasHeader("X-Request-2")); |
| |
| adapter->SetExtraHeaderByName("X-Request-3", "Baz"); |
| EXPECT_TRUE(adapter->HasHeader("X-Request-3")); |
| |
| EXPECT_EQ(kTestRedirectURL, redirect_url); |
| |
| adapter->SetDestructionCallback(ignored_destruction_callback.Get()); |
| })); |
| |
| // The delegate will also be called twice to process a response, first when |
| // the redirect is received and again for the redirect response. |
| EXPECT_CALL(*delegate, ProcessResponse(_, _)) |
| .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { |
| EXPECT_EQ(GURL("https://google.com"), adapter->GetOrigin()); |
| EXPECT_TRUE(adapter->IsMainFrame()); |
| |
| const net::HttpResponseHeaders* headers = adapter->GetHeaders(); |
| EXPECT_TRUE(headers->HasHeader("X-Response-1")); |
| EXPECT_TRUE(headers->HasHeader("X-Response-2")); |
| adapter->RemoveHeader("X-Response-2"); |
| |
| EXPECT_EQ(kTestRedirectURL, redirect_url); |
| })) |
| .WillOnce(Invoke([&](ResponseAdapter* adapter, const GURL& redirect_url) { |
| EXPECT_EQ(GURL("https://youtube.com"), adapter->GetOrigin()); |
| EXPECT_TRUE(adapter->IsMainFrame()); |
| |
| const net::HttpResponseHeaders* headers = adapter->GetHeaders(); |
| // This is a new response and so previous headers should not carry over. |
| EXPECT_FALSE(headers->HasHeader("X-Response-1")); |
| EXPECT_FALSE(headers->HasHeader("X-Response-2")); |
| |
| EXPECT_TRUE(headers->HasHeader("X-Response-3")); |
| EXPECT_TRUE(headers->HasHeader("X-Response-4")); |
| adapter->RemoveHeader("X-Response-3"); |
| |
| EXPECT_EQ(GURL(), redirect_url); |
| })); |
| |
| // Set up a redirect and final response. |
| { |
| net::RedirectInfo redirect_info; |
| redirect_info.new_url = kTestRedirectURL; |
| // An HTTPS to HTTPS redirect such as this wouldn't normally change the |
| // referrer but we do for testing purposes. |
| redirect_info.new_referrer = kTestURL.spec(); |
| |
| network::ResourceResponseHead redirect_head; |
| redirect_head.headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); |
| redirect_head.headers->AddHeader("X-Response-1: Foo"); |
| redirect_head.headers->AddHeader("X-Response-2: Bar"); |
| |
| network::ResourceResponseHead response_head; |
| response_head.headers = base::MakeRefCounted<net::HttpResponseHeaders>(""); |
| response_head.headers->AddHeader("X-Response-3: Foo"); |
| response_head.headers->AddHeader("X-Response-4: Bar"); |
| std::string body("Hello."); |
| network::URLLoaderCompletionStatus status; |
| status.decoded_body_length = body.size(); |
| |
| factory()->AddResponse(kTestURL, response_head, body, status, |
| {{redirect_info, redirect_head}}); |
| } |
| |
| // Wait for the request to complete and check the response. |
| base::RunLoop().RunUntilIdle(); |
| EXPECT_EQ(net::OK, loader()->NetError()); |
| const network::ResourceResponseHead* response_head = loader()->ResponseInfo(); |
| ASSERT_TRUE(response_head && response_head->headers); |
| EXPECT_FALSE(response_head->headers->HasHeader("X-Response-3")); |
| EXPECT_TRUE(response_head->headers->HasHeader("X-Response-4")); |
| ASSERT_TRUE(response_body()); |
| EXPECT_EQ("Hello.", *response_body()); |
| |
| // NOTE: TestURLLoaderFactory currently does not expose modifications to |
| // request headers and so we cannot verify that the modifications have been |
| // passed to the target URLLoader. |
| } |
| |
| TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, TargetFactoryFailure) { |
| network::mojom::URLLoaderFactoryPtr factory_ptr; |
| auto factory_request = mojo::MakeRequest(&factory_ptr); |
| network::mojom::URLLoaderFactoryPtrInfo target_factory_ptr_info; |
| auto target_factory_request = mojo::MakeRequest(&target_factory_ptr_info); |
| |
| // Without a target factory the proxy will process no requests. |
| auto delegate = std::make_unique<MockDelegate>(); |
| EXPECT_CALL(*delegate, ProcessRequest(_, _)).Times(0); |
| |
| auto proxying_factory = std::make_unique<ProxyingURLLoaderFactory>( |
| std::move(delegate), NullWebContentsGetter(), std::move(factory_request), |
| std::move(target_factory_ptr_info), base::DoNothing()); |
| |
| // Close |target_factory_request| instead of binding it to a URLLoaderFactory. |
| // Spin the message loop so that the connection error handler can run. |
| target_factory_request = nullptr; |
| base::RunLoop().RunUntilIdle(); |
| |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GURL("https://google.com"); |
| auto loader = network::SimpleURLLoader::Create(std::move(request), |
| TRAFFIC_ANNOTATION_FOR_TESTS); |
| loader->DownloadToStringOfUnboundedSizeUntilCrashAndDie( |
| factory_ptr.get(), |
| base::BindOnce( |
| &ChromeSigninProxyingURLLoaderFactoryTest::OnDownloadComplete, |
| base::Unretained(this))); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(response_body()); |
| EXPECT_EQ(net::ERR_FAILED, loader->NetError()); |
| } |
| |
| TEST_F(ChromeSigninProxyingURLLoaderFactoryTest, RequestKeepAlive) { |
| // Start the request. |
| auto request = std::make_unique<network::ResourceRequest>(); |
| request->url = GURL("https://google.com"); |
| base::WeakPtr<MockDelegate> delegate = StartRequest(std::move(request)); |
| base::RunLoop().RunUntilIdle(); |
| |
| // Close the factory binding and spin the message loop again to allow the |
| // connection error handler to be called. |
| CloseFactoryBinding(); |
| base::RunLoop().RunUntilIdle(); |
| |
| // The ProxyingURLLoaderFactory should not have been destroyed yet because |
| // there is still an in progress request that has not been completed. |
| EXPECT_TRUE(delegate); |
| |
| // Complete the request. |
| factory()->AddResponse("https://google.com", "Hello."); |
| base::RunLoop().RunUntilIdle(); |
| |
| EXPECT_FALSE(delegate); |
| EXPECT_EQ(net::OK, loader()->NetError()); |
| ASSERT_TRUE(response_body()); |
| EXPECT_EQ("Hello.", *response_body()); |
| } |
| |
| } // namespace signin |