// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifndef NET_COOKIES_CANONICAL_COOKIE_H_
#define NET_COOKIES_CANONICAL_COOKIE_H_

#include <compare>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>

#include "base/types/pass_key.h"
#include "crypto/process_bound_string.h"
#include "net/base/net_export.h"
#include "net/cookies/cookie_access_result.h"
#include "net/cookies/cookie_base.h"
#include "net/cookies/cookie_constants.h"
#include "net/cookies/cookie_options.h"
#include "net/cookies/ref_unique_cookie_key.h"
#include "net/cookies/unique_cookie_key.h"
#include "url/third_party/mozilla/url_parse.h"

class GURL;

namespace base {
class Time;
}  // namespace base

namespace net {

class ParsedCookie;
class CanonicalCookie;
class CookieInclusionStatus;
class CookiePartitionKey;

struct CookieWithAccessResult;
struct CookieAndLineWithAccessResult;

using CookieList = std::vector<CanonicalCookie>;
using CookieAndLineAccessResultList =
    std::vector<CookieAndLineWithAccessResult>;
using CookieAccessResultList = std::vector<CookieWithAccessResult>;

// Represents the call sites of CanonicalCookie::FromStorage, used for metrics
// so we can better identify any callers that are not enforcing cookie name and
// value size limits as expected.
// LINT.IfChange(CanonicalCookieFromStorageCallSite)
enum class CanonicalCookieFromStorageCallSite {
  kAndroidCookiesFetcherRestoreUtil,
  kChromeOsCookieSyncConversions,
  kOauthMultiloginResult,
  kIosSystemCookieUtil,
  kSqlitePersistentCookieStore,
  kCookieManager,
  kCookieManagerMojomTraits,
  kRestrictedCookieManager,
  kTests,
};
// LINT.ThenChange(/tools/metrics/histograms/metadata/cookie/histograms.xml:CanonicalCookieFromStorageCallSite)

// Represents a real/concrete cookie, which may be sent on requests or set by a
// response if the request context and attributes allow it.
class NET_EXPORT CanonicalCookie : public CookieBase {
 public:
  // Various reasons why `IsCanonical` and `IsCanonicalForFromStorage` can fail.
  enum class CanonicalizationFailure {
    kInvalidExpiryDate,
    kUnparseableName,
    kUnparseableValue,
    kInvalidName,
    kInvalidValue,
    kInconsistentCreationAndLastAccessDate,
    kNonAsciiCharactersDisallowed,
    kInvalidDomain,
    kInvalidPath,
    kInvalidHostPrefix,
    kInvalidSecurePrefix,
    kEmptyNameWithHiddenPrefix,
    kPartitionedInsecure,
  };

  // Carries metadata related to the canonicalization results for a given
  // cookie.
  class CanonicalizationResult {
   public:
    CanonicalizationResult() = delete;

    CanonicalizationResult(base::PassKey<CanonicalCookie>,
                           std::optional<CanonicalizationFailure> failure);

    friend bool operator==(const CanonicalizationResult&,
                           const CanonicalizationResult&) = default;

    bool operator==(CanonicalizationFailure failure) const {
      return failure_ == failure;
    }

    explicit operator bool() const { return !failure_.has_value(); }

    NET_EXPORT friend std::ostream& operator<<(
        std::ostream& os,
        const CanonicalizationResult& result);

   private:
    std::optional<CanonicalizationFailure> failure_;
  };

  CanonicalCookie();
  CanonicalCookie(const CanonicalCookie& other);
  CanonicalCookie(CanonicalCookie&& other);
  CanonicalCookie& operator=(const CanonicalCookie& other);
  CanonicalCookie& operator=(CanonicalCookie&& other);
  ~CanonicalCookie() override;

  // This constructor does not validate or canonicalize their inputs;
  // the resulting CanonicalCookies should not be relied on to be canonical
  // unless the caller has done appropriate validation and canonicalization
  // themselves.
  //
  // NOTE: Prefer using Create, CreateSanitizedCookie, or FromStorage (depending
  // on the use case) over directly using this constructor.
  //
  // NOTE: Do not add any defaults to this constructor, we want every caller to
  // understand and choose their inputs.
  CanonicalCookie(base::PassKey<CanonicalCookie>,
                  std::string name,
                  std::string value,
                  std::string domain,
                  std::string path,
                  base::Time creation,
                  base::Time expiration,
                  base::Time last_access,
                  base::Time last_update,
                  bool secure,
                  bool httponly,
                  CookieSameSite same_site,
                  CookiePriority priority,
                  std::optional<CookiePartitionKey> partition_key,
                  CookieSourceScheme scheme_secure,
                  int source_port,
                  CookieSourceType source_type);

