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 | |
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.
-rw-r--r-- | src/mongo/client/mongo_uri.cpp | 64 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_test.cpp | 151 | ||||
-rw-r--r-- | src/mongo/util/SConscript | 10 | ||||
-rw-r--r-- | src/mongo/util/dns_name.h | 451 | ||||
-rw-r--r-- | src/mongo/util/dns_name_test.cpp | 266 | ||||
-rw-r--r-- | src/mongo/util/dns_query.h | 4 |
6 files changed, 865 insertions, 81 deletions
diff --git a/src/mongo/client/mongo_uri.cpp b/src/mongo/client/mongo_uri.cpp index 91a069eb703..47ea195c362 100644 --- a/src/mongo/client/mongo_uri.cpp +++ b/src/mongo/client/mongo_uri.cpp @@ -45,6 +45,7 @@ #include "mongo/client/sasl_client_authenticate.h" #include "mongo/db/namespace_string.h" #include "mongo/stdx/utility.h" +#include "mongo/util/dns_name.h" #include "mongo/util/dns_query.h" #include "mongo/util/hex.h" #include "mongo/util/mongoutils/str.h" @@ -234,28 +235,12 @@ MongoURI::OptionsMap addTXTOptions(std::map<std::string, std::string> options, return {std::make_move_iterator(begin(options)), std::make_move_iterator(end(options))}; } - -std::string stripHost(const std::string& hostname) { - return hostname.substr(hostname.find('.') + 1); -} - -bool isWithinDomain(std::string hostname, std::string domain) { - auto removeFQDNRoot = [](std::string name) -> std::string { - if (name.back() == '.') { - name.pop_back(); - } - return name; - }; - hostname = stripHost(removeFQDNRoot(std::move(hostname))); - domain = removeFQDNRoot(std::move(domain)); - return hostname == domain; -} } // namespace MongoURI MongoURI::parseImpl(const std::string& url) { const StringData urlSD(url); - // 1. Validate and remove the scheme prefix mongodb:// + // 1. Validate and remove the scheme prefix `mongodb://` or `mongodb+srv://` const bool isSeedlist = urlSD.startsWith(kURISRVPrefix); if (!(urlSD.startsWith(kURIPrefix) || isSeedlist)) { return MongoURI(uassertStatusOK(ConnectionString::parse(url))); @@ -365,27 +350,40 @@ MongoURI MongoURI::parseImpl(const std::string& url) { uasserted(ErrorCodes::FailedToParse, "Only a single server may be specified with a mongo+srv:// url."); } - const int dots = std::count(begin(canonicalHost), end(canonicalHost), '.'); - const int requiredDots = (canonicalHost.back() == '.') + 2; - if (dots < requiredDots) { + + const mongo::dns::HostName host(canonicalHost); + + if (host.nameComponents().size() < 3) { uasserted(ErrorCodes::FailedToParse, "A server specified with a mongo+srv:// url must have at least 3 hostname " "components separated by dots ('.')"); } - const auto domain = stripHost(canonicalHost); - auto srvEntries = dns::lookupSRVRecords("_mongodb._tcp." + canonicalHost); + + const mongo::dns::HostName srvSubdomain("_mongodb._tcp"); + + const auto srvEntries = + dns::lookupSRVRecords(srvSubdomain.resolvedIn(host).canonicalName()); + + auto makeFQDN = [](dns::HostName hostName) { + hostName.forceQualification(); + return hostName; + }; + + const mongo::dns::HostName domain = makeFQDN(host.parentDomain()); servers.clear(); - std::transform(std::make_move_iterator(begin(srvEntries)), - std::make_move_iterator(end(srvEntries)), - back_inserter(servers), - [&domain](auto&& srv) { - if (!isWithinDomain(srv.host, domain)) { - uasserted(ErrorCodes::FailedToParse, - "Hostname "s + srv.host + " is not within the domain "s + - domain); - } - return HostAndPort(std::move(srv.host), srv.port); - }); + using std::begin; + using std::end; + std::transform( + begin(srvEntries), end(srvEntries), back_inserter(servers), [&domain](auto&& srv) { + const dns::HostName target(srv.host); // FQDN + + if (!domain.contains(target)) { + uasserted(ErrorCodes::FailedToParse, + str::stream() << "Hostname " << target << " is not within the domain " + << domain); + } + return HostAndPort(srv.host, srv.port); + }); } // 6. Split the auth database and connection options string by the first, unescaped ?, diff --git a/src/mongo/client/mongo_uri_test.cpp b/src/mongo/client/mongo_uri_test.cpp index b8734f2d57a..9584bfd6f9a 100644 --- a/src/mongo/client/mongo_uri_test.cpp +++ b/src/mongo/client/mongo_uri_test.cpp @@ -603,6 +603,7 @@ TEST(MongoURI, srvRecordTest) { using namespace mongo; enum Expectation : bool { success = true, failure = false }; const struct { + int lineNumber; std::string uri; std::string user; std::string password; @@ -612,7 +613,8 @@ TEST(MongoURI, srvRecordTest) { Expectation expectation; } tests[] = { // Test some non-SRV URIs to make sure that they do not perform expansions - {"mongodb://test1.test.build.10gen.cc:12345/", + {__LINE__, + "mongodb://test1.test.build.10gen.cc:12345/", "", "", "", @@ -620,7 +622,8 @@ TEST(MongoURI, srvRecordTest) { {}, success}, - {"mongodb://test6.test.build.10gen.cc:12345/", + {__LINE__, + "mongodb://test6.test.build.10gen.cc:12345/", "", "", "", @@ -629,7 +632,8 @@ TEST(MongoURI, srvRecordTest) { success}, // Test a sample URI against each provided testing DNS entry - {"mongodb+srv://test1.test.build.10gen.cc/", + {__LINE__, + "mongodb+srv://test1.test.build.10gen.cc/", "", "", "", @@ -638,7 +642,8 @@ TEST(MongoURI, srvRecordTest) { success}, // Test a sample URI against each provided testing DNS entry - {"mongodb+srv://test1.test.build.10gen.cc/?ssl=false", + {__LINE__, + "mongodb+srv://test1.test.build.10gen.cc/?ssl=false", "", "", "", @@ -646,7 +651,36 @@ TEST(MongoURI, srvRecordTest) { {{"ssl", "false"}}, success}, - {"mongodb+srv://user:password@test2.test.build.10gen.cc/" + // Test a sample URI against the need for deep DNS relation + {__LINE__, + "mongodb+srv://test18.test.build.10gen.cc/?replicaSet=repl0", + "", + "", + "", + { + {"localhost.sub.test.build.10gen.cc.", 27017}, + }, + { + {"ssl", "true"}, {"replicaSet", "repl0"}, + }, + success}, + + // Test a sample URI with FQDN against the need for deep DNS relation + {__LINE__, + "mongodb+srv://test18.test.build.10gen.cc./?replicaSet=repl0", + "", + "", + "", + { + {"localhost.sub.test.build.10gen.cc.", 27017}, + }, + { + {"ssl", "true"}, {"replicaSet", "repl0"}, + }, + success}, + + {__LINE__, + "mongodb+srv://user:password@test2.test.build.10gen.cc/" "database?someOption=someValue&someOtherOption=someOtherValue", "user", "password", @@ -656,7 +690,8 @@ TEST(MongoURI, srvRecordTest) { success}, - {"mongodb+srv://user:password@test3.test.build.10gen.cc/" + {__LINE__, + "mongodb+srv://user:password@test3.test.build.10gen.cc/" "database?someOption=someValue&someOtherOption=someOtherValue", "user", "password", @@ -666,7 +701,8 @@ TEST(MongoURI, srvRecordTest) { success}, - {"mongodb+srv://user:password@test5.test.build.10gen.cc/" + {__LINE__, + "mongodb+srv://user:password@test5.test.build.10gen.cc/" "database?someOption=someValue&someOtherOption=someOtherValue", "user", "password", @@ -679,7 +715,8 @@ TEST(MongoURI, srvRecordTest) { {"ssl", "true"}}, success}, - {"mongodb+srv://user:password@test5.test.build.10gen.cc/" + {__LINE__, + "mongodb+srv://user:password@test5.test.build.10gen.cc/" "database?someOption=someValue&authSource=anotherDB&someOtherOption=someOtherValue", "user", "password", @@ -693,11 +730,19 @@ TEST(MongoURI, srvRecordTest) { {"ssl", "true"}}, success}, - {"mongodb+srv://test6.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test6.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test6.test.build.10gen.cc/database", "", "", "database", {}, {}, failure}, + {__LINE__, + "mongodb+srv://test6.test.build.10gen.cc/database", + "", + "", + "database", + {}, + {}, + failure}, - {"mongodb+srv://test6.test.build.10gen.cc/?authSource=anotherDB", + {__LINE__, + "mongodb+srv://test6.test.build.10gen.cc/?authSource=anotherDB", "", "", "", @@ -705,7 +750,8 @@ TEST(MongoURI, srvRecordTest) { {}, failure}, - {"mongodb+srv://test6.test.build.10gen.cc/?irrelevantOption=irrelevantValue", + {__LINE__, + "mongodb+srv://test6.test.build.10gen.cc/?irrelevantOption=irrelevantValue", "", "", "", @@ -714,7 +760,8 @@ TEST(MongoURI, srvRecordTest) { failure}, - {"mongodb+srv://test6.test.build.10gen.cc/" + {__LINE__, + "mongodb+srv://test6.test.build.10gen.cc/" "?irrelevantOption=irrelevantValue&authSource=anotherDB", "", "", @@ -723,7 +770,8 @@ TEST(MongoURI, srvRecordTest) { {}, failure}, - {"mongodb+srv://test7.test.build.10gen.cc./?irrelevantOption=irrelevantValue", + {__LINE__, + "mongodb+srv://test7.test.build.10gen.cc./?irrelevantOption=irrelevantValue", "", "", "", @@ -731,11 +779,12 @@ TEST(MongoURI, srvRecordTest) { {}, failure}, - {"mongodb+srv://test7.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test7.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test8.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test8.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test10.test.build.10gen.cc./?irrelevantOption=irrelevantValue", + {__LINE__, + "mongodb+srv://test10.test.build.10gen.cc./?irrelevantOption=irrelevantValue", "", "", "", @@ -743,7 +792,8 @@ TEST(MongoURI, srvRecordTest) { {}, failure}, - {"mongodb+srv://test11.test.build.10gen.cc./?irrelevantOption=irrelevantValue", + {__LINE__, + "mongodb+srv://test11.test.build.10gen.cc./?irrelevantOption=irrelevantValue", "", "", "", @@ -751,36 +801,39 @@ TEST(MongoURI, srvRecordTest) { {}, failure}, - {"mongodb+srv://test12.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test13.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test14.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test15.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test16.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test17.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test18.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test19.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - - {"mongodb+srv://test12.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test13.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test14.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test15.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test16.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test17.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test18.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test19.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test12.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test13.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test14.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test15.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test16.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test17.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test19.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + + {__LINE__, "mongodb+srv://test12.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test13.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test14.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test15.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test16.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test17.test.build.10gen.cc/", "", "", "", {}, {}, failure}, + {__LINE__, "mongodb+srv://test19.test.build.10gen.cc/", "", "", "", {}, {}, failure}, }; for (const auto& test : tests) { auto rs = MongoURI::parse(test.uri); if (test.expectation == failure) { - ASSERT_FALSE(rs.getStatus().isOK()) << "Failing URI: " << test.uri; + ASSERT_FALSE(rs.getStatus().isOK()) << "Failing URI: " << test.uri + << " data on line: " << test.lineNumber; continue; } - ASSERT_OK(rs.getStatus()); + ASSERT_OK(rs.getStatus()) << "Failed on URI: " << test.uri + << " data on line: " << test.lineNumber; auto rv = rs.getValue(); - ASSERT_EQ(rv.getUser(), test.user); - ASSERT_EQ(rv.getPassword(), test.password); - ASSERT_EQ(rv.getDatabase(), test.database); + ASSERT_EQ(rv.getUser(), test.user) << "Failed on URI: " << test.uri + << " data on line: " << test.lineNumber; + ASSERT_EQ(rv.getPassword(), test.password) << "Failed on URI: " << test.uri + << " data on line : " << test.lineNumber; + ASSERT_EQ(rv.getDatabase(), test.database) << "Failed on URI: " << test.uri + << " data on line : " << test.lineNumber; std::vector<std::pair<std::string, std::string>> options(begin(rv.getOptions()), end(rv.getOptions())); std::sort(begin(options), end(options)); @@ -793,12 +846,16 @@ TEST(MongoURI, srvRecordTest) { mongo::unittest::log() << "Option: \"" << options[i].first << "=" << options[i].second << "\" doesn't equal: \"" << expectedOptions[i].first << "=" - << expectedOptions[i].second << "\"" << std::endl; - std::cerr << "Failing URI: \"" << test.uri << "\"" << std::endl; + << expectedOptions[i].second << "\"" + << " data on line: " << test.lineNumber << std::endl; + std::cerr << "Failing URI: \"" << test.uri << "\"" + << " data on line: " << test.lineNumber << std::endl; ASSERT(false); } } - ASSERT_EQ(options.size(), expectedOptions.size()) << "Failing URI: " << test.uri; + ASSERT_EQ(options.size(), expectedOptions.size()) << "Failing URI: " + << " data on line: " << test.lineNumber + << test.uri; std::vector<HostAndPort> hosts(begin(rv.getServers()), end(rv.getServers())); std::sort(begin(hosts), end(hosts)); @@ -806,9 +863,13 @@ TEST(MongoURI, srvRecordTest) { std::sort(begin(expectedHosts), end(expectedHosts)); for (std::size_t i = 0; i < std::min(hosts.size(), expectedHosts.size()); ++i) { - ASSERT_EQ(hosts[i], expectedHosts[i]); + ASSERT_EQ(hosts[i], expectedHosts[i]) << "Failed on URI: " << test.uri + << " at host number" << i + << " data on line: " << test.lineNumber; } - ASSERT_TRUE(hosts.size() == expectedHosts.size()); + ASSERT_TRUE(hosts.size() == expectedHosts.size()) + << "Failed on URI: " << test.uri << " Found " << hosts.size() << " hosts, expected " + << expectedHosts.size() << " data on line: " << test.lineNumber; } } diff --git a/src/mongo/util/SConscript b/src/mongo/util/SConscript index 54caf685b28..c9414e0c281 100644 --- a/src/mongo/util/SConscript +++ b/src/mongo/util/SConscript @@ -492,7 +492,15 @@ env.CppUnitTest( LIBDEPS=[ 'dns_query', "$BUILD_DIR/mongo/base", - ] + ], +) + +env.CppUnitTest( + target='dns_name_test', + source=['dns_name_test.cpp'], + LIBDEPS=[ + "$BUILD_DIR/mongo/base", + ], ) env.Library( 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 diff --git a/src/mongo/util/dns_name_test.cpp b/src/mongo/util/dns_name_test.cpp new file mode 100644 index 00000000000..7a8c9d4821c --- /dev/null +++ b/src/mongo/util/dns_name_test.cpp @@ -0,0 +1,266 @@ +/** + * 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. + */ + + +#include "mongo/util/dns_name.h" + +#include "mongo/stdx/utility.h" +#include "mongo/unittest/unittest.h" + +using namespace std::literals::string_literals; + +namespace mongo { +namespace { +TEST(DNSNameTest, CorrectParsing) { + enum FQDNBool : bool { kIsFQDN = true, kNotFQDN = false }; + const struct { + std::string input; + std::vector<std::string> parsedDomains; + FQDNBool isFQDN; + } tests[] = { + {"com."s, {"com"s}, kIsFQDN}, + {"com"s, {"com"s}, kNotFQDN}, + {"mongodb.com."s, {"com"s, "mongodb"s}, kIsFQDN}, + {"mongodb.com"s, {"com"s, "mongodb"s}, kNotFQDN}, + {"atlas.mongodb.com."s, {"com"s, "mongodb"s, "atlas"s}, kIsFQDN}, + {"atlas.mongodb.com"s, {"com"s, "mongodb"s, "atlas"s}, kNotFQDN}, + {"server.atlas.mongodb.com."s, {"com"s, "mongodb"s, "atlas"s, "server"s}, kIsFQDN}, + {"server.atlas.mongodb.com"s, {"com"s, "mongodb"s, "atlas"s, "server"s}, kNotFQDN}, + }; + + for (const auto& test : tests) { + const ::mongo::dns::HostName host(test.input); + + ASSERT_EQ(host.nameComponents().size(), test.parsedDomains.size()); + for (std::size_t i = 0; i < host.nameComponents().size(); ++i) { + ASSERT_EQ(host.nameComponents()[i], test.parsedDomains[i]); + } + ASSERT(host.isFQDN() == test.isFQDN); + } +} + +TEST(DNSNameTest, CanonicalName) { + const struct { + std::string input; + std::string result; + } tests[] = { + {"com."s, "com."s}, + {"com"s, "com"s}, + {"mongodb.com."s, "mongodb.com."s}, + {"mongodb.com"s, "mongodb.com"s}, + {"atlas.mongodb.com."s, "atlas.mongodb.com."s}, + {"atlas.mongodb.com"s, "atlas.mongodb.com"s}, + {"server.atlas.mongodb.com."s, "server.atlas.mongodb.com."s}, + {"server.atlas.mongodb.com"s, "server.atlas.mongodb.com"s}, + }; + + for (const auto& test : tests) { + const ::mongo::dns::HostName host(test.input); + + ASSERT_EQ(host.canonicalName(), test.result); + } +} + +TEST(DNSNameTest, NoncanonicalName) { + const struct { + std::string input; + std::string result; + } tests[] = { + {"com."s, "com"s}, + {"com"s, "com"s}, + {"mongodb.com."s, "mongodb.com"s}, + {"mongodb.com"s, "mongodb.com"s}, + {"atlas.mongodb.com."s, "atlas.mongodb.com"s}, + {"atlas.mongodb.com"s, "atlas.mongodb.com"s}, + {"server.atlas.mongodb.com."s, "server.atlas.mongodb.com"s}, + {"server.atlas.mongodb.com"s, "server.atlas.mongodb.com"s}, + }; + + for (const auto& test : tests) { + const ::mongo::dns::HostName host(test.input); + + ASSERT_EQ(host.noncanonicalName(), test.result); + } +} + +TEST(DNSNameTest, Contains) { + enum IsSubdomain : bool { kIsSubdomain = true, kNotSubdomain = false }; + enum TripsCheck : bool { kFailure = true, kSuccess = false }; + const struct { + std::string domain; + std::string subdomain; + IsSubdomain isSubdomain; + TripsCheck tripsCheck; + } tests[] = { + {"com."s, "mongodb.com."s, kIsSubdomain, kSuccess}, + {"com"s, "mongodb.com"s, kIsSubdomain, kFailure}, + {"com."s, "mongodb.com"s, kNotSubdomain, kFailure}, + {"com"s, "mongodb.com."s, kNotSubdomain, kFailure}, + + {"com."s, "atlas.mongodb.com."s, kIsSubdomain, kSuccess}, + {"com"s, "atlas.mongodb.com"s, kIsSubdomain, kFailure}, + {"com."s, "atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"com"s, "atlas.mongodb.com."s, kNotSubdomain, kFailure}, + + {"org."s, "atlas.mongodb.com."s, kNotSubdomain, kSuccess}, + {"org"s, "atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"org."s, "atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"org"s, "atlas.mongodb.com."s, kNotSubdomain, kFailure}, + + {"com."s, "com."s, kNotSubdomain, kSuccess}, + {"com"s, "com."s, kNotSubdomain, kFailure}, + {"com."s, "com"s, kNotSubdomain, kFailure}, + {"com"s, "com"s, kNotSubdomain, kFailure}, + + {"mongodb.com."s, "mongodb.com."s, kNotSubdomain, kSuccess}, + {"mongodb.com."s, "mongodb.com"s, kNotSubdomain, kFailure}, + {"mongodb.com"s, "mongodb.com."s, kNotSubdomain, kFailure}, + {"mongodb.com"s, "mongodb.com"s, kNotSubdomain, kFailure}, + + {"mongodb.com."s, "atlas.mongodb.com."s, kIsSubdomain, kSuccess}, + {"mongodb.com"s, "atlas.mongodb.com"s, kIsSubdomain, kFailure}, + {"mongodb.com."s, "atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"mongodb.com"s, "atlas.mongodb.com."s, kNotSubdomain, kFailure}, + + {"mongodb.com."s, "server.atlas.mongodb.com."s, kIsSubdomain, kSuccess}, + {"mongodb.com"s, "server.atlas.mongodb.com"s, kIsSubdomain, kFailure}, + {"mongodb.com."s, "server.atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"mongodb.com"s, "server.atlas.mongodb.com."s, kNotSubdomain, kFailure}, + + {"mongodb.org."s, "server.atlas.mongodb.com."s, kNotSubdomain, kSuccess}, + {"mongodb.org"s, "server.atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"mongodb.org."s, "server.atlas.mongodb.com"s, kNotSubdomain, kFailure}, + {"mongodb.org"s, "server.atlas.mongodb.com."s, kNotSubdomain, kFailure}, + }; + + for (const auto& test : tests) { + const ::mongo::dns::HostName domain(test.domain); + const ::mongo::dns::HostName subdomain(test.subdomain); + + try { + ASSERT(test.isSubdomain == domain.contains(subdomain)); + ASSERT(!test.tripsCheck); + } catch (const ExceptionFor<ErrorCodes::DNSRecordTypeMismatch>&) { + ASSERT(test.tripsCheck); + } + } +} + +TEST(DNSNameTest, Resolution) { + enum Failure : bool { kFails = true, kSucceeds = false }; + enum FQDNBool : bool { kIsFQDN = true, kNotFQDN = false }; + const struct { + std::string domain; + std::string subdomain; + std::string result; + + Failure fails; + FQDNBool isFQDN; + } tests[] = { + {"mongodb.com."s, "atlas"s, "atlas.mongodb.com."s, kSucceeds, kIsFQDN}, + {"mongodb.com"s, "atlas"s, "atlas.mongodb.com"s, kSucceeds, kNotFQDN}, + + {"mongodb.com."s, "server.atlas"s, "server.atlas.mongodb.com."s, kSucceeds, kIsFQDN}, + {"mongodb.com"s, "server.atlas"s, "server.atlas.mongodb.com"s, kSucceeds, kNotFQDN}, + + {"mongodb.com."s, "atlas."s, "FAILS"s, kFails, kNotFQDN}, + {"mongodb.com"s, "atlas."s, "FAILS"s, kFails, kNotFQDN}, + }; + + for (const auto& test : tests) + try { + const ::mongo::dns::HostName domain(test.domain); + const ::mongo::dns::HostName subdomain(test.subdomain); + const ::mongo::dns::HostName resolved = [&] { + try { + const ::mongo::dns::HostName rv = subdomain.resolvedIn(domain); + return rv; + } catch (const ExceptionFor<ErrorCodes::DNSRecordTypeMismatch>&) { + ASSERT(test.fails); + throw; + } + }(); + ASSERT(!test.fails); + + ASSERT_EQ(test.result, resolved.canonicalName()); + ASSERT(test.isFQDN == resolved.isFQDN()); + } catch (const ExceptionFor<ErrorCodes::DNSRecordTypeMismatch>&) { + ASSERT(test.fails); + } +} + +TEST(DNSNameTest, ForceQualification) { + enum FQDNBool : bool { kIsFQDN = true, kNotFQDN = false }; + using Qualification = ::mongo::dns::HostName::Qualification; + const struct { + std::string domain; + FQDNBool startedFQDN; + ::mongo::dns::HostName::Qualification forced; + FQDNBool becameFQDN; + std::string becameCanonical; + } tests[] = { + {"mongodb.com."s, kIsFQDN, Qualification::kFullyQualified, kIsFQDN, "mongodb.com."s}, + {"mongodb.com"s, kNotFQDN, Qualification::kFullyQualified, kIsFQDN, "mongodb.com."s}, + + {"atlas.mongodb.com."s, + kIsFQDN, + Qualification::kFullyQualified, + kIsFQDN, + "atlas.mongodb.com."s}, + {"atlas.mongodb.com"s, + kNotFQDN, + Qualification::kFullyQualified, + kIsFQDN, + "atlas.mongodb.com."s}, + + {"mongodb.com."s, kIsFQDN, Qualification::kRelativeName, kNotFQDN, "mongodb.com"s}, + {"mongodb.com"s, kNotFQDN, Qualification::kRelativeName, kNotFQDN, "mongodb.com"s}, + + {"atlas.mongodb.com."s, + kIsFQDN, + Qualification::kRelativeName, + kNotFQDN, + "atlas.mongodb.com"s}, + {"atlas.mongodb.com"s, + kNotFQDN, + Qualification::kRelativeName, + kNotFQDN, + "atlas.mongodb.com"s}, + }; + + for (const auto& test : tests) { + ::mongo::dns::HostName domain(test.domain); + ASSERT(stdx::as_const(domain).isFQDN() == test.startedFQDN); + domain.forceQualification(test.forced); + ASSERT(stdx::as_const(domain).isFQDN() == test.becameFQDN); + + ASSERT_EQ(stdx::as_const(domain).canonicalName(), test.becameCanonical); + } +} +} // namespace +} // namespace mongo diff --git a/src/mongo/util/dns_query.h b/src/mongo/util/dns_query.h index e8b203c8f71..53786e15f5c 100644 --- a/src/mongo/util/dns_query.h +++ b/src/mongo/util/dns_query.h @@ -96,7 +96,7 @@ std::vector<std::string> lookupTXTRecords(const std::string& service); /** * Returns a group of strings containing text from DNS TXT entries for a specified service. * If the lookup fails because the record doesn't exist, an empty vector is returned. - * THROWS: `DBException` with `ErrorCodes::DNSProtocolError` as th status value if the DNS lookup + * THROWS: `DBException` with `ErrorCodes::DNSProtocolError` as the status value if the DNS lookup * fails for any other reason. */ std::vector<std::string> getTXTRecords(const std::string& service); @@ -105,7 +105,7 @@ std::vector<std::string> getTXTRecords(const std::string& service); * Returns a group of strings containing Address entries for a specified service. * THROWS: `DBException` with `ErrorCodes::DNSHostNotFound` as the status value if the entry is not * found and `ErrorCodes::DNSProtocolError` as the status value if the DNS lookup fails, for any - * other reason + * other reason. * NOTE: This function mostly exists to provide an easy test of the OS DNS APIs in our test driver. */ std::vector<std::string> lookupARecords(const std::string& service); |