]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3830] Backport CVE-2025-32801 to v2_4
authorThomas Markwalder <tmark@isc.org>
Tue, 29 Apr 2025 14:24:05 +0000 (10:24 -0400)
committerAndrei Pavel <andrei@isc.org>
Wed, 7 May 2025 12:54:54 +0000 (15:54 +0300)
modified:   doc/sphinx/arm/hooks.rst
modified:   src/bin/agent/tests/ca_cfg_mgr_unittests.cc
modified:   src/bin/agent/tests/get_config_unittest.cc
modified:   src/bin/agent/tests/test_callout_libraries.h.in
modified:   src/bin/d2/tests/d2_cfg_mgr_unittests.cc
modified:   src/bin/d2/tests/d2_process_tests.sh.in
modified:   src/bin/d2/tests/d2_process_unittests.cc
modified:   src/bin/d2/tests/get_config_unittest.cc
modified:   src/bin/d2/tests/test_callout_libraries.h.in
modified:   src/bin/d2/tests/test_configured_libraries.h.in
modified:   src/bin/dhcp4/tests/config_parser_unittest.cc
modified:   src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
modified:   src/bin/dhcp4/tests/dhcp4_process_tests.sh.in
modified:   src/bin/dhcp4/tests/hooks_unittest.cc
modified:   src/bin/dhcp4/tests/test_libraries.h.in
modified:   src/bin/dhcp6/tests/config_parser_unittest.cc
modified:   src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
modified:   src/bin/dhcp6/tests/dhcp6_process_tests.sh.in
modified:   src/bin/dhcp6/tests/hooks_unittest.cc
modified:   src/bin/dhcp6/tests/test_libraries.h.in
modified:   src/lib/dhcpsrv/tests/Makefile.am
modified:   src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
modified:   src/lib/dhcpsrv/tests/test_libraries.h.in
modified:   src/lib/hooks/Makefile.am
modified:   src/lib/hooks/hooks_parser.cc
modified:   src/lib/hooks/hooks_parser.h
modified:   src/lib/hooks/tests/Makefile.am
modified:   src/lib/hooks/tests/hooks_manager_unittest.cc
modified:   src/lib/util/Makefile.am
new file:   src/lib/util/filesystem.cc
new file:   src/lib/util/filesystem.h
modified:   src/lib/util/tests/Makefile.am
new file:   src/lib/util/tests/filesystem_unittests.cc

33 files changed:
doc/sphinx/arm/hooks.rst
src/bin/agent/tests/ca_cfg_mgr_unittests.cc
src/bin/agent/tests/get_config_unittest.cc
src/bin/agent/tests/test_callout_libraries.h.in
src/bin/d2/tests/d2_cfg_mgr_unittests.cc
src/bin/d2/tests/d2_process_tests.sh.in
src/bin/d2/tests/d2_process_unittests.cc
src/bin/d2/tests/get_config_unittest.cc
src/bin/d2/tests/test_callout_libraries.h.in
src/bin/d2/tests/test_configured_libraries.h.in
src/bin/dhcp4/tests/config_parser_unittest.cc
src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc
src/bin/dhcp4/tests/dhcp4_process_tests.sh.in
src/bin/dhcp4/tests/hooks_unittest.cc
src/bin/dhcp4/tests/test_libraries.h.in
src/bin/dhcp6/tests/config_parser_unittest.cc
src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc
src/bin/dhcp6/tests/dhcp6_process_tests.sh.in
src/bin/dhcp6/tests/hooks_unittest.cc
src/bin/dhcp6/tests/test_libraries.h.in
src/lib/dhcpsrv/tests/Makefile.am
src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc
src/lib/dhcpsrv/tests/test_libraries.h.in
src/lib/hooks/Makefile.am
src/lib/hooks/hooks_parser.cc
src/lib/hooks/hooks_parser.h
src/lib/hooks/tests/Makefile.am
src/lib/hooks/tests/hooks_manager_unittest.cc
src/lib/util/Makefile.am
src/lib/util/filesystem.cc [new file with mode: 0644]
src/lib/util/filesystem.h [new file with mode: 0644]
src/lib/util/tests/Makefile.am
src/lib/util/tests/filesystem_unittests.cc [new file with mode: 0644]

index af31587d173de50875495a3f0c47f8b7ab7cce08..156a094d809c9af5a97e1ffc6ebe8dc31df2af42 100644 (file)
@@ -216,6 +216,46 @@ configuration would be:
    because the parameters specified for the library (or the files those
    parameters point to) may have changed.
 
+As of Kea 2.4.2, hook libraries may only be loaded from the default installation
+directory determined during compilation and shown in the config report as
+"Hooks directory".  This value may be overridden at startup by setting the
+environment variable ``KEA_HOOKS_PATH`` to the desired path.  If a path other
+than this value is used in a ``library`` element Kea will emit an error and refuse
+to load the library. For ease of use ``library`` elements may simply omit path
+components, specifying the file name only as shown below:
+
+.. code-block:: json
+
+    {
+        "Dhcp6": {
+            "hooks-libraries": [
+                {
+                    "library": "first_custom_hooks_example.so"
+                },
+                {
+                    "library": "second_custom_hooks_example.so"
+                }
+            ]
+        }
+    }
+
+This snippet (on Debian 12) is equivalent to:
+
+.. code-block:: json
+
+    {
+        "Dhcp6": {
+            "hooks-libraries": [
+                {
+                    "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/first_custom_hooks_example.so"
+                },
+                {
+                    "library": "/usr/lib/x86_64-linux-gnu/kea/hooks/second_custom_hooks_example.so"
+                }
+            ]
+        }
+    }
+
 Libraries may have additional parameters that are not mandatory, in the
 sense that there may be libraries that do not require them. However, for any
 given library there is often a requirement to specify a certain
index a7b40c55671ae548933edbe4b2e307c1da22c973..73f5a85cd69e48600ac9a84d780526f3fa25a509 100644 (file)
@@ -11,6 +11,7 @@
 #include <process/testutils/d_test_stubs.h>
 #include <process/d_cfg_mgr.h>
 #include <http/basic_auth_config.h>
+#include <hooks/hooks_parser.h>
 #include <agent/tests/test_callout_libraries.h>
 #include <agent/tests/test_data_files_config.h>
 #include <boost/pointer_cast.hpp>
