]> git.ipfire.org Git - thirdparty/kea.git/commitdiff
[#539, !330] OPT_STRING_TYPE options now trim trailing nulls
authorThomas Markwalder <tmark@isc.org>
Thu, 23 May 2019 19:07:43 +0000 (15:07 -0400)
committerThomas Markwalder <tmark@isc.org>
Fri, 24 May 2019 14:13:07 +0000 (10:13 -0400)
src/lib/dhcp/option_data_types.*
    OptionDataTypeUtil::readString() - modified to trim off trailing
    nulls and to throw if the result is zero length

src/lib/dhcp/option_string.*
    OptionString::setValue()
    OptionString::unpack() - modified to trim off trailing
    nulls and to throw if the result is zero length

src/lib/dhcp/tests/option_data_types_unittest.cc
src/lib/dhcp/tests/option_string_unittest.cc
src/lib/dhcp/tests/pkt4_unittest.cc
    Udpated and/or added tests to verify behavior
    with permutations of nulls in inputs

src/lib/dhcp/option_data_types.cc
src/lib/dhcp/option_data_types.h
src/lib/dhcp/option_string.cc
src/lib/dhcp/option_string.h
src/lib/dhcp/tests/option_data_types_unittest.cc
src/lib/dhcp/tests/option_string_unittest.cc
src/lib/dhcp/tests/pkt4_unittest.cc

index e159aa08654f2812c56d0b48207e8591cd60bb68..a64707a9baff4341a016ebd8ad81a9858e063759 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2019 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
@@ -574,8 +574,19 @@ std::string
 OptionDataTypeUtil::readString(const std::vector<uint8_t>& buf) {
     std::string value;
     if (!buf.empty()) {
-        value.insert(value.end(), buf.begin(), buf.end());
+        // Per RFC 2132, section 2 we need to drop trailing NULLs
+        auto begin = buf.begin();
+        auto end = buf.end();
+        for ( ; end != begin && *(end - 1) == 0x0; --end);
+
+        if (std::distance(begin, end) == 0) {
+            isc_throw(isc::OutOfRange, "string value carried by the option "
+                      << " contained only NULLs");
+        }
+
+        value.insert(value.end(), begin, end);
     }
+
     return (value);
 }
 
index 358dcc07287ab1a1580ccf5b7ccc7b098f79c5aa..1408b48c649f3a738289932fdcacdcd36411b8b7 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2019 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
@@ -610,9 +610,11 @@ public:
 
     /// @brief Read string value from a buffer.
     ///
+    /// To be compliant with RFC 2132, Sec. 2, trailing NULLs are trimmed.
     /// @param buf input buffer.
     ///
     /// @return string value being read.
+    /// @throw isc::dhcp::OutOfRange is the payload contains only NULLs.
     static std::string readString(const std::vector<uint8_t>& buf);
 
     /// @brief Write UTF8-encoded string into a buffer.
index 1f5c12f329edc917e20fedd3dbf723a0e08d156a..2b185520a5af0078f40963bacef2bafb0d697aef 100644 (file)
@@ -50,7 +50,18 @@ OptionString::setValue(const std::string& value) {
                   << getType() << "' must not be empty");
     }
 
-    setData(value.begin(), value.end());
+    // Trim off any trailing NULLs.
+    auto begin = value.begin();
+    auto end = value.end();
+    for ( ; end != begin && *(end - 1) == 0x0; --end);
+
+    if (std::distance(begin, end) == 0) {
+        isc_throw(isc::OutOfRange, "string value carried by the option '"
+                  << getType() << "' contained only NULLs");
+    }
+
+    // Now set the value.
+    setData(begin, end);
 }
 
 
