]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#3860] Added $INCLUDE
authorFrancis Dupont <fdupont@isc.org>
Wed, 20 Aug 2025 14:02:46 +0000 (16:02 +0200)
committerFrancis Dupont <fdupont@isc.org>
Fri, 12 Sep 2025 21:44:54 +0000 (23:44 +0200)
src/hooks/dhcp/radius/client_dictionary.cc
src/hooks/dhcp/radius/client_dictionary.h
src/hooks/dhcp/radius/tests/dictionary_unittests.cc

index c5c4aab92a974dbfe3fe0b52b212bda8bd4838c2..c247437ef03d2a1619b816b10bdf5e5d1b3c1a2f 100644 (file)
@@ -180,7 +180,7 @@ AttrDefs::add(IntCstDefPtr def) {
 }
 
 void
-AttrDefs::parseLine(const string& line) {
+AttrDefs::parseLine(const string& line, unsigned int depth) {
     // Ignore empty lines.
     if (line.empty()) {
         return;
@@ -196,6 +196,14 @@ AttrDefs::parseLine(const string& line) {
     if (tokens.empty()) {
         return;
     }
+    // $INCLUDE include.
+    if (tokens[0] == "$INCLUDE") {
+        if (tokens.size() != 2) {
+            isc_throw(Unexpected, "expected 2 tokens, got " << tokens.size());
+        }
+        readDictionary(tokens[1], depth + 1);
+        return;
+    }
     // Attribute definition.
     if (tokens[0] == "ATTRIBUTE") {
         if (tokens.size() != 4) {
@@ -255,7 +263,10 @@ AttrDefs::parseLine(const string& line) {
 }
 
 void
-AttrDefs::readDictionary(const string& path) {
+AttrDefs::readDictionary(const string& path, unsigned depth) {
+    if (depth >= 5) {
+        isc_throw(BadValue, "Too many nested $INCLUDE");
+    }
     ifstream ifs(path);
     if (!ifs.is_open()) {
         isc_throw(BadValue, "can't open dictionary '" << path << "': "
@@ -265,23 +276,24 @@ AttrDefs::readDictionary(const string& path) {
         isc_throw(BadValue, "bad dictionary '" << path << "'");
     }
     try {
-        readDictionary(ifs);
+        readDictionary(ifs, depth);
         ifs.close();
     } catch (const exception& ex) {
         ifs.close();
-        isc_throw(BadValue, ex.what() << " in dictionary '" << path << "'");
+        isc_throw(BadValue, ex.what() << " in dictionary '" << path << "'"
+                  << (depth > 0 ? "," : ""));
     }
 }
 
 void
-AttrDefs::readDictionary(istream& is) {
+AttrDefs::readDictionary(istream& is, unsigned int depth) {
     size_t lines = 0;
     string line;
     try {
         while (is.good()) {
             ++lines;
             getline(is, line);
-            parseLine(line);
+            parseLine(line, depth);
         }
         if (!is.eof()) {
             isc_throw(BadValue, "I/O error: " << strerror(errno));
index c949cc5dcc370758dbe4ff581cc873464e76b57f..c3b16e8edcb4efee9b190faf3a42619d8df1c82c 100644 (file)
@@ -223,18 +223,22 @@ public:
     /// @brief Read a dictionary from a file.
     ///
     /// Fills attribute and integer constant definition tables from
-    /// a dictionary file.
+    /// a dictionary file. Recursion depth is initialized to 0,
+    /// incremented by includes and limited to 5.
     ///
     /// @param path dictionary file path.
-    void readDictionary(const std::string& path);
+    /// @param depth recursion depth.
+    void readDictionary(const std::string& path, unsigned int depth = 0);
 
     /// @brief Read a dictionary from an input stream.
     ///
     /// Fills attribute and integer constant definition tables from
-    /// a dictionary input stream.
+    /// a dictionary input stream. Recursion depth is initialized to 0,
+    /// incremented by includes and limited to 5.
     ///
     /// @param is input stream.
-    void readDictionary(std::istream& is);
+    /// @param depth recursion depth.
+    void readDictionary(std::istream& is, unsigned int depth = 0);
 
     /// @brief Check if a list of standard attribute definitions
     /// are available and correct.
@@ -255,7 +259,8 @@ protected:
     /// @brief Parse a dictionary line.
     ///
     /// @param line line to parse.
-    void parseLine(const std::string& line);
+    /// @param depth recursion depth.
+    void parseLine(const std::string& line, unsigned int depth);
 
     /// @brief Attribute definition container.
     AttrDefContainer container_;
index addfa28ae247fa5d523ca091e1166cdf5eebd3ba..dc39ea9cb2901095f0bd74a34be2111243688d21 100644 (file)
@@ -47,29 +47,49 @@ public:
     /// @brief Destructor.
     virtual ~DictionaryTest() {
         AttrDefs::instance().clear();
+        static_cast<void>(remove(TEST_DICT));
     }
 
     /// @brief Parse a line.
     ///
     /// @param line line to parse.
-    void parseLine(const string& line) {
+    /// @param depth recursion depth.
+    void parseLine(const string& line, unsigned int depth = 0) {
         istringstream is(line + "\n");
-        AttrDefs::instance().readDictionary(is);
+        AttrDefs::instance().readDictionary(is, depth);
     }
 
     /// @brief Parse a list of lines.
     ///
     /// @param lines list of lines.
-    void parseLines(const list<string>& lines) {
+    /// @param depth recursion depth.
+    void parseLines(const list<string>& lines, unsigned int depth = 0) {
         string content;
         for (auto const& line : lines) {
             content += line + "\n";
         }
         istringstream is(content);
-        AttrDefs::instance().readDictionary(is);
+        AttrDefs::instance().readDictionary(is, depth);
     }
+
+    /// @brief writes specified content to a file.
+    ///
+    /// @param file_name name of file to be written.
+    /// @param content content to be written to file.
+    void writeFile(const std::string& file_name, const std::string& content) {
+        static_cast<void>(remove(file_name.c_str()));
+        ofstream out(file_name.c_str(), ios::trunc);
+        EXPECT_TRUE(out.is_open());
+        out << content;
+        out.close();
+    }
+
+    /// Name of a dictionary file used during tests.
+    static const char* TEST_DICT;
 };
 
+const char* DictionaryTest::TEST_DICT  = "test-dict";
+
 // Verifies standards definitions can be read from the dictionary.
 TEST_F(DictionaryTest, standard) {
     ASSERT_NO_THROW_LOG(AttrDefs::instance().readDictionary(TEST_DICTIONARY));
@@ -213,6 +233,41 @@ TEST_F(DictionaryTest, hookAttributes) {
         checkStandardDefs(RadiusConfigParser::USED_STANDARD_ATTR_DEFS));
 }
 
+// Verifies the $INCLUDE entry.
+TEST_F(DictionaryTest, include) {
+    list<string> include;
+    include.push_back("# Including the dictonary");
+    include.push_back(string("$INCLUDE ") + string(TEST_DICTIONARY));
+    include.push_back("# Dictionary included");
+    //    include.push_back("VALUE Vendor-Specific ISC 2495");
+    include.push_back("VALUE ARAP-Security ISC 2495");
+    EXPECT_NO_THROW_LOG(parseLines(include));
+    EXPECT_NO_THROW_LOG(AttrDefs::instance().
+        checkStandardDefs(RadiusConfigParser::USED_STANDARD_ATTR_DEFS));
+    //    auto isc = AttrDefs::instance().getByName(PW_VENDOR_SPECIFIC, "ISC");
+    auto isc = AttrDefs::instance().getByName(PW_ARAP_SECURITY, "ISC");
+    ASSERT_TRUE(isc);
+    EXPECT_EQ(2495, isc->value_);
+
+    // max depth is 5.
+    EXPECT_THROW_MSG(parseLines(include, 4), BadValue,
+                     "Too many nested $INCLUDE at line 2");
+}
+
+// Verifies the $INCLUDE entry can't eat the stack.
+TEST_F(DictionaryTest, includeLimit) {
+    string include = "$INCLUDE " + string(TEST_DICT) + "\n";
+    writeFile(TEST_DICT, include);
+    string expected = "Too many nested $INCLUDE ";
+    expected += "at line 1 in dictionary 'test-dict', ";
+    expected += "at line 1 in dictionary 'test-dict', ";
+    expected += "at line 1 in dictionary 'test-dict', ";
+    expected += "at line 1 in dictionary 'test-dict', ";
+    expected += "at line 1";
+    EXPECT_THROW_MSG(parseLine(string("$INCLUDE ") + string(TEST_DICT)),
+                     BadValue, expected);
+}
+
 namespace {
 
 // RAII device freeing the glob buffer when going out of scope.