diff --git a/src/httpserver.cpp b/src/httpserver.cpp index 9fafc20104b..e7a3b8fe694 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -905,4 +905,17 @@ bool HTTPRequest::LoadBody(LineReader& reader) return true; } + +bool HTTPServer::EventNewConnectionAccepted(NodeId node_id, + const CService& me, + const CService& them) +{ + auto client = std::make_shared(node_id, them); + // Point back to the server + client->m_server = this; + LogDebug(BCLog::HTTP, "HTTP Connection accepted from %s (id=%d)\n", client->m_origin, client->m_node_id); + m_connected_clients.emplace(client->m_node_id, std::move(client)); + m_no_clients = false; + return true; +} } // namespace http_bitcoin diff --git a/src/httpserver.h b/src/httpserver.h index 2e92d2f9985..33de955667d 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -12,6 +12,7 @@ #include #include +#include #include #include @@ -204,6 +205,7 @@ private: namespace http_bitcoin { using util::LineReader; +using NodeId = SockMan::Id; // shortest valid request line, used by libevent in evhttp_parse_request_line() static const size_t MIN_REQUEST_LINE_LENGTH{strlen("GET / HTTP/1.0")}; @@ -256,6 +258,85 @@ public: bool LoadHeaders(LineReader& reader); bool LoadBody(LineReader& reader); }; + +class HTTPServer; + +class HTTPClient +{ +public: + // ID provided by SockMan, inherited by HTTPServer + NodeId m_node_id; + // Remote address of connected client + CService m_addr; + // IP:port of connected client, cached for logging purposes + std::string m_origin; + // Pointer back to the server so we can call Sockman I/O methods from the client + // Ok to remain null for unit tests. + HTTPServer* m_server; + + explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr) + { + m_origin = addr.ToStringAddrPort(); + }; + + // Disable copies (should only be used as shared pointers) + HTTPClient(const HTTPClient&) = delete; + HTTPClient& operator=(const HTTPClient&) = delete; +}; + +class HTTPServer : public SockMan +{ +public: + // Set in the Sockman I/O loop and only checked by main thread when shutting + // down to wait for all clients to be disconnected. + std::atomic_bool m_no_clients{true}; + + //! Connected clients with live HTTP connections + std::unordered_map> m_connected_clients; + + /** + * Be notified when a new connection has been accepted. + * @param[in] node_id Id of the newly accepted connection. + * @param[in] me The address and port at our side of the connection. + * @param[in] them The address and port at the peer's side of the connection. + * @retval true The new connection was accepted at the higher level. + * @retval false The connection was refused at the higher level, so the + * associated socket and node_id should be discarded by `SockMan`. + */ + virtual bool EventNewConnectionAccepted(NodeId node_id, const CService& me, const CService& them) override; + + /** + * Called when the socket is ready to send data and `ShouldTryToSend()` has + * returned true. This is where the higher level code serializes its messages + * and calls `SockMan::SendBytes()`. + * @param[in] node_id Id of the node whose socket is ready to send. + * @param[out] cancel_recv Should always be set upon return and if it is true, + * then the next attempt to receive data from that node will be omitted. + */ + virtual void EventReadyToSend(NodeId node_id, bool& cancel_recv) override {}; + + /** + * Called when new data has been received. + * @param[in] node_id Connection for which the data arrived. + * @param[in] data Received data. + */ + virtual void EventGotData(NodeId node_id, std::span data) override {}; + + /** + * Called when the remote peer has sent an EOF on the socket. This is a graceful + * close of their writing side, we can still send and they will receive, if it + * makes sense at the application level. + * @param[in] node_id Node whose socket got EOF. + */ + virtual void EventGotEOF(NodeId node_id) override {}; + + /** + * Called when we get an irrecoverable error trying to read from a socket. + * @param[in] node_id Node whose socket got an error. + * @param[in] errmsg Message describing the error. + */ + virtual void EventGotPermanentReadError(NodeId node_id, const std::string& errmsg) override {}; +}; } // namespace http_bitcoin #endif // BITCOIN_HTTPSERVER_H diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 5b5af4b98e8..309e8995238 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -12,10 +13,38 @@ using http_bitcoin::HTTPHeaders; using http_bitcoin::HTTPRequest; using http_bitcoin::HTTPResponse; +using http_bitcoin::HTTPServer; using http_bitcoin::MAX_HEADERS_SIZE; using util::LineReader; -BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) +// Reading request captured from bitcoin-cli +const std::string full_request = + "504f5354202f20485454502f312e310d0a486f73743a203132372e302e302e310d" + "0a436f6e6e656374696f6e3a20636c6f73650d0a436f6e74656e742d547970653a" + "206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a" + "204261736963205831396a6232397261575666587a6f354f4751354f4451334d57" + "4e6d4e6a67304e7a417a59546b7a4e32457a4e7a6b305a44466c4f4451314e6a5a" + "6d5954526b5a6a4a694d7a466b596a68684f4449345a4759344d6a566a4f546735" + "5a4749344f54566c0d0a436f6e74656e742d4c656e6774683a2034360d0a0d0a7b" + "226d6574686f64223a22676574626c6f636b636f756e74222c22706172616d7322" + "3a5b5d2c226964223a317d0a"; + +/// Save the value of CreateSock and restore it when the test ends. +class HTTPTestingSetup : public BasicTestingSetup +{ +public: + explicit HTTPTestingSetup() : m_create_sock_orig{CreateSock} {}; + + ~HTTPTestingSetup() + { + CreateSock = m_create_sock_orig; + } + +private: + const decltype(CreateSock) m_create_sock_orig; +}; + +BOOST_FIXTURE_TEST_SUITE(httpserver_tests, HTTPTestingSetup) BOOST_AUTO_TEST_CASE(test_query_parameters) { @@ -129,17 +158,6 @@ BOOST_AUTO_TEST_CASE(http_response_tests) BOOST_AUTO_TEST_CASE(http_request_tests) { { - // Reading request captured from bitcoin-cli - const std::string full_request = - "504f5354202f20485454502f312e310d0a486f73743a203132372e302e302e310d" - "0a436f6e6e656374696f6e3a20636c6f73650d0a436f6e74656e742d547970653a" - "206170706c69636174696f6e2f6a736f6e0d0a417574686f72697a6174696f6e3a" - "204261736963205831396a6232397261575666587a6f354f4751354f4451334d57" - "4e6d4e6a67304e7a417a59546b7a4e32457a4e7a6b305a44466c4f4451314e6a5a" - "6d5954526b5a6a4a694d7a466b596a68684f4449345a4759344d6a566a4f546735" - "5a4749344f54566c0d0a436f6e74656e742d4c656e6774683a2034360d0a0d0a7b" - "226d6574686f64223a22676574626c6f636b636f756e74222c22706172616d7322" - "3a5b5d2c226964223a317d0a"; HTTPRequest req; std::vector buffer{TryParseHex(full_request).value()}; LineReader reader(buffer, MAX_HEADERS_SIZE); @@ -260,4 +278,65 @@ BOOST_AUTO_TEST_CASE(http_request_tests) BOOST_CHECK(!req.LoadBody(reader)); } } + +BOOST_AUTO_TEST_CASE(http_client_server_tests) +{ + // Queue of connected sockets returned by listening socket (represents network interface) + std::shared_ptr accepted_sockets{std::make_shared()}; + + CreateSock = [&accepted_sockets](int, int, int) { + // This is a mock Listening Socket that the HTTP server will "bind" to and + // listen to for incoming connections. We won't need to access its I/O + // pipes because we don't read or write directly to it. It will return + // Connected Sockets from the queue via its Accept() method. + return std::make_unique(std::make_shared(), accepted_sockets); + }; + + { + // I/O pipes of one mock Connected Socket we can read and write to. + std::shared_ptr connected_socket_pipes(std::make_shared()); + + // Insert the payload: a correctly formatted HTTP request + std::vector buffer{TryParseHex(full_request).value()}; + connected_socket_pipes->recv.PushBytes(buffer.data(), buffer.size()); + + // Mock Connected Socket that represents a client. + // It needs I/O pipes but its queue can remain empty + std::unique_ptr connected_socket{std::make_unique(connected_socket_pipes, std::make_shared())}; + + // Prepare queue of accepted_sockets: just one connection with no data + accepted_sockets->Push(std::move(connected_socket)); + + // Instantiate server + HTTPServer server = HTTPServer(); + BOOST_REQUIRE(server.m_no_clients); + + // This address won't actually get used because we stubbed CreateSock() + const std::optional addr{Lookup("127.0.0.1", 8333, false)}; + bilingual_str strError; + // Bind to mock Listening Socket + BOOST_REQUIRE(server.BindAndStartListening(addr.value(), strError)); + // Start the I/O loop, accepting connections + SockMan::Options sockman_options; + server.StartSocketsThreads(sockman_options); + + // Wait up to one second for mock client to connect. + // Given that the mock client is itself a mock socket + // with hard-coded data it should only take a fraction of that. + int attempts{100}; + while (attempts > 0) + { + if (!server.m_no_clients) break; + + std::this_thread::sleep_for(10ms); + --attempts; + } + BOOST_REQUIRE(!server.m_no_clients); + + // Close server + server.interruptNet(); + // Wait for I/O loop to finish, after all sockets are closed + server.JoinSocketsThreads(); + } +} BOOST_AUTO_TEST_SUITE_END()