@@ -74,11 +85,16 @@ OptionString::pack(isc::util::OutputBuffer& buf) const {
 void
 OptionString::unpack(OptionBufferConstIter begin,
                      OptionBufferConstIter end) {
+    // Trim off trailing null(s)
+    for ( ; end != begin && *(end - 1) == 0x0; --end);
+
     if (std::distance(begin, end) == 0) {
         isc_throw(isc::OutOfRange, "failed to parse an option '"
                   << getType() << "' holding string value"
-                  << " - empty value is not accepted");
+                  << "' was empty or contained only NULLs");
     }
+
+    // Now set the data.
     setData(begin, end);
 }
 
index 51be5c2394859c160f561c6b18ba14b57125045e..c590fe1fe2b30fe5c81669d675ca66eac30b547e 100644 (file)
@@ -20,7 +20,8 @@ namespace dhcp {
 ///
 /// This class represents an option carrying a single string value.
 /// Currently this class imposes that the minimal length of the carried
-/// string is 1.
+/// string is 1.  Per RFC 2132, Sec 2 trailing NULLs are trimmed during
+/// either construction or unpacking.
 ///
 /// @todo In the future this class may be extended with some more string
 /// content checks and encoding methods if required.
@@ -32,7 +33,7 @@ public:
     /// This constructor creates an instance of option which carries a
     /// string value specified as constructor's parameter. This constructor
     /// is most often used to create an instance of an option which will
-    /// be sent in the outgoing packet.
+    /// be sent in the outgoing packet.  Trailing NULLs will be trimmed.
     ///
     /// @param u universe (V4 or V6).
     /// @param type option code.
@@ -46,6 +47,7 @@ public:
     ///
     /// This constructor creates an instance of the option from the provided
     /// chunk of buffer. This buffer may hold the data received on the wire.
+    /// Trailing NULLs will be trimmed.
     ///
     /// @param u universe (V4 or V6).
     /// @param type option code.
@@ -71,6 +73,8 @@ public:
 
     /// @brief Sets the string value to be held by the option.
     ///
+    /// Trailing NULLs will be trimmed.
+    ///
     /// @param value string value to be set.
     ///
     /// @throw isc::OutOfRange if a string value to be set is empty.
@@ -91,6 +95,7 @@ public:
     /// it does not decode the option code and length, so the iterators must
     /// point to the beginning and end of the option payload respectively.
     /// The size of the decoded payload must be at least 1 byte.
+    /// Trailing NULLs will be trimmed.
     ///
     /// @param begin the iterator pointing to the option payload.
     /// @param end the iterator pointing to the end of the option payload.
index f90ebec72359660fc8946b58a75ffbcd59b5357d..e43fb2b13cdf4e58fa83974c592d90b4f701f7aa 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2012-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2012-2019 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
@@ -847,6 +847,7 @@ TEST_F(OptionDataTypesTest, writePsid) {
 // The purpose of this test is to verify that the string
 // can be read from a buffer correctly.
 TEST_F(OptionDataTypesTest, readString) {
+
     // Prepare a buffer with some string in it.
     std::vector<uint8_t> buf;
     writeString("hello world", buf);
@@ -858,6 +859,34 @@ TEST_F(OptionDataTypesTest, readString) {
     );
     // Check that it is valid.
     EXPECT_EQ("hello world", value);
+
+    // Only nulls should throw.
+    OptionBuffer buffer = { 0, 0 };
+    ASSERT_THROW(OptionDataTypeUtil::readString(buffer), isc::OutOfRange);
+
+    // One trailing null should trim off.
+    buffer = {'o', 'n', 'e', 0 };
+    ASSERT_NO_THROW(value = OptionDataTypeUtil::readString(buffer));
+    EXPECT_EQ(3, value.length());
+    EXPECT_EQ(value, std::string("one"));
+
+    // More than one trailing null should trim off.
+    buffer = { 't', 'h', 'r', 'e', 'e', 0, 0, 0 };
+    ASSERT_NO_THROW(value = OptionDataTypeUtil::readString(buffer));
+    EXPECT_EQ(5, value.length());
+    EXPECT_EQ(value, std::string("three"));
+
+    // Embedded null should be left in place.
+    buffer = { 'e', 'm', 0, 'b', 'e', 'd' };
+    ASSERT_NO_THROW(value = OptionDataTypeUtil::readString(buffer));
+    EXPECT_EQ(6, value.length());
+    EXPECT_EQ(value, (std::string{"em\0bed", 6}));
+
+    // Leading null should be left in place.
+    buffer = { 0, 'l', 'e', 'a', 'd', 'i', 'n', 'g' };
+    ASSERT_NO_THROW(value = OptionDataTypeUtil::readString(buffer));
+    EXPECT_EQ(8, value.length());
+    EXPECT_EQ(value, (std::string{"\0leading", 8}));
 }
 
 // The purpose of this test is to verify that a string can be
index 26c84fade3f6baa9cfb1cc6795aa6957f1587ca0..ae9346e1fc5ef3c8be3b4f92e27596146b4df7e3 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2013-2017 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2013-2019 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
@@ -55,6 +55,11 @@ TEST_F(OptionStringTest, constructorFromString) {
     // Check that an attempt to use empty string in the constructor
     // will result in an exception.
     EXPECT_THROW(OptionString(Option::V6, 123, ""), isc::OutOfRange);
+
+    // Check that an attempt to use string containgin only nulls
+    // in the constructor will result in an exception.
+    std::string nulls{"\0\0",2};
+    EXPECT_THROW(OptionString(Option::V6, 123, nulls), isc::OutOfRange);
 }
 
 // This test verifies that the constructor which creates an option instance
@@ -69,6 +74,13 @@ TEST_F(OptionStringTest, constructorFromBuffer) {
         isc::OutOfRange
     );
 
+    // NULLs should result in an exception.
+    std::vector<uint8_t>nulls = { 0, 0, 0 };
+    EXPECT_THROW(
+        OptionString(Option::V4, 234, nulls.begin(), nulls.begin()),
+        isc::OutOfRange
+    );
+
     // Declare option as a scoped pointer here so as its scope is
     // function wide. The initialization (constructor invocation)
     // is pushed to the ASSERT_NO_THROW macro below, as it may
@@ -145,7 +157,7 @@ TEST_F(OptionStringTest, pack) {
     // And create a string from it.
     std::string test_string(data.begin(), data.end());
     // This string should be equal to the string used to create
-    // option's instance. 
+    // option's instance.
     EXPECT_TRUE(option_value == test_string);
 }
 
@@ -163,4 +175,67 @@ TEST_F(OptionStringTest, toText) {
     EXPECT_EQ("type=00512, len=00016: \"is a filler text\" (string)", optv6.toText());
 }
 
+// This test checks proper handling of trailing and embedded NULLs in
+// data use to create or option value.
+TEST_F(OptionStringTest, setValueNullsHandling) {
+    OptionString optv4(Option::V4, 123, "123");
+
+    // Only nulls should throw.
+    ASSERT_THROW(optv4.setValue(std::string{"\0\0", 2}), isc::OutOfRange);
+
+    // One trailing null should trim off.
+    ASSERT_NO_THROW(optv4.setValue(std::string{"one\0", 4}));
+    EXPECT_EQ(3, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), std::string("one"));
+
+    // More than one trailing null should trim off.
+    ASSERT_NO_THROW(optv4.setValue(std::string{"three\0\0\0", 8}));
+    EXPECT_EQ(5, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), std::string("three"));
+
+    // Embedded null should be left in place.
+    ASSERT_NO_THROW(optv4.setValue(std::string{"em\0bed", 6}));
+    EXPECT_EQ(6, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), (std::string{"em\0bed", 6}));
+
+    // Leading null should be left in place.
+    ASSERT_NO_THROW(optv4.setValue(std::string{"\0leading", 8}));
+    EXPECT_EQ(8, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), (std::string{"\0leading", 8}));
+}
+
+// This test checks proper handling of trailing and embedded NULLs in
+// data use to create or option value.
+TEST_F(OptionStringTest, unpackNullsHandling) {
+    OptionString optv4(Option::V4, 123, "123");
+
+    // Only nulls should throw.
+    OptionBuffer buffer = { 0, 0 };
+    ASSERT_THROW(optv4.unpack(buffer.begin(), buffer.end()), isc::OutOfRange);
+
+    // One trailing null should trim off.
+    buffer = {'o', 'n', 'e', 0 };
+    ASSERT_NO_THROW(optv4.unpack(buffer.begin(), buffer.end()));
+    EXPECT_EQ(3, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), std::string("one"));
+
+    // More than one trailing null should trim off.
+    buffer = { 't', 'h', 'r', 'e', 'e', 0, 0, 0 };
+    ASSERT_NO_THROW(optv4.unpack(buffer.begin(), buffer.end()));
+    EXPECT_EQ(5, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), std::string("three"));
+
+    // Embedded null should be left in place.
+    buffer = { 'e', 'm', 0, 'b', 'e', 'd' };
+    ASSERT_NO_THROW(optv4.unpack(buffer.begin(), buffer.end()));
+    EXPECT_EQ(6, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), (std::string{"em\0bed", 6}));
+
+    // Leading null should be left in place.
+    buffer = { 0, 'l', 'e', 'a', 'd', 'i', 'n', 'g' };
+    ASSERT_NO_THROW(optv4.unpack(buffer.begin(), buffer.end()));
+    EXPECT_EQ(8, optv4.getValue().length());
+    EXPECT_EQ(optv4.getValue(), (std::string{"\0leading", 8}));
+}
+
 } // anonymous namespace
