Botcraft 26.3
Loading...
Searching...
No Matches
Https.cpp
Go to the documentation of this file.
1#ifdef USE_ENCRYPTION
2
5
6#include <asio/connect.hpp>
7#include <asio/ip/tcp.hpp>
8#include <asio/read.hpp>
9#include <asio/read_until.hpp>
10#include <asio/ssl.hpp>
11#include <asio/streambuf.hpp>
12#include <openssl/x509.h>
13
14#include <algorithm>
15#include <sstream>
16#include <stdexcept>
17
18#if defined(_WIN32)
19#include <wincrypt.h>
20#include <windows.h>
21// wincrypt.h #defines names that clash with OpenSSL's, so undo them
22#undef X509_NAME
23#undef X509_EXTENSIONS
24#undef X509_CERT_PAIR
25#undef PKCS7_ISSUER_AND_SERIAL
26#undef PKCS7_SIGNER_INFO
27#undef OCSP_REQUEST
28#undef OCSP_RESPONSE
29#elif defined(__APPLE__)
30#include <Security/Security.h>
31#include <CoreFoundation/CoreFoundation.h>
32#endif
33
34
35namespace Botcraft
36{
37 namespace
38 {
39 void AddDERCert(X509_STORE* store, const unsigned char* data, const long len)
40 {
41 X509* x509 = d2i_X509(nullptr, &data, len);
42 if (x509 != nullptr)
43 {
44 X509_STORE_add_cert(store, x509);
45 X509_free(x509);
46 }
47 }
48
49 void LoadSystemCertificates(asio::ssl::context& ctx)
50 {
51 ctx.set_default_verify_paths();
52
53#ifdef _WIN32
54 X509_STORE* store = SSL_CTX_get_cert_store(ctx.native_handle());
55 for (const char* name : { "ROOT", "CA" })
56 {
57 HCERTSTORE hStore = CertOpenSystemStoreA(0, name);
58 if (!hStore)
59 {
60 continue;
61 }
62 PCCERT_CONTEXT cert = nullptr;
63 while ((cert = CertEnumCertificatesInStore(hStore, cert)) != nullptr)
64 {
65 AddDERCert(store, cert->pbCertEncoded, cert->cbCertEncoded);
66 }
67
68 CertCloseStore(hStore, 0);
69 }
70#elif defined __APPLE__
71 X509_STORE* store = SSL_CTX_get_cert_store(ctx.native_handle());
72 CFArrayRef certs = nullptr;
73 if (SecTrustCopyAnchorCertificates(&certs) == errSecSuccess && certs)
74 {
75 for (CFIndex i = 0; i < CFArrayGetCount(certs); ++i)
76 {
77 auto cert = (SecCertificateRef)CFArrayGetValueAtIndex(certs, i);
78 CFDataRef der = SecCertificateCopyData(cert);
79 if (der)
80 {
81 AddDERCert(store, CFDataGetBytePtr(der), CFDataGetLength(der));
82 CFRelease(der);
83 }
84 }
85 CFRelease(certs);
86 }
87#else
88 for (const char* path : {"/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", "/etc/ssl/ca-bundle.pem", "/etc/ssl/cert.pem" })
89 {
90 asio::error_code ec;
91 ctx.load_verify_file(path, ec);
92 }
93#endif
94 }
95
96 WebRequestResponse WebRequest(
97 const std::string& host,
98 const std::string& raw_request
99 )
100 {
101 asio::io_context io_context;
102
103 // Get a list of endpoints corresponding to the server name.
104 asio::ip::tcp::resolver resolver(io_context);
105 asio::ip::tcp::resolver::results_type endpoints = resolver.resolve(host, "https");
106
107 // Create a permanent context with system certificates loaded
108 static asio::ssl::context ctx = []() {
109 asio::ssl::context c(asio::ssl::context::tls_client);
110 c.set_options(asio::ssl::context::default_workarounds | asio::ssl::context::no_sslv2 | asio::ssl::context::no_sslv3);
111 LoadSystemCertificates(c);
112 return c;
113 }();
114
115 asio::ssl::stream<asio::ip::tcp::socket> socket(io_context, ctx);
116 socket.set_verify_mode(asio::ssl::verify_none);
117 socket.set_verify_callback(asio::ssl::host_name_verification(host));
118 SSL_set_tlsext_host_name(socket.native_handle(), host.c_str());
119 asio::connect(socket.lowest_layer(), endpoints);
120 asio::error_code ec{};
121 socket.handshake(socket.client, ec);
122 if (ec)
123 {
124 LOG_ERROR("Error while performing Web SSL handshake: " << ec << "(" << ec.message() << ")");
125 return WebRequestResponse{
127 418, // "I'm a teapot"
128 ec.message()
129 };
130 }
131 socket.lowest_layer().set_option(asio::ip::tcp::no_delay(true));
132
133 // Send the request
134 asio::streambuf request;
135 std::ostream request_stream(&request);
136 request_stream << raw_request;
137
138 asio::write(socket, request);
139
140 WebRequestResponse web_response;
141
142 // Read the response status line. The response streambuf will automatically
143 // grow to accommodate the entire line. The growth may be limited by passing
144 // a maximum size to the streambuf constructor.
145 asio::streambuf response;
146 asio::read_until(socket, response, "\r\n");
147
148 // Check that response is OK.
149 std::istream response_stream(&response);
150 std::string http_version;
151 response_stream >> http_version;
152 response_stream >> web_response.status_code;
153 std::getline(response_stream, web_response.status_message);
154
155 // Remove any \r in status message
156 web_response.status_message.erase(std::remove(web_response.status_message.begin(), web_response.status_message.end(), '\r'),
157 web_response.status_message.end());
158
159 if (!response_stream || http_version.substr(0, 5) != "HTTP/")
160 {
161 LOG_ERROR("Invalid response during web request");
162 web_response.response = {};
163 return web_response;
164 }
165
166 // Empty response
167 if (web_response.status_code == 204)
168 {
169 web_response.response = {};
170 return web_response;
171 }
172
173 // Read the response headers, which are terminated by a blank line.
174 asio::read_until(socket, response, "\r\n\r\n");
175
176 // Process the response headers.
177 long long int data_length = -1;
178 bool chunked = false;
179 std::string header;
180 while (std::getline(response_stream, header) && header != "\r")
181 {
182 // Remove trailing \r
183 if (!header.empty() && header.back() == '\r')
184 {
185 header.pop_back();
186 }
187
188 if (header.find("Content-Length: ") == 0)
189 {
190 data_length = std::stoll(header.substr(16));
191 }
192 else if (header.find("Transfer-Encoding:") == 0 && header.find("chunked") != std::string::npos)
193 {
194 chunked = true;
195 }
196 }
197
198 // Write whatever content we already have to output.
199 std::stringstream output_stringstream;
200 if (response.size() > 0)
201 {
202 output_stringstream << &response;
203 }
204
205 // Read until EOF, writing data to output as we go.
206 asio::error_code error;
207 while (asio::read(socket, response, asio::transfer_at_least(1), error))
208 {
209 output_stringstream << &response;
210 }
211 const std::string raw_body = output_stringstream.str();
212
213 if (error != asio::error::eof && (data_length < 0 || raw_body.size() != static_cast<size_t>(data_length)))
214 {
215 LOG_ERROR("Error trying to read web request response, Error:\n " << error);
216 web_response.response = {};
217 return web_response;
218 }
219
220 if (chunked)
221 {
222 std::string decoded_body;
223 bool decode_ok = false;
224 size_t pos = 0;
225
226 while (pos < raw_body.size())
227 {
228 const size_t line_end = raw_body.find("\r\n", pos);
229 if (line_end == std::string::npos)
230 {
231 // This means the response is probably malformed
232 break;
233 }
234
235 std::string size_line = raw_body.substr(pos, line_end - pos);
236 const size_t semicolon = size_line.find(";");
237 if (semicolon != std::string::npos)
238 {
239 size_line.resize(semicolon);
240 }
241
242 const size_t chunk_size = std::stoull(size_line, nullptr, 16);
243
244 pos = line_end + 2;
245 if (chunk_size == 0)
246 {
247 decode_ok = true;
248 break;
249 }
250
251 if (pos + chunk_size > raw_body.size())
252 {
253 break;
254 }
255
256 decoded_body.append(raw_body, pos, chunk_size);
257 // Skip chunk data + "\r\n"
258 pos += chunk_size + 2;
259 }
260
261 if (!decode_ok)
262 {
263 LOG_ERROR("Malformed chunked web request response");
264 web_response.response = {};
265 return web_response;
266 }
267
268 web_response.response = ProtocolCraft::Json::Parse(decoded_body, true);
269 }
270 else
271 {
272 web_response.response = ProtocolCraft::Json::Parse(raw_body, true);
273 }
274
275 return web_response;
276 }
277 }
278
280 const std::string& host, const std::string& endpoint,
281 const std::string& content_type, const std::string& accept,
282 const std::string& authorization, const std::string& data
283 )
284 {
285 // Form the request. We specify the "Connection: close" header so that the
286 // server will close the socket after transmitting the response. This will
287 // allow us to treat all data up until the EOF as the content.
288 std::string raw_request = "";
289 raw_request += "POST " + endpoint + " HTTP/1.1 \r\n";
290 raw_request += "Host: " + host + "\r\n";
291 raw_request += "User-Agent: C/1.0\r\n";
292 raw_request += "Content-Type: " + content_type + " \r\n";
293 raw_request += "Accept: " + accept + "\r\n";
294 if (!authorization.empty())
295 {
296 raw_request += "Authorization: " + authorization + "\r\n";
297 }
298 raw_request += "Content-Length: " + std::to_string(data.length()) + "\r\n";
299 raw_request += "Connection: close\r\n\r\n";
300 raw_request += data;
301
302 return WebRequest(host, raw_request);
303 }
304
306 const std::string& host,
307 const std::string& endpoint,
308 const std::string& authorization)
309 {
310 // Form the request. We specify the "Connection: close" header so that the
311 // server will close the socket after transmitting the response. This will
312 // allow us to treat all data up until the EOF as the content.
313 std::string raw_request = "";
314 raw_request += "GET " + endpoint + " HTTP/1.1 \r\n";
315 raw_request += "Host: " + host + "\r\n";
316 if (!authorization.empty())
317 {
318 raw_request += "Authorization: " + authorization + "\r\n";
319 }
320 raw_request += "User-Agent: C/1.0\r\n";
321 raw_request += "Connection: close\r\n\r\n";
322
323 return WebRequest(host, raw_request);
324 }
325
327 const WebRequestResponse& response,
328 const std::string& s,
329 const std::vector<std::string>& fields,
330 const unsigned int expected_status
331 )
332 {
333 if (response.status_code != expected_status)
334 {
335 throw std::runtime_error("Status " + std::to_string(response.status_code) + " (" + response.status_message + ") during " + s + ":\n" + response.response.Dump(4));
336 }
337 for (const std::string& f : fields)
338 {
339 if (!response.response.contains(f))
340 {
341 throw std::runtime_error("No " + f + " in " + s + " response");
342 }
343 }
344 return response.response;
345 }
346}
347#endif // USE_ENCRYPTION
#define LOG_ERROR(osstream)
Definition Logger.hpp:45
Main class, basically a JsonVariant with extra utility functions it doesn't inherit JsonVariant direc...
Definition Json.hpp:45
std::string Dump(const int indent=-1, const char indent_char=' ') const
public dump interface
Definition Json.cpp:287
bool contains(const std::string &s) const
Definition Json.cpp:232
WebRequestResponse GETRequest(const std::string &host, const std::string &endpoint, const std::string &authorization="")
Send a GET request with ssl stuff.
Definition Https.cpp:305
ProtocolCraft::Json::Value CheckJsonResponse(const WebRequestResponse &response, const std::string &s, const std::vector< std::string > &fields, const unsigned int expected_status=200)
Check that a WebRequestResponse has the right fields and return code, throw if not.
Definition Https.cpp:326
WebRequestResponse POSTRequest(const std::string &host, const std::string &endpoint, const std::string &content_type, const std::string &accept, const std::string &authorization, const std::string &data)
Send a POST request with ssl stuff.
Definition Https.cpp:279
Value Parse(std::string_view::const_iterator iter, size_t length, bool no_except=false)
Parse a string_view from iter for at most length characters.
Definition Json.cpp:390
ProtocolCraft::Json::Value response
Definition Https.hpp:13