From: Thomas Markwalder Date: Tue, 29 Apr 2025 14:24:05 +0000 (-0400) Subject: [#3830] Backport CVE-2025-32801 to v2_4 X-Git-Tag: Kea-2.4.2~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8f1b2f0facbcee178b4600d8360b7373c20426be;p=thirdparty%2Fkea.git [#3830] Backport CVE-2025-32801 to v2_4 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 --- diff --git a/doc/sphinx/arm/hooks.rst b/doc/sphinx/arm/hooks.rst index af31587d17..156a094d80 100644 --- a/doc/sphinx/arm/hooks.rst +++ b/doc/sphinx/arm/hooks.rst @@ -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 diff --git a/src/bin/agent/tests/ca_cfg_mgr_unittests.cc b/src/bin/agent/tests/ca_cfg_mgr_unittests.cc index a7b40c5567..73f5a85cd6 100644 --- a/src/bin/agent/tests/ca_cfg_mgr_unittests.cc +++ b/src/bin/agent/tests/ca_cfg_mgr_unittests.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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. diff --git a/src/bin/agent/tests/get_config_unittest.cc b/src/bin/agent/tests/get_config_unittest.cc index 40d9c502f9..e5f741925f 100644 --- a/src/bin/agent/tests/get_config_unittest.cc +++ b/src/bin/agent/tests/get_config_unittest.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -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"; diff --git a/src/bin/agent/tests/test_callout_libraries.h.in b/src/bin/agent/tests/test_callout_libraries.h.in index 78f51c8815..7e888ba732 100644 --- a/src/bin/agent/tests/test_callout_libraries.h.in +++ b/src/bin/agent/tests/test_callout_libraries.h.in @@ -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 diff --git a/src/bin/d2/tests/d2_cfg_mgr_unittests.cc b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc index 5e27ff8a5c..7e2aee5263 100644 --- a/src/bin/d2/tests/d2_cfg_mgr_unittests.cc +++ b/src/bin/d2/tests/d2_cfg_mgr_unittests.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -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. diff --git a/src/bin/d2/tests/d2_process_tests.sh.in b/src/bin/d2/tests/d2_process_tests.sh.in index 895b9a2d92..408e8e0fad 100644 --- a/src/bin/d2/tests/d2_process_tests.sh.in +++ b/src/bin/d2/tests/d2_process_tests.sh.in @@ -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\": diff --git a/src/bin/d2/tests/d2_process_unittests.cc b/src/bin/d2/tests/d2_process_unittests.cc index 4dda27551d..cfff351cfa 100644 --- a/src/bin/d2/tests/d2_process_unittests.cc +++ b/src/bin/d2/tests/d2_process_unittests.cc @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -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" diff --git a/src/bin/d2/tests/get_config_unittest.cc b/src/bin/d2/tests/get_config_unittest.cc index 0936e82160..7ca53b86c4 100644 --- a/src/bin/d2/tests/get_config_unittest.cc +++ b/src/bin/d2/tests/get_config_unittest.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -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"; diff --git a/src/bin/d2/tests/test_callout_libraries.h.in b/src/bin/d2/tests/test_callout_libraries.h.in index a88d131011..82602dfcc9 100644 --- a/src/bin/d2/tests/test_callout_libraries.h.in +++ b/src/bin/d2/tests/test_callout_libraries.h.in @@ -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 diff --git a/src/bin/d2/tests/test_configured_libraries.h.in b/src/bin/d2/tests/test_configured_libraries.h.in index 2b235ae68f..9feb0bb341 100644 --- a/src/bin/d2/tests/test_configured_libraries.h.in +++ b/src/bin/d2/tests/test_configured_libraries.h.in @@ -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 diff --git a/src/bin/dhcp4/tests/config_parser_unittest.cc b/src/bin/dhcp4/tests/config_parser_unittest.cc index 2d98963a43..8765795e78 100644 --- a/src/bin/dhcp4/tests/config_parser_unittest.cc +++ b/src/bin/dhcp4/tests/config_parser_unittest.cc @@ -29,6 +29,7 @@ #include #include #include +#include #include #include #include @@ -278,6 +279,7 @@ protected: virtual void SetUp() { std::vector 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)); diff --git a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc index ee0e28a31b..a135c64ea8 100644 --- a/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc +++ b/src/bin/dhcp4/tests/ctrl_dhcp4_srv_unittest.cc @@ -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)); diff --git a/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in b/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in index 76e7177094..0ea9174d72 100644 --- a/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in +++ b/src/bin/dhcp4/tests/dhcp4_process_tests.sh.in @@ -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\": diff --git a/src/bin/dhcp4/tests/hooks_unittest.cc b/src/bin/dhcp4/tests/hooks_unittest.cc index 905ce87a6a..8dd8581f25 100644 --- a/src/bin/dhcp4/tests/hooks_unittest.cc +++ b/src/bin/dhcp4/tests/hooks_unittest.cc @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -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" } )", diff --git a/src/bin/dhcp4/tests/test_libraries.h.in b/src/bin/dhcp4/tests/test_libraries.h.in index 9b9a243b8b..00725d246b 100644 --- a/src/bin/dhcp4/tests/test_libraries.h.in +++ b/src/bin/dhcp4/tests/test_libraries.h.in @@ -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 diff --git a/src/bin/dhcp6/tests/config_parser_unittest.cc b/src/bin/dhcp6/tests/config_parser_unittest.cc index bd8435fd48..735fe4622b 100644 --- a/src/bin/dhcp6/tests/config_parser_unittest.cc +++ b/src/bin/dhcp6/tests/config_parser_unittest.cc @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -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(remove(LOAD_MARKER_FILE)); static_cast(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)); diff --git a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc index f05a40212b..ac52dd8183 100644 --- a/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc +++ b/src/bin/dhcp6/tests/ctrl_dhcp6_srv_unittest.cc @@ -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)); diff --git a/src/bin/dhcp6/tests/dhcp6_process_tests.sh.in b/src/bin/dhcp6/tests/dhcp6_process_tests.sh.in index 0857287937..d209e3c40b 100644 --- a/src/bin/dhcp6/tests/dhcp6_process_tests.sh.in +++ b/src/bin/dhcp6/tests/dhcp6_process_tests.sh.in @@ -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\": diff --git a/src/bin/dhcp6/tests/hooks_unittest.cc b/src/bin/dhcp6/tests/hooks_unittest.cc index c34aa763c6..ed1bcaf2b0 100644 --- a/src/bin/dhcp6/tests/hooks_unittest.cc +++ b/src/bin/dhcp6/tests/hooks_unittest.cc @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -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" } )", diff --git a/src/bin/dhcp6/tests/test_libraries.h.in b/src/bin/dhcp6/tests/test_libraries.h.in index 95d74592ad..237c56dc91 100644 --- a/src/bin/dhcp6/tests/test_libraries.h.in +++ b/src/bin/dhcp6/tests/test_libraries.h.in @@ -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 diff --git a/src/lib/dhcpsrv/tests/Makefile.am b/src/lib/dhcpsrv/tests/Makefile.am index a73d0f34a7..992ce5b3cf 100644 --- a/src/lib/dhcpsrv/tests/Makefile.am +++ b/src/lib/dhcpsrv/tests/Makefile.am @@ -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) diff --git a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc index de9f6a2e5d..372a154aca 100644 --- a/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc +++ b/src/lib/dhcpsrv/tests/dhcp_parsers_unittest.cc @@ -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 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 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 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 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 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 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 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 (: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 (: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 hooks_libraries = HooksManager::getLibraryNames(); EXPECT_TRUE(hooks_libraries.empty()); diff --git a/src/lib/dhcpsrv/tests/test_libraries.h.in b/src/lib/dhcpsrv/tests/test_libraries.h.in index 5a5545eae6..e972b2cff6 100644 --- a/src/lib/dhcpsrv/tests/test_libraries.h.in +++ b/src/lib/dhcpsrv/tests/test_libraries.h.in @@ -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 diff --git a/src/lib/hooks/Makefile.am b/src/lib/hooks/Makefile.am index 5b9bddb2b9..e97fcc81b8 100644 --- a/src/lib/hooks/Makefile.am +++ b/src/lib/hooks/Makefile.am @@ -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) diff --git a/src/lib/hooks/hooks_parser.cc b/src/lib/hooks/hooks_parser.cc index cfd4a7e8f5..4664113216 100644 --- a/src/lib/hooks/hooks_parser.cc +++ b/src/lib/hooks/hooks_parser.cc @@ -11,17 +11,36 @@ #include #include #include +#include #include + #include 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)); +} + } } diff --git a/src/lib/hooks/hooks_parser.h b/src/lib/hooks/hooks_parser.h index 4bec9a6aff..f37d8070f7 100644 --- a/src/lib/hooks/hooks_parser.h +++ b/src/lib/hooks/hooks_parser.h @@ -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 diff --git a/src/lib/hooks/tests/Makefile.am b/src/lib/hooks/tests/Makefile.am index 8cbe4138fa..803f53876b 100644 --- a/src/lib/hooks/tests/Makefile.am +++ b/src/lib/hooks/tests/Makefile.am @@ -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) diff --git a/src/lib/hooks/tests/hooks_manager_unittest.cc b/src/lib/hooks/tests/hooks_manager_unittest.cc index a36d42ce0d..110bce4917 100644 --- a/src/lib/hooks/tests/hooks_manager_unittest.cc +++ b/src/lib/hooks/tests/hooks_manager_unittest.cc @@ -8,7 +8,9 @@ #include #include +#include #include +#include #include #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(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 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 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 diff --git a/src/lib/util/Makefile.am b/src/lib/util/Makefile.am index e2833a9649..f631523d0c 100644 --- a/src/lib/util/Makefile.am +++ b/src/lib/util/Makefile.am @@ -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 index 0000000000..6990c114ff --- /dev/null +++ b/src/lib/util/filesystem.cc @@ -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 + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include + +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 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 index 0000000000..be78267036 --- /dev/null +++ b/src/lib/util/filesystem.h @@ -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 +#include + +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 diff --git a/src/lib/util/tests/Makefile.am b/src/lib/util/tests/Makefile.am index 5f8b1e8570..935ec09712 100644 --- a/src/lib/util/tests/Makefile.am +++ b/src/lib/util/tests/Makefile.am @@ -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 index 0000000000..37cd24504b --- /dev/null +++ b/src/lib/util/tests/filesystem_unittests.cc @@ -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 + +#include +#include +#include + +#include +#include + +#include + +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