mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-07-18 02:50:57 +02:00
http: Implement HTTPRequest class
HTTP Request message: https://datatracker.ietf.org/doc/html/rfc1945#section-5 Request Line aka Control Line aka first line: https://datatracker.ietf.org/doc/html/rfc1945#section-5.1 See message_read_status() in libevent http.c for how `MORE_DATA_EXPECTED` is handled there
This commit is contained in:
@@ -717,6 +717,7 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
|
||||
}
|
||||
|
||||
namespace http_bitcoin {
|
||||
using util::Split;
|
||||
|
||||
std::optional<std::string> HTTPHeaders::FindFirst(const std::string_view key) const
|
||||
{
|
||||
@@ -818,4 +819,74 @@ std::string HTTPResponse::StringifyHeaders() const
|
||||
HTTPStatusReasonString(m_status),
|
||||
m_headers.Stringify());
|
||||
}
|
||||
|
||||
bool HTTPRequest::LoadControlData(LineReader& reader)
|
||||
{
|
||||
auto maybe_line = reader.ReadLine();
|
||||
if (!maybe_line) return false;
|
||||
const std::string_view& request_line = *maybe_line;
|
||||
|
||||
// Request Line aka Control Data https://httpwg.org/specs/rfc9110.html#rfc.section.6.2
|
||||
// Three words separated by spaces, terminated by \n or \r\n
|
||||
if (request_line.length() < MIN_REQUEST_LINE_LENGTH) throw std::runtime_error("HTTP request line too short");
|
||||
|
||||
// NUL is not a valid tchar and would silently truncate
|
||||
// C-string-based parsers rather than being rejected as malformed.
|
||||
// tchar: https://www.rfc-editor.org/info/rfc7230/#section-3.2.6
|
||||
if (request_line.find('\0') != std::string_view::npos) throw std::runtime_error("Invalid request line contains NUL");
|
||||
|
||||
const std::vector<std::string_view> parts{Split<std::string_view>(request_line, " ")};
|
||||
if (parts.size() != 3) throw std::runtime_error("HTTP request line malformed");
|
||||
m_method = parts[0];
|
||||
m_target = parts[1];
|
||||
|
||||
if (parts[2].rfind("HTTP/") != 0) throw std::runtime_error("HTTP request line malformed");
|
||||
|
||||
// Version is exactly two decimal digits separated by a decimal point
|
||||
// https://httpwg.org/specs/rfc9110.html#rfc.section.2.5
|
||||
const std::vector<std::string_view> version_parts{Split<std::string_view>(parts[2].substr(5), ".")};
|
||||
if (version_parts.size() != 2) throw std::runtime_error("HTTP request line malformed");
|
||||
if (version_parts[0].size() != 1 || version_parts[1].size() != 1) throw std::runtime_error("HTTP bad version");
|
||||
auto major = ToIntegral<uint8_t>(version_parts[0]);
|
||||
auto minor = ToIntegral<uint8_t>(version_parts[1]);
|
||||
if (!major || !minor || major != 1 || minor > 9) throw std::runtime_error("HTTP bad version");
|
||||
m_version.major = major.value();
|
||||
m_version.minor = minor.value();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool HTTPRequest::LoadHeaders(LineReader& reader)
|
||||
{
|
||||
return m_headers.Read(reader);
|
||||
}
|
||||
|
||||
bool HTTPRequest::LoadBody(LineReader& reader)
|
||||
{
|
||||
// https://httpwg.org/specs/rfc9112.html#message.body
|
||||
|
||||
// No Content-length or Transfer-Encoding header means no body, see libevent evhttp_get_body()
|
||||
// TODO: we must also implement Transfer-Encoding for chunk-reading
|
||||
auto content_length_values{m_headers.FindAll("Content-Length")};
|
||||
if (content_length_values.empty()) return true;
|
||||
|
||||
// Duplicate Content-Length headers are allowed only if they all have the same value
|
||||
// https://www.rfc-editor.org/rfc/rfc7230#section-3.3.3
|
||||
const auto& first_content_length_value{content_length_values[0]};
|
||||
for (size_t i = 1; i < content_length_values.size(); ++i) {
|
||||
if (content_length_values[i] != first_content_length_value) throw std::runtime_error("Differing Content-Length values");
|
||||
}
|
||||
|
||||
const auto content_length{ToIntegral<uint64_t>(first_content_length_value)};
|
||||
if (!content_length) throw std::runtime_error("Cannot parse Content-Length value");
|
||||
|
||||
if (*content_length > MAX_BODY_SIZE) throw ContentTooLargeError("Max body size exceeded");
|
||||
|
||||
// Not enough data in buffer for expected body
|
||||
if (reader.Remaining() < *content_length) return false;
|
||||
|
||||
m_body = reader.ReadLength(*content_length);
|
||||
|
||||
return true;
|
||||
}
|
||||
} // namespace http_bitcoin
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
#include <functional>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include <rpc/protocol.h>
|
||||
#include <util/byte_units.h>
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
|
||||
@@ -192,6 +194,10 @@ private:
|
||||
};
|
||||
|
||||
namespace http_bitcoin {
|
||||
using util::LineReader;
|
||||
|
||||
//! Shortest valid request line, used by libevent in evhttp_parse_request_line()
|
||||
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.
|
||||
@@ -199,6 +205,15 @@ namespace http_bitcoin {
|
||||
//! And libevent http.c evhttp_parse_headers_()
|
||||
constexpr size_t MAX_HEADERS_SIZE{8192};
|
||||
|
||||
//! Maximum size of an HTTP request body
|
||||
constexpr uint64_t MAX_BODY_SIZE{32_MiB};
|
||||
|
||||
//! Thrown when a request body exceeds MAX_BODY_SIZE
|
||||
//! 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:
|
||||
@@ -258,6 +273,30 @@ public:
|
||||
|
||||
std::string StringifyHeaders() const;
|
||||
};
|
||||
|
||||
class HTTPRequest
|
||||
{
|
||||
public:
|
||||
std::string m_method;
|
||||
std::string m_target;
|
||||
HTTPVersion m_version;
|
||||
HTTPHeaders m_headers;
|
||||
std::string m_body;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
/// @}
|
||||
};
|
||||
} // namespace http_bitcoin
|
||||
|
||||
#endif // BITCOIN_HTTPSERVER_H
|
||||
|
||||
@@ -11,8 +11,11 @@
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
using http_bitcoin::HTTPHeaders;
|
||||
using http_bitcoin::HTTPRequest;
|
||||
using http_bitcoin::HTTPResponse;
|
||||
using http_bitcoin::MAX_BODY_SIZE;
|
||||
using http_bitcoin::MAX_HEADERS_SIZE;
|
||||
using util::LineReader;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
|
||||
|
||||
@@ -165,4 +168,208 @@ BOOST_AUTO_TEST_CASE(http_response_tests)
|
||||
"Content-Length: 41\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(http_request_tests)
|
||||
{
|
||||
{
|
||||
// Reading request captured from bitcoin-cli
|
||||
constexpr std::string_view full_request = "POST / HTTP/1.1\r\n"
|
||||
"Host: 127.0.0.1\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Authorization: Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl\r\n"
|
||||
"Content-Length: 46\r\n"
|
||||
"\r\n"
|
||||
R"({"method":"getblockcount","params":[],"id":1})""\n";
|
||||
HTTPRequest req;
|
||||
LineReader reader(full_request, MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(req.LoadBody(reader));
|
||||
BOOST_CHECK_EQUAL(req.m_method, "POST");
|
||||
BOOST_CHECK_EQUAL(req.m_target, "/");
|
||||
BOOST_CHECK_EQUAL(req.m_version.major, 1);
|
||||
BOOST_CHECK_EQUAL(req.m_version.minor, 1);
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Connection"), "close");
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Type"), "application/json");
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Content-Length"), "46");
|
||||
BOOST_CHECK_EQUAL(req.m_body.size(), 46);
|
||||
BOOST_CHECK_EQUAL(req.m_body, R"({"method":"getblockcount","params":[],"id":1})""\n");
|
||||
}
|
||||
{
|
||||
// Malformed: no spaces between data
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
|
||||
}
|
||||
{
|
||||
// Malformed: too many spaces
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line malformed"});
|
||||
}
|
||||
{
|
||||
// Malformed: slash missing before version
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
|
||||
}
|
||||
{
|
||||
// Malformed: no decimal in version
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP request line too short"});
|
||||
}
|
||||
{
|
||||
// Malformed: version is not a number
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
|
||||
}
|
||||
{
|
||||
// Malformed: version is out of range
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/2.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
|
||||
}
|
||||
{
|
||||
// Malformed: version is out of range
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/0.9\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
|
||||
}
|
||||
{
|
||||
// Malformed: version is out of range
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/-1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
|
||||
}
|
||||
{
|
||||
// Malformed: version is not exactly two integers and a dot
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"HTTP bad version"});
|
||||
}
|
||||
{
|
||||
// Malformed: contains NUL
|
||||
HTTPRequest req;
|
||||
LineReader reader{std::string_view{"GET /safe\0/etc/passwd HTTP/1.00\r\nHost: 127.0.0.1\r\n\r\n", 50}, MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(req.LoadControlData(reader), std::runtime_error, HasReason{"Invalid request line contains NUL"});
|
||||
}
|
||||
{
|
||||
// Malformed: differing Content-Length values, case insensitive
|
||||
constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
|
||||
"Host: 127.0.0.1\n"
|
||||
"Content-Length: 8\n"
|
||||
"content-length: 9\n\n"
|
||||
"12345678";
|
||||
HTTPRequest req;
|
||||
util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Differing Content-Length values"});
|
||||
}
|
||||
{
|
||||
// Ok: multiple same Content-Length values
|
||||
constexpr std::string_view differing_length = "GET / HTTP/1.1\n"
|
||||
"Host: 127.0.0.1\n"
|
||||
"Content-Length: 8\n"
|
||||
"content-length: 8\n\n"
|
||||
"12345678";
|
||||
HTTPRequest req;
|
||||
util::LineReader reader{differing_length, /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(req.LoadBody(reader));
|
||||
}
|
||||
{
|
||||
// Ok
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(req.LoadBody(reader));
|
||||
BOOST_CHECK_EQUAL(req.m_method, "GET");
|
||||
BOOST_CHECK_EQUAL(req.m_target, "/");
|
||||
BOOST_CHECK_EQUAL(req.m_version.major, 1);
|
||||
BOOST_CHECK_EQUAL(req.m_version.minor, 0);
|
||||
BOOST_CHECK_EQUAL(req.m_headers.FindFirst("Host"), "127.0.0.1");
|
||||
// no body is OK
|
||||
BOOST_CHECK_EQUAL(req.m_body.size(), 0);
|
||||
}
|
||||
{
|
||||
// Malformed: missing colon
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK_EXCEPTION(req.LoadHeaders(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
|
||||
}
|
||||
{
|
||||
// We might not have received enough data from the client which is not
|
||||
// an error. We return false so the caller can try again later when the
|
||||
// buffer has more data.
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nHost: ", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(!req.LoadHeaders(reader));
|
||||
}
|
||||
{
|
||||
// No Content-Length: body is not read
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(req.LoadBody(reader));
|
||||
// Don't try to read request body if Content-Length is missing
|
||||
BOOST_CHECK_EQUAL(req.m_body.size(), 0);
|
||||
}
|
||||
{
|
||||
// Malformed: Content-Length is not a number
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
|
||||
}
|
||||
{
|
||||
// Malformed: Content-Length is negative
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nContent-Length: -8\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK_EXCEPTION(req.LoadBody(reader), std::runtime_error, HasReason{"Cannot parse Content-Length value"});
|
||||
}
|
||||
{
|
||||
// Content-Length exceeds limit
|
||||
constexpr auto excessive_size{MAX_BODY_SIZE + 1};
|
||||
std::string huge_body(excessive_size, 'x');
|
||||
const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(excessive_size) + "\r\n\r\n" + std::move(huge_body)};
|
||||
HTTPRequest req;
|
||||
LineReader reader(request, MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK_EXCEPTION(req.LoadBody(reader), http_bitcoin::ContentTooLargeError, HasReason{"Max body size exceeded"});
|
||||
}
|
||||
{
|
||||
// Content-Length exactly on the limit
|
||||
std::string max_body(MAX_BODY_SIZE, 'x');
|
||||
const std::string request{"GET / HTTP/1.0\r\nContent-Length: " + util::ToString(MAX_BODY_SIZE) + "\r\n\r\n" + std::move(max_body)};
|
||||
HTTPRequest req;
|
||||
LineReader reader(request, MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(req.LoadBody(reader));
|
||||
}
|
||||
{
|
||||
// Content-Length indicates more data than we have in the buffer.
|
||||
// Not an error; we wait for more data before completing the body.
|
||||
HTTPRequest req;
|
||||
LineReader reader("GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n" R"({"method":"getblockcount"})", MAX_HEADERS_SIZE);
|
||||
BOOST_CHECK(req.LoadControlData(reader));
|
||||
BOOST_CHECK(req.LoadHeaders(reader));
|
||||
BOOST_CHECK(!req.LoadBody(reader));
|
||||
}
|
||||
}
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user