chromium/android_webview/browser/ip_protection/aw_ip_protection_config_provider.cc

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

#include "android_webview/browser/ip_protection/aw_ip_protection_config_provider.h"

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

#include "android_webview/browser/aw_browser_context.h"
#include "android_webview/browser/aw_browser_process.h"
#include "android_webview/browser/ip_protection/aw_ip_protection_config_provider_factory.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/metrics/histogram_functions.h"
#include "base/numerics/safe_conversions.h"
#include "base/ranges/algorithm.h"
#include "base/sequence_checker.h"
#include "base/strings/strcat.h"
#include "base/task/bind_post_task.h"
#include "base/task/task_traits.h"
#include "base/task/thread_pool.h"
#include "base/threading/sequence_bound.h"
#include "components/ip_protection/android/ip_protection_token_ipc_fetcher.h"
#include "components/ip_protection/common/ip_protection_config_provider_helper.h"
#include "components/ip_protection/common/ip_protection_data_types.h"
#include "components/ip_protection/common/ip_protection_proxy_config_fetcher.h"
#include "components/ip_protection/common/ip_protection_telemetry.h"
#include "components/version_info/android/channel_getter.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "google_apis/google_api_keys.h"
#include "mojo/public/cpp/bindings/message.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "net/base/features.h"
#include "net/base/proxy_chain.h"
#include "net/base/proxy_server.h"
#include "net/base/proxy_string_util.h"
#include "net/third_party/quiche/src/quiche/blind_sign_auth/blind_sign_auth.h"
#include "net/third_party/quiche/src/quiche/blind_sign_auth/proto/blind_sign_auth_options.pb.h"
#include "net/third_party/quiche/src/quiche/blind_sign_auth/proto/spend_token_data.pb.h"
#include "third_party/abseil-cpp/absl/status/status.h"

namespace android_webview {

using ::ip_protection::TryGetAuthTokensAndroidResult;

AwIpProtectionConfigProvider::AwIpProtectionConfigProvider(
    AwBrowserContext* aw_browser_context)
    : aw_browser_context_(aw_browser_context),
      token_fetcher_task_runner_(base::ThreadPool::CreateSequencedTaskRunner(
          {base::MayBlock(), base::TaskPriority::BEST_EFFORT,
           base::TaskShutdownBehavior::SKIP_ON_SHUTDOWN})) {}

AwIpProtectionConfigProvider::~AwIpProtectionConfigProvider() = default;

void AwIpProtectionConfigProvider::SetUp() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (!ip_protection_token_ipc_fetcher_) {
    ip_protection_token_ipc_fetcher_ =
        base::SequenceBound<ip_protection::IpProtectionTokenIpcFetcher>(
            token_fetcher_task_runner_);
  }

  if (!ip_protection_proxy_config_fetcher_) {
    CHECK(aw_browser_context_);
    ip_protection_proxy_config_fetcher_ =
        std::make_unique<ip_protection::IpProtectionProxyConfigFetcher>(
            aw_browser_context_->GetDefaultStoragePartition()
                ->GetURLLoaderFactoryForBrowserProcess()
                .get(),
            ip_protection::IpProtectionConfigProviderHelper::kWebViewIpBlinding,
            google_apis::GetAPIKey(version_info::android::GetChannel()));
  }
}

void AwIpProtectionConfigProvider::SetUpForTesting(
    std::unique_ptr<ip_protection::IpProtectionProxyConfigRetriever>
        ip_protection_proxy_config_retriever,
    std::unique_ptr<quiche::BlindSignAuthInterface> bsa) {
  // Carefully destroy any existing values in the correct order.
  ip_protection_token_ipc_fetcher_.Reset();
  ip_protection_proxy_config_fetcher_ = nullptr;

  ip_protection_token_ipc_fetcher_ =
      base::SequenceBound<ip_protection::IpProtectionTokenIpcFetcher>(
          token_fetcher_task_runner_, std::move(bsa));
  ip_protection_proxy_config_fetcher_ =
      std::make_unique<ip_protection::IpProtectionProxyConfigFetcher>(
          std::move(ip_protection_proxy_config_retriever));
}

void AwIpProtectionConfigProvider::GetProxyList(GetProxyListCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(!is_shutting_down_);
  SetUp();

  // If IP Protection is disabled then don't attempt to get a proxy list.
  if (!IsIpProtectionEnabled()) {
    std::move(callback).Run(/*proxy_chains=*/std::nullopt,
                            /*geo_hint=*/std::nullopt);
    return;
  }

  ip_protection_proxy_config_fetcher_->CallGetProxyConfig(
      std::move(callback), /*oauth_token=*/std::nullopt);
}

