diff options
author | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-04-23 17:27:07 -0400 |
---|---|---|
committer | A. Jesse Jiryu Davis <jesse@mongodb.com> | 2019-04-24 21:55:52 -0400 |
commit | 5e346eb4b9bb3ec678d4eac36b3f0a18c1c10939 (patch) | |
tree | 4af3ebd9563c61399458158d88faf7e6dc796ff4 /src/mongo | |
parent | b9c656f6e2d0b0b7676927538ed7ac3278c23322 (diff) | |
download | mongo-5e346eb4b9bb3ec678d4eac36b3f0a18c1c10939.tar.gz |
SERVER-4999 Normalize all hostnames to lowercase
Hostnames passed to replSetInitiate, replSetReconfig, addShard, etc. are
all normalized by replacing ASCII uppercase characters with lowercase
characters, consistent with how MongoDB drivers treat hostnames.
Fixes undefined behavior in mongo::str::toLower().
Diffstat (limited to 'src/mongo')
-rw-r--r-- | src/mongo/client/replica_set_monitor_scan_test.cpp | 38 | ||||
-rw-r--r-- | src/mongo/db/repl/isself_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/repl/repl_set_config_test.cpp | 19 | ||||
-rw-r--r-- | src/mongo/db/repl/repl_set_heartbeat_response_test.cpp | 2 | ||||
-rw-r--r-- | src/mongo/db/s/config/sharding_catalog_manager_add_shard_test.cpp | 4 | ||||
-rw-r--r-- | src/mongo/db/s/config/sharding_catalog_manager_create_database_test.cpp | 6 | ||||
-rw-r--r-- | src/mongo/dbtests/replica_set_monitor_test.cpp | 30 | ||||
-rw-r--r-- | src/mongo/unittest/unittest.h | 5 | ||||
-rw-r--r-- | src/mongo/util/net/hostandport.cpp | 7 | ||||
-rw-r--r-- | src/mongo/util/net/hostandport.h | 2 | ||||
-rw-r--r-- | src/mongo/util/net/hostandport_test.cpp | 24 | ||||
-rw-r--r-- | src/mongo/util/str.h | 3 |
12 files changed, 132 insertions, 12 deletions
diff --git a/src/mongo/client/replica_set_monitor_scan_test.cpp b/src/mongo/client/replica_set_monitor_scan_test.cpp index 02af7f2b7e6..6518d1b4a98 100644 --- a/src/mongo/client/replica_set_monitor_scan_test.cpp +++ b/src/mongo/client/replica_set_monitor_scan_test.cpp @@ -1023,6 +1023,44 @@ TEST_F(CoreScanTest, TwoPrimaries2ndHasOlderConfigVersion) { ASSERT_EQUALS(state->configVersion, 2); } +// U+2603, Snowman, encoded as UTF-8. +#define SNOWMAN "\xe2\x98\x83" + +TEST_F(CoreScanTest, CaseInsensitive) { + const auto host0 = "a"; + const auto host1 = SNOWMAN "B"; + const auto host2 = "c" SNOWMAN; + const std::vector<HostAndPort> seeds = { + HostAndPort(host0), HostAndPort(host1), HostAndPort(host2)}; + const MongoURI uri(ConnectionString::forReplicaSet("name", seeds)); + auto state = makeState(basicUri); + Refresher refresher(state); + + // mock all replies + for (size_t i = 0; i != basicSeeds.size(); ++i) { + refresher.receivedIsMaster(HostAndPort(seeds[i]), + -1, + BSON("setName" + << "name" + << "ismaster" + << (i == 0) + << "secondary" + << (i != 0) + << "hosts" + // Different order and capitalization. + << BSON_ARRAY("A" + << "C" SNOWMAN + << SNOWMAN "b") + << "ok" + << true)); + } + + ASSERT_EQUALS(state->nodes.size(), static_cast<size_t>(3)); + for (const auto& seed : seeds) { + ASSERT(state->findNode(HostAndPort(seed))); + } +} + using MaxStalenessMSTest = ReplicaSetMonitorTest; /** diff --git a/src/mongo/db/repl/isself_test.cpp b/src/mongo/db/repl/isself_test.cpp index 511ef122ecf..bbb03d0b26c 100644 --- a/src/mongo/db/repl/isself_test.cpp +++ b/src/mongo/db/repl/isself_test.cpp @@ -79,6 +79,10 @@ TEST_F(ServiceContextTest, DetectsSameHostIPv6) { #endif } +TEST_F(ServiceContextTest, DetectsSameHostDifferentCapitalization) { + ASSERT(isSelf(HostAndPort("LoCalHOST", serverGlobalParams.port), getGlobalServiceContext())); +} + } // namespace } // namespace repl diff --git a/src/mongo/db/repl/repl_set_config_test.cpp b/src/mongo/db/repl/repl_set_config_test.cpp index 7f3af1345bb..14295fc86fd 100644 --- a/src/mongo/db/repl/repl_set_config_test.cpp +++ b/src/mongo/db/repl/repl_set_config_test.cpp @@ -975,6 +975,25 @@ TEST(ReplSetConfig, ValidateFailsWithDuplicateMemberId) { ASSERT_EQUALS(ErrorCodes::BadValue, status); } +TEST(ReplSetConfig, ValidateFailsWithDuplicateHostnameCaseInsensitive) { + ReplSetConfig config; + Status status = config.initialize(BSON("_id" + << "rs0" + << "version" + << 1 + << "protocolVersion" + << 1 + << "members" + << BSON_ARRAY(BSON("_id" << 0 << "host" + << "localhost:12345") + << BSON("_id" << 1 << "host" + << "LOCALHOST:12345")))); + ASSERT_OK(status); + + status = config.validate(); + ASSERT_EQUALS(ErrorCodes::BadValue, status); +} + TEST(ReplSetConfig, ValidateFailsWithInvalidMember) { ReplSetConfig config; Status status = config.initialize(BSON("_id" diff --git a/src/mongo/db/repl/repl_set_heartbeat_response_test.cpp b/src/mongo/db/repl/repl_set_heartbeat_response_test.cpp index 72c7d31190a..cd9c331dcd3 100644 --- a/src/mongo/db/repl/repl_set_heartbeat_response_test.cpp +++ b/src/mongo/db/repl/repl_set_heartbeat_response_test.cpp @@ -118,7 +118,7 @@ TEST(ReplSetHeartbeatResponse, DefaultConstructThenSlowlyBuildToFullObj) { ASSERT_EQUALS(Timestamp(0, 10), hbResponseObj["durableOpTime"]["ts"].timestamp()); ASSERT_EQUALS(config.toBSON().toString(), hbResponseObj["config"].Obj().toString()); ASSERT_EQUALS(2, hbResponseObj["state"].numberLong()); - ASSERT_EQUALS("syncTarget:27017", hbResponseObj["syncingTo"].String()); + ASSERT_EQUALS_CI("syncTarget:27017", hbResponseObj["syncingTo"].String()); initializeResult = hbResponseObjRoundTripChecker.initialize(hbResponseObj, 0, /*requireWallTime*/ true); diff --git a/src/mongo/db/s/config/sharding_catalog_manager_add_shard_test.cpp b/src/mongo/db/s/config/sharding_catalog_manager_add_shard_test.cpp index ce9460cf3e6..23a107a43b5 100644 --- a/src/mongo/db/s/config/sharding_catalog_manager_add_shard_test.cpp +++ b/src/mongo/db/s/config/sharding_catalog_manager_add_shard_test.cpp @@ -287,7 +287,7 @@ protected: auto foundShard = assertGet(getShardDoc(operationContext(), expectedShard.getName())); ASSERT_EQUALS(expectedShard.getName(), foundShard.getName()); - ASSERT_EQUALS(expectedShard.getHost(), foundShard.getHost()); + ASSERT_EQUALS_CI(expectedShard.getHost(), foundShard.getHost()); ASSERT_EQUALS(expectedShard.getMaxSizeMB(), foundShard.getMaxSizeMB()); ASSERT_EQUALS(expectedShard.getDraining(), foundShard.getDraining()); ASSERT_EQUALS((int)expectedShard.getState(), (int)foundShard.getState()); @@ -332,7 +332,7 @@ protected: auto logEntry = assertGet(ChangeLogType::fromBSON(logEntryBSON)); ASSERT_EQUALS(addedShard.getName(), logEntry.getDetails()["name"].String()); - ASSERT_EQUALS(addedShard.getHost(), logEntry.getDetails()["host"].String()); + ASSERT_EQUALS_CI(addedShard.getHost(), logEntry.getDetails()["host"].String()); } void forwardAddShardNetwork(Date_t when) { diff --git a/src/mongo/db/s/config/sharding_catalog_manager_create_database_test.cpp b/src/mongo/db/s/config/sharding_catalog_manager_create_database_test.cpp index 59fe4af9e98..895783e6254 100644 --- a/src/mongo/db/s/config/sharding_catalog_manager_create_database_test.cpp +++ b/src/mongo/db/s/config/sharding_catalog_manager_create_database_test.cpp @@ -106,7 +106,7 @@ TEST_F(CreateDatabaseTest, createDatabaseSuccess) { // Return size information about first shard onCommand([&](const RemoteCommandRequest& request) { - ASSERT_EQUALS(s0.getHost(), request.target.toString()); + ASSERT_EQUALS(HostAndPort(s0.getHost()), request.target); ASSERT_EQUALS("admin", request.dbname); std::string cmdName = request.cmdObj.firstElement().fieldName(); ASSERT_EQUALS("listDatabases", cmdName); @@ -121,7 +121,7 @@ TEST_F(CreateDatabaseTest, createDatabaseSuccess) { // Return size information about second shard onCommand([&](const RemoteCommandRequest& request) { - ASSERT_EQUALS(s1.getHost(), request.target.toString()); + ASSERT_EQUALS(HostAndPort(s1.getHost()), request.target); ASSERT_EQUALS("admin", request.dbname); std::string cmdName = request.cmdObj.firstElement().fieldName(); ASSERT_EQUALS("listDatabases", cmdName); @@ -136,7 +136,7 @@ TEST_F(CreateDatabaseTest, createDatabaseSuccess) { // Return size information about third shard onCommand([&](const RemoteCommandRequest& request) { - ASSERT_EQUALS(s2.getHost(), request.target.toString()); + ASSERT_EQUALS(HostAndPort(s2.getHost()), request.target); ASSERT_EQUALS("admin", request.dbname); std::string cmdName = request.cmdObj.firstElement().fieldName(); ASSERT_EQUALS("listDatabases", cmdName); diff --git a/src/mongo/dbtests/replica_set_monitor_test.cpp b/src/mongo/dbtests/replica_set_monitor_test.cpp index 89ae6bd1085..f168d7bbad9 100644 --- a/src/mongo/dbtests/replica_set_monitor_test.cpp +++ b/src/mongo/dbtests/replica_set_monitor_test.cpp @@ -29,9 +29,6 @@ #include "mongo/platform/basic.h" -#include <set> -#include <vector> - #include "mongo/base/init.h" #include "mongo/client/connpool.h" #include "mongo/client/dbclient_rs.h" @@ -41,6 +38,11 @@ #include "mongo/dbtests/mock/mock_replica_set.h" #include "mongo/unittest/unittest.h" +#include <boost/algorithm/string/case_conv.hpp> + +#include <set> +#include <vector> + namespace mongo { namespace { @@ -373,5 +375,27 @@ TEST_F(TwoNodeWithTags, SecDownRetryExpiredTimeout) { monitor.reset(); } +// Tests that hostnames with different capitalization are considered equal. +TEST_F(TwoNodeWithTags, CaseInsensitive) { + const auto primaryOnly = mongo::ReadPreference::PrimaryOnly; + const auto secondaryOnly = mongo::ReadPreference::SecondaryOnly; + + MockReplicaSet* replSet = getReplSet(); + + // Initialize with upcased primary hostname. + set<HostAndPort> seedList; + seedList.insert(HostAndPort(boost::algorithm::to_lower_copy(replSet->getPrimary()))); + auto monitor = ReplicaSetMonitor::createIfNeeded(replSet->getSetName(), seedList); + monitor->runScanForMockReplicaSet(); + + auto selectMember = [&](mongo::ReadPreference pref) { + return monitor->getHostOrRefresh(ReadPreferenceSetting(pref, TagSet()), Milliseconds(1)) + .get() + .toString(); + }; + + ASSERT_EQ(selectMember(primaryOnly), replSet->getPrimary()); + ASSERT_EQ(selectMember(secondaryOnly), replSet->getSecondaries()[0]); +} } // namespace } // namespace mongo diff --git a/src/mongo/unittest/unittest.h b/src/mongo/unittest/unittest.h index d00338348a9..9eba2c2bc3c 100644 --- a/src/mongo/unittest/unittest.h +++ b/src/mongo/unittest/unittest.h @@ -113,6 +113,11 @@ #define ASSERT_APPROX_EQUAL(a, b, ABSOLUTE_ERR) ASSERT_LTE(std::abs((a) - (b)), ABSOLUTE_ERR) /** + * ASCII case-insensitive comparison. + */ +#define ASSERT_EQUALS_CI(a, b) ASSERT_EQUALS(str::toLower(a), str::toLower(b)) + +/** * Assert a function call returns its input unchanged. */ #define ASSERT_IDENTITY(INPUT, FUNCTION) \ diff --git a/src/mongo/util/net/hostandport.cpp b/src/mongo/util/net/hostandport.cpp index d81a81fab36..5ff178b9bff 100644 --- a/src/mongo/util/net/hostandport.cpp +++ b/src/mongo/util/net/hostandport.cpp @@ -58,7 +58,9 @@ HostAndPort::HostAndPort(StringData text) { uassertStatusOK(initialize(text)); } -HostAndPort::HostAndPort(const std::string& h, int p) : _host(h), _port(p) {} +// Normalize hostname by lowercasing ASCII uppercase characters. Ignore non-ASCII characters. +// Assume input is ASCII or UTF-8. +HostAndPort::HostAndPort(const std::string& h, int p) : _host(str::toLower(h)), _port(p) {} HostAndPort::HostAndPort(SockAddr addr) : _addr(std::move(addr)) { uassertStatusOK(initialize(_addr->toString(true))); @@ -207,7 +209,8 @@ Status HostAndPort::initialize(StringData s) { } else { port = -1; } - _host = hostPart.toString(); + + _host = str::toLower(hostPart); _port = port; return Status::OK(); } diff --git a/src/mongo/util/net/hostandport.h b/src/mongo/util/net/hostandport.h index 03e3ef2ee5e..08d44a95319 100644 --- a/src/mongo/util/net/hostandport.h +++ b/src/mongo/util/net/hostandport.h @@ -53,6 +53,8 @@ class StatusWith; * Composed of some name component, followed optionally by a colon and a numeric port. The name * might be an IPv4 or IPv6 address or a relative or fully qualified host name, or an absolute * path to a unix socket. + * + * Hostnames are converted to lowercase. */ struct HostAndPort { /** diff --git a/src/mongo/util/net/hostandport_test.cpp b/src/mongo/util/net/hostandport_test.cpp index 69163791b3a..a1149fcf967 100644 --- a/src/mongo/util/net/hostandport_test.cpp +++ b/src/mongo/util/net/hostandport_test.cpp @@ -39,6 +39,9 @@ namespace mongo { namespace { +// U+2603, Snowman, encoded as UTF-8. +#define SNOWMAN "\xe2\x98\x83" + TEST(HostAndPort, BasicLessThanComparison) { // Not less than self. ASSERT_FALSE(HostAndPort("a", 1) < HostAndPort("a", 1)); @@ -50,6 +53,12 @@ TEST(HostAndPort, BasicLessThanComparison) { // Then, order by port number. ASSERT_LESS_THAN(HostAndPort("a", 1), HostAndPort("a", 2)); ASSERT_FALSE(HostAndPort("a", 2) < HostAndPort("a", 1)); + + // Case insensitive. + ASSERT_LESS_THAN(HostAndPort("A", 1), HostAndPort("a", 2)); + ASSERT_LESS_THAN(HostAndPort("a", 1), HostAndPort("A", 2)); + ASSERT_FALSE(HostAndPort("A", 2) < HostAndPort("a", 1)); + ASSERT_FALSE(HostAndPort("a", 2) < HostAndPort("A", 1)); } TEST(HostAndPort, BasicEquality) { @@ -59,11 +68,23 @@ TEST(HostAndPort, BasicEquality) { ASSERT_FALSE(HostAndPort("a", 1) != HostAndPort("a", 1)); ASSERT_NOT_EQUALS(HostAndPort("b", 1), HostAndPort("a", 1)); + // Case insensitive. + ASSERT_EQUALS(HostAndPort("A", 1), HostAndPort("a", 1)); + ASSERT_FALSE(HostAndPort("A", 1) != HostAndPort("a", 1)); + ASSERT_NOT_EQUALS(HostAndPort("B", 1), HostAndPort("a", 1)); + // Comparison on port field ASSERT_FALSE(HostAndPort("a", 1) == HostAndPort("a", 2)); ASSERT_NOT_EQUALS(HostAndPort("a", 1), HostAndPort("a", 2)); } +TEST(HostAndPort, CaseNormalization) { + ASSERT_EQUALS(HostAndPort(SNOWMAN "A", 1), HostAndPort(SNOWMAN "a", 1)); + ASSERT_EQUALS(HostAndPort("A" SNOWMAN, 1), HostAndPort("a" SNOWMAN, 1)); + ASSERT_NOT_EQUALS(HostAndPort(SNOWMAN "B", 1), HostAndPort(SNOWMAN "a", 1)); + ASSERT_NOT_EQUALS(HostAndPort("B" SNOWMAN, 1), HostAndPort("a" SNOWMAN, 1)); +} + TEST(HostAndPort, ImplicitPortSelection) { ASSERT_EQUALS(HostAndPort("a", -1), HostAndPort("a", int(ServerGlobalParams::DefaultDBPort))); ASSERT_EQUALS(int(ServerGlobalParams::DefaultDBPort), HostAndPort("a", -1).port()); @@ -106,6 +127,9 @@ TEST(HostAndPort, StaticParseFunction) { HostAndPort("abc.def", 3421)); ASSERT_EQUALS(unittest::assertGet(HostAndPort::parse("[243:1bc]:21")), HostAndPort("243:1bc", 21)); + ASSERT_EQUALS(unittest::assertGet(HostAndPort::parse("aBcD:21")), HostAndPort("abcd", 21)); + ASSERT_EQUALS(unittest::assertGet(HostAndPort::parse("aB" SNOWMAN "cD:21")), + HostAndPort("ab" SNOWMAN "cd", 21)); } TEST(HostAndPort, RoundTripAbility) { diff --git a/src/mongo/util/str.h b/src/mongo/util/str.h index bddfff282b2..853fee0c53c 100644 --- a/src/mongo/util/str.h +++ b/src/mongo/util/str.h @@ -302,7 +302,8 @@ inline std::string toLower(StringData input) { for (std::string::size_type i = 0; i < sz; i++) { char c = input[i]; - copy[i] = (char)tolower((int)c); + // See https://en.cppreference.com/w/cpp/string/byte/tolower + copy[i] = static_cast<char>(tolower(static_cast<unsigned char>(c))); } copy[sz] = 0; return copy; |