OpenTTD Source 20260109-master-g241b5fcdfe
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
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
71 {
72 std::lock_guard<std::mutex> lock(_http_callback_mutex);
73 _http_callbacks.erase(std::remove(_http_callbacks.begin(), _http_callbacks.end(), &this->callback), _http_callbacks.end());
74 }
75
76 const std::string uri;
78 const std::string data;
79};
80
81static std::thread _http_thread;
82static std::atomic<bool> _http_thread_exit = false;
83static std::queue<std::unique_ptr<NetworkHTTPRequest>> _http_requests;
84static std::mutex _http_mutex;
85static std::condition_variable _http_cv;
86#if defined(UNIX)
87static std::string _http_ca_file = "";
88static std::string _http_ca_path = "";
89#endif /* UNIX */
90
91/* static */ void NetworkHTTPSocketHandler::Connect(std::string_view uri, HTTPCallback *callback, std::string &&data)
92{
93#if defined(UNIX)
94 if (_http_ca_file.empty() && _http_ca_path.empty()) {
95 callback->OnFailure();
96 return;
97 }
98#endif /* UNIX */
99
100 std::lock_guard<std::mutex> lock(_http_mutex);
101 _http_requests.push(std::make_unique<NetworkHTTPRequest>(uri, callback, std::move(data)));
102 _http_cv.notify_one();
103}
104
106{
107 std::lock_guard<std::mutex> lock(_http_callback_mutex);
108
109 {
110 std::lock_guard<std::mutex> lock_new(_new_http_callback_mutex);
111 if (!_new_http_callbacks.empty()) {
112 /* We delay adding new callbacks, as HandleQueue() below might add a new callback. */
113 _http_callbacks.insert(_http_callbacks.end(), _new_http_callbacks.begin(), _new_http_callbacks.end());
114 _new_http_callbacks.clear();
115 }
116 }
117
118 for (auto &callback : _http_callbacks) {
119 callback->HandleQueue();
120 }
121}
122
123void CurlSetOption(CURL *curl, auto option, auto value)
124{
125 CURLcode res = curl_easy_setopt(curl, option, value);
126 if (res != CURLE_OK) {
127 Debug(net, 0, "Could not execute curl_easy_setopt for {} [{}]", option, res);
128 }
129}
130
131void HttpThread()
132{
133 CURL *curl = curl_easy_init();
134 assert(curl != nullptr);
135
136 for (;;) {
137 std::unique_lock<std::mutex> lock(_http_mutex);
138
139 /* Wait for a new request. */
140 while (_http_requests.empty() && !_http_thread_exit) {
141 _http_cv.wait(lock);
142 }
143 if (_http_thread_exit) break;
144
145 std::unique_ptr<NetworkHTTPRequest> request = std::move(_http_requests.front());
146 _http_requests.pop();
147
148 /* Release the lock, as we will take a while to process the request. */
149 lock.unlock();
150
151 /* Reset to default settings. */
152 curl_easy_reset(curl);
153 curl_slist *headers = nullptr;
154
155 if (_debug_net_level >= 5) {
156 CurlSetOption(curl, CURLOPT_VERBOSE, 1L);
157 }
158
159 /* Setup some default options. */
160 std::string user_agent = fmt::format("OpenTTD/{}", GetNetworkRevisionString());
161 CurlSetOption(curl, CURLOPT_USERAGENT, user_agent.c_str());
162 CurlSetOption(curl, CURLOPT_FOLLOWLOCATION, 1L);
163 CurlSetOption(curl, CURLOPT_MAXREDIRS, 5L);
164
165 /* Ensure we validate the certificate and hostname of the server. */
166#if defined(UNIX)
167 CurlSetOption(curl, CURLOPT_CAINFO, _http_ca_file.empty() ? nullptr : _http_ca_file.c_str());
168 CurlSetOption(curl, CURLOPT_CAPATH, _http_ca_path.empty() ? nullptr : _http_ca_path.c_str());
169#endif /* UNIX */
170 CurlSetOption(curl, CURLOPT_SSL_VERIFYHOST, 2);
171 CurlSetOption(curl, CURLOPT_SSL_VERIFYPEER, true);
172
173 /* Give the connection about 10 seconds to complete. */
174 CurlSetOption(curl, CURLOPT_CONNECTTIMEOUT, 10L);
175
176 /* Set a buffer of 100KiB, as the default of 16KiB seems a bit small. */
177 CurlSetOption(curl, CURLOPT_BUFFERSIZE, 100L * 1024L);
178
179 /* Fail our call if we don't receive a 2XX return value. */
180 CurlSetOption(curl, CURLOPT_FAILONERROR, 1L);
181
182 /* Prepare POST body and URI. */
183 if (!request->data.empty()) {
184 /* When the payload starts with a '{', it is a JSON payload. */
185 if (request->data.starts_with("{")) {
186 headers = curl_slist_append(headers, "Content-Type: application/json");
187 } else {
188 headers = curl_slist_append(headers, "Content-Type: application/x-www-form-urlencoded");
189 }
190
191 CurlSetOption(curl, CURLOPT_POST, 1L);
192 CurlSetOption(curl, CURLOPT_POSTFIELDS, request->data.c_str());
193 CurlSetOption(curl, CURLOPT_HTTPHEADER, headers);
194 }
195 CurlSetOption(curl, CURLOPT_URL, request->uri.c_str());
196
197 /* Setup our (C-style) callback function which we pipe back into the callback. */
198 CurlSetOption(curl, CURLOPT_WRITEFUNCTION, +[](char *ptr, size_t size, size_t nmemb, void *userdata) -> size_t {
199 Debug(net, 6, "HTTP callback: {} bytes", size * nmemb);
200 HTTPThreadSafeCallback *callback = static_cast<HTTPThreadSafeCallback *>(userdata);
201
202 /* Copy the buffer out of CURL. OnReceiveData() will free it when done. */
203 std::unique_ptr<char[]> buffer = std::make_unique<char[]>(size * nmemb);
204 std::copy_n(ptr, size * nmemb, buffer.get());
205 callback->OnReceiveData(std::move(buffer), size * nmemb);
206
207 return size * nmemb;
208 });
209 CurlSetOption(curl, CURLOPT_WRITEDATA, &request->callback);
210
211 /* Create a callback from which we can cancel. Sadly, there is no other
212 * thread-safe way to do this. If the connection went idle, it can take
213 * up to a second before this callback is called. There is little we can
214 * do about this. */
215 CurlSetOption(curl, CURLOPT_NOPROGRESS, 0L);
216 CurlSetOption(curl, CURLOPT_XFERINFOFUNCTION, +[](void *userdata, curl_off_t /*dltotal*/, curl_off_t /*dlnow*/, curl_off_t /*ultotal*/, curl_off_t /*ulnow*/) -> int {
217 const HTTPThreadSafeCallback *callback = static_cast<HTTPThreadSafeCallback *>(userdata);
218 return (callback->cancelled || _http_thread_exit) ? 1 : 0;
219 });
220 CurlSetOption(curl, CURLOPT_XFERINFODATA, &request->callback);
221
222 /* Perform the request. */
223 CURLcode res = curl_easy_perform(curl);
224
225 curl_slist_free_all(headers);
226
227 if (res == CURLE_OK) {
228 Debug(net, 1, "HTTP request succeeded");
229 request->callback.OnReceiveData(nullptr, 0);
230 } else {
231 long status_code = 0;
232 curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &status_code);
233
234 /* No need to be verbose about rate limiting. */
235 Debug(net, (request->callback.cancelled || _http_thread_exit || status_code == HTTP_429_TOO_MANY_REQUESTS) ? 1 : 0, "HTTP request failed: status_code: {}, error: {}", status_code, curl_easy_strerror(res));
236 request->callback.OnFailure();
237 }
238
239 /* Wait till the callback tells us all data is dequeued, or _http_thread_exit has been set. */
240 request->callback.WaitTillEmptyOrCondition([]() -> bool {
241 return _http_thread_exit;
242 });
243 }
244
245 curl_easy_cleanup(curl);
246}
247
249{
250 curl_global_init(CURL_GLOBAL_DEFAULT);
251
252#if defined(UNIX)
253 /* Depending on the Linux distro, certificates can either be in
254 * a bundle or a folder, in a wide range of different locations.
255 * Try to find what location is used by this OS. */
256 for (auto &ca_file : _certificate_files) {
257 if (FileExists(ca_file)) {
258 _http_ca_file = ca_file;
259 break;
260 }
261 }
262 if (_http_ca_file.empty()) {
263 for (auto &ca_path : _certificate_directories) {
264 if (FileExists(ca_path)) {
265 _http_ca_path = ca_path;
266 break;
267 }
268 }
269 }
270 Debug(net, 3, "Using certificate file: {}", _http_ca_file.empty() ? "none" : _http_ca_file);
271 Debug(net, 3, "Using certificate path: {}", _http_ca_path.empty() ? "none" : _http_ca_path);
272
273 /* Tell the user why HTTPS will not be working. */
274 if (_http_ca_file.empty() && _http_ca_path.empty()) {
275 Debug(net, 0, "No certificate files or directories found, HTTPS will not work!");
276 }
277#endif /* UNIX */
278
279 _http_thread_exit = false;
280 StartNewThread(&_http_thread, "ottd:http", &HttpThread);
281}
282
284{
285 _http_thread_exit = true;
286
287 /* Ensure the callbacks are handled. This is mostly needed as we send
288 * a survey just before close, and that might be pending here. */
290
291 {
292 std::lock_guard<std::mutex> lock(_http_mutex);
293 _http_cv.notify_one();
294 }
295
296 if (_http_thread.joinable()) {
297 _http_thread.join();
298 }
299
300 curl_global_cleanup();
301}
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:46
Single HTTP request.
Definition http_curl.cpp:52
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
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:77
const std::string uri
URI to connect to.
Definition http_curl.cpp:76
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
bool FileExists(std::string_view filename)
Test whether the given filename exists.
Definition fileio.cpp:132
Basic functions to send and receive HTTP packets.
void NetworkHTTPInitialize()
Initialize the HTTP socket handler.
void NetworkHTTPUninitialize()
Uninitialize the HTTP socket handler.
Shared functions for implementations of HTTP requests.
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
virtual void OnFailure()=0
An error has occurred and the connection has been closed.
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