diff options
author | ADAM David Alan Martin <adam.martin@10gen.com> | 2017-10-31 15:49:49 -0400 |
---|---|---|
committer | ADAM David Alan Martin <adam.martin@10gen.com> | 2017-10-31 15:49:49 -0400 |
commit | 400b86d2963b30730cccfe7ae6829e6101998ac8 (patch) | |
tree | 2741ecc5a785e60afc80e4d76ec1fa9f15144b5d /src/mongo/client | |
parent | 2a8818d4fd7b833b82997c9996aca9eb14471f09 (diff) | |
download | mongo-400b86d2963b30730cccfe7ae6829e6101998ac8.tar.gz |
SERVER-31061 Add `mongo+srv://` support for URIs.
The shell now supports parsing and handling `mongo+srv://` style URIs,
as part of the DNS Seedlist support. These URIs require DNS SRV and
TXT record lookups, for extra configuration options. The shell also
supports a (limited) form of connection-failover -- when initially
connecting to a non-replica-set cluster, the shell will try each
host listed, in order, until a connection can be established.
Diffstat (limited to 'src/mongo/client')
-rw-r--r-- | src/mongo/client/SConscript | 3 | ||||
-rw-r--r-- | src/mongo/client/connection_string.cpp | 3 | ||||
-rw-r--r-- | src/mongo/client/connection_string.h | 15 | ||||
-rw-r--r-- | src/mongo/client/connection_string_connect.cpp | 33 | ||||
-rw-r--r-- | src/mongo/client/connpool.cpp | 4 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri.cpp | 172 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri.h | 35 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_test.cpp | 158 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json | 6 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json | 10 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json | 14 | ||||
-rw-r--r-- | src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json | 4 |
12 files changed, 328 insertions, 129 deletions
diff --git a/src/mongo/client/SConscript b/src/mongo/client/SConscript index 16dd55dafa0..1956ae58109 100644 --- a/src/mongo/client/SConscript +++ b/src/mongo/client/SConscript @@ -24,6 +24,9 @@ env.Library( ], LIBDEPS=[ '$BUILD_DIR/mongo/util/net/network', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/util/dns_query', ] ) diff --git a/src/mongo/client/connection_string.cpp b/src/mongo/client/connection_string.cpp index 16a6e78440d..5e8a5f4e0bb 100644 --- a/src/mongo/client/connection_string.cpp +++ b/src/mongo/client/connection_string.cpp @@ -121,9 +121,6 @@ void ConnectionString::_finishInit() { uassert(ErrorCodes::FailedToParse, "Cannot specify a replica set name for a ConnectionString of type MASTER", _setName.empty()); - uassert(ErrorCodes::FailedToParse, - "ConnectionStrings of type MASTER must contain exactly one server", - _servers.size() == 1); break; case SET: uassert(ErrorCodes::FailedToParse, diff --git a/src/mongo/client/connection_string.h b/src/mongo/client/connection_string.h index 2e888b56dad..0467a77dce3 100644 --- a/src/mongo/client/connection_string.h +++ b/src/mongo/client/connection_string.h @@ -28,6 +28,7 @@ #pragma once +#include <memory> #include <sstream> #include <string> #include <vector> @@ -119,10 +120,10 @@ public: bool operator==(const ConnectionString& other) const; bool operator!=(const ConnectionString& other) const; - DBClientBase* connect(StringData applicationName, - std::string& errmsg, - double socketTimeout = 0, - const MongoURI* uri = nullptr) const; + std::unique_ptr<DBClientBase> connect(StringData applicationName, + std::string& errmsg, + double socketTimeout = 0, + const MongoURI* uri = nullptr) const; static StatusWith<ConnectionString> parse(const std::string& url); @@ -139,9 +140,9 @@ public: virtual ~ConnectionHook() {} // Returns an alternative connection object for a string - virtual DBClientBase* connect(const ConnectionString& c, - std::string& errmsg, - double socketTimeout) = 0; + virtual std::unique_ptr<DBClientBase> connect(const ConnectionString& c, + std::string& errmsg, + double socketTimeout) = 0; }; static void setConnectionHook(ConnectionHook* hook) { diff --git a/src/mongo/client/connection_string_connect.cpp b/src/mongo/client/connection_string_connect.cpp index e4c99dd37e1..d824fa6be78 100644 --- a/src/mongo/client/connection_string_connect.cpp +++ b/src/mongo/client/connection_string_connect.cpp @@ -46,10 +46,10 @@ namespace mongo { stdx::mutex ConnectionString::_connectHookMutex; ConnectionString::ConnectionHook* ConnectionString::_connectHook = NULL; -DBClientBase* ConnectionString::connect(StringData applicationName, - std::string& errmsg, - double socketTimeout, - const MongoURI* uri) const { +std::unique_ptr<DBClientBase> ConnectionString::connect(StringData applicationName, + std::string& errmsg, + double socketTimeout, + const MongoURI* uri) const { MongoURI newURI{}; if (uri) { newURI = *uri; @@ -57,15 +57,18 @@ DBClientBase* ConnectionString::connect(StringData applicationName, switch (_type) { case MASTER: { - auto c = stdx::make_unique<DBClientConnection>(true, 0, std::move(newURI)); - - c->setSoTimeout(socketTimeout); - LOG(1) << "creating new connection to:" << _servers[0]; - if (!c->connect(_servers[0], applicationName, errmsg)) { - return 0; + for (const auto& server : _servers) { + auto c = stdx::make_unique<DBClientConnection>(true, 0, newURI); + + c->setSoTimeout(socketTimeout); + LOG(1) << "creating new connection to:" << server; + if (!c->connect(server, applicationName, errmsg)) { + continue; + } + LOG(1) << "connected connection!"; + return std::move(c); } - LOG(1) << "connected connection!"; - return c.release(); + return nullptr; } case SET: { @@ -74,9 +77,9 @@ DBClientBase* ConnectionString::connect(StringData applicationName, if (!set->connect()) { errmsg = "connect failed to replica set "; errmsg += toString(); - return 0; + return nullptr; } - return set.release(); + return std::move(set); } case CUSTOM: { @@ -91,7 +94,7 @@ DBClientBase* ConnectionString::connect(StringData applicationName, _connectHook); // Double-checked lock, since this will never be active during normal operation - DBClientBase* replacementConn = _connectHook->connect(*this, errmsg, socketTimeout); + auto replacementConn = _connectHook->connect(*this, errmsg, socketTimeout); log() << "replacing connection to " << this->toString() << " with " << (replacementConn ? replacementConn->getServerAddress() : "(empty)"); diff --git a/src/mongo/client/connpool.cpp b/src/mongo/client/connpool.cpp index 30325079fab..6c5a3d9cdfa 100644 --- a/src/mongo/client/connpool.cpp +++ b/src/mongo/client/connpool.cpp @@ -256,7 +256,7 @@ DBClientBase* DBConnectionPool::get(const ConnectionString& url, double socketTi // If no connections for this host are available in the PoolForHost (that is, all the // connections have been checked out, or none have been created yet), create a new connection. string errmsg; - c = url.connect(StringData(), errmsg, socketTimeout); + c = url.connect(StringData(), errmsg, socketTimeout).release(); uassert(13328, _name + ": connect failed " + url.toString() + " : " + errmsg, c); return _finishCreate(url.toString(), socketTimeout, c); @@ -277,7 +277,7 @@ DBClientBase* DBConnectionPool::get(const string& host, double socketTimeout) { const ConnectionString cs(uassertStatusOK(ConnectionString::parse(host))); string errmsg; - c = cs.connect(StringData(), errmsg, socketTimeout); + c = cs.connect(StringData(), errmsg, socketTimeout).release(); if (!c) throw SocketException(SocketException::CONNECT_ERROR, host, diff --git a/src/mongo/client/mongo_uri.cpp b/src/mongo/client/mongo_uri.cpp index e2356881290..e7c500d719b 100644 --- a/src/mongo/client/mongo_uri.cpp +++ b/src/mongo/client/mongo_uri.cpp @@ -34,24 +34,26 @@ #include <utility> +#include <boost/algorithm/string/case_conv.hpp> +#include <boost/algorithm/string/classification.hpp> +#include <boost/algorithm/string/find_iterator.hpp> +#include <boost/algorithm/string/predicate.hpp> + #include "mongo/base/status_with.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/client/dbclientinterface.h" #include "mongo/client/sasl_client_authenticate.h" #include "mongo/db/namespace_string.h" +#include "mongo/util/dns_query.h" #include "mongo/util/hex.h" #include "mongo/util/mongoutils/str.h" -#include <boost/algorithm/string/case_conv.hpp> -#include <boost/algorithm/string/classification.hpp> -#include <boost/algorithm/string/find_iterator.hpp> -#include <boost/algorithm/string/predicate.hpp> - 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://"}; +} // namespace /** * RFC 3986 Section 2.1 - Percent Encoding @@ -94,7 +96,8 @@ namespace mongo { namespace { /** - * Helper Method for MongoURI::parse() to split a string into exactly 2 pieces by a char delimeter + * Helper Method for MongoURI::parse() to split a string into exactly 2 pieces by a char + * delimeter. */ std::pair<StringData, StringData> partitionForward(StringData str, const char c) { const auto delim = str.find(c); @@ -105,8 +108,8 @@ std::pair<StringData, StringData> partitionForward(StringData str, const char c) } /** - * Helper method for MongoURI::parse() to split a string into exactly 2 pieces by a char delimiter - * searching backward from the end of the string. + * Helper method for MongoURI::parse() to split a string into exactly 2 pieces by a char + * delimiter searching backward from the end of the string. */ std::pair<StringData, StringData> partitionBackward(StringData str, const char c) { const auto delim = str.rfind(c); @@ -121,17 +124,17 @@ std::pair<StringData, StringData> partitionBackward(StringData str, const char c * * foo=bar&baz=qux&... */ -StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url) { +MongoURI::OptionsMap parseOptions(StringData options, StringData url) { MongoURI::OptionsMap ret; if (options.empty()) { return ret; } if (options.find('?') != std::string::npos) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "URI Cannot Contain multiple questions marks for mongodb:// URL: " - << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() + << "URI Cannot Contain multiple questions marks for mongodb:// URL: " + << url); } const auto optionsStr = options.toString(); @@ -141,16 +144,16 @@ StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url ++i) { const auto opt = boost::copy_range<std::string>(*i); if (opt.empty()) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "Missing a key/value pair in the options for mongodb:// URL: " - << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() + << "Missing a key/value pair in the options for mongodb:// URL: " + << url); } const auto kvPair = partitionForward(opt, '='); const auto keyRaw = kvPair.first; if (keyRaw.empty()) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Missing a key for key/value pair in the options for mongodb:// URL: " @@ -158,7 +161,7 @@ StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url } const auto key = uriDecode(keyRaw); if (!key.isOK()) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Key '" << keyRaw << "' in options cannot properly be URL decoded for mongodb:// URL: " @@ -166,14 +169,14 @@ StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url } const auto valRaw = kvPair.second; if (valRaw.empty()) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "Missing value for key '" << keyRaw - << "' in the options for mongodb:// URL: " - << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() << "Missing value for key '" << keyRaw + << "' in the options for mongodb:// URL: " + << url); } const auto val = uriDecode(valRaw); if (!val.isOK()) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Value '" << valRaw << "' for key '" << keyRaw << "' in options cannot properly be URL decoded for mongodb:// URL: " @@ -186,20 +189,38 @@ StatusWith<MongoURI::OptionsMap> parseOptions(StringData options, StringData url return ret; } +MongoURI::OptionsMap addTXTOptions(MongoURI::OptionsMap 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; + + // 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)); + } + + return options; +} } // namespace -StatusWith<MongoURI> MongoURI::parse(const std::string& url) { +MongoURI MongoURI::parseImpl(const std::string& url) { const StringData urlSD(url); // 1. Validate and remove the scheme prefix mongodb:// - if (!urlSD.startsWith(kURIPrefix)) { - const auto cs_status = ConnectionString::parse(url); - if (!cs_status.isOK()) { - return cs_status.getStatus(); - } - return MongoURI(cs_status.getValue()); + const bool isSeedlist = urlSD.startsWith(kURISRVPrefix); + if (!(urlSD.startsWith(kURIPrefix) || isSeedlist)) { + return MongoURI(uassertStatusOK(ConnectionString::parse(url))); } - const auto uriWithoutPrefix = urlSD.substr(kURIPrefix.size()); + const auto uriWithoutPrefix = urlSD.substr(urlSD.find("://") + 3); // 2. Split the string by the first, unescaped / (if any), yielding: // split[0]: User information and host identifers @@ -211,14 +232,15 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { // 2.b Make sure that there are no question marks in the left side of the / // as any options after the ? must still have the / delimeter if (databaseAndOptions.empty() && userAndHostInfo.find('?') != std::string::npos) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "URI must contain slash delimeter between hosts and options for mongodb:// URL: " << url); } - // 3. Split the user information and host identifiers string by the last, unescaped @, yielding: + // 3. Split the user information and host identifiers string by the last, unescaped @, + // yielding: // split[0]: User information // split[1]: Host identifiers; const auto userAndHost = partitionBackward(userAndHostInfo, '@'); @@ -237,18 +259,21 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { }; if (containsColonOrAt(usernameSD)) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "Username must be URL Encoded for mongodb:// URL: " << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() << "Username must be URL Encoded for mongodb:// URL: " + << url); } + if (containsColonOrAt(passwordSD)) { - return Status(ErrorCodes::FailedToParse, - str::stream() << "Password must be URL Encoded for mongodb:// URL: " << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() << "Password must be URL Encoded for mongodb:// URL: " + << url); } // Get the username and make sure it did not fail to decode const auto usernameWithStatus = uriDecode(usernameSD); if (!usernameWithStatus.isOK()) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Username cannot properly be URL decoded for mongodb:// URL: " << url); } @@ -257,7 +282,7 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { // Get the password and make sure it did not fail to decode const auto passwordWithStatus = uriDecode(passwordSD); if (!passwordWithStatus.isOK()) - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Password cannot properly be URL decoded for mongodb:// URL: " << url); const auto password = passwordWithStatus.getValue(); @@ -271,7 +296,7 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { ++i) { const auto hostWithStatus = uriDecode(boost::copy_range<std::string>(*i)); if (!hostWithStatus.isOK()) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "Host cannot properly be URL decoded for mongodb:// URL: " << url); } @@ -282,23 +307,35 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { } if ((host.find('/') != std::string::npos) && !StringData(host).endsWith(".sock")) { - return Status( + throw DBException( ErrorCodes::FailedToParse, str::stream() << "'" << host << "' in '" << url << "' appears to be a unix socket, but does not end in '.sock'"); } - const auto statusHostAndPort = HostAndPort::parse(host); - if (!statusHostAndPort.isOK()) { - return statusHostAndPort.getStatus(); - } - servers.push_back(statusHostAndPort.getValue()); + servers.push_back(uassertStatusOK(HostAndPort::parse(host))); } if (servers.empty()) { - return Status(ErrorCodes::FailedToParse, "No server(s) specified"); + throw DBException(ErrorCodes::FailedToParse, "No server(s) specified"); } - // 6. Split the auth database and connection options string by the first, unescaped ?, yielding: + const std::string canonicalHost = servers.front().host(); + // If we're in seedlist mode, lookup the SRV record for `_mongodb._tcp` on the specified + // domain name. Take that list of servers as the new list of servers. + if (isSeedlist) { + if (servers.size() > 1) { + throw DBException(ErrorCodes::FailedToParse, + "Only a single server may be specified with a mongo+srv:// url."); + } + 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); + }); + } + + // 6. Split the auth database and connection options string by the first, unescaped ?, + // yielding: // split[0] = auth database // split[1] = connection options const auto dbAndOpts = partitionForward(databaseAndOptions, '?'); @@ -306,10 +343,10 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { const auto connectionOptions = dbAndOpts.second; const auto databaseWithStatus = uriDecode(databaseSD); if (!databaseWithStatus.isOK()) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "Database name cannot properly be URL decoded for mongodb:// URL: " - << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() << "Database name cannot properly be URL " + "decoded for mongodb:// URL: " + << url); } const auto database = databaseWithStatus.getValue(); @@ -320,35 +357,32 @@ StatusWith<MongoURI> MongoURI::parse(const std::string& url) { if (!database.empty() && !NamespaceString::validDBName(database, NamespaceString::DollarInDbNameBehavior::Disallow)) { - return Status(ErrorCodes::FailedToParse, - str::stream() - << "Database name cannot have reserved characters for mongodb:// URL: " - << url); + throw DBException(ErrorCodes::FailedToParse, + str::stream() << "Database name cannot have reserved " + "characters for mongodb:// URL: " + << url); } // 8. Validate, split, and URL decode the connection options - const auto optsWith = parseOptions(connectionOptions, url); - if (!optsWith.isOK()) { - return optsWith.getStatus(); - } - const auto options = optsWith.getValue(); + auto options = + addTXTOptions(parseOptions(connectionOptions, url), canonicalHost, url, isSeedlist); // If a replica set option was specified, store it in the 'setName' field. const auto optIter = options.find("replicaSet"); std::string setName; - if (optIter != options.end()) { + if (optIter != end(options)) { setName = optIter->second; invariant(!setName.empty()); } - if ((servers.size() > 1) && setName.empty()) { - return Status(ErrorCodes::FailedToParse, - "Cannot list multiple servers in URL without 'replicaSet' option"); - } - ConnectionString cs( setName.empty() ? ConnectionString::MASTER : ConnectionString::SET, servers, setName); return MongoURI(std::move(cs), username, password, database, std::move(options)); } +StatusWith<MongoURI> MongoURI::parse(const std::string& url) try { + return parseImpl(url); +} catch (const std::exception&) { + return exceptionToStatus(); +} } // namespace mongo diff --git a/src/mongo/client/mongo_uri.h b/src/mongo/client/mongo_uri.h index f7a7c4aeb81..11d948a526c 100644 --- a/src/mongo/client/mongo_uri.h +++ b/src/mongo/client/mongo_uri.h @@ -1,7 +1,7 @@ /** * Copyright (C) 2015 MongoDB Inc. * - * This program is free software: you can redistribute it and/or modify + * 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. * @@ -43,7 +43,6 @@ #include "mongo/util/net/hostandport.h" namespace mongo { - /** * Encode a string for embedding in a URI. * Replaces reserved bytes with %xx sequences. @@ -51,6 +50,7 @@ namespace mongo { * Optionally allows passthrough characters to remain unescaped. */ void uriEncode(std::ostream& ss, StringData str, StringData passthrough = ""_sd); + inline std::string uriEncode(StringData str, StringData passthrough = ""_sd) { std::ostringstream ss; uriEncode(ss, str, passthrough); @@ -66,9 +66,13 @@ StatusWith<std::string> uriDecode(StringData str); /** * MongoURI handles parsing of URIs for mongodb, and falls back to old-style * ConnectionString parsing. It's used primarily by the shell. - * It parses URIs with the following format: + * It parses URIs with the following formats: * * mongodb://[usr:pwd@]host1[:port1]...[,hostN[:portN]]][/[db][?options]] + * mongodb+srv://[usr:pwd@]host[/[db][?options]] + * + * `mongodb+srv://` URIs will perform DNS SRV and TXT lookups and expand per the DNS Seedlist + * specification. * * While this format is generally RFC 3986 compliant, some exceptions do exist: * 1. The 'host' field, as defined by section 3.2.2 is expanded in the following ways: @@ -96,6 +100,11 @@ StatusWith<std::string> uriDecode(StringData str); */ class MongoURI { public: + // Note that, because this map is used for DNS TXT record injection on options, there is a + // requirement on its behavior for `insert`: insert must not replace or update existing values + // -- this gives the desired behavior that user-specified values override TXT record specified + // values. `std::map` and `std::unordered_map` satisfy this requirement. Make sure that + // whichever map type is used provides that guarantee. using OptionsMap = std::map<std::string, std::string>; static StatusWith<MongoURI> parse(const std::string& url); @@ -140,20 +149,21 @@ public: // server (say a member of a replica-set), you can pass in its HostAndPort information to // get a new URI with the same info, except type() will be MASTER and getServers() will // be the single host you pass in. - MongoURI cloneURIForServer(const HostAndPort& hostAndPort) const { - return MongoURI(ConnectionString(hostAndPort), _user, _password, _database, _options); + MongoURI cloneURIForServer(HostAndPort hostAndPort) const { + return MongoURI( + ConnectionString(std::move(hostAndPort)), _user, _password, _database, _options); } ConnectionString::ConnectionType type() const { return _connectString.type(); } - explicit MongoURI(const ConnectionString connectString) - : _connectString(std::move(connectString)){}; + explicit MongoURI(const ConnectionString& connectString) : _connectString(connectString){}; MongoURI() = default; friend std::ostream& operator<<(std::ostream&, const MongoURI&); + friend StringBuilder& operator<<(StringBuilder&, const MongoURI&); private: @@ -166,10 +176,12 @@ private: _user(user), _password(password), _database(database), - _options(std::move(options)){}; + _options(std::move(options)) {} BSONObj _makeAuthObjFromOptions(int maxWireVersion) const; + static MongoURI parseImpl(const std::string& url); + ConnectionString _connectString; std::string _user; std::string _password; @@ -178,13 +190,10 @@ private: }; inline std::ostream& operator<<(std::ostream& ss, const MongoURI& uri) { - ss << uri._connectString; - return ss; + return ss << uri._connectString; } inline StringBuilder& operator<<(StringBuilder& sb, const MongoURI& uri) { - sb << uri._connectString; - return sb; + return sb << uri._connectString; } - } // namespace mongo diff --git a/src/mongo/client/mongo_uri_test.cpp b/src/mongo/client/mongo_uri_test.cpp index 00edcb110f4..5153452f9ec 100644 --- a/src/mongo/client/mongo_uri_test.cpp +++ b/src/mongo/client/mongo_uri_test.cpp @@ -1,7 +1,7 @@ /** * Copyright (C) 2009-2015 MongoDB Inc. * - * This program is free software: you can redistribute it and/or modify + * 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. * @@ -506,8 +506,8 @@ TEST(MongoURI, CloneURIForServer) { /** * These tests come from the Mongo Uri Specifications for the drivers found at: * https://github.com/mongodb/specifications/tree/master/source/connection-string/tests - * They have been slighly altered as the Drivers specification is slighly different - * from the server specification. + * They have been altered as the Drivers specification is somewhat different from the shell + * implementation. */ TEST(MongoURI, specTests) { const std::string files[] = { @@ -599,4 +599,156 @@ TEST(MongoURI, specTests) { } } +TEST(MongoURI, srvRecordTest) { + using namespace mongo; + const struct { + std::string uri; + std::string user; + std::string password; + std::string database; + std::vector<HostAndPort> hosts; + std::map<std::string, std::string> options; + } tests[] = { + // Test some non-SRV URIs to make sure that they do not perform expansions + {"mongodb://test1.test.build.10gen.cc:12345/", + "", + "", + "", + {{"test1.test.build.10gen.cc", 12345}}, + {}}, + {"mongodb://test6.test.build.10gen.cc:12345/", + "", + "", + "", + {{"test6.test.build.10gen.cc", 12345}}, + {}}, + + // Test a sample URI against each provided testing DNS entry + {"mongodb+srv://test1.test.build.10gen.cc/", + "", + "", + "", + {{"localhost.build.10gen.cc.", 27017}, {"localhost.build.10gen.cc.", 27018}}, + {}}, + + {"mongodb+srv://user:password@test2.test.build.10gen.cc/" + "database?someOption=someValue&someOtherOption=someOtherValue", + "user", + "password", + "database", + {{"localhost.build.10gen.cc.", 27018}, {"localhost.build.10gen.cc.", 27019}}, + {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}}}, + + + {"mongodb+srv://user:password@test3.test.build.10gen.cc/" + "database?someOption=someValue&someOtherOption=someOtherValue", + "user", + "password", + "database", + {{"localhost.build.10gen.cc.", 27017}}, + {{"someOption", "someValue"}, {"someOtherOption", "someOtherValue"}}}, + + + {"mongodb+srv://user:password@test5.test.build.10gen.cc/" + "database?someOption=someValue&someOtherOption=someOtherValue", + "user", + "password", + "database", + {{"localhost.build.10gen.cc.", 27017}}, + {{"someOption", "someValue"}, + {"someOtherOption", "someOtherValue"}, + {"connectTimeoutMS", "300000"}, + {"socketTimeoutMS", "300000"}}}, + + {"mongodb+srv://user:password@test5.test.build.10gen.cc/" + "database?someOption=someValue&socketTimeoutMS=100&someOtherOption=someOtherValue", + "user", + "password", + "database", + {{"localhost.build.10gen.cc.", 27017}}, + {{"someOption", "someValue"}, + {"someOtherOption", "someOtherValue"}, + {"connectTimeoutMS", "300000"}, + {"socketTimeoutMS", "100"}}}, + + {"mongodb+srv://test6.test.build.10gen.cc/", + "", + "", + "", + {{"localhost.build.10gen.cc.", 27017}}, + {{"connectTimeoutMS", "200000"}, {"socketTimeoutMS", "200000"}}}, + + {"mongodb+srv://test6.test.build.10gen.cc/database", + "", + "", + "database", + {{"localhost.build.10gen.cc.", 27017}}, + {{"connectTimeoutMS", "200000"}, {"socketTimeoutMS", "200000"}}}, + + {"mongodb+srv://test6.test.build.10gen.cc/?connectTimeoutMS=300000", + "", + "", + "", + {{"localhost.build.10gen.cc.", 27017}}, + {{"connectTimeoutMS", "300000"}, {"socketTimeoutMS", "200000"}}}, + + {"mongodb+srv://test6.test.build.10gen.cc/?irrelevantOption=irrelevantValue", + "", + "", + "", + {{"localhost.build.10gen.cc.", 27017}}, + {{"connectTimeoutMS", "200000"}, + {"socketTimeoutMS", "200000"}, + {"irrelevantOption", "irrelevantValue"}}}, + + + {"mongodb+srv://test6.test.build.10gen.cc/" + "?irrelevantOption=irrelevantValue&connectTimeoutMS=300000", + "", + "", + "", + {{"localhost.build.10gen.cc.", 27017}}, + {{"connectTimeoutMS", "300000"}, + {"socketTimeoutMS", "200000"}, + {"irrelevantOption", "irrelevantValue"}}}, + }; + + for (const auto& test : tests) { + auto rs = MongoURI::parse(test.uri); + ASSERT_OK(rs.getStatus()); + auto rv = rs.getValue(); + ASSERT_EQ(rv.getUser(), test.user); + ASSERT_EQ(rv.getPassword(), test.password); + ASSERT_EQ(rv.getDatabase(), test.database); + std::vector<std::pair<std::string, std::string>> options(begin(rv.getOptions()), + end(rv.getOptions())); + std::sort(begin(options), end(options)); + std::vector<std::pair<std::string, std::string>> expectedOptions(begin(test.options), + end(test.options)); + std::sort(begin(expectedOptions), end(expectedOptions)); + + for (std::size_t i = 0; i < std::min(options.size(), expectedOptions.size()); ++i) { + if (options[i] != expectedOptions[i]) { + 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; + ASSERT(false); + } + } + ASSERT_EQ(options.size(), expectedOptions.size()); + + std::vector<HostAndPort> hosts(begin(rv.getServers()), end(rv.getServers())); + std::sort(begin(hosts), end(hosts)); + auto expectedHosts = test.hosts; + 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_TRUE(hosts.size() == expectedHosts.size()); + } +} + } // namespace diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json b/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json index 8a1fda580a8..5af3797ccdb 100644 --- a/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-host-identifiers.json @@ -108,7 +108,7 @@ { "description": "Multiple hosts (mixed formats)", "uri": "mongodb://127.0.0.1,[::1]:27018,example.com:27019", - "valid": false, + "valid": true, "warning": false, "hosts": [ { @@ -160,7 +160,7 @@ { "description": "UTF-8 hosts", "uri": "mongodb://bücher.example.com,umläut.example.com/", - "valid": false, + "valid": true, "warning": false, "hosts": [ { @@ -200,4 +200,4 @@ } } ] -}
\ No newline at end of file +} diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json index d167c315bbc..f4f78883298 100644 --- a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-absolute.json @@ -62,7 +62,7 @@ ], "options": null, "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -82,7 +82,7 @@ ], "options": null, "uri": "mongodb://127.0.0.1:27017,%2Ftmp%2Fmongodb-27017.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -102,7 +102,7 @@ ], "options": null, "uri": "mongodb://mongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -260,7 +260,7 @@ ], "options": null, "uri": "mongodb://%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin", - "valid": false, + "valid": true, "warning": false }, { @@ -312,7 +312,7 @@ "w": 1 }, "uri": "mongodb://bob:bar@%2Ftmp%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock/admin?w=1", - "valid": false, + "valid": true, "warning": false }, { diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json index 8da45c4e1b9..a2d737ba72a 100644 --- a/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-unix-sockets-relative.json @@ -62,7 +62,7 @@ ], "options": null, "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -104,7 +104,7 @@ ], "options": null, "uri": "mongodb://rel%2Fmongodb-27017.sock,%2Ftmp%2Fmongodb-27018.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -146,7 +146,7 @@ ], "options": null, "uri": "mongodb://127.0.0.1:27017,rel%2Fmongodb-27017.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -188,7 +188,7 @@ ], "options": null, "uri": "mongodb://mongodb-27017.sock,rel%2Fmongodb-27018.sock", - "valid": false, + "valid": true, "warning": false }, { @@ -302,7 +302,7 @@ ], "options": null, "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin", - "valid": false, + "valid": true, "warning": false }, { @@ -352,7 +352,7 @@ ], "options": null, "uri": "mongodb://rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin", - "valid": false, + "valid": true, "warning": false }, { @@ -404,7 +404,7 @@ "w": 1 }, "uri": "mongodb://bob:bar@rel%2Fmongodb-27017.sock,rel%2Fmongodb-27018.sock/admin?w=1", - "valid": false, + "valid": true, "warning": false }, { diff --git a/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json b/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json index a44236e7b62..68228c6ddec 100644 --- a/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json +++ b/src/mongo/client/mongo_uri_tests/mongo-uri-valid-auth.json @@ -135,7 +135,7 @@ ], "options": null, "uri": "mongodb://alice:secret@127.0.0.1,example.com:27018", - "valid": false, + "valid": true, "warning": false }, { @@ -159,7 +159,7 @@ ], "options": null, "uri": "mongodb://alice:secret@example.com,[::1]:27019/admin", - "valid": false, + "valid": true, "warning": false }, { |