| /* |
| * Copyright (c) 2015, Google Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are |
| * met: |
| * |
| * * Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * * Redistributions in binary form must reproduce the above |
| * copyright notice, this list of conditions and the following disclaimer |
| * in the documentation and/or other materials provided with the |
| * distribution. |
| * * Neither the name of Google Inc. nor the names of its |
| * contributors may be used to endorse or promote products derived from |
| * this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
| * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
| * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
| * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
| * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
| * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
| * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
| * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "third_party/blink/renderer/core/loader/frame_fetch_context.h" |
| |
| #include <memory> |
| |
| #include "base/test/scoped_feature_list.h" |
| #include "build/build_config.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/public/common/device_memory/approximated_device_memory.h" |
| #include "third_party/blink/public/common/features.h" |
| #include "third_party/blink/public/mojom/fetch/fetch_api_request.mojom-blink.h" |
| #include "third_party/blink/public/mojom/loader/request_context_frame_type.mojom-blink.h" |
| #include "third_party/blink/public/platform/scheduler/web_scoped_virtual_time_pauser.h" |
| #include "third_party/blink/public/platform/web_client_hints_type.h" |
| #include "third_party/blink/public/platform/web_document_subresource_filter.h" |
| #include "third_party/blink/public/platform/web_insecure_request_policy.h" |
| #include "third_party/blink/public/platform/web_runtime_features.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/frame/ad_tracker.h" |
| #include "third_party/blink/renderer/core/frame/frame_owner.h" |
| #include "third_party/blink/renderer/core/frame/frame_types.h" |
| #include "third_party/blink/renderer/core/frame/local_dom_window.h" |
| #include "third_party/blink/renderer/core/frame/local_frame_view.h" |
| #include "third_party/blink/renderer/core/frame/navigator.h" |
| #include "third_party/blink/renderer/core/frame/settings.h" |
| #include "third_party/blink/renderer/core/html/html_iframe_element.h" |
| #include "third_party/blink/renderer/core/loader/document_loader.h" |
| #include "third_party/blink/renderer/core/loader/empty_clients.h" |
| #include "third_party/blink/renderer/core/loader/frame_resource_fetcher_properties.h" |
| #include "third_party/blink/renderer/core/loader/subresource_filter.h" |
| #include "third_party/blink/renderer/core/page/page.h" |
| #include "third_party/blink/renderer/core/testing/dummy_page_holder.h" |
| #include "third_party/blink/renderer/platform/heap/heap.h" |
| #include "third_party/blink/renderer/platform/instrumentation/use_counter.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/fetch_initiator_info.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_loader_options.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_request.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_timing_info.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/unique_identifier.h" |
| #include "third_party/blink/renderer/platform/loader/testing/mock_resource.h" |
| #include "third_party/blink/renderer/platform/network/network_state_notifier.h" |
| #include "third_party/blink/renderer/platform/testing/histogram_tester.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/weborigin/kurl.h" |
| #include "third_party/blink/renderer/platform/weborigin/reporting_disposition.h" |
| |
| namespace blink { |
| |
| using Checkpoint = testing::StrictMock<testing::MockFunction<void(int)>>; |
| |
| class FrameFetchContextMockLocalFrameClient : public EmptyLocalFrameClient { |
| public: |
| FrameFetchContextMockLocalFrameClient() : EmptyLocalFrameClient() {} |
| MOCK_METHOD0(DidDisplayContentWithCertificateErrors, void()); |
| MOCK_METHOD2(DispatchDidLoadResourceFromMemoryCache, |
| void(const ResourceRequest&, const ResourceResponse&)); |
| MOCK_METHOD0(UserAgent, String()); |
| MOCK_METHOD0(MayUseClientLoFiForImageRequests, bool()); |
| MOCK_CONST_METHOD0(GetPreviewsStateForFrame, WebURLRequest::PreviewsState()); |
| }; |
| |
| class FixedPolicySubresourceFilter : public WebDocumentSubresourceFilter { |
| public: |
| FixedPolicySubresourceFilter(LoadPolicy policy, |
| int* filtered_load_counter, |
| bool is_associated_with_ad_subframe) |
| : policy_(policy), filtered_load_counter_(filtered_load_counter) {} |
| |
| LoadPolicy GetLoadPolicy(const WebURL& resource_url, |
| mojom::RequestContextType) override { |
| return policy_; |
| } |
| |
| LoadPolicy GetLoadPolicyForWebSocketConnect(const WebURL& url) override { |
| return policy_; |
| } |
| |
| void ReportDisallowedLoad() override { ++*filtered_load_counter_; } |
| |
| bool ShouldLogToConsole() override { return false; } |
| |
| private: |
| const LoadPolicy policy_; |
| int* filtered_load_counter_; |
| }; |
| |
| class FrameFetchContextTest : public testing::Test { |
| protected: |
| void SetUp() override { RecreateFetchContext(); } |
| |
| void RecreateFetchContext(const KURL& url = KURL(), |
| const String& feature_policy_header = String()) { |
| dummy_page_holder = std::make_unique<DummyPageHolder>(IntSize(500, 500)); |
| dummy_page_holder->GetPage().SetDeviceScaleFactorDeprecated(1.0); |
| if (url.IsValid()) { |
| auto params = WebNavigationParams::CreateWithHTMLBuffer( |
| SharedBuffer::Create(), url); |
| if (!feature_policy_header.IsEmpty()) { |
| params->response.SetHttpHeaderField(http_names::kFeaturePolicy, |
| feature_policy_header); |
| } |
| dummy_page_holder->GetFrame().Loader().CommitNavigation( |
| std::move(params), nullptr /* extra_data */); |
| blink::test::RunPendingTasks(); |
| ASSERT_EQ(url.GetString(), |
| dummy_page_holder->GetDocument().Url().GetString()); |
| } |
| document = &dummy_page_holder->GetDocument(); |
| owner = MakeGarbageCollected<DummyFrameOwner>(); |
| } |
| |
| FrameFetchContext* GetFetchContext() { |
| return static_cast<FrameFetchContext*>(&document->Fetcher()->Context()); |
| } |
| |
| // Call the method for the actual test cases as only this fixture is specified |
| // as a friend class. |
| void SetFirstPartyCookie(ResourceRequest& request) { |
| GetFetchContext()->SetFirstPartyCookie(request); |
| } |
| |
| scoped_refptr<const SecurityOrigin> GetTopFrameOrigin() { |
| return GetFetchContext()->GetTopFrameOrigin(); |
| } |
| |
| std::unique_ptr<DummyPageHolder> dummy_page_holder; |
| // We don't use the DocumentLoader directly in any tests, but need to keep it |
| // around as long as the ResourceFetcher and Document live due to indirect |
| // usage. |
| Persistent<Document> document; |
| |
| Persistent<DummyFrameOwner> owner; |
| }; |
| |
| class FrameFetchContextSubresourceFilterTest : public FrameFetchContextTest { |
| protected: |
| void SetUp() override { |
| FrameFetchContextTest::SetUp(); |
| filtered_load_callback_counter_ = 0; |
| } |
| |
| void TearDown() override { |
| document->Loader()->SetSubresourceFilter(nullptr); |
| FrameFetchContextTest::TearDown(); |
| } |
| |
| int GetFilteredLoadCallCount() const { |
| return filtered_load_callback_counter_; |
| } |
| |
| void SetFilterPolicy(WebDocumentSubresourceFilter::LoadPolicy policy, |
| bool is_associated_with_ad_subframe = false) { |
| document->Loader()->SetSubresourceFilter( |
| MakeGarbageCollected<SubresourceFilter>( |
| document->ToExecutionContext(), |
| std::make_unique<FixedPolicySubresourceFilter>( |
| policy, &filtered_load_callback_counter_, |
| is_associated_with_ad_subframe))); |
| } |
| |
| base::Optional<ResourceRequestBlockedReason> CanRequest() { |
| return CanRequestInternal(ReportingDisposition::kReport); |
| } |
| |
| base::Optional<ResourceRequestBlockedReason> CanRequestKeepAlive() { |
| return CanRequestInternal(ReportingDisposition::kReport, |
| true /* keepalive */); |
| } |
| |
| base::Optional<ResourceRequestBlockedReason> CanRequestPreload() { |
| return CanRequestInternal(ReportingDisposition::kSuppressReporting); |
| } |
| |
| base::Optional<ResourceRequestBlockedReason> CanRequestAndVerifyIsAd( |
| bool expect_is_ad) { |
| base::Optional<ResourceRequestBlockedReason> reason = |
| CanRequestInternal(ReportingDisposition::kReport); |
| ResourceRequest request(KURL("http://example.com/")); |
| EXPECT_EQ(expect_is_ad, GetFetchContext()->CalculateIfAdSubresource( |
| request, ResourceType::kMock)); |
| return reason; |
| } |
| |
| void AppendExecutingScriptToAdTracker(const String& url) { |
| AdTracker* ad_tracker = document->GetFrame()->GetAdTracker(); |
| ad_tracker->WillExecuteScript(document->ToExecutionContext(), url); |
| } |
| |
| void AppendAdScriptToAdTracker(const KURL& ad_script_url) { |
| AdTracker* ad_tracker = document->GetFrame()->GetAdTracker(); |
| ad_tracker->AppendToKnownAdScripts(*document->ToExecutionContext(), |
| ad_script_url.GetString()); |
| } |
| |
| private: |
| base::Optional<ResourceRequestBlockedReason> CanRequestInternal( |
| ReportingDisposition reporting_disposition, |
| bool keepalive = false) { |
| const KURL input_url("http://example.com/"); |
| ResourceRequest resource_request(input_url); |
| resource_request.SetKeepalive(keepalive); |
| resource_request.SetRequestorOrigin(document->Fetcher() |
| ->GetProperties() |
| .GetFetchClientSettingsObject() |
| .GetSecurityOrigin()); |
| ResourceLoaderOptions options; |
| return GetFetchContext()->CanRequest( |
| ResourceType::kImage, resource_request, input_url, options, |
| reporting_disposition, ResourceRequest::RedirectStatus::kNoRedirect); |
| } |
| |
| int filtered_load_callback_counter_; |
| }; |
| |
| // This test class sets up a mock frame loader client. |
| class FrameFetchContextMockedLocalFrameClientTest |
| : public FrameFetchContextTest { |
| protected: |
| void SetUp() override { |
| url = KURL("https://example.test/foo"); |
| http_url = KURL("http://example.test/foo"); |
| main_resource_url = KURL("https://example.test"); |
| different_host_url = KURL("https://different.example.test/foo"); |
| client = MakeGarbageCollected< |
| testing::NiceMock<FrameFetchContextMockLocalFrameClient>>(); |
| dummy_page_holder = |
| std::make_unique<DummyPageHolder>(IntSize(500, 500), nullptr, client); |
| dummy_page_holder->GetPage().SetDeviceScaleFactorDeprecated(1.0); |
| Page::InsertOrdinaryPageForTesting(&dummy_page_holder->GetPage()); |
| document = &dummy_page_holder->GetDocument(); |
| document->SetURL(main_resource_url); |
| owner = MakeGarbageCollected<DummyFrameOwner>(); |
| } |
| |
| KURL url; |
| KURL http_url; |
| KURL main_resource_url; |
| KURL different_host_url; |
| |
| Persistent<testing::NiceMock<FrameFetchContextMockLocalFrameClient>> client; |
| }; |
| |
| class FrameFetchContextModifyRequestTest : public FrameFetchContextTest { |
| public: |
| FrameFetchContextModifyRequestTest() |
| : example_origin(SecurityOrigin::Create(KURL("https://example.test/"))) {} |
| |
| protected: |
| void ModifyRequestForCSP(ResourceRequest& resource_request, |
| mojom::RequestContextFrameType frame_type) { |
| document->GetFrame()->Loader().RecordLatestRequiredCSP(); |
| document->GetFrame()->Loader().ModifyRequestForCSP( |
| resource_request, |
| &document->Fetcher()->GetProperties().GetFetchClientSettingsObject(), |
| document.Get(), frame_type); |
| } |
| |
| void ExpectUpgrade(const char* input, const char* expected) { |
| ExpectUpgrade(input, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kNone, expected); |
| } |
| |
| void ExpectUpgrade(const char* input, |
| mojom::RequestContextType request_context, |
| mojom::RequestContextFrameType frame_type, |
| const char* expected) { |
| const KURL input_url(input); |
| const KURL expected_url(expected); |
| |
| ResourceRequest resource_request(input_url); |
| resource_request.SetRequestContext(request_context); |
| |
| ModifyRequestForCSP(resource_request, frame_type); |
| |
| EXPECT_EQ(expected_url.GetString(), resource_request.Url().GetString()); |
| EXPECT_EQ(expected_url.Protocol(), resource_request.Url().Protocol()); |
| EXPECT_EQ(expected_url.Host(), resource_request.Url().Host()); |
| EXPECT_EQ(expected_url.Port(), resource_request.Url().Port()); |
| EXPECT_EQ(expected_url.HasPort(), resource_request.Url().HasPort()); |
| EXPECT_EQ(expected_url.GetPath(), resource_request.Url().GetPath()); |
| } |
| |
| void ExpectUpgradeInsecureRequestHeader( |
| const char* input, |
| mojom::RequestContextFrameType frame_type, |
| bool should_prefer) { |
| const KURL input_url(input); |
| |
| ResourceRequest resource_request(input_url); |
| resource_request.SetRequestContext(mojom::RequestContextType::SCRIPT); |
| |
| ModifyRequestForCSP(resource_request, frame_type); |
| |
| EXPECT_EQ( |
| should_prefer ? String("1") : String(), |
| resource_request.HttpHeaderField(http_names::kUpgradeInsecureRequests)); |
| |
| // Calling modifyRequestForCSP more than once shouldn't affect the |
| // header. |
| if (should_prefer) { |
| GetFetchContext()->ModifyRequestForCSP(resource_request); |
| EXPECT_EQ("1", resource_request.HttpHeaderField( |
| http_names::kUpgradeInsecureRequests)); |
| } |
| } |
| |
| void ExpectIsAutomaticUpgradeSet(const char* input, |
| const char* main_frame, |
| WebInsecureRequestPolicy policy, |
| bool expected_value) { |
| const KURL input_url(input); |
| const KURL main_frame_url(main_frame); |
| ResourceRequest resource_request(input_url); |
| // TODO(crbug.com/1026464, carlosil): Default behavior currently is to not |
| // autoupgrade images, setting the context to AUDIO to ensure the upgrade |
| // flow runs, this can be switched back to IMAGE once autoupgrades launch |
| // for them. |
| resource_request.SetRequestContext(mojom::RequestContextType::AUDIO); |
| |
| RecreateFetchContext(main_frame_url); |
| document->GetSecurityContext().SetInsecureRequestPolicy(policy); |
| |
| ModifyRequestForCSP(resource_request, |
| mojom::RequestContextFrameType::kNone); |
| |
| EXPECT_EQ(expected_value, resource_request.IsAutomaticUpgrade()); |
| } |
| |
| void ExpectSetRequiredCSPRequestHeader( |
| const char* input, |
| mojom::RequestContextFrameType frame_type, |
| const AtomicString& expected_required_csp) { |
| const KURL input_url(input); |
| ResourceRequest resource_request(input_url); |
| resource_request.SetRequestContext(mojom::RequestContextType::SCRIPT); |
| |
| ModifyRequestForCSP(resource_request, frame_type); |
| |
| EXPECT_EQ(expected_required_csp, |
| resource_request.HttpHeaderField(http_names::kSecRequiredCSP)); |
| } |
| |
| void SetFrameOwnerBasedOnFrameType(mojom::RequestContextFrameType frame_type, |
| HTMLIFrameElement* iframe, |
| const AtomicString& potential_value) { |
| if (frame_type != mojom::RequestContextFrameType::kNested) { |
| document->GetFrame()->SetOwner(nullptr); |
| return; |
| } |
| |
| iframe->setAttribute(html_names::kCspAttr, potential_value); |
| document->GetFrame()->SetOwner(iframe); |
| } |
| |
| scoped_refptr<const SecurityOrigin> example_origin; |
| }; |
| |
| TEST_F(FrameFetchContextModifyRequestTest, UpgradeInsecureResourceRequests) { |
| struct TestCase { |
| const char* original; |
| const char* upgraded; |
| } tests[] = { |
| {"http://example.test/image.png", "https://example.test/image.png"}, |
| {"http://example.test:80/image.png", |
| "https://example.test:443/image.png"}, |
| {"http://example.test:1212/image.png", |
| "https://example.test:1212/image.png"}, |
| |
| {"https://example.test/image.png", "https://example.test/image.png"}, |
| {"https://example.test:80/image.png", |
| "https://example.test:80/image.png"}, |
| {"https://example.test:1212/image.png", |
| "https://example.test:1212/image.png"}, |
| |
| {"ftp://example.test/image.png", "ftp://example.test/image.png"}, |
| {"ftp://example.test:21/image.png", "ftp://example.test:21/image.png"}, |
| {"ftp://example.test:1212/image.png", |
| "ftp://example.test:1212/image.png"}, |
| }; |
| |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kUpgradeInsecureRequests); |
| |
| for (const auto& test : tests) { |
| document->GetSecurityContext().ClearInsecureNavigationsToUpgradeForTest(); |
| |
| // We always upgrade for FrameTypeNone. |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kNone, test.upgraded); |
| |
| // We never upgrade for FrameTypeNested. This is done on the browser |
| // process. |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kNested, test.original); |
| |
| // We do not upgrade for FrameTypeTopLevel or FrameTypeAuxiliary... |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kTopLevel, test.original); |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kAuxiliary, test.original); |
| |
| // unless the request context is RequestContextForm. |
| ExpectUpgrade(test.original, mojom::RequestContextType::FORM, |
| mojom::RequestContextFrameType::kTopLevel, test.upgraded); |
| ExpectUpgrade(test.original, mojom::RequestContextType::FORM, |
| mojom::RequestContextFrameType::kAuxiliary, test.upgraded); |
| |
| // Or unless the host of the resource is in the document's |
| // InsecureNavigationsSet: |
| document->GetSecurityContext().AddInsecureNavigationUpgrade( |
| example_origin->Host().Impl()->GetHash()); |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kTopLevel, test.upgraded); |
| ExpectUpgrade(test.original, mojom::RequestContextType::SCRIPT, |
| mojom::RequestContextFrameType::kAuxiliary, test.upgraded); |
| } |
| } |
| |
| TEST_F(FrameFetchContextModifyRequestTest, |
| DoNotUpgradeInsecureResourceRequests) { |
| base::test::ScopedFeatureList feature_list; |
| feature_list.InitAndDisableFeature(blink::features::kMixedContentAutoupgrade); |
| |
| RecreateFetchContext(KURL("https://secureorigin.test/image.png")); |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kLeaveInsecureRequestsAlone); |
| |
| ExpectUpgrade("http://example.test/image.png", |
| "http://example.test/image.png"); |
| ExpectUpgrade("http://example.test:80/image.png", |
| "http://example.test:80/image.png"); |
| ExpectUpgrade("http://example.test:1212/image.png", |
| "http://example.test:1212/image.png"); |
| |
| ExpectUpgrade("https://example.test/image.png", |
| "https://example.test/image.png"); |
| ExpectUpgrade("https://example.test:80/image.png", |
| "https://example.test:80/image.png"); |
| ExpectUpgrade("https://example.test:1212/image.png", |
| "https://example.test:1212/image.png"); |
| |
| ExpectUpgrade("ftp://example.test/image.png", "ftp://example.test/image.png"); |
| ExpectUpgrade("ftp://example.test:21/image.png", |
| "ftp://example.test:21/image.png"); |
| ExpectUpgrade("ftp://example.test:1212/image.png", |
| "ftp://example.test:1212/image.png"); |
| } |
| |
| TEST_F(FrameFetchContextModifyRequestTest, IsAutomaticUpgradeSet) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(features::kMixedContentAutoupgrade); |
| ExpectIsAutomaticUpgradeSet("http://example.test/image.png", |
| "https://example.test", |
| kLeaveInsecureRequestsAlone, true); |
| } |
| |
| TEST_F(FrameFetchContextModifyRequestTest, IsAutomaticUpgradeNotSet) { |
| base::test::ScopedFeatureList scoped_feature_list; |
| scoped_feature_list.InitAndEnableFeature(features::kMixedContentAutoupgrade); |
| // Upgrade shouldn't happen if the resource is already https. |
| ExpectIsAutomaticUpgradeSet("https://example.test/image.png", |
| "https://example.test", |
| kLeaveInsecureRequestsAlone, false); |
| // Upgrade shouldn't happen if the site is http. |
| ExpectIsAutomaticUpgradeSet("http://example.test/image.png", |
| "http://example.test", |
| kLeaveInsecureRequestsAlone, false); |
| |
| // Flag shouldn't be set if upgrade was due to upgrade-insecure-requests. |
| ExpectIsAutomaticUpgradeSet("http://example.test/image.png", |
| "https://example.test", kUpgradeInsecureRequests, |
| false); |
| } |
| |
| TEST_F(FrameFetchContextModifyRequestTest, SendUpgradeInsecureRequestHeader) { |
| struct TestCase { |
| const char* to_request; |
| mojom::RequestContextFrameType frame_type; |
| bool should_prefer; |
| } tests[] = {{"http://example.test/page.html", |
| mojom::RequestContextFrameType::kAuxiliary, true}, |
| {"http://example.test/page.html", |
| mojom::RequestContextFrameType::kNested, true}, |
| {"http://example.test/page.html", |
| mojom::RequestContextFrameType::kNone, false}, |
| {"http://example.test/page.html", |
| mojom::RequestContextFrameType::kTopLevel, true}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kAuxiliary, true}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kNested, true}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kNone, false}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kTopLevel, true}}; |
| |
| // This should work correctly both when the FrameFetchContext has a Document, |
| // and when it doesn't (e.g. during main frame navigations), so run through |
| // the tests both before and after providing a document to the context. |
| for (const auto& test : tests) { |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kLeaveInsecureRequestsAlone); |
| ExpectUpgradeInsecureRequestHeader(test.to_request, test.frame_type, |
| test.should_prefer); |
| |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kUpgradeInsecureRequests); |
| ExpectUpgradeInsecureRequestHeader(test.to_request, test.frame_type, |
| test.should_prefer); |
| } |
| |
| for (const auto& test : tests) { |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kLeaveInsecureRequestsAlone); |
| ExpectUpgradeInsecureRequestHeader(test.to_request, test.frame_type, |
| test.should_prefer); |
| |
| document->GetSecurityContext().SetInsecureRequestPolicy( |
| kUpgradeInsecureRequests); |
| ExpectUpgradeInsecureRequestHeader(test.to_request, test.frame_type, |
| test.should_prefer); |
| } |
| } |
| |
| TEST_F(FrameFetchContextModifyRequestTest, SendRequiredCSPHeader) { |
| struct TestCase { |
| const char* to_request; |
| mojom::RequestContextFrameType frame_type; |
| } tests[] = { |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kAuxiliary}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kNested}, |
| {"https://example.test/page.html", mojom::RequestContextFrameType::kNone}, |
| {"https://example.test/page.html", |
| mojom::RequestContextFrameType::kTopLevel}}; |
| |
| auto* iframe = MakeGarbageCollected<HTMLIFrameElement>(*document); |
| const AtomicString& required_csp = AtomicString("default-src 'none'"); |
| const AtomicString& another_required_csp = AtomicString("default-src 'self'"); |
| |
| for (const auto& test : tests) { |
| SetFrameOwnerBasedOnFrameType(test.frame_type, iframe, required_csp); |
| ExpectSetRequiredCSPRequestHeader( |
| test.to_request, test.frame_type, |
| test.frame_type == mojom::RequestContextFrameType::kNested |
| ? required_csp |
| : g_null_atom); |
| |
| SetFrameOwnerBasedOnFrameType(test.frame_type, iframe, |
| another_required_csp); |
| ExpectSetRequiredCSPRequestHeader( |
| test.to_request, test.frame_type, |
| test.frame_type == mojom::RequestContextFrameType::kNested |
| ? another_required_csp |
| : g_null_atom); |
| } |
| } |
| |
| class FrameFetchContextHintsTest : public FrameFetchContextTest { |
| public: |
| FrameFetchContextHintsTest() = default; |
| |
| void SetUp() override { |
| // Set the document URL to a secure document. |
| RecreateFetchContext(KURL("https://www.example.com/")); |
| Settings* settings = document->GetSettings(); |
| settings->SetScriptEnabled(true); |
| } |
| |
| protected: |
| void ExpectHeader(const char* input, |
| const char* header_name, |
| bool is_present, |
| const char* header_value, |
| float width = 0) { |
| SCOPED_TRACE(testing::Message() << header_name); |
| ClientHintsPreferences hints_preferences; |
| |
| FetchParameters::ResourceWidth resource_width; |
| if (width > 0) { |
| resource_width.width = width; |
| resource_width.is_set = true; |
| } |
| |
| const KURL input_url(input); |
| ResourceRequest resource_request(input_url); |
| |
| GetFetchContext()->AddClientHintsIfNecessary( |
| hints_preferences, resource_width, resource_request); |
| |
| EXPECT_EQ(is_present ? String(header_value) : String(), |
| resource_request.HttpHeaderField(header_name)); |
| } |
| |
| String GetHeaderValue(const char* input, const char* header_name) { |
| ClientHintsPreferences hints_preferences; |
| FetchParameters::ResourceWidth resource_width; |
| const KURL input_url(input); |
| ResourceRequest resource_request(input_url); |
| GetFetchContext()->AddClientHintsIfNecessary( |
| hints_preferences, resource_width, resource_request); |
| return resource_request.HttpHeaderField(header_name); |
| } |
| }; |
| |
| // Verify that the client hints should be attached for subresources fetched |
| // over secure transport. Tests when the persistent client hint feature is |
| // enabled. |
| TEST_F(FrameFetchContextHintsTest, MonitorDeviceMemorySecureTransport) { |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "4"); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", false, ""); |
| // Without a feature policy header, the client hints should be sent only to |
| // the first party origins. |
| // Device-memory is a legacy hint that's sent on Android regardless of Feature |
| // Policy delegation. |
| #if defined(OS_ANDROID) |
| ExpectHeader("https://www.someother-example.com/1.gif", "Device-Memory", true, |
| "4"); |
| #else |
| ExpectHeader("https://www.someother-example.com/1.gif", "Device-Memory", |
| false, ""); |
| #endif |
| } |
| |
| // Verify that client hints are not attached when the resources do not belong to |
| // a secure context. |
| TEST_F(FrameFetchContextHintsTest, MonitorDeviceMemoryHintsInsecureContext) { |
| // Verify that client hints are not attached when the resources do not belong |
| // to a secure context and the persistent client hint features is enabled. |
| ExpectHeader("http://www.example.com/1.gif", "Device-Memory", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| ExpectHeader("http://www.example.com/1.gif", "Device-Memory", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "DPR", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| // Verify that client hints are attched when the resources belong to a local |
| // context. |
| TEST_F(FrameFetchContextHintsTest, MonitorDeviceMemoryHintsLocalContext) { |
| RecreateFetchContext(KURL("http://localhost/")); |
| document->GetSettings()->SetScriptEnabled(true); |
| ExpectHeader("http://localhost/1.gif", "Device-Memory", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| ExpectHeader("http://localhost/1.gif", "Device-Memory", true, "4"); |
| ExpectHeader("http://localhost/1.gif", "DPR", false, ""); |
| ExpectHeader("http://localhost/1.gif", "Width", false, ""); |
| ExpectHeader("http://localhost/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorDeviceMemoryHints) { |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "4"); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(2048); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "2"); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(64385); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "8"); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(768); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "0.5"); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorDPRHints) { |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDpr); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", true, "1"); |
| dummy_page_holder->GetPage().SetDeviceScaleFactorDeprecated(2.5); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", true, "2.5"); |
| ExpectHeader("https://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorDPRHintsInsecureTransport) { |
| ExpectHeader("http://www.example.com/1.gif", "DPR", false, ""); |
| dummy_page_holder->GetPage().SetDeviceScaleFactorDeprecated(2.5); |
| ExpectHeader("http://www.example.com/1.gif", "DPR", false, " "); |
| ExpectHeader("http://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorResourceWidthHints) { |
| ExpectHeader("https://www.example.com/1.gif", "Width", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kResourceWidth); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ExpectHeader("https://www.example.com/1.gif", "Width", true, "500", 500); |
| ExpectHeader("https://www.example.com/1.gif", "Width", true, "667", 666.6666); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| dummy_page_holder->GetPage().SetDeviceScaleFactorDeprecated(2.5); |
| ExpectHeader("https://www.example.com/1.gif", "Width", true, "1250", 500); |
| ExpectHeader("https://www.example.com/1.gif", "Width", true, "1667", |
| 666.6666); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorViewportWidthHints) { |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kViewportWidth); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", true, "500"); |
| dummy_page_holder->GetFrameView().SetLayoutSizeFixedToFrameSize(false); |
| dummy_page_holder->GetFrameView().SetLayoutSize(IntSize(800, 800)); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", true, "800"); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", true, "800", |
| 666.6666); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorLangHint) { |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kLang); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| |
| document->domWindow()->navigator()->SetLanguagesForTesting("en-US"); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", true, |
| "\"en-US\""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| |
| document->domWindow()->navigator()->SetLanguagesForTesting("en,de,fr"); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", true, |
| "\"en\", \"de\", \"fr\""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| |
| document->domWindow()->navigator()->SetLanguagesForTesting( |
| "en-US,fr_FR,de-DE,es"); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", true, |
| "\"en-US\", \"fr-FR\", \"de-DE\", \"es\""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorUAHints) { |
| // `Sec-CH-UA` is always sent for secure requests |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA", true, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA", false, ""); |
| |
| // `Sec-CH-UA-*` requires opt-in. |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Platform", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| |
| { |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAArch); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", true, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| } |
| |
| { |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAPlatform); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", true, |
| ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| } |
| |
| { |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAModel); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", true, ""); |
| |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("http://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| } |
| } |
| |
| TEST_F(FrameFetchContextHintsTest, MonitorAllHints) { |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Width", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "rtt", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "downlink", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "ect", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", false, ""); |
| |
| // `Sec-CH-UA` is special. |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA", true, ""); |
| |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDpr); |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kResourceWidth); |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kViewportWidth); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kRtt); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDownlink); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kEct); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kLang); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUA); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAArch); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAPlatform); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAModel); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", true, "4"); |
| ExpectHeader("https://www.example.com/1.gif", "DPR", true, "1"); |
| ExpectHeader("https://www.example.com/1.gif", "Width", true, "400", 400); |
| ExpectHeader("https://www.example.com/1.gif", "Viewport-Width", true, "500"); |
| |
| document->domWindow()->navigator()->SetLanguagesForTesting("en,de,fr"); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-Lang", true, |
| "\"en\", \"de\", \"fr\""); |
| |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA", true, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Arch", true, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Platform", true, ""); |
| ExpectHeader("https://www.example.com/1.gif", "Sec-CH-UA-Model", true, ""); |
| |
| // Value of network quality client hints may vary, so only check if the |
| // header is present and the values are non-negative/non-empty. |
| bool conversion_ok = false; |
| int rtt_header_value = GetHeaderValue("https://www.example.com/1.gif", "rtt") |
| .ToIntStrict(&conversion_ok); |
| EXPECT_TRUE(conversion_ok); |
| EXPECT_LE(0, rtt_header_value); |
| |
| float downlink_header_value = |
| GetHeaderValue("https://www.example.com/1.gif", "downlink") |
| .ToFloat(&conversion_ok); |
| EXPECT_TRUE(conversion_ok); |
| EXPECT_LE(0, downlink_header_value); |
| |
| EXPECT_LT( |
| 0u, |
| GetHeaderValue("https://www.example.com/1.gif", "ect").Ascii().length()); |
| } |
| |
| // Verify that the client hints should be attached for third-party subresources |
| // fetched over secure transport, when specifically allowed by feature policy. |
| TEST_F(FrameFetchContextHintsTest, MonitorAllHintsFeaturePolicy) { |
| RecreateFetchContext( |
| KURL("https://www.example.com/"), |
| "ch-dpr *; ch-device-memory *; ch-downlink *; ch-ect *; ch-lang *;" |
| "ch-rtt *; ch-ua *; ch-ua-arch *; ch-ua-platform *; ch-ua-model *;" |
| "ch-viewport-width *; ch-width *"); |
| document->GetSettings()->SetScriptEnabled(true); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDpr); |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kResourceWidth); |
| preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kViewportWidth); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kRtt); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDownlink); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kEct); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kLang); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUA); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAArch); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAPlatform); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kUAModel); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| |
| // Verify that all client hints are sent to a third-party origin, with this |
| // feature policy header. |
| ExpectHeader("https://www.example.net/1.gif", "DPR", true, "1"); |
| ExpectHeader("https://www.example.net/1.gif", "Device-Memory", true, "4"); |
| |
| document->domWindow()->navigator()->SetLanguagesForTesting("en,de,fr"); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-Lang", true, |
| "\"en\", \"de\", \"fr\""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA", true, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Arch", true, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Platform", true, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Model", true, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Width", true, "400", 400); |
| ExpectHeader("https://www.example.net/1.gif", "Viewport-Width", true, "500"); |
| |
| // Value of network quality client hints may vary, so only check if the |
| // header is present and the values are non-negative/non-empty. |
| bool conversion_ok = false; |
| int rtt_header_value = GetHeaderValue("https://www.example.com/1.gif", "rtt") |
| .ToIntStrict(&conversion_ok); |
| EXPECT_TRUE(conversion_ok); |
| EXPECT_LE(0, rtt_header_value); |
| |
| float downlink_header_value = |
| GetHeaderValue("https://www.example.com/1.gif", "downlink") |
| .ToFloat(&conversion_ok); |
| EXPECT_TRUE(conversion_ok); |
| EXPECT_LE(0, downlink_header_value); |
| |
| EXPECT_LT( |
| 0u, |
| GetHeaderValue("https://www.example.com/1.gif", "ect").Ascii().length()); |
| } |
| |
| // Verify that only the specifically allowed client hints are attached for |
| // third-party subresources fetched over secure transport. |
| TEST_F(FrameFetchContextHintsTest, MonitorSomeHintsFeaturePolicy) { |
| RecreateFetchContext(KURL("https://www.example.com/"), |
| "ch-device-memory 'self' https://www.example.net"); |
| document->GetSettings()->SetScriptEnabled(true); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDpr); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| // With a feature policy header, the client hints should be sent to the |
| // declared third party origins. |
| ExpectHeader("https://www.example.net/1.gif", "Device-Memory", true, "4"); |
| // Device-memory is a legacy hint that's sent on Android regardless of Feature |
| // Policy delegation. |
| #if defined(OS_ANDROID) |
| ExpectHeader("https://www.someother-example.com/1.gif", "Device-Memory", true, |
| "4"); |
| #else |
| ExpectHeader("https://www.someother-example.com/1.gif", "Device-Memory", |
| false, ""); |
| #endif |
| // `Sec-CH-UA` is special. |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA", true, ""); |
| |
| // Other hints not declared in the policy are still not attached. |
| ExpectHeader("https://www.example.net/1.gif", "downlink", false, ""); |
| ExpectHeader("https://www.example.net/1.gif", "ect", false, ""); |
| // DPR is a legacy hint that's sent on Android regardless of Feature Policy |
| // delegation. |
| #if defined(OS_ANDROID) |
| ExpectHeader("https://www.example.net/1.gif", "DPR", true, "1"); |
| #else |
| ExpectHeader("https://www.example.net/1.gif", "DPR", false, ""); |
| #endif |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-Lang", false, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Arch", false, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Platform", false, |
| ""); |
| ExpectHeader("https://www.example.net/1.gif", "Sec-CH-UA-Model", false, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Width", false, ""); |
| ExpectHeader("https://www.example.net/1.gif", "Viewport-Width", false, ""); |
| } |
| |
| // Verify that the client hints are not attached for third-party subresources |
| // fetched over insecure transport, even when specifically allowed by feature |
| // policy. |
| TEST_F(FrameFetchContextHintsTest, MonitorHintsFeaturePolicyInsecureContext) { |
| RecreateFetchContext(KURL("https://www.example.com/"), "ch-device-memory *"); |
| document->GetSettings()->SetScriptEnabled(true); |
| ExpectHeader("https://www.example.com/1.gif", "Device-Memory", false, ""); |
| ClientHintsPreferences preferences; |
| preferences.SetShouldSendForTesting(mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().UpdateFrom(preferences); |
| ApproximatedDeviceMemory::SetPhysicalMemoryMBForTesting(4096); |
| // Device-Memory hint in this case is sent to all (and only) secure origins. |
| ExpectHeader("https://www.example.net/1.gif", "Device-Memory", true, "4"); |
| ExpectHeader("http://www.example.net/1.gif", "Device-Memory", false, ""); |
| } |
| |
| TEST_F(FrameFetchContextTest, SubResourceCachePolicy) { |
| // Reset load event state: if the load event is finished, we ignore the |
| // DocumentLoader load type. |
| document->open(); |
| ASSERT_FALSE(document->LoadEventFinished()); |
| |
| // Default case |
| ResourceRequest request("http://www.example.com/mock"); |
| EXPECT_EQ(mojom::FetchCacheMode::kDefault, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| request, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // WebFrameLoadType::kReload should not affect sub-resources |
| document->Loader()->SetLoadType(WebFrameLoadType::kReload); |
| EXPECT_EQ(mojom::FetchCacheMode::kDefault, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| request, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // Conditional request |
| document->Loader()->SetLoadType(WebFrameLoadType::kStandard); |
| ResourceRequest conditional("http://www.example.com/mock"); |
| conditional.SetHttpHeaderField(http_names::kIfModifiedSince, "foo"); |
| EXPECT_EQ(mojom::FetchCacheMode::kValidateCache, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| conditional, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // WebFrameLoadType::kReloadBypassingCache |
| document->Loader()->SetLoadType(WebFrameLoadType::kReloadBypassingCache); |
| EXPECT_EQ(mojom::FetchCacheMode::kBypassCache, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| request, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // WebFrameLoadType::kReloadBypassingCache with a conditional request |
| document->Loader()->SetLoadType(WebFrameLoadType::kReloadBypassingCache); |
| EXPECT_EQ(mojom::FetchCacheMode::kBypassCache, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| conditional, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // Back/forward navigation |
| document->Loader()->SetLoadType(WebFrameLoadType::kBackForward); |
| EXPECT_EQ(mojom::FetchCacheMode::kForceCache, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| request, ResourceType::kMock, FetchParameters::kNoDefer)); |
| |
| // Back/forward navigation with a conditional request |
| document->Loader()->SetLoadType(WebFrameLoadType::kBackForward); |
| EXPECT_EQ(mojom::FetchCacheMode::kForceCache, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| conditional, ResourceType::kMock, FetchParameters::kNoDefer)); |
| } |
| |
| // Tests if "Save-Data" header is correctly added on the first load and reload. |
| TEST_F(FrameFetchContextTest, EnableDataSaver) { |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(true); |
| // Recreate the fetch context so that the updated save data settings are read. |
| RecreateFetchContext(); |
| |
| ResourceRequest resource_request("http://www.example.com"); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ("on", resource_request.HttpHeaderField("Save-Data")); |
| |
| // Subsequent call to addAdditionalRequestHeaders should not append to the |
| // save-data header. |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ("on", resource_request.HttpHeaderField("Save-Data")); |
| } |
| |
| // Tests if "Save-Data" header is not added when the data saver is disabled. |
| TEST_F(FrameFetchContextTest, DisabledDataSaver) { |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(false); |
| // Recreate the fetch context so that the updated save data settings are read. |
| RecreateFetchContext(); |
| |
| ResourceRequest resource_request("http://www.example.com"); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ(String(), resource_request.HttpHeaderField("Save-Data")); |
| } |
| |
| // Tests if reload variants can reflect the current data saver setting. |
| TEST_F(FrameFetchContextTest, ChangeDataSaverConfig) { |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(true); |
| // Recreate the fetch context so that the updated save data settings are read. |
| RecreateFetchContext(); |
| ResourceRequest resource_request("http://www.example.com"); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ("on", resource_request.HttpHeaderField("Save-Data")); |
| |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(false); |
| RecreateFetchContext(); |
| document->Loader()->SetLoadType(WebFrameLoadType::kReload); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ(String(), resource_request.HttpHeaderField("Save-Data")); |
| |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(true); |
| RecreateFetchContext(); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ("on", resource_request.HttpHeaderField("Save-Data")); |
| |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(false); |
| RecreateFetchContext(); |
| document->Loader()->SetLoadType(WebFrameLoadType::kReload); |
| GetFetchContext()->AddAdditionalRequestHeaders(resource_request); |
| EXPECT_EQ(String(), resource_request.HttpHeaderField("Save-Data")); |
| } |
| |
| TEST_F(FrameFetchContextSubresourceFilterTest, Filter) { |
| SetFilterPolicy(WebDocumentSubresourceFilter::kDisallow); |
| |
| EXPECT_EQ(ResourceRequestBlockedReason::kSubresourceFilter, |
| CanRequestAndVerifyIsAd(true)); |
| EXPECT_EQ(1, GetFilteredLoadCallCount()); |
| |
| EXPECT_EQ(ResourceRequestBlockedReason::kSubresourceFilter, |
| CanRequestAndVerifyIsAd(true)); |
| EXPECT_EQ(2, GetFilteredLoadCallCount()); |
| |
| EXPECT_EQ(ResourceRequestBlockedReason::kSubresourceFilter, |
| CanRequestPreload()); |
| EXPECT_EQ(2, GetFilteredLoadCallCount()); |
| |
| EXPECT_EQ(ResourceRequestBlockedReason::kSubresourceFilter, |
| CanRequestAndVerifyIsAd(true)); |
| EXPECT_EQ(3, GetFilteredLoadCallCount()); |
| } |
| |
| TEST_F(FrameFetchContextSubresourceFilterTest, Allow) { |
| SetFilterPolicy(WebDocumentSubresourceFilter::kAllow); |
| |
| EXPECT_EQ(base::nullopt, CanRequestAndVerifyIsAd(false)); |
| EXPECT_EQ(0, GetFilteredLoadCallCount()); |
| |
| EXPECT_EQ(base::nullopt, CanRequestPreload()); |
| EXPECT_EQ(0, GetFilteredLoadCallCount()); |
| } |
| |
| TEST_F(FrameFetchContextSubresourceFilterTest, DuringOnFreeze) { |
| document->SetFreezingInProgress(true); |
| // Only keepalive requests should succeed during onfreeze. |
| EXPECT_EQ(ResourceRequestBlockedReason::kOther, CanRequest()); |
| EXPECT_EQ(base::nullopt, CanRequestKeepAlive()); |
| document->SetFreezingInProgress(false); |
| EXPECT_EQ(base::nullopt, CanRequest()); |
| EXPECT_EQ(base::nullopt, CanRequestKeepAlive()); |
| } |
| |
| TEST_F(FrameFetchContextSubresourceFilterTest, WouldDisallow) { |
| SetFilterPolicy(WebDocumentSubresourceFilter::kWouldDisallow); |
| |
| EXPECT_EQ(base::nullopt, CanRequestAndVerifyIsAd(true)); |
| EXPECT_EQ(0, GetFilteredLoadCallCount()); |
| |
| EXPECT_EQ(base::nullopt, CanRequestPreload()); |
| EXPECT_EQ(0, GetFilteredLoadCallCount()); |
| } |
| |
| TEST_F(FrameFetchContextTest, AddAdditionalRequestHeadersWhenDetached) { |
| const KURL document_url("https://www2.example.com/fuga/hoge.html"); |
| const String origin = "https://www2.example.com"; |
| ResourceRequest request(KURL("https://localhost/")); |
| request.SetHttpMethod("PUT"); |
| |
| GetNetworkStateNotifier().SetSaveDataEnabledOverride(true); |
| |
| dummy_page_holder = nullptr; |
| |
| GetFetchContext()->AddAdditionalRequestHeaders(request); |
| |
| EXPECT_EQ(String(), request.HttpHeaderField("Save-Data")); |
| } |
| |
| TEST_F(FrameFetchContextTest, ResourceRequestCachePolicyWhenDetached) { |
| ResourceRequest request(KURL("https://localhost/")); |
| |
| dummy_page_holder = nullptr; |
| |
| EXPECT_EQ(mojom::FetchCacheMode::kDefault, |
| GetFetchContext()->ResourceRequestCachePolicy( |
| request, ResourceType::kRaw, FetchParameters::kNoDefer)); |
| } |
| |
| TEST_F(FrameFetchContextMockedLocalFrameClientTest, |
| PrepareRequestWhenDetached) { |
| Checkpoint checkpoint; |
| |
| EXPECT_CALL(checkpoint, Call(1)); |
| EXPECT_CALL(*client, UserAgent()).WillOnce(testing::Return(String("hi"))); |
| EXPECT_CALL(checkpoint, Call(2)); |
| |
| checkpoint.Call(1); |
| dummy_page_holder = nullptr; |
| checkpoint.Call(2); |
| |
| ResourceRequest request(KURL("https://localhost/")); |
| WebScopedVirtualTimePauser virtual_time_pauser; |
| GetFetchContext()->PrepareRequest(request, FetchInitiatorInfo(), |
| virtual_time_pauser, ResourceType::kRaw); |
| |
| EXPECT_EQ("hi", request.HttpHeaderField(http_names::kUserAgent)); |
| } |
| |
| TEST_F(FrameFetchContextTest, AddResourceTimingWhenDetached) { |
| scoped_refptr<ResourceTimingInfo> info = ResourceTimingInfo::Create( |
| "type", base::TimeTicks() + base::TimeDelta::FromSecondsD(0.3), |
| mojom::RequestContextType::UNSPECIFIED); |
| |
| dummy_page_holder = nullptr; |
| |
| GetFetchContext()->AddResourceTiming(*info); |
| // Should not crash. |
| } |
| |
| TEST_F(FrameFetchContextTest, AllowImageWhenDetached) { |
| const KURL url("https://www.example.com/"); |
| |
| dummy_page_holder = nullptr; |
| |
| EXPECT_TRUE(GetFetchContext()->AllowImage(true, url)); |
| EXPECT_TRUE(GetFetchContext()->AllowImage(false, url)); |
| } |
| |
| TEST_F(FrameFetchContextTest, PopulateResourceRequestWhenDetached) { |
| const KURL url("https://www.example.com/"); |
| ResourceRequest request(url); |
| |
| ClientHintsPreferences client_hints_preferences; |
| client_hints_preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kDeviceMemory); |
| client_hints_preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kDpr); |
| client_hints_preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kResourceWidth); |
| client_hints_preferences.SetShouldSendForTesting( |
| mojom::WebClientHintsType::kViewportWidth); |
| |
| FetchParameters::ResourceWidth resource_width; |
| ResourceLoaderOptions options; |
| |
| document->GetFrame()->GetClientHintsPreferences().SetShouldSendForTesting( |
| mojom::WebClientHintsType::kDeviceMemory); |
| document->GetFrame()->GetClientHintsPreferences().SetShouldSendForTesting( |
| mojom::WebClientHintsType::kDpr); |
| document->GetFrame()->GetClientHintsPreferences().SetShouldSendForTesting( |
| mojom::WebClientHintsType::kResourceWidth); |
| document->GetFrame()->GetClientHintsPreferences().SetShouldSendForTesting( |
| mojom::WebClientHintsType::kViewportWidth); |
| |
| dummy_page_holder = nullptr; |
| |
| GetFetchContext()->PopulateResourceRequest( |
| ResourceType::kRaw, client_hints_preferences, resource_width, request); |
| // Should not crash. |
| } |
| |
| TEST_F(FrameFetchContextTest, SetFirstPartyCookieWhenDetached) { |
| const KURL document_url("https://www2.example.com/foo/bar"); |
| RecreateFetchContext(document_url); |
| |
| const KURL url("https://www.example.com/hoge/fuga"); |
| ResourceRequest request(url); |
| |
| dummy_page_holder = nullptr; |
| |
| SetFirstPartyCookie(request); |
| |
| EXPECT_TRUE(request.SiteForCookies().IsEquivalent( |
| net::SiteForCookies::FromUrl(document_url))); |
| } |
| |
| TEST_F(FrameFetchContextTest, TopFrameOrigin) { |
| const KURL document_url("https://www2.example.com/foo/bar"); |
| RecreateFetchContext(document_url); |
| const SecurityOrigin* origin = document->GetSecurityOrigin(); |
| |
| const KURL url("https://www.example.com/hoge/fuga"); |
| ResourceRequest request(url); |
| |
| EXPECT_EQ(origin, GetTopFrameOrigin()); |
| } |
| |
| TEST_F(FrameFetchContextTest, TopFrameOriginDetached) { |
| const KURL document_url("https://www2.example.com/foo/bar"); |
| RecreateFetchContext(document_url); |
| const SecurityOrigin* origin = document->GetSecurityOrigin(); |
| |
| const KURL url("https://www.example.com/hoge/fuga"); |
| ResourceRequest request(url); |
| |
| dummy_page_holder = nullptr; |
| |
| EXPECT_EQ(origin, GetTopFrameOrigin()); |
| } |
| |
| } // namespace blink |