Merge pull request #1689 from aspnet/release/2.1

Merge Release/2.1
This commit is contained in:
Mikael Mengistu 2018-03-22 19:42:08 +00:00 committed by GitHub
commit c5b6be6b55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 211 additions and 13 deletions

View File

@ -269,6 +269,52 @@ describe("HttpConnection", () => {
}
});
it("does not select ServerSentEvents transport when not available in environment", async (done) => {
const serverSentEventsTransport = { transport: "ServerSentEvents", transferFormats: [ "Text" ] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => ({ connectionId: "42", availableTransports: [serverSentEventsTransport] })),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// ServerSentEvents is only transport returned from server but is not selected
// because there is no support in the environment, leading to the following error
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
});
it("does not select WebSockets transport when not available in environment", async (done) => {
const webSocketsTransport = { transport: "WebSockets", transferFormats: [ "Text" ] };
const options: IHttpConnectionOptions = {
...commonOptions,
httpClient: new TestHttpClient()
.on("POST", (r) => ({ connectionId: "42", availableTransports: [webSocketsTransport] })),
} as IHttpConnectionOptions;
const connection = new HttpConnection("http://tempuri.org", options);
try {
await connection.start(TransferFormat.Text);
fail();
done();
} catch (e) {
// WebSockets is only transport returned from server but is not selected
// because there is no support in the environment, leading to the following error
expect(e.message).toBe("Unable to initialize any of the available transports.");
done();
}
});
describe(".constructor", () => {
it("throws if no Url is provided", async () => {
// Force TypeScript to let us call the constructor incorrectly :)

View File

@ -160,8 +160,13 @@ export class HttpConnection implements IConnection {
const transferFormats = endpoint.transferFormats.map((s) => TransferFormat[s]);
if (!requestedTransport || transport === requestedTransport) {
if (transferFormats.indexOf(requestedTransferFormat) >= 0) {
this.logger.log(LogLevel.Trace, `Selecting transport '${TransportType[transport]}'`);
return transport;
if ((transport === TransportType.WebSockets && typeof WebSocket === "undefined") ||
(transport === TransportType.ServerSentEvents && typeof EventSource === "undefined")) {
this.logger.log(LogLevel.Trace, `Skipping transport '${TransportType[transport]}' because it is not supported in your environment.'`);
} else {
this.logger.log(LogLevel.Trace, `Selecting transport '${TransportType[transport]}'`);
return transport;
}
} else {
this.logger.log(LogLevel.Trace, `Skipping transport '${TransportType[transport]}' because it does not support the requested transfer format '${TransferFormat[requestedTransferFormat]}'.`);
}

View File

@ -3,8 +3,10 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Protocols;
using Microsoft.AspNetCore.SignalR.Internal.Formatters;
using Microsoft.AspNetCore.Sockets;
@ -41,15 +43,18 @@ namespace Microsoft.AspNetCore.SignalR.Internal.Protocol
{
while (BinaryMessageParser.TryParseMessage(ref input, out var payload))
{
messages.Add(ParseMessage(payload.ToArray(), binder));
var isArray = MemoryMarshal.TryGetArray(payload, out var arraySegment);
// This will never be false unless we started using un-managed buffers
Debug.Assert(isArray);
messages.Add(ParseMessage(arraySegment.Array, arraySegment.Offset, binder));
}
return messages.Count > 0;
}
private static HubMessage ParseMessage(byte[] input, IInvocationBinder binder)
private static HubMessage ParseMessage(byte[] input, int startOffset, IInvocationBinder binder)
{
using (var unpacker = Unpacker.Create(input))
using (var unpacker = Unpacker.Create(input, startOffset))
{
_ = ReadArrayLength(unpacker, "elementCount");

View File

@ -98,6 +98,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
private static readonly Action<ILogger, string, Exception> _transportFailed =
LoggerMessage.Define<string>(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {TransportName} because it failed to initialize.");
private static readonly Action<ILogger, Exception> _webSocketsNotSupportedByOperatingSystem =
LoggerMessage.Define(LogLevel.Debug, new EventId(30, "WebSocketsNotSupportedByOperatingSystem"), "Skipping WebSockets because they are not supported by the operating system.");
public static void HttpConnectionStarting(ILogger logger)
{
_httpConnectionStarting(logger, null);
@ -260,6 +263,11 @@ namespace Microsoft.AspNetCore.Sockets.Client
_transportFailed(logger, transport.ToString(), ex);
}
}
public static void WebSocketsNotSupportedByOperatingSystem(ILogger logger)
{
_webSocketsNotSupportedByOperatingSystem(logger, null);
}
}
}
}

View File

@ -8,6 +8,7 @@ using System.IO;
using System.IO.Pipelines;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http.Features;
@ -25,6 +26,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
public partial class HttpConnection : IConnection
{
private static readonly TimeSpan HttpClientTimeout = TimeSpan.FromSeconds(120);
private static readonly Version Windows8Version = new Version(6, 2);
private readonly ILoggerFactory _loggerFactory;
private readonly ILogger _logger;
@ -203,6 +205,7 @@ namespace Microsoft.AspNetCore.Sockets.Client
var connectUrl = Url;
if (_requestedTransportType == TransportType.WebSockets)
{
// if we're running on Windows 7 this could throw because the OS does not support web sockets
Log.StartingTransport(_logger, _requestedTransportType, connectUrl);
await StartTransport(connectUrl, _requestedTransportType, transferFormat);
}
@ -233,6 +236,12 @@ namespace Microsoft.AspNetCore.Sockets.Client
continue;
}
if (transportType == TransportType.WebSockets && !IsWebSocketsSupported())
{
Log.WebSocketsNotSupportedByOperatingSystem(_logger);
continue;
}
try
{
if ((transportType & _requestedTransportType) == 0)
@ -711,6 +720,26 @@ namespace Microsoft.AspNetCore.Sockets.Client
}
}
private static bool IsWebSocketsSupported()
{
#if NETCOREAPP2_1
// .NET Core 2.1 and above has a managed implementation
return true;
#else
bool isWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
if (!isWindows)
{
// Assume other OSes have websockets
return true;
}
else
{
// Windows 8 and above has websockets
return Environment.OSVersion.Version >= Windows8Version;
}
#endif
}
// Internal because it's used by logging to avoid ToStringing prematurely.
internal enum ConnectionState
{

View File

@ -54,8 +54,9 @@ namespace Microsoft.AspNetCore.Sockets.Client
Log.StartTransport(_logger, transferFormat);
var startTcs = new TaskCompletionSource<object>(TaskContinuationOptions.RunContinuationsAsynchronously);
var sendTask = SendUtils.SendMessages(url, _application, _httpClient, _httpOptions, _transportCts, _logger);
var receiveTask = OpenConnection(_application, url, _transportCts.Token);
var receiveTask = OpenConnection(_application, url, startTcs, _transportCts.Token);
Running = Task.WhenAll(sendTask, receiveTask).ContinueWith(t =>
{
@ -66,17 +67,30 @@ namespace Microsoft.AspNetCore.Sockets.Client
return t;
}).Unwrap();
return Task.CompletedTask;
return startTcs.Task;
}
private async Task OpenConnection(IDuplexPipe application, Uri url, CancellationToken cancellationToken)
private async Task OpenConnection(IDuplexPipe application, Uri url, TaskCompletionSource<object> startTcs, CancellationToken cancellationToken)
{
Log.StartReceive(_logger);
var request = new HttpRequestMessage(HttpMethod.Get, url);
SendUtils.PrepareHttpRequest(request, _httpOptions);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/event-stream"));
var response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
HttpResponseMessage response;
try
{
response = await _httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
startTcs.TrySetResult(null);
}
catch (Exception ex)
{
Log.TransportStopping(_logger);
startTcs.TrySetException(ex);
return;
}
using (var stream = await response.Content.ReadAsStreamAsync())
{

View File

@ -368,7 +368,7 @@ namespace Microsoft.AspNetCore.Sockets
logScope.ConnectionId = connection.ConnectionId;
// Get the bytes for the connection id
var negotiateResponseBuffer = Encoding.UTF8.GetBytes(GetNegotiatePayload(connection.ConnectionId, options));
var negotiateResponseBuffer = Encoding.UTF8.GetBytes(GetNegotiatePayload(connection.ConnectionId, context, options));
Log.NegotiationRequest(_logger);
@ -377,7 +377,7 @@ namespace Microsoft.AspNetCore.Sockets
return context.Response.Body.WriteAsync(negotiateResponseBuffer, 0, negotiateResponseBuffer.Length);
}
private static string GetNegotiatePayload(string connectionId, HttpSocketOptions options)
private static string GetNegotiatePayload(string connectionId, HttpContext context, HttpSocketOptions options)
{
var sb = new StringBuilder();
using (var jsonWriter = new JsonTextWriter(new StringWriter(sb)))
@ -387,18 +387,25 @@ namespace Microsoft.AspNetCore.Sockets
jsonWriter.WriteValue(connectionId);
jsonWriter.WritePropertyName("availableTransports");
jsonWriter.WriteStartArray();
if ((options.Transports & TransportType.WebSockets) != 0)
if (ServerHasWebSockets(context.Features))
{
WriteTransport(jsonWriter, nameof(TransportType.WebSockets), TransferFormat.Text | TransferFormat.Binary);
if ((options.Transports & TransportType.WebSockets) != 0)
{
WriteTransport(jsonWriter, nameof(TransportType.WebSockets), TransferFormat.Text | TransferFormat.Binary);
}
}
if ((options.Transports & TransportType.ServerSentEvents) != 0)
{
WriteTransport(jsonWriter, nameof(TransportType.ServerSentEvents), TransferFormat.Text);
}
if ((options.Transports & TransportType.LongPolling) != 0)
{
WriteTransport(jsonWriter, nameof(TransportType.LongPolling), TransferFormat.Text | TransferFormat.Binary);
}
jsonWriter.WriteEndArray();
jsonWriter.WriteEndObject();
}
@ -406,6 +413,11 @@ namespace Microsoft.AspNetCore.Sockets
return sb.ToString();
}
private static bool ServerHasWebSockets(IFeatureCollection features)
{
return features.Get<IHttpWebSocketFeature>() != null;
}
private static void WriteTransport(JsonWriter writer, string transportName, TransferFormat supportedTransferFormats)
{
writer.WriteStartObject();

View File

@ -370,6 +370,59 @@ namespace Microsoft.AspNetCore.SignalR.Client.Tests
});
}
[Fact]
public async Task SSEWontStartIfSuccessfulConnectionIsNotEstablished()
{
using (StartLog(out var loggerFactory))
{
var httpHandler = new TestHttpMessageHandler();
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", (_, __) =>
{
return Task.FromResult(ResponseUtils.CreateResponse(HttpStatusCode.InternalServerError));
});
var sse = new ServerSentEventsTransport(new HttpClient(httpHandler));
await WithConnectionAsync(
CreateConnection(httpHandler, loggerFactory: loggerFactory, url: null, transport: sse),
async (connection, closed) =>
{
await Assert.ThrowsAsync<InvalidOperationException>(
() => connection.StartAsync(TransferFormat.Text).OrTimeout());
});
}
}
[Fact]
public async Task SSEWaitsForResponseToStart()
{
using (StartLog(out var loggerFactory))
{
var httpHandler = new TestHttpMessageHandler();
var connectResponseTcs = new TaskCompletionSource<object>();
httpHandler.OnGet("/?id=00000000-0000-0000-0000-000000000000", async (_, __) =>
{
await connectResponseTcs.Task;
return ResponseUtils.CreateResponse(HttpStatusCode.Accepted);
});
var sse = new ServerSentEventsTransport(new HttpClient(httpHandler));
await WithConnectionAsync(
CreateConnection(httpHandler, loggerFactory: loggerFactory, url: null, transport: sse),
async (connection, closed) =>
{
var startTask = connection.StartAsync(TransferFormat.Text).OrTimeout();
Assert.False(connectResponseTcs.Task.IsCompleted);
Assert.False(startTask.IsCompleted);
connectResponseTcs.TrySetResult(null);
await startTask;
});
}
}
[Fact]
public async Task TransportIsStoppedWhenConnectionIsStopped()
{

View File

@ -157,6 +157,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests
var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory);
var context = new DefaultHttpContext();
context.Features.Set<IHttpResponseFeature>(new ResponseFeature());
context.Features.Set<IHttpWebSocketFeature>(new TestWebSocketConnectionFeature());
var services = new ServiceCollection();
services.AddSingleton<TestEndPoint>();
services.AddOptions();
@ -1225,6 +1226,31 @@ namespace Microsoft.AspNetCore.Sockets.Tests
}
}
[Fact]
public async Task NegotiateDoesNotReturnWebSocketsWhenNotAvailable()
{
using (StartLog(out var loggerFactory, LogLevel.Debug))
{
var manager = CreateConnectionManager(loggerFactory);
var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory);
var context = new DefaultHttpContext();
context.Features.Set<IHttpResponseFeature>(new ResponseFeature());
var services = new ServiceCollection();
services.AddSingleton<TestEndPoint>();
services.AddOptions();
var ms = new MemoryStream();
context.Request.Path = "/foo";
context.Request.Method = "POST";
context.Response.Body = ms;
await dispatcher.ExecuteNegotiateAsync(context, new HttpSocketOptions { Transports = TransportType.WebSockets });
var negotiateResponse = JsonConvert.DeserializeObject<JObject>(Encoding.UTF8.GetString(ms.ToArray()));
var availableTransports = (JArray)negotiateResponse["availableTransports"];
Assert.Empty(availableTransports);
}
}
private class RejectHandler : TestAuthenticationHandler
{
protected override bool ShouldAccept => false;