From ad27c92e01758c96e7ace4cba13574e0d97a761d Mon Sep 17 00:00:00 2001 From: Mark Benvenuto Date: Thu, 4 Aug 2016 17:29:34 -0400 Subject: [PATCH] SERVER-24611 Implement ClientMetadata class --- jstests/core/client_metadata_ismaster.js | 12 + jstests/noPassthrough/currentop_query.js | 2 + src/mongo/base/error_codes.err | 4 + src/mongo/client/connection_pool.cpp | 2 +- src/mongo/client/connection_string.h | 4 +- .../client/connection_string_connect.cpp | 9 +- src/mongo/client/connpool.cpp | 4 +- src/mongo/client/dbclient.cpp | 24 +- src/mongo/client/dbclient_rs.cpp | 6 +- src/mongo/client/dbclient_rs.h | 2 + src/mongo/client/dbclient_rs_test.cpp | 52 +-- src/mongo/client/dbclientinterface.h | 7 +- src/mongo/client/mongo_uri.h | 2 +- src/mongo/client/mongo_uri_connect.cpp | 4 +- src/mongo/client/mongo_uri_test.cpp | 3 +- src/mongo/client/scoped_db_conn_test.cpp | 2 +- src/mongo/db/clientlistplugin.cpp | 11 + src/mongo/db/cloner.cpp | 2 +- src/mongo/db/commands/clone_collection.cpp | 2 +- src/mongo/db/commands/copydb.cpp | 2 +- .../db/commands/copydb_start_commands.cpp | 4 +- src/mongo/db/commands/current_op.cpp | 11 + src/mongo/db/repl/SConscript | 1 + src/mongo/db/repl/oplogreader.cpp | 2 +- src/mongo/db/repl/replication_info.cpp | 33 ++ src/mongo/db/repl/rollback_source_impl.cpp | 4 +- src/mongo/dbtests/mock/mock_conn_registry.cpp | 2 +- .../dbtests/mock/mock_dbclient_connection.cpp | 4 +- .../dbtests/mock/mock_dbclient_connection.h | 6 +- .../executor/network_interface_asio_auth.cpp | 3 + src/mongo/rpc/SConscript | 26 ++ src/mongo/rpc/metadata/client_metadata.cpp | 400 ++++++++++++++++++ src/mongo/rpc/metadata/client_metadata.h | 230 ++++++++++ .../rpc/metadata/client_metadata_ismaster.cpp | 77 ++++ .../rpc/metadata/client_metadata_ismaster.h | 89 ++++ .../rpc/metadata/client_metadata_test.cpp | 296 +++++++++++++ src/mongo/s/commands/SConscript | 1 + .../s/commands/cluster_is_master_cmd.cpp | 34 ++ src/mongo/scripting/mozjs/mongo.cpp | 2 +- src/mongo/shell/bench.cpp | 2 +- src/mongo/shell/shell_utils.cpp | 2 +- src/mongo/shell/shell_utils_launcher.cpp | 3 +- src/mongo/tools/sniffer.cpp | 2 +- 43 files changed, 1325 insertions(+), 65 deletions(-) create mode 100644 jstests/core/client_metadata_ismaster.js create mode 100644 src/mongo/rpc/metadata/client_metadata.cpp create mode 100644 src/mongo/rpc/metadata/client_metadata.h create mode 100644 src/mongo/rpc/metadata/client_metadata_ismaster.cpp create mode 100644 src/mongo/rpc/metadata/client_metadata_ismaster.h create mode 100644 src/mongo/rpc/metadata/client_metadata_test.cpp diff --git a/jstests/core/client_metadata_ismaster.js b/jstests/core/client_metadata_ismaster.js new file mode 100644 index 0000000000000..e5aa7d2547a6f --- /dev/null +++ b/jstests/core/client_metadata_ismaster.js @@ -0,0 +1,12 @@ +// Test that verifies client metadata behavior for isMaster + +(function() { + "use strict"; + + // Verify that a isMaster request fails if it contains client metadata, and it is not first. + // The shell sends isMaster on the first connection + var result = db.runCommand({"isMaster": 1, "client": {"application": "foobar"}}); + assert.commandFailed(result); + assert.eq(result.code, ErrorCodes.ClientMetadataCannotBeMutated, tojson(result)); + +})(); diff --git a/jstests/noPassthrough/currentop_query.js b/jstests/noPassthrough/currentop_query.js index 287678b4eab02..e01413ab58ce6 100644 --- a/jstests/noPassthrough/currentop_query.js +++ b/jstests/noPassthrough/currentop_query.js @@ -71,6 +71,8 @@ assert.commandWorked(result); if (result.inprog.length === 1) { + assert.eq(result.inprog[0].appName, "MongoDB Shell", tojson(result)); + return true; } diff --git a/src/mongo/base/error_codes.err b/src/mongo/base/error_codes.err index cee5d694d4db5..104ed4c8f84c7 100644 --- a/src/mongo/base/error_codes.err +++ b/src/mongo/base/error_codes.err @@ -181,6 +181,10 @@ error_code("WindowsPdhError", 179) error_code("BadPerfCounterPath", 180) error_code("AmbiguousIndexKeyPattern", 181) error_code("InvalidViewDefinition", 182); +error_code("ClientMetadataMissingField", 183) +error_code("ClientMetadataAppNameTooLarge", 184) +error_code("ClientMetadataDocumentTooLarge", 185) +error_code("ClientMetadataCannotBeMutated", 186) # Non-sequential error codes (for compatibility only) error_code("SocketException", 9001) diff --git a/src/mongo/client/connection_pool.cpp b/src/mongo/client/connection_pool.cpp index 5278ba4a854be..b70acd25dafbb 100644 --- a/src/mongo/client/connection_pool.cpp +++ b/src/mongo/client/connection_pool.cpp @@ -184,7 +184,7 @@ ConnectionPool::ConnectionList::iterator ConnectionPool::acquireConnection( // the number of seconds with a fractional part. conn->setSoTimeout(durationCount(timeout) / 1000.0); - uassertStatusOK(conn->connect(target)); + uassertStatusOK(conn->connect(target, StringData())); conn->port().setTag(conn->port().getTag() | _messagingPortTags); if (isInternalAuthSet()) { diff --git a/src/mongo/client/connection_string.h b/src/mongo/client/connection_string.h index 91836e36812f0..245174794d99e 100644 --- a/src/mongo/client/connection_string.h +++ b/src/mongo/client/connection_string.h @@ -116,7 +116,9 @@ class ConnectionString { bool operator==(const ConnectionString& other) const; bool operator!=(const ConnectionString& other) const; - DBClientBase* connect(std::string& errmsg, double socketTimeout = 0) const; + DBClientBase* connect(StringData applicationName, + std::string& errmsg, + double socketTimeout = 0) const; static StatusWith parse(const std::string& url); diff --git a/src/mongo/client/connection_string_connect.cpp b/src/mongo/client/connection_string_connect.cpp index 4356735492e1a..64c2258fc5d68 100644 --- a/src/mongo/client/connection_string_connect.cpp +++ b/src/mongo/client/connection_string_connect.cpp @@ -45,13 +45,15 @@ namespace mongo { stdx::mutex ConnectionString::_connectHookMutex; ConnectionString::ConnectionHook* ConnectionString::_connectHook = NULL; -DBClientBase* ConnectionString::connect(std::string& errmsg, double socketTimeout) const { +DBClientBase* ConnectionString::connect(StringData applicationName, + std::string& errmsg, + double socketTimeout) const { switch (_type) { case MASTER: { auto c = stdx::make_unique(true); c->setSoTimeout(socketTimeout); LOG(1) << "creating new connection to:" << _servers[0]; - if (!c->connect(_servers[0], errmsg)) { + if (!c->connect(_servers[0], applicationName, errmsg)) { return 0; } LOG(1) << "connected connection!"; @@ -59,7 +61,8 @@ DBClientBase* ConnectionString::connect(std::string& errmsg, double socketTimeou } case SET: { - auto set = stdx::make_unique(_setName, _servers, socketTimeout); + auto set = stdx::make_unique( + _setName, _servers, applicationName, socketTimeout); if (!set->connect()) { errmsg = "connect failed to replica set "; errmsg += toString(); diff --git a/src/mongo/client/connpool.cpp b/src/mongo/client/connpool.cpp index 411ff30177eba..6b35ea9f399be 100644 --- a/src/mongo/client/connpool.cpp +++ b/src/mongo/client/connpool.cpp @@ -235,7 +235,7 @@ DBClientBase* DBConnectionPool::get(const ConnectionString& url, double socketTi } string errmsg; - c = url.connect(errmsg, socketTimeout); + c = url.connect(StringData(), errmsg, socketTimeout); uassert(13328, _name + ": connect failed " + url.toString() + " : " + errmsg, c); return _finishCreate(url.toString(), socketTimeout, c); @@ -256,7 +256,7 @@ DBClientBase* DBConnectionPool::get(const string& host, double socketTimeout) { const ConnectionString cs(uassertStatusOK(ConnectionString::parse(host))); string errmsg; - c = cs.connect(errmsg, socketTimeout); + c = cs.connect(StringData(), errmsg, socketTimeout); if (!c) throw SocketException(SocketException::CONNECT_ERROR, host, diff --git a/src/mongo/client/dbclient.cpp b/src/mongo/client/dbclient.cpp index cbf72c4ae7d84..af53009904a65 100644 --- a/src/mongo/client/dbclient.cpp +++ b/src/mongo/client/dbclient.cpp @@ -55,6 +55,7 @@ #include "mongo/rpc/factory.h" #include "mongo/rpc/get_status_from_command_result.h" #include "mongo/rpc/metadata.h" +#include "mongo/rpc/metadata/client_metadata.h" #include "mongo/rpc/reply_interface.h" #include "mongo/rpc/request_builder_interface.h" #include "mongo/s/stale_exception.h" // for RecvStaleConfigException @@ -74,6 +75,7 @@ #include "mongo/util/password_digest.h" #include "mongo/util/represent_as.h" #include "mongo/util/time_support.h" +#include "mongo/util/version.h" namespace mongo { @@ -712,7 +714,8 @@ class ScopedForceOpQuery { /** * Initializes the wire version of conn, and returns the isMaster reply. */ -executor::RemoteCommandResponse initWireVersion(DBClientConnection* conn) { +executor::RemoteCommandResponse initWireVersion(DBClientConnection* conn, + StringData applicationName) { try { // We need to force the usage of OP_QUERY on this command, even if we have previously // detected support for OP_COMMAND on a connection. This is necessary to handle the case @@ -731,6 +734,12 @@ executor::RemoteCommandResponse initWireVersion(DBClientConnection* conn) { bob.append("hostInfo", sb.str()); } + Status serializeStatus = ClientMetadata::serialize( + "MongoDB Internal Client", mongo::versionString, applicationName, &bob); + if (!serializeStatus.isOK()) { + return serializeStatus; + } + Date_t start{Date_t::now()}; auto result = conn->runCommandWithMetadata("admin", "isMaster", rpc::makeEmptyMetadata(), bob.done()); @@ -754,8 +763,10 @@ executor::RemoteCommandResponse initWireVersion(DBClientConnection* conn) { } // namespace -bool DBClientConnection::connect(const HostAndPort& server, std::string& errmsg) { - auto connectStatus = connect(server); +bool DBClientConnection::connect(const HostAndPort& server, + StringData applicationName, + std::string& errmsg) { + auto connectStatus = connect(server, applicationName); if (!connectStatus.isOK()) { errmsg = connectStatus.reason(); return false; @@ -763,13 +774,14 @@ bool DBClientConnection::connect(const HostAndPort& server, std::string& errmsg) return true; } -Status DBClientConnection::connect(const HostAndPort& serverAddress) { +Status DBClientConnection::connect(const HostAndPort& serverAddress, StringData applicationName) { auto connectStatus = connectSocketOnly(serverAddress); if (!connectStatus.isOK()) { return connectStatus; } - auto swIsMasterReply = initWireVersion(this); + _applicationName = applicationName.toString(); + auto swIsMasterReply = initWireVersion(this, applicationName); if (!swIsMasterReply.isOK()) { _failed = true; return swIsMasterReply.status; @@ -903,7 +915,7 @@ void DBClientConnection::_checkConnection() { LOG(_logLevel) << "trying reconnect to " << toString() << endl; string errmsg; _failed = false; - auto connectStatus = connect(_serverAddress); + auto connectStatus = connect(_serverAddress, _applicationName); if (!connectStatus.isOK()) { _failed = true; LOG(_logLevel) << "reconnect " << toString() << " failed " << errmsg << endl; diff --git a/src/mongo/client/dbclient_rs.cpp b/src/mongo/client/dbclient_rs.cpp index 3c0015b763647..ae18bd070d052 100644 --- a/src/mongo/client/dbclient_rs.cpp +++ b/src/mongo/client/dbclient_rs.cpp @@ -135,8 +135,9 @@ bool DBClientReplicaSet::_authPooledSecondaryConn = true; DBClientReplicaSet::DBClientReplicaSet(const string& name, const vector& servers, + StringData applicationName, double so_timeout) - : _setName(name), _so_timeout(so_timeout) { + : _setName(name), _applicationName(applicationName.toString()), _so_timeout(so_timeout) { _rsm = ReplicaSetMonitor::createIfNeeded(name, set(servers.begin(), servers.end())); } @@ -303,7 +304,8 @@ DBClientConnection* DBClientReplicaSet::checkMaster() { // Needs to perform a dynamic_cast because we need to set the replSet // callback. We should eventually not need this after we remove the // callback. - newConn = dynamic_cast(connStr.connect(errmsg, _so_timeout)); + newConn = dynamic_cast( + connStr.connect(_applicationName, errmsg, _so_timeout)); } catch (const AssertionException& ex) { errmsg = ex.toString(); } diff --git a/src/mongo/client/dbclient_rs.h b/src/mongo/client/dbclient_rs.h index 12951a2cf692f..60e615d54dcf2 100644 --- a/src/mongo/client/dbclient_rs.h +++ b/src/mongo/client/dbclient_rs.h @@ -59,6 +59,7 @@ class DBClientReplicaSet : public DBClientBase { * connections. */ DBClientReplicaSet(const std::string& name, const std::vector& servers, + StringData applicationName, double so_timeout = 0); virtual ~DBClientReplicaSet(); @@ -295,6 +296,7 @@ class DBClientReplicaSet : public DBClientBase { ReplicaSetMonitorPtr _getMonitor(); std::string _setName; + std::string _applicationName; std::shared_ptr _rsm; HostAndPort _masterHost; diff --git a/src/mongo/client/dbclient_rs_test.cpp b/src/mongo/client/dbclient_rs_test.cpp index bb70542f3d2c7..b9500da611a20 100644 --- a/src/mongo/client/dbclient_rs_test.cpp +++ b/src/mongo/client/dbclient_rs_test.cpp @@ -99,7 +99,7 @@ class BasicRS : public unittest::Test { void assertOneOfNodesSelected(MockReplicaSet* replSet, ReadPreference rp, const std::vector hostNames) { - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); ReplicaSetMonitor::get(replSet->getSetName())->startOrContinueRefresh().refreshAll(); bool secondaryOk = (rp != ReadPreference::PrimaryOnly); auto tagSet = secondaryOk ? TagSet() : TagSet::primaryOnly(); @@ -119,7 +119,7 @@ void assertNodeSelected(MockReplicaSet* replSet, ReadPreference rp, StringData h TEST_F(BasicRS, QueryPrimary) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryOnly, BSONArray()); @@ -136,7 +136,7 @@ TEST_F(BasicRS, CommandPrimary) { TEST_F(BasicRS, QuerySecondaryOnly) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryOnly, BSONArray()); @@ -154,7 +154,7 @@ TEST_F(BasicRS, CommandSecondaryOnly) { TEST_F(BasicRS, QueryPrimaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); // Need up-to-date view, since either host is valid if view is stale. ReplicaSetMonitor::get(replSet->getSetName())->startOrContinueRefresh().refreshAll(); @@ -174,7 +174,7 @@ TEST_F(BasicRS, CommandPrimaryPreferred) { TEST_F(BasicRS, QuerySecondaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); // Need up-to-date view, since either host is valid if view is stale. ReplicaSetMonitor::get(replSet->getSetName())->startOrContinueRefresh().refreshAll(); @@ -232,7 +232,7 @@ void assertRunCommandWithReadPrefThrows(MockReplicaSet* replSet, ReadPreference bool secondaryOk = !isPrimaryOnly; TagSet ts = isPrimaryOnly ? TagSet::primaryOnly() : TagSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); ASSERT_THROWS(replConn.runCommandWithMetadata( "foo", "whoami", makeMetadata(rp, ts, secondaryOk), BSON("dbStats" << 1)), AssertionException); @@ -240,7 +240,7 @@ void assertRunCommandWithReadPrefThrows(MockReplicaSet* replSet, ReadPreference TEST_F(AllNodesDown, QueryPrimary) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryOnly, BSONArray()); @@ -253,7 +253,7 @@ TEST_F(AllNodesDown, CommandPrimary) { TEST_F(AllNodesDown, QuerySecondaryOnly) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryOnly, BSONArray()); @@ -266,7 +266,7 @@ TEST_F(AllNodesDown, CommandSecondaryOnly) { TEST_F(AllNodesDown, QueryPrimaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryPreferred, BSONArray()); @@ -279,7 +279,7 @@ TEST_F(AllNodesDown, CommandPrimaryPreferred) { TEST_F(AllNodesDown, QuerySecondaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryPreferred, BSONArray()); @@ -292,7 +292,7 @@ TEST_F(AllNodesDown, CommandSecondaryPreferred) { TEST_F(AllNodesDown, QueryNearest) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::Nearest, BSONArray()); @@ -333,7 +333,7 @@ class PrimaryDown : public unittest::Test { TEST_F(PrimaryDown, QueryPrimary) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryOnly, BSONArray()); @@ -346,7 +346,7 @@ TEST_F(PrimaryDown, CommandPrimary) { TEST_F(PrimaryDown, QuerySecondaryOnly) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryOnly, BSONArray()); @@ -364,7 +364,7 @@ TEST_F(PrimaryDown, CommandSecondaryOnly) { TEST_F(PrimaryDown, QueryPrimaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryPreferred, BSONArray()); @@ -382,7 +382,7 @@ TEST_F(PrimaryDown, CommandPrimaryPreferred) { TEST_F(PrimaryDown, QuerySecondaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryPreferred, BSONArray()); @@ -400,7 +400,7 @@ TEST_F(PrimaryDown, CommandSecondaryPreferred) { TEST_F(PrimaryDown, Nearest) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::Nearest, BSONArray()); @@ -440,7 +440,7 @@ class SecondaryDown : public unittest::Test { TEST_F(SecondaryDown, QueryPrimary) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryOnly, BSONArray()); @@ -457,7 +457,7 @@ TEST_F(SecondaryDown, CommandPrimary) { TEST_F(SecondaryDown, QuerySecondaryOnly) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryOnly, BSONArray()); @@ -470,7 +470,7 @@ TEST_F(SecondaryDown, CommandSecondaryOnly) { TEST_F(SecondaryDown, QueryPrimaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::PrimaryPreferred, BSONArray()); @@ -487,7 +487,7 @@ TEST_F(SecondaryDown, CommandPrimaryPreferred) { TEST_F(SecondaryDown, QuerySecondaryPreferred) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::SecondaryPreferred, BSONArray()); @@ -504,7 +504,7 @@ TEST_F(SecondaryDown, CommandSecondaryPreferred) { TEST_F(SecondaryDown, QueryNearest) { MockReplicaSet* replSet = getReplSet(); - DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts()); + DBClientReplicaSet replConn(replSet->getSetName(), replSet->getHosts(), StringData()); Query query; query.readPref(mongo::ReadPreference::Nearest, BSONArray()); @@ -647,7 +647,7 @@ TEST_F(TaggedFiveMemberRS, ConnShouldPinIfSameSettings) { vector seedList; seedList.push_back(HostAndPort(replSet->getPrimary())); - DBClientReplicaSet replConn(replSet->getSetName(), seedList); + DBClientReplicaSet replConn(replSet->getSetName(), seedList, StringData()); string dest; { @@ -675,7 +675,7 @@ TEST_F(TaggedFiveMemberRS, ConnShouldNotPinIfHostMarkedAsFailed) { vector seedList; seedList.push_back(HostAndPort(replSet->getPrimary())); - DBClientReplicaSet replConn(replSet->getSetName(), seedList); + DBClientReplicaSet replConn(replSet->getSetName(), seedList, StringData()); string dest; { @@ -708,7 +708,7 @@ TEST_F(TaggedFiveMemberRS, ConnShouldNotPinIfDiffMode) { vector seedList; seedList.push_back(HostAndPort(replSet->getPrimary())); - DBClientReplicaSet replConn(replSet->getSetName(), seedList); + DBClientReplicaSet replConn(replSet->getSetName(), seedList, StringData()); // Need up-to-date view to ensure there are multiple valid choices. ReplicaSetMonitor::get(replSet->getSetName())->startOrContinueRefresh().refreshAll(); @@ -740,7 +740,7 @@ TEST_F(TaggedFiveMemberRS, ConnShouldNotPinIfDiffTag) { vector seedList; seedList.push_back(HostAndPort(replSet->getPrimary())); - DBClientReplicaSet replConn(replSet->getSetName(), seedList); + DBClientReplicaSet replConn(replSet->getSetName(), seedList, StringData()); // Need up-to-date view to ensure there are multiple valid choices. ReplicaSetMonitor::get(replSet->getSetName())->startOrContinueRefresh().refreshAll(); @@ -776,7 +776,7 @@ TEST_F(TaggedFiveMemberRS, SlaveConnReturnsSecConn) { vector seedList; seedList.push_back(HostAndPort(replSet->getPrimary())); - DBClientReplicaSet replConn(replSet->getSetName(), seedList); + DBClientReplicaSet replConn(replSet->getSetName(), seedList, StringData()); // Need up-to-date view since slaveConn() uses SecondaryPreferred, and this test assumes it // knows about at least one secondary. diff --git a/src/mongo/client/dbclientinterface.h b/src/mongo/client/dbclientinterface.h index cc2be6c600edc..8eeef420cb0d2 100644 --- a/src/mongo/client/dbclientinterface.h +++ b/src/mongo/client/dbclientinterface.h @@ -975,7 +975,9 @@ class DBClientConnection : public DBClientBase { * @param errmsg any relevant error message will appended to the string * @return false if fails to connect. */ - virtual bool connect(const HostAndPort& server, std::string& errmsg); + virtual bool connect(const HostAndPort& server, + StringData applicationName, + std::string& errmsg); /** * Semantically equivalent to the previous connect method, but returns a Status @@ -986,7 +988,7 @@ class DBClientConnection : public DBClientBase { * @param a hook to validate the 'isMaster' reply received during connection. If the hook * fails, the connection will be terminated and a non-OK status will be returned. */ - Status connect(const HostAndPort& server); + Status connect(const HostAndPort& server, StringData applicationName); /** * This version of connect does not run 'isMaster' after creating a TCP connection to the @@ -1131,6 +1133,7 @@ class DBClientConnection : public DBClientBase { HostAndPort _serverAddress; std::string _resolvedAddress; + std::string _applicationName; void _checkConnection(); diff --git a/src/mongo/client/mongo_uri.h b/src/mongo/client/mongo_uri.h index 2898fed2cb21a..8a0574540c8c7 100644 --- a/src/mongo/client/mongo_uri.h +++ b/src/mongo/client/mongo_uri.h @@ -72,7 +72,7 @@ class MongoURI { static StatusWith parse(const std::string& url); - DBClientBase* connect(std::string& errmsg) const; + DBClientBase* connect(StringData applicationName, std::string& errmsg) const; const std::string& getUser() const { return _user; diff --git a/src/mongo/client/mongo_uri_connect.cpp b/src/mongo/client/mongo_uri_connect.cpp index d5b1bb0d0df18..7909e0bd5cdfe 100644 --- a/src/mongo/client/mongo_uri_connect.cpp +++ b/src/mongo/client/mongo_uri_connect.cpp @@ -164,7 +164,7 @@ BSONObj MongoURI::_makeAuthObjFromOptions(int maxWireVersion) const { return bob.obj(); } -DBClientBase* MongoURI::connect(std::string& errmsg) const { +DBClientBase* MongoURI::connect(StringData applicationName, std::string& errmsg) const { double socketTimeout = 0.0; OptionsMap::const_iterator it = _options.find("socketTimeoutMS"); @@ -177,7 +177,7 @@ DBClientBase* MongoURI::connect(std::string& errmsg) const { } } - auto ret = _connectString.connect(errmsg, socketTimeout); + auto ret = _connectString.connect(applicationName, errmsg, socketTimeout); if (!ret) { return ret; } diff --git a/src/mongo/client/mongo_uri_test.cpp b/src/mongo/client/mongo_uri_test.cpp index 81e626e4607b4..05d288818d860 100644 --- a/src/mongo/client/mongo_uri_test.cpp +++ b/src/mongo/client/mongo_uri_test.cpp @@ -30,6 +30,7 @@ #include "mongo/client/mongo_uri.h" +#include "mongo/base/string_data.h" #include "mongo/unittest/unittest.h" namespace { @@ -330,7 +331,7 @@ TEST(MongoURI, ValidButBadURIsFailToConnect) { ASSERT_TRUE(uri.isValid()); std::string errmsg; - auto dbclient = uri.connect(errmsg); + auto dbclient = uri.connect(mongo::StringData(), errmsg); ASSERT_EQ(dbclient, static_cast(nullptr)); } diff --git a/src/mongo/client/scoped_db_conn_test.cpp b/src/mongo/client/scoped_db_conn_test.cpp index ba54e4ec73309..cd86ed998398a 100644 --- a/src/mongo/client/scoped_db_conn_test.cpp +++ b/src/mongo/client/scoped_db_conn_test.cpp @@ -224,7 +224,7 @@ class DummyServerFixture : public unittest::Test { // Make sure the dummy server is up and running before proceeding while (true) { - auto connectStatus = conn.connect(HostAndPort{TARGET_HOST}); + auto connectStatus = conn.connect(HostAndPort{TARGET_HOST}, StringData()); if (connectStatus.isOK()) { break; } diff --git a/src/mongo/db/clientlistplugin.cpp b/src/mongo/db/clientlistplugin.cpp index 713e9a176f06b..8204eeb74a914 100644 --- a/src/mongo/db/clientlistplugin.cpp +++ b/src/mongo/db/clientlistplugin.cpp @@ -40,6 +40,8 @@ #include "mongo/db/operation_context.h" #include "mongo/db/service_context.h" #include "mongo/db/stats/fill_locker_info.h" +#include "mongo/rpc/metadata/client_metadata.h" +#include "mongo/rpc/metadata/client_metadata_ismaster.h" #include "mongo/util/mongoutils/html.h" #include "mongo/util/stringutils.h" @@ -199,6 +201,15 @@ class CurrentOpContexts : public Command { client->reportState(b); + const auto& clientMetadata = + ClientMetadataIsMasterState::get(client).getClientMetadata(); + if (clientMetadata) { + auto appName = clientMetadata.get().getApplicationName(); + if (!appName.empty()) { + b.append("applicationName", appName); + } + } + const OperationContext* txn = client->getOperationContext(); b.appendBool("active", static_cast(txn)); if (txn) { diff --git a/src/mongo/db/cloner.cpp b/src/mongo/db/cloner.cpp index e924fa7312b7a..d43b145d776f3 100644 --- a/src/mongo/db/cloner.cpp +++ b/src/mongo/db/cloner.cpp @@ -542,7 +542,7 @@ Status Cloner::copyDb(OperationContext* txn, // nothing to do } else if (!masterSameProcess) { std::string errmsg; - unique_ptr con(cs.connect(errmsg)); + unique_ptr con(cs.connect(StringData(), errmsg)); if (!con.get()) { return Status(ErrorCodes::HostUnreachable, errmsg); } diff --git a/src/mongo/db/commands/clone_collection.cpp b/src/mongo/db/commands/clone_collection.cpp index 76ef137767355..a6539a5c179c9 100644 --- a/src/mongo/db/commands/clone_collection.cpp +++ b/src/mongo/db/commands/clone_collection.cpp @@ -148,7 +148,7 @@ class CmdCloneCollection : public Command { Cloner cloner; unique_ptr myconn; myconn.reset(new DBClientConnection()); - if (!myconn->connect(HostAndPort(fromhost), errmsg)) + if (!myconn->connect(HostAndPort(fromhost), StringData(), errmsg)) return false; cloner.setConnection(myconn.release()); diff --git a/src/mongo/db/commands/copydb.cpp b/src/mongo/db/commands/copydb.cpp index 92d721b70e14a..9bfbde8d67aa0 100644 --- a/src/mongo/db/commands/copydb.cpp +++ b/src/mongo/db/commands/copydb.cpp @@ -201,7 +201,7 @@ class CmdCopyDb : public Command { // If fromSelf leave the cloner's conn empty, it will use a DBDirectClient instead. const ConnectionString cs(uassertStatusOK(ConnectionString::parse(fromhost))); - DBClientBase* conn = cs.connect(errmsg); + DBClientBase* conn = cs.connect(StringData(), errmsg); if (!conn) { return false; } diff --git a/src/mongo/db/commands/copydb_start_commands.cpp b/src/mongo/db/commands/copydb_start_commands.cpp index 8426b14d0722a..1af5e4be983c6 100644 --- a/src/mongo/db/commands/copydb_start_commands.cpp +++ b/src/mongo/db/commands/copydb_start_commands.cpp @@ -113,7 +113,7 @@ class CmdCopyDbGetNonce : public Command { const ConnectionString cs(uassertStatusOK(ConnectionString::parse(fromhost))); auto& authConn = CopyDbAuthConnection::forClient(txn->getClient()); - authConn.reset(cs.connect(errmsg)); + authConn.reset(cs.connect(StringData(), errmsg)); if (!authConn) { return false; } @@ -202,7 +202,7 @@ class CmdCopyDbSaslStart : public Command { } auto& authConn = CopyDbAuthConnection::forClient(txn->getClient()); - authConn.reset(cs.connect(errmsg)); + authConn.reset(cs.connect(StringData(), errmsg)); if (!authConn.get()) { return false; } diff --git a/src/mongo/db/commands/current_op.cpp b/src/mongo/db/commands/current_op.cpp index 10cf0f81272f1..555fbd1efe5e5 100644 --- a/src/mongo/db/commands/current_op.cpp +++ b/src/mongo/db/commands/current_op.cpp @@ -45,6 +45,8 @@ #include "mongo/db/namespace_string.h" #include "mongo/db/operation_context.h" #include "mongo/db/stats/fill_locker_info.h" +#include "mongo/rpc/metadata/client_metadata.h" +#include "mongo/rpc/metadata/client_metadata_ismaster.h" #include "mongo/util/log.h" namespace mongo { @@ -146,6 +148,15 @@ class CurrentOpCommand : public Command { // The client information client->reportState(infoBuilder); + const auto& clientMetadata = + ClientMetadataIsMasterState::get(txn->getClient()).getClientMetadata(); + if (clientMetadata) { + auto appName = clientMetadata.get().getApplicationName(); + if (!appName.empty()) { + infoBuilder.append("appName", appName); + } + } + // Operation context specific information infoBuilder.appendBool("active", static_cast(opCtx)); if (opCtx) { diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index 959f4e07611bd..3fd9ec9a787e9 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -1072,6 +1072,7 @@ env.Library( '$BUILD_DIR/mongo/client/clientdriver', '$BUILD_DIR/mongo/db/auth/authcore', '$BUILD_DIR/mongo/db/commands', + '$BUILD_DIR/mongo/rpc/client_metadata', '$BUILD_DIR/mongo/db/concurrency/lock_manager', '$BUILD_DIR/mongo/db/curop', '$BUILD_DIR/mongo/db/lasterror', diff --git a/src/mongo/db/repl/oplogreader.cpp b/src/mongo/db/repl/oplogreader.cpp index 749c6112db977..cb83a12d67603 100644 --- a/src/mongo/db/repl/oplogreader.cpp +++ b/src/mongo/db/repl/oplogreader.cpp @@ -93,7 +93,7 @@ bool OplogReader::connect(const HostAndPort& host) { _conn = shared_ptr( new DBClientConnection(false, durationCount(kSocketTimeout))); string errmsg; - if (!_conn->connect(host, errmsg) || !replAuthenticate(_conn.get())) { + if (!_conn->connect(host, StringData(), errmsg) || !replAuthenticate(_conn.get())) { resetConnection(); error() << errmsg << endl; return false; diff --git a/src/mongo/db/repl/replication_info.cpp b/src/mongo/db/repl/replication_info.cpp index be4ddb9e3e453..cb74b9ec9af2e 100644 --- a/src/mongo/db/repl/replication_info.cpp +++ b/src/mongo/db/repl/replication_info.cpp @@ -25,6 +25,7 @@ * 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::kFTDC #include "mongo/platform/basic.h" @@ -50,6 +51,8 @@ #include "mongo/db/storage/storage_options.h" #include "mongo/db/wire_version.h" #include "mongo/executor/network_interface.h" +#include "mongo/rpc/metadata/client_metadata.h" +#include "mongo/rpc/metadata/client_metadata_ismaster.h" #include "mongo/s/write_ops/batched_command_request.h" #include "mongo/util/map_util.h" @@ -243,6 +246,36 @@ class CmdIsMaster : public Command { executor::NetworkInterface::kMessagingPortKeepOpen); } } + + auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(txn->getClient()); + bool seenIsMaster = clientMetadataIsMasterState.hasSeenIsMaster(); + if (!seenIsMaster) { + clientMetadataIsMasterState.setSeenIsMaster(); + } + + BSONElement element = cmdObj[kMetadataDocumentName]; + if (!element.eoo()) { + if (seenIsMaster) { + return Command::appendCommandStatus( + result, + Status(ErrorCodes::ClientMetadataCannotBeMutated, + "The client metadata document may only be sent in the first isMaster")); + } + + auto swParseClientMetadata = ClientMetadata::parse(element); + + if (!swParseClientMetadata.getStatus().isOK()) { + return Command::appendCommandStatus(result, swParseClientMetadata.getStatus()); + } + + invariant(swParseClientMetadata.getValue()); + + swParseClientMetadata.getValue().get().logClientMetadata(txn->getClient()); + + clientMetadataIsMasterState.setClientMetadata( + txn->getClient(), std::move(swParseClientMetadata.getValue())); + } + appendReplicationInfo(txn, result, 0); if (serverGlobalParams.clusterRole == ClusterRole::ConfigServer) { diff --git a/src/mongo/db/repl/rollback_source_impl.cpp b/src/mongo/db/repl/rollback_source_impl.cpp index f416af8c716f5..27513953dcaa8 100644 --- a/src/mongo/db/repl/rollback_source_impl.cpp +++ b/src/mongo/db/repl/rollback_source_impl.cpp @@ -72,7 +72,9 @@ void RollbackSourceImpl::copyCollectionFromRemote(OperationContext* txn, const NamespaceString& nss) const { std::string errmsg; std::unique_ptr tmpConn(new DBClientConnection()); - uassert(15908, errmsg, tmpConn->connect(_source, errmsg) && replAuthenticate(tmpConn.get())); + uassert(15908, + errmsg, + tmpConn->connect(_source, StringData(), errmsg) && replAuthenticate(tmpConn.get())); // cloner owns _conn in unique_ptr Cloner cloner; diff --git a/src/mongo/dbtests/mock/mock_conn_registry.cpp b/src/mongo/dbtests/mock/mock_conn_registry.cpp index 2a34309a0443f..61388e337f749 100644 --- a/src/mongo/dbtests/mock/mock_conn_registry.cpp +++ b/src/mongo/dbtests/mock/mock_conn_registry.cpp @@ -91,7 +91,7 @@ mongo::DBClientBase* MockConnRegistry::MockConnHook::connect(const ConnectionStr const string hostName(connString.toString()); MockDBClientConnection* conn = _registry->connect(hostName); - if (!conn->connect(hostName.c_str(), errmsg)) { + if (!conn->connect(hostName.c_str(), StringData(), errmsg)) { // Assumption: connect never throws, so no leak. delete conn; diff --git a/src/mongo/dbtests/mock/mock_dbclient_connection.cpp b/src/mongo/dbtests/mock/mock_dbclient_connection.cpp index 5815f9d959e20..8fbcb3319b523 100644 --- a/src/mongo/dbtests/mock/mock_dbclient_connection.cpp +++ b/src/mongo/dbtests/mock/mock_dbclient_connection.cpp @@ -46,7 +46,9 @@ MockDBClientConnection::MockDBClientConnection(MockRemoteDBServer* remoteServer, MockDBClientConnection::~MockDBClientConnection() {} -bool MockDBClientConnection::connect(const char* hostName, std::string& errmsg) { +bool MockDBClientConnection::connect(const char* hostName, + StringData applicationName, + std::string& errmsg) { if (_remoteServer->isRunning()) { _remoteServerInstanceID = _remoteServer->getInstanceID(); return true; diff --git a/src/mongo/dbtests/mock/mock_dbclient_connection.h b/src/mongo/dbtests/mock/mock_dbclient_connection.h index 21e2bdc3c8377..d680fbdad997f 100644 --- a/src/mongo/dbtests/mock/mock_dbclient_connection.h +++ b/src/mongo/dbtests/mock/mock_dbclient_connection.h @@ -58,10 +58,10 @@ class MockDBClientConnection : public mongo::DBClientConnection { // DBClientBase methods // - bool connect(const char* hostName, std::string& errmsg); + bool connect(const char* hostName, StringData applicationName, std::string& errmsg); - inline bool connect(const HostAndPort& host, std::string& errmsg) { - return connect(host.toString().c_str(), errmsg); + inline bool connect(const HostAndPort& host, StringData applicationName, std::string& errmsg) { + return connect(host.toString().c_str(), applicationName, errmsg); } bool runCommand(const std::string& dbname, diff --git a/src/mongo/executor/network_interface_asio_auth.cpp b/src/mongo/executor/network_interface_asio_auth.cpp index 5f5fe523f9ce7..5dacda44e20ff 100644 --- a/src/mongo/executor/network_interface_asio_auth.cpp +++ b/src/mongo/executor/network_interface_asio_auth.cpp @@ -42,10 +42,12 @@ #include "mongo/rpc/factory.h" #include "mongo/rpc/get_status_from_command_result.h" #include "mongo/rpc/legacy_request_builder.h" +#include "mongo/rpc/metadata/client_metadata.h" #include "mongo/rpc/reply_interface.h" #include "mongo/stdx/memory.h" #include "mongo/util/log.h" #include "mongo/util/net/ssl_manager.h" +#include "mongo/util/version.h" namespace mongo { namespace executor { @@ -62,6 +64,7 @@ void NetworkInterfaceASIO::_runIsMaster(AsyncOp* op) { BSONObjBuilder bob; bob.append("isMaster", 1); bob.append("hangUpOnStepDown", false); + ClientMetadata::serialize(_options.instanceName, mongo::versionString, &bob); if (Command::testCommandsEnabled) { // Only include the host:port of this process in the isMaster command request if test diff --git a/src/mongo/rpc/SConscript b/src/mongo/rpc/SConscript index 7822e2161892f..c3b2b73ddaff8 100644 --- a/src/mongo/rpc/SConscript +++ b/src/mongo/rpc/SConscript @@ -139,6 +139,7 @@ env.Library( 'metadata/repl_set_metadata.cpp', ], LIBDEPS=[ + 'client_metadata', '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/bson/util/bson_extract', '$BUILD_DIR/mongo/client/read_preference', @@ -194,3 +195,28 @@ env.CppUnitTest( ], LIBDEPS=['metadata'] ) + +env.Library( + target='client_metadata', + source=[ + 'metadata/client_metadata.cpp', + 'metadata/client_metadata_ismaster.cpp', + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + "$BUILD_DIR/mongo/util/concurrency/spin_lock", + '$BUILD_DIR/mongo/util/decorable', + '$BUILD_DIR/mongo/util/net/hostandport', + "$BUILD_DIR/mongo/util/processinfo", + ], +) + +env.CppUnitTest( + target='client_metadata_test', + source=[ + 'metadata/client_metadata_test.cpp', + ], + LIBDEPS=[ + 'client_metadata', + ] +) diff --git a/src/mongo/rpc/metadata/client_metadata.cpp b/src/mongo/rpc/metadata/client_metadata.cpp new file mode 100644 index 0000000000000..ea03d5e0f84e8 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata.cpp @@ -0,0 +1,400 @@ +/** + * Copyright (C) 2016 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/rpc/metadata/client_metadata.h" + +#include + +#include "mongo/base/status.h" +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/db/operation_context.h" +#include "mongo/util/log.h" +#include "mongo/util/mongoutils/str.h" +#include "mongo/util/processinfo.h" + +namespace mongo { + +namespace { + +constexpr auto kApplication = "application"_sd; +constexpr auto kDriver = "driver"_sd; +constexpr auto kOperatingSystem = "os"_sd; + +constexpr auto kArchitecture = "architecture"_sd; +constexpr auto kName = "name"_sd; +constexpr auto kType = "type"_sd; +constexpr auto kVersion = "version"_sd; + +constexpr uint32_t kMaxMetadataDocumentByteLength = 512U; +constexpr uint32_t kMaxApplicationNameByteLength = 128U; + +} // namespace + +StatusWith> ClientMetadata::parse(const BSONElement& element) { + if (element.eoo()) { + return {boost::none}; + } + + if (!element.isABSONObj()) { + return Status(ErrorCodes::TypeMismatch, "The client metadata document must be a document"); + } + + ClientMetadata clientMetadata; + Status s = clientMetadata.parseClientMetadataDocument(element.Obj()); + if (!s.isOK()) { + return s; + } + + return {std::move(clientMetadata)}; +} + +Status ClientMetadata::parseClientMetadataDocument(const BSONObj& doc) { + if (static_cast(doc.objsize()) > kMaxMetadataDocumentByteLength) { + return Status(ErrorCodes::ClientMetadataDocumentTooLarge, + str::stream() << "The client metadata document must be less then or equal to " + << kMaxMetadataDocumentByteLength + << "bytes"); + } + + // Get a copy so that we can take a stable reference to the app name inside + BSONObj docOwned = doc.getOwned(); + + StringData appName; + bool foundDriver = false; + bool foundOperatingSystem = false; + + BSONObjIterator i(docOwned); + while (i.more()) { + BSONElement e = i.next(); + StringData name = e.fieldNameStringData(); + + if (name == kApplication) { + // Application is an optional sub-document, but we require it to be a document if + // specified. + if (!e.isABSONObj()) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "The '" << kApplication + << "' field is required to be a BSON document in the " + "client metadata document"); + } + + auto swAppName = parseApplicationDocument(e.Obj()); + if (!swAppName.getStatus().isOK()) { + return swAppName.getStatus(); + } + + appName = swAppName.getValue(); + + } else if (name == kDriver) { + if (!e.isABSONObj()) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "The '" << kDriver << "' field is required to be a " + "BSON document in the client " + "metadata document"); + } + + Status s = validateDriverDocument(e.Obj()); + if (!s.isOK()) { + return s; + } + + foundDriver = true; + } else if (name == kOperatingSystem) { + if (!e.isABSONObj()) { + return Status(ErrorCodes::TypeMismatch, + str::stream() << "The '" << kOperatingSystem + << "' field is required to be a BSON document in the " + "client metadata document"); + } + + Status s = validateOperatingSystemDocument(e.Obj()); + if (!s.isOK()) { + return s; + } + + foundOperatingSystem = true; + } + + // Ignore other fields as extra fields are allowed. + } + + // Driver is a required sub document. + if (!foundDriver) { + return Status(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required sub-document '" << kDriver + << "' in the client metadata document"); + } + + // OS is a required sub document. + if (!foundOperatingSystem) { + return Status(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required sub-document '" << kOperatingSystem + << "' in the client metadata document"); + } + + _document = std::move(docOwned); + _appName = std::move(appName); + + return Status::OK(); +} + +StatusWith ClientMetadata::parseApplicationDocument(const BSONObj& doc) { + BSONObjIterator i(doc); + + while (i.more()) { + BSONElement e = i.next(); + StringData name = e.fieldNameStringData(); + + // Name is the only required field, and any other fields are simply ignored. + if (name == kName) { + + if (e.type() != String) { + return { + ErrorCodes::TypeMismatch, + str::stream() << "The '" << kApplication << "." << kName + << "' field must be a string in the client metadata document"}; + } + + StringData value = e.checkAndGetStringData(); + + if (value.size() > kMaxApplicationNameByteLength) { + return {ErrorCodes::ClientMetadataAppNameTooLarge, + str::stream() << "The '" << kApplication << "." << kName + << "' field must be less then or equal to " + << kMaxApplicationNameByteLength + << " bytes in the client metadata document"}; + } + + return {std::move(value)}; + } + } + + return {StringData()}; +} + +Status ClientMetadata::validateDriverDocument(const BSONObj& doc) { + bool foundName = false; + bool foundVersion = false; + + BSONObjIterator i(doc); + while (i.more()) { + BSONElement e = i.next(); + StringData name = e.fieldNameStringData(); + + if (name == kName) { + if (e.type() != String) { + return Status( + ErrorCodes::TypeMismatch, + str::stream() << "The '" << kDriver << "." << kName + << "' field must be a string in the client metadata document"); + } + + foundName = true; + } else if (name == kVersion) { + if (e.type() != String) { + return Status( + ErrorCodes::TypeMismatch, + str::stream() << "The '" << kDriver << "." << kVersion + << "' field must be a string in the client metadata document"); + } + + foundVersion = true; + } + } + + if (foundName == false) { + return Status(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required field '" << kDriver << "." << kName + << "' in the client metadata document"); + } + + if (foundVersion == false) { + return Status(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required field '" << kDriver << "." << kVersion + << "' in the client metadata document"); + } + + return Status::OK(); +} + +Status ClientMetadata::validateOperatingSystemDocument(const BSONObj& doc) { + bool foundType = false; + + BSONObjIterator i(doc); + while (i.more()) { + BSONElement e = i.next(); + StringData name = e.fieldNameStringData(); + + if (name == kType) { + if (e.type() != String) { + return Status( + ErrorCodes::TypeMismatch, + str::stream() << "The '" << kOperatingSystem << "." << kType + << "' field must be a string in the client metadata document"); + } + + foundType = true; + } + } + + if (foundType == false) { + return Status(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required field '" << kOperatingSystem << "." + << kType + << "' in the client metadata document"); + } + + return Status::OK(); +} + +void ClientMetadata::serialize(StringData driverName, + StringData driverVersion, + BSONObjBuilder* builder) { + + ProcessInfo processInfo; + + serializePrivate(driverName, + driverVersion, + processInfo.getOsType(), + processInfo.getOsName(), + processInfo.getArch(), + processInfo.getOsVersion(), + builder); +} + +void ClientMetadata::serializePrivate(StringData driverName, + StringData driverVersion, + StringData osType, + StringData osName, + StringData osArchitecture, + StringData osVersion, + BSONObjBuilder* builder) { + invariant(!driverName.empty() && !driverVersion.empty() && !osType.empty() && !osName.empty() && + !osArchitecture.empty() && !osVersion.empty()); + + BSONObjBuilder metaObjBuilder(builder->subobjStart(kMetadataDocumentName)); + + { + BSONObjBuilder subObjBuilder(metaObjBuilder.subobjStart(kDriver)); + subObjBuilder.append(kName, driverName); + subObjBuilder.append(kVersion, driverVersion); + } + + { + BSONObjBuilder subObjBuilder(metaObjBuilder.subobjStart(kOperatingSystem)); + subObjBuilder.append(kType, osType); + subObjBuilder.append(kName, osName); + subObjBuilder.append(kArchitecture, osArchitecture); + subObjBuilder.append(kVersion, osVersion); + } +} + +Status ClientMetadata::serialize(StringData driverName, + StringData driverVersion, + StringData appName, + BSONObjBuilder* builder) { + + ProcessInfo processInfo; + + return serializePrivate(driverName, + driverVersion, + processInfo.getOsType(), + processInfo.getOsName(), + processInfo.getArch(), + processInfo.getOsVersion(), + appName, + builder); +} + +Status ClientMetadata::serializePrivate(StringData driverName, + StringData driverVersion, + StringData osType, + StringData osName, + StringData osArchitecture, + StringData osVersion, + StringData appName, + BSONObjBuilder* builder) { + invariant(!driverName.empty() && !driverVersion.empty() && !osType.empty() && !osName.empty() && + !osArchitecture.empty() && !osVersion.empty()); + + if (appName.size() > kMaxApplicationNameByteLength) { + return Status(ErrorCodes::ClientMetadataAppNameTooLarge, + str::stream() << "The '" << kApplication << "." << kName + << "' field must be less then or equal to " + << kMaxApplicationNameByteLength + << " bytes in the client metadata document"); + } + + { + BSONObjBuilder metaObjBuilder(builder->subobjStart(kMetadataDocumentName)); + + if (!appName.empty()) { + BSONObjBuilder subObjBuilder(metaObjBuilder.subobjStart(kApplication)); + subObjBuilder.append(kName, appName); + } + + { + BSONObjBuilder subObjBuilder(metaObjBuilder.subobjStart(kDriver)); + subObjBuilder.append(kName, driverName); + subObjBuilder.append(kVersion, driverVersion); + } + + { + BSONObjBuilder subObjBuilder(metaObjBuilder.subobjStart(kOperatingSystem)); + subObjBuilder.append(kType, osType); + subObjBuilder.append(kName, osName); + subObjBuilder.append(kArchitecture, osArchitecture); + subObjBuilder.append(kVersion, osVersion); + } + } + + return Status::OK(); +} + +StringData ClientMetadata::getApplicationName() const { + return _appName; +} + +const BSONObj& ClientMetadata::getDocument() const { + return _document; +} + +void ClientMetadata::logClientMetadata(Client* client) const { + invariant(!getDocument().isEmpty()); + log() << "received client metadata from " << client->getRemote().toString() << " " + << client->desc() << ": " << getDocument(); +} + +} // namespace mongo diff --git a/src/mongo/rpc/metadata/client_metadata.h b/src/mongo/rpc/metadata/client_metadata.h new file mode 100644 index 0000000000000..92646e3a88d47 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata.h @@ -0,0 +1,230 @@ +/** + * Copyright (C) 2016 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. + */ + +#pragma once + +#include + +#include "mongo/base/disallow_copying.h" +#include "mongo/base/status.h" +#include "mongo/base/status_with.h" +#include "mongo/base/string_data.h" +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" + +namespace mongo { + +class Client; +class ClientBasic; +class OperationContext; + +constexpr auto kMetadataDocumentName = "client"_sd; + +/** + * The ClientMetadata class is responsible for parsing the client metadata document that is received + * in isMaster from clients. This class also provides static methods for client libraries to create + * a valid client metadata document. + * + * Example document of isMaster request with client metadata document: + * { + * "isMaster" : 1, + * "client" : { + * "application" : { // Optional + * "name" : "string" // Optional with caveats + * }, + * "driver" : { // Required, Informational Only + * "name" : "string", // Required, Informational Only + * "version" : "string" // Required, Informational Only + * }, + * "os" : { // Required, Informational Only + * "type" : "string", // Required, Informational Only, See note + * "name" : "string", // Optional, Informational Only + * "architecture" : "string", // Optional, Informational Only + * "version" : "string" // Optional, Informational Only + * } + * } + * } + * + * For this classes' purposes, the client metadata document is the sub-document in "client". It is + * allowed to contain additional fields that are not listed in the example above. These additional + * fields are ignore by this class. The "os" document "type" field is required (defaults to + * "unknown" in Mongo Drivers). The "driver", and "os" documents while required, are for + * informational purposes only. The content is logged to disk but otherwise ignored. + * + * See Driver Specification: "MongoDB Handshake" for more information. + */ +class ClientMetadata { + MONGO_DISALLOW_COPYING(ClientMetadata); + +public: + ClientMetadata(ClientMetadata&&) = default; + ClientMetadata& operator=(ClientMetadata&&) = default; + + /** + * Parse and validate a client metadata document contained in an isMaster request. + * + * Empty or non-existent sub-documents are permitted. Non-empty documents are required to have + * the fields driver.name, driver.version, and os.type which must be strings. + * + * Returns an empty optional if element is empty. + */ + static StatusWith> parse(const BSONElement& element); + + /** + * Create a new client metadata document with os information from the ProcessInfo class. + * + * This method outputs the "client" field, and client metadata sub-document in the + * BSONObjBuilder: + * + * "client" : { + * "driver" : { + * "name" : "string", + * "version" : "string" + * }, + * "os" : { + * "type" : "string", + * "name" : "string", + * "architecture" : "string", + * "version" : "string" + * } + * } + */ + static void serialize(StringData driverName, StringData driverVersion, BSONObjBuilder* builder); + + /** + * Create a new client metadata document with os information from the ProcessInfo class. + * + * driverName - name of the driver, must not be empty + * driverVersion - a string for the driver version, must not be empty + * + * Notes: appName must be <= 128 bytes otherwise an error is returned. It may be empty in which + * case it is omitted from the output document. + * + * This method outputs the "client" field, and client metadata sub-document in the + * BSONObjBuilder: + * + * "client" : { + * "application" : { + * "name" : "string" + * }, + * "driver" : { + * "name" : "string", + * "version" : "string" + * }, + * "os" : { + * "type" : "string", + * "name" : "string", + * "architecture" : "string", + * "version" : "string" + * } + * } + */ + static Status serialize(StringData driverName, + StringData driverVersion, + StringData appName, + BSONObjBuilder* builder); + + /** + * Get the Application Name for the client metadata document. + * + * Used to log Application Name in slow operation reports, and into system.profile. + * Return: May be empty. + */ + StringData getApplicationName() const; + + /** + * Get the BSON Document of the client metadata document. In the example above in the class + * comment, this is the document in the "client" field. + * + * Return: May be empty. + */ + const BSONObj& getDocument() const; + + /** + * Log client and client metadata information to disk. + */ + void logClientMetadata(Client* client) const; + +public: + /** + * Create a new client metadata document. + * + * Exposed for Unit Test purposes + */ + static void serializePrivate(StringData driverName, + StringData driverVersion, + StringData osType, + StringData osName, + StringData osArchitecture, + StringData osVersion, + BSONObjBuilder* builder); + + /** + * Create a new client metadata document. + * + * driverName - name of the driver + * driverVersion - a string for the driver version + * osType - name of host operating system of client, i.e. uname -s + * osName - name of operating system distro, i.e. "Ubuntu..." or "Microsoft Windows 8" + * osArchitecture - architecture of host operating system, i.e. uname -p + * osVersion - operating system version, i.e. uname -v + * + * Notes: appName must be <= 128 bytes otherwise an error is returned. It may be empty in which + * case it is omitted from the output document. All other fields must not be empty. + * + * Exposed for Unit Test purposes + */ + static Status serializePrivate(StringData driverName, + StringData driverVersion, + StringData osType, + StringData osName, + StringData osArchitecture, + StringData osVersion, + StringData appName, + BSONObjBuilder* builder); + +private: + ClientMetadata() = default; + + Status parseClientMetadataDocument(const BSONObj& doc); + static Status validateDriverDocument(const BSONObj& doc); + static Status validateOperatingSystemDocument(const BSONObj& doc); + static StatusWith parseApplicationDocument(const BSONObj& doc); + +private: + // Parsed Client Metadata document + // May be empty + // Owned + BSONObj _document; + + // Application Name extracted from the client metadata document. + // May be empty + StringData _appName; +}; + +} // namespace mongo diff --git a/src/mongo/rpc/metadata/client_metadata_ismaster.cpp b/src/mongo/rpc/metadata/client_metadata_ismaster.cpp new file mode 100644 index 0000000000000..53164ed33d262 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata_ismaster.cpp @@ -0,0 +1,77 @@ +/** + * Copyright (C) 2016 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. + */ + +#include "mongo/platform/basic.h" + +#include "mongo/rpc/metadata/client_metadata_ismaster.h" + +#include + +#include "mongo/base/init.h" +#include "mongo/base/status.h" +#include "mongo/db/client.h" +#include "mongo/db/operation_context.h" +#include "mongo/db/service_context.h" +#include "mongo/stdx/memory.h" + +namespace mongo { + +namespace { + +const auto getClientMetadataIsMasterState = + ClientBasic::declareDecoration(); + +} // namespace + +ClientMetadataIsMasterState& ClientMetadataIsMasterState::get(ClientBasic* client) { + return getClientMetadataIsMasterState(*client); +} + +bool ClientMetadataIsMasterState::hasSeenIsMaster() const { + return _hasSeenIsMaster; +} + +void ClientMetadataIsMasterState::setSeenIsMaster() { + invariant(!_hasSeenIsMaster); + _hasSeenIsMaster = true; +} + +const boost::optional& ClientMetadataIsMasterState::getClientMetadata() const { + return _clientMetadata; +} + +void ClientMetadataIsMasterState::setClientMetadata( + Client* client, boost::optional clientMetadata) { + auto& state = get(client); + + stdx::lock_guard lk(*client); + state._clientMetadata = std::move(clientMetadata); +} + + +} // namespace mongo diff --git a/src/mongo/rpc/metadata/client_metadata_ismaster.h b/src/mongo/rpc/metadata/client_metadata_ismaster.h new file mode 100644 index 0000000000000..9bd274ae4f6b1 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata_ismaster.h @@ -0,0 +1,89 @@ +/** +* Copyright (C) 2016 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. +*/ + +#pragma once + +#include +#include +#include + +#include "mongo/base/disallow_copying.h" +#include "mongo/rpc/metadata/client_metadata.h" + +namespace mongo { + +class Client; +class ClientBasic; + +/** + * ClientMetadataIsMasterState is responsible for tracking whether the client metadata document has + * been received by the specified Client object. + */ +class ClientMetadataIsMasterState { + MONGO_DISALLOW_COPYING(ClientMetadataIsMasterState); + +public: + ClientMetadataIsMasterState() = default; + + static ClientMetadataIsMasterState& get(ClientBasic* client); + + /** + * Get the optional client metadata object. + */ + const boost::optional& getClientMetadata() const; + + /** + * Set the optional client metadata object. + */ + static void setClientMetadata(Client* client, boost::optional clientMetadata); + + /** + * Check a flag to indicate that isMaster has been seen for this Client. + */ + bool hasSeenIsMaster() const; + + /** + * Set a flag to indicate that isMaster has been seen for this Client. + */ + void setSeenIsMaster(); + +private: + // Optional client metadata document. + // Set if client sees isMaster cmd or as part of OP_Command processing. + // Thread-Safety: + // Can be read and written from the thread owning "Client". + // Can be read from other threads if they hold the "Client" lock. + boost::optional _clientMetadata{boost::none}; + + // Indicates whether we have seen an is master for this client. + // Thread-Safety: + // None - must be only be read and written from the thread owning "Client". + bool _hasSeenIsMaster{false}; +}; + +} // namespace mongo diff --git a/src/mongo/rpc/metadata/client_metadata_test.cpp b/src/mongo/rpc/metadata/client_metadata_test.cpp new file mode 100644 index 0000000000000..8d64e5e769786 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata_test.cpp @@ -0,0 +1,296 @@ +/** + * Copyright (C) 2016 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::kDefault + +#include "mongo/platform/basic.h" + +#include "mongo/rpc/metadata/client_metadata.h" + +#include +#include + +#include "mongo/bson/bsonobj.h" +#include "mongo/bson/bsonobjbuilder.h" +#include "mongo/unittest/unittest.h" +#include "mongo/util/log.h" + +namespace mongo { + +constexpr auto kMetadataDoc = "client"_sd; +constexpr auto kApplication = "application"_sd; +constexpr auto kDriver = "driver"_sd; +constexpr auto kName = "name"_sd; +constexpr auto kType = "type"_sd; +constexpr auto kVersion = "version"_sd; +constexpr auto kOperatingSystem = "os"_sd; +constexpr auto kArchitecture = "architecture"_sd; + +constexpr auto kUnknown = "unkown"_sd; + +#define ASSERT_DOC_OK(...) \ + do { \ + auto _swParseStatus = \ + ClientMetadata::parse(BSON(kMetadataDoc << BSON(__VA_ARGS__))[kMetadataDoc]); \ + ASSERT_OK(_swParseStatus.getStatus()); \ + } while (0) + +#define ASSERT_DOC_NOT_OK(...) \ + do { \ + auto _swParseStatus = \ + ClientMetadata::parse(BSON(kMetadataDoc << BSON(__VA_ARGS__))[kMetadataDoc]); \ + ASSERT_NOT_OK(_swParseStatus.getStatus()); \ + } while (0) + + +TEST(ClientMetadatTest, TestLoopbackTest) { + // Serialize without application name + { + BSONObjBuilder builder; + ASSERT_OK(ClientMetadata::serializePrivate("a", "b", "c", "d", "e", "f", "g", &builder)); + + auto obj = builder.obj(); + auto swParseStatus = ClientMetadata::parse(obj[kMetadataDoc]); + ASSERT_OK(swParseStatus.getStatus()); + ASSERT_EQUALS("g", swParseStatus.getValue().get().getApplicationName()); + + BSONObj outDoc = + BSON(kMetadataDoc << BSON( + kApplication << BSON(kName << "g") << kDriver + << BSON(kName << "a" << kVersion << "b") + << kOperatingSystem + << BSON(kType << "c" << kName << "d" << kArchitecture << "e" + << kVersion + << "f"))); + ASSERT_EQUALS(obj, outDoc); + } + + // Serialize without application name + { + BSONObjBuilder builder; + ClientMetadata::serializePrivate("a", "b", "c", "d", "e", "f", &builder); + + auto obj = builder.obj(); + auto swParseStatus = ClientMetadata::parse(obj[kMetadataDoc]); + ASSERT_OK(swParseStatus.getStatus()); + + BSONObj outDoc = BSON( + kMetadataDoc << BSON( + kDriver << BSON(kName << "a" << kVersion << "b") << kOperatingSystem + << BSON(kType << "c" << kName << "d" << kArchitecture << "e" << kVersion + << "f"))); + ASSERT_EQUALS(obj, outDoc); + } + + // Serialize with the os information automatically computed + { + BSONObjBuilder builder; + ASSERT_OK(ClientMetadata::serialize("a", "b", "f", &builder)); + + auto obj = builder.obj(); + + auto swParse = ClientMetadata::parse(obj[kMetadataDoc]); + ASSERT_OK(swParse.getStatus()); + ASSERT_EQUALS("f", swParse.getValue().get().getApplicationName()); + } +} + +// Mixed: no client metadata document +TEST(ClientMetadatTest, TestEmptyDoc) { + { + auto parseStatus = ClientMetadata::parse(BSONElement()); + + ASSERT_OK(parseStatus.getStatus()); + } + + { + auto obj = BSON("client" << BSONObj()); + auto parseStatus = ClientMetadata::parse(obj[kMetadataDoc]); + + ASSERT_NOT_OK(parseStatus.getStatus()); + } +} + +// Positive: test with only required fields +TEST(ClientMetadatTest, TestRequiredOnlyFields) { + // Without app name + ASSERT_DOC_OK(kDriver << BSON(kName << "n1" << kVersion << "v1") << kOperatingSystem + << BSON(kType << kUnknown)); + + // With AppName + ASSERT_DOC_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); +} + + +// Positive: test with app_name spelled wrong fields +TEST(ClientMetadatTest, TestWithAppNameSpelledWrong) { + ASSERT_DOC_OK(kApplication << BSON("extra" + << "1") + << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); +} + +// Positive: test with empty application document +TEST(ClientMetadatTest, TestWithEmptyApplication) { + ASSERT_DOC_OK(kApplication << BSONObj() << kDriver << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); +} + +// Negative: test with appplication wrong type +TEST(ClientMetadatTest, TestNegativeWithAppNameWrongType) { + ASSERT_DOC_NOT_OK(kApplication << "1" << kDriver << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); +} + +// Positive: test with extra fields +TEST(ClientMetadatTest, TestExtraFields) { + ASSERT_DOC_OK(kApplication << BSON(kName << "1" + << "extra" + << "v1") + << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_OK(kApplication << BSON(kName << "1" + << "extra" + << "v1") + << kDriver + << BSON(kName << "n1" << kVersion << "v1" + << "extra" + << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_OK(kApplication << BSON(kName << "1" + << "extra" + << "v1") + << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown << "extra" + << "v1")); + ASSERT_DOC_OK(kApplication << BSON(kName << "1" + << "extra" + << "v1") + << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown) + << "extra" + << "v1"); +} + +// Negative: only application specified +TEST(ClientMetadatTest, TestNegativeOnlyApplication) { + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << "1" + << "extra" + << "v1")); +} + +// Negative: all combinations of only missing 1 required field +TEST(ClientMetadatTest, TestNegativeMissingRequiredOneField) { + ASSERT_DOC_NOT_OK(kDriver << BSON(kVersion << "v1") << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_NOT_OK(kDriver << BSON(kName << "n1") << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_NOT_OK(kDriver << BSON(kName << "n1" << kVersion << "v1")); +} + +// Negative: document with wrong types for required fields +TEST(ClientMetadatTest, TestNegativeWrongTypes) { + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << 1) << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << 1 << kVersion << "v1") + << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << "n1" << kVersion << 1) + << kOperatingSystem + << BSON(kType << kUnknown)); + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << "n1" << kVersion << "v1") + << kOperatingSystem + << BSON(kType << 1)); +} + +// Negative: document larger than 512 bytes +TEST(ClientMetadatTest, TestNegativeLargeDocument) { + { + std::string str(350, 'x'); + ASSERT_DOC_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << "n1" << kVersion << "1") + << kOperatingSystem + << BSON(kType << kUnknown) + << "extra" + << str); + } + { + std::string str(512, 'x'); + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << "1") << kDriver + << BSON(kName << "n1" << kVersion << "1") + << kOperatingSystem + << BSON(kType << kUnknown) + << "extra" + << str); + } +} + +// Negative: document with app_name larger than 128 bytes +TEST(ClientMetadatTest, TestNegativeLargeAppName) { + { + std::string str(128, 'x'); + ASSERT_DOC_OK(kApplication << BSON(kName << str) << kDriver + << BSON(kName << "n1" << kVersion << "1") + << kOperatingSystem + << BSON(kType << kUnknown)); + + BSONObjBuilder builder; + ASSERT_OK(ClientMetadata::serialize("n1", "1", str, &builder)); + } + { + std::string str(129, 'x'); + ASSERT_DOC_NOT_OK(kApplication << BSON(kName << str) << kDriver + << BSON(kName << "n1" << kVersion << "1") + << kOperatingSystem + << BSON(kType << kUnknown)); + + BSONObjBuilder builder; + ASSERT_NOT_OK(ClientMetadata::serialize("n1", "1", str, &builder)); + } +} + +} // namespace mongo diff --git a/src/mongo/s/commands/SConscript b/src/mongo/s/commands/SConscript index 7306f9a5fac63..3a16737708f0d 100644 --- a/src/mongo/s/commands/SConscript +++ b/src/mongo/s/commands/SConscript @@ -77,6 +77,7 @@ env.Library( '$BUILD_DIR/mongo/db/commands/killcursors_common', '$BUILD_DIR/mongo/db/pipeline/aggregation', '$BUILD_DIR/mongo/db/views/views', + '$BUILD_DIR/mongo/rpc/client_metadata', '$BUILD_DIR/mongo/s/cluster_ops_impl', '$BUILD_DIR/mongo/s/coreshard', '$BUILD_DIR/mongo/s/mongoscore', diff --git a/src/mongo/s/commands/cluster_is_master_cmd.cpp b/src/mongo/s/commands/cluster_is_master_cmd.cpp index 191e04e7f546b..391ad28b0392a 100644 --- a/src/mongo/s/commands/cluster_is_master_cmd.cpp +++ b/src/mongo/s/commands/cluster_is_master_cmd.cpp @@ -28,10 +28,14 @@ #include "mongo/platform/basic.h" +#include "mongo/db/client.h" #include "mongo/db/commands.h" +#include "mongo/db/operation_context.h" #include "mongo/db/server_options.h" #include "mongo/db/server_parameters.h" #include "mongo/db/wire_version.h" +#include "mongo/rpc/metadata/client_metadata.h" +#include "mongo/rpc/metadata/client_metadata_ismaster.h" #include "mongo/s/grid.h" #include "mongo/s/write_ops/batched_command_request.h" #include "mongo/util/map_util.h" @@ -68,6 +72,36 @@ class CmdIsMaster : public Command { int options, std::string& errmsg, BSONObjBuilder& result) { + + auto& clientMetadataIsMasterState = ClientMetadataIsMasterState::get(txn->getClient()); + bool seenIsMaster = clientMetadataIsMasterState.hasSeenIsMaster(); + if (!seenIsMaster) { + clientMetadataIsMasterState.setSeenIsMaster(); + } + + BSONElement element = cmdObj[kMetadataDocumentName]; + if (!element.eoo()) { + if (seenIsMaster) { + return Command::appendCommandStatus( + result, + Status(ErrorCodes::ClientMetadataCannotBeMutated, + "The client metadata document may only be sent in the first isMaster")); + } + + auto swParseClientMetadata = ClientMetadata::parse(element); + + if (!swParseClientMetadata.getStatus().isOK()) { + return Command::appendCommandStatus(result, swParseClientMetadata.getStatus()); + } + + invariant(swParseClientMetadata.getValue()); + + swParseClientMetadata.getValue().get().logClientMetadata(txn->getClient()); + + clientMetadataIsMasterState.setClientMetadata( + txn->getClient(), std::move(swParseClientMetadata.getValue())); + } + result.appendBool("ismaster", true); result.append("msg", "isdbgrid"); result.appendNumber("maxBsonObjectSize", BSONObjMaxUserSize); diff --git a/src/mongo/scripting/mozjs/mongo.cpp b/src/mongo/scripting/mozjs/mongo.cpp index e9cbc1991f48b..ad7daff0f61ba 100644 --- a/src/mongo/scripting/mozjs/mongo.cpp +++ b/src/mongo/scripting/mozjs/mongo.cpp @@ -640,7 +640,7 @@ void MongoExternalInfo::construct(JSContext* cx, JS::CallArgs args) { auto cs = statusWithHost.getValue(); std::string errmsg; - std::unique_ptr conn(cs.connect(errmsg)); + std::unique_ptr conn(cs.connect("MongoDB Shell", errmsg)); if (!conn.get()) { uasserted(ErrorCodes::InternalError, errmsg); diff --git a/src/mongo/shell/bench.cpp b/src/mongo/shell/bench.cpp index 2f5fc52359c0e..111dda578df21 100644 --- a/src/mongo/shell/bench.cpp +++ b/src/mongo/shell/bench.cpp @@ -485,7 +485,7 @@ DBClientBase* BenchRunConfig::createConnection() const { const ConnectionString connectionString = uassertStatusOK(ConnectionString::parse(host)); std::string errorMessage; - DBClientBase* connection = connectionString.connect(errorMessage); + DBClientBase* connection = connectionString.connect("BenchRun", errorMessage); uassert(16158, errorMessage, connection != NULL); return connection; diff --git a/src/mongo/shell/shell_utils.cpp b/src/mongo/shell/shell_utils.cpp index ee65163e09eef..e4edd8d939191 100644 --- a/src/mongo/shell/shell_utils.cpp +++ b/src/mongo/shell/shell_utils.cpp @@ -318,7 +318,7 @@ void ConnectionRegistry::killOperationsOnAllConnections(bool withPrompt) const { const ConnectionString cs(status.getValue()); string errmsg; - std::unique_ptr conn(cs.connect(errmsg)); + std::unique_ptr conn(cs.connect("MongoDB Shell", errmsg)); if (!conn) { continue; } diff --git a/src/mongo/shell/shell_utils_launcher.cpp b/src/mongo/shell/shell_utils_launcher.cpp index 73e0cd6a65fd3..7d09458992251 100644 --- a/src/mongo/shell/shell_utils_launcher.cpp +++ b/src/mongo/shell/shell_utils_launcher.cpp @@ -856,7 +856,8 @@ inline void kill_wrapper(ProcessId pid, int sig, int port, const BSONObj& opt) { // try { DBClientConnection conn; - conn.connect(HostAndPort{"127.0.0.1:" + BSONObjBuilder::numStr(port)}); + conn.connect(HostAndPort{"127.0.0.1:" + BSONObjBuilder::numStr(port)}, + "MongoDB Shell"); BSONElement authObj = opt["auth"]; diff --git a/src/mongo/tools/sniffer.cpp b/src/mongo/tools/sniffer.cpp index b5081a361edd4..70b1d77dfb3ee 100644 --- a/src/mongo/tools/sniffer.cpp +++ b/src/mongo/tools/sniffer.cpp @@ -404,7 +404,7 @@ void processMessage(Connection& c, Message& m) { std::shared_ptr conn = forwarder[c]; if (!conn) { conn.reset(new DBClientConnection(true)); - uassertStatusOK(conn->connect(mongo::HostAndPort{forwardAddress})); + uassertStatusOK(conn->connect(mongo::HostAndPort{forwardAddress}, "mongosniff")); forwarder[c] = conn; } if (m.operation() == mongo::dbQuery || m.operation() == mongo::dbGetMore) {