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:
Matthew Zipkin 2024-10-16 14:18:45 -04:00 committed by Matthew Zipkin
parent 12dbb0d4ca
commit 34e03406cf
No known key found for this signature in database
GPG Key ID: E7E2984B6289C93A
3 changed files with 219 additions and 0 deletions

View File

@ -784,6 +784,8 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
namespace http_bitcoin {
using util::SplitString;
std::optional<std::string> HTTPHeaders::Find(const std::string key) const
{
const auto it = m_map.find(key);
@ -851,4 +853,56 @@ std::string HTTPResponse::StringifyHeaders() const
{
return strprintf("HTTP/%d.%d %d %s\r\n%s", m_version_major, m_version_minor, m_status, m_reason, m_headers.Stringify());
}
bool HTTPRequest::LoadControlData(LineReader& reader)
{
auto maybe_line = reader.ReadLine();
if (!maybe_line) return false;
std::string 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");
const std::vector<std::string> parts{SplitString(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");
const std::vector<std::string> version_parts{SplitString(parts[2].substr(5), ".")};
if (version_parts.size() != 2) throw std::runtime_error("HTTP request line malformed");
auto major = ToIntegral<int>(version_parts[0]);
auto minor = ToIntegral<int>(version_parts[1]);
if (!major || !minor) throw std::runtime_error("HTTP request line malformed");
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_value{m_headers.Find("Content-Length")};
if (!content_length_value) return true;
uint64_t content_length;
if (!ParseUInt64(content_length_value.value(), &content_length)) throw std::runtime_error("Cannot parse Content-Length value");
// Not enough data in buffer for expected body
if (reader.Left() < content_length) return false;
m_body = reader.ReadLength(content_length);
return true;
}
} // namespace http_bitcoin

View File

@ -203,6 +203,14 @@ private:
};
namespace http_bitcoin {
using util::LineReader;
// 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")};
// maximum size of http request (request line + headers)
// see https://github.com/bitcoin/bitcoin/issues/6425
static const size_t MAX_HEADERS_SIZE{8192};
class HTTPHeaders
{
public:
@ -229,6 +237,25 @@ public:
std::string StringifyHeaders() const;
};
class HTTPRequest
{
public:
std::string m_method;
std::string m_target;
// Default protocol version is used by error responses to unreadable requests
int m_version_major{1};
int m_version_minor{1};
HTTPHeaders m_headers;
std::string m_body;
// Readers return false if they need more data from the
// socket to parse properly. They throw errors if
// the data is invalid.
bool LoadControlData(LineReader& reader);
bool LoadHeaders(LineReader& reader);
bool LoadBody(LineReader& reader);
};
} // namespace http_bitcoin
#endif // BITCOIN_HTTPSERVER_H

View File

@ -10,7 +10,10 @@
#include <boost/test/unit_test.hpp>
using http_bitcoin::HTTPHeaders;
using http_bitcoin::HTTPRequest;
using http_bitcoin::HTTPResponse;
using http_bitcoin::MAX_HEADERS_SIZE;
using util::LineReader;
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
@ -122,4 +125,139 @@ BOOST_AUTO_TEST_CASE(http_response_tests)
"Date: Tue, 15 Oct 2024 17:54:12 GMT\r\n"
"\r\n");
}
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);
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.Find("Host").value(), "127.0.0.1");
BOOST_CHECK_EQUAL(req.m_headers.Find("Connection").value(), "close");
BOOST_CHECK_EQUAL(req.m_headers.Find("Content-Type").value(), "application/json");
BOOST_CHECK_EQUAL(req.m_headers.Find("Authorization").value(), "Basic X19jb29raWVfXzo5OGQ5ODQ3MWNmNjg0NzAzYTkzN2EzNzk0ZDFlODQ1NjZmYTRkZjJiMzFkYjhhODI4ZGY4MjVjOTg5ZGI4OTVl");
BOOST_CHECK_EQUAL(req.m_headers.Find("Content-Length").value(), "46");
BOOST_CHECK_EQUAL(req.m_body.size(), 46);
BOOST_CHECK_EQUAL(req.m_body, "{\"method\":\"getblockcount\",\"params\":[],\"id\":1}\n");
}
{
const std::string too_short_request_line = "GET/HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(too_short_request_line)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error);
}
{
const std::string malformed_request_line = "GET / HTTP / 1.0\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(malformed_request_line)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error);
}
{
const std::string malformed_request_line = "GET / HTTP1.0\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(malformed_request_line)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error);
}
{
const std::string malformed_request_line = "GET / HTTP/11\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(malformed_request_line)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error);
}
{
const std::string malformed_request_line = "GET / HTTP/1.x\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(malformed_request_line)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK_THROW(req.LoadControlData(reader), std::runtime_error);
}
{
const std::string ok_request_line = "GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(ok_request_line)};
LineReader reader(buffer, 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.Find("Host").value(), "127.0.0.1");
// no body is OK
BOOST_CHECK_EQUAL(req.m_body.size(), 0);
}
{
const std::string malformed_headers = "GET / HTTP/1.0\r\nHost=127.0.0.1\r\n\r\n";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(malformed_headers)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK(req.LoadControlData(reader));
BOOST_CHECK_THROW(req.LoadHeaders(reader), std::runtime_error);
}
{
// 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.
const std::string incomplete_headers = "GET / HTTP/1.0\r\nHost: ";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(incomplete_headers)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK(req.LoadControlData(reader));
BOOST_CHECK(!req.LoadHeaders(reader));
}
{
const std::string no_content_length = "GET / HTTP/1.0\r\n\r\n{\"method\":\"getblockcount\"}";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(no_content_length)};
LineReader reader(buffer, 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);
}
{
const std::string bad_content_length = "GET / HTTP/1.0\r\nContent-Length: eleven\r\n\r\n{\"method\":\"getblockcount\"}";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(bad_content_length)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK(req.LoadControlData(reader));
BOOST_CHECK(req.LoadHeaders(reader));
BOOST_CHECK_THROW(req.LoadBody(reader), std::runtime_error);
}
{
// Content-Length indicates more data than we have in the buffer.
// Again, not an error just try again later.
const std::string excessive_content_length = "GET / HTTP/1.0\r\nContent-Length: 1024\r\n\r\n{\"method\":\"getblockcount\"}";
HTTPRequest req;
std::vector<std::byte> buffer{StringToBuffer(excessive_content_length)};
LineReader reader(buffer, MAX_HEADERS_SIZE);
BOOST_CHECK(req.LoadControlData(reader));
BOOST_CHECK(req.LoadHeaders(reader));
BOOST_CHECK(!req.LoadBody(reader));
}
}
BOOST_AUTO_TEST_SUITE_END()