From b1c82c00667af055b7ae6363a14dd3ac6bc31a8f Mon Sep 17 00:00:00 2001 From: Chris Ross Date: Tue, 8 Jul 2014 13:55:03 -0700 Subject: [PATCH] #96 Enable AspNet->Owin WebSockets. --- .../Microsoft.AspNet.Owin.kproj | 2 + src/Microsoft.AspNet.Owin/OwinConstants.cs | 1 + src/Microsoft.AspNet.Owin/OwinEnvironment.cs | 12 ++ .../WebSockets/OwinWebSocketAcceptContext.cs | 15 +- .../WebSockets/WebSocketAcceptAdapter.cs | 88 +++++++++ .../WebSockets/WebSocketAdapter.cs | 172 ++++++++++++++++++ 6 files changed, 287 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs create mode 100644 src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs diff --git a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj index 44a2b3584a..931181cfd8 100644 --- a/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj +++ b/src/Microsoft.AspNet.Owin/Microsoft.AspNet.Owin.kproj @@ -29,6 +29,8 @@ + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/OwinConstants.cs b/src/Microsoft.AspNet.Owin/OwinConstants.cs index 7589b57308..b86dfc1a75 100644 --- a/src/Microsoft.AspNet.Owin/OwinConstants.cs +++ b/src/Microsoft.AspNet.Owin/OwinConstants.cs @@ -130,6 +130,7 @@ namespace Microsoft.AspNet.Owin // 3.1. Startup public const string Version = "websocket.Version"; + public const string VersionValue = "1.0"; // 3.2. Per Request diff --git a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs index 914837c083..781ba621e2 100644 --- a/src/Microsoft.AspNet.Owin/OwinEnvironment.cs +++ b/src/Microsoft.AspNet.Owin/OwinEnvironment.cs @@ -8,6 +8,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Net; +using System.Net.WebSockets; using System.Security.Cryptography.X509Certificates; using System.Security.Principal; using System.Threading; @@ -20,6 +21,12 @@ using Microsoft.AspNet.PipelineCore.Security; namespace Microsoft.AspNet.Owin { using SendFileFunc = Func; + using WebSocketAcceptAlt = + Func + < + IWebSocketAcceptContext, // WebSocket Accept parameters + Task + >; public class OwinEnvironment : IDictionary { @@ -76,6 +83,11 @@ namespace Microsoft.AspNet.Owin feature => new Func(() => feature.GetClientCertificateAsync(CancellationToken.None)))); } + if (context.IsWebSocketRequest) + { + _entries.Add(OwinConstants.WebSocket.AcceptAlt, new FeatureMap(feature => new WebSocketAcceptAlt(feature.AcceptAsync))); + } + _context.Items[typeof(HttpContext).FullName] = _context; // Store for lookup when we transition back out of OWIN } diff --git a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs index b23e2dbdcb..d67be74199 100644 --- a/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs +++ b/src/Microsoft.AspNet.Owin/WebSockets/OwinWebSocketAcceptContext.cs @@ -8,18 +8,23 @@ namespace Microsoft.AspNet.Owin { public class OwinWebSocketAcceptContext : IWebSocketAcceptContext { - private IDictionary _options = new Dictionary(1); + private IDictionary _options; - public OwinWebSocketAcceptContext() + public OwinWebSocketAcceptContext() : this(new Dictionary(1)) { } + public OwinWebSocketAcceptContext(IDictionary options) + { + _options = options; + } + public string SubProtocol { get { object obj; - if (_options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) + if (_options != null && _options.TryGetValue(OwinConstants.WebSocket.SubProtocol, out obj)) { return (string)obj; } @@ -27,6 +32,10 @@ namespace Microsoft.AspNet.Owin } set { + if (_options == null) + { + _options = new Dictionary(1); + } _options[OwinConstants.WebSocket.SubProtocol] = value; } } diff --git a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs new file mode 100644 index 0000000000..5d5b4a5f3b --- /dev/null +++ b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAcceptAdapter.cs @@ -0,0 +1,88 @@ +// 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; +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 WebSocketAcceptAdapter + { + private IDictionary _env; + private WebSocketAcceptAlt _accept; + private AppFunc _callback; + private IDictionary _options; + + public WebSocketAcceptAdapter(IDictionary env, WebSocketAcceptAlt accept) + { + _env = env; + _accept = accept; + } + + private void AcceptWebSocket(IDictionary options, AppFunc callback) + { + _options = options; + _callback = callback; + _env[OwinConstants.ResponseStatusCode] = 101; + } + + public static AppFunc AdaptWebSockets(AppFunc next) + { + return async environment => + { + object accept; + if (environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out accept) && accept is WebSocketAcceptAlt) + { + var adapter = new WebSocketAcceptAdapter(environment, (WebSocketAcceptAlt)accept); + + environment[OwinConstants.WebSocket.Accept] = new WebSocketAccept(adapter.AcceptWebSocket); + await next(environment); + if ((int)environment[OwinConstants.ResponseStatusCode] == 101 && adapter._callback != null) + { + IWebSocketAcceptContext acceptContext = null; + object obj; + if (adapter._options.TryGetValue(typeof(IWebSocketAcceptContext).FullName, out obj)) + { + acceptContext = obj as IWebSocketAcceptContext; + } + else if (adapter._options != null) + { + acceptContext = new OwinWebSocketAcceptContext(adapter._options); + } + + var webSocket = await adapter._accept(acceptContext); + var webSocketAdapter = new WebSocketAdapter(webSocket, (CancellationToken)environment[OwinConstants.CallCancelled]); + await adapter._callback(webSocketAdapter.Environment); + await webSocketAdapter.CleanupAsync(); + } + } + else + { + await next(environment); + } + }; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs new file mode 100644 index 0000000000..245157b6c1 --- /dev/null +++ b/src/Microsoft.AspNet.Owin/WebSockets/WebSocketAdapter.cs @@ -0,0 +1,172 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNet.Owin +{ + using WebSocketCloseAsync = + Func; + using WebSocketReceiveAsync = + Func /* data */, + CancellationToken /* cancel */, + Task>>; + using WebSocketReceiveTuple = + Tuple; + using WebSocketSendAsync = + Func /* data */, + int /* messageType */, + bool /* endOfMessage */, + CancellationToken /* cancel */, + Task>; + + public class WebSocketAdapter + { + private readonly WebSocket _webSocket; + private readonly IDictionary _environment; + private readonly CancellationToken _cancellationToken; + private readonly WebSocket _context; + + internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) + { + _webSocket = webSocket; + _cancellationToken = ct; + + _environment = new Dictionary(); + _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); + _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); + _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); + _environment[OwinConstants.WebSocket.CallCancelled] = ct; + _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; + + _environment[typeof(WebSocket).FullName] = webSocket; + } + + internal IDictionary Environment + { + get { return _environment; } + } + + internal Task SendAsync(ArraySegment buffer, int messageType, bool endOfMessage, CancellationToken cancel) + { + // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. + if (messageType == 0x8) + { + return RedirectSendToCloseAsync(buffer, cancel); + } + else if (messageType == 0x9 || messageType == 0xA) + { + // Ping & Pong, not allowed by the underlying APIs, silently discard. + return Task.FromResult(0); + } + + return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); + } + + internal async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancel) + { + WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); + + if (nativeResult.MessageType == WebSocketMessageType.Close) + { + _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); + _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; + } + + return new WebSocketReceiveTuple( + EnumToOpCode(nativeResult.MessageType), + nativeResult.EndOfMessage, + nativeResult.Count); + } + + internal Task CloseAsync(int status, string description, CancellationToken cancel) + { + return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); + } + + private Task RedirectSendToCloseAsync(ArraySegment buffer, CancellationToken cancel) + { + if (buffer.Array == null || buffer.Count == 0) + { + return CloseAsync(1000, string.Empty, cancel); + } + else if (buffer.Count >= 2) + { + // Unpack the close message. + int statusCode = + (buffer.Array[buffer.Offset] << 8) + | buffer.Array[buffer.Offset + 1]; + string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); + + return CloseAsync(statusCode, description, cancel); + } + else + { + throw new ArgumentOutOfRangeException("buffer"); + } + } + + internal async Task CleanupAsync() + { + switch (_webSocket.State) + { + case WebSocketState.Closed: // Closed gracefully, no action needed. + case WebSocketState.Aborted: // Closed abortively, no action needed. + break; + case WebSocketState.CloseReceived: + // Echo what the client said, if anything. + await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, + _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); + break; + case WebSocketState.Open: + case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. + _webSocket.Abort(); + break; + default: + throw new ArgumentOutOfRangeException("state", _webSocket.State, string.Empty); + } + } + + 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); + } + } + } +}