  // Creates a new `CanonicalCookie` from the `cookie_line` and the
  // `creation_time`.  Canonicalizes inputs.  May return nullptr if
  // an attribute value is invalid.  `url` must be valid.  `creation_time` may
  // not be null. Sets optional `status` to the relevant CookieInclusionStatus
  // if provided.  `server_time` indicates what the server sending us the Cookie
  // thought the current time was when the cookie was produced.  This is used to
  // adjust for clock skew between server and host.
  //
  // SameSite and HttpOnly related parameters are not checked here,
  // so creation of CanonicalCookies with e.g. SameSite=Strict from a cross-site
  // context is allowed. Create() also does not check whether `url` has a secure
  // scheme if attempting to create a Secure cookie. The Secure, SameSite, and
  // HttpOnly related parameters should be checked when setting the cookie in
  // the CookieStore.
  //
  // The partition_key argument only needs to be present if the cookie line
  // contains the Partitioned attribute. If the cookie line will never contain
  // that attribute, you should use std::nullopt to indicate you intend to
  // always create an unpartitioned cookie. If partition_key has a value but the
  // cookie line does not contain the Partitioned attribute, the resulting
  // cookie will be unpartitioned. If the partition_key is null, then the cookie
  // will be unpartitioned even when the cookie line has the Partitioned
  // attribute.
  //
  // If a cookie is returned, `cookie->IsCanonical()` will be true.
  //
  // NOTE: Do not add any defaults to this constructor, we want every caller to
  // understand and choose their inputs.
  static std::unique_ptr<CanonicalCookie> Create(
      const GURL& url,
      std::string_view cookie_line,
      base::Time creation_time,
      std::optional<base::Time> server_time,
      std::optional<CookiePartitionKey> cookie_partition_key,
      CookieSourceType source_type,
      CookieInclusionStatus* status
#if BUILDFLAG(ARKWEB_COOKIE)
      , bool block_truncated = true);
#else // BUILDFLAG(ARKWEB_COOKIE)
      );
#endif // BUILDFLAG(ARKWEB_COOKIE)

  // Create a canonical cookie based on sanitizing the passed inputs in the
  // context of the passed URL.  Returns a null unique pointer if the inputs
  // cannot be sanitized.  If `status` is provided it will have any relevant
  // CookieInclusionStatus rejection reasons set. If a cookie is created,
  // `cookie->IsCanonical()` will be true.
  //
  // NOTE: Do not add any defaults to this constructor, we want every caller to
  // understand and choose their inputs.
  static std::unique_ptr<CanonicalCookie> CreateSanitizedCookie(
      const GURL& url,
      const std::string& name,
      const std::string& value,
      const std::string& domain,
      const std::string& path,
      base::Time creation_time,
      base::Time expiration_time,
      base::Time last_access_time,
      bool secure,
      bool http_only,
      CookieSameSite same_site,
      CookiePriority priority,
      std::optional<CookiePartitionKey> partition_key,
      CookieInclusionStatus* status);

  // FromStorage is a factory method which is meant for creating a new
  // CanonicalCookie using properties of a previously existing cookie
  // that was already ingested into the cookie store.
  // This should NOT be used to create a new CanonicalCookie that was not
  // already in the store.
  // Returns nullptr if the resulting cookie is not canonical,
  // i.e. cc->IsCanonical() returns false.
  //
  // NOTE: Do not add any defaults to this constructor, we want every caller to
  // understand and choose their inputs.
  static std::unique_ptr<CanonicalCookie> FromStorage(
      std::string name,
      std::string value,
      std::string domain,
      std::string path,
      base::Time creation,
      base::Time expiration,
      base::Time last_access,
      base::Time last_update,
      bool secure,
      bool httponly,
      CookieSameSite same_site,
      CookiePriority priority,
      std::optional<CookiePartitionKey> partition_key,
      CookieSourceScheme source_scheme,
      int source_port,
      CookieSourceType source_type,
      CanonicalCookieFromStorageCallSite call_site);

