Skip to content

Commit 7d59ae1

Browse files
mikaelm12BrennanConroy
authored andcommitted
SignalR ConnectionToken/ConnectionAddress feature (#13773)
1 parent ea8c2b9 commit 7d59ae1

13 files changed

+354
-102
lines changed

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

Lines changed: 87 additions & 0 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
{
@@ -156,6 +162,87 @@ await WithConnectionAsync(
156162
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
157163
}
158164

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/?negotiateVersion=1&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/?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
244+
}
245+
159246
[Fact]
160247
public async Task NegotiateThatReturnsUrlGetFollowed()
161248
{

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/Http.Connections.Client/src/HttpConnection.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public partial class HttpConnection : ConnectionContext, IConnectionInherentKeep
4242
private readonly HttpConnectionOptions _httpConnectionOptions;
4343
private ITransport _transport;
4444
private readonly ITransportFactory _transportFactory;
45+
private string _connectionToken;
4546
private string _connectionId;
4647
private readonly ConnectionLogScope _logScope;
4748
private readonly ILoggerFactory _loggerFactory;
@@ -342,7 +343,7 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel
342343
}
343344

344345
// This should only need to happen once
345-
var connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
346+
var connectUrl = CreateConnectUrl(uri, _connectionToken);
346347

347348
// We're going to search for the transfer format as a string because we don't want to parse
348349
// all the transfer formats in the negotiation response, and we want to allow transfer formats
@@ -383,7 +384,7 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel
383384
if (negotiationResponse == null)
384385
{
385386
negotiationResponse = await GetNegotiationResponseAsync(uri, cancellationToken);
386-
connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
387+
connectUrl = CreateConnectUrl(uri, _connectionToken);
387388
}
388389

389390
Log.StartingTransport(_logger, transportType, connectUrl);
@@ -609,7 +610,19 @@ private static bool IsWebSocketsSupported()
609610
private async Task<NegotiationResponse> GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
610611
{
611612
var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, cancellationToken);
612-
_connectionId = negotiationResponse.ConnectionId;
613+
// If the negotiationVersion is greater than zero then we know that the negotiation response contains a
614+
// connectionToken that will be required to conenct. Otherwise we just set the connectionId and the
615+
// connectionToken on the client to the same value.
616+
if (negotiationResponse.Version > 0)
617+
{
618+
_connectionId = negotiationResponse.ConnectionId;
619+
_connectionToken = negotiationResponse.ConnectionToken;
620+
}
621+
else
622+
{
623+
_connectionToken = _connectionId = negotiationResponse.ConnectionId;
624+
}
625+
613626
_logScope.ConnectionId = _connectionId;
614627
return negotiationResponse;
615628
}

src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netcoreapp.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public NegotiationResponse() { }
3434
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3535
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3636
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
37+
public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3738
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3839
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3940
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

src/SignalR/common/Http.Connections.Common/ref/Microsoft.AspNetCore.Http.Connections.Common.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public NegotiationResponse() { }
3434
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3535
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3636
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
37+
public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3738
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3839
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
3940
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }

src/SignalR/common/Http.Connections.Common/src/NegotiateProtocol.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public static class NegotiateProtocol
1515
{
1616
private const string ConnectionIdPropertyName = "connectionId";
1717
private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName);
18+
private const string ConnectionTokenPropertyName = "connectionToken";
19+
private static JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName);
1820
private const string UrlPropertyName = "url";
1921
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
2022
private const string AccessTokenPropertyName = "accessToken";
@@ -71,6 +73,11 @@ public static void WriteResponse(NegotiationResponse response, IBufferWriter<byt
7173
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
7274
}
7375

76+
if (response.Version > 0 && !string.IsNullOrEmpty(response.ConnectionToken))
77+
{
78+
writer.WriteString(ConnectionTokenPropertyNameBytes, response.ConnectionToken);
79+
}
80+
7481
writer.WriteStartArray(AvailableTransportsPropertyNameBytes);
7582

7683
if (response.AvailableTransports != null)
@@ -127,6 +134,7 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
127134
reader.EnsureObjectStart();
128135

129136
string connectionId = null;
137+
string connectionToken = null;
130138
string url = null;
131139
string accessToken = null;
132140
List<AvailableTransport> availableTransports = null;
@@ -151,6 +159,10 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
151159
{
152160
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
153161
}
162+
else if (reader.ValueTextEquals(ConnectionTokenPropertyNameBytes.EncodedUtf8Bytes))
163+
{
164+
connectionToken = reader.ReadAsString(ConnectionTokenPropertyName);
165+
}
154166
else if (reader.ValueTextEquals(NegotiateVersionPropertyNameBytes.EncodedUtf8Bytes))
155167
{
156168
version = reader.ReadAsInt32(NegotiateVersionPropertyName).GetValueOrDefault();
@@ -202,6 +214,14 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
202214
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
203215
}
204216

217+
if (version > 0)
218+
{
219+
if (connectionToken == null)
220+
{
221+
throw new InvalidDataException($"Missing required property '{ConnectionTokenPropertyName}'.");
222+
}
223+
}
224+
205225
if (availableTransports == null)
206226
{
207227
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
@@ -211,6 +231,7 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
211231
return new NegotiationResponse
212232
{
213233
ConnectionId = connectionId,
234+
ConnectionToken = connectionToken,
214235
Url = url,
215236
AccessToken = accessToken,
216237
AvailableTransports = availableTransports,

src/SignalR/common/Http.Connections.Common/src/NegotiationResponse.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class NegotiationResponse
1010
public string Url { get; set; }
1111
public string AccessToken { get; set; }
1212
public string ConnectionId { get; set; }
13+
public string ConnectionToken { get; set; }
1314
public int Version { get; set; }
1415
public IList<AvailableTransport> AvailableTransports { get; set; }
1516
public string Error { get; set; }

src/SignalR/common/Http.Connections/src/Internal/HttpConnectionContext.cs

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ internal class HttpConnectionContext : ConnectionContext,
4848
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
4949
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
5050
/// </summary>
51-
/// <param name="id"></param>
51+
/// <param name="connectionId"></param>
52+
/// <param name="connectionToken"></param>
5253
/// <param name="logger"></param>
53-
public HttpConnectionContext(string id, ILogger logger)
54+
public HttpConnectionContext(string connectionId, string connectionToken, ILogger logger)
5455
{
55-
ConnectionId = id;
56+
ConnectionId = connectionId;
57+
ConnectionToken = connectionToken;
5658
LastSeenUtc = DateTime.UtcNow;
5759

5860
// The default behavior is that both formats are supported.
@@ -74,8 +76,8 @@ public HttpConnectionContext(string id, ILogger logger)
7476
Features.Set<IConnectionInherentKeepAliveFeature>(this);
7577
}
7678

77-
public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
78-
: this(id, logger)
79+
internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
80+
: this(id, null, logger)
7981
{
8082
Transport = transport;
8183
Application = application;
@@ -113,6 +115,8 @@ public DateTime? LastSeenUtcIfInactive
113115

114116
public override string ConnectionId { get; set; }
115117

118+
internal string ConnectionToken { get; set; }
119+
116120
public override IFeatureCollection Features { get; }
117121

118122
public ClaimsPrincipal User { get; set; }

0 commit comments

Comments
 (0)