/** * Copyright (C) 2015 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 . * * 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. */ #define MONGO_LOG_DEFAULT_COMPONENT ::mongo::logger::LogComponent::kNetwork #include "mongo/platform/basic.h" #include "mongo/client/mongo_uri.h" #include #include #include #include #include #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/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 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` as the other parameter. const std::vector> permittedTXTOptions = {{"authSource"s, ""s}, {"replicaSet"s, ""s}}; } // namespace /** * RFC 3986 Section 2.1 - Percent Encoding * * Encode data elements in a way which will allow them to be embedded * into a mongodb:// URI safely. */ void mongo::uriEncode(std::ostream& ss, StringData toEncode, StringData passthrough) { for (const auto& c : toEncode) { if ((c == '-') || (c == '_') || (c == '.') || (c == '~') || isalnum(c) || (passthrough.find(c) != std::string::npos)) { ss << c; } else { // Encoding anything not included in section 2.3 "Unreserved characters" ss << '%' << hexits[(c >> 4) & 0xF] << hexits[c & 0xF]; } } } mongo::StatusWith mongo::uriDecode(StringData toDecode) { StringBuilder out; for (size_t i = 0; i < toDecode.size(); ++i) { const auto c = toDecode[i]; if (c == '%') { if (i + 2 > toDecode.size()) { return Status(ErrorCodes::FailedToParse, "Encountered partial escape sequence at end of string"); } out << fromHex(toDecode.substr(i + 1, 2)); i += 2; } else { out << c; } } return out.str(); } namespace mongo { namespace { /** * Helper Method for MongoURI::parse() to split a string into exactly 2 pieces by a char * delimeter. */ std::pair partitionForward(StringData str, const char c) { const auto delim = str.find(c); if (delim == std::string::npos) { return {str, StringData()}; } return {str.substr(0, delim), str.substr(delim + 1)}; } /** * 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 partitionBackward(StringData str, const char c) { const auto delim = str.rfind(c); if (delim == std::string::npos) { return {StringData(), str}; } return {str.substr(0, delim), str.substr(delim + 1)}; } /** * Breakout method for parsing application/x-www-form-urlencoded option pairs * * foo=bar&baz=qux&... * * A `std::map` 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. */ std::map parseOptions(StringData options, StringData url) { std::map ret; if (options.empty()) { return ret; } if (options.find('?') != std::string::npos) { uasserted( ErrorCodes::FailedToParse, str::stream() << "URI Cannot Contain multiple questions marks for mongodb:// URL: " << url); } const auto optionsStr = options.toString(); for (auto i = boost::make_split_iterator(optionsStr, boost::first_finder("&", boost::is_iequal())); i != std::remove_reference::type{}; ++i) { const auto opt = boost::copy_range(*i); if (opt.empty()) { uasserted(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()) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Missing a key for key/value pair in the options for mongodb:// URL: " << url); } const auto key = uriDecode(keyRaw); if (!key.isOK()) { uasserted( ErrorCodes::FailedToParse, str::stream() << "Key '" << keyRaw << "' in options cannot properly be URL decoded for mongodb:// URL: " << url); } const auto valRaw = kvPair.second; if (valRaw.empty()) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Missing value for key '" << keyRaw << "' in the options for mongodb:// URL: " << url); } const auto val = uriDecode(valRaw); if (!val.isOK()) { uasserted( ErrorCodes::FailedToParse, str::stream() << "Value '" << valRaw << "' for key '" << keyRaw << "' in options cannot properly be URL decoded for mongodb:// URL: " << url); } ret[key.getValue()] = val.getValue(); } return ret; } MongoURI::OptionsMap addTXTOptions(std::map 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. 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."); } 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 MongoURI MongoURI::parseImpl(const std::string& url) { const StringData urlSD(url); // 1. Validate and remove the scheme prefix mongodb:// const bool isSeedlist = urlSD.startsWith(kURISRVPrefix); if (!(urlSD.startsWith(kURIPrefix) || isSeedlist)) { return MongoURI(uassertStatusOK(ConnectionString::parse(url))); } 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 // split[1]: Auth database and connection options const auto userAndDb = partitionForward(uriWithoutPrefix, '/'); const auto userAndHostInfo = userAndDb.first; const auto databaseAndOptions = userAndDb.second; // 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) { uasserted( 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: // split[0]: User information // split[1]: Host identifiers; const auto userAndHost = partitionBackward(userAndHostInfo, '@'); const auto userInfo = userAndHost.first; const auto hostIdentifiers = userAndHost.second; // 4. Validate, split (if applicable), and URL decode the user information, yielding: // split[0] = username // split[1] = password const auto userAndPass = partitionForward(userInfo, ':'); const auto usernameSD = userAndPass.first; const auto passwordSD = userAndPass.second; const auto containsColonOrAt = [](StringData str) { return (str.find(':') != std::string::npos) || (str.find('@') != std::string::npos); }; if (containsColonOrAt(usernameSD)) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Username must be URL Encoded for mongodb:// URL: " << url); } if (containsColonOrAt(passwordSD)) { uasserted(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()) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Username cannot properly be URL decoded for mongodb:// URL: " << url); } const auto username = usernameWithStatus.getValue(); // Get the password and make sure it did not fail to decode const auto passwordWithStatus = uriDecode(passwordSD); if (!passwordWithStatus.isOK()) uasserted(ErrorCodes::FailedToParse, str::stream() << "Password cannot properly be URL decoded for mongodb:// URL: " << url); const auto password = passwordWithStatus.getValue(); // 5. Validate, split, and URL decode the host identifiers. const auto hostIdentifiersStr = hostIdentifiers.toString(); std::vector servers; for (auto i = boost::make_split_iterator(hostIdentifiersStr, boost::first_finder(",", boost::is_iequal())); i != std::remove_reference::type{}; ++i) { const auto hostWithStatus = uriDecode(boost::copy_range(*i)); if (!hostWithStatus.isOK()) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Host cannot properly be URL decoded for mongodb:// URL: " << url); } const auto host = hostWithStatus.getValue(); if (host.empty()) { continue; } if ((host.find('/') != std::string::npos) && !StringData(host).endsWith(".sock")) { uasserted( ErrorCodes::FailedToParse, str::stream() << "'" << host << "' in '" << url << "' appears to be a unix socket, but does not end in '.sock'"); } servers.push_back(uassertStatusOK(HostAndPort::parse(host))); } if (servers.empty()) { uasserted(ErrorCodes::FailedToParse, "No server(s) specified"); } 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) { 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(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 ?, // yielding: // split[0] = auth database // split[1] = connection options const auto dbAndOpts = partitionForward(databaseAndOptions, '?'); const auto databaseSD = dbAndOpts.first; const auto connectionOptions = dbAndOpts.second; const auto databaseWithStatus = uriDecode(databaseSD); if (!databaseWithStatus.isOK()) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Database name cannot properly be URL " "decoded for mongodb:// URL: " << url); } const auto database = databaseWithStatus.getValue(); // 7. Validate the database contains no prohibited characters // Prohibited characters: // slash ("/"), backslash ("\"), space (" "), double-quote ("""), or dollar sign ("$") // period (".") is also prohibited, but drivers MAY allow periods if (!database.empty() && !NamespaceString::validDBName(database, NamespaceString::DollarInDbNameBehavior::Disallow)) { uasserted(ErrorCodes::FailedToParse, str::stream() << "Database name cannot have reserved " "characters for mongodb:// URL: " << url); } // 8. Validate, split, and URL decode the connection options auto options = addTXTOptions(parseOptions(connectionOptions, url), canonicalHost, url, isSeedlist); // If a replica set option was specified, store it in the 'setName' field. auto optIter = options.find("replicaSet"); std::string setName; if (optIter != end(options)) { setName = optIter->second; invariant(!setName.empty()); } // If an appname option was specified, validate that is 128 bytes or less. optIter = options.find("appname"); if (optIter != end(options) && optIter->second.length() > 128) { uasserted(ErrorCodes::FailedToParse, str::stream() << "appname cannot exceed 128 characters: " << optIter->second); } boost::optional retryWrites = boost::none; optIter = options.find("retryWrites"); if (optIter != end(options)) { if (optIter->second == "true") { retryWrites.reset(true); } else if (optIter->second == "false") { retryWrites.reset(false); } else { uasserted(ErrorCodes::FailedToParse, str::stream() << "retryWrites must be either \"true\" or \"false\""); } } ConnectionString cs( setName.empty() ? ConnectionString::MASTER : ConnectionString::SET, servers, setName); return MongoURI( std::move(cs), username, password, database, std::move(retryWrites), std::move(options)); } StatusWith MongoURI::parse(const std::string& url) try { return parseImpl(url); } catch (const std::exception&) { return exceptionToStatus(); } const boost::optional MongoURI::getAppName() const { const auto optIter = _options.find("appname"); if (optIter != end(_options)) { return optIter->second; } else { return boost::none; } } } // namespace mongo