diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2021-02-01 17:00:59 +0000 |
---|---|---|
committer | Evergreen Agent <no-reply@evergreen.mongodb.com> | 2021-02-10 04:54:37 +0000 |
commit | 92dfc822d41714b47bc20e260aafb54884909acc (patch) | |
tree | 137b627c77938dd87b96b349ef090d9910485104 /src | |
parent | d7b4c6bcaa61dc54d830007657babdd6337f9cfb (diff) | |
download | mongo-92dfc822d41714b47bc20e260aafb54884909acc.tar.gz |
SERVER-53150 Specify input/output to hello command
Diffstat (limited to 'src')
-rw-r--r-- | src/mongo/db/repl/SConscript | 17 | ||||
-rw-r--r-- | src/mongo/db/repl/hello.idl | 247 | ||||
-rw-r--r-- | src/mongo/db/repl/hello_auth.cpp | 28 | ||||
-rw-r--r-- | src/mongo/db/repl/hello_auth.h | 3 | ||||
-rw-r--r-- | src/mongo/db/repl/replication_info.cpp | 217 | ||||
-rw-r--r-- | src/mongo/rpc/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/client_metadata.cpp | 109 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/client_metadata.h | 20 | ||||
-rw-r--r-- | src/mongo/rpc/metadata/client_metadata.idl | 44 | ||||
-rw-r--r-- | src/mongo/s/commands/SConscript | 1 | ||||
-rw-r--r-- | src/mongo/s/commands/cluster_hello_cmd.cpp | 140 | ||||
-rw-r--r-- | src/mongo/transport/message_compressor_manager.cpp | 41 | ||||
-rw-r--r-- | src/mongo/transport/message_compressor_manager.h | 7 | ||||
-rw-r--r-- | src/mongo/transport/message_compressor_manager_test.cpp | 41 |
14 files changed, 595 insertions, 321 deletions
diff --git a/src/mongo/db/repl/SConscript b/src/mongo/db/repl/SConscript index b2747bf9a24..7dcf76a2ff7 100644 --- a/src/mongo/db/repl/SConscript +++ b/src/mongo/db/repl/SConscript @@ -1199,6 +1199,20 @@ env.Library( ) env.Library( + target="hello_command", + source=[ + "hello.idl", + ], + LIBDEPS=[ + '$BUILD_DIR/mongo/base', + ], + LIBDEPS_PRIVATE=[ + '$BUILD_DIR/mongo/idl/idl_parser', + '$BUILD_DIR/mongo/rpc/metadata', + ], +) + +env.Library( target="replication_info", source=[ "replication_info.cpp", @@ -1208,6 +1222,7 @@ env.Library( '$BUILD_DIR/mongo/client/clientdriver_network', '$BUILD_DIR/mongo/db/auth/auth', '$BUILD_DIR/mongo/db/auth/saslauth', + '$BUILD_DIR/mongo/db/commands/test_commands_enabled', '$BUILD_DIR/mongo/db/dbhelpers', '$BUILD_DIR/mongo/db/query_exec', "$BUILD_DIR/mongo/util/fail_point", @@ -1221,6 +1236,7 @@ env.Library( '$BUILD_DIR/mongo/db/stats/counters', '$BUILD_DIR/mongo/transport/message_compressor', 'hello_auth', + 'hello_command', 'primary_only_service', 'replication_auth', 'split_horizon', @@ -1744,6 +1760,7 @@ env.Library( '$BUILD_DIR/mongo/base', '$BUILD_DIR/mongo/db/auth/authentication_session', '$BUILD_DIR/mongo/db/auth/authservercommon', + 'hello_command', ], ) diff --git a/src/mongo/db/repl/hello.idl b/src/mongo/db/repl/hello.idl new file mode 100644 index 00000000000..452b1423565 --- /dev/null +++ b/src/mongo/db/repl/hello.idl @@ -0,0 +1,247 @@ +# Copyright (C) 2021-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +global: + cpp_namespace: "mongo" + +imports: + - "mongo/db/auth/auth_types.idl" + - "mongo/db/repl/replication_types.idl" + - "mongo/idl/basic_types.idl" + - "mongo/rpc/metadata/client_metadata.idl" + - "mongo/rpc/topology_version.idl" + +structs: + HelloInternalClientField: + description: "Specifies min/max wire protocol versions" + strict: true + fields: + minWireVersion: + # Currently ignored + type: safeInt + default: 0 + maxWireVersion: + type: safeInt + + HelloLastWrite: + description: "Most recent op/write times for this node" + strict: true + fields: + opTime: + type: optime + optional: true + lastWriteDate: + type: date + optional: true + majorityOpTime: + type: optime + optional: true + majorityWriteDate: + type: date + optional: true + + HelloCommandReply: + description: "Reply to 'hello' command" + strict: true + fields: + helloOk: + type: bool + default: true + clientSupportsHello: + type: bool + optional: true + configsvr: + type: safeInt + optional: true + maxBsonObjectSize: + type: safeInt64 + optional: true + maxMessageSizeBytes: + type: safeInt64 + optional: true + maxWriteBatchSize: + type: safeInt64 + optional: true + localTime: + type: date + optional: true + logicalSessionTimeoutMinutes: + type: safeInt + optional: true + connectionId: + type: safeInt64 + optional: true + minWireVersion: + type: safeInt + optional: true + maxWireVersion: + type: safeInt + optional: true + readOnly: + type: bool + optional: true + compression: + type: array<string> + optional: true + automationServiceDescriptor: + type: string + optional: true + saslSupportedMechs: + type: array<string> + optional: true + speculativeAuthenticate: + type: object + optional: true + msg: + type: string + optional: true + ## + ## ReplicationInfo + ## + topologyVersion: + type: TopologyVersion + ismaster: + # Replies will contain 'ismaster' OR 'isWritablePrimary', not both + type: bool + optional: true + isWritablePrimary: + type: bool + optional: true + ## + ## Using ReplSets + ## + hosts: + type: array<string> + optional: true + passives: + type: array<string> + optional: true + arbiters: + type: array<string> + optional: true + setName: + type: string + optional: true + primary: + type: string + optional: true + secondary: + type: bool + optional: true + info: + type: string + optional: true + isreplicaset: + type: bool + optional: true + setVersion: + type: safeInt + optional: true + arbiterOnly: + type: bool + optional: true + passive: + type: bool + optional: true + hidden: + type: bool + optional: true + buildIndexes: + type: bool + optional: true + slaveDelay: + # Reply will contain either slaveDelay or secondaryDelaySecs, but not both. + type: safeInt64 + optional: true + secondaryDelaySecs: + type: safeInt64 + optional: true + tags: + type: object + optional: true + me: + type: string + optional: true + electionId: + type: objectid + optional: true + lastWrite: + type: HelloLastWrite + optional: true + +commands: + hello: + # Aliases: 'isMaster', 'ismaster' + description: "Check if this server is primary for a replica set { hello: 1 }" + command_name: hello + namespace: ignored + cpp_name: HelloCommand + api_version: "1" + reply_type: HelloCommandReply + strict: true + fields: + awaitable: + type: safeBool + optional: true + unstable: true + forShell: + type: safeBool + default: false + unstable: true + hostInfo: + type: string + default: false + hangUpOnStepDown: + type: safeBool + default: true + internalClient: + type: HelloInternalClientField + optional: true + unstable: true + client: + type: ClientMetadata + optional: true + topologyVersion: + type: TopologyVersion + optional: true + maxAwaitTimeMS: + type: safeInt64 + optional: true + validator: { gte: 0 } + helloOk: + type: safeBool + optional: true + compression: + type: array<string> + optional: true + saslSupportedMechs: + type: UserName + optional: true + speculativeAuthenticate: + type: object + optional: true diff --git a/src/mongo/db/repl/hello_auth.cpp b/src/mongo/db/repl/hello_auth.cpp index b88ea5b1325..efc79fdc1dc 100644 --- a/src/mongo/db/repl/hello_auth.cpp +++ b/src/mongo/db/repl/hello_auth.cpp @@ -39,39 +39,33 @@ namespace mongo { -void handleHelloAuth(OperationContext* opCtx, BSONObj cmdObj, BSONObjBuilder* result) { - auto ssme = cmdObj[auth::kSaslSupportedMechanisms]; - if (ssme.type() == BSONType::String) { +void handleHelloAuth(OperationContext* opCtx, const HelloCommand& cmd, BSONObjBuilder* result) { + // saslSupportedMechs: UserName -> List[String] + if (auto userName = cmd.getSaslSupportedMechs()) { AuthenticationSession::doStep( opCtx, AuthenticationSession::StepType::kSaslSupportedMechanisms, [&](auto session) { - UserName userName = uassertStatusOK(UserName::parse(ssme.String())); - auto& saslMechanismRegistry = SASLServerMechanismRegistry::get(opCtx->getServiceContext()); - saslMechanismRegistry.advertiseMechanismNamesForUser(opCtx, userName, result); + saslMechanismRegistry.advertiseMechanismNamesForUser(opCtx, *userName, result); }); } - auto sae = cmdObj[auth::kSpeculativeAuthenticate]; - if (sae.eoo()) { + // speculativeAuthenticate: SaslStart -> SaslReply or Authenticate -> AuthenticateReply + auto specAuth = cmd.getSpeculativeAuthenticate(); + if (!specAuth) { return; } uassert(ErrorCodes::BadValue, - str::stream() << "hello." << auth::kSpeculativeAuthenticate << " must be an Object", - sae.type() == Object); - auto specAuth = sae.Obj(); - - uassert(ErrorCodes::BadValue, str::stream() << "hello." << auth::kSpeculativeAuthenticate << " must be a non-empty Object", - !specAuth.isEmpty()); - auto specCmd = specAuth.firstElementFieldNameStringData(); + !specAuth->isEmpty()); + auto specCmd = specAuth->firstElementFieldNameStringData(); if (specCmd == saslStartCommandName) { - doSpeculativeSaslStart(opCtx, specAuth, result); + doSpeculativeSaslStart(opCtx, *specAuth, result); } else if (specCmd == auth::kAuthenticateCommand) { - doSpeculativeAuthenticate(opCtx, specAuth, result); + doSpeculativeAuthenticate(opCtx, *specAuth, result); } else { uasserted(51769, str::stream() << "hello." << auth::kSpeculativeAuthenticate diff --git a/src/mongo/db/repl/hello_auth.h b/src/mongo/db/repl/hello_auth.h index 1ec1b4d1658..8f8a73269db 100644 --- a/src/mongo/db/repl/hello_auth.h +++ b/src/mongo/db/repl/hello_auth.h @@ -32,6 +32,7 @@ #include "mongo/bson/bsonobj.h" #include "mongo/bson/bsonobjbuilder.h" #include "mongo/db/operation_context.h" +#include "mongo/db/repl/hello_gen.h" namespace mongo { @@ -41,6 +42,6 @@ namespace mongo { * This will attach supported mechanisms or invoke the behavior of saslStart/authenticate commands * as appropriate. */ -void handleHelloAuth(OperationContext* opCtx, BSONObj cmdObj, BSONObjBuilder* result); +void handleHelloAuth(OperationContext* opCtx, const HelloCommand& cmd, BSONObjBuilder* result); } // namespace mongo diff --git a/src/mongo/db/repl/replication_info.cpp b/src/mongo/db/repl/replication_info.cpp index 7c051a60ebd..ba7c3131844 100644 --- a/src/mongo/db/repl/replication_info.cpp +++ b/src/mongo/db/repl/replication_info.cpp @@ -39,6 +39,7 @@ #include "mongo/client/dbclient_connection.h" #include "mongo/db/client.h" #include "mongo/db/commands/server_status.h" +#include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/curop.h" #include "mongo/db/db_raii.h" #include "mongo/db/dbhelpers.h" @@ -50,6 +51,7 @@ #include "mongo/db/ops/write_ops.h" #include "mongo/db/query/internal_plans.h" #include "mongo/db/repl/hello_auth.h" +#include "mongo/db/repl/hello_gen.h" #include "mongo/db/repl/hello_response.h" #include "mongo/db/repl/primary_only_service.h" #include "mongo/db/repl/replication_auth.h" @@ -101,7 +103,7 @@ TopologyVersion appendReplicationInfo(OperationContext* opCtx, bool appendReplicationProcess, bool useLegacyResponseFields, boost::optional<TopologyVersion> clientTopologyVersion, - boost::optional<long long> maxAwaitTimeMS) { + boost::optional<std::int64_t> maxAwaitTimeMS) { TopologyVersion topologyVersion; ReplicationCoordinator* replCoord = ReplicationCoordinator::get(opCtx); if (replCoord->getSettings().usingReplSets()) { @@ -237,11 +239,14 @@ public: } } oplogInfoServerStatus; +const std::string kAutomationServiceDescriptorFieldName = + HelloCommandReply::kAutomationServiceDescriptorFieldName.toString(); + class CmdHello : public BasicCommandWithReplyBuilderInterface { public: CmdHello() : CmdHello(kHelloString, {}) {} - const std::set<std::string>& apiVersions() const { + const std::set<std::string>& apiVersions() const final { return kApiVersions1; } @@ -271,13 +276,15 @@ public: const BSONObj& cmdObj, rpc::ReplyBuilderInterface* replyBuilder) final { CommandHelpers::handleMarkKillOnClientDisconnect(opCtx); + const bool apiStrict = APIParameters::get(opCtx).getAPIStrict().value_or(false); + auto cmd = HelloCommand::parse({"hello", apiStrict}, cmdObj); waitInHello.pauseWhileSet(opCtx); /* currently request to arbiter is (somewhat arbitrarily) an ismaster request that is not authenticated. */ - if (cmdObj["forShell"].trueValue()) { + if (cmd.getForShell()) { LastError::get(opCtx->getClient()).disable(); } @@ -285,8 +292,7 @@ public: transport::Session::TagMask sessionTagsToUnset = 0; // Tag connections to avoid closing them on stepdown. - auto hangUpElement = cmdObj["hangUpOnStepDown"]; - if (!hangUpElement.eoo() && !hangUpElement.trueValue()) { + if (!cmd.getHangUpOnStepDown()) { sessionTagsToSet |= transport::Session::kKeepOpen; } @@ -299,51 +305,18 @@ public: // Parse the optional 'internalClient' field. This is provided by incoming connections from // mongod and mongos. - auto internalClientElement = cmdObj["internalClient"]; - if (internalClientElement) { + if (auto internalClient = cmd.getInternalClient()) { sessionTagsToSet |= transport::Session::kInternalClient; sessionTagsToUnset |= transport::Session::kExternalClientKeepOpen; - uassert(ErrorCodes::TypeMismatch, - str::stream() << "'internalClient' must be of type Object, but was of type " - << typeName(internalClientElement.type()), - internalClientElement.type() == BSONType::Object); - - bool foundMaxWireVersion = false; - for (auto&& elem : internalClientElement.Obj()) { - auto fieldName = elem.fieldNameStringData(); - if (fieldName == "minWireVersion") { - // We do not currently use 'internalClient.minWireVersion'. - continue; - } else if (fieldName == "maxWireVersion") { - foundMaxWireVersion = true; - - uassert(ErrorCodes::TypeMismatch, - str::stream() << "'maxWireVersion' field of 'internalClient' must be " - "of type int, but was of type " - << typeName(elem.type()), - elem.type() == BSONType::NumberInt); - - // All incoming connections from mongod/mongos of earlier versions should be - // closed if the featureCompatibilityVersion is bumped to 3.6. - if (elem.numberInt() >= - WireSpec::instance().get()->incomingInternalClient.maxWireVersion) { - sessionTagsToSet |= - transport::Session::kLatestVersionInternalClientKeepOpen; - } else { - sessionTagsToUnset |= - transport::Session::kLatestVersionInternalClientKeepOpen; - } - } else { - uasserted(ErrorCodes::BadValue, - str::stream() << "Unrecognized field of 'internalClient': '" - << fieldName << "'"); - } + // All incoming connections from mongod/mongos of earlier versions should be + // closed if the featureCompatibilityVersion is bumped to 3.6. + if (internalClient->getMaxWireVersion() >= + WireSpec::instance().get()->incomingInternalClient.maxWireVersion) { + sessionTagsToSet |= transport::Session::kLatestVersionInternalClientKeepOpen; + } else { + sessionTagsToUnset |= transport::Session::kLatestVersionInternalClientKeepOpen; } - - uassert(ErrorCodes::BadValue, - "Missing required field 'maxWireVersion' of 'internalClient'", - foundMaxWireVersion); } else { sessionTagsToUnset |= (transport::Session::kInternalClient | transport::Session::kLatestVersionInternalClientKeepOpen); @@ -369,53 +342,36 @@ public: // If a client is following the awaitable hello protocol, maxAwaitTimeMS should be // present if and only if topologyVersion is present in the request. - auto topologyVersionElement = cmdObj["topologyVersion"]; - auto maxAwaitTimeMSField = cmdObj["maxAwaitTimeMS"]; + auto clientTopologyVersion = cmd.getTopologyVersion(); + auto maxAwaitTimeMS = cmd.getMaxAwaitTimeMS(); auto curOp = CurOp::get(opCtx); - - // The awaitable field is optional, but if it exists, it requires both other fields. - // Note that the awaitable field only exists to make filtering out these operations - // via currentOp queries easier and is not otherwise explicitly used. - { - bool awaitable = true; - Status status = bsonExtractBooleanField(cmdObj, "awaitable", &awaitable); - if (status.isOK() && (!topologyVersionElement || !maxAwaitTimeMSField)) { - uassert(5135800, - "A request marked awaitable must contain both 'topologyVersion' and " - "'maxAwaitTimeMS'", - !awaitable); - } - } - - boost::optional<TopologyVersion> clientTopologyVersion; - boost::optional<long long> maxAwaitTimeMS; boost::optional<ScopeGuard<std::function<void()>>> timerGuard; - if (topologyVersionElement && maxAwaitTimeMSField) { - clientTopologyVersion = TopologyVersion::parse(IDLParserErrorContext("TopologyVersion"), - topologyVersionElement.Obj()); + if (clientTopologyVersion && maxAwaitTimeMS) { uassert(31372, "topologyVersion must have a non-negative counter", clientTopologyVersion->getCounter() >= 0); - { - long long parsedMaxAwaitTimeMS; - uassertStatusOK( - bsonExtractIntegerField(cmdObj, "maxAwaitTimeMS", &parsedMaxAwaitTimeMS)); - maxAwaitTimeMS = parsedMaxAwaitTimeMS; - } - - uassert(31373, "maxAwaitTimeMS must be a non-negative integer", *maxAwaitTimeMS >= 0); - - LOGV2_DEBUG(23904, 3, "Using maxAwaitTimeMS for awaitable hello protocol."); + LOGV2_DEBUG(23904, + 3, + "Using maxAwaitTimeMS for awaitable hello protocol", + "maxAwaitTimeMS"_attr = maxAwaitTimeMS.get()); curOp->pauseTimer(); timerGuard.emplace([curOp]() { curOp->resumeTimer(); }); } else { + // The awaitable field is optional, but if it exists, it requires both other fields. + // Note that the awaitable field only exists to make filtering out these operations + // via currentOp queries easier and is not otherwise explicitly used. + uassert(5135800, + "A request marked awaitable must contain both 'topologyVersion' and " + "'maxAwaitTimeMS'", + !cmd.getAwaitable() || !cmd.getAwaitable()); + uassert(31368, - (topologyVersionElement + (clientTopologyVersion ? "A request with a 'topologyVersion' must include 'maxAwaitTimeMS'" : "A request with 'maxAwaitTimeMS' must include a 'topologyVersion'"), - !topologyVersionElement && !maxAwaitTimeMSField); + !clientTopologyVersion && !maxAwaitTimeMS); } auto result = replyBuilder->getBodyBuilder(); @@ -424,21 +380,18 @@ public: // handshake for an incoming connection if the client supports the hello command. Clients // that specify 'helloOk' do not rely on "not master" error message parsing, which means // that we can safely return "not primary" error messages instead. - bool helloOk = client->supportsHello(); - Status status = bsonExtractBooleanField(cmdObj, "helloOk", &helloOk); - if (status.isOK()) { + if (auto helloOk = cmd.getHelloOk()) { // If the hello request contains a "helloOk" field, set _supportsHello on the Client // to the value. - client->setSupportsHello(helloOk); + client->setSupportsHello(*helloOk); // Attach helloOk: true to the response so that the client knows the server supports // the hello command. - result.append("helloOk", true); - } else if (status.code() != ErrorCodes::NoSuchKey) { - uassertStatusOK(status); + result.append(HelloCommandReply::kHelloOkFieldName, true); } if (MONGO_unlikely(appendHelloOkToHelloResponse.shouldFail())) { - result.append("clientSupportsHello", client->supportsHello()); + result.append(HelloCommandReply::kClientSupportsHelloFieldName, + client->supportsHello()); } auto currentTopologyVersion = appendReplicationInfo( @@ -447,35 +400,44 @@ public: timerGuard.reset(); // Resume curOp timer. if (serverGlobalParams.clusterRole == ClusterRole::ConfigServer) { - const int configServerModeNumber = 2; - result.append("configsvr", configServerModeNumber); + constexpr int kConfigServerModeNumber = 2; + result.append(HelloCommandReply::kConfigsvrFieldName, kConfigServerModeNumber); } - result.appendNumber("maxBsonObjectSize", BSONObjMaxUserSize); - result.appendNumber("maxMessageSizeBytes", MaxMessageSizeBytes); - result.appendNumber("maxWriteBatchSize", write_ops::kMaxWriteBatchSize); - result.appendDate("localTime", jsTime()); - result.append("logicalSessionTimeoutMinutes", localLogicalSessionTimeoutMinutes); - result.appendNumber("connectionId", opCtx->getClient()->getConnectionId()); - - if (auto wireSpec = WireSpec::instance().get(); internalClientElement) { - result.append("minWireVersion", wireSpec->incomingInternalClient.minWireVersion); - result.append("maxWireVersion", wireSpec->incomingInternalClient.maxWireVersion); + result.appendNumber(HelloCommandReply::kMaxBsonObjectSizeFieldName, BSONObjMaxUserSize); + result.appendNumber(HelloCommandReply::kMaxMessageSizeBytesFieldName, MaxMessageSizeBytes); + result.appendNumber(HelloCommandReply::kMaxWriteBatchSizeFieldName, + write_ops::kMaxWriteBatchSize); + result.appendDate(HelloCommandReply::kLocalTimeFieldName, jsTime()); + result.append(HelloCommandReply::kLogicalSessionTimeoutMinutesFieldName, + localLogicalSessionTimeoutMinutes); + result.appendNumber(HelloCommandReply::kConnectionIdFieldName, + opCtx->getClient()->getConnectionId()); + + + if (auto wireSpec = WireSpec::instance().get(); cmd.getInternalClient()) { + result.append(HelloCommandReply::kMinWireVersionFieldName, + wireSpec->incomingInternalClient.minWireVersion); + result.append(HelloCommandReply::kMaxWireVersionFieldName, + wireSpec->incomingInternalClient.maxWireVersion); } else { - result.append("minWireVersion", wireSpec->incomingExternalClient.minWireVersion); - result.append("maxWireVersion", wireSpec->incomingExternalClient.maxWireVersion); + result.append(HelloCommandReply::kMinWireVersionFieldName, + wireSpec->incomingExternalClient.minWireVersion); + result.append(HelloCommandReply::kMaxWireVersionFieldName, + wireSpec->incomingExternalClient.maxWireVersion); } - result.append("readOnly", storageGlobalParams.readOnly); + result.append(HelloCommandReply::kReadOnlyFieldName, storageGlobalParams.readOnly); const auto& params = ServerParameterSet::getGlobal()->getMap(); - if (auto iter = params.find("automationServiceDescriptor"); - iter != params.end() && iter->second) - iter->second->append(opCtx, result, "automationServiceDescriptor"); + if (auto iter = params.find(kAutomationServiceDescriptorFieldName); + iter != params.end() && iter->second) { + iter->second->append(opCtx, result, kAutomationServiceDescriptorFieldName); + } if (opCtx->getClient()->session()) { MessageCompressorManager::forSession(opCtx->getClient()->session()) - .serverNegotiate(cmdObj, &result); + .serverNegotiate(cmd.getCompression(), &result); } if (opCtx->isExhaust()) { @@ -483,7 +445,7 @@ public: uassert(51756, "An isMaster or hello request with exhaust must specify 'maxAwaitTimeMS'", - maxAwaitTimeMSField); + maxAwaitTimeMS); invariant(clientTopologyVersion); InExhaustHello::get(opCtx->getClient()->session().get()) @@ -495,30 +457,45 @@ public: // command parameters should be reused as the next BSONObj command parameters. replyBuilder->setNextInvocation(boost::none); } else { - BSONObjBuilder nextInvocationBuilder; - for (auto&& elt : cmdObj) { - if (elt.fieldNameStringData() == "topologyVersion"_sd) { - BSONObjBuilder topologyVersionBuilder( - nextInvocationBuilder.subobjStart("topologyVersion")); - currentTopologyVersion.serialize(&topologyVersionBuilder); + BSONObjBuilder niBuilder; + for (const auto& elem : cmdObj) { + if (elem.fieldNameStringData() == HelloCommand::kTopologyVersionFieldName) { + BSONObjBuilder tvBuilder( + niBuilder.subobjStart(HelloCommand::kTopologyVersionFieldName)); + currentTopologyVersion.serialize(&tvBuilder); } else { - nextInvocationBuilder.append(elt); + niBuilder.append(elem); } } - replyBuilder->setNextInvocation(nextInvocationBuilder.obj()); + replyBuilder->setNextInvocation(niBuilder.obj()); } } - handleHelloAuth(opCtx, cmdObj, &result); + handleHelloAuth(opCtx, cmd, &result); + if (getTestCommandsEnabled()) { + validateResult(&result); + } return true; } + void validateResult(BSONObjBuilder* result) { + auto ret = result->asTempObj(); + if (ret[ErrorReply::kErrmsgFieldName].eoo()) { + // Nominal success case, parse the object as-is. + HelloCommandReply::parse({"hello.reply"}, ret); + } else { + // Something went wrong, still try to parse, but accept a few ignorable fields. + StringDataSet ignorable({ErrorReply::kCodeFieldName, ErrorReply::kErrmsgFieldName}); + HelloCommandReply::parse({"hello.reply"}, ret.removeFields(ignorable)); + } + } + protected: CmdHello(const StringData cmdName, const std::initializer_list<StringData>& alias) : BasicCommandWithReplyBuilderInterface(cmdName, alias) {} - virtual bool useLegacyResponseFields() { + virtual bool useLegacyResponseFields() const { return false; } @@ -528,7 +505,7 @@ class CmdIsMaster : public CmdHello { public: CmdIsMaster() : CmdHello(kCamelCaseIsMasterString, {kLowerCaseIsMasterString}) {} - std::string help() const override { + std::string help() const final { return "Check if this server is primary for a replica set\n" "{ isMaster : 1 }"; } @@ -537,7 +514,7 @@ protected: // Parse the command name, which should be one of the following: hello, isMaster, or // ismaster. If the command is "hello", we must attach an "isWritablePrimary" response field // instead of "ismaster" and "secondaryDelaySecs" response field instead of "slaveDelay". - bool useLegacyResponseFields() override { + bool useLegacyResponseFields() const final { return true; } diff --git a/src/mongo/rpc/SConscript b/src/mongo/rpc/SConscript index 957647e5721..257e556da03 100644 --- a/src/mongo/rpc/SConscript +++ b/src/mongo/rpc/SConscript @@ -135,6 +135,7 @@ env.Library( target='client_metadata', source=[ 'metadata/client_metadata.cpp', + 'metadata/client_metadata.idl', ], LIBDEPS=[ '$BUILD_DIR/mongo/base', diff --git a/src/mongo/rpc/metadata/client_metadata.cpp b/src/mongo/rpc/metadata/client_metadata.cpp index a4a8767aadc..6ea09b44b02 100644 --- a/src/mongo/rpc/metadata/client_metadata.cpp +++ b/src/mongo/rpc/metadata/client_metadata.cpp @@ -84,7 +84,7 @@ const auto getOperationState = OperationContext::declareDecoration<ClientMetadat } // namespace -StatusWith<boost::optional<ClientMetadata>> ClientMetadata::parse(const BSONElement& element) { +StatusWith<boost::optional<ClientMetadata>> ClientMetadata::parse(const BSONElement& element) try { if (element.eoo()) { return {boost::none}; } @@ -93,84 +93,50 @@ StatusWith<boost::optional<ClientMetadata>> ClientMetadata::parse(const BSONElem 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)}; + return boost::make_optional(parseFromBSON(element.Obj())); +} catch (const DBException& ex) { + return ex.toStatus(); } -Status ClientMetadata::parseClientMetadataDocument(const BSONObj& doc) { +ClientMetadata::ClientMetadata(BSONObj doc) { uint32_t maxLength = kMaxMongoDMetadataDocumentByteLength; if (isMongos()) { maxLength = kMaxMongoSMetadataDocumentByteLength; } - if (static_cast<uint32_t>(doc.objsize()) > maxLength) { - return Status(ErrorCodes::ClientMetadataDocumentTooLarge, - str::stream() << "The client metadata document must be less then or equal to " - << maxLength << "bytes"); - } + uassert(ErrorCodes::ClientMetadataDocumentTooLarge, + str::stream() << "The client metadata document must be less then or equal to " + << maxLength << "bytes", + static_cast<uint32_t>(doc.objsize()) <= maxLength); + + const auto isobj = [](StringData name, const BSONElement& e) { + uassert(ErrorCodes::TypeMismatch, + str::stream() + << "The '" << name + << "' field is required to be a BSON document in the client metadata document", + e.isABSONObj()); + }; // Get a copy so that we can take a stable reference to the app name inside - BSONObj docOwned = doc.getOwned(); + _document = doc.getOwned(); - StringData appName; bool foundDriver = false; bool foundOperatingSystem = false; - - BSONObjIterator i(docOwned); - while (i.more()) { - BSONElement e = i.next(); - StringData name = e.fieldNameStringData(); + for (const auto& e : _document) { + auto 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(); - + isobj(kApplication, e); + _appName = uassertStatusOK(parseApplicationDocument(e.Obj())); } 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; - } - + isobj(kDriver, e); + uassertStatusOK(validateDriverDocument(e.Obj())); 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; - } - + isobj(kOperatingSystem, e); + uassertStatusOK(validateOperatingSystemDocument(e.Obj())); foundOperatingSystem = true; } @@ -178,23 +144,16 @@ Status ClientMetadata::parseClientMetadataDocument(const BSONObj& doc) { } // Driver is a required sub document. - if (!foundDriver) { - return Status(ErrorCodes::ClientMetadataMissingField, - str::stream() << "Missing required sub-document '" << kDriver - << "' in the client metadata document"); - } + uassert(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required sub-document '" << kDriver + << "' in the client metadata document", + foundDriver); // 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(); + uassert(ErrorCodes::ClientMetadataMissingField, + str::stream() << "Missing required sub-document '" << kOperatingSystem + << "' in the client metadata document", + foundOperatingSystem); } StatusWith<StringData> ClientMetadata::parseApplicationDocument(const BSONObj& doc) { diff --git a/src/mongo/rpc/metadata/client_metadata.h b/src/mongo/rpc/metadata/client_metadata.h index cc3d870a4f0..309eb5668e0 100644 --- a/src/mongo/rpc/metadata/client_metadata.h +++ b/src/mongo/rpc/metadata/client_metadata.h @@ -80,10 +80,16 @@ constexpr auto kMetadataDocumentName = "client"_sd; * See Driver Specification: "MongoDB Handshake" for more information. */ class ClientMetadata { - ClientMetadata(const ClientMetadata&) = delete; - ClientMetadata& operator=(const ClientMetadata&) = delete; - public: + explicit ClientMetadata(BSONObj obj); + + ClientMetadata(const ClientMetadata& src) : ClientMetadata(src._document) {} + ClientMetadata& operator=(const ClientMetadata& src) { + ClientMetadata copy(src._document); + *this = std::move(copy); + return *this; + } + ClientMetadata(ClientMetadata&&) = default; ClientMetadata& operator=(ClientMetadata&&) = default; @@ -145,6 +151,13 @@ public: static StatusWith<boost::optional<ClientMetadata>> parse(const BSONElement& element); /** + * Wrapper for BSONObj constructor used by IDL parsers. + */ + static ClientMetadata parseFromBSON(BSONObj obj) { + return ClientMetadata(obj); + } + + /** * 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 @@ -311,7 +324,6 @@ public: private: ClientMetadata() = default; - Status parseClientMetadataDocument(const BSONObj& doc); static Status validateDriverDocument(const BSONObj& doc); static Status validateOperatingSystemDocument(const BSONObj& doc); static StatusWith<StringData> parseApplicationDocument(const BSONObj& doc); diff --git a/src/mongo/rpc/metadata/client_metadata.idl b/src/mongo/rpc/metadata/client_metadata.idl new file mode 100644 index 00000000000..d1726153d98 --- /dev/null +++ b/src/mongo/rpc/metadata/client_metadata.idl @@ -0,0 +1,44 @@ +# Copyright (C) 2021-present MongoDB, Inc. +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the Server Side Public License, version 1, +# as published by MongoDB, Inc. +# +# 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 +# Server Side Public License for more details. +# +# You should have received a copy of the Server Side Public License +# along with this program. If not, see +# <http://www.mongodb.com/licensing/server-side-public-license>. +# +# 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 Server Side 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. +# + +global: + cpp_namespace: "mongo" + cpp_includes: + - "mongo/rpc/metadata/client_metadata.h" + +imports: + - "mongo/idl/basic_types.idl" + +types: + ClientMetadata: + description: "Client metadata document" + bson_serialization_type: object + cpp_type: ClientMetadata + serializer: "mongo::ClientMetadata::getDocument" + deserializer: "mongo::ClientMetadata::parseFromBSON" + diff --git a/src/mongo/s/commands/SConscript b/src/mongo/s/commands/SConscript index 275ae43565a..0d20f144e62 100644 --- a/src/mongo/s/commands/SConscript +++ b/src/mongo/s/commands/SConscript @@ -132,6 +132,7 @@ env.Library( '$BUILD_DIR/mongo/db/query/map_reduce_output_format', '$BUILD_DIR/mongo/db/read_write_concern_defaults', '$BUILD_DIR/mongo/db/repl/hello_auth', + '$BUILD_DIR/mongo/db/repl/hello_command', '$BUILD_DIR/mongo/db/shared_request_handling', '$BUILD_DIR/mongo/db/stats/api_version_metrics', '$BUILD_DIR/mongo/db/stats/counters', diff --git a/src/mongo/s/commands/cluster_hello_cmd.cpp b/src/mongo/s/commands/cluster_hello_cmd.cpp index 630e82d041c..69040ef342d 100644 --- a/src/mongo/s/commands/cluster_hello_cmd.cpp +++ b/src/mongo/s/commands/cluster_hello_cmd.cpp @@ -34,11 +34,13 @@ #include "mongo/bson/util/bson_extract.h" #include "mongo/db/client.h" #include "mongo/db/commands.h" +#include "mongo/db/commands/test_commands_enabled.h" #include "mongo/db/curop.h" #include "mongo/db/logical_session_id.h" #include "mongo/db/operation_context.h" #include "mongo/db/ops/write_ops.h" #include "mongo/db/repl/hello_auth.h" +#include "mongo/db/repl/hello_gen.h" #include "mongo/db/wire_version.h" #include "mongo/logv2/log.h" #include "mongo/rpc/metadata/client_metadata.h" @@ -60,21 +62,22 @@ namespace { constexpr auto kHelloString = "hello"_sd; constexpr auto kCamelCaseIsMasterString = "isMaster"_sd; constexpr auto kLowerCaseIsMasterString = "ismaster"_sd; - +const std::string kAutomationServiceDescriptorFieldName = + HelloCommandReply::kAutomationServiceDescriptorFieldName.toString(); class CmdHello : public BasicCommandWithReplyBuilderInterface { public: CmdHello() : CmdHello(kHelloString, {}) {} - const std::set<std::string>& apiVersions() const { + const std::set<std::string>& apiVersions() const final { return kApiVersions1; } - bool supportsWriteConcern(const BSONObj& cmd) const override { + bool supportsWriteConcern(const BSONObj& cmd) const final { return false; } - AllowedOnSecondary secondaryAllowed(ServiceContext*) const override { + AllowedOnSecondary secondaryAllowed(ServiceContext*) const final { return AllowedOnSecondary::kAlways; } @@ -84,11 +87,11 @@ public: void addRequiredPrivileges(const std::string& dbname, const BSONObj& cmdObj, - std::vector<Privilege>* out) const override { + std::vector<Privilege>* out) const final { // No auth required } - bool requiresAuth() const override { + bool requiresAuth() const final { return false; } @@ -97,6 +100,8 @@ public: const BSONObj& cmdObj, rpc::ReplyBuilderInterface* replyBuilder) final { CommandHelpers::handleMarkKillOnClientDisconnect(opCtx); + const bool apiStrict = APIParameters::get(opCtx).getAPIStrict().value_or(false); + auto cmd = HelloCommand::parse({"hello", apiStrict}, cmdObj); waitInHello.pauseWhileSet(opCtx); @@ -105,51 +110,38 @@ public: // If a client is following the awaitable hello protocol, maxAwaitTimeMS should be // present if and only if topologyVersion is present in the request. - auto topologyVersionElement = cmdObj["topologyVersion"]; - auto maxAwaitTimeMSField = cmdObj["maxAwaitTimeMS"]; + auto clientTopologyVersion = cmd.getTopologyVersion(); + auto maxAwaitTimeMS = cmd.getMaxAwaitTimeMS(); auto curOp = CurOp::get(opCtx); - - // The awaitable field is optional, but if it exists, it requires both other fields. - // Note that the awaitable field only exists to make filtering out these operations - // via currentOp queries easier and is not otherwise explicitly used. - { - bool awaitable = true; - Status status = bsonExtractBooleanField(cmdObj, "awaitable", &awaitable); - if (status.isOK() && (!topologyVersionElement || !maxAwaitTimeMSField)) { - uassert(5135801, - "A request marked awaitable must contain both 'topologyVersion' and " - "'maxAwaitTimeMS'", - !awaitable); - } - } - - boost::optional<TopologyVersion> clientTopologyVersion; boost::optional<Date_t> deadline; boost::optional<ScopeGuard<std::function<void()>>> timerGuard; - if (topologyVersionElement && maxAwaitTimeMSField) { - clientTopologyVersion = TopologyVersion::parse(IDLParserErrorContext("TopologyVersion"), - topologyVersionElement.Obj()); + if (clientTopologyVersion && maxAwaitTimeMS) { uassert(51758, "topologyVersion must have a non-negative counter", clientTopologyVersion->getCounter() >= 0); - long long maxAwaitTimeMS; - uassertStatusOK(bsonExtractIntegerField(cmdObj, "maxAwaitTimeMS", &maxAwaitTimeMS)); - - uassert(51759, "maxAwaitTimeMS must be a non-negative integer", maxAwaitTimeMS >= 0); + uassert(51759, "maxAwaitTimeMS must be a non-negative integer", *maxAwaitTimeMS >= 0); deadline = opCtx->getServiceContext()->getPreciseClockSource()->now() + - Milliseconds(maxAwaitTimeMS); + Milliseconds(*maxAwaitTimeMS); LOGV2_DEBUG(23871, 3, "Using maxAwaitTimeMS for awaitable hello protocol."); curOp->pauseTimer(); timerGuard.emplace([curOp]() { curOp->resumeTimer(); }); } else { + // The awaitable field is optional, but if it exists, it requires both other fields. + // Note that the awaitable field only exists to make filtering out these operations + // via currentOp queries easier and is not otherwise explicitly used. + uassert(5135801, + "A request marked awaitable must contain both 'topologyVersion' and " + "'maxAwaitTimeMS'", + !cmd.getAwaitable() || !cmd.getAwaitable().get()); + uassert(51760, - (topologyVersionElement + (clientTopologyVersion ? "A request with a 'topologyVersion' must include 'maxAwaitTimeMS'" : "A request with 'maxAwaitTimeMS' must include a 'topologyVersion'"), - !topologyVersionElement && !maxAwaitTimeMSField); + !clientTopologyVersion && !maxAwaitTimeMS); } auto result = replyBuilder->getBodyBuilder(); @@ -166,52 +158,54 @@ public: // Try to parse the optional 'helloOk' field. On mongos, if we see this field, we will // respond with helloOk: true so the client knows that it can continue to send the hello // command to mongos. - bool helloOk; - Status status = bsonExtractBooleanField(cmdObj, "helloOk", &helloOk); - if (status.isOK()) { + if (auto helloOk = cmd.getHelloOk()) { // If the hello request contains a "helloOk" field, set _supportsHello on the Client // to the value. - client->setSupportsHello(helloOk); + client->setSupportsHello(*helloOk); // Attach helloOk: true to the response so that the client knows the server supports // the hello command. result.append("helloOk", true); - } else if (status.code() != ErrorCodes::NoSuchKey) { - uassertStatusOK(status); } if (MONGO_unlikely(appendHelloOkToHelloResponse.shouldFail())) { result.append("clientSupportsHello", client->supportsHello()); } - result.appendNumber("maxBsonObjectSize", BSONObjMaxUserSize); - result.appendNumber("maxMessageSizeBytes", MaxMessageSizeBytes); - result.appendNumber("maxWriteBatchSize", write_ops::kMaxWriteBatchSize); - result.appendDate("localTime", jsTime()); - result.append("logicalSessionTimeoutMinutes", localLogicalSessionTimeoutMinutes); - result.appendNumber("connectionId", opCtx->getClient()->getConnectionId()); + result.appendNumber(HelloCommandReply::kMaxBsonObjectSizeFieldName, BSONObjMaxUserSize); + result.appendNumber(HelloCommandReply::kMaxMessageSizeBytesFieldName, MaxMessageSizeBytes); + result.appendNumber(HelloCommandReply::kMaxWriteBatchSizeFieldName, + write_ops::kMaxWriteBatchSize); + result.appendDate(HelloCommandReply::kLocalTimeFieldName, jsTime()); + result.append(HelloCommandReply::kLogicalSessionTimeoutMinutesFieldName, + localLogicalSessionTimeoutMinutes); + result.appendNumber(HelloCommandReply::kConnectionIdFieldName, + opCtx->getClient()->getConnectionId()); // Mongos tries to keep exactly the same version range of the server for which // it is compiled. auto wireSpec = WireSpec::instance().get(); - result.append("maxWireVersion", wireSpec->incomingExternalClient.maxWireVersion); - result.append("minWireVersion", wireSpec->incomingExternalClient.minWireVersion); + result.append(HelloCommandReply::kMaxWireVersionFieldName, + wireSpec->incomingExternalClient.maxWireVersion); + result.append(HelloCommandReply::kMinWireVersionFieldName, + wireSpec->incomingExternalClient.minWireVersion); { const auto& serverParams = ServerParameterSet::getGlobal()->getMap(); - auto iter = serverParams.find("automationServiceDescriptor"); - if (iter != serverParams.end() && iter->second) - iter->second->append(opCtx, result, "automationServiceDescriptor"); + auto iter = serverParams.find(kAutomationServiceDescriptorFieldName); + if (iter != serverParams.end() && iter->second) { + iter->second->append(opCtx, result, kAutomationServiceDescriptorFieldName); + } } MessageCompressorManager::forSession(opCtx->getClient()->session()) - .serverNegotiate(cmdObj, &result); + .serverNegotiate(cmd.getCompression(), &result); if (opCtx->isExhaust()) { LOGV2_DEBUG(23872, 3, "Using exhaust for hello protocol"); uassert(51763, "A hello/isMaster request with exhaust must specify 'maxAwaitTimeMS'", - maxAwaitTimeMSField); + maxAwaitTimeMS); invariant(clientTopologyVersion); InExhaustHello::get(opCtx->getClient()->session().get()) @@ -224,45 +218,59 @@ public: // command parameters should be reused as the next BSONObj command parameters. replyBuilder->setNextInvocation(boost::none); } else { - BSONObjBuilder nextInvocationBuilder; - for (auto&& elt : cmdObj) { - if (elt.fieldNameStringData() == "topologyVersion"_sd) { - BSONObjBuilder topologyVersionBuilder( - nextInvocationBuilder.subobjStart("topologyVersion")); - currentMongosTopologyVersion.serialize(&topologyVersionBuilder); + BSONObjBuilder niBuilder; + for (const auto& elem : cmdObj) { + if (elem.fieldNameStringData() == HelloCommand::kTopologyVersionFieldName) { + BSONObjBuilder tvBuilder( + niBuilder.subobjStart(HelloCommand::kTopologyVersionFieldName)); + currentMongosTopologyVersion.serialize(&tvBuilder); } else { - nextInvocationBuilder.append(elt); + niBuilder.append(elem); } } - replyBuilder->setNextInvocation(nextInvocationBuilder.obj()); + replyBuilder->setNextInvocation(niBuilder.obj()); } } - handleHelloAuth(opCtx, cmdObj, &result); + handleHelloAuth(opCtx, cmd, &result); + + if (getTestCommandsEnabled()) { + validateResult(&result); + } return true; } + void validateResult(BSONObjBuilder* result) { + auto ret = result->asTempObj(); + if (ret[ErrorReply::kErrmsgFieldName].eoo()) { + // Nominal success case, parse the object as-is. + HelloCommandReply::parse({"hello.reply"}, ret); + } else { + // Something went wrong, still try to parse, but accept a few ignorable fields. + StringDataSet ignorable({ErrorReply::kCodeFieldName, ErrorReply::kErrmsgFieldName}); + HelloCommandReply::parse({"hello.reply"}, ret.removeFields(ignorable)); + } + } + protected: CmdHello(const StringData cmdName, const std::initializer_list<StringData>& alias) : BasicCommandWithReplyBuilderInterface(cmdName, alias) {} - virtual bool useLegacyResponseFields() { + virtual bool useLegacyResponseFields() const { return false; } } hello; class CmdIsMaster : public CmdHello { - public: CmdIsMaster() : CmdHello(kCamelCaseIsMasterString, {kLowerCaseIsMasterString}) {} protected: - bool useLegacyResponseFields() override { + bool useLegacyResponseFields() const final { return true; } - } isMaster; } // namespace diff --git a/src/mongo/transport/message_compressor_manager.cpp b/src/mongo/transport/message_compressor_manager.cpp index 4b6f547b372..b6a60cdcebd 100644 --- a/src/mongo/transport/message_compressor_manager.cpp +++ b/src/mongo/transport/message_compressor_manager.cpp @@ -256,24 +256,23 @@ void MessageCompressorManager::clientFinish(const BSONObj& input) { } } -void MessageCompressorManager::serverNegotiate(const BSONObj& input, BSONObjBuilder* output) { +void MessageCompressorManager::serverNegotiate( + const boost::optional<std::vector<StringData>>& clientCompressors, BSONObjBuilder* result) { LOGV2_DEBUG(22934, 3, "Starting server-side compression negotiation"); - auto elem = input.getField("compression"); - // If the "compression" field is missing, then this isMaster request is requesting information - // rather than doing a negotiation - if (elem.eoo()) { + // No advertised compressions, just asking for the last negotiated result. + if (!clientCompressors) { // If we haven't negotiated any compressors yet, then don't append anything to the // output - this makes this compatible with older versions of MongoDB that don't // support compression. - if (_negotiated.size() > 0) { - BSONArrayBuilder sub(output->subarrayStart("compression")); + std::vector<std::string> ret; + if (_negotiated.empty()) { + LOGV2_DEBUG(22935, 3, "Compression negotiation not requested by client"); + } else { + BSONArrayBuilder sub(result->subarrayStart("compression")); for (const auto& algo : _negotiated) { - sub.append(algo->getName()); + sub << algo->getName(); } - sub.doneFast(); - } else { - LOGV2_DEBUG(22935, 3, "Compression negotiation not requested by client"); } return; } @@ -283,16 +282,13 @@ void MessageCompressorManager::serverNegotiate(const BSONObj& input, BSONObjBuil _negotiated.clear(); // First we go through all the compressor names that the client has requested support for - BSONObj theirObj = elem.Obj(); - - if (!theirObj.nFields()) { + if (clientCompressors->empty()) { LOGV2_DEBUG(22936, 3, "No compressors provided"); return; } - for (const auto& elem : theirObj) { + for (const auto& curName : *clientCompressors) { MessageCompressorBase* cur; - auto curName = elem.checkAndGetStringData(); // If the MessageCompressorRegistry knows about a compressor with that name, then it is // valid and we add it to our list of negotiated compressors. if ((cur = _registry->getCompressor(curName))) { @@ -313,14 +309,13 @@ void MessageCompressorManager::serverNegotiate(const BSONObj& input, BSONObjBuil // If the number of compressors that were eventually negotiated is greater than 0, then // we should send that back to the client. - if (_negotiated.size() > 0) { - BSONArrayBuilder sub(output->subarrayStart("compression")); - for (auto algo : _negotiated) { - sub.append(algo->getName()); - } - sub.doneFast(); - } else { + if (_negotiated.empty()) { LOGV2_DEBUG(22939, 3, "Could not agree on compressor to use"); + } else { + BSONArrayBuilder sub(result->subarrayStart("compression")); + for (const auto& algo : _negotiated) { + sub << algo->getName(); + } } } diff --git a/src/mongo/transport/message_compressor_manager.h b/src/mongo/transport/message_compressor_manager.h index 467d942b4c4..9f1a1de35e3 100644 --- a/src/mongo/transport/message_compressor_manager.h +++ b/src/mongo/transport/message_compressor_manager.h @@ -81,14 +81,11 @@ public: /* * Called by a server that has received an isMaster request. * - * This looks for a BSON array called "compression" in input and appends the union of that - * array and the result of _registry->getCompressorNames(). The first name in the compression - * array in input will be used in subsequent calls to compressMessage - * * If no compressors are configured that match those requested by the client, then it will * not append anything to the BSONObjBuilder output. */ - void serverNegotiate(const BSONObj& input, BSONObjBuilder* output); + void serverNegotiate(const boost::optional<std::vector<StringData>>& clientCompressors, + BSONObjBuilder*); /* * Returns a new Message containing the compressed contentx of 'msg'. If compressorId is null, diff --git a/src/mongo/transport/message_compressor_manager_test.cpp b/src/mongo/transport/message_compressor_manager_test.cpp index 979e75d0842..24cff651933 100644 --- a/src/mongo/transport/message_compressor_manager_test.cpp +++ b/src/mongo/transport/message_compressor_manager_test.cpp @@ -85,7 +85,8 @@ void checkNegotiationResult(const BSONObj& result, const std::vector<std::string } } -void checkServerNegotiation(const BSONObj& input, const std::vector<std::string>& expected) { +void checkServerNegotiation(const boost::optional<std::vector<StringData>>& input, + const std::vector<std::string>& expected) { auto registry = buildRegistry(); MessageCompressorManager manager(®istry); @@ -105,8 +106,8 @@ void checkFidelity(const Message& msg, std::unique_ptr<MessageCompressorBase> co registry.finalizeSupportedCompressors().transitional_ignore(); MessageCompressorManager mgr(®istry); - auto negotiator = BSON("isMaster" << 1 << "compression" << BSON_ARRAY(compressorName)); BSONObjBuilder negotiatorOut; + std::vector<StringData> negotiator({compressorName}); mgr.serverNegotiate(negotiator, &negotiatorOut); checkNegotiationResult(negotiatorOut.done(), {compressorName}); @@ -182,26 +183,46 @@ Message buildMessage() { TEST(MessageCompressorManager, NoCompressionRequested) { auto input = BSON("isMaster" << 1); - checkServerNegotiation(input, {}); + checkServerNegotiation(boost::none, {}); } TEST(MessageCompressorManager, NormalCompressionRequested) { - auto input = BSON("isMaster" << 1 << "compression" << BSON_ARRAY("noop")); + std::vector<StringData> input{"noop"_sd}; checkServerNegotiation(input, {"noop"}); } TEST(MessageCompressorManager, BadCompressionRequested) { - auto input = BSON("isMaster" << 1 << "compression" << BSON_ARRAY("fakecompressor")); + std::vector<StringData> input{"fakecompressor"_sd}; checkServerNegotiation(input, {}); } TEST(MessageCompressorManager, BadAndGoodCompressionRequested) { - auto input = BSON("isMaster" << 1 << "compression" - << BSON_ARRAY("fakecompressor" - << "noop")); + std::vector<StringData> input{"fakecompressor"_sd, "noop"_sd}; checkServerNegotiation(input, {"noop"}); } +// Transitional: Parse BSON "isMaster"-like docs for compressor lists. +boost::optional<std::vector<StringData>> parseBSON(BSONObj input) { + auto elem = input["compression"]; + if (!elem) { + return boost::none; + } + + uassert(ErrorCodes::BadValue, + str::stream() << "'compression' is not an array: " << elem, + elem.type() == Array); + + std::vector<StringData> ret; + for (const auto& e : elem.Obj()) { + uassert(ErrorCodes::BadValue, + str::stream() << "'compression' element is not a string: " << e, + e.type() == String); + ret.push_back(e.valueStringData()); + } + + return ret; +} + TEST(MessageCompressorManager, FullNormalCompression) { auto registry = buildRegistry(); MessageCompressorManager clientManager(®istry); @@ -213,7 +234,7 @@ TEST(MessageCompressorManager, FullNormalCompression) { checkNegotiationResult(clientObj, {"noop"}); BSONObjBuilder serverOutput; - serverManager.serverNegotiate(clientObj, &serverOutput); + serverManager.serverNegotiate(parseBSON(clientObj), &serverOutput); auto serverObj = serverOutput.done(); checkNegotiationResult(serverObj, {"noop"}); @@ -285,7 +306,7 @@ TEST(MessageCompressorManager, SERVER_28008) { clientManager.clientBegin(&clientOutput); auto clientObj = clientOutput.done(); BSONObjBuilder serverOutput; - serverManager.serverNegotiate(clientObj, &serverOutput); + serverManager.serverNegotiate(parseBSON(clientObj), &serverOutput); auto serverObj = serverOutput.done(); clientManager.clientFinish(serverObj); |