diff options
author | ADAM David Alan Martin <adam.martin@10gen.com> | 2017-11-28 19:00:40 -0500 |
---|---|---|
committer | ADAM David Alan Martin <adam.martin@10gen.com> | 2017-11-28 19:36:37 -0500 |
commit | 8bd7560bc14bd4f708096088c97342a78bf565e6 (patch) | |
tree | 0edcb036c352df350c3ea8f402377b7fc46f360f | |
parent | a7c94eeb904473b4bbb568ca44f52676f805fc4f (diff) | |
download | mongo-8bd7560bc14bd4f708096088c97342a78bf565e6.tar.gz |
SERVER-32059 Implement Drivers spec changes to SRV
-rw-r--r-- | src/mongo/client/SConscript | 9 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri.cpp | 87 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_test.cpp | 118 |
3 files changed, 170 insertions, 44 deletions
diff --git a/src/mongo/client/SConscript b/src/mongo/client/SConscript index 1956ae58109..ffe65b7cc89 100644 --- a/src/mongo/client/SConscript +++ b/src/mongo/client/SConscript @@ -34,6 +34,15 @@ env.CppUnitTest( target='connection_string_test', source=[ 'connection_string_test.cpp', + ], + LIBDEPS=[ + 'clientdriver', + ] +) + +env.CppUnitTest( + target='mongo_uri_test', + source=[ 'mongo_uri_test.cpp', ], LIBDEPS=[ diff --git a/src/mongo/client/mongo_uri.cpp b/src/mongo/client/mongo_uri.cpp index ba2bbbc1384..d9dca575e40 100644 --- a/src/mongo/client/mongo_uri.cpp +++ b/src/mongo/client/mongo_uri.cpp @@ -44,15 +44,23 @@ #include "mongo/client/dbclientinterface.h" #include "mongo/client/sasl_client_authenticate.h" #include "mongo/db/namespace_string.h" +#include "mongo/stdx/utility.h" #include "mongo/util/dns_query.h" #include "mongo/util/hex.h" #include "mongo/util/mongoutils/str.h" +using namespace std::literals::string_literals; + namespace { constexpr std::array<char, 16> hexits{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; const mongo::StringData kURIPrefix{"mongodb://"}; const mongo::StringData kURISRVPrefix{"mongodb+srv://"}; + +// This vector must remain sorted. It is over pairs to facilitate a call to `std::includes` using +// a `std::map<std::string, std::string>` as the other parameter. +const std::vector<std::pair<std::string, std::string>> permittedTXTOptions = {{"authSource"s, ""s}, + {"replicaSet"s, ""s}}; } // namespace /** @@ -123,9 +131,13 @@ std::pair<StringData, StringData> partitionBackward(StringData str, const char c * Breakout method for parsing application/x-www-form-urlencoded option pairs * * foo=bar&baz=qux&... + * + * A `std::map<std::string, std::string>` is returned, to facilitate setwise operations from the STL + * on multiple parsed option sources. STL setwise operations require sorted lists. A map is used + * instead of a vector of pairs to permit insertion-is-not-overwrite behavior. */ -MongoURI::OptionsMap parseOptions(StringData options, StringData url) { - MongoURI::OptionsMap ret; +std::map<std::string, std::string> parseOptions(StringData options, StringData url) { + std::map<std::string, std::string> ret; if (options.empty()) { return ret; } @@ -188,26 +200,55 @@ MongoURI::OptionsMap parseOptions(StringData options, StringData url) { return ret; } -MongoURI::OptionsMap addTXTOptions(MongoURI::OptionsMap options, +MongoURI::OptionsMap addTXTOptions(std::map<std::string, std::string> options, const std::string& host, const StringData url, const bool isSeedlist) { // If there is no seedlist mode, then don't add any TXT options. if (!isSeedlist) return options; + options.insert({"ssl", "true"}); // Get all TXT records and parse them as options, adding them to the options set. - const auto txtRecords = dns::getTXTRecords(host); - - for (const auto& record : txtRecords) { - auto txtOptions = parseOptions(record, url); - // Note that, `std::map` and `std::unordered_map` insert does not replace existing - // values -- this gives the desired behavior that user-specified values override TXT - // record specified values. - options.insert(begin(txtOptions), end(txtOptions)); + auto txtRecords = dns::getTXTRecords(host); + if (txtRecords.empty()) { + return {std::make_move_iterator(begin(options)), std::make_move_iterator(end(options))}; + } + + if (txtRecords.size() > 1) { + uasserted(ErrorCodes::FailedToParse, "Encountered multiple TXT records for: "s + url); + } + + auto txtOptions = parseOptions(txtRecords.front(), url); + if (!std::includes( + begin(permittedTXTOptions), + end(permittedTXTOptions), + begin(stdx::as_const(txtOptions)), + end(stdx::as_const(txtOptions)), + [](const auto& lhs, const auto& rhs) { return std::get<0>(lhs) < std::get<0>(rhs); })) { + uasserted(ErrorCodes::FailedToParse, "Encountered invalid options in TXT record."); } - return options; + options.insert(std::make_move_iterator(begin(txtOptions)), + std::make_move_iterator(end(txtOptions))); + + 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 @@ -324,11 +365,27 @@ 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) { + 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); servers.clear(); - std::transform(begin(srvEntries), end(srvEntries), back_inserter(servers), [](auto& srv) { - return HostAndPort(std::move(srv.host), srv.port); - }); + 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); + }); } // 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 c361346eef7..b8734f2d57a 100644 --- a/src/mongo/client/mongo_uri_test.cpp +++ b/src/mongo/client/mongo_uri_test.cpp @@ -601,6 +601,7 @@ TEST(MongoURI, specTests) { TEST(MongoURI, srvRecordTest) { using namespace mongo; + enum Expectation : bool { success = true, failure = false }; const struct { std::string uri; std::string user; @@ -608,6 +609,7 @@ TEST(MongoURI, srvRecordTest) { std::string database; std::vector<HostAndPort> hosts; std::map<std::string, std::string> options; + Expectation expectation; } tests[] = { // Test some non-SRV URIs to make sure that they do not perform expansions {"mongodb://test1.test.build.10gen.cc:12345/", @@ -615,13 +617,16 @@ TEST(MongoURI, srvRecordTest) { "", "", {{"test1.test.build.10gen.cc", 12345}}, - {}}, + {}, + success}, + {"mongodb://test6.test.build.10gen.cc:12345/", "", "", "", {{"test6.test.build.10gen.cc", 12345}}, - {}}, + {}, + success}, // Test a sample URI against each provided testing DNS entry {"mongodb+srv://test1.test.build.10gen.cc/", @@ -629,7 +634,17 @@ TEST(MongoURI, srvRecordTest) { "", "", {{"localhost.test.build.10gen.cc.", 27017}, {"localhost.test.build.10gen.cc.", 27018}}, - {}}, + {{"ssl", "true"}}, + success}, + + // Test a sample URI against each provided testing DNS entry + {"mongodb+srv://test1.test.build.10gen.cc/?ssl=false", + "", + "", + "", + {{"localhost.test.build.10gen.cc.", 27017}, {"localhost.test.build.10gen.cc.", 27018}}, + {{"ssl", "false"}}, + success}, {"mongodb+srv://user:password@test2.test.build.10gen.cc/" "database?someOption=someValue&someOtherOption=someOtherValue", @@ -637,7 +652,8 @@ TEST(MongoURI, srvRecordTest) { "password", "database", {{"localhost.test.build.10gen.cc.", 27018}, {"localhost.test.build.10gen.cc.", 27019}}, - {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}}}, + {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}, {"ssl", "true"}}, + success}, {"mongodb+srv://user:password@test3.test.build.10gen.cc/" @@ -646,7 +662,8 @@ TEST(MongoURI, srvRecordTest) { "password", "database", {{"localhost.test.build.10gen.cc.", 27017}}, - {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}}}, + {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}, {"ssl", "true"}}, + success}, {"mongodb+srv://user:password@test5.test.build.10gen.cc/" @@ -658,7 +675,9 @@ TEST(MongoURI, srvRecordTest) { {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}, {"replicaSet", "repl0"}, - {"authSource", "thisDB"}}}, + {"authSource", "thisDB"}, + {"ssl", "true"}}, + success}, {"mongodb+srv://user:password@test5.test.build.10gen.cc/" "database?someOption=someValue&authSource=anotherDB&someOtherOption=someOtherValue", @@ -670,52 +689,93 @@ TEST(MongoURI, srvRecordTest) { {"someOtherOption", "someOtherValue"}, {"replicaSet", "repl0"}, {"replicaSet", "repl0"}, - {"authSource", "anotherDB"}}}, + {"authSource", "anotherDB"}, + {"ssl", "true"}}, + success}, + + {"mongodb+srv://test6.test.build.10gen.cc/", "", "", "", {}, {}, failure}, - {"mongodb+srv://test6.test.build.10gen.cc/", + {"mongodb+srv://test6.test.build.10gen.cc/database", "", "", "database", {}, {}, failure}, + + {"mongodb+srv://test6.test.build.10gen.cc/?authSource=anotherDB", "", "", "", - {{"localhost.test.build.10gen.cc.", 27017}}, - {{"replicaSet", "repl0"}, {"authSource", "otherDB"}}}, + {}, + {}, + failure}, - {"mongodb+srv://test6.test.build.10gen.cc/database", + {"mongodb+srv://test6.test.build.10gen.cc/?irrelevantOption=irrelevantValue", "", "", - "database", - {{"localhost.test.build.10gen.cc.", 27017}}, - {{"replicaSet", "repl0"}, {"authSource", "otherDB"}}}, + "", + {}, + {}, + failure}, - {"mongodb+srv://test6.test.build.10gen.cc/?authSource=anotherDB", + + {"mongodb+srv://test6.test.build.10gen.cc/" + "?irrelevantOption=irrelevantValue&authSource=anotherDB", "", "", "", - {{"localhost.test.build.10gen.cc.", 27017}}, - {{"replicaSet", "repl0"}, {"authSource", "anotherDB"}}}, + {}, + {}, + failure}, - {"mongodb+srv://test6.test.build.10gen.cc/?irrelevantOption=irrelevantValue", + {"mongodb+srv://test7.test.build.10gen.cc./?irrelevantOption=irrelevantValue", "", "", "", - {{"localhost.test.build.10gen.cc.", 27017}}, - {{"replicaSet", "repl0"}, - {"authSource", "otherDB"}, - {"irrelevantOption", "irrelevantValue"}}}, + {}, + {}, + failure}, + {"mongodb+srv://test7.test.build.10gen.cc./", "", "", "", {}, {}, failure}, - {"mongodb+srv://test6.test.build.10gen.cc/" - "?irrelevantOption=irrelevantValue&authSource=anotherDB", + {"mongodb+srv://test8.test.build.10gen.cc./", "", "", "", {}, {}, failure}, + + {"mongodb+srv://test10.test.build.10gen.cc./?irrelevantOption=irrelevantValue", "", "", "", - {{"localhost.test.build.10gen.cc.", 27017}}, - {{"replicaSet", "repl0"}, - {"authSource", "anotherDB"}, - {"irrelevantOption", "irrelevantValue"}}}, + {}, + {}, + failure}, + + {"mongodb+srv://test11.test.build.10gen.cc./?irrelevantOption=irrelevantValue", + "", + "", + "", + {}, + {}, + 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}, }; for (const auto& test : tests) { auto rs = MongoURI::parse(test.uri); + if (test.expectation == failure) { + ASSERT_FALSE(rs.getStatus().isOK()) << "Failing URI: " << test.uri; + continue; + } ASSERT_OK(rs.getStatus()); auto rv = rs.getValue(); ASSERT_EQ(rv.getUser(), test.user); @@ -738,7 +798,7 @@ TEST(MongoURI, srvRecordTest) { ASSERT(false); } } - ASSERT_EQ(options.size(), expectedOptions.size()); + ASSERT_EQ(options.size(), expectedOptions.size()) << "Failing URI: " << test.uri; std::vector<HostAndPort> hosts(begin(rv.getServers()), end(rv.getServers())); std::sort(begin(hosts), end(hosts)); |