| // 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 "base/task/post_task.h" |
| #include "chrome/browser/browser_process.h" |
| #include "chrome/browser/chromeos/app_mode/arc/arc_kiosk_app_manager.h" |
| #include "chrome/browser/chromeos/app_mode/kiosk_app_manager.h" |
| #include "chrome/browser/chromeos/profiles/profile_helper.h" |
| #include "chrome/browser/chromeos/settings/device_oauth2_token_service.h" |
| #include "chrome/browser/chromeos/settings/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 "google_apis/gaia/oauth2_token_service.h" |
| #include "net/base/load_flags.h" |
| #include "net/http/http_request_headers.h" |
| #include "remoting/host/it2me/it2me_native_messaging_host_chromeos.h" |
| #include "services/network/public/cpp/simple_url_loader.h" |
| #include "ui/base/user_activity/user_activity_detector.h" |
| |
| namespace policy { |
| |
| namespace { |
| |
| // TODO(https://crbug.com/864455): move these constants to some place |
| // that they can be reused by both this code and It2MeNativeMessagingHost. |
| |
| // Communication with CRD Host, messages sent to host: |
| constexpr char kCRDMessageTypeKey[] = "type"; |
| |
| constexpr char kCRDMessageHello[] = "hello"; |
| constexpr char kCRDMessageConnect[] = "connect"; |
| constexpr char kCRDMessageDisconnect[] = "disconnect"; |
| |
| // Communication with CRD Host, messages received from host: |
| constexpr char kCRDResponseHello[] = "helloResponse"; |
| constexpr char kCRDResponseConnect[] = "connectResponse"; |
| constexpr char kCRDStateChanged[] = "hostStateChanged"; |
| constexpr char kCRDResponseDisconnect[] = "disconnectResponse"; |
| |
| // Connect message parameters: |
| constexpr char kCRDConnectUserName[] = "userName"; |
| constexpr char kCRDConnectAuth[] = "authServiceWithToken"; |
| constexpr char kCRDConnectXMPPServer[] = "xmppServerAddress"; |
| constexpr char kCRDConnectXMPPTLS[] = "xmppServerUseTls"; |
| constexpr char kCRDConnectDirectoryBot[] = "directoryBotJid"; |
| constexpr char kCRDConnectICEConfig[] = "iceConfig"; |
| constexpr char kCRDConnectNoDialogs[] = "noDialogs"; |
| constexpr char kCRDTerminateUponInput[] = "terminateUponInput"; |
| |
| // Connect message parameter values: |
| constexpr char kCRDConnectXMPPServerValue[] = "talk.google.com:443"; |
| constexpr char kCRDConnectDirectoryBotValue[] = "remoting@bot.talk.google.com"; |
| |
| // CRD host states we care about: |
| constexpr char kCRDStateKey[] = "state"; |
| constexpr char kCRDStateError[] = "ERROR"; |
| constexpr char kCRDStateStarting[] = "STARTING"; |
| constexpr char kCRDStateAccessCodeRequested[] = "REQUESTED_ACCESS_CODE"; |
| constexpr char kCRDStateDomainError[] = "INVALID_DOMAIN_ERROR"; |
| constexpr char kCRDStateAccessCode[] = "RECEIVED_ACCESS_CODE"; |
| constexpr char kCRDStateRemoteDisconnected[] = "DISCONNECTED"; |
| constexpr char kCRDStateRemoteConnected[] = "CONNECTED"; |
| |
| constexpr char kCRDErrorCodeKey[] = "error_code"; |
| constexpr char kCRDAccessCodeKey[] = "accessCode"; |
| constexpr char kCRDAccessCodeLifetimeKey[] = "accessCodeLifetime"; |
| |
| constexpr char kCRDConnectClientKey[] = "client"; |
| |
| constexpr char kICEConfigURL[] = |
| "https://www.googleapis.com/chromoting/v1/@me/iceconfig"; |
| |
| // OAuth2 Token scopes |
| constexpr char kCloudDevicesOAuth2Scope[] = |
| "https://www.googleapis.com/auth/clouddevices"; |
| constexpr char kChromotingOAuth2Scope[] = |
| "https://www.googleapis.com/auth/chromoting"; |
| |
| net::NetworkTrafficAnnotationTag CreateIceConfigRequestAnnotation() { |
| return net::DefineNetworkTrafficAnnotation("CRD_ice_config_request", R"( |
| semantics { |
| sender: "Chrome Remote Desktop" |
| description: |
| "Request is used by Chrome Remote Desktop to fetch ICE " |
| "configuration which contains list of STUN & TURN servers and TURN " |
| "credentials." |
| trigger: |
| "When a Chrome Remote Desktop session is being connected and " |
| "periodically while a session is active, as necessary. Currently " |
| "the API issues credentials that expire every 24 hours, so this " |
| "request will only be sent again while session is active more than " |
| "24 hours and it needs to renegotiate the ICE connection. The 24 " |
| "hour period is controlled by the server and may change. In some " |
| "cases, e.g. if direct connection is used, it will not trigger " |
| "periodically." |
| data: "None." |
| destination: GOOGLE_OWNED_SERVICE |
| } |
| policy { |
| cookies_allowed: NO |
| setting: |
| "This feature cannot be disabled by settings. You can block Chrome " |
| "Remote Desktop as specified here: " |
| "https://support.google.com/chrome/?p=remote_desktop" |
| chrome_policy { |
| RemoteAccessHostFirewallTraversal { |
| policy_options {mode: MANDATORY} |
| RemoteAccessHostFirewallTraversal: false |
| } |
| } |
| } |
| comments: |
| "Above specified policy is only applicable on the host side and " |
| "doesn't have effect in Android and iOS client apps. The product " |
| "is shipped separately from Chromium, except on Chrome OS." |
| )"); |
| } |
| |
| } // namespace |
| |
| CRDHostDelegate::CRDHostDelegate() |
| : OAuth2TokenService::Consumer("crd_host_delegate"), weak_factory_(this) {} |
| |
| CRDHostDelegate::~CRDHostDelegate() { |
| } |
| |
| 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 && |
| chromeos::DeviceOAuth2TokenServiceFactory::Get() != nullptr; |
| } |
| |
| bool CRDHostDelegate::IsRunningKiosk() const { |
| auto* user_manager = user_manager::UserManager::Get(); |
| if (!user_manager->IsLoggedInAsKioskApp() && |
| !user_manager->IsLoggedInAsArcKioskApp()) { |
| return false; |
| } |
| if (!GetKioskProfile()) |
| return false; |
| |
| if (user_manager->IsLoggedInAsKioskApp()) { |
| chromeos::KioskAppManager* manager = chromeos::KioskAppManager::Get(); |
| if (manager->GetAutoLaunchApp().empty()) |
| return false; |
| chromeos::KioskAppManager::App app; |
| CHECK(manager->GetApp(manager->GetAutoLaunchApp(), &app)); |
| return app.was_auto_launched_with_zero_delay; |
| } else { // ARC Kiosk |
| return chromeos::ArcKioskAppManager::Get() |
| ->current_app_was_auto_launched_with_zero_delay(); |
| } |
| } |
| |
| 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_success_callback_); |
| DCHECK(!error_callback_); |
| chromeos::DeviceOAuth2TokenService* oauth_service = |
| chromeos::DeviceOAuth2TokenServiceFactory::Get(); |
| |
| OAuth2TokenService::ScopeSet scopes; |
| scopes.insert(GaiaConstants::kGoogleUserInfoEmail); |
| scopes.insert(GaiaConstants::kGoogleTalkOAuth2Scope); |
| scopes.insert(kCloudDevicesOAuth2Scope); |
| scopes.insert(kChromotingOAuth2Scope); |
| |
| oauth_success_callback_ = std::move(success_callback); |
| error_callback_ = std::move(error_callback); |
| |
| oauth_request_ = oauth_service->StartRequest( |
| oauth_service->GetRobotAccountId(), scopes, this); |
| } |
| |
| void CRDHostDelegate::OnGetTokenSuccess( |
| const OAuth2TokenService::Request* request, |
| const OAuth2AccessTokenConsumer::TokenResponse& token_response) { |
| oauth_request_.reset(); |
| error_callback_.Reset(); |
| std::move(oauth_success_callback_).Run(token_response.access_token); |
| } |
| |
| void CRDHostDelegate::OnGetTokenFailure( |
| const OAuth2TokenService::Request* request, |
| const GoogleServiceAuthError& error) { |
| oauth_request_.reset(); |
| oauth_success_callback_.Reset(); |
| std::move(error_callback_) |
| .Run(DeviceCommandStartCRDSessionJob::FAILURE_NO_OAUTH_TOKEN, |
| error.ToString()); |
| } |
| |
| void CRDHostDelegate::FetchICEConfig( |
| const std::string& oauth_token, |
| DeviceCommandStartCRDSessionJob::ICEConfigCallback success_callback, |
| DeviceCommandStartCRDSessionJob::ErrorCallback error_callback) { |
| DCHECK(!ice_success_callback_); |
| DCHECK(!error_callback_); |
| |
| ice_success_callback_ = std::move(success_callback); |
| error_callback_ = std::move(error_callback); |
| |
| auto ice_request = std::make_unique<network::ResourceRequest>(); |
| ice_request->url = GURL(kICEConfigURL); |
| ice_request->load_flags = |
| net::LOAD_DO_NOT_SEND_COOKIES | net::LOAD_DO_NOT_SAVE_COOKIES; |
| |
| ice_request->headers.SetHeader(net::HttpRequestHeaders::kAuthorization, |
| "Bearer " + oauth_token); |
| auto loader_factory = |
| content::BrowserContext::GetDefaultStoragePartition(GetKioskProfile()) |
| ->GetURLLoaderFactoryForBrowserProcess(); |
| |
| ice_config_loader_ = network::SimpleURLLoader::Create( |
| std::move(ice_request), CreateIceConfigRequestAnnotation()); |
| ice_config_loader_->DownloadToString( |
| loader_factory.get(), |
| base::BindOnce(&CRDHostDelegate::OnICEConfigurationLoaded, |
| weak_factory_.GetWeakPtr()), |
| network::SimpleURLLoader::kMaxBoundedStringDownloadSize); |
| } |
| |
| void CRDHostDelegate::OnICEConfigurationLoaded( |
| std::unique_ptr<std::string> response_body) { |
| ice_config_loader_.reset(); |
| if (response_body) { |
| std::unique_ptr<base::Value> value = |
| base::JSONReader::ReadDeprecated(*response_body); |
| if (!value || !value->is_dict()) { |
| ice_success_callback_.Reset(); |
| std::move(error_callback_) |
| .Run(DeviceCommandStartCRDSessionJob::FAILURE_NO_ICE_CONFIG, |
| "Could not parse config"); |
| return; |
| } |
| auto* config = value->FindKeyOfType("data", base::Value::Type::DICTIONARY); |
| if (config) { |
| error_callback_.Reset(); |
| std::move(ice_success_callback_).Run(std::move(*config)); |
| return; |
| } |
| } |
| |
| ice_success_callback_.Reset(); |
| std::move(error_callback_) |
| .Run(DeviceCommandStartCRDSessionJob::FAILURE_NO_ICE_CONFIG, |
| std::string()); |
| } |
| |
| void CRDHostDelegate::StartCRDHostAndGetCode( |
| const std::string& oauth_token, |
| base::Value ice_config, |
| bool terminate_upon_input, |
| DeviceCommandStartCRDSessionJob::AccessCodeCallback success_callback, |
| DeviceCommandStartCRDSessionJob::ErrorCallback error_callback) { |
| DCHECK(!host_); |
| DCHECK(!code_success_callback_); |
| DCHECK(!error_callback_); |
| |
| // Store all parameters for future connect call. |
| base::Value connect_params(base::Value::Type::DICTIONARY); |
| std::string username = |
| chromeos::DeviceOAuth2TokenServiceFactory::Get()->GetRobotAccountId(); |
| |
| connect_params.SetKey(kCRDConnectUserName, base::Value(username)); |
| connect_params.SetKey(kCRDConnectAuth, base::Value("oauth2:" + oauth_token)); |
| connect_params.SetKey(kCRDConnectXMPPServer, |
| base::Value(kCRDConnectXMPPServerValue)); |
| connect_params.SetKey(kCRDConnectXMPPTLS, base::Value(true)); |
| connect_params.SetKey(kCRDConnectDirectoryBot, |
| base::Value(kCRDConnectDirectoryBotValue)); |
| connect_params.SetKey(kCRDConnectICEConfig, std::move(ice_config)); |
| connect_params.SetKey(kCRDConnectNoDialogs, base::Value(true)); |
| connect_params.SetKey(kCRDTerminateUponInput, |
| 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_ = remoting::CreateIt2MeNativeMessagingHostForChromeOS( |
| base::CreateSingleThreadTaskRunnerWithTraits( |
| {content::BrowserThread::IO}), |
| base::CreateSingleThreadTaskRunnerWithTraits( |
| {content::BrowserThread::UI}), |
| g_browser_process->policy_service()); |
| host_->Start(this); |
| |
| base::Value params(base::Value::Type::DICTIONARY); |
| SendMessageToHost(kCRDMessageHello, params); |
| } |
| |
| void CRDHostDelegate::PostMessageFromNativeHost(const std::string& message) { |
| std::unique_ptr<base::Value> message_value = |
| base::JSONReader::ReadDeprecated(message); |
| if (!message_value->is_dict()) { |
| OnProtocolBroken("Message is not a dictionary"); |
| return; |
| } |
| |
| auto* type_value = message_value->FindKeyOfType(kCRDMessageTypeKey, |
| base::Value::Type::STRING); |
| if (!type_value) { |
| OnProtocolBroken("Message without type"); |
| return; |
| } |
| std::string type = type_value->GetString(); |
| |
| if (type == kCRDResponseHello) { |
| OnHelloResponse(); |
| return; |
| } else if (type == kCRDResponseConnect) { |
| // Ok, just ignore. |
| return; |
| } else if (type == kCRDResponseDisconnect) { |
| OnDisconnectResponse(); |
| return; |
| } else if (type == kCRDStateChanged) { |
| // Handle CRD host state changes |
| auto* state_value = |
| message_value->FindKeyOfType(kCRDStateKey, base::Value::Type::STRING); |
| if (!state_value) { |
| OnProtocolBroken("No state in message"); |
| return; |
| } |
| std::string state = state_value->GetString(); |
| |
| if (state == kCRDStateAccessCode) { |
| OnStateReceivedAccessCode(*message_value); |
| } else if (state == kCRDStateRemoteConnected) { |
| OnStateRemoteConnected(*message_value); |
| } else if (state == kCRDStateRemoteDisconnected) { |
| OnStateRemoteDisconnected(); |
| } else if (state == kCRDStateError || state == kCRDStateDomainError) { |
| OnStateError(state, *message_value); |
| } else if (state == kCRDStateStarting || |
| state == kCRDStateAccessCodeRequested) { |
| // Just ignore these states. |
| } else { |
| LOG(WARNING) << "Unhandled state :" << type; |
| } |
| return; |
| } |
| LOG(WARNING) << "Unknown message type :" << type; |
| } |
| |
| void CRDHostDelegate::OnHelloResponse() { |
| // Host is initialized, start connection. |
| SendMessageToHost(kCRDMessageConnect, 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(std::string error_state, |
| base::Value& message) { |
| std::string error_message; |
| if (error_state == kCRDStateDomainError) { |
| error_message = "CRD Error : Invalid domain"; |
| } else { |
| auto* error_code_value = |
| message.FindKeyOfType(kCRDErrorCodeKey, base::Value::Type::STRING); |
| if (error_code_value) |
| error_message = error_code_value->GetString(); |
| else |
| error_message = "Unknown CRD Error"; |
| } |
| // 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 Error state " + error_state); |
| code_success_callback_.Reset(); |
| } |
| // Shut down host, if any |
| ShutdownHost(); |
| } |
| |
| void CRDHostDelegate::OnStateRemoteConnected(base::Value& message) { |
| remote_connected_ = true; |
| // TODO(antrim): set up watchdog timer (session duration). |
| auto* client_value = |
| message.FindKeyOfType(kCRDConnectClientKey, base::Value::Type::STRING); |
| if (client_value) { |
| VLOG(1) << "Remote connection by " << client_value->GetString(); |
| } |
| } |
| |
| 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_) |
| 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(kCRDMessageDisconnect, params); |
| } |
| |
| void CRDHostDelegate::OnStateReceivedAccessCode(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(kCRDMessageDisconnect, params); |
| } |
| return; |
| } |
| |
| auto* code_value = |
| message.FindKeyOfType(kCRDAccessCodeKey, base::Value::Type::STRING); |
| auto* code_lifetime_value = message.FindKeyOfType(kCRDAccessCodeLifetimeKey, |
| base::Value::Type::INTEGER); |
| if (!code_value || !code_lifetime_value) { |
| OnProtocolBroken("Can not obtain access code"); |
| return; |
| } |
| // TODO(antrim): set up watchdog timer (access code lifetime). |
| command_awaiting_crd_access_code_ = false; |
| std::move(code_success_callback_).Run(std::string(code_value->GetString())); |
| error_callback_.Reset(); |
| } |
| |
| void CRDHostDelegate::CloseChannel(const std::string& error_message) { |
| 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) { |
| std::string message_json; |
| params.SetKey(kCRDMessageTypeKey, 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) { |
| 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()); |
| } |
| |
| } // namespace policy |