chromium/chrome/browser/nearby_sharing/tachyon_ice_config_fetcher.cc

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

#include "chrome/browser/nearby_sharing/tachyon_ice_config_fetcher.h"

#include <optional>

#include "base/functional/bind.h"
#include "base/metrics/histogram_functions.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/time/time.h"
#include "base/unguessable_token.h"
#include "chrome/browser/nearby_sharing/instantmessaging/token_fetcher.h"
#include "chrome/browser/nearby_sharing/proto/duration.pb.h"
#include "chrome/browser/nearby_sharing/proto/ice.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon_common.pb.h"
#include "chrome/browser/nearby_sharing/proto/tachyon_enums.pb.h"
#include "chrome/services/sharing/public/cpp/sharing_webrtc_metrics.h"
#include "chromeos/ash/components/nearby/common/client/nearby_http_result.h"
#include "components/cross_device/logging/logging.h"
#include "google_apis/gaia/gaia_constants.h"
#include "net/base/load_flags.h"
#include "services/network/public/cpp/resource_request.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "services/network/public/cpp/simple_url_loader.h"
#include "third_party/icu/source/common/unicode/locid.h"
#include "url/gurl.h"

namespace {

namespace tachyon_proto = nearbyshare::tachyon_proto;

const char kIceConfigApiUrl[] =
    "https://instantmessaging-pa.googleapis.com/v1/peertopeer:geticeserver";

const char kAuthorizationHeaderFormat[] = "Authorization: Bearer %s";

// Timeout for network calls to Tachyon servers.
constexpr base::TimeDelta kNetworkTimeout = base::Seconds(4);

// Response with 2 ice server configs takes ~1KB. A loose upper bound of 16KB is
// chosen to avoid breaking the flow in case the response has longer URLs in ice
// configs.
constexpr int kMaxBodySize = 16 * 1024;

const char kAppName[] = "Nearby";
const tachyon_proto::IdType::Type kTachyonIdType =
    tachyon_proto::IdType::NEARBY_ID;
constexpr int kMajorVersion = 1;
constexpr int kMinorVersion = 24;
constexpr int kPointVersion = 0;

const net::NetworkTrafficAnnotationTag kTrafficAnnotation =
    net::DefineNetworkTrafficAnnotation("tachyon_ice_config_fetcher", R"(
        semantics {
          sender: "TachyonIceConfigFetcher"
          description:
            "Fetches ice server configurations for p2p webrtc connection as "
            "described in "
            "https://www.w3.org/TR/webrtc/#rtciceserver-dictionary."
          trigger:
            "User uses any Chrome cross-device sharing feature and selects one"
            " of their devices to send the data to."
          data: "No data is sent in the request."
          destination: GOOGLE_OWNED_SERVICE
          }
          policy {
            cookies_allowed: NO
            setting:
              "Users can disable this behavior by signing out of Chrome."
            chrome_policy {
              BrowserSignin {
                policy_options {mode: MANDATORY}
                BrowserSignin: 0
              }
            }
          })");

// Returns the ISO country code for the locale currently set as the
// user's device language.
const std::string GetCurrentCountryCode() {
  return icu::Locale::getDefault().getCountry();
}

void BuildLocationHint(tachyon_proto::LocationHint* location_hint,
                       const std::string& location,
                       tachyon_proto::LocationStandard_Format format) {
  location_hint->set_location(location);
  location_hint->set_format(format);
}

void BuildId(tachyon_proto::Id* req_id, const std::string& id) {
  DCHECK(req_id);
  req_id->set_id(id);
  req_id->set_app(kAppName);
  req_id->set_type(kTachyonIdType);
  BuildLocationHint(req_id->mutable_location_hint(), GetCurrentCountryCode(),
                    tachyon_proto::LocationStandard_Format::
                        LocationStandard_Format_ISO_3166_1_ALPHA_2);
}

void BuildHeader(tachyon_proto::RequestHeader* header) {
  DCHECK(header);
  header->set_request_id(base::UnguessableToken::Create().ToString());
  header->set_app(kAppName);
  BuildId(header->mutable_requester_id(), std::string());
  tachyon_proto::ClientInfo* info = header->mutable_client_info();
  info->set_api_version(tachyon_proto::ApiVersion::V4);
  info->set_platform_type(tachyon_proto::Platform::DESKTOP);
  info->set_major(kMajorVersion);
  info->set_minor(kMinorVersion);
  info->set_point(kPointVersion);
}

tachyon_proto::GetICEServerRequest BuildRequest() {
  tachyon_proto::GetICEServerRequest request;
  BuildHeader(request.mutable_header());
  return request;
}

void RecordResultMetric(const ash::nearby::NearbyHttpStatus& http_status) {
  bool success = http_status.IsSuccess();
  base::UmaHistogramBoolean(
      "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher.Result",
      success);
  if (!success) {
    base::UmaHistogramSparse(
        "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher."
        "FailureReason",
        http_status.GetResultCodeForMetrics());
  }
}

void RecordCacheHitMetric(bool cache_hit) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher.CacheHit",
      cache_hit);
}

