diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 41577b2ad6d..0124c3048fa 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -145,6 +145,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 @@ -152,6 +153,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 @@ -229,7 +231,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 new file mode 100644 index 00000000000..ee5389cf47c --- /dev/null +++ b/src/common/sockman.cpp @@ -0,0 +1,535 @@ +// 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 +#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. */ +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; +} + +bool SockMan::BindAndStartListening(const CService& to, bilingual_str& err_msg) +{ + // Create socket for listening for incoming connections + 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(to.GetSAFamily(), SOCK_STREAM, IPPROTO_TCP)}; + if (!sock) { + 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, 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 (to.IsIPv6()) { +#ifdef IPV6_V6ONLY + 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 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(&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; + } + + // Listen for incoming connections + if (sock->Listen(SOMAXCONN) == SOCKET_ERROR) { + err_msg = strprintf(_("Cannot listen on %s: %s"), to.ToStringAddrPort(), NetworkErrorString(WSAGetLastError())); + return false; + } + + m_listen.emplace_back(std::move(sock)); + + return true; +} + +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); + + 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(); + } + + if (m_thread_socket_handler.joinable()) { + m_thread_socket_handler.join(); + } +} + +std::optional +SockMan::ConnectAndMakeId(const std::variant& to, + bool is_important, + std::optional proxy, + bool& proxy_failed, + 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)) { + 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()}; + + { + LOCK(m_connected_mutex); + m_connected.emplace(id, std::make_shared(std::move(sock), + std::move(i2p_transient_session))); + } + + return id; +} + +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(); +} + +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::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; + + 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; + } + + Assume(conn.me.IsI2P()); + Assume(conn.peer.IsI2P()); + + NewSockAccepted(std::move(conn.sock), conn.me, conn.peer); + + 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); + } +} + +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); + + 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 new file mode 100644 index 00000000000..7c4a61cb779 --- /dev/null +++ b/src/common/sockman.h @@ -0,0 +1,463 @@ +// 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 +#include + +#include +#include +#include +#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 + * - 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 +{ +public: + /** + * Each connection is assigned an unique id of this type. + */ + 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`. + * Should be called before `StartSocketsThreads()`. + * @param[in] to Where to bind. + * @param[out] err_msg Error string if an error occurs. + * @retval true Success. + * @retval false Failure, `err_msg` will be set. + */ + bool BindAndStartListening(const CService& to, bilingual_str& err_msg); + + /** + * 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}, + 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(); + + /** + * 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. + * @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) + EXCLUSIVE_LOCKS_REQUIRED(!m_connected_mutex, !m_unused_i2p_sessions_mutex); + + /** + * 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. + */ + 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; + +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: + + /** + * 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. + // + + /** + * Be notified when a new connection has been accepted. + * @param[in] 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 id should be discarded by `SockMan`. + */ + virtual bool EventNewConnectionAccepted(Id id, + 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. + // + + /** + * 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 call to EventReadyToSend(). + */ + 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; + + /** + * 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. + * @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 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() + 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); + + /** + * 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 + * 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`. + */ + 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); + + /** + * 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; + + /** + * 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/httprpc.cpp b/src/httprpc.cpp index 57893702b8b..4cb901f3763 100644 --- a/src/httprpc.cpp +++ b/src/httprpc.cpp @@ -9,8 +9,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -26,6 +28,8 @@ #include #include +using node::NodeContext; +using http_bitcoin::HTTPRequest; using util::SplitString; using util::TrimStringView; @@ -38,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 @@ -62,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; }; @@ -85,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) @@ -156,7 +154,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; } @@ -370,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; } diff --git a/src/httpserver.cpp b/src/httpserver.cpp index bd2dec19b97..080d6db0153 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -7,16 +7,19 @@ #include #include #include +#include #include #include #include #include #include // For HTTP status codes +#include #include #include #include #include #include +#include #include #include @@ -32,20 +35,8 @@ #include #include -#include -#include -#include -#include -#include -#include -#include - -#include - using common::InvalidPortErrMsg; - -/** Maximum size of http request (request line + headers) */ -static const size_t MAX_HEADERS_SIZE = 8192; +using http_bitcoin::HTTPRequest; /** HTTP request work item */ class HTTPWorkItem final : public HTTPClosure @@ -137,10 +128,7 @@ 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; //! Work queue for handling longer requests off the event loop thread @@ -148,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) @@ -241,51 +172,25 @@ 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); } -/** 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", @@ -295,16 +200,13 @@ 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); 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; @@ -338,25 +240,13 @@ static void http_request_cb(struct evhttp_request* req, void* arg) } } -/** Callback to reject HTTP requests after shutdown. */ -static void http_reject_request_cb(struct evhttp_request* req, void*) +static void RejectAllRequests(std::unique_ptr hreq) { LogDebug(BCLog::HTTP, "Rejecting request while shutting down\n"); - evhttp_send_error(req, HTTP_SERVUNAVAIL, nullptr); + hreq->WriteReply(HTTP_SERVICE_UNAVAILABLE); } -/** 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"); -} - -/** 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; @@ -381,33 +271,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 */ @@ -417,343 +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); -} - -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"); -} - -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 -} -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); -} - -HTTPRequest::RequestMethod HTTPRequest::GetRequestMethod() const -{ - switch (evhttp_request_get_command(req)) { - case EVHTTP_REQ_GET: - return GET; - case EVHTTP_REQ_POST: - return POST; - case EVHTTP_REQ_HEAD: - return HEAD; - case EVHTTP_REQ_PUT: - return PUT; - default: - return 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; -} - void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler) { LogDebug(BCLog::HTTP, "Registering HTTP handler for %s (exactmatch %d)\n", prefix, exactMatch); @@ -775,3 +307,664 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch) pathHandlers.erase(i); } } + +namespace http_bitcoin { +using util::SplitString; + +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; +} + +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 + + 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; + + if (!ParseUInt64Hex(maybe_chunk_size.value(), &chunk_size)) throw std::runtime_error("Invalid chunk size"); + + bool last_chunk{chunk_size == 0}; + + 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); + } + + // 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; + } +} + +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; + + // 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; + } + + 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()))}; + + 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 + // 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()); + } + + 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); + + // 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) +{ + 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 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); + m_send_ready = false; + m_prevent_disconnect = false; + m_disconnect = true; + 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); + + // 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; + } + } else { + m_send_ready = true; + m_prevent_disconnect = true; + } + } + + 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() +{ + const auto now{Now()}; + for (auto it = m_connected_clients.begin(); it != m_connected_clients.end();) { + 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 { + ++it; + } + } + m_no_clients = m_connected_clients.size() == 0; +} + +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; + // 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; + 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 + auto client{GetClientById(node_id)}; + if (client == nullptr) { + return; + } + + // Reset idle timeout + client->m_idle_since = Now(); + + // 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(), + 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 + req->WriteReply(HTTP_BAD_REQUEST); + client->m_disconnect = true; + 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)); + } +} + +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 + 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)}; + if (it != m_connected_clients.end()) { + return it->second; + } + return nullptr; +} + +bool InitHTTPServer(const util::SignalInterrupt& interrupt) +{ + if (!InitHTTPAllowList()) + return false; + + // Create HTTPServer + g_http_server = std::make_unique(MaybeDispatchRequestToWorker); + + 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}; + 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 6535dc6086c..9e44d11f615 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -6,10 +6,17 @@ #define BITCOIN_HTTPSERVER_H #include +#include #include #include #include +#include +#include +#include +#include +#include + namespace util { class SignalInterrupt; } // namespace util @@ -27,10 +34,272 @@ static const int DEFAULT_HTTP_WORKQUEUE=64; static const int DEFAULT_HTTP_SERVER_TIMEOUT=30; -struct evhttp_request; -struct event_base; -class CService; -class HTTPRequest; +enum HTTPRequestMethod { + UNKNOWN, + GET, + POST, + HEAD, + PUT +}; + +/** Event handler closure. + */ +class HTTPClosure +{ +public: + virtual void operator()() = 0; + virtual ~HTTPClosure() = default; +}; + +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")}; +// 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: + 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; +}; + +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; +}; + +class HTTPClient; + +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; + + // 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. + bool LoadControlData(LineReader& reader); + 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 = {}); + 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); + +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; + + // 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{}; + + // 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}; + + // 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}; + + // 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(); + }; + + // 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; +}; + +class HTTPServer : public SockMan +{ +private: + void CloseConnectionInternal(std::shared_ptr& client); + +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; + + // 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}; + + // 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. + * @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; + + /** + * 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. + * 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; +}; /** Initialize HTTP server. * Call this before RegisterHTTPHandler or EventBase(). @@ -45,12 +314,10 @@ void StartHTTPServer(); void InterruptHTTPServer(); /** Stop HTTP server */ void StopHTTPServer(); - -/** Change logging level for libevent. */ -void UpdateHTTPServerLogging(bool enable); +} // namespace http_bitcoin /** 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. @@ -59,136 +326,4 @@ void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPR /** 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. - */ -struct event_base* EventBase(); - -/** 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(); - - enum RequestMethod { - UNKNOWN, - GET, - POST, - HEAD, - PUT - }; - - /** Get requested URI. - */ - std::string GetURI() const; - - /** Get CService (address:ip) for the origin of the http request. - */ - CService GetPeer() const; - - /** Get request method. - */ - RequestMethod 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); - -/** Event handler closure. - */ -class HTTPClosure -{ -public: - virtual void operator()() = 0; - 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; -}; - #endif // BITCOIN_HTTPSERVER_H diff --git a/src/init.cpp b/src/init.cpp index fae45eb90a5..c16f275bfd1 100644 --- a/src/init.cpp +++ b/src/init.cpp @@ -121,6 +121,10 @@ using common::AmountErrMsg; using common::InvalidPortErrMsg; using common::ResolveErrMsg; +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/net.cpp b/src/net.cpp index 735985a8414..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] @@ -335,7 +331,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 +342,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 +353,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,30 +369,15 @@ 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; } 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) { @@ -456,54 +437,26 @@ 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); + 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 +465,17 @@ 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); } // Check any other resolved address (if any) if we fail to connect - if (!sock) { + if (!node_id.has_value()) { continue; } @@ -525,31 +483,24 @@ 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 = GetNewNodeId(); - uint64_t nonce = GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(id).Finalize(); - if (!addr_bind.IsValid()) { - addr_bind = GetBindAddress(*sock); - } - CNode* pnode = new CNode(id, - std::move(sock), + const uint64_t nonce{GetDeterministicRandomizer(RANDOMIZER_ID_LOCALHOSTNONCE).Write(node_id.value()).Finalize()}; + CNode* pnode = new CNode(node_id.value(), target_addr, CalculateKeyedNetGroup(target_addr), nonce, - addr_bind, + me, pszDest ? pszDest : "", conn_type, /*inbound_onion=*/false, CNodeOptions{ .permission_flags = permission_flags, - .i2p_sam_session = std::move(i2p_transient_session), .recv_flood_size = nReceiveFloodSize, .use_v2transport = use_v2transport, }); 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; } @@ -557,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)) { @@ -1596,8 +1529,10 @@ Transport::Info V2Transport::GetInfo() const noexcept return info; } -std::pair CConnman::SocketSendData(CNode& node) const +std::pair CConnman::SendMessagesAsBytes(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) @@ -1622,45 +1557,27 @@ std::pair CConnman::SocketSendData(CNode& node) const 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; } @@ -1672,9 +1589,24 @@ 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}; } +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. @@ -1689,11 +1621,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,82 +1648,49 @@ 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; } -void CConnman::AcceptConnection(const ListenSocket& hListenSocket) { - struct sockaddr_storage sockaddr; - socklen_t len = sizeof(sockaddr); - auto sock = hListenSocket.sock->Accept((struct sockaddr*)&sockaddr, &len); +bool CConnman::EventNewConnectionAccepted(SockMan::Id id, + const CService& me, + const CService& them) +{ + const CService addr_bind{MaybeFlipIPv6toCJDNS(me)}; + const CService addr{MaybeFlipIPv6toCJDNS(them)}; - 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))}; + int nInbound = 0; NetPermissionFlags permission_flags = NetPermissionFlags::None; - hListenSocket.AddSocketPermissionFlags(permission_flags); - - 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; + auto it{m_listen_permissions.find(addr_bind)}; + if (it != m_listen_permissions.end()) { + NetPermissions::AddFlag(permission_flags, it->second); + } AddWhitelistPermissionFlags(permission_flags, addr, vWhitelistedRangeIncoming); { LOCK(m_nodes_mutex); - for (const CNode* pnode : m_nodes) { + for (const auto& [_, pnode] : m_nodes) { if (pnode->IsInboundConn()) nInbound++; } } if (!fNetworkActive) { LogDebug(BCLog::NET, "connection from %s dropped: not accepting new connections\n", addr.ToStringAddrPort()); - 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()); + return false; } // Don't accept connections from banned peers. @@ -1799,7 +1698,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, 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. @@ -1807,7 +1706,7 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, 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) @@ -1815,11 +1714,10 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, 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; } } - NodeId id = GetNewNodeId(); 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(); @@ -1829,7 +1727,6 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, const bool use_v2transport(local_services & NODE_P2P_V2); CNode* pnode = new CNode(id, - std::move(sock), CAddress{addr, NODE_NONE}, CalculateKeyedNetGroup(addr), nonce, @@ -1847,7 +1744,7 @@ void CConnman::CreateNodeFromAcceptedSocket(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, @@ -1859,11 +1756,12 @@ void CConnman::CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, // 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) { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::optional max_connections; switch (conn_type) { case ConnectionType::INBOUND: @@ -1884,8 +1782,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; @@ -1898,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); @@ -1912,7 +1827,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; @@ -1921,40 +1836,41 @@ 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(); + + MarkAsDisconnectAndCloseConnection(*pnode); + + // 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); } } { @@ -2050,187 +1966,131 @@ bool CConnman::InactivityCheck(const CNode& node) const return false; } -Sock::EventsPerSock CConnman::GenerateWaitSockets(Span nodes) +void CConnman::EventReadyToSend(SockMan::Id id, bool& cancel_recv) { - Sock::EventsPerSock events_per_sock; + AssertLockNotHeld(m_nodes_mutex); - for (const ListenSocket& hListenSocket : vhListenSocket) { - events_per_sock.emplace(hListenSocket.sock, Sock::Events{Sock::RECV}); + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + cancel_recv = true; + return; } - 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; - } - if (!select_recv && !select_send) continue; + const auto [bytes_sent, data_left] = WITH_LOCK(node->cs_vSend, return SendMessagesAsBytes(*node);); - 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; + // 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::SocketHandler() +void CConnman::EventGotData(SockMan::Id id, std::span data) { - AssertLockNotHeld(m_total_bytes_sent_mutex); + AssertLockNotHeld(mutexMsgProc); + AssertLockNotHeld(m_nodes_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); + CNode* node{GetNodeById(id)}; + if (node == nullptr) { + return; } - // 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_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; - sendSet = it->second.occurred & Sock::SEND; - errorSet = it->second.occurred & Sock::ERR; - } - } - - if (sendSet) { - // Send data - auto [bytes_sent, data_left] = WITH_LOCK(pnode->cs_vSend, return SocketSendData(*pnode)); - if (bytes_sent) { - RecordBytesSent(bytes_sent); - - // 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; - } - } - - 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) - { - 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(); - } - } - else if (nBytes == 0) - { - // socket closed gracefully - if (!pnode->fDisconnect) { - LogDebug(BCLog::NET, "socket closed, %s\n", pnode->DisconnectMsg(fLogIPs)); - } - pnode->CloseSocketDisconnect(); - } - else if (nBytes < 0) - { - // error - 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(); - } - } - } - - if (InactivityCheck(*pnode)) pnode->fDisconnect = true; + bool notify = false; + if (!node->ReceiveMsgBytes(data, notify)) { + LogDebug(BCLog::NET, + "receiving message bytes failed, %s\n", + node->DisconnectMsg(fLogIPs) + ); + MarkAsDisconnectAndCloseConnection(*node); + } + RecordBytesRecv(data.size()); + if (notify) { + node->MarkReceivedMsgsForProcessing(); + WakeMessageHandler(); } } -void CConnman::SocketHandlerListening(const Sock::EventsPerSock& events_per_sock) +void CConnman::EventGotEOF(SockMan::Id id) { - for (const ListenSocket& listen_socket : vhListenSocket) { - if (interruptNet) { - return; - } - const auto it = events_per_sock.find(listen_socket.sock); - if (it != events_per_sock.end() && it->second.occurred & Sock::RECV) { - AcceptConnection(listen_socket); - } + 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); + } + MarkAsDisconnectAndCloseConnection(*node); +} + +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); + } + MarkAsDisconnectAndCloseConnection(*node); +} + +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; +} + +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::ThreadSocketHandler() +void CConnman::EventIOLoopCompletedForAll() { - AssertLockNotHeld(m_total_bytes_sent_mutex); + AssertLockNotHeld(m_nodes_mutex); + AssertLockNotHeld(m_reconnections_mutex); - while (!interruptNet) - { - DisconnectNodes(); - NotifyNumConnectionsChanged(); - SocketHandler(); - } + DisconnectNodes(); + NotifyNumConnectionsChanged(); } void CConnman::WakeMessageHandler() @@ -2393,7 +2253,6 @@ void CConnman::DumpAddresses() void CConnman::ProcessAddrFetch() { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); std::string strDest; { LOCK(m_addr_fetches_mutex); @@ -2435,7 +2294,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; } } @@ -2453,7 +2312,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; } @@ -2467,7 +2326,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; } @@ -2513,7 +2372,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 @@ -2638,7 +2496,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++; @@ -2883,7 +2741,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); } @@ -2909,7 +2767,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(); } @@ -2954,7 +2812,6 @@ std::vector CConnman::GetAddedNodeInfo(bool include_connected) co void CConnman::ThreadOpenAddedConnections() { - AssertLockNotHeld(m_unused_i2p_sessions_mutex); AssertLockNotHeld(m_reconnections_mutex); while (true) { @@ -2984,7 +2841,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); // @@ -3013,7 +2869,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()]; @@ -3068,118 +2924,24 @@ void CConnman::ThreadMessageHandler() } } -void CConnman::ThreadI2PAcceptIncoming() +void CConnman::EventI2PStatus(const CService& addr, SockMan::I2PStatus new_status) { - 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 = [&]() { - interruptNet.sleep_for(err_wait); - if (err_wait < err_wait_cap) { - err_wait += 1s; + switch (new_status) { + case SockMan::I2PStatus::START_LISTENING: + if (!m_i2p_advertising_listen_addr) { + AddLocal(addr, LOCAL_MANUAL); + m_i2p_advertising_listen_addr = true; } - }; - - while (!interruptNet) { - - if (!m_i2p_sam_session->Listen(conn)) { - if (advertising_listen_addr && conn.me.IsValid()) { - RemoveLocal(conn.me); - advertising_listen_addr = false; - } - SleepOnFailure(); - continue; + break; + case SockMan::I2PStatus::STOP_LISTENING: + if (m_i2p_advertising_listen_addr && addr.IsValid()) { + RemoveLocal(addr); + m_i2p_advertising_listen_addr = false; } - - if (!advertising_listen_addr) { - AddLocal(conn.me, LOCAL_MANUAL); - advertising_listen_addr = true; - } - - if (!m_i2p_sam_session->Accept(conn)) { - SleepOnFailure(); - continue; - } - - CreateNodeFromAcceptedSocket(std::move(conn.sock), NetPermissionFlags::None, conn.me, conn.peer); - - err_wait = err_wait_begin; + break; } } -bool CConnman::BindListenPort(const CService& addrBind, bilingual_str& strError, NetPermissionFlags permissions) -{ - 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; - } - - vhListenSocket.emplace_back(std::move(sock), permissions); - return true; -} - void Discover() { if (!fDiscover) @@ -3221,11 +2983,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(); @@ -3242,13 +2999,18 @@ bool CConnman::Bind(const CService& addr_, unsigned int flags, NetPermissionFlag const CService addr{MaybeFlipIPv6toCJDNS(addr_)}; bilingual_str strError; - if (!BindListenPort(addr, strError, permissions)) { + 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)) { AddLocal(addr, LOCAL_BIND); } @@ -3304,12 +3066,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()) { @@ -3352,8 +3108,16 @@ 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"); + } + + StartSocketsThreads(sockman_options); if (!gArgs.GetBoolArg("-dnsseed", DEFAULT_DNSSEED)) LogPrintf("DNS seeding disabled\n"); @@ -3380,11 +3144,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); @@ -3438,9 +3197,8 @@ void CConnman::Interrupt() void CConnman::StopThreads() { - if (threadI2PAcceptIncoming.joinable()) { - threadI2PAcceptIncoming.join(); - } + JoinSocketsThreads(); + if (threadMessageHandler.joinable()) threadMessageHandler.join(); if (threadOpenConnections.joinable()) @@ -3449,8 +3207,6 @@ void CConnman::StopThreads() threadOpenAddedConnections.join(); if (threadDNSAddressSeed.joinable()) threadDNSAddressSeed.join(); - if (threadSocketHandler.joinable()) - threadSocketHandler.join(); } void CConnman::StopNodes() @@ -3470,11 +3226,11 @@ 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(); + MarkAsDisconnectAndCloseConnection(*pnode); DeleteNode(pnode); } @@ -3482,7 +3238,8 @@ void CConnman::StopNodes() DeleteNode(pnode); } m_nodes_disconnected.clear(); - vhListenSocket.clear(); + m_listen_permissions.clear(); + StopListening(); semOutbound.reset(); semAddnode.reset(); } @@ -3600,7 +3357,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++; } @@ -3626,7 +3383,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); @@ -3648,7 +3405,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; @@ -3666,14 +3423,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) @@ -3791,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, @@ -3802,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}, @@ -3814,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); @@ -3885,7 +3639,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 @@ -3902,27 +3655,24 @@ 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) { - std::tie(nBytesSent, std::ignore) = SocketSendData(*pnode); + SendMessagesAsBytes(*pnode); } } - if (nBytesSent) RecordBytesSent(nBytesSent); } 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); } @@ -3942,7 +3692,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 e64d9a67f46..4a7dd219a0b 100644 --- a/src/net.h +++ b/src/net.h @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -41,8 +42,8 @@ #include #include #include -#include #include +#include #include #include @@ -94,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; @@ -662,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; @@ -678,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. */ @@ -695,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}; @@ -879,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, @@ -941,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); } @@ -987,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); }; /** @@ -1048,7 +1022,7 @@ protected: ~NetEventsInterface() = default; }; -class CConnman +class CConnman : private SockMan { public: @@ -1136,7 +1110,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(); @@ -1151,7 +1125,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); } @@ -1160,7 +1134,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); } @@ -1221,7 +1195,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; @@ -1273,81 +1247,71 @@ public: bool MultipleManualOrFullOutboundConns(Network net) const EXCLUSIVE_LOCKS_REQUIRED(m_nodes_mutex); 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_} - { - } - - 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 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); - void ThreadI2PAcceptIncoming(); - void AcceptConnection(const ListenSocket& hListenSocket); + + /// 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; /** - * 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. + * Create a `CNode` object and add it to the `m_nodes` member. + * @param[in] 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 on success + * @retval false on failure, meaning that the associated socket and node_id should be discarded */ - void CreateNodeFromAcceptedSocket(std::unique_ptr&& sock, - NetPermissionFlags permission_flags, - const CService& addr_bind, - const CService& addr); + 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. */ bool InactivityCheck(const CNode& node) const; - /** - * 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); + void EventReadyToSend(SockMan::Id id, bool& cancel_recv) override + 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); + virtual void EventGotData(SockMan::Id id, std::span data) override + EXCLUSIVE_LOCKS_REQUIRED(!mutexMsgProc, !m_nodes_mutex); - /** - * 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_total_bytes_sent_mutex, !mutexMsgProc); + virtual void EventGotEOF(SockMan::Id id) override + EXCLUSIVE_LOCKS_REQUIRED(!m_nodes_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); + 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); + + 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); - 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; @@ -1363,15 +1327,14 @@ 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); - 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); + std::pair SendMessagesAsBytes(CNode& node) EXCLUSIVE_LOCKS_REQUIRED(node.cs_vSend) + EXCLUSIVE_LOCKS_REQUIRED(!m_total_bytes_sent_mutex); void DumpAddresses(); @@ -1430,7 +1393,11 @@ private: unsigned int nSendBufferMaxSize{0}; 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; @@ -1441,11 +1408,12 @@ 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::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; - std::atomic nLastNodeId{0}; unsigned int nPrevNodeCount{0}; // Stores number of full-tx connections (outbound and manual) per network @@ -1540,27 +1508,10 @@ 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 @@ -1591,20 +1542,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. */ @@ -1626,13 +1563,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 @@ -1645,8 +1576,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 3f6d0f3e42b..45612d7de20 100644 --- a/src/net_processing.cpp +++ b/src/net_processing.cpp @@ -5077,10 +5077,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/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; diff --git a/src/rest.cpp b/src/rest.cpp index 44984b360ff..2c22d9a6fd1 100644 --- a/src/rest.cpp +++ b/src/rest.cpp @@ -37,6 +37,7 @@ #include +using http_bitcoin::HTTPRequest; using node::GetTransaction; using node::NodeContext; using util::SplitString; 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/rpc/node.cpp b/src/rpc/node.cpp index 5e36273cf49..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) { - UpdateHTTPServerLogging(LogInstance().WillLogCategory(BCLog::LIBEVENT)); - } UniValue result(UniValue::VOBJ); for (const auto& logCatActive : LogInstance().LogCategoriesList()) { 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/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 a62d227da8e..da5efc70523 100644 --- a/src/test/fuzz/connman.cpp +++ b/src/test/fuzz/connman.cpp @@ -64,13 +64,15 @@ 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{*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).release()}; - connman.AddTestNode(p2p_node); + CNode& p2p_node{*ConsumeNodeAsUniquePtr(fuzzed_data_provider, node_id++).release()}; + connman.AddTestNode(p2p_node, std::make_unique(fuzzed_data_provider)); } LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000) { @@ -103,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/http_request.cpp b/src/test/fuzz/http_request.cpp index f13f1c72a51..eb97a57ff2d 100644 --- a/src/test/fuzz/http_request.cpp +++ b/src/test/fuzz/http_request.cpp @@ -10,47 +10,25 @@ #include #include -#include -#include -#include -#include - #include #include #include #include +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*); -std::string RequestMethodString(HTTPRequest::RequestMethod m); +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}; - const HTTPRequest::RequestMethod request_method = http_request.GetRequestMethod(); + HTTPRequest http_request; + const HTTPRequestMethod request_method = http_request.GetRequestMethod(); (void)RequestMethodString(request_method); (void)http_request.GetURI(); (void)http_request.GetHeader("Host"); @@ -62,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/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 6e568ad1cfd..12348982c37 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 698001a7f15..077ed0c6c49 100644 --- a/src/test/fuzz/util/net.h +++ b/src/test/fuzz/util/net.h @@ -231,7 +231,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(); @@ -242,7 +241,6 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional(node_id, - sock, address, keyed_net_group, local_host_nonce, @@ -253,7 +251,6 @@ auto ConsumeNode(FuzzedDataProvider& fuzzed_data_provider, const std::optional +#include +#include +#include #include +#include #include -BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) +using http_bitcoin::HTTPClient; +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; + +// 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) { + // 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 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 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%"); +} + +// Ensure new behavior matches old behavior +template +void test_query_parameters(func GetQueryParameterFromUri) { std::string uri {}; // No parameters @@ -35,8 +102,341 @@ 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_bitcoin) +{ + test_query_parameters(http_bitcoin::GetQueryParameterFromUri); +} + +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_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_CASE(http_request_tests) +{ + { + 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.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"); + 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_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()}; + + 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)); + + // 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() + 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); + + { + 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"); + BOOST_CHECK_EQUAL(requests.front()->GetPeer().ToStringAddrPort(), "5.5.5.5:6789"); + + // 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); + + // 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 + server.JoinSocketsThreads(); + } } BOOST_AUTO_TEST_SUITE_END() diff --git a/src/test/net_peer_connection_tests.cpp b/src/test/net_peer_connection_tests.cpp index e60ce8b99d3..dfee9367669 100644 --- a/src/test/net_peer_connection_tests.cpp +++ b/src/test/net_peer_connection_tests.cpp @@ -65,7 +65,6 @@ void AddPeer(NodeId& id, std::vector& 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, @@ -117,9 +116,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 +133,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 +150,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/net_tests.cpp b/src/test/net_tests.cpp index 62e541b5b39..b97a012161a 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, @@ -606,7 +602,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, @@ -660,7 +655,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, @@ -681,7 +675,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, @@ -818,7 +811,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, @@ -893,7 +885,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 3e717341d87..a8a0d7f5724 100644 --- a/src/test/util/net.h +++ b/src/test/util/net.h @@ -15,6 +15,7 @@ #include #include #include +#include #include #include @@ -45,16 +46,23 @@ struct ConnmanTestMsg : public CConnman { m_peer_connect_timeout = timeout; } - std::vector TestNodes() + auto TestNodes() { LOCK(m_nodes_mutex); 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); - 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 +70,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(); @@ -88,8 +96,9 @@ 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); + + using CConnman::MarkAsDisconnectAndCloseConnection; }; constexpr ServiceFlags ALL_SERVICE_FLAGS[]{ diff --git a/src/test/util_string_tests.cpp b/src/test/util_string_tests.cpp index 65ee140b6e9..911c97b8e69 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,57 @@ 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_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/test/util_tests.cpp b/src/test/util_tests.cpp index 4cacbd1151f..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"); @@ -1045,6 +1052,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 eed8bf81370..357e564fed9 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); @@ -479,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 75030983d21..2a9cb093961 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. @@ -367,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) @@ -395,6 +412,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: 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 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. */ 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") diff --git a/test/functional/interface_http.py b/test/functional/interface_http.py index dbdceb52d15..695fee638d8 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,74 @@ 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 + # 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() 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)