  // Create a CanonicalCookie that is not guaranteed to actually be Canonical
  // for tests. Use this only if you want to bypass parameter validation to
  // create a cookie that otherwise shouldn't be possible to store.
  static std::unique_ptr<CanonicalCookie> CreateUnsafeCookieForTesting(
      const std::string& name,
      const std::string& value,
      const std::string& domain,
      const std::string& path,
      base::Time creation,
      base::Time expiration,
      base::Time last_access,
      base::Time last_update,
      bool secure,
      bool httponly,
      CookieSameSite same_site,
      CookiePriority priority,
      std::optional<CookiePartitionKey> partition_key = std::nullopt,
      CookieSourceScheme scheme_secure = CookieSourceScheme::kUnset,
      int source_port = url::PORT_UNSPECIFIED,
      CookieSourceType source_type = CookieSourceType::kUnknown);

  // Like Create but with some more friendly defaults for use in tests.
  static std::unique_ptr<CanonicalCookie> CreateForTesting(
      const GURL& url,
      const std::string& cookie_line,
      base::Time creation_time,
      std::optional<base::Time> server_time = std::nullopt,
      std::optional<CookiePartitionKey> cookie_partition_key = std::nullopt,
      CookieSourceType source_type = CookieSourceType::kUnknown,
      CookieInclusionStatus* status = nullptr);

  friend auto operator<=>(const CanonicalCookie& left,
                          const CanonicalCookie& right) {
    // Use the cookie properties that uniquely identify a cookie to determine
    // ordering.
    return left.RefUniqueKey() <=> right.RefUniqueKey();
  }

  friend bool operator==(const CanonicalCookie& left,
                         const CanonicalCookie& right) {
    return left.RefUniqueKey() == right.RefUniqueKey();
  }

  // See CookieBase for other accessors.
  std::string Value() const;
  base::Time ExpiryDate() const { return expiry_date_; }
  base::Time LastAccessDate() const { return last_access_date_; }
  base::Time LastUpdateDate() const { return last_update_date_; }
  bool IsPersistent() const { return !expiry_date_.is_null(); }
  CookiePriority Priority() const { return priority_; }
  CookieSourceType SourceType() const { return source_type_; }

  bool IsExpired(base::Time current) const {
    return !expiry_date_.is_null() && current >= expiry_date_;
  }

  // Are the cookies considered equivalent in the eyes of RFC 2965.
  // The RFC says that name must match (case-sensitive), domain must
  // match (case insensitive), and path must match (case sensitive).
  // For the case insensitive domain compare, we rely on the domain
  // having been canonicalized (in
  // GetCookieDomainWithString->CanonicalizeHost).
  // If partitioned cookies are enabled, then we check the cookies have the same
  // partition key in addition to the checks in RFC 2965.
  //
  // To support origin-bound cookies the check will also include the source
  // scheme and/or port depending on the state of the associated feature.
  // Additionally, domain cookies get a slightly different check which does not
  // include the source port.
  bool IsEquivalent(const CanonicalCookie& ecc) const {
    // It seems like it would make sense to take secure, httponly, and samesite
    // into account, but the RFC doesn't specify this.
    return RefUniqueKey() == ecc.RefUniqueKey();
  }

  // Checks a looser set of equivalency rules than 'IsEquivalent()' in order
  // to support the stricter 'Secure' behaviors specified in Step 12 of
  // https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-05#section-5.4
  // which originated from the proposal in
  // https://tools.ietf.org/html/draft-ietf-httpbis-cookie-alone#section-3
  //
  // Returns 'true' if this cookie's name matches |secure_cookie|, and this
  // cookie is a domain-match for |secure_cookie| (or vice versa), and
  // |secure_cookie|'s path is "on" this cookie's path (as per
  // 'IsOnPath()'). If partitioned cookies are enabled, it also checks that
  // the cookie has the same partition key as |secure_cookie|.
  //
  // Note that while the domain-match cuts both ways (e.g. 'example.com'
  // matches 'www.example.com' in either direction), the path-match is
  // unidirectional (e.g. '/login/en' matches '/login' and '/', but
  // '/login' and '/' do not match '/login/en').
  //
  // Conceptually:
  // If new_cookie.IsEquivalentForSecureCookieMatching(secure_cookie) is
  // true, this means that new_cookie would "shadow" secure_cookie: they
  // would would be indistinguishable when serialized into a Cookie header.
  // This is important because, if an attacker is attempting to set
  // new_cookie, it should not be allowed to mislead the server into using
  // new_cookie's value instead of secure_cookie's.
  //
  // The reason for the asymmetric path comparison ("cookie1=bad; path=/a/b"
  // from an insecure source is not allowed if "cookie1=good; secure;
  // path=/a" exists, but "cookie2=bad; path=/a" from an insecure source is
  // allowed if "cookie2=good; secure; path=/a/b" exists) is because cookies
  // in the Cookie header are serialized with longer path first. (See
  // CookieSorter in cookie_monster.cc.) That is, they would be serialized
  // as "Cookie: cookie1=bad; cookie1=good" in one case, and "Cookie:
  // cookie2=good; cookie2=bad" in the other case. The first scenario is not
  // allowed because the attacker injects the bad value, whereas the second
  // scenario is ok because the good value is still listed first.
  bool IsEquivalentForSecureCookieMatching(
      const CanonicalCookie& secure_cookie) const;

