commit 19ce709f5d628b9dd3495defa4dc3217b05e4cda parent 37247889e0fe34be0163202861d8b2049b8c9e04 Author: JohnMcPMS <johnmcp@microsoft.com> Date: Fri, 11 Oct 2024 14:55:38 -0700 Add hash mismatch telemetry details (#4857) ## Issue From looking at our hash mismatch data, there were two main modes: 1. Lots of actual hashes that are the result of hashing a zero-byte file 2. Other hashes with small numbers of cases, including some non-one counts. This suggests at least some cases with consistent results even if they are not what we expected. My suspicion is that there are some cases where a successful HTTP response is made, but the content is actually a webpage or similar. This would happen with captive portals or potentially firewalls. ## Change Collect the total size and content type of downloads and add them to our existing hash mismatch telemetry events. Retry downloading when we get a zero-byte file. Report a different error when a zero-byte file is the final result of downloading (also requires the hash to not match, so you can still have a zero-byte installer for what that is worth...). ## Validation Updates the downloader tests for new fields. Adds a new pair of tests for zero-byte file downloads. Diffstat:
36 files changed, 372 insertions(+), 747 deletions(-)
diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt @@ -126,6 +126,8 @@ decompressor dedupe deigh deleteifnotneeded +deliveryoptimization +deliveryoptimizationerrors DENYWR desktopappinstaller devhome @@ -338,6 +340,7 @@ msft msftrubengu MSIHASH MSIXHASH +MSIXSTRM msstore MSZIP mszyml @@ -405,6 +408,7 @@ PACL PARAMETERMAP pathparts Patil +pbstr pcb PCCERT PCs diff --git a/doc/windows/package-manager/winget/returnCodes.md b/doc/windows/package-manager/winget/returnCodes.md @@ -145,6 +145,7 @@ ms.localizationpriority: medium | 0x8A150083 | -1978335101 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED | Failed to retrieve Microsoft Store package license. | | 0x8A150084 | -1978335100 | APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED | The Microsoft Store package does not support download command. | | 0x8A150085 | -1978335099 | APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN | Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have required privilege. | +| 0x8A150086 | -1978335098 | APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE | Downloaded zero byte installer; ensure that your network connection is working properly. | ## Install errors. diff --git a/src/AppInstallerCLICore/ExecutionContextData.h b/src/AppInstallerCLICore/ExecutionContextData.h @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include <AppInstallerDownloader.h> #include <winget/RepositorySource.h> #include <winget/Manifest.h> #include <winget/ARPCorrelation.h> @@ -34,7 +35,7 @@ namespace AppInstaller::CLI::Execution Manifest, PackageVersion, Installer, - HashPair, + DownloadHashInfo, InstallerPath, LogPath, InstallerArgs, @@ -128,9 +129,9 @@ namespace AppInstaller::CLI::Execution }; template <> - struct DataMapping<Data::HashPair> + struct DataMapping<Data::DownloadHashInfo> { - using value_t = std::pair<std::vector<uint8_t>, std::vector<uint8_t>>; + using value_t = std::pair<std::vector<uint8_t>, Utility::DownloadResult>; }; template <> diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -308,6 +308,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(InstallersAbortTerminal); WINGET_DEFINE_RESOURCE_STRINGID(InstallersRequireInstallLocation); WINGET_DEFINE_RESOURCE_STRINGID(InstallerTypeArgumentDescription); + WINGET_DEFINE_RESOURCE_STRINGID(InstallerZeroByteFile); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowInstallSuccess); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowRegistrationDeferred); WINGET_DEFINE_RESOURCE_STRINGID(InstallFlowReturnCodeAlreadyInstalled); diff --git a/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp b/src/AppInstallerCLICore/Workflows/DownloadFlow.cpp @@ -172,15 +172,15 @@ namespace AppInstaller::CLI::Workflow // Checks the file hash for an existing installer file. // Returns true if the file exists and its hash matches, false otherwise. // If the hash does not match, deletes the file. - bool ExistingInstallerFileHasHashMatch(const SHA256::HashBuffer& expectedHash, const std::filesystem::path& filePath, SHA256::HashBuffer& fileHash) + bool ExistingInstallerFileHasHashMatch(const SHA256::HashBuffer& expectedHash, const std::filesystem::path& filePath, SHA256::HashDetails& fileHashDetails) { if (std::filesystem::exists(filePath)) { AICLI_LOG(CLI, Info, << "Found existing installer file at '" << filePath << "'. Verifying file hash."); std::ifstream inStream{ filePath, std::ifstream::binary }; - fileHash = SHA256::ComputeHash(inStream); + fileHashDetails = SHA256::ComputeHashDetails(inStream); - if (SHA256::AreEqual(expectedHash, fileHash)) + if (SHA256::AreEqual(expectedHash, fileHashDetails.Hash)) { return true; } @@ -280,11 +280,11 @@ namespace AppInstaller::CLI::Workflow // Try looking for the file with and without extension. auto installerPath = GetInstallerBaseDownloadPath(context); auto installerFilename = GetInstallerPreHashValidationFileName(context); - SHA256::HashBuffer fileHash; - if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHash)) + SHA256::HashDetails fileHashDetails; + if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHashDetails)) { installerFilename = GetInstallerPostHashValidationFileName(context); - if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHash)) + if (!ExistingInstallerFileHasHashMatch(installer.Sha256, installerPath / installerFilename, fileHashDetails)) { // No match return; @@ -293,7 +293,8 @@ namespace AppInstaller::CLI::Workflow AICLI_LOG(CLI, Info, << "Existing installer file hash matches. Will use existing installer."); context.Add<Execution::Data::InstallerPath>(installerPath / installerFilename); - context.Add<Execution::Data::HashPair>(std::make_pair(installer.Sha256, fileHash)); + context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, + DownloadResult{ std::move(fileHashDetails.Hash), fileHashDetails.SizeInBytes })); } void GetInstallerDownloadPath(Execution::Context& context) @@ -325,7 +326,7 @@ namespace AppInstaller::CLI::Workflow context.Reporter.Info() << Resource::String::Downloading << ' ' << Execution::UrlEmphasis << installer.Url << std::endl; - std::optional<std::vector<BYTE>> hash; + DownloadResult downloadResult; constexpr int MaxRetryCount = 2; constexpr std::chrono::seconds maximumWaitTimeAllowed = 60s; @@ -334,15 +335,22 @@ namespace AppInstaller::CLI::Workflow bool success = false; try { - hash = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, + downloadResult = context.Reporter.ExecuteWithProgress(std::bind(Utility::Download, installer.Url, installerPath, Utility::DownloadType::Installer, std::placeholders::_1, - true, downloadInfo)); - success = true; + if (downloadResult.SizeInBytes == 0) + { + AICLI_LOG(CLI, Info, << "Got zero byte file; retrying download after a short wait..."); + std::this_thread::sleep_for(5s); + } + else + { + success = true; + } } catch (const ServiceUnavailableException& sue) { @@ -388,13 +396,13 @@ namespace AppInstaller::CLI::Workflow } } - if (!hash) + if (downloadResult.Sha256Hash.empty()) { context.Reporter.Info() << Resource::String::Cancelled << std::endl; AICLI_TERMINATE_CONTEXT(E_ABORT); } - context.Add<Execution::Data::HashPair>(std::make_pair(installer.Sha256, hash.value())); + context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, downloadResult)); } void GetMsixSignatureHash(Execution::Context& context) @@ -408,9 +416,14 @@ namespace AppInstaller::CLI::Workflow // Signature hash is only used for streaming installs, which don't use proxy Msix::MsixInfo msixInfo(installer.Url); - auto signatureHash = msixInfo.GetSignatureHash(); - context.Add<Execution::Data::HashPair>(std::make_pair(installer.SignatureSha256, signatureHash)); + DownloadResult hashInfo{ msixInfo.GetSignatureHash() }; + // Value is ASCII for MSIXSTRM + // A sentinel value to indicate that this is a streaming hash rather than a download. + // The primary purpose is to prevent us from falling into the code path for zero byte files. + hashInfo.SizeInBytes = 0x4D5349585354524D; + + context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.SignatureSha256, hashInfo)); context.Add<Execution::Data::MsixDigests>({ std::make_pair(installer.Url, msixInfo.GetDigest()) }); } catch (...) @@ -427,17 +440,23 @@ namespace AppInstaller::CLI::Workflow void VerifyInstallerHash(Execution::Context& context) { - const auto& hashPair = context.Get<Execution::Data::HashPair>(); + const auto& [expectedHash, downloadResult] = context.Get<Execution::Data::DownloadHashInfo>(); if (!std::equal( - hashPair.first.begin(), - hashPair.first.end(), - hashPair.second.begin())) + expectedHash.begin(), + expectedHash.end(), + downloadResult.Sha256Hash.begin())) { bool overrideHashMismatch = context.Args.Contains(Execution::Args::Type::HashOverride); const auto& manifest = context.Get<Execution::Data::Manifest>(); - Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, hashPair.first, hashPair.second, overrideHashMismatch); + Logging::Telemetry().LogInstallerHashMismatch(manifest.Id, manifest.Version, manifest.Channel, expectedHash, downloadResult.Sha256Hash, overrideHashMismatch, downloadResult.SizeInBytes, downloadResult.ContentType); + + if (downloadResult.SizeInBytes == 0) + { + context.Reporter.Error() << Resource::String::InstallerZeroByteFile << std::endl; + AICLI_TERMINATE_CONTEXT(APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE); + } // If running as admin, do not allow the user to override the hash failure. if (Runtime::IsRunningAsAdmin()) @@ -526,8 +545,9 @@ namespace AppInstaller::CLI::Workflow // Get the hash from the installer file const auto& installerPath = context.Get<Execution::Data::InstallerPath>(); std::ifstream inStream{ installerPath, std::ifstream::binary }; - auto existingFileHash = SHA256::ComputeHash(inStream); - context.Add<Execution::Data::HashPair>(std::make_pair(installer.Sha256, existingFileHash)); + auto existingFileHashDetails = SHA256::ComputeHashDetails(inStream); + context.Add<Execution::Data::DownloadHashInfo>(std::make_pair(installer.Sha256, + DownloadResult{ existingFileHashDetails.Hash, existingFileHashDetails.SizeInBytes })); } else if (installer.EffectiveInstallerType() == InstallerTypeEnum::MSStore) { diff --git a/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp b/src/AppInstallerCLICore/Workflows/MSStoreInstallerHandler.cpp @@ -101,8 +101,8 @@ namespace AppInstaller::CLI::Workflow } // Verify hash - const auto& hashPair = subContext.Get<Execution::Data::HashPair>(); - if (std::equal(hashPair.first.begin(), hashPair.first.end(), hashPair.second.begin())) + const auto& hashPair = subContext.Get<Execution::Data::DownloadHashInfo>(); + if (std::equal(hashPair.first.begin(), hashPair.first.end(), hashPair.second.Sha256Hash.begin())) { AICLI_LOG(CLI, Info, << "Microsoft Store package hash verified"); subContext.Reporter.Info() << Resource::String::MSStoreDownloadPackageHashVerified << std::endl; diff --git a/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj b/src/AppInstallerCLIE2ETests/AppInstallerCLIE2ETests.csproj @@ -60,6 +60,7 @@ <ItemGroup> <None Remove="TestData\Configuration\ShowDetails_TestRepo_0_3.yml" /> <None Remove="TestData\Configuration\WithParameters_0_3.yml" /> + <None Remove="TestData\empty" /> <None Remove="TestData\Manifests\TestUpgradeAddsDependency.1.0.yaml" /> <None Remove="TestData\Manifests\TestUpgradeAddsDependency.2.0.yaml" /> <None Remove="TestData\Manifests\TestUpgradeAddsDependencyDependent.1.0.yaml" /> diff --git a/src/AppInstallerCLIE2ETests/Constants.cs b/src/AppInstallerCLIE2ETests/Constants.cs @@ -270,6 +270,8 @@ namespace AppInstallerCLIE2ETests public const int ERROR_REPAIR_NOT_SUPPORTED = unchecked((int)0x8A15007C); public const int ERROR_ADMIN_CONTEXT_REPAIR_PROHIBITED = unchecked((int)0x8A15007D); + public const int ERROR_INSTALLER_ZERO_BYTE_FILE = unchecked((int)0x8A150086); + public const int ERROR_INSTALL_PACKAGE_IN_USE = unchecked((int)0x8A150101); public const int ERROR_INSTALL_INSTALL_IN_PROGRESS = unchecked((int)0x8A150102); public const int ERROR_INSTALL_FILE_IN_USE = unchecked((int)0x8A150103); diff --git a/src/AppInstallerCLIE2ETests/DownloadCommand.cs b/src/AppInstallerCLIE2ETests/DownloadCommand.cs @@ -6,8 +6,8 @@ namespace AppInstallerCLIE2ETests { - using System.IO; - using AppInstallerCLIE2ETests.Helpers; + using System.IO; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using NUnit.Framework; using Windows.System; @@ -27,8 +27,8 @@ namespace AppInstallerCLIE2ETests var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.PackageDependency --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); - Assert.True(TestCommon.VerifyInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US")); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + TestCommon.AssertInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US"); + TestCommon.AssertInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US"); } /// <summary> @@ -42,7 +42,7 @@ namespace AppInstallerCLIE2ETests Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); Assert.True(result.StdOut.Contains("Dependencies skipped.")); Assert.IsFalse(Directory.Exists(Path.Combine(downloadDir, Constants.Dependencies))); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US"); } /// <summary> @@ -55,7 +55,7 @@ namespace AppInstallerCLIE2ETests var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --version {packageVersion}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); } /// <summary> @@ -67,7 +67,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"{Constants.ExeInstallerPackageId} --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); } /// <summary> @@ -79,7 +79,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --scope user --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US"); } /// <summary> @@ -91,7 +91,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --scope machine --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -103,7 +103,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --installer-type zip --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true); } /// <summary> @@ -115,7 +115,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --installer-type msi --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -127,7 +127,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --architecture x86 --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -139,7 +139,7 @@ namespace AppInstallerCLIE2ETests var downloadDir = TestCommon.GetRandomTestDir(); var result = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestMultipleInstallers --locale zh-CN --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true); } /// <summary> @@ -152,5 +152,28 @@ namespace AppInstallerCLIE2ETests var errorResult = TestCommon.RunAICLICommand("download", $"AppInstallerTest.TestExeSha256Mismatch --download-directory {downloadDir}"); Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALLER_HASH_MISMATCH, errorResult.ExitCode); } + + /// <summary> + /// Downloads the zero byte test installer with a hash mismatch. + /// </summary> + [Test] + public void DownloadZeroByteFileWithHashMismatch() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var errorResult = TestCommon.RunAICLICommand("download", $"ZeroByteFile.IncorrectHash --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.ERROR_INSTALLER_ZERO_BYTE_FILE, errorResult.ExitCode); + } + + /// <summary> + /// Downloads the zero byte test installer. + /// </summary> + [Test] + public void DownloadZeroByteFile() + { + var downloadDir = TestCommon.GetRandomTestDir(); + var result = TestCommon.RunAICLICommand("download", $"ZeroByteFile.CorrectHash --download-directory {downloadDir}"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + TestCommon.AssertInstallerDownload(downloadDir, "ZeroByteFile-CorrectHash", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US"); + } } -}- \ No newline at end of file +} diff --git a/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs b/src/AppInstallerCLIE2ETests/Helpers/TestCommon.cs @@ -499,7 +499,7 @@ namespace AppInstallerCLIE2ETests.Helpers } /// <summary> - /// Verify installer and manifest downloaded correctly and cleanup. + /// Assert installer and manifest downloaded correctly and cleanup. /// </summary> /// <param name="downloadDir">Download directory.</param> /// <param name="name">Package name.</param> @@ -510,8 +510,7 @@ namespace AppInstallerCLIE2ETests.Helpers /// <param name="locale">Installer locale.</param> /// <param name="isArchive">Boolean value indicating whether the installer is an archive.</param> /// <param name="cleanup">Boolean value indicating whether to remove the installer file and directory.</param> - /// <returns>True if success.</returns> - public static bool VerifyInstallerDownload( + public static void AssertInstallerDownload( string downloadDir, string name, string version, @@ -554,21 +553,14 @@ namespace AppInstallerCLIE2ETests.Helpers string installerDownloadPath = Path.Combine(downloadDir, expectedFileName + installerExtension); string manifestDownloadPath = Path.Combine(downloadDir, expectedFileName + ".yaml"); - bool downloadResult = false; + Assert.IsTrue(Directory.Exists(downloadDir), $"Download directory does not exist: {downloadDir}"); + Assert.IsTrue(File.Exists(installerDownloadPath), $"Installer file does not exist: {installerDownloadPath}"); + Assert.IsTrue(File.Exists(manifestDownloadPath), $"Manifest file does not exist: {manifestDownloadPath}"); - if (Directory.Exists(downloadDir) && File.Exists(installerDownloadPath) && File.Exists(manifestDownloadPath)) + if (cleanup) { - downloadResult = true; - - if (cleanup) - { - File.Delete(installerDownloadPath); - File.Delete(manifestDownloadPath); - Directory.Delete(downloadDir, true); - } + Directory.Delete(downloadDir, true); } - - return downloadResult; } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs b/src/AppInstallerCLIE2ETests/Interop/DownloadInterop.cs @@ -1,4 +1,4 @@ -// ----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // <copyright file="DownloadInterop.cs" company="Microsoft Corporation"> // Copyright (c) Microsoft Corporation. Licensed under the MIT License. // </copyright> @@ -8,8 +8,8 @@ namespace AppInstallerCLIE2ETests.Interop { using System; using System.IO; - using System.Threading.Tasks; - using AppInstallerCLIE2ETests.Helpers; + using System.Threading.Tasks; + using AppInstallerCLIE2ETests.Helpers; using Microsoft.Management.Deployment; using Microsoft.Management.Deployment.Projection; using NUnit.Framework; @@ -66,8 +66,8 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); - Assert.True(TestCommon.VerifyInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US")); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + TestCommon.AssertInstallerDownload(dependenciesDir, "TestPortableExe", "3.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Portable, "en-US"); + TestCommon.AssertInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US"); } /// <summary> @@ -94,7 +94,7 @@ namespace AppInstallerCLIE2ETests.Interop Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); var dependenciesDir = Path.Combine(downloadDir, Constants.Dependencies); Assert.IsFalse(Directory.Exists(dependenciesDir)); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestPackageDependency", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "en-US"); } /// <summary> @@ -118,7 +118,7 @@ namespace AppInstallerCLIE2ETests.Interop var packageVersion = "2.0.0.0"; Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ExeInstallerPackageId}_{packageVersion}"); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); } /// <summary> @@ -142,7 +142,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe)); + TestCommon.AssertInstallerDownload(downloadDir, "TestExeInstaller", "2.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Exe); } /// <summary> @@ -167,7 +167,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.User, PackageInstallerType.Nullsoft, "en-US"); } /// <summary> @@ -192,7 +192,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -217,7 +217,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true); } /// <summary> @@ -242,7 +242,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -267,7 +267,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X86, TestCommon.Scope.Machine, PackageInstallerType.Msi, "en-US"); } /// <summary> @@ -292,7 +292,7 @@ namespace AppInstallerCLIE2ETests.Interop // Assert Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true)); + TestCommon.AssertInstallerDownload(downloadDir, "TestMultipleInstallers", "1.0.0.0", ProcessorArchitecture.X64, TestCommon.Scope.Unknown, PackageInstallerType.Exe, "zh-CN", true); } /// <summary> diff --git a/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs b/src/AppInstallerCLIE2ETests/Interop/GroupPolicyForInterop.cs @@ -158,7 +158,7 @@ namespace AppInstallerCLIE2ETests.Interop Assert.AreEqual(DownloadResultStatus.Ok, downloadResult.Status); var packageVersion = "2.0.0.0"; string downloadDir = Path.Combine(TestCommon.GetDefaultDownloadDirectory(), $"{Constants.ModifyRepairInstaller}_{packageVersion}"); - Assert.True(TestCommon.VerifyInstallerDownload(downloadDir, "TestModifyRepair", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Burn, "en-US")); + TestCommon.AssertInstallerDownload(downloadDir, "TestModifyRepair", packageVersion, ProcessorArchitecture.X86, TestCommon.Scope.Unknown, PackageInstallerType.Burn, "en-US"); } } } diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/ZeroByteFile_CorrectHash.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/ZeroByteFile_CorrectHash.yaml @@ -0,0 +1,14 @@ +PackageIdentifier: ZeroByteFile.CorrectHash +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: ZeroByteFile-CorrectHash +ShortDescription: Points to a zero byte installer file using the correct hash +Publisher: Microsoft Corporation +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/TestData/empty + InstallerType: exe + InstallerSha256: E3B0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852B855 +ManifestType: singleton +ManifestVersion: 1.6.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/Manifests/ZeroByteFile_IncorrectHash.yaml b/src/AppInstallerCLIE2ETests/TestData/Manifests/ZeroByteFile_IncorrectHash.yaml @@ -0,0 +1,14 @@ +PackageIdentifier: ZeroByteFile.IncorrectHash +PackageVersion: 1.0.0.0 +PackageLocale: en-US +PackageName: ZeroByteFile-IncorrectHash +ShortDescription: Points to a zero byte installer file using the incorrect hash +Publisher: Microsoft Corporation +License: Test +Installers: + - Architecture: x86 + InstallerUrl: https://localhost:5001/TestKit/TestData/empty + InstallerType: exe + InstallerSha256: BAD0C44298FC1C149AFBF4C8996FB92427AE41E4649B934CA495991B7852BBAD +ManifestType: singleton +ManifestVersion: 1.6.0 diff --git a/src/AppInstallerCLIE2ETests/TestData/empty b/src/AppInstallerCLIE2ETests/TestData/empty diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -3133,4 +3133,10 @@ Please specify one of them using the --source option to proceed.</value> <data name="WINGET_CONFIG_ERROR_PARAMETER_INTEGRITY_BOUNDARY" xml:space="preserve"> <value>Parameter cannot be passed across integrity boundary.</value> </data> + <data name="APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE" xml:space="preserve"> + <value>Downloaded zero byte installer; ensure that your network connection is working properly.</value> + </data> + <data name="InstallerZeroByteFile" xml:space="preserve"> + <value>Downloaded zero byte installer; ensure that your network connection is working properly.</value> + </data> </root> \ No newline at end of file diff --git a/src/AppInstallerCLITests/Downloader.cpp b/src/AppInstallerCLITests/Downloader.cpp @@ -17,10 +17,10 @@ TEST_CASE("DownloadValidFileAndVerifyHash", "[Downloader]") // Todo: point to files from our repo when the repo goes public ProgressCallback callback; - auto result = Download("https://raw.githubusercontent.com/microsoft/msix-packaging/master/LICENSE", tempFile.GetPath(), DownloadType::Manifest, callback, true); + auto result = Download("https://raw.githubusercontent.com/microsoft/msix-packaging/master/LICENSE", tempFile.GetPath(), DownloadType::Manifest, callback); - REQUIRE(result.has_value()); - auto resultHash = result.value(); + REQUIRE(!result.Sha256Hash.empty()); + auto resultHash = result.Sha256Hash; auto expectedHash = SHA256::ConvertToBytes("d2a45116709136462ee7a1c42f0e75f0efa258fe959b1504dc8ea4573451b759"); REQUIRE(std::equal( @@ -28,7 +28,12 @@ TEST_CASE("DownloadValidFileAndVerifyHash", "[Downloader]") expectedHash.end(), resultHash.begin())); - REQUIRE(std::filesystem::file_size(tempFile.GetPath()) > 0); + uint64_t expectedFileSize = 1119; + REQUIRE(result.SizeInBytes == expectedFileSize); + REQUIRE(std::filesystem::file_size(tempFile.GetPath()) == expectedFileSize); + + REQUIRE(result.ContentType); + REQUIRE(!result.ContentType.value().empty()); // Verify motw content std::filesystem::path motwFile(tempFile); @@ -47,17 +52,17 @@ TEST_CASE("DownloadValidFileAndCancel", "[Downloader]") ProgressCallback callback; - std::optional<std::vector<BYTE>> waitResult; + DownloadResult waitResult; std::thread waitThread([&] { - waitResult = Download("https://aka.ms/win32-x64-user-stable", tempFile.GetPath(), DownloadType::Installer, callback, true); + waitResult = Download("https://aka.ms/win32-x64-user-stable", tempFile.GetPath(), DownloadType::Installer, callback); }); callback.Cancel(); waitThread.join(); - REQUIRE(!waitResult.has_value()); + REQUIRE(waitResult.Sha256Hash.empty()); } TEST_CASE("DownloadInvalidUrl", "[Downloader]") @@ -67,7 +72,7 @@ TEST_CASE("DownloadInvalidUrl", "[Downloader]") ProgressCallback callback; - REQUIRE_THROWS(Download("blargle-flargle-fluff", tempFile.GetPath(), DownloadType::Installer, callback, true)); + REQUIRE_THROWS(Download("blargle-flargle-fluff", tempFile.GetPath(), DownloadType::Installer, callback)); } TEST_CASE("HttpStream_ReadLastFullPage", "[HttpStream]") diff --git a/src/AppInstallerCLITests/InstallFlow.cpp b/src/AppInstallerCLITests/InstallFlow.cpp @@ -34,7 +34,7 @@ void OverrideForDirectMsi(TestContext& context) context.Override({ DownloadInstallerFile, [](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); // We don't have an msi installer for tests, but we won't execute it anyway context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); } }); @@ -92,7 +92,7 @@ TEST_CASE("InstallFlow_RenameFromEncodedUrl", "[InstallFlow][workflow]") OverrideForCheckExistingInstaller(context); context.Override({ DownloadInstallerFile, [](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); auto installerPath = std::filesystem::temp_directory_path(); installerPath /= "EncodedUrlTest.exe"; std::filesystem::copy(TestDataFile("AppInstallerTestExeInstaller.exe"), installerPath, std::filesystem::copy_options::overwrite_existing); @@ -124,7 +124,7 @@ TEST_CASE("InstallFlow_RenameFromInvalidFileCharacterUrl", "[InstallFlow][workfl OverrideForCheckExistingInstaller(context); context.Override({ DownloadInstallerFile, [](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); auto installerPath = std::filesystem::temp_directory_path(); installerPath /= "InvalidFileCharacterUrlTest.exe"; std::filesystem::copy(TestDataFile("AppInstallerTestExeInstaller.exe"), installerPath, std::filesystem::copy_options::overwrite_existing); diff --git a/src/AppInstallerCLITests/WorkFlow.cpp b/src/AppInstallerCLITests/WorkFlow.cpp @@ -45,7 +45,7 @@ TEST_CASE("VerifyInstallerTrustLevelAndUpdateInstallerFileMotw", "[DownloadInsta std::ostringstream updateMotwOutput; TestContext context{ updateMotwOutput, std::cin }; auto previousThreadGlobals = context.SetForCurrentThread(); - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); context.Add<Data::InstallerPath>(testInstallerPath); auto packageVersion = std::make_shared<TestPackageVersion>(Manifest{}); auto testSource = std::make_shared<TestSource>(); diff --git a/src/AppInstallerCLITests/WorkflowCommon.cpp b/src/AppInstallerCLITests/WorkflowCommon.cpp @@ -558,7 +558,7 @@ namespace TestCommon context.Override({ DownloadInstallerFile, [](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); }, expectedUseCount }); @@ -573,7 +573,7 @@ namespace TestCommon { context.Override({ DownloadInstallerFile, [&installationLog](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); auto dependency = Dependency(DependencyType::Package, context.Get<Execution::Data::Manifest>().Id, context.Get<Execution::Data::Manifest>().Version); @@ -602,7 +602,7 @@ namespace TestCommon { context.Override({ DownloadInstallerFile, [](TestContext& context) { - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); context.Add<Data::InstallerPath>(TestDataFile("AppInstallerTestExeInstaller.exe")); } }); @@ -626,7 +626,7 @@ namespace TestCommon std::ifstream inStream{ tempInstallerPath, std::ifstream::binary }; SHA256::HashBuffer fileHash = SHA256::ComputeHash(inStream); - context.Add<Data::HashPair>({ fileHash, fileHash }); + context.Add<Data::DownloadHashInfo>({ fileHash, DownloadResult{ fileHash } }); } }); context.Override({ RenameDownloadedInstaller, [](TestContext&) @@ -732,7 +732,7 @@ namespace TestCommon std::ofstream file(installerPath, std::ofstream::out | std::ofstream::trunc); file << installer.Url; file.close(); - context.Add<Data::HashPair>({ {}, {} }); + context.Add<Data::DownloadHashInfo>({ {}, {} }); } }); } } diff --git a/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp b/src/AppInstallerCommonCore/AppInstallerTelemetry.cpp @@ -546,8 +546,12 @@ namespace AppInstaller::Logging std::string_view channel, const std::vector<uint8_t>& expected, const std::vector<uint8_t>& actual, - bool overrideHashMismatch) const noexcept + bool overrideHashMismatch, + uint64_t downloadSizeInBytes, + const std::optional<std::string>& contentType) const noexcept { + std::string actualContentType = contentType.value_or(std::string{}); + if (IsTelemetryEnabled()) { AICLI_TraceLoggingWriteActivity( @@ -559,6 +563,8 @@ namespace AppInstaller::Logging TraceLoggingBinary(expected.data(), static_cast<ULONG>(expected.size()), "Expected"), TraceLoggingBinary(actual.data(), static_cast<ULONG>(actual.size()), "Actual"), TraceLoggingBool(overrideHashMismatch, "Override"), + TraceLoggingUInt64(downloadSizeInBytes, "ActualSize"), + AICLI_TraceLoggingStringView(actualContentType, "ContentType"), TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance), TraceLoggingKeyword(MICROSOFT_KEYWORD_CRITICAL_DATA)); @@ -570,6 +576,8 @@ namespace AppInstaller::Logging m_summary.HashMismatchExpected = expected; m_summary.HashMismatchActual = actual; m_summary.HashMismatchOverride = overrideHashMismatch; + m_summary.HashMismatchActualSize = downloadSizeInBytes; + m_summary.HashMismatchContentType = actualContentType; } } @@ -578,7 +586,7 @@ namespace AppInstaller::Logging << Utility::SHA256::ConvertToString(expected) << "] does not match download [" << Utility::SHA256::ConvertToString(actual) - << ']'); + << "] with file size [" << downloadSizeInBytes << "] and content type [" << actualContentType << "]"); } void TelemetryTraceLogger::LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) const noexcept @@ -812,6 +820,8 @@ namespace AppInstaller::Logging TraceLoggingBinary(m_summary.HashMismatchExpected.data(), static_cast<ULONG>(m_summary.HashMismatchExpected.size()), "HashMismatchExpected"), TraceLoggingBinary(m_summary.HashMismatchActual.data(), static_cast<ULONG>(m_summary.HashMismatchActual.size()), "HashMismatchActual"), TraceLoggingBool(m_summary.HashMismatchOverride, "HashMismatchOverride"), + TraceLoggingUInt64(m_summary.HashMismatchActualSize, "HashMismatchActualSize"), + AICLI_TraceLoggingStringView(m_summary.HashMismatchContentType, "HashMismatchContentType"), AICLI_TraceLoggingStringView(m_summary.InstallerExecutionType, "InstallerExecutionType"), TraceLoggingUInt32(m_summary.InstallerErrorCode, "InstallerErrorCode"), AICLI_TraceLoggingStringView(m_summary.UninstallerExecutionType, "UninstallerExecutionType"), diff --git a/src/AppInstallerCommonCore/DODownloader.cpp b/src/AppInstallerCommonCore/DODownloader.cpp @@ -8,20 +8,44 @@ #include "winget/UserSettings.h" // TODO: Get this from the Windows SDK when available -#include "external/do.h" +#define DODownloadProperty_HttpRedirectionTarget static_cast<DODownloadProperty>(DODownloadProperty_NonVolatile + 1) +#define DODownloadProperty_HttpResponseHeaders static_cast<DODownloadProperty>(DODownloadProperty_HttpRedirectionTarget + 1) +#define DODownloadProperty_HttpServerIPAddress static_cast<DODownloadProperty>(DODownloadProperty_HttpResponseHeaders + 1) +#define DODownloadProperty_HttpStatusCode static_cast<DODownloadProperty>(DODownloadProperty_HttpServerIPAddress + 1) namespace AppInstaller::Utility { - namespace DeliveryOptimization + namespace { -// TODO: Once the SDK headers are available, remove these defines -#define DO_E_DOWNLOAD_NO_PROGRESS HRESULT(0x80D02002L) // Download of a file saw no progress within the defined period + std::optional<std::string> ExtractContentType(const std::optional<std::string>& headers) + { + if (!headers) + { + return std::nullopt; + } + + static constexpr std::string_view s_ContentType = "content-type:"sv; + auto headerLines = Utility::SplitIntoLines(headers.value()); + + for (const auto& header : headerLines) + { + std::string_view headerView = header; + if (header.length() >= s_ContentType.length()) + { + std::string lowerFragment = ToLower(headerView.substr(0, s_ContentType.length())); + if (s_ContentType == lowerFragment) + { + return Trim(header.substr(s_ContentType.length())); + } + } + } -#define DO_E_BLOCKED_BY_COST_TRANSFER_POLICY HRESULT(0x80D03801L) // DO core paused the job due to cost policy restrictions -#define DO_E_BLOCKED_BY_CELLULAR_POLICY HRESULT(0x80D03803L) // DO core paused the job due to detection of cellular network and policy restrictions -#define DO_E_BLOCKED_BY_POWER_STATE HRESULT(0x80D03804L) // DO core paused the job due to detection of power state change into non-AC mode -#define DO_E_BLOCKED_BY_NO_NETWORK HRESULT(0x80D03805L) // DO core paused the job due to loss of network connectivity + return std::nullopt; + } + } + namespace DeliveryOptimization + { // Represents a download work item for Delivery Optimization. struct Download { @@ -106,6 +130,23 @@ namespace AppInstaller::Utility THROW_IF_FAILED(m_download->SetProperty(prop, &var)); } + template<typename T> + std::optional<T> TryGetProperty(DODownloadProperty prop) + { + std::optional<T> result; + wil::unique_variant var; + HRESULT hr = m_download->GetProperty(prop, &var); + if (SUCCEEDED(hr)) + { + T value; + if (ExtractFromVariant(var, value)) + { + result = std::move(value); + } + } + return result; + } + void Uri(std::string_view uri) { SetProperty(DODownloadProperty_Uri, uri); @@ -186,6 +227,22 @@ namespace AppInstaller::Utility } private: + bool ExtractFromVariant(const VARIANT& var, std::string& value) + { + if (var.vt == VT_BSTR && var.bstrVal != nullptr) + { + value = Utility::ConvertToUTF8(var.bstrVal); + return true; + } + else if (var.vt == (VT_BSTR | VT_BYREF) && var.pbstrVal != nullptr && *var.pbstrVal != nullptr) + { + value = Utility::ConvertToUTF8(*var.pbstrVal); + return true; + } + + return false; + } + wil::com_ptr<IDODownload> m_download; }; @@ -221,7 +278,7 @@ namespace AppInstaller::Utility { } - IFACEMETHOD(OnStatusChange)(IDODownload*, DO_DOWNLOAD_STATUS* status) + IFACEMETHOD(OnStatusChange)(IDODownload*, const DO_DOWNLOAD_STATUS* status) { { std::lock_guard<std::mutex> guard(m_statusMutex); @@ -341,11 +398,10 @@ namespace AppInstaller::Utility // Debugging tip: // From an elevated PowerShell, run: // > Get-DeliveryOptimizationLog | Set-Content doLogs.txt - std::optional<std::vector<BYTE>> DODownload( + DownloadResult DODownload( const std::string& url, const std::filesystem::path& dest, IProgressCallback& progress, - bool computeHash, std::optional<DownloadInfo> info) { AICLI_LOG(Core, Info, << "DeliveryOptimization downloading from url: " << url); @@ -397,15 +453,22 @@ namespace AppInstaller::Utility // Wait returns true for success, false for cancellation, and throws on error. if (callback->Wait()) { + // Grab the headers so that we can use them later + std::optional<std::string> responseHeaders = download.TryGetProperty<std::string>(DODownloadProperty_HttpResponseHeaders); + // Finalize is required to flush the data and change the file name. download.Finalize(); AICLI_LOG(Core, Info, << "Download completed."); - if (computeHash) - { - std::ifstream inStream{ dest, std::ifstream::binary }; - return SHA256::ComputeHash(inStream); - } + std::ifstream inStream{ dest, std::ifstream::binary }; + auto hashDetails = SHA256::ComputeHashDetails(inStream); + + DownloadResult result; + result.Sha256Hash = std::move(hashDetails.Hash); + result.SizeInBytes = hashDetails.SizeInBytes; + result.ContentType = ExtractContentType(responseHeaders); + + return result; } return {}; diff --git a/src/AppInstallerCommonCore/DODownloader.h b/src/AppInstallerCommonCore/DODownloader.h @@ -15,11 +15,10 @@ namespace AppInstaller::Utility // url: The url to be downloaded from. http->https redirection is allowed. // dest: The stream to be downloaded to. // computeHash: Optional. Indicates if SHA256 hash should be calculated when downloading. - std::optional<std::vector<BYTE>> DODownload( + DownloadResult DODownload( const std::string& url, const std::filesystem::path& dest, IProgressCallback& progress, - bool computeHash, std::optional<DownloadInfo> info); // Returns true if the error from DODownload should be treated as fatal; diff --git a/src/AppInstallerCommonCore/Downloader.cpp b/src/AppInstallerCommonCore/Downloader.cpp @@ -27,6 +27,47 @@ namespace AppInstaller::Utility { namespace { + std::wstring GetHttpQueryString(const wil::unique_hinternet& urlFile, DWORD queryProperty) + { + std::wstring result = {}; + DWORD length = 0; + if (!HttpQueryInfoW(urlFile.get(), + queryProperty, + &result[0], + &length, + nullptr)) + { + auto lastError = GetLastError(); + if (lastError == ERROR_INSUFFICIENT_BUFFER) + { + // lpdwBufferLength contains the size, in bytes, of a buffer large enough to receive the requested information + // without the nul char. not the exact buffer size. + auto size = static_cast<size_t>(length) / sizeof(wchar_t); + result.resize(size + 1); + if (HttpQueryInfoW(urlFile.get(), + queryProperty, + &result[0], + &length, + nullptr)) + { + // because the buffer can be bigger remove possible null chars + result.erase(result.find(L'\0')); + } + else + { + AICLI_LOG(Core, Error, << "Error retrieving header value [" << queryProperty << "]: " << GetLastError()); + result.clear(); + } + } + else + { + AICLI_LOG(Core, Error, << "Error retrieving header [" << queryProperty << "]: " << GetLastError()); + } + } + + return result; + } + // Gets the retry after value in terms of a delay in seconds std::chrono::seconds GetRetryAfter(const HttpDateOrDeltaHeaderValue& retryAfter) { @@ -56,47 +97,15 @@ namespace AppInstaller::Utility std::chrono::seconds GetRetryAfter(const wil::unique_hinternet& urlFile) { - std::wstring retryAfter = {}; - DWORD length = 0; - if (!HttpQueryInfoW(urlFile.get(), - HTTP_QUERY_RETRY_AFTER, - &retryAfter, - &length, - nullptr)) - { - auto lastError = GetLastError(); - if (lastError == ERROR_INSUFFICIENT_BUFFER) - { - // lpdwBufferLength contains the size, in bytes, of a buffer large enough to receive the requested information - // without the nul char. not the exact buffer size. - auto size = static_cast<size_t>(length) / sizeof(wchar_t); - retryAfter.resize(size + 1); - if (HttpQueryInfoW(urlFile.get(), - HTTP_QUERY_RETRY_AFTER, - &retryAfter[0], - &length, - nullptr)) - { - // because the buffer can be bigger remove possible null chars - retryAfter.erase(retryAfter.find(L'\0')); - return AppInstaller::Utility::GetRetryAfter(retryAfter); - } - } - else - { - AICLI_LOG(Core, Error, << "Error retrieving Retry-After header: " << GetLastError()); - } - } - - return 0s; + std::wstring retryAfter = GetHttpQueryString(urlFile, HTTP_QUERY_RETRY_AFTER); + return retryAfter.empty() ? 0s : AppInstaller::Utility::GetRetryAfter(retryAfter); } } - std::optional<std::vector<BYTE>> WinINetDownloadToStream( + DownloadResult WinINetDownloadToStream( const std::string& url, std::ostream& dest, - IProgressCallback& progress, - bool computeHash) + IProgressCallback& progress) { // For AICLI_LOG usages with string literals. #pragma warning(push) @@ -181,6 +190,9 @@ namespace AppInstaller::Utility nullptr); AICLI_LOG(Core, Verbose, << "Download size: " << contentLength); + std::string contentType = Utility::ConvertToUTF8(GetHttpQueryString(urlFile, HTTP_QUERY_CONTENT_TYPE)); + AICLI_LOG(Core, Verbose, << "Content Type: " << contentType); + // Setup hash engine SHA256 hashEngine; @@ -203,10 +215,7 @@ namespace AppInstaller::Utility THROW_LAST_ERROR_IF_MSG(!readSuccess, "InternetReadFile() failed."); - if (computeHash) - { - hashEngine.Add(buffer.get(), bytesRead); - } + hashEngine.Add(buffer.get(), bytesRead); dest.write((char*)buffer.get(), bytesRead); @@ -227,12 +236,11 @@ namespace AppInstaller::Utility THROW_HR_IF(APPINSTALLER_CLI_ERROR_DOWNLOAD_SIZE_MISMATCH, bytesDownloaded != contentLength); } - std::vector<BYTE> result; - if (computeHash) - { - result = hashEngine.Get(); - AICLI_LOG(Core, Info, << "Download hash: " << SHA256::ConvertToString(result)); - } + DownloadResult result; + result.SizeInBytes = static_cast<uint64_t>(bytesDownloaded); + result.ContentType = std::move(contentType); + result.Sha256Hash = hashEngine.Get(); + AICLI_LOG(Core, Info, << "Download hash: " << SHA256::ConvertToString(result.Sha256Hash)); AICLI_LOG(Core, Info, << "Download completed."); @@ -283,24 +291,22 @@ namespace AppInstaller::Utility return result; } - std::optional<std::vector<BYTE>> DownloadToStream( + DownloadResult DownloadToStream( const std::string& url, std::ostream& dest, DownloadType, IProgressCallback& progress, - bool computeHash, std::optional<DownloadInfo>) { THROW_HR_IF(E_INVALIDARG, url.empty()); - return WinINetDownloadToStream(url, dest, progress, computeHash); + return WinINetDownloadToStream(url, dest, progress); } - std::optional<std::vector<BYTE>> Download( + DownloadResult Download( const std::string& url, const std::filesystem::path& dest, DownloadType type, IProgressCallback& progress, - bool computeHash, std::optional<DownloadInfo> info) { THROW_HR_IF(E_INVALIDARG, url.empty()); @@ -320,7 +326,7 @@ namespace AppInstaller::Utility { try { - auto result = DODownload(url, dest, progress, computeHash, info); + auto result = DODownload(url, dest, progress, info); // Since we cannot pre-apply to the file with DO, post-apply the MotW to the file. // Only do so if the file exists, because cancellation will not throw here. if (std::filesystem::exists(dest)) @@ -362,7 +368,7 @@ namespace AppInstaller::Utility // Use std::ofstream::app to append to previous empty file so that it will not // create a new file and clear motw. std::ofstream outfile(dest, std::ofstream::binary | std::ofstream::app); - return WinINetDownloadToStream(url, outfile, progress, computeHash); + return WinINetDownloadToStream(url, outfile, progress); } using namespace std::string_view_literals; diff --git a/src/AppInstallerCommonCore/FileCache.cpp b/src/AppInstallerCommonCore/FileCache.cpp @@ -51,12 +51,12 @@ namespace AppInstaller::Caching { try { - auto downloadHash = Utility::DownloadToStream(fullPath, *result, Utility::DownloadType::Manifest, emptyCallback, !expectedHash.empty()); + auto downloadResult = Utility::DownloadToStream(fullPath, *result, Utility::DownloadType::Manifest, emptyCallback); if (!expectedHash.empty() && - (!downloadHash || !Utility::SHA256::AreEqual(expectedHash, downloadHash.value()))) + !Utility::SHA256::AreEqual(expectedHash, downloadResult.Sha256Hash)) { - AICLI_LOG(Core, Verbose, << "Invalid hash from [" << fullPath << "]: expected [" << Utility::SHA256::ConvertToString(expectedHash) << "], got [" << (downloadHash ? Utility::SHA256::ConvertToString(*downloadHash) : "null") << "]"); + AICLI_LOG(Core, Verbose, << "Invalid hash from [" << fullPath << "]: expected [" << Utility::SHA256::ConvertToString(expectedHash) << "], got [" << Utility::SHA256::ConvertToString(downloadResult.Sha256Hash) << "]"); THROW_HR(APPINSTALLER_CLI_ERROR_SOURCE_DATA_INTEGRITY_FAILURE); } diff --git a/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h b/src/AppInstallerCommonCore/Public/AppInstallerDownloader.h @@ -36,6 +36,14 @@ namespace AppInstaller::Utility std::string ContentId; }; + // Properties about the downloaded file. + struct DownloadResult + { + std::vector<BYTE> Sha256Hash; + uint64_t SizeInBytes = 0; + std::optional<std::string> ContentType; + }; + // An exception that indicates that a remote service is too busy/unavailable and may contain data on when to try again. struct ServiceUnavailableException : public wil::ResultException { @@ -52,12 +60,11 @@ namespace AppInstaller::Utility // dest: The stream to be downloaded to. // computeHash: Optional. Indicates if SHA256 hash should be calculated when downloading. // downloadInfo: Optional. Currently only used by DO to identify the download. - std::optional<std::vector<BYTE>> DownloadToStream( + DownloadResult DownloadToStream( const std::string& url, std::ostream& dest, DownloadType type, IProgressCallback& progress, - bool computeHash = false, std::optional<DownloadInfo> downloadInfo = {}); // Downloads a file from the given URL and places it in the given location. @@ -65,12 +72,11 @@ namespace AppInstaller::Utility // dest: The path to local file to be downloaded to. // computeHash: Optional. Indicates if SHA256 hash should be calculated when downloading. // downloadInfo: Optional. Currently only used by DO to identify the download. - std::optional<std::vector<BYTE>> Download( + DownloadResult Download( const std::string& url, const std::filesystem::path& dest, DownloadType type, IProgressCallback& progress, - bool computeHash = false, std::optional<DownloadInfo> downloadInfo = {}); // Gets the headers for the given URL. diff --git a/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h b/src/AppInstallerCommonCore/Public/AppInstallerTelemetry.h @@ -101,6 +101,8 @@ namespace AppInstaller::Logging std::vector<uint8_t> HashMismatchExpected; std::vector<uint8_t> HashMismatchActual; bool HashMismatchOverride = false; + uint64_t HashMismatchActualSize = 0; + std::string HashMismatchContentType; // LogInstallerFailure std::string InstallerExecutionType; @@ -232,7 +234,9 @@ namespace AppInstaller::Logging std::string_view channel, const std::vector<uint8_t>& expected, const std::vector<uint8_t>& actual, - bool overrideHashMismatch) const noexcept; + bool overrideHashMismatch, + uint64_t downloadSizeInBytes, + const std::optional<std::string>& contentType) const noexcept; // Logs a failed installation attempt. void LogInstallerFailure(std::string_view id, std::string_view version, std::string_view channel, std::string_view type, uint32_t errorCode) const noexcept; diff --git a/src/AppInstallerCommonCore/external/README.md b/src/AppInstallerCommonCore/external/README.md @@ -1,3 +0,0 @@ -This is a temporary location to store headers from external sources: -1. do.h - - This header for the DeliveryOptimization COM API is not included in the Windows SDK currently, but should be in the near future. While we wait for this fix, we will use this file.- \ No newline at end of file diff --git a/src/AppInstallerCommonCore/external/do.h b/src/AppInstallerCommonCore/external/do.h @@ -1,566 +0,0 @@ - - -/* this ALWAYS GENERATED file contains the definitions for the interfaces */ - - - /* File created by MIDL compiler version 8.01.0626 */ -/* @@MIDL_FILE_HEADING( ) */ - - - -/* verify that the <rpcndr.h> version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCNDR_H_VERSION__ -#define __REQUIRED_RPCNDR_H_VERSION__ 500 -#endif - -/* verify that the <rpcsal.h> version is high enough to compile this file*/ -#ifndef __REQUIRED_RPCSAL_H_VERSION__ -#define __REQUIRED_RPCSAL_H_VERSION__ 100 -#endif - -#include "rpc.h" -#include "rpcndr.h" - -#ifndef __RPCNDR_H_VERSION__ -#error this stub requires an updated version of <rpcndr.h> -#endif /* __RPCNDR_H_VERSION__ */ - -#ifndef COM_NO_WINDOWS_H -#include "windows.h" -#include "ole2.h" -#endif /*COM_NO_WINDOWS_H*/ - -#ifndef __do_h__ -#define __do_h__ - -#if defined(_MSC_VER) && (_MSC_VER >= 1020) -#pragma once -#endif - -#ifndef DECLSPEC_XFGVIRT -#if _CONTROL_FLOW_GUARD_XFG -#define DECLSPEC_XFGVIRT(base, func) __declspec(xfg_virtual(base, func)) -#else -#define DECLSPEC_XFGVIRT(base, func) -#endif -#endif - -/* Forward Declarations */ - -#ifndef __IDODownload_FWD_DEFINED__ -#define __IDODownload_FWD_DEFINED__ -typedef interface IDODownload IDODownload; - -#endif /* __IDODownload_FWD_DEFINED__ */ - - -#ifndef __IDODownloadStatusCallback_FWD_DEFINED__ -#define __IDODownloadStatusCallback_FWD_DEFINED__ -typedef interface IDODownloadStatusCallback IDODownloadStatusCallback; - -#endif /* __IDODownloadStatusCallback_FWD_DEFINED__ */ - - -#ifndef __IDOManager_FWD_DEFINED__ -#define __IDOManager_FWD_DEFINED__ -typedef interface IDOManager IDOManager; - -#endif /* __IDOManager_FWD_DEFINED__ */ - - -#ifndef __DeliveryOptimization_FWD_DEFINED__ -#define __DeliveryOptimization_FWD_DEFINED__ - -#ifdef __cplusplus -typedef class DeliveryOptimization DeliveryOptimization; -#else -typedef struct DeliveryOptimization DeliveryOptimization; -#endif /* __cplusplus */ - -#endif /* __DeliveryOptimization_FWD_DEFINED__ */ - - -/* header files for imported files */ -#include "oaidl.h" - -#ifdef __cplusplus -extern "C"{ -#endif - - -/* interface __MIDL_itf_do_0000_0000 */ -/* [local] */ - -typedef struct _DO_DOWNLOAD_RANGE - { - UINT64 Offset; - UINT64 Length; - } DO_DOWNLOAD_RANGE; - -typedef struct _DO_DOWNLOAD_RANGES_INFO - { - UINT RangeCount; - /* [size_is] */ DO_DOWNLOAD_RANGE Ranges[ 1 ]; - } DO_DOWNLOAD_RANGES_INFO; - -typedef -enum _DODownloadState - { - DODownloadState_Created = 0, - DODownloadState_Transferring = ( DODownloadState_Created + 1 ) , - DODownloadState_Transferred = ( DODownloadState_Transferring + 1 ) , - DODownloadState_Finalized = ( DODownloadState_Transferred + 1 ) , - DODownloadState_Aborted = ( DODownloadState_Finalized + 1 ) , - DODownloadState_Paused = ( DODownloadState_Aborted + 1 ) - } DODownloadState; - -typedef struct _DO_DOWNLOAD_STATUS - { - UINT64 BytesTotal; - UINT64 BytesTransferred; - DODownloadState State; - HRESULT Error; - HRESULT ExtendedError; - } DO_DOWNLOAD_STATUS; - -typedef -enum _DODownloadCostPolicy - { - DODownloadCostPolicy_Always = 0, - DODownloadCostPolicy_Unrestricted = ( DODownloadCostPolicy_Always + 1 ) , - DODownloadCostPolicy_Standard = ( DODownloadCostPolicy_Unrestricted + 1 ) , - DODownloadCostPolicy_NoRoaming = ( DODownloadCostPolicy_Standard + 1 ) , - DODownloadCostPolicy_NoSurcharge = ( DODownloadCostPolicy_NoRoaming + 1 ) , - DODownloadCostPolicy_NoCellular = ( DODownloadCostPolicy_NoSurcharge + 1 ) - } DODownloadCostPolicy; - -typedef -enum _DODownloadProperty - { - DODownloadProperty_Id = 0, - DODownloadProperty_Uri = ( DODownloadProperty_Id + 1 ) , - DODownloadProperty_ContentId = ( DODownloadProperty_Uri + 1 ) , - DODownloadProperty_DisplayName = ( DODownloadProperty_ContentId + 1 ) , - DODownloadProperty_LocalPath = ( DODownloadProperty_DisplayName + 1 ) , - DODownloadProperty_HttpCustomHeaders = ( DODownloadProperty_LocalPath + 1 ) , - DODownloadProperty_CostPolicy = ( DODownloadProperty_HttpCustomHeaders + 1 ) , - DODownloadProperty_SecurityFlags = ( DODownloadProperty_CostPolicy + 1 ) , - DODownloadProperty_CallbackFreqPercent = ( DODownloadProperty_SecurityFlags + 1 ) , - DODownloadProperty_CallbackFreqSeconds = ( DODownloadProperty_CallbackFreqPercent + 1 ) , - DODownloadProperty_NoProgressTimeoutSeconds = ( DODownloadProperty_CallbackFreqSeconds + 1 ) , - DODownloadProperty_ForegroundPriority = ( DODownloadProperty_NoProgressTimeoutSeconds + 1 ) , - DODownloadProperty_BlockingMode = ( DODownloadProperty_ForegroundPriority + 1 ) , - DODownloadProperty_CallbackInterface = ( DODownloadProperty_BlockingMode + 1 ) , - DODownloadProperty_StreamInterface = ( DODownloadProperty_CallbackInterface + 1 ) , - DODownloadProperty_SecurityContext = ( DODownloadProperty_StreamInterface + 1 ) , - DODownloadProperty_NetworkToken = ( DODownloadProperty_SecurityContext + 1 ) , - DODownloadProperty_CorrelationVector = ( DODownloadProperty_NetworkToken + 1 ) , - DODownloadProperty_DecryptionInfo = ( DODownloadProperty_CorrelationVector + 1 ) , - DODownloadProperty_IntegrityCheckInfo = ( DODownloadProperty_DecryptionInfo + 1 ) , - DODownloadProperty_IntegrityCheckMandatory = ( DODownloadProperty_IntegrityCheckInfo + 1 ) , - DODownloadProperty_TotalSizeBytes = ( DODownloadProperty_IntegrityCheckMandatory + 1 ) , - DODownloadProperty_DisallowOnCellular = ( DODownloadProperty_TotalSizeBytes + 1 ) , - DODownloadProperty_HttpCustomAuthHeaders = ( DODownloadProperty_DisallowOnCellular + 1 ) - } DODownloadProperty; - -typedef struct _DO_DOWNLOAD_ENUM_CATEGORY - { - DODownloadProperty Property; - LPCWSTR Value; - } DO_DOWNLOAD_ENUM_CATEGORY; - - - -extern RPC_IF_HANDLE __MIDL_itf_do_0000_0000_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_do_0000_0000_v0_0_s_ifspec; - -#ifndef __IDODownload_INTERFACE_DEFINED__ -#define __IDODownload_INTERFACE_DEFINED__ - -/* interface IDODownload */ -/* [uuid][object] */ - - -EXTERN_C const IID IID_IDODownload; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("FBBD7FC0-C147-4727-A38D-827EF071EE77") - IDODownload : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE Start( - /* [unique][in] */ __RPC__in_opt DO_DOWNLOAD_RANGES_INFO *ranges) = 0; - - virtual HRESULT STDMETHODCALLTYPE Pause( void) = 0; - - virtual HRESULT STDMETHODCALLTYPE Abort( void) = 0; - - virtual HRESULT STDMETHODCALLTYPE Finalize( void) = 0; - - virtual HRESULT STDMETHODCALLTYPE GetStatus( - /* [out] */ __RPC__out DO_DOWNLOAD_STATUS *status) = 0; - - virtual HRESULT STDMETHODCALLTYPE GetProperty( - /* [in] */ DODownloadProperty propId, - /* [out] */ __RPC__out VARIANT *propVal) = 0; - - virtual HRESULT STDMETHODCALLTYPE SetProperty( - /* [in] */ DODownloadProperty propId, - /* [in] */ __RPC__in VARIANT *propVal) = 0; - - }; - - -#else /* C style interface */ - - typedef struct IDODownloadVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in IDODownload * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in IDODownload * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in IDODownload * This); - - DECLSPEC_XFGVIRT(IDODownload, Start) - HRESULT ( STDMETHODCALLTYPE *Start )( - __RPC__in IDODownload * This, - /* [unique][in] */ __RPC__in_opt DO_DOWNLOAD_RANGES_INFO *ranges); - - DECLSPEC_XFGVIRT(IDODownload, Pause) - HRESULT ( STDMETHODCALLTYPE *Pause )( - __RPC__in IDODownload * This); - - DECLSPEC_XFGVIRT(IDODownload, Abort) - HRESULT ( STDMETHODCALLTYPE *Abort )( - __RPC__in IDODownload * This); - - DECLSPEC_XFGVIRT(IDODownload, Finalize) - HRESULT ( STDMETHODCALLTYPE *Finalize )( - __RPC__in IDODownload * This); - - DECLSPEC_XFGVIRT(IDODownload, GetStatus) - HRESULT ( STDMETHODCALLTYPE *GetStatus )( - __RPC__in IDODownload * This, - /* [out] */ __RPC__out DO_DOWNLOAD_STATUS *status); - - DECLSPEC_XFGVIRT(IDODownload, GetProperty) - HRESULT ( STDMETHODCALLTYPE *GetProperty )( - __RPC__in IDODownload * This, - /* [in] */ DODownloadProperty propId, - /* [out] */ __RPC__out VARIANT *propVal); - - DECLSPEC_XFGVIRT(IDODownload, SetProperty) - HRESULT ( STDMETHODCALLTYPE *SetProperty )( - __RPC__in IDODownload * This, - /* [in] */ DODownloadProperty propId, - /* [in] */ __RPC__in VARIANT *propVal); - - END_INTERFACE - } IDODownloadVtbl; - - interface IDODownload - { - CONST_VTBL struct IDODownloadVtbl *lpVtbl; - }; - - - -#ifdef COBJMACROS - - -#define IDODownload_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define IDODownload_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define IDODownload_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define IDODownload_Start(This,ranges) \ - ( (This)->lpVtbl -> Start(This,ranges) ) - -#define IDODownload_Pause(This) \ - ( (This)->lpVtbl -> Pause(This) ) - -#define IDODownload_Abort(This) \ - ( (This)->lpVtbl -> Abort(This) ) - -#define IDODownload_Finalize(This) \ - ( (This)->lpVtbl -> Finalize(This) ) - -#define IDODownload_GetStatus(This,status) \ - ( (This)->lpVtbl -> GetStatus(This,status) ) - -#define IDODownload_GetProperty(This,propId,propVal) \ - ( (This)->lpVtbl -> GetProperty(This,propId,propVal) ) - -#define IDODownload_SetProperty(This,propId,propVal) \ - ( (This)->lpVtbl -> SetProperty(This,propId,propVal) ) - -#endif /* COBJMACROS */ - - -#endif /* C style interface */ - - - - -#endif /* __IDODownload_INTERFACE_DEFINED__ */ - - -#ifndef __IDODownloadStatusCallback_INTERFACE_DEFINED__ -#define __IDODownloadStatusCallback_INTERFACE_DEFINED__ - -/* interface IDODownloadStatusCallback */ -/* [uuid][object] */ - - -EXTERN_C const IID IID_IDODownloadStatusCallback; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("D166E8E3-A90E-4392-8E87-05E996D3747D") - IDODownloadStatusCallback : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE OnStatusChange( - /* [in] */ __RPC__in_opt IDODownload *download, - /* [in] */ __RPC__in DO_DOWNLOAD_STATUS *status) = 0; - - }; - - -#else /* C style interface */ - - typedef struct IDODownloadStatusCallbackVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in IDODownloadStatusCallback * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in IDODownloadStatusCallback * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in IDODownloadStatusCallback * This); - - DECLSPEC_XFGVIRT(IDODownloadStatusCallback, OnStatusChange) - HRESULT ( STDMETHODCALLTYPE *OnStatusChange )( - __RPC__in IDODownloadStatusCallback * This, - /* [in] */ __RPC__in_opt IDODownload *download, - /* [in] */ __RPC__in DO_DOWNLOAD_STATUS *status); - - END_INTERFACE - } IDODownloadStatusCallbackVtbl; - - interface IDODownloadStatusCallback - { - CONST_VTBL struct IDODownloadStatusCallbackVtbl *lpVtbl; - }; - - - -#ifdef COBJMACROS - - -#define IDODownloadStatusCallback_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define IDODownloadStatusCallback_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define IDODownloadStatusCallback_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define IDODownloadStatusCallback_OnStatusChange(This,download,status) \ - ( (This)->lpVtbl -> OnStatusChange(This,download,status) ) - -#endif /* COBJMACROS */ - - -#endif /* C style interface */ - - - - -#endif /* __IDODownloadStatusCallback_INTERFACE_DEFINED__ */ - - -#ifndef __IDOManager_INTERFACE_DEFINED__ -#define __IDOManager_INTERFACE_DEFINED__ - -/* interface IDOManager */ -/* [uuid][object] */ - - -EXTERN_C const IID IID_IDOManager; - -#if defined(__cplusplus) && !defined(CINTERFACE) - - MIDL_INTERFACE("400E2D4A-1431-4C1A-A748-39CA472CFDB1") - IDOManager : public IUnknown - { - public: - virtual HRESULT STDMETHODCALLTYPE CreateDownload( - /* [out] */ __RPC__deref_out_opt IDODownload **download) = 0; - - virtual HRESULT STDMETHODCALLTYPE EnumDownloads( - /* [unique][in] */ __RPC__in_opt DO_DOWNLOAD_ENUM_CATEGORY *category, - /* [out] */ __RPC__deref_out_opt IEnumUnknown **ppEnum) = 0; - - }; - - -#else /* C style interface */ - - typedef struct IDOManagerVtbl - { - BEGIN_INTERFACE - - DECLSPEC_XFGVIRT(IUnknown, QueryInterface) - HRESULT ( STDMETHODCALLTYPE *QueryInterface )( - __RPC__in IDOManager * This, - /* [in] */ __RPC__in REFIID riid, - /* [annotation][iid_is][out] */ - _COM_Outptr_ void **ppvObject); - - DECLSPEC_XFGVIRT(IUnknown, AddRef) - ULONG ( STDMETHODCALLTYPE *AddRef )( - __RPC__in IDOManager * This); - - DECLSPEC_XFGVIRT(IUnknown, Release) - ULONG ( STDMETHODCALLTYPE *Release )( - __RPC__in IDOManager * This); - - DECLSPEC_XFGVIRT(IDOManager, CreateDownload) - HRESULT ( STDMETHODCALLTYPE *CreateDownload )( - __RPC__in IDOManager * This, - /* [out] */ __RPC__deref_out_opt IDODownload **download); - - DECLSPEC_XFGVIRT(IDOManager, EnumDownloads) - HRESULT ( STDMETHODCALLTYPE *EnumDownloads )( - __RPC__in IDOManager * This, - /* [unique][in] */ __RPC__in_opt DO_DOWNLOAD_ENUM_CATEGORY *category, - /* [out] */ __RPC__deref_out_opt IEnumUnknown **ppEnum); - - END_INTERFACE - } IDOManagerVtbl; - - interface IDOManager - { - CONST_VTBL struct IDOManagerVtbl *lpVtbl; - }; - - - -#ifdef COBJMACROS - - -#define IDOManager_QueryInterface(This,riid,ppvObject) \ - ( (This)->lpVtbl -> QueryInterface(This,riid,ppvObject) ) - -#define IDOManager_AddRef(This) \ - ( (This)->lpVtbl -> AddRef(This) ) - -#define IDOManager_Release(This) \ - ( (This)->lpVtbl -> Release(This) ) - - -#define IDOManager_CreateDownload(This,download) \ - ( (This)->lpVtbl -> CreateDownload(This,download) ) - -#define IDOManager_EnumDownloads(This,category,ppEnum) \ - ( (This)->lpVtbl -> EnumDownloads(This,category,ppEnum) ) - -#endif /* COBJMACROS */ - - -#endif /* C style interface */ - - - - -#endif /* __IDOManager_INTERFACE_DEFINED__ */ - - - -#ifndef __DeliveryOptimization_LIBRARY_DEFINED__ -#define __DeliveryOptimization_LIBRARY_DEFINED__ - -/* library DeliveryOptimization */ -/* [uuid] */ - - -EXTERN_C const IID LIBID_DeliveryOptimization; - -EXTERN_C const CLSID CLSID_DeliveryOptimization; - -#ifdef __cplusplus - -class DECLSPEC_UUID("5b99fa76-721c-423c-adac-56d03c8a8007") -DeliveryOptimization; -#endif -#endif /* __DeliveryOptimization_LIBRARY_DEFINED__ */ - -/* interface __MIDL_itf_do_0000_0004 */ -/* [local] */ - -#define DO_LENGTH_TO_EOF (UINT64)(-1) - -#define DecryptionInfo_KeyData L"KeyData" -#define DecryptionInfo_EncryptionBufferSize L"EncryptionBufferSize" -#define DecryptionInfo_AlgorithmName L"AlgorithmName" -#define DecryptionInfo_ChainingMode L"ChainingMode" - -#define IntegrityCheckInfo_PiecesHashFileUrl L"PiecesHashFileUrl" -#define IntegrityCheckInfo_PiecesHashFileDigest L"PiecesHashFileDigest" -#define IntegrityCheckInfo_PiecesHashFileDigestAlgorithm L"PiecesHashFileDigestAlgorithm" -#define IntegrityCheckInfo_HashOfHashes L"HashOfHashes" - - -extern RPC_IF_HANDLE __MIDL_itf_do_0000_0004_v0_0_c_ifspec; -extern RPC_IF_HANDLE __MIDL_itf_do_0000_0004_v0_0_s_ifspec; - -/* Additional Prototypes for ALL interfaces */ - -unsigned long __RPC_USER VARIANT_UserSize( __RPC__in unsigned long *, unsigned long , __RPC__in VARIANT * ); -unsigned char * __RPC_USER VARIANT_UserMarshal( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in VARIANT * ); -unsigned char * __RPC_USER VARIANT_UserUnmarshal(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out VARIANT * ); -void __RPC_USER VARIANT_UserFree( __RPC__in unsigned long *, __RPC__in VARIANT * ); - -unsigned long __RPC_USER VARIANT_UserSize64( __RPC__in unsigned long *, unsigned long , __RPC__in VARIANT * ); -unsigned char * __RPC_USER VARIANT_UserMarshal64( __RPC__in unsigned long *, __RPC__inout_xcount(0) unsigned char *, __RPC__in VARIANT * ); -unsigned char * __RPC_USER VARIANT_UserUnmarshal64(__RPC__in unsigned long *, __RPC__in_xcount(0) unsigned char *, __RPC__out VARIANT * ); -void __RPC_USER VARIANT_UserFree64( __RPC__in unsigned long *, __RPC__in VARIANT * ); - -/* end of Additional Prototypes */ - -#ifdef __cplusplus -} -#endif - -#endif - - diff --git a/src/AppInstallerCommonCore/pch.h b/src/AppInstallerCommonCore/pch.h @@ -16,6 +16,8 @@ #include <SoftPub.h> #include <WinTrust.h> #include <wincrypt.h> +#include <deliveryoptimization.h> +#include <deliveryoptimizationerrors.h> #include "TraceLogging.h" diff --git a/src/AppInstallerSharedLib/Errors.cpp b/src/AppInstallerSharedLib/Errors.cpp @@ -221,6 +221,7 @@ namespace AppInstaller WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED, "Failed to retrieve Microsoft Store package license."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED, "The Microsoft Store package does not support download."), WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN, "Failed to retrieve Microsoft Store package license. The Microsoft Entra Id account does not have the required privilege."), + WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE, "Downloaded zero byte installer; ensure that your network connection is working properly."), // Install errors. WINGET_HRESULT_INFO(APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE, "Application is currently running. Exit the application then try again."), diff --git a/src/AppInstallerSharedLib/Public/AppInstallerErrors.h b/src/AppInstallerSharedLib/Public/AppInstallerErrors.h @@ -151,6 +151,7 @@ #define APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED ((HRESULT)0x8A150083) #define APPINSTALLER_CLI_ERROR_SFSCLIENT_PACKAGE_NOT_SUPPORTED ((HRESULT)0x8A150084) #define APPINSTALLER_CLI_ERROR_LICENSING_API_FAILED_FORBIDDEN ((HRESULT)0x8A150085) +#define APPINSTALLER_CLI_ERROR_INSTALLER_ZERO_BYTE_FILE ((HRESULT)0x8A150086) // Install errors. #define APPINSTALLER_CLI_ERROR_INSTALL_PACKAGE_IN_USE ((HRESULT)0x8A150101) diff --git a/src/AppInstallerSharedLib/Public/AppInstallerSHA256.h b/src/AppInstallerSharedLib/Public/AppInstallerSHA256.h @@ -23,6 +23,12 @@ namespace AppInstaller::Utility { constexpr static size_t HashBufferSizeInBytes = 32; constexpr static size_t HashStringSizeInChars = 64; + struct HashDetails + { + HashBuffer Hash; + uint64_t SizeInBytes = 0; + }; + SHA256(); // Adds the next chunk of data to the hash. @@ -56,6 +62,9 @@ namespace AppInstaller::Utility { // Computes the hash from a given stream. static HashBuffer ComputeHash(std::istream& in); + // Computes the hash from a given stream. + static HashDetails ComputeHashDetails(std::istream& in); + // Computes the hash from a given file path. static HashBuffer ComputeHashFromFile(const std::filesystem::path& path); diff --git a/src/AppInstallerSharedLib/SHA256.cpp b/src/AppInstallerSharedLib/SHA256.cpp @@ -122,6 +122,11 @@ namespace AppInstaller::Utility { SHA256::HashBuffer SHA256::ComputeHash(std::istream& in) { + return ComputeHashDetails(in).Hash; + } + + SHA256::HashDetails SHA256::ComputeHashDetails(std::istream& in) + { // Throw exceptions on badbit auto excState = in.exceptions(); auto revertExcState = wil::scope_exit([excState, &in]() { in.exceptions(excState); }); @@ -131,19 +136,25 @@ namespace AppInstaller::Utility { auto buffer = std::make_unique<uint8_t[]>(bufferSize); SHA256 hasher; + uint64_t totalSize = 0; while (in.good()) { in.read((char*)(buffer.get()), bufferSize); - if (in.gcount()) + std::streamsize bytesRead = in.gcount(); + if (bytesRead) { - hasher.Add(buffer.get(), static_cast<size_t>(in.gcount())); + hasher.Add(buffer.get(), static_cast<size_t>(bytesRead)); + totalSize += static_cast<uint64_t>(bytesRead); } } if (in.eof()) { - return hasher.Get(); + HashDetails result; + result.Hash = hasher.Get(); + result.SizeInBytes = totalSize; + return result; } else { @@ -151,7 +162,6 @@ namespace AppInstaller::Utility { } } - SHA256::HashBuffer SHA256::ComputeHashFromFile(const std::filesystem::path& path) { std::ifstream inStream{ path, std::ifstream::binary }; diff --git a/src/LocalhostWebServer/Startup.cs b/src/LocalhostWebServer/Startup.cs @@ -63,13 +63,14 @@ namespace LocalhostWebServer provider.Mappings[".appxbundle"] = "application/vns.ms-appx"; provider.Mappings[".mszyml"] = "application/x-ms-zip-yaml"; - //Enable static file serving app.UseStaticFiles(new StaticFileOptions { FileProvider = new PhysicalFileProvider(StaticFileRoot), RequestPath = StaticFileRequestPath, ContentTypeProvider = provider, + ServeUnknownFileTypes = true, + DefaultContentType = "application/octet-stream" }); app.UseDirectoryBrowser(new DirectoryBrowserOptions diff --git a/src/WinGetUtil/Exports.cpp b/src/WinGetUtil/Exports.cpp @@ -529,12 +529,12 @@ extern "C" THROW_HR_IF(E_INVALIDARG, computeHash && sha256HashLength != 32); AppInstaller::ProgressCallback callback; - auto hashValue = Download(ConvertToUTF8(url), filePath, DownloadType::WinGetUtil, callback, computeHash); + auto downloadResult = Download(ConvertToUTF8(url), filePath, DownloadType::WinGetUtil, callback); // At this point, if computeHash is set we have verified that the buffer is valid and 32 bytes. if (computeHash) { - const auto& hash = hashValue.value(); + const auto& hash = downloadResult.Sha256Hash; // The SHA 256 hash length should always be 32 bytes. THROW_HR_IF(E_UNEXPECTED, hash.size() != sha256HashLength);