| // Copyright 2019 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 "ash/services/device_sync/cryptauth_scheduler_impl.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "ash/components/multidevice/logging/logging.h" |
| #include "ash/services/device_sync/pref_names.h" |
| #include "ash/services/device_sync/proto/cryptauth_logging.h" |
| #include "ash/services/device_sync/value_string_encoding.h" |
| #include "base/base64.h" |
| #include "base/memory/ptr_util.h" |
| #include "chromeos/ash/components/network/network_state.h" |
| #include "chromeos/ash/components/network/network_state_handler.h" |
| #include "components/prefs/pref_registry_simple.h" |
| #include "components/prefs/pref_service.h" |
| |
| namespace ash { |
| |
| namespace device_sync { |
| |
| namespace { |
| |
| constexpr base::TimeDelta kZeroTimeDelta = base::Seconds(0); |
| |
| // The default period between successful enrollments in days. Superseded by the |
| // ClientDirective's checkin_delay_millis sent by CryptAuth. |
| constexpr base::TimeDelta kDefaultRefreshPeriod = base::Days(30); |
| |
| // The default period, in hours, between Enrollment/DeviceSync attempts if the |
| // previous Enrollment/DeviceSync attempt failed. Superseded by the |
| // ClientDirective's retry_period_millis sent by CryptAuth. |
| constexpr base::TimeDelta kDefaultRetryPeriod = base::Hours(12); |
| |
| // The time to wait before an "immediate" retry attempt after a failed |
| // Enrollment/DeviceSync attempt. Note: Some request types are throttled by |
| // CryptAuth if more than one is sent within a five-minute window. |
| constexpr base::TimeDelta kImmediateRetryDelay = base::Minutes(5); |
| |
| // The default number of "immediate" retries after a failed |
| // Enrollment/DeviceSync attempt. Superseded by the ClientDirective's |
| // retry_attempts sent by CryptAuth. |
| const int kDefaultMaxImmediateRetries = 3; |
| |
| const char kNoClientDirective[] = "[No ClientDirective]"; |
| |
| const char kNoClientMetadata[] = "[No ClientMetadata]"; |
| |
| bool IsClientDirectiveValid( |
| const cryptauthv2::ClientDirective& client_directive) { |
| return client_directive.checkin_delay_millis() > 0 && |
| client_directive.retry_period_millis() > 0 && |
| client_directive.retry_attempts() >= 0; |
| } |
| |
| // Fills a ClientDirective with our chosen default parameters. This |
| // ClientDirective is used until a ClientDirective is received from CryptAuth. |
| cryptauthv2::ClientDirective CreateDefaultClientDirective() { |
| cryptauthv2::ClientDirective client_directive; |
| client_directive.set_checkin_delay_millis( |
| kDefaultRefreshPeriod.InMilliseconds()); |
| client_directive.set_retry_period_millis( |
| kDefaultRetryPeriod.InMilliseconds()); |
| client_directive.set_retry_attempts(kDefaultMaxImmediateRetries); |
| |
| return client_directive; |
| } |
| |
| cryptauthv2::ClientDirective BuildClientDirective(PrefService* pref_service) { |
| DCHECK(pref_service); |
| const base::Value& encoded_client_directive = |
| pref_service->GetValue(prefs::kCryptAuthSchedulerClientDirective); |
| if (encoded_client_directive.GetString() == kNoClientDirective) |
| return CreateDefaultClientDirective(); |
| |
| absl::optional<cryptauthv2::ClientDirective> client_directive_from_pref = |
| util::DecodeProtoMessageFromValueString<cryptauthv2::ClientDirective>( |
| &encoded_client_directive); |
| |
| return client_directive_from_pref.value_or(CreateDefaultClientDirective()); |
| } |
| |
| cryptauthv2::ClientMetadata BuildClientMetadata( |
| size_t retry_count, |
| const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason, |
| const absl::optional<std::string>& session_id) { |
| cryptauthv2::ClientMetadata client_metadata; |
| client_metadata.set_retry_count(retry_count); |
| client_metadata.set_invocation_reason(invocation_reason); |
| if (session_id) |
| client_metadata.set_session_id(*session_id); |
| |
| return client_metadata; |
| } |
| |
| } // namespace |
| |
| // static |
| CryptAuthSchedulerImpl::Factory* |
| CryptAuthSchedulerImpl::Factory::test_factory_ = nullptr; |
| |
| // static |
| std::unique_ptr<CryptAuthScheduler> CryptAuthSchedulerImpl::Factory::Create( |
| PrefService* pref_service, |
| NetworkStateHandler* network_state_handler, |
| base::Clock* clock, |
| std::unique_ptr<base::OneShotTimer> enrollment_timer, |
| std::unique_ptr<base::OneShotTimer> device_sync_timer) { |
| if (test_factory_) { |
| return test_factory_->CreateInstance(pref_service, network_state_handler, |
| clock, std::move(enrollment_timer), |
| std::move(device_sync_timer)); |
| } |
| |
| return base::WrapUnique(new CryptAuthSchedulerImpl( |
| pref_service, network_state_handler, clock, std::move(enrollment_timer), |
| std::move(device_sync_timer))); |
| } |
| |
| // static |
| void CryptAuthSchedulerImpl::Factory::SetFactoryForTesting( |
| Factory* test_factory) { |
| test_factory_ = test_factory; |
| } |
| |
| CryptAuthSchedulerImpl::Factory::~Factory() = default; |
| |
| // static |
| void CryptAuthSchedulerImpl::RegisterPrefs(PrefRegistrySimple* registry) { |
| registry->RegisterStringPref(prefs::kCryptAuthSchedulerClientDirective, |
| kNoClientDirective); |
| registry->RegisterStringPref( |
| prefs::kCryptAuthSchedulerNextEnrollmentRequestClientMetadata, |
| kNoClientMetadata); |
| registry->RegisterStringPref( |
| prefs::kCryptAuthSchedulerNextDeviceSyncRequestClientMetadata, |
| kNoClientMetadata); |
| registry->RegisterTimePref( |
| prefs::kCryptAuthSchedulerLastEnrollmentAttemptTime, base::Time()); |
| registry->RegisterTimePref( |
| prefs::kCryptAuthSchedulerLastDeviceSyncAttemptTime, base::Time()); |
| registry->RegisterTimePref( |
| prefs::kCryptAuthSchedulerLastSuccessfulEnrollmentTime, base::Time()); |
| registry->RegisterTimePref( |
| prefs::kCryptAuthSchedulerLastSuccessfulDeviceSyncTime, base::Time()); |
| } |
| |
| CryptAuthSchedulerImpl::CryptAuthSchedulerImpl( |
| PrefService* pref_service, |
| NetworkStateHandler* network_state_handler, |
| base::Clock* clock, |
| std::unique_ptr<base::OneShotTimer> enrollment_timer, |
| std::unique_ptr<base::OneShotTimer> device_sync_timer) |
| : pref_service_(pref_service), |
| network_state_handler_(network_state_handler), |
| clock_(clock), |
| client_directive_(BuildClientDirective(pref_service)) { |
| DCHECK(pref_service_); |
| DCHECK(network_state_handler_); |
| DCHECK(clock_); |
| DCHECK(IsClientDirectiveValid(client_directive_)); |
| |
| request_timers_[RequestType::kEnrollment] = std::move(enrollment_timer); |
| request_timers_[RequestType::kDeviceSync] = std::move(device_sync_timer); |
| |
| // Queue up the most recently scheduled requests if applicable. |
| InitializePendingRequest(RequestType::kEnrollment); |
| InitializePendingRequest(RequestType::kDeviceSync); |
| } |
| |
| CryptAuthSchedulerImpl::~CryptAuthSchedulerImpl() { |
| if (network_state_handler_) |
| network_state_handler_->RemoveObserver(this, FROM_HERE); |
| } |
| |
| // static |
| std::string CryptAuthSchedulerImpl::GetLastAttemptTimePrefName( |
| RequestType request_type) { |
| switch (request_type) { |
| case RequestType::kEnrollment: |
| return prefs::kCryptAuthSchedulerLastEnrollmentAttemptTime; |
| case RequestType::kDeviceSync: |
| return prefs::kCryptAuthSchedulerLastDeviceSyncAttemptTime; |
| } |
| } |
| |
| // static |
| std::string CryptAuthSchedulerImpl::GetLastSuccessTimePrefName( |
| RequestType request_type) { |
| switch (request_type) { |
| case RequestType::kEnrollment: |
| return prefs::kCryptAuthSchedulerLastSuccessfulEnrollmentTime; |
| case RequestType::kDeviceSync: |
| return prefs::kCryptAuthSchedulerLastSuccessfulDeviceSyncTime; |
| } |
| } |
| |
| // static |
| std::string CryptAuthSchedulerImpl::GetPendingRequestPrefName( |
| RequestType request_type) { |
| switch (request_type) { |
| case RequestType::kEnrollment: |
| return prefs::kCryptAuthSchedulerNextEnrollmentRequestClientMetadata; |
| case RequestType::kDeviceSync: |
| return prefs::kCryptAuthSchedulerNextDeviceSyncRequestClientMetadata; |
| } |
| } |
| |
| void CryptAuthSchedulerImpl::OnEnrollmentSchedulingStarted() { |
| OnSchedulingStarted(RequestType::kEnrollment); |
| } |
| |
| void CryptAuthSchedulerImpl::OnDeviceSyncSchedulingStarted() { |
| OnSchedulingStarted(RequestType::kDeviceSync); |
| } |
| |
| void CryptAuthSchedulerImpl::RequestEnrollment( |
| const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason, |
| const absl::optional<std::string>& session_id) { |
| MakeRequest(RequestType::kEnrollment, invocation_reason, session_id); |
| } |
| |
| void CryptAuthSchedulerImpl::RequestDeviceSync( |
| const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason, |
| const absl::optional<std::string>& session_id) { |
| MakeRequest(RequestType::kDeviceSync, invocation_reason, session_id); |
| } |
| |
| void CryptAuthSchedulerImpl::HandleEnrollmentResult( |
| const CryptAuthEnrollmentResult& enrollment_result) { |
| HandleResult(RequestType::kEnrollment, enrollment_result.IsSuccess(), |
| enrollment_result.client_directive()); |
| } |
| |
| void CryptAuthSchedulerImpl::HandleDeviceSyncResult( |
| const CryptAuthDeviceSyncResult& device_sync_result) { |
| // Note: "Success" for DeviceSync means no errors, not even non-fatal errors. |
| HandleResult(RequestType::kDeviceSync, device_sync_result.IsSuccess(), |
| device_sync_result.client_directive()); |
| } |
| |
| absl::optional<base::Time> |
| CryptAuthSchedulerImpl::GetLastSuccessfulEnrollmentTime() const { |
| return GetLastSuccessTime(RequestType::kEnrollment); |
| } |
| |
| absl::optional<base::Time> |
| CryptAuthSchedulerImpl::GetLastSuccessfulDeviceSyncTime() const { |
| return GetLastSuccessTime(RequestType::kDeviceSync); |
| } |
| |
| base::TimeDelta CryptAuthSchedulerImpl::GetRefreshPeriod() const { |
| return base::Milliseconds(client_directive_.checkin_delay_millis()); |
| } |
| absl::optional<base::TimeDelta> |
| CryptAuthSchedulerImpl::GetTimeToNextEnrollmentRequest() const { |
| return GetTimeToNextRequest(RequestType::kEnrollment); |
| } |
| |
| absl::optional<base::TimeDelta> |
| CryptAuthSchedulerImpl::GetTimeToNextDeviceSyncRequest() const { |
| return GetTimeToNextRequest(RequestType::kDeviceSync); |
| } |
| |
| bool CryptAuthSchedulerImpl::IsWaitingForEnrollmentResult() const { |
| return IsWaitingForResult(RequestType::kEnrollment); |
| } |
| |
| bool CryptAuthSchedulerImpl::IsWaitingForDeviceSyncResult() const { |
| return IsWaitingForResult(RequestType::kDeviceSync); |
| } |
| |
| size_t CryptAuthSchedulerImpl::GetNumConsecutiveEnrollmentFailures() const { |
| return GetNumConsecutiveFailures(RequestType::kEnrollment); |
| } |
| |
| size_t CryptAuthSchedulerImpl::GetNumConsecutiveDeviceSyncFailures() const { |
| return GetNumConsecutiveFailures(RequestType::kDeviceSync); |
| } |
| |
| void CryptAuthSchedulerImpl::DefaultNetworkChanged( |
| const NetworkState* network) { |
| // The updated default network may not be online. |
| if (!DoesMachineHaveNetworkConnectivity()) |
| return; |
| |
| // Now that the device has connectivity, reschedule requests. |
| ScheduleNextRequest(RequestType::kEnrollment); |
| ScheduleNextRequest(RequestType::kDeviceSync); |
| } |
| |
| void CryptAuthSchedulerImpl::OnShuttingDown() { |
| DCHECK(network_state_handler_); |
| network_state_handler_->RemoveObserver(this, FROM_HERE); |
| network_state_handler_ = nullptr; |
| } |
| |
| void CryptAuthSchedulerImpl::OnSchedulingStarted(RequestType request_type) { |
| if (!network_state_handler_->HasObserver(this)) |
| network_state_handler_->AddObserver(this, FROM_HERE); |
| |
| ScheduleNextRequest(request_type); |
| } |
| |
| void CryptAuthSchedulerImpl::MakeRequest( |
| RequestType request_type, |
| const cryptauthv2::ClientMetadata::InvocationReason& invocation_reason, |
| const absl::optional<std::string>& session_id) { |
| request_timers_[request_type]->Stop(); |
| |
| pending_requests_[request_type] = |
| BuildClientMetadata(0 /* retry_count */, invocation_reason, session_id); |
| |
| ScheduleNextRequest(request_type); |
| } |
| |
| void CryptAuthSchedulerImpl::HandleResult( |
| RequestType request_type, |
| bool success, |
| const absl::optional<cryptauthv2::ClientDirective>& new_client_directive) { |
| DCHECK(current_requests_[request_type]); |
| DCHECK(!request_timers_[request_type]->IsRunning()); |
| |
| base::Time now = clock_->Now(); |
| |
| pref_service_->SetTime(GetLastAttemptTimePrefName(request_type), now); |
| |
| if (new_client_directive && IsClientDirectiveValid(*new_client_directive)) { |
| client_directive_ = *new_client_directive; |
| PA_LOG(VERBOSE) << "New client directive:\n" << client_directive_; |
| pref_service_->Set( |
| prefs::kCryptAuthSchedulerClientDirective, |
| util::EncodeProtoMessageAsValueString(&client_directive_)); |
| } |
| |
| // If successful, process InvokeNext field of ClientDirective. If unsuccessful |
| // and a more immediate request isn't pending, queue up the failure recovery |
| // attempt. |
| if (success) { |
| pref_service_->SetTime(GetLastSuccessTimePrefName(request_type), now); |
| |
| HandleInvokeNext(client_directive_.invoke_next(), |
| current_requests_[request_type]->session_id()); |
| } else if (!pending_requests_[request_type]) { |
| current_requests_[request_type]->set_retry_count( |
| current_requests_[request_type]->retry_count() + 1); |
| pending_requests_[request_type] = current_requests_[request_type]; |
| } |
| |
| current_requests_[request_type].reset(); |
| |
| // Because the ClientDirective might have changed, we update both timers. |
| ScheduleNextRequest(RequestType::kEnrollment); |
| ScheduleNextRequest(RequestType::kDeviceSync); |
| } |
| |
| void CryptAuthSchedulerImpl::HandleInvokeNext( |
| const ::google::protobuf::RepeatedPtrField<cryptauthv2::InvokeNext>& |
| invoke_next_list, |
| const std::string& session_id) { |
| for (const cryptauthv2::InvokeNext& invoke_next : invoke_next_list) { |
| if (invoke_next.service() == cryptauthv2::ENROLLMENT) { |
| PA_LOG(VERBOSE) << "Enrollment requested by InvokeNext"; |
| RequestEnrollment(cryptauthv2::ClientMetadata::SERVER_INITIATED, |
| session_id); |
| } else if (invoke_next.service() == cryptauthv2::DEVICE_SYNC) { |
| PA_LOG(VERBOSE) << "DeviceSync requested by InvokeNext"; |
| RequestDeviceSync(cryptauthv2::ClientMetadata::SERVER_INITIATED, |
| session_id); |
| } else { |
| PA_LOG(WARNING) << "Unknown InvokeNext TargetService " |
| << invoke_next.service(); |
| } |
| } |
| } |
| |
| absl::optional<base::Time> CryptAuthSchedulerImpl::GetLastSuccessTime( |
| RequestType request_type) const { |
| base::Time time = |
| pref_service_->GetTime(GetLastSuccessTimePrefName(request_type)); |
| if (time.is_null()) |
| return absl::nullopt; |
| |
| return time; |
| } |
| |
| absl::optional<base::TimeDelta> CryptAuthSchedulerImpl::GetTimeToNextRequest( |
| RequestType request_type) const { |
| // Request already in progress. |
| if (IsWaitingForResult(request_type)) |
| return kZeroTimeDelta; |
| |
| // No pending request. |
| const auto it = pending_requests_.find(request_type); |
| if (it == pending_requests_.end() || !it->second) |
| return absl::nullopt; |
| |
| int64_t retry_count = it->second->retry_count(); |
| cryptauthv2::ClientMetadata::InvocationReason invocation_reason = |
| it->second->invocation_reason(); |
| |
| // If we are not recovering from failure, attempt all but periodic requests |
| // immediately. |
| if (retry_count == 0) { |
| if (invocation_reason != cryptauthv2::ClientMetadata::PERIODIC) |
| return kZeroTimeDelta; |
| |
| absl::optional<base::Time> last_success_time = |
| GetLastSuccessTime(request_type); |
| DCHECK(last_success_time); |
| |
| base::TimeDelta time_since_last_success = |
| clock_->Now() - *last_success_time; |
| return std::max(kZeroTimeDelta, |
| GetRefreshPeriod() - time_since_last_success); |
| } |
| |
| base::TimeDelta time_since_last_attempt = |
| clock_->Now() - |
| pref_service_->GetTime(GetLastAttemptTimePrefName(request_type)); |
| |
| // Recover from failure using immediate retry. |
| DCHECK(retry_count > 0); |
| if (retry_count < client_directive_.retry_attempts()) { |
| return std::max(kZeroTimeDelta, |
| kImmediateRetryDelay - time_since_last_attempt); |
| } |
| |
| // Recover from failure after expending allotted immediate retries. |
| return std::max(kZeroTimeDelta, |
| base::Milliseconds(client_directive_.retry_period_millis()) - |
| time_since_last_attempt); |
| } |
| |
| bool CryptAuthSchedulerImpl::IsWaitingForResult( |
| RequestType request_type) const { |
| const auto it = current_requests_.find(request_type); |
| return (it != current_requests_.end() && it->second); |
| } |
| |
| size_t CryptAuthSchedulerImpl::GetNumConsecutiveFailures( |
| RequestType request_type) const { |
| const auto current_request_it = current_requests_.find(request_type); |
| if (current_request_it != current_requests_.end() && |
| current_request_it->second) { |
| return current_request_it->second->retry_count(); |
| } |
| |
| const auto pending_request_it = pending_requests_.find(request_type); |
| if (pending_request_it != pending_requests_.end() && |
| pending_request_it->second) { |
| return pending_request_it->second->retry_count(); |
| } |
| |
| return 0; |
| } |
| |
| bool CryptAuthSchedulerImpl::DoesMachineHaveNetworkConnectivity() const { |
| if (!network_state_handler_) |
| return false; |
| |
| // TODO(khorimoto): IsConnectedState() can still return true if connected to |
| // a captive portal; use the "online" boolean once we fetch data via the |
| // networking Mojo API. See https://crbug.com/862420. |
| const NetworkState* default_network = |
| network_state_handler_->DefaultNetwork(); |
| return default_network && default_network->IsConnectedState(); |
| } |
| |
| void CryptAuthSchedulerImpl::InitializePendingRequest( |
| RequestType request_type) { |
| // Queue up the persisted scheduled request if applicable. |
| const base::Value* client_metadata_from_pref = |
| pref_service_->Get(GetPendingRequestPrefName(request_type)); |
| if (client_metadata_from_pref->GetString() != kNoClientMetadata) { |
| pending_requests_[request_type] = |
| util::DecodeProtoMessageFromValueString<cryptauthv2::ClientMetadata>( |
| client_metadata_from_pref); |
| } |
| |
| // If we are recovering from a failure, reset the failure count to 1 in the |
| // hopes that the restart solved the issue. This will allow for immediate |
| // retries again if permitted by the ClientDirective. |
| if (pending_requests_[request_type] && |
| pending_requests_[request_type]->retry_count() > 0) { |
| pending_requests_[request_type]->set_retry_count(1); |
| } |
| } |
| |
| void CryptAuthSchedulerImpl::ScheduleNextRequest(RequestType request_type) { |
| // Wait for the current attempt to finish before determining the next request |
| // in case we need to recover from a failure. |
| if (IsWaitingForResult(request_type)) |
| return; |
| |
| // For Enrollment only, if no request has already been explicitly made, |
| // schedule a periodic attempt. |
| if (request_type == RequestType::kEnrollment && |
| !pending_requests_[request_type]) { |
| pending_requests_[request_type] = |
| BuildClientMetadata(0 /* retry_count */, |
| GetLastSuccessTime(request_type) |
| ? cryptauthv2::ClientMetadata::PERIODIC |
| : cryptauthv2::ClientMetadata::INITIALIZATION, |
| absl::nullopt /* session_id */); |
| } |
| |
| // Schedule a first-time DeviceSync if one has never successfully completed. |
| // However, unlike Enrollment, there are no periodic DeviceSyncs. |
| if (request_type == RequestType::kDeviceSync && |
| !pending_requests_[request_type] && !GetLastSuccessTime(request_type)) { |
| pending_requests_[request_type] = BuildClientMetadata( |
| 0 /* retry_count */, cryptauthv2::ClientMetadata::INITIALIZATION, |
| absl::nullopt /* session_id */); |
| } |
| |
| if (!pending_requests_[request_type]) { |
| // By this point, only DeviceSync can have no requests pending because it |
| // does not schedule periodic syncs. |
| DCHECK_EQ(RequestType::kDeviceSync, request_type); |
| pref_service_->SetString(GetPendingRequestPrefName(request_type), |
| kNoClientMetadata); |
| return; |
| } |
| |
| // Persist the pending request even if scheduling hasn't started yet. |
| pref_service_->Set(GetPendingRequestPrefName(request_type), |
| util::EncodeProtoMessageAsValueString( |
| &pending_requests_[request_type].value())); |
| |
| bool has_scheduling_started = (request_type == RequestType::kEnrollment && |
| HasEnrollmentSchedulingStarted()) || |
| (request_type == RequestType::kDeviceSync && |
| HasDeviceSyncSchedulingStarted()); |
| if (!has_scheduling_started) |
| return; |
| |
| absl::optional<base::TimeDelta> delay = GetTimeToNextRequest(request_type); |
| DCHECK(delay); |
| request_timers_[request_type]->Start( |
| FROM_HERE, *delay, |
| base::BindOnce(&CryptAuthSchedulerImpl::OnTimerFired, |
| base::Unretained(this), request_type)); |
| } |
| |
| void CryptAuthSchedulerImpl::OnTimerFired(RequestType request_type) { |
| DCHECK(!current_requests_[request_type]); |
| DCHECK(pending_requests_[request_type]); |
| |
| if (!DoesMachineHaveNetworkConnectivity()) { |
| std::string type_string = |
| request_type == RequestType::kEnrollment ? "Enrollment" : "DeviceSync"; |
| PA_LOG(INFO) << type_string |
| << " triggered while the device is offline. Waiting " |
| << "for online connectivity before making request."; |
| return; |
| } |
| |
| current_requests_[request_type] = pending_requests_[request_type]; |
| pending_requests_[request_type].reset(); |
| |
| switch (request_type) { |
| case RequestType::kEnrollment: { |
| absl::optional<cryptauthv2::PolicyReference> policy_reference = |
| absl::nullopt; |
| if (client_directive_.has_policy_reference()) |
| policy_reference = client_directive_.policy_reference(); |
| |
| NotifyEnrollmentRequested(*current_requests_[request_type], |
| policy_reference); |
| return; |
| } |
| case RequestType::kDeviceSync: |
| NotifyDeviceSyncRequested(*current_requests_[request_type]); |
| return; |
| } |
| } |
| |
| } // namespace device_sync |
| |
| } // namespace ash |