void AwIpProtectionConfigProvider::TryGetAuthTokens(
    uint32_t batch_size,
    network::mojom::IpProtectionProxyLayer proxy_layer,
    TryGetAuthTokensCallback callback) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(!is_shutting_down_);
  SetUp();

  // The `batch_size` is cast to an `int` for use by BlindSignAuth, so check
  // for overflow here.
  if (!base::IsValueInRangeForNumericType<int>(batch_size)) {
    receivers_.ReportBadMessage("Invalid batch_size");
    return;
  }

  // If IP Protection is disabled then don't attempt to fetch tokens.
  if (!IsIpProtectionEnabled()) {
    TryGetAuthTokensComplete(
        /*bsa_tokens=*/std::nullopt, std::move(callback),
        TryGetAuthTokensAndroidResult::kFailedDisabled);
    return;
  }

  auto quiche_proxy_layer =
      proxy_layer == network::mojom::IpProtectionProxyLayer::kProxyA
          ? quiche::ProxyLayer::kProxyA
          : quiche::ProxyLayer::kProxyB;
  FetchBlindSignedToken(base::checked_cast<int>(batch_size), quiche_proxy_layer,
                        std::move(callback));
}

void AwIpProtectionConfigProvider::FetchBlindSignedToken(
    int batch_size,
    quiche::ProxyLayer quiche_proxy_layer,
    TryGetAuthTokensCallback callback) {
  auto bsa_get_tokens_start_time = base::TimeTicks::Now();
  ip_protection_token_ipc_fetcher_
      .AsyncCall(
          &ip_protection::IpProtectionTokenIpcFetcher::FetchBlindSignedToken)
      .WithArgs(
          /*access_token=*/std::nullopt, batch_size, quiche_proxy_layer,
          base::BindPostTaskToCurrentDefault(base::BindOnce(
              &AwIpProtectionConfigProvider::OnFetchBlindSignedTokenCompleted,
              weak_ptr_factory_.GetWeakPtr(), bsa_get_tokens_start_time,
              std::move(callback))));
}

void AwIpProtectionConfigProvider::OnFetchBlindSignedTokenCompleted(
    base::TimeTicks bsa_get_tokens_start_time,
    TryGetAuthTokensCallback callback,
    absl::StatusOr<std::vector<quiche::BlindSignToken>> tokens) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  if (is_shutting_down_) {
    return;
  }
  if (!tokens.ok()) {
    TryGetAuthTokensAndroidResult result;
    switch (tokens.status().code()) {
      case absl::StatusCode::kUnavailable:
        result = TryGetAuthTokensAndroidResult::kFailedBSATransient;
        break;
      case absl::StatusCode::kFailedPrecondition:
        result = TryGetAuthTokensAndroidResult::kFailedBSAPersistent;
        break;
      default:
        result = TryGetAuthTokensAndroidResult::kFailedBSAOther;
        break;
    }
    VLOG(2) << "AwIpProtectionConfigProvider::OnFetchBlindSignedTokenCompleted "
               "got an error: "
            << static_cast<int>(result);
    TryGetAuthTokensComplete(/*bsa_tokens=*/std::nullopt, std::move(callback),
                             result);
    return;
  }

  if (tokens.value().size() == 0) {
    VLOG(2) << "AwIpProtectionConfigProvider::"
               "OnFetchBlindSignedTokenCompleted called with no tokens";
    TryGetAuthTokensComplete(
        /*bsa_tokens=*/std::nullopt, std::move(callback),
        TryGetAuthTokensAndroidResult::kFailedBSAOther);
    return;
  }

  std::vector<ip_protection::BlindSignedAuthToken> bsa_tokens;
  for (const quiche::BlindSignToken& token : tokens.value()) {
    std::optional<ip_protection::BlindSignedAuthToken> converted_token =
        ip_protection::IpProtectionConfigProviderHelper::
            CreateBlindSignedAuthToken(token);
    if (!converted_token.has_value() || converted_token->token.empty()) {
      VLOG(2) << "AwIpProtectionConfigProvider::"
                 "OnFetchBlindSignedTokenCompleted failed to convert "
                 "`quiche::BlindSignAuth` token to a "
                 "`network::mojom::BlindSignedAuthToken`";
      TryGetAuthTokensComplete(
          /*bsa_tokens=*/std::nullopt, std::move(callback),
          TryGetAuthTokensAndroidResult::kFailedBSAOther);
      return;
    } else {
      bsa_tokens.push_back(std::move(converted_token).value());
    }
  }

  const base::TimeTicks current_time = base::TimeTicks::Now();
  TryGetAuthTokensComplete(std::move(bsa_tokens), std::move(callback),
                           TryGetAuthTokensAndroidResult::kSuccess,
                           current_time - bsa_get_tokens_start_time);
}

