]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3609] Checkpoint: finished HTTP config
authorFrancis Dupont <fdupont@isc.org>
Mon, 18 Nov 2024 16:51:58 +0000 (17:51 +0100)
committerFrancis Dupont <fdupont@isc.org>
Fri, 22 Nov 2024 08:55:31 +0000 (09:55 +0100)
src/lib/config/http_command_response_creator.cc
src/lib/config/tests/http_command_response_creator_unittests.cc

index a2953f3d584221cd9e18fd0e34d0ab95cfa53a2a..b6900cff4169c44854bbd7adc89359889b1abcce 100644 (file)
@@ -80,21 +80,34 @@ createStockHttpResponseInternal(const HttpRequestPtr& request,
     }
     // This will generate the response holding JSON content.
     HttpResponsePtr response(new HttpResponseJson(http_version, status_code));
+    // Add extra headers.
+    if (config_) {
+        copyHttpHeaders(config_->getHttpHeaders(), *response);
+    }
     return (response);
 }
 
 HttpResponsePtr
 HttpCommandResponseCreator::createDynamicHttpResponse(HttpRequestPtr request) {
+    CfgHttpHeaders headers;
     HttpResponseJsonPtr http_response;
 
     // Check the basic HTTP authentication.
     if (config_) {
+        headers = config_->getHttpHeaders();
         const HttpAuthConfigPtr& auth = config_->getAuthConfig();
         if (auth) {
             http_response = auth->checkAuth(*this, 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_)) {
@@ -121,6 +134,15 @@ HttpCommandResponseCreator::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);
     }
 
index d3f732a5ce9d290080c2ef82e4997f57219d614e..45022465a97322b575d6f49dd71c16c6b2913e8b 100644 (file)
@@ -146,6 +146,16 @@ public:
         return (createAnswer(CONTROL_RESULT_SUCCESS, arguments));
     }
 
+    /// @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 HTTP control socket configuration.
     HttpCommandConfigPtr http_config_;
 
@@ -196,6 +206,35 @@ TEST_F(HttpCommandResponseCreatorTest, 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(HttpCommandResponseCreatorTest, createStockHttpResponseHeaders) {
+    // 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);
+    setHttpConfig();
+    http_config_->setHttpHeaders(headers);
+
+    setHttpCreator();
+
+    // 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(HttpCommandResponseCreatorTest, createDynamicHttpResponse) {
     setHttpCreator();
@@ -232,6 +271,39 @@ TEST_F(HttpCommandResponseCreatorTest, createDynamicHttpResponse) {
                 string::npos);
 }
 
+// Test that the server responds with extra headers for a command response.
+TEST_F(HttpCommandResponseCreatorTest, createDynamicHttpResponseHeaders) {
+    // 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);
+    setHttpConfig();
+    http_config_->setHttpHeaders(headers);
+
+    setHttpCreator();
+
+    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"]);
+}
+
 // Test successful server response without emulating agent response.
 TEST_F(HttpCommandResponseCreatorTest, createDynamicHttpResponseNoEmulation) {
     // Create the response creator setting emulate_agent_response to false;
@@ -324,6 +396,45 @@ TEST_F(HttpCommandResponseCreatorTest, basicAuthReject) {
     EXPECT_EQ("HTTP/1.1 401 Unauthorized", response->toBriefString());
 }
 
+// Test that the server responds with extra headers for auth reject response.
+TEST_F(HttpCommandResponseCreatorTest, basicAuthRejectHeaders) {
+    // Create basic HTTP authentication configuration.
+    BasicHttpAuthConfigPtr basic(new BasicHttpAuthConfig());
+    EXPECT_NO_THROW(basic->add("test", "", "123\xa3", ""));
+    setHttpConfig(false, basic);
+
+    // 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);
+    http_config_->setHttpHeaders(headers);
+
+    setHttpCreator(false);
+
+    setBasicContext(request_);
+
+    // Body: "foo" command has been registered in the test fixture constructor.
+    request_->context()->body_ = "{ \"command\": \"foo\" }";
+
+    // Add no basic HTTP authentication.
+
+    // 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 basic HTTP authentication - accept case.
 // Empty case was handled in createDynamicHttpResponseNoEmulation.
 TEST_F(HttpCommandResponseCreatorTest, basicAuthAccept) {