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