blob: bb96809adfc7198f1ecd8785bd148ed9f6ca9a8c [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/strings/string_util.h"
#include "base/strings/stringprintf.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 "url/gurl.h"
namespace network {
namespace cors {
namespace {
base::Optional<std::string> GetHeaderString(
const scoped_refptr<net::HttpResponseHeaders>& headers,
const std::string& header_name) {
std::string header_value;
if (!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) {
std::vector<std::string> filtered_headers;
for (const auto& header : headers.GetHeaderVector()) {
// Exclude CORS-safelisted headers.
if (cors::IsCORSSafelistedHeader(header.key, header.value))
continue;
// Exclude the forbidden headers because they may be added by the user
// agent. They must be checked separately and rejected for
// JavaScript-initiated requests.
if (cors::IsForbiddenHeader(header.key))
continue;
filtered_headers.push_back(base::ToLowerASCII(header.key));
}
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 |= 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->headers.SetHeader(
cors::header_names::kAccessControlRequestMethod, request.method);
std::string request_headers =
CreateAccessControlRequestHeadersHeader(request.headers);
if (!request_headers.empty()) {
preflight_request->headers.SetHeader(
cors::header_names::kAccessControlRequestHeaders, request_headers);
}
if (request.is_external_request) {
preflight_request->headers.SetHeader(
cors::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): Remove the following line once the network service is
// enabled by default.
preflight_request->skip_service_worker = true;
// 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);
// TODO(toyoshim): Reflect --allow-file-access-from-files flag.
*detected_error_status = CheckPreflightAccess(
final_url, head.headers->response_code(),
GetHeaderString(head.headers,
cors::header_names::kAccessControlAllowOrigin),
GetHeaderString(head.headers,
cors::header_names::kAccessControlAllowCredentials),
original_request.fetch_credentials_mode,
tainted ? url::Origin() : *original_request.request_initiator,
false /* allow_file_origin */);
if (*detected_error_status)
return nullptr;
base::Optional<mojom::CORSError> error;
error = CheckPreflight(head.headers->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<mojom::CORSError> error =
result->EnsureAllowedCrossOriginMethod(original_request.method);
if (error)
return CORSErrorStatus(*error, original_request.method);
std::string detected_error_header;
error = result->EnsureAllowedCrossOriginHeaders(original_request.headers,
&detected_error_header);
if (error)
return CORSErrorStatus(*error, detected_error_header);
return base::nullopt;
}
// TODO(toyoshim): Remove this class once the Network Service is enabled.
// This wrapper class is used to tell the actual request's |request_id| to call
// CreateLoaderAndStart() for the legacy implementation that requires the ID
// to be unique while SimpleURLLoader always set it to 0.
class WrappedLegacyURLLoaderFactory final : public mojom::URLLoaderFactory {
public:
static WrappedLegacyURLLoaderFactory* GetSharedInstance() {
static WrappedLegacyURLLoaderFactory factory;
return &factory;
}
~WrappedLegacyURLLoaderFactory() override = default;
void SetFactoryAndRequestId(mojom::URLLoaderFactory* factory,
int32_t request_id) {
factory_ = factory;
request_id_ = request_id;
}
void CheckIdle() {
DCHECK(!factory_);
DCHECK_EQ(0, request_id_);
}
// mojom::URLLoaderFactory:
void CreateLoaderAndStart(::network::mojom::URLLoaderRequest loader,
int32_t routing_id,
int32_t request_id,
uint32_t options,
const network::ResourceRequest& request,
::network::mojom::URLLoaderClientPtr client,
const net::MutableNetworkTrafficAnnotationTag&
traffic_annotation) override {
factory_->CreateLoaderAndStart(std::move(loader), routing_id, request_id_,
options, request, std::move(client),
traffic_annotation);
factory_ = nullptr;
request_id_ = 0;
}
void Clone(mojom::URLLoaderFactoryRequest factory) override {
// Should not be called because retry logic is disabled to use
// SimpleURLLoader. Could not work correctly by design.
NOTREACHED();
}
private:
mojom::URLLoaderFactory* factory_ = nullptr;
int32_t request_id_ = 0;
};
} // namespace
class PreflightController::PreflightLoader final {
public:
PreflightLoader(PreflightController* controller,
CompletionCallback completion_callback,
const ResourceRequest& request,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag,
base::OnceCallback<void()> preflight_finalizer)
: controller_(controller),
completion_callback_(std::move(completion_callback)),
original_request_(request),
tainted_(tainted),
preflight_finalizer_(std::move(preflight_finalizer)) {
loader_ = SimpleURLLoader::Create(CreatePreflightRequest(request, tainted),
annotation_tag);
}
void Request(mojom::URLLoaderFactory* loader_factory, int32_t request_id) {
DCHECK(loader_);
loader_->SetOnRedirectCallback(base::BindRepeating(
&PreflightLoader::HandleRedirect, base::Unretained(this)));
loader_->SetOnResponseStartedCallback(base::BindRepeating(
&PreflightLoader::HandleResponseHeader, base::Unretained(this)));
// TODO(toyoshim): Stop using WrappedLegacyURLLoaderFactory once the Network
// Service is enabled by default. This is a workaround to use an allowed
// request_id in the legacy URLLoaderFactory.
WrappedLegacyURLLoaderFactory::GetSharedInstance()->SetFactoryAndRequestId(
loader_factory, request_id);
loader_->DownloadToString(
WrappedLegacyURLLoaderFactory::GetSharedInstance(),
base::BindOnce(&PreflightLoader::HandleResponseBody,
base::Unretained(this)),
0);
WrappedLegacyURLLoaderFactory::GetSharedInstance()->CheckIdle();
}
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));
RemoveFromController();
// |this| is deleted here.
}
void HandleResponseHeader(const GURL& final_url,
const ResourceResponseHead& head) {
FinalizeLoader();
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_);
}
// TODO(toyoshim): Check the spec if we cache |result| regardless of
// following checks.
if (!detected_error_status) {
controller_->AppendToCache(*original_request_.request_initiator,
original_request_.url, std::move(result));
}
std::move(completion_callback_)
.Run(detected_error_status ? net::ERR_FAILED : net::OK,
detected_error_status);
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);
RemoveFromController();
// |this| is deleted here.
}
void FinalizeLoader() {
DCHECK(loader_);
if (preflight_finalizer_)
std::move(preflight_finalizer_).Run();
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_;
// Holds caller's information.
PreflightController::CompletionCallback completion_callback_;
const ResourceRequest original_request_;
const bool tainted_;
// This is needed because we sometimes need to cancel the preflight loader
// synchronously.
// TODO(yhirano): Remove this when the network service is fully enabled.
base::OnceCallback<void()> preflight_finalizer_;
DISALLOW_COPY_AND_ASSIGN(PreflightLoader);
};
// static
std::unique_ptr<ResourceRequest>
PreflightController::CreatePreflightRequestForTesting(
const ResourceRequest& request,
bool tainted) {
return CreatePreflightRequest(request, tainted);
}
// static
PreflightController* PreflightController::GetDefaultController() {
static PreflightController controller;
return &controller;
}
PreflightController::PreflightController() = default;
PreflightController::~PreflightController() = default;
void PreflightController::PerformPreflightCheck(
CompletionCallback callback,
int32_t request_id,
const ResourceRequest& request,
bool tainted,
const net::NetworkTrafficAnnotationTag& annotation_tag,
mojom::URLLoaderFactory* loader_factory,
base::OnceCallback<void()> preflight_finalizer) {
DCHECK(request.request_initiator);
if (!request.is_external_request &&
cache_.CheckIfRequestCanSkipPreflight(
request.request_initiator->Serialize(), request.url,
request.fetch_credentials_mode, request.method, request.headers)) {
std::move(callback).Run(net::OK, base::nullopt);
return;
}
auto emplaced_pair = loaders_.emplace(std::make_unique<PreflightLoader>(
this, std::move(callback), request, tainted, annotation_tag,
std::move(preflight_finalizer)));
(*emplaced_pair.first)->Request(loader_factory, request_id);
}
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