http: Begin implementation of HTTPClient and HTTPServer

This commit is contained in:
Matthew Zipkin 2024-10-30 16:11:20 -04:00 committed by Matthew Zipkin
parent 34e03406cf
commit 2658144186
No known key found for this signature in database
GPG Key ID: E7E2984B6289C93A
3 changed files with 185 additions and 12 deletions

View File

@ -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<HTTPClient>(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

View File

@ -12,6 +12,7 @@
#include <string>
#include <rpc/protocol.h>
#include <common/sockman.h>
#include <util/strencodings.h>
#include <util/string.h>
@ -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<NodeId, std::shared_ptr<HTTPClient>> 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<const uint8_t> 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

View File

@ -4,6 +4,7 @@
#include <httpserver.h>
#include <rpc/protocol.h>
#include <test/util/net.h>
#include <test/util/setup_common.h>
#include <util/strencodings.h>
@ -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<std::byte> buffer{TryParseHex<std::byte>(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<DynSock::Queue> accepted_sockets{std::make_shared<DynSock::Queue>()};
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<DynSock>(std::make_shared<DynSock::Pipes>(), accepted_sockets);
};
{
// I/O pipes of one mock Connected Socket we can read and write to.
std::shared_ptr<DynSock::Pipes> connected_socket_pipes(std::make_shared<DynSock::Pipes>());
// Insert the payload: a correctly formatted HTTP request
std::vector<std::byte> buffer{TryParseHex<std::byte>(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<DynSock> connected_socket{std::make_unique<DynSock>(connected_socket_pipes, std::make_shared<DynSock::Queue>())};
// 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<CService> 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()