OpenTTD Source 20260109-master-g241b5fcdfe
http_winhttp.cpp
Go to the documentation of this file.
1/*
2 * This file is part of OpenTTD.
3 * OpenTTD is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, version 2.
4 * OpenTTD is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
5 * See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with OpenTTD. If not, see <https://www.gnu.org/licenses/old-licenses/gpl-2.0>.
6 */
7
10#include "../../stdafx.h"
11#include "../../debug.h"
12#include "../../rev.h"
13#include "../network_internal.h"
14
15#include "http.h"
16#include "http_shared.h"
17
18#include <mutex>
19#include <winhttp.h>
20
21#include "../../safeguards.h"
22
23static HINTERNET _winhttp_session = nullptr;
24
27private:
28 const std::wstring uri;
30 const std::string data;
31
32 HINTERNET connection = nullptr;
33 HINTERNET request = nullptr;
34 std::atomic<bool> finished = false;
35 int depth = 0;
36
37public:
38 NetworkHTTPRequest(std::wstring &&uri, HTTPCallback *callback, std::string &&data);
39
41
42 void Connect();
43 bool Receive();
44 void WinHttpCallback(DWORD code, void *info, DWORD length);
45};
46
47static std::vector<NetworkHTTPRequest *> _http_requests;
48static std::vector<NetworkHTTPRequest *> _new_http_requests;
49static std::mutex _new_http_requests_mutex;
50
51static std::vector<HTTPThreadSafeCallback *> _http_callbacks;
52static std::vector<HTTPThreadSafeCallback *> _new_http_callbacks;
53static std::mutex _http_callback_mutex;
54static std::mutex _new_http_callback_mutex;
55
63NetworkHTTPRequest::NetworkHTTPRequest(std::wstring &&uri, HTTPCallback *callback, std::string &&data) :
64 uri(std::move(uri)),
65 callback(callback),
66 data(std::move(data))
67{
68 std::lock_guard<std::mutex> lock(_new_http_callback_mutex);
69 _new_http_callbacks.push_back(&this->callback);
70}
71
72static std::string GetLastErrorAsString()
73{
74 wchar_t buffer[512];
75
76 DWORD error_code = GetLastError();
77
78 if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_FROM_HMODULE | FORMAT_MESSAGE_IGNORE_INSERTS, GetModuleHandle(L"winhttp.dll"), error_code,
79 MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), buffer, static_cast<DWORD>(std::size(buffer)), nullptr) == 0) {
80 return fmt::format("unknown error {}", error_code);
81 }
82
83 return FS2OTTD(buffer);
84}
85
98void NetworkHTTPRequest::WinHttpCallback(DWORD code, void *info, DWORD length)
99{
100 if (this->finished) return;
101
102 switch (code) {
103 case WINHTTP_CALLBACK_STATUS_RESOLVING_NAME:
104 case WINHTTP_CALLBACK_STATUS_NAME_RESOLVED:
105 case WINHTTP_CALLBACK_STATUS_CONNECTING_TO_SERVER:
106 case WINHTTP_CALLBACK_STATUS_CONNECTED_TO_SERVER:
107 case WINHTTP_CALLBACK_STATUS_SENDING_REQUEST:
108 case WINHTTP_CALLBACK_STATUS_REQUEST_SENT:
109 case WINHTTP_CALLBACK_STATUS_RECEIVING_RESPONSE:
110 case WINHTTP_CALLBACK_STATUS_RESPONSE_RECEIVED:
111 case WINHTTP_CALLBACK_STATUS_CLOSING_CONNECTION:
112 case WINHTTP_CALLBACK_STATUS_CONNECTION_CLOSED:
113 case WINHTTP_CALLBACK_STATUS_HANDLE_CREATED:
114 case WINHTTP_CALLBACK_STATUS_HANDLE_CLOSING:
115 /* We don't care about these events, and explicitly ignore them. */
116 break;
117
118 case WINHTTP_CALLBACK_STATUS_REDIRECT:
119 /* Make sure we are not in a redirect loop. */
120 if (this->depth++ > 5) {
121 Debug(net, 0, "HTTP request failed: too many redirects");
122 this->callback.OnFailure();
123 this->finished = true;
124 return;
125 }
126 break;
127
128 case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
129 /* Next step: read response. */
130 WinHttpReceiveResponse(this->request, nullptr);
131 break;
132
133 case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
134 {
135 /* Retrieve the status code. */
136 DWORD status_code = 0;
137 DWORD status_code_size = sizeof(status_code);
138 WinHttpQueryHeaders(this->request, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &status_code, &status_code_size, WINHTTP_NO_HEADER_INDEX);
139 Debug(net, 3, "HTTP request status code: {}", status_code);
140
141 /* If there is any error, we simply abort the request. */
142 if (status_code >= 400) {
143 /* No need to be verbose about rate limiting. */
144 Debug(net, status_code == HTTP_429_TOO_MANY_REQUESTS ? 1 : 0, "HTTP request failed: status-code {}", status_code);
145 this->callback.OnFailure();
146 this->finished = true;
147 return;
148 }
149
150 /* Next step: query for any data. */
151 WinHttpQueryDataAvailable(this->request, nullptr);
152 } break;
153
154 case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
155 {
156 /* Retrieve the amount of data available to process. */
157 DWORD size = *(DWORD *)info;
158
159 /* Next step: read the data in a temporary allocated buffer.
160 * The buffer will be freed by OnReceiveData() in the next step. */
161 char *buffer = size == 0 ? nullptr : new char[size];
162 WinHttpReadData(this->request, buffer, size, 0);
163 } break;
164
165 case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
166 Debug(net, 6, "HTTP callback: {} bytes", length);
167
168 this->callback.OnReceiveData(std::unique_ptr<char[]>(static_cast<char *>(info)), length);
169
170 if (length == 0) {
171 /* Next step: no more data available: request is finished. */
172 this->finished = true;
173 Debug(net, 1, "HTTP request succeeded");
174 } else {
175 /* Next step: query for more data. */
176 WinHttpQueryDataAvailable(this->request, nullptr);
177 }
178
179 break;
180
181 case WINHTTP_CALLBACK_STATUS_SECURE_FAILURE:
182 case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
183 Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString());
184 this->callback.OnFailure();
185 this->finished = true;
186 break;
187
188 default:
189 Debug(net, 0, "HTTP request failed: unexpected callback code 0x{:x}", code);
190 this->callback.OnFailure();
191 this->finished = true;
192 return;
193 }
194}
195
196static void CALLBACK StaticWinHttpCallback(HINTERNET, DWORD_PTR context, DWORD code, void *info, DWORD length)
197{
198 if (context == 0) return;
199
200 NetworkHTTPRequest *request = (NetworkHTTPRequest *)context;
201 request->WinHttpCallback(code, info, length);
202}
203
212{
213 Debug(net, 1, "HTTP request to {}", std::string(uri.begin(), uri.end()));
214
215 URL_COMPONENTS url_components = {};
216 wchar_t scheme[32];
217 wchar_t hostname[128];
218 wchar_t url_path[4096];
219
220 /* Convert the URL to its components. */
221 url_components.dwStructSize = sizeof(url_components);
222 url_components.lpszScheme = scheme;
223 url_components.dwSchemeLength = static_cast<DWORD>(std::size(scheme));
224 url_components.lpszHostName = hostname;
225 url_components.dwHostNameLength = static_cast<DWORD>(std::size(hostname));
226 url_components.lpszUrlPath = url_path;
227 url_components.dwUrlPathLength = static_cast<DWORD>(std::size(url_path));
228 WinHttpCrackUrl(this->uri.c_str(), 0, 0, &url_components);
229
230 /* Create the HTTP connection. */
231 this->connection = WinHttpConnect(_winhttp_session, url_components.lpszHostName, url_components.nPort, 0);
232 if (this->connection == nullptr) {
233 Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString());
234 this->callback.OnFailure();
235 this->finished = true;
236 return;
237 }
238
239 this->request = WinHttpOpenRequest(connection, data.empty() ? L"GET" : L"POST", url_components.lpszUrlPath, nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, url_components.nScheme == INTERNET_SCHEME_HTTPS ? WINHTTP_FLAG_SECURE : 0);
240 if (this->request == nullptr) {
241 WinHttpCloseHandle(this->connection);
242
243 Debug(net, 0, "HTTP request failed: {}", GetLastErrorAsString());
244 this->callback.OnFailure();
245 this->finished = true;
246 return;
247 }
248
249 /* Send the request (possibly with a payload). */
250 if (data.empty()) {
251 WinHttpSendRequest(this->request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, WINHTTP_NO_REQUEST_DATA, 0, 0, reinterpret_cast<DWORD_PTR>(this));
252 } else {
253 /* When the payload starts with a '{', it is a JSON payload. */
254 LPCWSTR content_type = data.starts_with("{") ? L"Content-Type: application/json\r\n" : L"Content-Type: application/x-www-form-urlencoded\r\n";
255 WinHttpSendRequest(this->request, content_type, -1, const_cast<char *>(data.data()), static_cast<DWORD>(data.size()), static_cast<DWORD>(data.size()), reinterpret_cast<DWORD_PTR>(this));
256 }
257}
258
265{
266 if (this->callback.cancelled && !this->finished) {
267 Debug(net, 1, "HTTP request failed: cancelled by user");
268 this->callback.OnFailure();
269 this->finished = true;
270 /* Fall-through, as we are waiting for IsQueueEmpty() to happen. */
271 }
272
273 return this->finished && this->callback.IsQueueEmpty();
274}
275
282{
283 if (this->request) {
284 WinHttpCloseHandle(this->request);
285 WinHttpCloseHandle(this->connection);
286 }
287
288 std::lock_guard<std::mutex> lock(_http_callback_mutex);
289 _http_callbacks.erase(std::remove(_http_callbacks.begin(), _http_callbacks.end(), &this->callback), _http_callbacks.end());
290}
291
292/* static */ void NetworkHTTPSocketHandler::Connect(std::string_view uri, HTTPCallback *callback, std::string &&data)
293{
294 auto request = new NetworkHTTPRequest(std::wstring(uri.begin(), uri.end()), callback, std::move(data));
295 request->Connect();
296
297 std::lock_guard<std::mutex> lock(_new_http_requests_mutex);
298 _new_http_requests.push_back(request);
299}
300
302{
303 /* Process all callbacks. */
304 {
305 std::lock_guard<std::mutex> lock(_http_callback_mutex);
306
307 {
308 std::lock_guard<std::mutex> lock(_new_http_callback_mutex);
309 if (!_new_http_callbacks.empty()) {
310 /* We delay adding new callbacks, as HandleQueue() below might add a new callback. */
311 _http_callbacks.insert(_http_callbacks.end(), _new_http_callbacks.begin(), _new_http_callbacks.end());
312 _new_http_callbacks.clear();
313 }
314 }
315
316 for (auto &callback : _http_callbacks) {
317 callback->HandleQueue();
318 }
319 }
320
321 /* Process all requests. */
322 {
323 std::lock_guard<std::mutex> lock(_new_http_requests_mutex);
324 if (!_new_http_requests.empty()) {
325 /* We delay adding new requests, as Receive() below can cause a callback which adds a new requests. */
326 _http_requests.insert(_http_requests.end(), _new_http_requests.begin(), _new_http_requests.end());
327 _new_http_requests.clear();
328 }
329 }
330
331 if (_http_requests.empty()) return;
332
333 for (auto it = _http_requests.begin(); it != _http_requests.end(); /* nothing */) {
334 NetworkHTTPRequest *cur = *it;
335
336 if (cur->Receive()) {
337 it = _http_requests.erase(it);
338 delete cur;
339 continue;
340 }
341
342 ++it;
343 }
344}
345
347{
348 /* We create a single session, from which we build up every other request. */
349 std::string user_agent = fmt::format("OpenTTD/{}", GetNetworkRevisionString());
350 _winhttp_session = WinHttpOpen(std::wstring(user_agent.begin(), user_agent.end()).c_str(), WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, WINHTTP_FLAG_ASYNC);
351
352 /* Set the callback function for all requests. The "context" maps it back into the actual request instance. */
353 WinHttpSetStatusCallback(_winhttp_session, StaticWinHttpCallback, WINHTTP_CALLBACK_FLAG_ALL_NOTIFICATIONS, 0);
354
355 /* 10 seconds timeout for requests. */
356 WinHttpSetTimeouts(_winhttp_session, 10000, 10000, 10000, 10000);
357}
358
360{
361 WinHttpCloseHandle(_winhttp_session);
362}
Converts a HTTPCallback to a Thread-Safe variant.
Definition http_shared.h:20
void OnFailure()
Similar to HTTPCallback::OnFailure, but thread-safe.
Definition http_shared.h:37
void OnReceiveData(std::unique_ptr< char[]> data, size_t length)
Similar to HTTPCallback::OnReceiveData, but thread-safe.
Definition http_shared.h:46
bool IsQueueEmpty()
Check if the queue is empty.
Definition http_shared.h:92
Single HTTP request.
Definition http_curl.cpp:52
bool Receive()
Poll and process the HTTP request/response.
std::atomic< bool > finished
Whether we are finished with the request.
HINTERNET connection
Current connection object.
const std::string data
Data to send, if any.
Definition http_curl.cpp:78
~NetworkHTTPRequest()
Destructor of the HTTP request.
Definition http_curl.cpp:70
const std::wstring uri
URI to connect to.
NetworkHTTPRequest(std::string_view uri, HTTPCallback *callback, std::string &&data)
Create a new HTTP request.
Definition http_curl.cpp:61
HINTERNET request
Current request object.
void Connect()
Start the HTTP request handling.
HTTPThreadSafeCallback callback
Callback to send data back on.
Definition http_curl.cpp:77
void WinHttpCallback(DWORD code, void *info, DWORD length)
Callback from the WinHTTP library, called when-ever something changes about the HTTP request status.
const std::string uri
URI to connect to.
Definition http_curl.cpp:76
int depth
Current redirect depth we are in.
static void Connect(std::string_view uri, HTTPCallback *callback, std::string &&data="")
Connect to the given URI.
Definition http_curl.cpp:91
static void HTTPReceive()
Do the receiving for all HTTP connections.
#define Debug(category, level, format_string,...)
Output a line of debugging information.
Definition debug.h:37
Basic functions to send and receive HTTP packets.
Shared functions for implementations of HTTP requests.
void NetworkHTTPInitialize()
Initialize the HTTP socket handler.
void NetworkHTTPUninitialize()
Uninitialize the HTTP socket handler.
std::string_view GetNetworkRevisionString()
Get the network version string used by this build.
Callback for when the HTTP handler has something to tell us.
Definition http.h:18
std::string FS2OTTD(std::wstring_view name)
Convert to OpenTTD's encoding from a wide string.
Definition win32.cpp:340
std::mutex lock
synchronization for playback status fields
Definition win32_m.cpp:35