// Copyright 2020 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/client/nearby_share_client_impl.h"
#include <memory>
#include "base/base64url.h"
#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/memory/ptr_util.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/nearby_sharing/client/nearby_share_http_notifier.h"
#include "chrome/browser/nearby_sharing/common/nearby_share_switches.h"
#include "chromeos/ash/components/nearby/common/client/nearby_api_call_flow_impl.h"
#include "chromeos/ash/components/nearby/common/client/nearby_http_result.h"
#include "components/cross_device/logging/logging.h"
#include "components/signin/public/base/consent_level.h"
#include "components/signin/public/identity_manager/access_token_info.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/primary_account_access_token_fetcher.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#include "third_party/nearby/sharing/proto/certificate_rpc.pb.h"
#include "third_party/nearby/sharing/proto/contact_rpc.pb.h"
#include "third_party/nearby/sharing/proto/device_rpc.pb.h"
#include "third_party/nearby/sharing/proto/rpc_resources.pb.h"
namespace {
// -------------------- Nearby Share Service v1 Endpoints --------------------
const char kDefaultNearbyShareV1HTTPHost[] =
"https://nearbysharing-pa.googleapis.com";
const char kNearbyShareV1Path[] = "v1/";
const char kListContactPeoplePath[] = "contactRecords";
const char kListPublicCertificatesPath[] = "publicCertificates";
const char kPageSize[] = "page_size";
const char kPageToken[] = "page_token";
const char kSecretIds[] = "secret_ids";
const char kNearbyShareOAuth2Scope[] =
"https://www.googleapis.com/auth/nearbysharing-pa";
// Creates the full Nearby Share v1 URL for endpoint to the API with
// |request_path|.
GURL CreateV1RequestUrl(const std::string& request_path) {
base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
GURL google_apis_url = command_line->HasSwitch(switches::kNearbyShareHTTPHost)
? GURL(command_line->GetSwitchValueASCII(
switches::kNearbyShareHTTPHost))
: GURL(kDefaultNearbyShareV1HTTPHost);
return google_apis_url.Resolve(kNearbyShareV1Path + request_path);
}
ash::nearby::NearbyApiCallFlow::QueryParameters
ListContactPeopleRequestToQueryParameters(
const nearby::sharing::proto::ListContactPeopleRequest& request) {
ash::nearby::NearbyApiCallFlow::QueryParameters query_parameters;
if (request.page_size() > 0) {
query_parameters.emplace_back(kPageSize,
base::NumberToString(request.page_size()));
}
if (!request.page_token().empty()) {
query_parameters.emplace_back(kPageToken, request.page_token());
}
return query_parameters;
}
ash::nearby::NearbyApiCallFlow::QueryParameters
ListPublicCertificatesRequestToQueryParameters(
const nearby::sharing::proto::ListPublicCertificatesRequest& request) {
ash::nearby::NearbyApiCallFlow::QueryParameters query_parameters;
if (request.page_size() > 0) {
query_parameters.emplace_back(kPageSize,
base::NumberToString(request.page_size()));
}
if (!request.page_token().empty()) {
query_parameters.emplace_back(kPageToken, request.page_token());
}
for (const std::string& id : request.secret_ids()) {
// NOTE: One Platform requires that byte fields be URL-safe base64 encoded.
std::string encoded_id;
base::Base64UrlEncode(id, base::Base64UrlEncodePolicy::INCLUDE_PADDING,
&encoded_id);
query_parameters.emplace_back(kSecretIds, encoded_id);
}
return query_parameters;
}
// TODO(crbug.com/1103471): Update "chrome_policy" when a Nearby Share
// enterprise policy is created.
const net::PartialNetworkTrafficAnnotationTag& GetUpdateDeviceAnnotation() {
static const net::PartialNetworkTrafficAnnotationTag annotation =
net::DefinePartialNetworkTrafficAnnotation("nearby_share_update_device",
"oauth2_api_call_flow",
R"(
semantics {
sender: "Nearby Share"
description:
"Used as part of the Nearby Share feature that allows users to "
"share files or text with trusted contacts within a certain physical "
"proximity. The call sends the local device's user-defined name and "
"their list of allowed contacts to the Google-owned Nearby server. "
"Nearby-Share-specific crypto data from the local device is also "
"uploaded to the server and distributed to trusted contacts to help "
"establish an authenticated channel during the Nearby Share flow. "
"This crypto data can be immediately invalidated by the local device "
"at any time without needing to communicate with the server. For "
"example, it expires after three days and new data needs to be "
"uploaded. Crypto data is also invalidated if the user's list of "
"allowed contacts changes. The server returns the local device "
"user's full name and icon URL if available on the Google server."
trigger:
"Automatically called daily to retrieve any updates to the user's "
"full name or icon URL. This request is also sent whenever the user "
"changes their device name in settings, whenever the user changes "
"their list of allowed contacts, or whenever new crypto data is "
"generated by the local device and needs to be shared with trusted "
"contacts."
data:
"Sends an OAuth 2.0 token, the local device's name, contact, and/or "
"Nearby-Share-specific crypto data. Possibly receives the user's "
"full name and icon URL from the Google server."
destination: GOOGLE_OWNED_SERVICE
}
policy {
setting:
"Only sent when Nearby Share is enabled and the user is signed in "
"with their Google account."
chrome_policy {
SigninAllowed {
SigninAllowed: false
}
}
})");
return annotation;
}
// TODO(crbug.com/1103471): Update "chrome_policy" when a Nearby Share
// enterprise policy is created.
const net::PartialNetworkTrafficAnnotationTag& GetContactsAnnotation() {
static const net::PartialNetworkTrafficAnnotationTag annotation =
net::DefinePartialNetworkTrafficAnnotation("nearby_share_contacts",
"oauth2_api_call_flow",
R"(
semantics {
sender: "Nearby Share"
description:
"Used as part of the Nearby Share feature that allows users to "
"share files or text with trusted contacts within a certain physical "
"proximity. The call retrieves the user's list of contacts from the "
"Google-owned People server via the Google-owned Nearby server."
trigger:
"Called multiple times a day to check for possible updates to the "
"users's contact list. It is also invoked during Nearby Share "
"onboarding and when the user is in the Nearby Share settings."
data:
"Sends an OAuth 2.0 token. Receives the user's contact list, which "
"includes phone numbers and email addresses."
destination: GOOGLE_OWNED_SERVICE
}
policy {
setting:
"Only sent when Nearby Share is enabled and the user is signed in "
"with their Google account."
chrome_policy {
SigninAllowed {
SigninAllowed: false
}
}
})");
return annotation;
}
// TODO(crbug.com/1103471): Update "chrome_policy" when a Nearby Share
// enterprise policy is created.
const net::PartialNetworkTrafficAnnotationTag&
GetListPublicCertificatesAnnotation() {
static const net::PartialNetworkTrafficAnnotationTag annotation =
net::DefinePartialNetworkTrafficAnnotation(
"nearby_share_list_public_certificates", "oauth2_api_call_flow",
R"(
semantics {
sender: "Nearby Share"
description:
"Used as part of the Nearby Share feature that allows users to "
"share files or text with trusted contacts within a certain physical "
"proximity. The call retrieves Nearby-Share-specific crypto data "
"from the Google-owned Nearby server. The data was uploaded by other "
"devices and is needed to establish an authenticated connection with "
"those device during the Nearby Share flow."
trigger:
"Automatically called at least once a day to retrieve any updates to "
"the list of crypto data. It is also called when Nearby Share is in "
"use to ensure up-to-date data."
data:
"Sends an OAuth 2.0 token. Receives Nearby-Share-specific crypto "
"necessary for establishing an authenticated channel with other "
"devices."
destination: GOOGLE_OWNED_SERVICE
}
policy {
setting:
"Only sent when Nearby Share is enabled and the user is signed in "
"with their Google account."
chrome_policy {
SigninAllowed {
SigninAllowed: false
}
}
})");
return annotation;
}
} // namespace
NearbyShareClientImpl::NearbyShareClientImpl(
std::unique_ptr<ash::nearby::NearbyApiCallFlow> api_call_flow,
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
NearbyShareHttpNotifier* notifier)
: api_call_flow_(std::move(api_call_flow)),
identity_manager_(identity_manager),
url_loader_factory_(std::move(url_loader_factory)),
notifier_(notifier),
has_call_started_(false) {}
NearbyShareClientImpl::~NearbyShareClientImpl() = default;
void NearbyShareClientImpl::UpdateDevice(
const nearby::sharing::proto::UpdateDeviceRequest& request,
UpdateDeviceCallback&& callback,
ErrorCallback&& error_callback) {
notifier_->NotifyOfRequest(request);
MakeApiCall(CreateV1RequestUrl(request.device().name()), RequestType::kPatch,
request.SerializeAsString(),
/*request_as_query_parameters=*/std::nullopt, std::move(callback),
std::move(error_callback), GetUpdateDeviceAnnotation());
}
void NearbyShareClientImpl::ListContactPeople(
const nearby::sharing::proto::ListContactPeopleRequest& request,
ListContactPeopleCallback&& callback,
ErrorCallback&& error_callback) {
notifier_->NotifyOfRequest(request);
MakeApiCall(CreateV1RequestUrl(kListContactPeoplePath), RequestType::kGet,
/*serialized_request=*/std::nullopt,
ListContactPeopleRequestToQueryParameters(request),
std::move(callback), std::move(error_callback),
GetContactsAnnotation());
}
void NearbyShareClientImpl::ListPublicCertificates(
const nearby::sharing::proto::ListPublicCertificatesRequest& request,
ListPublicCertificatesCallback&& callback,
ErrorCallback&& error_callback) {
notifier_->NotifyOfRequest(request);
MakeApiCall(
CreateV1RequestUrl(request.parent() + "/" + kListPublicCertificatesPath),
RequestType::kGet, /*serialized_request=*/std::nullopt,
ListPublicCertificatesRequestToQueryParameters(request),
std::move(callback), std::move(error_callback),
GetListPublicCertificatesAnnotation());
}
std::string NearbyShareClientImpl::GetAccessTokenUsed() {
return access_token_used_;
}
template <class ResponseProto>
void NearbyShareClientImpl::MakeApiCall(
const GURL& request_url,
RequestType request_type,
const std::optional<std::string>& serialized_request,
const std::optional<ash::nearby::NearbyApiCallFlow::QueryParameters>&
request_as_query_parameters,
base::OnceCallback<void(const ResponseProto&)>&& response_callback,
ErrorCallback&& error_callback,
const net::PartialNetworkTrafficAnnotationTag& partial_traffic_annotation) {
DCHECK(!has_call_started_)
<< "NearbyShareClientImpl::MakeApiCall(): Tried to make an API "
<< "call, but the client had already been used.";
has_call_started_ = true;
api_call_flow_->SetPartialNetworkTrafficAnnotation(
partial_traffic_annotation);
request_url_ = request_url;
error_callback_ = std::move(error_callback);
OAuth2AccessTokenManager::ScopeSet scopes;
scopes.insert(kNearbyShareOAuth2Scope);
access_token_fetcher_ =
std::make_unique<signin::PrimaryAccountAccessTokenFetcher>(
"nearby_share_client", identity_manager_, scopes,
base::BindOnce(
&NearbyShareClientImpl::OnAccessTokenFetched<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(), request_type, serialized_request,
request_as_query_parameters, std::move(response_callback)),
signin::PrimaryAccountAccessTokenFetcher::Mode::kWaitUntilAvailable,
signin::ConsentLevel::kSignin);
}
template <class ResponseProto>
void NearbyShareClientImpl::OnAccessTokenFetched(
RequestType request_type,
const std::optional<std::string>& serialized_request,
const std::optional<ash::nearby::NearbyApiCallFlow::QueryParameters>&
request_as_query_parameters,
base::OnceCallback<void(const ResponseProto&)>&& response_callback,
GoogleServiceAuthError error,
signin::AccessTokenInfo access_token_info) {
access_token_fetcher_.reset();
if (error.state() != GoogleServiceAuthError::NONE) {
OnApiCallFailed(ash::nearby::NearbyHttpError::kAuthenticationError);
return;
}
access_token_used_ = access_token_info.token;
switch (request_type) {
case RequestType::kGet:
DCHECK(request_as_query_parameters && !serialized_request);
api_call_flow_->StartGetRequest(
request_url_, *request_as_query_parameters, url_loader_factory_,
access_token_used_,
base::BindOnce(&NearbyShareClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(),
std::move(response_callback)),
base::BindOnce(&NearbyShareClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
case RequestType::kPost:
DCHECK(serialized_request && !request_as_query_parameters);
api_call_flow_->StartPostRequest(
request_url_, *serialized_request, url_loader_factory_,
access_token_used_,
base::BindOnce(&NearbyShareClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(),
std::move(response_callback)),
base::BindOnce(&NearbyShareClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
case RequestType::kPatch:
DCHECK(serialized_request && !request_as_query_parameters);
api_call_flow_->StartPatchRequest(
request_url_, *serialized_request, url_loader_factory_,
access_token_used_,
base::BindOnce(&NearbyShareClientImpl::OnFlowSuccess<ResponseProto>,
weak_ptr_factory_.GetWeakPtr(),
std::move(response_callback)),
base::BindOnce(&NearbyShareClientImpl::OnApiCallFailed,
weak_ptr_factory_.GetWeakPtr()));
break;
}
}
template <class ResponseProto>
void NearbyShareClientImpl::OnFlowSuccess(
base::OnceCallback<void(const ResponseProto&)>&& result_callback,
const std::string& serialized_response) {
ResponseProto response;
if (!response.ParseFromString(serialized_response)) {
OnApiCallFailed(ash::nearby::NearbyHttpError::kResponseMalformed);
return;
}
notifier_->NotifyOfResponse(response);
std::move(result_callback).Run(response);
}
void NearbyShareClientImpl::OnApiCallFailed(
ash::nearby::NearbyHttpError error) {
CD_LOG(ERROR, Feature::NS)
<< "Nearby Share RPC call failed with error " << error;
std::move(error_callback_).Run(error);
}
NearbyShareClientFactoryImpl::NearbyShareClientFactoryImpl(
signin::IdentityManager* identity_manager,
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory,
NearbyShareHttpNotifier* notifier)
: identity_manager_(identity_manager),
url_loader_factory_(std::move(url_loader_factory)),
notifier_(notifier) {}
NearbyShareClientFactoryImpl::~NearbyShareClientFactoryImpl() = default;
std::unique_ptr<NearbyShareClient>
NearbyShareClientFactoryImpl::CreateInstance() {
return std::make_unique<NearbyShareClientImpl>(
std::make_unique<ash::nearby::NearbyApiCallFlowImpl>(), identity_manager_,
url_loader_factory_, notifier_);
}