From 7745ea523ca517f0f19522a1783a33f51c2ff09b Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 14 Jan 2025 17:24:22 +0100 Subject: [PATCH 01/42] net: separate the listening socket from the permissions They were coupled in `struct ListenSocket`, but the socket belongs to the lower level transport protocol, whereas the permissions are specific to the higher Bitcoin P2P protocol. --- src/net.cpp | 14 ++++++++++---- src/net.h | 16 +++++++++------- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 735985a8414..3c4d99ddd9e 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1755,7 +1755,10 @@ void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { const CService addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(*sock))}; NetPermissionFlags permission_flags = NetPermissionFlags::None; - hListenSocket.AddSocketPermissionFlags(permission_flags); + auto it{m_listen_permissions.find(addr_bind)}; + if (it != m_listen_permissions.end()) { + NetPermissions::AddFlag(permission_flags, it->second); + } CreateNodeFromAcceptedSocket(std::move(sock), permission_flags, addr_bind, addr); } @@ -3111,7 +3114,7 @@ void CConnman::ThreadI2PAcceptIncoming() } } -bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, NetPermissionFlags permissions) +bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError) { int nOne = 1; @@ -3176,7 +3179,7 @@ bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, return false; } - vhListenSocket.emplace_back(std::move(sock), permissions); + vhListenSocket.emplace_back(std::move(sock)); return true; } @@ -3242,13 +3245,15 @@ bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlag const CService addr{MaybeFlipIPv6toCJDNS(addr_)}; bilingual_str strError; - if (!BindListenPort(addr, strError, permissions)) { + if (!BindListenPort(addr, strError)) { if ((flags & BF_REPORT_ERROR) && m_client_interface) { m_client_interface->ThreadSafeMessageBox(strError, "", CClientUIInterface::MSG_ERROR); } return false; } + m_listen_permissions.emplace(addr, permissions); + if (addr.IsRoutable() && fDiscover && !(flags & BF_DONT_ADVERTISE) && !NetPermissions::HasFlag(permissions, NetPermissionFlags::NoBan)) { AddLocal(addr, LOCAL_BIND); } @@ -3482,6 +3487,7 @@ void CConnman::StopNodes() DeleteNode(pnode); } m_nodes_disconnected.clear(); + m_listen_permissions.clear(); vhListenSocket.clear(); semOutbound.reset(); semAddnode.reset(); diff --git a/src/net.h b/src/net.h index e64d9a67f46..8ece1071278 100644 --- a/src/net.h +++ b/src/net.h @@ -1276,21 +1276,17 @@ private: struct ListenSocket { public: std::shared_ptr sock; - inline void AddSocketPermissionFlags(NetPermissionFlags& flags) const { NetPermissions::AddFlag(flags, m_permissions); } - ListenSocket(std::shared_ptr sock_, NetPermissionFlags permissions_) - : sock{sock_}, m_permissions{permissions_} + ListenSocket(std::shared_ptr sock_) + : sock{sock_} { } - - private: - NetPermissionFlags m_permissions; }; //! returns the time left in the current max outbound cycle //! in case of no limit, it will always return 0 std::chrono::seconds GetMaxOutboundTimeLeftInCycle_() const EXCLUSIVE_LOCKS_REQUIRED(m_total_bytes_sent_mutex); - bool BindListenPort(const CService& bindAddr, bilingual_str& strError, NetPermissionFlags permissions); + bool BindListenPort(const CService& bindAddr, bilingual_str& strError); bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions); bool InitBinds(const Options& options); @@ -1431,6 +1427,12 @@ private: unsigned int nReceiveFloodSize{0}; std::vector vhListenSocket; + + /** + * Permissions that incoming peers get based on our listening address they connected to. + */ + std::unordered_map m_listen_permissions; + std::atomic fNetworkActive{true}; bool fAddressesInitialized{false}; AddrMan& addrman; From 69ac6802ba790652c2fe0f92f70bb6d74dd388fa Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 28 Jan 2025 14:36:24 +0100 Subject: [PATCH 02/42] net: drop CConnman::ListenSocket Now that `CConnman::ListenSocket` is a `struct` that contains only one member variable of type `std::shared_ptr`, drop `ListenSocket` and use `shared_ptr` directly. Replace the vector of `ListenSocket` with a vector of `shared_ptr`. --- src/net.cpp | 19 ++++++++++--------- src/net.h | 16 +++++----------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 3c4d99ddd9e..cccb7376562 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1732,10 +1732,10 @@ bool CConnman::AttemptToEvictConnection() return false; } -void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { +void CConnman::AcceptConnection(const Sock& listen_sock) { struct sockaddr_storage sockaddr; socklen_t len = sizeof(sockaddr); - auto sock = hListenSocket.sock->Accept((struct sockaddr*)&sockaddr, &len); + auto sock = listen_sock.Accept((struct sockaddr*)&sockaddr, &len); if (!sock) { const int nErr = WSAGetLastError(); @@ -2057,8 +2057,8 @@ Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) { Sock::EventsPerSock events_per_sock; - for (const ListenSocket& hListenSocket : vhListenSocket) { - events_per_sock.emplace(hListenSocket.sock, Sock::Events{Sock::RECV}); + for (const auto& sock : m_listen) { + events_per_sock.emplace(sock, Sock::Events{Sock::RECV}); } for (CNode* pnode : nodes) { @@ -2213,13 +2213,13 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock) { - for (const ListenSocket& listen_socket : vhListenSocket) { + for (const auto& sock : m_listen) { if (interruptNet) { return; } - const auto it = events_per_sock.find(listen_socket.sock); + const auto it = events_per_sock.find(sock); if (it != events_per_sock.end() && it->second.occurred & Sock::RECV) { - AcceptConnection(listen_socket); + AcceptConnection(*sock); } } } @@ -3179,7 +3179,8 @@ bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError) return false; } - vhListenSocket.emplace_back(std::move(sock)); + m_listen.emplace_back(std::move(sock)); + return true; } @@ -3488,7 +3489,7 @@ void CConnman::StopNodes() } m_nodes_disconnected.clear(); m_listen_permissions.clear(); - vhListenSocket.clear(); + m_listen.clear(); semOutbound.reset(); semAddnode.reset(); } diff --git a/src/net.h b/src/net.h index 8ece1071278..bbee5b1fb71 100644 --- a/src/net.h +++ b/src/net.h @@ -1273,15 +1273,6 @@ public: bool MultipleManualOrFullOutboundConns(Network net) const EXCLUSIVE_LOCKS_REQUIRED(m_nodes_mutex); private: - struct ListenSocket { - public: - std::shared_ptr sock; - ListenSocket(std::shared_ptr sock_) - : sock{sock_} - { - } - }; - //! returns the time left in the current max outbound cycle //! in case of no limit, it will always return 0 std::chrono::seconds GetMaxOutboundTimeLeftInCycle_() const EXCLUSIVE_LOCKS_REQUIRED(m_total_bytes_sent_mutex); @@ -1296,7 +1287,7 @@ private: void ThreadOpenConnections(std::vector connect, Span seed_nodes) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !m_reconnections_mutex); void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); void ThreadI2PAcceptIncoming(); - void AcceptConnection(const ListenSocket& hListenSocket); + void AcceptConnection(const Sock& listen_sock); /** * Create a `CNode` object from a socket that has just been accepted and add the node to @@ -1426,7 +1417,10 @@ private: unsigned int nSendBufferMaxSize{0}; unsigned int nReceiveFloodSize{0}; - std::vector vhListenSocket; + /** + * List of listening sockets. + */ + std::vector> m_listen; /** * Permissions that incoming peers get based on our listening address they connected to. From 98ba5c796526d924a6033cb00c933e71cd86830d Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 23 Aug 2024 15:36:40 +0200 Subject: [PATCH 03/42] net: split CConnman::BindListenPort() off CConnman Introduce a new low-level socket managing class `SockMan` and move the `CConnman::BindListenPort()` method to it. --- src/CMakeLists.txt | 1 + src/common/sockman.cpp | 85 ++++++++++++++++++++++++++++++++++++++++++ src/common/sockman.h | 44 ++++++++++++++++++++++ src/net.cpp | 72 +---------------------------------- src/net.h | 9 +---- 5 files changed, 133 insertions(+), 78 deletions(-) create mode 100644 src/common/sockman.cpp create mode 100644 src/common/sockman.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index b762b03880d..ed37c69e555 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -123,6 +123,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL common/run_command.cpp common/settings.cpp common/signmessage.cpp + common/sockman.cpp common/system.cpp common/url.cpp compressor.cpp diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp new file mode 100644 index 00000000000..25527db8e07 --- /dev/null +++ b/src/common/sockman.cpp @@ -0,0 +1,85 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit/. + +#include // IWYU pragma: keep + +#include +#include +#include +#include + +bool SockMan::BindListenPort(const CService& addrBind, bilingual_str& strError) +{ + int nOne = 1; + + // Create socket for listening for incoming connections + struct sockaddr_storage sockaddr; + socklen_t len = sizeof(sockaddr); + if (!addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)) + { + strError = Untranslated(strprintf("Bind address family for %s not supported", addrBind.ToStringAddrPort())); + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + return false; + } + + std::unique_ptr sock = CreateSock(addrBind.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP); + if (!sock) { + strError = Untranslated(strprintf("Couldn't open socket for incoming connections (socket returned error %s)", NetworkErrorString(WSAGetLastError()))); + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + return false; + } + + // 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, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { + strError = Untranslated(strprintf("Error setting SO_REUSEADDR on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); + LogPrintf("%s\n", strError.original); + } + + // 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 (addrBind.IsIPv6()) { +#ifdef IPV6_V6ONLY + if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { + strError = Untranslated(strprintf("Error setting IPV6_V6ONLY on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); + LogPrintf("%s\n", strError.original); + } +#endif +#ifdef WIN32 + int nProtLevel = PROTECTION_LEVEL_UNRESTRICTED; + if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_PROTECTION_LEVEL, (const char*)&nProtLevel, sizeof(int)) == SOCKET_ERROR) { + strError = Untranslated(strprintf("Error setting IPV6_PROTECTION_LEVEL on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); + LogPrintf("%s\n", strError.original); + } +#endif + } + + if (sock->Bind(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { + int nErr = WSAGetLastError(); + if (nErr == WSAEADDRINUSE) + strError = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), addrBind.ToStringAddrPort(), CLIENT_NAME); + else + strError = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), addrBind.ToStringAddrPort(), NetworkErrorString(nErr)); + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + return false; + } + LogPrintf("Bound to %s\n", addrBind.ToStringAddrPort()); + + // Listen for incoming connections + if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) + { + strError = strprintf(_("Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); + LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + return false; + } + + m_listen.emplace_back(std::move(sock)); + + return true; +} + +void SockMan::StopListening() +{ + m_listen.clear(); +} diff --git a/src/common/sockman.h b/src/common/sockman.h new file mode 100644 index 00000000000..84499cdacd9 --- /dev/null +++ b/src/common/sockman.h @@ -0,0 +1,44 @@ +// Copyright (c) 2024-present The Bitcoin Core developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or https://opensource.org/license/mit/. + +#ifndef BITCOIN_COMMON_SOCKMAN_H +#define BITCOIN_COMMON_SOCKMAN_H + +#include +#include +#include + +#include +#include + +/** + * A socket manager class which handles socket operations. + * To use this class, inherit from it and implement the pure virtual methods. + * Handled operations: + * - binding and listening on sockets + */ +class SockMan +{ +public: + /** + * Bind to a new address:port, start listening and add the listen socket to `m_listen`. + * @param[in] addrBind Where to bind. + * @param[out] strError Error string if an error occurs. + * @retval true Success. + * @retval false Failure, `strError` will be set. + */ + bool BindListenPort(const CService& addrBind, bilingual_str& strError); + + /** + * Stop listening by closing all listening sockets. + */ + void StopListening(); + + /** + * List of listening sockets. + */ + std::vector> m_listen; +}; + +#endif // BITCOIN_COMMON_SOCKMAN_H diff --git a/src/net.cpp b/src/net.cpp index cccb7376562..67d5aea0266 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3114,76 +3114,6 @@ void CConnman::ThreadI2PAcceptIncoming() } } -bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError) -{ - int nOne = 1; - - // Create socket for listening for incoming connections - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - if (!addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)) - { - strError = Untranslated(strprintf("Bind address family for %s not supported", addrBind.ToStringAddrPort())); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); - return false; - } - - std::unique_ptr sock = CreateSock(addrBind.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP); - if (!sock) { - strError = Untranslated(strprintf("Couldn't open socket for incoming connections (socket returned error %s)", NetworkErrorString(WSAGetLastError()))); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); - return false; - } - - // 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, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting SO_REUSEADDR on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); - } - - // 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 (addrBind.IsIPv6()) { -#ifdef IPV6_V6ONLY - if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting IPV6_V6ONLY on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); - } -#endif -#ifdef WIN32 - int nProtLevel = PROTECTION_LEVEL_UNRESTRICTED; - if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_PROTECTION_LEVEL, (const char*)&nProtLevel, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting IPV6_PROTECTION_LEVEL on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); - } -#endif - } - - if (sock->Bind(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { - int nErr = WSAGetLastError(); - if (nErr == WSAEADDRINUSE) - strError = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), addrBind.ToStringAddrPort(), CLIENT_NAME); - else - strError = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), addrBind.ToStringAddrPort(), NetworkErrorString(nErr)); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); - return false; - } - LogPrintf("Bound to %s\n", addrBind.ToStringAddrPort()); - - // Listen for incoming connections - if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) - { - strError = strprintf(_("Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); - return false; - } - - m_listen.emplace_back(std::move(sock)); - - return true; -} - void Discover() { if (!fDiscover) @@ -3489,7 +3419,7 @@ void CConnman::StopNodes() } m_nodes_disconnected.clear(); m_listen_permissions.clear(); - m_listen.clear(); + StopListening(); semOutbound.reset(); semAddnode.reset(); } diff --git a/src/net.h b/src/net.h index bbee5b1fb71..cc9b82421ef 100644 --- a/src/net.h +++ b/src/net.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -1048,7 +1049,7 @@ protected: ~NetEventsInterface() = default; }; -class CConnman +class CConnman : private SockMan { public: @@ -1277,7 +1278,6 @@ private: //! in case of no limit, it will always return 0 std::chrono::seconds GetMaxOutboundTimeLeftInCycle_() const EXCLUSIVE_LOCKS_REQUIRED(m_total_bytes_sent_mutex); - bool BindListenPort(const CService& bindAddr, bilingual_str& strError); bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions); bool InitBinds(const Options& options); @@ -1417,11 +1417,6 @@ private: unsigned int nSendBufferMaxSize{0}; unsigned int nReceiveFloodSize{0}; - /** - * List of listening sockets. - */ - std::vector> m_listen; - /** * Permissions that incoming peers get based on our listening address they connected to. */ From 9d4e7e3bd7040059210fda89ed824b4dd56c4a11 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 17 Sep 2024 15:05:46 +0200 Subject: [PATCH 04/42] style: modernize the style of SockMan::BindListenPort() It was copied verbatim from `CConnman::BindListenPort()` in the previous commit. Modernize its variables and style and log the error messages from the caller. Also categorize the informative messages to the "net" category because they are quite specific to the networking layer. --- src/common/sockman.cpp | 82 +++++++++++++++++++-------------- src/common/sockman.h | 8 ++-- src/net.cpp | 5 +- test/functional/feature_port.py | 10 ++-- 4 files changed, 60 insertions(+), 45 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 25527db8e07..bd661789aff 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -9,68 +9,80 @@ #include #include -bool SockMan::BindListenPort(const CService& addrBind, bilingual_str& strError) +bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) { - int nOne = 1; - // Create socket for listening for incoming connections - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - if (!addrBind.GetSockAddr((struct sockaddr*)&sockaddr, &len)) - { - strError = Untranslated(strprintf("Bind address family for %s not supported", addrBind.ToStringAddrPort())); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + sockaddr_storage storage; + socklen_t len{sizeof(storage)}; + if (!to.GetSockAddr(reinterpret_cast(&storage), &len)) { + err_msg = Untranslated(strprintf("Bind address family for %s not supported", to.ToStringAddrPort())); return false; } - std::unique_ptr sock = CreateSock(addrBind.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP); + std::unique_ptr sock{CreateSock(to.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP)}; if (!sock) { - strError = Untranslated(strprintf("Couldn't open socket for incoming connections (socket returned error %s)", NetworkErrorString(WSAGetLastError()))); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + err_msg = Untranslated(strprintf("Cannot create %s listen socket: %s", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError()))); return false; } + int one{1}; + // 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, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting SO_REUSEADDR on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); + if (sock->SetSockOpt(SOL_SOCKET, SO_REUSEADDR, reinterpret_cast(&one), sizeof(one)) == SOCKET_ERROR) { + LogPrintLevel(BCLog::NET, + BCLog::Level::Info, + "Cannot set SO_REUSEADDR on %s listen socket: %s, continuing anyway\n", + 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 (addrBind.IsIPv6()) { + if (to.IsIPv6()) { #ifdef IPV6_V6ONLY - if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, (sockopt_arg_type)&nOne, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting IPV6_V6ONLY on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); + if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_V6ONLY, reinterpret_cast(&one), sizeof(one)) == SOCKET_ERROR) { + LogPrintLevel(BCLog::NET, + BCLog::Level::Info, + "Cannot set IPV6_V6ONLY on %s listen socket: %s, continuing anyway\n", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError())); } #endif #ifdef WIN32 - int nProtLevel = PROTECTION_LEVEL_UNRESTRICTED; - if (sock->SetSockOpt(IPPROTO_IPV6, IPV6_PROTECTION_LEVEL, (const char*)&nProtLevel, sizeof(int)) == SOCKET_ERROR) { - strError = Untranslated(strprintf("Error setting IPV6_PROTECTION_LEVEL on socket: %s, continuing anyway", NetworkErrorString(WSAGetLastError()))); - LogPrintf("%s\n", strError.original); + int prot_level{PROTECTION_LEVEL_UNRESTRICTED}; + if (sock->SetSockOpt(IPPROTO_IPV6, + IPV6_PROTECTION_LEVEL, + reinterpret_cast(&prot_level), + sizeof(prot_level)) == SOCKET_ERROR) { + LogPrintLevel(BCLog::NET, + BCLog::Level::Info, + "Cannot set IPV6_PROTECTION_LEVEL on %s listen socket: %s, continuing anyway\n", + to.ToStringAddrPort(), + NetworkErrorString(WSAGetLastError())); } #endif } - if (sock->Bind(reinterpret_cast(&sockaddr), len) == SOCKET_ERROR) { - int nErr = WSAGetLastError(); - if (nErr == WSAEADDRINUSE) - strError = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), addrBind.ToStringAddrPort(), CLIENT_NAME); - else - strError = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), addrBind.ToStringAddrPort(), NetworkErrorString(nErr)); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + if (sock->Bind(reinterpret_cast(&storage), len) == SOCKET_ERROR) { + const int err{WSAGetLastError()}; + if (err == WSAEADDRINUSE) { + err_msg = strprintf(_("Unable to bind to %s on this computer. %s is probably already running."), + to.ToStringAddrPort(), + CLIENT_NAME); + } else { + err_msg = strprintf(_("Unable to bind to %s on this computer (bind returned error %s)"), + to.ToStringAddrPort(), + NetworkErrorString(err)); + } return false; } - LogPrintf("Bound to %s\n", addrBind.ToStringAddrPort()); // Listen for incoming connections - if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) - { - strError = strprintf(_("Listening for incoming connections failed (listen returned error %s)"), NetworkErrorString(WSAGetLastError())); - LogPrintLevel(BCLog::NET, BCLog::Level::Error, "%s\n", strError.original); + if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) { + err_msg = strprintf(_("Cannot listen on %s: %s"), to.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); return false; } diff --git a/src/common/sockman.h b/src/common/sockman.h index 84499cdacd9..b977d49f42b 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -23,12 +23,12 @@ class SockMan public: /** * Bind to a new address:port, start listening and add the listen socket to `m_listen`. - * @param[in] addrBind Where to bind. - * @param[out] strError Error string if an error occurs. + * @param[in] to Where to bind. + * @param[out] err_msg Error string if an error occurs. * @retval true Success. - * @retval false Failure, `strError` will be set. + * @retval false Failure, `err_msg` will be set. */ - bool BindListenPort(const CService& addrBind, bilingual_str& strError); + bool BindAndStartListening(const CService& to, bilingual_str& err_msg); /** * Stop listening by closing all listening sockets. diff --git a/src/net.cpp b/src/net.cpp index 67d5aea0266..1181f4a6555 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3176,13 +3176,16 @@ bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlag const CService addr{MaybeFlipIPv6toCJDNS(addr_)}; bilingual_str strError; - if (!BindListenPort(addr, strError)) { + if (!BindAndStartListening(addr, strError)) { + LogError("%s", strError.original); if ((flags & BF_REPORT_ERROR) && m_client_interface) { m_client_interface->ThreadSafeMessageBox(strError, "", CClientUIInterface::MSG_ERROR); } return false; } + LogPrintLevel(BCLog::NET, BCLog::Level::Info, "Bound to and listening on %s", addr.ToStringAddrPort()); + m_listen_permissions.emplace(addr, permissions); if (addr.IsRoutable() && fDiscover && !(flags & BF_DONT_ADVERTISE) && !NetPermissions::HasFlag(permissions, NetPermissionFlags::NoBan)) { diff --git a/test/functional/feature_port.py b/test/functional/feature_port.py index 2746d7d79c1..1317583c4ca 100755 --- a/test/functional/feature_port.py +++ b/test/functional/feature_port.py @@ -29,23 +29,23 @@ class PortTest(BitcoinTestFramework): port2 = p2p_port(self.num_nodes + 5) self.log.info("When starting with -port, bitcoind binds to it and uses port + 1 for an onion bind") - with node.assert_debug_log(expected_msgs=[f'Bound to 0.0.0.0:{port1}', f'Bound to 127.0.0.1:{port1 + 1}']): + with node.assert_debug_log(expected_msgs=[f'Bound to and listening on 0.0.0.0:{port1}', f'Bound to and listening on 127.0.0.1:{port1 + 1}']): self.restart_node(0, extra_args=["-listen", f"-port={port1}"]) self.log.info("When specifying -port multiple times, only the last one is taken") - with node.assert_debug_log(expected_msgs=[f'Bound to 0.0.0.0:{port2}', f'Bound to 127.0.0.1:{port2 + 1}'], unexpected_msgs=[f'Bound to 0.0.0.0:{port1}']): + with node.assert_debug_log(expected_msgs=[f'Bound to and listening on 0.0.0.0:{port2}', f'Bound to and listening on 127.0.0.1:{port2 + 1}'], unexpected_msgs=[f'Bound to and listening on 0.0.0.0:{port1}']): self.restart_node(0, extra_args=["-listen", f"-port={port1}", f"-port={port2}"]) self.log.info("When specifying ports with both -port and -bind, the one from -port is ignored") - with node.assert_debug_log(expected_msgs=[f'Bound to 0.0.0.0:{port2}'], unexpected_msgs=[f'Bound to 0.0.0.0:{port1}']): + with node.assert_debug_log(expected_msgs=[f'Bound to and listening on 0.0.0.0:{port2}'], unexpected_msgs=[f'Bound to and listening on 0.0.0.0:{port1}']): self.restart_node(0, extra_args=["-listen", f"-port={port1}", f"-bind=0.0.0.0:{port2}"]) self.log.info("When -bind specifies no port, the values from -port and -bind are combined") - with self.nodes[0].assert_debug_log(expected_msgs=[f'Bound to 0.0.0.0:{port1}']): + with self.nodes[0].assert_debug_log(expected_msgs=[f'Bound to and listening on 0.0.0.0:{port1}']): self.restart_node(0, extra_args=["-listen", f"-port={port1}", "-bind=0.0.0.0"]) self.log.info("When an onion bind specifies no port, the value from -port, incremented by 1, is taken") - with self.nodes[0].assert_debug_log(expected_msgs=[f'Bound to 127.0.0.1:{port1 + 1}']): + with self.nodes[0].assert_debug_log(expected_msgs=[f'Bound to and listening on 127.0.0.1:{port1 + 1}']): self.restart_node(0, extra_args=["-listen", f"-port={port1}", "-bind=127.0.0.1=onion"]) self.log.info("Invalid values for -port raise errors") From c31fc1a993a2843e776a52494da37e686b27462c Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 26 Aug 2024 13:14:53 +0200 Subject: [PATCH 05/42] net: split CConnman::AcceptConnection() off CConnman Move the `CConnman::AcceptConnection()` method to `SockMan` and split parts of it: * the flip-to-CJDNS part: to just after the `AcceptConnection()` call * the permissions part: at the start of `CreateNodeFromAcceptedSocket()` --- src/common/sockman.cpp | 21 ++++++++++++++++++ src/common/sockman.h | 9 ++++++++ src/net.cpp | 49 ++++++++++++++---------------------------- src/net.h | 3 --- 4 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index bd661789aff..2b9dac0b059 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -91,6 +91,27 @@ bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) return true; } +std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) +{ + struct sockaddr_storage sockaddr; + socklen_t len = sizeof(sockaddr); + auto sock = listen_sock.Accept((struct sockaddr*)&sockaddr, &len); + + if (!sock) { + const int nErr = WSAGetLastError(); + if (nErr != WSAEWOULDBLOCK) { + LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); + } + return {}; + } + + if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr, len)) { + LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); + } + + return sock; +} + void SockMan::StopListening() { m_listen.clear(); diff --git a/src/common/sockman.h b/src/common/sockman.h index b977d49f42b..580e5b189e0 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -17,6 +17,7 @@ * To use this class, inherit from it and implement the pure virtual methods. * Handled operations: * - binding and listening on sockets + * - accepting incoming connections */ class SockMan { @@ -30,6 +31,14 @@ public: */ bool BindAndStartListening(const CService& to, bilingual_str& err_msg); + /** + * Accept a connection. + * @param[in] listen_sock Socket on which to accept the connection. + * @param[out] addr Address of the peer that was accepted. + * @return Newly created socket for the accepted connection. + */ + std::unique_ptr AcceptConnection(const Sock& listen_sock, CService& addr); + /** * Stop listening by closing all listening sockets. */ diff --git a/src/net.cpp b/src/net.cpp index 1181f4a6555..e38b75f283b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1732,27 +1732,11 @@ bool CConnman::AttemptToEvictConnection() return false; } -void CConnman::AcceptConnection(const Sock& listen_sock) { - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - auto sock = listen_sock.Accept((struct sockaddr*)&sockaddr, &len); - - if (!sock) { - const int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK) { - LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); - } - return; - } - - CService addr; - if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr, len)) { - LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); - } else { - addr = MaybeFlipIPv6toCJDNS(addr); - } - - const CService addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(*sock))}; +void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, + const CService& addr_bind, + const CService& addr) +{ + int nInbound = 0; NetPermissionFlags permission_flags = NetPermissionFlags::None; auto it{m_listen_permissions.find(addr_bind)}; @@ -1760,16 +1744,6 @@ void CConnman::AcceptConnection(const Sock& listen_sock) { NetPermissions::AddFlag(permission_flags, it->second); } - CreateNodeFromAcceptedSocket(std::move(sock), permission_flags, addr_bind, addr); -} - -void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, - NetPermissionFlags permission_flags, - const CService& addr_bind, - const CService& addr) -{ - int nInbound = 0; - AddWhitelistPermissionFlags(permission_flags, addr, vWhitelistedRangeIncoming); { @@ -2219,7 +2193,16 @@ void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock } const auto it = events_per_sock.find(sock); if (it != events_per_sock.end() && it->second.occurred & Sock::RECV) { - AcceptConnection(*sock); + CService addr_accepted; + + auto sock_accepted{AcceptConnection(*sock, addr_accepted)}; + + if (sock_accepted) { + addr_accepted = MaybeFlipIPv6toCJDNS(addr_accepted); + const CService addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(*sock))}; + + CreateNodeFromAcceptedSocket(std::move(sock_accepted), addr_bind, addr_accepted); + } } } } @@ -3108,7 +3091,7 @@ void CConnman::ThreadI2PAcceptIncoming() continue; } - CreateNodeFromAcceptedSocket(std::move(conn.sock), NetPermissionFlags::None, conn.me, conn.peer); + CreateNodeFromAcceptedSocket(std::move(conn.sock), conn.me, conn.peer); err_wait = err_wait_begin; } diff --git a/src/net.h b/src/net.h index cc9b82421ef..01fc644e40c 100644 --- a/src/net.h +++ b/src/net.h @@ -1287,18 +1287,15 @@ private: void ThreadOpenConnections(std::vector connect, Span seed_nodes) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !m_reconnections_mutex); void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); void ThreadI2PAcceptIncoming(); - void AcceptConnection(const Sock& listen_sock); /** * Create a `CNode` object from a socket that has just been accepted and add the node to * the `m_nodes` member. * @param[in] sock Connected socket to communicate with the peer. - * @param[in] permission_flags The peer's permissions. * @param[in] addr_bind The address and port at our side of the connection. * @param[in] addr The address and port at the peer's side of the connection. */ void CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, - NetPermissionFlags permission_flags, const CService& addr_bind, const CService& addr); From 221c9224b223120e64400afd1f8970489fcac231 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 17 Sep 2024 17:29:07 +0200 Subject: [PATCH 06/42] style: modernize the style of SockMan::AcceptConnection() --- src/common/sockman.cpp | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 2b9dac0b059..830d3777f41 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -93,19 +93,23 @@ bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) { - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - auto sock = listen_sock.Accept((struct sockaddr*)&sockaddr, &len); + sockaddr_storage storage; + socklen_t len{sizeof(storage)}; + + auto sock{listen_sock.Accept(reinterpret_cast(&storage), &len)}; if (!sock) { - const int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK) { - LogPrintf("socket error accept failed: %s\n", NetworkErrorString(nErr)); + const int err{WSAGetLastError()}; + if (err != WSAEWOULDBLOCK) { + LogPrintLevel(BCLog::NET, + BCLog::Level::Error, + "Cannot accept new connection: %s\n", + NetworkErrorString(err)); } return {}; } - if (!addr.SetSockAddr((const struct sockaddr*)&sockaddr, len)) { + if (!addr.SetSockAddr(reinterpret_cast(&storage), len)) { LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); } From 25203720a1e023b38a0e8d0ced88fe33f323a133 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 27 Aug 2024 11:25:24 +0200 Subject: [PATCH 07/42] net: move the generation of ids for new nodes from CConnman to SockMan Move `CConnman::GetNewNodeId()` to `SockMan::GetNewId()`. Avoid using the word "node" because that is too specific for `CConnman`. --- src/common/sockman.cpp | 5 +++++ src/common/sockman.h | 18 ++++++++++++++++++ src/net.cpp | 9 ++------- src/net.h | 5 +---- src/node/txreconciliation.cpp | 2 +- 5 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 830d3777f41..4f64910cc86 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -116,6 +116,11 @@ std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CServic return sock; } +SockMan::Id SockMan::GetNewId() +{ + return m_next_id.fetch_add(1, std::memory_order_relaxed); +} + void SockMan::StopListening() { m_listen.clear(); diff --git a/src/common/sockman.h b/src/common/sockman.h index 580e5b189e0..633ee5638be 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -22,6 +23,11 @@ class SockMan { public: + /** + * Each connection is assigned an unique id of this type. + */ + using Id = int64_t; + /** * Bind to a new address:port, start listening and add the listen socket to `m_listen`. * @param[in] to Where to bind. @@ -39,6 +45,11 @@ public: */ std::unique_ptr AcceptConnection(const Sock& listen_sock, CService& addr); + /** + * Generate an id for a newly created connection. + */ + Id GetNewId(); + /** * Stop listening by closing all listening sockets. */ @@ -48,6 +59,13 @@ public: * List of listening sockets. */ std::vector> m_listen; + +private: + + /** + * The id to assign to the next created connection. Used to generate ids of connections. + */ + std::atomic m_next_id{0}; }; #endif // BITCOIN_COMMON_SOCKMAN_H diff --git a/src/net.cpp b/src/net.cpp index e38b75f283b..aef680b68ea 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -526,7 +526,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo AddWhitelistPermissionFlags(permission_flags, target_addr, whitelist_permissions); // Add node - NodeId id = GetNewNodeId(); + NodeId id = GetNewId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); if (!addr_bind.IsValid()) { addr_bind = GetBindAddress(*sock); @@ -1796,7 +1796,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, } } - NodeId id = GetNewNodeId(); + NodeId id = GetNewId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); const bool inbound_onion = std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); @@ -3138,11 +3138,6 @@ CConnman::CConnman(uint64_t nSeed0In, uint64_t nSeed1In, AddrMan& addrman_in, SetNetworkActive(network_active); } -NodeId CConnman::GetNewNodeId() -{ - return nLastNodeId.fetch_add(1, std::memory_order_relaxed); -} - uint16_t CConnman::GetDefaultPort(Network net) const { return net == NET_I2P ? I2P_SAM31_PORT : m_params.GetDefaultPort(); diff --git a/src/net.h b/src/net.h index 01fc644e40c..a160e31eba9 100644 --- a/src/net.h +++ b/src/net.h @@ -95,7 +95,7 @@ static const size_t DEFAULT_MAXSENDBUFFER = 1 * 1000; static constexpr bool DEFAULT_V2_TRANSPORT{true}; -typedef int64_t NodeId; +using NodeId = SockMan::Id; struct AddedNodeParams { std::string m_added_node; @@ -1352,8 +1352,6 @@ private: void DeleteNode(CNode* pnode); - NodeId GetNewNodeId(); - /** (Try to) send data from node's vSendMsg. Returns (bytes_sent, data_left). */ std::pair SocketSendData(CNode& node) const EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend); @@ -1433,7 +1431,6 @@ private: std::vector m_nodes GUARDED_BY(m_nodes_mutex); std::list m_nodes_disconnected; mutable RecursiveMutex m_nodes_mutex; - std::atomic nLastNodeId{0}; unsigned int nPrevNodeCount{0}; // Stores number of full-tx connections (outbound and manual) per network diff --git a/src/node/txreconciliation.cpp b/src/node/txreconciliation.cpp index e6e19c5756b..9e0d62a2343 100644 --- a/src/node/txreconciliation.cpp +++ b/src/node/txreconciliation.cpp @@ -87,7 +87,7 @@ public: LogPrintLevel(BCLog::TXRECONCILIATION, BCLog::Level::Debug, "Pre-register peer=%d\n", peer_id); const uint64_t local_salt{FastRandomContext().rand64()}; - // We do this exactly once per peer (which are unique by NodeId, see GetNewNodeId) so it's + // We do this exactly once per peer (which are unique by id, see SockMan::GetNewId()) so it's // safe to assume we don't have this record yet. Assume(m_states.emplace(peer_id, local_salt).second); return local_salt; From a367d556fd59dd54e491316acf3b4b3c7184f0e3 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 27 Aug 2024 16:11:35 +0200 Subject: [PATCH 08/42] net: move CConnman-specific parts away from ThreadI2PAcceptIncoming() CConnman-specific or in other words, Bitcoin P2P specific. Now the `ThreadI2PAcceptIncoming()` method is protocol agnostic and can be moved to `SockMan`. --- src/common/sockman.cpp | 2 ++ src/common/sockman.h | 34 ++++++++++++++++++++++++++++++++++ src/net.cpp | 29 ++++++++++++++++++++--------- src/net.h | 6 ++++++ 4 files changed, 62 insertions(+), 9 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 4f64910cc86..f6fc85f20d2 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -125,3 +125,5 @@ void SockMan::StopListening() { m_listen.clear(); } + +void SockMan::EventI2PStatus(const CService&, I2PStatus) {} diff --git a/src/common/sockman.h b/src/common/sockman.h index 633ee5638be..329b5fbbb3a 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -28,6 +28,23 @@ public: */ using Id = int64_t; + /** + * Possible status changes that can be passed to `EventI2PStatus()`. + */ + enum class I2PStatus : uint8_t { + /// The listen succeeded and we are now listening for incoming I2P connections. + START_LISTENING, + + /// The listen failed and now we are not listening (even if START_LISTENING was signaled before). + STOP_LISTENING, + }; + + virtual ~SockMan() = default; + + // + // Non-virtual functions, to be reused by children classes. + // + /** * Bind to a new address:port, start listening and add the listen socket to `m_listen`. * @param[in] to Where to bind. @@ -62,6 +79,23 @@ public: private: + // + // Pure virtual functions must be implemented by children classes. + // + + // + // Non-pure virtual functions can be overridden by children classes or left + // alone to use the default implementation from SockMan. + // + + /** + * Be notified of a change in the state of the I2P connectivity. + * The default behavior, implemented by `SockMan`, is to ignore this event. + * @param[in] addr The address we started or stopped listening on. + * @param[in] new_status New status. + */ + virtual void EventI2PStatus(const CService& addr, I2PStatus new_status); + /** * The id to assign to the next created connection. Used to generate ids of connections. */ diff --git a/src/net.cpp b/src/net.cpp index aef680b68ea..fd398e08b48 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -3054,13 +3054,30 @@ void CConnman::ThreadMessageHandler() } } +void CConnman::EventI2PStatus(const CService& addr, SockMan::I2PStatus new_status) +{ + switch (new_status) { + case SockMan::I2PStatus::START_LISTENING: + if (!m_i2p_advertising_listen_addr) { + AddLocal(addr, LOCAL_MANUAL); + m_i2p_advertising_listen_addr = true; + } + break; + case SockMan::I2PStatus::STOP_LISTENING: + if (m_i2p_advertising_listen_addr && addr.IsValid()) { + RemoveLocal(addr); + m_i2p_advertising_listen_addr = false; + } + break; + } +} + void CConnman::ThreadI2PAcceptIncoming() { static constexpr auto err_wait_begin = 1s; static constexpr auto err_wait_cap = 5min; auto err_wait = err_wait_begin; - bool advertising_listen_addr = false; i2p::Connection conn; auto SleepOnFailure = [&]() { @@ -3073,18 +3090,12 @@ void CConnman::ThreadI2PAcceptIncoming() while (!interruptNet) { if (!m_i2p_sam_session->Listen(conn)) { - if (advertising_listen_addr && conn.me.IsValid()) { - RemoveLocal(conn.me); - advertising_listen_addr = false; - } + EventI2PStatus(conn.me, SockMan::I2PStatus::STOP_LISTENING); SleepOnFailure(); continue; } - if (!advertising_listen_addr) { - AddLocal(conn.me, LOCAL_MANUAL); - advertising_listen_addr = true; - } + EventI2PStatus(conn.me, SockMan::I2PStatus::START_LISTENING); if (!m_i2p_sam_session->Accept(conn)) { SleepOnFailure(); diff --git a/src/net.h b/src/net.h index a160e31eba9..0edf6acfbb7 100644 --- a/src/net.h +++ b/src/net.h @@ -1286,6 +1286,12 @@ private: void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_unused_i2p_sessions_mutex); void ThreadOpenConnections(std::vector connect, Span seed_nodes) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !m_reconnections_mutex); void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); + + /// Whether we are currently advertising our I2P address (via `AddLocal()`). + bool m_i2p_advertising_listen_addr{false}; + + virtual void EventI2PStatus(const CService& addr, SockMan::I2PStatus new_status) override; + void ThreadI2PAcceptIncoming(); /** From be1d7418c15e275136161bb965d406691c961892 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 27 Aug 2024 16:23:31 +0200 Subject: [PATCH 09/42] net: move I2P-accept-incoming code from CConnman to SockMan --- src/CMakeLists.txt | 2 +- src/common/sockman.cpp | 55 +++++++++++++++++++++++++++++++ src/common/sockman.h | 74 ++++++++++++++++++++++++++++++++++++++++++ src/net.cpp | 69 +++++++++------------------------------ src/net.h | 31 ++++-------------- 5 files changed, 151 insertions(+), 80 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ed37c69e555..9fd3e48a06b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -131,6 +131,7 @@ add_library(bitcoin_common STATIC EXCLUDE_FROM_ALL core_write.cpp deploymentinfo.cpp external_signer.cpp + i2p.cpp init/common.cpp kernel/chainparams.cpp key.cpp @@ -208,7 +209,6 @@ add_library(bitcoin_node STATIC EXCLUDE_FROM_ALL headerssync.cpp httprpc.cpp httpserver.cpp - i2p.cpp index/base.cpp index/blockfilterindex.cpp index/coinstatsindex.cpp diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index f6fc85f20d2..a534971e6c2 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -8,6 +8,7 @@ #include #include #include +#include bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) { @@ -91,6 +92,24 @@ bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) return true; } +void SockMan::StartSocketsThreads(const Options& options) +{ + if (options.i2p.has_value()) { + m_i2p_sam_session = std::make_unique( + options.i2p->private_key_file, options.i2p->sam_proxy, &interruptNet); + + m_thread_i2p_accept = + std::thread(&util::TraceThread, options.i2p->accept_thread_name, [this] { ThreadI2PAccept(); }); + } +} + +void SockMan::JoinSocketsThreads() +{ + if (m_thread_i2p_accept.joinable()) { + m_thread_i2p_accept.join(); + } +} + std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) { sockaddr_storage storage; @@ -127,3 +146,39 @@ void SockMan::StopListening() } void SockMan::EventI2PStatus(const CService&, I2PStatus) {} + +void SockMan::ThreadI2PAccept() +{ + static constexpr auto err_wait_begin = 1s; + static constexpr auto err_wait_cap = 5min; + auto err_wait = err_wait_begin; + + i2p::Connection conn; + + auto SleepOnFailure = [&]() { + interruptNet.sleep_for(err_wait); + if (err_wait < err_wait_cap) { + err_wait += 1s; + } + }; + + while (!interruptNet) { + + if (!m_i2p_sam_session->Listen(conn)) { + EventI2PStatus(conn.me, SockMan::I2PStatus::STOP_LISTENING); + SleepOnFailure(); + continue; + } + + EventI2PStatus(conn.me, SockMan::I2PStatus::START_LISTENING); + + if (!m_i2p_sam_session->Accept(conn)) { + SleepOnFailure(); + continue; + } + + EventNewConnectionAccepted(std::move(conn.sock), conn.me, conn.peer); + + err_wait = err_wait_begin; + } +} diff --git a/src/common/sockman.h b/src/common/sockman.h index 329b5fbbb3a..6e09f777ac8 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -5,12 +5,16 @@ #ifndef BITCOIN_COMMON_SOCKMAN_H #define BITCOIN_COMMON_SOCKMAN_H +#include #include +#include +#include #include #include #include #include +#include #include /** @@ -18,6 +22,7 @@ * To use this class, inherit from it and implement the pure virtual methods. * Handled operations: * - binding and listening on sockets + * - starting of necessary threads to process socket operations * - accepting incoming connections */ class SockMan @@ -47,6 +52,7 @@ public: /** * Bind to a new address:port, start listening and add the listen socket to `m_listen`. + * Should be called before `StartSocketsThreads()`. * @param[in] to Where to bind. * @param[out] err_msg Error string if an error occurs. * @retval true Success. @@ -54,6 +60,38 @@ public: */ bool BindAndStartListening(const CService& to, bilingual_str& err_msg); + /** + * Options to influence `StartSocketsThreads()`. + */ + struct Options { + struct I2P { + explicit I2P(const fs::path& file, const Proxy& proxy, std::string_view accept_thread_name) + : private_key_file{file}, + sam_proxy{proxy}, + accept_thread_name{accept_thread_name} + {} + + const fs::path private_key_file; + const Proxy sam_proxy; + const std::string_view accept_thread_name; + }; + + /** + * I2P options. If set then a thread will be started that will accept incoming I2P connections. + */ + std::optional i2p; + }; + + /** + * Start the necessary threads for sockets IO. + */ + void StartSocketsThreads(const Options& options); + + /** + * Join (wait for) the threads started by `StartSocketsThreads()` to exit. + */ + void JoinSocketsThreads(); + /** * Accept a connection. * @param[in] listen_sock Socket on which to accept the connection. @@ -72,6 +110,21 @@ public: */ void StopListening(); + /** + * This is signaled when network activity should cease. + * A pointer to it is saved in `m_i2p_sam_session`, so make sure that + * the lifetime of `interruptNet` is not shorter than + * the lifetime of `m_i2p_sam_session`. + */ + CThreadInterrupt interruptNet; + + /** + * I2P SAM session. + * Used to accept incoming and make outgoing I2P connections from a persistent + * address. + */ + std::unique_ptr m_i2p_sam_session; + /** * List of listening sockets. */ @@ -83,6 +136,16 @@ private: // Pure virtual functions must be implemented by children classes. // + /** + * Be notified when a new connection has been accepted. + * @param[in] sock Connected socket to communicate with the peer. + * @param[in] me The address and port at our side of the connection. + * @param[in] them The address and port at the peer's side of the connection. + */ + virtual void EventNewConnectionAccepted(std::unique_ptr&& sock, + const CService& me, + const CService& them) = 0; + // // Non-pure virtual functions can be overridden by children classes or left // alone to use the default implementation from SockMan. @@ -96,10 +159,21 @@ private: */ virtual void EventI2PStatus(const CService& addr, I2PStatus new_status); + /** + * Accept incoming I2P connections in a loop and call + * `EventNewConnectionAccepted()` for each new connection. + */ + void ThreadI2PAccept(); + /** * The id to assign to the next created connection. Used to generate ids of connections. */ std::atomic m_next_id{0}; + + /** + * Thread that accepts incoming I2P connections in a loop, can be stopped via `interruptNet`. + */ + std::thread m_thread_i2p_accept; }; #endif // BITCOIN_COMMON_SOCKMAN_H diff --git a/src/net.cpp b/src/net.cpp index fd398e08b48..9eefcc43dde 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1732,9 +1732,9 @@ bool CConnman::AttemptToEvictConnection() return false; } -void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, - const CService& addr_bind, - const CService& addr) +void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, + const CService& addr_bind, + const CService& addr) { int nInbound = 0; @@ -2201,7 +2201,7 @@ void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock addr_accepted = MaybeFlipIPv6toCJDNS(addr_accepted); const CService addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(*sock))}; - CreateNodeFromAcceptedSocket(std::move(sock_accepted), addr_bind, addr_accepted); + EventNewConnectionAccepted(std::move(sock_accepted), addr_bind, addr_accepted); } } } @@ -3072,42 +3072,6 @@ void CConnman::EventI2PStatus(const CService& addr, SockMan::I2PStatus new_statu } } -void CConnman::ThreadI2PAcceptIncoming() -{ - static constexpr auto err_wait_begin = 1s; - static constexpr auto err_wait_cap = 5min; - auto err_wait = err_wait_begin; - - i2p::Connection conn; - - auto SleepOnFailure = [&]() { - interruptNet.sleep_for(err_wait); - if (err_wait < err_wait_cap) { - err_wait += 1s; - } - }; - - while (!interruptNet) { - - if (!m_i2p_sam_session->Listen(conn)) { - EventI2PStatus(conn.me, SockMan::I2PStatus::STOP_LISTENING); - SleepOnFailure(); - continue; - } - - EventI2PStatus(conn.me, SockMan::I2PStatus::START_LISTENING); - - if (!m_i2p_sam_session->Accept(conn)) { - SleepOnFailure(); - continue; - } - - CreateNodeFromAcceptedSocket(std::move(conn.sock), conn.me, conn.peer); - - err_wait = err_wait_begin; - } -} - void Discover() { if (!fDiscover) @@ -3232,12 +3196,6 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) return false; } - Proxy i2p_sam; - if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) { - m_i2p_sam_session = std::make_unique(gArgs.GetDataDirNet() / "i2p_private_key", - i2p_sam, &interruptNet); - } - // Randomize the order in which we may query seednode to potentially prevent connecting to the same one every restart (and signal that we have restarted) std::vector seed_nodes = connOptions.vSeedNodes; if (!seed_nodes.empty()) { @@ -3283,6 +3241,15 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) // Send and receive from sockets, accept connections threadSocketHandler = std::thread(&util::TraceThread, "net", [this] { ThreadSocketHandler(); }); + SockMan::Options sockman_options; + + Proxy i2p_sam; + if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) { + sockman_options.i2p.emplace(gArgs.GetDataDirNet() / "i2p_private_key", i2p_sam, "i2paccept"); + } + + StartSocketsThreads(sockman_options); + if (!gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED)) LogPrintf("DNS seeding disabled\n"); else @@ -3308,11 +3275,6 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) // Process messages threadMessageHandler = std::thread(&util::TraceThread, "msghand", [this] { ThreadMessageHandler(); }); - if (m_i2p_sam_session) { - threadI2PAcceptIncoming = - std::thread(&util::TraceThread, "i2paccept", [this] { ThreadI2PAcceptIncoming(); }); - } - // Dump network addresses scheduler.scheduleEvery([this] { DumpAddresses(); }, DUMP_PEERS_INTERVAL); @@ -3366,9 +3328,8 @@ void CConnman::Interrupt() void CConnman::StopThreads() { - if (threadI2PAcceptIncoming.joinable()) { - threadI2PAcceptIncoming.join(); - } + JoinSocketsThreads(); + if (threadMessageHandler.joinable()) threadMessageHandler.join(); if (threadOpenConnections.joinable()) diff --git a/src/net.h b/src/net.h index 0edf6acfbb7..aa9c3574f0f 100644 --- a/src/net.h +++ b/src/net.h @@ -1292,18 +1292,15 @@ private: virtual void EventI2PStatus(const CService& addr, SockMan::I2PStatus new_status) override; - void ThreadI2PAcceptIncoming(); - /** - * Create a `CNode` object from a socket that has just been accepted and add the node to - * the `m_nodes` member. + * Create a `CNode` object and add it to the `m_nodes` member. * @param[in] sock Connected socket to communicate with the peer. - * @param[in] addr_bind The address and port at our side of the connection. - * @param[in] addr The address and port at the peer's side of the connection. + * @param[in] me The address and port at our side of the connection. + * @param[in] them The address and port at the peer's side of the connection. */ - void CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, - const CService& addr_bind, - const CService& addr); + virtual void EventNewConnectionAccepted(std::unique_ptr&& sock, + const CService& me, + const CService& them) override; void DisconnectNodes() EXCLUSIVE_LOCKS_REQUIRED(!m_reconnections_mutex, !m_nodes_mutex); void NotifyNumConnectionsChanged(); @@ -1531,27 +1528,11 @@ private: Mutex mutexMsgProc; std::atomic flagInterruptMsgProc{false}; - /** - * This is signaled when network activity should cease. - * A pointer to it is saved in `m_i2p_sam_session`, so make sure that - * the lifetime of `interruptNet` is not shorter than - * the lifetime of `m_i2p_sam_session`. - */ - CThreadInterrupt interruptNet; - - /** - * I2P SAM session. - * Used to accept incoming and make outgoing I2P connections from a persistent - * address. - */ - std::unique_ptr m_i2p_sam_session; - std::thread threadDNSAddressSeed; std::thread threadSocketHandler; std::thread threadOpenAddedConnections; std::thread threadOpenConnections; std::thread threadMessageHandler; - std::thread threadI2PAcceptIncoming; /** flag for deciding to connect to an extra outbound peer, * in excess of m_max_outbound_full_relay From 93df2db96d60b3a2fb1a3af522564f6806e8d360 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Fri, 20 Sep 2024 13:27:28 +0200 Subject: [PATCH 10/42] net: index nodes in CConnman by id Change `CConnman::m_nodes` from `std::vector` to `std::unordered_map` because interaction between `CConnman` and `SockMan` is going to be based on `NodeId` and finding a node by its id would better be fast. Change `PeerManagerImpl::EvictExtraOutboundPeers()` to account for nodes no longer always being in order of id. The old code would have failed to update `next_youngest_peer` correctly if `CConnman::m_nodes` hadn't always had nodes in ascending order of id. During fuzzing make sure that we don't generate duplicate `CNode` ids. The easiest way to do that is to use sequential ids. As a nice side effect the existent search-by-id operations in `CConnman::AttemptToEvictConnection()`, `CConnman::DisconnectNode()` and `CConnman::ForNode()` now become `O(1)` (were `O(number of nodes)`), as well as the erase in `CConnman::DisconnectNodes()`. --- src/net.cpp | 161 +++++++++++++------------ src/net.h | 12 +- src/net_processing.cpp | 13 +- src/rpc/net.cpp | 4 + src/test/fuzz/connman.cpp | 5 +- src/test/net_peer_connection_tests.cpp | 10 +- src/test/util/net.h | 8 +- 7 files changed, 115 insertions(+), 98 deletions(-) diff --git a/src/net.cpp b/src/net.cpp index 9eefcc43dde..1b42ac34b37 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -335,7 +335,7 @@ bool IsLocal(const CService& addr) CNode* CConnman::FindNode(const CNetAddr& ip) { LOCK(m_nodes_mutex); - for (CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (static_cast(pnode->addr) == ip) { return pnode; } @@ -346,7 +346,7 @@ CNode* CConnman::FindNode(const CNetAddr& ip) CNode* CConnman::FindNode(const std::string& addrName) { LOCK(m_nodes_mutex); - for (CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->m_addr_name == addrName) { return pnode; } @@ -357,7 +357,7 @@ CNode* CConnman::FindNode(const std::string& addrName) CNode* CConnman::FindNode(const CService& addr) { LOCK(m_nodes_mutex); - for (CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (static_cast(pnode->addr) == addr) { return pnode; } @@ -373,7 +373,7 @@ bool CConnman::AlreadyConnectedToAddress(const CAddress& addr) bool CConnman::CheckIncomingNonce(uint64_t nonce) { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (!pnode->fSuccessfullyConnected && !pnode->IsInboundConn() && pnode->GetLocalNonce() == nonce) return false; } @@ -1689,11 +1689,11 @@ bool CConnman::AttemptToEvictConnection() { LOCK(m_nodes_mutex); - for (const CNode* node : m_nodes) { + for (const auto& [id, node] : m_nodes) { if (node->fDisconnect) continue; NodeEvictionCandidate candidate{ - .id = node->GetId(), + .id = id, .m_connected = node->m_connected, .m_min_ping_time = node->m_min_ping_time, .m_last_block_time = node->m_last_block_time, @@ -1716,18 +1716,18 @@ bool CConnman::AttemptToEvictConnection() return false; } LOCK(m_nodes_mutex); - for (CNode* pnode : m_nodes) { - if (pnode->GetId() == *node_id_to_evict) { - LogDebug(BCLog::NET, "selected %s connection for eviction, %s", pnode->ConnectionTypeAsString(), pnode->DisconnectMsg(fLogIPs)); - TRACEPOINT(net, evicted_inbound_connection, - pnode->GetId(), - pnode->m_addr_name.c_str(), - pnode->ConnectionTypeAsString().c_str(), - pnode->ConnectedThroughNetwork(), - Ticks(pnode->m_connected)); - pnode->fDisconnect = true; - return true; - } + auto it{m_nodes.find(*node_id_to_evict)}; + if (it != m_nodes.end()) { + auto node{it->second}; + LogDebug(BCLog::NET, "selected %s connection for eviction, %s", node->ConnectionTypeAsString(), node->DisconnectMsg(fLogIPs)); + TRACEPOINT(net, evicted_inbound_connection, + node->GetId(), + node->m_addr_name.c_str(), + node->ConnectionTypeAsString().c_str(), + node->ConnectedThroughNetwork(), + Ticks(node->m_connected)); + node->fDisconnect = true; + return true; } return false; } @@ -1748,7 +1748,7 @@ void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->IsInboundConn()) nInbound++; } } @@ -1824,7 +1824,7 @@ void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, m_msgproc->InitializeNode(*pnode, local_services); { LOCK(m_nodes_mutex); - m_nodes.push_back(pnode); + m_nodes.emplace(id, pnode); } LogDebug(BCLog::NET, "connection from %s accepted\n", addr.ToStringAddrPort()); TRACEPOINT(net, inbound_connection, @@ -1861,8 +1861,11 @@ bool CConnman::AddConnection(const std::string& address, ConnectionType conn_typ } // no default case, so the compiler can warn about missing cases // Count existing connections - int existing_connections = WITH_LOCK(m_nodes_mutex, - return std::count_if(m_nodes.begin(), m_nodes.end(), [conn_type](CNode* node) { return node->m_conn_type == conn_type; });); + int existing_connections = WITH_LOCK( + m_nodes_mutex, return std::count_if(m_nodes.begin(), m_nodes.end(), [conn_type](const auto& pair) { + const auto node{pair.second}; + return node->m_conn_type == conn_type; + });); // Max connections of specified type already exist if (max_connections != std::nullopt && existing_connections >= max_connections) return false; @@ -1889,7 +1892,7 @@ void CConnman::DisconnectNodes() if (!fNetworkActive) { // Disconnect any connected nodes - for (CNode* pnode : m_nodes) { + for (auto& [_, pnode] : m_nodes) { if (!pnode->fDisconnect) { LogDebug(BCLog::NET, "Network not active, %s\n", pnode->DisconnectMsg(fLogIPs)); pnode->fDisconnect = true; @@ -1898,40 +1901,42 @@ void CConnman::DisconnectNodes() } // Disconnect unused nodes - std::vector nodes_copy = m_nodes; - for (CNode* pnode : nodes_copy) - { - if (pnode->fDisconnect) - { - // remove from m_nodes - m_nodes.erase(remove(m_nodes.begin(), m_nodes.end(), pnode), m_nodes.end()); + for (auto it{m_nodes.begin()}; it != m_nodes.end();) { + auto id{it->first}; + auto pnode{it->second}; - // Add to reconnection list if appropriate. We don't reconnect right here, because - // the creation of a connection is a blocking operation (up to several seconds), - // and we don't want to hold up the socket handler thread for that long. - if (pnode->m_transport->ShouldReconnectV1()) { - reconnections_to_add.push_back({ - .addr_connect = pnode->addr, - .grant = std::move(pnode->grantOutbound), - .destination = pnode->m_dest, - .conn_type = pnode->m_conn_type, - .use_v2transport = false}); - LogDebug(BCLog::NET, "retrying with v1 transport protocol for peer=%d\n", pnode->GetId()); - } - - // release outbound grant (if any) - pnode->grantOutbound.Release(); - - // close socket and cleanup - pnode->CloseSocketDisconnect(); - - // update connection count by network - if (pnode->IsManualOrFullOutboundConn()) --m_network_conn_counts[pnode->addr.GetNetwork()]; - - // hold in disconnected pool until all refs are released - pnode->Release(); - m_nodes_disconnected.push_back(pnode); + if (!pnode->fDisconnect) { + ++it; + continue; } + + it = m_nodes.erase(it); + + // Add to reconnection list if appropriate. We don't reconnect right here, because + // the creation of a connection is a blocking operation (up to several seconds), + // and we don't want to hold up the socket handler thread for that long. + if (pnode->m_transport->ShouldReconnectV1()) { + reconnections_to_add.push_back({ + .addr_connect = pnode->addr, + .grant = std::move(pnode->grantOutbound), + .destination = pnode->m_dest, + .conn_type = pnode->m_conn_type, + .use_v2transport = false}); + LogDebug(BCLog::NET, "retrying with v1 transport protocol for peer=%d\n", id); + } + + // release outbound grant (if any) + pnode->grantOutbound.Release(); + + // close socket and cleanup + pnode->CloseSocketDisconnect(); + + // update connection count by network + if (pnode->IsManualOrFullOutboundConn()) --m_network_conn_counts[pnode->addr.GetNetwork()]; + + // hold in disconnected pool until all refs are released + pnode->Release(); + m_nodes_disconnected.push_back(pnode); } } { @@ -2421,7 +2426,7 @@ int CConnman::GetFullOutboundConnCount() const int nRelevant = 0; { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->fSuccessfullyConnected && pnode->IsFullOutboundConn()) ++nRelevant; } } @@ -2439,7 +2444,7 @@ int CConnman::GetExtraFullOutboundCount() const int full_outbound_peers = 0; { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && pnode->IsFullOutboundConn()) { ++full_outbound_peers; } @@ -2453,7 +2458,7 @@ int CConnman::GetExtraBlockRelayCount() const int block_relay_peers = 0; { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->fSuccessfullyConnected && !pnode->fDisconnect && pnode->IsBlockOnlyConn()) { ++block_relay_peers; } @@ -2624,7 +2629,7 @@ void CConnman::ThreadOpenConnections(const std::vector connect, Spa { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->IsFullOutboundConn()) nOutboundFullRelay++; if (pnode->IsBlockOnlyConn()) nOutboundBlockRelay++; @@ -2869,7 +2874,7 @@ std::vector CConnman::GetCurrentBlockRelayOnlyConns() const { std::vector ret; LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->IsBlockOnlyConn()) { ret.push_back(pnode->addr); } @@ -2895,7 +2900,7 @@ std::vector CConnman::GetAddedNodeInfo(bool include_connected) co std::map> mapConnectedByName; { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->addr.IsValid()) { mapConnected[pnode->addr] = pnode->IsInboundConn(); } @@ -2999,7 +3004,7 @@ void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFai m_msgproc->InitializeNode(*pnode, m_local_services); { LOCK(m_nodes_mutex); - m_nodes.push_back(pnode); + m_nodes.emplace(pnode->GetId(), pnode); // update connection count by network if (pnode->IsManualOrFullOutboundConn()) ++m_network_conn_counts[pnode->addr.GetNetwork()]; @@ -3359,9 +3364,9 @@ void CConnman::StopNodes() } // Delete peer connections. - std::vector nodes; + decltype(m_nodes) nodes; WITH_LOCK(m_nodes_mutex, nodes.swap(m_nodes)); - for (CNode* pnode : nodes) { + for (auto& [_, pnode] : nodes) { LogDebug(BCLog::NET, "Stopping node, %s", pnode->DisconnectMsg(fLogIPs)); pnode->CloseSocketDisconnect(); DeleteNode(pnode); @@ -3490,7 +3495,7 @@ size_t CConnman::GetNodeCount(ConnectionDirection flags) const return m_nodes.size(); int nNum = 0; - for (const auto& pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (flags & (pnode->IsInboundConn() ? ConnectionDirection::In : ConnectionDirection::Out)) { nNum++; } @@ -3516,7 +3521,7 @@ void CConnman::GetNodeStats(std::vector& vstats) const vstats.clear(); LOCK(m_nodes_mutex); vstats.reserve(m_nodes.size()); - for (CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { vstats.emplace_back(); pnode->CopyStats(vstats.back()); vstats.back().m_mapped_as = GetMappedAS(pnode->addr); @@ -3538,7 +3543,7 @@ bool CConnman::DisconnectNode(const CSubNet& subnet) { bool disconnected = false; LOCK(m_nodes_mutex); - for (CNode* pnode : m_nodes) { + for (auto& [_, pnode] : m_nodes) { if (subnet.Match(pnode->addr)) { LogDebug(BCLog::NET, "disconnect by subnet%s match, %s", (fLogIPs ? strprintf("=%s", subnet.ToString()) : ""), pnode->DisconnectMsg(fLogIPs)); pnode->fDisconnect = true; @@ -3556,14 +3561,14 @@ bool CConnman::DisconnectNode(const CNetAddr& addr) bool CConnman::DisconnectNode(NodeId id) { LOCK(m_nodes_mutex); - for(CNode* pnode : m_nodes) { - if (id == pnode->GetId()) { - LogDebug(BCLog::NET, "disconnect by id, %s", pnode->DisconnectMsg(fLogIPs)); - pnode->fDisconnect = true; - return true; - } + auto it{m_nodes.find(id)}; + if (it == m_nodes.end()) { + return false; } - return false; + auto node{it->second}; + LogDebug(BCLog::NET, "disconnect by id, %s", node->DisconnectMsg(fLogIPs)); + node->fDisconnect = true; + return true; } void CConnman::RecordBytesRecv(uint64_t bytes) @@ -3808,11 +3813,9 @@ bool CConnman::ForNode(NodeId id, std::function func) { CNode* found = nullptr; LOCK(m_nodes_mutex); - for (auto&& pnode : m_nodes) { - if(pnode->GetId() == id) { - found = pnode; - break; - } + auto it{m_nodes.find(id)}; + if (it != m_nodes.end()) { + found = it->second; } return found != nullptr && NodeFullyConnected(found) && func(found); } diff --git a/src/net.h b/src/net.h index aa9c3574f0f..4abaf4bdec9 100644 --- a/src/net.h +++ b/src/net.h @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -1152,7 +1153,7 @@ public: void ForEachNode(const NodeFn& func) { LOCK(m_nodes_mutex); - for (auto&& node : m_nodes) { + for (auto& [_, node] : m_nodes) { if (NodeFullyConnected(node)) func(node); } @@ -1161,7 +1162,7 @@ public: void ForEachNode(const NodeFn& func) const { LOCK(m_nodes_mutex); - for (auto&& node : m_nodes) { + for (auto& [_, node] : m_nodes) { if (NodeFullyConnected(node)) func(node); } @@ -1431,7 +1432,7 @@ private: std::vector m_added_node_params GUARDED_BY(m_added_nodes_mutex); mutable Mutex m_added_nodes_mutex; - std::vector m_nodes GUARDED_BY(m_nodes_mutex); + std::unordered_map m_nodes GUARDED_BY(m_nodes_mutex); std::list m_nodes_disconnected; mutable RecursiveMutex m_nodes_mutex; unsigned int nPrevNodeCount{0}; @@ -1617,8 +1618,9 @@ private: { { LOCK(connman.m_nodes_mutex); - m_nodes_copy = connman.m_nodes; - for (auto& node : m_nodes_copy) { + m_nodes_copy.reserve(connman.m_nodes.size()); + for (auto& [_, node] : connman.m_nodes) { + m_nodes_copy.push_back(node); node->AddRef(); } } diff --git a/src/net_processing.cpp b/src/net_processing.cpp index 1da3ec9d211..ab9f200f4fc 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5082,10 +5082,15 @@ void PeerManagerImpl::EvictExtraOutboundPeers(std::chrono::seconds now) m_connman.ForEachNode([&](CNode* pnode) { if (!pnode->IsBlockOnlyConn() || pnode->fDisconnect) return; - if (pnode->GetId() > youngest_peer.first) { - next_youngest_peer = youngest_peer; - youngest_peer.first = pnode->GetId(); - youngest_peer.second = pnode->m_last_block_time; + if (pnode->GetId() > next_youngest_peer.first) { + if (pnode->GetId() > youngest_peer.first) { + next_youngest_peer = youngest_peer; + youngest_peer.first = pnode->GetId(); + youngest_peer.second = pnode->m_last_block_time; + } else { + next_youngest_peer.first = pnode->GetId(); + next_youngest_peer.second = pnode->m_last_block_time; + } } }); NodeId to_disconnect = youngest_peer.first; diff --git a/src/rpc/net.cpp b/src/rpc/net.cpp index bda07365e0e..26ab94fa937 100644 --- a/src/rpc/net.cpp +++ b/src/rpc/net.cpp @@ -203,6 +203,10 @@ static RPCHelpMan getpeerinfo() std::vector vstats; connman.GetNodeStats(vstats); + std::sort(vstats.begin(), vstats.end(), [](const CNodeStats& a, const CNodeStats& b) { + return a.nodeid < b.nodeid; + }); + UniValue ret(UniValue::VARR); for (const CNodeStats& stats : vstats) { diff --git a/src/test/fuzz/connman.cpp b/src/test/fuzz/connman.cpp index a62d227da8e..5d2bdaf98b5 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -64,12 +64,13 @@ FUZZ_TARGET(connman, .init = initialize_connman) connman.Init(options); CNetAddr random_netaddr; - CNode random_node = ConsumeNode(fuzzed_data_provider); + NodeId node_id{0}; + CNode random_node = ConsumeNode(fuzzed_data_provider, node_id++); CSubNet random_subnet; std::string random_string; LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) { - CNode& p2p_node{*ConsumeNodeAsUniquePtr(fuzzed_data_provider).release()}; + CNode& p2p_node{*ConsumeNodeAsUniquePtr(fuzzed_data_provider, node_id++).release()}; connman.AddTestNode(p2p_node); } diff --git a/src/test/net_peer_connection_tests.cpp b/src/test/net_peer_connection_tests.cpp index e60ce8b99d3..0bbf5e9e7fc 100644 --- a/src/test/net_peer_connection_tests.cpp +++ b/src/test/net_peer_connection_tests.cpp @@ -117,9 +117,9 @@ BOOST_FIXTURE_TEST_CASE(test_addnode_getaddednodeinfo_and_connection_detection, BOOST_CHECK_EQUAL(nodes.back()->ConnectedThroughNetwork(), Network::NET_CJDNS); BOOST_TEST_MESSAGE("Call AddNode() for all the peers"); - for (auto node : connman->TestNodes()) { + for (const auto& [id, node] : connman->TestNodes()) { BOOST_CHECK(connman->AddNode({/*m_added_node=*/node->addr.ToStringAddrPort(), /*m_use_v2transport=*/true})); - BOOST_TEST_MESSAGE(strprintf("peer id=%s addr=%s", node->GetId(), node->addr.ToStringAddrPort())); + BOOST_TEST_MESSAGE(strprintf("peer id=%s addr=%s", id, node->addr.ToStringAddrPort())); } BOOST_TEST_MESSAGE("\nCall AddNode() with 2 addrs resolving to existing localhost addnode entry; neither should be added"); @@ -134,7 +134,7 @@ BOOST_FIXTURE_TEST_CASE(test_addnode_getaddednodeinfo_and_connection_detection, BOOST_CHECK(connman->GetAddedNodeInfo(/*include_connected=*/false).empty()); // Test AddedNodesContain() - for (auto node : connman->TestNodes()) { + for (const auto& [_, node] : connman->TestNodes()) { BOOST_CHECK(connman->AddedNodesContain(node->addr)); } AddPeer(id, nodes, *peerman, *connman, ConnectionType::OUTBOUND_FULL_RELAY); @@ -151,12 +151,12 @@ BOOST_FIXTURE_TEST_CASE(test_addnode_getaddednodeinfo_and_connection_detection, } BOOST_TEST_MESSAGE("\nCheck that all connected peers are correctly detected as connected"); - for (auto node : connman->TestNodes()) { + for (const auto& [_, node] : connman->TestNodes()) { BOOST_CHECK(connman->AlreadyConnectedPublic(node->addr)); } // Clean up - for (auto node : connman->TestNodes()) { + for (const auto& [_, node] : connman->TestNodes()) { peerman->FinalizeNode(*node); } connman->ClearTestNodes(); diff --git a/src/test/util/net.h b/src/test/util/net.h index 3e717341d87..7215df22c35 100644 --- a/src/test/util/net.h +++ b/src/test/util/net.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -45,7 +46,7 @@ struct ConnmanTestMsg : public CConnman { m_peer_connect_timeout = timeout; } - std::vector TestNodes() + auto TestNodes() { LOCK(m_nodes_mutex); return m_nodes; @@ -54,7 +55,8 @@ struct ConnmanTestMsg : public CConnman { void AddTestNode(CNode& node) { LOCK(m_nodes_mutex); - m_nodes.push_back(&node); + auto [_, inserted] = m_nodes.emplace(node.GetId(), &node); + Assert(inserted); if (node.IsManualOrFullOutboundConn()) ++m_network_conn_counts[node.addr.GetNetwork()]; } @@ -62,7 +64,7 @@ struct ConnmanTestMsg : public CConnman { void ClearTestNodes() { LOCK(m_nodes_mutex); - for (CNode* node : m_nodes) { + for (auto& [_, node] : m_nodes) { delete node; } m_nodes.clear(); From 1cea2e4b84838891c782f28810eefc09c3dd6415 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Sat, 21 Sep 2024 10:05:34 +0200 Subject: [PATCH 11/42] net: isolate P2P specifics from GenerateWaitSockets() Move the parts of `CConnman::GenerateWaitSockets()` that are specific to the Bitcoin-P2P protocol to dedicated methods: `ShouldTryToSend()` and `ShouldTryToRecv()`. This brings us one step closer to moving `GenerateWaitSockets()` to the protocol agnostic `SockMan` (which would call `ShouldTry...()` from `CConnman`). --- src/common/sockman.cpp | 4 +++ src/common/sockman.h | 15 +++++++++++ src/net.cpp | 56 +++++++++++++++++++++++++++++++++--------- src/net.h | 14 +++++++++-- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index a534971e6c2..b7e0d3d9e5e 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -145,6 +145,10 @@ void SockMan::StopListening() m_listen.clear(); } +bool SockMan::ShouldTryToSend(Id id) const { return true; } + +bool SockMan::ShouldTryToRecv(Id id) const { return true; } + void SockMan::EventI2PStatus(const CService&, I2PStatus) {} void SockMan::ThreadI2PAccept() diff --git a/src/common/sockman.h b/src/common/sockman.h index 6e09f777ac8..2b12a97cc56 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -151,6 +151,21 @@ private: // alone to use the default implementation from SockMan. // + /** + * Can be used to temporarily pause sends on a connection. + * The implementation in SockMan always returns true. + * @param[in] id Connection for which to confirm or omit the next send. + */ + virtual bool ShouldTryToSend(Id id) const; + + /** + * SockMan would only call Recv() on a connection's socket if this returns true. + * Can be used to temporarily pause receives on a connection. + * The implementation in SockMan always returns true. + * @param[in] id Connection for which to confirm or omit the next receive. + */ + virtual bool ShouldTryToRecv(Id id) const; + /** * Be notified of a change in the state of the I2P connectivity. * The default behavior, implemented by `SockMan`, is to ignore this event. diff --git a/src/net.cpp b/src/net.cpp index 1b42ac34b37..114f41f2e74 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1675,6 +1675,16 @@ std::pair CConnman::SocketSendData(CNode& node) const return {nSentSize, data_left}; } +CNode* CConnman::GetNodeById(NodeId node_id) const +{ + LOCK(m_nodes_mutex); + auto it{m_nodes.find(node_id)}; + if (it != m_nodes.end()) { + return it->second; + } + return nullptr; +} + /** Try to find a connection to evict when the node is full. * Extreme care must be taken to avoid opening the node to attacker * triggered network partitioning. @@ -2032,8 +2042,37 @@ bool CConnman::InactivityCheck(const CNode& node) const return false; } +bool CConnman::ShouldTryToSend(SockMan::Id id) const +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return false; + } + LOCK(node->cs_vSend); + // Sending is possible if either there are bytes to send right now, or if there will be + // once a potential message from vSendMsg is handed to the transport. GetBytesToSend + // determines both of these in a single call. + const auto& [to_send, more, _msg_type] = node->m_transport->GetBytesToSend(!node->vSendMsg.empty()); + return !to_send.empty() || more; +} + +bool CConnman::ShouldTryToRecv(SockMan::Id id) const +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return false; + } + return !node->fPauseRecv; +} + Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) { + AssertLockNotHeld(m_nodes_mutex); + Sock::EventsPerSock events_per_sock; for (const auto& sock : m_listen) { @@ -2041,16 +2080,8 @@ Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) } for (CNode* pnode : nodes) { - bool select_recv = !pnode->fPauseRecv; - bool select_send; - { - LOCK(pnode->cs_vSend); - // Sending is possible if either there are bytes to send right now, or if there will be - // once a potential message from vSendMsg is handed to the transport. GetBytesToSend - // determines both of these in a single call. - const auto& [to_send, more, _msg_type] = pnode->m_transport->GetBytesToSend(!pnode->vSendMsg.empty()); - select_send = !to_send.empty() || more; - } + const bool select_recv{ShouldTryToRecv(pnode->GetId())}; + const bool select_send{ShouldTryToSend(pnode->GetId())}; if (!select_recv && !select_send) continue; LOCK(pnode->m_sock_mutex); @@ -2113,8 +2144,8 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, } const auto it = events_per_sock.find(pnode->m_sock); if (it != events_per_sock.end()) { - recvSet = it->second.occurred & Sock::RECV; - sendSet = it->second.occurred & Sock::SEND; + recvSet = it->second.occurred & Sock::RECV; // Sock::RECV could only be set if ShouldTryToRecv() has returned true in GenerateWaitSockets(). + sendSet = it->second.occurred & Sock::SEND; // Sock::SEND could only be set if ShouldTryToSend() has returned true in GenerateWaitSockets(). errorSet = it->second.occurred & Sock::ERR; } } @@ -2214,6 +2245,7 @@ void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock void CConnman::ThreadSocketHandler() { + AssertLockNotHeld(m_nodes_mutex); AssertLockNotHeld(m_total_bytes_sent_mutex); while (!interruptNet) diff --git a/src/net.h b/src/net.h index 4abaf4bdec9..794f43189b1 100644 --- a/src/net.h +++ b/src/net.h @@ -1308,17 +1308,25 @@ private: /** Return true if the peer is inactive and should be disconnected. */ bool InactivityCheck(const CNode& node) const; + virtual bool ShouldTryToSend(SockMan::Id id) const override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + + virtual bool ShouldTryToRecv(SockMan::Id id) const override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + /** * Generate a collection of sockets to check for IO readiness. * @param[in] nodes Select from these nodes' sockets. * @return sockets to check for readiness */ - Sock::EventsPerSock GenerateWaitSockets(Span nodes); + Sock::EventsPerSock GenerateWaitSockets(Span nodes) + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); /** * Check connected and listening sockets for IO readiness and process them accordingly. */ - void SocketHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex, !mutexMsgProc); + void SocketHandler() + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_total_bytes_sent_mutex, !mutexMsgProc); /** * Do the read/write for connected sockets that are ready for IO. @@ -1431,6 +1439,8 @@ private: // connection string and whether to use v2 p2p std::vector m_added_node_params GUARDED_BY(m_added_nodes_mutex); + CNode* GetNodeById(NodeId node_id) const EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + mutable Mutex m_added_nodes_mutex; std::unordered_map m_nodes GUARDED_BY(m_nodes_mutex); std::list m_nodes_disconnected; From f0b40d1c06024984e046666f75ead9a2aed9b952 Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Sat, 21 Sep 2024 10:31:53 +0200 Subject: [PATCH 12/42] net: isolate P2P specifics from SocketHandlerConnected() and ThreadSocketHandler() Move some parts of `CConnman::SocketHandlerConnected()` and `CConnman::ThreadSocketHandler()` that are specific to the Bitcoin-P2P protocol to dedicated methods: `EventIOLoopCompletedForOne(id)` and `EventIOLoopCompletedForAll()`. This brings us one step closer to moving `SocketHandlerConnected()` and `ThreadSocketHandler()` to the protocol agnostic `SockMan` (which would call `EventIOLoopCompleted...()` from `CConnman`). --- src/common/sockman.cpp | 4 ++++ src/common/sockman.h | 17 +++++++++++++++++ src/net.cpp | 29 ++++++++++++++++++++++++++--- src/net.h | 8 +++++++- 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index b7e0d3d9e5e..2313f7f4b80 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -149,6 +149,10 @@ bool SockMan::ShouldTryToSend(Id id) const { return true; } bool SockMan::ShouldTryToRecv(Id id) const { return true; } +void SockMan::EventIOLoopCompletedForOne(Id id) {} + +void SockMan::EventIOLoopCompletedForAll() {} + void SockMan::EventI2PStatus(const CService&, I2PStatus) {} void SockMan::ThreadI2PAccept() diff --git a/src/common/sockman.h b/src/common/sockman.h index 2b12a97cc56..267a8689686 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -166,6 +166,23 @@ private: */ virtual bool ShouldTryToRecv(Id id) const; + /** + * SockMan has completed the current send+recv iteration for a given connection. + * It will do another send+recv for this connection after processing all other connections. + * Can be used to execute periodic tasks for a given connection. + * The implementation in SockMan does nothing. + * @param[in] id Connection for which send+recv has been done. + */ + virtual void EventIOLoopCompletedForOne(Id id); + + /** + * SockMan has completed send+recv for all connections. + * Can be used to execute periodic tasks for all connections, like closing + * connections due to higher level logic. + * The implementation in SockMan does nothing. + */ + virtual void EventIOLoopCompletedForAll(); + /** * Be notified of a change in the state of the I2P connectivity. * The default behavior, implemented by `SockMan`, is to ignore this event. diff --git a/src/net.cpp b/src/net.cpp index 114f41f2e74..1ff80a928aa 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -2069,6 +2069,29 @@ bool CConnman::ShouldTryToRecv(SockMan::Id id) const return !node->fPauseRecv; } +void CConnman::EventIOLoopCompletedForOne(SockMan::Id id) +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return; + } + + if (InactivityCheck(*node)) { + node->fDisconnect = true; + } +} + +void CConnman::EventIOLoopCompletedForAll() +{ + AssertLockNotHeld(m_nodes_mutex); + AssertLockNotHeld(m_reconnections_mutex); + + DisconnectNodes(); + NotifyNumConnectionsChanged(); +} + Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) { AssertLockNotHeld(m_nodes_mutex); @@ -2125,6 +2148,7 @@ void CConnman::SocketHandler() void CConnman::SocketHandlerConnected(const std::vector& nodes, const Sock::EventsPerSock& events_per_sock) { + AssertLockNotHeld(m_nodes_mutex); AssertLockNotHeld(m_total_bytes_sent_mutex); for (CNode* pnode : nodes) { @@ -2217,7 +2241,7 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, } } - if (InactivityCheck(*pnode)) pnode->fDisconnect = true; + EventIOLoopCompletedForOne(pnode->GetId()); } } @@ -2250,8 +2274,7 @@ void CConnman::ThreadSocketHandler() while (!interruptNet) { - DisconnectNodes(); - NotifyNumConnectionsChanged(); + EventIOLoopCompletedForAll(); SocketHandler(); } } diff --git a/src/net.h b/src/net.h index 794f43189b1..aff25d2f46b 100644 --- a/src/net.h +++ b/src/net.h @@ -1314,6 +1314,12 @@ private: virtual bool ShouldTryToRecv(SockMan::Id id) const override EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + virtual void EventIOLoopCompletedForOne(SockMan::Id id) override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + + virtual void EventIOLoopCompletedForAll() override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_reconnections_mutex); + /** * Generate a collection of sockets to check for IO readiness. * @param[in] nodes Select from these nodes' sockets. @@ -1335,7 +1341,7 @@ private: */ void SocketHandlerConnected(const std::vector& nodes, const Sock::EventsPerSock& events_per_sock) - EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex, !mutexMsgProc); + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_total_bytes_sent_mutex, !mutexMsgProc); /** * Accept incoming connections, one from each read-ready listening socket. From ab9de0f226333a9b3a92ed2740cd40840b65bc5b Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Sun, 22 Sep 2024 12:11:42 +0200 Subject: [PATCH 13/42] net: isolate all remaining P2P specifics from SocketHandlerConnected() Introduce 4 new methods for the interaction between `CConnman` and `SockMan`: * `EventReadyToSend()`: called when there is readiness to send and do the actual sending of data. * `EventGotData()`, `EventGotEOF()`, `EventGotPermanentReadError()`: called when the corresponing recv events occur. These methods contain logic that is specific to the Bitcoin-P2P protocol and move it away from `CConnman::SocketHandlerConnected()` which will become a protocol agnostic method of `SockMan`. Also, move the counting of sent bytes to `CConnman::SocketSendData()` - both callers of that method called `RecordBytesSent()` just after the call, so move it from the callers to inside `CConnman::SocketSendData()`. --- src/common/sockman.h | 32 +++++++++++ src/net.cpp | 132 ++++++++++++++++++++++++++++++------------- src/net.h | 15 ++++- 3 files changed, 140 insertions(+), 39 deletions(-) diff --git a/src/common/sockman.h b/src/common/sockman.h index 267a8689686..8562bedf163 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -146,6 +146,38 @@ private: const CService& me, const CService& them) = 0; + /** + * Called when the socket is ready to send data and `ShouldTryToSend()` has + * returned true. This is where the higher level code serializes its messages + * and calls `SockMan::SendBytes()`. + * @param[in] id Id of the connection whose socket is ready to send. + * @param[out] cancel_recv Should always be set upon return and if it is true, + * then the next attempt to receive data from that connection will be omitted. + */ + virtual void EventReadyToSend(Id id, bool& cancel_recv) = 0; + + /** + * Called when new data has been received. + * @param[in] id Connection for which the data arrived. + * @param[in] data Received data. + */ + virtual void EventGotData(Id id, std::span data) = 0; + + /** + * Called when the remote peer has sent an EOF on the socket. This is a graceful + * close of their writing side, we can still send and they will receive, if it + * makes sense at the application level. + * @param[in] id Connection whose socket got EOF. + */ + virtual void EventGotEOF(Id id) = 0; + + /** + * Called when we get an irrecoverable error trying to read from a socket. + * @param[in] id Connection whose socket got an error. + * @param[in] errmsg Message describing the error. + */ + virtual void EventGotPermanentReadError(Id id, const std::string& errmsg) = 0; + // // Non-pure virtual functions can be overridden by children classes or left // alone to use the default implementation from SockMan. diff --git a/src/net.cpp b/src/net.cpp index 1ff80a928aa..4b06070382b 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1596,8 +1596,10 @@ Transport::Info V2Transport::GetInfo() const noexcept return info; } -std::pair CConnman::SocketSendData(CNode& node) const +std::pair CConnman::SocketSendData(CNode& node) { + AssertLockNotHeld(m_total_bytes_sent_mutex); + auto it = node.vSendMsg.begin(); size_t nSentSize = 0; bool data_left{false}; //!< second return value (whether unsent data remains) @@ -1672,6 +1674,11 @@ std::pair CConnman::SocketSendData(CNode& node) const assert(node.m_send_memusage == 0); } node.vSendMsg.erase(node.vSendMsg.begin(), it); + + if (nSentSize > 0) { + RecordBytesSent(nSentSize); + } + return {nSentSize, data_left}; } @@ -2042,6 +2049,83 @@ bool CConnman::InactivityCheck(const CNode& node) const return false; } +void CConnman::EventReadyToSend(SockMan::Id id, bool& cancel_recv) +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + cancel_recv = true; + return; + } + + const auto [bytes_sent, data_left] = WITH_LOCK(node->cs_vSend, return SocketSendData(*node);); + + // If both receiving and (non-optimistic) sending were possible, we first attempt + // sending. If that succeeds, but does not fully drain the send queue, do not + // attempt to receive. This avoids needlessly queueing data if the remote peer + // is slow at receiving data, by means of TCP flow control. We only do this when + // sending actually succeeded to make sure progress is always made; otherwise a + // deadlock would be possible when both sides have data to send, but neither is + // receiving. + cancel_recv = bytes_sent > 0 && data_left; +} + +void CConnman::EventGotData(SockMan::Id id, std::span data) +{ + AssertLockNotHeld(mutexMsgProc); + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return; + } + + bool notify = false; + if (!node->ReceiveMsgBytes(data, notify)) { + LogDebug(BCLog::NET, + "receiving message bytes failed, %s\n", + node->DisconnectMsg(fLogIPs) + ); + node->CloseSocketDisconnect(); + } + RecordBytesRecv(data.size()); + if (notify) { + node->MarkReceivedMsgsForProcessing(); + WakeMessageHandler(); + } +} + +void CConnman::EventGotEOF(SockMan::Id id) +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return; + } + + if (!node->fDisconnect) { + LogDebug(BCLog::NET, "socket closed for peer=%d\n", id); + } + node->CloseSocketDisconnect(); +} + +void CConnman::EventGotPermanentReadError(SockMan::Id id, const std::string& errmsg) +{ + AssertLockNotHeld(m_nodes_mutex); + + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return; + } + + if (!node->fDisconnect) { + LogDebug(BCLog::NET, "socket recv error for peer=%d: %s\n", id, errmsg); + } + node->CloseSocketDisconnect(); +} + bool CConnman::ShouldTryToSend(SockMan::Id id) const { AssertLockNotHeld(m_nodes_mutex); @@ -2175,19 +2259,12 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, } if (sendSet) { - // Send data - auto [bytes_sent, data_left] = WITH_LOCK(pnode->cs_vSend, return SocketSendData(*pnode)); - if (bytes_sent) { - RecordBytesSent(bytes_sent); + bool cancel_recv; - // If both receiving and (non-optimistic) sending were possible, we first attempt - // sending. If that succeeds, but does not fully drain the send queue, do not - // attempt to receive. This avoids needlessly queueing data if the remote peer - // is slow at receiving data, by means of TCP flow control. We only do this when - // sending actually succeeded to make sure progress is always made; otherwise a - // deadlock would be possible when both sides have data to send, but neither is - // receiving. - if (data_left) recvSet = false; + EventReadyToSend(pnode->GetId(), cancel_recv); + + if (cancel_recv) { + recvSet = false; } } @@ -2205,27 +2282,11 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, } if (nBytes > 0) { - bool notify = false; - if (!pnode->ReceiveMsgBytes({pchBuf, (size_t)nBytes}, notify)) { - LogDebug(BCLog::NET, - "receiving message bytes failed, %s\n", - pnode->DisconnectMsg(fLogIPs) - ); - pnode->CloseSocketDisconnect(); - } - RecordBytesRecv(nBytes); - if (notify) { - pnode->MarkReceivedMsgsForProcessing(); - WakeMessageHandler(); - } + EventGotData(pnode->GetId(), {pchBuf, static_cast(nBytes)}); } else if (nBytes == 0) { - // socket closed gracefully - if (!pnode->fDisconnect) { - LogDebug(BCLog::NET, "socket closed, %s\n", pnode->DisconnectMsg(fLogIPs)); - } - pnode->CloseSocketDisconnect(); + EventGotEOF(pnode->GetId()); } else if (nBytes < 0) { @@ -2233,10 +2294,7 @@ void CConnman::SocketHandlerConnected(const std::vector& nodes, int nErr = WSAGetLastError(); if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) { - if (!pnode->fDisconnect) { - LogDebug(BCLog::NET, "socket recv error, %s: %s\n", pnode->DisconnectMsg(fLogIPs), NetworkErrorString(nErr)); - } - pnode->CloseSocketDisconnect(); + EventGotPermanentReadError(pnode->GetId(), NetworkErrorString(nErr)); } } } @@ -3835,7 +3893,6 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) msg.data.data() ); - size_t nBytesSent = 0; { LOCK(pnode->cs_vSend); // Check if the transport still has unsent bytes, and indicate to it that we're about to @@ -3858,10 +3915,9 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) // results in sendable bytes there, but with V2Transport this is not the case (it may // still be in the handshake). if (queue_was_empty && more) { - std::tie(nBytesSent, std::ignore) = SocketSendData(*pnode); + SocketSendData(*pnode); } } - if (nBytesSent) RecordBytesSent(nBytesSent); } bool CConnman::ForNode(NodeId id, std::function func) diff --git a/src/net.h b/src/net.h index aff25d2f46b..a786a8a8d7f 100644 --- a/src/net.h +++ b/src/net.h @@ -1308,6 +1308,18 @@ private: /** Return true if the peer is inactive and should be disconnected. */ bool InactivityCheck(const CNode& node) const; + void EventReadyToSend(SockMan::Id id, bool& cancel_recv) override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + + virtual void EventGotData(SockMan::Id id, std::span data) override + EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc, !m_nodes_mutex); + + virtual void EventGotEOF(SockMan::Id id) override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + + virtual void EventGotPermanentReadError(SockMan::Id id, const std::string& errmsg) override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); + virtual bool ShouldTryToSend(SockMan::Id id) const override EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); @@ -1371,7 +1383,8 @@ private: void DeleteNode(CNode* pnode); /** (Try to) send data from node's vSendMsg. Returns (bytes_sent, data_left). */ - std::pair SocketSendData(CNode& node) const EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend); + std::pair SocketSendData(CNode& node) + EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend, !m_total_bytes_sent_mutex); void DumpAddresses(); From 455185b66596e18cc3584233e54ed9d6c34b48be Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 23 Sep 2024 12:50:25 +0200 Subject: [PATCH 14/42] net: split CConnman::ConnectNode() Move the protocol agnostic parts of `CConnman::ConnectNode()` into `SockMan::ConnectAndMakeId()` and leave the Bitcoin-P2P specific stuff in `CConnman::ConnectNode()`. Move the protocol agnostic `CConnman::m_unused_i2p_sessions`, its mutex and `MAX_UNUSED_I2P_SESSIONS_SIZE` to `SockMan`. Move `GetBindAddress()` from `net.cpp` to `sockman.cpp`. --- src/common/sockman.cpp | 91 ++++++++++++++++++++++++++++++++++++++ src/common/sockman.h | 57 ++++++++++++++++++++++++ src/net.cpp | 99 ++++++++++++------------------------------ src/net.h | 35 +++------------ src/test/util/net.h | 3 +- 5 files changed, 184 insertions(+), 101 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 2313f7f4b80..02fc425d28c 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -10,6 +10,20 @@ #include #include +/** Get the bind address for a socket as CService. */ +CService GetBindAddress(const Sock& sock) +{ + CService addr_bind; + struct sockaddr_storage sockaddr_bind; + socklen_t sockaddr_bind_len = sizeof(sockaddr_bind); + if (!sock.GetSockName((struct sockaddr*)&sockaddr_bind, &sockaddr_bind_len)) { + addr_bind.SetSockAddr((const struct sockaddr*)&sockaddr_bind, sockaddr_bind_len); + } else { + LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "getsockname failed\n"); + } + return addr_bind; +} + bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) { // Create socket for listening for incoming connections @@ -110,6 +124,83 @@ void SockMan::JoinSocketsThreads() } } +std::optional +SockMan::ConnectAndMakeId(const std::variant& to, + bool is_important, + std::optional proxy, + bool& proxy_failed, + CService& me, + std::unique_ptr& sock, + std::unique_ptr& i2p_transient_session) +{ + AssertLockNotHeld(m_unused_i2p_sessions_mutex); + + Assume(!me.IsValid()); + + if (std::holds_alternative(to)) { + const CService& addr_to{std::get(to)}; + if (addr_to.IsI2P()) { + if (!Assume(proxy.has_value())) { + return std::nullopt; + } + + i2p::Connection conn; + bool connected{false}; + + if (m_i2p_sam_session) { + connected = m_i2p_sam_session->Connect(addr_to, conn, proxy_failed); + } else { + { + LOCK(m_unused_i2p_sessions_mutex); + if (m_unused_i2p_sessions.empty()) { + i2p_transient_session = std::make_unique(proxy.value(), &interruptNet); + } else { + i2p_transient_session.swap(m_unused_i2p_sessions.front()); + m_unused_i2p_sessions.pop(); + } + } + connected = i2p_transient_session->Connect(addr_to, conn, proxy_failed); + if (!connected) { + LOCK(m_unused_i2p_sessions_mutex); + if (m_unused_i2p_sessions.size() < MAX_UNUSED_I2P_SESSIONS_SIZE) { + m_unused_i2p_sessions.emplace(i2p_transient_session.release()); + } + } + } + + if (connected) { + sock = std::move(conn.sock); + me = conn.me; + } + } else if (proxy.has_value()) { + sock = ConnectThroughProxy(proxy.value(), addr_to.ToStringAddr(), addr_to.GetPort(), proxy_failed); + } else { + sock = ConnectDirectly(addr_to, is_important); + } + } else { + if (!Assume(proxy.has_value())) { + return std::nullopt; + } + + const auto& hostport{std::get(to)}; + + bool dummy_proxy_failed; + sock = ConnectThroughProxy(proxy.value(), hostport.host, hostport.port, dummy_proxy_failed); + } + + if (!sock) { + return std::nullopt; + } + + if (!me.IsValid()) { + me = GetBindAddress(*sock); + } + + const Id id{GetNewId()}; + + return id; +} + std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) { sockaddr_storage storage; diff --git a/src/common/sockman.h b/src/common/sockman.h index 8562bedf163..33d368630b3 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -14,9 +14,14 @@ #include #include +#include +#include #include +#include #include +CService GetBindAddress(const Sock& sock); + /** * A socket manager class which handles socket operations. * To use this class, inherit from it and implement the pure virtual methods. @@ -24,6 +29,7 @@ * - binding and listening on sockets * - starting of necessary threads to process socket operations * - accepting incoming connections + * - making outbound connections */ class SockMan { @@ -92,6 +98,37 @@ public: */ void JoinSocketsThreads(); + /** + * A more readable std::tuple for host and port. + */ + struct StringHostIntPort { + const std::string& host; + uint16_t port; + }; + + /** + * Make an outbound connection, save the socket internally and return a newly generated connection id. + * @param[in] to The address to connect to, either as CService or a host as string and port as + * an integer, if the later is used, then `proxy` must be valid. + * @param[in] is_important If true, then log failures with higher severity. + * @param[in] proxy Proxy to connect through, if set. + * @param[out] proxy_failed If `proxy` is valid and the connection failed because of the + * proxy, then it will be set to true. + * @param[out] me If the connection was successful then this is set to the address on the + * local side of the socket. + * @param[out] sock Connected socket, if the operation is successful. + * @param[out] i2p_transient_session I2P session, if the operation is successful. + * @return Newly generated id, or std::nullopt if the operation fails. + */ + std::optional ConnectAndMakeId(const std::variant& to, + bool is_important, + std::optional proxy, + bool& proxy_failed, + CService& me, + std::unique_ptr& sock, + std::unique_ptr& i2p_transient_session) + EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + /** * Accept a connection. * @param[in] listen_sock Socket on which to accept the connection. @@ -132,6 +169,12 @@ public: private: + /** + * Cap on the size of `m_unused_i2p_sessions`, to ensure it does not + * unexpectedly use too much memory. + */ + static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10}; + // // Pure virtual functions must be implemented by children classes. // @@ -238,6 +281,20 @@ private: * Thread that accepts incoming I2P connections in a loop, can be stopped via `interruptNet`. */ std::thread m_thread_i2p_accept; + + /** + * Mutex protecting m_i2p_sam_sessions. + */ + Mutex m_unused_i2p_sessions_mutex; + + /** + * A pool of created I2P SAM transient sessions that should be used instead + * of creating new ones in order to reduce the load on the I2P network. + * Creating a session in I2P is not cheap, thus if this is not empty, then + * pick an entry from it instead of creating a new session. If connecting to + * a host fails, then the created session is put to this pool for reuse. + */ + std::queue> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex); }; #endif // BITCOIN_COMMON_SOCKMAN_H diff --git a/src/net.cpp b/src/net.cpp index 4b06070382b..e8a1a1301d1 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -380,23 +380,8 @@ bool CConnman::CheckIncomingNonce(uint64_t nonce) return true; } -/** Get the bind address for a socket as CService. */ -static CService GetBindAddress(const Sock& sock) -{ - CService addr_bind; - struct sockaddr_storage sockaddr_bind; - socklen_t sockaddr_bind_len = sizeof(sockaddr_bind); - if (!sock.GetSockName((struct sockaddr*)&sockaddr_bind, &sockaddr_bind_len)) { - addr_bind.SetSockAddr((const struct sockaddr*)&sockaddr_bind, sockaddr_bind_len); - } else { - LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "getsockname failed\n"); - } - return addr_bind; -} - CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type, bool use_v2transport) { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); assert(conn_type != ConnectionType::INBOUND); if (pszDest == nullptr) { @@ -458,52 +443,28 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo // Connect std::unique_ptr sock; Proxy proxy; - CService addr_bind; - assert(!addr_bind.IsValid()); std::unique_ptr i2p_transient_session; + std::optional node_id; + CService me; + for (auto& target_addr: connect_to) { if (target_addr.IsValid()) { const bool use_proxy{GetProxy(target_addr.GetNetwork(), proxy)}; bool proxyConnectionFailed = false; - if (target_addr.IsI2P() && use_proxy) { - i2p::Connection conn; - bool connected{false}; - - if (m_i2p_sam_session) { - connected = m_i2p_sam_session->Connect(target_addr, conn, proxyConnectionFailed); - } else { - { - LOCK(m_unused_i2p_sessions_mutex); - if (m_unused_i2p_sessions.empty()) { - i2p_transient_session = - std::make_unique(proxy, &interruptNet); - } else { - i2p_transient_session.swap(m_unused_i2p_sessions.front()); - m_unused_i2p_sessions.pop(); - } - } - connected = i2p_transient_session->Connect(target_addr, conn, proxyConnectionFailed); - if (!connected) { - LOCK(m_unused_i2p_sessions_mutex); - if (m_unused_i2p_sessions.size() < MAX_UNUSED_I2P_SESSIONS_SIZE) { - m_unused_i2p_sessions.emplace(i2p_transient_session.release()); - } - } - } - - if (connected) { - sock = std::move(conn.sock); - addr_bind = conn.me; - } - } else if (use_proxy) { + if (use_proxy && !target_addr.IsI2P()) { LogPrintLevel(BCLog::PROXY, BCLog::Level::Debug, "Using proxy: %s to connect to %s\n", proxy.ToString(), target_addr.ToStringAddrPort()); - sock = ConnectThroughProxy(proxy, target_addr.ToStringAddr(), target_addr.GetPort(), proxyConnectionFailed); - } else { - // no proxy needed (none set for target network) - sock = ConnectDirectly(target_addr, conn_type == ConnectionType::MANUAL); } + + node_id = ConnectAndMakeId(target_addr, + /*is_important=*/conn_type == ConnectionType::MANUAL, + use_proxy ? std::optional{proxy} : std::nullopt, + proxyConnectionFailed, + me, + sock, + i2p_transient_session); + if (!proxyConnectionFailed) { // If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to // the proxy, mark this as an attempt. @@ -512,12 +473,19 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo } else if (pszDest && GetNameProxy(proxy)) { std::string host; uint16_t port{default_port}; - SplitHostPort(std::string(pszDest), port, host); - bool proxyConnectionFailed; - sock = ConnectThroughProxy(proxy, host, port, proxyConnectionFailed); + SplitHostPort(pszDest, port, host); + + bool dummy; + node_id = ConnectAndMakeId(StringHostIntPort{host, port}, + /*is_important=*/conn_type == ConnectionType::MANUAL, + proxy, + dummy, + me, + sock, + i2p_transient_session); } // Check any other resolved address (if any) if we fail to connect - if (!sock) { + if (!node_id.has_value()) { continue; } @@ -525,18 +493,13 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo std::vector whitelist_permissions = conn_type == ConnectionType::MANUAL ? vWhitelistedRangeOutgoing : std::vector{}; AddWhitelistPermissionFlags(permission_flags, target_addr, whitelist_permissions); - // Add node - NodeId id = GetNewId(); - uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); - if (!addr_bind.IsValid()) { - addr_bind = GetBindAddress(*sock); - } - CNode* pnode = new CNode(id, + const uint64_t nonce{GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(node_id.value()).Finalize()}; + CNode* pnode = new CNode(node_id.value(), std::move(sock), target_addr, CalculateKeyedNetGroup(target_addr), nonce, - addr_bind, + me, pszDest ? pszDest : "", conn_type, /*inbound_onion=*/false, @@ -549,7 +512,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo pnode->AddRef(); // We're making a new connection, harvest entropy from the time (and our peer count) - RandAddEvent((uint32_t)id); + RandAddEvent(static_cast(node_id.value())); return pnode; } @@ -1857,7 +1820,6 @@ void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type, bool use_v2transport = false) { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::optional max_connections; switch (conn_type) { case ConnectionType::INBOUND: @@ -2497,7 +2459,6 @@ void CConnman::DumpAddresses() void CConnman::ProcessAddrFetch() { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::string strDest; { LOCK(m_addr_fetches_mutex); @@ -2617,7 +2578,6 @@ bool CConnman::MaybePickPreferredNetwork(std::optional& network) void CConnman::ThreadOpenConnections(const std::vector connect, Span seed_nodes) { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); AssertLockNotHeld(m_reconnections_mutex); FastRandomContext rng; // Connect to specific addresses @@ -3058,7 +3018,6 @@ std::vector CConnman::GetAddedNodeInfo(bool include_connected) co void CConnman::ThreadOpenAddedConnections() { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); AssertLockNotHeld(m_reconnections_mutex); while (true) { @@ -3088,7 +3047,6 @@ void CConnman::ThreadOpenAddedConnections() // if successful, this moves the passed grant to the constructed node void CConnman::OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant&& grant_outbound, const char *pszDest, ConnectionType conn_type, bool use_v2transport) { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); assert(conn_type != ConnectionType::INBOUND); // @@ -3946,7 +3904,6 @@ uint64_t CConnman::CalculateKeyedNetGroup(const CNetAddr& address) const void CConnman::PerformReconnections() { AssertLockNotHeld(m_reconnections_mutex); - AssertLockNotHeld(m_unused_i2p_sessions_mutex); while (true) { // Move first element of m_reconnections to todo (avoiding an allocation inside the lock). decltype(m_reconnections) todo; diff --git a/src/net.h b/src/net.h index a786a8a8d7f..ec5f1917491 100644 --- a/src/net.h +++ b/src/net.h @@ -42,7 +42,6 @@ #include #include #include -#include #include #include #include @@ -1138,7 +1137,7 @@ public: bool GetNetworkActive() const { return fNetworkActive; }; bool GetUseAddrmanOutgoing() const { return m_use_addrman_outgoing; }; void SetNetworkActive(bool active); - void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant&& grant_outbound, const char* strDest, ConnectionType conn_type, bool use_v2transport) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + void OpenNetworkConnection(const CAddress& addrConnect, bool fCountFailure, CSemaphoreGrant&& grant_outbound, const char* strDest, ConnectionType conn_type, bool use_v2transport); bool CheckIncomingNonce(uint64_t nonce); void ASMapHealthCheck(); @@ -1223,7 +1222,7 @@ public: * - Max total outbound connection capacity filled * - Max connection capacity for type is filled */ - bool AddConnection(const std::string& address, ConnectionType conn_type, bool use_v2transport) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + bool AddConnection(const std::string& address, ConnectionType conn_type, bool use_v2transport); size_t GetNodeCount(ConnectionDirection) const; std::map getNetLocalAddresses() const; @@ -1282,10 +1281,10 @@ private: bool Bind(const CService& addr, unsigned int flags, NetPermissionFlags permissions); bool InitBinds(const Options& options); - void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_unused_i2p_sessions_mutex, !m_reconnections_mutex); + void ThreadOpenAddedConnections() EXCLUSIVE_LOCKS_REQUIRED(!m_added_nodes_mutex, !m_reconnections_mutex); void AddAddrFetch(const std::string& strDest) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex); - void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_unused_i2p_sessions_mutex); - void ThreadOpenConnections(std::vector connect, Span seed_nodes) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_unused_i2p_sessions_mutex, !m_reconnections_mutex); + void ProcessAddrFetch() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex); + void ThreadOpenConnections(std::vector connect, Span seed_nodes) EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_added_nodes_mutex, !m_nodes_mutex, !m_reconnections_mutex); void ThreadMessageHandler() EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc); /// Whether we are currently advertising our I2P address (via `AddLocal()`). @@ -1377,7 +1376,7 @@ private: bool AlreadyConnectedToAddress(const CAddress& addr); bool AttemptToEvictConnection(); - CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type, bool use_v2transport) EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + CNode* ConnectNode(CAddress addrConnect, const char *pszDest, bool fCountFailure, ConnectionType conn_type, bool use_v2transport); void AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr, const std::vector& ranges) const; void DeleteNode(CNode* pnode); @@ -1593,20 +1592,6 @@ private: */ bool whitelist_relay; - /** - * Mutex protecting m_i2p_sam_sessions. - */ - Mutex m_unused_i2p_sessions_mutex; - - /** - * A pool of created I2P SAM transient sessions that should be used instead - * of creating new ones in order to reduce the load on the I2P network. - * Creating a session in I2P is not cheap, thus if this is not empty, then - * pick an entry from it instead of creating a new session. If connecting to - * a host fails, then the created session is put to this pool for reuse. - */ - std::queue> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex); - /** * Mutex protecting m_reconnections. */ @@ -1628,13 +1613,7 @@ private: std::list m_reconnections GUARDED_BY(m_reconnections_mutex); /** Attempt reconnections, if m_reconnections non-empty. */ - void PerformReconnections() EXCLUSIVE_LOCKS_REQUIRED(!m_reconnections_mutex, !m_unused_i2p_sessions_mutex); - - /** - * Cap on the size of `m_unused_i2p_sessions`, to ensure it does not - * unexpectedly use too much memory. - */ - static constexpr size_t MAX_UNUSED_I2P_SESSIONS_SIZE{10}; + void PerformReconnections() EXCLUSIVE_LOCKS_REQUIRED(!m_reconnections_mutex); /** * RAII helper to atomically create a copy of `m_nodes` and add a reference diff --git a/src/test/util/net.h b/src/test/util/net.h index 7215df22c35..c164507a20d 100644 --- a/src/test/util/net.h +++ b/src/test/util/net.h @@ -90,8 +90,7 @@ struct ConnmanTestMsg : public CConnman { bool AlreadyConnectedPublic(const CAddress& addr) { return AlreadyConnectedToAddress(addr); }; - CNode* ConnectNodePublic(PeerManager& peerman, const char* pszDest, ConnectionType conn_type) - EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + CNode* ConnectNodePublic(PeerManager& peerman, const char* pszDest, ConnectionType conn_type); }; constexpr ServiceFlags ALL_SERVICE_FLAGS[]{ From 52106d013657485aaecc3d0ba4dcea26de71f6ec Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Tue, 24 Sep 2024 09:41:47 +0200 Subject: [PATCH 15/42] net: tweak EventNewConnectionAccepted() Move `MaybeFlipIPv6toCJDNS()`, which is Bitcoin P2P specific from the callers of `CConnman::EventNewConnectionAccepted()` to inside that method. Move the IsSelectable check, the `TCP_NODELAY` option set and the generation of new connection id out of `CConnman::EventNewConnectionAccepted()` because those are protocol agnostic. Move those to a new method `SockMan::NewSockAccepted()` which is called instead of `CConnman::EventNewConnectionAccepted()`. --- src/common/sockman.cpp | 25 ++++++++++++++++++++++++- src/common/sockman.h | 13 ++++++++++++- src/net.cpp | 29 ++++++++--------------------- src/net.h | 4 +++- 4 files changed, 47 insertions(+), 24 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 02fc425d28c..4ddbfcb8edf 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -226,6 +226,26 @@ std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CServic return sock; } +void SockMan::NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) +{ + if (!sock->IsSelectable()) { + LogPrintf("connection from %s dropped: non-selectable socket\n", them.ToStringAddrPort()); + return; + } + + // According to the internet TCP_NODELAY is not carried into accepted sockets + // on all platforms. Set it again here just to be sure. + const int on{1}; + if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { + LogDebug(BCLog::NET, "connection from %s: unable to set TCP_NODELAY, continuing anyway\n", + them.ToStringAddrPort()); + } + + const Id id{GetNewId()}; + + EventNewConnectionAccepted(id, std::move(sock), me, them); +} + SockMan::Id SockMan::GetNewId() { return m_next_id.fetch_add(1, std::memory_order_relaxed); @@ -276,7 +296,10 @@ void SockMan::ThreadI2PAccept() continue; } - EventNewConnectionAccepted(std::move(conn.sock), conn.me, conn.peer); + Assume(conn.me.IsI2P()); + Assume(conn.peer.IsI2P()); + + NewSockAccepted(std::move(conn.sock), conn.me, conn.peer); err_wait = err_wait_begin; } diff --git a/src/common/sockman.h b/src/common/sockman.h index 33d368630b3..f417987c3cc 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -137,6 +137,15 @@ public: */ std::unique_ptr AcceptConnection(const Sock& listen_sock, CService& addr); + /** + * After a new socket with a peer has been created, configure its flags, + * make a new connection id and call `EventNewConnectionAccepted()`. + * @param[in] sock The newly created socket. + * @param[in] me Address at our end of the connection. + * @param[in] them Address of the new peer. + */ + void NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them); + /** * Generate an id for a newly created connection. */ @@ -181,11 +190,13 @@ private: /** * Be notified when a new connection has been accepted. + * @param[in] id Id of the newly accepted connection. * @param[in] sock Connected socket to communicate with the peer. * @param[in] me The address and port at our side of the connection. * @param[in] them The address and port at the peer's side of the connection. */ - virtual void EventNewConnectionAccepted(std::unique_ptr&& sock, + virtual void EventNewConnectionAccepted(Id id, + std::unique_ptr&& sock, const CService& me, const CService& them) = 0; diff --git a/src/net.cpp b/src/net.cpp index e8a1a1301d1..06eaabbe9ba 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -1712,10 +1712,14 @@ bool CConnman::AttemptToEvictConnection() return false; } -void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, - const CService& addr_bind, - const CService& addr) +void CConnman::EventNewConnectionAccepted(SockMan::Id id, + std::unique_ptr&& sock, + const CService& me, + const CService& them) { + const CService addr_bind{MaybeFlipIPv6toCJDNS(me)}; + const CService addr{MaybeFlipIPv6toCJDNS(them)}; + int nInbound = 0; NetPermissionFlags permission_flags = NetPermissionFlags::None; @@ -1738,19 +1742,6 @@ void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, return; } - if (!sock->IsSelectable()) { - LogPrintf("connection from %s dropped: non-selectable socket\n", addr.ToStringAddrPort()); - return; - } - - // According to the internet TCP_NODELAY is not carried into accepted sockets - // on all platforms. Set it again here just to be sure. - const int on{1}; - if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { - LogDebug(BCLog::NET, "connection from %s: unable to set TCP_NODELAY, continuing anyway\n", - addr.ToStringAddrPort()); - } - // Don't accept connections from banned peers. bool banned = m_banman && m_banman->IsBanned(addr); if (!NetPermissions::HasFlag(permission_flags, NetPermissionFlags::NoBan) && banned) @@ -1776,7 +1767,6 @@ void CConnman::EventNewConnectionAccepted(std::unique_ptr&& sock, } } - NodeId id = GetNewId(); uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); const bool inbound_onion = std::find(m_onion_binds.begin(), m_onion_binds.end(), addr_bind) != m_onion_binds.end(); @@ -2278,10 +2268,7 @@ void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock auto sock_accepted{AcceptConnection(*sock, addr_accepted)}; if (sock_accepted) { - addr_accepted = MaybeFlipIPv6toCJDNS(addr_accepted); - const CService addr_bind{MaybeFlipIPv6toCJDNS(GetBindAddress(*sock))}; - - EventNewConnectionAccepted(std::move(sock_accepted), addr_bind, addr_accepted); + NewSockAccepted(std::move(sock_accepted), GetBindAddress(*sock), addr_accepted); } } } diff --git a/src/net.h b/src/net.h index ec5f1917491..5e935e2a226 100644 --- a/src/net.h +++ b/src/net.h @@ -1294,11 +1294,13 @@ private: /** * Create a `CNode` object and add it to the `m_nodes` member. + * @param[in] id Id of the newly accepted connection. * @param[in] sock Connected socket to communicate with the peer. * @param[in] me The address and port at our side of the connection. * @param[in] them The address and port at the peer's side of the connection. */ - virtual void EventNewConnectionAccepted(std::unique_ptr&& sock, + virtual void EventNewConnectionAccepted(SockMan::Id id, + std::unique_ptr&& sock, const CService& me, const CService& them) override; From 08dc1ee704d72d679e15456411dd08b0b606ad1b Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 23 Sep 2024 11:03:32 +0200 Subject: [PATCH 16/42] net: move sockets from CNode to SockMan Move `CNode::m_sock` and `CNode::m_i2p_sam_session` to `SockMan::m_connected`. Also move all the code that handles sockets to `SockMan`. `CNode::CloseSocketDisconnect()` becomes `CConnman::MarkAsDisconnectAndCloseConnection()`. `CConnman::SocketSendData()` is renamed to `CConnman::SendMessagesAsBytes()` and its sockets-touching bits are moved to `SockMan::SendBytes()`. `CConnman::GenerateWaitSockets()` goes to `SockMan::GenerateWaitSockets()`. `CConnman::ThreadSocketHandler()` and `CConnman::SocketHandler()` are combined into `SockMan::ThreadSocketHandler()`. `CConnman::SocketHandlerConnected()` goes to `SockMan::SocketHandlerConnected()`. `CConnman::SocketHandlerListening()` goes to `SockMan::SocketHandlerListening()`. --- src/common/sockman.cpp | 239 +++++++++++++++++++- src/common/sockman.h | 180 +++++++++++++-- src/net.cpp | 293 ++++--------------------- src/net.h | 74 +------ src/test/denialofservice_tests.cpp | 6 - src/test/fuzz/connman.cpp | 14 +- src/test/fuzz/net.cpp | 3 - src/test/fuzz/p2p_handshake.cpp | 2 +- src/test/fuzz/p2p_headers_presync.cpp | 2 +- src/test/fuzz/process_message.cpp | 2 +- src/test/fuzz/process_messages.cpp | 2 +- src/test/fuzz/util/net.h | 3 - src/test/net_peer_connection_tests.cpp | 1 - src/test/net_tests.cpp | 9 - src/test/util/net.h | 8 + 15 files changed, 482 insertions(+), 356 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 4ddbfcb8edf..4a87344a0cd 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -10,8 +10,14 @@ #include #include +#include + +// The set of sockets cannot be modified while waiting +// The sleep time needs to be small to avoid new sockets stalling +static constexpr auto SELECT_TIMEOUT{50ms}; + /** Get the bind address for a socket as CService. */ -CService GetBindAddress(const Sock& sock) +static CService GetBindAddress(const Sock& sock) { CService addr_bind; struct sockaddr_storage sockaddr_bind; @@ -108,6 +114,9 @@ bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) void SockMan::StartSocketsThreads(const Options& options) { + m_thread_socket_handler = std::thread( + &util::TraceThread, options.socket_handler_thread_name, [this] { ThreadSocketHandler(); }); + if (options.i2p.has_value()) { m_i2p_sam_session = std::make_unique( options.i2p->private_key_file, options.i2p->sam_proxy, &interruptNet); @@ -122,6 +131,10 @@ void SockMan::JoinSocketsThreads() if (m_thread_i2p_accept.joinable()) { m_thread_i2p_accept.join(); } + + if (m_thread_socket_handler.joinable()) { + m_thread_socket_handler.join(); + } } std::optional @@ -129,12 +142,14 @@ SockMan::ConnectAndMakeId(const std::variant& to, bool is_important, std::optional proxy, bool& proxy_failed, - CService& me, - std::unique_ptr& sock, - std::unique_ptr& i2p_transient_session) + CService& me) { + AssertLockNotHeld(m_connected_mutex); AssertLockNotHeld(m_unused_i2p_sessions_mutex); + std::unique_ptr sock; + std::unique_ptr i2p_transient_session; + Assume(!me.IsValid()); if (std::holds_alternative(to)) { @@ -198,6 +213,12 @@ SockMan::ConnectAndMakeId(const std::variant& to, const Id id{GetNewId()}; + { + LOCK(m_connected_mutex); + m_connected.emplace(id, std::make_shared(std::move(sock), + std::move(i2p_transient_session))); + } + return id; } @@ -228,6 +249,8 @@ std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CServic void SockMan::NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) { + AssertLockNotHeld(m_connected_mutex); + if (!sock->IsSelectable()) { LogPrintf("connection from %s dropped: non-selectable socket\n", them.ToStringAddrPort()); return; @@ -243,7 +266,14 @@ void SockMan::NewSockAccepted(std::unique_ptr&& sock, const CService& me, const Id id{GetNewId()}; - EventNewConnectionAccepted(id, std::move(sock), me, them); + { + LOCK(m_connected_mutex); + m_connected.emplace(id, std::make_shared(std::move(sock))); + } + + if (!EventNewConnectionAccepted(id, me, them)) { + CloseConnection(id); + } } SockMan::Id SockMan::GetNewId() @@ -251,6 +281,52 @@ SockMan::Id SockMan::GetNewId() return m_next_id.fetch_add(1, std::memory_order_relaxed); } +bool SockMan::CloseConnection(Id id) +{ + LOCK(m_connected_mutex); + return m_connected.erase(id) > 0; +} + +ssize_t SockMan::SendBytes(Id id, + std::span data, + bool will_send_more, + std::string& errmsg) const +{ + AssertLockNotHeld(m_connected_mutex); + + if (data.empty()) { + return 0; + } + + auto sockets{GetConnectionSockets(id)}; + if (!sockets) { + // Bail out immediately and just leave things in the caller's send queue. + return 0; + } + + int flags{MSG_NOSIGNAL | MSG_DONTWAIT}; +#ifdef MSG_MORE + if (will_send_more) { + flags |= MSG_MORE; + } +#endif + + const ssize_t sent{WITH_LOCK( + sockets->mutex, + return sockets->sock->Send(reinterpret_cast(data.data()), data.size(), flags);)}; + + if (sent >= 0) { + return sent; + } + + const int err{WSAGetLastError()}; + if (err == WSAEWOULDBLOCK || err == WSAEMSGSIZE || err == WSAEINTR || err == WSAEINPROGRESS) { + return 0; + } + errmsg = NetworkErrorString(err); + return -1; +} + void SockMan::StopListening() { m_listen.clear(); @@ -266,8 +342,17 @@ void SockMan::EventIOLoopCompletedForAll() {} void SockMan::EventI2PStatus(const CService&, I2PStatus) {} +void SockMan::TestOnlyAddExistentConnection(Id id, std::unique_ptr&& sock) +{ + LOCK(m_connected_mutex); + const auto result{m_connected.emplace(id, std::make_shared(std::move(sock)))}; + assert(result.second); +} + void SockMan::ThreadI2PAccept() { + AssertLockNotHeld(m_connected_mutex); + static constexpr auto err_wait_begin = 1s; static constexpr auto err_wait_cap = 5min; auto err_wait = err_wait_begin; @@ -304,3 +389,147 @@ void SockMan::ThreadI2PAccept() err_wait = err_wait_begin; } } + +void SockMan::ThreadSocketHandler() +{ + AssertLockNotHeld(m_connected_mutex); + + while (!interruptNet) { + EventIOLoopCompletedForAll(); + + // Check for the readiness of the already connected sockets and the + // listening sockets in one call ("readiness" as in poll(2) or + // select(2)). If none are ready, wait for a short while and return + // empty sets. + auto io_readiness{GenerateWaitSockets()}; + if (io_readiness.events_per_sock.empty() || + // WaitMany() may as well be a static method, the context of the first Sock in the vector is not relevant. + !io_readiness.events_per_sock.begin()->first->WaitMany(SELECT_TIMEOUT, + io_readiness.events_per_sock)) { + interruptNet.sleep_for(SELECT_TIMEOUT); + } + + // Service (send/receive) each of the already connected sockets. + SocketHandlerConnected(io_readiness); + + // Accept new connections from listening sockets. + SocketHandlerListening(io_readiness.events_per_sock); + } +} + +SockMan::IOReadiness SockMan::GenerateWaitSockets() +{ + AssertLockNotHeld(m_connected_mutex); + + IOReadiness io_readiness; + + for (const auto& sock : m_listen) { + io_readiness.events_per_sock.emplace(sock, Sock::Events{Sock::RECV}); + } + + auto connected_snapshot{WITH_LOCK(m_connected_mutex, return m_connected;)}; + + for (const auto& [id, sockets] : connected_snapshot) { + const bool select_recv{ShouldTryToRecv(id)}; + const bool select_send{ShouldTryToSend(id)}; + if (!select_recv && !select_send) continue; + + Sock::Event event = (select_send ? Sock::SEND : 0) | (select_recv ? Sock::RECV : 0); + io_readiness.events_per_sock.emplace(sockets->sock, Sock::Events{event}); + io_readiness.ids_per_sock.emplace(sockets->sock, id); + } + + return io_readiness; +} + +void SockMan::SocketHandlerConnected(const IOReadiness& io_readiness) +{ + AssertLockNotHeld(m_connected_mutex); + + for (const auto& [sock, events] : io_readiness.events_per_sock) { + if (interruptNet) { + return; + } + + auto it{io_readiness.ids_per_sock.find(sock)}; + if (it == io_readiness.ids_per_sock.end()) { + continue; + } + const Id id{it->second}; + + bool send_ready = events.occurred & Sock::SEND; // Sock::SEND could only be set if ShouldTryToSend() has returned true in GenerateWaitSockets(). + bool recv_ready = events.occurred & Sock::RECV; // Sock::RECV could only be set if ShouldTryToRecv() has returned true in GenerateWaitSockets(). + bool err_ready = events.occurred & Sock::ERR; + + if (send_ready) { + bool cancel_recv; + + EventReadyToSend(id, cancel_recv); + + if (cancel_recv) { + recv_ready = false; + } + } + + if (recv_ready || err_ready) { + uint8_t buf[0x10000]; // typical socket buffer is 8K-64K + + auto sockets{GetConnectionSockets(id)}; + if (!sockets) { + continue; + } + + const ssize_t nrecv{WITH_LOCK( + sockets->mutex, + return sockets->sock->Recv(buf, sizeof(buf), MSG_DONTWAIT);)}; + + if (nrecv < 0) { // In all cases (including -1 and 0) EventIOLoopCompletedForOne() should be executed after this, don't change the code to skip it. + const int err = WSAGetLastError(); + if (err != WSAEWOULDBLOCK && err != WSAEMSGSIZE && err != WSAEINTR && err != WSAEINPROGRESS) { + EventGotPermanentReadError(id, NetworkErrorString(err)); + } + } else if (nrecv == 0) { + EventGotEOF(id); + } else { + EventGotData(id, {buf, static_cast(nrecv)}); + } + } + + EventIOLoopCompletedForOne(id); + } +} + +void SockMan::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock) +{ + AssertLockNotHeld(m_connected_mutex); + + for (const auto& sock : m_listen) { + if (interruptNet) { + return; + } + const auto it = events_per_sock.find(sock); + if (it != events_per_sock.end() && it->second.occurred & Sock::RECV) { + CService addr_accepted; + + auto sock_accepted{AcceptConnection(*sock, addr_accepted)}; + + if (sock_accepted) { + NewSockAccepted(std::move(sock_accepted), GetBindAddress(*sock), addr_accepted); + } + } + } +} + +std::shared_ptr SockMan::GetConnectionSockets(Id id) const +{ + LOCK(m_connected_mutex); + + auto it{m_connected.find(id)}; + if (it == m_connected.end()) { + // There is no socket in case we've already disconnected, or in test cases without + // real connections. + return {}; + } + + return it->second; +} diff --git a/src/common/sockman.h b/src/common/sockman.h index f417987c3cc..fcb8d9e1976 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -16,12 +16,11 @@ #include #include #include +#include #include #include #include -CService GetBindAddress(const Sock& sock); - /** * A socket manager class which handles socket operations. * To use this class, inherit from it and implement the pure virtual methods. @@ -30,6 +29,8 @@ CService GetBindAddress(const Sock& sock); * - starting of necessary threads to process socket operations * - accepting incoming connections * - making outbound connections + * - closing connections + * - waiting for IO readiness on sockets and doing send/recv accordingly */ class SockMan { @@ -70,6 +71,8 @@ public: * Options to influence `StartSocketsThreads()`. */ struct Options { + std::string_view socket_handler_thread_name; + struct I2P { explicit I2P(const fs::path& file, const Proxy& proxy, std::string_view accept_thread_name) : private_key_file{file}, @@ -116,18 +119,14 @@ public: * proxy, then it will be set to true. * @param[out] me If the connection was successful then this is set to the address on the * local side of the socket. - * @param[out] sock Connected socket, if the operation is successful. - * @param[out] i2p_transient_session I2P session, if the operation is successful. * @return Newly generated id, or std::nullopt if the operation fails. */ std::optional ConnectAndMakeId(const std::variant& to, bool is_important, std::optional proxy, bool& proxy_failed, - CService& me, - std::unique_ptr& sock, - std::unique_ptr& i2p_transient_session) - EXCLUSIVE_LOCKS_REQUIRED(!m_unused_i2p_sessions_mutex); + CService& me) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex, !m_unused_i2p_sessions_mutex); /** * Accept a connection. @@ -144,13 +143,39 @@ public: * @param[in] me Address at our end of the connection. * @param[in] them Address of the new peer. */ - void NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them); + void NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); /** * Generate an id for a newly created connection. */ Id GetNewId(); + /** + * Destroy a given connection by closing its socket and release resources occupied by it. + * @param[in] id Connection to destroy. + * @return Whether the connection existed and its socket was closed by this call. + */ + bool CloseConnection(Id id) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Try to send some data over the given connection. + * @param[in] id Identifier of the connection. + * @param[in] data The data to send, it might happen that only a prefix of this is sent. + * @param[in] will_send_more Used as an optimization if the caller knows that they will + * be sending more data soon after this call. + * @param[out] errmsg If <0 is returned then this will contain a human readable message + * explaining the error. + * @retval >=0 The number of bytes actually sent. + * @retval <0 A permanent error has occurred. + */ + ssize_t SendBytes(Id id, + std::span data, + bool will_send_more, + std::string& errmsg) const + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + /** * Stop listening by closing all listening sockets. */ @@ -176,6 +201,17 @@ public: */ std::vector> m_listen; +protected: + + /** + * During some tests mocked sockets are created outside of `SockMan`, make it + * possible to add those so that send/recv can be exercised. + * @param[in] id Connection id to add. + * @param[in,out] sock Socket to associate with the added connection. + */ + void TestOnlyAddExistentConnection(Id id, std::unique_ptr&& sock) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + private: /** @@ -191,12 +227,13 @@ private: /** * Be notified when a new connection has been accepted. * @param[in] id Id of the newly accepted connection. - * @param[in] sock Connected socket to communicate with the peer. * @param[in] me The address and port at our side of the connection. * @param[in] them The address and port at the peer's side of the connection. + * @retval true The new connection was accepted at the higher level. + * @retval false The connection was refused at the higher level, so the + * associated socket and id should be discarded by `SockMan`. */ - virtual void EventNewConnectionAccepted(Id id, - std::unique_ptr&& sock, + virtual bool EventNewConnectionAccepted(Id id, const CService& me, const CService& them) = 0; @@ -239,8 +276,9 @@ private: /** * Can be used to temporarily pause sends on a connection. + * SockMan would only call EventReadyToSend() if this returns true. * The implementation in SockMan always returns true. - * @param[in] id Connection for which to confirm or omit the next send. + * @param[in] id Connection for which to confirm or omit the next call to EventReadyToSend(). */ virtual bool ShouldTryToSend(Id id) const; @@ -277,17 +315,122 @@ private: */ virtual void EventI2PStatus(const CService& addr, I2PStatus new_status); + /** + * The sockets used by a connection - a data socket and an optional I2P session socket. + */ + struct ConnectionSockets { + explicit ConnectionSockets(std::unique_ptr&& s) + : sock{std::move(s)} + { + } + + explicit ConnectionSockets(std::shared_ptr&& s, std::unique_ptr&& sess) + : sock{std::move(s)}, + i2p_transient_session{std::move(sess)} + { + } + + /** + * Mutex that serializes the Send() and Recv() calls on `sock`. + */ + Mutex mutex; + + /** + * Underlying socket. + * `shared_ptr` (instead of `unique_ptr`) is used to avoid premature close of the + * underlying file descriptor by one thread while another thread is poll(2)-ing + * it for activity. + * @see https://github.com/bitcoin/bitcoin/issues/21744 for details. + */ + std::shared_ptr sock; + + /** + * When transient I2P sessions are used, then each connection has its own session, otherwise + * all connections use the session from `m_i2p_sam_session` and share the same I2P address. + * I2P sessions involve a data/transport socket (in `sock`) and a control socket + * (in `i2p_transient_session`). For transient sessions, once the data socket `sock` is + * closed, the control socket is not going to be used anymore and would be just taking + * resources. Storing it here makes its deletion together with `sock` automatic. + */ + std::unique_ptr i2p_transient_session; + }; + + /** + * Info about which socket has which event ready and its connection id. + */ + struct IOReadiness { + /** + * Map of socket -> socket events. For example: + * socket1 -> { requested = SEND|RECV, occurred = RECV } + * socket2 -> { requested = SEND, occurred = SEND } + */ + Sock::EventsPerSock events_per_sock; + + /** + * Map of socket -> connection id (in `m_connected`). For example + * socket1 -> id=23 + * socket2 -> id=56 + */ + std::unordered_map + ids_per_sock; + }; + /** * Accept incoming I2P connections in a loop and call * `EventNewConnectionAccepted()` for each new connection. */ - void ThreadI2PAccept(); + void ThreadI2PAccept() + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Check connected and listening sockets for IO readiness and process them accordingly. + */ + void ThreadSocketHandler() + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Generate a collection of sockets to check for IO readiness. + * @return Sockets to check for readiness plus an aux map to find the + * corresponding connection id given a socket. + */ + IOReadiness GenerateWaitSockets() + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Do the read/write for connected sockets that are ready for IO. + * @param[in] io_readiness Which sockets are ready and their connection ids. + */ + void SocketHandlerConnected(const IOReadiness& io_readiness) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Accept incoming connections, one from each read-ready listening socket. + * @param[in] events_per_sock Sockets that are ready for IO. + */ + void SocketHandlerListening(const Sock::EventsPerSock& events_per_sock) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Retrieve an entry from m_connected. + * @param[in] id Connection id to search for. + * @return ConnectionSockets for the given connection id or empty shared_ptr if not found. + */ + std::shared_ptr GetConnectionSockets(Id id) const + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); /** * The id to assign to the next created connection. Used to generate ids of connections. */ std::atomic m_next_id{0}; + /** + * Thread that sends to and receives from sockets and accepts connections. + */ + std::thread m_thread_socket_handler; + /** * Thread that accepts incoming I2P connections in a loop, can be stopped via `interruptNet`. */ @@ -306,6 +449,15 @@ private: * a host fails, then the created session is put to this pool for reuse. */ std::queue> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex); + + mutable Mutex m_connected_mutex; + + /** + * Sockets for existent connections. + * The `shared_ptr` makes it possible to create a snapshot of this by simply copying + * it (under `m_connected_mutex`). + */ + std::unordered_map> m_connected GUARDED_BY(m_connected_mutex); }; #endif // BITCOIN_COMMON_SOCKMAN_H diff --git a/src/net.cpp b/src/net.cpp index 06eaabbe9ba..7b2d5e8a2c4 100644 --- a/src/net.cpp +++ b/src/net.cpp @@ -107,10 +107,6 @@ enum BindFlags { BF_DONT_ADVERTISE = (1U << 1), }; -// The set of sockets cannot be modified while waiting -// The sleep time needs to be small to avoid new sockets stalling -static const uint64_t SELECT_TIMEOUT_MILLISECONDS = 50; - const std::string NET_MESSAGE_TYPE_OTHER = "*other*"; static const uint64_t RANDOMIZER_ID_NETGROUP = 0x6c0edd8036ef4036ULL; // SHA256("netgroup")[0:8] @@ -441,9 +437,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo } // Connect - std::unique_ptr sock; Proxy proxy; - std::unique_ptr i2p_transient_session; std::optional node_id; CService me; @@ -461,9 +455,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo /*is_important=*/conn_type == ConnectionType::MANUAL, use_proxy ? std::optional{proxy} : std::nullopt, proxyConnectionFailed, - me, - sock, - i2p_transient_session); + me); if (!proxyConnectionFailed) { // If a connection to the node was attempted, and failure (if any) is not caused by a problem connecting to @@ -480,9 +472,7 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo /*is_important=*/conn_type == ConnectionType::MANUAL, proxy, dummy, - me, - sock, - i2p_transient_session); + me); } // Check any other resolved address (if any) if we fail to connect if (!node_id.has_value()) { @@ -495,7 +485,6 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo const uint64_t nonce{GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(node_id.value()).Finalize()}; CNode* pnode = new CNode(node_id.value(), - std::move(sock), target_addr, CalculateKeyedNetGroup(target_addr), nonce, @@ -505,7 +494,6 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo /*inbound_onion=*/false, CNodeOptions{ .permission_flags = permission_flags, - .i2p_sam_session = std::move(i2p_transient_session), .recv_flood_size = nReceiveFloodSize, .use_v2transport = use_v2transport, }); @@ -520,24 +508,6 @@ CNode* CConnman::ConnectNode(CAddress addrConnect, const char *pszDest, bool fCo return nullptr; } -void CNode::CloseSocketDisconnect() -{ - fDisconnect = true; - LOCK(m_sock_mutex); - if (m_sock) { - LogDebug(BCLog::NET, "Resetting socket for peer=%d%s", GetId(), LogIP(fLogIPs)); - m_sock.reset(); - - TRACEPOINT(net, closed_connection, - GetId(), - m_addr_name.c_str(), - ConnectionTypeAsString().c_str(), - ConnectedThroughNetwork(), - Ticks(m_connected)); - } - m_i2p_sam_session.reset(); -} - void CConnman::AddWhitelistPermissionFlags(NetPermissionFlags& flags, const CNetAddr &addr, const std::vector& ranges) const { for (const auto& subnet : ranges) { if (subnet.m_subnet.Match(addr)) { @@ -1559,7 +1529,7 @@ Transport::Info V2Transport::GetInfo() const noexcept return info; } -std::pair CConnman::SocketSendData(CNode& node) +std::pair CConnman::SendMessagesAsBytes(CNode& node) { AssertLockNotHeld(m_total_bytes_sent_mutex); @@ -1587,45 +1557,27 @@ std::pair CConnman::SocketSendData(CNode& node) if (expected_more.has_value()) Assume(!data.empty() == *expected_more); expected_more = more; data_left = !data.empty(); // will be overwritten on next loop if all of data gets sent - int nBytes = 0; - if (!data.empty()) { - LOCK(node.m_sock_mutex); - // There is no socket in case we've already disconnected, or in test cases without - // real connections. In these cases, we bail out immediately and just leave things - // in the send queue and transport. - if (!node.m_sock) { - break; - } - int flags = MSG_NOSIGNAL | MSG_DONTWAIT; -#ifdef MSG_MORE - if (more) { - flags |= MSG_MORE; - } -#endif - nBytes = node.m_sock->Send(reinterpret_cast(data.data()), data.size(), flags); - } - if (nBytes > 0) { + + std::string errmsg; + const ssize_t sent{SendBytes(node.GetId(), data, more, errmsg)}; + if (sent > 0) { node.m_last_send = GetTime(); - node.nSendBytes += nBytes; + node.nSendBytes += sent; // Notify transport that bytes have been processed. - node.m_transport->MarkBytesSent(nBytes); + node.m_transport->MarkBytesSent(sent); // Update statistics per message type. if (!msg_type.empty()) { // don't report v2 handshake bytes for now - node.AccountForSentBytes(msg_type, nBytes); + node.AccountForSentBytes(msg_type, sent); } - nSentSize += nBytes; - if ((size_t)nBytes != data.size()) { + nSentSize += sent; + if (static_cast(sent) != data.size()) { // could not send full message; stop sending more break; } } else { - if (nBytes < 0) { - // error - int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) { - LogDebug(BCLog::NET, "socket send error, %s: %s\n", node.DisconnectMsg(fLogIPs), NetworkErrorString(nErr)); - node.CloseSocketDisconnect(); - } + if (sent < 0) { + LogDebug(BCLog::NET, "socket send error, %s: %s\n", node.DisconnectMsg(fLogIPs), errmsg); + MarkAsDisconnectAndCloseConnection(node); } break; } @@ -1712,8 +1664,7 @@ bool CConnman::AttemptToEvictConnection() return false; } -void CConnman::EventNewConnectionAccepted(SockMan::Id id, - std::unique_ptr&& sock, +bool CConnman::EventNewConnectionAccepted(SockMan::Id id, const CService& me, const CService& them) { @@ -1739,7 +1690,7 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, if (!fNetworkActive) { LogDebug(BCLog::NET, "connection from %s dropped: not accepting new connections\n", addr.ToStringAddrPort()); - return; + return false; } // Don't accept connections from banned peers. @@ -1747,7 +1698,7 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, if (!NetPermissions::HasFlag(permission_flags, NetPermissionFlags::NoBan) && banned) { LogDebug(BCLog::NET, "connection from %s dropped (banned)\n", addr.ToStringAddrPort()); - return; + return false; } // Only accept connections from discouraged peers if our inbound slots aren't (almost) full. @@ -1755,7 +1706,7 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, if (!NetPermissions::HasFlag(permission_flags, NetPermissionFlags::NoBan) && nInbound + 1 >= m_max_inbound && discouraged) { LogDebug(BCLog::NET, "connection from %s dropped (discouraged)\n", addr.ToStringAddrPort()); - return; + return false; } if (nInbound >= m_max_inbound) @@ -1763,7 +1714,7 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, if (!AttemptToEvictConnection()) { // No connection to evict, disconnect the new connection LogDebug(BCLog::NET, "failed to find an eviction candidate - connection dropped (full)\n"); - return; + return false; } } @@ -1776,7 +1727,6 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, const bool use_v2transport(local_services & NODE_P2P_V2); CNode* pnode = new CNode(id, - std::move(sock), CAddress{addr, NODE_NONE}, CalculateKeyedNetGroup(addr), nonce, @@ -1806,6 +1756,8 @@ void CConnman::EventNewConnectionAccepted(SockMan::Id id, // We received a new connection, harvest entropy from the time (and our peer count) RandAddEvent((uint32_t)id); + + return true; } bool CConnman::AddConnection(const std::string& address, ConnectionType conn_type, bool use_v2transport = false) @@ -1847,6 +1799,20 @@ bool CConnman::AddConnection(const std::string& address, ConnectionType conn_typ return true; } +void CConnman::MarkAsDisconnectAndCloseConnection(CNode& node) +{ + node.fDisconnect = true; + if (CloseConnection(node.GetId())) { + LogDebug(BCLog::NET, "Closed sockets for peer=%d%s", node.GetId(), node.LogIP(fLogIPs)); + TRACEPOINT(net, closed_connection, + node.GetId(), + node.m_addr_name.c_str(), + node.ConnectionTypeAsString().c_str(), + node.ConnectedThroughNetwork(), + Ticks(node.m_connected)); + } +} + void CConnman::DisconnectNodes() { AssertLockNotHeld(m_nodes_mutex); @@ -1897,8 +1863,7 @@ void CConnman::DisconnectNodes() // release outbound grant (if any) pnode->grantOutbound.Release(); - // close socket and cleanup - pnode->CloseSocketDisconnect(); + MarkAsDisconnectAndCloseConnection(*pnode); // update connection count by network if (pnode->IsManualOrFullOutboundConn()) --m_network_conn_counts[pnode->addr.GetNetwork()]; @@ -2011,7 +1976,7 @@ void CConnman::EventReadyToSend(SockMan::Id id, bool& cancel_recv) return; } - const auto [bytes_sent, data_left] = WITH_LOCK(node->cs_vSend, return SocketSendData(*node);); + const auto [bytes_sent, data_left] = WITH_LOCK(node->cs_vSend, return SendMessagesAsBytes(*node);); // If both receiving and (non-optimistic) sending were possible, we first attempt // sending. If that succeeds, but does not fully drain the send queue, do not @@ -2039,7 +2004,7 @@ void CConnman::EventGotData(SockMan::Id id, std::span data) "receiving message bytes failed, %s\n", node->DisconnectMsg(fLogIPs) ); - node->CloseSocketDisconnect(); + MarkAsDisconnectAndCloseConnection(*node); } RecordBytesRecv(data.size()); if (notify) { @@ -2060,7 +2025,7 @@ void CConnman::EventGotEOF(SockMan::Id id) if (!node->fDisconnect) { LogDebug(BCLog::NET, "socket closed for peer=%d\n", id); } - node->CloseSocketDisconnect(); + MarkAsDisconnectAndCloseConnection(*node); } void CConnman::EventGotPermanentReadError(SockMan::Id id, const std::string& errmsg) @@ -2075,7 +2040,7 @@ void CConnman::EventGotPermanentReadError(SockMan::Id id, const std::string& err if (!node->fDisconnect) { LogDebug(BCLog::NET, "socket recv error for peer=%d: %s\n", id, errmsg); } - node->CloseSocketDisconnect(); + MarkAsDisconnectAndCloseConnection(*node); } bool CConnman::ShouldTryToSend(SockMan::Id id) const @@ -2128,164 +2093,6 @@ void CConnman::EventIOLoopCompletedForAll() NotifyNumConnectionsChanged(); } -Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) -{ - AssertLockNotHeld(m_nodes_mutex); - - Sock::EventsPerSock events_per_sock; - - for (const auto& sock : m_listen) { - events_per_sock.emplace(sock, Sock::Events{Sock::RECV}); - } - - for (CNode* pnode : nodes) { - const bool select_recv{ShouldTryToRecv(pnode->GetId())}; - const bool select_send{ShouldTryToSend(pnode->GetId())}; - if (!select_recv && !select_send) continue; - - LOCK(pnode->m_sock_mutex); - if (pnode->m_sock) { - Sock::Event event = (select_send ? Sock::SEND : 0) | (select_recv ? Sock::RECV : 0); - events_per_sock.emplace(pnode->m_sock, Sock::Events{event}); - } - } - - return events_per_sock; -} - -void CConnman::SocketHandler() -{ - AssertLockNotHeld(m_total_bytes_sent_mutex); - - Sock::EventsPerSock events_per_sock; - - { - const NodesSnapshot snap{*this, /*shuffle=*/false}; - - const auto timeout = std::chrono::milliseconds(SELECT_TIMEOUT_MILLISECONDS); - - // Check for the readiness of the already connected sockets and the - // listening sockets in one call ("readiness" as in poll(2) or - // select(2)). If none are ready, wait for a short while and return - // empty sets. - events_per_sock = GenerateWaitSockets(snap.Nodes()); - if (events_per_sock.empty() || !events_per_sock.begin()->first->WaitMany(timeout, events_per_sock)) { - interruptNet.sleep_for(timeout); - } - - // Service (send/receive) each of the already connected nodes. - SocketHandlerConnected(snap.Nodes(), events_per_sock); - } - - // Accept new connections from listening sockets. - SocketHandlerListening(events_per_sock); -} - -void CConnman::SocketHandlerConnected(const std::vector& nodes, - const Sock::EventsPerSock& events_per_sock) -{ - AssertLockNotHeld(m_nodes_mutex); - AssertLockNotHeld(m_total_bytes_sent_mutex); - - for (CNode* pnode : nodes) { - if (interruptNet) - return; - - // - // Receive - // - bool recvSet = false; - bool sendSet = false; - bool errorSet = false; - { - LOCK(pnode->m_sock_mutex); - if (!pnode->m_sock) { - continue; - } - const auto it = events_per_sock.find(pnode->m_sock); - if (it != events_per_sock.end()) { - recvSet = it->second.occurred & Sock::RECV; // Sock::RECV could only be set if ShouldTryToRecv() has returned true in GenerateWaitSockets(). - sendSet = it->second.occurred & Sock::SEND; // Sock::SEND could only be set if ShouldTryToSend() has returned true in GenerateWaitSockets(). - errorSet = it->second.occurred & Sock::ERR; - } - } - - if (sendSet) { - bool cancel_recv; - - EventReadyToSend(pnode->GetId(), cancel_recv); - - if (cancel_recv) { - recvSet = false; - } - } - - if (recvSet || errorSet) - { - // typical socket buffer is 8K-64K - uint8_t pchBuf[0x10000]; - int nBytes = 0; - { - LOCK(pnode->m_sock_mutex); - if (!pnode->m_sock) { - continue; - } - nBytes = pnode->m_sock->Recv(pchBuf, sizeof(pchBuf), MSG_DONTWAIT); - } - if (nBytes > 0) - { - EventGotData(pnode->GetId(), {pchBuf, static_cast(nBytes)}); - } - else if (nBytes == 0) - { - EventGotEOF(pnode->GetId()); - } - else if (nBytes < 0) - { - // error - int nErr = WSAGetLastError(); - if (nErr != WSAEWOULDBLOCK && nErr != WSAEMSGSIZE && nErr != WSAEINTR && nErr != WSAEINPROGRESS) - { - EventGotPermanentReadError(pnode->GetId(), NetworkErrorString(nErr)); - } - } - } - - EventIOLoopCompletedForOne(pnode->GetId()); - } -} - -void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock) -{ - for (const auto& sock : m_listen) { - if (interruptNet) { - return; - } - const auto it = events_per_sock.find(sock); - if (it != events_per_sock.end() && it->second.occurred & Sock::RECV) { - CService addr_accepted; - - auto sock_accepted{AcceptConnection(*sock, addr_accepted)}; - - if (sock_accepted) { - NewSockAccepted(std::move(sock_accepted), GetBindAddress(*sock), addr_accepted); - } - } - } -} - -void CConnman::ThreadSocketHandler() -{ - AssertLockNotHeld(m_nodes_mutex); - AssertLockNotHeld(m_total_bytes_sent_mutex); - - while (!interruptNet) - { - EventIOLoopCompletedForAll(); - SocketHandler(); - } -} - void CConnman::WakeMessageHandler() { { @@ -3301,11 +3108,10 @@ bool CConnman::Start(CScheduler& scheduler, const Options& connOptions) fMsgProcWake = false; } - // Send and receive from sockets, accept connections - threadSocketHandler = std::thread(&util::TraceThread, "net", [this] { ThreadSocketHandler(); }); - SockMan::Options sockman_options; + sockman_options.socket_handler_thread_name = "net"; + Proxy i2p_sam; if (GetProxy(NET_I2P, i2p_sam) && connOptions.m_i2p_accept_incoming) { sockman_options.i2p.emplace(gArgs.GetDataDirNet() / "i2p_private_key", i2p_sam, "i2paccept"); @@ -3401,8 +3207,6 @@ void CConnman::StopThreads() threadOpenAddedConnections.join(); if (threadDNSAddressSeed.joinable()) threadDNSAddressSeed.join(); - if (threadSocketHandler.joinable()) - threadSocketHandler.join(); } void CConnman::StopNodes() @@ -3426,7 +3230,7 @@ void CConnman::StopNodes() WITH_LOCK(m_nodes_mutex, nodes.swap(m_nodes)); for (auto& [_, pnode] : nodes) { LogDebug(BCLog::NET, "Stopping node, %s", pnode->DisconnectMsg(fLogIPs)); - pnode->CloseSocketDisconnect(); + MarkAsDisconnectAndCloseConnection(*pnode); DeleteNode(pnode); } @@ -3744,7 +3548,6 @@ static std::unique_ptr MakeTransport(NodeId id, bool use_v2transport, } CNode::CNode(NodeId idIn, - std::shared_ptr sock, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, @@ -3755,7 +3558,6 @@ CNode::CNode(NodeId idIn, CNodeOptions&& node_opts) : m_transport{MakeTransport(idIn, node_opts.use_v2transport, conn_type_in == ConnectionType::INBOUND)}, m_permission_flags{node_opts.permission_flags}, - m_sock{sock}, m_connected{GetTime()}, addr{addrIn}, addrBind{addrBindIn}, @@ -3767,8 +3569,7 @@ CNode::CNode(NodeId idIn, m_conn_type{conn_type_in}, id{idIn}, nLocalHostNonce{nLocalHostNonceIn}, - m_recv_flood_size{node_opts.recv_flood_size}, - m_i2p_sam_session{std::move(node_opts.i2p_sam_session)} + m_recv_flood_size{node_opts.recv_flood_size} { if (inbound_onion) assert(conn_type_in == ConnectionType::INBOUND); @@ -3854,13 +3655,13 @@ void CConnman::PushMessage(CNode* pnode, CSerializedNetMsg&& msg) // If there was nothing to send before, and there is now (predicted by the "more" value // returned by the GetBytesToSend call above), attempt "optimistic write": - // because the poll/select loop may pause for SELECT_TIMEOUT_MILLISECONDS before actually + // because the poll/select loop may pause for a while before actually // doing a send, try sending from the calling thread if the queue was empty before. // With a V1Transport, more will always be true here, because adding a message always // results in sendable bytes there, but with V2Transport this is not the case (it may // still be in the handshake). if (queue_was_empty && more) { - SocketSendData(*pnode); + SendMessagesAsBytes(*pnode); } } } diff --git a/src/net.h b/src/net.h index 5e935e2a226..4a7dd219a0b 100644 --- a/src/net.h +++ b/src/net.h @@ -663,7 +663,6 @@ public: struct CNodeOptions { NetPermissionFlags permission_flags = NetPermissionFlags::None; - std::unique_ptr i2p_sam_session = nullptr; bool prefer_evict = false; size_t recv_flood_size{DEFAULT_MAXRECEIVEBUFFER * 1000}; bool use_v2transport = false; @@ -679,16 +678,6 @@ public: const NetPermissionFlags m_permission_flags; - /** - * Socket used for communication with the node. - * May not own a Sock object (after `CloseSocketDisconnect()` or during tests). - * `shared_ptr` (instead of `unique_ptr`) is used to avoid premature close of - * the underlying file descriptor by one thread while another thread is - * poll(2)-ing it for activity. - * @see https://github.com/bitcoin/bitcoin/issues/21744 for details. - */ - std::shared_ptr m_sock GUARDED_BY(m_sock_mutex); - /** Sum of GetMemoryUsage of all vSendMsg entries. */ size_t m_send_memusage GUARDED_BY(cs_vSend){0}; /** Total number of bytes sent on the wire to this peer. */ @@ -696,7 +685,6 @@ public: /** Messages still to be fed to m_transport->SetMessageToSend. */ std::deque vSendMsg GUARDED_BY(cs_vSend); Mutex cs_vSend; - Mutex m_sock_mutex; Mutex cs_vRecv; uint64_t nRecvBytes GUARDED_BY(cs_vRecv){0}; @@ -880,7 +868,6 @@ public: std::atomic m_min_ping_time{std::chrono::microseconds::max()}; CNode(NodeId id, - std::shared_ptr sock, const CAddress& addrIn, uint64_t nKeyedNetGroupIn, uint64_t nLocalHostNonceIn, @@ -942,8 +929,6 @@ public: nRefCount--; } - void CloseSocketDisconnect() EXCLUSIVE_LOCKS_REQUIRED(!m_sock_mutex); - void CopyStats(CNodeStats& stats) EXCLUSIVE_LOCKS_REQUIRED(!m_subver_mutex, !m_addr_local_mutex, !cs_vSend, !cs_vRecv); std::string ConnectionTypeAsString() const { return ::ConnectionTypeAsString(m_conn_type); } @@ -988,18 +973,6 @@ private: mapMsgTypeSize mapSendBytesPerMsgType GUARDED_BY(cs_vSend); mapMsgTypeSize mapRecvBytesPerMsgType GUARDED_BY(cs_vRecv); - - /** - * If an I2P session is created per connection (for outbound transient I2P - * connections) then it is stored here so that it can be destroyed when the - * socket is closed. I2P sessions involve a data/transport socket (in `m_sock`) - * and a control socket (in `m_i2p_sam_session`). For transient sessions, once - * the data socket is closed, the control socket is not going to be used anymore - * and is just taking up resources. So better close it as soon as `m_sock` is - * closed. - * Otherwise this unique_ptr is empty. - */ - std::unique_ptr m_i2p_sam_session GUARDED_BY(m_sock_mutex); }; /** @@ -1295,15 +1268,21 @@ private: /** * Create a `CNode` object and add it to the `m_nodes` member. * @param[in] id Id of the newly accepted connection. - * @param[in] sock Connected socket to communicate with the peer. * @param[in] me The address and port at our side of the connection. * @param[in] them The address and port at the peer's side of the connection. + * @retval true on success + * @retval false on failure, meaning that the associated socket and node_id should be discarded */ - virtual void EventNewConnectionAccepted(SockMan::Id id, - std::unique_ptr&& sock, + virtual bool EventNewConnectionAccepted(SockMan::Id id, const CService& me, const CService& them) override; + /** + * Mark a node as disconnected and close its connection with the peer. + * @param[in] node Node to disconnect. + */ + void MarkAsDisconnectAndCloseConnection(CNode& node); + void DisconnectNodes() EXCLUSIVE_LOCKS_REQUIRED(!m_reconnections_mutex, !m_nodes_mutex); void NotifyNumConnectionsChanged(); /** Return true if the peer is inactive and should be disconnected. */ @@ -1333,36 +1312,6 @@ private: virtual void EventIOLoopCompletedForAll() override EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_reconnections_mutex); - /** - * Generate a collection of sockets to check for IO readiness. - * @param[in] nodes Select from these nodes' sockets. - * @return sockets to check for readiness - */ - Sock::EventsPerSock GenerateWaitSockets(Span nodes) - EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex); - - /** - * Check connected and listening sockets for IO readiness and process them accordingly. - */ - void SocketHandler() - EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_total_bytes_sent_mutex, !mutexMsgProc); - - /** - * Do the read/write for connected sockets that are ready for IO. - * @param[in] nodes Nodes to process. The socket of each node is checked against `what`. - * @param[in] events_per_sock Sockets that are ready for IO. - */ - void SocketHandlerConnected(const std::vector& nodes, - const Sock::EventsPerSock& events_per_sock) - EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_mutex, !m_total_bytes_sent_mutex, !mutexMsgProc); - - /** - * Accept incoming connections, one from each read-ready listening socket. - * @param[in] events_per_sock Sockets that are ready for IO. - */ - void SocketHandlerListening(const Sock::EventsPerSock& events_per_sock); - - void ThreadSocketHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex, !mutexMsgProc, !m_nodes_mutex, !m_reconnections_mutex); void ThreadDNSAddressSeed() EXCLUSIVE_LOCKS_REQUIRED(!m_addr_fetches_mutex, !m_nodes_mutex); uint64_t CalculateKeyedNetGroup(const CNetAddr& ad) const; @@ -1384,8 +1333,8 @@ private: void DeleteNode(CNode* pnode); /** (Try to) send data from node's vSendMsg. Returns (bytes_sent, data_left). */ - std::pair SocketSendData(CNode& node) - EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend, !m_total_bytes_sent_mutex); + std::pair SendMessagesAsBytes(CNode& node) EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend) + EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex); void DumpAddresses(); @@ -1560,7 +1509,6 @@ private: std::atomic flagInterruptMsgProc{false}; std::thread threadDNSAddressSeed; - std::thread threadSocketHandler; std::thread threadOpenAddedConnections; std::thread threadOpenConnections; std::thread threadMessageHandler; diff --git a/src/test/denialofservice_tests.cpp b/src/test/denialofservice_tests.cpp index 9ee7e9c9fe2..d2e62c7395c 100644 --- a/src/test/denialofservice_tests.cpp +++ b/src/test/denialofservice_tests.cpp @@ -55,7 +55,6 @@ BOOST_AUTO_TEST_CASE(outbound_slow_chain_eviction) CAddress addr1(ip(0xa0b0c001), NODE_NONE); NodeId id{0}; CNode dummyNode1{id++, - /*sock=*/nullptr, addr1, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -121,7 +120,6 @@ void AddRandomOutboundPeer(NodeId& id, std::vector& vNodes, PeerManager& } vNodes.emplace_back(new CNode{id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -320,7 +318,6 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) banman->ClearBanned(); NodeId id{0}; nodes[0] = new CNode{id++, - /*sock=*/nullptr, addr[0], /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -340,7 +337,6 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) BOOST_CHECK(!banman->IsDiscouraged(other_addr)); // Different address, not discouraged nodes[1] = new CNode{id++, - /*sock=*/nullptr, addr[1], /*nKeyedNetGroupIn=*/1, /*nLocalHostNonceIn=*/1, @@ -370,7 +366,6 @@ BOOST_AUTO_TEST_CASE(peer_discouragement) // Make sure non-IP peers are discouraged and disconnected properly. nodes[2] = new CNode{id++, - /*sock=*/nullptr, addr[2], /*nKeyedNetGroupIn=*/1, /*nLocalHostNonceIn=*/1, @@ -412,7 +407,6 @@ BOOST_AUTO_TEST_CASE(DoS_bantime) CAddress addr(ip(0xa0b0c001), NODE_NONE); NodeId id{0}; CNode dummyNode{id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/4, /*nLocalHostNonceIn=*/4, diff --git a/src/test/fuzz/connman.cpp b/src/test/fuzz/connman.cpp index 5d2bdaf98b5..da5efc70523 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -65,13 +65,14 @@ FUZZ_TARGET(connman, .init = initialize_connman) CNetAddr random_netaddr; NodeId node_id{0}; - CNode random_node = ConsumeNode(fuzzed_data_provider, node_id++); + CNode& random_node{*ConsumeNodeAsUniquePtr(fuzzed_data_provider, node_id++).release()}; + connman.AddTestNode(random_node, std::make_unique(fuzzed_data_provider)); CSubNet random_subnet; std::string random_string; LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 100) { CNode& p2p_node{*ConsumeNodeAsUniquePtr(fuzzed_data_provider, node_id++).release()}; - connman.AddTestNode(p2p_node); + connman.AddTestNode(p2p_node, std::make_unique(fuzzed_data_provider)); } LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) { @@ -104,6 +105,15 @@ FUZZ_TARGET(connman, .init = initialize_connman) [&] { connman.DisconnectNode(random_subnet); }, + [&] { + if (fuzzed_data_provider.ConsumeBool()) { + auto nonexistent_node{ConsumeNodeAsUniquePtr(fuzzed_data_provider, node_id++)}; + connman.MarkAsDisconnectAndCloseConnection(*nonexistent_node); + } else { + CNode& existent_node{*connman.TestNodes().begin()->second}; + connman.MarkAsDisconnectAndCloseConnection(existent_node); + } + }, [&] { connman.ForEachNode([](auto) {}); }, diff --git a/src/test/fuzz/net.cpp b/src/test/fuzz/net.cpp index 1a0de7aa363..8d02e5c4efd 100644 --- a/src/test/fuzz/net.cpp +++ b/src/test/fuzz/net.cpp @@ -42,9 +42,6 @@ FUZZ_TARGET(net, .init = initialize_net) LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) { CallOneOf( fuzzed_data_provider, - [&] { - node.CloseSocketDisconnect(); - }, [&] { CNodeStats stats; node.CopyStats(stats); diff --git a/src/test/fuzz/p2p_handshake.cpp b/src/test/fuzz/p2p_handshake.cpp index d608efd87ac..8d5d65655b4 100644 --- a/src/test/fuzz/p2p_handshake.cpp +++ b/src/test/fuzz/p2p_handshake.cpp @@ -65,7 +65,7 @@ FUZZ_TARGET(p2p_handshake, .init = ::initialize) const auto num_peers_to_add = fuzzed_data_provider.ConsumeIntegralInRange(1, 3); for (int i = 0; i < num_peers_to_add; ++i) { peers.push_back(ConsumeNodeAsUniquePtr(fuzzed_data_provider, i).release()); - connman.AddTestNode(*peers.back()); + connman.AddTestNode(*peers.back(), std::make_unique(fuzzed_data_provider)); peerman->InitializeNode( *peers.back(), static_cast(fuzzed_data_provider.ConsumeIntegral())); diff --git a/src/test/fuzz/p2p_headers_presync.cpp b/src/test/fuzz/p2p_headers_presync.cpp index ed7041ad1f1..94d3f290d4d 100644 --- a/src/test/fuzz/p2p_headers_presync.cpp +++ b/src/test/fuzz/p2p_headers_presync.cpp @@ -60,7 +60,7 @@ void HeadersSyncSetup::ResetAndInitialize() for (auto conn_type : conn_types) { CAddress addr{}; - m_connections.push_back(new CNode(id++, nullptr, addr, 0, 0, addr, "", conn_type, false)); + m_connections.push_back(new CNode(id++, addr, 0, 0, addr, "", conn_type, false)); CNode& p2p_node = *m_connections.back(); connman.Handshake( diff --git a/src/test/fuzz/process_message.cpp b/src/test/fuzz/process_message.cpp index 4bd38a1ac68..e94f5b2b3d7 100644 --- a/src/test/fuzz/process_message.cpp +++ b/src/test/fuzz/process_message.cpp @@ -68,7 +68,7 @@ FUZZ_TARGET(process_message, .init = initialize_process_message) } CNode& p2p_node = *ConsumeNodeAsUniquePtr(fuzzed_data_provider).release(); - connman.AddTestNode(p2p_node); + connman.AddTestNode(p2p_node, std::make_unique(fuzzed_data_provider)); FillNode(fuzzed_data_provider, connman, p2p_node); const auto mock_time = ConsumeTime(fuzzed_data_provider); diff --git a/src/test/fuzz/process_messages.cpp b/src/test/fuzz/process_messages.cpp index 0688868c02b..dbb221e6056 100644 --- a/src/test/fuzz/process_messages.cpp +++ b/src/test/fuzz/process_messages.cpp @@ -60,7 +60,7 @@ FUZZ_TARGET(process_messages, .init = initialize_process_messages) FillNode(fuzzed_data_provider, connman, p2p_node); - connman.AddTestNode(p2p_node); + connman.AddTestNode(p2p_node, std::make_unique(fuzzed_data_provider)); } LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 30) diff --git a/src/test/fuzz/util/net.h b/src/test/fuzz/util/net.h index cc73cdff4b7..e2ea4a34077 100644 --- a/src/test/fuzz/util/net.h +++ b/src/test/fuzz/util/net.h @@ -221,7 +221,6 @@ template auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional& node_id_in = std::nullopt) noexcept { const NodeId node_id = node_id_in.value_or(fuzzed_data_provider.ConsumeIntegralInRange(0, std::numeric_limits::max())); - const auto sock = std::make_shared(fuzzed_data_provider); const CAddress address = ConsumeAddress(fuzzed_data_provider); const uint64_t keyed_net_group = fuzzed_data_provider.ConsumeIntegral(); const uint64_t local_host_nonce = fuzzed_data_provider.ConsumeIntegral(); @@ -232,7 +231,6 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional(node_id, - sock, address, keyed_net_group, local_host_nonce, @@ -243,7 +241,6 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional& nodes, PeerManager& peerman, Connm const bool inbound_onion{onion_peer && conn_type == ConnectionType::INBOUND}; nodes.emplace_back(new CNode{++id, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp index 5f0f05c842a..b4d898b3eab 100644 --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -60,7 +60,6 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) std::string pszDest; std::unique_ptr pnode1 = std::make_unique(id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -78,7 +77,6 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) BOOST_CHECK_EQUAL(pnode1->ConnectedThroughNetwork(), Network::NET_IPV4); std::unique_ptr pnode2 = std::make_unique(id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/1, /*nLocalHostNonceIn=*/1, @@ -96,7 +94,6 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) BOOST_CHECK_EQUAL(pnode2->ConnectedThroughNetwork(), Network::NET_IPV4); std::unique_ptr pnode3 = std::make_unique(id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -114,7 +111,6 @@ BOOST_AUTO_TEST_CASE(cnode_simple_test) BOOST_CHECK_EQUAL(pnode3->ConnectedThroughNetwork(), Network::NET_IPV4); std::unique_ptr pnode4 = std::make_unique(id++, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/1, /*nLocalHostNonceIn=*/1, @@ -613,7 +609,6 @@ BOOST_AUTO_TEST_CASE(ipv4_peer_with_ipv6_addrMe_test) ipv4AddrPeer.s_addr = 0xa0b0c001; CAddress addr = CAddress(CService(ipv4AddrPeer, 7777), NODE_NETWORK); std::unique_ptr pnode = std::make_unique(/*id=*/0, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -667,7 +662,6 @@ BOOST_AUTO_TEST_CASE(get_local_addr_for_peer_port) in_addr peer_out_in_addr; peer_out_in_addr.s_addr = htonl(0x01020304); CNode peer_out{/*id=*/0, - /*sock=*/nullptr, /*addrIn=*/CAddress{CService{peer_out_in_addr, 8333}, NODE_NETWORK}, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -688,7 +682,6 @@ BOOST_AUTO_TEST_CASE(get_local_addr_for_peer_port) in_addr peer_in_in_addr; peer_in_in_addr.s_addr = htonl(0x05060708); CNode peer_in{/*id=*/0, - /*sock=*/nullptr, /*addrIn=*/CAddress{CService{peer_in_in_addr, 8333}, NODE_NETWORK}, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -825,7 +818,6 @@ BOOST_AUTO_TEST_CASE(initial_advertise_from_version_message) in_addr peer_in_addr; peer_in_addr.s_addr = htonl(0x01020304); CNode peer{/*id=*/0, - /*sock=*/nullptr, /*addrIn=*/CAddress{CService{peer_in_addr, 8333}, NODE_NETWORK}, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, @@ -900,7 +892,6 @@ BOOST_AUTO_TEST_CASE(advertise_local_address) { auto CreatePeer = [](const CAddress& addr) { return std::make_unique(/*id=*/0, - /*sock=*/nullptr, addr, /*nKeyedNetGroupIn=*/0, /*nLocalHostNonceIn=*/0, diff --git a/src/test/util/net.h b/src/test/util/net.h index c164507a20d..a8a0d7f5724 100644 --- a/src/test/util/net.h +++ b/src/test/util/net.h @@ -52,6 +52,12 @@ struct ConnmanTestMsg : public CConnman { return m_nodes; } + void AddTestNode(CNode& node, std::unique_ptr&& sock) + { + TestOnlyAddExistentConnection(node.GetId(), std::move(sock)); + AddTestNode(node); + } + void AddTestNode(CNode& node) { LOCK(m_nodes_mutex); @@ -91,6 +97,8 @@ struct ConnmanTestMsg : public CConnman { bool AlreadyConnectedPublic(const CAddress& addr) { return AlreadyConnectedToAddress(addr); }; CNode* ConnectNodePublic(PeerManager& peerman, const char* pszDest, ConnectionType conn_type); + + using CConnman::MarkAsDisconnectAndCloseConnection; }; constexpr ServiceFlags ALL_SERVICE_FLAGS[]{ From 741f17e51ddac002fadbc735e2ca650117594a3c Mon Sep 17 00:00:00 2001 From: Vasil Dimov Date: Mon, 23 Sep 2024 11:05:59 +0200 Subject: [PATCH 17/42] net: move-only: improve encapsulation of SockMan `SockMan` members `AcceptConnection()` `NewSockAccepted()` `GetNewId()` `m_i2p_sam_session` `m_listen` are now used only by `SockMan`, thus make them private. --- src/common/sockman.cpp | 118 ++++++++++++++++++++--------------------- src/common/sockman.h | 70 ++++++++++++------------ 2 files changed, 94 insertions(+), 94 deletions(-) diff --git a/src/common/sockman.cpp b/src/common/sockman.cpp index 4a87344a0cd..ee5389cf47c 100644 --- a/src/common/sockman.cpp +++ b/src/common/sockman.cpp @@ -222,65 +222,6 @@ SockMan::ConnectAndMakeId(const std::variant& to, return id; } -std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) -{ - sockaddr_storage storage; - socklen_t len{sizeof(storage)}; - - auto sock{listen_sock.Accept(reinterpret_cast(&storage), &len)}; - - if (!sock) { - const int err{WSAGetLastError()}; - if (err != WSAEWOULDBLOCK) { - LogPrintLevel(BCLog::NET, - BCLog::Level::Error, - "Cannot accept new connection: %s\n", - NetworkErrorString(err)); - } - return {}; - } - - if (!addr.SetSockAddr(reinterpret_cast(&storage), len)) { - LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); - } - - return sock; -} - -void SockMan::NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) -{ - AssertLockNotHeld(m_connected_mutex); - - if (!sock->IsSelectable()) { - LogPrintf("connection from %s dropped: non-selectable socket\n", them.ToStringAddrPort()); - return; - } - - // According to the internet TCP_NODELAY is not carried into accepted sockets - // on all platforms. Set it again here just to be sure. - const int on{1}; - if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { - LogDebug(BCLog::NET, "connection from %s: unable to set TCP_NODELAY, continuing anyway\n", - them.ToStringAddrPort()); - } - - const Id id{GetNewId()}; - - { - LOCK(m_connected_mutex); - m_connected.emplace(id, std::make_shared(std::move(sock))); - } - - if (!EventNewConnectionAccepted(id, me, them)) { - CloseConnection(id); - } -} - -SockMan::Id SockMan::GetNewId() -{ - return m_next_id.fetch_add(1, std::memory_order_relaxed); -} - bool SockMan::CloseConnection(Id id) { LOCK(m_connected_mutex); @@ -417,6 +358,65 @@ void SockMan::ThreadSocketHandler() } } +std::unique_ptr SockMan::AcceptConnection(const Sock& listen_sock, CService& addr) +{ + sockaddr_storage storage; + socklen_t len{sizeof(storage)}; + + auto sock{listen_sock.Accept(reinterpret_cast(&storage), &len)}; + + if (!sock) { + const int err{WSAGetLastError()}; + if (err != WSAEWOULDBLOCK) { + LogPrintLevel(BCLog::NET, + BCLog::Level::Error, + "Cannot accept new connection: %s\n", + NetworkErrorString(err)); + } + return {}; + } + + if (!addr.SetSockAddr(reinterpret_cast(&storage), len)) { + LogPrintLevel(BCLog::NET, BCLog::Level::Warning, "Unknown socket family\n"); + } + + return sock; +} + +void SockMan::NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) +{ + AssertLockNotHeld(m_connected_mutex); + + if (!sock->IsSelectable()) { + LogPrintf("connection from %s dropped: non-selectable socket\n", them.ToStringAddrPort()); + return; + } + + // According to the internet TCP_NODELAY is not carried into accepted sockets + // on all platforms. Set it again here just to be sure. + const int on{1}; + if (sock->SetSockOpt(IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)) == SOCKET_ERROR) { + LogDebug(BCLog::NET, "connection from %s: unable to set TCP_NODELAY, continuing anyway\n", + them.ToStringAddrPort()); + } + + const Id id{GetNewId()}; + + { + LOCK(m_connected_mutex); + m_connected.emplace(id, std::make_shared(std::move(sock))); + } + + if (!EventNewConnectionAccepted(id, me, them)) { + CloseConnection(id); + } +} + +SockMan::Id SockMan::GetNewId() +{ + return m_next_id.fetch_add(1, std::memory_order_relaxed); +} + SockMan::IOReadiness SockMan::GenerateWaitSockets() { AssertLockNotHeld(m_connected_mutex); diff --git a/src/common/sockman.h b/src/common/sockman.h index fcb8d9e1976..7c4a61cb779 100644 --- a/src/common/sockman.h +++ b/src/common/sockman.h @@ -128,29 +128,6 @@ public: CService& me) EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex, !m_unused_i2p_sessions_mutex); - /** - * Accept a connection. - * @param[in] listen_sock Socket on which to accept the connection. - * @param[out] addr Address of the peer that was accepted. - * @return Newly created socket for the accepted connection. - */ - std::unique_ptr AcceptConnection(const Sock& listen_sock, CService& addr); - - /** - * After a new socket with a peer has been created, configure its flags, - * make a new connection id and call `EventNewConnectionAccepted()`. - * @param[in] sock The newly created socket. - * @param[in] me Address at our end of the connection. - * @param[in] them Address of the new peer. - */ - void NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) - EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); - - /** - * Generate an id for a newly created connection. - */ - Id GetNewId(); - /** * Destroy a given connection by closing its socket and release resources occupied by it. * @param[in] id Connection to destroy. @@ -189,18 +166,6 @@ public: */ CThreadInterrupt interruptNet; - /** - * I2P SAM session. - * Used to accept incoming and make outgoing I2P connections from a persistent - * address. - */ - std::unique_ptr m_i2p_sam_session; - - /** - * List of listening sockets. - */ - std::vector> m_listen; - protected: /** @@ -391,6 +356,29 @@ private: void ThreadSocketHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + /** + * Accept a connection. + * @param[in] listen_sock Socket on which to accept the connection. + * @param[out] addr Address of the peer that was accepted. + * @return Newly created socket for the accepted connection. + */ + std::unique_ptr AcceptConnection(const Sock& listen_sock, CService& addr); + + /** + * After a new socket with a peer has been created, configure its flags, + * make a new connection id and call `EventNewConnectionAccepted()`. + * @param[in] sock The newly created socket. + * @param[in] me Address at our end of the connection. + * @param[in] them Address of the new peer. + */ + void NewSockAccepted(std::unique_ptr&& sock, const CService& me, const CService& them) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex); + + /** + * Generate an id for a newly created connection. + */ + Id GetNewId(); + /** * Generate a collection of sockets to check for IO readiness. * @return Sockets to check for readiness plus an aux map to find the @@ -450,6 +438,18 @@ private: */ std::queue> m_unused_i2p_sessions GUARDED_BY(m_unused_i2p_sessions_mutex); + /** + * I2P SAM session. + * Used to accept incoming and make outgoing I2P connections from a persistent + * address. + */ + std::unique_ptr m_i2p_sam_session; + + /** + * List of listening sockets. + */ + std::vector> m_listen; + mutable Mutex m_connected_mutex; /** From 4ccc28f8c39e11a27f7df28c89412298a8155bef Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 10 Mar 2025 11:17:53 -0400 Subject: [PATCH 18/42] test: cover -rpcservertimeout --- test/functional/interface_http.py | 47 +++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index dbdceb52d15..324a0883516 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -8,6 +8,7 @@ from test_framework.test_framework import BitcoinTestFramework from test_framework.util import assert_equal, str_to_b64str import http.client +import time import urllib.parse class HTTPBasicsTest (BitcoinTestFramework): @@ -104,6 +105,52 @@ class HTTPBasicsTest (BitcoinTestFramework): out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) + self.log.info("Check -rpcservertimeout") + self.restart_node(2, extra_args=["-rpcservertimeout=1"]) + # This is the amount of time the server will wait for a client to + # send a complete request. Test it by sending an incomplete but + # so-far otherwise well-formed HTTP request, and never finishing it. + + # Copied from http_incomplete_test_() in regress_http.c in libevent. + # A complete request would have an additional "\r\n" at the end. + http_request = "GET /test1 HTTP/1.1\r\nHost: somehost\r\n" + + # Get the underlying socket from HTTP connection so we can send something unusual + conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) + conn.connect() + sock = conn.sock + sock.sendall(http_request.encode("utf-8")) + # Wait for response, but expect a timeout disconnection after 1 second + start = time.time() + res = sock.recv(1024) + stop = time.time() + assert res == b"" + assert stop - start >= 1 + # definitely closed + try: + conn.request('GET', '/') + conn.getresponse() + except ConnectionResetError: + pass + + # Sanity check + http_request = "GET /test2 HTTP/1.1\r\nHost: somehost\r\n\r\n" + conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) + conn.connect() + sock = conn.sock + sock.sendall(http_request.encode("utf-8")) + res = sock.recv(1024) + assert res.startswith(b"HTTP/1.1 404 Not Found") + # still open + conn.request('GET', '/') + conn.getresponse() + + # Because we have set -rpcservertimeout so low, the persistent connection + # created by AuthServiceProxy for this node when the test framework + # started has likely closed. Force the test framework to use a fresh + # new connection for the next RPC otherwise the cleanup process + # calling `stop` will raise a connection error. + self.nodes[2]._set_conn() if __name__ == '__main__': HTTPBasicsTest(__file__).main() From 3fa1408958bd984521fbac5437f82f2b8f6c2140 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 12 Mar 2025 13:35:28 -0400 Subject: [PATCH 19/42] test: cover "chunked" Transfer-Encoding --- test/functional/interface_http.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index 324a0883516..695fee638d8 100755 --- a/test/functional/interface_http.py +++ b/test/functional/interface_http.py @@ -105,6 +105,28 @@ class HTTPBasicsTest (BitcoinTestFramework): out1 = conn.getresponse() assert_equal(out1.status, http.client.BAD_REQUEST) + self.log.info("Check HTTP request encoded with chunked transfer") + headers_chunked = headers.copy() + headers_chunked.update({"Transfer-encoding": "chunked"}) + body_chunked = [ + b'{"method": "submitblock", "params": ["', + b'0A' * 1000000, + b'0B' * 1000000, + b'0C' * 1000000, + b'0D' * 1000000, + b'"]}' + ] + conn = http.client.HTTPConnection(urlNode2.hostname, urlNode2.port) + conn.connect() + conn.request( + method='POST', + url='/', + body=iter(body_chunked), + headers=headers_chunked, + encode_chunked=True) + out1 = conn.getresponse().read() + assert out1 == b'{"result":"high-hash","error":null}\n' + self.log.info("Check -rpcservertimeout") self.restart_node(2, extra_args=["-rpcservertimeout=1"]) # This is the amount of time the server will wait for a client to From d6ea26c2fecb0f79b4b94ebfbcfad303e700098b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 12 Mar 2025 13:31:47 -0400 Subject: [PATCH 20/42] string: implement ParseUInt64Hex --- src/test/util_tests.cpp | 20 ++++++++++++++++++++ src/util/strencodings.cpp | 18 ++++++++++++++++++ src/util/strencodings.h | 8 ++++++++ 3 files changed, 46 insertions(+) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 4cacbd1151f..491e683f5f6 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -1045,6 +1045,26 @@ BOOST_AUTO_TEST_CASE(test_ParseUInt64) BOOST_CHECK(!ParseUInt64("-1234", &n)); } +BOOST_AUTO_TEST_CASE(test_ParseUInt64Hex) +{ + uint64_t n; + // Valid values + BOOST_CHECK(ParseUInt64Hex("1234", nullptr)); + BOOST_CHECK(ParseUInt64Hex("1234", &n) && n == 4660); + BOOST_CHECK(ParseUInt64Hex("a", &n) && n == 10); + BOOST_CHECK(ParseUInt64Hex("0000000a", &n) && n == 10); + BOOST_CHECK(ParseUInt64Hex("100", &n) && n == 256); + BOOST_CHECK(ParseUInt64Hex("DEADbeef", &n) && n == 3735928559); + BOOST_CHECK(ParseUInt64Hex("FfFfFfFf", &n) && n == 4294967295); + // Invalid values + BOOST_CHECK(!ParseUInt64Hex("123456789", &n)); + BOOST_CHECK(!ParseUInt64Hex("", &n)); + BOOST_CHECK(!ParseUInt64Hex("-1", &n)); + BOOST_CHECK(!ParseUInt64Hex("10 00", &n)); + BOOST_CHECK(!ParseUInt64Hex("1 ", &n)); + BOOST_CHECK(!ParseUInt64Hex("0xAB", &n)); +} + BOOST_AUTO_TEST_CASE(test_FormatParagraph) { BOOST_CHECK_EQUAL(FormatParagraph("", 79, 0), ""); diff --git a/src/util/strencodings.cpp b/src/util/strencodings.cpp index 15cb40aba13..03b6c1314be 100644 --- a/src/util/strencodings.cpp +++ b/src/util/strencodings.cpp @@ -251,6 +251,24 @@ bool ParseUInt64(std::string_view str, uint64_t* out) return ParseIntegral(str, out); } +bool ParseUInt64Hex(std::string_view str, uint64_t* out) +{ + if (str.size() > 8) return false; + if (str.size() < 1) return false; + uint64_t total{0}; + auto it = str.begin(); + while (it != str.end()) { + auto v = HexDigit(*(it++)); + if (v < 0) return false; + total <<= 4; + total |= v; + } + if (out != nullptr) { + *out = total; + } + return true; +} + std::string FormatParagraph(std::string_view in, size_t width, size_t indent) { assert(width >= indent); diff --git a/src/util/strencodings.h b/src/util/strencodings.h index d0809162faf..e092612bfd7 100644 --- a/src/util/strencodings.h +++ b/src/util/strencodings.h @@ -229,6 +229,14 @@ std::optional ToIntegral(std::string_view str) */ [[nodiscard]] bool ParseUInt64(std::string_view str, uint64_t *out); +/** + * Convert hexadecimal string to unsigned 64-bit integer, with 4-bit + * resolution (odd length strings are acceptable without leading "0") + * @returns true if the entire string could be parsed as valid integer, + * false if not, or in case of overflow. + */ +[[nodiscard]] bool ParseUInt64Hex(std::string_view str, uint64_t *out); + /** * Format a paragraph of text to a fixed width, adding spaces for * indentation to any added line. From c7a6f23e8aff3a06541dd362949344a14f88e4a7 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 30 May 2024 15:34:58 -0400 Subject: [PATCH 21/42] string: add CaseInsensitiveComparator https://httpwg.org/specs/rfc9110.html#rfc.section.5.1 Field names in HTTP headers are case-insensitive. This comparator will be used in the headers map to search by key. In libevent these are compared in lowercase: evhttp_find_header() evutil_ascii_strcasecmp() EVUTIL_TOLOWER_() --- src/test/util_string_tests.cpp | 12 ++++++++++++ src/util/strencodings.h | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp index 65ee140b6e9..965c76ec3d0 100644 --- a/src/test/util_string_tests.cpp +++ b/src/test/util_string_tests.cpp @@ -2,6 +2,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include #include #include @@ -146,4 +147,15 @@ BOOST_AUTO_TEST_CASE(ConstevalFormatString_NumSpec) HasReason{"tinyformat: Too many conversion specifiers in format string"}); } +BOOST_AUTO_TEST_CASE(case_insensitive_comparator_test) +{ + CaseInsensitiveComparator cmp; + BOOST_CHECK(cmp("A", "B")); + BOOST_CHECK(cmp("A", "b")); + BOOST_CHECK(cmp("a", "B")); + BOOST_CHECK(!cmp("B", "A")); + BOOST_CHECK(!cmp("B", "a")); + BOOST_CHECK(!cmp("b", "A")); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/strencodings.h b/src/util/strencodings.h index e092612bfd7..e4eec2544de 100644 --- a/src/util/strencodings.h +++ b/src/util/strencodings.h @@ -403,6 +403,24 @@ struct Hex { }; } // namespace detail +struct CaseInsensitiveComparator { + // Helper function for locale-independent tolower + static char tolowercase(char c) + { + return static_cast(ToLower(static_cast(c))); + } + + bool operator()(const std::string& s1, const std::string& s2) const + { + return std::lexicographical_compare( + s1.begin(), s1.end(), + s2.begin(), s2.end(), + [](unsigned char c1, unsigned char c2) { + return tolowercase(c1) < tolowercase(c2); + }); + } +}; + /** * ""_hex is a compile-time user-defined literal returning a * `std::array`, equivalent to ParseHex(). Variants provided: From 908e75cd0d82d944f604c42967a8083ca85fbb7c Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 3 Jun 2024 13:37:12 -0400 Subject: [PATCH 22/42] time: implement and test RFC7231 timestamp string HTTP 1.1 responses require a timestamp header with a specific format, specified in: https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 --- src/test/util_tests.cpp | 7 +++++++ src/util/time.cpp | 17 +++++++++++++++++ src/util/time.h | 6 ++++++ 3 files changed, 30 insertions(+) diff --git a/src/test/util_tests.cpp b/src/test/util_tests.cpp index 491e683f5f6..0b23ff68b8c 100644 --- a/src/test/util_tests.cpp +++ b/src/test/util_tests.cpp @@ -385,6 +385,13 @@ BOOST_AUTO_TEST_CASE(util_FormatISO8601Date) BOOST_CHECK_EQUAL(FormatISO8601Date(1317425777), "2011-09-30"); } +BOOST_AUTO_TEST_CASE(util_FormatRFC7231DateTime) +{ + BOOST_CHECK_EQUAL(FormatRFC7231DateTime(253402214400), "Fri, 31 Dec 9999 00:00:00 GMT"); + BOOST_CHECK_EQUAL(FormatRFC7231DateTime(1717429609), "Mon, 03 Jun 2024 15:46:49 GMT"); + BOOST_CHECK_EQUAL(FormatRFC7231DateTime(0), "Thu, 01 Jan 1970 00:00:00 GMT"); +} + BOOST_AUTO_TEST_CASE(util_FormatMoney) { BOOST_CHECK_EQUAL(FormatMoney(0), "0.00"); diff --git a/src/util/time.cpp b/src/util/time.cpp index cafc27e0d05..4ebdbe24edd 100644 --- a/src/util/time.cpp +++ b/src/util/time.cpp @@ -17,6 +17,9 @@ #include #include +static const std::string weekdays[7] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"}; +static const std::string months[12] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + void UninterruptibleSleep(const std::chrono::microseconds& n) { std::this_thread::sleep_for(n); } static std::atomic g_mock_time{}; //!< For testing @@ -116,6 +119,20 @@ std::optional ParseISO8601DateTime(std::string_view str) return int64_t{TicksSinceEpoch(tp)}; } +std::string FormatRFC7231DateTime(int64_t nTime) +{ + const std::chrono::sys_seconds secs{std::chrono::seconds{nTime}}; + const auto days{std::chrono::floor(secs)}; + // 1970-01-01 was a Thursday + std::string weekday{weekdays[(days.time_since_epoch().count() + 4) % 7]}; + const std::chrono::year_month_day ymd{days}; + std::string month{months[unsigned{ymd.month()} - 1]}; + const std::chrono::hh_mm_ss hms{secs - days}; + // examples: Mon, 27 Jul 2009 12:28:53 GMT + // Fri, 31 May 2024 19:18:04 GMT + return strprintf("%03s, %02u %03s %04i %02i:%02i:%02i GMT", weekday, unsigned{ymd.day()}, month, signed{ymd.year()}, hms.hours().count(), hms.minutes().count(), hms.seconds().count()); +} + struct timeval MillisToTimeval(int64_t nTimeout) { struct timeval timeout; diff --git a/src/util/time.h b/src/util/time.h index c43b306ff24..c1a7093b2c1 100644 --- a/src/util/time.h +++ b/src/util/time.h @@ -134,6 +134,12 @@ std::string FormatISO8601DateTime(int64_t nTime); std::string FormatISO8601Date(int64_t nTime); std::optional ParseISO8601DateTime(std::string_view str); +/** + * RFC7231 formatting https://www.rfc-editor.org/rfc/rfc7231#section-7.1.1.1 + * Used in HTTP/1.1 responses + */ +std::string FormatRFC7231DateTime(int64_t nTime); + /** * Convert milliseconds to a struct timeval for e.g. select. */ From a7db47d592d65e303994732b0b3ed421626bf385 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 27 Sep 2024 15:22:17 -0400 Subject: [PATCH 23/42] string: add LineReader This is a helper struct to parse HTTP messages from data in buffers from sockets. HTTP messages begin with headers which are CRLF-terminated lines (\n or \r\n) followed by an arbitrary amount of body data. Whitespace is trimmed from the field lines but not the body. https://httpwg.org/specs/rfc9110.html#rfc.section.5 --- src/test/util_string_tests.cpp | 42 ++++++++++++++++++++++++++++++++++ src/util/strencodings.cpp | 7 ++++++ src/util/strencodings.h | 9 ++++++++ src/util/string.cpp | 37 ++++++++++++++++++++++++++++++ src/util/string.h | 20 ++++++++++++++++ 5 files changed, 115 insertions(+) diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp index 965c76ec3d0..911c97b8e69 100644 --- a/src/test/util_string_tests.cpp +++ b/src/test/util_string_tests.cpp @@ -158,4 +158,46 @@ BOOST_AUTO_TEST_CASE(case_insensitive_comparator_test) BOOST_CHECK(!cmp("b", "A")); } +BOOST_AUTO_TEST_CASE(line_reader_test) +{ + { + // Check three lines terminated by \n, \r\n, and end of buffer, trimming whitespace + const std::vector input{StringToBuffer("once upon a time\n there was a dog \r\nwho liked food")}; + LineReader reader(input, /*max_read=*/128); + std::optional line1{reader.ReadLine()}; + BOOST_CHECK_EQUAL(reader.Left(), 33); + std::optional line2{reader.ReadLine()}; + BOOST_CHECK_EQUAL(reader.Left(), 14); + std::optional line3{reader.ReadLine()}; + std::optional line4{reader.ReadLine()}; + BOOST_CHECK(line1); + BOOST_CHECK(line2); + BOOST_CHECK(line3); + BOOST_CHECK(!line4); + BOOST_CHECK_EQUAL(line1.value(), "once upon a time"); + BOOST_CHECK_EQUAL(line2.value(), "there was a dog"); + BOOST_CHECK_EQUAL(line3.value(), "who liked food"); + } + { + // Do not exceed max_read while searching for EOL + const std::vector input1{StringToBuffer("once upon a time there was a dog\nwho liked food")}; + LineReader reader1(input1, /*max_read=*/10); + BOOST_CHECK_THROW(reader1.ReadLine(), std::runtime_error); + + const std::vector input2{StringToBuffer("once upon\n a time there was a dog who liked food")}; + LineReader reader2(input2, /*max_read=*/10); + BOOST_CHECK_EQUAL(reader2.ReadLine(), "once upon"); + BOOST_CHECK_THROW(reader2.ReadLine(), std::runtime_error); + } + { + // Read specific number of bytes regardless of max_read or \n unless buffer is too short + const std::vector input{StringToBuffer("once upon a time\n there was a dog \r\nwho liked food")}; + LineReader reader(input, /*max_read=*/1); + BOOST_CHECK_EQUAL(reader.ReadLength(3), "onc"); + BOOST_CHECK_EQUAL(reader.ReadLength(8), "e upon a"); + BOOST_CHECK_EQUAL(reader.ReadLength(8), " time\n t"); + BOOST_CHECK_THROW(reader.ReadLength(128), std::runtime_error); + } +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/src/util/strencodings.cpp b/src/util/strencodings.cpp index 03b6c1314be..0eda34fa556 100644 --- a/src/util/strencodings.cpp +++ b/src/util/strencodings.cpp @@ -497,3 +497,10 @@ std::optional ParseByteUnits(std::string_view str, ByteUnit default_mu } return *parsed_num * unit_amount; } + +std::vector StringToBuffer(const std::string& str) +{ + return std::vector( + reinterpret_cast(str.data()), + reinterpret_cast(str.data() + str.size())); +} diff --git a/src/util/strencodings.h b/src/util/strencodings.h index e4eec2544de..0b413bba80d 100644 --- a/src/util/strencodings.h +++ b/src/util/strencodings.h @@ -375,6 +375,15 @@ std::string Capitalize(std::string str); */ std::optional ParseByteUnits(std::string_view str, ByteUnit default_multiplier); +/** + * Returns a byte vector filled with data from a string. Used to test string- + * encoded data from a socket like HTTP headers. + * + * @param[in] str the string to convert into bytes + * @returns byte vector + */ +std::vector StringToBuffer(const std::string& str); + namespace util { /** consteval version of HexDigit() without the lookup table. */ consteval uint8_t ConstevalHexDigit(const char c) diff --git a/src/util/string.cpp b/src/util/string.cpp index 47c6b74d4fd..959eff03530 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -13,4 +13,41 @@ void ReplaceAll(std::string& in_out, const std::string& search, const std::strin if (search.empty()) return; in_out = std::regex_replace(in_out, std::regex(search), substitute); } + +LineReader::LineReader(std::span buffer, size_t max_read) + : start(buffer.begin()), end(buffer.end()), max_read(max_read), it(buffer.begin()) {} + +std::optional LineReader::ReadLine() +{ + if (it == end) { + return std::nullopt; + } + + auto line_start = it; + std::string line{}; + while (it != end) { + char c = static_cast(*it); + line += c; + ++it; + if (c == '\n') break; + if ((size_t)std::distance(line_start, it) >= max_read) throw std::runtime_error("max_read exceeded by LineReader"); + } + + line = TrimString(line); // delete trailing \r and/or \n + return line; +} + +// Ignores max_read but won't overflow +std::string LineReader::ReadLength(size_t len) +{ + if (Left() < len) throw std::runtime_error("Not enough data in buffer"); + std::string out(reinterpret_cast(&(*it)), len); + it += len; + return out; +} + +size_t LineReader::Left() const +{ + return std::distance(it, end); +} } // namespace util diff --git a/src/util/string.h b/src/util/string.h index b523e6ef4e7..d77f373b500 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -11,6 +11,7 @@ #include #include #include +#include #include #include // IWYU pragma: export #include // IWYU pragma: export @@ -248,6 +249,25 @@ template return obj.size() >= PREFIX_LEN && std::equal(std::begin(prefix), std::end(prefix), std::begin(obj)); } + +struct LineReader { + const std::span::iterator start; + const std::span::iterator end; + const size_t max_read; + std::span::iterator it; + + explicit LineReader(std::span buffer, size_t max_read); + + // Returns a string from current iterator position up to next \n + // and advances iterator, does not return trailing \n or \r. + // Will not search for \n past max_read. + std::optional ReadLine(); + // Returns string from current iterator position of specified length + // and advances iterator. May exceed max_read but will not read past end of buffer. + std::string ReadLength(size_t len); + // Returns remaining size of bytes in buffer + size_t Left() const; +}; } // namespace util #endif // BITCOIN_UTIL_STRING_H From a31b62f92644e2d89966dc06767c88c4ac64afc8 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 30 Sep 2024 12:28:54 -0400 Subject: [PATCH 24/42] http: enclose libevent-dependent code in a namespace This commit is a no-op to isolate HTTP methods and objects that depend on libevent. Following commits will add replacement objects and methods in a new namespace for testing and review before switching over the server. --- src/httprpc.cpp | 1 + src/httpserver.cpp | 6 ++++++ src/httpserver.h | 8 +++++++- src/init.cpp | 4 ++++ src/rest.cpp | 1 + src/rpc/node.cpp | 2 +- src/test/fuzz/http_request.cpp | 2 ++ src/test/httpserver_tests.cpp | 1 + 8 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 57893702b8b..159f97966e6 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -26,6 +26,7 @@ #include #include +using http_libevent::HTTPRequest; using util::SplitString; using util::TrimStringView; diff --git a/src/httpserver.cpp b/src/httpserver.cpp index bd2dec19b97..6fba353527d 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -43,6 +43,7 @@ #include using common::InvalidPortErrMsg; +using http_libevent::HTTPRequest; /** Maximum size of http request (request line + headers) */ static const size_t MAX_HEADERS_SIZE = 8192; @@ -438,6 +439,7 @@ static void libevent_log_cb(int severity, const char *msg) LogPrintLevel(BCLog::LIBEVENT, level, "%s\n", msg); } +namespace http_libevent { bool InitHTTPServer(const util::SignalInterrupt& interrupt) { if (!InitHTTPAllowList()) @@ -559,6 +561,7 @@ void StopHTTPServer() g_work_queue.reset(); LogDebug(BCLog::HTTP, "Stopped HTTP server\n"); } +} // namespace http_libevent struct event_base* EventBase() { @@ -591,6 +594,8 @@ void HTTPEvent::trigger(struct timeval* tv) else evtimer_add(ev, tv); // trigger after timeval passed } + +namespace http_libevent { HTTPRequest::HTTPRequest(struct evhttp_request* _req, const util::SignalInterrupt& interrupt, bool _replySent) : req(_req), m_interrupt(interrupt), replySent(_replySent) { @@ -753,6 +758,7 @@ std::optional GetQueryParameterFromUri(const char* uri, const std:: return result; } +} // namespace http_libevent void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler) { diff --git a/src/httpserver.h b/src/httpserver.h index 6535dc6086c..cd8d0724a6f 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -30,6 +30,8 @@ static const int DEFAULT_HTTP_SERVER_TIMEOUT=30; struct evhttp_request; struct event_base; class CService; + +namespace http_libevent { class HTTPRequest; /** Initialize HTTP server. @@ -48,9 +50,10 @@ void StopHTTPServer(); /** Change logging level for libevent. */ void UpdateHTTPServerLogging(bool enable); +} // namespace http_libevent /** Handler for requests to a certain HTTP path */ -typedef std::function HTTPRequestHandler; +typedef std::function HTTPRequestHandler; /** Register handler for prefix. * If multiple handlers match a prefix, the first-registered one will * be invoked. @@ -64,6 +67,7 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch); */ struct event_base* EventBase(); +namespace http_libevent { /** In-flight HTTP request. * Thin C++ wrapper around evhttp_request. */ @@ -145,6 +149,7 @@ public: void WriteReply(int nStatus, std::span reply); }; + /** Get the query parameter value from request uri for a specified key, or std::nullopt if the key * is not found. * @@ -158,6 +163,7 @@ public: * @param[in] key represents the query parameter of which the value is returned */ std::optional GetQueryParameterFromUri(const char* uri, const std::string& key); +} // namespace http_libevent /** Event handler closure. */ diff --git a/src/init.cpp b/src/init.cpp index 3cfd301fbab..5ecb3e61c6c 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -121,6 +121,10 @@ using common::AmountErrMsg; using common::InvalidPortErrMsg; using common::ResolveErrMsg; +using http_libevent::InitHTTPServer; +using http_libevent::InterruptHTTPServer; +using http_libevent::StartHTTPServer; +using http_libevent::StopHTTPServer; using node::ApplyArgsManOptions; using node::BlockManager; using node::CalculateCacheSizes; diff --git a/src/rest.cpp b/src/rest.cpp index 44984b360ff..1b2e1e14e18 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -37,6 +37,7 @@ #include +using http_libevent::HTTPRequest; using node::GetTransaction; using node::NodeContext; using util::SplitString; diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index 5e36273cf49..fb851ee9a07 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -256,7 +256,7 @@ static RPCHelpMan logging() // Update libevent logging if BCLog::LIBEVENT has changed. if (changed_log_categories & BCLog::LIBEVENT) { - UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); + http_libevent::UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); } UniValue result(UniValue::VOBJ); diff --git a/src/test/fuzz/http_request.cpp b/src/test/fuzz/http_request.cpp index f13f1c72a51..712c020eeac 100644 --- a/src/test/fuzz/http_request.cpp +++ b/src/test/fuzz/http_request.cpp @@ -20,6 +20,8 @@ #include #include +using http_libevent::HTTPRequest; + extern "C" int evhttp_parse_firstline_(struct evhttp_request*, struct evbuffer*); extern "C" int evhttp_parse_headers_(struct evhttp_request*, struct evbuffer*); diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index c95a777e80c..6edaf1a7747 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -11,6 +11,7 @@ BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) { + using http_libevent::GetQueryParameterFromUri; std::string uri {}; // No parameters From 47cbb2c15398b7451d04c155a7f637d2b10f5ac8 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 30 Sep 2024 13:22:29 -0400 Subject: [PATCH 25/42] http: Implement HTTPHeaders class see: https://www.rfc-editor.org/rfc/rfc2616#section-4.2 https://www.rfc-editor.org/rfc/rfc7231#section-5 https://www.rfc-editor.org/rfc/rfc7231#section-7 https://httpwg.org/specs/rfc9111.html#header.field.definitions --- src/httpserver.cpp | 66 +++++++++++++++++++++++++++++++++++ src/httpserver.h | 19 ++++++++++ src/test/httpserver_tests.cpp | 54 ++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 6fba353527d..b5767508344 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -781,3 +781,69 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch) pathHandlers.erase(i); } } + + +namespace http_bitcoin { +std::optional HTTPHeaders::Find(const std::string key) const +{ + const auto it = m_map.find(key); + if (it == m_map.end()) return std::nullopt; + return it->second; +} + +void HTTPHeaders::Write(const std::string key, const std::string value) +{ + // If present, append value to list + const auto existing_value = Find(key); + if (existing_value) { + m_map[key] = existing_value.value() + ", " + value; + } else { + m_map[key] = value; + } +} + +void HTTPHeaders::Remove(const std::string key) +{ + m_map.erase(key); +} + +bool HTTPHeaders::Read(util::LineReader& reader) +{ + // Headers https://httpwg.org/specs/rfc9110.html#rfc.section.6.3 + // A sequence of Field Lines https://httpwg.org/specs/rfc9110.html#rfc.section.5.2 + do { + auto maybe_line = reader.ReadLine(); + if (!maybe_line) return false; + std::string line = *maybe_line; + + // An empty line indicates end of the headers section https://www.rfc-editor.org/rfc/rfc2616#section-4 + if (line.length() == 0) break; + + // Header line must have at least one ":" + // keys are not allowed to have delimiters like ":" but values are + // https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2 + const size_t pos{line.find(':')}; + if (pos == std::string::npos) throw std::runtime_error("HTTP header missing colon (:)"); + + // Whitespace is optional + std::string key = util::TrimString(line.substr(0, pos)); + std::string value = util::TrimString(line.substr(pos + 1)); + Write(key, value); + } while (true); + + return true; +} + +std::string HTTPHeaders::Stringify() const +{ + std::string out; + for (auto it = m_map.begin(); it != m_map.end(); ++it) { + out += it->first + ": " + it->second + "\r\n"; + } + + // Headers are terminated by an empty line + out += "\r\n"; + + return out; +} +} // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index cd8d0724a6f..ec1fefc5bca 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -6,10 +6,14 @@ #define BITCOIN_HTTPSERVER_H #include +#include #include #include #include +#include +#include + namespace util { class SignalInterrupt; } // namespace util @@ -197,4 +201,19 @@ private: struct event* ev; }; +namespace http_bitcoin { +class HTTPHeaders +{ +public: + std::optional Find(const std::string key) const; + void Write(const std::string key, const std::string value); + void Remove(const std::string key); + bool Read(util::LineReader& reader); + std::string Stringify() const; + +private: + std::map m_map; +}; +} // namespace http_bitcoin + #endif // BITCOIN_HTTPSERVER_H diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 6edaf1a7747..7c5ba7a3ad7 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -4,9 +4,12 @@ #include #include +#include #include +using http_bitcoin::HTTPHeaders; + BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) @@ -40,4 +43,55 @@ BOOST_AUTO_TEST_CASE(test_query_parameters) uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%"; BOOST_CHECK_EXCEPTION(GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters")); } + +BOOST_AUTO_TEST_CASE(http_headers_tests) +{ + { + // Writing response headers + HTTPHeaders headers{}; + BOOST_CHECK(!headers.Find("Cache-Control")); + headers.Write("Cache-Control", "no-cache"); + // Check case-insensitive key matching + BOOST_CHECK_EQUAL(headers.Find("Cache-Control").value(), "no-cache"); + BOOST_CHECK_EQUAL(headers.Find("cache-control").value(), "no-cache"); + // Additional values are comma-separated and appended + headers.Write("Cache-Control", "no-store"); + BOOST_CHECK_EQUAL(headers.Find("Cache-Control").value(), "no-cache, no-store"); + // Add a few more + headers.Write("Pie", "apple"); + headers.Write("Sandwich", "ham"); + headers.Write("Coffee", "black"); + BOOST_CHECK_EQUAL(headers.Find("Pie").value(), "apple"); + // Remove + headers.Remove("Pie"); + BOOST_CHECK(!headers.Find("Pie")); + // Combine for transmission + // std::map sorts alphabetically by key, no order is specified for HTTP + BOOST_CHECK_EQUAL( + headers.Stringify(), + "Cache-Control: no-cache, no-store\r\n" + "Coffee: black\r\n" + "Sandwich: ham\r\n\r\n"); + } + { + // Reading request headers captured from bitcoin-cli + std::vector buffer{TryParseHex( + "486f73743a203132372e302e302e310d0a436f6e6e656374696f6e3a20636c6f73" + "650d0a436f6e74656e742d547970653a206170706c69636174696f6e2f6a736f6e" + "0d0a417574686f72697a6174696f6e3a204261736963205831396a623239726157" + "5666587a6f7a597a4a6b4e5441784e44466c4d474a69596d56684d5449354f4467" + "334e7a49354d544d334e54526d4e54686b4e6a63324f574d775a5459785a6a677a" + "4e5467794e7a4577595459314f47526b596a566d5a4751330d0a436f6e74656e74" + "2d4c656e6774683a2034360d0a0d0a").value()}; + util::LineReader reader(buffer, /*max_read=*/1028); + HTTPHeaders headers{}; + headers.Read(reader); + BOOST_CHECK_EQUAL(headers.Find("Host").value(), "127.0.0.1"); + BOOST_CHECK_EQUAL(headers.Find("Connection").value(), "close"); + BOOST_CHECK_EQUAL(headers.Find("Content-Type").value(), "application/json"); + BOOST_CHECK_EQUAL(headers.Find("Authorization").value(), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3"); + BOOST_CHECK_EQUAL(headers.Find("Content-Length").value(), "46"); + BOOST_CHECK(!headers.Find("Pizza")); + } +} BOOST_AUTO_TEST_SUITE_END() From 12dbb0d4cac947a25103b443b7f90bc1952ed98a Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 16 Oct 2024 10:57:06 -0400 Subject: [PATCH 26/42] http: Implement HTTPResponse class HTTP Response message: https://datatracker.ietf.org/doc/html/rfc1945#section-6 Status line (first line of response): https://datatracker.ietf.org/doc/html/rfc1945#section-6.1 Status code definitions: https://datatracker.ietf.org/doc/html/rfc1945#section-9 --- src/httpserver.cpp | 5 +++++ src/httpserver.h | 15 +++++++++++++++ src/rpc/protocol.h | 14 ++++++++++++++ src/test/httpserver_tests.cpp | 28 ++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index b5767508344..e42e40098dc 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -846,4 +846,9 @@ std::string HTTPHeaders::Stringify() const return out; } + +std::string HTTPResponse::StringifyHeaders() const +{ + return strprintf("HTTP/%d.%d %d %s\r\n%s", m_version_major, m_version_minor, m_status, m_reason, m_headers.Stringify()); +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index ec1fefc5bca..d78fed17018 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -11,6 +11,7 @@ #include #include +#include #include #include @@ -214,6 +215,20 @@ public: private: std::map m_map; }; + +class HTTPResponse +{ +public: + int m_version_major; + int m_version_minor; + HTTPStatusCode m_status; + std::string m_reason; + HTTPHeaders m_headers; + std::vector m_body; + bool m_keep_alive{false}; + + std::string StringifyHeaders() const; +}; } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H diff --git a/src/rpc/protocol.h b/src/rpc/protocol.h index 0574335c964..fc413f74269 100644 --- a/src/rpc/protocol.h +++ b/src/rpc/protocol.h @@ -20,6 +20,20 @@ enum HTTPStatusCode HTTP_SERVICE_UNAVAILABLE = 503, }; +// Copied from libevent http.c success_phrases[] and client_error_phrases[] +// TODO: Should HTTPStatusCode and HTTPReason be moved since they are not RPC protocols? +const std::map HTTPReason{ + {HTTP_OK, "OK"}, + {HTTP_NO_CONTENT, "No Content"}, + {HTTP_BAD_REQUEST, "Bad Request"}, + {HTTP_UNAUTHORIZED, "Unauthorized"}, + {HTTP_FORBIDDEN, "Forbidden"}, + {HTTP_NOT_FOUND, "Not Found"}, + {HTTP_BAD_METHOD, "Method Not Allowed"}, + {HTTP_INTERNAL_SERVER_ERROR, "Internal Server Error"}, + {HTTP_SERVICE_UNAVAILABLE, "Service Unavailable"}, +}; + //! Bitcoin RPC error codes enum RPCErrorCode { diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 7c5ba7a3ad7..29cf4145118 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -3,12 +3,14 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include using http_bitcoin::HTTPHeaders; +using http_bitcoin::HTTPResponse; BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) @@ -94,4 +96,30 @@ BOOST_AUTO_TEST_CASE(http_headers_tests) BOOST_CHECK(!headers.Find("Pizza")); } } + +BOOST_AUTO_TEST_CASE(http_response_tests) +{ + // Typical HTTP 1.1 response headers + HTTPHeaders headers{}; + headers.Write("Content-Type", "application/json"); + headers.Write("Date", "Tue, 15 Oct 2024 17:54:12 GMT"); + headers.Write("Content-Length", "41"); + // Response points to headers which already exist because some of them + // are set before we even know what the response will be. + HTTPResponse res; + res.m_version_major = 1; + res.m_version_minor = 1; + res.m_status = HTTP_OK; + res.m_reason = HTTPReason.find(res.m_status)->second; + res.m_body = StringToBuffer("{\"result\":865793,\"error\":null,\"id\":null\"}"); + // Everything except the body, which might be raw bytes instead of a string + res.m_headers = std::move(headers); + BOOST_CHECK_EQUAL( + res.StringifyHeaders(), + "HTTP/1.1 200 OK\r\n" + "Content-Length: 41\r\n" + "Content-Type: application/json\r\n" + "Date: Tue, 15 Oct 2024 17:54:12 GMT\r\n" + "\r\n"); +} BOOST_AUTO_TEST_SUITE_END() From 34e03406cf0782d451eb88e67dd902559d57124c Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 16 Oct 2024 14:18:45 -0400 Subject: [PATCH 27/42] http: Implement HTTPRequest class HTTP Request message: https://datatracker.ietf.org/doc/html/rfc1945#section-5 Request Line aka Control Line aka first line: https://datatracker.ietf.org/doc/html/rfc1945#section-5.1 See message_read_status() in libevent http.c for how `MORE_DATA_EXPECTED` is handled there --- src/httpserver.cpp | 54 +++++++++++++ src/httpserver.h | 27 +++++++ src/test/httpserver_tests.cpp | 138 ++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index e42e40098dc..9fafc20104b 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -784,6 +784,8 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch) namespace http_bitcoin { +using util::SplitString; + std::optional HTTPHeaders::Find(const std::string key) const { const auto it = m_map.find(key); @@ -851,4 +853,56 @@ std::string HTTPResponse::StringifyHeaders() const { return strprintf("HTTP/%d.%d %d %s\r\n%s", m_version_major, m_version_minor, m_status, m_reason, m_headers.Stringify()); } + +bool HTTPRequest::LoadControlData(LineReader& reader) +{ + auto maybe_line = reader.ReadLine(); + if (!maybe_line) return false; + std::string request_line = *maybe_line; + + // Request Line aka Control Data https://httpwg.org/specs/rfc9110.html#rfc.section.6.2 + // Three words separated by spaces, terminated by \n or \r\n + if (request_line.length() < MIN_REQUEST_LINE_LENGTH) throw std::runtime_error("HTTP request line too short"); + + const std::vector parts{SplitString(request_line, " ")}; + if (parts.size() != 3) throw std::runtime_error("HTTP request line malformed"); + m_method = parts[0]; + m_target = parts[1]; + + if (parts[2].rfind("HTTP/") != 0) throw std::runtime_error("HTTP request line malformed"); + const std::vector version_parts{SplitString(parts[2].substr(5), ".")}; + if (version_parts.size() != 2) throw std::runtime_error("HTTP request line malformed"); + auto major = ToIntegral(version_parts[0]); + auto minor = ToIntegral(version_parts[1]); + if (!major || !minor) throw std::runtime_error("HTTP request line malformed"); + m_version_major = major.value(); + m_version_minor = minor.value(); + + return true; +} + +bool HTTPRequest::LoadHeaders(LineReader& reader) +{ + return m_headers.Read(reader); +} + +bool HTTPRequest::LoadBody(LineReader& reader) +{ + // https://httpwg.org/specs/rfc9112.html#message.body + + // No Content-length or Transfer-Encoding header means no body, see libevent evhttp_get_body() + // TODO: we must also implement Transfer-Encoding for chunk-reading + auto content_length_value{m_headers.Find("Content-Length")}; + if (!content_length_value) return true; + + uint64_t content_length; + if (!ParseUInt64(content_length_value.value(), &content_length)) throw std::runtime_error("Cannot parse Content-Length value"); + + // Not enough data in buffer for expected body + if (reader.Left() < content_length) return false; + + m_body = reader.ReadLength(content_length); + + return true; +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index d78fed17018..2e92d2f9985 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -203,6 +203,14 @@ private: }; namespace http_bitcoin { +using util::LineReader; + +// shortest valid request line, used by libevent in evhttp_parse_request_line() +static const size_t MIN_REQUEST_LINE_LENGTH{strlen("GET / HTTP/1.0")}; +// maximum size of http request (request line + headers) +// see https://github.com/bitcoin/bitcoin/issues/6425 +static const size_t MAX_HEADERS_SIZE{8192}; + class HTTPHeaders { public: @@ -229,6 +237,25 @@ public: std::string StringifyHeaders() const; }; + +class HTTPRequest +{ +public: + std::string m_method; + std::string m_target; + // Default protocol version is used by error responses to unreadable requests + int m_version_major{1}; + int m_version_minor{1}; + HTTPHeaders m_headers; + std::string m_body; + + // Readers return false if they need more data from the + // socket to parse properly. They throw errors if + // the data is invalid. + bool LoadControlData(LineReader& reader); + bool LoadHeaders(LineReader& reader); + bool LoadBody(LineReader& reader); +}; } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 29cf4145118..5b5af4b98e8 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -10,7 +10,10 @@ #include using http_bitcoin::HTTPHeaders; +using http_bitcoin::HTTPRequest; using http_bitcoin::HTTPResponse; +using http_bitcoin::MAX_HEADERS_SIZE; +using util::LineReader; BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) @@ -122,4 +125,139 @@ BOOST_AUTO_TEST_CASE(http_response_tests) "Date: Tue, 15 Oct 2024 17:54:12 GMT\r\n" "\r\n"); } + +BOOST_AUTO_TEST_CASE(http_request_tests) +{ + { + // Reading request captured from bitcoin-cli + const std::string full_request = + "504f5354202f20485454502f312e310d0a486f73743a203132372e302e302e310d" + "0a436f6e6e656374696f6e3a20636c6f73650d0a436f6e74656e742d547970653a" + "206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a" + "204261736963205831396a6232397261575666587a6f354f4751354f4451334d57" + "4e6d4e6a67304e7a417a59546b7a4e32457a4e7a6b305a44466c4f4451314e6a5a" + "6d5954526b5a6a4a694d7a466b596a68684f4449345a4759344d6a566a4f546735" + "5a4749344f54566c0d0a436f6e74656e742d4c656e6774683a2034360d0a0d0a7b" + "226d6574686f64223a22676574626c6f636b636f756e74222c22706172616d7322" + "3a5b5d2c226964223a317d0a"; + HTTPRequest req; + std::vector buffer{TryParseHex(full_request).value()}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(req.LoadHeaders(reader)); + BOOST_CHECK(req.LoadBody(reader)); + BOOST_CHECK_EQUAL(req.m_method, "POST"); + BOOST_CHECK_EQUAL(req.m_target, "/"); + BOOST_CHECK_EQUAL(req.m_version_major, 1); + BOOST_CHECK_EQUAL(req.m_version_minor, 1); + BOOST_CHECK_EQUAL(req.m_headers.Find("Host").value(), "127.0.0.1"); + BOOST_CHECK_EQUAL(req.m_headers.Find("Connection").value(), "close"); + BOOST_CHECK_EQUAL(req.m_headers.Find("Content-Type").value(), "application/json"); + BOOST_CHECK_EQUAL(req.m_headers.Find("Authorization").value(), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl"); + BOOST_CHECK_EQUAL(req.m_headers.Find("Content-Length").value(), "46"); + BOOST_CHECK_EQUAL(req.m_body.size(), 46); + BOOST_CHECK_EQUAL(req.m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n"); + } + { + const std::string too_short_request_line = "GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(too_short_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error); + } + { + const std::string malformed_request_line = "GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(malformed_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error); + } + { + const std::string malformed_request_line = "GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(malformed_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error); + } + { + const std::string malformed_request_line = "GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(malformed_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error); + } + { + const std::string malformed_request_line = "GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(malformed_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error); + } + { + const std::string ok_request_line = "GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(ok_request_line)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(req.LoadHeaders(reader)); + BOOST_CHECK(req.LoadBody(reader)); + BOOST_CHECK_EQUAL(req.m_method, "GET"); + BOOST_CHECK_EQUAL(req.m_target, "/"); + BOOST_CHECK_EQUAL(req.m_version_major, 1); + BOOST_CHECK_EQUAL(req.m_version_minor, 0); + BOOST_CHECK_EQUAL(req.m_headers.Find("Host").value(), "127.0.0.1"); + // no body is OK + BOOST_CHECK_EQUAL(req.m_body.size(), 0); + } + { + const std::string malformed_headers = "GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n"; + HTTPRequest req; + std::vector buffer{StringToBuffer(malformed_headers)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK_THROW(req.LoadHeaders(reader), std::runtime_error); + } + { + // We might not have received enough data from the client which is not + // an error. We return false so the caller can try again later when the + // buffer has more data. + const std::string incomplete_headers = "GET / HTTP/1.0\r\nHost: "; + HTTPRequest req; + std::vector buffer{StringToBuffer(incomplete_headers)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(!req.LoadHeaders(reader)); + } + { + const std::string no_content_length = "GET / HTTP/1.0\r\n\r\n{\"method\":\"getblockcount\"}"; + HTTPRequest req; + std::vector buffer{StringToBuffer(no_content_length)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(req.LoadHeaders(reader)); + BOOST_CHECK(req.LoadBody(reader)); + // Don't try to read request body if Content-Length is missing + BOOST_CHECK_EQUAL(req.m_body.size(), 0); + } + { + const std::string bad_content_length = "GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n{\"method\":\"getblockcount\"}"; + HTTPRequest req; + std::vector buffer{StringToBuffer(bad_content_length)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(req.LoadHeaders(reader)); + BOOST_CHECK_THROW(req.LoadBody(reader), std::runtime_error); + } + { + // Content-Length indicates more data than we have in the buffer. + // Again, not an error just try again later. + const std::string excessive_content_length = "GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n{\"method\":\"getblockcount\"}"; + HTTPRequest req; + std::vector buffer{StringToBuffer(excessive_content_length)}; + LineReader reader(buffer, MAX_HEADERS_SIZE); + BOOST_CHECK(req.LoadControlData(reader)); + BOOST_CHECK(req.LoadHeaders(reader)); + BOOST_CHECK(!req.LoadBody(reader)); + } +} BOOST_AUTO_TEST_SUITE_END() From 26581441865654bcc65f6c730fde68dcbaade4b6 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 30 Oct 2024 16:11:20 -0400 Subject: [PATCH 28/42] http: Begin implementation of HTTPClient and HTTPServer --- src/httpserver.cpp | 13 +++++ src/httpserver.h | 81 ++++++++++++++++++++++++++ src/test/httpserver_tests.cpp | 103 ++++++++++++++++++++++++++++++---- 3 files changed, 185 insertions(+), 12 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 9fafc20104b..e7a3b8fe694 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -905,4 +905,17 @@ bool HTTPRequest::LoadBody(LineReader& reader) return true; } + +bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, + const CService& me, + const CService& them) +{ + auto client = std::make_shared(node_id, them); + // Point back to the server + client->m_server = this; + LogDebug(BCLog::HTTP, "HTTP Connection accepted from %s (id=%d)\n", client->m_origin, client->m_node_id); + m_connected_clients.emplace(client->m_node_id, std::move(client)); + m_no_clients = false; + return true; +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index 2e92d2f9985..33de955667d 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -204,6 +205,7 @@ private: namespace http_bitcoin { using util::LineReader; +using NodeId = SockMan::Id; // shortest valid request line, used by libevent in evhttp_parse_request_line() static const size_t MIN_REQUEST_LINE_LENGTH{strlen("GET / HTTP/1.0")}; @@ -256,6 +258,85 @@ public: bool LoadHeaders(LineReader& reader); bool LoadBody(LineReader& reader); }; + +class HTTPServer; + +class HTTPClient +{ +public: + // ID provided by SockMan, inherited by HTTPServer + NodeId m_node_id; + // Remote address of connected client + CService m_addr; + // IP:port of connected client, cached for logging purposes + std::string m_origin; + // Pointer back to the server so we can call Sockman I/O methods from the client + // Ok to remain null for unit tests. + HTTPServer* m_server; + + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) + { + m_origin = addr.ToStringAddrPort(); + }; + + // Disable copies (should only be used as shared pointers) + HTTPClient(const HTTPClient&) = delete; + HTTPClient& operator=(const HTTPClient&) = delete; +}; + +class HTTPServer : public SockMan +{ +public: + // Set in the Sockman I/O loop and only checked by main thread when shutting + // down to wait for all clients to be disconnected. + std::atomic_bool m_no_clients{true}; + + //! Connected clients with live HTTP connections + std::unordered_map> m_connected_clients; + + /** + * Be notified when a new connection has been accepted. + * @param[in] node_id Id of the newly accepted connection. + * @param[in] me The address and port at our side of the connection. + * @param[in] them The address and port at the peer's side of the connection. + * @retval true The new connection was accepted at the higher level. + * @retval false The connection was refused at the higher level, so the + * associated socket and node_id should be discarded by `SockMan`. + */ + virtual bool EventNewConnectionAccepted(NodeId node_id, const CService& me, const CService& them) override; + + /** + * Called when the socket is ready to send data and `ShouldTryToSend()` has + * returned true. This is where the higher level code serializes its messages + * and calls `SockMan::SendBytes()`. + * @param[in] node_id Id of the node whose socket is ready to send. + * @param[out] cancel_recv Should always be set upon return and if it is true, + * then the next attempt to receive data from that node will be omitted. + */ + virtual void EventReadyToSend(NodeId node_id, bool& cancel_recv) override {}; + + /** + * Called when new data has been received. + * @param[in] node_id Connection for which the data arrived. + * @param[in] data Received data. + */ + virtual void EventGotData(NodeId node_id, std::span data) override {}; + + /** + * Called when the remote peer has sent an EOF on the socket. This is a graceful + * close of their writing side, we can still send and they will receive, if it + * makes sense at the application level. + * @param[in] node_id Node whose socket got EOF. + */ + virtual void EventGotEOF(NodeId node_id) override {}; + + /** + * Called when we get an irrecoverable error trying to read from a socket. + * @param[in] node_id Node whose socket got an error. + * @param[in] errmsg Message describing the error. + */ + virtual void EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) override {}; +}; } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 5b5af4b98e8..309e8995238 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -12,10 +13,38 @@ using http_bitcoin::HTTPHeaders; using http_bitcoin::HTTPRequest; using http_bitcoin::HTTPResponse; +using http_bitcoin::HTTPServer; using http_bitcoin::MAX_HEADERS_SIZE; using util::LineReader; -BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) +// Reading request captured from bitcoin-cli +const std::string full_request = + "504f5354202f20485454502f312e310d0a486f73743a203132372e302e302e310d" + "0a436f6e6e656374696f6e3a20636c6f73650d0a436f6e74656e742d547970653a" + "206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a" + "204261736963205831396a6232397261575666587a6f354f4751354f4451334d57" + "4e6d4e6a67304e7a417a59546b7a4e32457a4e7a6b305a44466c4f4451314e6a5a" + "6d5954526b5a6a4a694d7a466b596a68684f4449345a4759344d6a566a4f546735" + "5a4749344f54566c0d0a436f6e74656e742d4c656e6774683a2034360d0a0d0a7b" + "226d6574686f64223a22676574626c6f636b636f756e74222c22706172616d7322" + "3a5b5d2c226964223a317d0a"; + +/// Save the value of CreateSock and restore it when the test ends. +class HTTPTestingSetup : public BasicTestingSetup +{ +public: + explicit HTTPTestingSetup() : m_create_sock_orig{CreateSock} {}; + + ~HTTPTestingSetup() + { + CreateSock = m_create_sock_orig; + } + +private: + const decltype(CreateSock) m_create_sock_orig; +}; + +BOOST_FIXTURE_TEST_SUITE(httpserver_tests, HTTPTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) { @@ -129,17 +158,6 @@ BOOST_AUTO_TEST_CASE(http_response_tests) BOOST_AUTO_TEST_CASE(http_request_tests) { { - // Reading request captured from bitcoin-cli - const std::string full_request = - "504f5354202f20485454502f312e310d0a486f73743a203132372e302e302e310d" - "0a436f6e6e656374696f6e3a20636c6f73650d0a436f6e74656e742d547970653a" - "206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a" - "204261736963205831396a6232397261575666587a6f354f4751354f4451334d57" - "4e6d4e6a67304e7a417a59546b7a4e32457a4e7a6b305a44466c4f4451314e6a5a" - "6d5954526b5a6a4a694d7a466b596a68684f4449345a4759344d6a566a4f546735" - "5a4749344f54566c0d0a436f6e74656e742d4c656e6774683a2034360d0a0d0a7b" - "226d6574686f64223a22676574626c6f636b636f756e74222c22706172616d7322" - "3a5b5d2c226964223a317d0a"; HTTPRequest req; std::vector buffer{TryParseHex(full_request).value()}; LineReader reader(buffer, MAX_HEADERS_SIZE); @@ -260,4 +278,65 @@ BOOST_AUTO_TEST_CASE(http_request_tests) BOOST_CHECK(!req.LoadBody(reader)); } } + +BOOST_AUTO_TEST_CASE(http_client_server_tests) +{ + // Queue of connected sockets returned by listening socket (represents network interface) + std::shared_ptr accepted_sockets{std::make_shared()}; + + CreateSock = [&accepted_sockets](int, int, int) { + // This is a mock Listening Socket that the HTTP server will "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(), accepted_sockets); + }; + + { + // I/O pipes of one mock Connected Socket we can read and write to. + std::shared_ptr connected_socket_pipes(std::make_shared()); + + // Insert the payload: a correctly formatted HTTP request + std::vector buffer{TryParseHex(full_request).value()}; + connected_socket_pipes->recv.PushBytes(buffer.data(), buffer.size()); + + // Mock Connected Socket that represents a client. + // It needs I/O pipes but its queue can remain empty + std::unique_ptr connected_socket{std::make_unique(connected_socket_pipes, std::make_shared())}; + + // Prepare queue of accepted_sockets: just one connection with no data + accepted_sockets->Push(std::move(connected_socket)); + + // Instantiate server + HTTPServer server = HTTPServer(); + BOOST_REQUIRE(server.m_no_clients); + + // This address won't actually get used because we stubbed CreateSock() + const std::optional addr{Lookup("127.0.0.1", 8333, false)}; + bilingual_str strError; + // Bind to mock Listening Socket + BOOST_REQUIRE(server.BindAndStartListening(addr.value(), strError)); + // Start the I/O loop, accepting connections + SockMan::Options sockman_options; + server.StartSocketsThreads(sockman_options); + + // Wait up to one second for mock client to connect. + // Given that the mock client is itself a mock socket + // with hard-coded data it should only take a fraction of that. + int attempts{100}; + while (attempts > 0) + { + if (!server.m_no_clients) break; + + std::this_thread::sleep_for(10ms); + --attempts; + } + BOOST_REQUIRE(!server.m_no_clients); + + // Close server + server.interruptNet(); + // Wait for I/O loop to finish, after all sockets are closed + server.JoinSocketsThreads(); + } +} BOOST_AUTO_TEST_SUITE_END() From 5a2d625e08c7cee0931e42d63a916621ceaa7709 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 31 Oct 2024 13:34:19 -0400 Subject: [PATCH 29/42] http: read requests from connected clients --- src/httpserver.cpp | 77 +++++++++++++++++++++++++++++++++++ src/httpserver.h | 27 +++++++++++- src/test/httpserver_tests.cpp | 23 ++++++++++- 3 files changed, 123 insertions(+), 4 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index e7a3b8fe694..d3783eb1cb0 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -906,6 +906,24 @@ bool HTTPRequest::LoadBody(LineReader& reader) return true; } +bool HTTPClient::ReadRequest(std::unique_ptr& req) +{ + LineReader reader(m_recv_buffer, MAX_HEADERS_SIZE); + + if (!req->LoadControlData(reader)) return false; + if (!req->LoadHeaders(reader)) return false; + if (!req->LoadBody(reader)) return false; + + // Remove the bytes read out of the buffer. + // If one of the above calls throws an error, the caller must + // catch it and disconnect the client. + m_recv_buffer.erase( + m_recv_buffer.begin(), + m_recv_buffer.begin() + (reader.it - reader.start)); + + return true; +} + bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, const CService& me, const CService& them) @@ -918,4 +936,63 @@ bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, m_no_clients = false; return true; } + +void HTTPServer::EventGotData(NodeId node_id, std::span data) +{ + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return; + } + + // Copy data from socket buffer to client receive buffer + client->m_recv_buffer.insert( + client->m_recv_buffer.end(), + reinterpret_cast(data.data()), + reinterpret_cast(data.data() + data.size()) + ); + + // Try reading (potentially multiple) HTTP requests from the buffer + while (client->m_recv_buffer.size() > 0) { + // Create a new request object and try to fill it with data from the receive buffer + auto req = std::make_unique(client); + try { + // Stop reading if we need more data from the client to parse a complete request + if (!client->ReadRequest(req)) break; + } catch (const std::runtime_error& e) { + LogDebug( + BCLog::HTTP, + "Error reading HTTP request from client %s (id=%lld): %s\n", + client->m_origin, + client->m_node_id, + e.what()); + + // We failed to read a complete request from the buffer + // TODO: respond with HTTP_BAD_REQUEST and disconnect + + break; + } + + // We read a complete request from the buffer into the queue + LogDebug( + BCLog::HTTP, + "Received a %s request for %s from %s (id=%lld)\n", + req->m_method, + req->m_target, + req->m_client->m_origin, + req->m_client->m_node_id); + + // handle request + m_request_dispatcher(std::move(req)); + } +} + +std::shared_ptr HTTPServer::GetClientById(NodeId node_id) const +{ + auto it{m_connected_clients.find(node_id)}; + if (it != m_connected_clients.end()) { + return it->second; + } + return nullptr; +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index 33de955667d..acd5b9fee54 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -240,6 +240,8 @@ public: std::string StringifyHeaders() const; }; +class HTTPClient; + class HTTPRequest { public: @@ -251,6 +253,13 @@ public: HTTPHeaders m_headers; std::string m_body; + // Keep a pointer to the client that made the request so + // we know who to respond to. + std::shared_ptr m_client; + explicit HTTPRequest(std::shared_ptr client) : m_client(client) {}; + // Null client for unit tests + explicit HTTPRequest() : m_client(nullptr) {}; + // Readers return false if they need more data from the // socket to parse properly. They throw errors if // the data is invalid. @@ -274,11 +283,19 @@ public: // Ok to remain null for unit tests. HTTPServer* m_server; + // In lieu of an intermediate transport class like p2p uses, + // we copy data from the socket buffer to the client object + // and attempt to read HTTP requests from here. + std::vector m_recv_buffer{}; + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) { m_origin = addr.ToStringAddrPort(); }; + // Try to read an HTTP request from the receive buffer + bool ReadRequest(std::unique_ptr& req); + // Disable copies (should only be used as shared pointers) HTTPClient(const HTTPClient&) = delete; HTTPClient& operator=(const HTTPClient&) = delete; @@ -287,13 +304,19 @@ public: class HTTPServer : public SockMan { public: + explicit HTTPServer(std::function)> func) : m_request_dispatcher(func) {}; + // Set in the Sockman I/O loop and only checked by main thread when shutting // down to wait for all clients to be disconnected. std::atomic_bool m_no_clients{true}; - //! Connected clients with live HTTP connections std::unordered_map> m_connected_clients; + // What to do with HTTP requests once received, validated and parsed + std::function)> m_request_dispatcher; + + std::shared_ptr GetClientById(NodeId node_id) const; + /** * Be notified when a new connection has been accepted. * @param[in] node_id Id of the newly accepted connection. @@ -320,7 +343,7 @@ public: * @param[in] node_id Connection for which the data arrived. * @param[in] data Received data. */ - virtual void EventGotData(NodeId node_id, std::span data) override {}; + virtual void EventGotData(NodeId node_id, std::span data) override; /** * Called when the remote peer has sent an EOF on the socket. This is a graceful diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 309e8995238..627251bbb10 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -10,6 +10,7 @@ #include +using http_bitcoin::HTTPClient; using http_bitcoin::HTTPHeaders; using http_bitcoin::HTTPRequest; using http_bitcoin::HTTPResponse; @@ -307,8 +308,17 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests) // Prepare queue of accepted_sockets: just one connection with no data accepted_sockets->Push(std::move(connected_socket)); - // Instantiate server - HTTPServer server = HTTPServer(); + // Prepare a request handler that just stores received requests so we can examine them + // Mutex is required to prevent a race between this test's main thread and the Sockman I/O loop. + Mutex requests_mutex; + std::deque> requests; + auto StoreRequest = [&](std::unique_ptr req) { + LOCK(requests_mutex); + requests.push_back(std::move(req)); + }; + + // Instantiate server with dead-end request handler + HTTPServer server = HTTPServer(StoreRequest); BOOST_REQUIRE(server.m_no_clients); // This address won't actually get used because we stubbed CreateSock() @@ -333,6 +343,15 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests) } BOOST_REQUIRE(!server.m_no_clients); + { + LOCK(requests_mutex); + // Connected client should have one request already from the static content. + BOOST_CHECK_EQUAL(requests.size(), 1); + + // Check the received request + BOOST_CHECK_EQUAL(requests.front()->m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n"); + } + // Close server server.interruptNet(); // Wait for I/O loop to finish, after all sockets are closed From 5d3678a23564116ffc9becb5da6b63d25bae3860 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 12 Mar 2025 14:51:31 -0400 Subject: [PATCH 30/42] http: support "chunked" Transfer-Encoding --- src/httpserver.cpp | 54 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 44 insertions(+), 10 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index d3783eb1cb0..da344911044 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -890,20 +890,54 @@ bool HTTPRequest::LoadBody(LineReader& reader) { // https://httpwg.org/specs/rfc9112.html#message.body - // No Content-length or Transfer-Encoding header means no body, see libevent evhttp_get_body() - // TODO: we must also implement Transfer-Encoding for chunk-reading - auto content_length_value{m_headers.Find("Content-Length")}; - if (!content_length_value) return true; + auto transfer_encoding_header = m_headers.Find("Transfer-Encoding"); + if (transfer_encoding_header && ToLower(transfer_encoding_header.value()) == "chunked") { + // Transfer-Encoding: https://datatracker.ietf.org/doc/html/rfc7230.html#section-3.3.1 + // Chunked Transfer Coding: https://datatracker.ietf.org/doc/html/rfc7230.html#section-4.1 + // see evhttp_handle_chunked_read() in libevent http.c + while (reader.Left() > 0) { + auto maybe_chunk_size = reader.ReadLine(); + if (!maybe_chunk_size) return false; + uint64_t chunk_size; - uint64_t content_length; - if (!ParseUInt64(content_length_value.value(), &content_length)) throw std::runtime_error("Cannot parse Content-Length value"); + if (!ParseUInt64Hex(maybe_chunk_size.value(), &chunk_size)) throw std::runtime_error("Invalid chunk size"); - // Not enough data in buffer for expected body - if (reader.Left() < content_length) return false; + bool last_chunk{chunk_size == 0}; - m_body = reader.ReadLength(content_length); + if (!last_chunk) { + // We are still expecting more data for this chunk + if (reader.Left() < chunk_size) { + return false; + } + // Pack chunk onto body + m_body += reader.ReadLength(chunk_size); + } - return true; + // Even though every chunk size is explicitly declared, + // they are still terminated by a CRLF we don't need. + auto crlf = reader.ReadLine(); + if (!crlf || crlf.value().size() != 0) throw std::runtime_error("Improperly terminated chunk"); + + if (last_chunk) return true; + } + + // We read all the chunks but never got the last chunk, wait for client to send more + return false; + } else { + // No Content-length or Transfer-Encoding header means no body, see libevent evhttp_get_body() + auto content_length_value{m_headers.Find("Content-Length")}; + if (!content_length_value) return true; + + uint64_t content_length; + if (!ParseUInt64(content_length_value.value(), &content_length)) throw std::runtime_error("Cannot parse Content-Length value"); + + // Not enough data in buffer for expected body + if (reader.Left() < content_length) return false; + + m_body = reader.ReadLength(content_length); + + return true; + } } bool HTTPClient::ReadRequest(std::unique_ptr& req) From 057d6488668aa5843058565557e27b83c4100c24 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Tue, 10 Dec 2024 20:02:55 -0500 Subject: [PATCH 31/42] http: compose and send replies to connected clients --- src/httpserver.cpp | 162 ++++++++++++++++++++++++++++++++++ src/httpserver.h | 41 ++++++++- src/test/httpserver_tests.cpp | 34 +++++++ 3 files changed, 236 insertions(+), 1 deletion(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index da344911044..fd61a821cc0 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -12,6 +12,7 @@ #include #include #include // For HTTP status codes +#include #include #include #include @@ -940,6 +941,92 @@ bool HTTPRequest::LoadBody(LineReader& reader) } } +void HTTPRequest::WriteReply(HTTPStatusCode status, std::span reply_body) +{ + HTTPResponse res; + + // Some response headers are determined in advance and stored in the request + res.m_headers = std::move(m_response_headers); + + // Response version matches request version + res.m_version_major = m_version_major; + res.m_version_minor = m_version_minor; + + // Add response code and look up reason string + res.m_status = status; + res.m_reason = HTTPReason.find(status)->second; + + // See libevent evhttp_response_needs_body() + // Response headers are different if no body is needed + bool needs_body{status != HTTP_NO_CONTENT && (status < 100 || status >= 200)}; + + // See libevent evhttp_make_header_response() + // Expected response headers depend on protocol version + if (m_version_major == 1) { + // HTTP/1.0 + if (m_version_minor == 0) { + auto connection_header{m_headers.Find("Connection")}; + if (connection_header && ToLower(connection_header.value()) == "keep-alive") { + res.m_headers.Write("Connection", "keep-alive"); + res.m_keep_alive = true; + } + } + + // HTTP/1.1 + if (m_version_minor >= 1) { + const int64_t now_seconds{TicksSinceEpoch(NodeClock::now())}; + res.m_headers.Write("Date", FormatRFC7231DateTime(now_seconds)); + + if (needs_body) { + res.m_headers.Write("Content-Length", strprintf("%d", reply_body.size())); + } + + // Default for HTTP/1.1 + res.m_keep_alive = true; + } + } + + if (needs_body && !res.m_headers.Find("Content-Type")) { + // Default type from libevent evhttp_new_object() + res.m_headers.Write("Content-Type", "text/html; charset=ISO-8859-1"); + } + + auto connection_header{m_headers.Find("Connection")}; + if (connection_header && ToLower(connection_header.value()) == "close") { + // Might not exist already but we need to replace it, not append to it + res.m_headers.Remove("Connection"); + res.m_headers.Write("Connection", "close"); + res.m_keep_alive = false; + } + + // Serialize the response headers + const std::string headers{res.StringifyHeaders()}; + const auto headers_bytes{std::as_bytes(std::span(headers.begin(), headers.end()))}; + + // Fill the send buffer with the complete serialized response headers + body + { + LOCK(m_client->m_send_mutex); + m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), headers_bytes.begin(), headers_bytes.end()); + + // We've been using std::span up until now but it is finally time to copy + // data. The original data will go out of scope when WriteReply() returns. + // This is analogous to the memcpy() in libevent's evbuffer_add() + m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), reply_body.begin(), reply_body.end()); + } + + // Inform Sockman I/O there is data that is ready to be sent to this client + // in the next loop iteration. + m_client->m_send_ready = true; + + LogDebug( + BCLog::HTTP, + "HTTPResponse (status code: %d size: %lld) added to send buffer for client %s (id=%lld)\n", + status, + headers_bytes.size() + reply_body.size(), + m_client->m_origin, + m_client->m_node_id); +} + bool HTTPClient::ReadRequest(std::unique_ptr& req) { LineReader reader(m_recv_buffer, MAX_HEADERS_SIZE); @@ -958,6 +1045,41 @@ bool HTTPClient::ReadRequest(std::unique_ptr& req) return true; } +bool HTTPClient::SendBytesFromBuffer() +{ + Assume(m_server); + + // Send as much data from this client's buffer as we can + LOCK(m_send_mutex); + if (!m_send_buffer.empty()) { + std::string err; + // We don't intend to "send more" because http responses are usually small and we want the kernel to send them right away. + ssize_t bytes_sent = m_server->SendBytes(m_node_id, MakeUCharSpan(m_send_buffer), /*will_send_more=*/false, err); + if (bytes_sent < 0) { + LogDebug( + BCLog::HTTP, + "Error sending HTTP response data to client %s (id=%lld): %s\n", + m_origin, + m_node_id, + err); + // TODO: disconnect + return false; + } + + Assume(static_cast(bytes_sent) <= m_send_buffer.size()); + m_send_buffer.erase(m_send_buffer.begin(), m_send_buffer.begin() + bytes_sent); + + LogDebug( + BCLog::HTTP, + "Sent %d bytes to client %s (id=%lld)\n", + bytes_sent, + m_origin, + m_node_id); + } + + return true; +} + bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, const CService& me, const CService& them) @@ -971,6 +1093,23 @@ bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, return true; } +void HTTPServer::EventReadyToSend(NodeId node_id, bool& cancel_recv) +{ + // Next attempt to receive data from this node is permitted + cancel_recv = false; + + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return; + } + + // SendBytesFromBuffer() returns true if we should keep the client around, + // false if we are done with it. Invert that boolean to inform Sockman + // whether it should cancel the next receive attempt from this client. + cancel_recv = !client->SendBytesFromBuffer(); +} + void HTTPServer::EventGotData(NodeId node_id, std::span data) { // Get the HTTPClient @@ -1021,6 +1160,29 @@ void HTTPServer::EventGotData(NodeId node_id, std::span data) } } +bool HTTPServer::ShouldTryToSend(NodeId node_id) const +{ + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return false; + } + + return client->m_send_ready; +} + +bool HTTPServer::ShouldTryToRecv(NodeId node_id) const +{ + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return false; + } + + // Don't try to receive again until we've cleared the send buffer to this client + return !client->m_send_ready; +} + std::shared_ptr HTTPServer::GetClientById(NodeId node_id) const { auto it{m_connected_clients.find(node_id)}; diff --git a/src/httpserver.h b/src/httpserver.h index acd5b9fee54..be3a4c56db5 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -266,6 +266,15 @@ public: bool LoadControlData(LineReader& reader); bool LoadHeaders(LineReader& reader); bool LoadBody(LineReader& reader); + + // Response headers may be set in advance before response body is known + HTTPHeaders m_response_headers; + void WriteReply(HTTPStatusCode status, std::span reply_body = {}); + void WriteReply(HTTPStatusCode status, const char* reply_body) { + auto reply_body_view = std::string_view(reply_body); + std::span byte_span(reinterpret_cast(reply_body_view.data()), reply_body_view.size()); + WriteReply(status, byte_span); + } }; class HTTPServer; @@ -288,6 +297,15 @@ public: // and attempt to read HTTP requests from here. std::vector m_recv_buffer{}; + // Response data destined for this client. + // Written to directly by http worker threads, read and erased by Sockman I/O + Mutex m_send_mutex; + std::vector m_send_buffer GUARDED_BY(m_send_mutex); + // Set true by worker threads after writing a response to m_send_buffer. + // Set false by the Sockman I/O thread after flushing m_send_buffer. + // Checked in the Sockman I/O loop to avoid locking m_send_mutex if there's nothing to send. + std::atomic_bool m_send_ready{false}; + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) { m_origin = addr.ToStringAddrPort(); @@ -296,6 +314,11 @@ public: // Try to read an HTTP request from the receive buffer bool ReadRequest(std::unique_ptr& req); + // Push data from m_send_buffer to the connected socket via m_server + // Returns false if we are done with this client and Sockman can + // therefore skip the next read operation from it. + bool SendBytesFromBuffer() EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex); + // Disable copies (should only be used as shared pointers) HTTPClient(const HTTPClient&) = delete; HTTPClient& operator=(const HTTPClient&) = delete; @@ -336,7 +359,7 @@ public: * @param[out] cancel_recv Should always be set upon return and if it is true, * then the next attempt to receive data from that node will be omitted. */ - virtual void EventReadyToSend(NodeId node_id, bool& cancel_recv) override {}; + virtual void EventReadyToSend(NodeId node_id, bool& cancel_recv) override; /** * Called when new data has been received. @@ -359,6 +382,22 @@ public: * @param[in] errmsg Message describing the error. */ virtual void EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) override {}; + + /** + * Can be used to temporarily pause sends on a connection. + * SockMan would only call EventReadyToSend() if this returns true. + * The implementation in SockMan always returns true. + * @param[in] node_id Connection for which to confirm or omit the next call to EventReadyToSend(). + */ + virtual bool ShouldTryToSend(NodeId node_id) const override; + + /** + * SockMan would only call Recv() on a connection's socket if this returns true. + * Can be used to temporarily pause receives on a connection. + * The implementation in SockMan always returns true. + * @param[in] node_id Connection for which to confirm or omit the next receive. + */ + virtual bool ShouldTryToRecv(NodeId node_id) const override; }; } // namespace http_bitcoin diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 627251bbb10..39e7fa5e693 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -3,6 +3,7 @@ // file COPYING or http://www.opensource.org/licenses/mit-license.php. #include +#include #include #include #include @@ -282,6 +283,10 @@ BOOST_AUTO_TEST_CASE(http_request_tests) BOOST_AUTO_TEST_CASE(http_client_server_tests) { + // Hard code the timestamp for the Date header in the HTTP response + // Wed Dec 11 00:47:09 2024 UTC + SetMockTime(1733878029); + // Queue of connected sockets returned by listening socket (represents network interface) std::shared_ptr accepted_sockets{std::make_shared()}; @@ -350,8 +355,37 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests) // Check the received request BOOST_CHECK_EQUAL(requests.front()->m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n"); + + // Respond to request + requests.front()->WriteReply(HTTP_OK, "874140\n"); } + // Check the sent response from the mock client at the other end of the mock socket + std::string expected = "HTTP/1.1 200 OK\r\n" + "Connection: close\r\n" + "Content-Length: 7\r\n" + "Content-Type: text/html; charset=ISO-8859-1\r\n" + "Date: Wed, 11 Dec 2024 00:47:09 GMT\r\n" + "\r\n" + "874140\n"; + std::string actual; + + // Wait up to one second for all the bytes to appear in the "send" pipe. + char buf[0x10000] = {}; + attempts = 100; + while (attempts > 0) + { + ssize_t bytes_read = connected_socket_pipes->send.GetBytes(buf, sizeof(buf), 0); + if (bytes_read > 0) { + actual.append(buf, bytes_read); + if (actual == expected) break; + } + + std::this_thread::sleep_for(10ms); + --attempts; + } + BOOST_CHECK_EQUAL(actual, expected); + // Close server server.interruptNet(); // Wait for I/O loop to finish, after all sockets are closed From d771b650f33b0a4bc12d594823cc47a1ea5ee88b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 3 Mar 2025 15:20:47 -0500 Subject: [PATCH 32/42] http: disconnect clients --- src/httpserver.cpp | 76 +++++++++++++++++++++++++++++++++-- src/httpserver.h | 33 ++++++++++++++- src/test/httpserver_tests.cpp | 11 +++++ 3 files changed, 115 insertions(+), 5 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index fd61a821cc0..4a0ffdb98bd 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -999,6 +999,8 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r res.m_keep_alive = false; } + m_client->m_keep_alive = res.m_keep_alive; + // Serialize the response headers const std::string headers{res.StringifyHeaders()}; const auto headers_bytes{std::as_bytes(std::span(headers.begin(), headers.end()))}; @@ -1062,7 +1064,9 @@ bool HTTPClient::SendBytesFromBuffer() m_origin, m_node_id, err); - // TODO: disconnect + m_send_ready = false; + m_prevent_disconnect = false; + m_disconnect = true; return false; } @@ -1075,11 +1079,47 @@ bool HTTPClient::SendBytesFromBuffer() bytes_sent, m_origin, m_node_id); + + // This check is inside the if(!empty) block meaning "there was data but now its gone". + // We shouldn't even be calling SendBytesFromBuffer() when the send buffer is empty, + // but for belt-and-suspenders, we don't want to modify the disconnect flags if SendBytesFromBuffer() was a no-op. + if (m_send_buffer.empty()) { + m_send_ready = false; + m_prevent_disconnect = false; + + // Our work is done here + if (!m_keep_alive) { + m_disconnect = true; + return false; + } + } } return true; } +void HTTPServer::CloseConnectionInternal(std::shared_ptr& client) +{ + if (CloseConnection(client->m_node_id)) { + LogDebug(BCLog::HTTP, "Disconnected HTTP client %s (id=%d)\n", client->m_origin, client->m_node_id); + } else { + LogDebug(BCLog::HTTP, "Failed to disconnect non-existent HTTP client %s (id=%d)\n", client->m_origin, client->m_node_id); + } +} + +void HTTPServer::DisconnectClients() +{ + for (auto it = m_connected_clients.begin(); it != m_connected_clients.end();) { + if ((it->second->m_disconnect || m_disconnect_all_clients) && !it->second->m_prevent_disconnect) { + CloseConnectionInternal(it->second); + it = m_connected_clients.erase(it); + } else { + ++it; + } + } + m_no_clients = m_connected_clients.size() == 0; +} + bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, const CService& me, const CService& them) @@ -1118,6 +1158,9 @@ void HTTPServer::EventGotData(NodeId node_id, std::span data) return; } + // Prevent disconnect until all requests are completely handled. + client->m_prevent_disconnect = true; + // Copy data from socket buffer to client receive buffer client->m_recv_buffer.insert( client->m_recv_buffer.end(), @@ -1141,8 +1184,8 @@ void HTTPServer::EventGotData(NodeId node_id, std::span data) e.what()); // We failed to read a complete request from the buffer - // TODO: respond with HTTP_BAD_REQUEST and disconnect - + req->WriteReply(HTTP_BAD_REQUEST); + client->m_disconnect = true; break; } @@ -1160,6 +1203,33 @@ void HTTPServer::EventGotData(NodeId node_id, std::span data) } } +void HTTPServer::EventGotEOF(NodeId node_id) +{ + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return; + } + + client->m_disconnect = true; +} + +void HTTPServer::EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) +{ + // Get the HTTPClient + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return; + } + + client->m_disconnect = true; +} + +void HTTPServer::EventIOLoopCompletedForAll() +{ + DisconnectClients(); +} + bool HTTPServer::ShouldTryToSend(NodeId node_id) const { // Get the HTTPClient diff --git a/src/httpserver.h b/src/httpserver.h index be3a4c56db5..c6244939495 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -306,6 +306,17 @@ public: // Checked in the Sockman I/O loop to avoid locking m_send_mutex if there's nothing to send. std::atomic_bool m_send_ready{false}; + // Set to true when we receive request data and set to false once m_send_buffer is cleared. + // Checked during DisconnectClients(). All of these operations take place in the Sockman I/O loop. + bool m_prevent_disconnect{false}; + + // Client request to keep connection open after all requests have been responded to. + // Set by (potentially multiple) worker threads and checked in the Sockman I/O loop. + std::atomic_bool m_keep_alive{false}; + + // Flag this client for disconnection on next loop + bool m_disconnect{false}; + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) { m_origin = addr.ToStringAddrPort(); @@ -326,6 +337,9 @@ public: class HTTPServer : public SockMan { +private: + void CloseConnectionInternal(std::shared_ptr& client); + public: explicit HTTPServer(std::function)> func) : m_request_dispatcher(func) {}; @@ -340,6 +354,13 @@ public: std::shared_ptr GetClientById(NodeId node_id) const; + // Close underlying connections where flagged + void DisconnectClients(); + + // Flag used during shutdown to bypass keep-alive flag. + // Set by main thread and read by Sockman I/O thread + std::atomic_bool m_disconnect_all_clients{false}; + /** * Be notified when a new connection has been accepted. * @param[in] node_id Id of the newly accepted connection. @@ -374,14 +395,22 @@ public: * makes sense at the application level. * @param[in] node_id Node whose socket got EOF. */ - virtual void EventGotEOF(NodeId node_id) override {}; + virtual void EventGotEOF(NodeId node_id) override; /** * Called when we get an irrecoverable error trying to read from a socket. * @param[in] node_id Node whose socket got an error. * @param[in] errmsg Message describing the error. */ - virtual void EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) override {}; + virtual void EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) override; + + /** + * SockMan has completed send+recv for all nodes. + * Can be used to execute periodic tasks for all nodes, like disconnecting + * nodes due to higher level logic. + * The implementation in SockMan does nothing. + */ + virtual void EventIOLoopCompletedForAll() override; /** * Can be used to temporarily pause sends on a connection. diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 39e7fa5e693..5362a0806ec 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -386,6 +386,17 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests) } BOOST_CHECK_EQUAL(actual, expected); + // Wait up to one second for connection to be closed + attempts = 100; + while (attempts > 0) + { + if (server.m_no_clients) break; + + std::this_thread::sleep_for(10ms); + --attempts; + } + BOOST_REQUIRE(server.m_no_clients); + // Close server server.interruptNet(); // Wait for I/O loop to finish, after all sockets are closed From 0765a0c3516291dd3b54646f352cb00eaf5dd029 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Sat, 1 Mar 2025 13:51:41 -0500 Subject: [PATCH 33/42] Allow http workers to send data optimistically as an optimization --- src/httpserver.cpp | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 4a0ffdb98bd..c2e0384776f 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -1005,9 +1005,11 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r const std::string headers{res.StringifyHeaders()}; const auto headers_bytes{std::as_bytes(std::span(headers.begin(), headers.end()))}; + bool send_buffer_was_empty{false}; // Fill the send buffer with the complete serialized response headers + body { LOCK(m_client->m_send_mutex); + send_buffer_was_empty = m_client->m_send_buffer.empty(); m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), headers_bytes.begin(), headers_bytes.end()); // We've been using std::span up until now but it is finally time to copy @@ -1016,10 +1018,6 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r m_client->m_send_buffer.insert(m_client->m_send_buffer.end(), reply_body.begin(), reply_body.end()); } - // Inform Sockman I/O there is data that is ready to be sent to this client - // in the next loop iteration. - m_client->m_send_ready = true; - LogDebug( BCLog::HTTP, "HTTPResponse (status code: %d size: %lld) added to send buffer for client %s (id=%lld)\n", @@ -1027,6 +1025,18 @@ void HTTPRequest::WriteReply(HTTPStatusCode status, std::span r headers_bytes.size() + reply_body.size(), m_client->m_origin, m_client->m_node_id); + + // If the send buffer was empty before we wrote this reply, we can try an + // optimistic send akin to CConnman::PushMessage() in which we + // push the data directly out the socket to client right now, instead + // of waiting for the next iteration of the Sockman I/O loop. + if (send_buffer_was_empty) { + m_client->SendBytesFromBuffer(); + } else { + // Inform Sockman I/O there is data that is ready to be sent to this client + // in the next loop iteration. + m_client->m_send_ready = true; + } } bool HTTPClient::ReadRequest(std::unique_ptr& req) @@ -1092,6 +1102,9 @@ bool HTTPClient::SendBytesFromBuffer() m_disconnect = true; return false; } + } else { + m_send_ready = true; + m_prevent_disconnect = true; } } From 5b61d650344b124659bafd6d395257a8c5f7df20 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 11 Dec 2024 13:21:31 -0500 Subject: [PATCH 34/42] define HTTP request methods at module level outside of class This is a refactor to prepare for matching the API of HTTPRequest definitions in both namespaces http_bitcoin and http_libevent. In particular, to provide a consistent return type for GetRequestMethod() in both classes. --- src/httprpc.cpp | 2 +- src/httpserver.cpp | 26 +++++++++++++------------- src/httpserver.h | 18 +++++++++--------- src/test/fuzz/http_request.cpp | 4 ++-- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 159f97966e6..7a31b1bc41e 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -157,7 +157,7 @@ static bool RPCAuthorized(const std::string& strAuth, std::string& strAuthUserna static bool HTTPReq_JSONRPC(const std::any& context, HTTPRequest* req) { // JSONRPC handles only POST - if (req->GetRequestMethod() != HTTPRequest::POST) { + if (req->GetRequestMethod() != HTTPRequestMethod::POST) { req->WriteReply(HTTP_BAD_METHOD, "JSONRPC server handles only POST requests"); return false; } diff --git a/src/httpserver.cpp b/src/httpserver.cpp index c2e0384776f..bc1d2157df3 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -243,18 +243,18 @@ static bool InitHTTPAllowList() } /** HTTP request method as string - use for logging only */ -std::string RequestMethodString(HTTPRequest::RequestMethod m) +std::string RequestMethodString(HTTPRequestMethod m) { switch (m) { - case HTTPRequest::GET: + case HTTPRequestMethod::GET: return "GET"; - case HTTPRequest::POST: + case HTTPRequestMethod::POST: return "POST"; - case HTTPRequest::HEAD: + case HTTPRequestMethod::HEAD: return "HEAD"; - case HTTPRequest::PUT: + case HTTPRequestMethod::PUT: return "PUT"; - case HTTPRequest::UNKNOWN: + case HTTPRequestMethod::UNKNOWN: return "unknown"; } // no default case, so the compiler can warn about missing cases assert(false); @@ -297,7 +297,7 @@ static void http_request_cb(struct evhttp_request* req, void* arg) } // Early reject unknown HTTP methods - if (hreq->GetRequestMethod() == HTTPRequest::UNKNOWN) { + if (hreq->GetRequestMethod() == HTTPRequestMethod::UNKNOWN) { LogDebug(BCLog::HTTP, "HTTP request from %s rejected: Unknown HTTP request method\n", hreq->GetPeer().ToStringAddrPort()); hreq->WriteReply(HTTP_BAD_METHOD); @@ -710,19 +710,19 @@ std::string HTTPRequest::GetURI() const return evhttp_request_get_uri(req); } -HTTPRequest::RequestMethod HTTPRequest::GetRequestMethod() const +HTTPRequestMethod HTTPRequest::GetRequestMethod() const { switch (evhttp_request_get_command(req)) { case EVHTTP_REQ_GET: - return GET; + return HTTPRequestMethod::GET; case EVHTTP_REQ_POST: - return POST; + return HTTPRequestMethod::POST; case EVHTTP_REQ_HEAD: - return HEAD; + return HTTPRequestMethod::HEAD; case EVHTTP_REQ_PUT: - return PUT; + return HTTPRequestMethod::PUT; default: - return UNKNOWN; + return HTTPRequestMethod::UNKNOWN; } } diff --git a/src/httpserver.h b/src/httpserver.h index c6244939495..a5a9ba3bca1 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -37,6 +37,14 @@ struct evhttp_request; struct event_base; class CService; +enum HTTPRequestMethod { + UNKNOWN, + GET, + POST, + HEAD, + PUT +}; + namespace http_libevent { class HTTPRequest; @@ -88,14 +96,6 @@ public: explicit HTTPRequest(struct evhttp_request* req, const util::SignalInterrupt& interrupt, bool replySent = false); ~HTTPRequest(); - enum RequestMethod { - UNKNOWN, - GET, - POST, - HEAD, - PUT - }; - /** Get requested URI. */ std::string GetURI() const; @@ -106,7 +106,7 @@ public: /** Get request method. */ - RequestMethod GetRequestMethod() const; + HTTPRequestMethod GetRequestMethod() const; /** Get the query parameter value from request uri for a specified key, or std::nullopt if the * key is not found. diff --git a/src/test/fuzz/http_request.cpp b/src/test/fuzz/http_request.cpp index 712c020eeac..331f8f6c27c 100644 --- a/src/test/fuzz/http_request.cpp +++ b/src/test/fuzz/http_request.cpp @@ -25,7 +25,7 @@ using http_libevent::HTTPRequest; extern "C" int evhttp_parse_firstline_(struct evhttp_request*, struct evbuffer*); extern "C" int evhttp_parse_headers_(struct evhttp_request*, struct evbuffer*); -std::string RequestMethodString(HTTPRequest::RequestMethod m); +std::string RequestMethodString(HTTPRequestMethod m); FUZZ_TARGET(http_request) { @@ -52,7 +52,7 @@ FUZZ_TARGET(http_request) util::SignalInterrupt interrupt; HTTPRequest http_request{evreq, interrupt, true}; - const HTTPRequest::RequestMethod request_method = http_request.GetRequestMethod(); + const HTTPRequestMethod request_method = http_request.GetRequestMethod(); (void)RequestMethodString(request_method); (void)http_request.GetURI(); (void)http_request.GetHeader("Host"); From 58fe646d3c9fcb5315321a7610029cd4d600d147 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 12 Dec 2024 11:44:52 -0500 Subject: [PATCH 35/42] Add helper methods to HTTPRequest to match original API These methods are called by http_request_cb() and are present in the original http_libevent::HTTPRequest. --- src/httpserver.cpp | 64 +++++++++++++++++++++++++++++++++++ src/httpserver.h | 12 +++++++ src/test/httpserver_tests.cpp | 52 +++++++++++++++++++++++++--- 3 files changed, 124 insertions(+), 4 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index bc1d2157df3..a54ca1325a6 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -941,6 +942,69 @@ bool HTTPRequest::LoadBody(LineReader& reader) } } +CService HTTPRequest::GetPeer() const +{ + return m_client->m_addr; +} + +HTTPRequestMethod HTTPRequest::GetRequestMethod() const +{ + if (m_method == "GET") return HTTPRequestMethod::GET; + if (m_method == "POST") return HTTPRequestMethod::POST; + if (m_method == "HEAD") return HTTPRequestMethod::HEAD; + if (m_method == "PUT") return HTTPRequestMethod::PUT; + return HTTPRequestMethod::UNKNOWN; +} + +std::optional HTTPRequest::GetQueryParameter(const std::string& key) const +{ + return GetQueryParameterFromUri(GetURI(), key); +} + +// See libevent http.c evhttp_parse_query_impl() +// and https://www.rfc-editor.org/rfc/rfc3986#section-3.4 +std::optional GetQueryParameterFromUri(const std::string& uri, const std::string& key) +{ + // Handle %XX encoding + std::string decoded_uri{UrlDecode(uri)}; + + // find query in URI + size_t start = decoded_uri.find("?"); + if (start == std::string::npos) return std::nullopt; + size_t end = decoded_uri.find("#", start); + if (end == std::string::npos) { + end = decoded_uri.length(); + } + const std::string query{decoded_uri.substr(start + 1, end - start - 1)}; + // find requested parameter in query + const std::vector params{SplitString(query, "&")}; + for (const std::string& param : params) { + size_t delim = param.find("="); + if (key == param.substr(0, delim)) { + if (delim == std::string::npos) { + return ""; + } else { + return param.substr(delim + 1); + } + } + } + return std::nullopt; +} + +std::pair HTTPRequest::GetHeader(const std::string& hdr) const +{ + std::optional found{m_headers.Find(hdr)}; + if (found.has_value()) { + return std::make_pair(true, found.value()); + } else + return std::make_pair(false, ""); +} + +void HTTPRequest::WriteHeader(const std::string& hdr, const std::string& value) +{ + m_response_headers.Write(hdr, value); +} + void HTTPRequest::WriteReply(HTTPStatusCode status, std::span reply_body) { HTTPResponse res; diff --git a/src/httpserver.h b/src/httpserver.h index a5a9ba3bca1..2a5c124eb6b 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -267,6 +267,16 @@ public: bool LoadHeaders(LineReader& reader); bool LoadBody(LineReader& reader); + // These methods reimplement the API from http_libevent::HTTPRequest + // for downstream JSONRPC and REST modules. + std::string GetURI() const {return m_target;}; + CService GetPeer() const; + HTTPRequestMethod GetRequestMethod() const; + std::optional GetQueryParameter(const std::string& key) const; + std::pair GetHeader(const std::string& hdr) const; + std::string ReadBody() const {return m_body;}; + void WriteHeader(const std::string& hdr, const std::string& value); + // Response headers may be set in advance before response body is known HTTPHeaders m_response_headers; void WriteReply(HTTPStatusCode status, std::span reply_body = {}); @@ -277,6 +287,8 @@ public: } }; +std::optional GetQueryParameterFromUri(const std::string& uri, const std::string& key); + class HTTPServer; class HTTPClient diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 5362a0806ec..0d58fe911a3 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -50,7 +50,37 @@ BOOST_FIXTURE_TEST_SUITE(httpserver_tests, HTTPTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) { - using http_libevent::GetQueryParameterFromUri; + // The legacy code that relied on libevent couldn't handle an invalid URI encoding. + // The new code is more tolerant and so we expect a difference in behavior. + // Re: libevent evhttp_uri_parse() see: + // "bugfix: rest: avoid segfault for invalid URI" https://github.com/bitcoin/bitcoin/pull/27468 + // "httpserver, rest: improving URI validation" https://github.com/bitcoin/bitcoin/pull/27253 + // Re: More tolerant URI decoding see: + // "refactor: Use our own implementation of urlDecode" https://github.com/bitcoin/bitcoin/pull/29904 + + std::string uri {}; + // This is an invalid URI because it contains a % that is not followed by two hex digits + uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%"; + // Old behavior: URI with invalid characters (%) raises a runtime error regardless of which query parameter is queried + BOOST_CHECK_EXCEPTION(http_libevent::GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters")); + // New behavior: Tolerate as much as we can even + BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1"); + BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p2").value(), "v2%"); + + // This is a valid URI because the %XX encoding is correct: `?p1=v1&p2=100%` + uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25"; + // Old behavior: libevent does not decode the URI before parsing, so it does not detect or return the query + // (libevent will parse the entire argument string as the uri path) + BOOST_CHECK(!http_libevent::GetQueryParameterFromUri(uri.c_str(), "p1").has_value()); + BOOST_CHECK(!http_libevent::GetQueryParameterFromUri(uri.c_str(), "p2").has_value()); + // New behavior: Decode before parsing the URI so reserved characters like ? & = are interpreted correctly + BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1"); + BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p2").value(), "100%"); +} + +// Ensure new behavior matches old behavior +template +void test_query_parameters(func GetQueryParameterFromUri) { std::string uri {}; // No parameters @@ -75,9 +105,20 @@ BOOST_AUTO_TEST_CASE(test_query_parameters) uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2"; BOOST_CHECK(!GetQueryParameterFromUri(uri.c_str(), "p1").has_value()); - // URI with invalid characters (%) raises a runtime error regardless of which query parameter is queried - uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%"; - BOOST_CHECK_EXCEPTION(GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters")); + // Multiple parameters, some characters encoded + uri = "/rest/endpoint/someresource.json?p1=v1%20&p2=100%25"; + BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1 "); + BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p2").value(), "100%"); +} + +BOOST_AUTO_TEST_CASE(test_query_parameters_libevent) +{ + test_query_parameters(http_libevent::GetQueryParameterFromUri); +} + +BOOST_AUTO_TEST_CASE(test_query_parameters_bitcoin) +{ + test_query_parameters(http_bitcoin::GetQueryParameterFromUri); } BOOST_AUTO_TEST_CASE(http_headers_tests) @@ -167,7 +208,9 @@ BOOST_AUTO_TEST_CASE(http_request_tests) BOOST_CHECK(req.LoadHeaders(reader)); BOOST_CHECK(req.LoadBody(reader)); BOOST_CHECK_EQUAL(req.m_method, "POST"); + BOOST_CHECK_EQUAL(req.GetRequestMethod(), HTTPRequestMethod::POST); BOOST_CHECK_EQUAL(req.m_target, "/"); + BOOST_CHECK_EQUAL(req.GetURI(), "/"); BOOST_CHECK_EQUAL(req.m_version_major, 1); BOOST_CHECK_EQUAL(req.m_version_minor, 1); BOOST_CHECK_EQUAL(req.m_headers.Find("Host").value(), "127.0.0.1"); @@ -355,6 +398,7 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests) // Check the received request BOOST_CHECK_EQUAL(requests.front()->m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n"); + BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789"); // Respond to request requests.front()->WriteReply(HTTP_OK, "874140\n"); From e56afcccbfd075b5221f0d41517aed72c84d7f52 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 12 Dec 2024 15:55:25 -0500 Subject: [PATCH 36/42] refactor: split http_request_cb into libevent callback and dispatch The original function is passed to libevent as a callback when HTTP requests are received and processed. It wrapped the libevent request object in a http_libevent::HTTPRequest and then handed that off to bitcoin for basic checks and finally dispatch to worker threads. In this commit we split the function after the http_libevent::HTTPRequest is created, and pass that object to a new function that maintains the logic of checking and dispatching. This will be the merge point for http_libevent and http_bitcoin, where HTTPRequest objects from either namespace have the same downstream lifecycle. --- src/httpserver.cpp | 58 +++++++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index a54ca1325a6..c15199d2350 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -261,34 +261,8 @@ std::string RequestMethodString(HTTPRequestMethod m) assert(false); } -/** HTTP request callback */ -static void http_request_cb(struct evhttp_request* req, void* arg) +static void MaybeDispatchRequestToWorker(std::unique_ptr hreq) { - evhttp_connection* conn{evhttp_request_get_connection(req)}; - // Track active requests - { - g_requests.AddRequest(req); - evhttp_request_set_on_complete_cb(req, [](struct evhttp_request* req, void*) { - g_requests.RemoveRequest(req); - }, nullptr); - evhttp_connection_set_closecb(conn, [](evhttp_connection* conn, void* arg) { - g_requests.RemoveConnection(conn); - }, nullptr); - } - - // Disable reading to work around a libevent bug, fixed in 2.1.9 - // See https://github.com/libevent/libevent/commit/5ff8eb26371c4dc56f384b2de35bea2d87814779 - // and https://github.com/bitcoin/bitcoin/pull/11593. - if (event_get_version_number() >= 0x02010600 && event_get_version_number() < 0x02010900) { - if (conn) { - bufferevent* bev = evhttp_connection_get_bufferevent(conn); - if (bev) { - bufferevent_disable(bev, EV_READ); - } - } - } - auto hreq{std::make_unique(req, *static_cast(arg))}; - // Early address-based allow check if (!ClientAllowed(hreq->GetPeer())) { LogDebug(BCLog::HTTP, "HTTP request from %s rejected: Client network is not allowed RPC access\n", @@ -341,6 +315,36 @@ static void http_request_cb(struct evhttp_request* req, void* arg) } } +/** HTTP request callback */ +static void http_request_cb(struct evhttp_request* req, void* arg) +{ + evhttp_connection* conn{evhttp_request_get_connection(req)}; + // Track active requests + { + g_requests.AddRequest(req); + evhttp_request_set_on_complete_cb(req, [](struct evhttp_request* req, void*) { + g_requests.RemoveRequest(req); + }, nullptr); + evhttp_connection_set_closecb(conn, [](evhttp_connection* conn, void* arg) { + g_requests.RemoveConnection(conn); + }, nullptr); + } + + // Disable reading to work around a libevent bug, fixed in 2.1.9 + // See https://github.com/libevent/libevent/commit/5ff8eb26371c4dc56f384b2de35bea2d87814779 + // and https://github.com/bitcoin/bitcoin/pull/11593. + if (event_get_version_number() >= 0x02010600 && event_get_version_number() < 0x02010900) { + if (conn) { + bufferevent* bev = evhttp_connection_get_bufferevent(conn); + if (bev) { + bufferevent_disable(bev, EV_READ); + } + } + } + auto hreq{std::make_unique(req, *static_cast(arg))}; + MaybeDispatchRequestToWorker(std::move(hreq)); +} + /** Callback to reject HTTP requests after shutdown. */ static void http_reject_request_cb(struct evhttp_request* req, void*) { From 24fba3bf62522d7ab821443bf032a70c2de9ffb1 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 15 Jan 2025 14:26:36 -0500 Subject: [PATCH 37/42] refactor: split HTTPBindAddresses into config parse and libevent setup The original function was already naturally split into two chunks: First, we parse and validate the users' RPC configuration for IPs and ports. Next we bind libevent's http server to the appropriate endpoints. This commit splits these chunks into two separate functions, leaving the argument parsing in the common space of the module and moving the libevent-specific binding into the http_libevent namespace. A future commit will implement http_bitcoin::HTTPBindAddresses to bind the validate list of endpoints by the new HTTP server. --- src/httpserver.cpp | 54 +++++++++++++++++++++++++--------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index c15199d2350..76aefc5ab75 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -362,8 +362,7 @@ static void ThreadHTTP(struct event_base* base) LogDebug(BCLog::HTTP, "Exited http event loop\n"); } -/** Bind HTTP server to specified addresses */ -static bool HTTPBindAddresses(struct evhttp* http) +static std::vector> GetBindAddresses() { uint16_t http_port{static_cast(gArgs.GetIntArg("-rpcport", BaseParams().RPCPort()))}; std::vector> endpoints; @@ -388,33 +387,12 @@ static bool HTTPBindAddresses(struct evhttp* http) std::string host; if (!SplitHostPort(strRPCBind, port, host)) { LogError("%s\n", InvalidPortErrMsg("-rpcbind", strRPCBind).original); - return false; + return {}; // empty } endpoints.emplace_back(host, port); } } - - // Bind addresses - for (std::vector >::iterator i = endpoints.begin(); i != endpoints.end(); ++i) { - LogPrintf("Binding RPC on address %s port %i\n", i->first, i->second); - evhttp_bound_socket *bind_handle = evhttp_bind_socket_with_handle(http, i->first.empty() ? nullptr : i->first.c_str(), i->second); - if (bind_handle) { - const std::optional addr{LookupHost(i->first, false)}; - if (i->first.empty() || (addr.has_value() && addr->IsBindAny())) { - LogPrintf("WARNING: the RPC server is not safe to expose to untrusted networks such as the public internet\n"); - } - // Set the no-delay option (disable Nagle's algorithm) on the TCP socket. - evutil_socket_t fd = evhttp_bound_socket_get_fd(bind_handle); - int one = 1; - if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (sockopt_arg_type)&one, sizeof(one)) == SOCKET_ERROR) { - LogInfo("WARNING: Unable to set TCP_NODELAY on RPC server socket, continuing anyway\n"); - } - boundSockets.push_back(bind_handle); - } else { - LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, i->second); - } - } - return !boundSockets.empty(); + return endpoints; } /** Simple wrapper to set thread name and run work queue */ @@ -446,6 +424,32 @@ static void libevent_log_cb(int severity, const char *msg) } namespace http_libevent { +/** Bind HTTP server to specified addresses */ +static bool HTTPBindAddresses(struct evhttp* http) +{ + std::vector> endpoints{GetBindAddresses()}; + for (std::vector >::iterator i = endpoints.begin(); i != endpoints.end(); ++i) { + LogPrintf("Binding RPC on address %s port %i\n", i->first, i->second); + evhttp_bound_socket *bind_handle = evhttp_bind_socket_with_handle(http, i->first.empty() ? nullptr : i->first.c_str(), i->second); + if (bind_handle) { + const std::optional addr{LookupHost(i->first, false)}; + if (i->first.empty() || (addr.has_value() && addr->IsBindAny())) { + LogPrintf("WARNING: the RPC server is not safe to expose to untrusted networks such as the public internet\n"); + } + // Set the no-delay option (disable Nagle's algorithm) on the TCP socket. + evutil_socket_t fd = evhttp_bound_socket_get_fd(bind_handle); + int one = 1; + if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (sockopt_arg_type)&one, sizeof(one)) == SOCKET_ERROR) { + LogInfo("WARNING: Unable to set TCP_NODELAY on RPC server socket, continuing anyway\n"); + } + boundSockets.push_back(bind_handle); + } else { + LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, i->second); + } + } + return !boundSockets.empty(); +} + bool InitHTTPServer(const util::SignalInterrupt& interrupt) { if (!InitHTTPAllowList()) From a4e7b776e63aadeacfdaa63e0aec4cbc44dcd365 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 15 Jan 2025 15:17:36 -0500 Subject: [PATCH 38/42] http: implement new server control methods to match legacy API --- src/httpserver.cpp | 103 +++++++++++++++++++++++++++++++++++++++++++++ src/httpserver.h | 14 ++++++ 2 files changed, 117 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 76aefc5ab75..8a5a1912bee 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -144,6 +144,7 @@ struct HTTPPathHandler static struct event_base* eventBase = nullptr; //! HTTP server static struct evhttp* eventHTTP = nullptr; +static std::unique_ptr g_http_server{nullptr}; //! List of subnets to allow RPC connections from static std::vector rpc_allow_subnets; //! Work queue for handling longer requests off the event loop thread @@ -315,6 +316,12 @@ static void MaybeDispatchRequestToWorker(std::unique_ptr hreq) } } +static void RejectAllRequests(std::unique_ptr hreq) +{ + LogDebug(BCLog::HTTP, "Rejecting request while shutting down\n"); + hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE); +} + /** HTTP request callback */ static void http_request_cb(struct evhttp_request* req, void* arg) { @@ -1346,4 +1353,100 @@ std::shared_ptr HTTPServer::GetClientById(NodeId node_id) const } return nullptr; } + +bool InitHTTPServer(const util::SignalInterrupt& interrupt) +{ + if (!InitHTTPAllowList()) + return false; + + // Create HTTPServer, using a dummy request handler just for this commit + g_http_server = std::make_unique([&](std::unique_ptr req){}); + + // Bind HTTP server to specified addresses + std::vector> endpoints{GetBindAddresses()}; + bool bind_success{false}; + for (std::vector >::iterator i = endpoints.begin(); i != endpoints.end(); ++i) { + LogPrintf("Binding RPC on address %s port %i\n", i->first, i->second); + const std::optional addr{Lookup(i->first, i->second, false)}; + if (addr) { + if (addr->IsBindAny()) { + LogPrintf("WARNING: the RPC server is not safe to expose to untrusted networks such as the public internet\n"); + } + bilingual_str strError; + if (!g_http_server->BindAndStartListening(addr.value(), strError)) { + LogPrintf("Binding RPC on address %s failed: %s\n", addr->ToStringAddrPort(), strError.original); + } else { + bind_success = true; + } + } else { + LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, i->second); + } + } + + if (!bind_success) { + LogPrintf("Unable to bind any endpoint for RPC server\n"); + return false; + } + + LogDebug(BCLog::HTTP, "Initialized HTTP server\n"); + int workQueueDepth = std::max((long)gArgs.GetIntArg("-rpcworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L); + LogDebug(BCLog::HTTP, "creating work queue of depth %d\n", workQueueDepth); + + g_work_queue = std::make_unique>(workQueueDepth); + + return true; +} + +static std::vector g_thread_http_workers; + +void StartHTTPServer() +{ + int rpcThreads = std::max((long)gArgs.GetIntArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L); + LogInfo("Starting HTTP server with %d worker threads\n", rpcThreads); + SockMan::Options sockman_options; + sockman_options.socket_handler_thread_name = "http"; + g_http_server->StartSocketsThreads(sockman_options); + + for (int i = 0; i < rpcThreads; i++) { + g_thread_http_workers.emplace_back(HTTPWorkQueueRun, g_work_queue.get(), i); + } +} + +void InterruptHTTPServer() +{ + LogDebug(BCLog::HTTP, "Interrupting HTTP server\n"); + if (g_http_server) { + // Reject all new requests + g_http_server->m_request_dispatcher = RejectAllRequests; + } + if (g_work_queue) { + // Stop workers, killing requests we haven't processed or responded to yet + g_work_queue->Interrupt(); + } +} + +void StopHTTPServer() +{ + LogDebug(BCLog::HTTP, "Stopping HTTP server\n"); + if (g_work_queue) { + LogDebug(BCLog::HTTP, "Waiting for HTTP worker threads to exit\n"); + for (auto& thread : g_thread_http_workers) { + thread.join(); + } + g_thread_http_workers.clear(); + } + if (g_http_server) { + // Disconnect clients as their remaining responses are flushed + g_http_server->m_disconnect_all_clients = true; + // Wait for all disconnections + while (!g_http_server->m_no_clients) { + std::this_thread::sleep_for(std::chrono::milliseconds{50}); + } + // Break sockman I/O loop: stop accepting connections, sending and receiving data + g_http_server->interruptNet(); + // Wait for sockman threads to exit + g_http_server->JoinSocketsThreads(); + } + LogDebug(BCLog::HTTP, "Stopped HTTP server\n"); +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index 2a5c124eb6b..39abb12e5fb 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -440,6 +440,20 @@ public: */ virtual bool ShouldTryToRecv(NodeId node_id) const override; }; + +/** Initialize HTTP server. + * Call this before RegisterHTTPHandler or EventBase(). + */ +bool InitHTTPServer(const util::SignalInterrupt& interrupt); +/** Start HTTP server. + * This is separate from InitHTTPServer to give users race-condition-free time + * to register their handlers between InitHTTPServer and StartHTTPServer. + */ +void StartHTTPServer(); +/** Interrupt HTTP server threads */ +void InterruptHTTPServer(); +/** Stop HTTP server */ +void StopHTTPServer(); } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H From f82c7173275736fe47d958b1a926361da862591b Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 10 Mar 2025 13:30:52 -0400 Subject: [PATCH 39/42] http: disconnect after idle timeout (-rpcservertimeout) --- src/httpserver.cpp | 13 ++++++++++++- src/httpserver.h | 7 +++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 8a5a1912bee..87c4369f554 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include @@ -1201,8 +1202,11 @@ void HTTPServer::CloseConnectionInternal(std::shared_ptr& client) void HTTPServer::DisconnectClients() { + const auto now{Now()}; for (auto it = m_connected_clients.begin(); it != m_connected_clients.end();) { - if ((it->second->m_disconnect || m_disconnect_all_clients) && !it->second->m_prevent_disconnect) { + bool timeout{now - it->second->m_idle_since > m_rpcservertimeout}; + if (((it->second->m_disconnect || m_disconnect_all_clients) && !it->second->m_prevent_disconnect) + || timeout) { CloseConnectionInternal(it->second); it = m_connected_clients.erase(it); } else { @@ -1219,6 +1223,8 @@ bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, auto client = std::make_shared(node_id, them); // Point back to the server client->m_server = this; + // Set timeout + client->m_idle_since = Now(); LogDebug(BCLog::HTTP, "HTTP Connection accepted from %s (id=%d)\n", client->m_origin, client->m_node_id); m_connected_clients.emplace(client->m_node_id, std::move(client)); m_no_clients = false; @@ -1250,6 +1256,9 @@ void HTTPServer::EventGotData(NodeId node_id, std::span data) return; } + // Reset idle timeout + client->m_idle_since = Now(); + // Prevent disconnect until all requests are completely handled. client->m_prevent_disconnect = true; @@ -1362,6 +1371,8 @@ bool InitHTTPServer(const util::SignalInterrupt& interrupt) // Create HTTPServer, using a dummy request handler just for this commit g_http_server = std::make_unique([&](std::unique_ptr req){}); + g_http_server->m_rpcservertimeout = std::chrono::seconds(gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)); + // Bind HTTP server to specified addresses std::vector> endpoints{GetBindAddresses()}; bool bind_success{false}; diff --git a/src/httpserver.h b/src/httpserver.h index 39abb12e5fb..cff1f287a7a 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -15,6 +15,7 @@ #include #include #include +#include namespace util { class SignalInterrupt; @@ -329,6 +330,9 @@ public: // Flag this client for disconnection on next loop bool m_disconnect{false}; + // Timestamp of last receive activity, used for -rpcservertimeout + SteadySeconds m_idle_since; + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) { m_origin = addr.ToStringAddrPort(); @@ -373,6 +377,9 @@ public: // Set by main thread and read by Sockman I/O thread std::atomic_bool m_disconnect_all_clients{false}; + // Idle timeout after which clients are disconnected + std::chrono::seconds m_rpcservertimeout; + /** * Be notified when a new connection has been accepted. * @param[in] node_id Id of the newly accepted connection. From abde6936e12cce360a91c7c735315f5e7ea725ad Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Fri, 17 Jan 2025 10:04:59 -0500 Subject: [PATCH 40/42] use CScheduler for HTTPRPCTimer This removes the dependency on libevent for scheduled events, like re-locking a wallet some time after decryption. --- src/httprpc.cpp | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 7a31b1bc41e..5d226cbaf8d 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -27,6 +29,7 @@ #include using http_libevent::HTTPRequest; +using node::NodeContext; using util::SplitString; using util::TrimStringView; @@ -39,22 +42,16 @@ static const char* WWW_AUTH_HEADER_DATA = "Basic realm=\"jsonrpc\""; class HTTPRPCTimer : public RPCTimerBase { public: - HTTPRPCTimer(struct event_base* eventBase, std::function& func, int64_t millis) : - ev(eventBase, false, func) + HTTPRPCTimer(NodeContext* context, std::function& func, int64_t millis) { - struct timeval tv; - tv.tv_sec = millis/1000; - tv.tv_usec = (millis%1000)*1000; - ev.trigger(&tv); + context->scheduler->scheduleFromNow(func, std::chrono::milliseconds(millis)); } -private: - HTTPEvent ev; }; class HTTPRPCTimerInterface : public RPCTimerInterface { public: - explicit HTTPRPCTimerInterface(struct event_base* _base) : base(_base) + explicit HTTPRPCTimerInterface(const std::any& context) : m_context(std::any_cast(context)) { } const char* Name() override @@ -63,10 +60,10 @@ public: } RPCTimerBase* NewTimer(std::function& func, int64_t millis) override { - return new HTTPRPCTimer(base, func, millis); + return new HTTPRPCTimer(m_context, func, millis); } private: - struct event_base* base; + NodeContext* m_context; }; @@ -371,9 +368,7 @@ bool StartHTTPRPC(const std::any& context) if (g_wallet_init_interface.HasWalletSupport()) { RegisterHTTPHandler("/wallet/", false, handle_rpc); } - struct event_base* eventBase = EventBase(); - assert(eventBase); - httpRPCTimerInterface = std::make_unique(eventBase); + httpRPCTimerInterface = std::make_unique(context); RPCSetTimerInterface(httpRPCTimerInterface.get()); return true; } From cbef75d47ba9cd71b92946a9f13efc100a6757ee Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 15 Jan 2025 15:44:16 -0500 Subject: [PATCH 41/42] http: switch servers from libevent to bitcoin --- src/httprpc.cpp | 4 ++-- src/httpserver.cpp | 15 ++++++++------- src/httpserver.h | 30 +++++++++++++++++++----------- src/init.cpp | 8 ++++---- src/rest.cpp | 2 +- src/test/fuzz/http_request.cpp | 31 ++----------------------------- test/functional/interface_rest.py | 8 +++++--- 7 files changed, 41 insertions(+), 57 deletions(-) diff --git a/src/httprpc.cpp b/src/httprpc.cpp index 5d226cbaf8d..4cb901f3763 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -28,8 +28,8 @@ #include #include -using http_libevent::HTTPRequest; using node::NodeContext; +using http_bitcoin::HTTPRequest; using util::SplitString; using util::TrimStringView; @@ -83,7 +83,7 @@ static void JSONErrorReply(HTTPRequest* req, UniValue objError, const JSONRPCReq Assume(jreq.m_json_version != JSONRPCVersion::V2); // Send error reply from json-rpc error object - int nStatus = HTTP_INTERNAL_SERVER_ERROR; + HTTPStatusCode nStatus = HTTP_INTERNAL_SERVER_ERROR; int code = objError.find_value("code").getInt(); if (code == RPC_INVALID_REQUEST) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 87c4369f554..d9d4fef9578 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -46,7 +46,7 @@ #include using common::InvalidPortErrMsg; -using http_libevent::HTTPRequest; +using http_bitcoin::HTTPRequest; /** Maximum size of http request (request line + headers) */ static const size_t MAX_HEADERS_SIZE = 8192; @@ -281,9 +281,6 @@ static void MaybeDispatchRequestToWorker(std::unique_ptr hreq) return; } - LogDebug(BCLog::HTTP, "Received a %s request for %s from %s\n", - RequestMethodString(hreq->GetRequestMethod()), SanitizeString(hreq->GetURI(), SAFE_CHARS_URI).substr(0, 100), hreq->GetPeer().ToStringAddrPort()); - // Find registered handler for prefix std::string strURI = hreq->GetURI(); std::string path; @@ -350,7 +347,11 @@ static void http_request_cb(struct evhttp_request* req, void* arg) } } auto hreq{std::make_unique(req, *static_cast(arg))}; - MaybeDispatchRequestToWorker(std::move(hreq)); + + // Disabled now that http_libevent is deprecated, or code won't compile. + // This line is currently unreachable and will be cleaned up in a future commit. + // MaybeDispatchRequestToWorker(std::move(hreq)); + Assume(false); } /** Callback to reject HTTP requests after shutdown. */ @@ -1368,8 +1369,8 @@ bool InitHTTPServer(const util::SignalInterrupt& interrupt) if (!InitHTTPAllowList()) return false; - // Create HTTPServer, using a dummy request handler just for this commit - g_http_server = std::make_unique([&](std::unique_ptr req){}); + // Create HTTPServer + g_http_server = std::make_unique(MaybeDispatchRequestToWorker); g_http_server->m_rpcservertimeout = std::chrono::seconds(gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)); diff --git a/src/httpserver.h b/src/httpserver.h index cff1f287a7a..876bb1046c8 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -46,6 +46,8 @@ enum HTTPRequestMethod { PUT }; +namespace http_bitcoin {}; + namespace http_libevent { class HTTPRequest; @@ -67,16 +69,6 @@ void StopHTTPServer(); void UpdateHTTPServerLogging(bool enable); } // namespace http_libevent -/** Handler for requests to a certain HTTP path */ -typedef std::function HTTPRequestHandler; -/** Register handler for prefix. - * If multiple handlers match a prefix, the first-registered one will - * be invoked. - */ -void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler); -/** Unregister handler for prefix */ -void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch); - /** Return evhttp event base. This can be used by submodules to * queue timers or custom events. */ @@ -281,11 +273,17 @@ public: // Response headers may be set in advance before response body is known HTTPHeaders m_response_headers; void WriteReply(HTTPStatusCode status, std::span reply_body = {}); - void WriteReply(HTTPStatusCode status, const char* reply_body) { + void WriteReply(HTTPStatusCode status, const char* reply_body) + { auto reply_body_view = std::string_view(reply_body); std::span byte_span(reinterpret_cast(reply_body_view.data()), reply_body_view.size()); WriteReply(status, byte_span); } + void WriteReply(HTTPStatusCode status, const std::string& reply_body) + { + std::span byte_span{reinterpret_cast(reply_body.data()), reply_body.size()}; + WriteReply(status, byte_span); + } }; std::optional GetQueryParameterFromUri(const std::string& uri, const std::string& key); @@ -463,4 +461,14 @@ void InterruptHTTPServer(); void StopHTTPServer(); } // namespace http_bitcoin +/** Handler for requests to a certain HTTP path */ +typedef std::function HTTPRequestHandler; +/** Register handler for prefix. + * If multiple handlers match a prefix, the first-registered one will + * be invoked. + */ +void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler); +/** Unregister handler for prefix */ +void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch); + #endif // BITCOIN_HTTPSERVER_H diff --git a/src/init.cpp b/src/init.cpp index 5ecb3e61c6c..15ee418e22b 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -121,10 +121,10 @@ using common::AmountErrMsg; using common::InvalidPortErrMsg; using common::ResolveErrMsg; -using http_libevent::InitHTTPServer; -using http_libevent::InterruptHTTPServer; -using http_libevent::StartHTTPServer; -using http_libevent::StopHTTPServer; +using http_bitcoin::InitHTTPServer; +using http_bitcoin::InterruptHTTPServer; +using http_bitcoin::StartHTTPServer; +using http_bitcoin::StopHTTPServer; using node::ApplyArgsManOptions; using node::BlockManager; using node::CalculateCacheSizes; diff --git a/src/rest.cpp b/src/rest.cpp index 1b2e1e14e18..2c22d9a6fd1 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -37,7 +37,7 @@ #include -using http_libevent::HTTPRequest; +using http_bitcoin::HTTPRequest; using node::GetTransaction; using node::NodeContext; using util::SplitString; diff --git a/src/test/fuzz/http_request.cpp b/src/test/fuzz/http_request.cpp index 331f8f6c27c..eb97a57ff2d 100644 --- a/src/test/fuzz/http_request.cpp +++ b/src/test/fuzz/http_request.cpp @@ -10,17 +10,12 @@ #include #include -#include -#include -#include -#include - #include #include #include #include -using http_libevent::HTTPRequest; +using http_bitcoin::HTTPRequest; extern "C" int evhttp_parse_firstline_(struct evhttp_request*, struct evbuffer*); extern "C" int evhttp_parse_headers_(struct evhttp_request*, struct evbuffer*); @@ -30,28 +25,9 @@ std::string RequestMethodString(HTTPRequestMethod m); FUZZ_TARGET(http_request) { FuzzedDataProvider fuzzed_data_provider{buffer.data(), buffer.size()}; - evhttp_request* evreq = evhttp_request_new(nullptr, nullptr); - assert(evreq != nullptr); - evreq->kind = EVHTTP_REQUEST; - evbuffer* evbuf = evbuffer_new(); - assert(evbuf != nullptr); - const std::vector http_buffer = ConsumeRandomLengthByteVector(fuzzed_data_provider, 4096); - evbuffer_add(evbuf, http_buffer.data(), http_buffer.size()); - // Avoid constructing requests that will be interpreted by libevent as PROXY requests to avoid triggering - // a nullptr dereference. The dereference (req->evcon->http_server) takes place in evhttp_parse_request_line - // and is a consequence of our hacky but necessary use of the internal function evhttp_parse_firstline_ in - // this fuzzing harness. The workaround is not aesthetically pleasing, but it successfully avoids the troublesome - // code path. " http:// HTTP/1.1\n" was a crashing input prior to this workaround. - const std::string http_buffer_str = ToLower(std::string{http_buffer.begin(), http_buffer.end()}); - if (http_buffer_str.find(" http://") != std::string::npos || http_buffer_str.find(" https://") != std::string::npos || - evhttp_parse_firstline_(evreq, evbuf) != 1 || evhttp_parse_headers_(evreq, evbuf) != 1) { - evbuffer_free(evbuf); - evhttp_request_free(evreq); - return; - } util::SignalInterrupt interrupt; - HTTPRequest http_request{evreq, interrupt, true}; + HTTPRequest http_request; const HTTPRequestMethod request_method = http_request.GetRequestMethod(); (void)RequestMethodString(request_method); (void)http_request.GetURI(); @@ -64,7 +40,4 @@ FUZZ_TARGET(http_request) assert(body.empty()); const CService service = http_request.GetPeer(); assert(service.ToStringAddrPort() == "[::]:0"); - - evbuffer_free(evbuf); - evhttp_request_free(evreq); } diff --git a/test/functional/interface_rest.py b/test/functional/interface_rest.py index 0e294696b0e..5458f70a6f1 100755 --- a/test/functional/interface_rest.py +++ b/test/functional/interface_rest.py @@ -282,10 +282,12 @@ class RESTTest (BitcoinTestFramework): assert_equal(len(json_obj), 1) # ensure that there is one header in the json response assert_equal(json_obj[0]['hash'], bb_hash) # request/response hash should be the same - # Check invalid uri (% symbol at the end of the request) - for invalid_uri in [f"/headers/{bb_hash}%", f"/blockfilterheaders/basic/{bb_hash}%", "/mempool/contents.json?%"]: + # Check tolerance for invalid URI (% symbol at the end of the request) + for invalid_uri in [f"/headers/{bb_hash}%", f"/blockfilterheaders/basic/{bb_hash}%"]: resp = self.test_rest_request(invalid_uri, ret_type=RetType.OBJ, status=400) - assert_equal(resp.read().decode('utf-8').rstrip(), "URI parsing failed, it likely contained RFC 3986 invalid characters") + assert_equal(resp.read().decode('utf-8').rstrip(), f"Invalid hash: {bb_hash}%") + resp = self.test_rest_request("/mempool/contents.json?%", ret_type=RetType.OBJ, status=200) + assert_equal(resp.read().decode('utf-8').rstrip(), "{}") # Compare with normal RPC block response rpc_block_json = self.nodes[0].getblock(bb_hash) From 93450294d88261ab82dc845971ae22b89bc2cf62 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Thu, 13 Mar 2025 13:57:58 -0400 Subject: [PATCH 42/42] httpserver: delete libevent! --- src/httpserver.cpp | 494 ---------------------------------- src/httpserver.h | 145 ---------- src/rpc/node.cpp | 10 +- src/test/httpserver_tests.cpp | 14 +- 4 files changed, 4 insertions(+), 659 deletions(-) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index d9d4fef9578..080d6db0153 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -35,22 +35,9 @@ #include #include -#include -#include -#include -#include -#include -#include -#include - -#include - using common::InvalidPortErrMsg; using http_bitcoin::HTTPRequest; -/** Maximum size of http request (request line + headers) */ -static const size_t MAX_HEADERS_SIZE = 8192; - /** HTTP request work item */ class HTTPWorkItem final : public HTTPClosure { @@ -141,10 +128,6 @@ struct HTTPPathHandler /** HTTP module state */ -//! libevent event loop -static struct event_base* eventBase = nullptr; -//! HTTP server -static struct evhttp* eventHTTP = nullptr; static std::unique_ptr g_http_server{nullptr}; //! List of subnets to allow RPC connections from static std::vector rpc_allow_subnets; @@ -153,63 +136,6 @@ static std::unique_ptr> g_work_queue{nullptr}; //! Handlers for (sub)paths static GlobalMutex g_httppathhandlers_mutex; static std::vector pathHandlers GUARDED_BY(g_httppathhandlers_mutex); -//! Bound listening sockets -static std::vector boundSockets; - -/** - * @brief Helps keep track of open `evhttp_connection`s with active `evhttp_requests` - * - */ -class HTTPRequestTracker -{ -private: - mutable Mutex m_mutex; - mutable std::condition_variable m_cv; - //! For each connection, keep a counter of how many requests are open - std::unordered_map m_tracker GUARDED_BY(m_mutex); - - void RemoveConnectionInternal(const decltype(m_tracker)::iterator it) EXCLUSIVE_LOCKS_REQUIRED(m_mutex) - { - m_tracker.erase(it); - if (m_tracker.empty()) m_cv.notify_all(); - } -public: - //! Increase request counter for the associated connection by 1 - void AddRequest(evhttp_request* req) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) - { - const evhttp_connection* conn{Assert(evhttp_request_get_connection(Assert(req)))}; - WITH_LOCK(m_mutex, ++m_tracker[conn]); - } - //! Decrease request counter for the associated connection by 1, remove connection if counter is 0 - void RemoveRequest(evhttp_request* req) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) - { - const evhttp_connection* conn{Assert(evhttp_request_get_connection(Assert(req)))}; - LOCK(m_mutex); - auto it{m_tracker.find(conn)}; - if (it != m_tracker.end() && it->second > 0) { - if (--(it->second) == 0) RemoveConnectionInternal(it); - } - } - //! Remove a connection entirely - void RemoveConnection(const evhttp_connection* conn) EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) - { - LOCK(m_mutex); - auto it{m_tracker.find(Assert(conn))}; - if (it != m_tracker.end()) RemoveConnectionInternal(it); - } - size_t CountActiveConnections() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) - { - return WITH_LOCK(m_mutex, return m_tracker.size()); - } - //! Wait until there are no more connections with active requests in the tracker - void WaitUntilEmpty() const EXCLUSIVE_LOCKS_REQUIRED(!m_mutex) - { - WAIT_LOCK(m_mutex, lock); - m_cv.wait(lock, [this]() EXCLUSIVE_LOCKS_REQUIRED(m_mutex) { return m_tracker.empty(); }); - } -}; -//! Track active requests -static HTTPRequestTracker g_requests; /** Check if a network address is allowed to access the HTTP server */ static bool ClientAllowed(const CNetAddr& netaddr) @@ -320,57 +246,6 @@ static void RejectAllRequests(std::unique_ptr hreq) hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE); } -/** HTTP request callback */ -static void http_request_cb(struct evhttp_request* req, void* arg) -{ - evhttp_connection* conn{evhttp_request_get_connection(req)}; - // Track active requests - { - g_requests.AddRequest(req); - evhttp_request_set_on_complete_cb(req, [](struct evhttp_request* req, void*) { - g_requests.RemoveRequest(req); - }, nullptr); - evhttp_connection_set_closecb(conn, [](evhttp_connection* conn, void* arg) { - g_requests.RemoveConnection(conn); - }, nullptr); - } - - // Disable reading to work around a libevent bug, fixed in 2.1.9 - // See https://github.com/libevent/libevent/commit/5ff8eb26371c4dc56f384b2de35bea2d87814779 - // and https://github.com/bitcoin/bitcoin/pull/11593. - if (event_get_version_number() >= 0x02010600 && event_get_version_number() < 0x02010900) { - if (conn) { - bufferevent* bev = evhttp_connection_get_bufferevent(conn); - if (bev) { - bufferevent_disable(bev, EV_READ); - } - } - } - auto hreq{std::make_unique(req, *static_cast(arg))}; - - // Disabled now that http_libevent is deprecated, or code won't compile. - // This line is currently unreachable and will be cleaned up in a future commit. - // MaybeDispatchRequestToWorker(std::move(hreq)); - Assume(false); -} - -/** Callback to reject HTTP requests after shutdown. */ -static void http_reject_request_cb(struct evhttp_request* req, void*) -{ - LogDebug(BCLog::HTTP, "Rejecting request while shutting down\n"); - evhttp_send_error(req, HTTP_SERVUNAVAIL, nullptr); -} - -/** Event dispatcher thread */ -static void ThreadHTTP(struct event_base* base) -{ - util::ThreadRename("http"); - LogDebug(BCLog::HTTP, "Entering http event loop\n"); - event_base_dispatch(base); - // Event loop will be interrupted by InterruptHTTPServer() - LogDebug(BCLog::HTTP, "Exited http event loop\n"); -} - static std::vector> GetBindAddresses() { uint16_t http_port{static_cast(gArgs.GetIntArg("-rpcport", BaseParams().RPCPort()))}; @@ -411,374 +286,6 @@ static void HTTPWorkQueueRun(WorkQueue* queue, int worker_num) queue->Run(); } -/** libevent event log callback */ -static void libevent_log_cb(int severity, const char *msg) -{ - BCLog::Level level; - switch (severity) { - case EVENT_LOG_DEBUG: - level = BCLog::Level::Debug; - break; - case EVENT_LOG_MSG: - level = BCLog::Level::Info; - break; - case EVENT_LOG_WARN: - level = BCLog::Level::Warning; - break; - default: // EVENT_LOG_ERR and others are mapped to error - level = BCLog::Level::Error; - break; - } - LogPrintLevel(BCLog::LIBEVENT, level, "%s\n", msg); -} - -namespace http_libevent { -/** Bind HTTP server to specified addresses */ -static bool HTTPBindAddresses(struct evhttp* http) -{ - std::vector> endpoints{GetBindAddresses()}; - for (std::vector >::iterator i = endpoints.begin(); i != endpoints.end(); ++i) { - LogPrintf("Binding RPC on address %s port %i\n", i->first, i->second); - evhttp_bound_socket *bind_handle = evhttp_bind_socket_with_handle(http, i->first.empty() ? nullptr : i->first.c_str(), i->second); - if (bind_handle) { - const std::optional addr{LookupHost(i->first, false)}; - if (i->first.empty() || (addr.has_value() && addr->IsBindAny())) { - LogPrintf("WARNING: the RPC server is not safe to expose to untrusted networks such as the public internet\n"); - } - // Set the no-delay option (disable Nagle's algorithm) on the TCP socket. - evutil_socket_t fd = evhttp_bound_socket_get_fd(bind_handle); - int one = 1; - if (setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, (sockopt_arg_type)&one, sizeof(one)) == SOCKET_ERROR) { - LogInfo("WARNING: Unable to set TCP_NODELAY on RPC server socket, continuing anyway\n"); - } - boundSockets.push_back(bind_handle); - } else { - LogPrintf("Binding RPC on address %s port %i failed.\n", i->first, i->second); - } - } - return !boundSockets.empty(); -} - -bool InitHTTPServer(const util::SignalInterrupt& interrupt) -{ - if (!InitHTTPAllowList()) - return false; - - // Redirect libevent's logging to our own log - event_set_log_callback(&libevent_log_cb); - // Update libevent's log handling. - UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); - -#ifdef WIN32 - evthread_use_windows_threads(); -#else - evthread_use_pthreads(); -#endif - - raii_event_base base_ctr = obtain_event_base(); - - /* Create a new evhttp object to handle requests. */ - raii_evhttp http_ctr = obtain_evhttp(base_ctr.get()); - struct evhttp* http = http_ctr.get(); - if (!http) { - LogPrintf("couldn't create evhttp. Exiting.\n"); - return false; - } - - evhttp_set_timeout(http, gArgs.GetIntArg("-rpcservertimeout", DEFAULT_HTTP_SERVER_TIMEOUT)); - evhttp_set_max_headers_size(http, MAX_HEADERS_SIZE); - evhttp_set_max_body_size(http, MAX_SIZE); - evhttp_set_gencb(http, http_request_cb, (void*)&interrupt); - - if (!HTTPBindAddresses(http)) { - LogPrintf("Unable to bind any endpoint for RPC server\n"); - return false; - } - - LogDebug(BCLog::HTTP, "Initialized HTTP server\n"); - int workQueueDepth = std::max((long)gArgs.GetIntArg("-rpcworkqueue", DEFAULT_HTTP_WORKQUEUE), 1L); - LogDebug(BCLog::HTTP, "creating work queue of depth %d\n", workQueueDepth); - - g_work_queue = std::make_unique>(workQueueDepth); - // transfer ownership to eventBase/HTTP via .release() - eventBase = base_ctr.release(); - eventHTTP = http_ctr.release(); - return true; -} - -void UpdateHTTPServerLogging(bool enable) { - if (enable) { - event_enable_debug_logging(EVENT_DBG_ALL); - } else { - event_enable_debug_logging(EVENT_DBG_NONE); - } -} - -static std::thread g_thread_http; -static std::vector g_thread_http_workers; - -void StartHTTPServer() -{ - int rpcThreads = std::max((long)gArgs.GetIntArg("-rpcthreads", DEFAULT_HTTP_THREADS), 1L); - LogInfo("Starting HTTP server with %d worker threads\n", rpcThreads); - g_thread_http = std::thread(ThreadHTTP, eventBase); - - for (int i = 0; i < rpcThreads; i++) { - g_thread_http_workers.emplace_back(HTTPWorkQueueRun, g_work_queue.get(), i); - } -} - -void InterruptHTTPServer() -{ - LogDebug(BCLog::HTTP, "Interrupting HTTP server\n"); - if (eventHTTP) { - // Reject requests on current connections - evhttp_set_gencb(eventHTTP, http_reject_request_cb, nullptr); - } - if (g_work_queue) { - g_work_queue->Interrupt(); - } -} - -void StopHTTPServer() -{ - LogDebug(BCLog::HTTP, "Stopping HTTP server\n"); - if (g_work_queue) { - LogDebug(BCLog::HTTP, "Waiting for HTTP worker threads to exit\n"); - for (auto& thread : g_thread_http_workers) { - thread.join(); - } - g_thread_http_workers.clear(); - } - // Unlisten sockets, these are what make the event loop running, which means - // that after this and all connections are closed the event loop will quit. - for (evhttp_bound_socket *socket : boundSockets) { - evhttp_del_accept_socket(eventHTTP, socket); - } - boundSockets.clear(); - { - if (const auto n_connections{g_requests.CountActiveConnections()}; n_connections != 0) { - LogDebug(BCLog::HTTP, "Waiting for %d connections to stop HTTP server\n", n_connections); - } - g_requests.WaitUntilEmpty(); - } - if (eventHTTP) { - // Schedule a callback to call evhttp_free in the event base thread, so - // that evhttp_free does not need to be called again after the handling - // of unfinished request connections that follows. - event_base_once(eventBase, -1, EV_TIMEOUT, [](evutil_socket_t, short, void*) { - evhttp_free(eventHTTP); - eventHTTP = nullptr; - }, nullptr, nullptr); - } - if (eventBase) { - LogDebug(BCLog::HTTP, "Waiting for HTTP event thread to exit\n"); - if (g_thread_http.joinable()) g_thread_http.join(); - event_base_free(eventBase); - eventBase = nullptr; - } - g_work_queue.reset(); - LogDebug(BCLog::HTTP, "Stopped HTTP server\n"); -} -} // namespace http_libevent - -struct event_base* EventBase() -{ - return eventBase; -} - -static void httpevent_callback_fn(evutil_socket_t, short, void* data) -{ - // Static handler: simply call inner handler - HTTPEvent *self = static_cast(data); - self->handler(); - if (self->deleteWhenTriggered) - delete self; -} - -HTTPEvent::HTTPEvent(struct event_base* base, bool _deleteWhenTriggered, const std::function& _handler): - deleteWhenTriggered(_deleteWhenTriggered), handler(_handler) -{ - ev = event_new(base, -1, 0, httpevent_callback_fn, this); - assert(ev); -} -HTTPEvent::~HTTPEvent() -{ - event_free(ev); -} -void HTTPEvent::trigger(struct timeval* tv) -{ - if (tv == nullptr) - event_active(ev, 0, 0); // immediately trigger event in main thread - else - evtimer_add(ev, tv); // trigger after timeval passed -} - -namespace http_libevent { -HTTPRequest::HTTPRequest(struct evhttp_request* _req, const util::SignalInterrupt& interrupt, bool _replySent) - : req(_req), m_interrupt(interrupt), replySent(_replySent) -{ -} - -HTTPRequest::~HTTPRequest() -{ - if (!replySent) { - // Keep track of whether reply was sent to avoid request leaks - LogPrintf("%s: Unhandled request\n", __func__); - WriteReply(HTTP_INTERNAL_SERVER_ERROR, "Unhandled request"); - } - // evhttpd cleans up the request, as long as a reply was sent. -} - -std::pair HTTPRequest::GetHeader(const std::string& hdr) const -{ - const struct evkeyvalq* headers = evhttp_request_get_input_headers(req); - assert(headers); - const char* val = evhttp_find_header(headers, hdr.c_str()); - if (val) - return std::make_pair(true, val); - else - return std::make_pair(false, ""); -} - -std::string HTTPRequest::ReadBody() -{ - struct evbuffer* buf = evhttp_request_get_input_buffer(req); - if (!buf) - return ""; - size_t size = evbuffer_get_length(buf); - /** Trivial implementation: if this is ever a performance bottleneck, - * internal copying can be avoided in multi-segment buffers by using - * evbuffer_peek and an awkward loop. Though in that case, it'd be even - * better to not copy into an intermediate string but use a stream - * abstraction to consume the evbuffer on the fly in the parsing algorithm. - */ - const char* data = (const char*)evbuffer_pullup(buf, size); - if (!data) // returns nullptr in case of empty buffer - return ""; - std::string rv(data, size); - evbuffer_drain(buf, size); - return rv; -} - -void HTTPRequest::WriteHeader(const std::string& hdr, const std::string& value) -{ - struct evkeyvalq* headers = evhttp_request_get_output_headers(req); - assert(headers); - evhttp_add_header(headers, hdr.c_str(), value.c_str()); -} - -/** Closure sent to main thread to request a reply to be sent to - * a HTTP request. - * Replies must be sent in the main loop in the main http thread, - * this cannot be done from worker threads. - */ -void HTTPRequest::WriteReply(int nStatus, std::span reply) -{ - assert(!replySent && req); - if (m_interrupt) { - WriteHeader("Connection", "close"); - } - // Send event to main http thread to send reply message - struct evbuffer* evb = evhttp_request_get_output_buffer(req); - assert(evb); - evbuffer_add(evb, reply.data(), reply.size()); - auto req_copy = req; - HTTPEvent* ev = new HTTPEvent(eventBase, true, [req_copy, nStatus]{ - evhttp_send_reply(req_copy, nStatus, nullptr, nullptr); - // Re-enable reading from the socket. This is the second part of the libevent - // workaround above. - if (event_get_version_number() >= 0x02010600 && event_get_version_number() < 0x02010900) { - evhttp_connection* conn = evhttp_request_get_connection(req_copy); - if (conn) { - bufferevent* bev = evhttp_connection_get_bufferevent(conn); - if (bev) { - bufferevent_enable(bev, EV_READ | EV_WRITE); - } - } - } - }); - ev->trigger(nullptr); - replySent = true; - req = nullptr; // transferred back to main thread -} - -CService HTTPRequest::GetPeer() const -{ - evhttp_connection* con = evhttp_request_get_connection(req); - CService peer; - if (con) { - // evhttp retains ownership over returned address string - const char* address = ""; - uint16_t port = 0; - -#ifdef HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR - evhttp_connection_get_peer(con, &address, &port); -#else - evhttp_connection_get_peer(con, (char**)&address, &port); -#endif // HAVE_EVHTTP_CONNECTION_GET_PEER_CONST_CHAR - - peer = MaybeFlipIPv6toCJDNS(LookupNumeric(address, port)); - } - return peer; -} - -std::string HTTPRequest::GetURI() const -{ - return evhttp_request_get_uri(req); -} - -HTTPRequestMethod HTTPRequest::GetRequestMethod() const -{ - switch (evhttp_request_get_command(req)) { - case EVHTTP_REQ_GET: - return HTTPRequestMethod::GET; - case EVHTTP_REQ_POST: - return HTTPRequestMethod::POST; - case EVHTTP_REQ_HEAD: - return HTTPRequestMethod::HEAD; - case EVHTTP_REQ_PUT: - return HTTPRequestMethod::PUT; - default: - return HTTPRequestMethod::UNKNOWN; - } -} - -std::optional HTTPRequest::GetQueryParameter(const std::string& key) const -{ - const char* uri{evhttp_request_get_uri(req)}; - - return GetQueryParameterFromUri(uri, key); -} - -std::optional GetQueryParameterFromUri(const char* uri, const std::string& key) -{ - evhttp_uri* uri_parsed{evhttp_uri_parse(uri)}; - if (!uri_parsed) { - throw std::runtime_error("URI parsing failed, it likely contained RFC 3986 invalid characters"); - } - const char* query{evhttp_uri_get_query(uri_parsed)}; - std::optional result; - - if (query) { - // Parse the query string into a key-value queue and iterate over it - struct evkeyvalq params_q; - evhttp_parse_query_str(query, ¶ms_q); - - for (struct evkeyval* param{params_q.tqh_first}; param != nullptr; param = param->next.tqe_next) { - if (param->key == key) { - result = param->value; - break; - } - } - evhttp_clear_headers(¶ms_q); - } - evhttp_uri_free(uri_parsed); - - return result; -} -} // namespace http_libevent - void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler) { LogDebug(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch); @@ -801,7 +308,6 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch) } } - namespace http_bitcoin { using util::SplitString; diff --git a/src/httpserver.h b/src/httpserver.h index 876bb1046c8..9e44d11f615 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -34,10 +34,6 @@ static const int DEFAULT_HTTP_WORKQUEUE=64; static const int DEFAULT_HTTP_SERVER_TIMEOUT=30; -struct evhttp_request; -struct event_base; -class CService; - enum HTTPRequestMethod { UNKNOWN, GET, @@ -46,124 +42,6 @@ enum HTTPRequestMethod { PUT }; -namespace http_bitcoin {}; - -namespace http_libevent { -class HTTPRequest; - -/** Initialize HTTP server. - * Call this before RegisterHTTPHandler or EventBase(). - */ -bool InitHTTPServer(const util::SignalInterrupt& interrupt); -/** Start HTTP server. - * This is separate from InitHTTPServer to give users race-condition-free time - * to register their handlers between InitHTTPServer and StartHTTPServer. - */ -void StartHTTPServer(); -/** Interrupt HTTP server threads */ -void InterruptHTTPServer(); -/** Stop HTTP server */ -void StopHTTPServer(); - -/** Change logging level for libevent. */ -void UpdateHTTPServerLogging(bool enable); -} // namespace http_libevent - -/** Return evhttp event base. This can be used by submodules to - * queue timers or custom events. - */ -struct event_base* EventBase(); - -namespace http_libevent { -/** In-flight HTTP request. - * Thin C++ wrapper around evhttp_request. - */ -class HTTPRequest -{ -private: - struct evhttp_request* req; - const util::SignalInterrupt& m_interrupt; - bool replySent; - -public: - explicit HTTPRequest(struct evhttp_request* req, const util::SignalInterrupt& interrupt, bool replySent = false); - ~HTTPRequest(); - - /** Get requested URI. - */ - std::string GetURI() const; - - /** Get CService (address:ip) for the origin of the http request. - */ - CService GetPeer() const; - - /** Get request method. - */ - HTTPRequestMethod GetRequestMethod() const; - - /** Get the query parameter value from request uri for a specified key, or std::nullopt if the - * key is not found. - * - * If the query string contains duplicate keys, the first value is returned. Many web frameworks - * would instead parse this as an array of values, but this is not (yet) implemented as it is - * currently not needed in any of the endpoints. - * - * @param[in] key represents the query parameter of which the value is returned - */ - std::optional GetQueryParameter(const std::string& key) const; - - /** - * Get the request header specified by hdr, or an empty string. - * Return a pair (isPresent,string). - */ - std::pair GetHeader(const std::string& hdr) const; - - /** - * Read request body. - * - * @note As this consumes the underlying buffer, call this only once. - * Repeated calls will return an empty string. - */ - std::string ReadBody(); - - /** - * Write output header. - * - * @note call this before calling WriteErrorReply or Reply. - */ - void WriteHeader(const std::string& hdr, const std::string& value); - - /** - * Write HTTP reply. - * nStatus is the HTTP status code to send. - * reply is the body of the reply. Keep it empty to send a standard message. - * - * @note Can be called only once. As this will give the request back to the - * main thread, do not call any other HTTPRequest methods after calling this. - */ - void WriteReply(int nStatus, std::string_view reply = "") - { - WriteReply(nStatus, std::as_bytes(std::span{reply})); - } - void WriteReply(int nStatus, std::span reply); -}; - - -/** Get the query parameter value from request uri for a specified key, or std::nullopt if the key - * is not found. - * - * If the query string contains duplicate keys, the first value is returned. Many web frameworks - * would instead parse this as an array of values, but this is not (yet) implemented as it is - * currently not needed in any of the endpoints. - * - * Helper function for HTTPRequest::GetQueryParameter. - * - * @param[in] uri is the entire request uri - * @param[in] key represents the query parameter of which the value is returned - */ -std::optional GetQueryParameterFromUri(const char* uri, const std::string& key); -} // namespace http_libevent - /** Event handler closure. */ class HTTPClosure @@ -173,29 +51,6 @@ public: virtual ~HTTPClosure() = default; }; -/** Event class. This can be used either as a cross-thread trigger or as a timer. - */ -class HTTPEvent -{ -public: - /** Create a new event. - * deleteWhenTriggered deletes this event object after the event is triggered (and the handler called) - * handler is the handler to call when the event is triggered. - */ - HTTPEvent(struct event_base* base, bool deleteWhenTriggered, const std::function& handler); - ~HTTPEvent(); - - /** Trigger the event. If tv is 0, trigger it immediately. Otherwise trigger it after - * the given time has elapsed. - */ - void trigger(struct timeval* tv); - - bool deleteWhenTriggered; - std::function handler; -private: - struct event* ev; -}; - namespace http_bitcoin { using util::LineReader; using NodeId = SockMan::Id; diff --git a/src/rpc/node.cpp b/src/rpc/node.cpp index fb851ee9a07..c3240fb23c6 100644 --- a/src/rpc/node.cpp +++ b/src/rpc/node.cpp @@ -240,24 +240,16 @@ static RPCHelpMan logging() }, RPCExamples{ HelpExampleCli("logging", "\"[\\\"all\\\"]\" \"[\\\"http\\\"]\"") - + HelpExampleRpc("logging", "[\"all\"], [\"libevent\"]") + + HelpExampleRpc("logging", "[\"all\"], [\"walletdb\"]") }, [&](const RPCHelpMan& self, const JSONRPCRequest& request) -> UniValue { - BCLog::CategoryMask original_log_categories = LogInstance().GetCategoryMask(); if (request.params[0].isArray()) { EnableOrDisableLogCategories(request.params[0], true); } if (request.params[1].isArray()) { EnableOrDisableLogCategories(request.params[1], false); } - BCLog::CategoryMask updated_log_categories = LogInstance().GetCategoryMask(); - BCLog::CategoryMask changed_log_categories = original_log_categories ^ updated_log_categories; - - // Update libevent logging if BCLog::LIBEVENT has changed. - if (changed_log_categories & BCLog::LIBEVENT) { - http_libevent::UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); - } UniValue result(UniValue::VOBJ); for (const auto& logCatActive : LogInstance().LogCategoriesList()) { diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 0d58fe911a3..40b5c12940a 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -61,18 +61,15 @@ BOOST_AUTO_TEST_CASE(test_query_parameters) std::string uri {}; // This is an invalid URI because it contains a % that is not followed by two hex digits uri = "/rest/endpoint/someresource.json?p1=v1&p2=v2%"; - // Old behavior: URI with invalid characters (%) raises a runtime error regardless of which query parameter is queried - BOOST_CHECK_EXCEPTION(http_libevent::GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters")); + // Old libevent behavior: URI with invalid characters (%) raised a runtime error regardless of which query parameter is queried // New behavior: Tolerate as much as we can even BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1"); BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p2").value(), "v2%"); // This is a valid URI because the %XX encoding is correct: `?p1=v1&p2=100%` uri = "/rest/endpoint/someresource.json%3Fp1%3Dv1%26p2%3D100%25"; - // Old behavior: libevent does not decode the URI before parsing, so it does not detect or return the query - // (libevent will parse the entire argument string as the uri path) - BOOST_CHECK(!http_libevent::GetQueryParameterFromUri(uri.c_str(), "p1").has_value()); - BOOST_CHECK(!http_libevent::GetQueryParameterFromUri(uri.c_str(), "p2").has_value()); + // Old behavior: libevent did not decode the URI before parsing, so it did not detect or return the query + // (libevent would parse the entire argument string as the uri path) // New behavior: Decode before parsing the URI so reserved characters like ? & = are interpreted correctly BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p1").value(), "v1"); BOOST_CHECK_EQUAL(http_bitcoin::GetQueryParameterFromUri(uri.c_str(), "p2").value(), "100%"); @@ -111,11 +108,6 @@ void test_query_parameters(func GetQueryParameterFromUri) { BOOST_CHECK_EQUAL(GetQueryParameterFromUri(uri.c_str(), "p2").value(), "100%"); } -BOOST_AUTO_TEST_CASE(test_query_parameters_libevent) -{ - test_query_parameters(http_libevent::GetQueryParameterFromUri); -} - BOOST_AUTO_TEST_CASE(test_query_parameters_bitcoin) { test_query_parameters(http_bitcoin::GetQueryParameterFromUri);