DODownloader.cpp (19850B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "DODownloader.h" 5 #include "Public/AppInstallerLogging.h" 6 #include "Public/AppInstallerSHA256.h" 7 #include "Public/AppInstallerStrings.h" 8 #include "winget/UserSettings.h" 9 10 // TODO: Get this from the Windows SDK when available 11 #define DODownloadProperty_HttpRedirectionTarget static_cast<DODownloadProperty>(DODownloadProperty_NonVolatile + 1) 12 #define DODownloadProperty_HttpResponseHeaders static_cast<DODownloadProperty>(DODownloadProperty_HttpRedirectionTarget + 1) 13 #define DODownloadProperty_HttpServerIPAddress static_cast<DODownloadProperty>(DODownloadProperty_HttpResponseHeaders + 1) 14 #define DODownloadProperty_HttpStatusCode static_cast<DODownloadProperty>(DODownloadProperty_HttpServerIPAddress + 1) 15 16 namespace AppInstaller::Utility 17 { 18 namespace 19 { 20 std::optional<std::string> ExtractContentType(const std::optional<std::string>& headers) 21 { 22 if (!headers) 23 { 24 return std::nullopt; 25 } 26 27 static constexpr std::string_view s_ContentType = "content-type:"sv; 28 auto headerLines = Utility::SplitIntoLines(headers.value()); 29 30 for (const auto& header : headerLines) 31 { 32 std::string_view headerView = header; 33 if (header.length() >= s_ContentType.length()) 34 { 35 std::string lowerFragment = ToLower(headerView.substr(0, s_ContentType.length())); 36 if (s_ContentType == lowerFragment) 37 { 38 return Trim(header.substr(s_ContentType.length())); 39 } 40 } 41 } 42 43 return std::nullopt; 44 } 45 } 46 47 namespace DeliveryOptimization 48 { 49 // Represents a download work item for Delivery Optimization. 50 struct Download 51 { 52 Download(IDOManager* manager) 53 { 54 THROW_IF_FAILED(manager->CreateDownload(&m_download)); 55 56 // Cloaking - sets the authentication information that will be used to make calls on the DO interface proxy. 57 // This will make sure DO server impersonates the correct client identity. 58 THROW_IF_FAILED(CoSetProxyBlanket( 59 m_download.get(), 60 RPC_C_AUTHN_DEFAULT, 61 RPC_C_AUTHZ_DEFAULT, 62 COLE_DEFAULT_PRINCIPAL, 63 RPC_C_AUTHN_LEVEL_DEFAULT, 64 RPC_C_IMP_LEVEL_IMPERSONATE, 65 NULL, 66 EOAC_DEFAULT)); 67 } 68 69 ~Download() 70 { 71 DO_DOWNLOAD_STATUS downloadStatus; 72 if (SUCCEEDED_LOG(m_download->GetStatus(&downloadStatus))) 73 { 74 if (downloadStatus.State == DODownloadState_Transferred) 75 { 76 // Calling IDODownload::Finalize() to inform DO that the DO job can be cleaned up. 77 // Otherwise, the resources associated with the job can be kept for a number of days 78 // until expiration set by DO. 79 (void)LOG_IF_FAILED(m_download->Finalize()); 80 } 81 else if (downloadStatus.State != DODownloadState_Finalized) 82 { 83 // For any other state, abort the download since it's no longer in use. 84 // This will allow DO to clean up the cache for the associated content ID. 85 (void)LOG_IF_FAILED(m_download->Abort()); 86 } 87 } 88 } 89 90 void SetProperty(DODownloadProperty prop, const std::wstring& value) 91 { 92 wil::unique_variant var; 93 var.bstrVal = ::SysAllocString(value.c_str()); 94 THROW_IF_NULL_ALLOC(var.bstrVal); 95 var.vt = VT_BSTR; 96 THROW_IF_FAILED(m_download->SetProperty(prop, &var)); 97 } 98 99 void SetProperty(DODownloadProperty prop, std::string_view value) 100 { 101 SetProperty(prop, Utility::ConvertToUTF16(value)); 102 } 103 104 void SetProperty(DODownloadProperty prop, uint32_t value) 105 { 106 wil::unique_variant var; 107 var.ulVal = value; 108 var.vt = VT_UI4; 109 THROW_IF_FAILED(m_download->SetProperty(prop, &var)); 110 } 111 112 void SetProperty(DODownloadProperty prop, bool value) 113 { 114 wil::unique_variant var; 115 var.boolVal = value ? VARIANT_TRUE : VARIANT_FALSE; 116 var.vt = VT_BOOL; 117 THROW_IF_FAILED(m_download->SetProperty(prop, &var)); 118 } 119 120 template<typename T> 121 void SetUnknownProperty(DODownloadProperty prop, T&& value) 122 { 123 wil::unique_variant var; 124 var.punkVal = nullptr; 125 var.vt = VT_UNKNOWN; 126 if (value) 127 { 128 THROW_IF_FAILED(value->QueryInterface(IID_PPV_ARGS(&var.punkVal))); 129 } 130 THROW_IF_FAILED(m_download->SetProperty(prop, &var)); 131 } 132 133 template<typename T> 134 std::optional<T> TryGetProperty(DODownloadProperty prop) 135 { 136 std::optional<T> result; 137 wil::unique_variant var; 138 HRESULT hr = m_download->GetProperty(prop, &var); 139 if (SUCCEEDED(hr)) 140 { 141 T value; 142 if (ExtractFromVariant(var, value)) 143 { 144 result = std::move(value); 145 } 146 } 147 return result; 148 } 149 150 void Uri(std::string_view uri) 151 { 152 SetProperty(DODownloadProperty_Uri, uri); 153 } 154 155 void ContentId(std::string_view contentId) 156 { 157 SetProperty(DODownloadProperty_ContentId, contentId); 158 } 159 160 void DisplayName(std::string_view displayName) 161 { 162 SetProperty(DODownloadProperty_DisplayName, displayName); 163 } 164 165 void LocalPath(const std::filesystem::path& localPath) 166 { 167 SetProperty(DODownloadProperty_LocalPath, localPath.wstring()); 168 } 169 170 void CorrelationVector(std::string_view correlationVector) 171 { 172 SetProperty(DODownloadProperty_CorrelationVector, correlationVector); 173 } 174 175 void NoProgressTimeoutSeconds(uint32_t noProgressTimeoutSeconds) 176 { 177 SetProperty(DODownloadProperty_NoProgressTimeoutSeconds, noProgressTimeoutSeconds); 178 } 179 180 void ForegroundPriority(bool foregroundPriority) 181 { 182 SetProperty(DODownloadProperty_ForegroundPriority, foregroundPriority); 183 } 184 185 void BlockingMode(bool blockingMode) 186 { 187 SetProperty(DODownloadProperty_BlockingMode, blockingMode); 188 } 189 190 void CallbackInterface(IDODownloadStatusCallback* callbackInterface) 191 { 192 SetUnknownProperty(DODownloadProperty_CallbackInterface, callbackInterface); 193 } 194 195 void StreamInterface(IStream* streamInterface) 196 { 197 SetUnknownProperty(DODownloadProperty_StreamInterface, streamInterface); 198 } 199 200 void CustomHeaders(const std::vector<DownloadRequestHeader>& headers) 201 { 202 // DODownloadProperty_HttpCustomAuthHeaders is not used (does not work in our auth scenario). It is only used when challenged. 203 std::string customHeaders; 204 for (const auto& header : headers) 205 { 206 customHeaders += header.Name + ": " + header.Value + "\r\n"; 207 } 208 209 if (!customHeaders.empty()) 210 { 211 SetProperty(DODownloadProperty_HttpCustomHeaders, customHeaders); 212 } 213 } 214 215 // Properties that may be interesting for future use: 216 // https://docs.microsoft.com/en-us/windows/win32/delivery_optimization/deliveryoptimizationdownloadtypes/ne-deliveryoptimizationdownloadtypes-dodownloadproperty 217 // - DODownloadProperty_CostPolicy :: Allow user to specify how to behave on metered networks 218 219 void Start() 220 { 221 DO_DOWNLOAD_RANGES_INFO emptyRanges{}; 222 emptyRanges.RangeCount = 0; 223 THROW_IF_FAILED(m_download->Start(&emptyRanges)); 224 } 225 226 // Returns true if Abort was successful; false if not. 227 bool Cancel() 228 { 229 return SUCCEEDED_LOG(m_download->Abort()); 230 } 231 232 void Finalize() 233 { 234 THROW_IF_FAILED(m_download->Finalize()); 235 } 236 237 DO_DOWNLOAD_STATUS Status() 238 { 239 DO_DOWNLOAD_STATUS result{}; 240 THROW_IF_FAILED(m_download->GetStatus(&result)); 241 return result; 242 } 243 244 private: 245 bool ExtractFromVariant(const VARIANT& var, std::string& value) 246 { 247 if (var.vt == VT_BSTR && var.bstrVal != nullptr) 248 { 249 value = Utility::ConvertToUTF8(var.bstrVal); 250 return true; 251 } 252 else if (var.vt == (VT_BSTR | VT_BYREF) && var.pbstrVal != nullptr && *var.pbstrVal != nullptr) 253 { 254 value = Utility::ConvertToUTF8(*var.pbstrVal); 255 return true; 256 } 257 258 return false; 259 } 260 261 wil::com_ptr<IDODownload> m_download; 262 }; 263 264 // The top level Delivery Optimization manager object. 265 struct Manager 266 { 267 Manager() 268 { 269 THROW_IF_FAILED(CoCreateInstance( 270 __uuidof(::DeliveryOptimization), 271 nullptr, 272 CLSCTX_LOCAL_SERVER, 273 IID_PPV_ARGS(&m_manager))); 274 } 275 276 Download CreateDownload() 277 { 278 return { m_manager.get() }; 279 } 280 281 private: 282 wil::com_ptr<IDOManager> m_manager; 283 }; 284 285 // Status callback handler 286 class DODownloadStatusCallback : public Microsoft::WRL::RuntimeClass< 287 Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>, 288 IDODownloadStatusCallback> 289 { 290 public: 291 DODownloadStatusCallback(IProgressCallback& progress) : 292 m_progress(progress) 293 { 294 } 295 296 IFACEMETHOD(OnStatusChange)(IDODownload*, const DO_DOWNLOAD_STATUS* status) 297 { 298 { 299 std::lock_guard<std::mutex> guard(m_statusMutex); 300 m_currentStatus = *status; 301 } 302 m_statusCV.notify_all(); 303 return S_OK; 304 } 305 306 static HRESULT Create( 307 IProgressCallback& progress, 308 DODownloadStatusCallback** result) 309 { 310 Microsoft::WRL::ComPtr<DODownloadStatusCallback> localResult = Microsoft::WRL::Make<DODownloadStatusCallback>(progress); 311 RETURN_IF_NULL_ALLOC(localResult); 312 313 *result = localResult.Detach(); 314 return S_OK; 315 } 316 317 // Simply breaks the wait in Wait; the progress object must already be cancelled to force it out. 318 void Cancel() 319 { 320 m_statusCV.notify_all(); 321 } 322 323 // Returns true on successful completion, false on cancellation, and throws on an error. 324 bool Wait() 325 { 326 std::unique_lock<std::mutex> lock(m_statusMutex); 327 328 // If there is no transfer status update for m_doNoProgressTimeout, we will fail. 329 auto timeoutTime = std::chrono::steady_clock::now() + Settings::User().Get<Settings::Setting::NetworkDOProgressTimeoutInSeconds>(); 330 std::optional<UINT64> initialTransferAmount; 331 bool transferChange = false; 332 333 while (!m_progress.IsCancelledBy(CancelReason::Any)) 334 { 335 if (!transferChange) 336 { 337 if (m_statusCV.wait_until(lock, timeoutTime) == std::cv_status::timeout) 338 { 339 THROW_HR(DO_E_DOWNLOAD_NO_PROGRESS); 340 } 341 } 342 else 343 { 344 m_statusCV.wait(lock); 345 } 346 347 // Since we just finished a wait, check for cancellation before handling anything else 348 if (m_progress.IsCancelledBy(CancelReason::Any)) 349 { 350 return false; 351 } 352 353 AICLI_LOG(Core, Verbose, << "DO State " << m_currentStatus.State << ", " << m_currentStatus.BytesTransferred << " / " << m_currentStatus.BytesTotal << 354 ", Error 0x" << Logging::SetHRFormat << m_currentStatus.Error << ", extended error 0x" << Logging::SetHRFormat << m_currentStatus.ExtendedError); 355 356 // No matter the state, we are considering any error set to be a failure 357 if (FAILED(m_currentStatus.Error)) 358 { 359 AICLI_LOG(Core, Error, << "DeliveryOptimization error: 0x" << Logging::SetHRFormat << m_currentStatus.Error << 360 ", extended error: 0x" << Logging::SetHRFormat << m_currentStatus.ExtendedError); 361 THROW_HR(m_currentStatus.Error); 362 } 363 364 switch (m_currentStatus.State) 365 { 366 // These states are ignored. 367 case DODownloadState_Created: 368 case DODownloadState_Paused: 369 break; 370 371 case DODownloadState_Transferring: 372 if (m_currentStatus.BytesTransferred || m_currentStatus.BytesTotal) 373 { 374 m_progress.OnProgress(m_currentStatus.BytesTransferred, m_currentStatus.BytesTotal, ProgressType::Bytes); 375 } 376 377 if (!initialTransferAmount) 378 { 379 initialTransferAmount = m_currentStatus.BytesTransferred; 380 } 381 else if (m_currentStatus.BytesTransferred != initialTransferAmount.value()) 382 { 383 transferChange = true; 384 } 385 break; 386 387 // These are considered to be 'done' 388 case DODownloadState_Transferred: 389 case DODownloadState_Finalized: 390 if (m_currentStatus.BytesTransferred || m_currentStatus.BytesTotal) 391 { 392 m_progress.OnProgress(m_currentStatus.BytesTransferred, m_currentStatus.BytesTotal, ProgressType::Bytes); 393 } 394 return true; 395 396 // This is the cancelled state 397 case DODownloadState_Aborted: 398 return false; 399 } 400 } 401 402 return false; 403 } 404 405 private: 406 IProgressCallback& m_progress; 407 std::mutex m_statusMutex; 408 std::condition_variable m_statusCV; 409 DO_DOWNLOAD_STATUS m_currentStatus = {}; 410 }; 411 } 412 413 // Debugging tip: 414 // From an elevated PowerShell, run: 415 // > Get-DeliveryOptimizationLog | Set-Content doLogs.txt 416 DownloadResult DODownload( 417 const std::string& url, 418 const std::filesystem::path& dest, 419 IProgressCallback& progress, 420 std::optional<DownloadInfo> info) 421 { 422 AICLI_LOG(Core, Info, << "DeliveryOptimization downloading from url: " << url); 423 424 // Remove the target file since DO will not overwrite 425 std::filesystem::remove(dest); 426 427 DeliveryOptimization::Manager manager; 428 DeliveryOptimization::Download download = manager.CreateDownload(); 429 430 wil::com_ptr<DeliveryOptimization::DODownloadStatusCallback> callback; 431 THROW_IF_FAILED(DeliveryOptimization::DODownloadStatusCallback::Create(progress, &callback)); 432 433 download.Uri(url); 434 download.ForegroundPriority(true); 435 download.LocalPath(dest); 436 download.CallbackInterface(callback.get()); 437 438 if (info) 439 { 440 if (!info->DisplayName.empty()) 441 { 442 download.DisplayName(info->DisplayName); 443 } 444 445 if (!info->ContentId.empty()) 446 { 447 download.ContentId(info->ContentId); 448 } 449 450 if (!info->RequestHeaders.empty()) 451 { 452 download.CustomHeaders(info->RequestHeaders); 453 } 454 } 455 456 download.Start(); 457 458 auto cancelLifetime = progress.SetCancellationFunction([&download, &callback]() 459 { 460 AICLI_LOG(Core, Info, << "Download cancelled."); 461 download.Cancel(); 462 callback->Cancel(); 463 }); 464 465 // Check to handle cancellation between Start and SetCancellationFunction 466 if (progress.IsCancelledBy(CancelReason::Any)) 467 { 468 AICLI_LOG(Core, Info, << "Download cancelled."); 469 download.Cancel(); 470 return {}; 471 } 472 473 // Wait returns true for success, false for cancellation, and throws on error. 474 if (callback->Wait()) 475 { 476 // Grab the headers so that we can use them later 477 std::optional<std::string> responseHeaders = download.TryGetProperty<std::string>(DODownloadProperty_HttpResponseHeaders); 478 479 // Finalize is required to flush the data and change the file name. 480 download.Finalize(); 481 AICLI_LOG(Core, Info, << "Download completed."); 482 483 std::ifstream inStream{ dest, std::ifstream::binary }; 484 auto hashDetails = SHA256::ComputeHashDetails(inStream); 485 486 DownloadResult result; 487 result.Sha256Hash = std::move(hashDetails.Hash); 488 result.SizeInBytes = hashDetails.SizeInBytes; 489 result.ContentType = ExtractContentType(responseHeaders); 490 491 return result; 492 } 493 494 return {}; 495 } 496 497 bool IsDOErrorFatal(HRESULT error) 498 { 499 // If this gets to be large, store in a sorted array and binary search on it. 500 // There will be more to update here, which we should be able to discover through telemetry. 501 return 502 error == DO_E_BLOCKED_BY_COST_TRANSFER_POLICY || 503 error == DO_E_BLOCKED_BY_CELLULAR_POLICY || 504 error == DO_E_BLOCKED_BY_POWER_STATE || 505 error == DO_E_BLOCKED_BY_NO_NETWORK; 506 } 507 }