blob: 756df9de8932bf9e648128fc6beb5c3b72d4d1f3 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "services/network/cors/preflight_controller.h"
#include <algorithm>
#include <vector>
#include "base/bind.h"
#include "base/no_destructor.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "services/network/public/cpp/cors/cors.h"
#include "services/network/public/cpp/cors/cors_error_status.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "services/network/public/mojom/url_loader.mojom.h"
#include "url/gurl.h"
namespace network {
namespace cors {
namespace {
int RetrieveCacheFlags(int load_flags) {
return load_flags & (net::LOAD_VALIDATE_CACHE | net::LOAD_BYPASS_CACHE |
net::LOAD_DISABLE_CACHE);
}
base::Optional<std::string> GetHeaderString(
const scoped_refptr<net::HttpResponseHeaders>& headers,
const std::string& header_name) {
std::string header_value;
if (!headers || !headers->GetNormalizedHeader(header_name, &header_value))
return base::nullopt;
return header_value;
}
// Algorithm step 3 of the CORS-preflight fetch,
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0, that requires
// - CORS-safelisted request-headers excluded
// - duplicates excluded
// - sorted lexicographically
// - byte-lowercased
std::string CreateAccessControlRequestHeadersHeader(
const net::HttpRequestHeaders& headers,
bool is_revalidating) {
// Exclude the forbidden headers because they may be added by the user
// agent. They must be checked separately and rejected for
// JavaScript-initiated requests.
std::vector<std::string> filtered_headers =
CorsUnsafeNotForbiddenRequestHeaderNames(headers.GetHeaderVector(),
is_revalidating);
if (filtered_headers.empty())
return std::string();
// Sort header names lexicographically.
std::sort(filtered_headers.begin(), filtered_headers.end());
return base::JoinString(filtered_headers, ",");
}
std::unique_ptr<ResourceRequest> CreatePreflightRequest(
const ResourceRequest& request,
bool tainted) {
DCHECK(!request.url.has_username());
DCHECK(!request.url.has_password());
std::unique_ptr<ResourceRequest> preflight_request =
std::make_unique<ResourceRequest>();
// Algorithm step 1 through 4 of the CORS-preflight fetch,
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0.
preflight_request->url = request.url;
preflight_request->method = "OPTIONS";
preflight_request->priority = request.priority;
preflight_request->fetch_request_context_type =
request.fetch_request_context_type;
preflight_request->referrer = request.referrer;
preflight_request->referrer_policy = request.referrer_policy;
preflight_request->fetch_credentials_mode =
mojom::FetchCredentialsMode::kOmit;
preflight_request->load_flags = RetrieveCacheFlags(request.load_flags);
preflight_request->load_flags |= net::LOAD_DO_NOT_SAVE_COOKIES;
preflight_request->load_flags |= net::LOAD_DO_NOT_SEND_COOKIES;
preflight_request->load_flags |= net::LOAD_DO_NOT_SEND_AUTH_DATA;
preflight_request->fetch_window_id = request.fetch_window_id;
preflight_request->render_frame_id = request.render_frame_id;
preflight_request->headers.SetHeader(
header_names::kAccessControlRequestMethod, request.method);
std::string request_headers = CreateAccessControlRequestHeadersHeader(
request.headers, request.is_revalidating);
if (!request_headers.empty()) {
preflight_request->headers.SetHeader(
header_names::kAccessControlRequestHeaders, request_headers);
}
if (request.is_external_request) {
preflight_request->headers.SetHeader(
header_names::kAccessControlRequestExternal, "true");
}
DCHECK(request.request_initiator);
preflight_request->request_initiator = request.request_initiator;
preflight_request->headers.SetHeader(
net::HttpRequestHeaders::kOrigin,
(tainted ? url::Origin() : *request.request_initiator).Serialize());
// TODO(toyoshim): Should not matter, but at this moment, it hits a sanity
// check in ResourceDispatcherHostImpl if |resource_type| isn't set.
preflight_request->resource_type = request.resource_type;
return preflight_request;
}
std::unique_ptr<PreflightResult> CreatePreflightResult(
const GURL& final_url,
const ResourceResponseHead& head,
const ResourceRequest& original_request,
bool tainted,
base::Optional<CorsErrorStatus>* detected_error_status) {
DCHECK(detected_error_status);
const int response_code = head.headers ? head.headers->response_code() : 0;
*detected_error_status = CheckPreflightAccess(
final_url, response_code,
GetHeaderString(head.headers, header_names::kAccessControlAllowOrigin),
GetHeaderString(head.headers,
header_names::kAccessControlAllowCredentials),
original_request.fetch_credentials_mode,
tainted ? url::Origin() : *original_request.request_initiator);
if (*detected_error_status)
return nullptr;
base::Optional<mojom::CorsError> error;
error = CheckPreflight(response_code);
if (error) {
*detected_error_status = CorsErrorStatus(*error);
return nullptr;
}
if (original_request.is_external_request) {
*detected_error_status = CheckExternalPreflight(GetHeaderString(
head.headers, header_names::kAccessControlAllowExternal));
if (*detected_error_status)
return nullptr;
}
auto result = PreflightResult::Create(
original_request.fetch_credentials_mode,
GetHeaderString(head.headers, header_names::kAccessControlAllowMethods),
GetHeaderString(head.headers, header_names::kAccessControlAllowHeaders),
GetHeaderString(head.headers, header_names::kAccessControlMaxAge),
&error);
if (error)
*detected_error_status = CorsErrorStatus(*error);
return result;
}
base::Optional<CorsErrorStatus> CheckPreflightResult(
PreflightResult* result,
const ResourceRequest& original_request) {
base::Optional<CorsErrorStatus> status =
result->EnsureAllowedCrossOriginMethod(original_request.method);
if (status)
return status;
return result->EnsureAllowedCrossOriginHeaders(
original_request.headers, original_request.is_revalidating);
}
} // namespace
class PreflightController::PreflightLoader final {
public:
PreflightLoader(PreflightController* controller,
CompletionCallback completion_callback,
const ResourceRequest& request,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag)
: controller_(controller),
completion_callback_(std::move(completion_callback)),
original_request_(request),
tainted_(tainted) {
loader_ = SimpleURLLoader::Create(CreatePreflightRequest(request, tainted),
annotation_tag);
}
void Request(mojom::URLLoaderFactory* loader_factory) {
DCHECK(loader_);
loader_->SetOnRedirectCallback(base::BindRepeating(
&PreflightLoader::HandleRedirect, base::Unretained(this)));
loader_->SetOnResponseStartedCallback(base::BindRepeating(
&PreflightLoader::HandleResponseHeader, base::Unretained(this)));
loader_->DownloadToString(
loader_factory,
base::BindOnce(&PreflightLoader::HandleResponseBody,
base::Unretained(this)),
0);
}
private:
void HandleRedirect(const net::RedirectInfo& redirect_info,
const network::ResourceResponseHead& response_head,
std::vector<std::string>* to_be_removed_headers) {
// Preflight should not allow any redirect.
FinalizeLoader();
std::move(completion_callback_)
.Run(net::ERR_FAILED,
CorsErrorStatus(mojom::CorsError::kPreflightDisallowedRedirect),
base::nullopt);
RemoveFromController();
// |this| is deleted here.
}
void HandleResponseHeader(const GURL& final_url,
const ResourceResponseHead& head) {
FinalizeLoader();
timing_info_.start_time = head.request_start;
timing_info_.response_end = base::TimeTicks::Now();
timing_info_.alpn_negotiated_protocol = head.alpn_negotiated_protocol;
timing_info_.connection_info = head.connection_info;
auto timing_allow_origin =
GetHeaderString(head.headers, "Timing-Allow-Origin");
if (timing_allow_origin)
timing_info_.timing_allow_origin = *timing_allow_origin;
timing_info_.transfer_size = head.encoded_data_length;
base::Optional<CorsErrorStatus> detected_error_status;
std::unique_ptr<PreflightResult> result = CreatePreflightResult(
final_url, head, original_request_, tainted_, &detected_error_status);
if (result) {
// Preflight succeeded. Check |original_request_| with |result|.
DCHECK(!detected_error_status);
detected_error_status =
CheckPreflightResult(result.get(), original_request_);
}
if (!(original_request_.load_flags & net::LOAD_DISABLE_CACHE) &&
!detected_error_status) {
controller_->AppendToCache(*original_request_.request_initiator,
original_request_.url, std::move(result));
}
base::Optional<PreflightTimingInfo> timing_info;
if (!detected_error_status)
timing_info = std::move(timing_info_);
std::move(completion_callback_)
.Run(detected_error_status ? net::ERR_FAILED : net::OK,
detected_error_status, std::move(timing_info));
RemoveFromController();
// |this| is deleted here.
}
void HandleResponseBody(std::unique_ptr<std::string> response_body) {
// Reached only when the request fails without receiving headers, e.g.
// unknown hosts, unreachable remote, reset by peer, and so on.
// See https://crbug.com/826868 for related discussion.
DCHECK(!response_body);
const int error = loader_->NetError();
DCHECK_NE(error, net::OK);
FinalizeLoader();
std::move(completion_callback_).Run(error, base::nullopt, base::nullopt);
RemoveFromController();
// |this| is deleted here.
}
void FinalizeLoader() {
DCHECK(loader_);
loader_.reset();
}
// Removes |this| instance from |controller_|. Once the method returns, |this|
// is already removed.
void RemoveFromController() { controller_->RemoveLoader(this); }
// PreflightController owns all PreflightLoader instances, and should outlive.
PreflightController* const controller_;
// Holds SimpleURLLoader instance for the CORS-preflight request.
std::unique_ptr<SimpleURLLoader> loader_;
PreflightTimingInfo timing_info_;
// Holds caller's information.
PreflightController::CompletionCallback completion_callback_;
const ResourceRequest original_request_;
const bool tainted_;
DISALLOW_COPY_AND_ASSIGN(PreflightLoader);
};
// static
std::unique_ptr<ResourceRequest>
PreflightController::CreatePreflightRequestForTesting(
const ResourceRequest& request,
bool tainted) {
return CreatePreflightRequest(request, tainted);
}
// static
std::unique_ptr<PreflightResult>
PreflightController::CreatePreflightResultForTesting(
const GURL& final_url,
const ResourceResponseHead& head,
const ResourceRequest& original_request,
bool tainted,
base::Optional<CorsErrorStatus>* detected_error_status) {
return CreatePreflightResult(final_url, head, original_request, tainted,
detected_error_status);
}
PreflightController::PreflightController() = default;
PreflightController::~PreflightController() = default;
void PreflightController::PerformPreflightCheck(
CompletionCallback callback,
const ResourceRequest& request,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag,
mojom::URLLoaderFactory* loader_factory) {
DCHECK(request.request_initiator);
if (!RetrieveCacheFlags(request.load_flags) && !request.is_external_request &&
cache_.CheckIfRequestCanSkipPreflight(
request.request_initiator->Serialize(), request.url,
request.fetch_credentials_mode, request.method, request.headers,
request.is_revalidating)) {
std::move(callback).Run(net::OK, base::nullopt, base::nullopt);
return;
}
auto emplaced_pair = loaders_.emplace(std::make_unique<PreflightLoader>(
this, std::move(callback), request, tainted, annotation_tag));
(*emplaced_pair.first)->Request(loader_factory);
}
void PreflightController::RemoveLoader(PreflightLoader* loader) {
auto it = loaders_.find(loader);
DCHECK(it != loaders_.end());
loaders_.erase(it);
}
void PreflightController::AppendToCache(
const url::Origin& origin,
const GURL& url,
std::unique_ptr<PreflightResult> result) {
cache_.AppendEntry(origin.Serialize(), url, std::move(result));
}
} // namespace cors
} // namespace network