void AwIpProtectionConfigProvider::TryGetAuthTokensComplete(
    std::optional<std::vector<ip_protection::BlindSignedAuthToken>> bsa_tokens,
    TryGetAuthTokensCallback callback,
    ip_protection::TryGetAuthTokensAndroidResult result,
    std::optional<base::TimeDelta> duration) {
  if (result == TryGetAuthTokensAndroidResult::kSuccess) {
    CHECK(bsa_tokens.has_value() && !bsa_tokens->empty());
  }

  ip_protection::Telemetry().AndroidTokenBatchFetchComplete(result, duration);

  std::optional<base::TimeDelta> backoff = CalculateBackoff(result);
  std::optional<base::Time> try_again_after;
  if (backoff) {
    if (*backoff == base::TimeDelta::Max()) {
      try_again_after = base::Time::Max();
    } else {
      try_again_after = base::Time::Now() + *backoff;
    }
  }
  DCHECK(bsa_tokens.has_value() || try_again_after.has_value());
  std::move(callback).Run(std::move(bsa_tokens), try_again_after);
}

std::optional<base::TimeDelta> AwIpProtectionConfigProvider::CalculateBackoff(
    TryGetAuthTokensAndroidResult result) {
  std::optional<base::TimeDelta> backoff;
  switch (result) {
    case TryGetAuthTokensAndroidResult::kSuccess:
      break;
    case TryGetAuthTokensAndroidResult::kFailedBSAPersistent:
    case TryGetAuthTokensAndroidResult::kFailedDisabled:
      backoff = base::TimeDelta::Max();
      break;
    case TryGetAuthTokensAndroidResult::kFailedBSATransient:
    case TryGetAuthTokensAndroidResult::kFailedBSAOther:
      backoff =
          ip_protection::IpProtectionConfigProviderHelper::kTransientBackoff;
      // Note that we calculate the backoff assuming that we've waited for
      // `last_try_get_auth_tokens_backoff_` time already, but this may not be
      // the case when:
      //  - Concurrent calls to `TryGetAuthTokens` from two network contexts are
      //  made and both fail in the same way
      //
      //  - The network service restarts (the new network context(s) won't know
      //  to backoff until after the first request(s))
      //
      // We can't do much about the first case, but for the others we could
      // track the backoff time here and not request tokens again until
      // afterward.
      if (last_try_get_auth_tokens_backoff_ &&
          last_try_get_auth_tokens_result_ == result) {
        backoff = *last_try_get_auth_tokens_backoff_ * 2;
      }
      break;
  }
  last_try_get_auth_tokens_result_ = result;
  last_try_get_auth_tokens_backoff_ = backoff;
  return backoff;
}

void AwIpProtectionConfigProvider::Shutdown() {
  if (is_shutting_down_) {
    return;
  }
  is_shutting_down_ = true;
  receivers_.Clear();

  ip_protection_token_ipc_fetcher_.Reset();
  aw_browser_context_ = nullptr;
  ip_protection_proxy_config_fetcher_ = nullptr;
}

// static
AwIpProtectionConfigProvider* AwIpProtectionConfigProvider::Get(
    AwBrowserContext* aw_browser_context) {
  return AwIpProtectionConfigProviderFactory::GetForAwBrowserContext(
      aw_browser_context);
}

void AwIpProtectionConfigProvider::AddNetworkService(
    mojo::PendingReceiver<network::mojom::IpProtectionConfigGetter>
        pending_receiver,
    mojo::PendingRemote<network::mojom::IpProtectionProxyDelegate>
        pending_remote) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  CHECK(!is_shutting_down_);
  receivers_.Add(this, std::move(pending_receiver));
  remotes_.Add(std::move(pending_remote));
}

// static
bool AwIpProtectionConfigProvider::CanIpProtectionBeEnabled() {
  return base::FeatureList::IsEnabled(net::features::kEnableIpProtectionProxy);
}

// TODO(b/335420700): Update to return feature flag.
bool AwIpProtectionConfigProvider::IsIpProtectionEnabled() {
  if (is_shutting_down_) {
    return false;
  }
  return CanIpProtectionBeEnabled();
}

}  // namespace android_webview