#include #include #include #include #include #include #include #include #include #include #include namespace xrpl { static std::optional httpClientSSLContext; void HTTPClient::initializeSSLContext( std::string const& sslVerifyDir, std::string const& sslVerifyFile, bool sslVerify, beast::Journal j) { httpClientSSLContext.emplace(sslVerifyDir, sslVerifyFile, sslVerify, j); } void HTTPClient::cleanupSSLContext() { httpClientSSLContext.reset(); } //------------------------------------------------------------------------------ // // Fetch a web page via http or https. // //------------------------------------------------------------------------------ class HTTPClientImp : public std::enable_shared_from_this, public HTTPClient { public: HTTPClientImp( boost::asio::io_context& io_context, unsigned short const port, std::size_t maxResponseSize, beast::Journal& j) : mSocket(io_context, httpClientSSLContext->context()) , mResolver(io_context) , mHeader(maxClientHeaderBytes) , mPort(port) , maxResponseSize_(maxResponseSize) , mDeadline(io_context) , j_(j) { } //-------------------------------------------------------------------------- void makeGet( std::string const& strPath, boost::asio::streambuf& sb, std::string const& strHost) { std::ostream osRequest(&sb); osRequest << "GET " << strPath << " HTTP/1.0\r\n" "Host: " << strHost << "\r\n" "Accept: */*\r\n" // YYY Do we need this line? "Connection: close\r\n\r\n"; } //-------------------------------------------------------------------------- void request( bool bSSL, std::deque deqSites, std::function< void(boost::asio::streambuf& sb, std::string const& strHost)> build, std::chrono::seconds timeout, std::function complete) { mSSL = bSSL; mDeqSites = deqSites; mBuild = build; mComplete = complete; mTimeout = timeout; httpsNext(); } //-------------------------------------------------------------------------- void get(bool bSSL, std::deque deqSites, std::string const& strPath, std::chrono::seconds timeout, std::function complete) { mComplete = complete; mTimeout = timeout; request( bSSL, deqSites, std::bind( &HTTPClientImp::makeGet, shared_from_this(), strPath, std::placeholders::_1, std::placeholders::_2), timeout, complete); } //-------------------------------------------------------------------------- void httpsNext() { JLOG(j_.trace()) << "Fetch: " << mDeqSites[0]; auto query = std::make_shared( mDeqSites[0], std::to_string(mPort), boost::asio::ip::resolver_query_base::numeric_service); mQuery = query; try { mDeadline.expires_after(mTimeout); } catch (boost::system::system_error const& e) { mShutdown = e.code(); JLOG(j_.trace()) << "expires_after: " << mShutdown.message(); mDeadline.async_wait(std::bind( &HTTPClientImp::handleDeadline, shared_from_this(), std::placeholders::_1)); } if (!mShutdown) { JLOG(j_.trace()) << "Resolving: " << mDeqSites[0]; mResolver.async_resolve( mQuery->host, mQuery->port, mQuery->flags, std::bind( &HTTPClientImp::handleResolve, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } if (mShutdown) invokeComplete(mShutdown); } void handleDeadline(boost::system::error_code const& ecResult) { if (ecResult == boost::asio::error::operation_aborted) { // Timer canceled because deadline no longer needed. JLOG(j_.trace()) << "Deadline cancelled."; // Aborter is done. } else if (ecResult) { JLOG(j_.trace()) << "Deadline error: " << mDeqSites[0] << ": " << ecResult.message(); // Can't do anything sound. abort(); } else { JLOG(j_.trace()) << "Deadline arrived."; // Mark us as shutting down. // XXX Use our own error code. mShutdown = boost::system::error_code{ boost::system::errc::bad_address, boost::system::system_category()}; // Cancel any resolving. mResolver.cancel(); // Stop the transaction. mSocket.async_shutdown(std::bind( &HTTPClientImp::handleShutdown, shared_from_this(), std::placeholders::_1)); } } void handleShutdown(boost::system::error_code const& ecResult) { if (ecResult) { JLOG(j_.trace()) << "Shutdown error: " << mDeqSites[0] << ": " << ecResult.message(); } } void handleResolve( boost::system::error_code const& ecResult, boost::asio::ip::tcp::resolver::results_type result) { if (!mShutdown) { mShutdown = ecResult ? ecResult : httpClientSSLContext->preConnectVerify( mSocket.SSLSocket(), mDeqSites[0]); } if (mShutdown) { JLOG(j_.trace()) << "Resolve error: " << mDeqSites[0] << ": " << mShutdown.message(); invokeComplete(mShutdown); } else { JLOG(j_.trace()) << "Resolve complete."; boost::asio::async_connect( mSocket.lowest_layer(), result, std::bind( &HTTPClientImp::handleConnect, shared_from_this(), std::placeholders::_1)); } } void handleConnect(boost::system::error_code const& ecResult) { if (!mShutdown) mShutdown = ecResult; if (mShutdown) { JLOG(j_.trace()) << "Connect error: " << mShutdown.message(); } if (!mShutdown) { JLOG(j_.trace()) << "Connected."; mShutdown = httpClientSSLContext->postConnectVerify( mSocket.SSLSocket(), mDeqSites[0]); if (mShutdown) { JLOG(j_.trace()) << "postConnectVerify: " << mDeqSites[0] << ": " << mShutdown.message(); } } if (mShutdown) { invokeComplete(mShutdown); } else if (mSSL) { mSocket.async_handshake( AutoSocket::ssl_socket::client, std::bind( &HTTPClientImp::handleRequest, shared_from_this(), std::placeholders::_1)); } else { handleRequest(ecResult); } } void handleRequest(boost::system::error_code const& ecResult) { if (!mShutdown) mShutdown = ecResult; if (mShutdown) { JLOG(j_.trace()) << "Handshake error:" << mShutdown.message(); invokeComplete(mShutdown); } else { JLOG(j_.trace()) << "Session started."; mBuild(mRequest, mDeqSites[0]); mSocket.async_write( mRequest, std::bind( &HTTPClientImp::handleWrite, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } } void handleWrite( boost::system::error_code const& ecResult, std::size_t bytes_transferred) { if (!mShutdown) mShutdown = ecResult; if (mShutdown) { JLOG(j_.trace()) << "Write error: " << mShutdown.message(); invokeComplete(mShutdown); } else { JLOG(j_.trace()) << "Wrote."; mSocket.async_read_until( mHeader, "\r\n\r\n", std::bind( &HTTPClientImp::handleHeader, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } } void handleHeader( boost::system::error_code const& ecResult, std::size_t bytes_transferred) { std::string strHeader{ {std::istreambuf_iterator(&mHeader)}, std::istreambuf_iterator()}; JLOG(j_.trace()) << "Header: \"" << strHeader << "\""; static boost::regex reStatus{ "\\`HTTP/1\\S+ (\\d{3}) .*\\'"}; // HTTP/1.1 200 OK static boost::regex reSize{ "\\`.*\\r\\nContent-Length:\\s+([0-9]+).*\\'", boost::regex::icase}; static boost::regex reBody{"\\`.*\\r\\n\\r\\n(.*)\\'"}; boost::smatch smMatch; // Match status code. if (!boost::regex_match(strHeader, smMatch, reStatus)) { // XXX Use our own error code. JLOG(j_.trace()) << "No status code"; invokeComplete(boost::system::error_code{ boost::system::errc::bad_address, boost::system::system_category()}); return; } mStatus = beast::lexicalCastThrow(std::string(smMatch[1])); if (boost::regex_match(strHeader, smMatch, reBody)) // we got some body mBody = smMatch[1]; std::size_t const responseSize = [&] { if (boost::regex_match(strHeader, smMatch, reSize)) return beast::lexicalCast( std::string(smMatch[1]), maxResponseSize_); return maxResponseSize_; }(); if (responseSize > maxResponseSize_) { JLOG(j_.trace()) << "Response field too large"; invokeComplete(boost::system::error_code{ boost::system::errc::value_too_large, boost::system::system_category()}); return; } if (responseSize == 0) { // no body wanted or available invokeComplete(ecResult, mStatus); } else if (mBody.size() >= responseSize) { // we got the whole thing invokeComplete(ecResult, mStatus, mBody); } else { mSocket.async_read( mResponse.prepare(responseSize - mBody.size()), boost::asio::transfer_all(), std::bind( &HTTPClientImp::handleData, shared_from_this(), std::placeholders::_1, std::placeholders::_2)); } } void handleData( boost::system::error_code const& ecResult, std::size_t bytes_transferred) { if (!mShutdown) mShutdown = ecResult; if (mShutdown && mShutdown != boost::asio::error::eof) { JLOG(j_.trace()) << "Read error: " << mShutdown.message(); invokeComplete(mShutdown); } else { if (mShutdown) { JLOG(j_.trace()) << "Complete."; } else { mResponse.commit(bytes_transferred); std::string strBody{ {std::istreambuf_iterator(&mResponse)}, std::istreambuf_iterator()}; invokeComplete(ecResult, mStatus, mBody + strBody); } } } // Call cancel the deadline timer and invoke the completion routine. void invokeComplete( boost::system::error_code const& ecResult, int iStatus = 0, std::string const& strData = "") { boost::system::error_code ecCancel; try { mDeadline.cancel(); } catch (boost::system::system_error const& e) { JLOG(j_.trace()) << "invokeComplete: Deadline cancel error: " << e.what(); ecCancel = e.code(); } JLOG(j_.debug()) << "invokeComplete: Deadline popping: " << mDeqSites.size(); if (!mDeqSites.empty()) { mDeqSites.pop_front(); } bool bAgain = true; if (mDeqSites.empty() || !ecResult) { // ecResult: !0 = had an error, last entry // iStatus: result, if no error // strData: data, if no error bAgain = mComplete && mComplete(ecResult ? ecResult : ecCancel, iStatus, strData); } if (!mDeqSites.empty() && bAgain) { httpsNext(); } } private: using pointer = std::shared_ptr; bool mSSL; AutoSocket mSocket; boost::asio::ip::tcp::resolver mResolver; struct Query { std::string host; std::string port; boost::asio::ip::resolver_query_base::flags flags; }; std::shared_ptr mQuery; boost::asio::streambuf mRequest; boost::asio::streambuf mHeader; boost::asio::streambuf mResponse; std::string mBody; unsigned short const mPort; std::size_t const maxResponseSize_; int mStatus; std::function mBuild; std::function mComplete; boost::asio::basic_waitable_timer mDeadline; // If not success, we are shutting down. boost::system::error_code mShutdown; std::deque mDeqSites; std::chrono::seconds mTimeout; beast::Journal j_; }; //------------------------------------------------------------------------------ void HTTPClient::get( bool bSSL, boost::asio::io_context& io_context, std::deque deqSites, unsigned short const port, std::string const& strPath, std::size_t responseMax, std::chrono::seconds timeout, std::function complete, beast::Journal& j) { auto client = std::make_shared(io_context, port, responseMax, j); client->get(bSSL, deqSites, strPath, timeout, complete); } void HTTPClient::get( bool bSSL, boost::asio::io_context& io_context, std::string strSite, unsigned short const port, std::string const& strPath, std::size_t responseMax, std::chrono::seconds timeout, std::function complete, beast::Journal& j) { std::deque deqSites(1, strSite); auto client = std::make_shared(io_context, port, responseMax, j); client->get(bSSL, deqSites, strPath, timeout, complete); } void HTTPClient::request( bool bSSL, boost::asio::io_context& io_context, std::string strSite, unsigned short const port, std::function setRequest, std::size_t responseMax, std::chrono::seconds timeout, std::function complete, beast::Journal& j) { std::deque deqSites(1, strSite); auto client = std::make_shared(io_context, port, responseMax, j); client->request(bSSL, deqSites, setRequest, timeout, complete); } } // namespace xrpl