| /* |
| * Copyright (C) 2006, 2007, 2010, 2011 Apple Inc. All rights reserved. |
| * (C) 2007 Graham Dennis (graham.dennis@gmail.com) |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. 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. |
| * 3. Neither the name of Apple Computer, Inc. ("Apple") 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 APPLE AND ITS 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 APPLE OR ITS 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/platform/loader/fetch/resource_loader.h" |
| |
| #include "services/network/public/mojom/fetch_api.mojom-blink.h" |
| #include "third_party/blink/public/mojom/blob/blob_registry.mojom-blink.h" |
| #include "third_party/blink/public/platform/code_cache_loader.h" |
| #include "third_party/blink/public/platform/platform.h" |
| #include "third_party/blink/public/platform/web_cors.h" |
| #include "third_party/blink/public/platform/web_data.h" |
| #include "third_party/blink/public/platform/web_security_origin.h" |
| #include "third_party/blink/public/platform/web_url_error.h" |
| #include "third_party/blink/public/platform/web_url_request.h" |
| #include "third_party/blink/public/platform/web_url_response.h" |
| #include "third_party/blink/renderer/platform/exported/wrapped_resource_request.h" |
| #include "third_party/blink/renderer/platform/exported/wrapped_resource_response.h" |
| #include "third_party/blink/renderer/platform/loader/cors/cors.h" |
| #include "third_party/blink/renderer/platform/loader/cors/cors_error_string.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/fetch_context.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_error.h" |
| #include "third_party/blink/renderer/platform/loader/fetch/resource_fetcher.h" |
| #include "third_party/blink/renderer/platform/network/http_names.h" |
| #include "third_party/blink/renderer/platform/network/http_parsers.h" |
| #include "third_party/blink/renderer/platform/network/mime/mime_type_registry.h" |
| #include "third_party/blink/renderer/platform/network/network_instrumentation.h" |
| #include "third_party/blink/renderer/platform/runtime_enabled_features.h" |
| #include "third_party/blink/renderer/platform/scheduler/public/thread_scheduler.h" |
| #include "third_party/blink/renderer/platform/shared_buffer.h" |
| #include "third_party/blink/renderer/platform/weborigin/scheme_registry.h" |
| #include "third_party/blink/renderer/platform/weborigin/security_violation_reporting_policy.h" |
| #include "third_party/blink/renderer/platform/wtf/assertions.h" |
| #include "third_party/blink/renderer/platform/wtf/text/string_builder.h" |
| #include "third_party/blink/renderer/platform/wtf/time.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| bool IsThrottlableRequestContext(WebURLRequest::RequestContext context) { |
| // Requests that could run long should not be throttled as they |
| // may stay there forever and avoid other requests from making |
| // progress. |
| // See https://crbug.com/837771 for the sample breakages. |
| return context != WebURLRequest::kRequestContextEventSource && |
| context != WebURLRequest::kRequestContextFetch && |
| context != WebURLRequest::kRequestContextXMLHttpRequest && |
| context != WebURLRequest::kRequestContextVideo && |
| context != WebURLRequest::kRequestContextAudio; |
| } |
| |
| } // namespace |
| |
| // CodeCacheRequest handles the requests to fetch data from code cache. |
| // This owns CodeCacheLoader that actually loads the data from the |
| // code cache. This class performs the necessary checks of matching the |
| // resource response time and the code cache response time before sending |
| // the data to the resource. It caches the data returned from the code cache |
| // if the response wasn't received. One CodeCacheRequest handles only one |
| // request. On a restart new CodeCacheRequest is created. |
| class ResourceLoader::CodeCacheRequest { |
| public: |
| CodeCacheRequest(std::unique_ptr<CodeCacheLoader> code_cache_loader, |
| const KURL& url, |
| bool defers_loading) |
| : status_(kNoRequestSent), |
| code_cache_loader_(std::move(code_cache_loader)), |
| gurl_(url), |
| defers_loading_(defers_loading), |
| weak_ptr_factory_(this) { |
| DCHECK(RuntimeEnabledFeatures::IsolatedCodeCacheEnabled()); |
| } |
| |
| ~CodeCacheRequest() = default; |
| |
| // Request data from code cache. |
| bool FetchFromCodeCache(WebURLLoader* url_loader, |
| ResourceLoader* resource_loader); |
| bool FetchFromCodeCacheSynchronously(ResourceLoader* resource_loader); |
| |
| // Notifies about the response from webURLLoader. Stores the |
| // resource_response_time that is used to validate responses from |
| // code cache. Might send cached code if available. |
| void DidReceiveResponse(const base::Time& resource_response_time, |
| ResourceLoader* resource_loader); |
| |
| // Stores the value of defers that is needed to restore the state |
| // once fetching from code cache is finished. Returns true if the |
| // request is handled here and hence need not be handled by the loader. |
| // Returns false otherwise. |
| bool SetDefersLoading(bool defers); |
| |
| private: |
| enum CodeCacheRequestStatus { |
| kNoRequestSent, |
| kPendingResponse, |
| kReceivedResponse |
| }; |
| |
| // Callback to receive data from CodeCacheLoader. |
| void DidReceiveCachedCode(ResourceLoader* loader, |
| const base::Time& response_time, |
| const std::vector<uint8_t>& data); |
| |
| // Process the response from code cache. |
| void ProcessCodeCacheResponse(const base::Time& response_time, |
| const std::vector<uint8_t>& data, |
| ResourceLoader* resource_loader); |
| |
| // Send |cache_code| if we got a response from code_cache_loader and the |
| // web_url_loader. |
| void MaybeSendCachedCode(const std::vector<uint8_t>& cached_code, |
| ResourceLoader* resource_loader); |
| |
| CodeCacheRequestStatus status_; |
| std::unique_ptr<CodeCacheLoader> code_cache_loader_; |
| const GURL gurl_; |
| bool defers_loading_ = false; |
| std::vector<uint8_t> cached_code_; |
| base::Time cached_code_response_time_; |
| base::Time resource_response_time_; |
| base::WeakPtrFactory<CodeCacheRequest> weak_ptr_factory_; |
| }; |
| |
| bool ResourceLoader::CodeCacheRequest::FetchFromCodeCache( |
| WebURLLoader* url_loader, |
| ResourceLoader* resource_loader) { |
| if (!code_cache_loader_) |
| return false; |
| DCHECK_EQ(status_, kNoRequestSent); |
| status_ = kPendingResponse; |
| |
| // Set defers loading before fetching data from code cache. This is to |
| // ensure that the resource receives cached code before the response data. |
| // This directly calls the WebURLLoader's SetDefersLoading without going |
| // through ResourceLoader. |
| url_loader->SetDefersLoading(true); |
| |
| CodeCacheLoader::FetchCodeCacheCallback callback = |
| base::BindOnce(&ResourceLoader::CodeCacheRequest::DidReceiveCachedCode, |
| weak_ptr_factory_.GetWeakPtr(), resource_loader); |
| code_cache_loader_->FetchFromCodeCache( |
| blink::mojom::CodeCacheType::kJavascript, gurl_, std::move(callback)); |
| return true; |
| } |
| |
| bool ResourceLoader::CodeCacheRequest::FetchFromCodeCacheSynchronously( |
| ResourceLoader* resource_loader) { |
| if (!code_cache_loader_) |
| return false; |
| DCHECK_EQ(status_, kNoRequestSent); |
| status_ = kPendingResponse; |
| |
| base::Time response_time; |
| std::vector<uint8_t> data; |
| code_cache_loader_->FetchFromCodeCacheSynchronously(gurl_, &response_time, |
| &data); |
| ProcessCodeCacheResponse(response_time, data, resource_loader); |
| return true; |
| } |
| |
| // This is called when a response is received from the WebURLLoader. We buffer |
| // the response_time if the response from code cache is not available yet. |
| void ResourceLoader::CodeCacheRequest::DidReceiveResponse( |
| const base::Time& resource_response_time, |
| ResourceLoader* resource_loader) { |
| resource_response_time_ = resource_response_time; |
| MaybeSendCachedCode(cached_code_, resource_loader); |
| } |
| |
| // Returns true if |this| handles |defers| and therefore the callsite, i.e. the |
| // loader, doesn't need to take care of it). Returns false otherwise. |
| bool ResourceLoader::CodeCacheRequest::SetDefersLoading(bool defers) { |
| defers_loading_ = defers; |
| if (status_ == kPendingResponse) { |
| // The flag doesn't need to be handled by the loader. The value is stored |
| // in |defers_loading_| and set once the response from the code cache is |
| // received. |
| return true; |
| } |
| return false; |
| } |
| |
| void ResourceLoader::CodeCacheRequest::DidReceiveCachedCode( |
| ResourceLoader* resource_loader, |
| const base::Time& response_time, |
| const std::vector<uint8_t>& data) { |
| ProcessCodeCacheResponse(response_time, data, resource_loader); |
| // Reset the deferred value to its original state. |
| DCHECK(resource_loader); |
| resource_loader->SetDefersLoading(defers_loading_); |
| } |
| |
| // This is called when a response is received from code cache. If the |
| // resource response time is not available the response is buffered and |
| // will be processed when the response is received from the URLLoader. |
| void ResourceLoader::CodeCacheRequest::ProcessCodeCacheResponse( |
| const base::Time& response_time, |
| const std::vector<uint8_t>& data, |
| ResourceLoader* resource_loader) { |
| status_ = kReceivedResponse; |
| cached_code_response_time_ = response_time; |
| |
| if (resource_response_time_.is_null()) { |
| // Wait for the response before we can send the cached code. |
| // TODO(crbug.com/866889): Pass this as a handle to avoid the overhead of |
| // copying this data. |
| cached_code_ = data; |
| return; |
| } |
| |
| MaybeSendCachedCode(data, resource_loader); |
| } |
| |
| void ResourceLoader::CodeCacheRequest::MaybeSendCachedCode( |
| const std::vector<uint8_t>& cached_code, |
| ResourceLoader* resource_loader) { |
| if (status_ != kReceivedResponse || cached_code_response_time_.is_null() || |
| resource_response_time_.is_null()) { |
| return; |
| } |
| |
| if (resource_response_time_ != cached_code_response_time_) { |
| resource_loader->ClearCachedCode(); |
| return; |
| } |
| |
| if (cached_code.size() != 0) { |
| resource_loader->SendCachedCodeToResource( |
| reinterpret_cast<const char*>(cached_code.data()), cached_code.size()); |
| } |
| } |
| |
| ResourceLoader* ResourceLoader::Create(ResourceFetcher* fetcher, |
| ResourceLoadScheduler* scheduler, |
| Resource* resource, |
| uint32_t inflight_keepalive_bytes) { |
| return new ResourceLoader(fetcher, scheduler, resource, |
| inflight_keepalive_bytes); |
| } |
| |
| ResourceLoader::ResourceLoader(ResourceFetcher* fetcher, |
| ResourceLoadScheduler* scheduler, |
| Resource* resource, |
| uint32_t inflight_keepalive_bytes) |
| : scheduler_client_id_(ResourceLoadScheduler::kInvalidClientId), |
| fetcher_(fetcher), |
| scheduler_(scheduler), |
| resource_(resource), |
| inflight_keepalive_bytes_(inflight_keepalive_bytes), |
| is_cache_aware_loading_activated_(false), |
| progress_binding_(this), |
| cancel_timer_(Context().GetLoadingTaskRunner(), |
| this, |
| &ResourceLoader::CancelTimerFired) { |
| DCHECK(resource_); |
| DCHECK(fetcher_); |
| |
| resource_->SetLoader(this); |
| } |
| |
| ResourceLoader::~ResourceLoader() = default; |
| |
| void ResourceLoader::Trace(blink::Visitor* visitor) { |
| visitor->Trace(fetcher_); |
| visitor->Trace(scheduler_); |
| visitor->Trace(resource_); |
| ResourceLoadSchedulerClient::Trace(visitor); |
| } |
| |
| bool ResourceLoader::ShouldFetchCodeCache() { |
| if (!RuntimeEnabledFeatures::IsolatedCodeCacheEnabled()) |
| return false; |
| |
| // TODO(crbug.com/867347): Enable fetching of code caches on non-main threads |
| // once code cache has its own mojo interface. Currently it is using |
| // RenderMessageFilter that is not available on non-main threads. |
| if (!IsMainThread()) |
| return false; |
| |
| const ResourceRequest& request = resource_->GetResourceRequest(); |
| if (!request.Url().ProtocolIsInHTTPFamily()) |
| return false; |
| if (request.GetRequestContext() == |
| WebURLRequest::kRequestContextServiceWorker) |
| return false; |
| if (request.DownloadToBlob()) |
| return false; |
| // If the resource type is script or MainResource (for inline scripts) fetch |
| // code cache. For others we need not fetch code cache. |
| if (resource_->GetType() != ResourceType::kScript && |
| resource_->GetType() != ResourceType::kMainResource) |
| return false; |
| return true; |
| } |
| |
| void ResourceLoader::Start() { |
| const ResourceRequest& request = resource_->GetResourceRequest(); |
| ActivateCacheAwareLoadingIfNeeded(request); |
| loader_ = Context().CreateURLLoader(request, resource_->Options()); |
| DCHECK_EQ(ResourceLoadScheduler::kInvalidClientId, scheduler_client_id_); |
| auto throttle_option = ResourceLoadScheduler::ThrottleOption::kThrottleable; |
| |
| // Synchronous requests should not work with throttling or stopping. Also, |
| // disables throttling for the case that can be used for aka long-polling |
| // requests, but allows stopping for long-polling requests. |
| // Top level frame main resource loads are also not throttleable or |
| // stoppable. We also disable throttling and stopping for non-http[s] |
| // requests. |
| if (resource_->Options().synchronous_policy == kRequestSynchronously || |
| (request.GetFrameType() == |
| network::mojom::RequestContextFrameType::kTopLevel && |
| resource_->GetType() == ResourceType::kMainResource) || |
| !request.Url().ProtocolIsInHTTPFamily()) { |
| throttle_option = |
| ResourceLoadScheduler::ThrottleOption::kCanNotBeStoppedOrThrottled; |
| } else if (!IsThrottlableRequestContext(request.GetRequestContext())) { |
| throttle_option = ResourceLoadScheduler::ThrottleOption::kStoppable; |
| } |
| |
| if (ShouldCheckCORSInResourceLoader()) { |
| const auto origin = resource_->GetOrigin(); |
| response_tainting_ = CORS::CalculateResponseTainting( |
| request.Url(), request.GetFetchRequestMode(), origin.get(), |
| GetCORSFlag() ? CORSFlag::Set : CORSFlag::Unset); |
| } |
| |
| scheduler_->Request(this, throttle_option, request.Priority(), |
| request.IntraPriorityValue(), &scheduler_client_id_); |
| } |
| |
| void ResourceLoader::Run() { |
| StartWith(resource_->GetResourceRequest()); |
| } |
| |
| void ResourceLoader::StartWith(const ResourceRequest& request) { |
| DCHECK_NE(ResourceLoadScheduler::kInvalidClientId, scheduler_client_id_); |
| DCHECK(loader_); |
| |
| if (resource_->Options().synchronous_policy == kRequestSynchronously && |
| Context().DefersLoading()) { |
| Cancel(); |
| return; |
| } |
| |
| is_downloading_to_blob_ = request.DownloadToBlob(); |
| |
| SetDefersLoading(Context().DefersLoading()); |
| |
| if (ShouldFetchCodeCache()) { |
| code_cache_request_ = std::make_unique<CodeCacheRequest>( |
| Context().CreateCodeCacheLoader(), request.Url(), |
| Context().DefersLoading()); |
| } |
| |
| if (is_cache_aware_loading_activated_) { |
| // Override cache policy for cache-aware loading. If this request fails, a |
| // reload with original request will be triggered in DidFail(). |
| ResourceRequest cache_aware_request(request); |
| cache_aware_request.SetCacheMode( |
| mojom::FetchCacheMode::kUnspecifiedOnlyIfCachedStrict); |
| loader_->LoadAsynchronously(WrappedResourceRequest(cache_aware_request), |
| this); |
| if (code_cache_request_) { |
| // Sets defers loading and initiates a fetch from code cache. |
| code_cache_request_->FetchFromCodeCache(loader_.get(), this); |
| } |
| return; |
| } |
| |
| if (resource_->Options().synchronous_policy == kRequestSynchronously) { |
| RequestSynchronously(request); |
| } else { |
| loader_->LoadAsynchronously(WrappedResourceRequest(request), this); |
| |
| if (code_cache_request_) { |
| // Sets defers loading and initiates a fetch from code cache. |
| code_cache_request_->FetchFromCodeCache(loader_.get(), this); |
| } |
| } |
| } |
| |
| void ResourceLoader::Release( |
| ResourceLoadScheduler::ReleaseOption option, |
| const ResourceLoadScheduler::TrafficReportHints& hints) { |
| DCHECK_NE(ResourceLoadScheduler::kInvalidClientId, scheduler_client_id_); |
| bool released = scheduler_->Release(scheduler_client_id_, option, hints); |
| DCHECK(released); |
| scheduler_client_id_ = ResourceLoadScheduler::kInvalidClientId; |
| } |
| |
| void ResourceLoader::Restart(const ResourceRequest& request) { |
| CHECK_EQ(resource_->Options().synchronous_policy, kRequestAsynchronously); |
| loader_ = Context().CreateURLLoader(request, resource_->Options()); |
| StartWith(request); |
| } |
| |
| void ResourceLoader::SetDefersLoading(bool defers) { |
| DCHECK(loader_); |
| // If CodeCacheRequest handles this, then no need to handle here. |
| if (code_cache_request_ && code_cache_request_->SetDefersLoading(defers)) |
| return; |
| |
| loader_->SetDefersLoading(defers); |
| if (defers) { |
| resource_->VirtualTimePauser().UnpauseVirtualTime(); |
| } else { |
| resource_->VirtualTimePauser().PauseVirtualTime(); |
| } |
| } |
| |
| void ResourceLoader::DidChangePriority(ResourceLoadPriority load_priority, |
| int intra_priority_value) { |
| if (scheduler_->IsRunning(scheduler_client_id_)) { |
| DCHECK(loader_); |
| DCHECK_NE(ResourceLoadScheduler::kInvalidClientId, scheduler_client_id_); |
| loader_->DidChangePriority( |
| static_cast<WebURLRequest::Priority>(load_priority), |
| intra_priority_value); |
| } else { |
| scheduler_->SetPriority(scheduler_client_id_, load_priority, |
| intra_priority_value); |
| } |
| } |
| |
| void ResourceLoader::ScheduleCancel() { |
| if (!cancel_timer_.IsActive()) |
| cancel_timer_.StartOneShot(TimeDelta(), FROM_HERE); |
| } |
| |
| void ResourceLoader::CancelTimerFired(TimerBase*) { |
| if (loader_ && !resource_->HasClientsOrObservers()) |
| Cancel(); |
| } |
| |
| void ResourceLoader::Cancel() { |
| HandleError( |
| ResourceError::CancelledError(resource_->LastResourceRequest().Url())); |
| } |
| |
| void ResourceLoader::CancelForRedirectAccessCheckError( |
| const KURL& new_url, |
| ResourceRequestBlockedReason blocked_reason) { |
| resource_->WillNotFollowRedirect(); |
| |
| if (loader_) { |
| HandleError( |
| ResourceError::CancelledDueToAccessCheckError(new_url, blocked_reason)); |
| } |
| } |
| |
| static bool IsManualRedirectFetchRequest(const ResourceRequest& request) { |
| return request.GetFetchRedirectMode() == |
| network::mojom::FetchRedirectMode::kManual && |
| request.GetRequestContext() == WebURLRequest::kRequestContextFetch; |
| } |
| |
| bool ResourceLoader::WillFollowRedirect( |
| const WebURL& new_url, |
| const WebURL& new_site_for_cookies, |
| const WebString& new_referrer, |
| WebReferrerPolicy new_referrer_policy, |
| const WebString& new_method, |
| const WebURLResponse& passed_redirect_response, |
| bool& report_raw_headers) { |
| DCHECK(!passed_redirect_response.IsNull()); |
| |
| if (is_cache_aware_loading_activated_) { |
| // Fail as cache miss if cached response is a redirect. |
| HandleError( |
| ResourceError::CacheMissError(resource_->LastResourceRequest().Url())); |
| return false; |
| } |
| |
| std::unique_ptr<ResourceRequest> new_request = |
| resource_->LastResourceRequest().CreateRedirectRequest( |
| new_url, new_method, new_site_for_cookies, new_referrer, |
| static_cast<ReferrerPolicy>(new_referrer_policy), |
| !passed_redirect_response.WasFetchedViaServiceWorker()); |
| |
| ResourceType resource_type = resource_->GetType(); |
| |
| const ResourceRequest& initial_request = resource_->GetResourceRequest(); |
| // The following parameters never change during the lifetime of a request. |
| WebURLRequest::RequestContext request_context = |
| initial_request.GetRequestContext(); |
| network::mojom::RequestContextFrameType frame_type = |
| initial_request.GetFrameType(); |
| network::mojom::FetchRequestMode fetch_request_mode = |
| initial_request.GetFetchRequestMode(); |
| network::mojom::FetchCredentialsMode fetch_credentials_mode = |
| initial_request.GetFetchCredentialsMode(); |
| |
| const ResourceLoaderOptions& options = resource_->Options(); |
| |
| const ResourceResponse& redirect_response( |
| passed_redirect_response.ToResourceResponse()); |
| |
| if (!IsManualRedirectFetchRequest(initial_request)) { |
| bool unused_preload = resource_->IsUnusedPreload(); |
| |
| // Don't send security violation reports for unused preloads. |
| SecurityViolationReportingPolicy reporting_policy = |
| unused_preload ? SecurityViolationReportingPolicy::kSuppressReporting |
| : SecurityViolationReportingPolicy::kReport; |
| |
| // CanRequest() checks only enforced CSP, so check report-only here to |
| // ensure that violations are sent. |
| Context().CheckCSPForRequest( |
| request_context, new_url, options, reporting_policy, |
| ResourceRequest::RedirectStatus::kFollowedRedirect); |
| |
| base::Optional<ResourceRequestBlockedReason> blocked_reason = |
| Context().CanRequest( |
| resource_type, *new_request, new_url, options, reporting_policy, |
| ResourceRequest::RedirectStatus::kFollowedRedirect); |
| |
| if (Context().IsAdResource(new_url, resource_type, |
| new_request->GetRequestContext())) { |
| new_request->SetIsAdResource(); |
| } |
| |
| if (blocked_reason) { |
| CancelForRedirectAccessCheckError(new_url, blocked_reason.value()); |
| return false; |
| } |
| |
| if (ShouldCheckCORSInResourceLoader()) { |
| scoped_refptr<const SecurityOrigin> origin = resource_->GetOrigin(); |
| base::Optional<network::CORSErrorStatus> cors_error = |
| CORS::CheckRedirectLocation( |
| new_url, fetch_request_mode, origin.get(), |
| GetCORSFlag() ? CORSFlag::Set : CORSFlag::Unset); |
| if (!cors_error && GetCORSFlag()) { |
| cors_error = |
| CORS::CheckAccess(new_url, redirect_response.HttpStatusCode(), |
| redirect_response.HttpHeaderFields(), |
| fetch_credentials_mode, *origin); |
| } |
| if (cors_error) { |
| HandleError(ResourceError(redirect_response.Url(), *cors_error)); |
| return false; |
| } |
| // If |actualResponse|’s location URL’s origin is not same origin with |
| // |request|’s current url’s origin and |request|’s origin is not same |
| // origin with |request|’s current url’s origin, then set |request|’s |
| // tainted origin flag. |
| if (origin && |
| !SecurityOrigin::AreSameSchemeHostPort(new_url, |
| redirect_response.Url()) && |
| !origin->CanRequest(redirect_response.Url())) { |
| origin = SecurityOrigin::CreateUniqueOpaque(); |
| new_request->SetRequestorOrigin(origin); |
| } |
| } |
| if (resource_type == ResourceType::kImage && |
| fetcher_->ShouldDeferImageLoad(new_url)) { |
| CancelForRedirectAccessCheckError(new_url, |
| ResourceRequestBlockedReason::kOther); |
| return false; |
| } |
| } |
| |
| bool cross_origin = |
| !SecurityOrigin::AreSameSchemeHostPort(redirect_response.Url(), new_url); |
| fetcher_->RecordResourceTimingOnRedirect(resource_.Get(), redirect_response, |
| cross_origin); |
| |
| base::Optional<ResourceResponse> redirect_response_with_type; |
| if (ShouldCheckCORSInResourceLoader()) { |
| new_request->SetAllowStoredCredentials(CORS::CalculateCredentialsFlag( |
| fetch_credentials_mode, response_tainting_)); |
| if (!redirect_response.WasFetchedViaServiceWorker()) { |
| auto response_type = response_tainting_; |
| if (initial_request.GetFetchRedirectMode() == |
| network::mojom::FetchRedirectMode::kManual) { |
| response_type = network::mojom::FetchResponseType::kOpaqueRedirect; |
| } |
| if (response_type != redirect_response.GetType()) { |
| redirect_response_with_type = redirect_response; |
| redirect_response_with_type->SetType(response_type); |
| } |
| } |
| } |
| // TODO(yhirano): Remove this once out-of-blink CORS is enabled. |
| const ResourceResponse& redirect_response_to_pass = |
| redirect_response_with_type ? *redirect_response_with_type |
| : redirect_response; |
| |
| // The following two calls may rewrite the new_request.Url() to |
| // something else not for rejecting redirect but for other reasons. |
| // E.g. WebFrameTestClient::WillSendRequest() and |
| // RenderFrameImpl::WillSendRequest(). We should reflect the |
| // rewriting but currently we cannot. So, compare new_request.Url() and |
| // new_url after calling them, and return false to make the redirect fail on |
| // mismatch. |
| |
| Context().PrepareRequest(*new_request, |
| FetchContext::RedirectType::kForRedirect); |
| if (Context().GetFrameScheduler()) { |
| WebScopedVirtualTimePauser virtual_time_pauser = |
| Context().GetFrameScheduler()->CreateWebScopedVirtualTimePauser( |
| resource_->Url().GetString(), |
| WebScopedVirtualTimePauser::VirtualTaskDuration::kNonInstant); |
| virtual_time_pauser.PauseVirtualTime(); |
| resource_->VirtualTimePauser() = std::move(virtual_time_pauser); |
| } |
| Context().DispatchWillSendRequest( |
| resource_->Identifier(), *new_request, redirect_response_to_pass, |
| resource_->GetType(), options.initiator_info); |
| |
| // First-party cookie logic moved from DocumentLoader in Blink to |
| // net::URLRequest in the browser. Assert that Blink didn't try to change it |
| // to something else. |
| DCHECK(KURL(new_site_for_cookies) == new_request->SiteForCookies()); |
| |
| // The following parameters never change during the lifetime of a request. |
| DCHECK_EQ(new_request->GetRequestContext(), request_context); |
| DCHECK_EQ(new_request->GetFrameType(), frame_type); |
| DCHECK_EQ(new_request->GetFetchRequestMode(), fetch_request_mode); |
| DCHECK_EQ(new_request->GetFetchCredentialsMode(), fetch_credentials_mode); |
| |
| if (new_request->Url() != KURL(new_url)) { |
| CancelForRedirectAccessCheckError(new_request->Url(), |
| ResourceRequestBlockedReason::kOther); |
| return false; |
| } |
| |
| if (!resource_->WillFollowRedirect(*new_request, redirect_response_to_pass)) { |
| CancelForRedirectAccessCheckError(new_request->Url(), |
| ResourceRequestBlockedReason::kOther); |
| return false; |
| } |
| |
| if (ShouldCheckCORSInResourceLoader()) { |
| bool new_cors_flag = |
| GetCORSFlag() || CORS::CalculateCORSFlag(new_request->Url(), |
| resource_->GetOrigin().get(), |
| fetch_request_mode); |
| resource_->MutableOptions().cors_flag = new_cors_flag; |
| // Cross-origin requests are only allowed certain registered schemes. |
| if (GetCORSFlag() && !SchemeRegistry::ShouldTreatURLSchemeAsCORSEnabled( |
| new_request->Url().Protocol())) { |
| HandleError( |
| ResourceError(new_request->Url(), |
| network::CORSErrorStatus( |
| network::mojom::CORSError::kCORSDisabledScheme))); |
| return false; |
| } |
| response_tainting_ = CORS::CalculateResponseTainting( |
| new_request->Url(), fetch_request_mode, resource_->GetOrigin().get(), |
| GetCORSFlag() ? CORSFlag::Set : CORSFlag::Unset); |
| } |
| |
| report_raw_headers = new_request->ReportRawHeaders(); |
| return true; |
| } |
| |
| void ResourceLoader::DidReceiveCachedMetadata(const char* data, int length) { |
| // TODO(mythria): HttpCache still sends the metadata when available. Just |
| // ignore it here. We have to modify HttpCache to not fetch metadata when |
| // this feature is enabled. |
| if (!RuntimeEnabledFeatures::IsolatedCodeCacheEnabled()) { |
| resource_->SetSerializedCachedMetadata(data, length); |
| } |
| } |
| |
| void ResourceLoader::SendCachedCodeToResource(const char* data, int length) { |
| resource_->SetSerializedCachedMetadata(data, length); |
| } |
| |
| void ResourceLoader::ClearCachedCode() { |
| Platform::Current()->ClearCodeCacheEntry( |
| Resource::ResourceTypeToCodeCacheType(resource_->GetType()), |
| resource_->Url()); |
| } |
| |
| void ResourceLoader::DidSendData(unsigned long long bytes_sent, |
| unsigned long long total_bytes_to_be_sent) { |
| resource_->DidSendData(bytes_sent, total_bytes_to_be_sent); |
| } |
| |
| FetchContext& ResourceLoader::Context() const { |
| return fetcher_->Context(); |
| } |
| |
| void ResourceLoader::DidReceiveResponse( |
| const WebURLResponse& web_url_response, |
| std::unique_ptr<WebDataConsumerHandle> handle) { |
| DCHECK(!web_url_response.IsNull()); |
| |
| if (Context().IsDetached()) { |
| // If the fetch context is already detached, we don't need further signals, |
| // so let's cancel the request. |
| HandleError(ResourceError::CancelledError(web_url_response.Url())); |
| return; |
| } |
| |
| ResourceType resource_type = resource_->GetType(); |
| |
| const ResourceRequest& initial_request = resource_->GetResourceRequest(); |
| // The following parameters never change during the lifetime of a request. |
| WebURLRequest::RequestContext request_context = |
| initial_request.GetRequestContext(); |
| network::mojom::FetchRequestMode fetch_request_mode = |
| initial_request.GetFetchRequestMode(); |
| |
| const ResourceLoaderOptions& options = resource_->Options(); |
| |
| const ResourceResponse& response = web_url_response.ToResourceResponse(); |
| |
| // Perform 'nosniff' checks against the original response instead of the 304 |
| // response for a successful revalidation. |
| const ResourceResponse& nosniffed_response = |
| (resource_->IsCacheValidator() && response.HttpStatusCode() == 304) |
| ? resource_->GetResponse() |
| : response; |
| base::Optional<ResourceRequestBlockedReason> blocked_reason = |
| CheckResponseNosniff(request_context, nosniffed_response); |
| if (blocked_reason) { |
| HandleError(ResourceError::CancelledDueToAccessCheckError( |
| response.Url(), blocked_reason.value())); |
| return; |
| } |
| |
| if (response.WasFetchedViaServiceWorker()) { |
| if (options.cors_handling_by_resource_fetcher == |
| kEnableCORSHandlingByResourceFetcher && |
| fetch_request_mode == network::mojom::FetchRequestMode::kCORS && |
| response.WasFallbackRequiredByServiceWorker()) { |
| ResourceRequest last_request = resource_->LastResourceRequest(); |
| DCHECK(!last_request.GetSkipServiceWorker()); |
| // This code handles the case when a controlling service worker doesn't |
| // handle a cross origin request. |
| if (!Context().ShouldLoadNewResource(resource_type)) { |
| // Cancel the request if we should not trigger a reload now. |
| HandleError(ResourceError::CancelledError(response.Url())); |
| return; |
| } |
| last_request.SetSkipServiceWorker(true); |
| Restart(last_request); |
| return; |
| } |
| |
| // If the response is fetched via ServiceWorker, the original URL of the |
| // response could be different from the URL of the request. We check the URL |
| // not to load the resources which are forbidden by the page CSP. |
| // https://w3c.github.io/webappsec-csp/#should-block-response |
| const KURL& original_url = response.OriginalURLViaServiceWorker(); |
| if (!original_url.IsEmpty()) { |
| // CanRequest() below only checks enforced policies: check report-only |
| // here to ensure violations are sent. |
| Context().CheckCSPForRequest( |
| request_context, original_url, options, |
| SecurityViolationReportingPolicy::kReport, |
| ResourceRequest::RedirectStatus::kFollowedRedirect); |
| |
| base::Optional<ResourceRequestBlockedReason> blocked_reason = |
| Context().CanRequest( |
| resource_type, initial_request, original_url, options, |
| SecurityViolationReportingPolicy::kReport, |
| ResourceRequest::RedirectStatus::kFollowedRedirect); |
| if (blocked_reason) { |
| HandleError(ResourceError::CancelledDueToAccessCheckError( |
| original_url, blocked_reason.value())); |
| return; |
| } |
| } |
| } |
| |
| base::Optional<ResourceResponse> response_with_type; |
| if (ShouldCheckCORSInResourceLoader() && |
| !response.WasFetchedViaServiceWorker() && |
| !(resource_->IsCacheValidator() && response.HttpStatusCode() == 304)) { |
| if (GetCORSFlag()) { |
| base::Optional<network::CORSErrorStatus> cors_error = CORS::CheckAccess( |
| response.Url(), response.HttpStatusCode(), |
| response.HttpHeaderFields(), |
| initial_request.GetFetchCredentialsMode(), *resource_->GetOrigin()); |
| if (cors_error) { |
| HandleError(ResourceError(response.Url(), *cors_error)); |
| return; |
| } |
| } |
| if (response_tainting_ != response.GetType()) { |
| response_with_type = response; |
| response_with_type->SetType(response_tainting_); |
| } |
| } |
| // TODO(yhirano): Remove this once out-of-blink CORS is enabled. |
| const ResourceResponse& response_to_pass = |
| response_with_type ? *response_with_type : response; |
| |
| // FrameType never changes during the lifetime of a request. |
| Context().DispatchDidReceiveResponse( |
| resource_->Identifier(), response_to_pass, initial_request.GetFrameType(), |
| request_context, resource_, |
| FetchContext::ResourceResponseType::kNotFromMemoryCache); |
| |
| resource_->ResponseReceived(response_to_pass, std::move(handle)); |
| |
| // Send the cached code after we notify that the response is received. |
| // Resource expects that we receive the response first before the |
| // corresponding cached code. |
| if (code_cache_request_) |
| code_cache_request_->DidReceiveResponse(response.ResponseTime(), this); |
| |
| if (!resource_->Loader()) |
| return; |
| |
| if (response_to_pass.HttpStatusCode() >= 400 && |
| !resource_->ShouldIgnoreHTTPStatusCodeErrors()) |
| HandleError(ResourceError::CancelledError(response_to_pass.Url())); |
| } |
| |
| void ResourceLoader::DidReceiveResponse(const WebURLResponse& response) { |
| DidReceiveResponse(response, nullptr); |
| } |
| |
| void ResourceLoader::DidStartLoadingResponseBody( |
| mojo::ScopedDataPipeConsumerHandle body) { |
| DCHECK(is_downloading_to_blob_); |
| DCHECK(!blob_response_started_); |
| blob_response_started_ = true; |
| |
| const ResourceResponse& response = resource_->GetResponse(); |
| AtomicString mime_type = response.MimeType(); |
| |
| mojom::blink::ProgressClientAssociatedPtrInfo progress_client_ptr; |
| progress_binding_.Bind(MakeRequest(&progress_client_ptr)); |
| |
| // Callback is bound to a WeakPersistent, as ResourceLoader is kept alive by |
| // ResourceFetcher as long as we still care about the result of the load. |
| mojom::blink::BlobRegistry* blob_registry = BlobDataHandle::GetBlobRegistry(); |
| blob_registry->RegisterFromStream( |
| mime_type.IsNull() ? g_empty_string : mime_type.LowerASCII(), "", |
| std::max(0ll, response.ExpectedContentLength()), std::move(body), |
| std::move(progress_client_ptr), |
| WTF::Bind(&ResourceLoader::FinishedCreatingBlob, |
| WrapWeakPersistent(this))); |
| } |
| |
| void ResourceLoader::DidReceiveData(const char* data, int length) { |
| CHECK_GE(length, 0); |
| |
| Context().DispatchDidReceiveData(resource_->Identifier(), data, length); |
| resource_->AppendData(data, length); |
| } |
| |
| void ResourceLoader::DidReceiveTransferSizeUpdate(int transfer_size_diff) { |
| DCHECK_GT(transfer_size_diff, 0); |
| Context().DispatchDidReceiveEncodedData(resource_->Identifier(), |
| transfer_size_diff); |
| } |
| |
| void ResourceLoader::DidFinishLoadingFirstPartInMultipart() { |
| network_instrumentation::EndResourceLoad( |
| resource_->Identifier(), |
| network_instrumentation::RequestOutcome::kSuccess); |
| |
| fetcher_->HandleLoaderFinish(resource_.Get(), TimeTicks(), |
| ResourceFetcher::kDidFinishFirstPartInMultipart, |
| 0, false); |
| } |
| |
| void ResourceLoader::DidFinishLoading(TimeTicks finish_time, |
| int64_t encoded_data_length, |
| int64_t encoded_body_length, |
| int64_t decoded_body_length, |
| bool should_report_corb_blocking) { |
| resource_->SetEncodedDataLength(encoded_data_length); |
| resource_->SetEncodedBodyLength(encoded_body_length); |
| resource_->SetDecodedBodyLength(decoded_body_length); |
| |
| if (is_downloading_to_blob_ && !blob_finished_ && blob_response_started_) { |
| load_did_finish_before_blob_ = |
| DeferedFinishLoadingInfo{finish_time, should_report_corb_blocking}; |
| return; |
| } |
| |
| Release(ResourceLoadScheduler::ReleaseOption::kReleaseAndSchedule, |
| ResourceLoadScheduler::TrafficReportHints(encoded_data_length, |
| decoded_body_length)); |
| loader_.reset(); |
| code_cache_request_.reset(); |
| |
| network_instrumentation::EndResourceLoad( |
| resource_->Identifier(), |
| network_instrumentation::RequestOutcome::kSuccess); |
| |
| fetcher_->HandleLoaderFinish( |
| resource_.Get(), finish_time, ResourceFetcher::kDidFinishLoading, |
| inflight_keepalive_bytes_, should_report_corb_blocking); |
| } |
| |
| void ResourceLoader::DidFail(const WebURLError& error, |
| int64_t encoded_data_length, |
| int64_t encoded_body_length, |
| int64_t decoded_body_length) { |
| resource_->SetEncodedDataLength(encoded_data_length); |
| resource_->SetEncodedBodyLength(encoded_body_length); |
| resource_->SetDecodedBodyLength(decoded_body_length); |
| HandleError(error); |
| } |
| |
| void ResourceLoader::HandleError(const ResourceError& error) { |
| if (is_cache_aware_loading_activated_ && error.IsCacheMiss() && |
| Context().ShouldLoadNewResource(resource_->GetType())) { |
| resource_->WillReloadAfterDiskCacheMiss(); |
| is_cache_aware_loading_activated_ = false; |
| Restart(resource_->GetResourceRequest()); |
| return; |
| } |
| if (error.CORSErrorStatus()) { |
| Context().AddErrorConsoleMessage( |
| CORS::GetErrorString( |
| *error.CORSErrorStatus(), resource_->GetResourceRequest().Url(), |
| resource_->LastResourceRequest().Url(), *resource_->GetOrigin(), |
| resource_->GetType(), resource_->Options().initiator_info.name), |
| FetchContext::kJSSource); |
| } |
| |
| Release(ResourceLoadScheduler::ReleaseOption::kReleaseAndSchedule, |
| ResourceLoadScheduler::TrafficReportHints::InvalidInstance()); |
| loader_.reset(); |
| code_cache_request_.reset(); |
| |
| network_instrumentation::EndResourceLoad( |
| resource_->Identifier(), network_instrumentation::RequestOutcome::kFail); |
| |
| fetcher_->HandleLoaderError(resource_.Get(), error, |
| inflight_keepalive_bytes_); |
| } |
| |
| void ResourceLoader::RequestSynchronously(const ResourceRequest& request) { |
| DCHECK(loader_); |
| DCHECK_EQ(request.Priority(), ResourceLoadPriority::kHighest); |
| |
| WrappedResourceRequest request_in(request); |
| WebURLResponse response_out; |
| base::Optional<WebURLError> error_out; |
| WebData data_out; |
| int64_t encoded_data_length = WebURLLoaderClient::kUnknownEncodedDataLength; |
| int64_t encoded_body_length = 0; |
| WebBlobInfo downloaded_blob; |
| loader_->LoadSynchronously(request_in, this, response_out, error_out, |
| data_out, encoded_data_length, encoded_body_length, |
| downloaded_blob); |
| |
| // A message dispatched while synchronously fetching the resource |
| // can bring about the cancellation of this load. |
| if (!loader_) |
| return; |
| int64_t decoded_body_length = data_out.size(); |
| if (error_out) { |
| DidFail(*error_out, encoded_data_length, encoded_body_length, |
| decoded_body_length); |
| return; |
| } |
| DidReceiveResponse(response_out); |
| if (!loader_) |
| return; |
| DCHECK_GE(response_out.ToResourceResponse().EncodedBodyLength(), 0); |
| |
| // TODO(crbug.com/867347): Enable fetching of code caches synchronously |
| // once code cache has its own mojo interface. Currently it is using |
| // RenderMessageFilter that is not available on non-main threads. |
| |
| // Follow the async case convention of not calling DidReceiveData or |
| // appending data to m_resource if the response body is empty. Copying the |
| // empty buffer is a noop in most cases, but is destructive in the case of |
| // a 304, where it will overwrite the cached data we should be reusing. |
| if (data_out.size()) { |
| data_out.ForEachSegment([this](const char* segment, size_t segment_size, |
| size_t segment_offset) { |
| DidReceiveData(segment, segment_size); |
| return true; |
| }); |
| } |
| |
| if (request.DownloadToBlob()) { |
| auto blob = downloaded_blob.GetBlobHandle(); |
| if (blob) |
| OnProgress(blob->size()); |
| FinishedCreatingBlob(blob); |
| } |
| DidFinishLoading(CurrentTimeTicks(), encoded_data_length, encoded_body_length, |
| decoded_body_length, false); |
| } |
| |
| void ResourceLoader::Dispose() { |
| loader_ = nullptr; |
| progress_binding_.Close(); |
| code_cache_request_.reset(); |
| |
| // Release() should be called to release |scheduler_client_id_| beforehand in |
| // DidFinishLoading() or DidFail(), but when a timer to call Cancel() is |
| // ignored due to GC, this case happens. We just release here because we can |
| // not schedule another request safely. See crbug.com/675947. |
| if (scheduler_client_id_ != ResourceLoadScheduler::kInvalidClientId) { |
| Release(ResourceLoadScheduler::ReleaseOption::kReleaseOnly, |
| ResourceLoadScheduler::TrafficReportHints::InvalidInstance()); |
| } |
| } |
| |
| void ResourceLoader::ActivateCacheAwareLoadingIfNeeded( |
| const ResourceRequest& request) { |
| DCHECK(!is_cache_aware_loading_activated_); |
| |
| if (resource_->Options().cache_aware_loading_enabled != |
| kIsCacheAwareLoadingEnabled) |
| return; |
| |
| // Synchronous requests are not supported. |
| if (resource_->Options().synchronous_policy == kRequestSynchronously) |
| return; |
| |
| // Don't activate on Resource revalidation. |
| if (resource_->IsCacheValidator()) |
| return; |
| |
| // Don't activate if cache policy is explicitly set. |
| if (request.GetCacheMode() != mojom::FetchCacheMode::kDefault) |
| return; |
| |
| // Don't activate if the page is controlled by service worker. |
| if (fetcher_->IsControlledByServiceWorker() != |
| blink::mojom::ControllerServiceWorkerMode::kNoController) { |
| return; |
| } |
| |
| is_cache_aware_loading_activated_ = true; |
| } |
| |
| bool ResourceLoader::ShouldBeKeptAliveWhenDetached() const { |
| return resource_->GetResourceRequest().GetKeepalive() && |
| resource_->GetResponse().IsNull(); |
| } |
| |
| scoped_refptr<base::SingleThreadTaskRunner> |
| ResourceLoader::GetLoadingTaskRunner() { |
| return Context().GetLoadingTaskRunner(); |
| } |
| |
| void ResourceLoader::OnProgress(uint64_t delta) { |
| DCHECK(!blob_finished_); |
| |
| if (scheduler_client_id_ == ResourceLoadScheduler::kInvalidClientId) |
| return; |
| |
| Context().DispatchDidReceiveData(resource_->Identifier(), nullptr, delta); |
| resource_->DidDownloadData(delta); |
| } |
| |
| void ResourceLoader::FinishedCreatingBlob( |
| const scoped_refptr<BlobDataHandle>& blob) { |
| DCHECK(!blob_finished_); |
| |
| if (scheduler_client_id_ == ResourceLoadScheduler::kInvalidClientId) |
| return; |
| |
| Context().DispatchDidDownloadToBlob(resource_->Identifier(), blob.get()); |
| resource_->DidDownloadToBlob(blob); |
| |
| blob_finished_ = true; |
| if (load_did_finish_before_blob_) { |
| const ResourceResponse& response = resource_->GetResponse(); |
| DidFinishLoading(load_did_finish_before_blob_->finish_time, |
| response.EncodedDataLength(), response.EncodedBodyLength(), |
| response.DecodedBodyLength(), |
| load_did_finish_before_blob_->should_report_corb_blocking); |
| } |
| } |
| |
| base::Optional<ResourceRequestBlockedReason> |
| ResourceLoader::CheckResponseNosniff( |
| WebURLRequest::RequestContext request_context, |
| const ResourceResponse& response) const { |
| bool sniffing_allowed = |
| ParseContentTypeOptionsHeader(response.HttpHeaderField( |
| HTTPNames::X_Content_Type_Options)) != kContentTypeOptionsNosniff; |
| if (sniffing_allowed) |
| return base::nullopt; |
| |
| String mime_type = response.HttpContentType(); |
| if (request_context == WebURLRequest::kRequestContextStyle && |
| !MIMETypeRegistry::IsSupportedStyleSheetMIMEType(mime_type)) { |
| Context().AddErrorConsoleMessage( |
| "Refused to apply style from '" + response.Url().ElidedString() + |
| "' because its MIME type ('" + mime_type + "') " + |
| "is not a supported stylesheet MIME type, and strict MIME checking " |
| "is enabled.", |
| FetchContext::kSecuritySource); |
| return ResourceRequestBlockedReason::kContentType; |
| } |
| // TODO(mkwst): Move the 'nosniff' bit of 'AllowedByNosniff::MimeTypeAsScript' |
| // here alongside the style checks, and put its use counters somewhere else. |
| |
| return base::nullopt; |
| } |
| |
| bool ResourceLoader::ShouldCheckCORSInResourceLoader() const { |
| return !RuntimeEnabledFeatures::OutOfBlinkCORSEnabled() && |
| resource_->Options().cors_handling_by_resource_fetcher == |
| kEnableCORSHandlingByResourceFetcher; |
| } |
| |
| } // namespace blink |