index c052865a3760b97eaf6af0cf3531e42282e67d2f..89c6142089d63c4981c7db593f440993c90fed78 100644 (file)
@@ -1,4 +1,4 @@
-// Copyright (C) 2011-2018 Internet Systems Consortium, Inc. ("ISC")
+// Copyright (C) 2011-2019 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
@@ -188,11 +188,12 @@ public:
         boost::shared_ptr<Option> x = pkt->getOption(12);
         ASSERT_TRUE(x); // option 1 should exist
         OptionStringPtr option12 = boost::static_pointer_cast<OptionString>(x);
+
         ASSERT_TRUE(option12);
         EXPECT_EQ(12, option12->getType());  // this should be option 12
         ASSERT_EQ(3, option12->getValue().length()); // it should be of length 3
         EXPECT_EQ(5, option12->len()); // total option length 5
-        EXPECT_EQ(0, memcmp(&option12->getValue()[0], opt_data_ptr + 2, 3)); // data len=3
+        EXPECT_EQ(0, memcmp(&option12->getValue()[0], opt_data_ptr + 2, 2)); // data len=3
         opt_data_ptr += x->len();
 
         x = pkt->getOption(14);
@@ -204,6 +205,7 @@ public:
         EXPECT_EQ(14, option14->getType());  // this should be option 14
         ASSERT_EQ(3, option14->getValue().length()); // it should be of length 3
         EXPECT_EQ(5, option14->len()); // total option length 5
