From: Marcin Siodelski Date: Wed, 7 Aug 2013 16:54:52 +0000 (+0200) Subject: [3082] Implemented DHCPv4 Client FQDN Option. X-Git-Tag: bind10-1.2.0beta1-release~259^2~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=1ceca44c4f118ea3d0246a7ac4b67331fcf4241c;p=thirdparty%2Fkea.git [3082] Implemented DHCPv4 Client FQDN Option. --- diff --git a/src/lib/dhcp/Makefile.am b/src/lib/dhcp/Makefile.am index 114c301f2a..7ef4fd3ea6 100644 --- a/src/lib/dhcp/Makefile.am +++ b/src/lib/dhcp/Makefile.am @@ -23,6 +23,7 @@ libb10_dhcp___la_SOURCES += iface_mgr_linux.cc libb10_dhcp___la_SOURCES += iface_mgr_sun.cc libb10_dhcp___la_SOURCES += libdhcp++.cc libdhcp++.h libb10_dhcp___la_SOURCES += option4_addrlst.cc option4_addrlst.h +libb10_dhcp___la_SOURCES += option4_client_fqdn.cc option4_client_fqdn.h libb10_dhcp___la_SOURCES += option6_ia.cc option6_ia.h libb10_dhcp___la_SOURCES += option6_iaaddr.cc option6_iaaddr.h libb10_dhcp___la_SOURCES += option6_addrlst.cc option6_addrlst.h diff --git a/src/lib/dhcp/option4_client_fqdn.cc b/src/lib/dhcp/option4_client_fqdn.cc new file mode 100644 index 0000000000..d705dc6d43 --- /dev/null +++ b/src/lib/dhcp/option4_client_fqdn.cc @@ -0,0 +1,387 @@ +// Copyright (C) 2013 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. + +#include +#include +#include +#include +#include +#include +#include + +namespace isc { +namespace dhcp { + +class Option4ClientFqdnImpl { +public: + uint8_t flags_; + Option4ClientFqdn::Rcode rcode1_; + Option4ClientFqdn::Rcode rcode2_; + boost::shared_ptr domain_name_; + Option4ClientFqdn::DomainNameType domain_name_type_; + + Option4ClientFqdnImpl(const uint8_t flag, + const Option4ClientFqdn::Rcode& rcode, + const std::string& domain_name, + const Option4ClientFqdn::DomainNameType name_type); + + Option4ClientFqdnImpl(OptionBufferConstIter first, + OptionBufferConstIter last); + + Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source); + + Option4ClientFqdnImpl& operator=(const Option4ClientFqdnImpl& source); + + void setDomainName(const std::string& domain_name, + const Option4ClientFqdn::DomainNameType name_type); + + static void checkFlags(const uint8_t flags); + + void parseWireData(OptionBufferConstIter first, + OptionBufferConstIter last); + +}; + +Option4ClientFqdnImpl:: +Option4ClientFqdnImpl(const uint8_t flag, + const Option4ClientFqdn::Rcode& rcode, + const std::string& domain_name, + const Option4ClientFqdn::DomainNameType name_type) + : flags_(flag), + rcode1_(rcode), + rcode2_(rcode), + domain_name_(), + domain_name_type_(name_type) { + + // Check if flags are correct. + checkFlags(flags_); + // Set domain name. It may throw an exception if domain name has wrong + // format. + setDomainName(domain_name, name_type); +} + +Option4ClientFqdnImpl::Option4ClientFqdnImpl(OptionBufferConstIter first, + OptionBufferConstIter last) + : rcode1_(Option4ClientFqdn::RCODE_CLIENT()), + rcode2_(Option4ClientFqdn::RCODE_CLIENT()) { + parseWireData(first, last); + // Verify that flags value was correct. + checkFlags(flags_); +} + +Option4ClientFqdnImpl:: +Option4ClientFqdnImpl(const Option4ClientFqdnImpl& source) + : flags_(source.flags_), + rcode1_(source.rcode1_), + rcode2_(source.rcode2_), + domain_name_(), + domain_name_type_(source.domain_name_type_) { + if (source.domain_name_) { + domain_name_.reset(new isc::dns::Name(*source.domain_name_)); + } +} + +Option4ClientFqdnImpl& +Option4ClientFqdnImpl::operator=(const Option4ClientFqdnImpl& source) { + domain_name_.reset(new isc::dns::Name(*source.domain_name_)); + + // Assignment is exception safe. + flags_ = source.flags_; + rcode1_ = source.rcode1_; + rcode2_ = source.rcode2_; + domain_name_type_ = source.domain_name_type_; + + return (*this); +} + +void +Option4ClientFqdnImpl:: +setDomainName(const std::string& domain_name, + const Option4ClientFqdn::DomainNameType name_type) { + // domain-name must be trimmed. Otherwise, string comprising spaces only + // would be treated as a fully qualified name. + std::string name = isc::util::str::trim(domain_name); + if (name.empty()) { + if (name_type == Option4ClientFqdn::FULL) { + isc_throw(InvalidOption4ClientFqdnDomainName, + "fully qualified domain-name must not be empty" + << " when setting new domain-name for DHCPv4 Client" + << " FQDN Option"); + } + // The special case when domain-name is empty is marked by setting the + // pointer to the domain-name object to NULL. + domain_name_.reset(); + + } else { + try { + domain_name_.reset(new isc::dns::Name(name)); + domain_name_type_ = name_type; + + } catch (const Exception& ex) { + isc_throw(InvalidOption4ClientFqdnDomainName, + "invalid domain-name value '" + << domain_name << "' when setting new domain-name for" + << " DHCPv4 Client FQDN Option"); + + } + } +} + +void +Option4ClientFqdnImpl::checkFlags(const uint8_t flags) { + // The Must Be Zero (MBZ) bits must not be set. + if ((flags & ~Option4ClientFqdn::FLAG_MASK) != 0) { + isc_throw(InvalidOption4ClientFqdnFlags, + "invalid DHCPv4 Client FQDN Option flags: 0x" + << std::hex << static_cast(flags) << std::dec); + } + + // According to RFC 4702, section 2.1. if the N bit is 1, the S bit + // MUST be 0. Checking it here. + if ((flags & (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S)) + == (Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S)) { + isc_throw(InvalidOption4ClientFqdnFlags, + "both N and S flag of the DHCPv4 Client FQDN Option are set." + << " According to RFC 4702, if the N bit is 1 the S bit" + << " MUST be 0"); + } +} + +void +Option4ClientFqdnImpl::parseWireData(OptionBufferConstIter first, + OptionBufferConstIter last) { + + // Buffer must comprise at least one byte with the flags. + // The domain-name may be empty. + if (std::distance(first, last) < Option4ClientFqdn::FIXED_FIELDS_LEN) { + isc_throw(OutOfRange, "DHCPv4 Client FQDN Option (" + << DHO_FQDN << ") is truncated"); + } + + // Parse flags + flags_ = *(first++); + + // Parse RCODE1 and RCODE2. + rcode1_ = Option4ClientFqdn::Rcode(*(first++)); + rcode2_ = Option4ClientFqdn::Rcode(*(first++)); + + // Parse domain-name if any. + if (std::distance(first, last) > 0) { + // The FQDN may comprise a partial domain-name. In this case it lacks + // terminating 0. If this is the case, we will need to add zero at + // the end because Name object constructor requires it. + if (*(last - 1) != 0) { + // Create temporary buffer and add terminating zero. + OptionBuffer buf(first, last); + buf.push_back(0); + // Reset domain name. + isc::util::InputBuffer name_buf(&buf[0], buf.size()); + domain_name_.reset(new isc::dns::Name(name_buf)); + // Terminating zero was missing, so set the domain-name type + // to partial. + domain_name_type_ = Option4ClientFqdn::PARTIAL; + } else { + // We are dealing with fully qualified domain name so there is + // no need to add terminating zero. Simply pass the buffer to + // Name object constructor. + isc::util::InputBuffer name_buf(&(*first), + std::distance(first, last)); + domain_name_.reset(new isc::dns::Name(name_buf)); + // Set the domain-type to fully qualified domain name. + domain_name_type_ = Option4ClientFqdn::FULL; + } + } +} + + Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag, const Rcode& rcode) + : Option(Option::V4, DHO_FQDN), + impl_(new Option4ClientFqdnImpl(flag, rcode, "", PARTIAL)) { +} + +Option4ClientFqdn::Option4ClientFqdn(const uint8_t flag, + const Rcode& rcode, + const std::string& domain_name, + const DomainNameType domain_name_type) + : Option(Option::V4, DHO_FQDN), + impl_(new Option4ClientFqdnImpl(flag, rcode, domain_name, + domain_name_type)) { +} + +Option4ClientFqdn::Option4ClientFqdn(OptionBufferConstIter first, + OptionBufferConstIter last) + : Option(Option::V4, DHO_FQDN, first, last), + impl_(new Option4ClientFqdnImpl(first, last)) { +} + +Option4ClientFqdn::~Option4ClientFqdn() { + delete(impl_); +} + +Option4ClientFqdn::Option4ClientFqdn(const Option4ClientFqdn& source) + : Option(source), + impl_(new Option4ClientFqdnImpl(*source.impl_)) { +} + +Option4ClientFqdn& +Option4ClientFqdn::operator=(const Option4ClientFqdn& source) { + Option4ClientFqdnImpl* old_impl = impl_; + impl_ = new Option4ClientFqdnImpl(*source.impl_); + delete(old_impl); + return (*this); +} + +bool +Option4ClientFqdn::getFlag(const Flag flag) const { + // Caller should query for one of the: E, N, S or O flags. However, there + // are valid enumerator values which should not be accepted by this function. + // For example a value of 0x3 is valid (because it belongs to the range between the + // lowest and highest enumerator). The value 0x3 represents two flags: + // S and O and would cause ambiguity. Therefore, we selectively check + // that the flag is equal to one of the explicit enumerator values. If + // not, throw an exception. + if (flag != FLAG_S && flag != FLAG_O && flag != FLAG_N && flag != FLAG_E) { + isc_throw(InvalidOption4ClientFqdnFlags, "invalid DHCPv4 Client FQDN" + << " Option flag specified, expected E, N, S or O"); + } + + return ((impl_->flags_ & flag) != 0); +} + +void +Option4ClientFqdn::setFlag(const Flag flag, const bool set_flag) { + // Check that flag is in range between 0x1 and 0x7. Note that this + // allows to set or clear multiple flags concurrently. + if (((flag & ~FLAG_MASK) != 0) || (flag == 0)) { + isc_throw(InvalidOption4ClientFqdnFlags, "invalid DHCPv4 Client FQDN" + << " Option flag " << std::hex + << static_cast(flag) << std::dec + << "is being set. Expected combination of E, N, S and O"); + } + + // Copy the current flags into local variable. That way we will be able + // to test new flags settings before applying them. + uint8_t new_flag = impl_->flags_; + if (set_flag) { + new_flag |= flag; + } else { + new_flag &= ~flag; + } + + // Check new flags. If they are valid, apply them. + Option4ClientFqdnImpl::checkFlags(new_flag); + impl_->flags_ = new_flag; +} + +void +Option4ClientFqdn::setRcode(const Rcode& rcode) { + impl_->rcode1_ = rcode; + impl_->rcode2_ = rcode; +} + +void +Option4ClientFqdn::resetFlags() { + impl_->flags_ = 0; +} + +std::string +Option4ClientFqdn::getDomainName() const { + if (impl_->domain_name_) { + return (impl_->domain_name_->toText(impl_->domain_name_type_ == + PARTIAL)); + } + // If an object holding domain-name is NULL it means that the domain-name + // is empty. + return (""); +} + +void +Option4ClientFqdn::packDomainName(isc::util::OutputBuffer& buf) const { + // Domain name, encoded as a set of labels. + isc::dns::LabelSequence labels(*impl_->domain_name_); + if (labels.getDataLength() > 0) { + size_t read_len = 0; + const uint8_t* data = labels.getData(&read_len); + if (impl_->domain_name_type_ == PARTIAL) { + --read_len; + } + buf.writeData(data, read_len); + } +} + +void +Option4ClientFqdn::setDomainName(const std::string& domain_name, + const DomainNameType domain_name_type) { + impl_->setDomainName(domain_name, domain_name_type); +} + +void +Option4ClientFqdn::resetDomainName() { + setDomainName("", PARTIAL); +} + +Option4ClientFqdn::DomainNameType +Option4ClientFqdn::getDomainNameType() const { + return (impl_->domain_name_type_); +} + +void +Option4ClientFqdn::pack(isc::util::OutputBuffer& buf) { + // Header = option code and length. + packHeader(buf); + // Flags field. + buf.writeUint8(impl_->flags_); + // RCODE1 and RCODE2 + buf.writeUint8(impl_->rcode1_.getCode()); + buf.writeUint8(impl_->rcode2_.getCode()); + // Domain name. + packDomainName(buf); +} + +void +Option4ClientFqdn::unpack(OptionBufferConstIter first, + OptionBufferConstIter last) { + setData(first, last); + impl_->parseWireData(first, last); +} + +std::string +Option4ClientFqdn::toText(int indent) { + std::ostringstream stream; + std::string in(indent, ' '); // base indentation + std::string in_add(2, ' '); // second-level indentation is 2 spaces long + stream << in << "type=" << type_ << "(CLIENT_FQDN)" << std::endl + << in << "flags:" << std::endl + << in << in_add << "N=" << (getFlag(FLAG_N) ? "1" : "0") << std::endl + << in << in_add << "E=" << (getFlag(FLAG_E) ? "1" : "0") << std::endl + << in << in_add << "O=" << (getFlag(FLAG_O) ? "1" : "0") << std::endl + << in << in_add << "S=" << (getFlag(FLAG_S) ? "1" : "0") << std::endl + << in << "domain-name='" << getDomainName() << "' (" + << (getDomainNameType() == PARTIAL ? "partial" : "full") + << ")" << std::endl; + + return (stream.str()); +} + +uint16_t +Option4ClientFqdn::len() { + // If domain name is partial, the NULL terminating character + // is not included and the option length have to be adjusted. + uint16_t domain_name_length = impl_->domain_name_type_ == FULL ? + impl_->domain_name_->getLength() : impl_->domain_name_->getLength() - 1; + + return (getHeaderLen() + FIXED_FIELDS_LEN + domain_name_length); +} + +} // end of isc::dhcp namespace +} // end of isc namespace diff --git a/src/lib/dhcp/option4_client_fqdn.h b/src/lib/dhcp/option4_client_fqdn.h new file mode 100644 index 0000000000..b02736df88 --- /dev/null +++ b/src/lib/dhcp/option4_client_fqdn.h @@ -0,0 +1,329 @@ +// Copyright (C) 2013 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 OPTION4_CLIENT_FQDN_H +#define OPTION4_CLIENT_FQDN_H + +#include +#include + +#include + +namespace isc { +namespace dhcp { + +/// @brief Exception thrown when invalid flags have been specified for +/// DHCPv4 Client FQDN %Option. +class InvalidOption4ClientFqdnFlags : public Exception { +public: + InvalidOption4ClientFqdnFlags(const char* file, size_t line, const char* what) : + isc::Exception(file, line, what) {} +}; + +/// @brief Exception thrown when invalid domain name is specified. +class InvalidOption4ClientFqdnDomainName : public Exception { +public: + InvalidOption4ClientFqdnDomainName(const char* file, size_t line, + const char* what) : + isc::Exception(file, line, what) {} +}; + +/// Forward declaration to implementation of @c Option4ClientFqdn class. +class Option4ClientFqdnImpl; + +/// @brief Represents DHCPv4 Client FQDN %Option (code 81). +/// +/// This option has been defined in the RFC 4702 and it has a following +/// structure: +/// - Code (1 octet) - option code (always equal to 81). +/// - Len (1 octet) - a length of the option. +/// - Flags (1 octet) - a field carrying "NEOS" flags described below. +/// - RCODE1 (1 octet) - deprecated field which should be set to 0 by the client +/// and set to 255 by the server. +/// - RCODE2 (1 octet) - deprecated, should be used in the same way as RCODE1. +/// - Domain Name - variable length field comprising partial or fully qualified +/// domain name. +/// +/// The flags field has the following structure: +/// @code +/// 0 1 2 3 4 5 6 7 +/// +-+-+-+-+-+-+-+-+ +/// | MBZ |N|E|O|S| +/// +-+-+-+-+-+-+-+-+ +/// @endcode +/// where: +/// - N flag specifies whether server should (0) or should not (1) perform DNS +/// Update, +/// - E flag indicates encoding of the Domain Name field. If this flag is set to 1 +/// it indicates canonical wire format without compression. 0 indicates the deprecated +/// ASCII format. +/// - O flag is set by the server to indicate that it has overridden client's +/// preference set with the S bit. +/// - S flag specifies whether server should (1) or should not (0) perform +/// forward (FQDN-to-address) updates. +/// +/// This class exposes a set of functions to modify flags and check their +/// correctness. +/// +/// Domain names being carried by DHCPv4 Client Fqdn %Option can be fully +/// qualified or partial. Partial domain names are encoded similar to the +/// fully qualified domain names, except that they lack terminating zero +/// at the end of their wire representation. It is also accepted to create an +/// instance of this option which has empty domain-name. Clients use empty +/// domain-names to indicate that server should generate complete fully +/// qualified domain-name. +/// +/// RFC 4702 mandates that the DHCP client sets RCODE1 and RCODE2 to 0 and that +/// server sets them to 255. This class allows to set the value for these +/// fields and both fields are always set to the same value. There is no way +/// to set them separately (e.g. set different value for RCODE1 and RCODE2). +/// However, there are no use cases which would require it. +/// +/// Design choice: This class uses pimpl idiom to separate the interface +/// from implementation specifics. Implementations may use different approaches +/// to handle domain names (mostly validation of the domain-names). The existing +/// @c isc::dns::Name class is a natural (and the simplest) choice to handle +/// domain-names. Use of this class however, implies that libdhcp must be linked +/// with libdns. At some point these libraries may need to be separated, i.e. to +/// support compilation and use of standalone DHCP server. This will require +/// that the part of implementation which deals with domain-names is modified to +/// not use classes from libdns. These changes will be transparent for this +/// interface. +class Option4ClientFqdn : public Option { +public: + + /// @brief Enumeration holding different flags used in the Client + /// FQDN %Option. + enum Flag { + FLAG_S = 0x01, + FLAG_O = 0x02, + FLAG_E = 0x04, + FLAG_N = 0x08 + }; + + /// @brief Represents the value of one of the RCODE1 or RCODE2 fields. + /// + /// Typically, RCODE values are set to 255 by the server and to 0 by the + /// clients (as per RFC 4702). + class Rcode { + public: + Rcode(const uint8_t rcode) + : rcode_(rcode) { } + + /// @brief Returns the value of the RCODE. + /// + /// Returned value can be directly used to create the on-wire format + /// of the DHCPv4 Client FQDN %Option. + uint8_t getCode() const { + return (rcode_); + } + + private: + uint8_t rcode_; + }; + + + /// @brief Type of the domain-name: partial or full. + enum DomainNameType { + PARTIAL, + FULL + }; + + /// @brief Mask which zeroes MBZ flag bits. + static const uint8_t FLAG_MASK = 0xF; + + /// @brief The size of the fixed fields within DHCPv4 Client Fqdn %Option. + /// + /// The fixed fields are: + /// - Flags + /// - RCODE1 + /// - RCODE2 + static const uint16_t FIXED_FIELDS_LEN = 3; + + /// @brief Constructor, creates option instance using flags and domain name. + /// + /// This constructor is used to create instance of the option which will be + /// included in outgoing messages. + /// + /// @param flag a combination of flags to be stored in flags field. + /// @param rcode @c Rcode object representing a value for RCODE1 and RCODE2 + /// fields of the option. Both fields are assigned the same value encapsulated + /// by the parameter. + /// @param domain_name a name to be stored in the domain-name field. + /// @param partial_domain_name indicates if the domain name is partial + /// (if true) or full (false). + explicit Option4ClientFqdn(const uint8_t flag, + const Rcode& rcode, + const std::string& domain_name, + const DomainNameType domain_name_type = FULL); + + /// @brief Constructor, creates option instance with empty domain name. + /// + /// This constructor creates an instance of the option with empty + /// domain-name. This domain-name is marked partial. + /// + /// @param flag a combination of flags to be stored in flags field. + /// @param rcode @c Rcode object representing a value for RCODE1 and RCODE2 + /// fields. Both fields are assigned the same value encapsulated by this + /// parameter. + Option4ClientFqdn(const uint8_t flag, const Rcode& rcode); + + /// @brief Constructor, creates an option instance from part of the buffer. + /// + /// This constructor is mainly used to parse options in the received + /// messages. Function parameters specify buffer bounds from which the + /// option should be created. The size of the buffer chunk, specified by + /// the constructor's parameters should be equal or larger than the size + /// of the option. Otherwise, constructor will throw an exception. + /// + /// @param first the lower bound of the buffer to create option from. + /// @param last the upper bound of the buffer to create option from. + explicit Option4ClientFqdn(OptionBufferConstIter first, + OptionBufferConstIter last); + + /// @brief Copy constructor + Option4ClientFqdn(const Option4ClientFqdn& source); + + /// @brief Destructor + virtual ~Option4ClientFqdn(); + + /// @brief Assignment operator + Option4ClientFqdn& operator=(const Option4ClientFqdn& source); + + /// @brief Checks if the specified flag of the DHCPv4 Client FQDN %Option + /// is set. + /// + /// @param flag an enum value specifying the flag to be checked. + /// + /// @return true if the bit of the specified flag is set, false otherwise. + bool getFlag(const Flag flag) const; + + /// @brief Modifies the value of the specified DHCPv4 Client Fqdn %Option + /// flag. + /// + /// @param flag an enum value specifying the flag to be modified. + /// @param set a boolean value which indicates whether flag should be + /// set (true), or cleared (false). + void setFlag(const Flag flag, const bool set); + + /// @brief Sets the flag field value to 0. + void resetFlags(); + + /// @brief Set Rcode value. + /// + /// @param rcode An @c Rcode object representing value of RCODE1 and RCODE2. + /// Both fields are assigned the same value. + void setRcode(const Rcode& rcode); + + /// @brief Returns the domain-name in the text format. + /// + /// If domain-name is partial, it lacks the dot at the end (e.g. myhost). + /// If domain-name is fully qualified, it has the dot at the end (e.g. + /// myhost.example.com.). + /// + /// @return domain-name in the text format. + std::string getDomainName() const; + + /// @brief Writes domain-name in the wire format into a buffer. + /// + /// The data being written are appended at the end of the buffer. + /// + /// @param [out] buf buffer where domain-name will be written. + void packDomainName(isc::util::OutputBuffer& buf) const; + + /// @brief Set new domain-name. + /// + /// @param domain_name domain name field value in the text format. + /// @param domain_name_type type of the domain name: partial or fully + /// qualified. + void setDomainName(const std::string& domain_name, + const DomainNameType domain_name_type); + + /// @brief Set empty domain-name. + /// + /// This function is equivalent to @c Option6ClientFqdn::setDomainName + /// with empty partial domain-name. It is exception safe. + void resetDomainName(); + + /// @brief Returns enumerator value which indicates whether domain-name is + /// partial or full. + /// + /// @return An enumerator value indicating whether domain-name is partial + /// or full. + DomainNameType getDomainNameType() const; + + /// @brief Writes option in the wire format into a buffer. + /// + /// @param [out] buf output buffer where option data will be stored. + virtual void pack(isc::util::OutputBuffer& buf); + + /// @brief Parses option from the received buffer. + /// + /// Method creates an instance of the DHCPv4 Client FQDN %Option from the + /// wire format. Parameters specify the bounds of the buffer to read option + /// data from. The size of the buffer limited by the specified parameters + /// should be equal or larger than size of the option (including its + /// header). Otherwise exception will be thrown. + /// + /// @param first lower bound of the buffer to parse option from. + /// @param last upper bound of the buffer to parse option from. + virtual void unpack(OptionBufferConstIter first, + OptionBufferConstIter last); + + /// @brief Returns string representation of the option. + /// + /// The string returned by the method comprises the bit value of each + /// option flag and the domain-name. + /// + /// @param indent number of spaces before printed text. + /// + /// @return string with text representation. + virtual std::string toText(int indent = 0); + + /// @brief Returns length of the complete option (data length + + /// DHCPv4 option header). + /// + /// @return length of the option. + virtual uint16_t len(); + + /// + /// @name Well known Rcode declarations for DHCPv4 Client FQDN %Option + /// + //@{ + /// @brief Rcode being set by the server. + inline static const Rcode& RCODE_SERVER() { + static Rcode rcode(255); + return (rcode); + } + + /// @brief Rcode being set by the client. + inline static const Rcode& RCODE_CLIENT() { + static Rcode rcode(0); + return (rcode); + } + //@} + +private: + + /// @brief A pointer to the implementation. + Option4ClientFqdnImpl* impl_; +}; + +/// A pointer to the @c Option4ClientFqdn object. +typedef boost::shared_ptr Option4ClientFqdnPtr; + +} // namespace isc::dhcp +} // namespace isc + +#endif // OPTION4_CLIENT_FQDN_H diff --git a/src/lib/dhcp/tests/Makefile.am b/src/lib/dhcp/tests/Makefile.am index 0216a0bf17..84e99defc6 100644 --- a/src/lib/dhcp/tests/Makefile.am +++ b/src/lib/dhcp/tests/Makefile.am @@ -31,6 +31,7 @@ libdhcp___unittests_SOURCES += hwaddr_unittest.cc libdhcp___unittests_SOURCES += iface_mgr_unittest.cc libdhcp___unittests_SOURCES += libdhcp++_unittest.cc libdhcp___unittests_SOURCES += option4_addrlst_unittest.cc +libdhcp___unittests_SOURCES += option4_client_fqdn_unittest.cc libdhcp___unittests_SOURCES += option6_addrlst_unittest.cc libdhcp___unittests_SOURCES += option6_ia_unittest.cc libdhcp___unittests_SOURCES += option6_iaaddr_unittest.cc diff --git a/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc b/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc new file mode 100644 index 0000000000..c53bda45b5 --- /dev/null +++ b/src/lib/dhcp/tests/option4_client_fqdn_unittest.cc @@ -0,0 +1,756 @@ +// Copyright (C) 2013 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. + +#include +#include +#include +#include +#include +#include + +namespace { + +using namespace isc; +using namespace isc::dhcp; + +// Redefine option flags here as uint8_t. They will be used to initialize +// elements of the arrays that are used in tests below. Note that use of +// enum values defined in Option4ClientFqdn class may cause compilation issues +// during uint8_t arrays initialization. That is because the underlying +// integral type used to represent enums is larger than one byte. +const uint8_t FLAG_S = 0x01; +const uint8_t FLAG_O = 0x02; +const uint8_t FLAG_E = 0x04; +const uint8_t FLAG_N = 0x08; + +// This test verifies that constructor accepts empty partial domain-name but +// does not accept empty fully qualified domain name. +TEST(Option4ClientFqdnTest, constructEmptyName) { + // Create an instance of the source option. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_SERVER(), + "", + Option4ClientFqdn::PARTIAL)) + ); + ASSERT_TRUE(option); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_TRUE(option->getDomainName().empty()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); + + // Constructor should not accept empty fully qualified domain name. + EXPECT_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_CLIENT(), + "", + Option4ClientFqdn::FULL), + InvalidOption4ClientFqdnDomainName); + // This check is similar to previous one, but using domain-name comprising + // a single space character. This should be treated as empty domain-name. + EXPECT_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_CLIENT(), + " ", + Option4ClientFqdn::FULL), + InvalidOption4ClientFqdnDomainName); + + // Try different constructor. + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_SERVER())) + ); + ASSERT_TRUE(option); + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_TRUE(option->getDomainName().empty()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); +} + +// This test verifies that copy constructor makes a copy of the option and +// the source option instance can be deleted (both instances don't share +// any resources). +TEST(Option4ClientFqdnTest, copyConstruct) { + // Create an instance of the source option. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_SERVER(), + "myhost.example.com", + Option4ClientFqdn::FULL)) + ); + ASSERT_TRUE(option); + + // Use copy constructor to create a second instance of the option. + boost::scoped_ptr option_copy; + ASSERT_NO_THROW( + option_copy.reset(new Option4ClientFqdn(*option)) + ); + ASSERT_TRUE(option_copy); + + // Copy construction should result in no shared resources between + // two objects. In particular, pointer to implementation should not + // be shared. Thus, we can release the source object now. + option.reset(); + + // Verify that all parameters have been copied to the target object. + EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_EQ("myhost.example.com.", option_copy->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, option_copy->getDomainNameType()); + + // Do another test with different parameters to verify that parameters + // change when copied object is changed. + + // Create an option with different parameters. + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_SERVER(), + "example", + Option4ClientFqdn::PARTIAL)) + ); + ASSERT_TRUE(option); + + // Call copy-constructor to copy the option. + ASSERT_NO_THROW( + option_copy.reset(new Option4ClientFqdn(*option)) + ); + ASSERT_TRUE(option_copy); + + option.reset(); + + EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_TRUE(option_copy->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option_copy->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_EQ("example", option_copy->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option_copy->getDomainNameType()); +} + +// This test verifies that the option in the on-wire format is parsed correctly. +TEST(Option4ClientFqdnTest, constructFromWire) { + const uint8_t in_data[] = { + FLAG_S, // flags + 0, // RCODE1 + 0, // RCODE2 + 6, 109, 121, 104, 111, 115, 116, // myhost. + 7, 101, 120, 97, 109, 112, 108, 101, // example. + 3, 99, 111, 109, 0 // com. + }; + size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]); + OptionBuffer in_buf(in_data, in_data + in_data_size); + + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end())) + ); + ASSERT_TRUE(option); + + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_EQ("myhost.example.com.", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); +} + +// This test verifies that truncated option is rejected. +TEST(Option4ClientFqdnTest, constructFromWireTruncated) { + // Empty buffer is invalid. It should be at least one octet long. + OptionBuffer in_buf; + for (uint8_t i = 0; i < 3; ++i) { + EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()), + OutOfRange) << "Test of the truncated buffer failed for" + << " buffer length " << static_cast(i); + in_buf.push_back(0); + } + + // Buffer is now 3 bytes long, so it should not fail now. + EXPECT_NO_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end())); +} + +// This test verifies that the option in the on-wire format with partial +// domain-name is parsed correctly. +TEST(Option4ClientFqdnTest, constructFromWirePartial) { + const uint8_t in_data[] = { + FLAG_N, // flags + 255, // RCODE1 + 255, // RCODE2 + 6, 109, 121, 104, 111, 115, 116 // myhost + }; + size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]); + OptionBuffer in_buf(in_data, in_data + in_data_size); + + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end())) + ); + ASSERT_TRUE(option); + + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_EQ("myhost", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); +} + +// This test verifies that the option in the on-wire format with empty +// domain-name is parsed correctly. +TEST(Option4ClientFqdnTest, constructFromWireEmpty) { + // Initialize the 3-byte long buffer. All bytes initialized to 0: + // Flags, RCODE1 and RCODE2. + OptionBuffer in_buf(3, 0); + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(in_buf.begin(), in_buf.end())) + ); + ASSERT_TRUE(option); + + // domain-name field should be empty because on-wire data comprised + // flags field only. + EXPECT_TRUE(option->getDomainName().empty()); +} + +// This test verifies that assignment operator can be used to assign one +// instance of the option to another. +TEST(Option4ClientFqdnTest, assignment) { + // Usually the smart pointer is used to declare options and call + // constructor within assert. Thanks to this approach, the option instance + // is in the function scope and only initialization is done within assert. + // In this particular test we can't use smart pointers because we are + // testing assignment operator like this: + // + // option2 = option; + // + // The two asserts below do not create the instances that we will used to + // test assignment. They just attempt to create instances of the options + // with the same parameters as those that will be created for the actual + // assignment test. If these asserts do not fail, we can create options + // for the assignment test, do not surround them with asserts and be sure + // they will not throw. + ASSERT_NO_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_SERVER(), + "myhost.example.com", + Option4ClientFqdn::FULL)); + + ASSERT_NO_THROW(Option4ClientFqdn(Option4ClientFqdn::FLAG_N, + Option4ClientFqdn::RCODE_SERVER(), + "myhost", + Option4ClientFqdn::PARTIAL)); + + // Create options with the same parameters as tested above. + + // Create first option. + Option4ClientFqdn option(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_SERVER(), + "myhost.example.com", + Option4ClientFqdn::FULL); + + // Verify that the values have been set correctly. + ASSERT_TRUE(option.getFlag(Option4ClientFqdn::FLAG_S)); + ASSERT_FALSE(option.getFlag(Option4ClientFqdn::FLAG_O)); + ASSERT_FALSE(option.getFlag(Option4ClientFqdn::FLAG_N)); + ASSERT_EQ("myhost.example.com.", option.getDomainName()); + ASSERT_EQ(Option4ClientFqdn::FULL, option.getDomainNameType()); + + // Create a second option. + Option4ClientFqdn option2(Option4ClientFqdn::FLAG_N, + Option4ClientFqdn::RCODE_SERVER(), + "myhost", + Option4ClientFqdn::PARTIAL); + + // Verify tha the values have been set correctly. + ASSERT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_S)); + ASSERT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_O)); + ASSERT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_N)); + ASSERT_EQ("myhost", option2.getDomainName()); + ASSERT_EQ(Option4ClientFqdn::PARTIAL, option2.getDomainNameType()); + + + // Make the assignment. + ASSERT_NO_THROW(option2 = option); + + // Both options should now have the same values. + EXPECT_TRUE(option2.getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option2.getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_EQ(option.getDomainName(), option2.getDomainName()); + EXPECT_EQ(option.getDomainNameType(), option2.getDomainNameType()); +} + +// This test verifies that constructor will throw an exception if invalid +// DHCPv6 Client FQDN Option flags are specified. +TEST(Option4ClientFqdnTest, constructInvalidFlags) { + // First, check that constructor does not throw an exception when + // valid flags values are provided. That way we eliminate the issue + // that constructor always throws exception. + uint8_t flags = 0; + ASSERT_NO_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")); + + // Invalid flags: The maximal value is 0xF when all flag bits are set + // (00001111b). The flag value of 0x18 sets the bit from the Must Be + // Zero (MBZ) bitset (00011000b). + flags = 0x18; + EXPECT_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com"), + InvalidOption4ClientFqdnFlags); + + // According to RFC 4702, section 2.1. if the N bit is set the S bit MUST + // be zero. If both are set, constructor is expected to throw. + flags = Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_S; + EXPECT_THROW(Option4ClientFqdn(flags, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com"), + InvalidOption4ClientFqdnFlags); +} + +// This test verifies that constructor which parses option from on-wire format +// will throw exception if parsed flags field is invalid. +TEST(Option4ClientFqdnTest, constructFromWireInvalidFlags) { + // Create a buffer which holds flags field only. Set valid flag field at + // at first to make sure that constructor doesn't always throw an exception. + OptionBuffer in_buf(3, 0); + in_buf[0] = FLAG_S; + ASSERT_NO_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end())); + + // Replace the flags with invalid value and verify that constructor throws + // appropriate exception. + in_buf[0] = FLAG_N | FLAG_S; + EXPECT_THROW(Option4ClientFqdn(in_buf.begin(), in_buf.end()), + InvalidOption4ClientFqdnFlags); +} + +// This test verifies that if invalid domain name is used the constructor +// will throw appropriate exception. +TEST(Option4ClientFqdnTest, constructInvalidName) { + // First, check that constructor does not throw when valid domain name + // is specified. That way we eliminate the possibility that constructor + // always throws exception. + ASSERT_NO_THROW(Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")); + + // Specify invalid domain name and expect that exception is thrown. + EXPECT_THROW(Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "my...host.example.com"), + InvalidOption4ClientFqdnDomainName); +} + +// This test verifies that getFlag throws an exception if flag value other +// than explicitly defined in the Option4ClientFqdn::Flag is spcified. +TEST(Option4ClientFqdnTest, getFlag) { + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + + // The 0x3 is a valid enumerator value (even though it is not explicitly + // included in the Option4ClientFqdn::Flag definition). The getFlag() + // function should not accept it. Only explicit values are accepted. + EXPECT_THROW(option->getFlag(static_cast(0x3)), + InvalidOption4ClientFqdnFlags); +} + +// This test verifies that flags can be modified and that incorrect flags +// are rejected. +TEST(Option4ClientFqdnTest, setFlag) { + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + + // All flags should be set to 0 initially. + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + + // Set N = 1 + ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, true)); + ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_N)); + + // Set O = 1 + ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_O, true)); + ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O)); + + // Set S = 1, this should throw exception because S and N must not + // be set in the same time. + ASSERT_THROW(option->setFlag(Option4ClientFqdn::FLAG_S, true), + InvalidOption4ClientFqdnFlags); + + // Set N = 0 + ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, false)); + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + + // Set S = 1, this should not result in exception because N has been + // cleared. + ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_S, true)); + ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + + // Set N = 1, this should result in exception because S = 1 + ASSERT_THROW(option->setFlag(Option4ClientFqdn::FLAG_N, true), + InvalidOption4ClientFqdnFlags); + + // Set O = 0 + ASSERT_NO_THROW(option->setFlag(Option4ClientFqdn::FLAG_O, false)); + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + + // Try out of bounds settings. + uint8_t flags = 0; + ASSERT_THROW(option->setFlag(static_cast(flags), + true), + InvalidOption4ClientFqdnFlags); + + flags = 0x14; + ASSERT_THROW(option->setFlag(static_cast(flags), + true), + InvalidOption4ClientFqdnFlags); +} + +// This test verifies that flags field of the option is set to 0 when resetFlags +// function is called. +TEST(Option4ClientFqdnTest, resetFlags) { + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S | + Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com", + Option4ClientFqdn::FULL)) + ); + ASSERT_TRUE(option); + + // Check that flags we set in the constructor are set. + ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + ASSERT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O)); + ASSERT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + + option->resetFlags(); + + // After reset, all flags should be 0. + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); +} + +// This test verifies that current domain-name can be replaced with a new +// domain-name. +TEST(Option4ClientFqdnTest, setDomainName) { + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_SERVER(), + "myhost.example.com", + Option4ClientFqdn::FULL)) + ); + ASSERT_TRUE(option); + ASSERT_EQ("myhost.example.com.", option->getDomainName()); + ASSERT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); + + // Partial domain-name. + ASSERT_NO_THROW(option->setDomainName("myhost", + Option4ClientFqdn::PARTIAL)); + EXPECT_EQ("myhost", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); + + // Fully qualified domain-name. + ASSERT_NO_THROW(option->setDomainName("example.com", + Option4ClientFqdn::FULL)); + EXPECT_EQ("example.com.", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); + + // Empty domain name (partial). This should be successful. + ASSERT_NO_THROW(option->setDomainName("", Option4ClientFqdn::PARTIAL)); + EXPECT_TRUE(option->getDomainName().empty()); + + // Fully qualified domain-names must not be empty. + EXPECT_THROW(option->setDomainName("", Option4ClientFqdn::FULL), + InvalidOption4ClientFqdnDomainName); + EXPECT_THROW(option->setDomainName(" ", Option4ClientFqdn::FULL), + InvalidOption4ClientFqdnDomainName); +} + +// This test verifies that current domain-name can be reset to empty one. +TEST(Option4ClientFqdnTest, resetDomainName) { + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_S, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com", + Option4ClientFqdn::FULL)) + ); + ASSERT_TRUE(option); + ASSERT_EQ("myhost.example.com.", option->getDomainName()); + ASSERT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); + + // Set the domain-name to empty one. + ASSERT_NO_THROW(option->resetDomainName()); + EXPECT_TRUE(option->getDomainName().empty()); +} + +// This test verifies on-wire format of the option is correctly created. +TEST(Option4ClientFqdnTest, pack) { + // Create option instance. Check that constructor doesn't throw. + const uint8_t flags = Option4ClientFqdn::FLAG_S; + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(flags, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + + // Prepare on-wire format of the option. + isc::util::OutputBuffer buf(10); + ASSERT_NO_THROW(option->pack(buf)); + + // Prepare reference data. + const uint8_t ref_data[] = { + 81, 23, // header + FLAG_S, // flags + 0, // RCODE1 + 0, // RCODE2 + 6, 109, 121, 104, 111, 115, 116, // myhost. + 7, 101, 120, 97, 109, 112, 108, 101, // example. + 3, 99, 111, 109, 0 // com. + }; + size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]); + + // Check if the buffer has the same length as the reference data, + // so as they can be compared directly. + ASSERT_EQ(ref_data_size, buf.getLength()); + EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength())); +} + +// This test verifies on-wire format of the option with partial domain name +// is correctly created. +TEST(Option4ClientFqdnTest, packPartial) { + // Create option instance. Check that constructor doesn't throw. + const uint8_t flags = Option4ClientFqdn::FLAG_S; + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(flags, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost", + Option4ClientFqdn::PARTIAL)) + ); + ASSERT_TRUE(option); + + // Prepare on-wire format of the option. + isc::util::OutputBuffer buf(10); + ASSERT_NO_THROW(option->pack(buf)); + + // Prepare reference data. + const uint8_t ref_data[] = { + 81, 10, // header + FLAG_S, // flags + 0, // RCODE1 + 0, // RCODE2 + 6, 109, 121, 104, 111, 115, 116 // myhost + }; + size_t ref_data_size = sizeof(ref_data) / sizeof(ref_data[0]); + + // Check if the buffer has the same length as the reference data, + // so as they can be compared directly. + ASSERT_EQ(ref_data_size, buf.getLength()); + EXPECT_EQ(0, memcmp(ref_data, buf.getData(), buf.getLength())); +} + +// This test verifies that on-wire option data holding fully qualified domain +// name is parsed correctly. +TEST(Option4ClientFqdnTest, unpack) { + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost", + Option4ClientFqdn::PARTIAL)) + ); + ASSERT_TRUE(option); + // Make sure that the parameters have been set correctly. Later in this + // test we will check that they will be replaced with new values when + // unpack is called. + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_EQ("myhost", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); + + const uint8_t in_data[] = { + FLAG_S, // flags + 0, // RCODE1 + 0, // RCODE2 + 6, 109, 121, 104, 111, 115, 116, // myhost. + 7, 101, 120, 97, 109, 112, 108, 101, // example. + 3, 99, 111, 109, 0 // com. + }; + size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]); + OptionBuffer in_buf(in_data, in_data + in_data_size); + + // Initialize new values from the on-wire format. + ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end())); + + // Check that new values are correct. + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_EQ("myhost.example.com.", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); +} + +// This test verifies that on-wire option data holding partial domain name +// is parsed correctly. +TEST(Option4ClientFqdnTest, unpackPartial) { + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + // Make sure that the parameters have been set correctly. Later in this + // test we will check that they will be replaced with new values when + // unpack is called. + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_EQ("myhost.example.com.", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::FULL, option->getDomainNameType()); + + const uint8_t in_data[] = { + FLAG_S, // flags + 0, // RCODE1 + 0, // RCODE2 + 6, 109, 121, 104, 111, 115, 116 // myhost + }; + size_t in_data_size = sizeof(in_data) / sizeof(in_data[0]); + OptionBuffer in_buf(in_data, in_data + in_data_size); + + // Initialize new values from the on-wire format. + ASSERT_NO_THROW(option->unpack(in_buf.begin(), in_buf.end())); + + // Check that new values are correct. + EXPECT_TRUE(option->getFlag(Option4ClientFqdn::FLAG_S)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_N)); + EXPECT_FALSE(option->getFlag(Option4ClientFqdn::FLAG_O)); + EXPECT_EQ("myhost", option->getDomainName()); + EXPECT_EQ(Option4ClientFqdn::PARTIAL, option->getDomainNameType()); +} + +// This test verifies that the empty buffer is rejected when decoding an option +// from on-wire format. +TEST(Option4ClientFqdnTest, unpackTruncated) { + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(Option4ClientFqdn::FLAG_O, + Option4ClientFqdn::RCODE_CLIENT())) + ); + ASSERT_TRUE(option); + + // Empty buffer is invalid. It should be at least 1 octet long. + OptionBuffer in_buf; + EXPECT_THROW(option->unpack(in_buf.begin(), in_buf.end()), OutOfRange); +} + +// This test verifies that string representation of the option returned by +// toText method is correctly formatted. +TEST(Option4ClientFqdnTest, toText) { + // Create option instance. Check that constructor doesn't throw. + uint8_t flags = Option4ClientFqdn::FLAG_N | Option4ClientFqdn::FLAG_O; + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(flags, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + + // The base indentation of the option will be set to 2. It should appear + // as follows. + std::string ref_string = + " type=81(CLIENT_FQDN)\n" + " flags:\n" + " N=1\n" + " E=0\n" + " O=1\n" + " S=0\n" + " domain-name='myhost.example.com.' (full)\n"; + const int indent = 2; + EXPECT_EQ(ref_string, option->toText(indent)); + + // Create another option with different parameters: + // - flags set to 0 + // - domain-name is now partial, not fully qualified + // Also, remove base indentation. + flags = 0; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(flags, + Option4ClientFqdn::RCODE_CLIENT(), + "myhost", + Option4ClientFqdn::PARTIAL)) + ); + ref_string = + "type=81(CLIENT_FQDN)\n" + "flags:\n" + " N=0\n" + " E=0\n" + " O=0\n" + " S=0\n" + "domain-name='myhost' (partial)\n"; + EXPECT_EQ(ref_string, option->toText()); +} + +// This test verifies that the correct length of the option in on-wire +// format is returned. +TEST(Option4ClientFqdnTest, len) { + // Create option instance. Check that constructor doesn't throw. + boost::scoped_ptr option; + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "myhost.example.com")) + ); + ASSERT_TRUE(option); + // This option comprises a header (2 octets), flag field (1 octet), + // RCODE1 and RCODE2 (2 octets) and wire representation of the + // domain name (length equal to the length of the string representation + // of the domain name + 1). + EXPECT_EQ(25, option->len()); + + // Let's check that the size will change when domain name of a different + // size is used. + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "example.com")) + ); + ASSERT_TRUE(option); + EXPECT_EQ(18, option->len()); + + ASSERT_NO_THROW( + option.reset(new Option4ClientFqdn(0, Option4ClientFqdn::RCODE_CLIENT(), + "myhost", + Option4ClientFqdn::PARTIAL)) + ); + ASSERT_TRUE(option); + EXPECT_EQ(12, option->len()); + } + +} // anonymous namespace