mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04:00
http: Begin implementation of HTTPClient and HTTPServer
This commit is contained in:
parent
67aa660c77
commit
df11fc1173
3 changed files with 185 additions and 12 deletions
|
@ -905,4 +905,17 @@ bool HTTPRequest::LoadBody(LineReader& reader)
|
||||||
|
|
||||||
return true;
|
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
|
} // namespace http_bitcoin
|
||||||
|
|
|
@ -12,6 +12,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <rpc/protocol.h>
|
#include <rpc/protocol.h>
|
||||||
|
#include <common/sockman.h>
|
||||||
#include <util/strencodings.h>
|
#include <util/strencodings.h>
|
||||||
#include <util/string.h>
|
#include <util/string.h>
|
||||||
|
|
||||||
|
@ -204,6 +205,7 @@ private:
|
||||||
|
|
||||||
namespace http_bitcoin {
|
namespace http_bitcoin {
|
||||||
using util::LineReader;
|
using util::LineReader;
|
||||||
|
using NodeId = SockMan::Id;
|
||||||
|
|
||||||
// shortest valid request line, used by libevent in evhttp_parse_request_line()
|
// 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")};
|
static const size_t MIN_REQUEST_LINE_LENGTH{strlen("GET / HTTP/1.0")};
|
||||||
|
@ -256,6 +258,85 @@ public:
|
||||||
bool LoadHeaders(LineReader& reader);
|
bool LoadHeaders(LineReader& reader);
|
||||||
bool LoadBody(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
|
} // namespace http_bitcoin
|
||||||
|
|
||||||
#endif // BITCOIN_HTTPSERVER_H
|
#endif // BITCOIN_HTTPSERVER_H
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <httpserver.h>
|
#include <httpserver.h>
|
||||||
#include <rpc/protocol.h>
|
#include <rpc/protocol.h>
|
||||||
|
#include <test/util/net.h>
|
||||||
#include <test/util/setup_common.h>
|
#include <test/util/setup_common.h>
|
||||||
#include <util/strencodings.h>
|
#include <util/strencodings.h>
|
||||||
|
|
||||||
|
@ -12,10 +13,38 @@
|
||||||
using http_bitcoin::HTTPHeaders;
|
using http_bitcoin::HTTPHeaders;
|
||||||
using http_bitcoin::HTTPRequest;
|
using http_bitcoin::HTTPRequest;
|
||||||
using http_bitcoin::HTTPResponse;
|
using http_bitcoin::HTTPResponse;
|
||||||
|
using http_bitcoin::HTTPServer;
|
||||||
using http_bitcoin::MAX_HEADERS_SIZE;
|
using http_bitcoin::MAX_HEADERS_SIZE;
|
||||||
using util::LineReader;
|
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)
|
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)
|
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;
|
HTTPRequest req;
|
||||||
std::vector<std::byte> buffer{TryParseHex<std::byte>(full_request).value()};
|
std::vector<std::byte> buffer{TryParseHex<std::byte>(full_request).value()};
|
||||||
LineReader reader(buffer, MAX_HEADERS_SIZE);
|
LineReader reader(buffer, MAX_HEADERS_SIZE);
|
||||||
|
@ -260,4 +278,65 @@ BOOST_AUTO_TEST_CASE(http_request_tests)
|
||||||
BOOST_CHECK(!req.LoadBody(reader));
|
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 minute 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{6000};
|
||||||
|
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()
|
BOOST_AUTO_TEST_SUITE_END()
|
||||||
|
|
Loading…
Add table
Reference in a new issue