| // Copyright 2015 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ash/webui/multidevice_debug/proximity_auth_webui_handler.h" |
| |
| #include <algorithm> |
| #include <memory> |
| #include <sstream> |
| #include <utility> |
| |
| #include "base/base64url.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/time_formatting.h" |
| #include "base/time/default_clock.h" |
| #include "base/time/default_tick_clock.h" |
| #include "base/values.h" |
| #include "chromeos/ash/components/multidevice/logging/logging.h" |
| #include "chromeos/ash/components/multidevice/software_feature_state.h" |
| #include "chromeos/ash/services/device_sync/proto/enum_util.h" |
| #include "components/prefs/pref_service.h" |
| #include "content/public/browser/browser_thread.h" |
| #include "content/public/browser/web_ui.h" |
| #include "device/bluetooth/public/cpp/bluetooth_uuid.h" |
| |
| namespace ash { |
| |
| namespace multidevice { |
| |
| namespace { |
| |
| constexpr const multidevice::SoftwareFeature kAllSoftareFeatures[] = { |
| multidevice::SoftwareFeature::kBetterTogetherHost, |
| multidevice::SoftwareFeature::kBetterTogetherClient, |
| multidevice::SoftwareFeature::kSmartLockHost, |
| multidevice::SoftwareFeature::kSmartLockClient, |
| multidevice::SoftwareFeature::kInstantTetheringHost, |
| multidevice::SoftwareFeature::kInstantTetheringClient, |
| multidevice::SoftwareFeature::kMessagesForWebHost, |
| multidevice::SoftwareFeature::kMessagesForWebClient, |
| multidevice::SoftwareFeature::kPhoneHubHost, |
| multidevice::SoftwareFeature::kPhoneHubClient, |
| multidevice::SoftwareFeature::kWifiSyncHost, |
| multidevice::SoftwareFeature::kWifiSyncClient, |
| multidevice::SoftwareFeature::kEcheHost, |
| multidevice::SoftwareFeature::kEcheClient, |
| multidevice::SoftwareFeature::kPhoneHubCameraRollHost, |
| multidevice::SoftwareFeature::kPhoneHubCameraRollClient}; |
| |
| // Keys in the JSON representation of a log message. |
| const char kLogMessageTextKey[] = "text"; |
| const char kLogMessageTimeKey[] = "time"; |
| const char kLogMessageFileKey[] = "file"; |
| const char kLogMessageLineKey[] = "line"; |
| const char kLogMessageSeverityKey[] = "severity"; |
| |
| // Keys in the JSON representation of a SyncState object for enrollment or |
| // device sync. |
| const char kSyncStateLastSuccessTime[] = "lastSuccessTime"; |
| const char kSyncStateNextRefreshTime[] = "nextRefreshTime"; |
| const char kSyncStateRecoveringFromFailure[] = "recoveringFromFailure"; |
| const char kSyncStateOperationInProgress[] = "operationInProgress"; |
| |
| // 9999 days in milliseconds. |
| const double kFakeInfinityMillis = 863913600000; |
| |
| double ConvertNextAttemptTimeToDouble(base::TimeDelta delta) { |
| // If no future attempt is scheduled, the next-attempt time is |
| // base::TimeDelta::Max(), which corresponds to an infinite double value. In |
| // order to store the next-attempt time as a double base::Value, |
| // std::isfinite() must be true. So, here we use 9999 days to represent the |
| // max next-attempt time to allow use with base::Value. |
| if (delta.is_max()) |
| return kFakeInfinityMillis; |
| |
| return delta.InMillisecondsF(); |
| } |
| |
| // Converts |log_message| to a raw dictionary value used as a JSON argument to |
| // JavaScript functions. |
| base::Value::Dict LogMessageToDictionary( |
| const multidevice::LogBuffer::LogMessage& log_message) { |
| base::Value::Dict dictionary; |
| dictionary.Set(kLogMessageTextKey, log_message.text); |
| dictionary.Set(kLogMessageTimeKey, |
| base::TimeFormatTimeOfDayWithMilliseconds(log_message.time)); |
| dictionary.Set(kLogMessageFileKey, log_message.file); |
| dictionary.Set(kLogMessageLineKey, log_message.line); |
| dictionary.Set(kLogMessageSeverityKey, |
| static_cast<int>(log_message.severity)); |
| return dictionary; |
| } |
| |
| // Keys in the JSON representation of an ExternalDeviceInfo proto. |
| const char kExternalDevicePublicKey[] = "publicKey"; |
| const char kExternalDevicePublicKeyTruncated[] = "publicKeyTruncated"; |
| const char kExternalDeviceFriendlyName[] = "friendlyDeviceName"; |
| const char kExternalDeviceNoPiiName[] = "noPiiName"; |
| const char kExternalDeviceUnlockKey[] = "unlockKey"; |
| const char kExternalDeviceMobileHotspot[] = "hasMobileHotspot"; |
| const char kExternalDeviceFeatureStates[] = "featureStates"; |
| |
| // Creates a SyncState JSON object that can be passed to the WebUI. |
| base::Value::Dict CreateSyncStateDictionary(double last_success_time, |
| double next_refresh_time, |
| bool is_recovering_from_failure, |
| bool is_enrollment_in_progress) { |
| base::Value::Dict sync_state; |
| sync_state.Set(kSyncStateLastSuccessTime, last_success_time); |
| sync_state.Set(kSyncStateNextRefreshTime, next_refresh_time); |
| sync_state.Set(kSyncStateRecoveringFromFailure, is_recovering_from_failure); |
| sync_state.Set(kSyncStateOperationInProgress, is_enrollment_in_progress); |
| return sync_state; |
| } |
| |
| std::string GenerateFeaturesString(const multidevice::RemoteDeviceRef& device) { |
| std::stringstream ss; |
| ss << "{"; |
| |
| bool logged_feature = false; |
| for (const auto& software_feature : kAllSoftareFeatures) { |
| multidevice::SoftwareFeatureState state = |
| device.GetSoftwareFeatureState(software_feature); |
| |
| // Only log features with values. |
| if (state == multidevice::SoftwareFeatureState::kNotSupported) |
| continue; |
| |
| logged_feature = true; |
| ss << software_feature << ": " << state << ", "; |
| } |
| |
| if (logged_feature) |
| ss.seekp(-2, ss.cur); // Remove last ", " from the stream. |
| |
| ss << "}"; |
| return ss.str(); |
| } |
| |
| } // namespace |
| |
| ProximityAuthWebUIHandler::ProximityAuthWebUIHandler( |
| device_sync::DeviceSyncClient* device_sync_client) |
| : device_sync_client_(device_sync_client), |
| web_contents_initialized_(false) { |
| CHECK(device_sync_client_); |
| } |
| |
| ProximityAuthWebUIHandler::~ProximityAuthWebUIHandler() { |
| multidevice::LogBuffer::GetInstance()->RemoveObserver(this); |
| |
| device_sync_client_->RemoveObserver(this); |
| } |
| |
| void ProximityAuthWebUIHandler::RegisterMessages() { |
| web_ui()->RegisterMessageCallback( |
| "onWebContentsInitialized", |
| base::BindRepeating(&ProximityAuthWebUIHandler::OnWebContentsInitialized, |
| base::Unretained(this))); |
| |
| web_ui()->RegisterMessageCallback( |
| "clearLogBuffer", |
| base::BindRepeating(&ProximityAuthWebUIHandler::ClearLogBuffer, |
| base::Unretained(this))); |
| |
| web_ui()->RegisterMessageCallback( |
| "getLogMessages", |
| base::BindRepeating(&ProximityAuthWebUIHandler::GetLogMessages, |
| base::Unretained(this))); |
| |
| web_ui()->RegisterMessageCallback( |
| "getLocalState", |
| base::BindRepeating(&ProximityAuthWebUIHandler::GetLocalState, |
| base::Unretained(this))); |
| |
| web_ui()->RegisterMessageCallback( |
| "forceEnrollment", |
| base::BindRepeating(&ProximityAuthWebUIHandler::ForceEnrollment, |
| base::Unretained(this))); |
| |
| web_ui()->RegisterMessageCallback( |
| "forceDeviceSync", |
| base::BindRepeating(&ProximityAuthWebUIHandler::ForceDeviceSync, |
| base::Unretained(this))); |
| } |
| |
| void ProximityAuthWebUIHandler::OnLogMessageAdded( |
| const multidevice::LogBuffer::LogMessage& log_message) { |
| web_ui()->CallJavascriptFunctionUnsafe("LogBufferInterface.onLogMessageAdded", |
| LogMessageToDictionary(log_message)); |
| } |
| |
| void ProximityAuthWebUIHandler::OnLogBufferCleared() { |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "LogBufferInterface.onLogBufferCleared"); |
| } |
| |
| void ProximityAuthWebUIHandler::OnEnrollmentFinished() { |
| // OnGetDebugInfo() will call NotifyOnEnrollmentFinished() with the enrollment |
| // state info. |
| enrollment_update_waiting_for_debug_info_ = true; |
| device_sync_client_->GetDebugInfo( |
| base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProximityAuthWebUIHandler::OnNewDevicesSynced() { |
| // OnGetDebugInfo() will call NotifyOnSyncFinished() with the device sync |
| // state info. |
| sync_update_waiting_for_debug_info_ = true; |
| device_sync_client_->GetDebugInfo( |
| base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProximityAuthWebUIHandler::OnWebContentsInitialized( |
| const base::Value::List& args) { |
| if (!web_contents_initialized_) { |
| device_sync_client_->AddObserver(this); |
| multidevice::LogBuffer::GetInstance()->AddObserver(this); |
| web_contents_initialized_ = true; |
| } |
| } |
| |
| void ProximityAuthWebUIHandler::GetLogMessages(const base::Value::List& args) { |
| base::Value::List json_logs; |
| for (const auto& log : *multidevice::LogBuffer::GetInstance()->logs()) { |
| json_logs.Append(LogMessageToDictionary(log)); |
| } |
| web_ui()->CallJavascriptFunctionUnsafe("LogBufferInterface.onGotLogMessages", |
| json_logs); |
| } |
| |
| void ProximityAuthWebUIHandler::ClearLogBuffer(const base::Value::List& args) { |
| // The OnLogBufferCleared() observer function will be called after the buffer |
| // is cleared. |
| multidevice::LogBuffer::GetInstance()->Clear(); |
| } |
| |
| void ProximityAuthWebUIHandler::ForceEnrollment(const base::Value::List& args) { |
| device_sync_client_->ForceEnrollmentNow( |
| base::BindOnce(&ProximityAuthWebUIHandler::OnForceEnrollmentNow, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProximityAuthWebUIHandler::ForceDeviceSync(const base::Value::List& args) { |
| device_sync_client_->ForceSyncNow( |
| base::BindOnce(&ProximityAuthWebUIHandler::OnForceSyncNow, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| void ProximityAuthWebUIHandler::GetLocalState(const base::Value::List& args) { |
| // OnGetDebugInfo() will call NotifyGotLocalState() with the enrollment and |
| // device sync state info. |
| get_local_state_update_waiting_for_debug_info_ = true; |
| device_sync_client_->GetDebugInfo( |
| base::BindOnce(&ProximityAuthWebUIHandler::OnGetDebugInfo, |
| weak_ptr_factory_.GetWeakPtr())); |
| } |
| |
| base::Value ProximityAuthWebUIHandler::GetTruncatedLocalDeviceId() { |
| std::optional<multidevice::RemoteDeviceRef> local_device_metadata = |
| device_sync_client_->GetLocalDeviceMetadata(); |
| |
| std::string device_id = |
| local_device_metadata |
| ? local_device_metadata->GetTruncatedDeviceIdForLogs() |
| : "Missing Device ID"; |
| |
| return base::Value(device_id); |
| } |
| |
| base::Value::List ProximityAuthWebUIHandler::GetRemoteDevicesList() { |
| base::Value::List devices_list_value; |
| |
| for (const auto& remote_device : device_sync_client_->GetSyncedDevices()) { |
| devices_list_value.Append(RemoteDeviceToDictionary(remote_device)); |
| } |
| |
| return devices_list_value; |
| } |
| |
| base::Value::Dict ProximityAuthWebUIHandler::RemoteDeviceToDictionary( |
| const multidevice::RemoteDeviceRef& remote_device) { |
| // Set the fields in the ExternalDeviceInfo proto. |
| base::Value::Dict dictionary; |
| dictionary.Set(kExternalDevicePublicKey, remote_device.GetDeviceId()); |
| dictionary.Set(kExternalDevicePublicKeyTruncated, |
| remote_device.GetTruncatedDeviceIdForLogs()); |
| dictionary.Set(kExternalDeviceFriendlyName, remote_device.name()); |
| dictionary.Set(kExternalDeviceNoPiiName, remote_device.pii_free_name()); |
| dictionary.Set(kExternalDeviceUnlockKey, |
| remote_device.GetSoftwareFeatureState( |
| multidevice::SoftwareFeature::kSmartLockHost) == |
| multidevice::SoftwareFeatureState::kEnabled); |
| dictionary.Set(kExternalDeviceMobileHotspot, |
| remote_device.GetSoftwareFeatureState( |
| multidevice::SoftwareFeature::kInstantTetheringHost) == |
| multidevice::SoftwareFeatureState::kSupported); |
| dictionary.Set(kExternalDeviceFeatureStates, |
| GenerateFeaturesString(remote_device)); |
| |
| return dictionary; |
| } |
| |
| void ProximityAuthWebUIHandler::OnForceEnrollmentNow(bool success) { |
| PA_LOG(VERBOSE) << "Force enrollment result: " << success; |
| } |
| |
| void ProximityAuthWebUIHandler::OnForceSyncNow(bool success) { |
| PA_LOG(VERBOSE) << "Force sync result: " << success; |
| } |
| |
| void ProximityAuthWebUIHandler::OnSetSoftwareFeatureState( |
| const std::string public_key, |
| device_sync::mojom::NetworkRequestResult result_code) { |
| std::string device_id = RemoteDevice::GenerateDeviceId(public_key); |
| |
| if (result_code == device_sync::mojom::NetworkRequestResult::kSuccess) { |
| PA_LOG(VERBOSE) << "Successfully set SoftwareFeature state for device: " |
| << device_id; |
| } else { |
| PA_LOG(ERROR) << "Failed to set SoftwareFeature state for device: " |
| << device_id << ", error code: " << result_code; |
| } |
| } |
| |
| void ProximityAuthWebUIHandler::OnGetDebugInfo( |
| device_sync::mojom::DebugInfoPtr debug_info_ptr) { |
| // If enrollment is not yet complete, no debug information is available. |
| if (!debug_info_ptr) |
| return; |
| |
| if (enrollment_update_waiting_for_debug_info_) { |
| enrollment_update_waiting_for_debug_info_ = false; |
| NotifyOnEnrollmentFinished( |
| true /* success */, |
| CreateSyncStateDictionary( |
| debug_info_ptr->last_enrollment_time |
| .InMillisecondsFSinceUnixEpoch(), |
| ConvertNextAttemptTimeToDouble( |
| debug_info_ptr->time_to_next_enrollment_attempt), |
| debug_info_ptr->is_recovering_from_enrollment_failure, |
| debug_info_ptr->is_enrollment_in_progress)); |
| } |
| |
| if (sync_update_waiting_for_debug_info_) { |
| sync_update_waiting_for_debug_info_ = false; |
| NotifyOnSyncFinished( |
| true /* was_sync_successful */, true /* changed */, |
| CreateSyncStateDictionary( |
| debug_info_ptr->last_sync_time.InMillisecondsFSinceUnixEpoch(), |
| ConvertNextAttemptTimeToDouble( |
| debug_info_ptr->time_to_next_sync_attempt), |
| debug_info_ptr->is_recovering_from_sync_failure, |
| debug_info_ptr->is_sync_in_progress)); |
| } |
| |
| if (get_local_state_update_waiting_for_debug_info_) { |
| get_local_state_update_waiting_for_debug_info_ = false; |
| NotifyGotLocalState( |
| GetTruncatedLocalDeviceId(), |
| CreateSyncStateDictionary( |
| debug_info_ptr->last_enrollment_time |
| .InMillisecondsFSinceUnixEpoch(), |
| ConvertNextAttemptTimeToDouble( |
| debug_info_ptr->time_to_next_enrollment_attempt), |
| debug_info_ptr->is_recovering_from_enrollment_failure, |
| debug_info_ptr->is_enrollment_in_progress), |
| CreateSyncStateDictionary( |
| debug_info_ptr->last_sync_time.InMillisecondsFSinceUnixEpoch(), |
| ConvertNextAttemptTimeToDouble( |
| debug_info_ptr->time_to_next_sync_attempt), |
| debug_info_ptr->is_recovering_from_sync_failure, |
| debug_info_ptr->is_sync_in_progress), |
| GetRemoteDevicesList()); |
| } |
| } |
| |
| void ProximityAuthWebUIHandler::NotifyOnEnrollmentFinished( |
| bool success, |
| base::Value::Dict enrollment_state) { |
| PA_LOG(VERBOSE) << "Enrollment attempt completed with success=" << success |
| << ":\n" |
| << enrollment_state; |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "LocalStateInterface.onEnrollmentStateChanged", enrollment_state); |
| } |
| |
| void ProximityAuthWebUIHandler::NotifyOnSyncFinished( |
| bool was_sync_successful, |
| bool changed, |
| base::Value::Dict device_sync_state) { |
| PA_LOG(VERBOSE) << "Device sync completed with result=" << was_sync_successful |
| << ":\n" |
| << device_sync_state; |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "LocalStateInterface.onDeviceSyncStateChanged", device_sync_state); |
| |
| if (changed) { |
| base::Value::List synced_devices = GetRemoteDevicesList(); |
| PA_LOG(VERBOSE) << "New unlock keys obtained after device sync:\n" |
| << synced_devices; |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "LocalStateInterface.onRemoteDevicesChanged", synced_devices); |
| } |
| } |
| |
| void ProximityAuthWebUIHandler::NotifyGotLocalState( |
| base::Value truncated_local_device_id, |
| base::Value::Dict enrollment_state, |
| base::Value::Dict device_sync_state, |
| base::Value::List synced_devices) { |
| PA_LOG(VERBOSE) << "==== Got Local State ====\n" |
| << "Device ID (truncated): " << truncated_local_device_id |
| << "\nEnrollment State: \n" |
| << enrollment_state << "Device Sync State: \n" |
| << device_sync_state << "Synced devices: \n" |
| << synced_devices; |
| web_ui()->CallJavascriptFunctionUnsafe( |
| "LocalStateInterface.onGotLocalState", truncated_local_device_id, |
| enrollment_state, device_sync_state, synced_devices); |
| } |
| |
| } // namespace multidevice |
| |
| } // namespace ash |