mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-04-29 14:59:39 -04: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:
parent
12bd25e2b5
commit
70d003ca10
3 changed files with 139 additions and 0 deletions
|
@ -781,3 +781,69 @@ void UnregisterHTTPHandler(const std::string &prefix, bool exactMatch)
|
|||
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
|
||||
|
|
|
@ -6,10 +6,14 @@
|
|||
#define BITCOIN_HTTPSERVER_H
|
||||
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <span>
|
||||
#include <string>
|
||||
|
||||
#include <util/strencodings.h>
|
||||
#include <util/string.h>
|
||||
|
||||
namespace util {
|
||||
class SignalInterrupt;
|
||||
} // namespace util
|
||||
|
@ -197,4 +201,19 @@ private:
|
|||
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
|
||||
|
|
|
@ -4,9 +4,12 @@
|
|||
|
||||
#include <httpserver.h>
|
||||
#include <test/util/setup_common.h>
|
||||
#include <util/strencodings.h>
|
||||
|
||||
#include <boost/test/unit_test.hpp>
|
||||
|
||||
using http_bitcoin::HTTPHeaders;
|
||||
|
||||
BOOST_FIXTURE_TEST_SUITE(httpserver_tests, BasicTestingSetup)
|
||||
|
||||
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%";
|
||||
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()
|
||||
|
|
Loading…
Add table
Reference in a new issue