| // Copyright 2025 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/compute_engine_service_client.h" |
| |
| #include <memory> |
| #include <optional> |
| #include <string> |
| #include <string_view> |
| #include <utility> |
| |
| #include "base/strings/stringprintf.h" |
| #include "net/base/load_flags.h" |
| #include "net/base/net_errors.h" |
| #include "net/traffic_annotation/network_traffic_annotation.h" |
| #include "remoting/base/http_status.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 "services/network/public/mojom/url_response_head.mojom.h" |
| |
| namespace remoting { |
| |
| namespace { |
| |
| // Compute Engine VM Instances always have an HTTP metadata server endpoint. |
| // Shielded Instances also provide an HTTPS endpoint. For compatibility, we |
| // use the HTTP endpoint for fail-fast decisions or to provide an identity token |
| // but rely on our backend services to validate the token and metadata. |
| // TODO: joedow - Add support for the HTTPS endpoint: |
| // https://cloud.google.com/compute/docs/metadata/querying-metadata#query-https-mds |
| constexpr char kHttpMetadataBaseUrl[] = |
| "http://metadata.google.internal/computeMetadata/v1/instance/" |
| "service-accounts/default"; |
| |
| constexpr size_t kMaxResponseSize = 4096; |
| |
| constexpr net::NetworkTrafficAnnotationTag kInstanceIdentityTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation( |
| "remoting_compute_engine_instance_identity_token", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Retrieves a Compute Engine VM Instance Identity token for use by " |
| "Chrome Remote Desktop." |
| trigger: |
| "The request is made when a Compute Engine VM Instance is " |
| "configured for remote acces via Chrome Remote Desktop. It is also " |
| "called when the host instance makes a backend service request as " |
| "the identity token is used to verify the origin of the request." |
| data: "Arbitrary payload data used to prevent replay attacks." |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { |
| email: "chromoting-team@google.com" |
| } |
| } |
| user_data { |
| type: ARBITRARY_DATA |
| } |
| last_reviewed: "2025-02-08" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request will not be sent if Chrome Remote Desktop is not " |
| "used within a Compute Engine VM Instance." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag kAccessTokenTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation( |
| "remoting_compute_engine_instance_access_token", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Retrieves an OAuth access token for the default service account " |
| "associated with the Compute Engine VM Instance." |
| trigger: |
| "The request is made when Chrome Remote Desktop is being run in a " |
| "Compute Engine Instance and needs to send a request to our API " |
| "using the default service account for the Compute Engine Instance." |
| data: "None" |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { |
| email: "chromoting-team@google.com" |
| } |
| } |
| user_data { |
| type: NONE |
| } |
| last_reviewed: "2025-02-08" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request will not be sent if Chrome Remote Desktop is not " |
| "used within a Compute Engine VM Instance." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| constexpr net::NetworkTrafficAnnotationTag kAccessTokenScopesTrafficAnnotation = |
| net::DefineNetworkTrafficAnnotation( |
| "remoting_compute_engine_instance_access_token_scopes", |
| R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Retrieves the set of OAuth scopes included in access tokens which " |
| "are generated for the default service account associated with the " |
| "Compute Engine VM Instance." |
| trigger: |
| "The request is made when Chrome Remote Desktop is being run in a " |
| "Compute Engine Instance and needs to determine if the default " |
| "service account has been configured properly to access our API." |
| data: "None" |
| destination: GOOGLE_OWNED_SERVICE |
| internal { |
| contacts { |
| email: "chromoting-team@google.com" |
| } |
| } |
| user_data { |
| type: NONE |
| } |
| last_reviewed: "2025-02-08" |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This request will not be sent if Chrome Remote Desktop is not " |
| "used within a Compute Engine VM Instance." |
| policy_exception_justification: |
| "Not implemented." |
| })"); |
| |
| } // namespace |
| |
| ComputeEngineServiceClient::ComputeEngineServiceClient( |
| scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) |
| : url_loader_factory_(url_loader_factory) {} |
| |
| ComputeEngineServiceClient::~ComputeEngineServiceClient() = default; |
| |
| void ComputeEngineServiceClient::GetInstanceIdentityToken( |
| std::string_view audience, |
| ResponseCallback callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| // Use 'format=full' to include project and instance details in the token. |
| ExecuteRequest(base::StringPrintf("%s/identity?audience=%s&format=full", |
| kHttpMetadataBaseUrl, audience), |
| kInstanceIdentityTrafficAnnotation, std::move(callback)); |
| } |
| |
| void ComputeEngineServiceClient::GetServiceAccountAccessToken( |
| ResponseCallback callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| ExecuteRequest(base::StringPrintf("%s/token", kHttpMetadataBaseUrl), |
| kAccessTokenTrafficAnnotation, std::move(callback)); |
| } |
| |
| void ComputeEngineServiceClient::GetServiceAccountScopes( |
| ResponseCallback callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| ExecuteRequest(base::StringPrintf("%s/scopes", kHttpMetadataBaseUrl), |
| kAccessTokenScopesTrafficAnnotation, std::move(callback)); |
| } |
| |
| void ComputeEngineServiceClient::CancelPendingRequests() { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| weak_ptr_factory_.InvalidateWeakPtrs(); |
| url_loader_.reset(); |
| } |
| |
| void ComputeEngineServiceClient::ExecuteRequest( |
| std::string_view url, |
| const net::NetworkTrafficAnnotationTag& network_annotation, |
| ResponseCallback callback) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| // TODO: joedow - Update to handle concurrent requests when needed. |
| CHECK(!url_loader_); |
| |
| auto resource_request = std::make_unique<network::ResourceRequest>(); |
| resource_request->url = GURL(url); |
| 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::kGetMethod; |
| // All Compute Engine Metadata requests must set this header. |
| resource_request->headers.SetHeader("Metadata-Flavor", "Google"); |
| |
| url_loader_ = network::SimpleURLLoader::Create(std::move(resource_request), |
| network_annotation); |
| url_loader_->SetTimeoutDuration(base::Seconds(60)); |
| url_loader_->SetAllowHttpErrorResults(false); |
| url_loader_->SetRetryOptions( |
| 3, network::SimpleURLLoader::RETRY_ON_5XX | |
| network::SimpleURLLoader::RETRY_ON_NETWORK_CHANGE); |
| |
| url_loader_->DownloadToString( |
| url_loader_factory_.get(), |
| base::BindOnce(&ComputeEngineServiceClient::OnRequestComplete, |
| weak_ptr_factory_.GetWeakPtr(), std::move(callback)), |
| kMaxResponseSize); |
| } |
| |
| void ComputeEngineServiceClient::OnRequestComplete( |
| ResponseCallback callback, |
| std::optional<std::string> response_body) { |
| DCHECK_CALLED_ON_VALID_THREAD(thread_checker_); |
| |
| net::Error net_error = static_cast<net::Error>(url_loader_->NetError()); |
| HttpStatus http_status = HttpStatus::OK(); |
| if (net_error != net::Error::OK && |
| net_error != net::Error::ERR_HTTP_RESPONSE_CODE_FAILURE) { |
| http_status = HttpStatus(net_error); |
| } else if (!url_loader_->ResponseInfo() || |
| !url_loader_->ResponseInfo()->headers || |
| url_loader_->ResponseInfo()->headers->response_code() <= 0) { |
| http_status = |
| HttpStatus(HttpStatus::Code::INTERNAL, |
| "Failed to get HTTP status from the response header."); |
| } else { |
| http_status = |
| HttpStatus(static_cast<net::HttpStatusCode>( |
| url_loader_->ResponseInfo()->headers->response_code()), |
| response_body.value_or(std::string())); |
| } |
| |
| if (!http_status.ok()) { |
| LOG(ERROR) << "Compute Engine API request failed. Code: " |
| << static_cast<int32_t>(http_status.error_code()) |
| << ", Message: " << http_status.error_message(); |
| } |
| |
| // Reset |url_loader_| since we've extracted the info we need from it. |
| // This will allow the caller to reuse this instance to make another request. |
| url_loader_.reset(); |
| |
| std::move(callback).Run(http_status); |
| } |
| |
| } // namespace remoting |