#96 Enable Owin->AspNet WebSockets.

This commit is contained in:
Chris Ross 2014-07-08 09:13:45 -07:00
parent 1ece87ef68
commit 31edabdfcb
6 changed files with 400 additions and 1 deletions

View File

@ -26,6 +26,9 @@
<Compile Include="OwinExtensions.cs" />
<Compile Include="OwinFeatureCollection.cs" />
<Compile Include="Utilities.cs" />
<Compile Include="WebSockets\OwinWebSocketAcceptContext.cs" />
<Compile Include="WebSockets\OwinWebSocketAdapter.cs" />
<Compile Include="WebSockets\OwinWebSocketAcceptAdapter.cs" />
</ItemGroup>
<Import Project="$(VSToolsPath)\AspNet\Microsoft.Web.AspNet.targets" Condition="'$(VSToolsPath)' != ''" />
</Project>

View File

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

View File

@ -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<string, object> Environment { get; set; }
@ -236,6 +238,32 @@ namespace Microsoft.AspNet.Owin
IAuthenticationHandler IHttpAuthenticationFeature.Handler { get; set; }
/// <summary>
/// Gets or sets if the underlying server supports WebSockets. This is disabled by default.
/// The value should be consistant across requests.
/// </summary>
public bool SupportsWebSockets { get; set; }
bool IHttpWebSocketFeature.IsWebSocketRequest
{
get
{
object obj;
return Environment.TryGetValue(OwinConstants.WebSocket.AcceptAlt, out obj);
}
}
Task<WebSocket> 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<IWebSocketAcceptContext, Task<WebSocket>>)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;
}
}

View File

@ -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<IDictionary<string, object>, Task>;
using WebSocketAccept =
Action
<
IDictionary<string, object>, // WebSocket Accept parameters
Func // WebSocketFunc callback
<
IDictionary<string, object>, // WebSocket environment
Task // Complete
>
>;
using WebSocketAcceptAlt =
Func
<
IWebSocketAcceptContext, // WebSocket Accept parameters
Task<WebSocket>
>;
public class OwinWebSocketAcceptAdapter
{
private WebSocketAccept _owinWebSocketAccept;
private TaskCompletionSource<int> _requestTcs = new TaskCompletionSource<int>();
private TaskCompletionSource<WebSocket> _acceptTcs = new TaskCompletionSource<WebSocket>();
private TaskCompletionSource<int> _upstreamWentAsync = new TaskCompletionSource<int>();
private string _subProtocol = null;
private OwinWebSocketAcceptAdapter(WebSocketAccept owinWebSocketAccept)
{
_owinWebSocketAccept = owinWebSocketAccept;
}
private Task RequestTask { get { return _requestTcs.Task; } }
private Task UpstreamTask { get; set; }
private TaskCompletionSource<int> UpstreamWentAsyncTcs { get { return _upstreamWentAsync; } }
private async Task<WebSocket> AcceptWebSocketAsync(IWebSocketAcceptContext context)
{
IDictionary<string, object> 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<string, object>(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<string, object> 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);
}
};
}
}
}

View File

@ -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<string, object> _options = new Dictionary<string, object>(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<string, object> Options
{
get { return _options; }
}
}
}

View File

@ -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<int /* closeStatus */,
string /* closeDescription */,
CancellationToken /* cancel */,
Task>;
using WebSocketReceiveAsync =
Func<ArraySegment<byte> /* data */,
CancellationToken /* cancel */,
Task<Tuple<int /* messageType */,
bool /* endOfMessage */,
int /* count */>>>;
using WebSocketSendAsync =
Func<ArraySegment<byte> /* data */,
int /* messageType */,
bool /* endOfMessage */,
CancellationToken /* cancel */,
Task>;
using RawWebSocketReceiveResult = Tuple<int, // type
bool, // end of message?
int>; // count
public class OwinWebSocketAdapter : WebSocket
{
private IDictionary<string, object> _websocketContext;
private WebSocketSendAsync _sendAsync;
private WebSocketReceiveAsync _receiveAsync;
private WebSocketCloseAsync _closeAsync;
private WebSocketState _state;
private string _subProtocol;
public OwinWebSocketAdapter(IDictionary<string, object> 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<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> 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<byte> 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<byte>(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);
}
}
}
}