winget-cli

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

DODownloader.cpp (19850B)


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