From f5bc018948f389559ccfaca128b07c9b17b3218b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 4 Jun 2025 13:52:56 -0400 Subject: [PATCH] http: Introduce HTTPServer class and implement binding to listening socket Introduce a new low-level socket managing class `HTTPServer`. BindAndStartListening() was copied from CConnMan's BindListenPort() in net.cpp and modernized. Unit-test it with a new class `SocketTestingSetup` which mocks `CreateSock()` and will enable mock client I/O in future commits. Co-authored-by: Vasil Dimov --- src/httpserver.cpp | 87 ++++++++++++++++++++++++++++++++++ src/httpserver.h | 32 +++++++++++++ src/test/httpserver_tests.cpp | 26 +++++++++- src/test/util/setup_common.cpp | 19 ++++++++ src/test/util/setup_common.h | 15 ++++++ 5 files changed, 178 insertions(+), 1 deletion(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 0b3d5d06cfb..310907ba691 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -2,6 +2,8 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include // IWYU pragma: keep + #include #include @@ -15,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -46,6 +49,9 @@ #include +//! Explicit alias for setting socket option methods. +static constexpr int SOCKET_OPTION_TRUE{1}; + using common::InvalidPortErrMsg; using http_libevent::HTTPRequest; @@ -889,4 +895,85 @@ bool HTTPRequest::LoadBody(LineReader& reader) return true; } + +util::Expected HTTPServer::BindAndStartListening(const CService& to) +{ + // Create socket for listening for incoming connections + sockaddr_storage storage; + auto sa = reinterpret_cast(&storage); + socklen_t len{sizeof(storage)}; + if (!to.GetSockAddr(sa, &len)) { + return util::Unexpected{strprintf("Bind address family for %s not supported", to.ToStringAddrPort())}; + } + + std::unique_ptr sock{CreateSock(to.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP)}; + if (!sock) { + return util::Unexpected{strprintf("Cannot create %s listen socket: %s", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError()))}; + } + + // Allow binding if the port is still in TIME_WAIT state after + // the program was closed and restarted. + if (sock->SetSockOpt(SOL_SOCKET, SO_REUSEADDR, &SOCKET_OPTION_TRUE, sizeof(SOCKET_OPTION_TRUE)) == SOCKET_ERROR) { + LogDebug(BCLog::HTTP, + "Cannot set SO_REUSEADDR on %s listen socket: %s, continuing anyway", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError())); + } + + // some systems don't have IPV6_V6ONLY but are always v6only; others do have the option + // and enable it by default or not. Try to enable it, if possible. + if (to.IsIPv6()) { +#ifdef IPV6_V6ONLY + if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, &SOCKET_OPTION_TRUE, sizeof(SOCKET_OPTION_TRUE)) == SOCKET_ERROR) { + LogDebug(BCLog::HTTP, + "Cannot set IPV6_V6ONLY on %s listen socket: %s, continuing anyway", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError())); + } +#endif +#ifdef WIN32 + int prot_level{PROTECTION_LEVEL_UNRESTRICTED}; + if (sock->SetSockOpt(IPPROTO_IPV6, + IPV6_PROTECTION_LEVEL, + &prot_level, + sizeof(prot_level)) == SOCKET_ERROR) { + LogDebug(BCLog::HTTP, + "Cannot set IPV6_PROTECTION_LEVEL on %s listen socket: %s, continuing anyway", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError())); + } +#endif + } + + if (sock->Bind(sa, len) == SOCKET_ERROR) { + const int err{WSAGetLastError()}; + if (err == WSAEADDRINUSE) { + return util::Unexpected{strprintf("Unable to bind to %s on this computer. %s is probably already running.", + to.ToStringAddrPort(), + CLIENT_NAME)}; + } else { + return util::Unexpected{strprintf("Unable to bind to %s on this computer (bind returned error %s)", + to.ToStringAddrPort(), + NetworkErrorString(err))}; + } + } + + // Listen for incoming connections + if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) { + return util::Unexpected{strprintf("Cannot listen on %s: %s", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError()))}; + } + + m_listen.emplace_back(std::move(sock)); + + return {}; +} + +void HTTPServer::StopListening() +{ + m_listen.clear(); +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index 6bf1827449d..ba93cbb8b97 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -6,13 +6,18 @@ #define BITCOIN_HTTPSERVER_H #include +#include #include #include #include #include +#include +#include #include #include +#include +#include #include #include @@ -297,6 +302,33 @@ public: bool LoadBody(LineReader& reader); /// @} }; + +class HTTPServer +{ +public: + /** + * Bind to a new address:port, start listening and add the listen socket to `m_listen`. + * @param[in] to Where to bind. + * @returns {} or the reason for failure. + */ + util::Expected BindAndStartListening(const CService& to); + + /** + * Stop listening by closing all listening sockets. + */ + void StopListening(); + + /** + * Get the number of sockets the server is bound to and listening on + */ + size_t GetListeningSocketCount() const { return m_listen.size(); } + +private: + /** + * List of listening sockets. + */ + std::vector> m_listen; +}; } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index e2b3f0c9b69..1114df05f1c 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -13,11 +13,12 @@ using http_bitcoin::HTTPHeaders; using http_bitcoin::HTTPRequest; using http_bitcoin::HTTPResponse; +using http_bitcoin::HTTPServer; using http_bitcoin::MAX_BODY_SIZE; using http_bitcoin::MAX_HEADERS_SIZE; using util::LineReader; -BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) +BOOST_FIXTURE_TEST_SUITE(httpserver_tests, SocketTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) { @@ -372,4 +373,27 @@ BOOST_AUTO_TEST_CASE(http_request_tests) BOOST_CHECK(!req.LoadBody(reader)); } } + +BOOST_AUTO_TEST_CASE(http_server_socket_tests) +{ + HTTPServer server; + + { + // We can only bind to NET_IPV4 and NET_IPV6 + CService onion_address{Lookup("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion", /*portDefault=*/0, /*fAllowLookup=*/false).value()}; + auto result{server.BindAndStartListening(onion_address)}; + BOOST_REQUIRE(!result); + BOOST_CHECK_EQUAL(result.error(), "Bind address family for aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaam2dqd.onion:0 not supported"); + } + + // This VALID address won't actually get used because we stubbed CreateSock() + CService addr_bind{Lookup("0.0.0.0", /*portDefault=*/0, /*fAllowLookup=*/false).value()}; + + // Init state + BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 0); + // Bind to mock Listening Socket + BOOST_REQUIRE(server.BindAndStartListening(addr_bind)); + // We are bound and listening + BOOST_REQUIRE_EQUAL(server.GetListeningSocketCount(), 1); +} BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/util/setup_common.cpp b/src/test/util/setup_common.cpp index 8097494dfa3..2ba63efd11f 100644 --- a/src/test/util/setup_common.cpp +++ b/src/test/util/setup_common.cpp @@ -639,6 +639,25 @@ std::vector TestChain100Setup::PopulateMempool(FastRandomContex return mempool_transactions; } +SocketTestingSetup::SocketTestingSetup() +{ + // "back up" the current CreateSock() so we can restore it after the test + m_create_sock_orig = CreateSock; + + CreateSock = [this](int, int, int) { + // This is a mock Listening Socket that a server can "bind" to and + // listen to for incoming connections. We won't need to access its I/O + // pipes because we don't read or write directly to it. It will return + // Connected Sockets from the queue via its Accept() method. + return std::make_unique(std::make_shared(), &m_accepted_sockets); + }; +}; + +SocketTestingSetup::~SocketTestingSetup() +{ + CreateSock = m_create_sock_orig; +} + /** * @returns a real block (0000000000013b8ab2cd513b0261a14096412195a72a0c4827d229dcc7e0f7af) * with 9 txs. diff --git a/src/test/util/setup_common.h b/src/test/util/setup_common.h index d03f5baefb8..7a8f6b70c8c 100644 --- a/src/test/util/setup_common.h +++ b/src/test/util/setup_common.h @@ -13,6 +13,7 @@ #include // IWYU pragma: export #include #include +#include #include #include #include // IWYU pragma: export @@ -249,6 +250,20 @@ std::unique_ptr MakeNoLogFileContext(const ChainType chain_type = ChainType:: return std::make_unique(chain_type, opts); } +class SocketTestingSetup : public BasicTestingSetup +{ +public: + explicit SocketTestingSetup(); + ~SocketTestingSetup(); + +private: + //! Save the original value of CreateSock here and restore it when the test ends. + decltype(CreateSock) m_create_sock_orig; + + //! Queue of connected sockets returned by listening socket (represents network interface) + DynSock::Queue m_accepted_sockets; +}; + CBlock getBlock13b8a(); #endif // BITCOIN_TEST_UTIL_SETUP_COMMON_H