void RecordTokenFetchSuccessMetric(bool token_fetch_successful) {
  base::UmaHistogramBoolean(
      "Nearby.Connections.InstantMessaging.TachyonIceConfigFetcher."
      "OAuthTokenFetchResult",
      token_fetch_successful);
}

bool IsLoaderSuccessful(const network::SimpleURLLoader* loader,
                        const std::string& request_id) {
  DCHECK(loader);
  ash::nearby::NearbyHttpStatus status =
      ash::nearby::NearbyHttpStatus(loader->NetError(), loader->ResponseInfo());

  RecordResultMetric(status);

  if (!status.IsSuccess()) {
    CD_LOG(ERROR, Feature::NEARBY_INFRA)
        << "TachyonIceConfigFetcher (request_id=" << request_id << ") "
        << status << " " << status.GetResultCodeForMetrics();
    return false;
  }

  CD_LOG(VERBOSE, Feature::NEARBY_INFRA)
      << "TachyonIceConfigFetcher (request_id=" << request_id
      << ") GetIceServers succeeded";
  return true;
}

std::vector<::sharing::mojom::IceServerPtr> GetDefaultIceServers() {
  ::sharing::mojom::IceServerPtr ice_server(::sharing::mojom::IceServer::New());
  ice_server->urls.emplace_back("stun:stun.l.google.com:19302");
  ice_server->urls.emplace_back("stun:stun1.l.google.com:19302");
  ice_server->urls.emplace_back("stun:stun2.l.google.com:19302");
  ice_server->urls.emplace_back("stun:stun3.l.google.com:19302");
  ice_server->urls.emplace_back("stun:stun4.l.google.com:19302");

  std::vector<::sharing::mojom::IceServerPtr> default_servers;
  default_servers.push_back(std::move(ice_server));
  return default_servers;
}

std::vector<::sharing::mojom::IceServerPtr> CloneIceServerList(
    const std::vector<::sharing::mojom::IceServerPtr>& server_list) {
  // Cannot use vector's default copy operation because IceServerPtr is move
  // only and has to be cloned.
  std::vector<::sharing::mojom::IceServerPtr> new_list;
  for (const auto& server : server_list) {
    new_list.push_back(server.Clone());
  }
  return new_list;
}

void OnOAuthTokenFetched(
    std::unique_ptr<TokenFetcher> token_fetcher,
    base::OnceCallback<void(const std::string& token)> callback,
    const std::string& token) {
  // It is safe to reset the token fetcher now.
  token_fetcher.reset();
  // Note: We do not do anything special for empty tokens.
  RecordTokenFetchSuccessMetric(/*token_fetch_successful=*/!token.empty());
  std::move(callback).Run(token);
}

void GetAccessToken(
    signin::IdentityManager* identity_manager,
    base::OnceCallback<void(const std::string& token)> callback) {
  std::unique_ptr<TokenFetcher> token_fetcher =
      std::make_unique<TokenFetcher>(identity_manager);
  TokenFetcher* token_fetcher_ptr = token_fetcher.get();

  // Pass the token fetcher in the closure so that its lifetime is the same as
  // the request. The access token is cached by the identity manager, so no
  // caching is necessary here.
  token_fetcher_ptr->GetAccessToken(base::BindOnce(
      &OnOAuthTokenFetched, std::move(token_fetcher), std::move(callback)));
}

}  // namespace

TachyonIceConfigFetcher::TachyonIceConfigFetcher(
    signin::IdentityManager* identity_manager,
    scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory)
    : identity_manager_(identity_manager),
      url_loader_factory_(std::move(url_loader_factory)) {}

TachyonIceConfigFetcher::~TachyonIceConfigFetcher() = default;

void TachyonIceConfigFetcher::GetIceServers(GetIceServersCallback callback) {
  // If a previous request cached the ICE servers and the expiration time hasn't
  // lapsed, return a copy of the cached servers immediately.
  if (ice_server_cache_ && ice_server_cache_expiration_ >= base::Time::Now()) {
    CD_LOG(VERBOSE, Feature::NEARBY_INFRA)
        << "TachyonIceConfigFetcher returning cached ice servers";
    std::move(callback).Run(CloneIceServerList(*ice_server_cache_));
    RecordCacheHitMetric(/*cache_hit=*/true);
    return;
  }

  GetAccessToken(
      identity_manager_,
      base::BindOnce(&TachyonIceConfigFetcher::GetIceServersWithToken,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback)));
  RecordCacheHitMetric(/*cache_hit=*/false);
}

