winget-cli

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

HttpClientHelper.cpp (10631B)


      1 // Copyright (c) Microsoft Corporation.
      2 // Licensed under the MIT License.
      3 #include "pch.h"
      4 #include <AppInstallerDownloader.h>
      5 #include <AppInstallerRuntime.h>
      6 #include <winget/HttpClientHelper.h>
      7 #include <winget/NetworkSettings.h>
      8 #include <winhttp.h>
      9 
     10 namespace AppInstaller::Http
     11 {
     12     namespace
     13     {
     14         // If the caller does not pass in a user agent header, put the default one on the request.
     15         void EnsureDefaultUserAgent(web::http::http_request& request)
     16         {
     17             static utility::string_t c_defaultUserAgent = Utility::ConvertToUTF16(AppInstaller::Runtime::GetDefaultUserAgent());
     18 
     19             if (!request.headers().has(web::http::header_names::user_agent))
     20             {
     21                 request.headers().add(web::http::header_names::user_agent, c_defaultUserAgent);
     22             }
     23         }
     24 
     25         void NativeHandleServerCertificateValidation(web::http::client::native_handle handle, const Certificates::PinningConfiguration& pinningConfiguration)
     26         {
     27             HINTERNET requestHandle = reinterpret_cast<HINTERNET>(handle);
     28 
     29             // Get certificate and pass along to pinning config
     30             wil::unique_cert_context certContext;
     31             DWORD bufferSize = sizeof(&certContext);
     32             THROW_IF_WIN32_BOOL_FALSE(WinHttpQueryOption(requestHandle, WINHTTP_OPTION_SERVER_CERT_CONTEXT, &certContext, &bufferSize));
     33 
     34             THROW_HR_IF(APPINSTALLER_CLI_ERROR_PINNED_CERTIFICATE_MISMATCH, !pinningConfiguration.Validate(certContext.get()));
     35         }
     36 
     37         std::chrono::seconds GetRetryAfter(const web::http::http_headers& headers)
     38         {
     39             auto retryAfterHeader = headers.find(web::http::header_names::retry_after);
     40             if (retryAfterHeader != headers.end())
     41             {
     42                 return AppInstaller::Utility::GetRetryAfter(retryAfterHeader->second.c_str());
     43             }
     44 
     45             return 0s;
     46         }
     47     }
     48 
     49     HttpClientHelper::HttpClientHelper(std::shared_ptr<web::http::http_pipeline_stage> stage)
     50         : m_defaultRequestHandlerStage(std::move(stage))
     51     {
     52         const auto& proxyUri = Settings::Network().GetProxyUri();
     53         if (proxyUri)
     54         {
     55             AICLI_LOG(Repo, Info, << "Setting proxy for REST HTTP Client helper to " << proxyUri.value());
     56             m_clientConfig.set_proxy(web::web_proxy{ Utility::ConvertToUTF16(proxyUri.value()) });
     57         }
     58         else
     59         {
     60             AICLI_LOG(Repo, Info, << "REST HTTP Client helper does not use proxy");
     61         }
     62     }
     63 
     64     pplx::task<web::http::http_response> HttpClientHelper::Post(
     65         const utility::string_t& uri,
     66         const web::json::value& body,
     67         const HttpClientHelper::HttpRequestHeaders& headers,
     68         const HttpClientHelper::HttpRequestHeaders& authHeaders) const
     69     {
     70         AICLI_LOG(Repo, Info, << "Sending http POST request to: " << utility::conversions::to_utf8string(uri));
     71         web::http::client::http_client client = GetClient(uri);
     72         web::http::http_request request{ web::http::methods::POST };
     73         request.headers().set_content_type(web::http::details::mime_types::application_json);
     74         request.set_body(body.serialize());
     75 
     76         // Add headers
     77         for (auto& pair : headers)
     78         {
     79             request.headers().add(pair.first, pair.second);
     80         }
     81         EnsureDefaultUserAgent(request);
     82 
     83         AICLI_LOG(Repo, Verbose, << "Http POST request details:\n" << utility::conversions::to_utf8string(request.to_string()));
     84 
     85         // Add auth headers after logging
     86         for (auto& pair : authHeaders)
     87         {
     88             request.headers().add(pair.first, pair.second);
     89         }
     90 
     91         return client.request(request);
     92     }
     93 
     94     std::optional<web::json::value> HttpClientHelper::HandlePost(
     95         const utility::string_t& uri,
     96         const web::json::value& body,
     97         const HttpClientHelper::HttpRequestHeaders& headers,
     98         const HttpClientHelper::HttpRequestHeaders& authHeaders,
     99         const HttpResponseHandler& customHandler) const try
    100     {
    101         web::http::http_response httpResponse;
    102         Post(uri, body, headers, authHeaders).then([&httpResponse](const web::http::http_response& response)
    103             {
    104                 httpResponse = response;
    105             }).wait();
    106 
    107         if (customHandler)
    108         {
    109             auto handlerResult = customHandler(httpResponse);
    110             if (!handlerResult.UseDefaultHandling)
    111             {
    112                 return std::move(handlerResult.Result);
    113             }
    114         }
    115 
    116         return ValidateAndExtractResponse(httpResponse);
    117     }
    118     catch (web::http::http_exception& exception)
    119     {
    120         RethrowAsWilException(exception);
    121     }
    122 
    123     pplx::task<web::http::http_response> HttpClientHelper::Get(
    124         const utility::string_t& uri,
    125         const HttpClientHelper::HttpRequestHeaders& headers,
    126         const HttpClientHelper::HttpRequestHeaders& authHeaders) const
    127     {
    128         AICLI_LOG(Repo, Info, << "Sending http GET request to: " << utility::conversions::to_utf8string(uri));
    129         web::http::client::http_client client = GetClient(uri);
    130         web::http::http_request request{ web::http::methods::GET };
    131         request.headers().set_content_type(web::http::details::mime_types::application_json);
    132 
    133         // Add headers
    134         for (auto& pair : headers)
    135         {
    136             request.headers().add(pair.first, pair.second);
    137         }
    138         EnsureDefaultUserAgent(request);
    139 
    140         AICLI_LOG(Repo, Verbose, << "Http GET request details:\n" << utility::conversions::to_utf8string(request.to_string()));
    141 
    142         // Add auth headers after logging
    143         for (auto& pair : authHeaders)
    144         {
    145             request.headers().add(pair.first, pair.second);
    146         }
    147 
    148         return client.request(request);
    149     }
    150 
    151     std::optional<web::json::value> HttpClientHelper::HandleGet(
    152         const utility::string_t& uri,
    153         const HttpClientHelper::HttpRequestHeaders& headers,
    154         const HttpClientHelper::HttpRequestHeaders& authHeaders,
    155         const HttpResponseHandler& customHandler) const try
    156     {
    157         web::http::http_response httpResponse;
    158         Get(uri, headers, authHeaders).then([&httpResponse](const web::http::http_response& response)
    159             {
    160                 httpResponse = response;
    161             }).wait();
    162 
    163         if (customHandler)
    164         {
    165             auto handlerResult = customHandler(httpResponse);
    166             if (!handlerResult.UseDefaultHandling)
    167             {
    168                 return std::move(handlerResult.Result);
    169             }
    170         }
    171 
    172         return ValidateAndExtractResponse(httpResponse);
    173     }
    174     catch (web::http::http_exception& exception)
    175     {
    176         RethrowAsWilException(exception);
    177     }
    178 
    179     void HttpClientHelper::SetPinningConfiguration(const Certificates::PinningConfiguration& configuration)
    180     {
    181         m_clientConfig.set_nativehandle_servercertificate_validation([pinConfig = configuration](web::http::client::native_handle handle)
    182             {
    183                 NativeHandleServerCertificateValidation(handle, pinConfig);
    184             });
    185     }
    186 
    187     web::http::client::http_client HttpClientHelper::GetClient(const utility::string_t& uri) const
    188     {
    189         web::http::client::http_client client{ uri, m_clientConfig };
    190 
    191         // Add default custom handlers if any.
    192         if (m_defaultRequestHandlerStage)
    193         {
    194             client.add_handler(m_defaultRequestHandlerStage);
    195         }
    196 
    197         return client;
    198     }
    199 
    200     std::optional<web::json::value> HttpClientHelper::ValidateAndExtractResponse(const web::http::http_response& response) const
    201     {
    202         AICLI_LOG(Repo, Info, << "Response status: " << response.status_code());
    203         // Ensure that we wait for the content to be ready before we log it; otherwise it will be truncated.
    204         AICLI_LOG_LARGE_STRING(Repo, Verbose, << "Response details:",
    205             response.content_ready().then([&](const web::http::http_response&) { return utility::conversions::to_utf8string(response.to_string()); }).get());
    206 
    207         std::optional<web::json::value> result;
    208         switch (response.status_code())
    209         {
    210         case web::http::status_codes::OK:
    211             result = ExtractJsonResponse(response);
    212             break;
    213 
    214         case web::http::status_codes::NotFound:
    215             THROW_HR(APPINSTALLER_CLI_ERROR_RESTAPI_ENDPOINT_NOT_FOUND);
    216 
    217         case web::http::status_codes::NoContent:
    218             result = {};
    219             break;
    220 
    221         case web::http::status_codes::BadRequest:
    222             THROW_HR(APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR);
    223 
    224         case web::http::status_codes::TooManyRequests:
    225         case web::http::status_codes::ServiceUnavailable:
    226             THROW_EXCEPTION(AppInstaller::Utility::ServiceUnavailableException(GetRetryAfter(response.headers())));
    227 
    228         default:
    229             THROW_HR(MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, response.status_code()));
    230         }
    231 
    232         return result;
    233     }
    234 
    235     std::optional<web::json::value> HttpClientHelper::ExtractJsonResponse(const web::http::http_response& response) const
    236     {
    237         utility::string_t contentType = response.headers().content_type();
    238 
    239         THROW_HR_IF(APPINSTALLER_CLI_ERROR_RESTAPI_UNSUPPORTED_MIME_TYPE,
    240             !contentType._Starts_with(web::http::details::mime_types::application_json));
    241 
    242         return response.extract_json().get();
    243     }
    244 
    245     [[noreturn]] void HttpClientHelper::RethrowAsWilException(const web::http::http_exception& exception)
    246     {
    247         // Some http_exceptions have no error code; default to REST internal error.
    248         HRESULT toThrow = APPINSTALLER_CLI_ERROR_RESTAPI_INTERNAL_ERROR;
    249 
    250         // 99% of the time this code comes from GetLastError.
    251         // In a few cases it will be 400; as in the HTTP status code.
    252         // Since that is the one case that http_client_winhttp.cpp uses, we map it specifically.
    253         // In the event that this makes no sense, ERROR_THREAD_MODE_ALREADY_BACKGROUND is Win32 error 400.
    254         int errorValue = exception.error_code().value();
    255         if (errorValue == web::http::status_codes::BadRequest)
    256         {
    257             toThrow = MAKE_HRESULT(SEVERITY_ERROR, FACILITY_HTTP, web::http::status_codes::BadRequest);
    258         }
    259         else if (errorValue)
    260         {
    261             toThrow = HRESULT_FROM_WIN32(errorValue);
    262         }
    263 
    264         THROW_HR_MSG(toThrow, "%hs", exception.what());
    265     }
    266 }