http_downloader_winhttp.cpp (11099B)
1 // SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin <stenzek@gmail.com> 2 // SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) 3 4 #include "http_downloader_winhttp.h" 5 6 #include "common/assert.h" 7 #include "common/log.h" 8 #include "common/string_util.h" 9 #include "common/timer.h" 10 11 #include <algorithm> 12 13 Log_SetChannel(HTTPDownloader); 14 15 HTTPDownloaderWinHttp::HTTPDownloaderWinHttp() : HTTPDownloader() 16 { 17 } 18 19 HTTPDownloaderWinHttp::~HTTPDownloaderWinHttp() 20 { 21 if (m_hSession) 22 { 23 WinHttpSetStatusCallback(m_hSession, nullptr, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, NULL); 24 WinHttpCloseHandle(m_hSession); 25 } 26 } 27 28 std::unique_ptr<HTTPDownloader> HTTPDownloader::Create(std::string user_agent) 29 { 30 std::unique_ptr<HTTPDownloaderWinHttp> instance(std::make_unique<HTTPDownloaderWinHttp>()); 31 if (!instance->Initialize(std::move(user_agent))) 32 return {}; 33 34 return instance; 35 } 36 37 bool HTTPDownloaderWinHttp::Initialize(std::string user_agent) 38 { 39 static constexpr DWORD dwAccessType = WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY; 40 41 m_hSession = WinHttpOpen(StringUtil::UTF8StringToWideString(user_agent).c_str(), dwAccessType, nullptr, nullptr, 42 WINHTTP_FLAG_ASYNC); 43 if (m_hSession == NULL) 44 { 45 ERROR_LOG("WinHttpOpen() failed: {}", GetLastError()); 46 return false; 47 } 48 49 const DWORD notification_flags = WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | WINHTTP_CALLBACK_FLAG_REQUEST_ERROR | 50 WINHTTP_CALLBACK_FLAG_HANDLES | WINHTTP_CALLBACK_FLAG_SECURE_FAILURE; 51 if (WinHttpSetStatusCallback(m_hSession, HTTPStatusCallback, notification_flags, NULL) == 52 WINHTTP_INVALID_STATUS_CALLBACK) 53 { 54 ERROR_LOG("WinHttpSetStatusCallback() failed: {}", GetLastError()); 55 return false; 56 } 57 58 return true; 59 } 60 61 void CALLBACK HTTPDownloaderWinHttp::HTTPStatusCallback(HINTERNET hRequest, DWORD_PTR dwContext, DWORD dwInternetStatus, 62 LPVOID lpvStatusInformation, DWORD dwStatusInformationLength) 63 { 64 Request* req = reinterpret_cast<Request*>(dwContext); 65 switch (dwInternetStatus) 66 { 67 case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED: 68 return; 69 70 case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING: 71 { 72 if (!req) 73 return; 74 75 DebugAssert(hRequest == req->hRequest); 76 77 HTTPDownloaderWinHttp* parent = static_cast<HTTPDownloaderWinHttp*>(req->parent); 78 std::unique_lock<std::mutex> lock(parent->m_pending_http_request_lock); 79 Assert(std::none_of(parent->m_pending_http_requests.begin(), parent->m_pending_http_requests.end(), 80 [req](HTTPDownloader::Request* it) { return it == req; })); 81 82 // we can clean up the connection as well 83 DebugAssert(req->hConnection != NULL); 84 WinHttpCloseHandle(req->hConnection); 85 delete req; 86 return; 87 } 88 89 case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR: 90 { 91 const WINHTTP_ASYNC_RESULT* res = reinterpret_cast<const WINHTTP_ASYNC_RESULT*>(lpvStatusInformation); 92 ERROR_LOG("WinHttp async function {} returned error {}", res->dwResult, res->dwError); 93 req->status_code = HTTP_STATUS_ERROR; 94 req->state.store(Request::State::Complete); 95 return; 96 } 97 case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE: 98 { 99 DEV_LOG("SendRequest complete"); 100 if (!WinHttpReceiveResponse(hRequest, nullptr)) 101 { 102 ERROR_LOG("WinHttpReceiveResponse() failed: {}", GetLastError()); 103 req->status_code = HTTP_STATUS_ERROR; 104 req->state.store(Request::State::Complete); 105 } 106 107 return; 108 } 109 case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE: 110 { 111 DEV_LOG("Headers available"); 112 113 DWORD buffer_size = sizeof(req->status_code); 114 if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, 115 WINHTTP_HEADER_NAME_BY_INDEX, &req->status_code, &buffer_size, WINHTTP_NO_HEADER_INDEX)) 116 { 117 ERROR_LOG("WinHttpQueryHeaders() for status code failed: {}", GetLastError()); 118 req->status_code = HTTP_STATUS_ERROR; 119 req->state.store(Request::State::Complete); 120 return; 121 } 122 123 buffer_size = sizeof(req->content_length); 124 if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, 125 WINHTTP_HEADER_NAME_BY_INDEX, &req->content_length, &buffer_size, 126 WINHTTP_NO_HEADER_INDEX)) 127 { 128 if (GetLastError() != ERROR_WINHTTP_HEADER_NOT_FOUND) 129 WARNING_LOG("WinHttpQueryHeaders() for content length failed: {}", GetLastError()); 130 131 req->content_length = 0; 132 } 133 134 DWORD content_type_length = 0; 135 if (!WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX, 136 WINHTTP_NO_OUTPUT_BUFFER, &content_type_length, WINHTTP_NO_HEADER_INDEX) && 137 GetLastError() == ERROR_INSUFFICIENT_BUFFER && content_type_length >= sizeof(content_type_length)) 138 { 139 std::wstring content_type_wstring; 140 content_type_wstring.resize((content_type_length / sizeof(wchar_t)) - 1); 141 if (WinHttpQueryHeaders(hRequest, WINHTTP_QUERY_CONTENT_TYPE, WINHTTP_HEADER_NAME_BY_INDEX, 142 content_type_wstring.data(), &content_type_length, WINHTTP_NO_HEADER_INDEX)) 143 { 144 req->content_type = StringUtil::WideStringToUTF8String(content_type_wstring); 145 } 146 } 147 148 DEV_LOG("Status code {}, content-length is {}", req->status_code, req->content_length); 149 req->data.reserve(req->content_length); 150 req->state = Request::State::Receiving; 151 152 // start reading 153 if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING) 154 { 155 ERROR_LOG("WinHttpQueryDataAvailable() failed: {}", GetLastError()); 156 req->status_code = HTTP_STATUS_ERROR; 157 req->state.store(Request::State::Complete); 158 } 159 160 return; 161 } 162 case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE: 163 { 164 DWORD bytes_available; 165 std::memcpy(&bytes_available, lpvStatusInformation, sizeof(bytes_available)); 166 if (bytes_available == 0) 167 { 168 // end of request 169 DEV_LOG("End of request '{}', {} bytes received", req->url, req->data.size()); 170 req->state.store(Request::State::Complete); 171 return; 172 } 173 174 // start the transfer 175 DEV_LOG("{} bytes available", bytes_available); 176 req->io_position = static_cast<u32>(req->data.size()); 177 req->data.resize(req->io_position + bytes_available); 178 if (!WinHttpReadData(hRequest, req->data.data() + req->io_position, bytes_available, nullptr) && 179 GetLastError() != ERROR_IO_PENDING) 180 { 181 ERROR_LOG("WinHttpReadData() failed: {}", GetLastError()); 182 req->status_code = HTTP_STATUS_ERROR; 183 req->state.store(Request::State::Complete); 184 } 185 186 return; 187 } 188 case WINHTTP_CALLBACK_STATUS_READ_COMPLETE: 189 { 190 DEV_LOG("Read of {} complete", dwStatusInformationLength); 191 192 const u32 new_size = req->io_position + dwStatusInformationLength; 193 Assert(new_size <= req->data.size()); 194 req->data.resize(new_size); 195 req->start_time = Common::Timer::GetCurrentValue(); 196 197 if (!WinHttpQueryDataAvailable(hRequest, nullptr) && GetLastError() != ERROR_IO_PENDING) 198 { 199 ERROR_LOG("WinHttpQueryDataAvailable() failed: {}", GetLastError()); 200 req->status_code = HTTP_STATUS_ERROR; 201 req->state.store(Request::State::Complete); 202 } 203 204 return; 205 } 206 default: 207 // unhandled, ignore 208 return; 209 } 210 } 211 212 HTTPDownloader::Request* HTTPDownloaderWinHttp::InternalCreateRequest() 213 { 214 Request* req = new Request(); 215 return req; 216 } 217 218 void HTTPDownloaderWinHttp::InternalPollRequests() 219 { 220 // noop - it uses windows's worker threads 221 } 222 223 bool HTTPDownloaderWinHttp::StartRequest(HTTPDownloader::Request* request) 224 { 225 Request* req = static_cast<Request*>(request); 226 227 std::wstring host_name; 228 host_name.resize(req->url.size()); 229 req->object_name.resize(req->url.size()); 230 231 URL_COMPONENTSW uc = {}; 232 uc.dwStructSize = sizeof(uc); 233 uc.lpszHostName = host_name.data(); 234 uc.dwHostNameLength = static_cast<DWORD>(host_name.size()); 235 uc.lpszUrlPath = req->object_name.data(); 236 uc.dwUrlPathLength = static_cast<DWORD>(req->object_name.size()); 237 238 const std::wstring url_wide(StringUtil::UTF8StringToWideString(req->url)); 239 if (!WinHttpCrackUrl(url_wide.c_str(), static_cast<DWORD>(url_wide.size()), 0, &uc)) 240 { 241 ERROR_LOG("WinHttpCrackUrl() failed: {}", GetLastError()); 242 req->callback(HTTP_STATUS_ERROR, std::string(), req->data); 243 delete req; 244 return false; 245 } 246 247 host_name.resize(uc.dwHostNameLength); 248 req->object_name.resize(uc.dwUrlPathLength); 249 250 req->hConnection = WinHttpConnect(m_hSession, host_name.c_str(), uc.nPort, 0); 251 if (!req->hConnection) 252 { 253 ERROR_LOG("Failed to start HTTP request for '{}': {}", req->url, GetLastError()); 254 req->callback(HTTP_STATUS_ERROR, std::string(), req->data); 255 delete req; 256 return false; 257 } 258 259 const DWORD request_flags = uc.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0; 260 req->hRequest = 261 WinHttpOpenRequest(req->hConnection, (req->type == HTTPDownloader::Request::Type::Post) ? L"POST" : L"GET", 262 req->object_name.c_str(), NULL, NULL, NULL, request_flags); 263 if (!req->hRequest) 264 { 265 ERROR_LOG("WinHttpOpenRequest() failed: {}", GetLastError()); 266 WinHttpCloseHandle(req->hConnection); 267 return false; 268 } 269 270 BOOL result; 271 if (req->type == HTTPDownloader::Request::Type::Post) 272 { 273 const std::wstring_view additional_headers(L"Content-Type: application/x-www-form-urlencoded\r\n"); 274 result = WinHttpSendRequest(req->hRequest, additional_headers.data(), static_cast<DWORD>(additional_headers.size()), 275 req->post_data.data(), static_cast<DWORD>(req->post_data.size()), 276 static_cast<DWORD>(req->post_data.size()), reinterpret_cast<DWORD_PTR>(req)); 277 } 278 else 279 { 280 result = WinHttpSendRequest(req->hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, 281 reinterpret_cast<DWORD_PTR>(req)); 282 } 283 284 if (!result && GetLastError() != ERROR_IO_PENDING) 285 { 286 ERROR_LOG("WinHttpSendRequest() failed: {}", GetLastError()); 287 req->status_code = HTTP_STATUS_ERROR; 288 req->state.store(Request::State::Complete); 289 } 290 291 DEV_LOG("Started HTTP request for '{}'", req->url); 292 req->state = Request::State::Started; 293 req->start_time = Common::Timer::GetCurrentValue(); 294 return true; 295 } 296 297 void HTTPDownloaderWinHttp::CloseRequest(HTTPDownloader::Request* request) 298 { 299 Request* req = static_cast<Request*>(request); 300 301 if (req->hRequest != NULL) 302 { 303 // req will be freed by the callback. 304 // the callback can fire immediately here if there's nothing running async, so don't touch req afterwards 305 WinHttpCloseHandle(req->hRequest); 306 return; 307 } 308 309 if (req->hConnection != NULL) 310 WinHttpCloseHandle(req->hConnection); 311 312 delete req; 313 }