blob: 0de3d8b1ea1b8da5384bebac93e5cf9ccda16b61 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/policy/remote_commands/crd_host_delegate.h"
#include "base/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "chrome/browser/ash/app_mode/arc/arc_kiosk_app_manager.h"
#include "chrome/browser/ash/app_mode/kiosk_app_manager.h"
#include "chrome/browser/ash/app_mode/web_app/web_kiosk_app_manager.h"
#include "chrome/browser/ash/profiles/profile_helper.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/device_identity/device_oauth2_token_service.h"
#include "chrome/browser/device_identity/device_oauth2_token_service_factory.h"
#include "components/user_manager/user_manager.h"
#include "content/public/browser/browser_task_traits.h"
#include "content/public/browser/browser_thread.h"
#include "content/public/browser/storage_partition.h"
#include "extensions/browser/api/messaging/native_message_host.h"
#include "google_apis/gaia/gaia_constants.h"
#include "net/base/load_flags.h"
#include "net/http/http_request_headers.h"
#include "remoting/host/it2me/it2me_constants.h"
#include "remoting/host/it2me/it2me_native_messaging_host_chromeos.h"
#include "ui/base/user_activity/user_activity_detector.h"
namespace policy {
namespace {
// Add a common prefix to all our logs, to make them easy to find.
#define CRD_DVLOG(level) DVLOG(level) << "CRD: "
#define CRD_LOG(level) LOG(level) << "CRD: "
// OAuth2 Token scopes
constexpr char kCloudDevicesOAuth2Scope[] =
"https://www.googleapis.com/auth/clouddevices";
constexpr char kChromotingRemoteSupportOAuth2Scope[] =
"https://www.googleapis.com/auth/chromoting.remote.support";
constexpr char kTachyonOAuth2Scope[] =
"https://www.googleapis.com/auth/tachyon";
class DefaultNativeMessageHostFactory
: public CRDHostDelegate::NativeMessageHostFactory {
public:
DefaultNativeMessageHostFactory() = default;
DefaultNativeMessageHostFactory(const DefaultNativeMessageHostFactory&) =
delete;
DefaultNativeMessageHostFactory& operator=(
const DefaultNativeMessageHostFactory&) = delete;
~DefaultNativeMessageHostFactory() override = default;
// CRDHostDelegate::NativeMessageHostFactory implementation:
std::unique_ptr<extensions::NativeMessageHost> CreateNativeMessageHostHost()
override {
return remoting::CreateIt2MeNativeMessagingHostForChromeOS(
content::GetIOThreadTaskRunner({}), content::GetUIThreadTaskRunner({}),
g_browser_process->policy_service());
}
};
std::string FormatErrorMessage(const std::string& error_state,
const base::Value& message) {
if (error_state == remoting::kHostStateDomainError) {
return "Invalid domain";
} else {
const std::string* error_code =
message.FindStringKey(remoting::kErrorMessageCode);
if (error_code)
return *error_code;
else
return "Unknown Error";
}
}
} // namespace
// Helper class that asynchronously fetches the OAuth token, and passes it to
// the given callback.
class CRDHostDelegate::OAuthTokenFetcher
: public OAuth2AccessTokenManager::Consumer {
public:
OAuthTokenFetcher(
DeviceOAuth2TokenService* oauth_service,
DeviceCommandStartCRDSessionJob::OAuthTokenCallback success_callback,
DeviceCommandStartCRDSessionJob::ErrorCallback error_callback)
: OAuth2AccessTokenManager::Consumer("crd_host_delegate"),
oauth_service_(*oauth_service),
success_callback_(std::move(success_callback)),
error_callback_(std::move(error_callback)) {
DCHECK(oauth_service);
}
OAuthTokenFetcher(const OAuthTokenFetcher&) = delete;
OAuthTokenFetcher& operator=(const OAuthTokenFetcher&) = delete;
~OAuthTokenFetcher() override = default;
void Start() {
CRD_DVLOG(1) << "Fetching OAuth access token";
OAuth2AccessTokenManager::ScopeSet scopes{
GaiaConstants::kGoogleUserInfoEmail, kCloudDevicesOAuth2Scope,
kChromotingRemoteSupportOAuth2Scope, kTachyonOAuth2Scope};
oauth_request_ = oauth_service_.StartAccessTokenRequest(scopes, this);
}
bool is_running() const { return oauth_request_ != nullptr; }
private:
// OAuth2AccessTokenManager::Consumer implementation:
void OnGetTokenSuccess(
const OAuth2AccessTokenManager::Request* request,
const OAuth2AccessTokenConsumer::TokenResponse& token_response) override {
CRD_DVLOG(1) << "Received OAuth access token";
std::move(success_callback_).Run(token_response.access_token);
oauth_request_.reset();
}
void OnGetTokenFailure(const OAuth2AccessTokenManager::Request* request,
const GoogleServiceAuthError& error) override {
CRD_DVLOG(1) << "Failed to get OAuth access token: " << error.ToString();
std::move(error_callback_)
.Run(DeviceCommandStartCRDSessionJob::FAILURE_NO_OAUTH_TOKEN,
error.ToString());
oauth_request_.reset();
}
DeviceOAuth2TokenService& oauth_service_;
DeviceCommandStartCRDSessionJob::OAuthTokenCallback success_callback_;
DeviceCommandStartCRDSessionJob::ErrorCallback error_callback_;
// Handler for the OAuth access token request.
// When deleted the token manager will cancel the request (and not call us).
std::unique_ptr<OAuth2AccessTokenManager::Request> oauth_request_;
};
CRDHostDelegate::CRDHostDelegate()
: CRDHostDelegate(std::make_unique<DefaultNativeMessageHostFactory>()) {}
CRDHostDelegate::CRDHostDelegate(
std::unique_ptr<NativeMessageHostFactory> factory)
: factory_(std::move(factory)) {
DCHECK(factory_);
}
CRDHostDelegate::~CRDHostDelegate() = default;
bool CRDHostDelegate::HasActiveSession() const {
return host_ != nullptr;
}
void CRDHostDelegate::TerminateSession(base::OnceClosure callback) {
DoShutdownHost();
std::move(callback).Run();
}
bool CRDHostDelegate::AreServicesReady() const {
return user_manager::UserManager::IsInitialized() &&
ui::UserActivityDetector::Get() != nullptr &&
chromeos::ProfileHelper::Get() != nullptr &&
oauth_service() != nullptr;
}
bool CRDHostDelegate::IsRunningKiosk() const {
auto* user_manager = user_manager::UserManager::Get();
if (!user_manager->IsLoggedInAsAnyKioskApp()) {
return false;
}
if (!GetKioskProfile())
return false;
if (user_manager->IsLoggedInAsKioskApp()) {
ash::KioskAppManager* manager = ash::KioskAppManager::Get();
if (manager->GetAutoLaunchApp().empty())
return false;
ash::KioskAppManager::App app;
CHECK(manager->GetApp(manager->GetAutoLaunchApp(), &app));
return app.was_auto_launched_with_zero_delay;
} else if (user_manager->IsLoggedInAsArcKioskApp()) {
return chromeos::ArcKioskAppManager::Get()
->current_app_was_auto_launched_with_zero_delay();
} else if (user_manager->IsLoggedInAsWebKioskApp()) {
return ash::WebKioskAppManager::Get()
->current_app_was_auto_launched_with_zero_delay();
}
NOTREACHED();
return false;
}
base::TimeDelta CRDHostDelegate::GetIdlenessPeriod() const {
return base::TimeTicks::Now() -
ui::UserActivityDetector::Get()->last_activity_time();
}
void CRDHostDelegate::FetchOAuthToken(
DeviceCommandStartCRDSessionJob::OAuthTokenCallback success_callback,
DeviceCommandStartCRDSessionJob::ErrorCallback error_callback) {
DCHECK(oauth_service());
DCHECK(!oauth_token_fetcher_ || !oauth_token_fetcher_->is_running());
oauth_token_fetcher_ = std::make_unique<OAuthTokenFetcher>(
oauth_service(), std::move(success_callback), std::move(error_callback));
oauth_token_fetcher_->Start();
}
void CRDHostDelegate::StartCRDHostAndGetCode(
const std::string& oauth_token,
bool terminate_upon_input,
DeviceCommandStartCRDSessionJob::AccessCodeCallback success_callback,
DeviceCommandStartCRDSessionJob::ErrorCallback error_callback) {
DCHECK(!host_);
DCHECK(!code_success_callback_);
DCHECK(!error_callback_);
DCHECK(oauth_service());
// Store all parameters for future connect call.
base::Value connect_params(base::Value::Type::DICTIONARY);
CoreAccountId account_id = oauth_service()->GetRobotAccountId();
// TODO(msarda): This conversion will not be correct once account id is
// migrated to be the Gaia ID on ChromeOS. Fix it.
std::string username = account_id.ToString();
connect_params.SetKey(remoting::kUserName, base::Value(username));
connect_params.SetKey(remoting::kAuthServiceWithToken,
base::Value("oauth2:" + oauth_token));
connect_params.SetKey(remoting::kSuppressUserDialogs, base::Value(true));
connect_params.SetKey(remoting::kSuppressNotifications, base::Value(true));
connect_params.SetKey(remoting::kTerminateUponInput,
base::Value(terminate_upon_input));
connect_params_ = std::move(connect_params);
remote_connected_ = false;
command_awaiting_crd_access_code_ = true;
code_success_callback_ = std::move(success_callback);
error_callback_ = std::move(error_callback);
// TODO(antrim): set up watchdog timer (reasonable cutoff).
host_ = factory_->CreateNativeMessageHostHost();
host_->Start(this);
base::Value params(base::Value::Type::DICTIONARY);
SendMessageToHost(remoting::kHelloMessage, params);
}
void CRDHostDelegate::PostMessageFromNativeHost(
const std::string& message_string) {
CRD_DVLOG(1) << "Received message from CRD host: " << message_string;
base::Optional<base::Value> message = base::JSONReader::Read(message_string);
if (!message) {
OnProtocolBroken("Message is invalid JSON");
return;
}
if (!message->is_dict()) {
OnProtocolBroken("Message is not a dictionary");
return;
}
const std::string* type_pointer =
message->FindStringKey(remoting::kMessageType);
if (!type_pointer) {
OnProtocolBroken("Message without type");
return;
}
const std::string& type = *type_pointer;
if (type == remoting::kHelloResponse) {
OnHelloResponse();
return;
} else if (type == remoting::kConnectResponse) {
// Ok, just ignore.
return;
} else if (type == remoting::kDisconnectResponse) {
OnDisconnectResponse();
return;
} else if (type == remoting::kHostStateChangedMessage ||
type == remoting::kErrorMessage) {
// Handle CRD host state changes
const std::string* state_pointer = message->FindStringKey(remoting::kState);
if (!state_pointer) {
OnProtocolBroken("No state in message");
return;
}
const std::string& state = *state_pointer;
if (state == remoting::kHostStateReceivedAccessCode) {
OnStateReceivedAccessCode(*message);
} else if (state == remoting::kHostStateConnected) {
OnStateRemoteConnected(*message);
} else if (state == remoting::kHostStateDisconnected) {
OnStateRemoteDisconnected();
} else if (state == remoting::kHostStateError ||
state == remoting::kHostStateDomainError) {
OnStateError(state, *message);
} else if (state == remoting::kHostStateStarting ||
state == remoting::kHostStateRequestedAccessCode) {
// Just ignore these states.
} else {
CRD_LOG(WARNING) << "Unhandled state :" << type;
}
return;
}
CRD_LOG(WARNING) << "Unknown message type: " << type;
}
void CRDHostDelegate::OnHelloResponse() {
// Host is initialized, start connection.
SendMessageToHost(remoting::kConnectMessage, connect_params_);
}
void CRDHostDelegate::OnDisconnectResponse() {
// Should happen only when remoting session finished and we
// have requested host to shut down, or when we have got second auth code
// without receiving connection.
DCHECK(!command_awaiting_crd_access_code_);
DCHECK(!remote_connected_);
ShutdownHost();
}
void CRDHostDelegate::OnStateError(const std::string& error_state,
const base::Value& message) {
// Notify callback if command is still running.
if (command_awaiting_crd_access_code_) {
command_awaiting_crd_access_code_ = false;
std::move(error_callback_)
.Run(DeviceCommandStartCRDSessionJob::FAILURE_CRD_HOST_ERROR,
"CRD State Error: " + FormatErrorMessage(error_state, message));
code_success_callback_.Reset();
}
// Shut down host, if any.
ShutdownHost();
}
void CRDHostDelegate::OnStateRemoteConnected(const base::Value& message) {
remote_connected_ = true;
// TODO(antrim): set up watchdog timer (session duration).
const std::string* client = message.FindStringKey(remoting::kClient);
if (client)
CRD_DVLOG(1) << "Remote connection by " << *client;
}
void CRDHostDelegate::OnStateRemoteDisconnected() {
// There could be a connection attempt that was not successful, we will
// receive "disconnected" message without actually receiving "connected".
if (!remote_connected_) {
CRD_DVLOG(1) << "Received disconnect out-of-order before connect";
return;
}
remote_connected_ = false;
// Remote has disconnected, time to send "disconnect" that would result
// in shutting down the host.
base::Value params(base::Value::Type::DICTIONARY);
SendMessageToHost(remoting::kDisconnectMessage, params);
}
void CRDHostDelegate::OnStateReceivedAccessCode(const base::Value& message) {
if (!command_awaiting_crd_access_code_) {
if (!remote_connected_) {
// We have already sent the access code back to the server which initiated
// this CRD session through a remote command, and we can not send a new
// access code. Assuming that the old access code is no longer valid, we
// can only terminate the current CRD session.
base::Value params(base::Value::Type::DICTIONARY);
SendMessageToHost(remoting::kDisconnectMessage, params);
}
return;
}
const std::string* access_code = message.FindStringKey(remoting::kAccessCode);
base::Optional<int> code_lifetime =
message.FindIntKey(remoting::kAccessCodeLifetime);
if (!access_code || !code_lifetime) {
OnProtocolBroken("Can not obtain access code");
return;
}
CRD_DVLOG(1) << "Got access code";
// TODO(antrim): set up watchdog timer (access code lifetime).
command_awaiting_crd_access_code_ = false;
std::move(code_success_callback_).Run(*access_code);
error_callback_.Reset();
}
void CRDHostDelegate::CloseChannel(const std::string& error_message) {
CRD_LOG(ERROR) << "CRD Host closed channel" << error_message;
command_awaiting_crd_access_code_ = false;
if (error_callback_) {
std::move(error_callback_)
.Run(DeviceCommandStartCRDSessionJob::FAILURE_CRD_HOST_ERROR,
error_message);
}
code_success_callback_.Reset();
ShutdownHost();
}
void CRDHostDelegate::SendMessageToHost(const std::string& type,
base::Value& params) {
CRD_DVLOG(1) << "Sending message of type '" << type << "' to CRD host.";
std::string message_json;
params.SetKey(remoting::kMessageType, base::Value(type));
base::JSONWriter::Write(params, &message_json);
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&CRDHostDelegate::DoSendMessage,
weak_factory_.GetWeakPtr(), message_json));
}
void CRDHostDelegate::DoSendMessage(const std::string& json) {
if (!host_)
return;
host_->OnMessage(json);
}
void CRDHostDelegate::OnProtocolBroken(const std::string& message) {
CRD_LOG(ERROR) << "Error communicating with CRD Host : " << message;
command_awaiting_crd_access_code_ = false;
std::move(error_callback_)
.Run(DeviceCommandStartCRDSessionJob::FAILURE_CRD_HOST_ERROR, message);
code_success_callback_.Reset();
ShutdownHost();
}
void CRDHostDelegate::ShutdownHost() {
if (!host_)
return;
base::ThreadTaskRunnerHandle::Get()->PostTask(
FROM_HERE, base::BindOnce(&CRDHostDelegate::DoShutdownHost,
weak_factory_.GetWeakPtr()));
}
void CRDHostDelegate::DoShutdownHost() {
host_.reset();
}
Profile* CRDHostDelegate::GetKioskProfile() const {
auto* user_manager = user_manager::UserManager::Get();
return chromeos::ProfileHelper::Get()->GetProfileByUser(
user_manager->GetActiveUser());
}
DeviceOAuth2TokenService* CRDHostDelegate::oauth_service() const {
return DeviceOAuth2TokenServiceFactory::Get();
}
} // namespace policy