DownloadFlow.cpp (34718B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "DownloadFlow.h" 5 #include "MSStoreInstallerHandler.h" 6 #include <winget/Filesystem.h> 7 #include <AppInstallerDeployment.h> 8 #include <AppInstallerDownloader.h> 9 #include <AppInstallerRuntime.h> 10 #include <AppInstallerMsixInfo.h> 11 #include <winget/AdminSettings.h> 12 #include <winget/GroupPolicy.h> 13 #include <winget/ManifestYamlWriter.h> 14 #include <winget/NetworkSettings.h> 15 16 namespace AppInstaller::CLI::Workflow 17 { 18 using namespace AppInstaller::Manifest; 19 using namespace AppInstaller::Repository; 20 using namespace AppInstaller::Utility; 21 using namespace AppInstaller::Settings; 22 using namespace std::string_view_literals; 23 24 namespace 25 { 26 constexpr std::string_view s_MicrosoftEntraIdAuthorizationHeader = "Authorization"sv; 27 // By default Azure blob storage does not accept Microsoft Entra Id authentication. 28 // https://learn.microsoft.com/en-us/rest/api/storageservices/versioning-for-the-azure-storage-services#authorize-requests-by-using-microsoft-entra-id-shared-key-or-shared-key-lite 29 constexpr std::string_view s_AzureBlobStorageApiVersionHeader = "x-ms-version"sv; 30 constexpr std::string_view s_AzureBlobStorageApiVersionValue = "2020-04-08"sv; 31 32 // Get the base download directory path for the installer. 33 // Also creates the directory as necessary. 34 std::filesystem::path GetInstallerBaseDownloadPath(Execution::Context& context) 35 { 36 const auto& manifest = context.Get<Execution::Data::Manifest>(); 37 38 std::filesystem::path tempInstallerPath = Runtime::GetPathTo(Runtime::PathName::Temp); 39 tempInstallerPath /= Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); 40 41 std::filesystem::create_directories(tempInstallerPath); 42 43 return tempInstallerPath; 44 } 45 46 // Get the file extension to be used for the installer file. 47 std::wstring_view GetInstallerFileExtension(Execution::Context& context) 48 { 49 const auto& installer = context.Get<Execution::Data::Installer>(); 50 switch (installer->BaseInstallerType) 51 { 52 case InstallerTypeEnum::Burn: 53 case InstallerTypeEnum::Exe: 54 case InstallerTypeEnum::Inno: 55 case InstallerTypeEnum::Nullsoft: 56 case InstallerTypeEnum::Portable: 57 return L".exe"sv; 58 case InstallerTypeEnum::Msi: 59 case InstallerTypeEnum::Wix: 60 return L".msi"sv; 61 case InstallerTypeEnum::Msix: 62 // Note: We may need to distinguish between .msix and .msixbundle in the future. 63 return L".msix"sv; 64 case InstallerTypeEnum::Zip: 65 return L".zip"sv; 66 default: 67 THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); 68 } 69 } 70 71 // Gets a file name that should not be able to ShellExecute. 72 std::filesystem::path GetInstallerPreHashValidationFileName(Execution::Context& context) 73 { 74 return { SHA256::ConvertToString(context.Get<Execution::Data::Installer>()->Sha256) }; 75 } 76 77 // Gets the file name that can be used to ShellExecute the file. 78 std::filesystem::path GetInstallerPostHashValidationFileName(Execution::Context& context) 79 { 80 // Get file name from download URI 81 std::filesystem::path filename = GetFileNameFromURI(context.Get<Execution::Data::Installer>()->Url); 82 std::wstring_view installerExtension = GetInstallerFileExtension(context); 83 84 // Assuming that we find a safe stem value in the URI, use it. 85 // This should be extremely common, but just in case fall back to the older name style. 86 if (filename.has_stem() && ((filename.wstring().size() + installerExtension.size()) < MAX_PATH)) 87 { 88 filename = filename.stem(); 89 } 90 else 91 { 92 const auto& manifest = context.Get<Execution::Data::Manifest>(); 93 filename = Utility::ConvertToUTF16(manifest.Id + '.' + manifest.Version); 94 } 95 96 filename += installerExtension; 97 98 // Make file name suitable for file system path 99 filename = Utility::ConvertToUTF16(Utility::MakeSuitablePathPart(filename.u8string())); 100 101 return filename; 102 } 103 104 // Gets the file name for the downloaded installer in the format of {id}_{version}_{architecture}_{scope}_{installerType}_{locale}. 105 std::filesystem::path GetInstallerDownloadOnlyFileName(Execution::Context& context, const std::wstring_view& extension = {}) 106 { 107 const auto& manifest = context.Get<Execution::Data::Manifest>(); 108 const auto& installer = context.Get<Execution::Data::Installer>().value(); 109 110 std::string packageName = manifest.CurrentLocalization.Get<Localization::PackageName>(); 111 std::string architecture{ ToString(installer.Arch) }; 112 std::string installerType{ InstallerTypeToString(installer.EffectiveInstallerType()) }; 113 114 std::string fileName = packageName; 115 116 if (!Version(manifest.Version).IsUnknown()) 117 { 118 fileName += '_' + manifest.Version; 119 } 120 121 if (installer.Scope != ScopeEnum::Unknown) 122 { 123 fileName += '_' + std::string{ ScopeToString(installer.Scope) }; 124 } 125 126 fileName += '_' + architecture + '_' + installerType; 127 128 std::string locale = !installer.Locale.empty() ? installer.Locale : manifest.CurrentLocalization.Locale; 129 if (!locale.empty()) 130 { 131 fileName += '_' + locale; 132 } 133 134 std::filesystem::path fileNamePath = Utility::ConvertToUTF16(fileName); 135 136 if (!extension.empty()) 137 { 138 fileNamePath += extension; 139 } 140 else 141 { 142 fileNamePath += GetInstallerFileExtension(context); 143 } 144 145 // Make file name suitable for file system path 146 fileNamePath = Utility::ConvertToUTF16(Utility::MakeSuitablePathPart(fileNamePath.u8string())); 147 return fileNamePath; 148 } 149 150 // Try to remove the installer file, ignoring any errors. 151 void RemoveInstallerFile(const std::filesystem::path& path) 152 { 153 try 154 { 155 std::filesystem::remove(path); 156 157 // It is assumed that the parent of the installer path will always be a directory 158 // If it isn't, then something went severely wrong. However, we will check that 159 // it is a directory here just to be safe. If it is an empty directory, remove it. 160 161 if (std::filesystem::is_directory(path.parent_path()) && 162 std::filesystem::is_empty(path.parent_path())) 163 { 164 std::filesystem::remove(path.parent_path()); 165 } 166 } 167 catch (const std::exception& e) 168 { 169 AICLI_LOG(CLI, Warning, << "Failed to remove installer file. Reason: " << e.what()); 170 } 171 catch (...) 172 { 173 AICLI_LOG(CLI, Warning, << "Failed to remove installer file. Reason unknown."); 174 } 175 176 } 177 178 // Checks the file hash for an existing installer file. 179 // Returns true if the file exists and its hash matches, false otherwise. 180 // If the hash does not match, deletes the file. 181 bool ExistingInstallerFileHasHashMatch(const SHA256::HashBuffer& expectedHash, const std::filesystem::path& filePath, SHA256::HashDetails& fileHashDetails) 182 { 183 if (std::filesystem::exists(filePath)) 184 { 185 AICLI_LOG(CLI, Info, << "Found existing installer file at '" << filePath << "'. Verifying file hash."); 186 std::ifstream inStream{ filePath, std::ifstream::binary }; 187 fileHashDetails = SHA256::ComputeHashDetails(inStream); 188 189 if (SHA256::AreEqual(expectedHash, fileHashDetails.Hash)) 190 { 191 return true; 192 } 193 194 AICLI_LOG(CLI, Info, << "Hash does not match. Removing existing installer file " << filePath); 195 RemoveInstallerFile(filePath); 196 } 197 198 return false; 199 } 200 201 std::string GetInstallerDownloadAuthenticationToken(const AppInstaller::Authentication::AuthenticationInfo& authInfo, Execution::Context& context) 202 { 203 // First check if authenticator is already created 204 auto& authenticatorsMap = context.Get<AppInstaller::CLI::Execution::Data::InstallerDownloadAuthenticators>(); 205 auto authenticatorItr = authenticatorsMap->find(authInfo); 206 if (authenticatorItr == authenticatorsMap->end()) 207 { 208 AppInstaller::Authentication::Authenticator authenticator{ authInfo, GetAuthenticationArguments(context) }; 209 authenticatorsMap->emplace(authInfo, std::move(authenticator)); 210 } 211 212 // Get the authenticator for auth. 213 authenticatorItr = authenticatorsMap->find(authInfo); 214 THROW_HR_IF(E_UNEXPECTED, authenticatorItr == authenticatorsMap->end()); 215 216 auto authResult = authenticatorItr->second.AuthenticateForToken(); 217 if (FAILED(authResult.Status)) 218 { 219 AICLI_LOG(Repo, Error, << "Authentication failed for installer download. Result: " << authResult.Status); 220 THROW_HR_MSG(authResult.Status, "Failed to authenticate for installer download."); 221 } 222 223 return authResult.Token; 224 } 225 226 // Get additional headers for installer download request. Auth headers are acquired here. 227 std::vector<DownloadRequestHeader> GetInstallerDownloadAuthenticationHeaders(const AppInstaller::Manifest::ManifestInstaller& installer, Execution::Context& context) 228 { 229 std::vector<DownloadRequestHeader> result; 230 231 switch (installer.AuthInfo.Type) 232 { 233 case AppInstaller::Authentication::AuthenticationType::None: 234 // No auth needed 235 break; 236 case AppInstaller::Authentication::AuthenticationType::MicrosoftEntraId: 237 case AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage: 238 context.Reporter.Info() << Execution::AuthenticationEmphasis << Resource::String::InstallerDownloadRequiresAuthentication << std::endl; 239 result.push_back({ std::string{ s_MicrosoftEntraIdAuthorizationHeader }, Authentication::CreateBearerToken(GetInstallerDownloadAuthenticationToken(installer.AuthInfo, context)), true }); 240 if (installer.AuthInfo.Type == AppInstaller::Authentication::AuthenticationType::MicrosoftEntraIdForAzureBlobStorage) 241 { 242 result.push_back({ std::string{ s_AzureBlobStorageApiVersionHeader }, std::string{ s_AzureBlobStorageApiVersionValue }, false }); 243 } 244 break; 245 case AppInstaller::Authentication::AuthenticationType::Unknown: 246 default: 247 THROW_HR_MSG(APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED, "The package installer requires authentication that is not supported."); 248 } 249 250 // Log result before return 251 std::string logMessage = "Installer download headers: "; 252 for (const auto& header : result) 253 { 254 logMessage += header.Name + ": " + (header.IsAuth ? "<Secret>" : header.Value) + "; "; 255 } 256 AICLI_LOG(CLI, Info, << logMessage); 257 258 return result; 259 } 260 } 261 262 void DownloadInstaller(Execution::Context& context) 263 { 264 // Check if file was already downloaded. 265 // This may happen after a failed installation or if the download was done 266 // separately before, e.g. on COM scenarios. 267 context << 268 ReportExecutionStage(ExecutionStage::Download) << 269 CheckForExistingInstaller; 270 271 if (context.IsTerminated()) 272 { 273 return; 274 } 275 276 bool installerDownloadOnly = WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly); 277 278 // CheckForExistingInstaller will set the InstallerPath if found 279 if (!context.Contains(Execution::Data::InstallerPath)) 280 { 281 const auto& installer = context.Get<Execution::Data::Installer>().value(); 282 switch (installer.BaseInstallerType) 283 { 284 case InstallerTypeEnum::Exe: 285 case InstallerTypeEnum::Burn: 286 case InstallerTypeEnum::Inno: 287 case InstallerTypeEnum::Msi: 288 case InstallerTypeEnum::Nullsoft: 289 case InstallerTypeEnum::Portable: 290 case InstallerTypeEnum::Wix: 291 case InstallerTypeEnum::Zip: 292 context << DownloadInstallerFile; 293 break; 294 case InstallerTypeEnum::Msix: 295 // If the signature hash is provided in the manifest and we are doing an install, 296 // we can just verify signature hash without a full download and do a streaming install. 297 // Even if we have the signature hash, we still do a full download if InstallerDownloadOnly 298 // flag is set, or if we need to use a proxy (as deployment APIs won't use proxy for us). 299 // Finally, we require the digest API for streaming install as well. 300 if (installer.SignatureSha256.empty() 301 || installerDownloadOnly 302 || Network().GetProxyUri() 303 || !Deployment::IsExpectedDigestsSupported()) 304 { 305 context << DownloadInstallerFile; 306 } 307 else 308 { 309 context << GetMsixSignatureHash; 310 } 311 break; 312 case InstallerTypeEnum::MSStore: 313 if (installerDownloadOnly) 314 { 315 context << 316 MSStoreDownload << 317 ExportManifest; 318 } 319 320 return; 321 default: 322 THROW_HR(HRESULT_FROM_WIN32(ERROR_NOT_SUPPORTED)); 323 } 324 } 325 326 context << 327 VerifyInstallerHash << 328 UpdateInstallerFileMotwIfApplicable << 329 RenameDownloadedInstaller; 330 331 if (installerDownloadOnly) 332 { 333 context << ExportManifest; 334 } 335 } 336 337 void CheckForExistingInstaller(Execution::Context& context) 338 { 339 const auto& installer = context.Get<Execution::Data::Installer>().value(); 340 if (installer.EffectiveInstallerType() == InstallerTypeEnum::MSStore) 341 { 342 // No installer is downloaded in this case 343 return; 344 } 345 346 // Try looking for the file with and without extension. 347 auto installerPath = GetInstallerBaseDownloadPath(context); 348 auto installerFilename = GetInstallerPreHashValidationFileName(context); 349 SHA256::HashDetails fileHashDetails; 350 if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHashDetails)) 351 { 352 installerFilename = GetInstallerPostHashValidationFileName(context); 353 if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHashDetails)) 354 { 355 // No match 356 return; 357 } 358 } 359 360 AICLI_LOG(CLI, Info, << "Existing installer file hash matches. Will use existing installer."); 361 context.Add<Execution::Data::InstallerPath>(installerPath / installerFilename); 362 context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, 363 DownloadResult{ std::move(fileHashDetails.Hash), fileHashDetails.SizeInBytes })); 364 } 365 366 void GetInstallerDownloadPath(Execution::Context& context) 367 { 368 if (!context.Contains(Execution::Data::InstallerPath)) 369 { 370 auto tempInstallerPath = GetInstallerBaseDownloadPath(context); 371 tempInstallerPath /= GetInstallerPreHashValidationFileName(context); 372 AICLI_LOG(CLI, Info, << "Generated temp download path: " << tempInstallerPath); 373 context.Add<Execution::Data::InstallerPath>(std::move(tempInstallerPath)); 374 } 375 } 376 377 void DownloadInstallerFile(Execution::Context& context) 378 { 379 context << GetInstallerDownloadPath; 380 if (context.IsTerminated()) 381 { 382 return; 383 } 384 385 const auto& installer = context.Get<Execution::Data::Installer>().value(); 386 const auto& installerPath = context.Get<Execution::Data::InstallerPath>(); 387 388 Utility::DownloadInfo downloadInfo{}; 389 downloadInfo.DisplayName = Resource::GetFixedString(Resource::FixedString::ProductName); 390 // Use the SHA256 hash of the installer as the identifier for the download 391 downloadInfo.ContentId = SHA256::ConvertToString(installer.Sha256); 392 393 try 394 { 395 downloadInfo.RequestHeaders = GetInstallerDownloadAuthenticationHeaders(installer, context); 396 } 397 catch (const wil::ResultException& re) 398 { 399 AICLI_LOG(CLI, Error, << "Authentication failed for installer download. Error code: " << re.GetErrorCode()); 400 401 if (re.GetErrorCode() == APPINSTALLER_CLI_ERROR_AUTHENTICATION_TYPE_NOT_SUPPORTED) 402 { 403 context.Reporter.Error() << Resource::String::InstallerDownloadAuthenticationNotSupported << std::endl; 404 } 405 else 406 { 407 context.Reporter.Error() << Resource::String::InstallerDownloadAuthenticationFailed << std::endl; 408 } 409 410 AICLI_TERMINATE_CONTEXT(re.GetErrorCode()); 411 } 412 413 context.Reporter.Info() << Resource::String::Downloading << ' ' << Execution::UrlEmphasis << installer.Url << std::endl; 414 415 DownloadResult downloadResult; 416 417 constexpr int MaxRetryCount = 2; 418 constexpr std::chrono::seconds maximumWaitTimeAllowed = 60s; 419 for (int retryCount = 0; retryCount < MaxRetryCount; ++retryCount) 420 { 421 bool success = false; 422 try 423 { 424 downloadResult = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, 425 installer.Url, 426 installerPath, 427 Utility::DownloadType::Installer, 428 std::placeholders::_1, 429 downloadInfo)); 430 431 // User cancelled. 432 if (downloadResult.Sha256Hash.empty()) 433 { 434 context.Reporter.Info() << Resource::String::Cancelled << std::endl; 435 AICLI_TERMINATE_CONTEXT(E_ABORT); 436 } 437 438 if (downloadResult.SizeInBytes == 0) 439 { 440 AICLI_LOG(CLI, Info, << "Got zero byte file; retrying download after a short wait..."); 441 std::this_thread::sleep_for(5s); 442 } 443 else 444 { 445 success = true; 446 } 447 } 448 catch (const ServiceUnavailableException& sue) 449 { 450 if (retryCount < MaxRetryCount - 1) 451 { 452 auto waitSecondsForRetry = sue.RetryAfter(); 453 if (waitSecondsForRetry > maximumWaitTimeAllowed) 454 { 455 throw; 456 } 457 458 bool waitCompleted = context.Reporter.ExecuteWithProgress([&waitSecondsForRetry](IProgressCallback& progress) 459 { 460 return ProgressCallback::Wait(progress, waitSecondsForRetry); 461 }); 462 463 if (!waitCompleted) 464 { 465 break; 466 } 467 } 468 else 469 { 470 throw; 471 } 472 } 473 catch (...) 474 { 475 if (retryCount < MaxRetryCount - 1) 476 { 477 AICLI_LOG(CLI, Info, << "Failed to download, waiting a bit and retry. Url: " << installer.Url); 478 Sleep(500); 479 } 480 else 481 { 482 throw; 483 } 484 } 485 486 if (success) 487 { 488 break; 489 } 490 } 491 492 context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, downloadResult)); 493 } 494 495 void GetMsixSignatureHash(Execution::Context& context) 496 { 497 // We use this when the server won't support streaming install to swap to download. 498 bool downloadInstead = false; 499 500 try 501 { 502 const auto& installer = context.Get<Execution::Data::Installer>().value(); 503 504 // Signature hash is only used for streaming installs, which don't use proxy 505 Msix::MsixInfo msixInfo(installer.Url); 506 507 DownloadResult hashInfo{ msixInfo.GetSignatureHash() }; 508 // Value is ASCII for MSIXSTRM 509 // A sentinel value to indicate that this is a streaming hash rather than a download. 510 // The primary purpose is to prevent us from falling into the code path for zero byte files. 511 hashInfo.SizeInBytes = 0x4D5349585354524D; 512 513 context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.SignatureSha256, hashInfo)); 514 context.Add<Execution::Data::MsixDigests>({ std::make_pair(installer.Url, msixInfo.GetDigest()) }); 515 } 516 catch (...) 517 { 518 AICLI_LOG(CLI, Info, << "Failed to get msix signature hash, fall back to direct download."); 519 downloadInstead = true; 520 } 521 522 if (downloadInstead) 523 { 524 context << DownloadInstallerFile; 525 } 526 } 527 528 void VerifyInstallerHash(Execution::Context& context) 529 { 530 const auto& [expectedHash, downloadResult] = context.Get<Execution::Data::DownloadHashInfo>(); 531 532 if (!std::equal( 533 expectedHash.begin(), 534 expectedHash.end(), 535 downloadResult.Sha256Hash.begin())) 536 { 537 bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); 538 539 const auto& manifest = context.Get<Execution::Data::Manifest>(); 540 Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, expectedHash, downloadResult.Sha256Hash, overrideHashMismatch, downloadResult.SizeInBytes, downloadResult.ContentType); 541 542 if (downloadResult.SizeInBytes == 0) 543 { 544 context.Reporter.Error() << Resource::String::InstallerZeroByteFile << std::endl; 545 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE); 546 } 547 548 // If running as admin, do not allow the user to override the hash failure. 549 if (Runtime::IsRunningAsAdmin()) 550 { 551 context.Reporter.Error() << Resource::String::InstallerHashMismatchAdminBlock << std::endl; 552 } 553 else if (!Settings::IsAdminSettingEnabled(Settings::BoolAdminSetting::InstallerHashOverride)) 554 { 555 context.Reporter.Error() << Resource::String::InstallerHashMismatchError << std::endl; 556 } 557 else if (overrideHashMismatch) 558 { 559 context.Reporter.Warn() << Resource::String::InstallerHashMismatchOverridden << std::endl; 560 return; 561 } 562 else 563 { 564 context.Reporter.Error() << Resource::String::InstallerHashMismatchOverrideRequired << std::endl; 565 } 566 567 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_HASH_MISMATCH); 568 } 569 else 570 { 571 AICLI_LOG(CLI, Info, << "Installer hash verified"); 572 context.Reporter.Info() << Resource::String::InstallerHashVerified << std::endl; 573 574 context.SetFlags(Execution::ContextFlag::InstallerHashMatched); 575 576 if (context.Contains(Execution::Data::PackageVersion) && 577 context.Get<Execution::Data::PackageVersion>()->GetSource() && 578 WI_IsFlagSet(context.Get<Execution::Data::PackageVersion>()->GetSource().GetDetails().TrustLevel, SourceTrustLevel::Trusted)) 579 { 580 context.SetFlags(Execution::ContextFlag::InstallerTrusted); 581 } 582 } 583 } 584 585 void UpdateInstallerFileMotwIfApplicable(Execution::Context& context) 586 { 587 // An initial MotW is always set to URLZONE_INTERNET at the time the file is downloaded. 588 // This function may change that to URLZONE_TRUSTED if appropriate 589 if (context.Contains(Execution::Data::InstallerPath)) 590 { 591 if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerTrusted)) 592 { 593 // We know the installer already went through multiple scans and we can trust it. 594 Utility::ApplyMotwIfApplicable(context.Get<Execution::Data::InstallerPath>(), URLZONE_TRUSTED); 595 } 596 else if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerHashMatched)) 597 { 598 // IAttachmentExecute performs some additional scans before setting MotW, for example invoking anti-virus. 599 // A policy can be set to always mark files from a given domain as trusted, so only do this 600 // on installers with the right hash to prevent trusting unknown installers. 601 const auto& installer = context.Get<Execution::Data::Installer>(); 602 HRESULT hr = Utility::ApplyMotwUsingIAttachmentExecuteIfApplicable(context.Get<Execution::Data::InstallerPath>(), installer.value().Url, URLZONE_INTERNET); 603 604 // Not using SUCCEEDED(hr) to check since there are cases file is missing after a successful scan 605 if (hr != S_OK) 606 { 607 switch (hr) 608 { 609 case INET_E_SECURITY_PROBLEM: 610 context.Reporter.Error() << Resource::String::InstallerBlockedByPolicy << std::endl; 611 break; 612 case E_FAIL: 613 context.Reporter.Error() << Resource::String::InstallerFailedVirusScan << std::endl; 614 break; 615 default: 616 context.Reporter.Error() << Resource::String::InstallerFailedSecurityCheck << std::endl; 617 } 618 619 AICLI_LOG(Fail, Error, << "Installer failed security check. Url: " << installer.value().Url << " Result: " << WINGET_OSTREAM_FORMAT_HRESULT(hr)); 620 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_SECURITY_CHECK_FAILED); 621 } 622 } 623 } 624 } 625 626 void ReverifyInstallerHash(Execution::Context& context) 627 { 628 const auto& installer = context.Get<Execution::Data::Installer>().value(); 629 630 if (context.Contains(Execution::Data::InstallerPath)) 631 { 632 // Get the hash from the installer file 633 const auto& installerPath = context.Get<Execution::Data::InstallerPath>(); 634 std::ifstream inStream{ installerPath, std::ifstream::binary }; 635 auto existingFileHashDetails = SHA256::ComputeHashDetails(inStream); 636 context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, 637 DownloadResult{ existingFileHashDetails.Hash, existingFileHashDetails.SizeInBytes })); 638 } 639 else if (installer.EffectiveInstallerType() == InstallerTypeEnum::MSStore) 640 { 641 // No installer file in this case 642 return; 643 } 644 else if (installer.EffectiveInstallerType() == InstallerTypeEnum::Msix && !installer.SignatureSha256.empty()) 645 { 646 // We didn't download the installer file before. Just verify the signature hash again. 647 context << GetMsixSignatureHash; 648 } 649 else 650 { 651 // No installer downloaded 652 AICLI_LOG(CLI, Error, << "Installer file not found."); 653 AICLI_TERMINATE_CONTEXT(HRESULT_FROM_WIN32(ERROR_FILE_NOT_FOUND)); 654 } 655 656 context << VerifyInstallerHash; 657 } 658 659 void RenameDownloadedInstaller(Execution::Context& context) 660 { 661 if (!context.Contains(Execution::Data::InstallerPath)) 662 { 663 // No installer downloaded, no need to rename anything. 664 return; 665 } 666 667 auto& installerPath = context.Get<Execution::Data::InstallerPath>(); 668 std::filesystem::path renamedDownloadedInstaller; 669 670 if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) 671 { 672 THROW_HR_IF(E_UNEXPECTED, !context.Contains(Execution::Data::DownloadDirectory)); 673 674 std::filesystem::path downloadDirectory = context.Get<Execution::Data::DownloadDirectory>(); 675 676 if (!std::filesystem::exists(downloadDirectory)) 677 { 678 std::filesystem::create_directories(downloadDirectory); 679 } 680 else 681 { 682 THROW_HR_IF(HRESULT_FROM_WIN32(ERROR_CANNOT_MAKE), !std::filesystem::is_directory(downloadDirectory)); 683 } 684 685 renamedDownloadedInstaller = downloadDirectory / GetInstallerDownloadOnlyFileName(context); 686 Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); 687 context.Reporter.Info() << Resource::String::InstallerDownloaded(Utility::LocIndView{ renamedDownloadedInstaller.u8string() }) << std::endl; 688 } 689 else 690 { 691 renamedDownloadedInstaller = installerPath; 692 renamedDownloadedInstaller.replace_filename(GetInstallerPostHashValidationFileName(context)); 693 694 if (installerPath == renamedDownloadedInstaller) 695 { 696 // In case we are reusing an existing downloaded file 697 return; 698 } 699 700 Filesystem::RenameFile(installerPath, renamedDownloadedInstaller); 701 } 702 703 installerPath.assign(renamedDownloadedInstaller); 704 AICLI_LOG(CLI, Info, << "Successfully renamed downloaded installer. Path: " << installerPath); 705 } 706 707 void RemoveInstaller(Execution::Context& context) 708 { 709 // Path may not be present if installed from a URL for MSIX 710 if (context.Contains(Execution::Data::InstallerPath)) 711 { 712 const auto& path = context.Get<Execution::Data::InstallerPath>(); 713 AICLI_LOG(CLI, Info, << "Removing installer: " << path); 714 RemoveInstallerFile(path); 715 } 716 } 717 718 void SetDownloadDirectory(Execution::Context& context) 719 { 720 if (!WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) 721 { 722 return; 723 } 724 725 if (context.Args.Contains(Execution::Args::Type::DownloadDirectory)) 726 { 727 context.Add<Execution::Data::DownloadDirectory>(std::filesystem::path{ Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::DownloadDirectory)) }); 728 } 729 else 730 { 731 std::filesystem::path downloadsDirectory = Settings::User().Get<Settings::Setting::DownloadDefaultDirectory>(); 732 733 if (downloadsDirectory.empty()) 734 { 735 downloadsDirectory = AppInstaller::Runtime::GetPathTo(AppInstaller::Runtime::PathName::UserProfileDownloads); 736 } 737 738 const auto& manifest = context.Get<Execution::Data::Manifest>(); 739 std::string packageDownloadFolderName = manifest.Id; 740 if (!Utility::Version{ manifest.Version }.IsUnknown()) 741 { 742 packageDownloadFolderName += '_' + manifest.Version; 743 } 744 context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / Utility::ConvertToUTF16(packageDownloadFolderName)); 745 } 746 } 747 748 void ExportManifest(Execution::Context& context) 749 { 750 const auto& downloadDirectory = context.Get<Execution::Data::DownloadDirectory>(); 751 const auto& manifest = context.Get<Execution::Data::Manifest>(); 752 const auto& installer = context.Get<Execution::Data::Installer>(); 753 754 std::filesystem::path manifestFileName = GetInstallerDownloadOnlyFileName(context, L".yaml"); 755 auto manifestDownloadPath = downloadDirectory / manifestFileName; 756 YamlWriter::OutputYamlFile(manifest, installer.value(), manifestDownloadPath); 757 AICLI_LOG(CLI, Info, << "Successfully generated manifest yaml. Path: " << manifestDownloadPath); 758 } 759 760 void EnsureSupportForDownload(Execution::Context& context) 761 { 762 // No checks needed if not download installer only. 763 if (WI_IsFlagClear(context.GetFlags(), Execution::ContextFlag::InstallerDownloadOnly)) 764 { 765 return; 766 } 767 768 const auto& installer = context.Get<Execution::Data::Installer>(); 769 770 if (installer->DownloadCommandProhibited) 771 { 772 context.Reporter.Error() << Resource::String::InstallerDownloadCommandProhibited << std::endl; 773 AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_DOWNLOAD_COMMAND_PROHIBITED); 774 } 775 } 776 777 void InitializeInstallerDownloadAuthenticatorsMap(Execution::Context& context) 778 { 779 context.Add<Execution::Data::InstallerDownloadAuthenticators>(std::make_shared<std::map<Authentication::AuthenticationInfo, Authentication::Authenticator>>()); 780 } 781 }