// 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;
}