Skip to content

Commit 08d4906

Browse files
authored
CSHARP-4018: Require hello_command + OP_MSG when loadBalanced=true (#1174)
* CSHARP-4018: Require hello_command + OP_MSG when loadBalanced=true * Fix null reference causing failed tests * Fix more null reference hazards * Incorporated review feedback
1 parent 7baa925 commit 08d4906

17 files changed

+405
-15
lines changed

src/MongoDB.Driver.Core/Core/Authentication/DefaultAuthenticator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public void Authenticate(IConnection connection, ConnectionDescription descripti
8686
if (!description.HelloResult.HasSaslSupportedMechs
8787
&& Feature.ScramSha256Authentication.IsSupported(description.MaxWireVersion))
8888
{
89-
var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi));
89+
var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced));
9090
var helloProtocol = HelloHelper.CreateProtocol(command, _serverApi);
9191
var helloResult = HelloHelper.GetResult(connection, helloProtocol, cancellationToken);
9292
var mergedHelloResult = new HelloResult(description.HelloResult.Wrapped.Merge(helloResult.Wrapped));
@@ -111,7 +111,7 @@ public async Task AuthenticateAsync(IConnection connection, ConnectionDescriptio
111111
if (!description.HelloResult.HasSaslSupportedMechs
112112
&& Feature.ScramSha256Authentication.IsSupported(description.MaxWireVersion))
113113
{
114-
var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi));
114+
var command = CustomizeInitialHelloCommand(HelloHelper.CreateCommand(_serverApi, loadBalanced: connection.Settings.LoadBalanced));
115115
var helloProtocol = HelloHelper.CreateProtocol(command, _serverApi);
116116
var helloResult = await HelloHelper.GetResultAsync(connection, helloProtocol, cancellationToken).ConfigureAwait(false);
117117
var mergedHelloResult = new HelloResult(description.HelloResult.Wrapped.Merge(helloResult.Wrapped));

src/MongoDB.Driver.Core/Core/Connections/HelloHelper.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ internal static BsonDocument CreateCommand(ServerApi serverApi, bool helloOk = f
5050
(topologyVersion != null && maxAwaitTime.HasValue),
5151
$"Both {nameof(topologyVersion)} and {nameof(maxAwaitTime)} must be filled or null.");
5252

53-
var helloCommandName = helloOk || serverApi != null ? "hello" : OppressiveLanguageConstants.LegacyHelloCommandName;
53+
var helloCommandName = helloOk || loadBalanced || serverApi != null ? "hello" : OppressiveLanguageConstants.LegacyHelloCommandName;
5454
return new BsonDocument
5555
{
5656
{ helloCommandName, 1 },

src/MongoDB.Driver.Core/Core/Servers/RoundTripTimeMonitor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ private void MonitorServer()
129129
}
130130
else
131131
{
132-
var helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk);
132+
var helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk, loadBalanced: _roundTripTimeConnection.Settings.LoadBalanced);
133133
var helloProtocol = HelloHelper.CreateProtocol(helloCommand, _serverApi);
134134

135135
var stopwatch = Stopwatch.StartNew();

src/MongoDB.Driver.Core/Core/Servers/ServerMonitor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,11 +189,11 @@ private CommandWireProtocol<BsonDocument> InitializeHelloProtocol(IConnection co
189189

190190
var veryLargeHeartbeatInterval = TimeSpan.FromDays(1); // the server doesn't support Infinite value, so we set just a big enough value
191191
var maxAwaitTime = _serverMonitorSettings.HeartbeatInterval == Timeout.InfiniteTimeSpan ? veryLargeHeartbeatInterval : _serverMonitorSettings.HeartbeatInterval;
192-
helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk, connection.Description.HelloResult.TopologyVersion, maxAwaitTime);
192+
helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk, connection.Description.HelloResult.TopologyVersion, maxAwaitTime, connection.Settings.LoadBalanced);
193193
}
194194
else
195195
{
196-
helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk);
196+
helloCommand = HelloHelper.CreateCommand(_serverApi, helloOk, loadBalanced: connection.Settings.LoadBalanced);
197197
}
198198

199199
return HelloHelper.CreateProtocol(helloCommand, _serverApi, commandResponseHandling);

src/MongoDB.Driver.Core/Core/WireProtocol/CommandWireProtocol.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@ private IWireProtocol<TCommandResult> CreateSupportedWireProtocol(IConnection co
193193
// the server supports OP_MSG.
194194
// As well since server API versioning is supported on MongoDB 5.0+, we also know that
195195
// OP_MSG will be supported regardless and can skip the server checks for other messages.
196-
if (_serverApi != null || connection.Description != null)
196+
if (_serverApi != null || connection.Description != null || connection.Settings.LoadBalanced)
197197
{
198198
return _cachedWireProtocol = CreateCommandUsingCommandMessageWireProtocol();
199199
}

tests/MongoDB.Driver.Core.Tests/Core/Authentication/MongoAWSAuthenticatorTests.cs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
using MongoDB.Driver.Core.Authentication;
2424
using MongoDB.Driver.Core.Authentication.External;
2525
using MongoDB.Driver.Core.Clusters;
26+
using MongoDB.Driver.Core.Configuration;
2627
using MongoDB.Driver.Core.Connections;
2728
using MongoDB.Driver.Core.Helpers;
2829
using MongoDB.Driver.Core.Misc;
@@ -214,6 +215,83 @@ public void Authenticate_should_send_serverApi_with_command_wire_protocol(
214215
sentMessages[1].Should().Be(GetExpectedSaslContinueCommandMessage(actualRequestId1, expectedClientSecondMessage, expectedServerApiString));
215216
}
216217

218+
[Theory]
219+
[ParameterAttributeData]
220+
public void Authenticate_with_loadBalancedConnection_should_use_command_wire_protocol(
221+
[Values(false, true)] bool async)
222+
{
223+
var dateTime = DateTime.UtcNow;
224+
var clientNonce = __randomByteGenerator.Generate(ClientNonceLength);
225+
var serverNonce = Combine(clientNonce, __randomByteGenerator.Generate(ClientNonceLength));
226+
var host = "sts.amazonaws.com";
227+
var credential = new UsernamePasswordCredential("$external", "permanentuser", "FAKEFAKEFAKEFAKEFAKEfakefakefakefakefake");
228+
229+
AwsSignatureVersion4.CreateAuthorizationRequest(
230+
dateTime,
231+
credential.Username,
232+
credential.Password,
233+
null,
234+
serverNonce,
235+
host,
236+
out var authHeader,
237+
out var timestamp);
238+
239+
var mockClock = new Mock<IClock>();
240+
mockClock.Setup(x => x.UtcNow).Returns(dateTime);
241+
242+
var mockRandomByteGenerator = new Mock<IRandomByteGenerator>();
243+
mockRandomByteGenerator.Setup(x => x.Generate(It.IsAny<int>())).Returns(clientNonce);
244+
245+
var expectedClientFirstMessage = new BsonDocument
246+
{
247+
{ "r", clientNonce },
248+
{ "p", (int)'n' }
249+
};
250+
var expectedClientSecondMessage = new BsonDocument
251+
{
252+
{ "a", authHeader },
253+
{ "d", timestamp }
254+
};
255+
var serverFirstMessage = new BsonDocument
256+
{
257+
{ "s", serverNonce },
258+
{ "h", host }
259+
};
260+
261+
var saslStartCommandResponseString = $"{{ conversationId : 1, done : false, payload : BinData(0,\"{ToBase64(serverFirstMessage.ToBson())}\"), ok : 1 }}";
262+
var saslContinueCommandResponseString = "{ conversationId : 1, done : true, payload : BinData(0,\"\"), ok : 1}";
263+
264+
var subject = new MongoAWSAuthenticator(credential, null, mockRandomByteGenerator.Object, Mock.Of<IExternalAuthenticationCredentialsProvider<AwsCredentials>>(), mockClock.Object, null);
265+
266+
var connection = new MockConnection(__serverId, new ConnectionSettings(loadBalanced:true), null);
267+
var saslStartResponse = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson(saslStartCommandResponseString));
268+
var saslContinueResponse = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson(saslContinueCommandResponseString));
269+
connection.EnqueueCommandResponseMessage(saslStartResponse);
270+
connection.EnqueueCommandResponseMessage(saslContinueResponse);
271+
connection.Description = null;
272+
273+
if (async)
274+
{
275+
subject.AuthenticateAsync(connection, __descriptionCommandWireProtocol, CancellationToken.None).GetAwaiter().GetResult();
276+
}
277+
else
278+
{
279+
subject.Authenticate(connection, __descriptionCommandWireProtocol, CancellationToken.None);
280+
}
281+
282+
SpinWait.SpinUntil(() => connection.GetSentMessages().Count >= 2, TimeSpan.FromSeconds(5)).Should().BeTrue();
283+
284+
var sentMessages = MessageHelper.TranslateMessagesToBsonDocuments(connection.GetSentMessages());
285+
sentMessages.Count.Should().Be(2);
286+
287+
var actualRequestId0 = sentMessages[0]["requestId"].AsInt32;
288+
var actualRequestId1 = sentMessages[1]["requestId"].AsInt32;
289+
290+
var expectedEndString = ", \"$readPreference\" : { \"mode\" : \"primaryPreferred\" }";
291+
sentMessages[0].Should().Be(GetExpectedSaslStartCommandMessage(actualRequestId0, expectedClientFirstMessage, expectedEndString));
292+
sentMessages[1].Should().Be(GetExpectedSaslContinueCommandMessage(actualRequestId1, expectedClientSecondMessage, expectedEndString));
293+
}
294+
217295
[Theory]
218296
[ParameterAttributeData]
219297
public void Authenticate_should_throw_an_AuthenticationException_when_authentication_fails(

tests/MongoDB.Driver.Core.Tests/Core/Authentication/MongoDBCRAuthenticatorTests.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using FluentAssertions;
2020
using MongoDB.Bson;
2121
using MongoDB.Driver.Core.Clusters;
22+
using MongoDB.Driver.Core.Configuration;
2223
using MongoDB.Driver.Core.Connections;
2324
using MongoDB.Driver.Core.Helpers;
2425
using MongoDB.Driver.Core.Misc;
@@ -171,5 +172,47 @@ public void Authenticate_should_send_serverApi_with_command_wire_protocol(
171172
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId0}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ getnonce : 1, $db : \"source\"{expectedServerApiString} }} }} ] }}");
172173
sentMessages[1].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId1}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ authenticate : 1, user : \"user\", nonce : \"2375531c32080ae8\", key : \"21742f26431831d5cfca035a08c5bdf6\", $db : \"source\"{expectedServerApiString} }} }} ] }}");
173174
}
175+
176+
[Theory]
177+
[ParameterAttributeData]
178+
public void Authenticate_with_loadBalancedConnection_should_use_command_wire_protocol(
179+
[Values(false, true)] bool async)
180+
{
181+
#pragma warning disable 618
182+
var subject = new MongoDBCRAuthenticator(__credential, null);
183+
#pragma warning restore 618
184+
185+
var connection = new MockConnection(__serverId, new ConnectionSettings(loadBalanced:true), null);
186+
var getNonceResponse = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson("{nonce: \"2375531c32080ae8\", ok: 1}"));
187+
var authenticateResponse = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson("{ok: 1}"));
188+
connection.EnqueueCommandResponseMessage(getNonceResponse);
189+
connection.EnqueueCommandResponseMessage(authenticateResponse);
190+
connection.Description = null;
191+
192+
var expectedRequestId = RequestMessage.CurrentGlobalRequestId + 1;
193+
194+
if (async)
195+
{
196+
subject.AuthenticateAsync(connection, __descriptionCommandWireProtocol, CancellationToken.None).GetAwaiter().GetResult();
197+
}
198+
else
199+
{
200+
subject.Authenticate(connection, __descriptionCommandWireProtocol, CancellationToken.None);
201+
}
202+
203+
SpinWait.SpinUntil(() => connection.GetSentMessages().Count >= 2, TimeSpan.FromSeconds(5)).Should().BeTrue();
204+
205+
var sentMessages = MessageHelper.TranslateMessagesToBsonDocuments(connection.GetSentMessages());
206+
sentMessages.Count.Should().Be(2);
207+
208+
var actualRequestId0 = sentMessages[0]["requestId"].AsInt32;
209+
var actualRequestId1 = sentMessages[1]["requestId"].AsInt32;
210+
actualRequestId0.Should().BeInRange(expectedRequestId, expectedRequestId + 10);
211+
actualRequestId1.Should().BeInRange(actualRequestId0 + 1, actualRequestId0 + 11);
212+
213+
var expectedEndString = ", \"$readPreference\" : { \"mode\" : \"primaryPreferred\" }";
214+
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId0}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ getnonce : 1, $db : \"source\"{expectedEndString} }} }} ] }}");
215+
sentMessages[1].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId1}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ authenticate : 1, user : \"user\", nonce : \"2375531c32080ae8\", key : \"21742f26431831d5cfca035a08c5bdf6\", $db : \"source\"{expectedEndString} }} }} ] }}");
216+
}
174217
}
175218
}