void TachyonIceConfigFetcher::GetIceServersWithToken(
    GetIceServersCallback callback,
    const std::string& token) {
  if (token.empty()) {
    CD_LOG(ERROR, Feature::NEARBY_INFRA)
        << "TachyonIceConfigFetcher failed to fetch OAuth access token, "
           "returning default ICE servers";
    std::move(callback).Run(GetDefaultIceServers());
    return;
  }

  auto resource_request = std::make_unique<network::ResourceRequest>();
  resource_request->url = GURL(kIceConfigApiUrl);
  resource_request->load_flags =
      net::LOAD_BYPASS_CACHE | net::LOAD_DISABLE_CACHE;
  resource_request->credentials_mode = network::mojom::CredentialsMode::kOmit;
  resource_request->method = net::HttpRequestHeaders::kPostMethod;
  resource_request->headers.AddHeaderFromString(
      base::StringPrintf(kAuthorizationHeaderFormat, token.c_str()));

  tachyon_proto::GetICEServerRequest request = BuildRequest();
  const std::string& request_id = request.header().request_id();

  auto url_loader = network::SimpleURLLoader::Create(
      std::move(resource_request), kTrafficAnnotation);
  auto* url_loader_ptr = url_loader.get();
  url_loader->SetTimeoutDuration(kNetworkTimeout);
  url_loader->AttachStringForUpload(request.SerializeAsString(),
                                    "application/x-protobuf");

  CD_LOG(VERBOSE, Feature::NEARBY_INFRA)
      << __func__
      << ": Requesting ICE Servers from Tachyon (request_id=" << request_id
      << ")";
  url_loader_ptr->DownloadToString(
      url_loader_factory_.get(),
      base::BindOnce(&TachyonIceConfigFetcher::OnIceServersResponse,
                     weak_ptr_factory_.GetWeakPtr(), std::move(callback),
                     request_id, std::move(url_loader)),
      kMaxBodySize);
}

void TachyonIceConfigFetcher::OnIceServersResponse(
    ::sharing::mojom::IceConfigFetcher::GetIceServersCallback callback,
    const std::string& request_id,
    std::unique_ptr<network::SimpleURLLoader> url_loader,
    std::unique_ptr<std::string> response_body) {
  std::vector<::sharing::mojom::IceServerPtr> ice_servers;

  if (IsLoaderSuccessful(url_loader.get(), request_id) && response_body)
    ice_servers = ParseIceServersResponse(*response_body, request_id);

  sharing::LogWebRtcIceConfigFetched(ice_servers.size());

  if (ice_servers.empty()) {
    CD_LOG(VERBOSE, Feature::NEARBY_INFRA)
        << "TachyonIceConfigFetcher (request_id=" << request_id
        << ") empty response, returning default ICE servers";
    ice_servers = GetDefaultIceServers();
  }

  std::move(callback).Run(std::move(ice_servers));
}

std::vector<::sharing::mojom::IceServerPtr>
TachyonIceConfigFetcher::ParseIceServersResponse(
    const std::string& serialized_proto,
    const std::string& request_id) {
  std::vector<::sharing::mojom::IceServerPtr> servers_mojo;
  tachyon_proto::GetICEServerResponse response;
  if (!response.ParseFromString(serialized_proto)) {
    CD_LOG(ERROR, Feature::NEARBY_INFRA)
        << __func__ << ": (request_id=" << request_id
        << ") Failed to parse response";
    return servers_mojo;
  }

  const tachyon_proto::ICEConfiguration& ice_config = response.ice_config();

  for (const tachyon_proto::ICEServerList& server : ice_config.ice_servers()) {
    if (!server.urls_size())
      continue;

    ::sharing::mojom::IceServerPtr server_mojo(
        ::sharing::mojom::IceServer::New());
    for (const std::string& url : server.urls()) {
      server_mojo->urls.emplace_back(url);
    }

    if (!server.username().empty())
      server_mojo->username.emplace(server.username());

    if (!server.credential().empty())
      server_mojo->credential.emplace(server.credential());

    servers_mojo.push_back(std::move(server_mojo));
  }

  if (ice_config.has_lifetime_duration()) {
    ice_server_cache_ = CloneIceServerList(servers_mojo);
    ice_server_cache_expiration_ =
        base::Time::Now() +
        base::Seconds(ice_config.lifetime_duration().seconds());
  }
  return servers_mojo;
}