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