/***************************************************************************** * Copyright (c) 2014-2025 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #if !defined(DISABLE_HTTP) && defined(_WIN32) #include "Http.h" #include "../Version.h" #include "../core/Console.hpp" #include "String.hpp" #include #include #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include #include namespace OpenRCT2::Http { static constexpr char kOpenRCT2UserAgent[] = "OpenRCT2/" kOpenRCT2Version; static void ThrowWin32Exception(const char* methodName) { auto errorCode = GetLastError(); auto msg = String::stdFormat("%s failed, error: %d.", methodName, errorCode); throw std::runtime_error(msg); } static const wchar_t* GetVerb(Method method) { switch (method) { case Method::GET: return L"GET"; case Method::POST: return L"POST"; case Method::PUT: return L"PUT"; default: throw std::runtime_error("Unsupported verb."); } } static int32_t ReadStatusCode(HINTERNET hRequest) { wchar_t headerBuffer[32]{}; auto headerBufferLen = static_cast(std::size(headerBuffer)); if (!WinHttpQueryHeaders( hRequest, WINHTTP_QUERY_STATUS_CODE, L"StatusCode", headerBuffer, &headerBufferLen, WINHTTP_NO_HEADER_INDEX)) { ThrowWin32Exception("WinHttpQueryHeaders"); } auto statusCode = std::stoi(headerBuffer); return statusCode < 0 || statusCode > 999 ? 0 : statusCode; } static std::map ReadHeaders(HINTERNET hRequest) { DWORD dwSize{}; WinHttpQueryHeaders( hRequest, WINHTTP_QUERY_RAW_HEADERS, WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX); std::wstring buffer; buffer.resize(dwSize); if (!WinHttpQueryHeaders( hRequest, WINHTTP_QUERY_RAW_HEADERS, WINHTTP_HEADER_NAME_BY_INDEX, buffer.data(), &dwSize, WINHTTP_NO_HEADER_INDEX)) ThrowWin32Exception("WinHttpQueryHeaders"); std::map headers; std::wstring wKey, wValue; constexpr int32_t kStateExpectKey = 0; constexpr int32_t kStateExpectWhitespaceValue = 1; constexpr int32_t kStateExpectValue = 2; int32_t state = kStateExpectKey; int32_t index = 0; for (auto c : buffer) { if (c == '\0') { // Ignore first header as that is the HTTP version which // we don't really count as a header. if (index != 0 && wKey.size() != 0) { auto key = String::toUtf8(wKey); auto value = String::toUtf8(wValue); headers[key] = std::move(value); } wKey.clear(); wValue.clear(); index++; state = kStateExpectKey; continue; } if (state == kStateExpectKey && c == ':') { state = kStateExpectWhitespaceValue; } else if (state == 1 && c == ' ') { state = kStateExpectValue; } else if (state == kStateExpectKey) { wKey.push_back(c); } else { state = kStateExpectValue; wValue.push_back(c); } } return headers; } static std::string ReadBody(HINTERNET hRequest) { std::string body; DWORD dwRealSize{}; DWORD dwDownloaded{}; do { // Check for available data. DWORD dwSizeToRead{}; if (!WinHttpQueryDataAvailable(hRequest, &dwSizeToRead)) ThrowWin32Exception("WinHttpQueryDataAvailable"); // Apparently some servers may report no bytes left to // download incorrectly, so still attempt to read... if (dwSizeToRead == 0) dwSizeToRead = 4096; body.resize(dwRealSize + dwSizeToRead); auto dst = reinterpret_cast(&body[dwRealSize]); dwDownloaded = 0; if (!WinHttpReadData(hRequest, dst, dwSizeToRead, &dwDownloaded)) ThrowWin32Exception("WinHttpReadData"); dwRealSize += dwDownloaded; } while (dwDownloaded > 0); body.resize(dwRealSize); body.shrink_to_fit(); return body; } Response Do(const Request& req) { HINTERNET hSession{}, hConnect{}, hRequest{}; try { URL_COMPONENTS url{}; url.dwStructSize = sizeof(url); url.dwSchemeLength = static_cast(-1); url.dwHostNameLength = static_cast(-1); url.dwUrlPathLength = static_cast(-1); url.dwExtraInfoLength = static_cast(-1); auto wUrl = String::toWideChar(req.url); if (!WinHttpCrackUrl(wUrl.c_str(), 0, 0, &url)) throw std::invalid_argument("Unable to parse URI."); auto userAgent = String::toWideChar(kOpenRCT2UserAgent); hSession = WinHttpOpen( userAgent.c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); if (hSession == nullptr) ThrowWin32Exception("WinHttpOpen"); auto wHostName = std::wstring(url.lpszHostName, url.dwHostNameLength); hConnect = WinHttpConnect(hSession, wHostName.c_str(), url.nPort, 0); if (hConnect == nullptr) ThrowWin32Exception("WinHttpConnect"); auto dwFlags = 0; if (lstrcmpiW(std::wstring(url.lpszScheme, url.dwSchemeLength).c_str(), L"https") == 0) { dwFlags = WINHTTP_FLAG_SECURE; } auto wVerb = GetVerb(req.method); auto wQuery = std::wstring(url.lpszUrlPath, url.dwUrlPathLength); hRequest = WinHttpOpenRequest( hConnect, wVerb, wQuery.c_str(), NULL, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, dwFlags); if (hRequest == nullptr) ThrowWin32Exception("WinHttpOpenRequest"); for (auto header : req.header) { auto fullHeader = String::toWideChar(header.first) + L": " + String::toWideChar(header.second); if (!WinHttpAddRequestHeaders(hRequest, fullHeader.c_str(), static_cast(-1L), WINHTTP_ADDREQ_FLAG_ADD)) ThrowWin32Exception("WinHttpAddRequestHeaders"); } auto reqBody = reinterpret_cast(const_cast(req.body.data())); auto reqBodyLen = static_cast(req.body.size()); if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, reqBody, reqBodyLen, reqBodyLen, 0)) ThrowWin32Exception("WinHttpSendRequest"); if (!WinHttpReceiveResponse(hRequest, NULL)) ThrowWin32Exception("WinHttpReceiveResponse"); auto statusCode = ReadStatusCode(hRequest); auto headers = ReadHeaders(hRequest); auto body = ReadBody(hRequest); Response response; response.body = std::move(body); response.status = static_cast(statusCode); auto it = headers.find("Content-Type"); if (it != headers.end()) { response.content_type = it->second; } response.header = std::move(headers); WinHttpCloseHandle(hSession); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hRequest); return response; } catch ([[maybe_unused]] const std::exception& e) { #ifdef DEBUG Console::Error::WriteLine("HTTP request failed: %s", e.what()); #endif WinHttpCloseHandle(hSession); WinHttpCloseHandle(hConnect); WinHttpCloseHandle(hRequest); throw; } } } // namespace OpenRCT2::Http #endif