+
         EXPECT_EQ(0, memcmp(&option14->getValue()[0], opt_data_ptr + 2, 3)); // data len=3
         opt_data_ptr += x->len();
 
@@ -1204,4 +1206,97 @@ TEST_F(Pkt4Test, truncatedVendorLength) {
     ASSERT_FALSE(x);
 }
 
+// Verifies that we handle text options that contain trailing
+// and embedded NULLs correctly.  Per RFC 2132, Sec 2 we should
+// be stripping trailing NULLs.  We've agreed to permit
+// embedded NULLs (for now).
+TEST_F(Pkt4Test, nullTerminatedOptions) {
+    // Construct the onwire packet.
+    vector<uint8_t> base_msg = generateTestPacket2();
+    base_msg.push_back(0x63); // magic cookie
+    base_msg.push_back(0x82);
+    base_msg.push_back(0x53);
+    base_msg.push_back(0x63);
+
+    base_msg.push_back(0x35); // message-type
+    base_msg.push_back(0x1);
+    base_msg.push_back(0x1);
+
+    int base_size = base_msg.size();
+
+    // We'll create four text options, with various combinations of NULLs.
+    vector<uint8_t> hostname = { DHO_HOST_NAME, 5, 't', 'w', 'o', 0, 0 };
+    vector<uint8_t> merit_dump = { DHO_MERIT_DUMP, 4, 'o', 'n', 'e', 0 };
+    vector<uint8_t> root_path = { DHO_ROOT_PATH, 4, 'n', 'o', 'n', 'e' };
+    vector<uint8_t> domain_name = { DHO_DOMAIN_NAME, 6, 'e', 'm', 0, 'b', 'e', 'd' };
+
+    // Add the options to the onwire packet.
+    vector<uint8_t> test_msg = base_msg;
+    test_msg.insert(test_msg.end(), hostname.begin(), hostname.end());
+    test_msg.insert(test_msg.end(), root_path.begin(), root_path.end());
+    test_msg.insert(test_msg.end(), merit_dump.begin(), merit_dump.end());
+    test_msg.insert(test_msg.end(), domain_name.begin(), domain_name.end());
+    test_msg.push_back(DHO_END);
+
+    boost::shared_ptr<Pkt4> pkt(new Pkt4(&test_msg[0], test_msg.size()));
+
+    // Unpack the onwire packet.
+    EXPECT_NO_THROW(
+        pkt->unpack()
+    );
+
+    EXPECT_EQ(DHCPDISCOVER, pkt->getType());
+
+    OptionPtr opt;
+    OptionStringPtr opstr;
+
+    // Now let's verify that each text option is as expected.
+    ASSERT_TRUE(opt = pkt->getOption(DHO_HOST_NAME));
+    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
+    EXPECT_EQ(3, opstr->getValue().length());
+    EXPECT_EQ("two", opstr->getValue());
+
+    ASSERT_TRUE(opt = pkt->getOption(DHO_MERIT_DUMP));
+    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
+    EXPECT_EQ(3, opstr->getValue().length());
+    EXPECT_EQ("one", opstr->getValue());
+
+    ASSERT_TRUE(opt = pkt->getOption(DHO_ROOT_PATH));
+    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
+    EXPECT_EQ(4, opstr->getValue().length());
+    EXPECT_EQ("none", opstr->getValue());
+
+    ASSERT_TRUE(opt = pkt->getOption(DHO_DOMAIN_NAME));
+    ASSERT_TRUE(opstr = boost::dynamic_pointer_cast<OptionString>(opt));
+    EXPECT_EQ(6, opstr->getValue().length());
+    std::string embed{"em\0bed", 6};
+    EXPECT_EQ(embed, opstr->getValue());
+
+
+    // Next we pack the packet, to make sure trailing NULLs have
+    // been eliminated, embedded NULLs are intact.
+    EXPECT_NO_THROW(
+        pkt->pack()
+    );
+
+    // Create a vector of our expectd packed option data.
+    vector<uint8_t> packed_opts =
+        {
+          DHO_HOST_NAME, 3, 't', 'w', 'o',
+          DHO_MERIT_DUMP, 3, 'o', 'n', 'e',
+          DHO_DOMAIN_NAME, 6, 'e', 'm', 0, 'b', 'e', 'd',
+          DHO_ROOT_PATH, 4, 'n', 'o', 'n', 'e',
+        };
+
+    const uint8_t* packed = static_cast<const uint8_t*>(pkt->getBuffer().getData());
+    int packed_len = pkt->getBuffer().getLength();
+
+    // Packed message options should be 3 bytes smaller than original onwire data.
+    int dif = packed_len - test_msg.size();
+    ASSERT_EQ(-3, dif);
+
+    // Make sure the packed content is as expected.
+    EXPECT_EQ(0, memcmp(&packed[base_size], &packed_opts[0], packed_opts.size()));
+}
+
 } // end of anonymous namespace