mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-03-17 13:22:03 +01:00
http: Begin implementation of HTTPClient and HTTPServer
This commit is contained in:
parent
34e03406cf
commit
2658144186
@ -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
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user