Skip to content
Next Next commit
[#3860] Added $INCLUDE
  • Loading branch information
fxdupont committed Aug 21, 2025
commit cb424a2659bb5476ce4f01eda9da41279b30eb79
24 changes: 18 additions & 6 deletions src/hooks/dhcp/radius/client_dictionary.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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) {
Expand Down Expand Up @@ -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 << "': "
Expand All @@ -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));
Expand Down
15 changes: 10 additions & 5 deletions src/hooks/dhcp/radius/client_dictionary.h
Original file line number Diff line number Diff line change
Expand Up @@ -223,18 +223,22 @@ class AttrDefs : public boost::noncopyable {
/// @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.
Expand All @@ -255,7 +259,8 @@ class AttrDefs : public boost::noncopyable {
/// @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_;
Expand Down
63 changes: 59 additions & 4 deletions src/hooks/dhcp/radius/tests/dictionary_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -47,29 +47,49 @@ class DictionaryTest : public ::testing::Test {
/// @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));
Expand Down Expand Up @@ -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.
Expand Down