From 31edabdfcb1aba6eecbd677d2a9dbeaf32400972 Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 8 Jul 2014 09:13:45 -0700 Subject: [PATCH] #96 Enable Owin->AspNet WebSockets. --- .../Microsoft.AspNet.Owin.kproj | 3 + src/Microsoft.AspNet.Owin/OwinConstants.cs | 1 + .../OwinFeatureCollection.cs | 38 +++- .../WebSockets/OwinWebSocketAcceptAdapter.cs | 129 ++++++++++++ .../WebSockets/OwinWebSocketAcceptContext.cs | 39 ++++ .../WebSockets/OwinWebSocketAdapter.cs | 191 ++++++++++++++++++ 6 files changed, 400 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs create mode 100644 src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs create mode 100644 src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAdapter.cs diff --git a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj index ea1daa11aa..44a2b3584a 100644 --- a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj +++ b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj @@ -26,6 +26,9 @@ + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/OwinConstants.cs b/src/Microsoft.AspNet.Owin/OwinConstants.cs index 1e1296b67d..7589b57308 100644 --- a/src/Microsoft.AspNet.Owin/OwinConstants.cs +++ b/src/Microsoft.AspNet.Owin/OwinConstants.cs @@ -134,6 +134,7 @@ namespace Microsoft.AspNet.Owin // 3.2. Per Request public const string Accept = "websocket.Accept"; + public const string AcceptAlt = "websocket.AcceptAlt"; // Non-spec // 4. Accept diff --git a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs index f546eaa930..bec1cd1292 100644 --- a/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs +++ b/src/Microsoft.AspNet.Owin/OwinFeatureCollection.cs @@ -7,15 +7,16 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.WebSockets; using System.Reflection; using System.Security.Claims; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.FeatureModel; using Microsoft.AspNet.HttpFeature; using Microsoft.AspNet.HttpFeature.Security; -using Microsoft.AspNet.FeatureModel; namespace Microsoft.AspNet.Owin { @@ -30,6 +31,7 @@ namespace Microsoft.AspNet.Owin IHttpClientCertificateFeature, IHttpRequestLifetimeFeature, IHttpAuthenticationFeature, + IHttpWebSocketFeature, IOwinEnvironmentFeature { public IDictionary Environment { get; set; } @@ -236,6 +238,32 @@ namespace Microsoft.AspNet.Owin IAuthenticationHandler IHttpAuthenticationFeature.Handler { get; set; } + /// + /// Gets or sets if the underlying server supports WebSockets. This is disabled by default. + /// The value should be consistant across requests. + /// + public bool SupportsWebSockets { get; set; } + + bool IHttpWebSocketFeature.IsWebSocketRequest + { + get + { + object obj; + return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj); + } + } + + Task IHttpWebSocketFeature.AcceptAsync(IWebSocketAcceptContext context) + { + object obj; + if (!Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj)) + { + throw new NotSupportedException("WebSockets are not supported"); // TODO: LOC + } + var accept = (Func>)obj; + return accept(context); + } + public int Revision { get { return 0; } // Not modifiable @@ -260,6 +288,10 @@ namespace Microsoft.AspNet.Owin { return SupportsClientCerts; } + else if (key == typeof(IHttpWebSocketFeature)) + { + return SupportsWebSockets; + } // The rest of the features are always supported. return true; @@ -288,6 +320,10 @@ namespace Microsoft.AspNet.Owin { keys.Add(typeof(IHttpClientCertificateFeature)); } + if (SupportsWebSockets) + { + keys.Add(typeof(IHttpWebSocketFeature)); + } return keys; } } diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs new file mode 100644 index 0000000000..ce45e4fc4f --- /dev/null +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptAdapter.cs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Net.WebSockets; +using System.Threading.Tasks; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.Owin +{ + using AppFunc = Func, Task>; + using WebSocketAccept = + Action + < + IDictionary, // WebSocket Accept parameters + Func // WebSocketFunc callback + < + IDictionary, // WebSocket environment + Task // Complete + > + >; + using WebSocketAcceptAlt = + Func + < + IWebSocketAcceptContext, // WebSocket Accept parameters + Task + >; + + public class OwinWebSocketAcceptAdapter + { + private WebSocketAccept _owinWebSocketAccept; + private TaskCompletionSource _requestTcs = new TaskCompletionSource(); + private TaskCompletionSource _acceptTcs = new TaskCompletionSource(); + private TaskCompletionSource _upstreamWentAsync = new TaskCompletionSource(); + private string _subProtocol = null; + + private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept) + { + _owinWebSocketAccept = owinWebSocketAccept; + } + + private Task RequestTask { get { return _requestTcs.Task; } } + private Task UpstreamTask { get; set; } + private TaskCompletionSource UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } } + + private async Task AcceptWebSocketAsync(IWebSocketAcceptContext context) + { + IDictionary options = null; + if (context is OwinWebSocketAcceptContext) + { + var acceptContext = context as OwinWebSocketAcceptContext; + options = acceptContext.Options; + _subProtocol = acceptContext.SubProtocol; + } + else if (context != null && context.SubProtocol != null) + { + options = new Dictionary(1) + { + { OwinConstants.WebSocket.SubProtocol, context.SubProtocol } + }; + _subProtocol = context.SubProtocol; + } + + // Accept may have been called synchronously on the original request thread, we might not have a task yet. Go async. + await _upstreamWentAsync.Task; + + _owinWebSocketAccept(options, OwinAcceptCallback); + _requestTcs.TrySetResult(0); // Let the pipeline unwind. + + return await _acceptTcs.Task; + } + + private Task OwinAcceptCallback(IDictionary webSocketContext) + { + _acceptTcs.TrySetResult(new OwinWebSocketAdapter(webSocketContext, _subProtocol)); + return UpstreamTask; + } + + // Make sure declined websocket requests complete. This is a no-op for accepted websocket requests. + private void EnsureCompleted(Task task) + { + if (task.IsCanceled) + { + _requestTcs.TrySetCanceled(); + } + else if (task.IsFaulted) + { + _requestTcs.TrySetException(task.Exception); + } + else + { + _requestTcs.TrySetResult(0); + } + } + + public static AppFunc AdaptWebSockets(AppFunc next) + { + return environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.Accept, out accept) && accept is WebSocketAccept) + { + var adapter = new OwinWebSocketAcceptAdapter((WebSocketAccept)accept); + + environment[OwinConstants.WebSocket.AcceptAlt] = new WebSocketAcceptAlt(adapter.AcceptWebSocketAsync); + + try + { + adapter.UpstreamTask = next(environment); + adapter.UpstreamWentAsyncTcs.TrySetResult(0); + adapter.UpstreamTask.ContinueWith(adapter.EnsureCompleted, TaskContinuationOptions.ExecuteSynchronously); + } + catch (Exception ex) + { + adapter.UpstreamWentAsyncTcs.TrySetException(ex); + throw; + } + + return adapter.RequestTask; + } + else + { + return next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs new file mode 100644 index 0000000000..b23e2dbdcb --- /dev/null +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using Microsoft.AspNet.HttpFeature; + +namespace Microsoft.AspNet.Owin +{ + public class OwinWebSocketAcceptContext : IWebSocketAcceptContext + { + private IDictionary _options = new Dictionary(1); + + public OwinWebSocketAcceptContext() + { + } + + public string SubProtocol + { + get + { + object obj; + if (_options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) + { + return (string)obj; + } + return null; + } + set + { + _options[OwinConstants.WebSocket.SubProtocol] = value; + } + } + + public IDictionary Options + { + get { return _options; } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAdapter.cs new file mode 100644 index 0000000000..d77fd57295 --- /dev/null +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAdapter.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Open Technologies, Inc. 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.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using System.Net.WebSockets; + +namespace Microsoft.AspNet.Owin +{ + // http://owin.org/extensions/owin-WebSocket-Extension-v0.4.0.htm + using WebSocketCloseAsync = + Func; + using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task>>; + using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + using RawWebSocketReceiveResult = Tuple; // count + + public class OwinWebSocketAdapter : WebSocket + { + private IDictionary _websocketContext; + private WebSocketSendAsync _sendAsync; + private WebSocketReceiveAsync _receiveAsync; + private WebSocketCloseAsync _closeAsync; + private WebSocketState _state; + private string _subProtocol; + + public OwinWebSocketAdapter(IDictionary websocketContext, string subProtocol) + { + _websocketContext = websocketContext; + _sendAsync = (WebSocketSendAsync)websocketContext[OwinConstants.WebSocket.SendAsync]; + _receiveAsync = (WebSocketReceiveAsync)websocketContext[OwinConstants.WebSocket.ReceiveAsync]; + _closeAsync = (WebSocketCloseAsync)websocketContext[OwinConstants.WebSocket.CloseAsync]; + _state = WebSocketState.Open; + _subProtocol = subProtocol; + } + + public override WebSocketCloseStatus? CloseStatus + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseStatus, out obj)) + { + return (WebSocketCloseStatus)obj; + } + return null; + } + } + + public override string CloseStatusDescription + { + get + { + object obj; + if (_websocketContext.TryGetValue(OwinConstants.WebSocket.ClientCloseDescription, out obj)) + { + return (string)obj; + } + return null; + } + } + + public override string SubProtocol + { + get + { + return _subProtocol; + } + } + + public override WebSocketState State + { + get + { + return _state; + } + } + + public override async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + var rawResult = await _receiveAsync(buffer, cancellationToken); + var messageType = OpCodeToEnum(rawResult.Item1); + if (messageType == WebSocketMessageType.Close) + { + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseReceived; + } + else if (State == WebSocketState.CloseSent) + { + _state = WebSocketState.Closed; + } + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2, CloseStatus, CloseStatusDescription); + } + else + { + return new WebSocketReceiveResult(rawResult.Item3, messageType, rawResult.Item2); + } + } + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + return _sendAsync(buffer, EnumToOpCode(messageType), endOfMessage, cancellationToken); + } + + public override async Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) + { + await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); + } + + byte[] buffer = new byte[1024]; + while (State == WebSocketState.CloseSent) + { + // Drain until close received + await ReceiveAsync(new ArraySegment(buffer), cancellationToken); + } + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // TODO: Validate state + if (State == WebSocketState.Open) + { + _state = WebSocketState.CloseSent; + } + else if (State == WebSocketState.CloseReceived) + { + _state = WebSocketState.Closed; + } + return _closeAsync((int)closeStatus, statusDescription, cancellationToken); + } + + public override void Abort() + { + _state = WebSocketState.Aborted; + } + + public override void Dispose() + { + _state = WebSocketState.Closed; + } + + private static WebSocketMessageType OpCodeToEnum(int messageType) + { + switch (messageType) + { + case 0x1: + return WebSocketMessageType.Text; + case 0x2: + return WebSocketMessageType.Binary; + case 0x8: + return WebSocketMessageType.Close; + default: + throw new ArgumentOutOfRangeException("messageType", messageType, string.Empty); + } + } + + private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) + { + switch (webSocketMessageType) + { + case WebSocketMessageType.Text: + return 0x1; + case WebSocketMessageType.Binary: + return 0x2; + case WebSocketMessageType.Close: + return 0x8; + default: + throw new ArgumentOutOfRangeException("webSocketMessageType", webSocketMessageType, string.Empty); + } + } + } +} \ No newline at end of file