1
0
mirror of https://github.com/OpenRCT2/OpenRCT2 synced 2026-01-15 11:03:00 +01:00
Files
OpenRCT2/src/openrct2/core/Http.WinHttp.cpp
2025-11-07 21:14:58 +01:00

245 lines
8.5 KiB
C++

/*****************************************************************************
* 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 <cstdio>
#include <stdexcept>
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#include <winhttp.h>
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<DWORD>(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<std::string, std::string> 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<std::string, std::string> 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<LPVOID>(&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<DWORD>(-1);
url.dwHostNameLength = static_cast<DWORD>(-1);
url.dwUrlPathLength = static_cast<DWORD>(-1);
url.dwExtraInfoLength = static_cast<DWORD>(-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<ULONG>(-1L), WINHTTP_ADDREQ_FLAG_ADD))
ThrowWin32Exception("WinHttpAddRequestHeaders");
}
auto reqBody = reinterpret_cast<LPVOID>(const_cast<char*>(req.body.data()));
auto reqBodyLen = static_cast<DWORD>(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<Status>(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