| // 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 "remoting/base/cloud_service_client.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <utility> |
| |
| #include "base/functional/bind.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/strings/string_split.h" |
| #include "base/strings/stringize_macros.h" |
| #include "google_apis/google_api_keys.h" |
| #include "net/http/http_request_headers.h" |
| #include "remoting/base/protobuf_http_request.h" |
| #include "remoting/base/protobuf_http_request_config.h" |
| #include "remoting/base/service_urls.h" |
| #include "remoting/base/version.h" |
| #include "remoting/proto/google/internal/remoting/cloud/v1alpha/empty.pb.h" |
| #include "remoting/proto/google/internal/remoting/cloud/v1alpha/network_traversal_service.pb.h" |
| #include "remoting/proto/google/internal/remoting/cloud/v1alpha/remote_access_host.pb.h" |
| #include "remoting/proto/google/internal/remoting/cloud/v1alpha/remote_access_service.pb.h" |
| #include "remoting/proto/google/internal/remoting/cloud/v1alpha/session_authz_service.pb.h" |
| #include "remoting/proto/google/remoting/cloud/v1/provisioning_service.pb.h" |
| #include "services/network/public/cpp/shared_url_loader_factory.h" |
| |
| namespace { |
| constexpr net::NetworkTrafficAnnotationTag kGenerateHostTokenTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("remoting_cloud_generate_host_token", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Generates a host token which is used to authorize a Chrome Remote " |
| "Desktop connection between a user and a 'Cloud host' running on " |
| "GCE (this machine)." |
| trigger: |
| "User connects to a remote access host instance running on GCE " |
| "which has been configured as a 'Cloud' host." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "An access token for the device robot account associated with this " |
| "machine." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-09-23" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the Chrome Remote Desktop host is not registered as a Cloud " |
| "host running on GCE." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag |
| kProvisionGceInstanceTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation( |
| "remoting_cloud_provision_gce_instance", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Registers a new Chrome Remote Desktop host for a GCE instance." |
| trigger: |
| "User runs the remoting_start_host command by typing it on the " |
| "terminal. Third party administrators might implement scripts to " |
| "run this automatically, but the Chrome Remote Desktop product " |
| "does not come with such scripts." |
| user_data { |
| type: EMAIL |
| type: OTHER |
| } |
| data: |
| "The email address of the account to configure CRD for and the " |
| "display name of the new remote access host instance which will be " |
| "shown in the Chrome Remote Desktop client website." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-03-29" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the start-host utility is not run with the cloud-user flag." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag kReauthorizeHostTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("remoting_cloud_reauthorize_host", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Reauthorizes the Chrome Remote Desktop connection between a user " |
| "and a 'Cloud host' running on GCE (this machine)." |
| trigger: |
| "User connects to a remote access host instance running on GCE " |
| "which has been configured as a 'Cloud' host." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "An access token for the device robot account associated with this " |
| "machine and an opaque session token generated by the backend " |
| "service." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-09-23" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the Chrome Remote Desktop host is not registered as a Cloud " |
| "host running on GCE." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag kSendHeartbeatTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("remoting_cloud_send_heartbeat", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Updates the last seen time in the Chrome Remote Desktop Directory " |
| "service for a given remote access host instance." |
| trigger: |
| "Configuring a CRD remote access host on a GCE Instance." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "An internal UUID to identify the remote access host instance." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-09-18" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the CRD host is not configured and run as a Cloud host." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag |
| kUpdateRemoteAccessHostTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation( |
| "remoting_cloud_update_remote_access_host", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Updates the Chrome Remote Desktop Directory service with " |
| "environment details and signaling information for a given remote " |
| "access host instance." |
| trigger: |
| "Configuring a CRD remote access host on a GCE Instance." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "Includes an internal UUID to identify the remote access host " |
| "instance, the name and version of the operating system, the " |
| "version of the CRD package installed, and a set of signaling ids " |
| "which the CRD client can use to send the host messages." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-09-18" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the CRD host is not configured and run as a Cloud host." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag |
| kVerifySessionTokenTrafficAnnotation = net::DefineNetworkTrafficAnnotation( |
| "remoting_cloud_verify_session_token", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Verifies a session token returned by the client device and " |
| "decodes the shared secret from the session token, to be used to " |
| "authorize a Chrome Remote Desktop connection between a user and a " |
| "Cloud host running on GCE (this device)." |
| trigger: |
| "User connects to a remote access host instance running on GCE " |
| "which has been configured as a 'Cloud' host." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "An access token for the device robot account associated with this " |
| "machine and an opaque session token generated by the backend " |
| "service." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-09-23" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the Chrome Remote Desktop host is not registered as a Cloud " |
| "host running on GCE." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag kGenerateIceConfigTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation("remoting_cloud_generate_ice_config", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Request used by Chrome Remote Desktop to fetch an ICE " |
| "(Interactive Connectivity Establishment) configuration which " |
| "contains a list of STUN (Session Traversal Utilities for NAT) & " |
| "TURN (Traversal Using Relay NAT) servers and TURN credentials. " |
| "Please see https://tools.ietf.org/html/rfc5245 for more details." |
| trigger: |
| "When a user connects to a remote access host instance running on " |
| "GCE which has been configured as a 'Cloud' host." |
| user_data { |
| type: OTHER |
| } |
| data: |
| "An access token for the device robot account associated with this " |
| "machine." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { owners: "//remoting/OWNERS" } |
| } |
| last_reviewed: "2024-10-08" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request cannot be stopped in settings, but will not be sent " |
| "if the Chrome Remote Desktop host is not registered as a Cloud " |
| "host running on GCE." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| // Remoting Cloud API using statements. |
| using ProvisionGceInstanceRequest = |
| google::remoting::cloud::v1::ProvisionGceInstanceRequest; |
| |
| // Remoting Cloud Private API using statements. |
| using Empty = google::internal::remoting::cloud::v1alpha::Empty; |
| using GenerateHostTokenRequest = |
| google::internal::remoting::cloud::v1alpha::GenerateHostTokenRequest; |
| using GenerateIceConfigRequest = |
| google::internal::remoting::cloud::v1alpha::GenerateIceConfigRequest; |
| using ReauthorizeHostRequest = |
| google::internal::remoting::cloud::v1alpha::ReauthorizeHostRequest; |
| using RemoteAccessHost = |
| google::internal::remoting::cloud::v1alpha::RemoteAccessHost; |
| using SendHeartbeatRequest = |
| google::internal::remoting::cloud::v1alpha::SendHeartbeatRequest; |
| using VerifySessionTokenRequest = |
| google::internal::remoting::cloud::v1alpha::VerifySessionTokenRequest; |
| |
| constexpr char kFtlResourceSeparator[] = "/chromoting_ftl_"; |
| |
| } // namespace |
| |
| namespace remoting { |
| |
| CloudServiceClient::CloudServiceClient( |
| const std::string& api_key, |
| OAuthTokenGetter* oauth_token_getter, |
| const std::string& base_service_url, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : api_key_(api_key), |
| http_client_(base_service_url, oauth_token_getter, url_loader_factory) {} |
| |
| CloudServiceClient::~CloudServiceClient() = default; |
| |
| void CloudServiceClient::ProvisionGceInstance( |
| const std::string& owner_email, |
| const std::string& display_name, |
| const std::string& public_key, |
| const std::optional<std::string>& existing_directory_id, |
| const std::optional<std::string>& instance_identity_token, |
| ProvisionGceInstanceCallback callback) { |
| constexpr char path[] = "/v1/provisioning:provisionGceInstance"; |
| |
| auto request = std::make_unique<ProvisionGceInstanceRequest>(); |
| request->set_owner_email(owner_email); |
| request->set_display_name(display_name); |
| request->set_public_key(public_key); |
| request->set_version(STRINGIZE(VERSION)); |
| if (existing_directory_id.has_value() && !existing_directory_id->empty()) { |
| request->set_existing_directory_id(*existing_directory_id); |
| } |
| if (instance_identity_token.has_value() && |
| !instance_identity_token->empty()) { |
| request->set_instance_identity_token(*instance_identity_token); |
| } |
| |
| ExecuteRequest(kProvisionGceInstanceTrafficAnnotation, path, api_key_, |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::SendHeartbeat(const std::string& directory_id, |
| std::string_view instance_identity_token, |
| SendHeartbeatCallback callback) { |
| constexpr char path[] = "/v1alpha/access:sendHeartbeat"; |
| |
| auto request = std::make_unique<SendHeartbeatRequest>(); |
| request->set_directory_id(directory_id); |
| request->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kSendHeartbeatTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::UpdateRemoteAccessHost( |
| const std::string& directory_id, |
| std::optional<std::string> host_version, |
| std::optional<std::string> signaling_id, |
| std::optional<std::string> offline_reason, |
| std::optional<std::string> os_name, |
| std::optional<std::string> os_version, |
| std::string_view instance_identity_token, |
| UpdateRemoteAccessHostCallback callback) { |
| constexpr char path[] = "/v1alpha/access:updateRemoteAccessHost"; |
| |
| auto host = std::make_unique<RemoteAccessHost>(); |
| |
| host->set_directory_id(directory_id); |
| if (host_version.has_value()) { |
| host->set_version(*host_version); |
| } |
| if (signaling_id.has_value()) { |
| auto parts = base::SplitStringUsingSubstr( |
| *signaling_id, kFtlResourceSeparator, base::TRIM_WHITESPACE, |
| base::SPLIT_WANT_ALL); |
| if (parts.size() == 2) { |
| host->mutable_tachyon_account_info()->set_account_id(std::move(parts[0])); |
| host->mutable_tachyon_account_info()->set_registration_id( |
| std::move(parts[1])); |
| } else { |
| LOG(WARNING) << "Invalid signaling_id provided: " << *signaling_id; |
| } |
| } |
| if (offline_reason.has_value()) { |
| host->set_offline_reason(*offline_reason); |
| } |
| if (os_name.has_value() && os_version.has_value()) { |
| host->mutable_operating_system_info()->set_name(*os_name); |
| host->mutable_operating_system_info()->set_version(*os_version); |
| } |
| host->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kUpdateRemoteAccessHostTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPatchMethod, std::move(host), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::GenerateHostToken( |
| std::string_view instance_identity_token, |
| GenerateHostTokenCallback callback) { |
| constexpr char path[] = "/v1alpha/sessionAuthz:generateHostToken"; |
| |
| auto request = std::make_unique<GenerateHostTokenRequest>(); |
| request->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kGenerateHostTokenTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::GenerateIceConfig( |
| std::string_view instance_identity_token, |
| GenerateIceConfigCallback callback) { |
| constexpr char path[] = "/v1alpha/networkTraversal:generateIceConfig"; |
| |
| auto request = std::make_unique<GenerateIceConfigRequest>(); |
| request->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kGenerateIceConfigTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::VerifySessionToken( |
| const std::string& session_token, |
| std::string_view instance_identity_token, |
| VerifySessionTokenCallback callback) { |
| constexpr char path[] = "/v1alpha/sessionAuthz:verifySessionToken"; |
| |
| auto request = std::make_unique<VerifySessionTokenRequest>(); |
| request->set_session_token(session_token); |
| request->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kVerifySessionTokenTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback)); |
| } |
| |
| void CloudServiceClient::ReauthorizeHost( |
| const std::string& session_reauth_token, |
| const std::string& session_id, |
| std::string_view instance_identity_token, |
| scoped_refptr<const ProtobufHttpRequestConfig::RetryPolicy> retry_policy, |
| ReauthorizeHostCallback callback) { |
| constexpr char path[] = "/v1alpha/sessionAuthz:reauthorizeHost"; |
| |
| auto request = std::make_unique<ReauthorizeHostRequest>(); |
| request->set_session_reauth_token(session_reauth_token); |
| request->set_session_id(session_id); |
| request->set_instance_identity_token(instance_identity_token); |
| |
| ExecuteRequest(kReauthorizeHostTrafficAnnotation, path, /*api_key=*/"", |
| net::HttpRequestHeaders::kPostMethod, std::move(request), |
| std::move(callback), std::move(retry_policy)); |
| } |
| |
| void CloudServiceClient::CancelPendingRequests() { |
| http_client_.CancelPendingRequests(); |
| } |
| |
| template <typename CallbackType> |
| void CloudServiceClient::ExecuteRequest( |
| const net::NetworkTrafficAnnotationTag& traffic_annotation, |
| const std::string& path, |
| const std::string& api_key, |
| const std::string& method, |
| std::unique_ptr<google::protobuf::MessageLite> request_message, |
| CallbackType callback, |
| scoped_refptr<const ProtobufHttpRequestConfig::RetryPolicy> retry_policy) { |
| auto request_config = |
| std::make_unique<ProtobufHttpRequestConfig>(traffic_annotation); |
| request_config->path = path; |
| if (api_key.empty()) { |
| request_config->authenticated = true; |
| } else { |
| request_config->api_key = api_key; |
| request_config->authenticated = false; |
| } |
| request_config->method = method; |
| request_config->retry_policy = std::move(retry_policy); |
| request_config->request_message = std::move(request_message); |
| auto request = |
| std::make_unique<ProtobufHttpRequest>(std::move(request_config)); |
| request->SetResponseCallback(std::move(callback)); |
| http_client_.ExecuteRequest(std::move(request)); |
| } |
| |
| // static |
| std::unique_ptr<CloudServiceClient> |
| CloudServiceClient::CreateForChromotingRobotAccount( |
| OAuthTokenGetter* oauth_token_getter, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { |
| return base::WrapUnique(new CloudServiceClient( |
| /*api_key=*/std::string(), oauth_token_getter, |
| ServiceUrls::GetInstance()->remoting_cloud_private_endpoint(), |
| url_loader_factory)); |
| } |
| |
| // static |
| std::unique_ptr<CloudServiceClient> CloudServiceClient::CreateForGcpProject( |
| const std::string& api_key, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { |
| return base::WrapUnique(new CloudServiceClient( |
| api_key, |
| /*oauth_token_getter=*/nullptr, |
| ServiceUrls::GetInstance()->remoting_cloud_public_endpoint(), |
| url_loader_factory)); |
| } |
| |
| // static |
| std::unique_ptr<CloudServiceClient> |
| CloudServiceClient::CreateForGceDefaultServiceAccount( |
| OAuthTokenGetter* oauth_token_getter, |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) { |
| return base::WrapUnique(new CloudServiceClient( |
| /*api_key=*/std::string(), oauth_token_getter, |
| ServiceUrls::GetInstance()->remoting_cloud_public_endpoint(), |
| url_loader_factory)); |
| } |
| |
| } // namespace remoting |