diff --git a/src/bin/agent/ca_cfg_mgr.cc b/src/bin/agent/ca_cfg_mgr.cc index f1d1047917..4cb9da5480 100644 --- a/src/bin/agent/ca_cfg_mgr.cc +++ b/src/bin/agent/ca_cfg_mgr.cc @@ -23,13 +23,14 @@ namespace isc { namespace agent { CtrlAgentCfgContext::CtrlAgentCfgContext() - : http_host_(""), http_port_(0), + : http_host_(""), http_port_(0), http_headers_(), trust_anchor_(""), cert_file_(""), key_file_(""), cert_required_(true) { } CtrlAgentCfgContext::CtrlAgentCfgContext(const CtrlAgentCfgContext& orig) : ConfigBase(), ctrl_sockets_(orig.ctrl_sockets_), http_host_(orig.http_host_), http_port_(orig.http_port_), + http_headers_(orig.http_headers_), trust_anchor_(orig.trust_anchor_), cert_file_(orig.cert_file_), key_file_(orig.key_file_), cert_required_(orig.cert_required_), hooks_config_(orig.hooks_config_), auth_config_(orig.auth_config_) { @@ -186,6 +187,10 @@ CtrlAgentCfgContext::toElement() const { ca->set("http-host", Element::create(http_host_)); // Set http-port ca->set("http-port", Element::create(static_cast(http_port_))); + // Set http-headers + if (!http_headers_.empty()) { + ca->set("http-headers", toElement(http_headers_)); + } // Set TLS setup when enabled if (!trust_anchor_.empty()) { ca->set("trust-anchor", Element::create(trust_anchor_)); diff --git a/src/bin/agent/ca_cfg_mgr.h b/src/bin/agent/ca_cfg_mgr.h index dbd841e15c..60f01d6699 100644 --- a/src/bin/agent/ca_cfg_mgr.h +++ b/src/bin/agent/ca_cfg_mgr.h @@ -10,6 +10,7 @@ #include #include #include +#include #include #include #include @@ -99,6 +100,20 @@ public: return (http_port_); } + /// @brief Sets http-headers parameter + /// + /// @param headers Collection of config HTTP headers. + void setHttpHeaders(const isc::http::CfgHttpHeaders& headers) { + http_headers_ = headers; + } + + /// @brief Returns http-headers parameter + /// + /// @return Collection of config HTTP headers. + const isc::http::CfgHttpHeaders& getHttpHeaders() const { + return (http_headers_); + } + /// @brief Sets HTTP authentication configuration. /// /// @note Only the basic HTTP authentication is supported. @@ -225,6 +240,9 @@ private: /// TCP port the CA should listen on. uint16_t http_port_; + /// Config HTTP headers. + isc::http::CfgHttpHeaders http_headers_; + /// Trust anchor aka Certificate Authority (can be a file name or /// a directory path). std::string trust_anchor_; diff --git a/src/bin/agent/simple_parser.cc b/src/bin/agent/simple_parser.cc index 7a6a76372c..e6e86df21a 100644 --- a/src/bin/agent/simple_parser.cc +++ b/src/bin/agent/simple_parser.cc @@ -162,6 +162,13 @@ AgentSimpleParser::parse(const CtrlAgentCfgContextPtr& ctx, ctx->setAuthConfig(auth); } + // HTTP headers are fifth. + ConstElementPtr headers_config = config->get("http-headers"); + if (headers_config) { + using namespace isc::http; + ctx->setHttpHeaders(parseCfgHttpHeaders(headers_config)); + } + // User context can be done at anytime. ConstElementPtr user_context = config->get("user-context"); if (user_context) { diff --git a/src/lib/http/Makefile.am b/src/lib/http/Makefile.am index 5935b9e8db..8650dd70c7 100644 --- a/src/lib/http/Makefile.am +++ b/src/lib/http/Makefile.am @@ -43,6 +43,7 @@ libkea_http_la_SOURCES += auth_log.cc auth_log.h libkea_http_la_SOURCES += auth_messages.cc auth_messages.h libkea_http_la_SOURCES += basic_auth_config.cc basic_auth_config.h libkea_http_la_SOURCES += basic_auth.cc basic_auth.h +libkea_http_la_SOURCES += cfg_http_header.h cfg_http_header.cc libkea_http_la_CXXFLAGS = $(AM_CXXFLAGS) libkea_http_la_CPPFLAGS = $(AM_CPPFLAGS) @@ -103,6 +104,7 @@ libkea_http_include_HEADERS = \ auth_messages.h \ basic_auth.h \ basic_auth_config.h \ + cfg_http_header.h \ client.h \ connection.h \ connection_pool.h \ diff --git a/src/lib/http/cfg_http_header.cc b/src/lib/http/cfg_http_header.cc new file mode 100644 index 0000000000..7c7edf3e6b --- /dev/null +++ b/src/lib/http/cfg_http_header.cc @@ -0,0 +1,95 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include +#include + +using namespace isc; +using namespace isc::data; +using namespace isc::dhcp; +using namespace std; + +namespace isc { +namespace http { + +ElementPtr +CfgHttpHeader::toElement() const { + ElementPtr map = isc::data::Element::createMap(); + contextToElement(map); + map->set("name", Element::create(name_)); + map->set("value", Element::create(value_)); + return (map); +} + +ElementPtr +toElement(const CfgHttpHeaders& headers) { + ElementPtr list = Element::createList(); + for (auto const& header : headers) { + list->add(header.toElement()); + } + return (list); +} + +namespace { + +const SimpleKeywords HTTP_HEADER_KEYWORDS = { + { "name", Element::string }, + { "value", Element::string }, + { "user-context", Element::map } +}; + +const SimpleRequiredKeywords HTTP_HEADER_REQUIRED = { "name", "value" }; + +CfgHttpHeader +parseCfgHttpHeader(const ConstElementPtr& config) { + if (!config) { + // Should not happen. + isc_throw(DhcpConfigError, "null 'http-headers' item"); + } + if (config->getType() != Element::map) { + isc_throw(DhcpConfigError, "invalid type specified for 'http-headers' " + "item (" << config->getPosition() << ")"); + } + SimpleParser::checkKeywords(HTTP_HEADER_KEYWORDS, config); + SimpleParser::checkRequired(HTTP_HEADER_REQUIRED, config); + string name = config->get("name")->stringValue(); + if (name.empty()) { + isc_throw(DhcpConfigError, "empty 'name' (" + << config->get("name")->getPosition() << ")"); + } + string value = config->get("value")->stringValue(); + if (value.empty()) { + isc_throw(DhcpConfigError, "empty 'value' (" + << config->get("value")->getPosition() << ")"); + } + CfgHttpHeader header(name, value); + ConstElementPtr user_context = config->get("user-context"); + if (user_context) { + header.setContext(user_context); + } + return (header); +} + +} + +CfgHttpHeaders +parseCfgHttpHeaders(const ConstElementPtr& config) { + CfgHttpHeaders headers; + if (!config) { + return (headers); + } + if (config->getType() != Element::list) { + isc_throw(DhcpConfigError, "invalid type specified for parameter " + "'http-headers' (" << config->getPosition() << ")"); + } + for (auto const& item : config->listValue()) { + headers.push_back(parseCfgHttpHeader(item)); + } + return (headers); +} + +} // namespace http +} // namespace isc diff --git a/src/lib/http/cfg_http_header.h b/src/lib/http/cfg_http_header.h new file mode 100644 index 0000000000..8adb34f802 --- /dev/null +++ b/src/lib/http/cfg_http_header.h @@ -0,0 +1,71 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#ifndef CFG_HTTP_HEADER_H +#define CFG_HTTP_HEADER_H + +#include +#include +#include +#include +#include + +namespace isc { +namespace http { + +/// @brief Config HTTP header. +class CfgHttpHeader : public isc::data::UserContext, public isc::data::CfgToElement { +public: + std::string name_; + std::string value_; + + /// @brief Constructor. + /// + /// @param name Header name. + /// @param value Header value. + CfgHttpHeader(const std::string& name, const std::string& value) + : name_(name), value_(value) { + } + + /// @brief Unparses config HTTP header. + /// + /// @return A pointer to unparsed header configuration. + virtual isc::data::ElementPtr toElement() const; +}; + +/// @brief Collection of config HTTP headers. +typedef std::vector CfgHttpHeaders; + +/// @brief Copy config HTTP headers to message. +/// +/// @tparam HTTP_MSG Either HttpRequest or HttpResponse. +/// @param headers Config HTTP headers. +/// @param message HTTP_MSG target object. +template +void copyHttpHeaders(const CfgHttpHeaders& headers, const HTTP_MSG& message) { + for (auto const& header : headers) { + message.context()->headers_. + push_back(HttpHeaderContext(header.name_, header.value_)); + } +} + +/// @brief Unparse config HTTP headers. +/// +/// @param headers Config HTTP headers. +/// @return A pointer to unparsed headers configuration. +isc::data::ElementPtr toElement(const CfgHttpHeaders& headers); + +/// @brief Parse config HTTP headers. +/// +/// @param config Element holding the HTTP headers configuration. +/// @return The HTTP headers. +/// @throw DhcpConfigError when the configuration is invalid. +CfgHttpHeaders parseCfgHttpHeaders(const isc::data::ConstElementPtr& config); + +} // namespace http +} // namespace isc + +#endif diff --git a/src/lib/http/tests/Makefile.am b/src/lib/http/tests/Makefile.am index b2f629d671..00b59a939d 100644 --- a/src/lib/http/tests/Makefile.am +++ b/src/lib/http/tests/Makefile.am @@ -29,6 +29,7 @@ TESTS += libhttp_unittests libhttp_unittests_SOURCES = basic_auth_unittests.cc libhttp_unittests_SOURCES += basic_auth_config_unittests.cc +libhttp_unittests_SOURCES += cfg_http_header_unittests.cc libhttp_unittests_SOURCES += connection_pool_unittests.cc libhttp_unittests_SOURCES += date_time_unittests.cc libhttp_unittests_SOURCES += http_header_unittests.cc diff --git a/src/lib/http/tests/cfg_http_header_unittests.cc b/src/lib/http/tests/cfg_http_header_unittests.cc new file mode 100644 index 0000000000..b88cdd092a --- /dev/null +++ b/src/lib/http/tests/cfg_http_header_unittests.cc @@ -0,0 +1,79 @@ +// Copyright (C) 2024 Internet Systems Consortium, Inc. ("ISC") +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +#include + +#include +#include + +using namespace isc::data; +using namespace isc::http; +using namespace std; + +namespace { + +// This test verifies copy to response. +TEST(CfgHttpHeaderTest, copy) { + // Create a response. + HttpResponse response(HttpVersion(1, 0), HttpStatusCode::OK); + // Create a HSTS header. + CfgHttpHeader hsts("Strict-Transport-Security", "max-age=31536000"); + // Create a random header. + CfgHttpHeader foobar("Foo", "bar"); + // Add them to a collection. + CfgHttpHeaders headers; + headers.push_back(hsts); + headers.push_back(foobar); + // Copy headers to response. + EXPECT_NO_THROW(copyHttpHeaders(headers, response)); + + // Verify. + auto const& got = response.context()->headers_; + ASSERT_EQ(2, got.size()); + EXPECT_EQ("Strict-Transport-Security", got[0].name_); + EXPECT_EQ("max-age=31536000", got[0].value_); + EXPECT_EQ("Foo", got[1].name_); + EXPECT_EQ("bar", got[1].value_); + + // Unparse. + string expected = "[ "; + expected += "{ \"name\": \"Strict-Transport-Security\", "; + expected += "\"value\": \"max-age=31536000\" }, "; + expected += "{ \"name\": \"Foo\", \"value\": \"bar\" } ]"; + EXPECT_EQ(expected, toElement(headers)->str()); +} + +// This test verifies parse and toElement behavior. +TEST(CfgHttpHeaderTest, parse) { + // Config. + string config = "[\n" + " {\n" + " \"name\": \"Strict-Transport-Security\",\n" + " \"value\": \"max-age=31536000\",\n" + " \"user-context\": { \"comment\": \"HSTS header\" }\n" + " },{\n" + " \"name\": \"Foo\", \"value\": \"bar\"\n" + " }\n" + " ]\n"; + ConstElementPtr json; + ASSERT_NO_THROW(json = Element::fromJSON(config)); + CfgHttpHeaders headers; + ASSERT_NO_THROW(headers = parseCfgHttpHeaders(json)); + ASSERT_EQ(2, headers.size()); + EXPECT_EQ("Strict-Transport-Security", headers[0].name_); + EXPECT_EQ("max-age=31536000", headers[0].value_); + ConstElementPtr user_context = headers[0].getContext(); + ASSERT_TRUE(user_context); + EXPECT_EQ("{ \"comment\": \"HSTS header\" }", user_context->str()); + EXPECT_EQ("Foo", headers[1].name_); + EXPECT_EQ("bar", headers[1].value_); + EXPECT_FALSE(headers[1].getContext()); + ConstElementPtr unparsed; + ASSERT_NO_THROW(unparsed = toElement(headers)); + EXPECT_TRUE(json->equals(*unparsed)); +} + +}