Skip to content

Commit 1ed31bd

Browse files
author
John Luo
authored
Merge pull request #14402 from aspnet/brecon/negotiateVersion
Negotiate protocol versioning and ConnectionID split
2 parents 6bc4d27 + 1aa29f0 commit 1ed31bd

30 files changed

+1158
-175
lines changed

src/SignalR/clients/csharp/Client/test/FunctionalTests/HubConnectionTests.cs

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,71 @@ public async Task CheckFixedMessage(string protocolName, HttpTransportType trans
114114
}
115115
}
116116

117+
[Fact]
118+
public async Task ServerRejectsClientWithOldProtocol()
119+
{
120+
bool ExpectedError(WriteContext writeContext)
121+
{
122+
return writeContext.LoggerName == typeof(HttpConnection).FullName &&
123+
writeContext.EventId.Name == "ErrorWithNegotiation";
124+
}
125+
126+
var protocol = HubProtocols["json"];
127+
using (StartServer<Startup>(out var server, ExpectedError))
128+
{
129+
var connectionBuilder = new HubConnectionBuilder()
130+
.WithLoggerFactory(LoggerFactory)
131+
.WithUrl(server.Url + "/negotiateProtocolVersion12", HttpTransportType.LongPolling);
132+
connectionBuilder.Services.AddSingleton(protocol);
133+
134+
var connection = connectionBuilder.Build();
135+
136+
try
137+
{
138+
var ex = await Assert.ThrowsAnyAsync<Exception>(() => connection.StartAsync()).OrTimeout();
139+
Assert.Equal("The client requested version '1', but the server does not support this version.", ex.Message);
140+
}
141+
catch (Exception ex)
142+
{
143+
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
144+
throw;
145+
}
146+
finally
147+
{
148+
await connection.DisposeAsync().OrTimeout();
149+
}
150+
}
151+
}
152+
153+
[Fact]
154+
public async Task ClientCanConnectToServerWithLowerMinimumProtocol()
155+
{
156+
var protocol = HubProtocols["json"];
157+
using (StartServer<Startup>(out var server))
158+
{
159+
var connectionBuilder = new HubConnectionBuilder()
160+
.WithLoggerFactory(LoggerFactory)
161+
.WithUrl(server.Url + "/negotiateProtocolVersionNegative", HttpTransportType.LongPolling);
162+
connectionBuilder.Services.AddSingleton(protocol);
163+
164+
var connection = connectionBuilder.Build();
165+
166+
try
167+
{
168+
await connection.StartAsync().OrTimeout();
169+
}
170+
catch (Exception ex)
171+
{
172+
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
173+
throw;
174+
}
175+
finally
176+
{
177+
await connection.DisposeAsync().OrTimeout();
178+
}
179+
}
180+
}
181+
117182
[Theory]
118183
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
119184
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)

src/SignalR/clients/csharp/Client/test/FunctionalTests/Startup.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,16 @@ public void Configure(IApplicationBuilder app)
6969

7070
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
7171

72+
endpoints.MapHub<TestHub>("/negotiateProtocolVersion12", options =>
73+
{
74+
options.MinimumProtocolVersion = 12;
75+
});
76+
77+
endpoints.MapHub<TestHub>("/negotiateProtocolVersionNegative", options =>
78+
{
79+
options.MinimumProtocolVersion = -1;
80+
});
81+
7282
endpoints.MapGet("/generateJwtToken", context =>
7383
{
7484
return context.Response.WriteAsync(GenerateJwtToken());

src/SignalR/clients/csharp/Client/test/UnitTests/HttpConnectionTests.Negotiate.cs

Lines changed: 196 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId()
3636
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
3737
}
3838

39+
[Fact]
40+
public Task NegotiateResponseWithNegotiateVersionRequiresConnectionToken()
41+
{
42+
return RunInvalidNegotiateResponseTest<InvalidDataException>(ResponseUtils.CreateNegotiationContent(negotiateVersion: 1, connectionToken: null), "Invalid negotiation response received.");
43+
}
44+
3945
[Fact]
4046
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
4147
{
@@ -50,12 +56,12 @@ public Task ConnectionCannotBeStartedIfNoTransportProvidedByServer()
5056
}
5157

5258
[Theory]
53-
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate")]
54-
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
55-
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
56-
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate")]
57-
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate")]
58-
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0")]
59+
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
60+
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
61+
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
62+
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
63+
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
64+
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0&negotiateVersion=1")]
5965
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
6066
{
6167
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
@@ -119,6 +125,124 @@ await WithConnectionAsync(
119125
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
120126
}
121127

128+
[Fact]
129+
public async Task NegotiateCanHaveNewFields()
130+
{
131+
string connectionId = null;
132+
133+
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
134+
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
135+
JsonConvert.SerializeObject(new
136+
{
137+
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
138+
availableTransports = new object[]
139+
{
140+
new
141+
{
142+
transport = "LongPolling",
143+
transferFormats = new[] { "Text" }
144+
},
145+
},
146+
newField = "ignore this",
147+
})));
148+
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
149+
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
150+
151+
using (var noErrorScope = new VerifyNoErrorsScope())
152+
{
153+
await WithConnectionAsync(
154+
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
155+
async (connection) =>
156+
{
157+
await connection.StartAsync().OrTimeout();
158+
connectionId = connection.ConnectionId;
159+
});
160+
}
161+
162+
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
163+
}
164+
165+
[Fact]
166+
public async Task ConnectionIdGetsSetWithNegotiateProtocolGreaterThanZero()
167+
{
168+
string connectionId = null;
169+
170+
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
171+
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
172+
JsonConvert.SerializeObject(new
173+
{
174+
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
175+
negotiateVersion = 1,
176+
connectionToken = "different-id",
177+
availableTransports = new object[]
178+
{
179+
new
180+
{
181+
transport = "LongPolling",
182+
transferFormats = new[] { "Text" }
183+
},
184+
},
185+
newField = "ignore this",
186+
})));
187+
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
188+
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
189+
190+
using (var noErrorScope = new VerifyNoErrorsScope())
191+
{
192+
await WithConnectionAsync(
193+
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
194+
async (connection) =>
195+
{
196+
await connection.StartAsync().OrTimeout();
197+
connectionId = connection.ConnectionId;
198+
});
199+
}
200+
201+
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
202+
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
203+
Assert.Equal("http://fakeuri.org/?id=different-id", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
204+
}
205+
206+
[Fact]
207+
public async Task ConnectionTokenFieldIsIgnoredForNegotiateIdLessThanOne()
208+
{
209+
string connectionId = null;
210+
211+
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
212+
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
213+
JsonConvert.SerializeObject(new
214+
{
215+
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
216+
connectionToken = "different-id",
217+
availableTransports = new object[]
218+
{
219+
new
220+
{
221+
transport = "LongPolling",
222+
transferFormats = new[] { "Text" }
223+
},
224+
},
225+
newField = "ignore this",
226+
})));
227+
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
228+
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
229+
230+
using (var noErrorScope = new VerifyNoErrorsScope())
231+
{
232+
await WithConnectionAsync(
233+
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
234+
async (connection) =>
235+
{
236+
await connection.StartAsync().OrTimeout();
237+
connectionId = connection.ConnectionId;
238+
});
239+
}
240+
241+
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
242+
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
243+
Assert.Equal("http://fakeuri.org/?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
244+
}
245+
122246
[Fact]
123247
public async Task NegotiateThatReturnsUrlGetFollowed()
124248
{
@@ -172,8 +296,8 @@ await WithConnectionAsync(
172296
});
173297
}
174298

