OpenTTD Source 20260911-master-gee2b2ac12a
http_curl.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
9
10#include "../../stdafx.h"
11#include "../../debug.h"
12#include "../../fileio_func.h"
13#include "../../rev.h"
14#include "../../thread.h"
15#include "../network_internal.h"
16
17#include "http.h"
18#include "http_shared.h"
19
20#include <atomic>
21#include <condition_variable>
22#include <curl/curl.h>
23#include <mutex>
24#include <queue>
25
26#include "../../safeguards.h"
27
28#if defined(UNIX)
30static constexpr std::initializer_list<std::string_view> _certificate_files = {
31 "/etc/ssl/certs/ca-certificates.crt"sv, // Debian/Ubuntu/Gentoo etc.
32 "/etc/pki/tls/certs/ca-bundle.crt"sv, // Fedora/RHEL 6
33 "/etc/ssl/ca-bundle.pem"sv, // OpenSUSE
34 "/etc/pki/tls/cacert.pem"sv, // OpenELEC
35 "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem"sv, // CentOS/RHEL 7
36 "/etc/ssl/cert.pem"sv, // Alpine Linux
37};
39static constexpr std::initializer_list<std::string_view> _certificate_directories = {
40 "/etc/ssl/certs"sv, // SLES10/SLES11, https://golang.org/issue/12139
41 "/etc/pki/tls/certs"sv, // Fedora/RHEL
42 "/system/etc/security/cacerts"sv, // Android
43};
44#endif /* UNIX */
45
46static std::vector<HTTPThreadSafeCallback *> _http_callbacks;
47static std::vector<HTTPThreadSafeCallback *> _new_http_callbacks;
48static std::mutex _http_callback_mutex;
49static std::mutex _new_http_callback_mutex;
50
53public:
61 NetworkHTTPRequest(std::string_view uri, HTTPCallback *callback, std::string &&data) :
62 uri(uri),
64 data(std::move(data))
65 {
66 std::lock_guard<std::mutex> lock(_new_http_callback_mutex);
67 _new_http_callbacks.push_back(&this->callback);
68 }
69
72 {
73 std::lock_guard<std::mutex> lock(_http_callback_mutex);
74 _http_callbacks.erase(std::remove(_http_callbacks.begin(), _http_callbacks.end(), &this->callback), _http_callbacks.end());
75 }
76
77 const std::string uri;
79 const std::string data;
80};
81
82static std::thread _http_thread;
83static std::atomic<bool> _http_thread_exit = false;
84static std::queue<std::unique_ptr<NetworkHTTPRequest>> _http_requests;
85static std::mutex _http_mutex;
86static std::condition_variable _http_cv;
87#if defined(UNIX)
88static std::string _http_ca_file = "";
89static std::string _http_ca_path = "";
90#endif /* UNIX */
91
92
93/* static */ void NetworkHTTPSocketHandler::Connect(std::string_view uri, HTTPCallback *callback, std::string &&data)
94{
95#if defined(UNIX)
96 if (_http_ca_file.empty() && _http_ca_path.empty()) {
97 callback->OnFailure();
98 return;
99 }
100#endif /* UNIX */
101
102 std::lock_guard<std::mutex> lock(_http_mutex);
103 _http_requests.push(std::make_unique<NetworkHTTPRequest>(uri, callback, std::move(data)));
104 _http_cv.notify_one();
105}
106
108{
109 std::lock_guard<std::mutex> lock(_http_callback_mutex);
110
111 {
112 std::lock_guard<std::mutex> lock_new(_new_http_callback_mutex);
113 if (!_new_http_callbacks.empty()) {
114 /* We delay adding new callbacks, as HandleQueue() below might add a new callback. */
116 _new_http_callbacks.clear();
117 }
118 }
119
120 for (auto &callback : _http_callbacks) {
121 callback->HandleQueue();
122 }
123}
124
131void CurlSetOption(CURL *curl, auto option, auto value)
132{
133 CURLcode res = curl_easy_setopt(curl, option, value);
134 if (res != CURLE_OK) {
135 Debug(Facility::Net, Severity::Critical, "Could not execute curl_easy_setopt for {} [{}]", option, res);
136 }
137}
138
141{
142 CURL *curl = curl_easy_init();
143 assert(curl != nullptr);
144
145 for (;;) {
146 std::unique_lock<std::mutex> lock(_http_mutex);
147
148 /* Wait for a new request. */
149 while (_http_requests.empty() && !_http_thread_exit) {
150 _http_cv.wait(lock);
151 }
152 if (_http_thread_exit) break;
153
154 std::unique_ptr<NetworkHTTPRequest> request = std::move(_http_requests.front());
155 _http_requests.pop();
156
157 /* Release the lock, as we will take a while to process the request. */
158 lock.unlock();
159
160 /* Reset to default settings. */
161 curl_easy_reset(curl);
162 curl_slist *headers = nullptr;
163
165 CurlSetOption(curl, CURLOPT_VERBOSE, 1L);
166 }
167
168 /* Setup some default options. */
169 std::string user_agent = fmt::format("OpenTTD/{}", GetNetworkRevisionString());
170 CurlSetOption(curl, CURLOPT_USERAGENT, user_agent.c_str());
171 CurlSetOption(curl, CURLOPT_FOLLOWLOCATION, 1L);
172 CurlSetOption(curl, CURLOPT_MAXREDIRS, 5L);
173
174 /* Ensure we validate the certificate and hostname of the server. */
175#if defined(UNIX)
176 CurlSetOption(curl, CURLOPT_CAINFO, _http_ca_file.empty() ? nullptr : _http_ca_file.c_str());
177 CurlSetOption(curl, CURLOPT_CAPATH, _http_ca_path.empty() ? nullptr : _http_ca_path.c_str());
178#endif /* UNIX */
179 CurlSetOption(curl, CURLOPT_SSL_VERIFYHOST, 2);
180 CurlSetOption(curl, CURLOPT_SSL_VERIFYPEER, true);
181
182 /* Give the connection about 10 seconds to complete. */
183 CurlSetOption(curl, CURLOPT_CONNECTTIMEOUT, 10L);
184
185 /* Set a buffer of 100KiB, as the default of 16KiB seems a bit small. */
186 CurlSetOption(curl, CURLOPT_BUFFERSIZE, 100L * 1024L);
187
188 /* Fail our call if we don't receive a 2XX return value. */
189 CurlSetOption(curl, CURLOPT_FAILONERROR, 1L);
190
191 /* Prepare POST body and URI. */
192 if (!request->data.empty()) {
193 /* When the payload starts with a '{', it is a JSON payload. */
194 if (request->data.starts_with("{")) {
195 headers = curl_slist_append(headers, "Content-Type: application/json");
196 } else {
197 headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
198 }
199
200 CurlSetOption(curl, CURLOPT_POST, 1L);
201 CurlSetOption(curl, CURLOPT_POSTFIELDS, request->data.c_str());
202 CurlSetOption(curl, CURLOPT_HTTPHEADER, headers);
203 }
204 CurlSetOption(curl, CURLOPT_URL, request->uri.c_str());
205
206 /* Setup our (C-style) callback function which we pipe back into the callback. */
207 CurlSetOption(curl, CURLOPT_WRITEFUNCTION, +[](char *ptr, size_t size, size_t nmemb, void *userdata) -> size_t {
208 Debug(Facility::Net, Severity::Debug2, "HTTP callback: {} bytes", size * nmemb);
209 HTTPThreadSafeCallback *callback = static_cast<HTTPThreadSafeCallback *>(userdata);
210
211 /* Copy the buffer out of CURL. OnReceiveData() will free it when done. */
212 std::unique_ptr<char[]> buffer = std::make_unique<char[]>(size * nmemb);
213 std::copy_n(ptr, size * nmemb, buffer.get());
214 callback->OnReceiveData(std::move(buffer), size * nmemb);
215
216 return size * nmemb;
217 });
218 CurlSetOption(curl, CURLOPT_WRITEDATA, &request->callback);
219
220 /* Create a callback from which we can cancel. Sadly, there is no other
221 * thread-safe way to do this. If the connection went idle, it can take
222 * up to a second before this callback is called. There is little we can
223 * do about this. */
224 CurlSetOption(curl, CURLOPT_NOPROGRESS, 0L);
225 CurlSetOption(curl, CURLOPT_XFERINFOFUNCTION, +[](void *userdata, curl_off_t /*dltotal*/, curl_off_t /*dlnow*/, curl_off_t /*ultotal*/, curl_off_t /*ulnow*/) -> int {
226 const HTTPThreadSafeCallback *callback = static_cast<HTTPThreadSafeCallback *>(userdata);
227 return (callback->cancelled || _http_thread_exit) ? 1 : 0;
228 });
229 CurlSetOption(curl, CURLOPT_XFERINFODATA, &request->callback);
230
231 /* Perform the request. */
232 CURLcode res = curl_easy_perform(curl);
233
234 curl_slist_free_all(headers);
235
236 if (res == CURLE_OK) {
237 Debug(Facility::Net, Severity::Error, "HTTP request succeeded");
238 request->callback.OnReceiveData(nullptr, 0);
239 } else {
240 long status_code = 0;
241 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code);
242
243 /* No need to be verbose about rate limiting. */
244 Debug(Facility::Net, (request->callback.cancelled || _http_thread_exit || status_code == HTTP_429_TOO_MANY_REQUESTS) ? Severity::Error : Severity::Critical, "HTTP request failed: status_code: {}, error: {}", status_code, curl_easy_strerror(res));
245 request->callback.OnFailure();
246 }
247
248 /* Wait till the callback tells us all data is dequeued, or _http_thread_exit has been set. */
249 request->callback.WaitTillEmptyOrCondition([]() -> bool {
250 return _http_thread_exit;
251 });
252 }
253
254 curl_easy_cleanup(curl);
255}
256
258{
259 curl_global_init(CURL_GLOBAL_DEFAULT);
260
261#if defined(UNIX)
262 /* Depending on the Linux distro, certificates can either be in
263 * a bundle or a folder, in a wide range of different locations.
264 * Try to find what location is used by this OS. */
265 for (auto &ca_file : _certificate_files) {
266 if (FileExists(ca_file)) {
267 _http_ca_file = ca_file;
268 break;
269 }
270 }
271 if (_http_ca_file.empty()) {
272 for (auto &ca_path : _certificate_directories) {
273 if (FileExists(ca_path)) {
274 _http_ca_path = ca_path;
275 break;
276 }
277 }
278 }
279 Debug(Facility::Net, Severity::Notice, "Using certificate file: {}", _http_ca_file.empty() ? "none" : _http_ca_file);
280 Debug(Facility::Net, Severity::Notice, "Using certificate path: {}", _http_ca_path.empty() ? "none" : _http_ca_path);
281
282 /* Tell the user why HTTPS will not be working. */
283 if (_http_ca_file.empty() && _http_ca_path.empty()) {
284 Debug(Facility::Net, Severity::Critical, "No certificate files or directories found, HTTPS will not work!");
285 }
286#endif /* UNIX */
287
288 _http_thread_exit = false;
289 StartNewThread(&_http_thread, "ottd:http", &HttpThread);
290}
291
293{
294 _http_thread_exit = true;
295
296 /* Ensure the callbacks are handled. This is mostly needed as we send
297 * a survey just before close, and that might be pending here. */
299
300 {
301 std::lock_guard<std::mutex> lock(_http_mutex);
302 _http_cv.notify_one();
303 }
304
305 if (_http_thread.joinable()) {
306 _http_thread.join();
307 }
308
309 curl_global_cleanup();
310}
Converts a HTTPCallback to a Thread-Safe variant.
Definition http_shared.h:20
void OnReceiveData(std::unique_ptr< char[]> data, size_t length)
Similar to HTTPCallback::OnReceiveData, but thread-safe.
Definition http_shared.h:53
std::atomic< bool > cancelled
Whether this callback has been cancelled, or not.
const std::string data
Data to send, if any.
Definition http_curl.cpp:79
~NetworkHTTPRequest()
Remove ourselves from the callback list.
Definition http_curl.cpp:71
NetworkHTTPRequest(std::string_view uri, HTTPCallback *callback, std::string &&data)
Create a new HTTP request.
Definition http_curl.cpp:61
HTTPThreadSafeCallback callback
Callback to send data back on.
Definition http_curl.cpp:78
const std::string uri
URI to connect to.
Definition http_curl.cpp:77
static void Connect(std::string_view uri, HTTPCallback *callback, std::string &&data="")
Connect to the given URI.
Definition http_curl.cpp:93
static void HTTPReceive()
Do the receiving for all HTTP connections.
Functions related to debugging.
bool IsVisibleSeverity(Facility facility, Severity severity)
Test if debug severity is visible for the given facility.
Definition debug.h:25
#define Debug(facility, severity, format_string,...)
Output a line of debugging information.
Definition debug.h:37
@ Net
Net message facility.
Definition debug_type.h:33
@ Debug1
Debug #1 - High level debug messages.
Definition debug_type.h:20
@ Notice
Notice.
Definition debug_type.h:18
@ Critical
Critical, user should know about this.
Definition debug_type.h:15
@ Debug2
Debug #2 - Low level debug messages.
Definition debug_type.h:21
@ Error
Error, but we are recovering.
Definition debug_type.h:16
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:134
Functions for standard in/out file operations.
Basic functions to send and receive HTTP packets.
constexpr int HTTP_429_TOO_MANY_REQUESTS
HTTP error code for when the client is doing too many requests.
Definition http.h:15
static std::mutex _http_mutex
Mutex to prevent concurrent access _http_requests.
Definition http_curl.cpp:85
static std::queue< std::unique_ptr< NetworkHTTPRequest > > _http_requests
HTTP requests that are currently running.
Definition http_curl.cpp:84
void NetworkHTTPInitialize()
Initialize the HTTP socket handler.
static std::vector< HTTPThreadSafeCallback * > _new_http_callbacks
Callbacks for the request that should be started.
Definition http_curl.cpp:47
static std::thread _http_thread
The thread running the HTTP requests.
Definition http_curl.cpp:82
static std::condition_variable _http_cv
Conditional variable to wake up the HTTP request thread.
Definition http_curl.cpp:86
static std::atomic< bool > _http_thread_exit
Whether to ask the HTTP request thread to stop.
Definition http_curl.cpp:83
static std::mutex _http_callback_mutex
Mutex to prevent concurrent access to _http_callbacks.
Definition http_curl.cpp:48
void CurlSetOption(CURL *curl, auto option, auto value)
Set some specific option and emit debug information upon failure.
static std::mutex _new_http_callback_mutex
Mutex to prevent concurrent access to _new_http_callbacks.
Definition http_curl.cpp:49
void HttpThread()
Thread entry point for the HTTP request thread.
void NetworkHTTPUninitialize()
Uninitialize the HTTP socket handler.
static std::vector< HTTPThreadSafeCallback * > _http_callbacks
Callback for the current requests.
Definition http_curl.cpp:46
Shared functions for implementations of HTTP requests.
static std::vector< HTTPThreadSafeCallback * > _new_http_callbacks
Callbacks for the request that should be started.
static std::mutex _http_callback_mutex
Mutex to prevent concurrent access to _http_callbacks.
static std::mutex _new_http_callback_mutex
Mutex to prevent concurrent access to _new_http_callbacks.
static std::vector< HTTPThreadSafeCallback * > _http_callbacks
Callback for the current requests.
std::string_view GetNetworkRevisionString()
Get the network version string used by this build.
Variables and function used internally.
Declaration of OTTD revision dependent variables.
A number of safeguards to prevent using unsafe methods.
Definition of base types and functions in a cross-platform compatible way.
Callback for when the HTTP handler has something to tell us.
Definition http.h:18
virtual void OnFailure()=0
An error has occurred and the connection has been closed.
Base of all threads.
bool StartNewThread(std::thread *thr, std::string_view name, TFn &&_Fx, TArgs &&... _Ax)
Start a new thread.
Definition thread.h:47
std::mutex lock
synchronization for playback status fields
Definition win32_m.cpp:35