Matthew Zipkin 2024-09-30 13:22:29 -04:00 committed by Matthew Zipkin
parent 12bd25e2b5
commit 70d003ca10
No known key found for this signature in database
GPG key ID: E7E2984B6289C93A
3 changed files with 139 additions and 0 deletions

View file

@ -781,3 +781,69 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
pathHandlers.erase(i); pathHandlers.erase(i);
} }
} }
namespace http_bitcoin {
std::optional<std::string> HTTPHeaders::Find(const std::string key) const
{
const auto it = m_map.find(key);
if (it == m_map.end()) return std::nullopt;
return it->second;
}
void HTTPHeaders::Write(const std::string key, const std::string value)
{
// If present, append value to list
const auto existing_value = Find(key);
if (existing_value) {
m_map[key] = existing_value.value() + ", " + value;
} else {
m_map[key] = value;
}
}
void HTTPHeaders::Remove(const std::string key)
{
m_map.erase(key);
}
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
do {
auto maybe_line = reader.ReadLine();
if (!maybe_line) return false;
const std::string& line = *maybe_line;
// An empty line indicates end of the headers section https://www.rfc-editor.org/rfc/rfc2616#section-4
if (line.length() == 0) break;
// 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::npos) throw std::runtime_error("HTTP header missing colon (:)");
// Whitespace is optional
std::string key = util::TrimString(line.substr(0, pos));
std::string value = util::TrimString(line.substr(pos + 1));
Write(key, value);
} while (true);
return true;
}
std::string HTTPHeaders::Stringify() const
{
std::string out;
for (auto it = m_map.begin(); it != m_map.end(); ++it) {
out += it->first + ": " + it->second + "\r\n";
}
// Headers are terminated by an empty line
out += "\r\n";
return out;
}
} // namespace http_bitcoin

View file

@ -6,10 +6,14 @@
#define BITCOIN_HTTPSERVER_H #define BITCOIN_HTTPSERVER_H
#include <functional> #include <functional>
#include <map>
#include <optional> #include <optional>
#include <span> #include <span>
#include <string> #include <string>
#include <util/strencodings.h>
#include <util/string.h>
namespace util { namespace util {
class SignalInterrupt; class SignalInterrupt;
} // namespace util } // namespace util
@ -197,4 +201,19 @@ private:
struct event* ev; struct event* ev;
}; };
namespace http_bitcoin {
class HTTPHeaders
{
public:
std::optional<std::string> Find(const std::string key) const;
void Write(const std::string key, const std::string value);
void Remove(const std::string key);
bool Read(util::LineReader& reader);
std::string Stringify() const;
private:
std::map<std::string, std::string, util::CaseInsensitiveComparator> m_map;
};
} // namespace http_bitcoin
#endif // BITCOIN_HTTPSERVER_H #endif // BITCOIN_HTTPSERVER_H

View file

@ -4,9 +4,12 @@
#include <httpserver.h> #include <httpserver.h>
#include <test/util/setup_common.h> #include <test/util/setup_common.h>
#include <util/strencodings.h>
#include <boost/test/unit_test.hpp> #include <boost/test/unit_test.hpp>
using http_bitcoin::HTTPHeaders;
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup) BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
BOOST_AUTO_TEST_CASE(test_query_parameters) BOOST_AUTO_TEST_CASE(test_query_parameters)
@ -40,4 +43,55 @@ BOOST_AUTO_TEST_CASE(test_query_parameters)
uri = "/rest/endpoint/someresource.json&p1=v1&p2=v2%"; 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_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.Find("Cache-Control"));
headers.Write("Cache-Control", "no-cache");
// Check case-insensitive key matching
BOOST_CHECK_EQUAL(headers.Find("Cache-Control").value(), "no-cache");
BOOST_CHECK_EQUAL(headers.Find("cache-control").value(), "no-cache");
// Additional values are comma-separated and appended
headers.Write("Cache-Control", "no-store");
BOOST_CHECK_EQUAL(headers.Find("Cache-Control").value(), "no-cache, no-store");
// Add a few more
headers.Write("Pie", "apple");
headers.Write("Sandwich", "ham");
headers.Write("Coffee", "black");
BOOST_CHECK_EQUAL(headers.Find("Pie").value(), "apple");
// Remove
headers.Remove("Pie");
BOOST_CHECK(!headers.Find("Pie"));
// Combine for transmission
// std::map sorts alphabetically by key, no order is specified for HTTP
BOOST_CHECK_EQUAL(
headers.Stringify(),
"Cache-Control: no-cache, no-store\r\n"
"Coffee: black\r\n"
"Sandwich: ham\r\n\r\n");
}
{
// Reading request headers captured from bitcoin-cli
std::vector<std::byte> buffer{TryParseHex<std::byte>(
"486f73743a203132372e302e302e310d0a436f6e6e656374696f6e3a20636c6f73"
"650d0a436f6e74656e742d547970653a206170706c69636174696f6e2f6a736f6e"
"0d0a417574686f72697a6174696f6e3a204261736963205831396a623239726157"
"5666587a6f7a597a4a6b4e5441784e44466c4d474a69596d56684d5449354f4467"
"334e7a49354d544d334e54526d4e54686b4e6a63324f574d775a5459785a6a677a"
"4e5467794e7a4577595459314f47526b596a566d5a4751330d0a436f6e74656e74"
"2d4c656e6774683a2034360d0a0d0a").value()};
util::LineReader reader(buffer, /*max_read=*/1028);
HTTPHeaders headers{};
headers.Read(reader);
BOOST_CHECK_EQUAL(headers.Find("Host").value(), "127.0.0.1");
BOOST_CHECK_EQUAL(headers.Find("Connection").value(), "close");
BOOST_CHECK_EQUAL(headers.Find("Content-Type").value(), "application/json");
BOOST_CHECK_EQUAL(headers.Find("Authorization").value(), "Basic X19jb29raWVfXzozYzJkNTAxNDFlMGJiYmVhMTI5ODg3NzI5MTM3NTRmNThkNjc2OWMwZTYxZjgzNTgyNzEwYTY1OGRkYjVmZGQ3");
BOOST_CHECK_EQUAL(headers.Find("Content-Length").value(), "46");
BOOST_CHECK(!headers.Find("Pizza"));
}
}
BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END()