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);
|
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
|
#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
|
||||||
|
|
|
@ -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()
|
||||||
|
|
Loading…
Add table
Reference in a new issue