mirror of
https://github.com/bitcoin/bitcoin.git
synced 2026-07-24 15:18:51 +02:00
http: Implement HTTPHeaders class
see: https://www.rfc-editor.org/rfc/rfc2616#section-4.2 https://www.rfc-editor.org/rfc/rfc7231#section-5 https://www.rfc-editor.org/rfc/rfc7231#section-7 https://httpwg.org/specs/rfc9111.html#header.field.definitions
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
@@ -714,3 +715,97 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
|
||||
pathHandlers.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
namespace http_bitcoin {
|
||||
|
||||
std::optional<std::string> HTTPHeaders::FindFirst(const std::string_view key) const
|
||||
{
|
||||
for (const auto& item : m_headers) {
|
||||
if (CaseInsensitiveEqual(key, item.first)) {
|
||||
return item.second;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::vector<std::string_view> HTTPHeaders::FindAll(const std::string_view key) const
|
||||
{
|
||||
std::vector<std::string_view> ret;
|
||||
for (const auto& item : m_headers) {
|
||||
if (CaseInsensitiveEqual(key, item.first)) {
|
||||
ret.push_back(item.second);
|
||||
}
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
void HTTPHeaders::Write(std::string&& key, std::string&& value)
|
||||
{
|
||||
m_headers.emplace_back(std::move(key), std::move(value));
|
||||
}
|
||||
|
||||
void HTTPHeaders::RemoveAll(std::string_view key)
|
||||
{
|
||||
auto moved = std::ranges::remove_if(m_headers, [key] (auto& pair) {
|
||||
return CaseInsensitiveEqual(key, pair.first);
|
||||
});
|
||||
m_headers.erase(moved.begin(), moved.end());
|
||||
}
|
||||
|
||||
bool HTTPHeaders::Read(util::LineReader& reader)
|
||||
{
|
||||
// Headers https://httpwg.org/specs/rfc9110.html#rfc.section.6.3
|
||||
// A sequence of Field Lines https://httpwg.org/specs/rfc9110.html#rfc.section.5.2
|
||||
while (auto maybe_line = reader.ReadLine()) {
|
||||
if (reader.Consumed() > MAX_HEADERS_SIZE) throw std::runtime_error("HTTP headers exceed size limit");
|
||||
|
||||
const std::string_view& line = *maybe_line;
|
||||
|
||||
// An empty line indicates end of the headers section https://www.rfc-editor.org/rfc/rfc2616#section-4
|
||||
if (line.empty()) return true;
|
||||
|
||||
// "Field values containing CR, LF, or NUL characters are invalid and dangerous"
|
||||
// https://httpwg.org/specs/rfc9110.html#rfc.section.5.5
|
||||
// A sender MUST NOT generate a bare CR (a CR character not immediately followed by LF)
|
||||
// within any protocol elements other than the content.
|
||||
// A recipient of such a bare CR MUST consider that element to be invalid...
|
||||
// https://httpwg.org/specs/rfc9112.html#rfc.section.2.2
|
||||
if (line.find_first_of("\r\n\0", 0, 3) != std::string_view::npos) throw std::runtime_error("Header contains invalid character");
|
||||
|
||||
// Header line must have at least one ":"
|
||||
// keys are not allowed to have delimiters like ":" but values are
|
||||
// https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
|
||||
const size_t pos{line.find(':')};
|
||||
if (pos == std::string_view::npos) throw std::runtime_error("HTTP header missing colon (:)");
|
||||
|
||||
// Whitespace is strictly not allowed in the field-name (key)
|
||||
// https://www.rfc-editor.org/rfc/rfc9110.html#section-5.6.2
|
||||
std::string_view key = line.substr(0, pos);
|
||||
if (key.find_first_of(" \t\n\r\f\v") != std::string_view::npos) throw std::runtime_error("Invalid header field-name contains whitespace");
|
||||
// Whitespace is optional in the value and can be trimmed
|
||||
std::string value = util::TrimString(std::string_view(line).substr(pos + 1));
|
||||
|
||||
// Header keys are Field Names: https://httpwg.org/specs/rfc9110.html#fields.names
|
||||
// which consist of "tokens": https://httpwg.org/specs/rfc9110.html#rfc.section.5.6.2
|
||||
// that can not be empty.
|
||||
if (key.empty()) throw std::runtime_error("Empty HTTP header name");
|
||||
|
||||
Write(std::string(key), std::move(value));
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string HTTPHeaders::Stringify() const
|
||||
{
|
||||
std::string out;
|
||||
for (const auto& [key, value] : m_headers) {
|
||||
out += key + ": " + value + "\r\n";
|
||||
}
|
||||
|
||||
// Headers are terminated by an empty line
|
||||
out += "\r\n";
|
||||
|
||||
return out;
|
||||
}
|
||||
} // namespace http_bitcoin
|
||||
|
||||
@@ -10,6 +10,9 @@
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
|
||||
namespace util {
|
||||
class SignalInterrupt;
|
||||
} // namespace util
|
||||
@@ -187,4 +190,50 @@ private:
|
||||
struct event* ev;
|
||||
};
|
||||
|
||||
namespace http_bitcoin {
|
||||
|
||||
//! 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_()
|
||||
constexpr size_t MAX_HEADERS_SIZE{8192};
|
||||
|
||||
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);
|
||||
/**
|
||||
* @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);
|
||||
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;
|
||||
};
|
||||
} // namespace http_bitcoin
|
||||
|
||||
#endif // BITCOIN_HTTPSERVER_H
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
#include <httpserver.h>
|
||||
#include <test/util/common.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/string.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
using http_bitcoin::HTTPHeaders;
|
||||
using http_bitcoin::MAX_HEADERS_SIZE;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
|
||||
|
||||
BOOST_AUTO_TEST_CASE(test_query_parameters)
|
||||
@@ -41,4 +45,103 @@ BOOST_AUTO_TEST_CASE(test_query_parameters)
|
||||
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%";
|
||||
BOOST_CHECK_EXCEPTION(GetQueryParameterFromUri(uri.c_str(), "p1"), std::runtime_error, HasReason("URI parsing failed, it likely contained RFC 3986 invalid characters"));
|
||||
}
|
||||
|
||||
BOOST_AUTO_TEST_CASE(http_headers_tests)
|
||||
{
|
||||
{
|
||||
// Writing response headers
|
||||
HTTPHeaders headers{};
|
||||
BOOST_CHECK(!headers.FindFirst("Cache-Control"));
|
||||
headers.Write("Cache-Control", "no-cache");
|
||||
// Check case-insensitive key matching
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("cache-control"), "no-cache");
|
||||
// Additional values are appended, compared case-insensitive
|
||||
headers.Write("cache-control", "max-age=60");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Cache-Control"), "no-cache");
|
||||
BOOST_CHECK((headers.FindAll("Cache-Control") == std::vector<std::string_view>{"no-cache", "max-age=60"}));
|
||||
// Add a few more
|
||||
headers.Write("Pie", "apple");
|
||||
headers.Write("Sandwich", "ham");
|
||||
headers.Write("Coffee", "black");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Pie"), "apple");
|
||||
// Remove
|
||||
headers.RemoveAll("Pie");
|
||||
BOOST_CHECK(!headers.FindFirst("Pie"));
|
||||
// Combine for transmission
|
||||
std::string headers_string{headers.Stringify()};
|
||||
BOOST_CHECK_EQUAL(headers_string, "Cache-Control: no-cache\r\n"
|
||||
"cache-control: max-age=60\r\n"
|
||||
"Sandwich: ham\r\n"
|
||||
"Coffee: black\r\n"
|
||||
"\r\n");
|
||||
}
|
||||
{
|
||||
// Reading request headers captured from bitcoin-cli
|
||||
constexpr std::string_view bitcoin_cli_headers = "Host: 127.0.0.1\r\n"
|
||||
"Connection: close\r\n"
|
||||
"Content-Type: application/json\r\n"
|
||||
"Authorization: Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3\r\n"
|
||||
"Content-Length: 46\r\n";
|
||||
util::LineReader reader(bitcoin_cli_headers, /*max_line_length=*/MAX_HEADERS_SIZE);
|
||||
HTTPHeaders headers{};
|
||||
headers.Read(reader);
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Host"), "127.0.0.1");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Connection"), "close");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Content-Type"), "application/json");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Authorization"), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("Content-Length"), "46");
|
||||
BOOST_CHECK(!headers.FindFirst("Pizza"));
|
||||
}
|
||||
// Ensure invalid headers are rejected
|
||||
{
|
||||
// missing a colon
|
||||
util::LineReader reader{"key value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP header missing colon (:)"});
|
||||
}
|
||||
{
|
||||
// missing a key
|
||||
util::LineReader reader{":value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Empty HTTP header name"});
|
||||
}
|
||||
{
|
||||
// contains NUL
|
||||
util::LineReader reader{std::string_view{"X-Custom: foo\0bar\n", 18}, /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
|
||||
}
|
||||
{
|
||||
// contains bare \r (not followed by \n)
|
||||
util::LineReader reader{std::string_view{"X-Custom: foo\rbar\n"}, /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
|
||||
}
|
||||
{
|
||||
// contains odd \r preceding the expected CRLF
|
||||
util::LineReader reader{"X-Custom: foo\r\r\n", /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Header contains invalid character"});
|
||||
}
|
||||
{
|
||||
// key contains whitespace
|
||||
util::LineReader reader{"key : value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"Invalid header field-name contains whitespace"});
|
||||
}
|
||||
{
|
||||
// Individual lines are below MAX_HEADERS_SIZE but the total is excessive
|
||||
std::string lines;
|
||||
lines.reserve(820 * 10);
|
||||
for (int i = 0; i < 820; ++i) {
|
||||
lines.append("key:value\n");
|
||||
}
|
||||
std::string_view excessive_headers{lines};
|
||||
BOOST_CHECK_GT(excessive_headers.size(), MAX_HEADERS_SIZE);
|
||||
util::LineReader reader{excessive_headers, /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
BOOST_CHECK_EXCEPTION(HTTPHeaders{}.Read(reader), std::runtime_error, HasReason{"HTTP headers exceed size limit"});
|
||||
}
|
||||
{
|
||||
// Ok
|
||||
util::LineReader reader{"key: value\n", /*max_line_length=*/MAX_HEADERS_SIZE};
|
||||
HTTPHeaders headers{};
|
||||
headers.Read(reader);
|
||||
BOOST_CHECK_EQUAL(headers.FindFirst("key"), "value");
|
||||
}
|
||||
}
|
||||
BOOST_AUTO_TEST_SUITE_END()
|
||||
|
||||
Reference in New Issue
Block a user