Skip to content

SignalR ConnectionToken/ConnectionAddress feature #13773

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Sep 16, 2019
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ public Task StartThrowsFormatExceptionIfNegotiationResponseHasNoConnectionId()
return RunInvalidNegotiateResponseTest<FormatException>(ResponseUtils.CreateNegotiationContent(connectionId: string.Empty), "Invalid connection id.");
}

[Fact]
public Task NegotiateResponseWithNegotiateVersionRequiresConnectionToken()
{
return RunInvalidNegotiateResponseTest<InvalidDataException>(ResponseUtils.CreateNegotiationContent(negotiateVersion: 1, connectionToken: null), "Invalid negotiation response received.");
}

[Fact]
public Task ConnectionCannotBeStartedIfNoCommonTransportsBetweenClientAndServer()
{
Expand Down Expand Up @@ -156,6 +162,87 @@ await WithConnectionAsync(
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
}

[Fact]
public async Task ConnectionIdGetsSetWithNegotiateProtocolGreaterThanZero()
{
string connectionId = null;

var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
negotiateVersion = 1,
connectionToken = "different-id",
availableTransports = new object[]
{
new
{
transport = "LongPolling",
transferFormats = new[] { "Text" }
},
},
newField = "ignore this",
})));
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

using (var noErrorScope = new VerifyNoErrorsScope())
{
await WithConnectionAsync(
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
async (connection) =>
{
await connection.StartAsync().OrTimeout();
connectionId = connection.ConnectionId;
});
}

Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=different-id", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
}

[Fact]
public async Task ConnectionTokenFieldIsIgnoredForNegotiateIdLessThanOne()
{
string connectionId = null;

var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
testHttpHandler.OnNegotiate((request, cancellationToken) => ResponseUtils.CreateResponse(HttpStatusCode.OK,
JsonConvert.SerializeObject(new
{
connectionId = "0rge0d00-0040-0030-0r00-000q00r00e00",
connectionToken = "different-id",
availableTransports = new object[]
{
new
{
transport = "LongPolling",
transferFormats = new[] { "Text" }
},
},
newField = "ignore this",
})));
testHttpHandler.OnLongPoll(cancellationToken => ResponseUtils.CreateResponse(HttpStatusCode.NoContent));
testHttpHandler.OnLongPollDelete((token) => ResponseUtils.CreateResponse(HttpStatusCode.Accepted));

using (var noErrorScope = new VerifyNoErrorsScope())
{
await WithConnectionAsync(
CreateConnection(testHttpHandler, loggerFactory: noErrorScope.LoggerFactory),
async (connection) =>
{
await connection.StartAsync().OrTimeout();
connectionId = connection.ConnectionId;
});
}

Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
Assert.Equal("http://fakeuri.org/?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh no, this is ugly. Don't have to do it in this PR, but we might want to remove the version from the query string after negotiate

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will file a follow up issue.

}

[Fact]
public async Task NegotiateThatReturnsUrlGetFollowed()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public static bool IsSocketSendRequest(HttpRequestMessage request)
}

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

Expand Down Expand Up @@ -92,7 +92,7 @@ public static string CreateNegotiationContent(string connectionId = "00000000-00
});
}

return JsonConvert.SerializeObject(new { connectionId, availableTransports });
return JsonConvert.SerializeObject(new { connectionId, availableTransports, connectionToken, negotiateVersion });
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ public partial class HttpConnection : ConnectionContext, IConnectionInherentKeep
private readonly HttpConnectionOptions _httpConnectionOptions;
private ITransport _transport;
private readonly ITransportFactory _transportFactory;
private string _connectionToken;
private string _connectionId;
private readonly ConnectionLogScope _logScope;
private readonly ILoggerFactory _loggerFactory;
Expand Down Expand Up @@ -342,7 +343,7 @@ private async Task SelectAndStartTransport(TransferFormat transferFormat, Cancel
}

// This should only need to happen once
var connectUrl = CreateConnectUrl(uri, negotiationResponse.ConnectionId);
var connectUrl = CreateConnectUrl(uri, _connectionToken);

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

