winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }