diff --git a/client-ts/signalr/spec/HttpConnection.spec.ts b/client-ts/signalr/spec/HttpConnection.spec.ts index 5006bce951..1cd521798f 100644 --- a/client-ts/signalr/spec/HttpConnection.spec.ts +++ b/client-ts/signalr/spec/HttpConnection.spec.ts @@ -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 :) diff --git a/client-ts/signalr/src/HttpConnection.ts b/client-ts/signalr/src/HttpConnection.ts index 974a4bdb08..614e51b416 100644 --- a/client-ts/signalr/src/HttpConnection.ts +++ b/client-ts/signalr/src/HttpConnection.ts @@ -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]}'.`); } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs index 3d02162943..00dc83ffdb 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.Log.cs @@ -98,6 +98,9 @@ namespace Microsoft.AspNetCore.Sockets.Client private static readonly Action _transportFailed = LoggerMessage.Define(LogLevel.Debug, new EventId(29, "TransportFailed"), "Skipping transport {TransportName} because it failed to initialize."); + private static readonly Action _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); + } } } } diff --git a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs index 227c0e619f..3adf223633 100644 --- a/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs +++ b/src/Microsoft.AspNetCore.Sockets.Client.Http/HttpConnection.cs @@ -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 { diff --git a/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs b/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs index 25f239877a..f09cad93a2 100644 --- a/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs +++ b/src/Microsoft.AspNetCore.Sockets.Http/HttpConnectionDispatcher.cs @@ -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() != null; + } + private static void WriteTransport(JsonWriter writer, string transportName, TransferFormat supportedTransferFormats) { writer.WriteStartObject(); diff --git a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs index b129825ba4..c6cb016cb8 100644 --- a/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs +++ b/test/Microsoft.AspNetCore.Sockets.Tests/HttpConnectionDispatcherTests.cs @@ -157,6 +157,7 @@ namespace Microsoft.AspNetCore.Sockets.Tests var dispatcher = new HttpConnectionDispatcher(manager, loggerFactory); var context = new DefaultHttpContext(); context.Features.Set(new ResponseFeature()); + context.Features.Set(new TestWebSocketConnectionFeature()); var services = new ServiceCollection(); services.AddSingleton(); 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(new ResponseFeature()); + var services = new ServiceCollection(); + services.AddSingleton(); + 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(Encoding.UTF8.GetString(ms.ToArray())); + var availableTransports = (JArray)negotiateResponse["availableTransports"]; + + Assert.Empty(availableTransports); + } + } + private class RejectHandler : TestAuthenticationHandler { protected override bool ShouldAccept => false;