From 34e03406cf0782d451eb88e67dd902559d57124c Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Wed, 16 Oct 2024 14:18:45 -0400 Subject: [PATCH] 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 --- src/httpserver.cpp | 54 +++++++++++++ src/httpserver.h | 27 +++++++ src/test/httpserver_tests.cpp | 138 ++++++++++++++++++++++++++++++++++ 3 files changed, 219 insertions(+) diff --git a/src/httpserver.cpp b/src/httpserver.cpp index e42e40098dc..9fafc20104b 100644 --- a/src/httpserver.cpp +++ b/src/httpserver.cpp @@ -784,6 +784,8 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch) namespace http_bitcoin { +using util::SplitString; + std::optional 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 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 version_parts{SplitString(parts[2].substr(5), ".")}; + if (version_parts.size() != 2) throw std::runtime_error("HTTP request line malformed"); + auto major = ToIntegral(version_parts[0]); + auto minor = ToIntegral(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 diff --git a/src/httpserver.h b/src/httpserver.h index d78fed17018..2e92d2f9985 100644 --- a/src/httpserver.h +++ b/src/httpserver.h @@ -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 diff --git a/src/test/httpserver_tests.cpp b/src/test/httpserver_tests.cpp index 29cf4145118..5b5af4b98e8 100644 --- a/src/test/httpserver_tests.cpp +++ b/src/test/httpserver_tests.cpp @@ -10,7 +10,10 @@ #include 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 buffer{TryParseHex(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 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 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 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 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 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 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 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 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 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 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 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()