diff --git a/doc/guide/logging.xml b/doc/guide/logging.xml
index 63c9cf3a38..25dd24749e 100644
--- a/doc/guide/logging.xml
+++ b/doc/guide/logging.xml
@@ -219,6 +219,11 @@
kea-dhcp4.dhcpsrv - this is a base
logger for the libdhcpsrv library.
+
+ kea-dhcp4.eval - this logger is used
+ to log messages relating to the client classification expression
+ evaluation code.
+
kea-dhcp4.hooks - this logger is used
to log messages related to management of hooks libraries, e.g.
@@ -302,6 +307,11 @@
kea-dhcp6.dhcpsrv - this is a base
logger for the libdhcpsrv library.
+
+ kea-dhcp6.eval - this logger is used
+ to log messages relating to the client classification expression
+ evaluation code.
+
kea-dhcp6.hooks - this logger is used
to log messages related to management of hooks libraries, e.g.
diff --git a/src/lib/eval/Makefile.am b/src/lib/eval/Makefile.am
index 1fd66b96f8..05c4902e08 100644
--- a/src/lib/eval/Makefile.am
+++ b/src/lib/eval/Makefile.am
@@ -12,14 +12,21 @@ AM_CXXFLAGS += $(WARNING_NO_MISSING_FIELD_INITIALIZERS_CFLAG)
lib_LTLIBRARIES = libkea-eval.la
libkea_eval_la_SOURCES =
+libkea_eval_la_SOURCES += eval_log.cc eval_log.h
libkea_eval_la_SOURCES += token.cc token.h
+nodist_libkea_eval_la_SOURCES = eval_messages.h eval_messages.cc
+
libkea_eval_la_CXXFLAGS = $(AM_CXXFLAGS)
libkea_eval_la_CPPFLAGS = $(AM_CPPFLAGS)
libkea_eval_la_LIBADD = $(top_builddir)/src/lib/exceptions/libkea-exceptions.la
libkea_eval_la_LIBADD += $(top_builddir)/src/lib/dhcp/libkea-dhcp++.la
+libkea_eval_la_LIBADD += $(top_builddir)/src/lib/log/libkea-log.la
+libkea_eval_la_LIBADD += $(top_builddir)/src/lib/util/libkea-util.la
+libkea_eval_la_LIBADD += $(LOG4CPLUS_LIBS) $(CRYPTO_LIBS)
+
libkea_eval_la_LDFLAGS = -no-undefined -version-info 3:0:0
-libkea_eval_la_LDFLAGS += $(LOG4CPLUS_LIBS) $(CRYPTO_LDFLAGS)
+libkea_eval_la_LDFLAGS += $(CRYPTO_LDFLAGS)
EXTRA_DIST = eval.dox
EXTRA_DIST += eval_messages.mes
@@ -29,6 +36,7 @@ eval_messages.h eval_messages.cc: s-messages
s-messages: eval_messages.mes
$(top_builddir)/src/lib/log/compiler/message $(top_srcdir)/src/lib/eval/eval_messages.mes
+ touch $@
# Tell Automake that the eval_messages.{cc,h} source files are created in the
# build process, so it must create these before doing anything else. Although
@@ -39,4 +47,4 @@ s-messages: eval_messages.mes
# first.
BUILT_SOURCES = eval_messages.h eval_messages.cc
-CLEANFILES = eval_messages.h eval_messages.cc
+CLEANFILES = eval_messages.h eval_messages.cc s-messages
diff --git a/src/lib/eval/eval_log.cc b/src/lib/eval/eval_log.cc
new file mode 100644
index 0000000000..35128f6ae5
--- /dev/null
+++ b/src/lib/eval/eval_log.cc
@@ -0,0 +1,26 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+/// Defines the logger used by the Eval (classification) code
+
+#include
+
+namespace isc {
+namespace dhcp {
+
+isc::log::Logger eval_logger("eval");
+
+} // namespace dhcp
+} // namespace isc
+
diff --git a/src/lib/eval/eval_log.h b/src/lib/eval/eval_log.h
new file mode 100644
index 0000000000..fb39198d25
--- /dev/null
+++ b/src/lib/eval/eval_log.h
@@ -0,0 +1,49 @@
+// Copyright (C) 2015 Internet Systems Consortium, Inc. ("ISC")
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
+// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
+// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
+// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
+// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
+// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
+// PERFORMANCE OF THIS SOFTWARE.
+
+#ifndef EVAL_LOG_H
+#define EVAL_LOG_H
+
+#include
+#include
+
+namespace isc {
+namespace dhcp {
+
+/// @brief Eval debug Logging levels
+///
+/// Defines the levels used to output debug messages in the eval (classification) code.
+/// Note that higher numbers equate to more verbose (and detailed) output.
+
+// The first level traces normal operations,
+const int EVAL_DBG_TRACE = DBGLVL_TRACE_BASIC;
+
+// The next level traces each call to hook code.
+const int EVAL_DBG_CALLS = DBGLVL_TRACE_BASIC_DATA;
+
+// Additional information on the calls. Report each call to a callout (even
+// if there are multiple callouts on a hook) and each status return.
+const int EVAL_DBG_EXTENDED_CALLS = DBGLVL_TRACE_DETAIL_DATA;
+
+/// @brief Eval Logger
+///
+/// Define the logger used to log messages. We could define it in multiple
+/// modules, but defining in a single module and linking to it saves time and
+/// space.
+extern isc::log::Logger eval_logger;
+
+} // namespace dhcp
+} // namespace isc
+
+#endif // EVAL_LOG_H
diff --git a/src/lib/eval/eval_messages.mes b/src/lib/eval/eval_messages.mes
index cca2b0e19e..3d86f758b4 100644
--- a/src/lib/eval/eval_messages.mes
+++ b/src/lib/eval/eval_messages.mes
@@ -18,3 +18,8 @@ $NAMESPACE isc::dhcp
This debug message indicates that the expression has been evaluated
to said value. This message is mostly useful during debugging of the
client classification expressions.
+
+% EVAL_SUBSTRING_BAD_PARAM_CONVERSION starting %1, length %2
+This debug message indicates that the parameter for the starting postion
+or length of the substring couldn't be converted to an integer. In this
+case the substring routine returns an empty string.
diff --git a/src/lib/eval/tests/Makefile.am b/src/lib/eval/tests/Makefile.am
index d220b2be46..c127429b61 100644
--- a/src/lib/eval/tests/Makefile.am
+++ b/src/lib/eval/tests/Makefile.am
@@ -2,6 +2,8 @@ SUBDIRS = .
AM_CPPFLAGS = -I$(top_builddir)/src/lib -I$(top_srcdir)/src/lib
AM_CPPFLAGS += $(BOOST_INCLUDES)
+AM_CPPFLAGS += -DLOGGING_SPEC_FILE=\"$(abs_top_srcdir)/src/lib/dhcpsrv/logging.spec\"
+
AM_CXXFLAGS = $(KEA_CXXFLAGS)
# Some versions of GCC warn about some versions of Boost regarding
diff --git a/src/lib/eval/tests/token_unittest.cc b/src/lib/eval/tests/token_unittest.cc
index 6b29763c62..273bb37fef 100644
--- a/src/lib/eval/tests/token_unittest.cc
+++ b/src/lib/eval/tests/token_unittest.cc
@@ -60,6 +60,40 @@ public:
OptionPtr option_str4_; ///< A string option for DHCPv4
OptionPtr option_str6_; ///< A string option for DHCPv6
+
+ /// @brief Verify that the substring eval works properly
+ ///
+ /// This function takes the parameters and sets up the value
+ /// stack then executes the eval and checks the results.
+ ///
+ /// @param test_string The string to operate on
+ /// @param test_start The postion to start when getting a substring
+ /// @param test_length The length of the substring to get
+ /// @param result_string The expected result of the eval
+ void verifySubstringEval(const std::string& test_string,
+ const std::string& test_start,
+ const std::string& test_length,
+ const std::string& result_string) {
+
+ // create the token
+ ASSERT_NO_THROW(t_.reset(new TokenSubstring()));
+
+ // push values on stack
+ values_.push(test_string);
+ values_.push(test_start);
+ values_.push(test_length);
+
+ // evaluate the token
+ EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+
+ // verify results
+ ASSERT_EQ(1, values_.size());
+ EXPECT_EQ(result_string, values_.top());
+
+ // remove result
+ values_.pop();
+ }
+
/// @todo: Add more option types here
};
@@ -197,3 +231,177 @@ TEST_F(TokenTest, optionEqualTrue) {
}
};
+
+// This test checks if an a token representing a substring request
+// throws an exception if there aren't enough values on the stack.
+// The stack from the top is: length, start, string.
+// The actual packet is not used.
+TEST_F(TokenTest, substringNotEnoughValues) {
+ ASSERT_NO_THROW(t_.reset(new TokenSubstring()));
+
+ // Subsring requires three values on the stack, try
+ // with 0, 1 and 2 all should thorw an exception
+ EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+ values_.push("");
+ EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+ values_.push("0");
+ EXPECT_THROW(t_->evaluate(*pkt4_, values_), EvalBadStack);
+
+ // Three should work
+ values_.push("0");
+ EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+
+ // As we had an empty string to start with we should have an empty
+ // one after the evaluate
+ ASSERT_EQ(1, values_.size());
+ EXPECT_EQ("", values_.top());
+}
+
+// Test getting the whole string in different ways
+TEST_F(TokenTest, substringWholeString) {
+ // Get the whole string
+ verifySubstringEval("foobar", "0", "6", "foobar");
+
+ // Get the whole string with "all"
+ verifySubstringEval("foobar", "0", "all", "foobar");
+
+ // Get the whole string with an extra long number
+ verifySubstringEval("foobar", "0", "123456", "foobar");
+
+ // Get the whole string counting from the back
+ verifySubstringEval("foobar", "-6", "all", "foobar");
+}
+
+// Test getting a suffix, in this case the last 3 characters
+TEST_F(TokenTest, substringTrailer) {
+ verifySubstringEval("foobar", "3", "3", "bar");
+ verifySubstringEval("foobar", "3", "all", "bar");
+ verifySubstringEval("foobar", "-3", "all", "bar");
+ verifySubstringEval("foobar", "-3", "123", "bar");
+}
+
+// Test getting the middle of the string in different ways
+TEST_F(TokenTest, substringMiddle) {
+ verifySubstringEval("foobar", "1", "4", "ooba");
+ verifySubstringEval("foobar", "-5", "4", "ooba");
+ verifySubstringEval("foobar", "-1", "-4", "ooba");
+ verifySubstringEval("foobar", "5", "-4", "ooba");
+}
+
+// Test getting the last letter in different ways
+TEST_F(TokenTest, substringLastLetter) {
+ verifySubstringEval("foobar", "5", "all", "r");
+ verifySubstringEval("foobar", "5", "1", "r");
+ verifySubstringEval("foobar", "5", "5", "r");
+ verifySubstringEval("foobar", "-1", "all", "r");
+ verifySubstringEval("foobar", "-1", "1", "r");
+ verifySubstringEval("foobar", "-1", "5", "r");
+}
+
+// Test we get only what is available if we ask for a longer string
+TEST_F(TokenTest, substringLength) {
+ // Test off the front
+ verifySubstringEval("foobar", "0", "-4", "");
+ verifySubstringEval("foobar", "1", "-4", "f");
+ verifySubstringEval("foobar", "2", "-4", "fo");
+ verifySubstringEval("foobar", "3", "-4", "foo");
+
+ // and the back
+ verifySubstringEval("foobar", "3", "4", "bar");
+ verifySubstringEval("foobar", "4", "4", "ar");
+ verifySubstringEval("foobar", "5", "4", "r");
+ verifySubstringEval("foobar", "6", "4", "");
+}
+
+// Test that we get nothing if the starting postion is out of the string
+TEST_F(TokenTest, substringStartingPosition) {
+ // Off the front
+ verifySubstringEval("foobar", "-7", "1", "");
+ verifySubstringEval("foobar", "-7", "-11", "");
+ verifySubstringEval("foobar", "-7", "all", "");
+
+ // and the back
+ verifySubstringEval("foobar", "6", "1", "");
+ verifySubstringEval("foobar", "6", "-11", "");
+ verifySubstringEval("foobar", "6", "all", "");
+}
+
+// Check what happens if we use strings that aren't numbers for start or length
+// We should return the empty string
+TEST_F(TokenTest, substringBadParams) {
+ verifySubstringEval("foobar", "0ick", "all", "");
+ verifySubstringEval("foobar", "ick0", "all", "");
+ verifySubstringEval("foobar", "ick", "all", "");
+ verifySubstringEval("foobar", "0", "ick", "");
+ verifySubstringEval("foobar", "0", "0ick", "");
+ verifySubstringEval("foobar", "0", "ick0", "");
+ verifySubstringEval("foobar", "0", "allaboard", "");
+}
+
+// lastly check that we don't get anything if the string is empty or
+// we don't ask for any characters from it.
+TEST_F(TokenTest, substringReturnEmpty) {
+ verifySubstringEval("", "0", "all", "");
+ verifySubstringEval("foobar", "0", "0", "");
+}
+
+// Check if we can use the substring and equal tokens together
+// We put the result on the stack first then the substring values
+// then evaluate the substring which should leave the original
+// result on the bottom with the substring result on next.
+// Evaulating the equals should produce true for the first
+// and false for the second.
+// throws an exception if there aren't enough values on the stack.
+// The stack from the top is: length, start, string.
+// The actual packet is not used.
+TEST_F(TokenTest, substringEquals) {
+ TokenPtr tequal;
+
+ ASSERT_NO_THROW(t_.reset(new TokenSubstring()));
+ ASSERT_NO_THROW(tequal.reset(new TokenEqual()));
+
+ // The final expected value
+ values_.push("ooba");
+
+ // The substring values
+ // Subsring requires three values on the stack, try
+ // with 0, 1 and 2 all should thorw an exception
+ values_.push("foobar");
+ values_.push("1");
+ values_.push("4");
+ EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+
+ // we should have two values on the stack
+ ASSERT_EQ(2, values_.size());
+
+ // next the equals eval
+ EXPECT_NO_THROW(tequal->evaluate(*pkt4_, values_));
+ ASSERT_EQ(1, values_.size());
+ EXPECT_EQ("true", values_.top());
+
+ // get rid of the result
+ values_.pop();
+
+ // and try it again but with a bad final value
+ // The final expected value
+ values_.push("foob");
+
+ // The substring values
+ // Subsring requires three values on the stack, try
+ // with 0, 1 and 2 all should thorw an exception
+ values_.push("foobar");
+ values_.push("1");
+ values_.push("4");
+ EXPECT_NO_THROW(t_->evaluate(*pkt4_, values_));
+
+ // we should have two values on the stack
+ ASSERT_EQ(2, values_.size());
+
+ // next the equals eval
+ EXPECT_NO_THROW(tequal->evaluate(*pkt4_, values_));
+ ASSERT_EQ(1, values_.size());
+ EXPECT_EQ("false", values_.top());
+
+}
diff --git a/src/lib/eval/token.cc b/src/lib/eval/token.cc
index 66ebf1c5b1..5720158ca3 100644
--- a/src/lib/eval/token.cc
+++ b/src/lib/eval/token.cc
@@ -13,6 +13,8 @@
// PERFORMANCE OF THIS SOFTWARE.
#include
+#include
+#include
#include
using namespace isc::dhcp;
@@ -53,3 +55,76 @@ TokenEqual::evaluate(const Pkt& /*pkt*/, ValueStack& values) {
else
values.push("false");
}
+
+void
+TokenSubstring::evaluate(const Pkt& /*pkt*/, ValueStack& values) {
+
+ if (values.size() < 3) {
+ isc_throw(EvalBadStack, "Incorrect stack order. Expected at least "
+ "3 values for substring operator, got " << values.size());
+ }
+
+ string len_str = values.top();
+ values.pop();
+ string start_str = values.top();
+ values.pop();
+ string string_str = values.top();
+ values.pop();
+
+ // If we have no string to start with we push an empty string and leave
+ if (string_str.empty()) {
+ values.push("");
+ return;
+ }
+
+ // Convert the starting position and length from strings to numbers
+ // the length may also be "all" in which case simply make it the
+ // length of the string.
+ // If we have a problem push an empty string and leave
+ int start_pos;
+ int length;
+ try {
+ start_pos = boost::lexical_cast(start_str);
+ if (len_str == "all") {
+ length = string_str.length();
+ } else {
+ length = boost::lexical_cast(len_str);
+ }
+ } catch (const boost::bad_lexical_cast&) {
+ LOG_DEBUG(eval_logger, EVAL_DBG_TRACE,
+ EVAL_SUBSTRING_BAD_PARAM_CONVERSION)
+ .arg(start_str)
+ .arg(len_str);
+
+ values.push("");
+ return;
+ }
+
+ const int string_length = string_str.length();
+ // If the starting postion is outside of the string push an
+ // empty string and leave
+ if ((start_pos < -string_length) || (start_pos >= string_length)) {
+ values.push("");
+ return;
+ }
+
+ // Adjust the values to be something for substr. We first figure out
+ // the starting postion, then update it and the length to get the
+ // characters before or after it depending on the sign of length
+ if (start_pos < 0) {
+ start_pos = string_length + start_pos;
+ }
+
+ if (length < 0) {
+ length = -length;
+ if (length <= start_pos){
+ start_pos -= length;
+ } else {
+ length = start_pos;
+ start_pos = 0;
+ }
+ }
+
+ // and finally get the substring
+ values.push(string_str.substr(start_pos, length));
+}
diff --git a/src/lib/eval/token.h b/src/lib/eval/token.h
index 930c707fc5..1e34051225 100644
--- a/src/lib/eval/token.h
+++ b/src/lib/eval/token.h
@@ -56,7 +56,7 @@ public:
/// - option[123] (a token that extracts value of option 123)
/// - == (an operator that compares two other tokens)
/// - substring(a,b,c) (an operator that takes three arguments: a string,
-/// first and last character)
+/// first character and length)
class Token {
public:
@@ -150,10 +150,65 @@ public:
/// either "true" or "false". It requires at least two parameters to be
/// present on stack.
///
- /// @throw EvalBadStack if there's less than 2 values on stack
+ /// @throw EvalBadStack if there are less than 2 values on stack
///
- /// @brief pkt (unused)
- /// @brief values - stack of values (2 arguments will be poped, 1 result
+ /// @param pkt (unused)
+ /// @param values - stack of values (2 arguments will be popped, 1 result
+ /// will be pushed)
+ void evaluate(const Pkt& pkt, ValueStack& values);
+};
+
+/// @brief Token that represents the substring operator (returns a portion
+/// of the supplied string)
+///
+/// This token represents substring(str, start, len) An operator that takes three
+/// arguments: a string, the first character and the length.
+class TokenSubstring : public Token {
+public:
+ /// @brief Constructor (does nothing)
+ TokenSubstring() {}
+
+ /// @brief Extract a substring from a string
+ ///
+ /// Evaluation does not use packet information. It requires at least
+ /// three values to be present on the stack. It will consume the top
+ /// three values on the stack as parameters and push the resulting substring
+ /// onto the stack. From the top it expects the values on the stack as:
+ /// - len
+ /// - start
+ /// - str
+ ///
+ /// str is the string to extract a substring from. If it is empty, an empty
+ /// string is pushed onto the value stack.
+ ///
+ /// start is the postion from which the code starts extracting the substring.
+ /// 0 is the first character and a negative number starts from the end, with
+ /// -1 being the last character. If the starting point is outside of the
+ /// original string an empty string is pushed onto the value stack.
+ ///
+ /// length is the number of characters from the string to extract.
+ /// "all" means all remaining characters from start to the end of string.
+ /// A negative number means to go from start towards the beginning of
+ /// the string, but doesn't include start.
+ /// If length is longer than the remaining portion of string
+ /// then the entire remaining portion is placed on the value stack.
+ ///
+ /// The following examples all use the base string "foobar", the first number
+ /// is the starting position and the second is the length. Note that
+ /// a negative length only selects which characters to extract it does not
+ /// indicate an attempt to reverse the string.
+ /// - 0, all => "foobar"
+ /// - 0, 6 => "foobar"
+ /// - 0, 4 => "foob"
+ /// - 2, all => "obar"
+ /// - 2, 6 => "obar"
+ /// - -1, all => "r"
+ /// - -1, -4 => "ooba"
+ ///
+ /// @throw EvalBadStack if there are less than 3 values on stack
+ ///
+ /// @param pkt (unused)
+ /// @param values - stack of values (3 arguments will be popped, 1 result
/// will be pushed)
void evaluate(const Pkt& pkt, ValueStack& values);
};