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

#include "components/url_rewrite/common/url_loader_throttle.h"

#include <string>
#include <string_view>

#include "base/logging.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "net/base/net_errors.h"
#include "net/url_request/redirect_info.h"
#include "services/network/public/cpp/http_request_headers_update_params.h"
#include "services/network/public/cpp/resource_request.h"
#include "url/url_constants.h"

namespace url_rewrite {
namespace {

// Returns a string representing the URL stripped of query and ref.
std::string ClearUrlQueryRef(const GURL& url) {
  GURL::Replacements replacements;
  replacements.ClearQuery();
  replacements.ClearRef();
  return url.ReplaceComponents(replacements).spec();
}

void ApplySubstituteQueryPattern(
    network::ResourceRequest* request,
    const mojom::UrlRequestRewriteSubstituteQueryPatternPtr&
        substitute_query_pattern) {
  std::string url_query = request->url.GetQuery();

  base::ReplaceSubstringsAfterOffset(&url_query, 0,
                                     substitute_query_pattern->pattern,
                                     substitute_query_pattern->substitution);

  GURL::Replacements replacements;
  replacements.SetQueryStr(url_query);
  request->url = request->url.ReplaceComponents(replacements);
}

void ApplyReplaceUrl(network::ResourceRequest* request,
                     const mojom::UrlRequestRewriteReplaceUrlPtr& replace_url) {
  if (!base::EndsWith(ClearUrlQueryRef(request->url),
                      replace_url->url_ends_with, base::CompareCase::SENSITIVE))
    return;

  GURL new_url = replace_url->new_url;
  if (new_url.SchemeIs(url::kDataScheme)) {
    request->url = new_url;
    return;
  }

  if (new_url.has_scheme() &&
      new_url.GetScheme().compare(request->url.GetScheme()) != 0) {
    // No cross-scheme redirect allowed.
    return;
  }

  GURL::Replacements replacements;
  std::string host = new_url.GetHost();
  replacements.SetHostStr(host);
  std::string port = new_url.GetPort();
  replacements.SetPortStr(port);
  std::string path = new_url.GetPath();
  replacements.SetPathStr(path);

  request->url = request->url.ReplaceComponents(replacements);
}

void ApplyRemoveHeader(
    network::ResourceRequest* request,
    const mojom::UrlRequestRewriteRemoveHeaderPtr& remove_header) {
  std::optional<std::string> query_pattern = remove_header->query_pattern;
  if (query_pattern && request->url.GetQuery().find(query_pattern.value()) ==
                           std::string::npos) {
    // Per the FIDL API, the header should be removed if there is no query
    // pattern or if the pattern matches. Neither is true here.
    return;
  }

  request->headers.RemoveHeader(remove_header->header_name);
  request->cors_exempt_headers.RemoveHeader(remove_header->header_name);
}

void ApplyAppendToQuery(
    network::ResourceRequest* request,
    const mojom::UrlRequestRewriteAppendToQueryPtr& append_to_query) {
  std::string url_query;
  if (request->url.has_query() && !request->url.GetQuery().empty()) {
    url_query = request->url.GetQuery() + "&";
  }
  url_query += append_to_query->query;

  GURL::Replacements replacements;
  replacements.SetQueryStr(url_query);
  request->url = request->url.ReplaceComponents(replacements);
}

bool HostMatches(std::string_view url_host, std::string_view rule_host) {
  const std::string_view kWildcard("*.");
  if (base::StartsWith(rule_host, kWildcard, base::CompareCase::SENSITIVE)) {
    if (base::EndsWith(url_host, rule_host.substr(1),
                       base::CompareCase::SENSITIVE)) {
      return true;
    }

    // Check |url_host| is exactly |rule_host| without the wildcard. i.e. if
    // |rule_host| is "*.test.xyz", check |url_host| is exactly "test.xyz".
    return base::CompareCaseInsensitiveASCII(url_host, rule_host.substr(2)) ==
           0;
  }
  return base::CompareCaseInsensitiveASCII(url_host, rule_host) == 0;
}

// Returns true if the host and scheme filters defined in |rule| match |url|.
bool RuleFiltersMatchUrl(const GURL& url,
                         const mojom::UrlRequestRulePtr& rule) {
  if (rule->hosts_filter) {
    bool found = false;
    for (const std::string_view host : rule->hosts_filter.value()) {
      if ((found = HostMatches(url.GetHost(), host))) {
        break;
      }
    }
    if (!found)
      return false;
  }

  if (rule->schemes_filter) {
    bool found = false;
    for (const auto& scheme : rule->schemes_filter.value()) {
      if (url.GetScheme().compare(scheme) == 0) {
        found = true;
        break;
      }
    }
    if (!found)
      return false;
  }

  return true;
}

// Returns true if |request| is either allowed or left unblocked by any rules.
bool IsRequestAllowed(network::ResourceRequest* request,
                      const mojom::UrlRequestRewriteRulesPtr& rules) {
  for (const auto& rule : rules->rules) {
    if (rule->actions.size() != 1)
      continue;

    if (rule->actions[0]->which() != mojom::UrlRequestAction::Tag::kPolicy)
      continue;

    if (!RuleFiltersMatchUrl(request->url, rule))
      continue;

    switch (rule->actions[0]->get_policy()) {
      case mojom::UrlRequestAccessPolicy::kAllow:
        return true;
      case mojom::UrlRequestAccessPolicy::kDeny:
        return false;
    }
  }

  return true;
}

}  // namespace

URLLoaderThrottle::URLLoaderThrottle(
    scoped_refptr<UrlRequestRewriteRules> rules,
    IsHeaderCorsExemptCallback is_header_cors_exempt_callback)
    : rules_(std::move(rules)),
      is_header_cors_exempt_callback_(
          std::move(is_header_cors_exempt_callback)) {
  DCHECK(rules_);
  DCHECK(is_header_cors_exempt_callback_);
}

URLLoaderThrottle::~URLLoaderThrottle() = default;

void URLLoaderThrottle::DetachFromCurrentSequence() {}

void URLLoaderThrottle::WillStartRequest(network::ResourceRequest* request,
                                         bool* defer) {
  if (!IsRequestAllowed(request, rules_->data)) {
    delegate_->CancelWithError(net::ERR_ABORTED,
                               "Resource load blocked by embedder policy.");
    return;
  }

  GURL current_url = request->url;
  for (const auto& rule : rules_->data->rules) {
    ApplyRule(request, rule);
    if (request->url != current_url) {
      if (!IsRequestAllowed(request, rules_->data)) {
        delegate_->CancelWithError(net::ERR_ABORTED,
                                   "Resource load blocked by embedder policy.");
        return;
      }
      current_url = request->url;
    }
  }

  if (!added_headers_.empty()) {
    original_origin_ = url::Origin::Create(request->url);
  }

  *defer = false;
}

void URLLoaderThrottle::WillRedirectRequest(
    net::RedirectInfo* redirect_info,
    const network::mojom::URLResponseHead& response_head,
    bool* defer,
    network::HttpRequestHeadersUpdateParams* headers_update_params) {
  if (added_headers_.empty()) {
    return;
  }

  if (!url::Origin::Create(redirect_info->new_url)
           .IsSameOriginWith(original_origin_)) {
    LOG(WARNING) << "Http headers [" << base::JoinString(added_headers_, ", ")
                 << "] will be removed due to cross origin redirection "
                    "from "
                 << original_origin_ << " to "
                 << url::Origin::Create(redirect_info->new_url);
    headers_update_params->removed_headers.insert(
        headers_update_params->removed_headers.end(),
        std::make_move_iterator(added_headers_.begin()),
        std::make_move_iterator(added_headers_.end()));
    added_headers_.clear();
  }
}

void URLLoaderThrottle::ApplyRule(network::ResourceRequest* request,
                                  const mojom::UrlRequestRulePtr& rule) {
  // Prevent applying rules on redirect navigations.
  if (request->navigation_redirect_chain.size() > 1u)
    return;

  if (!RuleFiltersMatchUrl(request->url, rule))
    return;

  for (const auto& rewrite : rule->actions)
    ApplyRewrite(request, rewrite);
}

void URLLoaderThrottle::ApplyRewrite(
    network::ResourceRequest* request,
    const mojom::UrlRequestActionPtr& rewrite) {
  switch (rewrite->which()) {
    case mojom::UrlRequestAction::Tag::kAddHeaders:
      ApplyAddHeaders(request, rewrite->get_add_headers());
      return;
    case mojom::UrlRequestAction::Tag::kRemoveHeader:
      ApplyRemoveHeader(request, rewrite->get_remove_header());
      return;
    case mojom::UrlRequestAction::Tag::kSubstituteQueryPattern:
      ApplySubstituteQueryPattern(request,
                                  rewrite->get_substitute_query_pattern());
      return;
    case mojom::UrlRequestAction::Tag::kReplaceUrl:
      ApplyReplaceUrl(request, rewrite->get_replace_url());
      return;
    case mojom::UrlRequestAction::Tag::kAppendToQuery:
      ApplyAppendToQuery(request, rewrite->get_append_to_query());
      return;
    case mojom::UrlRequestAction::Tag::kPolicy:
      // "Policy" is interpreted elsewhere; it is a no-op for rewriting.
      return;
  }
  NOTREACHED();  // Invalid enum value.
}

void URLLoaderThrottle::ApplyAddHeaders(
    network::ResourceRequest* request,
    const mojom::UrlRequestRewriteAddHeadersPtr& add_headers) {
  // Bucket each |header| into the regular/CORS-compliant header list or the
  // CORS-exempt header list.
  for (const auto& header : add_headers->headers) {
    if (request->headers.HasHeader(header->name) ||
        request->cors_exempt_headers.HasHeader(header->name)) {
      // Skip headers already present in the request at this point.
      continue;
    }
    if (is_header_cors_exempt_callback_.Run(header->name)) {
      request->cors_exempt_headers.SetHeader(header->name, header->value);
    } else {
      request->headers.SetHeader(header->name, header->value);
    }
    added_headers_.push_back(header->name);
  }
}

}  // namespace url_rewrite
