diff options
author | ADAM David Alan Martin <adam.martin@10gen.com> | 2018-06-26 15:25:32 -0400 |
---|---|---|
committer | ADAM David Alan Martin <adam.martin@10gen.com> | 2018-06-26 16:35:39 -0400 |
commit | 181c43bd006666b07441bb3be61b7324ef7dcc80 (patch) | |
tree | 6b9b232b0d0af97adff4f73d4d732909479592a7 /src/mongo/util/dns_name.h | |
parent | 5abdd989fbf8f21f9fc01addaf45e661ec793c81 (diff) | |
download | mongo-181c43bd006666b07441bb3be61b7324ef7dcc80.tar.gz |
SERVER-34563 Handle DNS names correctly in SRV record processing.
The current implementation of DNS name processing uses raw string
processing. This change alters the mechanism to use a proper DNS
name type which parses the hostname for proper processing.
Diffstat (limited to 'src/mongo/util/dns_name.h')
-rw-r--r-- | src/mongo/util/dns_name.h | 451 |
1 files changed, 451 insertions, 0 deletions
diff --git a/src/mongo/util/dns_name.h b/src/mongo/util/dns_name.h new file mode 100644 index 00000000000..cb4533b03ed --- /dev/null +++ b/src/mongo/util/dns_name.h @@ -0,0 +1,451 @@ +/** + * Copyright (C) 2018 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects for + * all of the code used other than as permitted herein. If you modify file(s) + * with this exception, you may extend this exception to your version of the + * file(s), but you are not obligated to do so. If you do not wish to do so, + * delete this exception statement from your version. If you delete this + * exception statement from all source files in the program, then also delete + * it in the license file. + */ + +#pragma once + +#include <algorithm> +#include <cctype> +#include <iostream> +#include <iterator> +#include <sstream> +#include <tuple> +#include <vector> + +#include "mongo/base/error_codes.h" +#include "mongo/base/string_data.h" +#include "mongo/bson/util/builder.h" +#include "mongo/util/assert_util.h" +#include "mongo/util/mongoutils/str.h" + +namespace mongo { +namespace dns { +namespace detail_dns_host_name { +using std::begin; +using std::end; +using std::rbegin; +using std::rend; + +class HostName; +bool operator==(const HostName& lhs, const HostName& rhs); + +/** + * A `dns::HostName` represents a DNS Hostname in a form which is suitable for programatic + * manipulation. + * + * Oftentimes it is inappropriate to operate on a domain name (DNS Hostname) as a string. Besides + * the obvious limitations and cognitive overhead implied by string processing, there are + * fundamental semantic conventions in the format of a DNS Hostname which need to be handled + * appropriately. This type, `dns::HostName` represents a DNS Hostname in native C++ types and + * provides a set of salient member functions which exhibit the expected behavior and semantics of a + * DNS Hostname. It is encouraged to rewrite code which handles domain names from raw string form + * into code using this type instead. A type which represents and obeys correct DNS Hostname + * semantics will help prevent bugs in name handling and resolution. + */ +class HostName { +public: + /** + * A `dns::HostName` can be either Fully Qualified (FQDN) or a relative name. + * + * Some member functions of `dns::HostName` may behave differently depending upon whether a + * Hostname is fully qualified or not. + */ + enum Qualification : bool { kRelativeName = false, kFullyQualified = true }; + +public: + /** + * Constructs a parsed DNS Hostname representation from the specified string. + * + * A DNS name can be fully qualified (ending in a '.') or unqualified (not ending in a '.'). + * When the specified `dnsName` string ends in a terminating dot (`'.'`) character, the + * constructed `dns::HostName` object will have the `Qualification::kFullyQualified` state, + * otherwise it will have the `Qualification::kRelativeName` state. This constructor will parse + * the specified name, separating it on the dot (`'.'`) tokens for simpler programatic + * processing. + * + * THROWS: `DBException` with `ErrorCodes::DNSRecordTypeMismatch` as the status value if the + * name is ill formatted. + */ + explicit HostName(StringData dnsName) { + if (dnsName.empty()) + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot have zero characters"); + + if (dnsName[0] == '.') + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot start with a '.' character."); + + enum ParserState { kFirstLetter, kNonPeriod, kPeriod }; + ParserState parserState = kFirstLetter; + + std::string name; + int idx = -1; + for (const char& ch : dnsName) { + ++idx; + if (ch == '.') { + if (parserState == kPeriod) { + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot have two adjacent '.' characters"); + } + parserState = kPeriod; + this->_nameComponents.push_back(std::move(name)); + name.clear(); + continue; + } + if (parserState == kPeriod) { + parserState = kFirstLetter; + } + + + invariant(ch != '.'); + + // We permit dashes and numbers. We also permit underscores for use with SRV records + // and such. + if (!(ch == '-' || std::isalnum(ch) || (ch == '_' && parserState == kFirstLetter))) { + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot have tokens other than dash or alphanumerics."); + } + // All domain names are represented in lower-case letters, because DNS is case + // insensitive. + name.push_back(std::tolower(ch)); + if (parserState == kFirstLetter) { + parserState = kNonPeriod; + } + } + + if (parserState == kPeriod) + fullyQualified = kFullyQualified; + else { + fullyQualified = kRelativeName; + _nameComponents.push_back(std::move(name)); + } + + if (_nameComponents.empty()) + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot have zero name elements"); + + checkForValidForm(); + + // Reverse all the names, once we've parsed them all in. + std::reverse(begin(_nameComponents), end(_nameComponents)); + } + + /** + * Returns whether this DNS Hostname has been fully qualified. + * + * A DNS Hostname is considered fully qualified, if the canonical specification of its name + * includes a trailing `'.'`. Fully Qualified Domain Names (FQDNs) are always resolved against + * the root name servers and indicate absolute names. Unqualified names are looked up against + * DNS configuration specific prefixes, recursively, until a match is found, which may not be + * the corresponding FQDN. + * + * RETURNS: True if this hostname is an FQDN and false otherwise. + */ + bool isFQDN() const { + return fullyQualified; + } + + /** + * Changes the qualification of this `dns::HostName` to the specified `qualification`. + * + * An unqualified domain hostname may exist as an artifact of other protocols wherein the actual + * qualification of that name is implied to be complete. When operating on such names in + * `dns::HostName` form, it may be necessary to alter the qualification after the fact. + * + * POST: The qualification of `*this` will be changed to the qualification specified by + * `qualification`. + */ + void forceQualification(const Qualification qualification = kFullyQualified) { + fullyQualified = qualification; + } + + /** + * Returns the complete canonical name for this `dns::HostName`. + * + * The canonical form for a DNS Hostname is the complete dotted DNS path, including a trailing + * dot (if the domain in question is fully qualified). A DNS Hostname which is fully qualified + * (ending in a trailing dot) will not compare equal (in string form) to a DNS Hostname which + * has not been fully qualified. This representation may be unsuitable for some use cases which + * involve relaxed qualification indications. + * + * RETURNS: A `std::string` which represents this DNS Hostname in complete canonical form. + */ + std::string canonicalName() const { + return str::stream() << *this; + } + + /** + * Returns the complete name for this `dns::HostName` in a form suitable for use with SSL + * certificate names. + * + * For myriad reasons, SSL certificates do not specify the fully qualified name of any host. + * When using `dns::HostName` objects in SSL aware code, it may be necessary to get an + * unqualified string form for use in certificate name comparisons. + * + * RETURNS: A `std::string` which represents this Hostname without a trailing dot (`'.'`). + */ + std::string noncanonicalName() const { + StringBuilder sb; + streamUnqualified(sb); + return sb.str(); + } + + /** + * Returns the number of subdomain components in this `dns::HostName`. + * + * A DNS Hostname is composed of at least one, and sometimes more, subdomains. This function + * indicates how many subdomains this `dns::HostName` specifier has. Each subdomain is + * separated by a single `'.'` character. + * + * RETURNS: The number of components in `this->nameComponents()` + */ + std::size_t depth() const { + return this->_nameComponents.size(); + } + + /** + * Returns a new `dns::HostName` object which represents the name of the DNS domain in which + * this object resides. + * + * All domains of depth greater than 1 are composed of multiple sub-domains. This function + * provides the next-level parent of the domain represented by `*this`. + * + * PRE: This `dns::HostName` must have at least two subdomains (`this->depth() > 1`). + * + * NOTE: The behavior of this function is undefined unless its preconditions are met. + * + * RETURNS: A `dns::HostName` which has one fewer domain specified. + */ + HostName parentDomain() const { + if (this->_nameComponents.size() == 1) { + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A top level domain has no subdomains in its name"); + } + HostName result = *this; + result._nameComponents.pop_back(); + return result; + } + + /** + * Returns true if the specified `candidate` Hostname would be resolved within `*this` as a + * hostname and false otherwise. + * + * Two domains can be said to have a "contains" relationship only when when both are Fully + * Qualified Domain Names (FQDNs). When either domain or both domains are unqualified, then it + * is impossible to know whether one could be resolved within the other correctly. + * + * PRE: This `this->isFQDN() && candidate.isFQDN()` must be true. Resolving unqualified names + * against other unqualified names has some implications on what the `contains` relationship + * would indicate. We sidestep those at this time. + * + * THROWS: `DBException` with `ErrorCodes::DNSRecordTypeMismatch` as the status value if + * `!this->isFQDN() || !candidate.isFQDN()`. + * + * RETURNS: False when `!candidate.isFQDN() || !this->isFQDN()`. False when `this->depth() >= + * candidate.depth()`. Otherwise a value equivalent to `[temp = candidate]{ while (temp.depth() + * > this->depth()) temp= temp.parentDomain(); return temp; }() == *this;` + */ + bool contains(const HostName& candidate) const { + if (!this->isFQDN() || !candidate.isFQDN()) { + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "Only FQDNs can be checked for subdomain relationships."); + } + return (_nameComponents.size() < candidate._nameComponents.size()) && + std::equal( + begin(_nameComponents), end(_nameComponents), begin(candidate._nameComponents)); + } + + /** + * Returns a new `dns::HostName` which represents the larger (possibly canonical) name that + * would be used to lookup `*this` within the domain of the specified `rhs`. + * + * Unqualified DNS Hostnames can be prepended to other DNS Hostnames to provide a DNS string + * which is equivalent to what a resolution of the unqualified name would be in the domain of + * the second (possibly qualified) name. + * + * PRE: `this->isFQDN() == false`. + * + * RETURNS: A `dns::HostName` which has a `canonicalName()` equivalent to `this->canonicalName() + * + rhs.canonicalName()`. + * + * THROWS: `DBException` with `ErrorCodes::DNSRecordTypeMismatch` as the status value if + * `this->isFQDN() == false`. + */ + HostName resolvedIn(const HostName& rhs) const { + if (this->fullyQualified) + uasserted( + ErrorCodes::DNSRecordTypeMismatch, + "A fully qualified Domain Name cannot be resolved within another domain name."); + HostName result = rhs; + result._nameComponents.insert( + end(result._nameComponents), begin(this->_nameComponents), end(this->_nameComponents)); + + return result; + } + + /** + * Returns an immutable reference to a `std::vector` of `std::string` which indicates the + * canonical path of this `dns::HostName`. + * + * Sometimes it is necessary to iterate over all of the elements of a domain name string. This + * function facilitates such iteration. + * + * RETURNS: A `const std::vector<std::string>&` which refers to all of the domain name + * components of `*this`. + */ + + const std::vector<std::string>& nameComponents() const& { + return this->_nameComponents; + } + + std::vector<std::string> nameComponents() && { + return std::move(this->_nameComponents); + } + + /** + * Compares two `dns::HostName` objects. + * + * Two `dns::HostName` objects compare equal when they both represent the same resolution path. + * This means that in addition to the lookup sequence (order of sub domains) being the same, the + * qualification of both objects must be the same. For example, `"www.google.com"` would not + * compare equal to `"www.google.com."` due to the presence of a trailing dot in the second + * case. + * + * RETURNS: True if `lhs` and `rhs` represent the same DNS path, and false otherwise. + */ + friend bool operator==(const HostName& lhs, const HostName& rhs); + + /** + * Compares two `dns::HostName` objects. + * + * RETURNS: `!(lhs == rhs)`. + */ + friend bool operator!=(const HostName& lhs, const HostName& rhs) { + return !(lhs == rhs); + } + + /** + * Streams a representation of the specified `hostName` to the specified `os` formatting stream. + * + * A canonical representation of `hostName` (with a trailing dot, `'.'`, when `hostName.isFQDN() + * == true`) will be placed into the formatting stream handled by `os`. + * + * RETURNS: A reference to the specified output stream `os`. + */ + friend std::ostream& operator<<(std::ostream& os, const HostName& hostName) { + if (hostName.fullyQualified) { + hostName.streamQualified(os); + } else { + hostName.streamUnqualified(os); + } + + return os; + } + + friend StringBuilder& operator<<(StringBuilder& os, const HostName& hostName) { + if (hostName.fullyQualified) { + hostName.streamQualified(os); + } else { + hostName.streamUnqualified(os); + } + + return os; + } + +private: + auto make_equality_lens() const { + return std::tie(fullyQualified, _nameComponents); + } + + // When printing fully qualified names to a stream, we need to always append a dot. + template <typename StreamLike> + void streamQualified(StreamLike& os) const { + invariant(fullyQualified); + streamCore(os); + os << '.'; + } + + // When printing unqualified names to a stream, we omit the trailing dot, even if needed. + template <typename StreamLike> + void streamUnqualified(StreamLike& os) const { + streamCore(os); + } + + // All streaming functions boil down into this central handler, for both `StringBuilder` and + // `std::ostream`. + template <typename StreamLike> + void streamCore(StreamLike& os) const { + std::for_each(rbegin(_nameComponents), + rend(_nameComponents), + [ first = true, &os ](const auto& component) mutable { + if (!first) + os << '.'; + first = false; + os << component; + }); + } + + // If there are exactly 4 name components, and they are not fully qualified, then they cannot be + // all numbers. This helper function is used in validating that IPv4 addresses are not passed + // to the constructor of this class. + void checkForValidForm() const { + if (this->_nameComponents.size() != 4) + return; + if (this->fullyQualified) + return; + + for (const auto& name : this->_nameComponents) { + // Any letters are good. A hyphen is okay too. + if (end(name) != std::find_if(begin(name), end(name), [](const char ch) { + return std::isalpha(ch) || ch == '-'; + })) + return; + } + + // If we couldn't find any letters or hyphens + uasserted(ErrorCodes::DNSRecordTypeMismatch, + "A Domain Name cannot be equivalent in form to an IPv4 address"); + } + + // Hostname components are stored in hierarchy order (reverse order from how a name is read by + // humans in text form). + std::vector<std::string> _nameComponents; + + // FQDNs and Relative Names are discriminated by this field. + Qualification fullyQualified; +}; +} // detail_dns_host_name + +// The `operator==` function has to be defined out-of-line, because it uses `make_equality_lens` +// which is an auto-deduced return type function defined later in the class body. +inline bool detail_dns_host_name::operator==(const HostName& lhs, const HostName& rhs) { + return lhs.make_equality_lens() == rhs.make_equality_lens(); +} + +using detail_dns_host_name::HostName; +} // namespace dns +} // namespace mongo |