xrpld
Loading...
Searching...
No Matches
libxrpl/net/HTTPClient.cpp
1#include <xrpl/net/HTTPClient.h>
2
3#include <xrpl/basics/Log.h>
4#include <xrpl/beast/core/LexicalCast.h>
5#include <xrpl/beast/utility/Journal.h>
6#include <xrpl/net/AutoSocket.h>
7#include <xrpl/net/HTTPClientSSLContext.h>
8
9#include <boost/asio/basic_waitable_timer.hpp>
10#include <boost/asio/completion_condition.hpp>
11#include <boost/asio/connect.hpp>
12#include <boost/asio/error.hpp>
13#include <boost/asio/io_context.hpp>
14#include <boost/asio/ip/resolver_query_base.hpp>
15#include <boost/asio/ip/tcp.hpp>
16#include <boost/regex/v5/regex.hpp>
17#include <boost/regex/v5/regex_match.hpp>
18#include <boost/system/detail/errc.hpp>
19#include <boost/system/detail/error_code.hpp>
20#include <boost/system/detail/system_category.hpp>
21#include <boost/system/system_error.hpp>
22
23#include <chrono>
24#include <cstddef>
25#include <cstdlib>
26#include <deque>
27#include <functional>
28#include <iterator>
29#include <memory>
30#include <optional>
31#include <ostream>
32#include <string>
33
34namespace xrpl {
35
37
38void
40 std::string const& sslVerifyDir,
41 std::string const& sslVerifyFile,
42 bool sslVerify,
44{
45 gHttpClientSslContext.emplace(sslVerifyDir, sslVerifyFile, sslVerify, j);
46}
47
48void
53
54//------------------------------------------------------------------------------
55//
56// Fetch a web page via http or https.
57//
58//------------------------------------------------------------------------------
59
60class HTTPClientImp : public std::enable_shared_from_this<HTTPClientImp>, public HTTPClient
61{
62public:
64 boost::asio::io_context& ioContext,
65 unsigned short const port,
66 std::size_t maxResponseSize,
67 beast::Journal const& j)
68 : socket_(
69 ioContext,
70 gHttpClientSslContext->context()) // NOLINT(bugprone-unchecked-optional-access)
71 , resolver_(ioContext)
73 , port_(port)
74 , maxResponseSize_(maxResponseSize)
75 , deadline_(ioContext)
76 , j_(j)
77 {
78 }
79
80 //--------------------------------------------------------------------------
81
82 void
83 // NOLINTNEXTLINE(readability-convert-member-functions-to-static)
84 makeGet(std::string const& strPath, boost::asio::streambuf& sb, std::string const& strHost)
85 {
86 std::ostream osRequest(&sb);
87
88 osRequest << "GET " << strPath
89 << " HTTP/1.0\r\n"
90 "Host: "
91 << strHost
92 << "\r\n"
93 "Accept: */*\r\n" // YYY Do we need this line?
94 "Connection: close\r\n\r\n";
95 }
96
97 //--------------------------------------------------------------------------
98
99 void
101 bool bSSL,
103 std::function<void(boost::asio::streambuf& sb, std::string const& strHost)> build,
104 std::chrono::seconds timeout,
105 std::function<bool(
106 boost::system::error_code const& ecResult,
107 int iStatus,
108 std::string const& strData)> complete)
109 {
110 ssl_ = bSSL;
111 deqSites_ = deqSites;
112 build_ = build;
113 complete_ = complete;
114 timeout_ = timeout;
115
116 httpsNext();
117 }
118
119 //--------------------------------------------------------------------------
120
121 void
122 get(bool bSSL,
124 std::string const& strPath,
125 std::chrono::seconds timeout,
126 std::function<bool(
127 boost::system::error_code const& ecResult,
128 int iStatus,
129 std::string const& strData)> complete)
130 {
131 complete_ = complete;
132 timeout_ = timeout;
133
134 request(
135 bSSL,
136 deqSites,
137 [self = shared_from_this(), strPath](
138 boost::asio::streambuf& sb, std::string const& strHost) {
139 self->makeGet(strPath, sb, strHost);
140 },
141 timeout,
142 complete);
143 }
144
145 //--------------------------------------------------------------------------
146
147 void
149 {
150 JLOG(j_.trace()) << "Fetch: " << deqSites_[0];
151
152 auto query = std::make_shared<Query>(
153 deqSites_[0],
155 boost::asio::ip::resolver_query_base::numeric_service);
156 query_ = query;
157
158 try
159 {
160 deadline_.expires_after(timeout_);
161 }
162 catch (boost::system::system_error const& e)
163 {
164 shutdown_ = e.code();
165
166 JLOG(j_.trace()) << "expires_after: " << shutdown_.message();
167 deadline_.async_wait([self = shared_from_this()](boost::system::error_code const& ec) {
168 self->handleDeadline(ec);
169 });
170 }
171
172 if (!shutdown_)
173 {
174 JLOG(j_.trace()) << "Resolving: " << deqSites_[0];
175
176 resolver_.async_resolve(
177 query_->host,
178 query_->port,
179 query_->flags,
180 [self = shared_from_this()](
181 boost::system::error_code const& ecResult,
182 boost::asio::ip::tcp::resolver::results_type results) {
183 self->handleResolve(ecResult, results);
184 });
185 }
186
187 if (shutdown_)
189 }
190
191 void
192 handleDeadline(boost::system::error_code const& ecResult)
193 {
194 if (ecResult == boost::asio::error::operation_aborted)
195 {
196 // Timer canceled because deadline no longer needed.
197 JLOG(j_.trace()) << "Deadline cancelled.";
198
199 // Aborter is done.
200 }
201 else if (ecResult)
202 {
203 JLOG(j_.trace()) << "Deadline error: " << deqSites_[0] << ": " << ecResult.message();
204
205 // Can't do anything sound.
206 std::abort();
207 }
208 else
209 {
210 JLOG(j_.trace()) << "Deadline arrived.";
211
212 // Mark us as shutting down.
213 // XXX Use our own error code.
214 shutdown_ = boost::system::error_code{
215 boost::system::errc::bad_address, boost::system::system_category()};
216
217 // Cancel any resolving.
218 resolver_.cancel();
219
220 // Stop the transaction.
221 socket_.asyncShutdown([self = shared_from_this()](boost::system::error_code const& ec) {
222 self->handleShutdown(ec);
223 });
224 }
225 }
226
227 void
228 handleShutdown(boost::system::error_code const& ecResult)
229 {
230 if (ecResult)
231 {
232 JLOG(j_.trace()) << "Shutdown error: " << deqSites_[0] << ": " << ecResult.message();
233 }
234 }
235
236 void
238 boost::system::error_code const& ecResult,
239 boost::asio::ip::tcp::resolver::results_type result)
240 {
241 if (!shutdown_)
242 {
243 shutdown_ = ecResult
244 ? ecResult
245 // gHttpClientSslContext always initialized before use
246 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
247 : gHttpClientSslContext->preConnectVerify(socket_.sslSocket(), deqSites_[0]);
248 }
249
250 if (shutdown_)
251 {
252 JLOG(j_.trace()) << "Resolve error: " << deqSites_[0] << ": " << shutdown_.message();
253
255 }
256 else
257 {
258 JLOG(j_.trace()) << "Resolve complete.";
259
260 boost::asio::async_connect(
261 socket_.lowestLayer(),
262 result,
263 [self = shared_from_this()](
264 boost::system::error_code const& ecResult,
265 boost::asio::ip::tcp::endpoint const&) { self->handleConnect(ecResult); });
266 }
267 }
268
269 void
270 handleConnect(boost::system::error_code const& ecResult)
271 {
272 if (!shutdown_)
273 shutdown_ = ecResult;
274
275 if (shutdown_)
276 {
277 JLOG(j_.trace()) << "Connect error: " << shutdown_.message();
278 }
279
280 if (!shutdown_)
281 {
282 JLOG(j_.trace()) << "Connected.";
283
284 // gHttpClientSslContext always initialized before use
285 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
286 shutdown_ = gHttpClientSslContext->postConnectVerify(socket_.sslSocket(), deqSites_[0]);
287
288 if (shutdown_)
289 {
290 JLOG(j_.trace()) << "postConnectVerify: " << deqSites_[0] << ": "
291 << shutdown_.message();
292 }
293 }
294
295 if (shutdown_)
296 {
298 }
299 else if (ssl_)
300 {
301 socket_.asyncHandshake(
302 AutoSocket::ssl_socket::client,
303 [self = shared_from_this()](boost::system::error_code const& ec) {
304 self->handleRequest(ec);
305 });
306 }
307 else
308 {
309 handleRequest(ecResult);
310 }
311 }
312
313 void
314 handleRequest(boost::system::error_code const& ecResult)
315 {
316 if (!shutdown_)
317 shutdown_ = ecResult;
318
319 if (shutdown_)
320 {
321 JLOG(j_.trace()) << "Handshake error:" << shutdown_.message();
322
324 }
325 else
326 {
327 JLOG(j_.trace()) << "Session started.";
328
330
331 socket_.asyncWrite(
332 request_,
333 [self = shared_from_this()](
334 boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
335 self->handleWrite(ecResult, bytesTransferred);
336 });
337 }
338 }
339
340 void
341 handleWrite(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
342 {
343 if (!shutdown_)
344 shutdown_ = ecResult;
345
346 if (shutdown_)
347 {
348 JLOG(j_.trace()) << "Write error: " << shutdown_.message();
349
351 }
352 else
353 {
354 JLOG(j_.trace()) << "Wrote.";
355
356 socket_.asyncReadUntil(
357 header_,
358 "\r\n\r\n",
359 [self = shared_from_this()](
360 boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
361 self->handleHeader(ecResult, bytesTransferred);
362 });
363 }
364 }
365
366 void
367 handleHeader(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
368 {
369 std::string strHeader{
371 JLOG(j_.trace()) << "Header: \"" << strHeader << "\"";
372
373 static boost::regex const kReStatus{R"(\`HTTP/1\S+ (\d{3}) .*\')"}; // HTTP/1.1 200 OK
374 static boost::regex const kReSize{
375 R"(\`.*\r\nContent-Length:\s+([0-9]+).*\')", boost::regex::icase};
376 static boost::regex const kReBody{R"(\`.*\r\n\r\n(.*)\')"};
377
378 boost::smatch smMatch;
379 // Match status code.
380 if (!boost::regex_match(strHeader, smMatch, kReStatus))
381 {
382 // XXX Use our own error code.
383 JLOG(j_.trace()) << "No status code";
385 boost::system::error_code{
386 boost::system::errc::bad_address, boost::system::system_category()});
387 return;
388 }
389
391
392 if (boost::regex_match(strHeader, smMatch, kReBody)) // we got some body
393 body_ = smMatch[1];
394
395 std::size_t const responseSize = [&] {
396 if (boost::regex_match(strHeader, smMatch, kReSize))
398 return maxResponseSize_;
399 }();
400
401 if (responseSize > maxResponseSize_)
402 {
403 JLOG(j_.trace()) << "Response field too large";
405 boost::system::error_code{
406 boost::system::errc::value_too_large, boost::system::system_category()});
407 return;
408 }
409
410 if (responseSize == 0)
411 {
412 // no body wanted or available
413 invokeComplete(ecResult, status_);
414 }
415 else if (body_.size() >= responseSize)
416 {
417 // we got the whole thing
418 invokeComplete(ecResult, status_, body_);
419 }
420 else
421 {
422 socket_.asyncRead(
423 response_.prepare(responseSize - body_.size()),
424 boost::asio::transfer_all(),
425 [self = shared_from_this()](
426 boost::system::error_code const& ecResult, std::size_t bytesTransferred) {
427 self->handleData(ecResult, bytesTransferred);
428 });
429 }
430 }
431
432 void
433 handleData(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
434 {
435 if (!shutdown_)
436 shutdown_ = ecResult;
437
438 if (shutdown_ && shutdown_ != boost::asio::error::eof)
439 {
440 JLOG(j_.trace()) << "Read error: " << shutdown_.message();
441
443 }
444 else
445 {
446 if (shutdown_)
447 {
448 JLOG(j_.trace()) << "Complete.";
449 }
450 else
451 {
452 response_.commit(bytesTransferred);
453 std::string const strBody{
455 invokeComplete(ecResult, status_, body_ + strBody);
456 }
457 }
458 }
459
460 // Call cancel the deadline timer and invoke the completion routine.
461 void
463 boost::system::error_code const& ecResult,
464 int iStatus = 0,
465 std::string const& strData = "")
466 {
467 boost::system::error_code ecCancel;
468 try
469 {
470 deadline_.cancel();
471 }
472 catch (boost::system::system_error const& e)
473 {
474 JLOG(j_.trace()) << "invokeComplete: Deadline cancel error: " << e.what();
475 ecCancel = e.code();
476 }
477
478 JLOG(j_.debug()) << "invokeComplete: Deadline popping: " << deqSites_.size();
479
480 if (!deqSites_.empty())
481 {
482 deqSites_.pop_front();
483 }
484
485 bool bAgain = true;
486
487 if (deqSites_.empty() || !ecResult)
488 {
489 // ecResult: !0 = had an error, last entry
490 // iStatus: result, if no error
491 // strData: data, if no error
492 bAgain = complete_ && complete_(ecResult ? ecResult : ecCancel, iStatus, strData);
493 }
494
495 if (!deqSites_.empty() && bAgain)
496 {
497 httpsNext();
498 }
499 }
500
501private:
503
504 bool ssl_{};
506 boost::asio::ip::tcp::resolver resolver_;
507
508 struct Query
509 {
512 boost::asio::ip::resolver_query_base::flags flags;
513 };
515
516 boost::asio::streambuf request_;
517 boost::asio::streambuf header_;
518 boost::asio::streambuf response_;
520 unsigned short const port_;
522 int status_{};
523 std::function<void(boost::asio::streambuf& sb, std::string const& strHost)> build_;
525 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
527
528 boost::asio::basic_waitable_timer<std::chrono::steady_clock> deadline_;
529
530 // If not success, we are shutting down.
531 boost::system::error_code shutdown_;
532
536};
537
538//------------------------------------------------------------------------------
539
540void
542 bool bSSL,
543 boost::asio::io_context& ioContext,
545 unsigned short const port,
546 std::string const& strPath,
547 std::size_t responseMax,
548 std::chrono::seconds timeout,
550 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
551 complete,
552 beast::Journal const& j)
553{
554 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
555 client->get(bSSL, deqSites, strPath, timeout, complete);
556}
557
558void
560 bool bSSL,
561 boost::asio::io_context& ioContext,
562 std::string strSite,
563 unsigned short const port,
564 std::string const& strPath,
565 std::size_t responseMax,
566 std::chrono::seconds timeout,
568 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
569 complete,
570 beast::Journal const& j)
571{
572 std::deque<std::string> const deqSites(1, strSite);
573
574 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
575 client->get(bSSL, deqSites, strPath, timeout, complete);
576}
577
578void
580 bool bSSL,
581 boost::asio::io_context& ioContext,
582 std::string strSite,
583 unsigned short const port,
584 std::function<void(boost::asio::streambuf& sb, std::string const& strHost)> setRequest,
585 std::size_t responseMax,
586 std::chrono::seconds timeout,
588 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
589 complete,
590 beast::Journal const& j)
591{
592 std::deque<std::string> const deqSites(1, strSite);
593
594 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
595 client->request(bSSL, deqSites, setRequest, timeout, complete);
596}
597
598} // namespace xrpl
T abort(T... args)
A generic endpoint for log messages.
Definition Journal.h:44
boost::asio::streambuf request_
void handleData(boost::system::error_code const &ecResult, std::size_t bytesTransferred)
void handleDeadline(boost::system::error_code const &ecResult)
boost::asio::streambuf response_
std::size_t const maxResponseSize_
std::chrono::seconds timeout_
void request(bool bSSL, std::deque< std::string > deqSites, std::function< void(boost::asio::streambuf &sb, std::string const &strHost)> build, std::chrono::seconds timeout, std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete)
void handleConnect(boost::system::error_code const &ecResult)
void handleHeader(boost::system::error_code const &ecResult, std::size_t bytesTransferred)
boost::system::error_code shutdown_
HTTPClientImp(boost::asio::io_context &ioContext, unsigned short const port, std::size_t maxResponseSize, beast::Journal const &j)
boost::asio::basic_waitable_timer< std::chrono::steady_clock > deadline_
std::shared_ptr< Query > query_
void makeGet(std::string const &strPath, boost::asio::streambuf &sb, std::string const &strHost)
void handleResolve(boost::system::error_code const &ecResult, boost::asio::ip::tcp::resolver::results_type result)
void handleRequest(boost::system::error_code const &ecResult)
void get(bool bSSL, std::deque< std::string > deqSites, std::string const &strPath, std::chrono::seconds timeout, std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete)
boost::asio::ip::tcp::resolver resolver_
boost::asio::streambuf header_
std::deque< std::string > deqSites_
std::shared_ptr< HTTPClient > pointer
void invokeComplete(boost::system::error_code const &ecResult, int iStatus=0, std::string const &strData="")
void handleWrite(boost::system::error_code const &ecResult, std::size_t bytesTransferred)
std::function< void(boost::asio::streambuf &sb, std::string const &strHost)> build_
std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete_
void handleShutdown(boost::system::error_code const &ecResult)
static void initializeSSLContext(std::string const &sslVerifyDir, std::string const &sslVerifyFile, bool sslVerify, beast::Journal j)
static constexpr auto kMaxClientHeaderBytes
Definition HTTPClient.h:25
static void cleanupSSLContext()
Destroys the global SSL context created by initializeSSLContext().
static void request(bool bSSL, boost::asio::io_context &ioContext, std::string strSite, unsigned short const port, std::function< void(boost::asio::streambuf &sb, std::string const &strHost)> build, std::size_t responseMax, std::chrono::seconds timeout, std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete, beast::Journal const &j)
static void get(bool bSSL, boost::asio::io_context &ioContext, std::deque< std::string > deqSites, unsigned short const port, std::string const &strPath, std::size_t responseMax, std::chrono::seconds timeout, std::function< bool(boost::system::error_code const &ecResult, int iStatus, std::string const &strData)> complete, beast::Journal const &j)
HTTPClient()=default
T make_shared(T... args)
constexpr Out lexicalCastThrow(In in)
Convert from one type to another, throw on error.
constexpr Out lexicalCast(In in, Out defaultValue=Out())
Convert from one type to another.
Use hash_* containers for keys that do not need a cryptographically secure hashing algorithm.
Definition algorithm.h:5
static std::optional< HTTPClientSSLContext > gHttpClientSslContext
boost::asio::ip::resolver_query_base::flags flags
T to_string(T... args)