http: read requests from connected clients

This commit is contained in:
Matthew Zipkin 2024-10-31 13:34:19 -04:00 committed by Matthew Zipkin
parent 2658144186
commit 5a2d625e08
No known key found for this signature in database
GPG Key ID: E7E2984B6289C93A
3 changed files with 123 additions and 4 deletions

View File

@ -906,6 +906,24 @@ bool HTTPRequest::LoadBody(LineReader& reader)
return true;
}
bool HTTPClient::ReadRequest(std::unique_ptr<HTTPRequest>& req)
{
LineReader reader(m_recv_buffer, MAX_HEADERS_SIZE);
if (!req->LoadControlData(reader)) return false;
if (!req->LoadHeaders(reader)) return false;
if (!req->LoadBody(reader)) return false;
// Remove the bytes read out of the buffer.
// If one of the above calls throws an error, the caller must
// catch it and disconnect the client.
m_recv_buffer.erase(
m_recv_buffer.begin(),
m_recv_buffer.begin() + (reader.it - reader.start));
return true;
}
bool HTTPServer::EventNewConnectionAccepted(NodeId node_id,
const CService& me,
const CService& them)
@ -918,4 +936,63 @@ bool HTTPServer::EventNewConnectionAccepted(NodeId node_id,
m_no_clients = false;
return true;
}
void HTTPServer::EventGotData(NodeId node_id, std::span<const uint8_t> data)
{
// Get the HTTPClient
auto client{GetClientById(node_id)};
if (client == nullptr) {
return;
}
// Copy data from socket buffer to client receive buffer
client->m_recv_buffer.insert(
client->m_recv_buffer.end(),
reinterpret_cast<const std::byte*>(data.data()),
reinterpret_cast<const std::byte*>(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<HTTPRequest>(client);
try {
// Stop reading if we need more data from the client to parse a complete request
if (!client->ReadRequest(req)) break;
} catch (const std::runtime_error& e) {
LogDebug(
BCLog::HTTP,
"Error reading HTTP request from client %s (id=%lld): %s\n",
client->m_origin,
client->m_node_id,
e.what());
// We failed to read a complete request from the buffer
// TODO: respond with HTTP_BAD_REQUEST and disconnect
break;
}
// We read a complete request from the buffer into the queue
LogDebug(
BCLog::HTTP,
"Received a %s request for %s from %s (id=%lld)\n",
req->m_method,
req->m_target,
req->m_client->m_origin,
req->m_client->m_node_id);
// handle request
m_request_dispatcher(std::move(req));
}
}
std::shared_ptr<HTTPClient> HTTPServer::GetClientById(NodeId node_id) const
{
auto it{m_connected_clients.find(node_id)};
if (it != m_connected_clients.end()) {
return it->second;
}
return nullptr;
}
} // namespace http_bitcoin

View File

@ -240,6 +240,8 @@ public:
std::string StringifyHeaders() const;
};
class HTTPClient;
class HTTPRequest
{
public:
@ -251,6 +253,13 @@ public:
HTTPHeaders m_headers;
std::string m_body;
// Keep a pointer to the client that made the request so
// we know who to respond to.
std::shared_ptr<HTTPClient> m_client;
explicit HTTPRequest(std::shared_ptr<HTTPClient> client) : m_client(client) {};
// Null client for unit tests
explicit HTTPRequest() : m_client(nullptr) {};
// Readers return false if they need more data from the
// socket to parse properly. They throw errors if
// the data is invalid.
@ -274,11 +283,19 @@ public:
// Ok to remain null for unit tests.
HTTPServer* m_server;
// In lieu of an intermediate transport class like p2p uses,
// we copy data from the socket buffer to the client object
// and attempt to read HTTP requests from here.
std::vector<std::byte> m_recv_buffer{};
explicit HTTPClient(NodeId node_id, CService addr) : m_node_id(node_id), m_addr(addr)
{
m_origin = addr.ToStringAddrPort();
};
// Try to read an HTTP request from the receive buffer
bool ReadRequest(std::unique_ptr<HTTPRequest>& req);
// Disable copies (should only be used as shared pointers)
HTTPClient(const HTTPClient&) = delete;
HTTPClient& operator=(const HTTPClient&) = delete;
@ -287,13 +304,19 @@ public:
class HTTPServer : public SockMan
{
public:
explicit HTTPServer(std::function<void(std::unique_ptr<HTTPRequest>)> 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<NodeId, std::shared_ptr<HTTPClient>> m_connected_clients;
// What to do with HTTP requests once received, validated and parsed
std::function<void(std::unique_ptr<HTTPRequest>)> m_request_dispatcher;
std::shared_ptr<HTTPClient> GetClientById(NodeId node_id) const;
/**
* Be notified when a new connection has been accepted.
* @param[in] node_id Id of the newly accepted connection.
@ -320,7 +343,7 @@ public:
* @param[in] node_id Connection for which the data arrived.
* @param[in] data Received data.
*/
virtual void EventGotData(NodeId node_id, std::span<const uint8_t> data) override {};
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

View File

@ -10,6 +10,7 @@
#include <boost/test/unit_test.hpp>
using http_bitcoin::HTTPClient;
using http_bitcoin::HTTPHeaders;
using http_bitcoin::HTTPRequest;
using http_bitcoin::HTTPResponse;
@ -307,8 +308,17 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests)
// Prepare queue of accepted_sockets: just one connection with no data
accepted_sockets->Push(std::move(connected_socket));
// Instantiate server
HTTPServer server = HTTPServer();
// Prepare a request handler that just stores received requests so we can examine them
// Mutex is required to prevent a race between this test's main thread and the Sockman I/O loop.
Mutex requests_mutex;
std::deque<std::unique_ptr<HTTPRequest>> requests;
auto StoreRequest = [&](std::unique_ptr<HTTPRequest> req) {
LOCK(requests_mutex);
requests.push_back(std::move(req));
};
// Instantiate server with dead-end request handler
HTTPServer server = HTTPServer(StoreRequest);
BOOST_REQUIRE(server.m_no_clients);
// This address won't actually get used because we stubbed CreateSock()
@ -333,6 +343,15 @@ BOOST_AUTO_TEST_CASE(http_client_server_tests)
}
BOOST_REQUIRE(!server.m_no_clients);
{
LOCK(requests_mutex);
// Connected client should have one request already from the static content.
BOOST_CHECK_EQUAL(requests.size(), 1);
// Check the received request
BOOST_CHECK_EQUAL(requests.front()->m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n");
}
// Close server
server.interruptNet();
// Wait for I/O loop to finish, after all sockets are closed