blob: 6af4f723bebff40d17c1bb8b8f7d66fb2268bf52 [file] [log] [blame]
/*
* Copyright 2017 The Cobalt Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <algorithm>
#include <cstring>
#include <iterator>
#include <string>
#include <vector>
#include "base/basictypes.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "cobalt/loader/cors_preflight.h"
#include "starboard/common/string.h"
namespace cobalt {
namespace loader {
namespace {
// definition of the following headers can be found at:
// https://fetch.spec.whatwg.org/#http-access-control-allow-origin
const char* kOriginheadername = "Origin: ";
const char* kAccessControlRequestMethod = "Access-Control-Request-Method: ";
const char* kAccessControlRequestHeaders = "Access-Control-Request-Headers: ";
const char* kAccessControlAllowOrigin = "Access-Control-Allow-Origin";
const char* kAccessControlAllowMethod = "Access-Control-Allow-Methods";
const char* kAccessControlAllowHeaders = "Access-Control-Allow-Headers";
const char* kAccessControlAllowCredentials = "Access-Control-Allow-Credentials";
const char* kAccessControlMaxAge = "Access-Control-Max-Age";
// https://fetch.spec.whatwg.org/#http-access-control-expose-headers
const char* kAccessControlExposeHeaders = "Access-Control-Expose-Headers";
// The following constants are used to decide if a request or response header is
// safe or not.
const char* kCORSSafelistedRequestHeaders[] = {"accept", "accept-language",
"content-language"};
const char* kAllowedMIMEType[] = {"application/x-www-form-urlencoded",
"multipart/form-data", "text/plain"};
const char* kSafelistedHeadernofail[] = {"dpr", "downlink", "save-data",
"viewport-width", "width"};
const char* kContentType = "content-type";
const char* kAuthorization = "authorization";
const char* kMethodNames[] = {"GET", "POST", "HEAD",
"DELETE", "PUT", "OPTIONS"};
const char* kCORSSafelistedResponseHeaders[] = {
"cache-control", "content-language", "content-type",
"expires", "last-modified", "pragma"};
const char* kForbiddenHeaders[] = {"accept-charset",
"accept-encoding",
"access-control-request-headers",
"access-control-request-method",
"connection",
"content-length",
"cookie",
"cookie2",
"date",
"dnt",
"expect",
"host",
"keep-alive",
"origin",
"referer",
"te",
"trailer",
"transfer-encoding",
"upgrade",
"via"};
// Returns true if input method is a CORS-safelisted method.
bool IsCORSSafelistedMethod(net::URLFetcher::RequestType input_type) {
if (input_type == net::URLFetcher::GET ||
input_type == net::URLFetcher::HEAD ||
input_type == net::URLFetcher::POST) {
return true;
}
return false;
}
#if __clang__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wtautological-compare"
#endif
const char* RequestTypeToMethodName(net::URLFetcher::RequestType request_type) {
if (request_type >= 0 && request_type < arraysize(kMethodNames)) {
return kMethodNames[request_type];
} else {
NOTREACHED();
return "";
}
}
#if __clang__
#pragma clang diagnostic push
#endif
// This constant is an imposed limit on the time an entry can be alive in
// the preflight cache if the provided max-age value is even greater.
// The number is the same as the limit in WebKit.
const int kPreflightCacheMaxAgeLimit = 600;
// This helper function checks if 'input_str' is in 'array' up to 'size'.
bool IsInArray(const char* input_str, const char* array[], size_t size) {
if (!input_str || *input_str == '\0') {
return false;
}
for (size_t i = 0; i < size; ++i) {
if (SbStringCompareNoCase(input_str, array[i]) == 0) {
return true;
}
}
return false;
}
// Returns true if there is a case-insensitive match of 'find_value_name' in
// 'field_values'.
bool HasFieldValue(const std::vector<std::string>& field_values,
const std::string& find_value_name) {
for (size_t i = 0; i < field_values.size(); i++) {
if (field_values[i].empty()) {
continue;
}
if (SbStringCompareNoCase(field_values[i].c_str(),
find_value_name.c_str()) == 0) {
return true;
}
}
return false;
}
} // namespace
CORSPreflight::CORSPreflight(GURL url, net::URLFetcher::RequestType method,
const network::NetworkModule* network_module,
base::Closure success_callback, std::string origin,
base::Closure error_callback,
scoped_refptr<CORSPreflightCache> preflight_cache)
: credentials_mode_is_include_(false),
force_preflight_(false),
url_(url),
method_(method),
network_module_(network_module),
origin_(origin),
error_callback_(error_callback),
success_callback_(success_callback),
preflight_cache_(preflight_cache) {
DCHECK(!url_.is_empty());
DCHECK(preflight_cache);
}
// https://fetch.spec.whatwg.org/#cors-safelisted-request-header
bool CORSPreflight::IsSafeRequestHeader(const std::string& name,
const std::string& value) {
// All comparison are case-insensitive.
// Header is safe if it's CORS-safelisted request-header.
if (IsInArray(name.c_str(), kCORSSafelistedRequestHeaders,
arraysize(kCORSSafelistedRequestHeaders))) {
return true;
}
// Safe if header name is 'Content-Type' and value is a match of
// kAllowedMIMEType.
if (SbStringCompareNoCase(name.c_str(), kContentType) == 0) {
std::vector<std::string> content_type_split = base::SplitString(
value, ";", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
auto begin_iter = content_type_split[0].cbegin();
auto end_iter = content_type_split[0].cend();
net::HttpUtil::TrimLWS(&begin_iter, &end_iter);
std::string content_type_no_space(begin_iter, end_iter);
if (IsInArray(content_type_no_space.c_str(), kAllowedMIMEType,
arraysize(kAllowedMIMEType))) {
return true;
}
}
// Safe if name is a match for kSafelistedHeadernofail and whose value, once
// extracted, is not failure.
if (IsInArray(name.c_str(), kSafelistedHeadernofail,
arraysize(kSafelistedHeadernofail))) {
// TODO: The extracting and verify result is not failure is not done yet.
// https://fetch.spec.whatwg.org/#extract-header-values
return true;
}
return false;
}
// https://fetch.spec.whatwg.org/#cors-safelisted-response-header-name
bool CORSPreflight::IsSafeResponseHeader(
const std::string& name,
const std::vector<std::string>& CORS_exposed_header_name_list,
bool credentials_mode_is_include) {
// Every check in this function is case-insensitive comparison.
// Header is safe if it's CORS-safelisted repsonse-header name.
if (IsInArray(name.c_str(), kCORSSafelistedResponseHeaders,
arraysize(kCORSSafelistedResponseHeaders))) {
return true;
}
// It's not safe if it's a forbidden header name.
if (IsInArray(name.c_str(), kForbiddenHeaders,
arraysize(kForbiddenHeaders))) {
return false;
}
// The following two steps checks if given header name is in CORS-exposed
// header-name list. If Access-Control-Expose-Headers header is '*', all
// header names in reponse should be in CORS-exposed header-name list.
if (CORS_exposed_header_name_list.size() == 1 &&
CORS_exposed_header_name_list.at(0) == "*" &&
!credentials_mode_is_include) {
return true;
}
for (size_t i = 0; i < CORS_exposed_header_name_list.size(); i++) {
if (SbStringCompareNoCase(CORS_exposed_header_name_list.at(i).c_str(),
name.c_str()) == 0) {
return true;
}
}
return false;
}
void CORSPreflight::GetServerAllowedHeaders(
const net::HttpResponseHeaders& response_headers,
std::vector<std::string>* expose_headers) {
size_t iter = 0;
std::string exposable_header;
while (response_headers.EnumerateHeader(&iter, kAccessControlExposeHeaders,
&exposable_header)) {
expose_headers->push_back(exposable_header);
}
}
bool CORSPreflight::IsPreflightNeeded() {
// Send preflight if force_preflight flag is on.
if (force_preflight_) {
return true;
}
// Preflight is not needed if the request method is CORS-safelisted request
// method and all headers are CORS-safelisted request-header.
std::vector<std::string> unsafe_headers;
if (method_ == net::URLFetcher::GET || method_ == net::URLFetcher::HEAD ||
method_ == net::URLFetcher::POST) {
net::HttpRequestHeaders::Iterator it(headers_);
while (it.GetNext()) {
if (!IsSafeRequestHeader(it.name(), it.value())) {
unsafe_headers.push_back(it.name());
}
}
if (unsafe_headers.empty()) {
return false;
}
}
// Check preflight cache for match.
return !preflight_cache_->HaveEntry(url_.spec(), origin_,
credentials_mode_is_include_, method_,
unsafe_headers);
}
bool CORSPreflight::Send() {
if (!IsPreflightNeeded()) {
return false;
}
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
// 1. Let preflight be a new request whose method is 'OPTIONS', url is
// request's current url, initiator is request's initiator, type is
// request's type, destination is request's destination, origin is
// request's origin, referrer is request's referrer, and referrer
// policy is request's referrer policy.
url_fetcher_ = net::URLFetcher::Create(url_, net::URLFetcher::OPTIONS, this);
url_fetcher_->SetRequestContext(
network_module_->url_request_context_getter().get());
url_fetcher_->AddExtraRequestHeader(kOriginheadername + origin_);
// 3. Let headers be the names of request's header list's headers,
// excluding CORS-safelisted request-headers and duplicates, sorted
// lexicographically, and byte-lowercased.
// 4. If headers is not empty, then:
// Let value be the items in headers separated from each other
// by `,`. Set `Access-Control-Request-Headers` to value in
// preflight's header list.
if (!headers_.IsEmpty()) {
net::HttpRequestHeaders::Iterator it(headers_);
std::string headers_string;
while (it.GetNext()) {
if (!headers_string.empty()) {
headers_string += ',';
}
headers_string += it.name();
}
url_fetcher_->AddExtraRequestHeader(kAccessControlRequestHeaders +
headers_string);
}
// 2. Set `Access-Control-Request-Method` to request's method in
// preflight's header list.
url_fetcher_->AddExtraRequestHeader(std::string(kAccessControlRequestMethod) +
RequestTypeToMethodName(method_));
// 5. Let response be the result of performing an HTTP-network-or-
// cache fetch using preflight.
Start();
return true;
}
void CORSPreflight::Start() {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
// Preflight does not allow redirect, status 300+ should not fail
url_fetcher_->SetStopOnRedirect(true);
url_fetcher_->Start();
}
void CORSPreflight::OnURLFetchComplete(const net::URLFetcher* source) {
DCHECK_CALLED_ON_VALID_THREAD(thread_checker_);
if (source->GetStatus().status() != net::URLRequestStatus::SUCCESS) {
error_callback_.Run();
return;
}
// Preflight response check:
// Procedure 6 of https://fetch.spec.whatwg.org/#cors-preflight-fetch-0
if (source->GetResponseHeaders()) {
net::HttpResponseHeaders* response_headers = source->GetResponseHeaders();
std::string methods, headernames;
// If status is not ok status, return network error
if (!CORSCheck(*response_headers, origin_, credentials_mode_is_include_) ||
source->GetResponseCode() < 200 || source->GetResponseCode() > 299) {
error_callback_.Run();
return;
}
// 3. Let headerNames be the result of extracting header list values given
// `Access-Control-Allow-Headers` and response's header list.
if (!response_headers->GetNormalizedHeader(kAccessControlAllowMethod,
&methods)) {
// 6. If methods is null and request's use-CORS-preflight flag is set,
// then set methods to a new list containing request's method.
if (force_preflight_) {
methods = RequestTypeToMethodName(method_);
}
}
response_headers->GetNormalizedHeader(kAccessControlAllowHeaders,
&headernames);
// 5. If methods or headerNames contains `*`, and request's credentials mode
// is "include", then return a network error.
std::vector<std::string> methods_vec = base::SplitString(
methods, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
std::vector<std::string> headernames_vec = base::SplitString(
headernames, ",", base::TRIM_WHITESPACE, base::SPLIT_WANT_ALL);
if ((HasFieldValue(methods_vec, "*") ||
HasFieldValue(headernames_vec, "*")) &&
credentials_mode_is_include_) {
error_callback_.Run();
return;
} // 7. If request's method is not in methods, is not a CORS-safelisted
// method, and methods does not contain `*`, then return a network error.
if (!HasFieldValue(methods_vec, RequestTypeToMethodName(method_)) &&
!IsCORSSafelistedMethod(method_) && !HasFieldValue(methods_vec, "*")) {
error_callback_.Run();
return;
}
// 8. If one of request's header list's names is a CORS non-wildcard
// request-header name and is not a byte-case-insensitive match for an
// item in headerNames, then return a network error.
// 9. If one of request's header list' names is not a byte-case-insensitive
// match for an item in headerNames, its corresponding header is not a
// CORS-safelisted request-header, and headerNames does not contain `*`,
// then return a network error.
net::HttpRequestHeaders::Iterator it(headers_);
while (it.GetNext()) {
if (HasFieldValue(headernames_vec, it.name())) {
continue;
}
if (SbStringCompareNoCase(it.name().c_str(), kAuthorization) == 0 ||
(!HasFieldValue(headernames_vec, "*") &&
!IsSafeRequestHeader(it.name(), it.value()))) {
error_callback_.Run();
return;
}
}
// step 10-18 for adding entry to preflight cache.
std::string max_age_str;
int max_age = 0;
if (response_headers->GetNormalizedHeader(kAccessControlMaxAge,
&max_age_str)) {
max_age = std::min(SbStringAToI(max_age_str.c_str()),
kPreflightCacheMaxAgeLimit);
}
preflight_cache_->AppendEntry(source->GetURL().spec(), origin_, max_age,
credentials_mode_is_include_, methods_vec,
headernames_vec);
} else {
DLOG(ERROR) << "CORS preflight did not get response headers";
error_callback_.Run();
}
success_callback_.Run();
}
// https://fetch.spec.whatwg.org/#concept-cors-check
bool CORSPreflight::CORSCheck(const net::HttpResponseHeaders& response_headers,
const std::string& serialized_origin,
bool credentials_mode_is_include) {
// 1. Let origin be the result of extracting header list values given `Access-
// Control-Allow-Origin` and response's header list.
std::string allowed_origin, empty_container, allow_credentials;
size_t iter = 0;
if (!response_headers.EnumerateHeader(&iter, kAccessControlAllowOrigin,
&allowed_origin)) {
DLOG(WARNING) << "Insecure cross-origin network request returned response "
"with no Access-Control-Allow-Origin header. Request "
"aborted.";
return false;
}
DCHECK(iter);
if (response_headers.EnumerateHeader(&iter, kAccessControlAllowOrigin,
&empty_container)) {
DLOG(WARNING) << "Insecure cross-origin network request returned response "
"with multiple Access-Control-Allow-Origin headers. "
"Behavior disallowed and request aborted";
return false;
}
// 3. If request's credentials mode is not "include" and origin is `*`, return
// success.
if (!credentials_mode_is_include && allowed_origin == "*") {
return true;
}
// 2. If origin is null or failure, return failure.
if (allowed_origin.empty()) {
return false;
}
// 4. If request's origin, serialized and UTF-8 encoded, is not origin, return
// failure.
if (allowed_origin != serialized_origin) {
DLOG(WARNING) << "Network request origin is not allowed by server's "
"Access-Control-Allow-Origin header, request aborted.";
return false;
}
// 5. If request's credentials mode is not "include", return success.
if (!credentials_mode_is_include) {
return true;
}
// 6. Let credentials be the result of extracting header list values given
// `Access-Control-Allow-Credentials` and response's header list.
if (response_headers.GetNormalizedHeader(kAccessControlAllowCredentials,
&allow_credentials)) {
// 7. If credentials is `true`, return success.
if (allow_credentials != "true") {
DLOG(WARNING)
<< "Network request failed because request want to include credential"
"but server disallow it.";
return false;
} else {
return true;
}
}
return false;
}
} // namespace loader
} // namespace cobalt