Detect availability of web sockets on client and server (#1682)

This commit is contained in:
James Newton-King 2018-03-22 12:35:31 +13:00 committed by GitHub
parent 71c2ddd155
commit 3f84eee116
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 132 additions and 6 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

@ -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

@ -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

@ -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;