mirror of
https://github.com/OpenRCT2/OpenRCT2
synced 2026-01-15 11:03:00 +01:00
245 lines
8.5 KiB
C++
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
|