summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorADAM David Alan Martin <adam.martin@10gen.com>2017-11-28 19:00:40 -0500
committerADAM David Alan Martin <adam.martin@10gen.com>2017-11-28 19:36:37 -0500
commit8bd7560bc14bd4f708096088c97342a78bf565e6 (patch)
tree0edcb036c352df350c3ea8f402377b7fc46f360f
parenta7c94eeb904473b4bbb568ca44f52676f805fc4f (diff)
downloadmongo-8bd7560bc14bd4f708096088c97342a78bf565e6.tar.gz
SERVER-32059 Implement Drivers spec changes to SRV
-rw-r--r--src/mongo/client/SConscript9
-rw-r--r--src/mongo/client/mongo_uri.cpp87
-rw-r--r--src/mongo/client/mongo_uri_test.cpp118
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));