175-
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
176-
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
299+
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
300+
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
177301
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
178302
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
179303
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
@@ -278,14 +402,76 @@ await WithConnectionAsync(
278402
});
279403
}
280404

281-
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
282-
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
405+
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
406+
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
283407
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
284408
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
285409
// Delete request
286410
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
287411
}
288412

413+
[Fact]
414+
public async Task NegotiateThatReturnsRedirectUrlDoesNotAddAnotherNegotiateVersionQueryString()
415+
{
416+
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
417+
var negotiateCount = 0;
418+
testHttpHandler.OnNegotiate((request, cancellationToken) =>
419+
{
420+
negotiateCount++;
421+
if (negotiateCount == 1)
422+
{
423+
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
424+
JsonConvert.SerializeObject(new
425+
{
426+
url = "https://another.domain.url/chat?negotiateVersion=1"
427+
}));
428+
}
429+
else
430+
{
431+
return ResponseUtils.CreateResponse(HttpStatusCode.OK,
432+
JsonConvert.SerializeObject(new
433+
{
434+
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
435+
availableTransports = new object[]
436+
{
437+
new
438+
{
439+
transport = "LongPolling",
440+
transferFormats = new[] { "Text" }
441+
},
442+
}
443+
}));
444+
}
445+
});
446+
447+
testHttpHandler.OnLongPoll((token) =>
448+
{
449+
var tcs = new TaskCompletionSource<HttpResponseMessage>(TaskCreationOptions.RunContinuationsAsynchronously);
450+
451+
token.Register(() => tcs.TrySetResult(ResponseUtils.CreateResponse(HttpStatusCode.NoContent)));
452+
453+
return tcs.Task;
454+
});
455+
456+
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
457+
458+
using (var noErrorScope = new VerifyNoErrorsScope())
459+
{
460+
await WithConnectionAsync(
461+
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
462+
async (connection) =>
463+
{
464+
await connection.StartAsync().OrTimeout();
465+
});
466+
}
467+
468+
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
469+
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
470+
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
471+
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
472+
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
473+
}
474+
289475
[Fact]
290476
public async Task StartSkipsOverTransportsThatTheClientDoesNotUnderstand()
291477
{

src/SignalR/clients/csharp/Client/test/UnitTests/ResponseUtils.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ public static bool IsSocketSendRequest(HttpRequestMessage request)
6262
}
6363

6464
public static string CreateNegotiationContent(string connectionId = "00000000-0000-0000-0000-000000000000",
65-
HttpTransportType? transportTypes = null)
65+
HttpTransportType? transportTypes = null, string connectionToken = "connection-token", int negotiateVersion = 0)
6666
{
6767
var availableTransports = new List<object>();
6868

@@ -92,7 +92,7 @@ public static string CreateNegotiationContent(string connectionId = "00000000-00
9292
});
9393
}
9494

95-
return JsonConvert.SerializeObject(new { connectionId, availableTransports });
95+
return JsonConvert.SerializeObject(new { connectionId, availableTransports, connectionToken, negotiateVersion });
9696
}
9797
}
9898
}

src/SignalR/clients/csharp/Client/test/UnitTests/TestHttpMessageHandler.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
14
using System;
25
using System.Collections.Generic;
36
using System.Net;
@@ -117,7 +120,7 @@ public static TestHttpMessageHandler CreateDefault()
117120
});
118121
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
119122
{
120-
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.StartsWith("/?id="))
123+
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.Contains("id="))
121124
{
122125
deleteCts.Cancel();
123126
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

0 commit comments

Comments
 (0)