  // Returns true if the |other| cookie's data members match, except for the
  // value. The heuristic is that, if the value changes, then `LastUpdateDate`
  // is likely different.
  bool IsProbablyEquivalentTo(const CanonicalCookie& other) const;

  // Returns true if the |other| cookie's data members (instance variables)
  // match, for comparing cookies in collections.
  bool HasEquivalentDataMembers(const CanonicalCookie& other) const;

  // Checks if a another cookie is equivalent to this one with respect to what
  // information about cookies is revealed to the web platform.
  //
  // If true, it implies that the two cookies would appear identical to cookie
  // change subscribers such as the CookieStore API or service workers.
  bool IsWebEquivalentTo(const CanonicalCookie& other) const;

  void SetLastAccessDate(base::Time date) { last_access_date_ = date; }

  std::string DebugString() const;

  // Returns a "null" time if expiration was unspecified or invalid.
  static base::Time ParseExpiration(const ParsedCookie& pc,
                                    base::Time current,
                                    base::Time server_time);

  // Per rfc6265bis the maximum expiry date is no further than 400 days in the
  // future.
  static base::Time ValidateAndAdjustExpiryDate(base::Time expiry_date,
                                                base::Time creation_date,
                                                net::CookieSourceScheme scheme);

  // Return whether this object is a valid CanonicalCookie(). If the object is
  // invalid, return the first reason found why not. Invalid cookies may be
  // constructed by the detailed constructor.
  // A cookie is considered canonical if-and-only-if:
  // * It can be created by CanonicalCookie::Create, or
  // * It is identical to a cookie created by CanonicalCookie::Create except
  //   that the creation time is null, or
  // * It can be derived from a cookie created by CanonicalCookie::Create by
  //   entry into and retrieval from a cookie store (specifically, this means
  //   by the setting of an creation time in place of a null creation time, and
  //   the setting of a last access time).
  // An additional requirement on a CanonicalCookie is that if the last
  // access time is non-null, the creation time must also be non-null and
  // greater than the last access time.
  CanonicalizationResult IsCanonical() const;

  // Return whether this object is a valid CanonicalCookie() when retrieving the
  // cookie from the persistent store; and if the CanonicalCookie is invalid,
  // return the first reason why not. Cookie that exist in the persistent store
  // may have been created before more recent changes to the definition of
  // "canonical". To ease the transition to the new definitions, and to prevent
  // users from having their cookies deleted, this function supports the older
  // definition of canonical. This function is intended to be temporary because
  // as the number of older cookies (which are non-compliant with the newer
  // definition of canonical) decay toward zero it can eventually be replaced by
  // `IsCanonical()` to enforce the newer definition of canonical.
  //
  // A cookie is considered canonical by this function if-and-only-if:
  // * It is considered canonical by IsCanonical()
  // * TODO(crbug.com/40787717): Add exceptions once IsCanonical() starts
  // enforcing them.
  CanonicalizationResult IsCanonicalForFromStorage() const;

  // Returns whether the effective SameSite mode is SameSite=None (i.e. no
  // SameSite restrictions).
  bool IsEffectivelySameSiteNone(CookieAccessSemantics access_semantics =
                                     CookieAccessSemantics::UNKNOWN) const;

  CookieEffectiveSameSite GetEffectiveSameSiteForTesting(
      CookieAccessSemantics access_semantics =
          CookieAccessSemantics::UNKNOWN) const;

  // Returns the cookie line (e.g. "cookie1=value1; cookie2=value2") represented
  // by |cookies|. The string is built in the same order as the given list.
  static std::string BuildCookieLine(const CookieList& cookies);

  // Same as above but takes a CookieAccessResultList
  // (ignores the access result).
  static std::string BuildCookieLine(const CookieAccessResultList& cookies);

  // Takes a single CanonicalCookie and returns a cookie line containing the
  // attributes of |cookie| formatted like a http set cookie header.
  // (e.g. "cookie1=value1; domain=abc.com; path=/; secure").
  static std::string BuildCookieAttributesLine(const CanonicalCookie& cookie);

