// Copyright 2018 The Chromium Authors
// 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 <string_view>
#include <utility>

#include "base/containers/fixed_flat_set.h"
#include "base/format_macros.h"
#include "base/functional/callback.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.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 "crypto/hash.h"
#include "net/http/http_response_headers.h"
#include "net/http/http_util.h"
#include "url/origin.h"

namespace content {

namespace {

bool IsUncachedHeader(std::string_view name) {
  DCHECK_EQ(name, base::ToLowerASCII(name));

  constexpr auto kUncachedHeaders = base::MakeFixedFlatSet<std::string_view>({
      // "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",
  });

  return kUncachedHeaders.contains(name);
}

// 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, /*delimiter=*/',',
      net::HttpUtil::NameValuePairsIterator::Values::NOT_REQUIRED,
      net::HttpUtil::NameValuePairsIterator::Quotes::STRICT_QUOTES);
  while (it.GetNext()) {
    std::string_view 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));
      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));
    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;
  }
  std::string_view 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;
    }
    std::string_view 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));
      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));
      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));
      return false;
    }

    std::string_view 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));
      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;
  }

  // https://wicg.github.io/webpackage/loading.html#parsing-b2-cbor-headers
  // 7. If responseHeaders does not contain `Content-Type`, return a failure.
  // [spec text]
  // Note: "Parsing b3 CBOR headers" algorithm should have the same step.
  // See https://github.com/WICG/webpackage/issues/555
  auto content_type_iter = out->response_headers().find("content-type");
  if (content_type_iter == out->response_headers().end()) {
    signed_exchange_utils::ReportErrorAndTraceEvent(
        devtools_proxy,
        "Exchange's inner response must have Content-Type header.");
    return false;
  }
  // https://wicg.github.io/webpackage/loading.html#parsing-b2-cbor-headers
  // 8. Set `X-Content-Type-Options`/`nosniff` in responseHeaders. [spec text]
  // Note: "Parsing b3 CBOR headers" algorithm should have the same step.
  // See https://github.com/WICG/webpackage/issues/555
  out->SetResponseHeader("x-content-type-options", "nosniff");

  // 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.
  if (signed_exchange_utils::GetSignedExchangeVersion(content_type_iter->second)
          .has_value()) {
    signed_exchange_utils::ReportErrorAndTraceEvent(
        devtools_proxy,
        base::StringPrintf(
            "Exchange's inner response must not be a signed-exchange. "
            "conetent-type: %s",
            content_type_iter->second));
    return false;
  }

  return true;
}

}  // namespace

// static
std::optional<SignedExchangeEnvelope> SignedExchangeEnvelope::Parse(
    SignedExchangeVersion version,
    const signed_exchange_utils::URLWithRawString& fallback_url,
    std::string_view 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;
  std::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 std::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 std::nullopt;
  }

  std::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 std::nullopt;
  }

  // TODO(crbug.com/40579739): 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 std::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(std::string_view name,
                                               std::string_view value) {
  std::string name_str(name);
  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), std::string(value));
  return true;
}

void SignedExchangeEnvelope::SetResponseHeader(std::string_view name,
                                               std::string_view value) {
  std::string name_str(name);
  DCHECK_EQ(name_str, base::ToLowerASCII(name))
      << "Response header names should be always lower-cased.";
  response_headers_[name_str] = std::string(value);
}

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));
}

void SignedExchangeEnvelope::set_cbor_header(base::span<const uint8_t> data) {
  cbor_header_ = std::vector<uint8_t>(data.begin(), data.end());
}

net::SHA256HashValue SignedExchangeEnvelope::ComputeHeaderIntegrity() const {
  return crypto::hash::Sha256(cbor_header());
}

}  // namespace content
