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 }