#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if !BOOST_OS_WINDOWS #include #include #endif #include #include #include #include #include #include #include #include namespace xrpl { /** A listening socket. */ template class Door : public IOList::Work, public std::enable_shared_from_this> { private: using clock_type = std::chrono::steady_clock; using timer_type = boost::asio::basic_waitable_timer; using error_code = boost::system::error_code; using yield_context = boost::asio::yield_context; using protocol_type = boost::asio::ip::tcp; using acceptor_type = protocol_type::acceptor; using endpoint_type = protocol_type::endpoint; using socket_type = boost::asio::ip::tcp::socket; using stream_type = boost::beast::tcp_stream; // Detects SSL on a socket class Detector : public IOList::Work, public std::enable_shared_from_this { private: Port const& port_; Handler& handler_; boost::asio::io_context& ioc_; stream_type stream_; socket_type& socket_; endpoint_type remoteAddress_; boost::asio::strand strand_; beast::Journal const j_; public: Detector( Port const& port, Handler& handler, boost::asio::io_context& ioc, stream_type&& stream, endpoint_type remoteAddress, beast::Journal j); void run(); void close() override; private: void doDetect(yield_context yield); }; beast::Journal const j_; Port const& port_; Handler& handler_; boost::asio::io_context& ioc_; acceptor_type acceptor_; boost::asio::strand strand_; bool ssl_{ port_.protocol.contains("https") || port_.protocol.contains("wss") || port_.protocol.contains("wss2") || port_.protocol.contains("peer")}; bool plain_{ port_.protocol.contains("http") || port_.protocol.contains("ws") || (port_.protocol.contains("ws2"))}; static constexpr std::chrono::milliseconds kInitialAcceptDelay{50}; static constexpr std::chrono::milliseconds kMaxAcceptDelay{2000}; std::chrono::milliseconds acceptDelay_{kInitialAcceptDelay}; boost::asio::steady_timer backoffTimer_; static constexpr std::uint64_t kMaxUsedFdPercent = 70; static constexpr std::chrono::milliseconds kFdSampleInterval{250}; clock_type::time_point fdSampleAt_; bool cachedThrottle_{false}; struct FDStats { std::uint64_t used{0}; std::uint64_t limit{0}; }; void reOpen(); std::optional queryFdStats() const; bool shouldThrottleForFds(); public: Door(Handler& handler, boost::asio::io_context& ioContext, Port const& port, beast::Journal j); // Work-around because we can't call shared_from_this in ctor void run(); /** Close the Door listening socket and connections. The listening socket is closed, and all open connections belonging to the Door are closed. Thread Safety: May be called concurrently */ void close() override; [[nodiscard]] endpoint_type getEndpoint() const { return acceptor_.local_endpoint(); } private: template void create( bool ssl, ConstBufferSequence const& buffers, stream_type&& stream, endpoint_type remoteAddress); void doAccept(yield_context yield); }; template Door::Detector::Detector( Port const& port, Handler& handler, boost::asio::io_context& ioc, stream_type&& stream, endpoint_type remoteAddress, beast::Journal j) : port_(port) , handler_(handler) , ioc_(ioc) , stream_(std::move(stream)) , socket_(stream_.socket()) , remoteAddress_(std::move(remoteAddress)) , strand_(boost::asio::make_strand(ioc_)) , j_(j) { } template void Door::Detector::run() { util::spawn( strand_, std::bind(&Detector::doDetect, this->shared_from_this(), std::placeholders::_1)); } template void Door::Detector::close() { stream_.close(); } template void Door::Detector::doDetect(boost::asio::yield_context doYield) { boost::beast::multi_buffer buf(16); stream_.expires_after(std::chrono::seconds(15)); boost::system::error_code ec; bool const ssl = async_detect_ssl(stream_, buf, doYield[ec]); stream_.expires_never(); if (!ec) { if (ssl) { if (auto sp = ios().template emplace>( port_, handler_, ioc_, j_, remoteAddress_, buf.data(), std::move(stream_))) sp->run(); return; } if (auto sp = ios().template emplace>( port_, handler_, ioc_, j_, remoteAddress_, buf.data(), std::move(stream_))) sp->run(); return; } if (ec != boost::asio::error::operation_aborted) { JLOG(j_.trace()) << "Error detecting ssl: " << ec.message() << " from " << remoteAddress_; } } //------------------------------------------------------------------------------ template void Door::reOpen() { error_code ec; if (acceptor_.is_open()) { acceptor_.close(ec); if (ec) { std::stringstream ss; ss << "Can't close acceptor: " << port_.name << ", " << ec.message(); JLOG(j_.error()) << ss.str(); Throw(ss.str()); } } endpoint_type const localAddress = endpoint_type(port_.ip, port_.port); acceptor_.open(localAddress.protocol(), ec); if (ec) { JLOG(j_.error()) << "Open port '" << port_.name << "' failed:" << ec.message(); Throw(); } acceptor_.set_option(boost::asio::ip::tcp::acceptor::reuse_address(true), ec); if (ec) { JLOG(j_.error()) << "Option for port '" << port_.name << "' failed:" << ec.message(); Throw(); } acceptor_.bind(localAddress, ec); if (ec) { JLOG(j_.error()) << "Bind port '" << port_.name << "' failed:" << ec.message(); Throw(); } acceptor_.listen(boost::asio::socket_base::max_listen_connections, ec); if (ec) { JLOG(j_.error()) << "Listen on port '" << port_.name << "' failed:" << ec.message(); Throw(); } JLOG(j_.info()) << "Opened " << port_; } template Door::Door( Handler& handler, boost::asio::io_context& ioContext, Port const& port, beast::Journal j) : j_(j) , port_(port) , handler_(handler) , ioc_(ioContext) , acceptor_(ioContext) , strand_(boost::asio::make_strand(ioContext)) , backoffTimer_(ioContext) , fdSampleAt_(clock_type::now() - kFdSampleInterval) { reOpen(); } template void Door::run() { util::spawn( strand_, std::bind(&Door::doAccept, this->shared_from_this(), std::placeholders::_1)); } template void Door::close() { if (!strand_.running_in_this_thread()) { return boost::asio::post( strand_, std::bind(&Door::close, this->shared_from_this())); } backoffTimer_.cancel(); error_code ec; acceptor_.close(ec); } //------------------------------------------------------------------------------ template template void Door::create( bool ssl, ConstBufferSequence const& buffers, stream_type&& stream, endpoint_type remoteAddress) { if (ssl) { if (auto sp = ios().template emplace>( port_, handler_, ioc_, j_, remoteAddress, buffers, std::move(stream))) sp->run(); return; } if (auto sp = ios().template emplace>( port_, handler_, ioc_, j_, remoteAddress, buffers, std::move(stream))) sp->run(); } template void Door::doAccept(boost::asio::yield_context doYield) { while (acceptor_.is_open()) { if (shouldThrottleForFds()) { JLOG(j_.warn()) << "Throttling do_accept for " << acceptDelay_.count() << "ms."; backoffTimer_.expires_after(acceptDelay_); boost::system::error_code tec; backoffTimer_.async_wait(doYield[tec]); acceptDelay_ = std::min(acceptDelay_ * 2, kMaxAcceptDelay); continue; } error_code ec; endpoint_type remoteAddress; stream_type stream(ioc_); socket_type& socket = stream.socket(); acceptor_.async_accept(socket, remoteAddress, doYield[ec]); if (ec) { if (ec == boost::asio::error::operation_aborted) break; if (ec == boost::asio::error::no_descriptors || ec == boost::asio::error::no_buffer_space) { char const* const cause = (ec == boost::asio::error::no_descriptors) ? "too many open files" : "kernel buffer space exhausted"; JLOG(j_.warn()) << "accept: " << cause << ". Pausing for " << acceptDelay_.count() << "ms."; backoffTimer_.expires_after(acceptDelay_); boost::system::error_code tec; backoffTimer_.async_wait(doYield[tec]); acceptDelay_ = std::min(acceptDelay_ * 2, kMaxAcceptDelay); } else { JLOG(j_.error()) << "accept error: " << ec.message(); } continue; } acceptDelay_ = kInitialAcceptDelay; if (ssl_ && plain_) { if (auto sp = ios().template emplace( port_, handler_, ioc_, std::move(stream), remoteAddress, j_)) sp->run(); } else if (ssl_ || plain_) { create(ssl_, boost::asio::null_buffers{}, std::move(stream), remoteAddress); } } } template std::optional::FDStats> Door::queryFdStats() const { #if BOOST_OS_WINDOWS return std::nullopt; #else FDStats s; struct rlimit rl{}; if (getrlimit(RLIMIT_NOFILE, &rl) != 0 || rl.rlim_cur == RLIM_INFINITY) return std::nullopt; s.limit = static_cast(rl.rlim_cur); #if BOOST_OS_LINUX static constexpr char const* kFdDir = "/proc/self/fd"; #else static constexpr char const* kFdDir = "/dev/fd"; #endif if (DIR* d = ::opendir(kFdDir)) { std::uint64_t cnt = 0; while (::readdir(d) != nullptr) ++cnt; ::closedir(d); // readdir counts '.', '..', and the DIR* itself shows in the list s.used = (cnt >= 3) ? (cnt - 3) : 0; return s; } return std::nullopt; #endif } template bool Door::shouldThrottleForFds() { #if BOOST_OS_WINDOWS return false; #else auto const now = clock_type::now(); if (now - fdSampleAt_ < kFdSampleInterval) return cachedThrottle_; fdSampleAt_ = now; auto const stats = queryFdStats(); cachedThrottle_ = stats && stats->limit > 0 && stats->used * 100 > stats->limit * kMaxUsedFdPercent; return cachedThrottle_; #endif } } // namespace xrpl