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

#ifdef UNSAFE_BUFFERS_BUILD
// TODO(crbug.com/390223051): Remove C-library calls to fix the errors.
#pragma allow_unsafe_libc_calls
#endif

#include "content/browser/isolated_origin_util.h"

#include <string>
#include <string_view>

#include "base/logging.h"
#include "base/strings/string_util.h"
#include "net/base/registry_controlled_domains/registry_controlled_domain.h"
#include "services/network/public/cpp/is_potentially_trustworthy.h"
#include "url/gurl.h"

const char* kAllSubdomainsWildcard = "[*.]";

namespace content {

IsolatedOriginPattern::IsolatedOriginPattern(std::string_view pattern)
    : isolate_all_subdomains_(false), is_valid_(false) {
  Parse(pattern);
}

IsolatedOriginPattern::IsolatedOriginPattern(const url::Origin& origin)
    : IsolatedOriginPattern(origin.GetURL().spec()) {}

IsolatedOriginPattern::~IsolatedOriginPattern() = default;
IsolatedOriginPattern::IsolatedOriginPattern(
    const IsolatedOriginPattern& other) = default;
IsolatedOriginPattern& IsolatedOriginPattern::operator=(
    const IsolatedOriginPattern& other) = default;
IsolatedOriginPattern::IsolatedOriginPattern(IsolatedOriginPattern&& other) =
    default;
IsolatedOriginPattern& IsolatedOriginPattern::operator=(
    IsolatedOriginPattern&& other) = default;

bool IsolatedOriginPattern::Parse(const std::string_view& unparsed_pattern) {
  pattern_ = std::string(unparsed_pattern);
  origin_ = url::Origin();
  isolate_all_subdomains_ = false;
  is_valid_ = false;

  size_t host_begin = unparsed_pattern.find(url::kStandardSchemeSeparator);
  if (host_begin == std::string_view::npos || host_begin == 0) {
    return false;
  }

  // Skip over the scheme separator.
  host_begin += strlen(url::kStandardSchemeSeparator);
  if (host_begin >= unparsed_pattern.size())
    return false;

  std::string_view scheme_part = unparsed_pattern.substr(0, host_begin);
  std::string_view host_part = unparsed_pattern.substr(host_begin);

  // Empty schemes or hosts are invalid for isolation purposes.
  if (host_part.size() == 0)
    return false;

  if (base::StartsWith(host_part, kAllSubdomainsWildcard)) {
    isolate_all_subdomains_ = true;
    host_part.remove_prefix(strlen(kAllSubdomainsWildcard));
  }

  GURL conformant_url(base::JoinString({scheme_part, host_part}, ""));
  origin_ = url::Origin::Create(conformant_url);

  // Ports are ignored when matching isolated origins (see also
  // https://crbug.com/914511).
  const std::string& scheme = origin_.scheme();
  int default_port = url::DefaultPortForScheme(scheme);
  if (origin_.port() != default_port) {
    LOG(ERROR) << "Ignoring port number in isolated origin: " << origin_;
    origin_ = url::Origin::Create(GURL(
        origin_.scheme() + url::kStandardSchemeSeparator + origin_.host()));
  }

  // Can't isolate subdomains of an IP address, must be a valid isolated origin
  // after processing.
  if ((conformant_url.HostIsIPAddress() && isolate_all_subdomains_) ||
      !IsolatedOriginUtil::IsValidIsolatedOrigin(origin_)) {
    origin_ = url::Origin();
    isolate_all_subdomains_ = false;
    return false;
  }

  DCHECK(!is_valid_ || !origin_.opaque());
  is_valid_ = true;
  return true;
}

// static
bool IsolatedOriginUtil::DoesOriginMatchIsolatedOrigin(
    const url::Origin& origin,
    const url::Origin& isolated_origin) {
  // Don't match subdomains if the isolated origin is an IP address.
  if (isolated_origin.GetURL().HostIsIPAddress())
    return origin == isolated_origin;

  // Compare scheme and hostname, but don't compare ports - see
  // https://crbug.com/914511.
  if (origin.scheme() != isolated_origin.scheme())
    return false;

  // Subdomains of an isolated origin are considered to be in the same isolated
  // origin.
  return origin.DomainIs(isolated_origin.host());
}

// static
bool IsolatedOriginUtil::IsValidIsolatedOrigin(const url::Origin& origin) {
  return IsValidIsolatedOriginImpl(origin,
                                   /* is_legacy_isolated_origin_check=*/true);
}

// static
bool IsolatedOriginUtil::IsValidOriginForOptInIsolation(
    const url::Origin& origin) {
  // Per https://html.spec.whatwg.org/C/#initialise-the-document-object,
  // non-secure contexts cannot be isolated via opt-in origin isolation.
  return IsValidIsolatedOriginImpl(
             origin, /* is_legacy_isolated_origin_check=*/false) &&
         network::IsOriginPotentiallyTrustworthy(origin);
}

// static
bool IsolatedOriginUtil::IsValidOriginForOptOutIsolation(
    const url::Origin& origin) {
  // Per https://html.spec.whatwg.org/C/#initialise-the-document-object,
  // non-secure contexts cannot be isolated via opt-in origin isolation,
  // but we allow non-secure contexts to opt-out for legacy sites.
  return IsValidIsolatedOriginImpl(origin,
                                   /* is_legacy_isolated_origin_check=*/false);
}

// static
bool IsolatedOriginUtil::IsValidIsolatedOriginImpl(
    const url::Origin& origin,
    bool is_legacy_isolated_origin_check) {
  if (origin.opaque())
    return false;

  // Isolated origins should have HTTP or HTTPS schemes.  Hosts in other
  // schemes may not be compatible with subdomain matching.
  GURL origin_gurl = origin.GetURL();
  if (!origin_gurl.SchemeIsHTTPOrHTTPS())
    return false;

  // IP addresses are allowed.
  if (origin_gurl.HostIsIPAddress())
    return true;

  // Disallow hosts such as http://co.uk/, which don't have a valid
  // registry-controlled domain.  This prevents subdomain matching from
  // grouping unrelated sites on a registry into the same origin.
  //
  // This is not relevant for opt-in origin isolation, which doesn't need to
  // match subdomains. (And it'd be bad to check this in that case, as it
  // prohibits http://localhost/; see https://crbug.com/1142894.)
  if (is_legacy_isolated_origin_check) {
    const bool has_registry_domain =
        net::registry_controlled_domains::HostHasRegistryControlledDomain(
            origin.host(),
            net::registry_controlled_domains::INCLUDE_UNKNOWN_REGISTRIES,
            net::registry_controlled_domains::INCLUDE_PRIVATE_REGISTRIES);
    if (!has_registry_domain)
      return false;
  }

  // Disallow hosts with a trailing dot for legacy isolated origins, but allow
  // them for opt-in origin isolation since the spec says that they represent
  // a distinct origin: https://url.spec.whatwg.org/#concept-domain.
  // TODO(alexmos): Legacy isolated origins should probably support trailing
  // dots as well, but enabling this would require carefully thinking about
  // whether hosts without a trailing dot should match it.
  if (is_legacy_isolated_origin_check && origin.host().back() == '.') {
    return false;
  }

  return true;
}

}  // namespace content