 private:
  FRIEND_TEST_ALL_PREFIXES(CanonicalCookieTest,
                           TestGetAndAdjustPortForTrustworthyUrls);
  FRIEND_TEST_ALL_PREFIXES(CanonicalCookieTest, TestHasHiddenPrefixName);

  // Returns the appropriate port value for the given `source_url` depending on
  // if the url is considered trustworthy or not.
  //
  // This function normally returns source_url.EffectiveIntPort(), but it can
  // return a different port value if:
  // * `source_url`'s scheme isn't cryptographically secure
  // * `url_is_trustworthy` is true
  // * `source_url`'s port is the default port for the scheme i.e.: 80
  // If all these conditions are true then the returned value will be 443 to
  // indicate that we're treating `source_url` as if it was secure.
  static int GetAndAdjustPortForTrustworthyUrls(const GURL& source_url,
                                                bool url_is_trustworthy);

  // Checks for values that could be misinterpreted as a cookie name prefix.
  static bool HasHiddenPrefixName(std::string_view cookie_value);

  // Helpers for use in canonicalization checks.
  static CanonicalizationResult Pass();
  static CanonicalizationResult Fail(CanonicalizationFailure failure);

  // CookieBase:
  base::TimeDelta GetLaxAllowUnsafeThresholdAge() const override;
  void PostIncludeForRequestURL(
      const CookieAccessResult& access_result,
      const CookieOptions& options_used,
      CookieOptions::SameSiteCookieContext::ContextType
          cookie_inclusion_context_used) const override;
  void PostIsSetPermittedInContext(
      const CookieAccessResult& access_result,
      const CookieOptions& options_used) const override;

  // Keep defaults here in sync with
  // services/network/public/interfaces/cookie_manager.mojom.
  // These are the fields specific to CanonicalCookie. See CookieBase for other
  // data fields.
  // If adding more data fields, please also adjust GetAllDataMembersAsTuple().
  std::optional<crypto::ProcessBoundString> value_;
  base::Time expiry_date_;
  base::Time last_access_date_;
  base::Time last_update_date_;
  CookiePriority priority_{COOKIE_PRIORITY_MEDIUM};
  CookieSourceType source_type_{CookieSourceType::kUnknown};
};

// Used to pass excluded cookie information when it's possible that the
// canonical cookie object may not be available.
struct NET_EXPORT CookieAndLineWithAccessResult {
  CookieAndLineWithAccessResult();
  CookieAndLineWithAccessResult(std::optional<CanonicalCookie> cookie,
                                std::string cookie_string,
                                CookieAccessResult access_result);
  CookieAndLineWithAccessResult(
      const CookieAndLineWithAccessResult& cookie_and_line_with_access_result);

  CookieAndLineWithAccessResult& operator=(
      const CookieAndLineWithAccessResult& cookie_and_line_with_access_result);

  CookieAndLineWithAccessResult(
      CookieAndLineWithAccessResult&& cookie_and_line_with_access_result);

  ~CookieAndLineWithAccessResult();

  std::optional<CanonicalCookie> cookie;
  std::string cookie_string;
  CookieAccessResult access_result;
};

struct CookieWithAccessResult {
  CanonicalCookie cookie;
  CookieAccessResult access_result;
};

NET_EXPORT std::ostream& operator<<(
    std::ostream& os,
    CanonicalCookie::CanonicalizationFailure failure);

// Provided to allow gtest to create more helpful error messages, instead of
// printing hex.
inline void PrintTo(const CanonicalCookie& cc, std::ostream* os) {
  *os << "{ name=" << cc.Name() << ", value=" << cc.Value() << " }";
}
inline void PrintTo(const CookieWithAccessResult& cwar, std::ostream* os) {
  *os << "{ ";
  PrintTo(cwar.cookie, os);
  *os << ", ";
  PrintTo(cwar.access_result, os);
  *os << " }";
}
inline void PrintTo(const CookieAndLineWithAccessResult& calwar,
                    std::ostream* os) {
  *os << "{ ";
  if (calwar.cookie) {
    PrintTo(*calwar.cookie, os);
  } else {
    *os << "nullopt";
  }
  *os << ", " << calwar.cookie_string << ", ";
  PrintTo(calwar.access_result, os);
  *os << " }";
}

}  // namespace net

#endif  // NET_COOKIES_CANONICAL_COOKIE_H_