Log.StartingTransport(_logger, transportType, connectUrl);
Expand Down Expand Up @@ -629,7 +630,19 @@ private static bool IsWebSocketsSupported()
private async Task<NegotiationResponse> GetNegotiationResponseAsync(Uri uri, CancellationToken cancellationToken)
{
var negotiationResponse = await NegotiateAsync(uri, _httpClient, _logger, cancellationToken);
_connectionId = negotiationResponse.ConnectionId;
// If the negotiationVersion is greater than zero then we know that the negotiation response contains a
// connectionToken that will be required to conenct. Otherwise we just set the connectionId and the
// connectionToken on the client to the same value.
if (negotiationResponse.Version > 0)
{
_connectionId = negotiationResponse.ConnectionId;
_connectionToken = negotiationResponse.ConnectionToken;
}
else
{
_connectionToken = _connectionId = negotiationResponse.ConnectionId;
}

_logScope.ConnectionId = _connectionId;
return negotiationResponse;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public NegotiationResponse() { }
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public NegotiationResponse() { }
public string AccessToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public System.Collections.Generic.IList<Microsoft.AspNetCore.Http.Connections.AvailableTransport> AvailableTransports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionId { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string ConnectionToken { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Error { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public string Url { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public int Version { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public static class NegotiateProtocol
{
private const string ConnectionIdPropertyName = "connectionId";
private static JsonEncodedText ConnectionIdPropertyNameBytes = JsonEncodedText.Encode(ConnectionIdPropertyName);
private const string ConnectionTokenPropertyName = "connectionToken";
private static JsonEncodedText ConnectionTokenPropertyNameBytes = JsonEncodedText.Encode(ConnectionTokenPropertyName);
private const string UrlPropertyName = "url";
private static JsonEncodedText UrlPropertyNameBytes = JsonEncodedText.Encode(UrlPropertyName);
private const string AccessTokenPropertyName = "accessToken";
Expand Down Expand Up @@ -71,6 +73,11 @@ public static void WriteResponse(NegotiationResponse response, IBufferWriter<byt
writer.WriteString(ConnectionIdPropertyNameBytes, response.ConnectionId);
}

if (response.Version > 0 & !string.IsNullOrEmpty(response.ConnectionToken))
{
writer.WriteString(ConnectionTokenPropertyNameBytes, response.ConnectionToken);
}

writer.WriteStartArray(AvailableTransportsPropertyNameBytes);

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

string connectionId = null;
string connectionToken = null;
string url = null;
string accessToken = null;
List<AvailableTransport> availableTransports = null;
Expand All @@ -151,6 +159,10 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
{
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
}
else if (reader.ValueTextEquals(ConnectionTokenPropertyNameBytes.EncodedUtf8Bytes))
{
connectionToken = reader.ReadAsString(ConnectionTokenPropertyName);
}
else if (reader.ValueTextEquals(NegotiateVersionPropertyNameBytes.EncodedUtf8Bytes))
{
version = reader.ReadAsInt32(NegotiateVersionPropertyName).GetValueOrDefault();
Expand Down Expand Up @@ -202,6 +214,14 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
throw new InvalidDataException($"Missing required property '{ConnectionIdPropertyName}'.");
}

if (version > 0)
{
if (connectionToken == null)
{
throw new InvalidDataException($"Missing required property '{ConnectionTokenPropertyName}'.");
}
}

if (availableTransports == null)
{
throw new InvalidDataException($"Missing required property '{AvailableTransportsPropertyName}'.");
Expand All @@ -211,6 +231,7 @@ public static NegotiationResponse ParseResponse(ReadOnlySpan<byte> content)
return new NegotiationResponse
{
ConnectionId = connectionId,
ConnectionToken = connectionToken,
Url = url,
AccessToken = accessToken,
AvailableTransports = availableTransports,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class NegotiationResponse
public string Url { get; set; }
public string AccessToken { get; set; }
public string ConnectionId { get; set; }
public string ConnectionToken { get; set; }
public int Version { get; set; }
public IList<AvailableTransport> AvailableTransports { get; set; }
public string Error { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,13 @@ internal class HttpConnectionContext : ConnectionContext,
/// Creates the DefaultConnectionContext without Pipes to avoid upfront allocations.
/// The caller is expected to set the <see cref="Transport"/> and <see cref="Application"/> pipes manually.
/// </summary>
/// <param name="id"></param>
/// <param name="connectionId"></param>
/// <param name="connectionToken"></param>
/// <param name="logger"></param>
public HttpConnectionContext(string id, ILogger logger)
public HttpConnectionContext(string connectionId, string connectionToken, ILogger logger)
{
ConnectionId = id;
ConnectionId = connectionId;
ConnectionToken = connectionToken;
LastSeenUtc = DateTime.UtcNow;

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

public HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
: this(id, logger)
internal HttpConnectionContext(string id, IDuplexPipe transport, IDuplexPipe application, ILogger logger = null)
: this(id, null, logger)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this only used for tests? If so, can we add a comment or something? Maybe make it internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we plan to bring this into 3.1 I don't think we can make it internal, but I agree that it should have been internal to start

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Internal class, make all the changes you want!

{
Transport = transport;
Application = application;
Expand Down Expand Up @@ -113,6 +115,8 @@ public DateTime? LastSeenUtcIfInactive

public override string ConnectionId { get; set; }

internal string ConnectionToken { get; set; }

public override IFeatureCollection Features { get; }

public ClaimsPrincipal User { get; set; }
Expand Down
Loading