blob: 7b8d67519e4bae3e250e20aafcab40dc9c15f42f [file] [log] [blame]
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/cors/cors_url_loader.h"
#include "base/bind.h"
#include "base/stl_util.h"
#include "net/base/load_flags.h"
#include "services/network/cors/preflight_controller.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "url/url_util.h"
namespace network {
namespace cors {
namespace {
bool NeedsPreflight(const ResourceRequest& request) {
if (!IsCorsEnabledRequestMode(request.fetch_request_mode))
return false;
if (request.is_external_request)
return true;
if (request.fetch_request_mode ==
mojom::FetchRequestMode::kCorsWithForcedPreflight) {
return true;
}
if (request.cors_preflight_policy ==
mojom::CorsPreflightPolicy::kPreventPreflight) {
return false;
}
if (!IsCorsSafelistedMethod(request.method))
return true;
return !CorsUnsafeNotForbiddenRequestHeaderNames(
request.headers.GetHeaderVector(), request.is_revalidating)
.empty();
}
} // namespace
CorsURLLoader::CorsURLLoader(
mojom::URLLoaderRequest loader_request,
int32_t routing_id,
int32_t request_id,
uint32_t options,
DeleteCallback delete_callback,
const ResourceRequest& resource_request,
mojom::URLLoaderClientPtr client,
const net::MutableNetworkTrafficAnnotationTag& traffic_annotation,
mojom::URLLoaderFactory* network_loader_factory,
const base::RepeatingCallback<void(int)>& request_finalizer,
const OriginAccessList* origin_access_list,
const OriginAccessList* factory_bound_origin_access_list,
PreflightController* preflight_controller)
: binding_(this, std::move(loader_request)),
routing_id_(routing_id),
request_id_(request_id),
options_(options),
delete_callback_(std::move(delete_callback)),
network_loader_factory_(network_loader_factory),
network_client_binding_(this),
request_(resource_request),
forwarding_client_(std::move(client)),
request_finalizer_(request_finalizer),
traffic_annotation_(traffic_annotation),
origin_access_list_(origin_access_list),
factory_bound_origin_access_list_(factory_bound_origin_access_list),
preflight_controller_(preflight_controller),
weak_factory_(this) {
binding_.set_connection_error_handler(base::BindOnce(
&CorsURLLoader::OnConnectionError, base::Unretained(this)));
DCHECK(network_loader_factory_);
DCHECK(origin_access_list_);
DCHECK(preflight_controller_);
SetCorsFlagIfNeeded();
}
CorsURLLoader::~CorsURLLoader() {
// Close pipes first to ignore possible subsequent callback invocations
// cased by |network_loader_|
network_client_binding_.Close();
}
void CorsURLLoader::Start() {
if (fetch_cors_flag_ &&
IsCorsEnabledRequestMode(request_.fetch_request_mode)) {
// Username and password should be stripped in a CORS-enabled request.
if (request_.url.has_username() || request_.url.has_password()) {
GURL::Replacements replacements;
replacements.SetUsernameStr("");
replacements.SetPasswordStr("");
request_.url = request_.url.ReplaceComponents(replacements);
}
}
StartRequest();
}
void CorsURLLoader::FollowRedirect(
const std::vector<std::string>& removed_headers,
const net::HttpRequestHeaders& modified_headers,
const base::Optional<GURL>& new_url) {
if (!network_loader_ || !deferred_redirect_url_) {
HandleComplete(URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
if (new_url &&
(new_url->GetOrigin() != deferred_redirect_url_->GetOrigin())) {
NOTREACHED() << "Can only change the URL within the same origin.";
HandleComplete(URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
deferred_redirect_url_.reset();
// When the redirect mode is "error", the client is not expected to
// call this function. Let's abort the request.
if (request_.fetch_redirect_mode == mojom::FetchRedirectMode::kError) {
HandleComplete(URLLoaderCompletionStatus(net::ERR_FAILED));
return;
}
for (const auto& name : removed_headers)
request_.headers.RemoveHeader(name);
request_.headers.MergeFrom(modified_headers);
request_.url = redirect_info_.new_url;
request_.method = redirect_info_.new_method;
request_.referrer = GURL(redirect_info_.new_referrer);
request_.referrer_policy = redirect_info_.new_referrer_policy;
// The request method can be changed to "GET". In this case we need to
// reset the request body manually.
if (request_.method == net::HttpRequestHeaders::kGetMethod)
request_.request_body = nullptr;
const bool original_fetch_cors_flag = fetch_cors_flag_;
SetCorsFlagIfNeeded();
// We cannot use FollowRedirect for a request with preflight (i.e., when both
// |fetch_cors_flag_| and |NeedsPreflight(request_)| are true).
//
// Additionally, when |original_fetch_cors_flag| is false,
// |fetch_cors_flag_| is true and |NeedsPreflight(request)| is false, the net/
// implementation won't attach an "origin" header on redirect, as the original
// request didn't have one. In such a case we need to re-issue a request
// manually in order to attach the correct origin header.
// For "no-cors" requests we rely on redirect logic in net/ (specifically
// in net/url_request/redirect_util.cc).
if ((original_fetch_cors_flag && !NeedsPreflight(request_)) ||
!fetch_cors_flag_) {
response_tainting_ = CalculateResponseTainting(
request_.url, request_.fetch_request_mode, request_.request_initiator,
fetch_cors_flag_, tainted_, origin_access_list_);
network_loader_->FollowRedirect(removed_headers, modified_headers, new_url);
return;
}
DCHECK_NE(request_.fetch_request_mode, mojom::FetchRequestMode::kNoCors);
if (request_finalizer_)
request_finalizer_.Run(request_id_);
network_client_binding_.Unbind();
StartRequest();
}
void CorsURLLoader::ProceedWithResponse() {
NOTREACHED();
}
void CorsURLLoader::SetPriority(net::RequestPriority priority,
int32_t intra_priority_value) {
if (network_loader_)
network_loader_->SetPriority(priority, intra_priority_value);
}
void CorsURLLoader::PauseReadingBodyFromNet() {
if (network_loader_)
network_loader_->PauseReadingBodyFromNet();
}
void CorsURLLoader::ResumeReadingBodyFromNet() {
if (network_loader_)
network_loader_->ResumeReadingBodyFromNet();
}
void CorsURLLoader::OnReceiveResponse(
const ResourceResponseHead& response_head) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
int response_status_code =
response_head.headers ? response_head.headers->response_code() : 0;
const bool is_304_for_revalidation =
request_.is_revalidating && response_status_code == 304;
if (fetch_cors_flag_ && !is_304_for_revalidation) {
const auto error_status = CheckAccess(
request_.url, response_status_code,
GetHeaderString(response_head, header_names::kAccessControlAllowOrigin),
GetHeaderString(response_head,
header_names::kAccessControlAllowCredentials),
request_.fetch_credentials_mode,
tainted_ ? url::Origin() : *request_.request_initiator);
if (error_status) {
HandleComplete(URLLoaderCompletionStatus(*error_status));
return;
}
}
ResourceResponseHead response_head_to_pass = response_head;
response_head_to_pass.response_type = response_tainting_;
forwarding_client_->OnReceiveResponse(response_head_to_pass);
}
void CorsURLLoader::OnReceiveRedirect(
const net::RedirectInfo& redirect_info,
const ResourceResponseHead& response_head) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
if (request_.fetch_redirect_mode == mojom::FetchRedirectMode::kManual) {
deferred_redirect_url_ = std::make_unique<GURL>(redirect_info.new_url);
forwarding_client_->OnReceiveRedirect(redirect_info, response_head);
return;
}
// If |CORS flag| is set and a CORS check for |request| and |response| returns
// failure, then return a network error.
if (fetch_cors_flag_ &&
IsCorsEnabledRequestMode(request_.fetch_request_mode)) {
const auto error_status = CheckAccess(
request_.url, response_head.headers->response_code(),
GetHeaderString(response_head, header_names::kAccessControlAllowOrigin),
GetHeaderString(response_head,
header_names::kAccessControlAllowCredentials),
request_.fetch_credentials_mode,
tainted_ ? url::Origin() : *request_.request_initiator);
if (error_status) {
HandleComplete(URLLoaderCompletionStatus(*error_status));
return;
}
}
// Because we initiate a new request on redirect in some cases, we cannot
// rely on the redirect logic in the network stack. Hence we need to
// implement some logic in
// https://fetch.spec.whatwg.org/#http-redirect-fetch here.
// If |request|’s redirect count is twenty, return a network error.
// Increase |request|’s redirect count by one.
if (redirect_count_++ == 20) {
HandleComplete(URLLoaderCompletionStatus(net::ERR_TOO_MANY_REDIRECTS));
return;
}
const auto error_status = CheckRedirectLocation(
redirect_info.new_url, request_.fetch_request_mode,
request_.request_initiator, fetch_cors_flag_, tainted_);
if (error_status) {
HandleComplete(URLLoaderCompletionStatus(*error_status));
return;
}
// TODO(yhirano): Implement the following (Note: this is needed when upload
// streaming is implemented):
// If |actualResponse|’s status is not 303, |request|’s body is non-null, and
// |request|’s body’s source is null, then return a network error.
// 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 (request_.request_initiator &&
(!url::Origin::Create(redirect_info.new_url)
.IsSameOriginWith(url::Origin::Create(request_.url)) &&
!request_.request_initiator->IsSameOriginWith(
url::Origin::Create(request_.url)))) {
tainted_ = true;
}
// TODO(yhirano): Implement the following:
// If either |actualResponse|’s status is 301 or 302 and |request|’s method is
// `POST`, or |actualResponse|’s status is 303, set |request|’s method to
// `GET` and request’s body to null.
// TODO(yhirano): Implement the following:
// Invoke |set request’s referrer policy on redirect| on |request| and
// |actualResponse|.
redirect_info_ = redirect_info;
deferred_redirect_url_ = std::make_unique<GURL>(redirect_info.new_url);
auto response_head_to_pass = response_head;
if (request_.fetch_redirect_mode == mojom::FetchRedirectMode::kManual) {
response_head_to_pass.response_type =
mojom::FetchResponseType::kOpaqueRedirect;
} else {
response_head_to_pass.response_type = response_tainting_;
}
forwarding_client_->OnReceiveRedirect(redirect_info, response_head_to_pass);
}
void CorsURLLoader::OnUploadProgress(int64_t current_position,
int64_t total_size,
OnUploadProgressCallback ack_callback) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
forwarding_client_->OnUploadProgress(current_position, total_size,
std::move(ack_callback));
}
void CorsURLLoader::OnReceiveCachedMetadata(const std::vector<uint8_t>& data) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
forwarding_client_->OnReceiveCachedMetadata(data);
}
void CorsURLLoader::OnTransferSizeUpdated(int32_t transfer_size_diff) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
forwarding_client_->OnTransferSizeUpdated(transfer_size_diff);
}
void CorsURLLoader::OnStartLoadingResponseBody(
mojo::ScopedDataPipeConsumerHandle body) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
DCHECK(!deferred_redirect_url_);
forwarding_client_->OnStartLoadingResponseBody(std::move(body));
}
void CorsURLLoader::OnComplete(const URLLoaderCompletionStatus& status) {
DCHECK(network_loader_);
DCHECK(forwarding_client_);
// |network_loader_| will call OnComplete at anytime when a problem happens
// inside the URLLoader, e.g. on URLLoader::OnConnectionError call. We need
// to expect it also happens even during redirect handling.
DCHECK(!deferred_redirect_url_ || status.error_code != net::OK);
URLLoaderCompletionStatus modified_status(status);
if (status.error_code == net::OK)
modified_status.cors_preflight_timing_info.swap(preflight_timing_info_);
HandleComplete(modified_status);
}
void CorsURLLoader::StartRequest() {
if (fetch_cors_flag_ && !base::ContainsValue(url::GetCorsEnabledSchemes(),
request_.url.scheme())) {
HandleComplete(URLLoaderCompletionStatus(
CorsErrorStatus(mojom::CorsError::kCorsDisabledScheme)));
return;
}
// If the |CORS flag| is set, |httpRequest|’s method is neither `GET` nor
// `HEAD`, or |httpRequest|’s mode is "websocket", then append
// `Origin`/the result of serializing a request origin with |httpRequest|, to
// |httpRequest|’s header list.
//
// We exclude navigation requests to keep the existing behavior.
// TODO(yhirano): Reconsider this.
if (request_.fetch_request_mode != mojom::FetchRequestMode::kNavigate &&
request_.request_initiator &&
(fetch_cors_flag_ ||
(request_.method != "GET" && request_.method != "HEAD"))) {
request_.headers.SetHeader(
net::HttpRequestHeaders::kOrigin,
(tainted_ ? url::Origin() : *request_.request_initiator).Serialize());
}
if (fetch_cors_flag_ &&
request_.fetch_request_mode == mojom::FetchRequestMode::kSameOrigin) {
DCHECK(request_.request_initiator);
HandleComplete(URLLoaderCompletionStatus(
CorsErrorStatus(mojom::CorsError::kDisallowedByMode)));
return;
}
response_tainting_ = CalculateResponseTainting(
request_.url, request_.fetch_request_mode, request_.request_initiator,
fetch_cors_flag_, tainted_, origin_access_list_);
if (!CalculateCredentialsFlag(request_.fetch_credentials_mode,
response_tainting_)) {
request_.load_flags |= net::LOAD_DO_NOT_SAVE_COOKIES;
request_.load_flags |= net::LOAD_DO_NOT_SEND_COOKIES;
request_.load_flags |= net::LOAD_DO_NOT_SEND_AUTH_DATA;
}
// Note that even when |NeedsPreflight(request_)| holds we don't make a
// preflight request when |fetch_cors_flag_| is false (e.g., when the origin
// of the url is equal to the origin of the request.
if (!fetch_cors_flag_ || !NeedsPreflight(request_)) {
StartNetworkRequest(net::OK, base::nullopt, base::nullopt);
return;
}
base::OnceCallback<void()> preflight_finalizer;
if (request_finalizer_)
preflight_finalizer = base::BindOnce(request_finalizer_, request_id_);
preflight_controller_->PerformPreflightCheck(
base::BindOnce(&CorsURLLoader::StartNetworkRequest,
weak_factory_.GetWeakPtr()),
request_id_, request_, tainted_,
net::NetworkTrafficAnnotationTag(traffic_annotation_),
network_loader_factory_, std::move(preflight_finalizer));
}
void CorsURLLoader::StartNetworkRequest(
int error_code,
base::Optional<CorsErrorStatus> status,
base::Optional<PreflightTimingInfo> preflight_timing_info) {
if (error_code != net::OK) {
HandleComplete(status ? URLLoaderCompletionStatus(*status)
: URLLoaderCompletionStatus(error_code));
return;
}
DCHECK(!status);
if (preflight_timing_info)
preflight_timing_info_.push_back(*preflight_timing_info);
mojom::URLLoaderClientPtr network_client;
network_client_binding_.Bind(mojo::MakeRequest(&network_client));
// Binding |this| as an unretained pointer is safe because
// |network_client_binding_| shares this object's lifetime.
network_client_binding_.set_connection_error_handler(base::BindOnce(
&CorsURLLoader::OnConnectionError, base::Unretained(this)));
network_loader_factory_->CreateLoaderAndStart(
mojo::MakeRequest(&network_loader_), routing_id_, request_id_, options_,
request_, std::move(network_client), traffic_annotation_);
}
void CorsURLLoader::HandleComplete(const URLLoaderCompletionStatus& status) {
forwarding_client_->OnComplete(status);
std::move(delete_callback_).Run(this);
// |this| is deleted here.
}
void CorsURLLoader::OnConnectionError() {
HandleComplete(URLLoaderCompletionStatus(net::ERR_ABORTED));
}
// This should be identical to CalculateCorsFlag defined in
// //third_party/blink/renderer/platform/loader/cors/cors.cc.
void CorsURLLoader::SetCorsFlagIfNeeded() {
if (fetch_cors_flag_)
return;
if (request_.fetch_request_mode == mojom::FetchRequestMode::kNavigate ||
request_.fetch_request_mode == mojom::FetchRequestMode::kNoCors) {
return;
}
if (request_.url.SchemeIs(url::kDataScheme))
return;
// CORS needs a proper origin (including a unique opaque origin). If the
// request doesn't have one, CORS should not work.
DCHECK(request_.request_initiator);
// The source origin and destination URL pair may be in the allow list.
switch (origin_access_list_->CheckAccessState(*request_.request_initiator,
request_.url)) {
case OriginAccessList::AccessState::kAllowed:
return;
case OriginAccessList::AccessState::kBlocked:
break;
case OriginAccessList::AccessState::kNotListed:
if (factory_bound_origin_access_list_->CheckAccessState(
*request_.request_initiator, request_.url) ==
OriginAccessList::AccessState::kAllowed) {
return;
}
break;
}
// When a request is initiated in a unique opaque origin (e.g., in a sandboxed
// iframe) and the blob is also created in the context, |request_initiator|
// is a unique opaque origin and url::Origin::Create(request_.url) is another
// unique opaque origin. url::Origin::IsSameOriginWith(p, q) returns false
// when both |p| and |q| are opaque, but in this case we want to say that the
// request is a same-origin request. Hence we don't set |fetch_cors_flag_|,
// assuming the request comes from a renderer and the origin is checked there
// (in BaseFetchContext::CanRequest).
// In the future blob URLs will not come here because there will be a
// separate URLLoaderFactory for blobs.
// TODO(yhirano): Remove this logic at the time.
if (request_.url.SchemeIsBlob() && request_.request_initiator->opaque() &&
url::Origin::Create(request_.url).opaque()) {
return;
}
if (request_.request_initiator->IsSameOriginWith(
url::Origin::Create(request_.url))) {
return;
}
fetch_cors_flag_ = true;
}
// Keep this in sync with the identical function
// blink::cors::CalculateResponseTainting.
//
// static
mojom::FetchResponseType CorsURLLoader::CalculateResponseTainting(
const GURL& url,
mojom::FetchRequestMode request_mode,
const base::Optional<url::Origin>& origin,
bool cors_flag,
bool tainted_origin,
const OriginAccessList* origin_access_list) {
if (url.SchemeIs(url::kDataScheme))
return mojom::FetchResponseType::kBasic;
if (cors_flag) {
DCHECK(IsCorsEnabledRequestMode(request_mode));
return mojom::FetchResponseType::kCors;
}
if (!origin) {
// This is actually not defined in the fetch spec, but in this case CORS
// is disabled so no one should care this value.
return mojom::FetchResponseType::kBasic;
}
if (request_mode == mojom::FetchRequestMode::kNoCors) {
if (tainted_origin ||
(!origin->IsSameOriginWith(url::Origin::Create(url)) &&
origin_access_list->CheckAccessState(*origin, url) !=
OriginAccessList::AccessState::kAllowed)) {
return mojom::FetchResponseType::kOpaque;
}
}
return mojom::FetchResponseType::kBasic;
}
base::Optional<std::string> CorsURLLoader::GetHeaderString(
const ResourceResponseHead& response,
const std::string& header_name) {
if (!response.headers)
return base::nullopt;
std::string header_value;
if (!response.headers->GetNormalizedHeader(header_name, &header_value))
return base::nullopt;
return header_value;
}
} // namespace cors
} // namespace network