blob: e5033462d252679999105d14770027edc6a032de [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 "content/browser/web_package/signed_exchange_envelope.h"
#include <utility>
#include "base/callback.h"
#include "base/format_macros.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_piece.h"
#include "base/strings/stringprintf.h"
#include "base/trace_event/trace_event.h"
#include "components/cbor/reader.h"
#include "content/browser/web_package/signed_exchange_consts.h"
#include "content/browser/web_package/signed_exchange_utils.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "url/origin.h"
namespace content {
namespace {
bool IsUncachedHeader(base::StringPiece name) {
DCHECK_EQ(name, base::ToLowerASCII(name));
const char* const kUncachedHeaders[] = {
// "Hop-by-hop header fields listed in the Connection header field
// (Section 6.1 of {{!RFC7230}})." [spec text]
// Note: The Connection header field itself is banned as uncached headers,
// so no-op.
// https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#stateful-headers
// TODO(kouhei): Dedupe the list with net/http/http_response_headers.cc
// kChallengeResponseHeaders and kCookieResponseHeaders.
"authentication-control",
"authentication-info",
"clear-site-data",
"optional-www-authenticate",
"proxy-authenticate",
"proxy-authentication-info",
"public-key-pins",
"sec-websocket-accept",
"set-cookie",
"set-cookie2",
"setprofile",
"strict-transport-security",
"www-authenticate",
// Other uncached header fields:
// https://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#uncached-headers
// TODO(kouhei): Dedupe the list with net/http/http_response_headers.cc
// kHopByHopResponseHeaders.
"connection",
"keep-alive",
"proxy-connection",
"trailer",
"transfer-encoding",
"upgrade",
};
for (const char* field : kUncachedHeaders) {
if (name == field)
return true;
}
return false;
}
// Returns if the response is cacheble by a shared cache, as per Section 3 of
// [RFC7234].
bool IsCacheableBySharedCache(const SignedExchangeEnvelope::HeaderMap& headers,
SignedExchangeDevToolsProxy* devtools_proxy) {
// As we require response code 200 which is cacheable by default, these two
// are trivially true:
// > o the response status code is understood by the cache, and
// > o the response either:
// > ...
// > * has a status code that is defined as cacheable by default (see
// > Section 4.2.2), or
// > ...
//
// Also, SXG version >= b3 do not have request method and headers, so these
// are not applicable:
// > o The request method is understood by the cache and defined as being
// > cacheable, and
// > o the Authorization header field (see Section 4.2 of [RFC7235]) does
// > not appear in the request, if the cache is shared, unless the
// > response explicitly allows it (see Section 3.2), and
//
// Hence, we have to check the two remaining clauses:
// > o the "no-store" cache directive (see Section 5.2) does not appear
// > in request or response header fields, and
// > o the "private" response directive (see Section 5.2.2.6) does not
// > appear in the response, if the cache is shared, and
//
// Note that this implementation does not recognize any cache control
// extensions.
auto found = headers.find("cache-control");
if (found == headers.end())
return true;
net::HttpUtil::NameValuePairsIterator it(
found->second.begin(), found->second.end(), ',',
net::HttpUtil::NameValuePairsIterator::Values::NOT_REQUIRED,
net::HttpUtil::NameValuePairsIterator::Quotes::STRICT_QUOTES);
while (it.GetNext()) {
auto name = it.name();
if (name == "no-store" || name == "private") {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Exchange's response must be cacheable by a shared cache, but "
"has cache-control: %s",
found->second.c_str()));
return false;
}
}
if (!it.valid()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Failed to parse cache-control header of the exchange. "
"cache-control: %s",
found->second.c_str()));
return false;
}
return true;
}
bool ParseResponseMap(const cbor::Value& value,
SignedExchangeEnvelope* out,
SignedExchangeDevToolsProxy* devtools_proxy) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"), "ParseResponseMap");
if (!value.is_map()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Expected response map, got non-map type. Actual type: %d",
static_cast<int>(value.type())));
return false;
}
const cbor::Value::MapValue& response_map = value.GetMap();
auto status_iter = response_map.find(
cbor::Value(kStatusKey, cbor::Value::Type::BYTE_STRING));
if (status_iter == response_map.end() ||
!status_iter->second.is_bytestring()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, ":status is not found or not a bytestring.");
return false;
}
base::StringPiece response_code_str =
status_iter->second.GetBytestringAsString();
int response_code;
if (!base::StringToInt(response_code_str, &response_code)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, "Failed to parse status code to integer.");
return false;
}
// TODO(kouhei): Add spec ref here once
// https://github.com/WICG/webpackage/issues/326 is resolved.
if (response_code != 200) {
signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy,
"Status code is not 200.");
return false;
}
out->set_response_code(static_cast<net::HttpStatusCode>(response_code));
for (const auto& it : response_map) {
if (!it.first.is_bytestring() || !it.second.is_bytestring()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, "Non-bytestring value in the response map.");
return false;
}
base::StringPiece name_str = it.first.GetBytestringAsString();
if (name_str == kStatusKey)
continue;
if (!net::HttpUtil::IsValidHeaderName(name_str)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf("Invalid header name. header_name: %s",
name_str.as_string().c_str()));
return false;
}
// https://tools.ietf.org/html/draft-yasskin-httpbis-origin-signed-exchanges-impl-02
// Section 3.2:
// "For each response header field in `exchange`, the header field's
// lowercase name as a byte string to the header field's value as a byte
// string."
if (name_str != base::ToLowerASCII(name_str)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Response header name should be lower-cased. header_name: %s",
name_str.as_string().c_str()));
return false;
}
// 4. If exchange's headers contains an uncached header field, as defined in
// Section 4.1, return "invalid". [spec text]
if (IsUncachedHeader(name_str)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Exchange contains stateful response header. header_name: %s",
name_str.as_string().c_str()));
return false;
}
base::StringPiece value_str = it.second.GetBytestringAsString();
if (!net::HttpUtil::IsValidHeaderValue(value_str)) {
signed_exchange_utils::ReportErrorAndTraceEvent(devtools_proxy,
"Invalid header value.");
return false;
}
if (!out->AddResponseHeader(name_str, value_str)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf("Duplicate header value. header_name: %s",
name_str.as_string().c_str()));
return false;
}
}
// https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#cross-origin-trust
// Step 5. If Section 3 of [RFC7234] forbids a shared cache from storing
// response,
// return “invalid”. [spec text]
if (!IsCacheableBySharedCache(out->response_headers(), devtools_proxy))
return false;
// https://wicg.github.io/webpackage/loading.html#parsing-a-signed-exchange
// Step 26. If parsedExchange’s response's status is a redirect status or the
// signed exchange version of parsedExchange’s response is not
// undefined, return a failure. [spec text]
if (net::HttpResponseHeaders::IsRedirectResponseCode(out->response_code())) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Exchange's response status must not be a redirect status. "
"status: %d",
response_code));
return false;
}
// Note: This does not reject content-type like "application/signed-exchange"
// (no "v=" parameter). In that case, SignedExchangeRequestHandler does not
// handle the inner response and UA just downloads it.
// See https://github.com/WICG/webpackage/issues/299 for details.
auto found = out->response_headers().find("content-type");
if (found != out->response_headers().end() &&
signed_exchange_utils::GetSignedExchangeVersion(found->second)
.has_value()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf(
"Exchange's inner response must not be a signed-exchange. "
"conetent-type: %s",
found->second.c_str()));
return false;
}
return true;
}
} // namespace
// static
base::Optional<SignedExchangeEnvelope> SignedExchangeEnvelope::Parse(
SignedExchangeVersion version,
const signed_exchange_utils::URLWithRawString& fallback_url,
base::StringPiece signature_header_field,
base::span<const uint8_t> cbor_header,
SignedExchangeDevToolsProxy* devtools_proxy) {
TRACE_EVENT0(TRACE_DISABLED_BY_DEFAULT("loading"),
"SignedExchangeEnvelope::Parse");
const auto& request_url = fallback_url;
cbor::Reader::DecoderError error;
base::Optional<cbor::Value> value = cbor::Reader::Read(cbor_header, &error);
if (!value.has_value()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy,
base::StringPrintf("Failed to decode Value. CBOR error: %s",
cbor::Reader::ErrorCodeToString(error)));
return base::nullopt;
}
SignedExchangeEnvelope ret;
ret.set_cbor_header(cbor_header);
ret.set_request_url(request_url);
if (!ParseResponseMap(*value, &ret, devtools_proxy)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, "Failed to parse response map.");
return base::nullopt;
}
base::Optional<std::vector<SignedExchangeSignatureHeaderField::Signature>>
signatures = SignedExchangeSignatureHeaderField::ParseSignature(
signature_header_field, devtools_proxy);
if (!signatures || signatures->empty()) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, "Failed to parse signature header field.");
return base::nullopt;
}
// TODO(https://crbug.com/850475): Support multiple signatures.
ret.signature_ = (*signatures)[0];
// https://wicg.github.io/webpackage/draft-yasskin-http-origin-signed-responses.html#cross-origin-trust
// If the signature’s “validity-url” parameter is not same-origin with
// exchange’s effective request URI (Section 5.5 of [RFC7230]), return
// “invalid” [spec text]
const GURL validity_url = ret.signature().validity_url.url;
if (!url::IsSameOriginWith(request_url.url, validity_url)) {
signed_exchange_utils::ReportErrorAndTraceEvent(
devtools_proxy, "Validity URL must be same-origin with request URL.");
return base::nullopt;
}
return std::move(ret);
}
SignedExchangeEnvelope::SignedExchangeEnvelope() = default;
SignedExchangeEnvelope::SignedExchangeEnvelope(const SignedExchangeEnvelope&) =
default;
SignedExchangeEnvelope::SignedExchangeEnvelope(SignedExchangeEnvelope&&) =
default;
SignedExchangeEnvelope::~SignedExchangeEnvelope() = default;
SignedExchangeEnvelope& SignedExchangeEnvelope::operator=(
SignedExchangeEnvelope&&) = default;
bool SignedExchangeEnvelope::AddResponseHeader(base::StringPiece name,
base::StringPiece value) {
std::string name_str = name.as_string();
DCHECK_EQ(name_str, base::ToLowerASCII(name))
<< "Response header names should be always lower-cased.";
if (response_headers_.find(name_str) != response_headers_.end())
return false;
response_headers_.emplace(std::move(name_str), value.as_string());
return true;
}
scoped_refptr<net::HttpResponseHeaders>
SignedExchangeEnvelope::BuildHttpResponseHeaders() const {
std::string header_str("HTTP/1.1 ");
header_str.append(base::NumberToString(response_code()));
header_str.append(" ");
header_str.append(net::GetHttpReasonPhrase(response_code()));
header_str.append(" \r\n");
for (const auto& it : response_headers()) {
header_str.append(it.first);
header_str.append(": ");
header_str.append(it.second);
header_str.append("\r\n");
}
header_str.append("\r\n");
return base::MakeRefCounted<net::HttpResponseHeaders>(
net::HttpUtil::AssembleRawHeaders(header_str.c_str(), header_str.size()));
}
void SignedExchangeEnvelope::set_cbor_header(base::span<const uint8_t> data) {
cbor_header_ = std::vector<uint8_t>(data.begin(), data.end());
}
} // namespace content