/*
 * Copyright (C) 2011 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1.  Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 * 2.  Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 3.  Neither the name of Google, Inc. ("Google") nor the names of
 *     its contributors may be used to endorse or promote products derived
 *     from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY GOOGLE AND ITS CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "third_party/blink/renderer/platform/weborigin/security_policy.h"

#include <memory>
#include <optional>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/no_destructor.h"
#include "base/strings/pattern.h"
#include "base/strings/string_split.h"
#include "base/synchronization/lock.h"
#include "build/build_config.h"
#include "services/network/public/cpp/cors/origin_access_list.h"
#include "services/network/public/mojom/referrer_policy.mojom-blink.h"
#include "third_party/blink/public/common/loader/referrer_utils.h"
#include "third_party/blink/public/common/switches.h"
#include "third_party/blink/public/platform/web_string.h"
#include "third_party/blink/renderer/platform/weborigin/kurl.h"
#include "third_party/blink/renderer/platform/weborigin/scheme_registry.h"
#include "third_party/blink/renderer/platform/weborigin/security_origin.h"
#include "third_party/blink/renderer/platform/wtf/hash_set.h"
#include "third_party/blink/renderer/platform/wtf/std_lib_extras.h"
#include "third_party/blink/renderer/platform/wtf/text/parsing_utilities.h"
#include "third_party/blink/renderer/platform/wtf/text/string_utf8_adaptor.h"
#include "third_party/blink/renderer/platform/wtf/threading.h"
#include "third_party/blink/renderer/platform/wtf/wtf.h"
#include "url/gurl.h"

namespace blink {

static base::Lock& GetLock() {
  DEFINE_THREAD_SAFE_STATIC_LOCAL(base::Lock, lock, ());
  return lock;
}

static network::cors::OriginAccessList& GetOriginAccessList() {
  DEFINE_THREAD_SAFE_STATIC_LOCAL(network::cors::OriginAccessList,
                                  origin_access_list, ());
  return origin_access_list;
}

bool SecurityPolicy::ShouldHideReferrer(const KURL& url, const KURL& referrer) {
  bool referrer_is_secure_url = referrer.ProtocolIs("https");
  bool scheme_is_allowed =
      SchemeRegistry::ShouldTreatURLSchemeAsAllowedForReferrer(
          referrer.Protocol());

  if (!scheme_is_allowed)
    return true;

  if (!referrer_is_secure_url)
    return false;

  bool url_is_secure_url = url.ProtocolIs("https");

  return !url_is_secure_url;
}

Referrer SecurityPolicy::GenerateReferrer(
    network::mojom::ReferrerPolicy referrer_policy,
    const KURL& url,
    const String& referrer) {
  network::mojom::ReferrerPolicy referrer_policy_no_default =
      ReferrerUtils::MojoReferrerPolicyResolveDefault(referrer_policy);
  // Empty (a possible input) and default (the value of `Referrer::NoReferrer`)
  // strings are not equivalent.
  if (referrer == Referrer::NoReferrer() || referrer.empty())
    return Referrer(Referrer::NoReferrer(), referrer_policy_no_default);

  KURL referrer_url = KURL(NullUrl(), referrer).UrlStrippedForUseAsReferrer();
  return GenerateReferrer(referrer_policy, url, referrer_url);
}

Referrer SecurityPolicy::GenerateReferrer(
    network::mojom::ReferrerPolicy referrer_policy,
    const KURL& url,
    const KURL& referrer_url) {
  // Enforce that the incoming KURL is pre-stripped (or invalid) since this
  // overload skips the initial stripping and validation pass.
  DCHECK(!referrer_url.IsValid() ||
         referrer_url == referrer_url.UrlStrippedForUseAsReferrer());

  network::mojom::ReferrerPolicy referrer_policy_no_default =
      ReferrerUtils::MojoReferrerPolicyResolveDefault(referrer_policy);

  if (!referrer_url.IsValid())
    return Referrer(Referrer::NoReferrer(), referrer_policy_no_default);

  // 5. Let referrerOrigin be the result of stripping referrerSource for use as
  // a referrer, with the origin-only flag set to true.
  // Creating referrerOrigin can be an expensive operation so we use a lambda
  // to create it dynamically only for the cases where it is used.
  std::optional<KURL> referrer_origin;
  auto get_referrer_origin = [&]() -> const KURL& {
    if (!referrer_origin) {
      referrer_origin.emplace(referrer_url);
      referrer_origin->SetPath(String());
      referrer_origin->SetQuery(String());
    }
    return *referrer_origin;
  };

  // 6. If the result of serializing referrerURL is a string whose length is
  // greater than 4096, set referrerURL to referrerOrigin.
  KURL final_referrer_url = referrer_url;
  if (final_referrer_url.GetString().length() > 4096) {
    final_referrer_url = get_referrer_origin();
  }

  switch (referrer_policy_no_default) {
    case network::mojom::ReferrerPolicy::kNever:
      return Referrer(Referrer::NoReferrer(), referrer_policy_no_default);
    case network::mojom::ReferrerPolicy::kAlways:
      return Referrer(final_referrer_url, referrer_policy_no_default);
    case network::mojom::ReferrerPolicy::kOrigin: {
      return Referrer(get_referrer_origin(), referrer_policy_no_default);
    }
    case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin: {
      if (!SecurityOrigin::AreSameOrigin(final_referrer_url, url)) {
        return Referrer(get_referrer_origin(), referrer_policy_no_default);
      }
      break;
    }
    case network::mojom::ReferrerPolicy::kSameOrigin: {
      if (!SecurityOrigin::AreSameOrigin(final_referrer_url, url)) {
        return Referrer(Referrer::NoReferrer(), referrer_policy_no_default);
      }
      return Referrer(final_referrer_url, referrer_policy_no_default);
    }
    case network::mojom::ReferrerPolicy::kStrictOrigin: {
      return Referrer(ShouldHideReferrer(url, final_referrer_url)
                          ? Referrer::NoReferrer()
                          : get_referrer_origin(),
                      referrer_policy_no_default);
    }
    case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin: {
      if (!SecurityOrigin::AreSameOrigin(final_referrer_url, url)) {
        return Referrer(ShouldHideReferrer(url, final_referrer_url)
                            ? Referrer::NoReferrer()
                            : get_referrer_origin(),
                        referrer_policy_no_default);
      }
      break;
    }
    case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade:
      break;
    case network::mojom::ReferrerPolicy::kDefault:
      NOTREACHED();
  }

  return Referrer(ShouldHideReferrer(url, final_referrer_url)
                      ? Referrer::NoReferrer()
                      : final_referrer_url,
                  referrer_policy_no_default);
}

bool SecurityPolicy::IsOriginAccessAllowed(
    const SecurityOrigin* active_origin,
    const SecurityOrigin* target_origin) {
  base::AutoLock locker(GetLock());
  return GetOriginAccessList().CheckAccessState(
             active_origin->ToUrlOrigin(),
             target_origin->ToUrlOrigin().GetURL()) ==
         network::cors::OriginAccessList::AccessState::kAllowed;
}

bool SecurityPolicy::IsOriginAccessToURLAllowed(
    const SecurityOrigin* active_origin,
    const KURL& url) {
  base::AutoLock locker(GetLock());
  return GetOriginAccessList().CheckAccessState(active_origin->ToUrlOrigin(),
                                                GURL(url)) ==
         network::cors::OriginAccessList::AccessState::kAllowed;
}

void SecurityPolicy::AddOriginAccessAllowListEntry(
    const SecurityOrigin& source_origin,
    const String& destination_protocol,
    const String& destination_domain,
    const uint16_t port,
    const network::mojom::CorsDomainMatchMode domain_match_mode,
    const network::mojom::CorsPortMatchMode port_match_mode,
    const network::mojom::CorsOriginAccessMatchPriority priority) {
  base::AutoLock locker(GetLock());
  GetOriginAccessList().AddAllowListEntryForOrigin(
      source_origin.ToUrlOrigin(), destination_protocol.Utf8(),
      destination_domain.Utf8(), port, domain_match_mode, port_match_mode,
      priority);
}

void SecurityPolicy::AddOriginAccessBlockListEntry(
    const SecurityOrigin& source_origin,
    const String& destination_protocol,
    const String& destination_domain,
    const uint16_t port,
    const network::mojom::CorsDomainMatchMode domain_match_mode,
    const network::mojom::CorsPortMatchMode port_match_mode,
    const network::mojom::CorsOriginAccessMatchPriority priority) {
  base::AutoLock locker(GetLock());
  GetOriginAccessList().AddBlockListEntryForOrigin(
      source_origin.ToUrlOrigin(), destination_protocol.Utf8(),
      destination_domain.Utf8(), port, domain_match_mode, port_match_mode,
      priority);
}

void SecurityPolicy::ClearOriginAccessListForOrigin(
    const SecurityOrigin& source_origin) {
  base::AutoLock locker(GetLock());
  GetOriginAccessList().ClearForOrigin(source_origin.ToUrlOrigin());
}

void SecurityPolicy::ClearOriginAccessList() {
  base::AutoLock locker(GetLock());
  GetOriginAccessList().Clear();
}

bool SecurityPolicy::ReferrerPolicyFromString(
    const StringView& policy,
    ReferrerPolicyLegacyKeywordsSupport legacy_keywords_support,
    network::mojom::ReferrerPolicy* result) {
  DCHECK(!policy.IsNull());
  bool support_legacy_keywords =
      (legacy_keywords_support == kSupportReferrerPolicyLegacyKeywords);

  if (EqualIgnoringAsciiCase(policy, "no-referrer") ||
      (support_legacy_keywords && (EqualIgnoringAsciiCase(policy, "never") ||
                                   EqualIgnoringAsciiCase(policy, "none")))) {
    *result = network::mojom::ReferrerPolicy::kNever;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "unsafe-url") ||
      (support_legacy_keywords && EqualIgnoringAsciiCase(policy, "always"))) {
    *result = network::mojom::ReferrerPolicy::kAlways;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "origin")) {
    *result = network::mojom::ReferrerPolicy::kOrigin;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "origin-when-cross-origin") ||
      (support_legacy_keywords &&
       EqualIgnoringAsciiCase(policy, "origin-when-crossorigin"))) {
    *result = network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "same-origin")) {
    *result = network::mojom::ReferrerPolicy::kSameOrigin;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "strict-origin")) {
    *result = network::mojom::ReferrerPolicy::kStrictOrigin;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "strict-origin-when-cross-origin")) {
    *result = network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin;
    return true;
  }
  if (EqualIgnoringAsciiCase(policy, "no-referrer-when-downgrade")) {
    *result = network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade;
    return true;
  }
  if (support_legacy_keywords && EqualIgnoringAsciiCase(policy, "default")) {
    *result = ReferrerUtils::NetToMojoReferrerPolicy(
        ReferrerUtils::GetDefaultNetReferrerPolicy());
    return true;
  }
  return false;
}

String SecurityPolicy::ReferrerPolicyAsString(
    network::mojom::ReferrerPolicy policy) {
  switch (policy) {
    case network::mojom::ReferrerPolicy::kAlways:
      return "unsafe-url";
    case network::mojom::ReferrerPolicy::kDefault:
      return "";
    case network::mojom::ReferrerPolicy::kNoReferrerWhenDowngrade:
      return "no-referrer-when-downgrade";
    case network::mojom::ReferrerPolicy::kNever:
      return "no-referrer";
    case network::mojom::ReferrerPolicy::kOrigin:
      return "origin";
    case network::mojom::ReferrerPolicy::kOriginWhenCrossOrigin:
      return "origin-when-cross-origin";
    case network::mojom::ReferrerPolicy::kSameOrigin:
      return "same-origin";
    case network::mojom::ReferrerPolicy::kStrictOrigin:
      return "strict-origin";
    case network::mojom::ReferrerPolicy::kStrictOriginWhenCrossOrigin:
      return "strict-origin-when-cross-origin";
  }
  NOTREACHED();
}

namespace {

template <typename CharType>
inline bool IsAsciiAlphaOrHyphen(CharType c) {
  return IsAsciiAlpha(c) || c == '-';
}

}  // namespace

bool SecurityPolicy::ReferrerPolicyFromHeaderValue(
    const String& header_value,
    ReferrerPolicyLegacyKeywordsSupport legacy_keywords_support,
    network::mojom::ReferrerPolicy* result) {
  network::mojom::ReferrerPolicy referrer_policy =
      network::mojom::ReferrerPolicy::kDefault;

  Vector<StringView> tokens = StringView(header_value).Split(',');
  for (const auto& token : tokens) {
    network::mojom::ReferrerPolicy current_result;
    auto stripped_token = token.StripWhiteSpace();
    if (SecurityPolicy::ReferrerPolicyFromString(
            stripped_token, legacy_keywords_support, &current_result)) {
      referrer_policy = current_result;
    } else {
      for (StringView::size_type i = 0; i < stripped_token.length(); ++i) {
        // SAFETY: length check above.
        if (!IsAsciiAlphaOrHyphen(UNSAFE_BUFFERS(stripped_token[i]))) {
          return false;
        }
      }
    }
  }

  if (referrer_policy == network::mojom::ReferrerPolicy::kDefault)
    return false;

  *result = referrer_policy;
  return true;
}

}  // namespace blink
