diff options
-rw-r--r-- | doc/sphinx/arm/agent.rst | 9 | ||||
-rw-r--r-- | doc/sphinx/arm/congestion-handling.rst | 2 | ||||
-rw-r--r-- | doc/sphinx/arm/ctrl-channel.rst | 5 | ||||
-rw-r--r-- | doc/sphinx/arm/dhcp4-srv.rst | 9 | ||||
-rw-r--r-- | doc/sphinx/arm/dhcp6-srv.rst | 9 | ||||
-rw-r--r-- | doc/sphinx/arm/hooks-lease-query.rst | 2 | ||||
-rw-r--r-- | doc/sphinx/arm/quickstart.rst | 2 | ||||
-rw-r--r-- | src/bin/agent/ca_response_creator.cc | 35 | ||||
-rw-r--r-- | src/bin/agent/tests/ca_response_creator_unittests.cc | 162 |
9 files changed, 232 insertions, 3 deletions
diff --git a/doc/sphinx/arm/agent.rst b/doc/sphinx/arm/agent.rst index f69696d1af..4862d46a92 100644 --- a/doc/sphinx/arm/agent.rst +++ b/doc/sphinx/arm/agent.rst @@ -56,6 +56,12 @@ The following example demonstrates the basic CA configuration. "Control-agent": { "http-host": "10.20.30.40", "http-port": 8000, + "http-headers": [ + { + "name": "Strict-Transport-Security", + "value": "max-age=31536000" + } + ], "trust-anchor": "/path/to/the/ca-cert.pem", "cert-file": "/path/to/the/agent-cert.pem", "key-file": "/path/to/the/agent-key.pem", @@ -114,6 +120,9 @@ different from the HA peer URLs, which are strictly for internal HA traffic between the peers. User commands should still be sent via the CA. +Since Kea 1.7.5 the ``http-headers`` parameter specifies a list of +extra HTTP headers to add to HTTP responses. + The ``trust-anchor``, ``cert-file``, ``key-file``, and ``cert-required`` parameters specify the TLS setup for HTTP, i.e. HTTPS. If these parameters are not specified, HTTP is used. The TLS/HTTPS support in Kea is diff --git a/doc/sphinx/arm/congestion-handling.rst b/doc/sphinx/arm/congestion-handling.rst index 68b7913036..0e54ebca0f 100644 --- a/doc/sphinx/arm/congestion-handling.rst +++ b/doc/sphinx/arm/congestion-handling.rst @@ -22,7 +22,7 @@ Congestion typically occurs when there is a network event that causes overly lar numbers of clients to simultaneously need leases, such as recovery after a network outage. In a well-planned deployment, the number and capacity of servers is matched to the maximum expected client load. If the load is routinely too -heavy, then the deployment needs to be re-evaluated. +heavy, then the deployment needs to be re-evaluated. The goal of congestion handling is to help servers mitigate the peak in traffic by fulfilling as many of the most relevant requests as possible diff --git a/doc/sphinx/arm/ctrl-channel.rst b/doc/sphinx/arm/ctrl-channel.rst index 3079069287..2a1c2f6f4b 100644 --- a/doc/sphinx/arm/ctrl-channel.rst +++ b/doc/sphinx/arm/ctrl-channel.rst @@ -207,6 +207,11 @@ depends on the specific command. } } +.. note:: + + Since Kea 2.7.5 it is possible to specify extra HTTP headers which + are added to HTTP responses. + .. _ctrl-channel-control-agent-command-response-format: Control Agent Command Response Format diff --git a/doc/sphinx/arm/dhcp4-srv.rst b/doc/sphinx/arm/dhcp4-srv.rst index bbb428e081..50283a2408 100644 --- a/doc/sphinx/arm/dhcp4-srv.rst +++ b/doc/sphinx/arm/dhcp4-srv.rst @@ -7777,6 +7777,9 @@ TLS is required). The ``socket-address`` (default ``127.0.0.1``) and ``socket-port`` (default 8000) specify an IP address and port to which the HTTP service will be bound. +Since Kea 1.7.5 the ``http-headers`` parameter specifies a list of +extra HTTP headers to add to HTTP responses. + The ``trust-anchor``, ``cert-file``, ``key-file``, and ``cert-required`` parameters specify the TLS setup for HTTP, i.e. HTTPS. If these parameters are not specified, HTTP is used. The TLS/HTTPS support in Kea is @@ -7832,6 +7835,12 @@ to detect configuration errors as soon as possible. "socket-type": "https", "socket-address": "10.20.30.40", "socket-port": 8004, + "http-headers": [ + { + "name": "Strict-Transport-Security", + "value": "max-age=31536000" + } + ], "trust-anchor": "/path/to/the/ca-cert.pem", "cert-file": "/path/to/the/agent-cert.pem", "key-file": "/path/to/the/agent-key.pem", diff --git a/doc/sphinx/arm/dhcp6-srv.rst b/doc/sphinx/arm/dhcp6-srv.rst index 4ea40f4321..14c89c70eb 100644 --- a/doc/sphinx/arm/dhcp6-srv.rst +++ b/doc/sphinx/arm/dhcp6-srv.rst @@ -7591,6 +7591,9 @@ TLS is required). The ``socket-address`` (default ``::1``) and ``socket-port`` (default 8000) specify an IP address and port to which the HTTP service will be bound. +Since Kea 1.7.5 the ``http-headers`` parameter specifies a list of +extra HTTP headers to add to HTTP responses. + The ``trust-anchor``, ``cert-file``, ``key-file``, and ``cert-required`` parameters specify the TLS setup for HTTP, i.e. HTTPS. If these parameters are not specified, HTTP is used. The TLS/HTTPS support in Kea is @@ -7646,6 +7649,12 @@ to detect configuration errors as soon as possible. "socket-type": "https", "socket-address": "2010:30:40::50", "socket-port": 8005, + "http-headers": [ + { + "name": "Strict-Transport-Security", + "value": "max-age=31536000" + } + ], "trust-anchor": "/path/to/the/ca-cert.pem", "cert-file": "/path/to/the/agent-cert.pem", "key-file": "/path/to/the/agent-key.pem", diff --git a/doc/sphinx/arm/hooks-lease-query.rst b/doc/sphinx/arm/hooks-lease-query.rst index ff8f223236..c7ddbe0023 100644 --- a/doc/sphinx/arm/hooks-lease-query.rst +++ b/doc/sphinx/arm/hooks-lease-query.rst @@ -649,7 +649,7 @@ and includes either the count of leases updated or the nature of the failure: This ``extended-info6-upgrade`` command must be called when: -- the database schema was upgraded from 2.4.1 or older version. +- the database schema was upgraded from 2.4.1 or older version. ``extended-info4-upgrade`` can be used when upgrading from 2.3.8 or older version. - Bulk Leasequery was not enabled; tables are maintained only when v6 BLQ is diff --git a/doc/sphinx/arm/quickstart.rst b/doc/sphinx/arm/quickstart.rst index 9ece8adf34..6cabbce77e 100644 --- a/doc/sphinx/arm/quickstart.rst +++ b/doc/sphinx/arm/quickstart.rst @@ -17,7 +17,7 @@ Quick Start Guide Using tarball :ref:`build-requirements` for details. 2. Download the Kea source tarball from - `the main isc.org downloads page <https://www.isc.org/download/>`__, + `the main isc.org downloads page <https://www.isc.org/download/>`__, `the ISC downloads site <https://downloads.isc.org/isc/kea/>`__, or `the ISC Cloudsmith page <https://cloudsmith.io/~isc/packages/?q=format%3Araw>`__. diff --git a/src/bin/agent/ca_response_creator.cc b/src/bin/agent/ca_response_creator.cc index ffe1d5d3ef..ddc010acbf 100644 --- a/src/bin/agent/ca_response_creator.cc +++ b/src/bin/agent/ca_response_creator.cc @@ -81,12 +81,30 @@ createStockHttpResponseInternal(const HttpRequestPtr& request, } // This will generate the response holding JSON content. HttpResponsePtr response(new HttpResponseJson(http_version, status_code)); + // See the comment below. + boost::shared_ptr<CtrlAgentController> controller = + boost::dynamic_pointer_cast<CtrlAgentController>(CtrlAgentController::instance()); + if (controller) { + CtrlAgentProcessPtr process = controller->getCtrlAgentProcess(); + if (process) { + CtrlAgentCfgMgrPtr cfgmgr = process->getCtrlAgentCfgMgr(); + if (cfgmgr) { + CtrlAgentCfgContextPtr ctx = cfgmgr->getCtrlAgentCfgContext(); + if (ctx) { + copyHttpHeaders(ctx->getHttpHeaders(), *response); + } + } + } + } return (response); } HttpResponsePtr CtrlAgentResponseCreator:: createDynamicHttpResponse(HttpRequestPtr request) { + // Extra headers. + CfgHttpHeaders headers; + // First check authentication. HttpResponseJsonPtr http_response; @@ -106,6 +124,7 @@ createDynamicHttpResponse(HttpRequestPtr request) { if (cfgmgr) { ctx = cfgmgr->getCtrlAgentCfgContext(); if (ctx) { + headers = ctx->getHttpHeaders(); const HttpAuthConfigPtr& auth = ctx->getAuthConfig(); if (auth) { // Check authentication. @@ -116,6 +135,13 @@ createDynamicHttpResponse(HttpRequestPtr request) { } } + // Pass extra headers to the hook. + bool auth_failed = false; + if (http_response) { + auth_failed = true; + copyHttpHeaders(headers, *http_response); + } + // Callout point for "http_auth". bool reset_handle = false; if (HooksManager::calloutsPresent(Hooks.hook_index_http_auth_)) { @@ -142,6 +168,15 @@ createDynamicHttpResponse(HttpRequestPtr request) { // The basic HTTP authentication check or a callout failed and // left a response. if (http_response) { + // Avoid to copy extra headers twice even this should not be required. + if (!auth_failed && !headers.empty()) { + copyHttpHeaders(headers, *http_response); + if (http_response->isFinalized()) { + // Argh! The response was already finalized. + http_response->reset(); + http_response->finalize(); + } + } return (http_response); } diff --git a/src/bin/agent/tests/ca_response_creator_unittests.cc b/src/bin/agent/tests/ca_response_creator_unittests.cc index df394deb9d..e45c2f363b 100644 --- a/src/bin/agent/tests/ca_response_creator_unittests.cc +++ b/src/bin/agent/tests/ca_response_creator_unittests.cc @@ -152,6 +152,16 @@ public: return (ctx); } + /// @brief Convert header vector to header map. + static std::map<std::string, std::string> + headers2map(std::vector<HttpHeaderContext> headers) { + std::map<std::string, std::string> result; + for (const auto& header : headers) { + result[header.name_] = header.value_; + } + return (result); + } + /// @brief Instance of the response creator. CtrlAgentResponseCreator response_creator_; @@ -191,6 +201,31 @@ TEST_F(CtrlAgentResponseCreatorTest, createStockHttpResponseCorrectVersion) { testStockResponse(HttpStatusCode::NO_CONTENT, "HTTP/1.1 204 No Content"); } +// Test that the server responds with extra headers for an error response. +TEST_F(CtrlAgentResponseCreatorTest, createStockHttpResponseHeaders) { + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + // Add a STS header. + CfgHttpHeader hsts("Strict-Transport-Security", "max-age=31536000"); + CfgHttpHeaders headers; + headers.push_back(hsts); + // Add a random header. + CfgHttpHeader foobar("Foo", "bar"); + headers.push_back(foobar); + ctx->setHttpHeaders(headers); + // Set request. + request_->context()->http_version_major_ = 1; + request_->context()->http_version_minor_ = 1; + const HttpStatusCode& status_code = HttpStatusCode::NO_CONTENT; + HttpResponsePtr response; + response = response_creator_.createStockHttpResponse(request_, status_code); + ASSERT_TRUE(response); + // Check that the two extra headers are in the response. + auto got = headers2map(response->context()->headers_); + EXPECT_EQ("max-age=31536000", got["Strict-Transport-Security"]); + EXPECT_EQ("bar", got["Foo"]); +} + // Test successful server response when the client specifies valid command. TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponse) { setBasicContext(request_); @@ -219,6 +254,39 @@ TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponse) { std::string::npos); } +// Test that the server responds with extra headers for a command response. +TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponseHeaders) { + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + // Add a STS header. + CfgHttpHeader hsts("Strict-Transport-Security", "max-age=31536000"); + CfgHttpHeaders headers; + headers.push_back(hsts); + // Add a random header. + CfgHttpHeader foobar("Foo", "bar"); + headers.push_back(foobar); + ctx->setHttpHeaders(headers); + + // From previous test. + setBasicContext(request_); + + // Body: "foo" command has been registered in the test fixture constructor. + request_->context()->body_ = "{ \"command\": \"foo\" }"; + + // All requests must be finalized before they can be processed. + ASSERT_NO_THROW(request_->finalize()); + + // Create response from the request. + HttpResponsePtr response; + ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_)); + ASSERT_TRUE(response); + + // Check that the two extra headers are in the response. + auto got = headers2map(response->context()->headers_); + EXPECT_EQ("max-age=31536000", got["Strict-Transport-Security"]); + EXPECT_EQ("bar", got["Foo"]); +} + // This test verifies that Internal Server Error is returned when invalid C++ // request type is used. This is considered an error in the server logic. TEST_F(CtrlAgentResponseCreatorTest, createDynamicHttpResponseInvalidType) { @@ -282,6 +350,44 @@ TEST_F(CtrlAgentResponseCreatorTest, noAuth) { EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos); } +// Test that the server responds with extra headers for no auth response. +TEST_F(CtrlAgentResponseCreatorTest, noAuthHeader) { + setBasicContext(request_); + + // Body: "list-commands" is natively supported by the command manager. + request_->context()->body_ = "{ \"command\": \"list-commands\" }"; + + // All requests must be finalized before they can be processed. + ASSERT_NO_THROW(request_->finalize()); + + // Require authentication. + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + BasicHttpAuthConfigPtr auth(new BasicHttpAuthConfig()); + ASSERT_NO_THROW(ctx->setAuthConfig(auth)); + auth->setRealm("ISC.ORG"); + auth->add("foo", "", "bar", ""); + + // Add a STS header. + CfgHttpHeader hsts("Strict-Transport-Security", "max-age=31536000"); + CfgHttpHeaders headers; + headers.push_back(hsts); + // Add a random header. + CfgHttpHeader foobar("Foo", "bar"); + headers.push_back(foobar); + ctx->setHttpHeaders(headers); + + HttpResponsePtr response; + ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_)); + EXPECT_TRUE(request_->getBasicAuth().empty()); + ASSERT_TRUE(response); + + // Check that the two extra headers are in the response. + auto got = headers2map(response->context()->headers_); + EXPECT_EQ("max-age=31536000", got["Strict-Transport-Security"]); + EXPECT_EQ("bar", got["Foo"]); +} + // Test successful server response when the client is authenticated. TEST_F(CtrlAgentResponseCreatorTest, basicAuth) { setBasicContext(request_); @@ -383,6 +489,62 @@ TEST_F(CtrlAgentResponseCreatorTest, hookNoAuth) { EXPECT_TRUE(response_json->toString().find(expected) != std::string::npos); } +// This test verifies that the server responds with extra headers for no +// auth response provided by the hool. +TEST_F(CtrlAgentResponseCreatorTest, hookNoAuthHeaders) { + setBasicContext(request_); + + // Body: "list-commands" is natively supported by the command manager. + // We add a random value in the extra entry: see next unit test + // for an explanation about how it is used. + auto r32 = isc::cryptolink::random(4); + ASSERT_EQ(4, r32.size()); + int extra = r32[0]; + extra = (extra << 8) | r32[1]; + extra = (extra << 8) | r32[2]; + extra = (extra << 8) | r32[3]; + request_->context()->body_ = "{ \"command\": \"list-commands\", "; + request_->context()->body_ += "\"extra\": " + std::to_string(extra) + " }"; + + // All requests must be finalized before they can be processed. + ASSERT_NO_THROW(request_->finalize()); + + // Setup hook. + CtrlAgentCfgContextPtr ctx = getCtrlAgentCfgContext(); + ASSERT_TRUE(ctx); + HooksConfig& hooks_cfg = ctx->getHooksConfig(); + std::string auth_cfg = "{ \"config\": {\n" + "\"type\": \"basic\",\n" + "\"realm\": \"ISC.ORG\",\n" + "\"clients\": [{\n" + " \"user\": \"foo\",\n" + " \"password\": \"bar\"\n" + " }]}}"; + ConstElementPtr auth_json; + ASSERT_NO_THROW(auth_json = Element::fromJSON(auth_cfg)); + hooks_cfg.add(std::string(BASIC_AUTH_LIBRARY), auth_json); + ASSERT_NO_THROW(hooks_cfg.loadLibraries(false)); + + // Add a STS header. + CfgHttpHeader hsts("Strict-Transport-Security", "max-age=31536000"); + CfgHttpHeaders headers; + headers.push_back(hsts); + // Add a random header. + CfgHttpHeader foobar("Foo", "bar"); + headers.push_back(foobar); + ctx->setHttpHeaders(headers); + + HttpResponsePtr response; + ASSERT_NO_THROW(response = response_creator_.createHttpResponse(request_)); + EXPECT_TRUE(request_->getBasicAuth().empty()); + ASSERT_TRUE(response); + + // Check that the two extra headers are in the response. + auto got = headers2map(response->context()->headers_); + EXPECT_EQ("max-age=31536000", got["Strict-Transport-Security"]); + EXPECT_EQ("bar", got["Foo"]); +} + // Test successful server response when the client is authenticated. TEST_F(CtrlAgentResponseCreatorTest, hookBasicAuth) { setBasicContext(request_); |