SignalR Negotiate protocol versioning (#13389)
This commit is contained in:
parent
ab1347ef20
commit
ea8c2b92f4
|
|
@ -114,6 +114,71 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ServerRejectsClientWithOldProtocol()
|
||||||
|
{
|
||||||
|
bool ExpectedError(WriteContext writeContext)
|
||||||
|
{
|
||||||
|
return writeContext.LoggerName == typeof(HttpConnection).FullName &&
|
||||||
|
writeContext.EventId.Name == "ErrorWithNegotiation";
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocol = HubProtocols["json"];
|
||||||
|
using (StartServer<Startup>(out var server, ExpectedError))
|
||||||
|
{
|
||||||
|
var connectionBuilder = new HubConnectionBuilder()
|
||||||
|
.WithLoggerFactory(LoggerFactory)
|
||||||
|
.WithUrl(server.Url + "/negotiateProtocolVersion12", HttpTransportType.LongPolling);
|
||||||
|
connectionBuilder.Services.AddSingleton(protocol);
|
||||||
|
|
||||||
|
var connection = connectionBuilder.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var ex = await Assert.ThrowsAnyAsync<Exception>(() => connection.StartAsync()).OrTimeout();
|
||||||
|
Assert.Equal("The client requested version '1', but the server does not support this version.", ex.Message);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ClientCanConnectToServerWithLowerMinimumProtocol()
|
||||||
|
{
|
||||||
|
var protocol = HubProtocols["json"];
|
||||||
|
using (StartServer<Startup>(out var server))
|
||||||
|
{
|
||||||
|
var connectionBuilder = new HubConnectionBuilder()
|
||||||
|
.WithLoggerFactory(LoggerFactory)
|
||||||
|
.WithUrl(server.Url + "/negotiateProtocolVersionNegative", HttpTransportType.LongPolling);
|
||||||
|
connectionBuilder.Services.AddSingleton(protocol);
|
||||||
|
|
||||||
|
var connection = connectionBuilder.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await connection.StartAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerFactory.CreateLogger<HubConnectionTests>().LogError(ex, "{ExceptionType} from test", ex.GetType().FullName);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await connection.DisposeAsync().OrTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
[MemberData(nameof(HubProtocolsAndTransportsAndHubPaths))]
|
||||||
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)
|
public async Task CanSendAndReceiveMessage(string protocolName, HttpTransportType transportType, string path)
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,16 @@ namespace Microsoft.AspNetCore.SignalR.Client.FunctionalTests
|
||||||
|
|
||||||
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
endpoints.MapHub<TestHub>("/default-nowebsockets", options => options.Transports = HttpTransportType.LongPolling | HttpTransportType.ServerSentEvents);
|
||||||
|
|
||||||
|
endpoints.MapHub<TestHub>("/negotiateProtocolVersion12", options =>
|
||||||
|
{
|
||||||
|
options.MinimumProtocolVersion = 12;
|
||||||
|
});
|
||||||
|
|
||||||
|
endpoints.MapHub<TestHub>("/negotiateProtocolVersionNegative", options =>
|
||||||
|
{
|
||||||
|
options.MinimumProtocolVersion = -1;
|
||||||
|
});
|
||||||
|
|
||||||
endpoints.MapGet("/generateJwtToken", context =>
|
endpoints.MapGet("/generateJwtToken", context =>
|
||||||
{
|
{
|
||||||
return context.Response.WriteAsync(GenerateJwtToken());
|
return context.Response.WriteAsync(GenerateJwtToken());
|
||||||
|
|
|
||||||
|
|
@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
var httpHandler = new TestHttpMessageHandler();
|
var httpHandler = new TestHttpMessageHandler();
|
||||||
|
|
||||||
var connectResponseTcs = new TaskCompletionSource<object>();
|
var connectResponseTcs = new TaskCompletionSource<object>();
|
||||||
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", async (_, __) =>
|
httpHandler.OnGet("/?negotiateVersion=1&id=00000000-0000-0000-0000-000000000000", async (_, __) =>
|
||||||
{
|
{
|
||||||
await connectResponseTcs.Task;
|
await connectResponseTcs.Task;
|
||||||
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
|
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
|
||||||
|
|
|
||||||
|
|
@ -50,12 +50,12 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
}
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate")]
|
[InlineData("http://fakeuri.org/", "http://fakeuri.org/negotiate?negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
|
[InlineData("http://fakeuri.org/?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0")]
|
[InlineData("http://fakeuri.org?q=1/0", "http://fakeuri.org/negotiate?q=1/0&negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate")]
|
[InlineData("http://fakeuri.org/endpoint", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate")]
|
[InlineData("http://fakeuri.org/endpoint/", "http://fakeuri.org/endpoint/negotiate?negotiateVersion=1")]
|
||||||
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0")]
|
[InlineData("http://fakeuri.org/endpoint?q=1/0", "http://fakeuri.org/endpoint/negotiate?q=1/0&negotiateVersion=1")]
|
||||||
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
|
public async Task CorrectlyHandlesQueryStringWhenAppendingNegotiateToUrl(string requestedUrl, string expectedNegotiate)
|
||||||
{
|
{
|
||||||
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
var testHttpHandler = new TestHttpMessageHandler(autoNegotiate: false);
|
||||||
|
|
@ -119,6 +119,43 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
|
Assert.Equal("0rge0d00-0040-0030-0r00-000q00r00e00", connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NegotiateCanHaveNewFields()
|
||||||
|
{
|
||||||
|
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",
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task NegotiateThatReturnsUrlGetFollowed()
|
public async Task NegotiateThatReturnsUrlGetFollowed()
|
||||||
{
|
{
|
||||||
|
|
@ -172,10 +209,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
|
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
||||||
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -278,10 +315,10 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Assert.Equal("http://fakeuri.org/negotiate", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
|
Assert.Equal("http://fakeuri.org/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[0].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat/negotiate", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat/negotiate?negotiateVersion=1", testHttpHandler.ReceivedRequests[1].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[2].RequestUri.ToString());
|
||||||
Assert.Equal("https://another.domain.url/chat?id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
Assert.Equal("https://another.domain.url/chat?negotiateVersion=1&id=0rge0d00-0040-0030-0r00-000q00r00e00", testHttpHandler.ReceivedRequests[3].RequestUri.ToString());
|
||||||
// Delete request
|
// Delete request
|
||||||
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
Assert.Equal(5, testHttpHandler.ReceivedRequests.Count);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
|
|
@ -117,7 +120,7 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
|
||||||
});
|
});
|
||||||
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
|
testHttpMessageHandler.OnRequest((request, next, cancellationToken) =>
|
||||||
{
|
{
|
||||||
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.StartsWith("/?id="))
|
if (request.Method.Equals(HttpMethod.Delete) && request.RequestUri.PathAndQuery.Contains("&id="))
|
||||||
{
|
{
|
||||||
deleteCts.Cancel();
|
deleteCts.Cancel();
|
||||||
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.Accepted));
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
// Not configurable on purpose, high enough that if we reach here, it's likely
|
// Not configurable on purpose, high enough that if we reach here, it's likely
|
||||||
// a buggy server
|
// a buggy server
|
||||||
private static readonly int _maxRedirects = 100;
|
private static readonly int _maxRedirects = 100;
|
||||||
|
private static readonly int _protocolVersionNumber = 1;
|
||||||
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
private static readonly Task<string> _noAccessToken = Task.FromResult<string>(null);
|
||||||
|
|
||||||
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
|
||||||
|
|
@ -428,8 +429,9 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
urlBuilder.Path += "/";
|
urlBuilder.Path += "/";
|
||||||
}
|
}
|
||||||
urlBuilder.Path += "negotiate";
|
urlBuilder.Path += "negotiate";
|
||||||
|
var uri = Utils.AppendQueryString(urlBuilder.Uri, $"negotiateVersion={_protocolVersionNumber}");
|
||||||
|
|
||||||
using (var request = new HttpRequestMessage(HttpMethod.Post, urlBuilder.Uri))
|
using (var request = new HttpRequestMessage(HttpMethod.Post, uri))
|
||||||
{
|
{
|
||||||
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
|
// Corefx changed the default version and High Sierra curlhandler tries to upgrade request
|
||||||
request.Version = new Version(1, 1);
|
request.Version = new Version(1, 1);
|
||||||
|
|
@ -466,7 +468,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Client
|
||||||
throw new FormatException("Invalid connection id.");
|
throw new FormatException("Invalid connection id.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return Utils.AppendQueryString(url, "id=" + connectionId);
|
return Utils.AppendQueryString(url, $"negotiateVersion={_protocolVersionNumber}&id=" + connectionId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
|
private async Task StartTransport(Uri connectUrl, HttpTransportType transportType, TransferFormat transferFormat, CancellationToken cancellationToken)
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,6 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
public string ConnectionId { [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 Error { [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 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 { } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,5 +36,6 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
public string ConnectionId { [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 Error { [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 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 { } }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,8 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
|
private static JsonEncodedText TransferFormatsPropertyNameBytes = JsonEncodedText.Encode(TransferFormatsPropertyName);
|
||||||
private const string ErrorPropertyName = "error";
|
private const string ErrorPropertyName = "error";
|
||||||
private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName);
|
private static JsonEncodedText ErrorPropertyNameBytes = JsonEncodedText.Encode(ErrorPropertyName);
|
||||||
|
private const string NegotiateVersionPropertyName = "negotiateVersion";
|
||||||
|
private static JsonEncodedText NegotiateVersionPropertyNameBytes = JsonEncodedText.Encode(NegotiateVersionPropertyName);
|
||||||
|
|
||||||
// Use C#7.3's ReadOnlySpan<byte> optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/
|
// Use C#7.3's ReadOnlySpan<byte> optimization for static data https://vcsjones.com/2019/02/01/csharp-readonly-span-bytes-static/
|
||||||
// Used to detect ASP.NET SignalR Server connection attempt
|
// Used to detect ASP.NET SignalR Server connection attempt
|
||||||
|
|
@ -41,6 +43,19 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
var writer = reusableWriter.GetJsonWriter();
|
var writer = reusableWriter.GetJsonWriter();
|
||||||
writer.WriteStartObject();
|
writer.WriteStartObject();
|
||||||
|
|
||||||
|
// If we already have an error its due to a protocol version incompatibility.
|
||||||
|
// We can just write the error and complete the JSON object and return.
|
||||||
|
if (!string.IsNullOrEmpty(response.Error))
|
||||||
|
{
|
||||||
|
writer.WriteString(ErrorPropertyNameBytes, response.Error);
|
||||||
|
writer.WriteEndObject();
|
||||||
|
writer.Flush();
|
||||||
|
Debug.Assert(writer.CurrentDepth == 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.WriteNumber(NegotiateVersionPropertyNameBytes, response.Version);
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(response.Url))
|
if (!string.IsNullOrEmpty(response.Url))
|
||||||
{
|
{
|
||||||
writer.WriteString(UrlPropertyNameBytes, response.Url);
|
writer.WriteString(UrlPropertyNameBytes, response.Url);
|
||||||
|
|
@ -116,6 +131,7 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
string accessToken = null;
|
string accessToken = null;
|
||||||
List<AvailableTransport> availableTransports = null;
|
List<AvailableTransport> availableTransports = null;
|
||||||
string error = null;
|
string error = null;
|
||||||
|
int version = 0;
|
||||||
|
|
||||||
var completed = false;
|
var completed = false;
|
||||||
while (!completed && reader.CheckRead())
|
while (!completed && reader.CheckRead())
|
||||||
|
|
@ -135,6 +151,10 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
{
|
{
|
||||||
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
|
connectionId = reader.ReadAsString(ConnectionIdPropertyName);
|
||||||
}
|
}
|
||||||
|
else if (reader.ValueTextEquals(NegotiateVersionPropertyNameBytes.EncodedUtf8Bytes))
|
||||||
|
{
|
||||||
|
version = reader.ReadAsInt32(NegotiateVersionPropertyName).GetValueOrDefault();
|
||||||
|
}
|
||||||
else if (reader.ValueTextEquals(AvailableTransportsPropertyNameBytes.EncodedUtf8Bytes))
|
else if (reader.ValueTextEquals(AvailableTransportsPropertyNameBytes.EncodedUtf8Bytes))
|
||||||
{
|
{
|
||||||
reader.CheckRead();
|
reader.CheckRead();
|
||||||
|
|
@ -195,6 +215,7 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
AccessToken = accessToken,
|
AccessToken = accessToken,
|
||||||
AvailableTransports = availableTransports,
|
AvailableTransports = availableTransports,
|
||||||
Error = error,
|
Error = error,
|
||||||
|
Version = version
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
public string AccessToken { get; set; }
|
public string AccessToken { get; set; }
|
||||||
public string ConnectionId { get; set; }
|
public string ConnectionId { get; set; }
|
||||||
|
public int Version { get; set; }
|
||||||
public IList<AvailableTransport> AvailableTransports { get; set; }
|
public IList<AvailableTransport> AvailableTransports { get; set; }
|
||||||
public string Error { get; set; }
|
public string Error { get; set; }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
public long ApplicationMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public long ApplicationMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizeData> AuthorizationData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public System.Collections.Generic.IList<Microsoft.AspNetCore.Authorization.IAuthorizeData> AuthorizationData { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
public Microsoft.AspNetCore.Http.Connections.LongPollingOptions LongPolling { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public Microsoft.AspNetCore.Http.Connections.LongPollingOptions LongPolling { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
|
public int MinimumProtocolVersion { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public long TransportMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public long TransportMaxBufferSize { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
public Microsoft.AspNetCore.Http.Connections.HttpTransportType Transports { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
|
||||||
public Microsoft.AspNetCore.Http.Connections.WebSocketOptions WebSockets { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
public Microsoft.AspNetCore.Http.Connections.WebSocketOptions WebSockets { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
|
||||||
|
|
|
||||||
|
|
@ -57,5 +57,11 @@ namespace Microsoft.AspNetCore.Http.Connections
|
||||||
/// Gets or sets the maximum buffer size of the application writer.
|
/// Gets or sets the maximum buffer size of the application writer.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public long ApplicationMaxBufferSize { get; set; }
|
public long ApplicationMaxBufferSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the minimum protocol verison supported by the server.
|
||||||
|
/// The default value is 0, the lowest possible protocol version.
|
||||||
|
/// </summary>
|
||||||
|
public int MinimumProtocolVersion { get; set; } = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
private static readonly Action<ILogger, string, Exception> _failedToReadHttpRequestBody =
|
private static readonly Action<ILogger, string, Exception> _failedToReadHttpRequestBody =
|
||||||
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body.");
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(14, "FailedToReadHttpRequestBody"), "Connection {TransportConnectionId} failed to read the HTTP request body.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, int, Exception> _negotiateProtocolVersionMismatch =
|
||||||
|
LoggerMessage.Define<int>(LogLevel.Debug, new EventId(15, "NegotiateProtocolVersionMismatch"), "The client requested version '{clientProtocolVersion}', but the server does not support this version.");
|
||||||
|
|
||||||
|
private static readonly Action<ILogger, string, Exception> _invalidNegotiateProtocolVersion =
|
||||||
|
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(16, "InvalidNegotiateProtocolVersion"), "The client requested an invalid protocol version '{queryStringVersionValue}'");
|
||||||
|
|
||||||
public static void ConnectionDisposed(ILogger logger, string connectionId)
|
public static void ConnectionDisposed(ILogger logger, string connectionId)
|
||||||
{
|
{
|
||||||
_connectionDisposed(logger, connectionId, null);
|
_connectionDisposed(logger, connectionId, null);
|
||||||
|
|
@ -121,6 +127,16 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
{
|
{
|
||||||
_failedToReadHttpRequestBody(logger, connectionId, ex);
|
_failedToReadHttpRequestBody(logger, connectionId, ex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void NegotiateProtocolVersionMismatch(ILogger logger, int clientProtocolVersion)
|
||||||
|
{
|
||||||
|
_negotiateProtocolVersionMismatch(logger, clientProtocolVersion, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void InvalidNegotiateProtocolVersion(ILogger logger, string requestedProtocolVersion)
|
||||||
|
{
|
||||||
|
_invalidNegotiateProtocolVersion(logger, requestedProtocolVersion, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
private readonly HttpConnectionManager _manager;
|
private readonly HttpConnectionManager _manager;
|
||||||
private readonly ILoggerFactory _loggerFactory;
|
private readonly ILoggerFactory _loggerFactory;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private static readonly int _protocolVersion = 1;
|
||||||
|
|
||||||
public HttpConnectionDispatcher(HttpConnectionManager manager, ILoggerFactory loggerFactory)
|
public HttpConnectionDispatcher(HttpConnectionManager manager, ILoggerFactory loggerFactory)
|
||||||
{
|
{
|
||||||
|
|
@ -306,9 +307,48 @@ namespace Microsoft.AspNetCore.Http.Connections.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void WriteNegotiatePayload(IBufferWriter<byte> writer, string connectionId, HttpContext context, HttpConnectionDispatcherOptions options)
|
private void WriteNegotiatePayload(IBufferWriter<byte> writer, string connectionId, HttpContext context, HttpConnectionDispatcherOptions options)
|
||||||
{
|
{
|
||||||
var response = new NegotiationResponse();
|
var response = new NegotiationResponse();
|
||||||
|
|
||||||
|
if (context.Request.Query.TryGetValue("NegotiateVersion", out var queryStringVersion))
|
||||||
|
{
|
||||||
|
// Set the negotiate response to the protocol we use.
|
||||||
|
var queryStringVersionValue = queryStringVersion.ToString();
|
||||||
|
if (int.TryParse(queryStringVersionValue, out var clientProtocolVersion))
|
||||||
|
{
|
||||||
|
if (clientProtocolVersion < options.MinimumProtocolVersion)
|
||||||
|
{
|
||||||
|
response.Error = $"The client requested version '{clientProtocolVersion}', but the server does not support this version.";
|
||||||
|
Log.NegotiateProtocolVersionMismatch(_logger, clientProtocolVersion);
|
||||||
|
NegotiateProtocol.WriteResponse(response, writer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
else if (clientProtocolVersion > _protocolVersion)
|
||||||
|
{
|
||||||
|
response.Version = _protocolVersion;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Version = clientProtocolVersion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response.Error = $"The client requested an invalid protocol version '{queryStringVersionValue}'";
|
||||||
|
Log.InvalidNegotiateProtocolVersion(_logger, queryStringVersionValue);
|
||||||
|
NegotiateProtocol.WriteResponse(response, writer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (options.MinimumProtocolVersion > 0)
|
||||||
|
{
|
||||||
|
// NegotiateVersion wasn't parsed meaning the client requests version 0.
|
||||||
|
response.Error = $"The client requested version '0', but the server does not support this version.";
|
||||||
|
NegotiateProtocol.WriteResponse(response, writer);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
response.ConnectionId = connectionId;
|
response.ConnectionId = connectionId;
|
||||||
response.AvailableTransports = new List<AvailableTransport>();
|
response.AvailableTransports = new List<AvailableTransport>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,64 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task InvalidNegotiateProtocolVersionThrows()
|
||||||
|
{
|
||||||
|
using (StartVerifiableLog())
|
||||||
|
{
|
||||||
|
var manager = CreateConnectionManager(LoggerFactory);
|
||||||
|
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<TestConnectionHandler>();
|
||||||
|
services.AddOptions();
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
context.Request.Path = "/foo";
|
||||||
|
context.Request.Method = "POST";
|
||||||
|
context.Response.Body = ms;
|
||||||
|
context.Request.QueryString = new QueryString("?negotiateVersion=Invalid");
|
||||||
|
|
||||||
|
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4 };
|
||||||
|
await dispatcher.ExecuteNegotiateAsync(context, options);
|
||||||
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
||||||
|
var error = negotiateResponse.Value<string>("error");
|
||||||
|
Assert.Equal("The client requested an invalid protocol version 'Invalid'", error);
|
||||||
|
|
||||||
|
var connectionId = negotiateResponse.Value<string>("connectionId");
|
||||||
|
Assert.Null(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task NoNegotiateVersionInQueryStringThrowsWhenMinProtocolVersionIsSet()
|
||||||
|
{
|
||||||
|
using (StartVerifiableLog())
|
||||||
|
{
|
||||||
|
var manager = CreateConnectionManager(LoggerFactory);
|
||||||
|
var dispatcher = new HttpConnectionDispatcher(manager, LoggerFactory);
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
services.AddSingleton<TestConnectionHandler>();
|
||||||
|
services.AddOptions();
|
||||||
|
var ms = new MemoryStream();
|
||||||
|
context.Request.Path = "/foo";
|
||||||
|
context.Request.Method = "POST";
|
||||||
|
context.Response.Body = ms;
|
||||||
|
context.Request.QueryString = new QueryString("");
|
||||||
|
|
||||||
|
var options = new HttpConnectionDispatcherOptions { TransportMaxBufferSize = 4, ApplicationMaxBufferSize = 4, MinimumProtocolVersion = 1 };
|
||||||
|
await dispatcher.ExecuteNegotiateAsync(context, options);
|
||||||
|
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
|
||||||
|
|
||||||
|
var error = negotiateResponse.Value<string>("error");
|
||||||
|
Assert.Equal("The client requested version '0', but the server does not support this version.", error);
|
||||||
|
|
||||||
|
var connectionId = negotiateResponse.Value<string>("connectionId");
|
||||||
|
Assert.Null(connectionId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData(HttpTransportType.LongPolling)]
|
[InlineData(HttpTransportType.LongPolling)]
|
||||||
[InlineData(HttpTransportType.ServerSentEvents)]
|
[InlineData(HttpTransportType.ServerSentEvents)]
|
||||||
|
|
|
||||||
|
|
@ -13,12 +13,17 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
public class NegotiateProtocolTests
|
public class NegotiateProtocolTests
|
||||||
{
|
{
|
||||||
[Theory]
|
[Theory]
|
||||||
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null)]
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 0)]
|
||||||
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null)]
|
[InlineData("{\"connectionId\":\"\",\"availableTransports\":[]}", "", new string[0], null, null, 0)]
|
||||||
[InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null)]
|
[InlineData("{\"url\": \"http://foo.com/chat\"}", null, null, "http://foo.com/chat", null, 0)]
|
||||||
[InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token")]
|
[InlineData("{\"url\": \"http://foo.com/chat\", \"accessToken\": \"token\"}", null, null, "http://foo.com/chat", "token", 0)]
|
||||||
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null)]
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"transport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0)]
|
||||||
public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken)
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 0)]
|
||||||
|
[InlineData("{\"negotiateVersion\":123,\"connectionId\":\"123\",\"availableTransports\":[{\"\\u0074ransport\":\"test\",\"transferFormats\":[]}]}", "123", new[] { "test" }, null, null, 123)]
|
||||||
|
[InlineData("{\"negotiateVersion\":123,\"negotiateVersion\":321,\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 321)]
|
||||||
|
[InlineData("{\"ignore\":123,\"negotiateVersion\":123,\"connectionId\":\"123\",\"availableTransports\":[]}", "123", new string[0], null, null, 123)]
|
||||||
|
[InlineData("{\"connectionId\":\"123\",\"availableTransports\":[],\"negotiateVersion\":123}", "123", new string[0], null, null, 123)]
|
||||||
|
public void ParsingNegotiateResponseMessageSuccessForValid(string json, string connectionId, string[] availableTransports, string url, string accessToken, int version)
|
||||||
{
|
{
|
||||||
var responseData = Encoding.UTF8.GetBytes(json);
|
var responseData = Encoding.UTF8.GetBytes(json);
|
||||||
var response = NegotiateProtocol.ParseResponse(responseData);
|
var response = NegotiateProtocol.ParseResponse(responseData);
|
||||||
|
|
@ -27,6 +32,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
|
Assert.Equal(availableTransports?.Length, response.AvailableTransports?.Count);
|
||||||
Assert.Equal(url, response.Url);
|
Assert.Equal(url, response.Url);
|
||||||
Assert.Equal(accessToken, response.AccessToken);
|
Assert.Equal(accessToken, response.AccessToken);
|
||||||
|
Assert.Equal(version, response.Version);
|
||||||
|
|
||||||
if (response.AvailableTransports != null)
|
if (response.AvailableTransports != null)
|
||||||
{
|
{
|
||||||
|
|
@ -82,7 +88,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
string json = Encoding.UTF8.GetString(writer.ToArray());
|
string json = Encoding.UTF8.GetString(writer.ToArray());
|
||||||
|
|
||||||
Assert.Equal("{\"availableTransports\":[]}", json);
|
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[]}", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,7 +107,7 @@ namespace Microsoft.AspNetCore.Http.Connections.Tests
|
||||||
|
|
||||||
string json = Encoding.UTF8.GetString(writer.ToArray());
|
string json = Encoding.UTF8.GetString(writer.ToArray());
|
||||||
|
|
||||||
Assert.Equal("{\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
|
Assert.Equal("{\"negotiateVersion\":0,\"availableTransports\":[{\"transport\":null,\"transferFormats\":[]}]}", json);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue