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 std::bind(
140 strPath,
141 std::placeholders::_1,
142 std::placeholders::_2),
143 timeout,
144 complete);
145 }
146
147 //--------------------------------------------------------------------------
148
149 void
151 {
152 JLOG(j_.trace()) << "Fetch: " << deqSites_[0];
153
154 auto query = std::make_shared<Query>(
155 deqSites_[0],
157 boost::asio::ip::resolver_query_base::numeric_service);
158 query_ = query;
159
160 try
161 {
162 deadline_.expires_after(timeout_);
163 }
164 catch (boost::system::system_error const& e)
165 {
166 shutdown_ = e.code();
167
168 JLOG(j_.trace()) << "expires_after: " << shutdown_.message();
169 deadline_.async_wait(
170 std::bind(
171 &HTTPClientImp::handleDeadline, shared_from_this(), std::placeholders::_1));
172 }
173
174 if (!shutdown_)
175 {
176 JLOG(j_.trace()) << "Resolving: " << deqSites_[0];
177
178 resolver_.async_resolve(
179 query_->host,
180 query_->port,
181 query_->flags,
182 std::bind(
185 std::placeholders::_1,
186 std::placeholders::_2));
187 }
188
189 if (shutdown_)
191 }
192
193 void
194 handleDeadline(boost::system::error_code const& ecResult)
195 {
196 if (ecResult == boost::asio::error::operation_aborted)
197 {
198 // Timer canceled because deadline no longer needed.
199 JLOG(j_.trace()) << "Deadline cancelled.";
200
201 // Aborter is done.
202 }
203 else if (ecResult)
204 {
205 JLOG(j_.trace()) << "Deadline error: " << deqSites_[0] << ": " << ecResult.message();
206
207 // Can't do anything sound.
208 std::abort();
209 }
210 else
211 {
212 JLOG(j_.trace()) << "Deadline arrived.";
213
214 // Mark us as shutting down.
215 // XXX Use our own error code.
216 shutdown_ = boost::system::error_code{
217 boost::system::errc::bad_address, boost::system::system_category()};
218
219 // Cancel any resolving.
220 resolver_.cancel();
221
222 // Stop the transaction.
223 socket_.asyncShutdown(
224 std::bind(
225 &HTTPClientImp::handleShutdown, shared_from_this(), std::placeholders::_1));
226 }
227 }
228
229 void
230 handleShutdown(boost::system::error_code const& ecResult)
231 {
232 if (ecResult)
233 {
234 JLOG(j_.trace()) << "Shutdown error: " << deqSites_[0] << ": " << ecResult.message();
235 }
236 }
237
238 void
240 boost::system::error_code const& ecResult,
241 boost::asio::ip::tcp::resolver::results_type result)
242 {
243 if (!shutdown_)
244 {
245 shutdown_ = ecResult
246 ? ecResult
247 // gHttpClientSslContext always initialized before use
248 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
249 : gHttpClientSslContext->preConnectVerify(socket_.sslSocket(), deqSites_[0]);
250 }
251
252 if (shutdown_)
253 {
254 JLOG(j_.trace()) << "Resolve error: " << deqSites_[0] << ": " << shutdown_.message();
255
257 }
258 else
259 {
260 JLOG(j_.trace()) << "Resolve complete.";
261
262 boost::asio::async_connect(
263 socket_.lowestLayer(),
264 result,
265 std::bind(
266 &HTTPClientImp::handleConnect, shared_from_this(), std::placeholders::_1));
267 }
268 }
269
270 void
271 handleConnect(boost::system::error_code const& ecResult)
272 {
273 if (!shutdown_)
274 shutdown_ = ecResult;
275
276 if (shutdown_)
277 {
278 JLOG(j_.trace()) << "Connect error: " << shutdown_.message();
279 }
280
281 if (!shutdown_)
282 {
283 JLOG(j_.trace()) << "Connected.";
284
285 // gHttpClientSslContext always initialized before use
286 // NOLINTNEXTLINE(bugprone-unchecked-optional-access)
287 shutdown_ = gHttpClientSslContext->postConnectVerify(socket_.sslSocket(), deqSites_[0]);
288
289 if (shutdown_)
290 {
291 JLOG(j_.trace()) << "postConnectVerify: " << deqSites_[0] << ": "
292 << shutdown_.message();
293 }
294 }
295
296 if (shutdown_)
297 {
299 }
300 else if (ssl_)
301 {
302 socket_.asyncHandshake(
303 AutoSocket::ssl_socket::client,
304 std::bind(
305 &HTTPClientImp::handleRequest, shared_from_this(), std::placeholders::_1));
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 std::bind(
336 std::placeholders::_1,
337 std::placeholders::_2));
338 }
339 }
340
341 void
342 handleWrite(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
343 {
344 if (!shutdown_)
345 shutdown_ = ecResult;
346
347 if (shutdown_)
348 {
349 JLOG(j_.trace()) << "Write error: " << shutdown_.message();
350
352 }
353 else
354 {
355 JLOG(j_.trace()) << "Wrote.";
356
357 socket_.asyncReadUntil(
358 header_,
359 "\r\n\r\n",
360 std::bind(
363 std::placeholders::_1,
364 std::placeholders::_2));
365 }
366 }
367
368 void
369 handleHeader(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
370 {
371 std::string strHeader{
373 JLOG(j_.trace()) << "Header: \"" << strHeader << "\"";
374
375 static boost::regex const kReStatus{"\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK
376 static boost::regex const kReSize{
377 "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase};
378 static boost::regex const kReBody{"\\`.*\\r\\n\\r\\n(.*)\\'"};
379
380 boost::smatch smMatch;
381 // Match status code.
382 if (!boost::regex_match(strHeader, smMatch, kReStatus))
383 {
384 // XXX Use our own error code.
385 JLOG(j_.trace()) << "No status code";
387 boost::system::error_code{
388 boost::system::errc::bad_address, boost::system::system_category()});
389 return;
390 }
391
393
394 if (boost::regex_match(strHeader, smMatch, kReBody)) // we got some body
395 body_ = smMatch[1];
396
397 std::size_t const responseSize = [&] {
398 if (boost::regex_match(strHeader, smMatch, kReSize))
400 return maxResponseSize_;
401 }();
402
403 if (responseSize > maxResponseSize_)
404 {
405 JLOG(j_.trace()) << "Response field too large";
407 boost::system::error_code{
408 boost::system::errc::value_too_large, boost::system::system_category()});
409 return;
410 }
411
412 if (responseSize == 0)
413 {
414 // no body wanted or available
415 invokeComplete(ecResult, status_);
416 }
417 else if (body_.size() >= responseSize)
418 {
419 // we got the whole thing
420 invokeComplete(ecResult, status_, body_);
421 }
422 else
423 {
424 socket_.asyncRead(
425 response_.prepare(responseSize - body_.size()),
426 boost::asio::transfer_all(),
427 std::bind(
430 std::placeholders::_1,
431 std::placeholders::_2));
432 }
433 }
434
435 void
436 handleData(boost::system::error_code const& ecResult, std::size_t bytesTransferred)
437 {
438 if (!shutdown_)
439 shutdown_ = ecResult;
440
441 if (shutdown_ && shutdown_ != boost::asio::error::eof)
442 {
443 JLOG(j_.trace()) << "Read error: " << shutdown_.message();
444
446 }
447 else
448 {
449 if (shutdown_)
450 {
451 JLOG(j_.trace()) << "Complete.";
452 }
453 else
454 {
455 response_.commit(bytesTransferred);
456 std::string const strBody{
458 invokeComplete(ecResult, status_, body_ + strBody);
459 }
460 }
461 }
462
463 // Call cancel the deadline timer and invoke the completion routine.
464 void
466 boost::system::error_code const& ecResult,
467 int iStatus = 0,
468 std::string const& strData = "")
469 {
470 boost::system::error_code ecCancel;
471 try
472 {
473 deadline_.cancel();
474 }
475 catch (boost::system::system_error const& e)
476 {
477 JLOG(j_.trace()) << "invokeComplete: Deadline cancel error: " << e.what();
478 ecCancel = e.code();
479 }
480
481 JLOG(j_.debug()) << "invokeComplete: Deadline popping: " << deqSites_.size();
482
483 if (!deqSites_.empty())
484 {
485 deqSites_.pop_front();
486 }
487
488 bool bAgain = true;
489
490 if (deqSites_.empty() || !ecResult)
491 {
492 // ecResult: !0 = had an error, last entry
493 // iStatus: result, if no error
494 // strData: data, if no error
495 bAgain = complete_ && complete_(ecResult ? ecResult : ecCancel, iStatus, strData);
496 }
497
498 if (!deqSites_.empty() && bAgain)
499 {
500 httpsNext();
501 }
502 }
503
504private:
506
507 bool ssl_{};
509 boost::asio::ip::tcp::resolver resolver_;
510
511 struct Query
512 {
515 boost::asio::ip::resolver_query_base::flags flags;
516 };
518
519 boost::asio::streambuf request_;
520 boost::asio::streambuf header_;
521 boost::asio::streambuf response_;
523 unsigned short const port_;
525 int status_{};
526 std::function<void(boost::asio::streambuf& sb, std::string const& strHost)> build_;
528 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
530
531 boost::asio::basic_waitable_timer<std::chrono::steady_clock> deadline_;
532
533 // If not success, we are shutting down.
534 boost::system::error_code shutdown_;
535
539};
540
541//------------------------------------------------------------------------------
542
543void
545 bool bSSL,
546 boost::asio::io_context& ioContext,
548 unsigned short const port,
549 std::string const& strPath,
550 std::size_t responseMax,
551 std::chrono::seconds timeout,
553 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
554 complete,
555 beast::Journal const& j)
556{
557 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
558 client->get(bSSL, deqSites, strPath, timeout, complete);
559}
560
561void
563 bool bSSL,
564 boost::asio::io_context& ioContext,
565 std::string strSite,
566 unsigned short const port,
567 std::string const& strPath,
568 std::size_t responseMax,
569 std::chrono::seconds timeout,
571 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
572 complete,
573 beast::Journal const& j)
574{
575 std::deque<std::string> const deqSites(1, strSite);
576
577 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
578 client->get(bSSL, deqSites, strPath, timeout, complete);
579}
580
581void
583 bool bSSL,
584 boost::asio::io_context& ioContext,
585 std::string strSite,
586 unsigned short const port,
587 std::function<void(boost::asio::streambuf& sb, std::string const& strHost)> setRequest,
588 std::size_t responseMax,
589 std::chrono::seconds timeout,
591 bool(boost::system::error_code const& ecResult, int iStatus, std::string const& strData)>
592 complete,
593 beast::Journal const& j)
594{
595 std::deque<std::string> const deqSites(1, strSite);
596
597 auto client = std::make_shared<HTTPClientImp>(ioContext, port, responseMax, j);
598 client->request(bSSL, deqSites, setRequest, timeout, complete);
599}
600
601} // namespace xrpl
T abort(T... args)
T bind(T... args)
A generic endpoint for log messages.
Definition Journal.h:38
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:23
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)
Out lexicalCastThrow(In in)
Convert from one type to another, throw on error.
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)