Files
bitcoin/src/httpserver.h
merge-script 4800cb7aea Merge bitcoin/bitcoin#35735: Add state to HTTPRequest
9954aa7728 http: don't parse any new requests from a client if m_req_busy = true (Matthew Zipkin)
c7db3ae1f9 test: cover HTTPRequest state machine (Matthew Zipkin)
90676e24ad Add state to HTTPRequest to avoid duplicate work over I/O cycles (Matthew Zipkin)
507e528e84 http: reuse HTTPHeaders to parse chunked trailer (Matthew Zipkin)
902d8908c9 http: only read one HTTPRequest at a time per client (Matthew Zipkin)

Pull request description:

  This PR reduces the memory consumption of the HTTP Server when reading data from connected clients, and improves performance especially when requests are large (i.e. requiring multiple TCP packets).

  In https://github.com/bitcoin/bitcoin/pull/35182 the server copies as much data as it can from the socket into application memory, and then tries to parse as many complete HTTP requests as possible from that data. If a request is discovered to be incomplete, the in-progress request is abandoned. The server tries again on the next I/O cycle to read the same data from the buffer, duplicating work as many times as it takes before the client finishes sending the request (or times out).

  This PR implements two improvements to this:
  1. Only parse one request at a time from the receive buffer. The server processes requests from each client in series anyway.
  2. Add state to `HTTPRequest` so it can be filled with data from the receive buffer over multiple I/O loop iterations without losing progress.

  If a client sends large or multiple requests, that data will sit in the kernel's socket buffer instead of the application memory. Eventually the socket buffer will fill up and TCP backpressure will kick in, dropping the TCP window to 0 and blocking the client from sending any more.

  A state machine for `HTTPRemoteClient` was [discussed previously](https://github.com/bitcoin/bitcoin/pull/35182#pullrequestreview-4322490068) to control resource consumption. Another nice benefit of this model (for a follow-up PR) will be to insert the RPC authentication check after reading 8kB-limited headers but before the 32MB-limited request body.

ACKs for top commit:
  winterrdog:
    re-ACK 9954aa7728
  janb84:
    re ACK 9954aa7728
  frankomosh:
    ACK 9954aa7728.
  fjahr:
    ACK 9954aa7728

Tree-SHA512: b7c913114283fbf1f360b40f6c65a01390a26731bf3b166f460ec260f9206f25d738b3a06887bfa839911c1c6aaf634448181da47a752a9a881aebd907e44868
2026-08-17 10:19:34 +01:00

622 lines
22 KiB
C++

// Copyright (c) 2015-present The Bitcoin Core developers
// Distributed under the MIT software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
#ifndef BITCOIN_HTTPSERVER_H
#define BITCOIN_HTTPSERVER_H
#include <atomic>
#include <functional>
#include <memory>
#include <optional>
#include <span>
#include <stdexcept>
#include <string>
#include <vector>
#include <netaddress.h>
#include <rpc/protocol.h>
#include <util/byte_units.h>
#include <util/expected.h>
#include <util/sock.h>
#include <util/strencodings.h>
#include <util/string.h>
#include <util/threadinterrupt.h>
#include <util/time.h>
namespace util {
class SignalInterrupt;
} // namespace util
/**
* The default value for `-rpcthreads`. This number of threads will be created at startup.
*/
inline constexpr int DEFAULT_HTTP_THREADS=16;
/**
* The default value for `-rpcworkqueue`. This is the maximum depth of the work queue,
* we don't allocate this number of work queue items upfront.
*/
inline constexpr int DEFAULT_HTTP_WORKQUEUE=64;
inline constexpr int DEFAULT_HTTP_SERVER_TIMEOUT=30;
enum class HTTPRequestMethod {
UNKNOWN,
GET,
POST,
HEAD,
PUT
};
namespace http_bitcoin {
class HTTPRequest;
}
/** Handler for requests to a certain HTTP path */
using HTTPRequestHandler = std::function<void(http_bitcoin::HTTPRequest* req, const std::string&)>;
/** Register handler for prefix.
* If multiple handlers match a prefix, the first-registered one will
* be invoked.
*/
void RegisterHTTPHandler(const std::string &prefix, bool exactMatch, const HTTPRequestHandler &handler);
/** Unregister handler for prefix */
void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch);
namespace http_bitcoin {
using util::LineReader;
//! Shortest valid request line, used by libevent in evhttp_parse_request_line()
inline constexpr size_t MIN_REQUEST_LINE_LENGTH = std::string_view("GET / HTTP/1.0").size();
//! Maximum size of each headers line in an HTTP request,
//! also the maximum size of all headers total.
//! See https://github.com/bitcoin/bitcoin/pull/6859
//! And libevent http.c evhttp_parse_headers_()
inline constexpr size_t MAX_HEADERS_SIZE{8192};
//! Maximum size of an HTTP request body
inline constexpr uint64_t MAX_BODY_SIZE{32_MiB};
//! Thrown when a request body exceeds MAX_BODY_SIZE (or *will* exceed, in chunked transfer)
//! so the server can reply with more specific code 413 (content too large) vs general 400 (bad request)
struct ContentTooLargeError : std::runtime_error {
using std::runtime_error::runtime_error;
};
class HTTPHeaders
{
public:
/**
* @param[in] key The field-name of the header to search for
* @returns The value of the first header that matches the provided key
* nullopt if key is not found
*/
std::optional<std::string> FindFirst(std::string_view key) const;
/**
* @param[in] key The field-name of the header to search for
* @returns Views into all values matching the provided key (valid while this object is alive)
*/
std::vector<std::string_view> FindAll(std::string_view key) const;
void Write(std::string&& key, std::string&& value);
/**
* @param[in] key The field-name of the header to search for and delete
*/
void RemoveAll(std::string_view key);
/**
* @param[in] reader A LineReader instance initialized with the client's receive buffer.
* @param[in] write Whether or not to write the parsed data to the object after validation.
* @returns false if LineReader hits the end of the buffer before reading an
* \n, meaning that we are still waiting on more data from the client.
* true after reading an entire HTTP headers section, terminated
* by an empty line and \n.
* @throws on exceeded read limit and on bad headers syntax (e.g. no ":" in a line)
*/
bool Read(util::LineReader& reader, bool write = true);
std::string Stringify() const;
private:
/**
* Headers can have duplicate field names, so we use a vector of key-value pairs instead of a map.
* https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
*/
std::vector<std::pair<std::string, std::string>> m_headers;
//! Track total bytes consumed in Read() for limit checks
size_t m_consumed{0};
};
struct HTTPVersion {
/**
* Default HTTP protocol version 1.1 is used by error responses
* when a request is unreadable.
*/
/// @{
uint8_t major{1};
uint8_t minor{1};
/// @}
};
class HTTPResponse
{
public:
HTTPVersion m_version;
HTTPStatusCode m_status{HTTP_INTERNAL_SERVER_ERROR};
HTTPHeaders m_headers;
std::string StringifyHeaders() const;
};
class HTTPRemoteClient;
class HTTPRequest
{
public:
HTTPRequestMethod m_method;
std::string m_target;
HTTPVersion m_version;
HTTPHeaders m_headers;
std::string m_body;
//! Pointer to the client that made the request so we know who to respond to.
std::shared_ptr<HTTPRemoteClient> m_client;
//! Response headers may be set in advance before response body is known
HTTPHeaders m_response_headers;
explicit HTTPRequest(std::shared_ptr<HTTPRemoteClient> client) : m_client{std::move(client)} {}
//! Construct with a null client for unit tests
explicit HTTPRequest() : m_client{} {}
/**
* Methods that attempt to parse HTTP request fields line-by-line
* from a receive buffer.
* @param[in] reader A LineReader object constructed over a span of data.
* @returns true If the request field was parsed.
* false If there was not enough data in the buffer to complete the field.
* @throws std::runtime_error if data is invalid.
*/
/// @{
bool LoadControlData(LineReader& reader);
bool LoadHeaders(LineReader& reader);
bool LoadBody(LineReader& reader);
/// @}
void WriteReply(HTTPStatusCode status, std::span<const std::byte> reply_body = {});
void WriteReply(HTTPStatusCode status, std::string_view reply_body_view)
{
WriteReply(status, std::as_bytes(std::span{reply_body_view}));
}
// These methods reimplement the API from http_libevent::HTTPRequest
// for downstream JSONRPC and REST modules.
std::string GetURI() const { return m_target; }
CService GetPeer() const;
HTTPRequestMethod GetRequestMethod() const { return m_method; }
std::optional<std::string> GetQueryParameter(std::string_view key) const;
std::pair<bool, std::string> GetHeader(std::string_view hdr) const;
std::string ReadBody() const { return m_body; }
void WriteHeader(std::string&& hdr, std::string&& value);
enum class State {
Init,
NeedsHeaders,
NeedsBody,
Complete,
Error
};
State GetState() const { return m_state; }
void SetState(State state) { m_state = state; }
// If a large request is sent with "Transfer-encoding: chunked" we may
// read the chunk size in a separate I/O loop iteration than the chunk
// of data itself. Store the chunk size value here until the chunk is read.
std::optional<uint64_t> m_chunk_size;
// We may also read a large chunk over multiple loop iterations.
// Track the progress of the chunk here.
uint64_t m_chunk_read{0};
private:
State m_state = State::Init;
};
class HTTPServer
{
public:
/**
* Each connection is assigned an unique id of this type.
*/
using Id = uint64_t;
explicit HTTPServer(std::function<void(std::unique_ptr<HTTPRequest>&&)> func)
: m_request_dispatcher{std::move(func)} {}
virtual ~HTTPServer()
{
Assume(!m_thread_socket_handler.joinable()); // Missing call to JoinSocketsThreads()
Assume(m_connected.empty()); // Missing call to DisconnectClients(), or disconnect flags not set
Assume(m_listen.empty()); // Missing call to StopListening()
}
/**
* Parse the user's -rpcallowip settings and populate m_allow_subnets
*/
bool InitHTTPAllowList();
/**
* Bind to a new address:port, start listening and add the listen socket to `m_listen`.
* @param[in] to Where to bind.
* @returns {} or the reason for failure.
*/
util::Expected<void, std::string> BindAndStartListening(const CService& to);
/**
* Stop listening by closing all listening sockets.
*/
void StopListening();
/**
* Get the number of sockets the server is bound to and listening on
*/
size_t GetListeningSocketCount() const { return m_listen.size(); }
/**
* Get the number of HTTPRemoteClients we are connected to
*/
size_t GetConnectionsCount() const { return m_connected_size.load(std::memory_order_acquire); }
/**
* Start the necessary threads for sockets IO.
*/
void StartSocketsThreads();
/**
* Join (wait for) the threads started by `StartSocketsThreads()` to exit.
*/
void JoinSocketsThreads();
/**
* Stop network activity
*/
void InterruptNet() { m_interrupt_net(); }
/**
* Start disconnecting clients when possible in the I/O loop
*/
void DisconnectAllClients() { m_disconnect_all_clients = true; }
/**
* Update the request handler method.
* Used for shutdown to reject new requests.
*/
void SetRequestHandler(std::function<void(std::unique_ptr<HTTPRequest>&&)> func)
EXCLUSIVE_LOCKS_REQUIRED(!m_request_dispatcher_mutex)
{
WITH_LOCK(m_request_dispatcher_mutex,
m_request_dispatcher = std::move(func));
}
/**
* Stop accepting new connections in the I/O loop.
* Must be called first in StopHTTPServer() before DisconnectAllClients().
* A connection accepted after the "wait for 0 connections" loop exits would
* remain in m_connected when the destructor is called.
*/
void StopAccepting() { m_stop_accepting = true; }
/**
* Set the idle client timeout (-rpcservertimeout)
*/
void SetServerTimeout(std::chrono::seconds seconds) { m_rpcservertimeout = seconds; }
/**
* Force-remove all remaining clients from m_connected without waiting for
* graceful disconnection. Must only be called after JoinSocketsThreads().
*/
void ClearConnectedClients();
private:
/**
* List of listening sockets.
*/
std::vector<std::shared_ptr<Sock>> m_listen;
/**
* The id to assign to the next created connection.
*/
std::atomic<Id> m_next_id{0};
/**
* List of HTTPRemoteClients with connected sockets.
* Connections will only be added and removed in the I/O thread, but
* shared pointers may be passed to worker threads to handle requests
* and send replies.
*/
std::vector<std::shared_ptr<HTTPRemoteClient>> m_connected;
/**
* Flag used during shutdown to stop accepting new connections.
* Set by main thread and read by the I/O thread.
*/
std::atomic_bool m_stop_accepting{false};
/**
* Flag used during shutdown.
* Overrides HTTPRemoteClient flags m_keep_alive and m_connection_busy.
* Set by main thread and read by the I/O thread.
*/
std::atomic_bool m_disconnect_all_clients{false};
/**
* The number of connected sockets.
* Updated from the I/O thread but safely readable from
* the main thread without locks.
*/
std::atomic<size_t> m_connected_size{0};
/**
* Info about which socket has which event ready and a reverse map
* back to the HTTPRemoteClient that owns the socket.
*/
struct IOReadiness {
/**
* Map of socket -> socket events. For example:
* socket1 -> { requested = SendEvent|RecvEvent, occurred = RecvEvent }
* socket2 -> { requested = SendEvent, occurred = SendEvent }
*/
Sock::EventsPerSock events_per_sock;
/**
* Map of socket -> HTTPRemoteClient. For example:
* socket1 -> HTTPRemoteClient{ id=23 }
* socket2 -> HTTPRemoteClient{ id=56 }
*/
std::unordered_map<Sock::EventsPerSock::key_type,
std::shared_ptr<HTTPRemoteClient>,
Sock::HashSharedPtrSock,
Sock::EqualSharedPtrSock>
httpclients_per_sock;
};
/**
* This is signaled when network activity should cease.
*/
CThreadInterrupt m_interrupt_net;
/**
* Thread that sends to and receives from sockets and accepts connections.
* Executes the I/O loop of the server.
*/
std::thread m_thread_socket_handler;
/*
* What to do with HTTP requests once received, validated and parsed.
* Set in main thread by server start and interrupt but read in
* worker threads.
*/
/// @{
mutable Mutex m_request_dispatcher_mutex;
std::function<void(std::unique_ptr<HTTPRequest>&&)> m_request_dispatcher GUARDED_BY(m_request_dispatcher_mutex);
/// @}
/**
* Idle timeout after which clients are disconnected
*/
std::chrono::seconds m_rpcservertimeout{DEFAULT_HTTP_SERVER_TIMEOUT};
/**
* List of subnets to allow HTTP connections from
*/
std::vector<CSubNet> m_allow_subnets;
/**
* Check an incoming connection's source IP against the allow list
*/
bool ClientAllowed(const CNetAddr& netaddr) const;
/**
* Accept a connection.
* @param[in] listen_sock Socket on which to accept the connection.
* @param[out] addr Address of the peer that was accepted.
* @return Newly created socket for the accepted connection.
*/
std::unique_ptr<Sock> AcceptConnection(const Sock& listen_sock, CService& addr);
/**
* Generate an id for a newly created connection.
*/
Id GetNewId();
/**
* After a new socket with a client has been created, configure its flags,
* make a new HTTPRemoteClient and Id and save its shared pointer.
* @param[in] sock The newly created socket.
* @param[in] addr Address of the new peer.
*/
void NewSockAccepted(std::unique_ptr<Sock>&& sock, const CService& addr);
/**
* Do the read/write for connected sockets that are ready for IO.
* @param[in] io_readiness Which sockets are ready and their corresponding HTTPRemoteClients.
*/
void SocketHandlerConnected(const IOReadiness& io_readiness) const
EXCLUSIVE_LOCKS_REQUIRED(!m_request_dispatcher_mutex);
/**
* Accept incoming connections, one from each read-ready listening socket.
* @param[in] events_per_sock Sockets that are ready for IO.
*/
void SocketHandlerListening(const Sock::EventsPerSock& events_per_sock);
/**
* Generate a collection of sockets to check for IO readiness.
* @return Sockets to check for readiness plus an aux map to find the
* corresponding HTTPRemoteClient given a socket.
*/
IOReadiness GenerateWaitSockets() const;
/**
* Check connected and listening sockets for IO readiness and process them accordingly.
* This is the main I/O loop of the server.
*/
void ThreadSocketHandler() EXCLUSIVE_LOCKS_REQUIRED(!m_request_dispatcher_mutex);
/**
* Try to read HTTPRequests from a client's receive buffer.
* Complete requests are dispatched, incomplete requests are
* left in the buffer to wait for more data. Some read errors
* will mark this client for disconnection.
* @param[in] client The HTTPRemoteClient to read requests from
*/
void MaybeDispatchRequestsFromClient(const std::shared_ptr<HTTPRemoteClient>& client) const
EXCLUSIVE_LOCKS_REQUIRED(!m_request_dispatcher_mutex);
/**
* Close underlying socket connections for flagged clients
* by removing their shared pointer from m_connected. If an HTTPRemoteClient
* is busy in a worker thread, its connection will be closed once that
* job is done and the HTTPRequest is out of scope.
*/
void DisconnectClients();
};
std::optional<std::string> GetQueryParameterFromUri(std::string_view uri, std::string_view key);
class HTTPRemoteClient
{
public:
//! ID provided by HTTPServer upon connection and instantiation
const HTTPServer::Id m_id;
//! Remote address of connected client
const CService m_addr;
//! IP:port of connected client, cached for logging purposes
const std::string m_origin;
/**
* 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::string m_recv_buffer{};
//! Requests from a client must be processed in the order in which
//! they were received, blocking on a per-client basis. We read
//! one request at a time from the socket buffer then pass it to a worker.
std::unique_ptr<HTTPRequest> m_req;
//! Set to true by the I/O thread when a request is popped off
//! and passed to a worker thread, reset to false by the worker thread.
std::atomic_bool m_req_busy{false};
/**
* Response data destined for this client.
* Written to by http worker threads, read and erased by HTTPServer I/O thread
*/
/// @{
Mutex m_send_mutex;
std::vector<std::byte> m_send_buffer GUARDED_BY(m_send_mutex);
/// @}
/**
* Set true by worker threads after writing a response to m_send_buffer.
* Set false by the HTTPServer I/O thread after flushing m_send_buffer.
* Checked in the HTTPServer I/O loop to decide whether to poll the socket for
* writeability or readability.
* Guarded by m_send_mutex so it stays consistent with m_send_buffer's emptiness:
* the two must always be updated together under the same lock.
*/
bool m_send_ready GUARDED_BY(m_send_mutex){false};
/**
* Mutex that serializes the Send() and Recv() calls on `m_sock`. Reading
* from the client occurs in the I/O thread but writing back to a client
* may occur in a worker thread.
*/
Mutex m_sock_mutex;
/**
* Underlying socket.
* `shared_ptr` (instead of `unique_ptr`) is used to avoid premature close of the
* underlying file descriptor by one thread while another thread is poll(2)-ing
* it for activity.
* @see https://github.com/bitcoin/bitcoin/issues/21744 for details.
*/
std::shared_ptr<Sock> m_sock GUARDED_BY(m_sock_mutex);
//! Initialized to true while server waits for first request from client.
//! Set to false after data is written to m_send_buffer and then that buffer is flushed to client.
//! Reset to true when we receive new request data from client.
//! Checked during DisconnectClients() and set by read/write operations
//! called in either the HTTPServer I/O loop or by a worker thread during an "optimistic send".
//! `m_connection_busy=true` can be overridden by `m_disconnect=true` (we disconnect).
std::atomic_bool m_connection_busy{true};
//! Client has requested to keep the connection open after all requests have been responded to.
//! Set by (potentially multiple) worker threads and checked in the HTTPServer I/O loop.
//! `m_keep_alive=true` can be overridden `by HTTPServer.m_disconnect_all_clients` (we disconnect).
std::atomic_bool m_keep_alive{false};
//! Flag this client for disconnection on next loop.
//! Either we have encountered a permanent error, or both sides of the socket are done
//! with the connection, e.g. our reply to a "Connection: close" request has been sent.
//! Might be set in a worker thread or in the I/O thread. When set to `true` we disconnect,
//! possibly overriding all other disconnect flags.
std::atomic_bool m_disconnect{false};
//! Timestamp of last send or receive activity, used for -rpcservertimeout.
//! Due to optimistic sends it may be updated in either a worker thread or in the
//! I/O thread. It is checked in the I/O thread to disconnect idle clients.
std::atomic<SteadySeconds> m_idle_since;
explicit HTTPRemoteClient(HTTPServer::Id id, const CService& addr, std::unique_ptr<Sock> socket)
: m_id(id), m_addr(addr), m_origin(addr.ToStringAddrPort()), m_sock{std::move(socket)}, m_idle_since{Now<SteadySeconds>()} {}
// Disable copies (should only be used as shared pointers)
HTTPRemoteClient(const HTTPRemoteClient&) = delete;
HTTPRemoteClient& operator=(const HTTPRemoteClient&) = delete;
//! Release any in-progress request. HTTPRequest holds a shared_ptr back to its
//! HTTPRemoteClient to keep the client alive from a worker thread. If a request
//! hasn't been moved to a worker yet it will prevent the client from destructing
//! and never close the socket. Therefore this must be called when disconnecting.
void ReleaseRequest() { m_req.reset(); }
/**
* Try to read an HTTP request from the receive buffer.
* Updates HTTPRequest.m_state and drains buffer on error.
* @param[in] req A HTTPRequest to read into
* @throws std::runtime_error if request is unreadable or violates protocol
*/
void ReadRequest(HTTPRequest& req);
/**
* Push data (if there is any) from client's m_send_buffer to the connected socket.
* @returns false if we are done with this client and HTTPServer can skip the next read operation from it.
*/
bool MaybeSendBytesFromBuffer() EXCLUSIVE_LOCKS_REQUIRED(!m_send_mutex, !m_sock_mutex);
};
/** Initialize HTTP server.
* Call this before RegisterHTTPHandler or EventBase().
*/
bool InitHTTPServer();
/** Start HTTP server.
* This is separate from InitHTTPServer to give users race-condition-free time
* to register their handlers between InitHTTPServer and StartHTTPServer.
*/
void StartHTTPServer();
/** Interrupt HTTP server threads */
void InterruptHTTPServer();
/** Stop HTTP server */
void StopHTTPServer();
} // namespace http_bitcoin
#endif // BITCOIN_HTTPSERVER_H