tests/MongoDB.Driver.Core.Tests/Core/Authentication/MongoDBX509AuthenticatorTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using FluentAssertions;
2020
using MongoDB.Bson;
2121
using MongoDB.Driver.Core.Clusters;
22+
using MongoDB.Driver.Core.Configuration;
2223
using MongoDB.Driver.Core.Connections;
2324
using MongoDB.Driver.Core.Helpers;
2425
using MongoDB.Driver.Core.Misc;
@@ -88,6 +89,42 @@ public void Authenticate_should_send_serverApi_with_command_wire_protocol(
8889
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ authenticate : 1, mechanism : \"MONGODB-X509\", user : \"CN=client,OU=kerneluser,O=10Gen,L=New York City,ST=New York,C=US\", $db : \"$external\"{expectedServerApiString} }} }} ] }}");
8990
}
9091

92+
[Theory]
93+
[ParameterAttributeData]
94+
public void Authenticate_with_loadBalancedConnection_should_use_command_wire_protocol(
95+
[Values(false, true)] bool async)
96+
{
97+
var subject = new MongoDBX509Authenticator("CN=client,OU=kerneluser,O=10Gen,L=New York City,ST=New York,C=US", null);
98+
99+
var connection = new MockConnection(__serverId, new ConnectionSettings(loadBalanced: true), null);
100+
var response = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson("{ok: 1}"));
101+
connection.EnqueueCommandResponseMessage(response);
102+
connection.Description = null;
103+
104+
var expectedRequestId = RequestMessage.CurrentGlobalRequestId + 1;
105+
106+
if (async)
107+
{
108+
subject.AuthenticateAsync(connection, __descriptionCommandWireProtocol, CancellationToken.None).GetAwaiter().GetResult();
109+
}
110+
else
111+
{
112+
subject.Authenticate(connection, __descriptionCommandWireProtocol, CancellationToken.None);
113+
}
114+
115+
SpinWait.SpinUntil(() => connection.GetSentMessages().Count >= 1, TimeSpan.FromSeconds(5)).Should().BeTrue();
116+
117+
var sentMessages = MessageHelper.TranslateMessagesToBsonDocuments(connection.GetSentMessages());
118+
sentMessages.Count.Should().Be(1);
119+
120+
var actualRequestId = sentMessages[0]["requestId"].AsInt32;
121+
actualRequestId.Should().BeInRange(expectedRequestId, expectedRequestId + 10);
122+
123+
var expectedEndString = ", \"$readPreference\" : { \"mode\" : \"primaryPreferred\" }";
124+
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ authenticate : 1, mechanism : \"MONGODB-X509\", user : \"CN=client,OU=kerneluser,O=10Gen,L=New York City,ST=New York,C=US\", $db : \"$external\"{expectedEndString} }} }} ] }}");
125+
}
126+
127+
91128
[Theory]
92129
[ParameterAttributeData]
93130
public void Authenticate_should_throw_an_AuthenticationException_when_authentication_fails(

tests/MongoDB.Driver.Core.Tests/Core/Authentication/PlainAuthenticatorTests.cs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
using FluentAssertions;
2020
using MongoDB.Bson;
2121
using MongoDB.Driver.Core.Clusters;
22+
using MongoDB.Driver.Core.Configuration;
2223
using MongoDB.Driver.Core.Connections;
2324
using MongoDB.Driver.Core.Helpers;
2425
using MongoDB.Driver.Core.Misc;
@@ -151,5 +152,40 @@ public void Authenticate_should_send_serverApi_with_command_wire_protocol(
151152
var expectedServerApiString = useServerApi ? ", apiVersion : \"1\", apiStrict : true, apiDeprecationErrors : true" : "";
152153
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ saslStart : 1, mechanism : \"PLAIN\", payload : new BinData(0, \"AHVzZXIAcGVuY2ls\"), $db : \"source\"{expectedServerApiString} }} }} ] }}");
153154
}
155+
156+
[Theory]
157+
[ParameterAttributeData]
158+
public void Authenticate_with_loadBalancedConnection_should_use_command_wire_protocol(
159+
[Values(false, true)] bool async)
160+
{
161+
var subject = new PlainAuthenticator(__credential, null);
162+
163+
var connection = new MockConnection(__serverId, new ConnectionSettings(loadBalanced:true), null);
164+
var saslStartResponse = MessageHelper.BuildCommandResponse(RawBsonDocumentHelper.FromJson("{ conversationId : 0, payload : BinData(0,\"\"), done : true, ok : 1 }"));
165+
connection.EnqueueCommandResponseMessage(saslStartResponse);
166+
connection.Description = null;
167+
168+
var expectedRequestId = RequestMessage.CurrentGlobalRequestId + 1;
169+
170+
if (async)
171+
{
172+
subject.AuthenticateAsync(connection, __descriptionCommandWireProtocol, CancellationToken.None).GetAwaiter().GetResult();
173+
}
174+
else
175+
{
176+
subject.Authenticate(connection, __descriptionCommandWireProtocol, CancellationToken.None);
177+
}
178+
179+
SpinWait.SpinUntil(() => connection.GetSentMessages().Count >= 1, TimeSpan.FromSeconds(5)).Should().BeTrue();
180+
181+
var sentMessages = MessageHelper.TranslateMessagesToBsonDocuments(connection.GetSentMessages());
182+
sentMessages.Count.Should().Be(1);
183+
184+
var actualRequestId = sentMessages[0]["requestId"].AsInt32;
185+
actualRequestId.Should().BeInRange(expectedRequestId, expectedRequestId + 10);
186+
187+
var expectedEndString = ", \"$readPreference\" : { \"mode\" : \"primaryPreferred\" }";
188+
sentMessages[0].Should().Be($"{{ opcode : \"opmsg\", requestId : {actualRequestId}, responseTo : 0, sections : [ {{ payloadType : 0, document : {{ saslStart : 1, mechanism : \"PLAIN\", payload : new BinData(0, \"AHVzZXIAcGVuY2ls\"), $db : \"source\"{expectedEndString} }} }} ] }}");
189+
}
154190
}
155191
}

0 commit comments

Comments
 (0)