From 2bad9044f28e5714a8625c7c6a0b24922a61b1f1 Mon Sep 17 00:00:00 2001 From: Lee *!* Clagett Date: Tue, 21 Jan 2025 09:56:52 -0500 Subject: [PATCH] Set response limits on http server connections --- .../epee/include/net/abstract_tcp_server2.h | 11 +- .../epee/include/net/abstract_tcp_server2.inl | 57 +++++- .../epee/include/net/http_protocol_handler.h | 17 +- .../include/net/http_protocol_handler.inl | 36 +++- .../epee/include/net/http_server_impl_base.h | 25 ++- src/rpc/core_rpc_server.cpp | 26 ++- src/rpc/core_rpc_server.h | 4 +- src/wallet/wallet_rpc_server.cpp | 11 +- .../functional_tests/functional_tests_rpc.py | 2 +- tests/unit_tests/CMakeLists.txt | 1 + tests/unit_tests/epee_http_server.cpp | 192 ++++++++++++++++++ 11 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 tests/unit_tests/epee_http_server.cpp diff --git a/contrib/epee/include/net/abstract_tcp_server2.h b/contrib/epee/include/net/abstract_tcp_server2.h index 3f9b95033f1..1e45e68096e 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.h +++ b/contrib/epee/include/net/abstract_tcp_server2.h @@ -65,6 +65,7 @@ #define MONERO_DEFAULT_LOG_CATEGORY "net" #define ABSTRACT_SERVER_SEND_QUE_MAX_COUNT 1000 +#define ABSTRACT_SERVER_SEND_QUE_MAX_BYTES_DEFAULT 100 * 1024 * 1024 namespace epee { @@ -170,6 +171,7 @@ namespace net_utils } read; struct { std::deque queue; + std::size_t total_bytes; bool wait_consume; } write; }; @@ -268,11 +270,17 @@ namespace net_utils struct shared_state : connection_basic_shared_state, t_protocol_handler::config_type { shared_state() - : connection_basic_shared_state(), t_protocol_handler::config_type(), pfilter(nullptr), plimit(nullptr), stop_signal_sent(false) + : connection_basic_shared_state(), + t_protocol_handler::config_type(), + pfilter(nullptr), + plimit(nullptr), + response_soft_limit(ABSTRACT_SERVER_SEND_QUE_MAX_BYTES_DEFAULT), + stop_signal_sent(false) {} i_connection_filter* pfilter; i_connection_limit* plimit; + std::size_t response_soft_limit; bool stop_signal_sent; }; @@ -380,6 +388,7 @@ namespace net_utils void set_connection_filter(i_connection_filter* pfilter); void set_connection_limit(i_connection_limit* plimit); + void set_response_soft_limit(std::size_t limit); void set_default_remote(epee::net_utils::network_address remote) { diff --git a/contrib/epee/include/net/abstract_tcp_server2.inl b/contrib/epee/include/net/abstract_tcp_server2.inl index 53c0d852c9c..39a58d1b261 100644 --- a/contrib/epee/include/net/abstract_tcp_server2.inl +++ b/contrib/epee/include/net/abstract_tcp_server2.inl @@ -497,10 +497,12 @@ namespace net_utils if (m_state.socket.cancel_write) { m_state.socket.cancel_write = false; m_state.data.write.queue.clear(); + m_state.data.write.total_bytes = 0; state_status_check(); } else if (ec.value()) { m_state.data.write.queue.clear(); + m_state.data.write.total_bytes = 0; interrupt(); } else { @@ -525,8 +527,11 @@ namespace net_utils start_timer(get_default_timeout(), true); } - assert(bytes_transferred == m_state.data.write.queue.back().size()); + const std::size_t byte_count = m_state.data.write.queue.back().size(); + assert(bytes_transferred == byte_count); m_state.data.write.queue.pop_back(); + m_state.data.write.total_bytes -= + std::min(m_state.data.write.total_bytes, byte_count); m_state.condition.notify_all(); start_write(); } @@ -670,8 +675,9 @@ namespace net_utils return; if (m_state.timers.throttle.out.wait_expire) return; - if (m_state.socket.wait_write) - return; + // \NOTE See on_terminating() comments + //if (m_state.socket.wait_write) + // return; if (m_state.socket.wait_shutdown) return; if (m_state.protocol.wait_init) @@ -729,8 +735,13 @@ namespace net_utils return; if (m_state.timers.throttle.out.wait_expire) return; - if (m_state.socket.wait_write) - return; + // Writes cannot be canceled due to `async_write` being a "composed" + // handler. ASIO has new cancellation routines, not available in 1.66, to + // handle this situation. The problem is that if cancel is called after an + // intermediate handler is queued, the op will not check the cancel flag in + // our code, and will instead queue up another write. + //if (m_state.socket.wait_write) + // return; if (m_state.socket.wait_shutdown) return; if (m_state.protocol.wait_init) @@ -757,6 +768,8 @@ namespace net_utils std::lock_guard guard(m_state.lock); if (m_state.status != status_t::RUNNING || m_state.socket.wait_handshake) return false; + if (std::numeric_limits::max() - m_state.data.write.total_bytes < message.size()) + return false; // Wait for the write queue to fall below the max. If it doesn't after a // randomized delay, drop the connection. @@ -774,7 +787,14 @@ namespace net_utils std::uniform_int_distribution<>(5000, 6000)(rng) ); }; - if (m_state.data.write.queue.size() <= ABSTRACT_SERVER_SEND_QUE_MAX_COUNT) + + // The bytes check intentionally does not include incoming message size. + // This allows for a soft overflow; a single http response will never fail + // this check, but multiple responses could. Clients can avoid this case + // by reading the entire response before making another request. P2P + // should never hit the MAX_BYTES check (when using default values). + if (m_state.data.write.queue.size() <= ABSTRACT_SERVER_SEND_QUE_MAX_COUNT && + m_state.data.write.total_bytes <= static_cast(connection_basic::get_state()).response_soft_limit) return true; m_state.data.write.wait_consume = true; bool success = m_state.condition.wait_for( @@ -783,14 +803,23 @@ namespace net_utils [this]{ return ( m_state.status != status_t::RUNNING || - m_state.data.write.queue.size() <= - ABSTRACT_SERVER_SEND_QUE_MAX_COUNT + ( + m_state.data.write.queue.size() <= + ABSTRACT_SERVER_SEND_QUE_MAX_COUNT && + m_state.data.write.total_bytes <= + static_cast(connection_basic::get_state()).response_soft_limit + ) ); } ); m_state.data.write.wait_consume = false; if (!success) { - terminate(); + // synchronize with intermediate writes on `m_strand` + auto self = connection::shared_from_this(); + boost::asio::post(m_strand, [this, self] { + std::lock_guard guard(m_state.lock); + terminate(); + }); return false; } else @@ -816,7 +845,9 @@ namespace net_utils ) { if (!wait_consume()) return false; + const std::size_t byte_count = message.size(); m_state.data.write.queue.emplace_front(std::move(message)); + m_state.data.write.total_bytes += byte_count; start_write(); } else { @@ -826,6 +857,7 @@ namespace net_utils m_state.data.write.queue.emplace_front( message.take_slice(CHUNK_SIZE) ); + m_state.data.write.total_bytes += m_state.data.write.queue.front().size(); start_write(); } } @@ -1369,6 +1401,13 @@ namespace net_utils } //--------------------------------------------------------------------------------- template + void boosted_tcp_server::set_response_soft_limit(const std::size_t limit) + { + assert(m_state != nullptr); // always set in constructor + m_state->response_soft_limit = limit; + } + //--------------------------------------------------------------------------------- + template bool boosted_tcp_server::run_server(size_t threads_count, bool wait, const boost::thread::attributes& attrs) { TRY_ENTRY(); diff --git a/contrib/epee/include/net/http_protocol_handler.h b/contrib/epee/include/net/http_protocol_handler.h index 258b07e2c5a..b1a82c57f4a 100644 --- a/contrib/epee/include/net/http_protocol_handler.h +++ b/contrib/epee/include/net/http_protocol_handler.h @@ -32,6 +32,7 @@ #include #include +#include #include "net_utils_base.h" #include "http_auth.h" #include "http_base.h" @@ -54,8 +55,12 @@ namespace net_utils { std::string m_folder; std::vector m_access_control_origins; + std::unordered_map m_connections; boost::optional m_user; size_t m_max_content_length{std::numeric_limits::max()}; + std::size_t m_connection_count{0}; + std::size_t m_max_ip_connections{5}; + std::size_t m_max_connections{100}; critical_section m_lock; }; @@ -70,7 +75,7 @@ namespace net_utils typedef http_server_config config_type; simple_http_connection_handler(i_service_endpoint* psnd_hndlr, config_type& config, t_connection_context& conn_context); - virtual ~simple_http_connection_handler(){} + virtual ~simple_http_connection_handler(); bool release_protocol() { @@ -86,10 +91,7 @@ namespace net_utils { return true; } - bool after_init_connection() - { - return true; - } + bool after_init_connection(); virtual bool handle_recv(const void* ptr, size_t cb); virtual bool handle_request(const http::http_request_info& query_info, http_response_info& response); @@ -146,6 +148,7 @@ namespace net_utils protected: i_service_endpoint* m_psnd_hndlr; t_connection_context& m_conn_context; + bool m_initialized; }; template @@ -212,10 +215,6 @@ namespace net_utils } void handle_qued_callback() {} - bool after_init_connection() - { - return true; - } private: //simple_http_connection_handler::config_type m_stub_config; diff --git a/contrib/epee/include/net/http_protocol_handler.inl b/contrib/epee/include/net/http_protocol_handler.inl index f7d2074b2e7..3df6740d67a 100644 --- a/contrib/epee/include/net/http_protocol_handler.inl +++ b/contrib/epee/include/net/http_protocol_handler.inl @@ -208,11 +208,45 @@ namespace net_utils m_newlines(0), m_bytes_read(0), m_psnd_hndlr(psnd_hndlr), - m_conn_context(conn_context) + m_conn_context(conn_context), + m_initialized(false) { } //-------------------------------------------------------------------------------------------- + template + simple_http_connection_handler::~simple_http_connection_handler() + { + try + { + if (m_initialized) + { + CRITICAL_REGION_LOCAL(m_config.m_lock); + m_config.m_connection_count -= std::min(std::size_t(1), m_config.m_connection_count); + auto elem = m_config.m_connections.find(m_conn_context.m_remote_address.host_str()); + if (elem != m_config.m_connections.end()) + { + if (elem->second == 1 || elem->second == 0) + m_config.m_connections.erase(elem); + else + --(elem->second); + } + } + } + catch (...) + {} + } + //-------------------------------------------------------------------------------------------- + template + bool simple_http_connection_handler::after_init_connection() + { + CRITICAL_REGION_LOCAL(m_config.m_lock); + ++m_config.m_connection_count; + ++m_config.m_connections[m_conn_context.m_remote_address.host_str()]; + m_initialized = true; + return true; + } + //-------------------------------------------------------------------------------------------- template bool simple_http_connection_handler::set_ready_state() { diff --git a/contrib/epee/include/net/http_server_impl_base.h b/contrib/epee/include/net/http_server_impl_base.h index 024f141b4ca..b25b45c88ec 100644 --- a/contrib/epee/include/net/http_server_impl_base.h +++ b/contrib/epee/include/net/http_server_impl_base.h @@ -44,7 +44,8 @@ namespace epee { template - class http_server_impl_base: public net_utils::http::i_http_server_handler + class http_server_impl_base: public net_utils::http::i_http_server_handler, + net_utils::i_connection_limit { public: @@ -60,7 +61,10 @@ namespace epee const std::string& bind_ipv6_address = "::", bool use_ipv6 = false, bool require_ipv4 = true, std::vector access_control_origins = std::vector(), boost::optional user = boost::none, - net_utils::ssl_options_t ssl_options = net_utils::ssl_support_t::e_ssl_support_autodetect) + net_utils::ssl_options_t ssl_options = net_utils::ssl_support_t::e_ssl_support_autodetect, + const std::size_t max_ip_connections = 5, + const std::size_t max_connections = 100, + const std::size_t response_soft_limit = 25 * 1024 * 1024) { //set self as callback handler @@ -75,6 +79,10 @@ namespace epee m_net_server.get_config_object().m_access_control_origins = std::move(access_control_origins); m_net_server.get_config_object().m_user = std::move(user); + m_net_server.get_config_object().m_max_ip_connections = max_ip_connections; + m_net_server.get_config_object().m_max_connections = max_connections; + m_net_server.set_response_soft_limit(response_soft_limit); + m_net_server.set_connection_limit(this); MGINFO("Binding on " << bind_ip << " (IPv4):" << bind_port); if (use_ipv6) @@ -131,6 +139,19 @@ namespace epee } protected: + + virtual bool is_host_limit(const net_utils::network_address& na) override final + { + auto& config = m_net_server.get_config_object(); + CRITICAL_REGION_LOCAL(config.m_lock); + if (config.m_max_connections <= config.m_connection_count) + return true; + const auto elem = config.m_connections.find(na.host_str()); + if (elem != config.m_connections.end()) + return config.m_max_ip_connections <= elem->second; + return false; + } + net_utils::boosted_tcp_server > m_net_server; }; } diff --git a/src/rpc/core_rpc_server.cpp b/src/rpc/core_rpc_server.cpp index 426d8b295f6..8ee5ac7bfda 100644 --- a/src/rpc/core_rpc_server.cpp +++ b/src/rpc/core_rpc_server.cpp @@ -163,6 +163,9 @@ namespace cryptonote command_line::add_arg(desc, arg_rpc_payment_difficulty); command_line::add_arg(desc, arg_rpc_payment_credits); command_line::add_arg(desc, arg_rpc_payment_allow_free_loopback); + command_line::add_arg(desc, arg_rpc_max_connections_per_ip); + command_line::add_arg(desc, arg_rpc_max_connections); + command_line::add_arg(desc, arg_rpc_response_soft_limit); } //------------------------------------------------------------------------------------------------------------------------------ core_rpc_server::core_rpc_server( @@ -400,7 +403,10 @@ namespace cryptonote const bool inited = epee::http_server_impl_base::init( rng, std::move(port), std::move(bind_ip_str), std::move(bind_ipv6_str), std::move(rpc_config->use_ipv6), std::move(rpc_config->require_ipv4), - std::move(rpc_config->access_control_origins), std::move(http_login), std::move(rpc_config->ssl_options) + std::move(rpc_config->access_control_origins), std::move(http_login), std::move(rpc_config->ssl_options), + command_line::get_arg(vm, arg_rpc_max_connections_per_ip), + command_line::get_arg(vm, arg_rpc_max_connections), + command_line::get_arg(vm, arg_rpc_response_soft_limit) ); m_net_server.get_config_object().m_max_content_length = MAX_RPC_CONTENT_LENGTH; @@ -3885,4 +3891,22 @@ namespace cryptonote , "Allow free access from the loopback address (ie, the local host)" , false }; + + const command_line::arg_descriptor core_rpc_server::arg_rpc_max_connections_per_ip = { + "rpc-max-connections-per-ip" + , "Max RPC connections per IP permitted" + , 5 + }; + + const command_line::arg_descriptor core_rpc_server::arg_rpc_max_connections = { + "rpc-max-connections" + , "Max RPC connections permitted" + , 100 + }; + + const command_line::arg_descriptor core_rpc_server::arg_rpc_response_soft_limit = { + "rpc-response-soft-limit" + , "Max response bytes that can be queued, enforced at next response attempt" + , 25 * 1024 * 1024 + }; } // namespace cryptonote diff --git a/src/rpc/core_rpc_server.h b/src/rpc/core_rpc_server.h index 6bd0fc25b69..1296475f64e 100644 --- a/src/rpc/core_rpc_server.h +++ b/src/rpc/core_rpc_server.h @@ -56,7 +56,6 @@ namespace cryptonote { public: - static const command_line::arg_descriptor arg_public_node; static const command_line::arg_descriptor arg_rpc_bind_port; static const command_line::arg_descriptor arg_rpc_restricted_bind_port; static const command_line::arg_descriptor arg_restricted_rpc; @@ -73,6 +72,9 @@ namespace cryptonote static const command_line::arg_descriptor arg_rpc_payment_difficulty; static const command_line::arg_descriptor arg_rpc_payment_credits; static const command_line::arg_descriptor arg_rpc_payment_allow_free_loopback; + static const command_line::arg_descriptor arg_rpc_max_connections_per_ip; + static const command_line::arg_descriptor arg_rpc_max_connections; + static const command_line::arg_descriptor arg_rpc_response_soft_limit; typedef epee::net_utils::connection_context_base connection_context; diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 33437606228..8fceb8bccc8 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -129,6 +129,9 @@ namespace const command_line::arg_descriptor arg_wallet_dir = {"wallet-dir", "Directory for newly created wallets"}; const command_line::arg_descriptor arg_prompt_for_password = {"prompt-for-password", "Prompts for password when not provided", false}; const command_line::arg_descriptor arg_no_initial_sync = {"no-initial-sync", "Skips the initial sync before listening for connections", false}; + const command_line::arg_descriptor arg_rpc_max_connections_per_ip = {"rpc-max-connections-per-ip", "Max RPC connections per IP permitted", 5}; + const command_line::arg_descriptor arg_rpc_max_connections = {"rpc-max-connections", "Max RPC connections permitted", 100}; + const command_line::arg_descriptor arg_rpc_response_soft_limit = {"rpc-response-soft-limit", "Max response bytes that can be queued, enforced at next response attempt", 25 * 1024 * 1024}; constexpr const char default_rpc_username[] = "monero"; @@ -331,7 +334,10 @@ namespace tools rng, std::move(bind_port), std::move(rpc_config->bind_ip), std::move(rpc_config->bind_ipv6_address), std::move(rpc_config->use_ipv6), std::move(rpc_config->require_ipv4), std::move(rpc_config->access_control_origins), std::move(http_login), - std::move(rpc_config->ssl_options) + std::move(rpc_config->ssl_options), + command_line::get_arg(vm, arg_rpc_max_connections_per_ip), + command_line::get_arg(vm, arg_rpc_max_connections), + command_line::get_arg(vm, arg_rpc_response_soft_limit) ); } //------------------------------------------------------------------------------------------------------------------------------ @@ -4974,6 +4980,9 @@ int main(int argc, char** argv) { command_line::add_arg(desc_params, arg_wallet_dir); command_line::add_arg(desc_params, arg_prompt_for_password); command_line::add_arg(desc_params, arg_no_initial_sync); + command_line::add_arg(desc_params, arg_rpc_max_connections_per_ip); + command_line::add_arg(desc_params, arg_rpc_max_connections); + command_line::add_arg(desc_params, arg_rpc_response_soft_limit); command_line::add_arg(hidden_options, daemonizer::arg_non_interactive); daemonizer::init_options(hidden_options, desc_params); diff --git a/tests/functional_tests/functional_tests_rpc.py b/tests/functional_tests/functional_tests_rpc.py index ca55ed3011e..f183751622f 100755 --- a/tests/functional_tests/functional_tests_rpc.py +++ b/tests/functional_tests/functional_tests_rpc.py @@ -51,7 +51,7 @@ FUNCTIONAL_TESTS_DIRECTORY = builddir + "/tests/functional_tests" DIFFICULTY = 10 -monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", str(DIFFICULTY), "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--zmq-pub", "monerod_zmq_pub", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--data-dir", "monerod_data_dir", "--log-level", "1"] +monerod_base = [builddir + "/bin/monerod", "--regtest", "--fixed-difficulty", str(DIFFICULTY), "--no-igd", "--p2p-bind-port", "monerod_p2p_port", "--rpc-bind-port", "monerod_rpc_port", "--zmq-rpc-bind-port", "monerod_zmq_port", "--zmq-pub", "monerod_zmq_pub", "--non-interactive", "--disable-dns-checkpoints", "--check-updates", "disabled", "--rpc-ssl", "disabled", "--data-dir", "monerod_data_dir", "--log-level", "1", "--rpc-max-connections-per-ip", "100", "--rpc-max-connections", "100"] monerod_extra = [ ["--offline"], ["--rpc-payment-address", "44SKxxLQw929wRF6BA9paQ1EWFshNnKhXM3qz6Mo3JGDE2YG3xyzVutMStEicxbQGRfrYvAAYxH6Fe8rnD56EaNwUiqhcwR", "--rpc-payment-difficulty", str(DIFFICULTY), "--rpc-payment-credits", "5000", "--offline"], diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index e329b7506fa..8e9b7a531f9 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -48,6 +48,7 @@ set(unit_tests_sources dns_resolver.cpp epee_boosted_tcp_server.cpp epee_levin_protocol_handler_async.cpp + epee_http_server.cpp epee_serialization.cpp epee_utils.cpp expect.cpp diff --git a/tests/unit_tests/epee_http_server.cpp b/tests/unit_tests/epee_http_server.cpp new file mode 100644 index 00000000000..bb4487a67c8 --- /dev/null +++ b/tests/unit_tests/epee_http_server.cpp @@ -0,0 +1,192 @@ +// Copyright (c) 2014-2024, The Monero Project +// +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without modification, are +// permitted provided that the following conditions are met: +// +// 1. Redistributions of source code must retain the above copyright notice, this list of +// conditions and the following disclaimer. +// +// 2. Redistributions in binary form must reproduce the above copyright notice, this list +// of conditions and the following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// 3. Neither the name of the copyright holder nor the names of its contributors may be +// used to endorse or promote products derived from this software without specific +// prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY +// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL +// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, +// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF +// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// + +#include +#include +#include +#include +#include +#include +#include "gtest/gtest.h" +#include "net/http_server_handlers_map2.h" +#include "net/http_server_impl_base.h" +#include "storages/portable_storage_template_helper.h" + +namespace +{ + constexpr const std::size_t payload_size = 26 * 1024 * 1024; + constexpr const std::size_t max_private_ips = 5; + struct dummy + { + struct request + { + BEGIN_KV_SERIALIZE_MAP() + END_KV_SERIALIZE_MAP() + }; + + struct response + { + BEGIN_KV_SERIALIZE_MAP() + KV_SERIALIZE(payload) + END_KV_SERIALIZE_MAP() + + std::string payload; + }; + }; + + std::string make_payload() + { + dummy::request body{}; + const auto body_serialized = epee::serialization::store_t_to_binary(body); + return std::string{ + reinterpret_cast(body_serialized.data()), + body_serialized.size() + }; + } + + struct http_server : epee::http_server_impl_base + { + using connection_context = epee::net_utils::connection_context_base; + + http_server() + : epee::http_server_impl_base(), + dummy_size(payload_size) + {} + + CHAIN_HTTP_TO_MAP2(connection_context); //forward http requests to uri map + + BEGIN_URI_MAP2() + MAP_URI_AUTO_BIN2("/dummy", on_dummy, dummy) + END_URI_MAP2() + + bool on_dummy(const dummy::request&, dummy::response& res, const connection_context *ctx = NULL) + { + res.payload.resize(dummy_size.load(), 'f'); + return true; + } + + std::atomic dummy_size; + }; +} // anonymous + +TEST(http_server, response_soft_limit) +{ + namespace http = boost::beast::http; + + http_server server{}; + server.init(nullptr, "8080"); + server.run(1, false); + + boost::system::error_code error{}; + boost::asio::io_context context{}; + boost::asio::ip::tcp::socket stream{context}; + stream.connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + EXPECT_FALSE(bool(error)); + + http::request req{http::verb::get, "/dummy", 11}; + req.set(http::field::host, "127.0.0.1"); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req.body() = make_payload(); + req.prepare_payload(); + http::write(stream, req, error); + EXPECT_FALSE(bool(error)); + + { + dummy::response payload{}; + boost::beast::flat_buffer buffer; + http::response> res; + http::read(stream, buffer, res, error); + EXPECT_FALSE(bool(error)); + EXPECT_EQ(200u, res.result_int()); + EXPECT_TRUE(epee::serialization::load_t_from_binary(payload, res.body())); + EXPECT_EQ(payload_size, std::count(payload.payload.begin(), payload.payload.end(), 'f')); + } + + while (!error) + http::write(stream, req, error); + server.send_stop_signal(); +} + +TEST(http_server, private_ip_limit) +{ + namespace http = boost::beast::http; + + http_server server{}; + server.dummy_size = 1; + server.init(nullptr, "8080"); + server.run(1, false); + + boost::system::error_code error{}; + boost::asio::io_context context{}; + + std::vector streams{}; + for (std::size_t i = 0; i < max_private_ips; ++i) + { + streams.emplace_back(context); + streams.back().connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + } + + boost::asio::ip::tcp::socket stream{context}; + stream.connect( + boost::asio::ip::tcp::endpoint{ + boost::asio::ip::make_address("127.0.0.1"), 8080 + }, + error + ); + bool failed = bool(error); + { + http::request req{http::verb::get, "/dummy", 11}; + req.set(http::field::host, "127.0.0.1"); + req.set(http::field::user_agent, BOOST_BEAST_VERSION_STRING); + req.body() = make_payload(); + req.prepare_payload(); + http::write(stream, req, error); + } + failed |= bool(error); + { + dummy::response payload{}; + boost::beast::flat_buffer buffer; + http::response> res; + + // make sure server ran async_accept code + http::read(stream, buffer, res, error); + } + failed |= bool(error); + EXPECT_TRUE(failed); +}