From: Francis Dupont Date: Wed, 20 Aug 2025 14:02:46 +0000 (+0200) Subject: [#3860] Added $INCLUDE X-Git-Tag: Kea-3.1.2~48 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=83d6ec8a0e20029eed3dfbad20701826fced720d;p=thirdparty%2Fkea.git [#3860] Added $INCLUDE --- diff --git a/src/hooks/dhcp/radius/client_dictionary.cc b/src/hooks/dhcp/radius/client_dictionary.cc index c5c4aab92a..c247437ef0 100644 --- a/src/hooks/dhcp/radius/client_dictionary.cc +++ b/src/hooks/dhcp/radius/client_dictionary.cc @@ -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)); diff --git a/src/hooks/dhcp/radius/client_dictionary.h b/src/hooks/dhcp/radius/client_dictionary.h index c949cc5dcc..c3b16e8edc 100644 --- a/src/hooks/dhcp/radius/client_dictionary.h +++ b/src/hooks/dhcp/radius/client_dictionary.h @@ -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_; diff --git a/src/hooks/dhcp/radius/tests/dictionary_unittests.cc b/src/hooks/dhcp/radius/tests/dictionary_unittests.cc index addfa28ae2..dc39ea9cb2 100644 --- a/src/hooks/dhcp/radius/tests/dictionary_unittests.cc +++ b/src/hooks/dhcp/radius/tests/dictionary_unittests.cc @@ -47,29 +47,49 @@ public: /// @brief Destructor. virtual ~DictionaryTest() { AttrDefs::instance().clear(); + static_cast(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& lines) { + /// @param depth recursion depth. + void parseLines(const list& 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(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 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.