@@ -423,6 +424,26 @@ const char* AGENT_CONFIGS[] = {
 /// @brief Class used for testing CfgMgr
 class AgentParserTest : public isc::process::ConfigParseTest {
 public:
+    virtual void SetUp() {
+        resetHooksPath();
+    }
+
+    virtual void TearDown() {
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : CA_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
+    }
 
     /// @brief Tries to load input text as a configuration
     ///
@@ -462,6 +483,8 @@ TEST_F(AgentParserTest, DISABLED_configParseEmpty) {
 
 // This test checks if a config with only HTTP parameters is parsed properly.
 TEST_F(AgentParserTest, configParseHttpOnly) {
+    setHooksTestPath();
+
     configParse(AGENT_CONFIGS[1], 0);
 
     CtrlAgentCfgContextPtr ctx = cfg_mgr_.getCtrlAgentCfgContext();
@@ -544,6 +567,8 @@ TEST_F(AgentParserTest, configParse3Sockets) {
 // name. In particular, it checks if such a library exists. Therefore we
 // can't use AGENT_CONFIGS[4] as is, but need to run it through path replacer.
 TEST_F(AgentParserTest, configParseHooks) {
+    setHooksTestPath();
+
     // Create the configuration with proper lib path.
     std::string cfg = pathReplacer(AGENT_CONFIGS[4], CALLOUT_LIBRARY);
     // The configuration should be successful.
index 40d9c502f93e37c53dd1e051606464048f806052..e5f741925ff300535b554d8e9dc3983b94bdc0cf 100644 (file)
@@ -12,6 +12,7 @@
 #include <process/testutils/d_test_stubs.h>
 #include <agent/ca_cfg_mgr.h>
 #include <agent/parser_context.h>
+#include <hooks/hooks_parser.h>
 #include <boost/scoped_ptr.hpp>
 #include <gtest/gtest.h>
 
@@ -28,6 +29,7 @@ using namespace isc::config;
 using namespace isc::data;
 using namespace isc::process;
 using namespace isc::test;
+using namespace isc::hooks;
 
 namespace {
 
@@ -134,6 +136,7 @@ class CtrlAgentGetCfgTest : public ConfigParseTest {
 public:
     CtrlAgentGetCfgTest()
     : rcode_(-1) {
+        resetHooksPath();
         srv_.reset(new NakedAgentCfgMgr());
         // Create fresh context.
         resetConfiguration();
@@ -141,6 +144,20 @@ public:
 
     ~CtrlAgentGetCfgTest() {
         resetConfiguration();
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : CA_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
     }
 
     /// @brief Parse and Execute configuration
@@ -245,6 +262,7 @@ public:
 
 /// Test a configuration
 TEST_F(CtrlAgentGetCfgTest, simple) {
+    setHooksTestPath();
 
     // get the simple configuration
     std::string simple_file = string(CFG_EXAMPLES) + "/" + "simple.json";
index 78f51c8815eb0649d96d05b65e9795f9de78e1a1..7e888ba732b776ae895bb39db830a442f3fbf7e4 100644 (file)
@@ -19,6 +19,9 @@ namespace {
 // Basic callout library with context_create and three "standard" callouts.
 static const char* CALLOUT_LIBRARY = "@abs_builddir@/.libs/libcallout.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* CA_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 #endif // TEST_LIBRARIES_H
index 5e27ff8a5c2a07806b591ffb763443a27c577138..7e2aee526365294940b5aa21d12830f4881f5a6a 100644 (file)
@@ -13,6 +13,7 @@
 #include <d2srv/d2_config.h>
 #include <d2srv/d2_simple_parser.h>
 #include <dhcpsrv/testutils/config_result_check.h>
+#include <hooks/hooks_parser.h>
 #include <process/testutils/d_test_stubs.h>
 #include <test_data_files_config.h>
 #include <util/encode/base64.h>
@@ -48,10 +49,25 @@ public:
 
     /// @brief Constructor
     D2CfgMgrTest():cfg_mgr_(new D2CfgMgr()), d2_params_() {
+        resetHooksPath();
     }
 
     /// @brief Destructor
     ~D2CfgMgrTest() {
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : D2_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
     }
 
     /// @brief Configuration manager instance.
@@ -470,6 +486,8 @@ TEST(D2CfgMgr, construction) {
 /// as it would be done by d2_process in response to a configuration update
 /// event.
 TEST_F(D2CfgMgrTest, fullConfig) {
+    setHooksTestPath();
+
     // Create a configuration with all of application level parameters, plus
     // both the forward and reverse ddns managers.  Both managers have two
     // domains with three servers per domain.
index 895b9a2d921bdbd5f3211fc5842519d635162c3f..408e8e0fad27056efa46e0c663e9fa796dd9acf1 100644 (file)
@@ -20,6 +20,8 @@ set -eu
 CFG_FILE="@abs_top_builddir@/src/bin/d2/tests/test_config.json"
 # Path to the D2 log file.
 LOG_FILE="@abs_top_builddir@/src/bin/d2/tests/test.log"
+# Set env KEA_HOOKS_PATH  to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/d2/tests/.libs"
 # D2 configuration to be stored in the configuration file.
 CONFIG="{
     \"DhcpDdns\":
index 4dda27551d0e02e71c10ca24a7764bc980d4ef0d..cfff351cfa90122d4c8487d8870dcaa15bdfcc08 100644 (file)
@@ -12,6 +12,7 @@
 #include <d2/d2_process.h>
 #include <d2/tests/test_configured_libraries.h>
 #include <dhcp_ddns/ncr_io.h>
+#include <hooks/hooks_parser.h>
 #include <process/testutils/d_test_stubs.h>
 
 #include <boost/date_time/posix_time/posix_time.hpp>
@@ -26,6 +27,7 @@ using namespace isc::config;
 using namespace isc::d2;
 using namespace isc::data;
 using namespace isc::process;
+using namespace isc::hooks;
 using namespace boost::posix_time;
 
 namespace {
@@ -64,10 +66,25 @@ public:
     D2ProcessTest() :
         D2Process("d2test",
                   asiolink::IOServicePtr(new isc::asiolink::IOService())) {
+        resetHooksPath();
     }
 
     /// @brief Destructor
     virtual ~D2ProcessTest() {
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : D2_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
     }
 
     /// @brief Callback that will invoke shutdown method.
@@ -650,6 +667,8 @@ TEST_F(D2ProcessTest, v6LoopbackTest) {
 
 /// @brief Check the configured callout (positive case).
 TEST_F(D2ProcessTest, configuredNoFail) {
+    setHooksTestPath();
+
     const char* config = "{\n"
         "\"hooks-libraries\": [ {\n"
         " \"library\": \"%LIBRARY%\",\n"
@@ -669,6 +688,8 @@ TEST_F(D2ProcessTest, configuredNoFail) {
 
 /// @brief Check the configured callout (negative case).
 TEST_F(D2ProcessTest, configuredFail) {
+    setHooksTestPath();
+
     const char* config = "{\n"
         "\"user-context\": { \"error\": \"Fail!\" },\n"
         "\"hooks-libraries\": [ {\n"
index 0936e82160d26298761813bcf6c7ca6e604f1160..7ca53b86c45e920768cbfe4b6c234b52e852d548 100644 (file)
@@ -11,6 +11,7 @@
 #include <d2/parser_context.h>
 #include <d2srv/d2_cfg_mgr.h>
 #include <d2srv/d2_config.h>
+#include <hooks/hooks_parser.h>
 #include <process/testutils/d_test_stubs.h>
 #include <testutils/user_context_utils.h>
 #include <gtest/gtest.h>
@@ -27,6 +28,7 @@ using namespace isc::config;
 using namespace isc::d2;
 using namespace isc::data;
 using namespace isc::process;
+using namespace isc::hooks;
 using namespace isc::test;
 
 namespace {
@@ -114,6 +116,7 @@ class D2GetConfigTest : public ConfigParseTest {
 public:
     D2GetConfigTest()
     : rcode_(-1) {
+        resetHooksPath();
         srv_.reset(new D2CfgMgr());
         // Enforce not verbose mode.
         Daemon::setVerbose(false);
@@ -123,6 +126,20 @@ public:
 
     ~D2GetConfigTest() {
         resetConfiguration();
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : D2_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
     }
 
     /// @brief Parse and Execute configuration
@@ -230,6 +247,7 @@ public:
 
 /// Test a configuration
 TEST_F(D2GetConfigTest, sample1) {
+    setHooksTestPath();
 
     // get the sample1 configuration
     std::string sample1_file = string(CFG_EXAMPLES) + "/" + "sample1.json";
index a88d131011acd2fa16339ce6830258508f91253e..82602dfcc96364f4eed52fb2bf137fafdf48a831 100644 (file)
@@ -19,6 +19,9 @@ namespace {
 // Basic callout library with context_create and three "standard" callouts.
 static const char* CALLOUT_LIBRARY = "@abs_builddir@/.libs/libcallout.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* D2_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 #endif // D2_TEST_CALLOUT_LIBRARIES_H
index 2b235ae68fbb838c63d9e5ec2059a82141c0daf3..9feb0bb3416e163ab7060a070a4b8269c0125873 100644 (file)
@@ -21,6 +21,9 @@ namespace {
 // content of the error entry.
 static const char* CONFIGURED_LIBRARY = "@abs_builddir@/.libs/libconfigured.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* D2_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 #endif // D2_TEST_CONFIGURED_LIBRARIES_H
index 2d98963a4353c75620c3a671aba033b100607dc1..8765795e785240bc0469c8aa89ec4c8ff3d09f27 100644 (file)
@@ -29,6 +29,7 @@
 #include <dhcpsrv/testutils/test_config_backend_dhcp4.h>
 #include <process/config_ctl_info.h>
 #include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
 #include <stats/stats_mgr.h>
 #include <testutils/log_utils.h>
 #include <testutils/gtest_utils.h>
@@ -278,6 +279,7 @@ protected:
     virtual void SetUp() {
         std::vector<std::string> libraries = HooksManager::getLibraryNames();
         ASSERT_TRUE(libraries.empty());
+        resetHooksPath();
     }
 
 public:
@@ -289,6 +291,7 @@ public:
         srv_.reset(new ControlledDhcpv4Srv(0));
         // Create fresh context.
         resetConfiguration();
+        resetHooksPath();
     }
 
 public:
@@ -302,6 +305,19 @@ public:
         EXPECT_EQ(expected_code, rcode_) << "error text:" << comment_->stringValue();
     }
 
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : DHCP4_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
+    }
+
     // Checks if the result of DHCP server configuration has
     // expected code (0 for success, other for failures) and
     // the text part. Also stores result in rcode_ and comment_.
@@ -4353,6 +4369,8 @@ TEST_F(Dhcp4ParserTest, InvalidLibrary) {
 
 // Verify the configuration of hooks libraries with two being specified.
 TEST_F(Dhcp4ParserTest, LibrariesSpecified) {
+    setHooksTestPath();
+
     // Marker files should not be present.
     EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
     EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
index ee0e28a31babacee56e63b1e2c389460fcdd3ac5..a135c64ea881c0ded76eb5196710f4f38498a944 100644 (file)
@@ -449,6 +449,7 @@ TEST_F(CtrlChannelDhcpv4SrvTest, commands) {
 // Check that the "libreload" command will reload libraries
 TEST_F(CtrlChannelDhcpv4SrvTest, libreload) {
     createUnixChannelServer();
+    ASSERT_TRUE(DHCP4_HOOKS_TEST_PATH);  // suppress unused warning.
 
     // Ensure no marker files to start with.
     ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
index 76e7177094f6d3c159dd3616f840faad7d62390b..0ea9174d72a1c731c4389cb1e6b33af89155e185 100644 (file)
@@ -26,6 +26,10 @@ LEASE_FILE="@abs_top_builddir@/src/bin/dhcp4/tests/test_leases.csv"
 export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
 # Path to test hooks library
 HOOK_PATH="@abs_top_builddir@/src/bin/dhcp4/tests/.libs/libco3.so"
+
+# Set env KEA_HOOKS_PATH to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/dhcp4/tests/.libs"
+
 # Kea configuration to be stored in the configuration file.
 CONFIG="{
     \"Dhcp4\":
index 905ce87a6afdc1e3b6d7e1f2636fe3abb82d9560..8dd8581f25f86aca8470b839dffbeb940e3fc05d 100644 (file)
@@ -21,6 +21,7 @@
 #include <dhcp4/tests/test_libraries.h>
 #include <hooks/server_hooks.h>
 #include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
 #include <hooks/callout_manager.h>
 #include <stats/stats_mgr.h>
 #include <util/multi_threading_mgr.h>
@@ -1003,6 +1004,7 @@ public:
     LoadUnloadDhcpv4SrvTest() {
         reset();
         MultiThreadingMgr::instance().setMode(false);
+        resetHooksPath();
     }
 
     /// @brief Destructor
@@ -1010,6 +1012,7 @@ public:
         server_.reset();
         reset();
         MultiThreadingMgr::instance().setMode(false);
+        resetHooksPath();
     };
 
     /// @brief Reset hooks data
@@ -1027,6 +1030,19 @@ public:
 
         CfgMgr::instance().clear();
     }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : DHCP4_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
+    }
 };
 
 // Checks if callouts installed on buffer4_receive are indeed called and the
@@ -3220,6 +3236,7 @@ TEST_F(LoadUnloadDhcpv4SrvTest, failLoadIncompatibleLibraries) {
 // Checks if callouts installed on the dhcp4_srv_configured ared indeed called
 // and all the necessary parameters are passed.
 TEST_F(LoadUnloadDhcpv4SrvTest, Dhcpv4SrvConfigured) {
+    setHooksTestPath();
     for (string parameters : {
         "",
         R"(, "parameters": { "mode": "fail-without-error" } )",
index 9b9a243b8bdd2123ba25f1194f6228ea5e83c35d..00725d246b9a1a3ea476103bb50350251d5d23f5 100644 (file)
@@ -26,6 +26,9 @@ const char* const CALLOUT_LIBRARY_3 = "@abs_builddir@/.libs/libco3.so";
 // Name of a library which is not present.
 const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCP4_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 
index bd8435fd48043e849df7e240415889e72de922ea..735fe4622bd7d0c18e84d7a98c6ff7de7b6cb181 100644 (file)
@@ -28,6 +28,7 @@
 #include <dhcpsrv/testutils/config_result_check.h>
 #include <dhcpsrv/testutils/test_config_backend_dhcp6.h>
 #include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
 #include <process/config_ctl_info.h>
 #include <stats/stats_mgr.h>
 #include <testutils/gtest_utils.h>
@@ -396,6 +397,8 @@ public:
 
         // Reset configuration for each test.
         resetConfiguration();
+
+        resetHooksPath();
     }
 
     ~Dhcp6ParserTest() {
@@ -405,8 +408,24 @@ public:
         // ... and delete the hooks library marker files if present
         static_cast<void>(remove(LOAD_MARKER_FILE));
         static_cast<void>(remove(UNLOAD_MARKER_FILE));
+
+        resetHooksPath();
     };
 
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : DHCP6_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
+    }
+
+
     // Checks if config_result (result of DHCP server configuration) has
     // expected code (0 for success, other for failures).
     // Also stores result in rcode_ and comment_.
@@ -4616,6 +4635,8 @@ TEST_F(Dhcp6ParserTest, InvalidLibrary) {
 
 // Verify the configuration of hooks libraries with two being specified.
 TEST_F(Dhcp6ParserTest, LibrariesSpecified) {
+    setHooksTestPath();
+
     // Marker files should not be present.
     EXPECT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
     EXPECT_FALSE(checkMarkerFileExists(UNLOAD_MARKER_FILE));
index f05a40212b526655a3d398d92ccb9aaeae06ea9c..ac52dd81832e54681114c13f73f338365c94238b 100644 (file)
@@ -466,6 +466,7 @@ TEST_F(CtrlDhcpv6SrvTest, commands) {
 // Check that the "libreload" command will reload libraries
 TEST_F(CtrlChannelDhcpv6SrvTest, libreload) {
     createUnixChannelServer();
+    ASSERT_TRUE(DHCP6_HOOKS_TEST_PATH);  // suppress unused warning.
 
     // Ensure no marker files to start with.
     ASSERT_FALSE(checkMarkerFileExists(LOAD_MARKER_FILE));
index 085728793714beb35337ba1b593efc039fa94437..d209e3c40b56326a2e99bfa4537b5b0eac595385 100644 (file)
@@ -26,6 +26,10 @@ LEASE_FILE="@abs_top_builddir@/src/bin/dhcp6/tests/test_leases.csv"
 export KEA_LFC_EXECUTABLE="@abs_top_builddir@/src/bin/lfc/kea-lfc"
 # Path to test hooks library
 HOOK_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/.libs/libco3.so"
+
+# Set env KEA_HOOKS_PATH  to override DEFAULT_HOOKS_PATH
+export KEA_HOOKS_PATH="@abs_top_builddir@/src/bin/dhcp6/tests/.libs"
+
 # Kea configuration to be stored in the configuration file.
 CONFIG="{
     \"Dhcp6\":
index c34aa763c695292ebd86060fc31f4970cfccc8b6..ed1bcaf2b00893c5804609a228a605cd9ee1df53 100644 (file)
@@ -26,6 +26,7 @@
 #include <dhcp6/tests/test_libraries.h>
 #include <hooks/server_hooks.h>
 #include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
 #include <hooks/callout_manager.h>
 #include <stats/stats_mgr.h>
 #include <util/buffer.h>
@@ -1055,6 +1056,8 @@ public:
     LoadUnloadDhcpv6SrvTest() : Dhcpv6SrvTest() {
         reset();
         MultiThreadingMgr::instance().setMode(false);
+
+        resetHooksPath();
     }
 
     /// @brief Destructor
@@ -1062,6 +1065,8 @@ public:
         server_.reset();
         reset();
         MultiThreadingMgr::instance().setMode(false);
+
+        resetHooksPath();
     };
 
     /// @brief Reset hooks data
@@ -1079,6 +1084,19 @@ public:
 
         CfgMgr::instance().clear();
     }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param explicit_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                            explicit_path : DHCP6_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
+    }
 };
 
 // Checks if callouts installed on buffer6_receive are indeed called and the
@@ -5589,6 +5607,8 @@ TEST_F(LoadUnloadDhcpv6SrvTest, failLoadIncompatibleLibraries) {
 // Checks if callouts installed on the dhcp6_srv_configured ared indeed called
 // and all the necessary parameters are passed.
 TEST_F(LoadUnloadDhcpv6SrvTest, Dhcpv6SrvConfigured) {
+    setHooksTestPath();
+
     for (string parameters : {
         "",
         R"(, "parameters": { "mode": "fail-without-error" } )",
index 95d74592ad4bbd5a6cae2263807195c01d3b57ec..237c56dc91c7f57b021f6a639540cc78d353838e 100644 (file)
@@ -24,6 +24,9 @@ const char* const CALLOUT_LIBRARY_3 = "@abs_builddir@/.libs/libco3.so";
 // Name of a library which is not present.
 const char* const NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCP6_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 
index a73d0f34a74b493b649f370d1d4cdf1e5ede3e88..992ce5b3cfe0692807d9955243ef219d5d72c05d 100644 (file)
@@ -6,6 +6,7 @@ AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\
 AM_CPPFLAGS += -DDHCP_DATA_DIR=\"$(abs_top_builddir)/src/lib/dhcpsrv/tests\"
 AM_CPPFLAGS += -DKEA_LFC_BUILD_DIR=\"$(abs_top_builddir)/src/bin/lfc\"
 AM_CPPFLAGS += -DINSTALL_PROG=\"$(abs_top_srcdir)/install-sh\"
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
 
 AM_CXXFLAGS = $(KEA_CXXFLAGS)
 
index de9f6a2e5d63c1d3acbb13426e5b0407f576c3ca..372a154acadc137ebd8f265b3ccf505f323a6d9d 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2023 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2025 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
@@ -171,11 +171,26 @@ public:
     ParseConfigTest()
         :family_(AF_INET6) {
         reset_context();
+        resetHooksPath();
     }
 
     ~ParseConfigTest() {
         reset_context();
         CfgMgr::instance().clear();
+        resetHooksPath();
+    }
+
+    /// @brief Sets the Hooks path from which hooks can be loaded.
+    /// @param custom_path path to use as the hooks path.
+    void setHooksTestPath(const std::string explicit_path = "") {
+        HooksLibrariesParser::getHooksPath(true,
+                                           (!explicit_path.empty() ?
+                                           explicit_path : DHCPSRV_HOOKS_TEST_PATH));
+    }
+
+    /// @brief Resets the hooks path to DEFAULT_HOOKS_PATH.
+    void resetHooksPath() {
+        HooksLibrariesParser::getHooksPath(true);
     }
 
     /// @brief Parses a configuration.
@@ -1917,6 +1932,8 @@ TEST_F(ParseConfigTest, noHooksLibraries) {
 
 // hooks-libraries element that contains a single library.
 TEST_F(ParseConfigTest, oneHooksLibrary) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -1950,6 +1967,8 @@ TEST_F(ParseConfigTest, oneHooksLibrary) {
 
 // hooks-libraries element that contains two libraries
 TEST_F(ParseConfigTest, twoHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -1986,6 +2005,8 @@ TEST_F(ParseConfigTest, twoHooksLibraries) {
 
 // Configure with two libraries, then reconfigure with the same libraries.
 TEST_F(ParseConfigTest, reconfigureSameHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -2037,6 +2058,8 @@ TEST_F(ParseConfigTest, reconfigureSameHooksLibraries) {
 // Configure the hooks with two libraries, then reconfigure with the same
 // libraries, but in reverse order.
 TEST_F(ParseConfigTest, reconfigureReverseHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -2074,6 +2097,8 @@ TEST_F(ParseConfigTest, reconfigureReverseHooksLibraries) {
 // Configure the hooks with two libraries, then reconfigure with
 // no libraries.
 TEST_F(ParseConfigTest, reconfigureZeroHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -2114,6 +2139,8 @@ TEST_F(ParseConfigTest, reconfigureZeroHooksLibraries) {
 
 // Check with a set of libraries, some of which are invalid.
 TEST_F(ParseConfigTest, invalidHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -2148,6 +2175,8 @@ TEST_F(ParseConfigTest, invalidHooksLibraries) {
 
 // Check that trying to reconfigure with an invalid set of libraries fails.
 TEST_F(ParseConfigTest, reconfigureInvalidHooksLibraries) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
@@ -2192,6 +2221,7 @@ TEST_F(ParseConfigTest, reconfigureInvalidHooksLibraries) {
 
 // Check that if hooks-libraries contains invalid syntax, it is detected.
 TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
+    setHooksTestPath("/opt/lib");
 
     // Element holds a mixture of (valid) maps and non-maps.
     string config1 = "{ \"hooks-libraries\": [ "
@@ -2226,7 +2256,8 @@ TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
         "{ \"library\": \"/opt/lib/lib1\" }, "
         "{ \"library\": \"\" } "
         "] }";
-    string error3 = "value of 'library' element must not be blank";
+    string error3 = "Configuration parsing failed: hooks library configuration error:"
+                    " path: '' has no filename (<string>:1:69)";
 
     rcode = parseConfiguration(config3);
     ASSERT_NE(0, rcode);
@@ -2239,11 +2270,12 @@ TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
         "{ \"library\": \"/opt/lib/lib1\" }, "
         "{ \"library\": \"      \" } "
         "] }";
-    string error4 = "value of 'library' element must not be blank";
+    string error4 = "Configuration parsing failed: hooks library configuration error:"
+                    " path: '' has no filename (<string>:1:69)";
 
     rcode = parseConfiguration(config4);
     ASSERT_NE(0, rcode);
-    EXPECT_TRUE(error_text_.find(error3) != string::npos) <<
+    EXPECT_TRUE(error_text_.find(error4) != string::npos) <<
         "Error text returned from parse failure is " << error_text_;
 
     // Element holds valid maps, except one that does not contain a
@@ -2264,6 +2296,8 @@ TEST_F(ParseConfigTest, invalidSyntaxHooksLibraries) {
 
 // Check that some parameters may have configuration parameters configured.
 TEST_F(ParseConfigTest, HooksLibrariesParameters) {
+    setHooksTestPath();
+
     // Check that no libraries are currently loaded
     vector<string> hooks_libraries = HooksManager::getLibraryNames();
     EXPECT_TRUE(hooks_libraries.empty());
index 5a5545eae64429046a3afb66afe86c6622379808..e972b2cff62af47480cd9a169e1ba86cb618f590 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2016 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2025 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
@@ -29,6 +29,9 @@ static const char* CALLOUT_PARAMS_LIBRARY = "@abs_builddir@/.libs/libco3.so";
 // Name of a library which is not present.
 static const char* NOT_PRESENT_LIBRARY = "@abs_builddir@/.libs/libnothere.so";
 
+// Test path to use for in place of DEFAULT_HOOKS_PATH
+static const char* DHCPSRV_HOOKS_TEST_PATH = "@abs_builddir@/.libs";
+
 } // anonymous namespace
 
 
index 5b9bddb2b933d43edefe441cb9c0f8662fe5e61a..e97fcc81b8694aa620554c60e0f59b5b0fabb78b 100644 (file)
@@ -1,6 +1,7 @@
 SUBDIRS = . tests
 
 AM_CPPFLAGS  = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
 AM_CPPFLAGS += $(BOOST_INCLUDES)
 AM_CXXFLAGS  = $(KEA_CXXFLAGS)
 
index cfd4a7e8f52b8b5d5e159cf0966c8a8519e02dbb..46641132162a50dfa5c3564166cf6ac510e386cd 100644 (file)
 #include <hooks/hooks_parser.h>
 #include <boost/algorithm/string.hpp>
 #include <boost/foreach.hpp>
+#include <util/filesystem.h>
 #include <util/strutil.h>
+
 #include <vector>
 
 using namespace std;
 using namespace isc::data;
 using namespace isc::hooks;
 using namespace isc::dhcp;
+using namespace isc::util::file;
 
 namespace isc {
 namespace hooks {
 
+std::string
+HooksLibrariesParser::getHooksPath(bool reset /* = false */, const std::string explicit_path /* = "" */) {
+    static std::string default_hooks_path = ""; 
+    if (default_hooks_path.empty() || reset) {
+        if (explicit_path.empty()) {
+            default_hooks_path = std::string(std::getenv("KEA_HOOKS_PATH") ?
+                                             std::getenv("KEA_HOOKS_PATH")
+                                             : DEFAULT_HOOKS_PATH);
+        } else {
+            default_hooks_path = explicit_path;
+        }
+    }
+
+    return (default_hooks_path);
+}
+
 // @todo use the flat style, split into list and item
 
 void
@@ -65,17 +84,12 @@ HooksLibrariesParser::parse(HooksConfig& libraries, ConstElementPtr value) {
 
                 // Get the name of the library and add it to the list after
                 // removing quotes.
-                libname = (entry_item.second)->stringValue();
-
-                // Remove leading/trailing quotes and any leading/trailing
-                // spaces.
-                boost::erase_all(libname, "\"");
-                libname = isc::util::str::trim(libname);
-                if (libname.empty()) {
+                try {
+                    libname = validatePath((entry_item.second)->stringValue());
+                } catch  (const std::exception& ex) {
                     isc_throw(DhcpConfigError, "hooks library configuration"
-                        " error: value of 'library' element must not be"
-                        " blank (" <<
-                        entry_item.second->getPosition() << ")");
+                        " error: " << ex.what() << " ("
+                        << entry_item.second->getPosition() << ")");
                 }
 
                 // Note we have found the library name.
@@ -106,5 +120,12 @@ HooksLibrariesParser::parse(HooksConfig& libraries, ConstElementPtr value) {
     }
 }
 
+std::string
+HooksLibrariesParser::validatePath(const std::string libpath,
+                                   bool enforce_path /* = true */) {
+    return (FileManager::validatePath(HooksLibrariesParser::getHooksPath(),
+                                      libpath, enforce_path));
+}
+
 }
 }
index 4bec9a6aff6d1a1da6ff5c633ce6a5c07f6de877..f37d8070f7e800c24d16ff8701689e1d8a56d709 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2017-2025 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
@@ -57,6 +57,32 @@ public:
     /// @param libraries parsed libraries information will be stored here
     /// @param value pointer to the content to be parsed
     void parse(HooksConfig& libraries, isc::data::ConstElementPtr value);
+
+    /// @brief Validates a library path against the supported path for hooks libraries.
+    ///
+    /// @param libpath library path to validate.
+    /// @param enforce_path enables validation against the supported path.
+    /// If false verifies only that the path contains a file name.
+    ///
+    /// @return validated path
+    static std::string validatePath(const std::string libpath,
+                                    bool enforce_path = true);
+
+    /// @brief Fetches the supported Hooks path.
+    ///
+    /// The first call to this function with no arguments will set the default
+    /// hooks path to either the value of DEFAULT_HOOKS_PATH or the environment
+    /// variable KEA_HOOKS_PATH if it is defined.  Subsequent calls with no
+    /// arguments will simply return this value.
+    ///
+    /// @param reset recalculate when true, defaults to false. This is for
+    /// testing purposes only.
+    /// @param explicit_path set default hooks path to this value. This is
+    /// for testing purposes only.
+    ///
+    /// @return String containing the default hooks path.
+    static std::string getHooksPath(bool reset = false,
+                                    const std::string explicit_path = "");
 };
 
 }; // namespace isc::hooks
index 8cbe4138fa6a187bc40a255d99b4403b97a41286..803f53876b4a0cc82ad0ff083945bc9f93759cce 100644 (file)
@@ -1,6 +1,7 @@
 SUBDIRS = .
 
 AM_CPPFLAGS  = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
+AM_CPPFLAGS += -DDEFAULT_HOOKS_PATH=\"$(libdir)/kea/hooks\"
 AM_CPPFLAGS += $(BOOST_INCLUDES)
 
 AM_CXXFLAGS  = $(KEA_CXXFLAGS)
index a36d42ce0da723e3a0031a92decc044854d00f1c..110bce4917aafd82a036c31212932a8f0f4e58cb 100644 (file)
@@ -8,7 +8,9 @@
 
 #include <hooks/callout_handle.h>
 #include <hooks/hooks_manager.h>
+#include <hooks/hooks_parser.h>
 #include <hooks/server_hooks.h>
+#include <testutils/gtest_utils.h>
 
 #include <hooks/tests/common_test_class.h>
 #define TEST_ASYNC_CALLOUT
@@ -1077,5 +1079,170 @@ TEST_F(HooksManagerTest, UnloadBeforeUnpark) {
     EXPECT_FALSE(unparked);
 }
 
+/// @brief Test fixture for hooks parsing.
+class HooksParserTest : public ::testing::Test {
+public:
+    /// @brief Constructor
+    HooksParserTest() {
+        // Save current value of the environment path.
+        char* env_path = std::getenv("KEA_HOOKS_PATH");
+        if (env_path) {
+            original_path_ = std::string(env_path);
+        }
+
+        // Clear the environment path.
+        unsetenv("KEA_HOOKS_PATH");
+    }
+
+    /// @brief Destructor
+    ~HooksParserTest() {
+        // Restore the original environment path.
+        if (!original_path_.empty()) {
+            setenv("KEA_HOOKS_PATH", original_path_.c_str(), 1);
+        } else {
+            unsetenv("KEA_HOOKS_PATH");
+        }
+    }
+
+    /// @brief Retains the environment variable's original value.
+    std::string original_path_;
+};
+
+TEST_F(HooksParserTest, getHooksPath) {
+    ASSERT_FALSE(std::getenv("KEA_HOOKS_PATH"));
+    auto hooks_path = HooksLibrariesParser::getHooksPath(true);
+    EXPECT_EQ(hooks_path, DEFAULT_HOOKS_PATH);
+}
+
+TEST_F(HooksParserTest, getHooksPathWithEnv) {
+    std::string evar("KEA_HOOKS_PATH=/tmp");
+    putenv(const_cast<char*>(evar.c_str()));
+    ASSERT_TRUE(std::getenv("KEA_HOOKS_PATH"));
+    auto hooks_path = HooksLibrariesParser::getHooksPath(true);
+    EXPECT_EQ(hooks_path, "/tmp");
+}
+
+TEST_F(HooksParserTest, getHooksPathExplicit) {
+    auto hooks_path = HooksLibrariesParser::getHooksPath(true, "/explicit/path");
+    EXPECT_EQ(hooks_path, "/explicit/path");
+}
+
+// Verifies HooksParser::validatePath() when enforce_path is true.
+TEST_F(HooksParserTest, validatePathEnforcePath) {
+    HooksLibrariesParser::getHooksPath(true);
+    std::string def_path(HooksLibrariesParser::getHooksPath());
+    struct Scenario {
+        int line_;
+        std::string lib_path_;
+        std::string exp_path_;
+        std::string exp_error_;
+    };
+
+    std::list<Scenario> scenarios = {
+    {
+        // Invalid parent path.
+        __LINE__,
+        "/var/lib/bs/mylib.so",
+        "",
+        string("invalid path specified: '/var/lib/bs', supported path is '" + def_path + "'")
+    },
+    {
+        // No file name.
+        __LINE__,
+        def_path + "/",
+        "",
+        string ("path: '" + def_path + "/' has no filename")
+    },
+    {
+        // File name only is valid.
+        __LINE__,
+        "mylib.so",
+        def_path + "/mylib.so",
+        ""
+    },
+    {
+        // Valid full path.
+        __LINE__,
+        def_path + "/mylib.so",
+        def_path + "/mylib.so",
+        ""
+    }
+    };
+
+    for (auto scenario : scenarios) {
+        std::ostringstream oss;
+        oss << " Scenario at line: " << scenario.line_;
+        SCOPED_TRACE(oss.str());
+        std::string validated_path;
+        if (scenario.exp_error_.empty()) {
+            ASSERT_NO_THROW_LOG(validated_path =
+                                HooksLibrariesParser::validatePath(scenario.lib_path_));
+            EXPECT_EQ(validated_path, scenario.exp_path_);
+        } else {
+            ASSERT_THROW_MSG(validated_path =
+                             HooksLibrariesParser::validatePath(scenario.lib_path_),
+                             BadValue, scenario.exp_error_);
+        }
+    }
+}
+
+// Verifies HooksParser::validatePath() when enforce_path is false.
+TEST_F(HooksParserTest, validatePathEnforcePathFalse) {
+    HooksLibrariesParser::getHooksPath(true);
+    std::string def_path(HooksLibrariesParser::getHooksPath());
+    struct Scenario {
+        int line_;
+        std::string lib_path_;
+        std::string exp_path_;
+        std::string exp_error_;
+    };
+
+    std::list<Scenario> scenarios = {
+    {
+        // Invalid parent path will fly.
+        __LINE__,
+        "/var/lib/bs/mylib.so",
+        "/var/lib/bs/mylib.so",
+        "",
+    },
+    {
+        // No file name.
+        __LINE__,
+        def_path + "/",
+        "",
+        string ("path: '" + def_path + "/' has no filename")
+    },
+    {
+        // File name only is valid.
+        __LINE__,
+        "mylib.so",
+        def_path + "/mylib.so",
+        ""
+    },
+    {
+        // Valid full path.
+        __LINE__,
+        def_path + "/mylib.so",
+        def_path + "/mylib.so",
+        ""
+    }
+    };
+
+    for (auto scenario : scenarios) {
+        std::ostringstream oss;
+        oss << " Scenario at line: " << scenario.line_;
+        SCOPED_TRACE(oss.str());
+        std::string validated_path;
+        if (scenario.exp_error_.empty()) {
+            ASSERT_NO_THROW_LOG(validated_path =
+                                HooksLibrariesParser::validatePath(scenario.lib_path_, false));
+            EXPECT_EQ(validated_path, scenario.exp_path_);
+        } else {
+            ASSERT_THROW_MSG(validated_path =
+                             HooksLibrariesParser::validatePath(scenario.lib_path_, false),
+                             BadValue, scenario.exp_error_);
+        }
+    }
+}
 
 } // Anonymous namespace
index e2833a9649bb510b12d7f4ff3040247840511413..f631523d0c76949ca0b3e987ee94ab7df4c9a5a3 100644 (file)
@@ -47,6 +47,7 @@ libkea_util_la_SOURCES += encode/base_n.cc encode/hex.h
 libkea_util_la_SOURCES += encode/binary_from_base32hex.h
 libkea_util_la_SOURCES += encode/binary_from_base16.h
 libkea_util_la_SOURCES += encode/utf8.cc encode/utf8.h
+libkea_util_la_SOURCES += filesystem.h filesystem.cc
 
 libkea_util_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
 
diff --git a/src/lib/util/filesystem.cc b/src/lib/util/filesystem.cc
new file mode 100644 (file)
index 0000000..6990c11
--- /dev/null
@@ -0,0 +1,232 @@
+// Copyright (C) 2021-2025 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 <config.h>
+
+#include <exceptions/exceptions.h>
+#include <util/filesystem.h>
+#include <util/strutil.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <fstream>
+#include <string>
+#include <filesystem>
+#include <iostream>
+
+#include <dirent.h>
+#include <fcntl.h>
+
+using namespace isc;
+using namespace isc::util::str;
+using namespace std;
+
+namespace isc {
+namespace util {
+namespace file {
+
+bool
+exists(string const& path) {
+    struct stat statbuf;
+    return (::stat(path.c_str(), &statbuf) == 0);
+}
+
+bool
+isFile(string const& path) {
+    struct stat statbuf;
+    if (::stat(path.c_str(), &statbuf) < 0) {
+        return (false);
+    }
+    return ((statbuf.st_mode & S_IFMT) == S_IFREG);
+}
+
+Umask::Umask(mode_t mask) : orig_umask_(umask(S_IWGRP | S_IWOTH)) {
+    umask(orig_umask_ | mask);
+}
+
+Umask::~Umask() {
+    umask(orig_umask_);
+}
+
+bool
+isSocket(string const& path) {
+    struct stat statbuf;
+    if (::stat(path.c_str(), &statbuf) < 0) {
+        return (false);
+    }
+    return ((statbuf.st_mode & S_IFMT) == S_IFSOCK);
+}
+
+Path::Path(string const& full_name) {
+    if (!full_name.empty()) {
+        bool dir_present = false;
+        // Find the directory.
+        size_t last_slash = full_name.find_last_of('/');
+        if (last_slash != string::npos) {
+            // Found the last slash, so extract directory component and
+            // set where the scan for the last_dot should terminate.
+            parent_path_ = full_name.substr(0, last_slash);
+            if (last_slash == full_name.size()) {
+                // The entire string was a directory, so exit and don't
+                // do any more searching.
+                return;
+            }
+
+            // Found a directory so note the fact.
+            dir_present = true;
+        }
+
+        // Now search backwards for the last ".".
+        size_t last_dot = full_name.find_last_of('.');
+        if ((last_dot == string::npos) || (dir_present && (last_dot < last_slash))) {
+            // Last "." either not found or it occurs to the left of the last
+            // slash if a directory was present (so it is part of a directory
+            // name).  In this case, the remainder of the string after the slash
+            // is the name part.
+            stem_ = full_name.substr(last_slash + 1);
+            return;
+        }
+
+        // Did find a valid dot, so it and everything to the right is the
+        // extension...
+        extension_ = full_name.substr(last_dot);
+
+        // ... and the name of the file is everything in between.
+        if ((last_dot - last_slash) > 1) {
+            stem_ = full_name.substr(last_slash + 1, last_dot - last_slash - 1);
+        }
+    }
+}
+
+string
+Path::str() const {
+    return (parent_path_ + ((parent_path_.empty() || parent_path_ == "/") ? string() : "/") + stem_ + extension_);
+}
+
+string
+Path::parentPath() const {
+    return (parent_path_);
+}
+
+string
+Path::stem() const {
+    return (stem_);
+}
+
+string
+Path::extension() const {
+    return (extension_);
+}
+
+string
+Path::filename() const {
+    return (stem_ + extension_);
+}
+
+Path&
+Path::replaceExtension(string const& replacement) {
+    string const trimmed_replacement(trim(replacement));
+    if (trimmed_replacement.empty()) {
+        extension_ = string();
+    } else {
+        size_t const last_dot(trimmed_replacement.find_last_of('.'));
+        if (last_dot == string::npos) {
+            extension_ = "." + trimmed_replacement;
+        } else {
+            extension_ = trimmed_replacement.substr(last_dot);
+        }
+    }
+    return (*this);
+}
+
+Path&
+Path::replaceParentPath(string const& replacement) {
+    string const trimmed_replacement(trim(replacement));
+    if (trimmed_replacement.empty()) {
+        parent_path_ = string();
+    } else if (trimmed_replacement == "/") {
+        parent_path_ = trimmed_replacement;
+    } else if (trimmed_replacement.at(trimmed_replacement.size() - 1) == '/') {
+        parent_path_ = trimmed_replacement.substr(0, trimmed_replacement.size() - 1);
+    } else {
+        parent_path_ = trimmed_replacement;
+    }
+    return (*this);
+}
+
+TemporaryDirectory::TemporaryDirectory() {
+    char dir[]("/tmp/kea-tmpdir-XXXXXX");
+    char const* dir_name = mkdtemp(dir);
+    if (!dir_name) {
+        isc_throw(Unexpected, "mkdtemp failed " << dir << ": " << strerror(errno));
+    }
+    dir_name_ = string(dir_name);
+}
+
+TemporaryDirectory::~TemporaryDirectory() {
+    DIR *dir(opendir(dir_name_.c_str()));
+    if (!dir) {
+        return;
+    }
+
+    std::unique_ptr<DIR, void(*)(DIR*)> defer(dir, [](DIR* d) { closedir(d); });
+
+    struct dirent *i;
+    string filepath;
+    while ((i = readdir(dir))) {
+        if (strcmp(i->d_name, ".") == 0 || strcmp(i->d_name, "..") == 0) {
+            continue;
+        }
+
+        filepath = dir_name_ + '/' + i->d_name;
+        remove(filepath.c_str());
+    }
+
+    rmdir(dir_name_.c_str());
+}
+
+string TemporaryDirectory::dirName() {
+    return dir_name_;
+}
+
+std::string
+FileManager::validatePath(const std::string supported_path_str, const std::string input_path_str,
+                          bool enforce_path /* = true */) {
+    // Remove the trailing "/" if it present so comparison to
+    // input's parent path functions.
+    auto supported_path_copy(supported_path_str);
+    if (supported_path_copy.back() == '/') {
+        supported_path_copy.pop_back();
+    }
+
+    Path input_path(trim(input_path_str));
+    auto filename = input_path.filename();
+    if (filename.empty()) {
+        isc_throw(BadValue, "path: '" << input_path.str() << "' has no filename");
+     }
+    auto parent_path = input_path.parentPath();
+    if (!parent_path.empty()) {
+         if (!enforce_path) {
+             // Security set to lax, let it fly.
+             return (input_path_str);
+         }
+         // We only allow absolute path equal to default. Catch an invalid path.
+        if (parent_path != supported_path_copy) {
+             isc_throw(BadValue, "invalid path specified: '"
+                      << parent_path << "', supported path is '"
+                      << supported_path_copy << "'");
+         }
+     }
+    std::string valid_path(supported_path_copy + "/" +  filename);
+    return (valid_path);
+}
+
+}  // namespace file
+}  // namespace util
+}  // namespace isc
diff --git a/src/lib/util/filesystem.h b/src/lib/util/filesystem.h
new file mode 100644 (file)
index 0000000..be78267
--- /dev/null
@@ -0,0 +1,168 @@
+// Copyright (C) 2021-2025 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 KEA_UTIL_FILESYSTEM_H
+#define KEA_UTIL_FILESYSTEM_H
+
+#include <sys/stat.h>
+#include <string>
+
+namespace isc {
+namespace util {
+namespace file {
+
+/// @brief Check if there is a file or directory at the given path.
+///
+/// @param path The path being checked.
+///
+/// @return True if the path points to a file or a directory, false otherwise.
+bool
+exists(const std::string& path);
+
+/// @brief Check if there is a file at the given path.
+///
+/// @param path The path being checked.
+///
+/// @return True if the path points to a file, false otherwise including
+/// if the pointed location does not exist.
+bool
+isFile(const std::string& path);
+
+/// @brief RAII device to limit access of created files.
+struct Umask {
+    /// @brief Constructor
+    ///
+    /// Set wanted bits in umask.
+    Umask(mode_t mask);
+
+    /// @brief Destructor.
+    ///
+    /// Restore umask.
+    ~Umask();
+
+private:
+    /// @brief Original umask.
+    mode_t orig_umask_;
+};
+
+bool
+isSocket(const std::string& path);
+
+/// @brief Paths on a filesystem
+struct Path {
+    /// @brief Constructor
+    ///
+    /// Splits the full name into components.
+    Path(std::string const& path);
+
+    /// @brief Get the path in textual format.
+    ///
+    /// Counterpart for std::filesystem::path::string.
+    ///
+    /// @return stored filename.
+    std::string str() const;
+
+    /// @brief Get the parent path.
+    ///
+    /// Counterpart for std::filesystem::path::parent_path.
+    ///
+    /// @return parent path of current path.
+    std::string parentPath() const;
+
+    /// @brief Get the base name of the file without the extension.
+    ///
+    /// Counterpart for std::filesystem::path::stem.
+    ///
+    /// @return the base name of current path without the extension.
+    std::string stem() const;
+
+    /// @brief Get the extension of the file.
+    ///
+    /// Counterpart for std::filesystem::path::extension.
+    ///
+    /// @return extension of current path.
+    std::string extension() const;
+
+    /// @brief Get the name of the file, extension included.
+    ///
+    /// Counterpart for std::filesystem::path::filename.
+    ///
+    /// @return name + extension of current path.
+    std::string filename() const;
+
+    /// @brief Identifies the extension in {replacement}, trims it, and
+    /// replaces this instance's extension with it.
+    ///
+    /// Counterpart for std::filesystem::path::replace_extension.
+    ///
+    /// The change is done in the members and {this} is returned to allow call
+    /// chaining.
+    ///
+    /// @param replacement The extension to replace with.
+    ///
+    /// @return The current instance after the replacement was done.
+    Path& replaceExtension(std::string const& replacement = std::string());
+
+    /// @brief Trims {replacement} and replaces this instance's parent path with
+    /// it.
+    ///
+    /// The change is done in the members and {this} is returned to allow call
+    /// chaining.
+    ///
+    /// @param replacement The parent path to replace with.
+    ///
+    /// @return The current instance after the replacement was done.
+    Path& replaceParentPath(std::string const& replacement = std::string());
+
+private:
+    /// @brief Parent path.
+    std::string parent_path_;
+
+    /// @brief Stem.
+    std::string stem_;
+
+    /// @brief File name extension.
+    std::string extension_;
+};
+
+struct TemporaryDirectory {
+    TemporaryDirectory();
+    ~TemporaryDirectory();
+    std::string dirName();
+private:
+    std::string dir_name_;
+};
+
+/// @brief Class that provides basic file related tasks.
+class FileManager {
+public:
+    /// @brief Validates a file path against a supported path.
+    ///
+    /// If the input path specifies a parent path and file name, the parent path
+    /// is validated against the supported path. If they match, the function returns
+    /// the validated path. If the input path contains only a file name the function
+    /// returns valid path using the supported path and the input path name.
+    ///
+    /// @param supported_path_str absolute path specifying the  supported path
+    /// of the file against which the input path is validated.
+    /// @param input_path_str file path to validate.
+    /// @param enforce_path enables validation against the supported path.  If false
+    /// verifies only that the path contains a file name.
+    ///
+    /// @return validated path as a string (supported path + input file name)
+    ///
+    /// @throw BadValue if the input path does not include a file name or if the
+    /// it the parent path does not path the supported path.
+    static std::string validatePath(const std::string supported_path_str,
+                                    const std::string input_path_str,
+                                    bool enforce_path = true);
+};
+
+}  // namespace file
+}  // namespace util
+}  // namespace isc
+
+#endif  // KEA_UTIL_FILESYSTEM_H
index 5f8b1e8570130e26ee7e37a8507e194269197048..935ec09712059b71397ed2198ce908eccabc48f8 100644 (file)
@@ -2,6 +2,7 @@ SUBDIRS = .
 
 AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
 AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DABS_SRCDIR=\"$(abs_srcdir)\"
 AM_CPPFLAGS += -DTEST_DATA_BUILDDIR=\"$(abs_builddir)\"
 # XXX: we'll pollute the top builddir for creating a temporary test file
 # used to bind a UNIX domain socket so we can minimize the risk of exceeding
@@ -60,6 +61,7 @@ run_unittests_SOURCES += utf8_unittest.cc
 run_unittests_SOURCES += versioned_csv_file_unittest.cc
 run_unittests_SOURCES += watch_socket_unittests.cc
 run_unittests_SOURCES += watched_thread_unittest.cc
+run_unittests_SOURCES += filesystem_unittests.cc
 
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
diff --git a/src/lib/util/tests/filesystem_unittests.cc b/src/lib/util/tests/filesystem_unittests.cc
new file mode 100644 (file)
index 0000000..37cd245
--- /dev/null
@@ -0,0 +1,79 @@
+// Copyright (C) 2015-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 <config.h>
+
+#include <exceptions/exceptions.h>
+#include <testutils/gtest_utils.h>
+#include <util/filesystem.h>
+
+#include <fstream>
+#include <string>
+
+#include <gtest/gtest.h>
+
+using namespace isc;
+using namespace isc::util::file;
+using namespace std;
+
+namespace {
+
+/// @brief Check that the components are split correctly.
+TEST(PathTest, components) {
+    // Complete name
+    Path fname("/alpha/beta/gamma.delta");
+    EXPECT_EQ("/alpha/beta/gamma.delta", fname.str());
+    EXPECT_EQ("/alpha/beta", fname.parentPath());
+    EXPECT_EQ("gamma", fname.stem());
+    EXPECT_EQ(".delta", fname.extension());
+    EXPECT_EQ("gamma.delta", fname.filename());
+}
+
+/// @brief Check replaceExtension.
+TEST(PathTest, replaceExtension) {
+    Path fname("a.b");
+    EXPECT_EQ("a.b", fname.str());
+
+    EXPECT_EQ("a", fname.replaceExtension("").str());
+    EXPECT_EQ("a.f", fname.replaceExtension(".f").str());
+    EXPECT_EQ("a.f", fname.replaceExtension("f").str());
+    EXPECT_EQ("a./c/d/", fname.replaceExtension(" /c/d/  ").str());
+    EXPECT_EQ("a.f", fname.replaceExtension("/c/d/e.f").str());
+    EXPECT_EQ("a.f", fname.replaceExtension("e.f").str());
+}
+
+/// @brief Check replaceParentPath.
+TEST(PathTest, replaceParentPath) {
+    Path fname("a.b");
+    EXPECT_EQ("", fname.parentPath());
+    EXPECT_EQ("a.b", fname.str());
+
+    fname.replaceParentPath("/just/some/dir/");
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
+    EXPECT_EQ("/just/some/dir/a.b", fname.str());
+
+    fname.replaceParentPath("/just/some/dir");
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
+    EXPECT_EQ("/just/some/dir/a.b", fname.str());
+
+    fname.replaceParentPath("/");
+    EXPECT_EQ("/", fname.parentPath());
+    EXPECT_EQ("/a.b", fname.str());
+
+    fname.replaceParentPath("");
+    EXPECT_EQ("", fname.parentPath());
+    EXPECT_EQ("a.b", fname.str());
+
+    fname = Path("/first/a.b");
+    EXPECT_EQ("/first", fname.parentPath());
+    EXPECT_EQ("/first/a.b", fname.str());
+
+    fname.replaceParentPath("/just/some/dir");
+    EXPECT_EQ("/just/some/dir", fname.parentPath());
+    EXPECT_EQ("/just/some/dir/a.b", fname.str());
+}
+
+}  // namespace