[go: nahoru, domu]

blob: f70b3ed28e2e69eb666c102e4465576bf8df2e31 [file] [log] [blame]
// Copyright 2023 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/host/setup/corp_host_starter.h"
#include <memory>
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/scoped_refptr.h"
#include "base/memory/weak_ptr.h"
#include "base/strings/string_util.h"
#include "base/task/single_thread_task_runner.h"
#include "base/threading/platform_thread.h"
#include "base/uuid.h"
#include "base/values.h"
#include "google_apis/gaia/gaia_oauth_client.h"
#include "google_apis/google_api_keys.h"
#include "net/base/network_interfaces.h"
#include "remoting/base/corp_service_client.h"
#include "remoting/base/hostname.h"
#include "remoting/base/protobuf_http_status.h"
#include "remoting/base/rsa_key_pair.h"
#include "remoting/host/host_config.h"
#include "remoting/host/pin_hash.h"
#include "remoting/host/setup/buildflags.h"
#include "remoting/host/setup/daemon_controller.h"
#include "remoting/host/setup/host_starter.h"
#include "services/network/public/cpp/shared_url_loader_factory.h"
#if BUILDFLAG(REMOTING_INTERNAL)
#include "remoting/internal/proto/helpers.h"
#else
#include "remoting/proto/internal_stubs.h" // nogncheck
#endif
namespace remoting {
namespace {
constexpr int kMaxGetTokensRetries = 3;
// A helper class which provisions a corp machine for Chrome Remote Desktop.
class CorpHostStarter : public HostStarter,
public gaia::GaiaOAuthClient::Delegate {
public:
CorpHostStarter(std::unique_ptr<gaia::GaiaOAuthClient> oauth_client,
std::unique_ptr<CorpServiceClient> corp_service_client);
CorpHostStarter(const CorpHostStarter&) = delete;
CorpHostStarter& operator=(const CorpHostStarter&) = delete;
~CorpHostStarter() override;
// HostStarter implementation.
void StartHost(Params params, CompletionCallback on_done) override;
// gaia::GaiaOAuthClient::Delegate implementation.
void OnGetTokensResponse(const std::string& refresh_token,
const std::string& access_token,
int expires_in_seconds) override;
void OnRefreshTokenResponse(const std::string& access_token,
int expires_in_seconds) override;
void OnGetUserEmailResponse(const std::string& user_email) override;
void OnOAuthError() override;
void OnNetworkError(int response_code) override;
private:
void StartHostProcess();
void OnExistingConfigLoaded(absl::optional<base::Value::Dict> config);
void OnProvisionCorpMachineResponse(
const ProtobufHttpStatus& status,
std::unique_ptr<internal::RemoteAccessHostV1Proto> response);
void OnHostStarted(DaemonController::AsyncResult result);
void OnHostStopped(DaemonController::AsyncResult result);
void GetOAuthTokens();
void NotifyError(const ProtobufHttpStatus& status);
Params start_host_params_;
std::string host_refresh_token_;
std::string service_account_email_;
scoped_refptr<remoting::RsaKeyPair> key_pair_;
bool has_existing_host_instance_ = false;
std::unique_ptr<gaia::GaiaOAuthClient> oauth_client_;
std::unique_ptr<CorpServiceClient> corp_service_client_;
scoped_refptr<remoting::DaemonController> daemon_controller_ =
remoting::DaemonController::Create();
gaia::OAuthClientInfo oauth_client_info_;
std::string authorization_code_;
CompletionCallback on_done_;
scoped_refptr<base::SingleThreadTaskRunner> main_task_runner_;
base::WeakPtr<CorpHostStarter> weak_ptr_;
base::WeakPtrFactory<CorpHostStarter> weak_ptr_factory_{this};
};
CorpHostStarter::CorpHostStarter(
std::unique_ptr<gaia::GaiaOAuthClient> oauth_client,
std::unique_ptr<CorpServiceClient> corp_service_client)
: oauth_client_(std::move(oauth_client)),
corp_service_client_(std::move(corp_service_client)) {
weak_ptr_ = weak_ptr_factory_.GetWeakPtr();
main_task_runner_ = base::SingleThreadTaskRunner::GetCurrentDefault();
}
CorpHostStarter::~CorpHostStarter() = default;
void CorpHostStarter::StartHost(Params params, CompletionCallback on_done) {
DCHECK(main_task_runner_->BelongsToCurrentThread());
DCHECK(!on_done_);
start_host_params_ = std::move(params);
if (start_host_params_.name.empty()) {
// Use the host FQDN if a name was not provided via the command line.
start_host_params_.name = GetHostname();
}
on_done_ = std::move(on_done);
key_pair_ = RsaKeyPair::Generate();
// Check to see if there is an existing host instance on this machine which
// needs to be cleaned up before we can create and start a new host instance.
daemon_controller_->GetConfig(
base::BindOnce(&CorpHostStarter::OnExistingConfigLoaded, weak_ptr_));
}
void CorpHostStarter::OnExistingConfigLoaded(
absl::optional<base::Value::Dict> config) {
absl::optional<std::string> existing_host_id;
if (config.has_value()) {
std::string* host_id = config->FindString("host_id");
if (host_id) {
has_existing_host_instance_ = true;
existing_host_id.emplace(*host_id);
// Formatted to make start_host output more readable.
LOG(INFO) << "\n Found existing host: `" << *existing_host_id << "`.\n"
<< " This instance will be deleted from the Directory before "
<< "creating the new host instance.";
}
}
corp_service_client_->ProvisionCorpMachine(
start_host_params_.owner_email, start_host_params_.name,
key_pair_->GetPublicKey(), std::move(existing_host_id),
base::BindOnce(&CorpHostStarter::OnProvisionCorpMachineResponse,
weak_ptr_));
}
void CorpHostStarter::OnGetTokensResponse(const std::string& refresh_token,
const std::string& access_token,
int expires_in_seconds) {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&CorpHostStarter::OnGetTokensResponse, weak_ptr_,
refresh_token, access_token, expires_in_seconds));
return;
}
// Store the refresh token since we will eventually write it into the config.
host_refresh_token_ = refresh_token;
// Get the email corresponding to the access token, since we don't need the
// access token for anything else, we don't store it.
oauth_client_->GetUserEmail(access_token, 1, this);
}
void CorpHostStarter::OnRefreshTokenResponse(const std::string& access_token,
int expires_in_seconds) {
// We never request a new access token, so this call is not expected.
NOTREACHED();
}
void CorpHostStarter::OnGetUserEmailResponse(const std::string& user_email) {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&CorpHostStarter::OnGetUserEmailResponse,
weak_ptr_, user_email));
return;
}
if (service_account_email_.compare(base::ToLowerASCII(user_email)) != 0) {
LOG(ERROR)
<< "authorization_code was created for `" << user_email << "` "
<< "which does not match the service account created for the host: `"
<< service_account_email_ << "`";
std::move(on_done_).Run(OAUTH_ERROR);
return;
}
StartHostProcess();
}
void CorpHostStarter::OnProvisionCorpMachineResponse(
const ProtobufHttpStatus& status,
std::unique_ptr<internal::RemoteAccessHostV1Proto> response) {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&CorpHostStarter::OnProvisionCorpMachineResponse,
weak_ptr_, status, std::move(response)));
return;
}
if (!status.ok()) {
NotifyError(status);
return;
}
authorization_code_ = internal::GetAuthorizationCode(*response);
if (authorization_code_.empty()) {
LOG(ERROR) << "No authorization code returned by the Directory.";
std::move(on_done_).Run(START_ERROR);
return;
}
service_account_email_ =
base::ToLowerASCII(internal::GetServiceAccount(*response));
start_host_params_.id = internal::GetHostId(*response);
if (has_existing_host_instance_) {
daemon_controller_->Stop(
base::BindOnce(&CorpHostStarter::OnHostStopped, weak_ptr_));
} else {
GetOAuthTokens();
}
}
void CorpHostStarter::OnHostStopped(DaemonController::AsyncResult result) {
bool stopped = false;
for (auto i = 0; !stopped && i < 10; i++) {
LOG(INFO) << "Attempting to stop the existing host instance...";
stopped =
(daemon_controller_->GetState() == DaemonController::STATE_STOPPED);
if (!stopped) {
base::PlatformThread::Sleep(base::Seconds(1));
}
}
if (!stopped) {
LOG(WARNING) << "Unable to stop existing host process. Setup will "
<< "continue, but you may need to restart the host to "
<< "complete it.";
} else {
LOG(INFO) << "Existing host instance stopped.";
}
GetOAuthTokens();
}
void CorpHostStarter::GetOAuthTokens() {
LOG(INFO) << "Requesting OAuth tokens for the robot account.";
// Now retrieve the access and refresh tokens for the service account.
oauth_client_info_.client_id =
google_apis::GetOAuth2ClientID(google_apis::CLIENT_REMOTING_HOST);
oauth_client_info_.client_secret =
google_apis::GetOAuth2ClientSecret(google_apis::CLIENT_REMOTING_HOST);
oauth_client_->GetTokensFromAuthCode(oauth_client_info_, authorization_code_,
kMaxGetTokensRetries, this);
}
void CorpHostStarter::StartHostProcess() {
LOG(INFO) << "Starting new host instance.";
// Start the host.
base::Value::Dict config;
config.Set(kHostOwnerConfigPath, start_host_params_.owner_email);
// Write `service_account_email_` twice for backwards compatibility reasons.
// If the host config only contains `service_account` and the package is down-
// graded, the host will go offline because `xmpp_login` will not be present.
// TODO(joedow): Remove the dual-write logic once M120 is rollback-safe.
config.Set(kServiceAccountConfigPath, service_account_email_);
config.Set(kDeprecatedXmppLoginConfigPath, service_account_email_);
config.Set(kOAuthRefreshTokenConfigPath, host_refresh_token_);
config.Set(kHostIdConfigPath, start_host_params_.id);
config.Set(kHostNameConfigPath, start_host_params_.name);
config.Set(kPrivateKeyConfigPath, key_pair_->ToString());
// Note: Hosts configured using this class do not have a PIN and therefore we
// do not need to store a hash value in the config for it.
daemon_controller_->SetConfigAndStart(
std::move(config), start_host_params_.enable_crash_reporting,
base::BindOnce(&CorpHostStarter::OnHostStarted, base::Unretained(this)));
}
void CorpHostStarter::OnHostStarted(DaemonController::AsyncResult result) {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE,
base::BindOnce(&CorpHostStarter::OnHostStarted, weak_ptr_, result));
return;
}
if (result != DaemonController::RESULT_OK) {
LOG(ERROR) << "Failed to start host: " << result;
// TODO(joedow): Decide whether to delete the host instance or update its
// offline reason in the Directory instead.
// TODO(joedow): Check to see if we need to run on_done here. If so, also
// check to see if the OAuth-based HostStarter needs that change as well.
return;
}
std::move(on_done_).Run(START_COMPLETE);
}
void CorpHostStarter::OnOAuthError() {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&CorpHostStarter::OnOAuthError, weak_ptr_));
return;
}
std::move(on_done_).Run(OAUTH_ERROR);
}
void CorpHostStarter::OnNetworkError(int response_code) {
if (!main_task_runner_->BelongsToCurrentThread()) {
main_task_runner_->PostTask(
FROM_HERE, base::BindOnce(&CorpHostStarter::OnNetworkError, weak_ptr_,
response_code));
return;
}
std::move(on_done_).Run(NETWORK_ERROR);
}
void CorpHostStarter::NotifyError(const ProtobufHttpStatus& status) {
ProtobufHttpStatus::Code error_code = status.error_code();
LOG(ERROR) << "\n Received error code: " << static_cast<int>(error_code)
<< ", message: " << status.error_message();
if (!status.response_body().empty()) {
// TODO(joedow): Parse this output in //remoting/internal and return a
// concise error message and accurate error code to increase debugability.
size_t pos = status.response_body().rfind("Caused by: ");
if (pos != std::string::npos) {
LOG(ERROR) << "\n Extended error information: \n"
<< status.response_body().substr(pos);
VLOG(1) << "\n Full error information: \n" << status.response_body();
} else {
LOG(ERROR) << "\n Failed to find extended error information, showing "
<< "full output:\n"
<< status.response_body();
}
}
// TODO(joedow): Add more error codes to HostStarter and use them here.
switch (error_code) {
case ProtobufHttpStatus::Code::PERMISSION_DENIED:
case ProtobufHttpStatus::Code::UNAUTHENTICATED:
std::move(on_done_).Run(OAUTH_ERROR);
return;
default:
std::move(on_done_).Run(NETWORK_ERROR);
}
}
} // namespace
std::unique_ptr<HostStarter> ProvisionCorpMachine(
scoped_refptr<network::SharedURLLoaderFactory> url_loader_factory) {
return std::make_unique<CorpHostStarter>(
std::make_unique<gaia::GaiaOAuthClient>(url_loader_factory),
std::make_unique<CorpServiceClient>(url_loader_factory));
}
} // namespace remoting