diff --git a/.gitignore b/.gitignore index 0479af7..caf28df 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,13 @@ src/Owin.WebSocket/bin src/Owin.WebSocket/obj -src/Owin.WebSocket/packages +src/Owin.WebSocket.Fleck/bin +src/Owin.WebSocket.Fleck/obj src/packages src/UnitTests/bin src/UnitTests/obj -src/build +src/UnitTests.Fleck/bin +src/UnitTests.Fleck/obj +*.DotSettings *.dll *.pdb *.suo diff --git a/src/Owin.WebSocket.Fleck/Extensions/AppBuilderExtensions.cs b/src/Owin.WebSocket.Fleck/Extensions/AppBuilderExtensions.cs new file mode 100644 index 0000000..2947114 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Extensions/AppBuilderExtensions.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.RegularExpressions; +using Fleck; +using Microsoft.Practices.ServiceLocation; + +namespace Owin.WebSocket.Extensions +{ + public static class AppBuilderExtensions + { + public static IAppBuilder MapFleckRoute(this IAppBuilder app, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + { + return app.MapFleckRoute(string.Empty, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapFleckRoute(this IAppBuilder app, string route, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + { + return app.MapFleckRoute(route, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapFleckRoute(this IAppBuilder app, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + where T : FleckWebSocketConnection, new() + { + return app.MapFleckRoute(string.Empty, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapFleckRoute(this IAppBuilder app, string route, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + where T : FleckWebSocketConnection, new() + { + var regex = string.IsNullOrWhiteSpace(regexPatternMatch) ? null : new Regex(regexPatternMatch, RegexOptions.Compiled | RegexOptions.IgnoreCase); + return app.Map(route, a => a.Use>(config, locator, regex)); + } + + public static IAppBuilder MapOwinFleckRoute(this IAppBuilder app, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + { + return app.MapOwinFleckRoute(string.Empty, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapOwinFleckRoute(this IAppBuilder app, string route, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + { + return app.MapOwinFleckRoute(route, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapOwinFleckRoute(this IAppBuilder app, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + where T : FleckWebSocketConnection, new() + { + return app.MapOwinFleckRoute(string.Empty, config, locator, regexPatternMatch); + } + + public static IAppBuilder MapOwinFleckRoute(this IAppBuilder app, string route, Action config, IServiceLocator locator = null, string regexPatternMatch = null) + where T : FleckWebSocketConnection, new() + { + var regex = string.IsNullOrWhiteSpace(regexPatternMatch) ? null : new Regex(regexPatternMatch, RegexOptions.Compiled | RegexOptions.IgnoreCase); + return app.Map(route, a => a.Use>(config, locator, regex)); + } + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/FleckWebSocketConnection.cs b/src/Owin.WebSocket.Fleck/FleckWebSocketConnection.cs new file mode 100644 index 0000000..c711898 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/FleckWebSocketConnection.cs @@ -0,0 +1,160 @@ +using System; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; +using Fleck; +using Microsoft.Owin; +using Owin.WebSocket.Models; + +namespace Owin.WebSocket +{ + public class FleckWebSocketConnection : WebSocketConnection, IOwinWebSocketConnection + { + private const string PingPongError = "Owin handles ping pong messages internally"; + + private readonly OwinWebSocketContext _context; + private readonly IOwinWebSocketConnection _connection; + private readonly IWebSocketConnectionInfo _connectionInfo; + + private bool _isAvailable; + + public FleckWebSocketConnection() + : this(1024*64) + { + } + + public FleckWebSocketConnection(int maxMessageSize) + : base(maxMessageSize) + { + _connection = this; + _context = new OwinWebSocketContext(_connection.Context, MaxMessageSize); + _connectionInfo = new FleckWebSocketConnectionInfo(_connection.Context); + } + + #region WebSocketConnection + + public override bool AuthenticateRequest(IOwinRequest request) + { + return _connection.OnAuthenticateRequest?.Invoke() ?? true; + } + + public override Task AuthenticateRequestAsync(IOwinRequest request) + { + return _connection.OnAuthenticateRequestAsync?.Invoke() ?? Task.FromResult(true); + } + + public override void OnOpen() + { + _isAvailable = true; + _connection.OnOpen?.Invoke(); + } + + public override Task OnOpenAsync() + { + return _connection.OnOpenAsync?.Invoke() ?? Task.FromResult(true); + } + + public override void OnClose(WebSocketCloseStatus? closeStatus, string closeStatusDescription) + { + _isAvailable = false; + _context.CloseStatus = closeStatus; + _context.CloseStatusDescription = closeStatusDescription; + _connection.OnClose?.Invoke(); + } + + public override Task OnCloseAsync(WebSocketCloseStatus? closeStatus, string closeStatusDescription) + { + return _connection.OnCloseAsync?.Invoke() ?? Task.FromResult(true); + } + + public override Task OnMessageReceived(ArraySegment message, WebSocketMessageType type) + { + if (type == WebSocketMessageType.Binary && (_connection.OnBinary != null || _connection.OnBinaryAsync != null)) + { + var array = message.ToArray(); + _connection.OnBinary?.Invoke(array); + return _connection.OnBinaryAsync?.Invoke(array) ?? Task.FromResult(true); + } + + if (type == WebSocketMessageType.Text && (_connection.OnMessage != null || _connection.OnMessageAsync!= null)) + { + var @string = Encoding.UTF8.GetString(message.Array, 0, message.Count); + _connection.OnMessage?.Invoke(@string); + return _connection.OnMessageAsync?.Invoke(@string) ?? Task.FromResult(true); + } + + return Task.FromResult(true); + } + + public override void OnReceiveError(Exception error) + { + _connection.OnError?.Invoke(error); + } + + #endregion + + #region IOwinWebSocketConnection + + IOwinWebSocketContext IOwinWebSocketConnection.Context => _context; + Func IOwinWebSocketConnection.OnOpenAsync { get; set; } + Func IOwinWebSocketConnection.OnCloseAsync { get; set; } + Func IOwinWebSocketConnection.OnMessageAsync { get; set; } + Func IOwinWebSocketConnection.OnBinaryAsync { get; set; } + Func IOwinWebSocketConnection.OnAuthenticateRequest { get; set; } + Func> IOwinWebSocketConnection.OnAuthenticateRequestAsync { get; set; } + + #endregion + + #region IWebSocketConnection + + Action IWebSocketConnection.OnOpen { get; set; } + Action IWebSocketConnection.OnClose { get; set; } + Action IWebSocketConnection.OnMessage { get; set; } + Action IWebSocketConnection.OnBinary { get; set; } + Action IWebSocketConnection.OnError { get; set; } + + Action IWebSocketConnection.OnPing + { + get { return null; } + set { throw new NotSupportedException(PingPongError); } + } + + Action IWebSocketConnection.OnPong + { + get { return null; } + set { throw new NotSupportedException(PingPongError); } + } + + IWebSocketConnectionInfo IWebSocketConnection.ConnectionInfo => _connectionInfo; + bool IWebSocketConnection.IsAvailable => _isAvailable; + + Task IWebSocketConnection.Send(string message) + { + var bytes = Encoding.UTF8.GetBytes(message); + return SendText(bytes, true); + } + + Task IWebSocketConnection.Send(byte[] message) + { + return SendBinary(message, true); + } + + Task IWebSocketConnection.SendPing(byte[] message) + { + throw new NotSupportedException(PingPongError); + } + + Task IWebSocketConnection.SendPong(byte[] message) + { + throw new NotSupportedException(PingPongError); + } + + void IWebSocketConnection.Close() + { + Close(WebSocketCloseStatus.NormalClosure, string.Empty).Wait(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/FleckWebSocketConnectionMiddleware.cs b/src/Owin.WebSocket.Fleck/FleckWebSocketConnectionMiddleware.cs new file mode 100644 index 0000000..6c0d746 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/FleckWebSocketConnectionMiddleware.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Text.RegularExpressions; +using System.Threading.Tasks; +using Fleck; +using Microsoft.Owin; +using Microsoft.Practices.ServiceLocation; + +namespace Owin.WebSocket +{ + public class FleckWebSocketConnectionMiddleware : OwinMiddleware + where T : FleckWebSocketConnection, new() + { + private readonly Action _config; + private readonly Action _owinConfig; + private readonly IServiceLocator _locator; + private readonly Regex _matchPattern; + + public FleckWebSocketConnectionMiddleware(OwinMiddleware next, Action config, IServiceLocator locator, Regex matchPattern) + : this(next, locator, matchPattern) + { + _config = config; + } + + public FleckWebSocketConnectionMiddleware(OwinMiddleware next, Action config, IServiceLocator locator, Regex matchPattern) + : this(next, locator, matchPattern) + { + _owinConfig = config; + } + + private FleckWebSocketConnectionMiddleware(OwinMiddleware next, IServiceLocator locator, Regex matchPattern) + : base(next) + { + _locator = locator; + _matchPattern = matchPattern; + } + + public override Task Invoke(IOwinContext context) + { + var matches = new Dictionary(); + + if (_matchPattern != null) + { + var match = _matchPattern.Match(context.Request.Path.Value); + if (!match.Success) + return Next.Invoke(context); + + for (var i = 1; i <= match.Groups.Count; i++) + { + var name = _matchPattern.GroupNameFromNumber(i); + var value = match.Groups[i]; + matches.Add(name, value.Value); + } + } + + var connection = _locator?.GetInstance() ?? new T(); + + _config?.Invoke(connection); + _owinConfig?.Invoke(connection); + + return connection.AcceptSocketAsync(context, matches); + } + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/IOwinWebSocketConnection.cs b/src/Owin.WebSocket.Fleck/IOwinWebSocketConnection.cs new file mode 100644 index 0000000..3c42355 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/IOwinWebSocketConnection.cs @@ -0,0 +1,18 @@ +using System; +using System.Threading.Tasks; +using Fleck; +using Owin.WebSocket.Models; + +namespace Owin.WebSocket +{ + public interface IOwinWebSocketConnection : IWebSocketConnection + { + IOwinWebSocketContext Context { get; } + Func OnOpenAsync { get; set; } + Func OnCloseAsync { get; set; } + Func OnMessageAsync { get; set; } + Func OnBinaryAsync { get; set; } + Func OnAuthenticateRequest { get; set; } + Func> OnAuthenticateRequestAsync { get; set; } + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/Models/FleckWebSocketConnectionInfo.cs b/src/Owin.WebSocket.Fleck/Models/FleckWebSocketConnectionInfo.cs new file mode 100644 index 0000000..f944311 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Models/FleckWebSocketConnectionInfo.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Fleck; +using Microsoft.Owin; + +namespace Owin.WebSocket.Models +{ + public class FleckWebSocketConnectionInfo : IWebSocketConnectionInfo + { + private readonly IOwinContext _context; + + public FleckWebSocketConnectionInfo(IOwinContext context) + { + _context = context; + } + + string IWebSocketConnectionInfo.SubProtocol + { + get + { + const string protocol = "Sec-WebSocket-Protocol"; + return _context.Request.Headers.ContainsKey(protocol) + ? _context.Request.Headers[protocol] + : string.Empty; + } + } + + string IWebSocketConnectionInfo.Origin + { + get + { + const string origin1 = "Origin"; + if (_context.Request.Headers.ContainsKey(origin1)) + return _context.Request.Headers[origin1]; + + const string origin2 = "Sec-WebSocket-Origin"; + return _context.Request.Headers.ContainsKey(origin2) + ? _context.Request.Headers[origin2] + : string.Empty; + } + } + + string IWebSocketConnectionInfo.Host => _context.Request.Host.Value; + string IWebSocketConnectionInfo.Path => _context.Request.Path.Value; + string IWebSocketConnectionInfo.ClientIpAddress => _context.Request.RemoteIpAddress; + int IWebSocketConnectionInfo.ClientPort => _context.Request.RemotePort ?? 0; + IDictionary IWebSocketConnectionInfo.Cookies => _context.Request.Cookies.ToDictionary(k => k.Key, v => v.Value); + IDictionary IWebSocketConnectionInfo.Headers => _context.Request.Headers.SelectMany(p => p.Value.Select(v => new { p.Key, Value = v })).ToDictionary(k => k.Key, v => v.Value); + + Guid IWebSocketConnectionInfo.Id { get; } = Guid.NewGuid(); + string IWebSocketConnectionInfo.NegotiatedSubProtocol { get; } = string.Empty; + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/Models/IOwinWebSocketContext.cs b/src/Owin.WebSocket.Fleck/Models/IOwinWebSocketContext.cs new file mode 100644 index 0000000..7bd0c36 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Models/IOwinWebSocketContext.cs @@ -0,0 +1,12 @@ +using System.Net.WebSockets; +using Microsoft.Owin; + +namespace Owin.WebSocket.Models +{ + public interface IOwinWebSocketContext : IOwinContext + { + int MaxMessageSize { get; } + WebSocketCloseStatus? CloseStatus { get; } + string CloseStatusDescription { get; } + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/Models/OwinWebSocketContext.cs b/src/Owin.WebSocket.Fleck/Models/OwinWebSocketContext.cs new file mode 100644 index 0000000..b05d4e0 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Models/OwinWebSocketContext.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using System.IO; +using System.Net.WebSockets; +using Microsoft.Owin; +using Microsoft.Owin.Security; + +namespace Owin.WebSocket.Models +{ + public class OwinWebSocketContext : IOwinWebSocketContext + { + private readonly IOwinContext _context; + + public OwinWebSocketContext(IOwinContext context, int maxMessageSize) + { + _context = context; + MaxMessageSize = maxMessageSize; + } + + public T Get(string key) + { + return _context.Get(key); + } + + public IOwinContext Set(string key, T value) + { + return _context.Set(key, value); + } + + public IOwinRequest Request => _context.Request; + public IOwinResponse Response => _context.Response; + public IAuthenticationManager Authentication => _context.Authentication; + public IDictionary Environment => _context.Environment; + + public TextWriter TraceOutput + { + get { return _context.TraceOutput; } + set { _context.TraceOutput = value; } + } + + public int MaxMessageSize { get; } + public WebSocketCloseStatus? CloseStatus { get; set; } + public string CloseStatusDescription { get; set; } + } +} \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/Owin.WebSocket.Fleck.csproj b/src/Owin.WebSocket.Fleck/Owin.WebSocket.Fleck.csproj new file mode 100644 index 0000000..82094b6 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Owin.WebSocket.Fleck.csproj @@ -0,0 +1,85 @@ + + + + + Debug + AnyCPU + {32050A9E-3FC9-4A71-85BF-6645AE68B82B} + Library + Properties + Owin.WebSocket + Owin.WebSocket.Fleck + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Fleck.0.13.0.57\lib\net40\Fleck.dll + True + + + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True + + + ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll + True + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + + + + + + + + + + + + + + + + + + + + + + + + {b1d742f7-39fa-498c-b22e-1a7a61c301e7} + Owin.WebSocket + + + + + \ No newline at end of file diff --git a/src/Owin.WebSocket.Fleck/Properties/AssemblyInfo.cs b/src/Owin.WebSocket.Fleck/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..86a9fb6 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Owin.WebSocket.Fleck")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Owin.WebSocket.Fleck")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("32050a9e-3fc9-4a71-85bf-6645ae68b82b")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/Owin.WebSocket.Fleck/packages.config b/src/Owin.WebSocket.Fleck/packages.config new file mode 100644 index 0000000..58dd4e4 --- /dev/null +++ b/src/Owin.WebSocket.Fleck/packages.config @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/src/Owin.WebSocket.sln b/src/Owin.WebSocket.sln index 76019f0..0c90524 100644 --- a/src/Owin.WebSocket.sln +++ b/src/Owin.WebSocket.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 2013 -VisualStudioVersion = 12.0.30501.0 +# Visual Studio 14 +VisualStudioVersion = 14.0.23107.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.WebSocket", "Owin.WebSocket\Owin.WebSocket.csproj", "{B1D742F7-39FA-498C-B22E-1A7A61C301E7}" EndProject @@ -14,6 +14,10 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".nuget", ".nuget", "{8DCED5 .nuget\NuGet.targets = .nuget\NuGet.targets EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Owin.WebSocket.Fleck", "Owin.WebSocket.Fleck\Owin.WebSocket.Fleck.csproj", "{32050A9E-3FC9-4A71-85BF-6645AE68B82B}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTests.Fleck", "UnitTests.Fleck\UnitTests.Fleck.csproj", "{7D1CEED6-9BAE-4A54-8902-71DE01112070}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -28,6 +32,14 @@ Global {E8AAA2FB-EA74-4120-B0A0-838926422AF1}.Debug|Any CPU.Build.0 = Debug|Any CPU {E8AAA2FB-EA74-4120-B0A0-838926422AF1}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8AAA2FB-EA74-4120-B0A0-838926422AF1}.Release|Any CPU.Build.0 = Release|Any CPU + {32050A9E-3FC9-4A71-85BF-6645AE68B82B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {32050A9E-3FC9-4A71-85BF-6645AE68B82B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {32050A9E-3FC9-4A71-85BF-6645AE68B82B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {32050A9E-3FC9-4A71-85BF-6645AE68B82B}.Release|Any CPU.Build.0 = Release|Any CPU + {7D1CEED6-9BAE-4A54-8902-71DE01112070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D1CEED6-9BAE-4A54-8902-71DE01112070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D1CEED6-9BAE-4A54-8902-71DE01112070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D1CEED6-9BAE-4A54-8902-71DE01112070}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/src/Owin.WebSocket/WebSocketConnection.cs b/src/Owin.WebSocket/WebSocketConnection.cs index a8de6b8..f369f43 100644 --- a/src/Owin.WebSocket/WebSocketConnection.cs +++ b/src/Owin.WebSocket/WebSocketConnection.cs @@ -34,7 +34,7 @@ public abstract class WebSocketConnection /// Queue of send operations to the client /// public TaskQueue QueueSend { get { return mWebSocket.SendQueue;} } - + protected WebSocketConnection(int maxMessageSize = 1024*64) { mCancellToken = new CancellationTokenSource(); @@ -180,13 +180,11 @@ public virtual Task OnCloseAsync(WebSocketCloseStatus? closeStatus, string close public virtual void OnReceiveError(Exception error) { } - + /// /// Receive one entire message from the web socket /// - - - internal async Task AcceptSocketAsync(IOwinContext context, IDictionary argumentMatches) + public async Task AcceptSocketAsync(IOwinContext context, IDictionary argumentMatches) { var accept = context.Get, Func, Task>>>("websocket.Accept"); if (accept == null) @@ -219,7 +217,6 @@ internal async Task AcceptSocketAsync(IOwinContext context, IDictionary _connections = new ConcurrentDictionary(); + private readonly ConcurrentQueue _messages = new ConcurrentQueue(); + + private int _idSeed; + + protected void AddConnection(int id, IWebSocketConnection connection) + { + _connections.TryAdd(id, connection); + } + + protected void RemoveConnection(int id) + { + IWebSocketConnection connection; + _connections.TryRemove(id, out connection); + } + + protected void Send(int id, string message) + { + _messages.Enqueue(message); + + if (id == -1) + return; + + foreach (var connectionPair in _connections) + { + if (!connectionPair.Value.IsAvailable) + continue; + + if (connectionPair.Key == id) + continue; + + connectionPair.Value.Send(message).Wait(); + } + } + + protected IList DequeueMessages() + { + var results = new List(_messages.Count); + string message; + while (_messages.TryDequeue(out message)) + { + results.Add(message); + } + + return results; + } + + protected void ConfigureIntegrationTestConnection(IWebSocketConnection connection) + { + ConfigureIntegrationTestConnectionAndGetId(connection); + } + + protected int ConfigureIntegrationTestConnectionAndGetId(IWebSocketConnection connection) + { + var id = Interlocked.Increment(ref _idSeed); + + connection.OnOpen = () => + { + AddConnection(id, connection); + Send(id, $"Open: {id}"); + }; + connection.OnClose = () => + { + Send(id, $"Close: {id}"); + RemoveConnection(id); + }; + connection.OnError = ex => + { + Send(-1, $"Error: {id} - {ex.ToString()}"); + }; + connection.OnMessage = m => + { + Send(id, $"User {id}: {m}"); + }; + + return id; + } + + protected void SendIntegrationTestMessages() + { + using (var client1 = new ClientWebSocket()) + using (var client2 = new ClientWebSocket()) + { + client1.ConnectAsync(new Uri("ws://localhost:8989"), CancellationToken.None).Wait(); + Task.Delay(100).Wait(); + + client2.ConnectAsync(new Uri("ws://localhost:8989"), CancellationToken.None).Wait(); + Task.Delay(100).Wait(); + + var bytes2 = new byte[1024]; + var segment2 = new ArraySegment(bytes2); + var receive2 = client2.ReceiveAsync(segment2, CancellationToken.None); + + var message1 = "Hello world"; + var bytes1 = Encoding.UTF8.GetBytes(message1); + var segment1 = new ArraySegment(bytes1); + client1.SendAsync(segment1, WebSocketMessageType.Text, true, CancellationToken.None).Wait(); + Task.Delay(100).Wait(); + + var result2 = receive2.Result; + Assert.AreEqual(WebSocketMessageType.Text, result2.MessageType); + var message3 = Encoding.UTF8.GetString(segment2.Array, 0, result2.Count); + Assert.AreEqual("User 1: Hello world", message3); + + client2.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).Wait(); + client2.Dispose(); + Task.Delay(100).Wait(); + + client1.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).Wait(); + client1.Dispose(); + Task.Delay(100).Wait(); + } + } + + protected void AssertIntegrationTestMessages() + { + var messages = DequeueMessages(); + Assert.AreEqual(5, messages.Count); + Assert.AreEqual("Open: 1", messages[0]); + Assert.AreEqual("Open: 2", messages[1]); + Assert.AreEqual("User 1: Hello world", messages[2]); + Assert.AreEqual("Close: 2", messages[3]); + Assert.AreEqual("Close: 1", messages[4]); + } + } +} \ No newline at end of file diff --git a/src/UnitTests.Fleck/OwinFleckTests.cs b/src/UnitTests.Fleck/OwinFleckTests.cs new file mode 100644 index 0000000..8dc42c4 --- /dev/null +++ b/src/UnitTests.Fleck/OwinFleckTests.cs @@ -0,0 +1,99 @@ +using System; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Owin.Hosting; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Owin.WebSocket.Extensions; + +namespace UnitTests.Fleck +{ + [TestClass] + public class OwinFleckTests : FleckTestsBase + { + [TestMethod] + public void FeatureParityTest() + { + using (WebApp.Start(new StartOptions("http://localhost:8989"), startup => + { + startup.MapOwinFleckRoute(ConfigureIntegrationTestConnection); + })) + { + SendIntegrationTestMessages(); + AssertIntegrationTestMessages(); + } + } + + [TestMethod] + public void AuthorizationTest() + { + using (WebApp.Start(new StartOptions("http://localhost:8989"), startup => + { + startup.MapOwinFleckRoute("/fleck", connection => + { + var id = ConfigureIntegrationTestConnectionAndGetId(connection); + + connection.OnAuthenticateRequest = () => + { + var result = id % 2 == 1; + Send(id, $"Auth {id}: {result}"); + return result; + }; + + }); + })) + using (var client1 = new ClientWebSocket()) + using (var client2 = new ClientWebSocket()) + using (var client3 = new ClientWebSocket()) + { + client1.ConnectAsync(new Uri("ws://localhost:8989/fleck"), CancellationToken.None).Wait(); + + try + { + client2.ConnectAsync(new Uri("ws://localhost:8989/fleck"), CancellationToken.None).Wait(); + Assert.Fail("Client 2 should not be unauthorized"); + } + catch (AggregateException ex) + { + Assert.AreEqual("Unable to connect to the remote server", ex.InnerException.Message); + } + + client3.ConnectAsync(new Uri("ws://localhost:8989/fleck"), CancellationToken.None).Wait(); + + var bytes3 = new byte[1024]; + var segment3 = new ArraySegment(bytes3); + var receive3 = client3.ReceiveAsync(segment3, CancellationToken.None); + + var message1 = "Hello world"; + var bytes1 = Encoding.UTF8.GetBytes(message1); + var segment1 = new ArraySegment(bytes1); + client1.SendAsync(segment1, WebSocketMessageType.Text, true, CancellationToken.None).Wait(); + + var result3 = receive3.Result; + Assert.AreEqual(WebSocketMessageType.Text, result3.MessageType); + var message3 = Encoding.UTF8.GetString(segment3.Array, 0, result3.Count); + Assert.AreEqual(message3, "User 1: Hello world"); + + client3.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).Wait(); + client3.Dispose(); + Task.Delay(100).Wait(); + + client1.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, string.Empty, CancellationToken.None).Wait(); + client1.Dispose(); + Task.Delay(100).Wait(); + } + + var messages = DequeueMessages(); + Assert.AreEqual(8, messages.Count); + Assert.AreEqual("Auth 1: True", messages[0]); + Assert.AreEqual("Open: 1", messages[1]); + Assert.AreEqual("Auth 2: False", messages[2]); + Assert.AreEqual("Auth 3: True", messages[3]); + Assert.AreEqual("Open: 3", messages[4]); + Assert.AreEqual("User 1: Hello world", messages[5]); + Assert.AreEqual("Close: 3", messages[6]); + Assert.AreEqual("Close: 1", messages[7]); + } + } +} \ No newline at end of file diff --git a/src/UnitTests.Fleck/Properties/AssemblyInfo.cs b/src/UnitTests.Fleck/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..3245973 --- /dev/null +++ b/src/UnitTests.Fleck/Properties/AssemblyInfo.cs @@ -0,0 +1,36 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("UnitTests.Fleck")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("UnitTests.Fleck")] +[assembly: AssemblyCopyright("Copyright © 2016")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("7d1ceed6-9bae-4a54-8902-71de01112070")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/src/UnitTests.Fleck/UnitTests.Fleck.csproj b/src/UnitTests.Fleck/UnitTests.Fleck.csproj new file mode 100644 index 0000000..d2c55dc --- /dev/null +++ b/src/UnitTests.Fleck/UnitTests.Fleck.csproj @@ -0,0 +1,112 @@ + + + + + Debug + AnyCPU + {7D1CEED6-9BAE-4A54-8902-71DE01112070} + Library + Properties + UnitTests.Fleck + UnitTests.Fleck + v4.5.2 + 512 + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + ..\packages\Fleck.0.13.0.57\lib\net40\Fleck.dll + True + + + ..\packages\FluentAssertions.4.2.1\lib\net45\FluentAssertions.dll + True + + + ..\packages\FluentAssertions.4.2.1\lib\net45\FluentAssertions.Core.dll + True + + + ..\packages\Microsoft.Owin.3.0.1\lib\net45\Microsoft.Owin.dll + True + + + ..\packages\Microsoft.Owin.Diagnostics.3.0.1\lib\net45\Microsoft.Owin.Diagnostics.dll + True + + + ..\packages\Microsoft.Owin.Host.HttpListener.3.0.1\lib\net45\Microsoft.Owin.Host.HttpListener.dll + True + + + ..\packages\Microsoft.Owin.Hosting.3.0.1\lib\net45\Microsoft.Owin.Hosting.dll + True + + + ..\packages\CommonServiceLocator.1.3\lib\portable-net4+sl5+netcore45+wpa81+wp8\Microsoft.Practices.ServiceLocation.dll + True + + + False + ..\..\..\..\..\Program Files (x86)\Microsoft Visual Studio 12.0\Common7\IDE\PublicAssemblies\Microsoft.VisualStudio.QualityTools.UnitTestFramework.dll + + + ..\packages\Owin.1.0\lib\net40\Owin.dll + True + + + + + + + + + + + + + + + + + + + + + + {32050a9e-3fc9-4a71-85bf-6645ae68b82b} + Owin.WebSocket.Fleck + + + {b1d742f7-39fa-498c-b22e-1a7a61c301e7} + Owin.WebSocket + + + + + + + + \ No newline at end of file diff --git a/src/UnitTests.Fleck/packages.config b/src/UnitTests.Fleck/packages.config new file mode 100644 index 0000000..b949e11 --- /dev/null +++ b/src/UnitTests.Fleck/packages.config @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/UnitTests/UnitTests.csproj b/src/UnitTests/UnitTests.csproj index b57db46..85e9042 100644 --- a/src/UnitTests/UnitTests.csproj +++ b/src/UnitTests/UnitTests.csproj @@ -8,7 +8,7 @@ Properties UnitTests UnitTests - v4.5.1 + v4.5.2 512 {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 10.0 @@ -18,6 +18,7 @@ UnitTest ..\Owin.WebSocket\ true + true diff --git a/src/UnitTests/WebSocketTests.cs b/src/UnitTests/WebSocketTests.cs index c3e9f32..ea19ea7 100644 --- a/src/UnitTests/WebSocketTests.cs +++ b/src/UnitTests/WebSocketTests.cs @@ -35,7 +35,7 @@ public static void Init(TestContext test) { sResolver = new TestResolver(); - WebApp.Start(new StartOptions("http://localhost:8989"), startup => + sWeb = WebApp.Start(new StartOptions("http://localhost:8989"), startup => { startup.MapWebSocketRoute(); startup.MapWebSocketRoute("/ws", sResolver); diff --git a/src/UnitTests/app.config b/src/UnitTests/app.config index 66e9d66..2687008 100644 --- a/src/UnitTests/app.config +++ b/src/UnitTests/app.config @@ -1,11 +1,11 @@ - + - - + + - \ No newline at end of file +