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/>.
9 * @file http_winhttp.cpp WinHTTP-based implementation for HTTP requests.
12 #include "../../stdafx.h"
13 #include "../../debug.h"
14 #include "../../rev.h"
15 #include "../network_internal.h"
18 #include "http_shared.h"
23 #include "../../safeguards.h"
25 static HINTERNET _winhttp_session
= nullptr;
27 /** Single HTTP request. */
28 class NetworkHTTPRequest
{
30 const std::wstring uri
; ///< URI to connect to.
31 HTTPThreadSafeCallback callback
; ///< Callback to send data back on.
32 const std::string data
; ///< Data to send, if any.
34 HINTERNET connection
= nullptr; ///< Current connection object.
35 HINTERNET request
= nullptr; ///< Current request object.
36 std::atomic
<bool> finished
= false; ///< Whether we are finished with the request.
37 int depth
= 0; ///< Current redirect depth we are in.
40 NetworkHTTPRequest(const std::wstring
&uri
, HTTPCallback
*callback
, const std::string
&data
);
42 ~NetworkHTTPRequest();
46 void WinHttpCallback(DWORD code
, void *info
, DWORD length
);
49 static std::vector
<NetworkHTTPRequest
*> _http_requests
;
50 static std::vector
<NetworkHTTPRequest
*> _new_http_requests
;
51 static std::mutex _new_http_requests_mutex
;
53 static std::vector
<HTTPThreadSafeCallback
*> _http_callbacks
;
54 static std::vector
<HTTPThreadSafeCallback
*> _new_http_callbacks
;
55 static std::mutex _http_callback_mutex
;
56 static std::mutex _new_http_callback_mutex
;
59 * Create a new HTTP request.
61 * @param uri the URI to connect to (https://.../..).
62 * @param callback the callback to send data back on.
63 * @param data the data we want to send. When non-empty, this will be a POST request, otherwise a GET request.
65 NetworkHTTPRequest::NetworkHTTPRequest(const std::wstring
&uri
, HTTPCallback
*callback
, const std::string
&data
) :
70 std::lock_guard
<std::mutex
> lock(_new_http_callback_mutex
);
71 _new_http_callbacks
.push_back(&this->callback
);
74 static std::string
GetLastErrorAsString()
78 DWORD error_code
= GetLastError();
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
);
85 return FS2OTTD(buffer
);
89 * Callback from the WinHTTP library, called when-ever something changes about the HTTP request status.
91 * The callback needs to call some WinHttp functions for certain states, so WinHttp continues
92 * to read the request. This also allows us to abort when things go wrong, by simply not calling
94 * Comments with "Next step:" mark where WinHttp needs a call to continue.
96 * @param code The code of the event.
97 * @param info The information about the event.
98 * @param length The length of the information.
100 void NetworkHTTPRequest::WinHttpCallback(DWORD code
, void *info
, DWORD length
)
102 if (this->finished
) return;
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. */
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;
130 case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE
:
131 /* Next step: read response. */
132 WinHttpReceiveResponse(this->request
, nullptr);
135 case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE
:
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
);
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;
152 /* Next step: query for any data. */
153 WinHttpQueryDataAvailable(this->request
, nullptr);
156 case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE
:
158 /* Retrieve the amount of data available to process. */
159 DWORD size
= *(DWORD
*)info
;
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);
167 case WINHTTP_CALLBACK_STATUS_READ_COMPLETE
:
168 Debug(net
, 6, "HTTP callback: {} bytes", length
);
170 this->callback
.OnReceiveData(std::unique_ptr
<char[]>(static_cast<char *>(info
)), length
);
173 /* Next step: no more data available: request is finished. */
174 this->finished
= true;
175 Debug(net
, 1, "HTTP request succeeded");
177 /* Next step: query for more data. */
178 WinHttpQueryDataAvailable(this->request
, nullptr);
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;
191 Debug(net
, 0, "HTTP request failed: unexepected callback code 0x{:x}", code
);
192 this->callback
.OnFailure();
193 this->finished
= true;
198 static void CALLBACK
StaticWinHttpCallback(HINTERNET
, DWORD_PTR context
, DWORD code
, void *info
, DWORD length
)
200 if (context
== 0) return;
202 NetworkHTTPRequest
*request
= (NetworkHTTPRequest
*)context
;
203 request
->WinHttpCallback(code
, info
, length
);
207 * Start the HTTP request handling.
209 * This is done in an async manner, so we can do other things while waiting for
210 * the HTTP request to finish. The actual receiving of the data is done in
213 void NetworkHTTPRequest::Connect()
215 Debug(net
, 1, "HTTP request to {}", std::string(uri
.begin(), uri
.end()));
217 URL_COMPONENTS url_components
= {};
219 wchar_t hostname
[128];
220 wchar_t url_path
[4096];
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
);
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;
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
);
245 Debug(net
, 0, "HTTP request failed: {}", GetLastErrorAsString());
246 this->callback
.OnFailure();
247 this->finished
= true;
251 /* Send the request (possibly with a payload). */
253 WinHttpSendRequest(this->request
, WINHTTP_NO_ADDITIONAL_HEADERS
, 0, WINHTTP_NO_REQUEST_DATA
, 0, 0, reinterpret_cast<DWORD_PTR
>(this));
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));
262 * Poll and process the HTTP request/response.
264 * @return True iff the request is done; no call to Receive() should be done after it returns true.
266 bool NetworkHTTPRequest::Receive()
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. */
275 return this->finished
&& this->callback
.IsQueueEmpty();
279 * Destructor of the HTTP request.
281 * Makes sure all handlers are closed, and all memory is free'd.
283 NetworkHTTPRequest::~NetworkHTTPRequest()
286 WinHttpCloseHandle(this->request
);
287 WinHttpCloseHandle(this->connection
);
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());
294 /* static */ void NetworkHTTPSocketHandler::Connect(const std::string
&uri
, HTTPCallback
*callback
, const std::string data
)
296 auto request
= new NetworkHTTPRequest(std::wstring(uri
.begin(), uri
.end()), callback
, data
);
299 std::lock_guard
<std::mutex
> lock(_new_http_requests_mutex
);
300 _new_http_requests
.push_back(request
);
303 /* static */ void NetworkHTTPSocketHandler::HTTPReceive()
305 /* Process all callbacks. */
307 std::lock_guard
<std::mutex
> lock(_http_callback_mutex
);
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();
318 for (auto &callback
: _http_callbacks
) {
319 callback
->HandleQueue();
323 /* Process all requests. */
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();
333 if (_http_requests
.empty()) return;
335 for (auto it
= _http_requests
.begin(); it
!= _http_requests
.end(); /* nothing */) {
336 NetworkHTTPRequest
*cur
= *it
;
338 if (cur
->Receive()) {
339 it
= _http_requests
.erase(it
);
348 void NetworkHTTPInitialize()
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
);
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);
357 /* 10 seconds timeout for requests. */
358 WinHttpSetTimeouts(_winhttp_session
, 10000, 10000, 10000, 10000);
361 void NetworkHTTPUninitialize()
363 WinHttpCloseHandle(_winhttp_session
);