├── WebSocketListener.snk ├── vtortola.WebSockets ├── GlobalSuppressions.cs ├── PingMode.cs ├── Http │ ├── HttpRequestDirection.cs │ ├── HeaderFlags.cs │ ├── WebSocketNegotiationResult.cs │ ├── HeaderAttribute.cs │ ├── WebSocketExtensionOption.cs │ ├── WebSocketExtension.cs │ ├── CookieParser.cs │ ├── WebSocketHttpRequest.cs │ ├── ResponseHeader.cs │ ├── WebSocketHttpResponse.cs │ ├── Headers.Enumerator.cs │ ├── HttpStatusDescription.cs │ ├── RequestHeader.cs │ └── WebSocketHandshake.cs ├── Header │ ├── WebSocketMessageType.cs │ └── WebSocketExtensionFlags.cs ├── HttpAuthenticationCallback.cs ├── Extensibility │ ├── IHttpFallback.cs │ ├── IWebSocketMessageExtension.cs │ ├── IWebSocketMessageExtensionContext.cs │ ├── IWebSocketConnectionExtension.cs │ ├── IHttpRequest.cs │ ├── WebSocketSecureConnectionExtension.cs │ ├── SslNetworkConnection.cs │ ├── WebSocketMessageExtensionCollection.cs │ ├── WebSocketConnectionExtensionCollection.cs │ └── WebSocketFactoryCollection.cs ├── WebSocketException.cs ├── vtortola.WebSockets.csproj.DotSettings ├── WebSocketCloseReason.cs ├── ILogger.cs ├── Tools │ ├── ThreadAbortException.cs │ ├── ArrayExtensions.cs │ ├── SafeEnd.cs │ ├── ArraySegmentExtension.cs │ ├── ReflectionHelper.cs │ ├── ThreadStaticRandom.cs │ ├── ExceptionExtentions.cs │ ├── ObjectPool.cs │ ├── HttpHelper.cs │ └── HeadersHelper.cs ├── Streams │ ├── WebSocketMessageReadStream.cs │ └── WebSocketMessageWriteStream.cs ├── Transports │ ├── Listener.cs │ ├── WebSocketTransport.cs │ ├── NamedPipes │ │ ├── NamedPipeEndPoint.cs │ │ ├── NamedPipeTransport.cs │ │ └── NamedPipeConnection.cs │ ├── NetworkConnection.cs │ ├── Tcp │ │ ├── TcpListener.cs │ │ └── TcpConnection.cs │ ├── UnixSockets │ │ ├── UnixSocketConnection.cs │ │ └── UnixSocketListener.cs │ └── Sockets │ │ └── SocketTransport.cs ├── NullLogger.cs ├── Async │ ├── AsyncCondition.cs │ ├── CancellationQueue.cs │ ├── AsyncQueue.cs │ └── PingQueue.cs ├── Properties │ ├── AssemblyInfo.cs │ └── RuntimeInformation.cs ├── WebSocketFactory.cs ├── ConsoleLogger.cs ├── DefaultBufferManager.cs ├── DebugLogger.cs ├── vtortola.WebSockets.csproj └── WebSocketStringExtensions.cs ├── nuget ├── vtortola.WebSocketListener.1.0.0.nupkg ├── vtortola.WebSocketListener.1.0.1.nupkg ├── vtortola.WebSocketListener.1.1.0.nupkg ├── vtortola.WebSocketListener.1.1.1.nupkg ├── deniszykov.WebSocketListener.4.2.4.nupkg ├── deniszykov.WebSocketListener.4.2.5.nupkg ├── deniszykov.WebSocketListener.4.2.6.nupkg ├── vtortola.WebSocketListener.2.0.0.0.nupkg ├── vtortola.WebSocketListener.2.0.1.0.nupkg ├── vtortola.WebSocketListener.2.1.0.0.nupkg ├── vtortola.WebSocketListener.2.1.1.0.nupkg ├── vtortola.WebSocketListener.2.1.3.0.nupkg ├── vtortola.WebSocketListener.2.1.4.0.nupkg ├── vtortola.WebSocketListener.2.1.5.0.nupkg ├── vtortola.WebSocketListener.2.1.7.0.nupkg ├── vtortola.WebSocketListener.2.1.8.0.nupkg ├── vtortola.WebSocketListener.2.1.9.0.nupkg ├── vtortola.WebSocketListener.2.2.0.1.nupkg ├── vtortola.WebSocketListener.2.2.0.2.nupkg ├── vtortola.WebSocketListener.2.2.0.3.nupkg ├── vtortola.WebSocketListener.2.2.1.0.nupkg └── vtortola.WebSocketListener.2.2.2.0.nupkg ├── vtortola.WebSockets.Rfc6455 ├── GlobalSuppressions.cs ├── Header │ ├── WebSocketFrameOption.cs │ ├── WebSocketFrameOptionExtensions.cs │ ├── ByteArrayExtensions.cs │ └── EndianBitConverter.cs ├── vtortola.WebSockets.Rfc6455.csproj.DotSettings ├── Properties │ └── AssemblyInfo.cs ├── WebSocketFactoryCollectionExtensions.cs ├── Ping │ ├── PingHandler.cs │ ├── ManualPing.cs │ ├── BandwidthSavingPing.cs │ └── LatencyControlPing.cs ├── WebSocketFactoryRfc6455.cs ├── vtortola.WebSockets.Rfc6455.csproj └── WebSocketRfc6455.cs ├── Tests └── WebSocketListener.UnitTests │ ├── WebSocketListener.snk │ ├── app.config │ ├── TimedQueueTests.cs │ ├── vtortola.WebSockets.UnitTests.csproj │ ├── CombinedStream.cs │ ├── HttpHelperTest.cs │ ├── DummyNetworkConnection.cs │ ├── TestLogger.cs │ ├── EndianBitConverterTests.cs │ ├── WebSocketFrameHeaderFlagsTests.cs │ ├── AsyncConditionSourceTests.cs │ ├── EchoServer.cs │ └── BufferManagerTests.cs ├── .editorconfig ├── vtortola.WebSockets.Deflate ├── vtortola.WebSockets.Deflate.csproj.DotSettings ├── Properties │ └── AssemblyInfo.cs ├── WebSocketMessageExtensionCollectionExtensions.cs ├── WebSocketDeflateContext.cs ├── WebSocketDeflateExtension.cs ├── vtortola.WebSockets.Deflate.csproj ├── WebSocketDeflateReadStream.cs └── WebSocketDeflateWriteStream.cs ├── Samples ├── EchoServer │ ├── EchoServer.csproj │ ├── Log4NetLogger.cs │ └── App.config ├── MonoEchoServer │ ├── MonoEchoServer.csproj │ └── App.config └── WebSocketClient │ ├── WebSocketClient.csproj │ ├── App.config │ └── Program.cs ├── appveyor.yml └── .gitattributes /WebSocketListener.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/WebSocketListener.snk -------------------------------------------------------------------------------- /vtortola.WebSockets/GlobalSuppressions.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/vtortola.WebSockets/GlobalSuppressions.cs -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.1.0.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.1.0.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.1.0.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.1.0.1.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.1.1.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.1.1.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.1.1.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.1.1.1.nupkg -------------------------------------------------------------------------------- /nuget/deniszykov.WebSocketListener.4.2.4.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/deniszykov.WebSocketListener.4.2.4.nupkg -------------------------------------------------------------------------------- /nuget/deniszykov.WebSocketListener.4.2.5.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/deniszykov.WebSocketListener.4.2.5.nupkg -------------------------------------------------------------------------------- /nuget/deniszykov.WebSocketListener.4.2.6.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/deniszykov.WebSocketListener.4.2.6.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.0.0.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.0.0.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.0.1.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.0.1.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.0.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.0.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.1.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.1.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.3.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.3.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.4.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.4.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.5.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.5.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.7.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.7.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.8.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.8.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.1.9.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.1.9.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.2.0.1.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.2.0.1.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.2.0.2.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.2.0.2.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.2.0.3.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.2.0.3.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.2.1.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.2.1.0.nupkg -------------------------------------------------------------------------------- /nuget/vtortola.WebSocketListener.2.2.2.0.nupkg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/nuget/vtortola.WebSocketListener.2.2.2.0.nupkg -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/GlobalSuppressions.cs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/vtortola.WebSockets.Rfc6455/GlobalSuppressions.cs -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/WebSocketListener.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deniszykov/WebSocketListener/HEAD/Tests/WebSocketListener.UnitTests/WebSocketListener.snk -------------------------------------------------------------------------------- /vtortola.WebSockets/PingMode.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets 2 | { 3 | public enum PingMode 4 | { 5 | Manual, 6 | LatencyControl, 7 | BandwidthSaving 8 | } 9 | } -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/HttpRequestDirection.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets.Http 2 | { 3 | public enum HttpRequestDirection 4 | { 5 | Incoming, 6 | Outgoing 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Header/WebSocketMessageType.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace vtortola.WebSockets 3 | { 4 | public enum WebSocketMessageType 5 | { 6 | Text = 1, 7 | Binary = 2, 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | end_of_line = crlf 8 | indent_style = space 9 | indent_size = 4 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /vtortola.WebSockets/HttpAuthenticationCallback.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | namespace vtortola.WebSockets 4 | { 5 | public delegate Task HttpAuthenticationCallback(WebSocketHttpRequest request, WebSocketHttpResponse response); 6 | } -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/HeaderFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Http 4 | { 5 | [Flags] 6 | public enum HeaderFlags 7 | { 8 | None = 0, 9 | Singleton = 0x1 << 0, 10 | DoNotSplit = 0x1 << 1, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Header/WebSocketFrameOption.cs: -------------------------------------------------------------------------------- 1 | 2 | namespace vtortola.WebSockets.Rfc6455 3 | { 4 | internal enum WebSocketFrameOption 5 | { 6 | Continuation = 0, 7 | Text = 1, 8 | Binary = 2, 9 | ConnectionClose = 8, 10 | Ping = 9, 11 | Pong = 10 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/IHttpFallback.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using vtortola.WebSockets.Transports; 3 | 4 | namespace vtortola.WebSockets 5 | { 6 | [PublicAPI] 7 | public interface IHttpFallback 8 | { 9 | void Post([NotNull] IHttpRequest request, [NotNull] NetworkConnection networkConnection); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /vtortola.WebSockets/WebSocketException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets 4 | { 5 | public class WebSocketException : Exception 6 | { 7 | public WebSocketException(string message) 8 | : base(message) 9 | { 10 | } 11 | public WebSocketException(string message, Exception inner) 12 | :base(message,inner) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Header/WebSocketFrameOptionExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets.Rfc6455 2 | { 3 | internal static class WebSocketFrameOptionExtensions 4 | { 5 | internal static bool IsData(this WebSocketFrameOption option) 6 | { 7 | return option == WebSocketFrameOption.Binary || option == WebSocketFrameOption.Text || option == WebSocketFrameOption.Continuation; 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/IWebSocketMessageExtension.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets 2 | { 3 | public interface IWebSocketMessageExtension 4 | { 5 | string Name { get; } 6 | 7 | bool TryNegotiate(WebSocketHttpRequest request, out WebSocketExtension extensionResponse, out IWebSocketMessageExtensionContext context); 8 | 9 | IWebSocketMessageExtension Clone(); 10 | 11 | string ToString(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vtortola.WebSockets/vtortola.WebSockets.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp60 -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/vtortola.WebSockets.Deflate.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp60 -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/vtortola.WebSockets.Rfc6455.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp60 -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("vtortola.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100DD799BC297D1586594801A2EE2E5FD9F8FB72002920C3E60C5C1A4681EC52CB4EA5BB90049B6649271B3CA2FDE367673C8F337D23CCF7F2FD407440B7EE251A9A82306C77259A3A3B445777DC290A00455A44C6590D9B802325376552D43DC99B4D10DEE52A5B1A0B18F113932B4550E7BA5E7DC1FB358D64B0B01A11BBA9BB7")] -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/IWebSocketMessageExtensionContext.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | 3 | namespace vtortola.WebSockets 4 | { 5 | public interface IWebSocketMessageExtensionContext 6 | { 7 | [NotNull] 8 | WebSocketMessageReadStream ExtendReader([NotNull] WebSocketMessageReadStream message); 9 | 10 | [NotNull] 11 | WebSocketMessageWriteStream ExtendWriter([NotNull] WebSocketMessageWriteStream message); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: InternalsVisibleTo("vtortola.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100DD799BC297D1586594801A2EE2E5FD9F8FB72002920C3E60C5C1A4681EC52CB4EA5BB90049B6649271B3CA2FDE367673C8F337D23CCF7F2FD407440B7EE251A9A82306C77259A3A3B445777DC290A00455A44C6590D9B802325376552D43DC99B4D10DEE52A5B1A0B18F113932B4550E7BA5E7DC1FB358D64B0B01A11BBA9BB7")] 5 | -------------------------------------------------------------------------------- /vtortola.WebSockets/WebSocketCloseReason.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets 2 | { 3 | public enum WebSocketCloseReason : short 4 | { 5 | NormalClose = 1000, 6 | GoingAway = 1001, 7 | ProtocolError = 1002, 8 | UnacceptableDataType = 1003, 9 | InvalidData = 1007, 10 | MessageViolatesPolicy = 1008, 11 | MessageToLarge = 1009, 12 | ExtensionRequired = 1010, 13 | UnexpectedCondition = 1011, 14 | TLSFailure = 105, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/IWebSocketConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using JetBrains.Annotations; 3 | using vtortola.WebSockets.Transports; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | public interface IWebSocketConnectionExtension 8 | { 9 | [ItemNotNull, NotNull] 10 | Task ExtendConnectionAsync([NotNull] NetworkConnection networkConnection); 11 | 12 | [NotNull] 13 | IWebSocketConnectionExtension Clone(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/app.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /vtortola.WebSockets/ILogger.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public interface ILogger 10 | { 11 | bool IsDebugEnabled { get; } 12 | bool IsWarningEnabled { get; } 13 | bool IsErrorEnabled { get; } 14 | 15 | void Debug(string message, Exception error = null); 16 | void Warning(string message, Exception error = null); 17 | void Error(string message, Exception error = null); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ThreadAbortException.cs: -------------------------------------------------------------------------------- 1 | // ReSharper disable once CheckNamespace 2 | namespace System.Threading 3 | { 4 | #if THREADABORTDUMMY 5 | internal sealed class ThreadAbortException : Exception 6 | { 7 | public ThreadAbortException() 8 | { 9 | } 10 | 11 | public ThreadAbortException(string message) : base(message) 12 | { 13 | } 14 | 15 | public ThreadAbortException(string message, Exception innerException) : base(message, innerException) 16 | { 17 | } 18 | } 19 | #endif 20 | } 21 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketNegotiationResult.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.ExceptionServices; 2 | 3 | namespace vtortola.WebSockets.Http 4 | { 5 | internal sealed class WebSocketNegotiationResult 6 | { 7 | public WebSocket Result { get; } 8 | public ExceptionDispatchInfo Error { get; } 9 | public WebSocketNegotiationResult(WebSocket result) 10 | { 11 | Result = result; 12 | } 13 | public WebSocketNegotiationResult(ExceptionDispatchInfo error) 14 | { 15 | Error = error; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/WebSocketMessageExtensionCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Deflate 4 | { 5 | public static class WebSocketMessageExtensionCollectionExtensions 6 | { 7 | public static WebSocketMessageExtensionCollection RegisterDeflateCompression(this WebSocketMessageExtensionCollection collection) 8 | { 9 | if (collection == null) throw new ArgumentNullException(nameof(collection)); 10 | 11 | collection.Add(new WebSocketDeflateExtension()); 12 | return collection; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/HeaderAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Http 4 | { 5 | [AttributeUsage(AttributeTargets.Field)] 6 | internal class HeaderAttribute : Attribute 7 | { 8 | public string Name { get; } 9 | public HeaderFlags Flags { get; set; } 10 | 11 | public HeaderAttribute() 12 | { 13 | 14 | } 15 | public HeaderAttribute(string headerName) 16 | { 17 | if (headerName == null) throw new ArgumentNullException(nameof(headerName)); 18 | 19 | this.Name = headerName; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/IHttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using JetBrains.Annotations; 5 | using vtortola.WebSockets.Http; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | [PublicAPI] 10 | public interface IHttpRequest 11 | { 12 | EndPoint LocalEndPoint { get; } 13 | EndPoint RemoteEndPoint { get; } 14 | Uri RequestUri { get; } 15 | Version HttpVersion { get; } 16 | bool IsSecure { get; } 17 | CookieCollection Cookies { get; } 18 | Headers Headers { get; } 19 | IDictionary Items { get; } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/WebSocketFactoryCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Rfc6455 4 | { 5 | public static class WebSocketFactoryCollectionExtensions 6 | { 7 | public static WebSocketFactoryCollection RegisterRfc6455(this WebSocketFactoryCollection collection, Action configure = null) 8 | { 9 | if (collection == null) throw new ArgumentNullException(nameof(collection)); 10 | 11 | var factory = new WebSocketFactoryRfc6455(); 12 | configure?.Invoke(factory); 13 | 14 | collection.Add(factory); 15 | 16 | return collection; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Streams/WebSocketMessageReadStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | public abstract class WebSocketMessageReadStream : WebSocketMessageStream 8 | { 9 | public abstract WebSocketMessageType MessageType { get; } 10 | public abstract WebSocketExtensionFlags Flags { get; } 11 | public sealed override bool CanRead => true; 12 | 13 | /// 14 | [Obsolete("Writing to the read stream is not allowed", true)] 15 | public sealed override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 16 | { 17 | throw new NotSupportedException(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Samples/EchoServer/EchoServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net45 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | 4 | namespace vtortola.WebSockets.Tools 5 | { 6 | internal static class ArrayExtensions 7 | { 8 | public static ResultT[] ConvertAll([NotNull] this SourceT[] sourceArray, [NotNull, InstantHandle] Func conversion) 9 | { 10 | if (sourceArray == null) throw new ArgumentNullException(nameof(sourceArray)); 11 | if (conversion == null) throw new ArgumentNullException(nameof(conversion)); 12 | 13 | var resultArray = new ResultT[sourceArray.Length]; 14 | for (int i = 0; i < sourceArray.Length; i++) 15 | resultArray[i] = conversion(sourceArray[i]); 16 | return resultArray; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Header/WebSocketExtensionFlags.cs: -------------------------------------------------------------------------------- 1 | namespace vtortola.WebSockets 2 | { 3 | public sealed class WebSocketExtensionFlags 4 | { 5 | private bool _rsv1, _rsv2, _rsv3; 6 | private readonly bool _none; 7 | public bool Rsv1 { get { return _rsv1; } set { _rsv1 = value && !_none; } } 8 | public bool Rsv2 { get { return _rsv2; } set { _rsv2 = value && !_none; } } 9 | public bool Rsv3 { get { return _rsv3; } set { _rsv3 = value && !_none; } } 10 | 11 | public static readonly WebSocketExtensionFlags None = new WebSocketExtensionFlags(true); 12 | 13 | public WebSocketExtensionFlags() 14 | { 15 | _none = false; 16 | } 17 | private WebSocketExtensionFlags(bool none) 18 | { 19 | _none = true; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Ping/PingHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading.Tasks; 4 | 5 | namespace vtortola.WebSockets.Rfc6455 6 | { 7 | partial class WebSocketConnectionRfc6455 8 | { 9 | private abstract class PingHandler 10 | { 11 | public abstract Task PingAsync(); 12 | public abstract void NotifyPong(ArraySegment pongBuffer); 13 | public abstract void NotifyActivity(); 14 | 15 | protected static TimeSpan TimestampToTimeSpan(long timestamp) 16 | { 17 | if (Stopwatch.IsHighResolution) 18 | return TimeSpan.FromTicks((long)(timestamp * (10000000D / Stopwatch.Frequency))); 19 | 20 | return TimeSpan.FromTicks(timestamp); 21 | } 22 | 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/Listener.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Net; 8 | using System.Threading.Tasks; 9 | 10 | namespace vtortola.WebSockets.Transports 11 | { 12 | internal abstract class Listener : IDisposable 13 | { 14 | public abstract IReadOnlyCollection LocalEndpoints { get; } 15 | 16 | public abstract Task AcceptConnectionAsync(); 17 | 18 | protected abstract void Dispose(bool disposed); 19 | 20 | /// 21 | void IDisposable.Dispose() 22 | { 23 | this.Dispose(disposed: true); 24 | } 25 | 26 | /// 27 | public abstract override string ToString(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Samples/MonoEchoServer/MonoEchoServer.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net45 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /Samples/WebSocketClient/WebSocketClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | net45 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/WebSocketDeflateContext.cs: -------------------------------------------------------------------------------- 1 | 2 | using System; 3 | 4 | namespace vtortola.WebSockets.Deflate 5 | { 6 | public sealed class WebSocketDeflateContext : IWebSocketMessageExtensionContext 7 | { 8 | public WebSocketMessageReadStream ExtendReader(WebSocketMessageReadStream message) 9 | { 10 | if (message == null) throw new ArgumentNullException(nameof(message)); 11 | 12 | if (message.Flags.Rsv1) 13 | return new WebSocketDeflateReadStream(message); 14 | else 15 | return message; 16 | } 17 | public WebSocketMessageWriteStream ExtendWriter(WebSocketMessageWriteStream message) 18 | { 19 | if (message == null) throw new ArgumentNullException(nameof(message)); 20 | 21 | message.ExtensionFlags.Rsv1 = true; 22 | return new WebSocketDeflateWriteStream(message); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/WebSocketTransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace vtortola.WebSockets.Transports 11 | { 12 | public abstract class WebSocketTransport 13 | { 14 | public abstract IReadOnlyCollection Schemes { get; } 15 | 16 | internal abstract Task ListenAsync(Uri address, WebSocketListenerOptions options); 17 | internal abstract Task ConnectAsync(Uri address, WebSocketListenerOptions options, CancellationToken cancellation); 18 | internal abstract bool ShouldUseSsl(Uri requestUri); 19 | 20 | /// 21 | public virtual WebSocketTransport Clone() 22 | { 23 | return (WebSocketTransport)this.MemberwiseClone(); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vtortola.WebSockets/NullLogger.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public sealed class NullLogger : ILogger 10 | { 11 | public static readonly NullLogger Instance = new NullLogger(); 12 | 13 | /// 14 | public bool IsDebugEnabled => false; 15 | /// 16 | public bool IsWarningEnabled => false; 17 | /// 18 | public bool IsErrorEnabled => false; 19 | /// 20 | public void Debug(string message, Exception error = null) 21 | { 22 | 23 | } 24 | /// 25 | public void Warning(string message, Exception error = null) 26 | { 27 | 28 | } 29 | /// 30 | public void Error(string message, Exception error = null) 31 | { 32 | 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Async/AsyncCondition.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets.Async 8 | { 9 | internal struct AsyncConditionVariable 10 | { 11 | private readonly AsyncConditionSource source; 12 | 13 | public bool IsSet => this.source != null && this.source.IsSet; 14 | 15 | public AsyncConditionVariable(AsyncConditionSource source) 16 | { 17 | if (source == null) throw new ArgumentNullException(nameof(source), "source != null"); 18 | 19 | this.source = source; 20 | } 21 | 22 | public AsyncConditionSource.Awaiter GetAwaiter() 23 | { 24 | if (this.source == null) throw new InvalidOperationException(); 25 | 26 | return this.source.GetAwaiter(); 27 | } 28 | 29 | public override string ToString() 30 | { 31 | return $"Condition: {this.IsSet}"; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /vtortola.WebSockets/Streams/WebSocketMessageWriteStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using JetBrains.Annotations; 5 | 6 | namespace vtortola.WebSockets 7 | { 8 | public abstract class WebSocketMessageWriteStream : WebSocketMessageStream 9 | { 10 | public sealed override bool CanWrite => true; 11 | 12 | [NotNull] 13 | public WebSocketExtensionFlags ExtensionFlags { get; } 14 | 15 | protected WebSocketMessageWriteStream() 16 | { 17 | this.ExtensionFlags = new WebSocketExtensionFlags(); 18 | } 19 | 20 | /// 21 | [Obsolete("Reading from the write stream is not allowed", true)] 22 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 23 | { 24 | throw new NotSupportedException(); 25 | } 26 | 27 | public abstract Task WriteAndCloseAsync([NotNull]byte[] buffer, int offset, int count, CancellationToken cancellationToken); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Async/CancellationQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Threading; 7 | 8 | namespace vtortola.WebSockets.Async 9 | { 10 | internal class CancellationQueue : NotificationQueue 11 | { 12 | public CancellationQueue(TimeSpan period) 13 | : base(period) { } 14 | 15 | protected override CancellationTokenSource CreateSubscriptionList() 16 | { 17 | return new CancellationTokenSource(); 18 | } 19 | 20 | protected override void NotifySubscribers(CancellationTokenSource subscriptionList) 21 | { 22 | try 23 | { 24 | subscriptionList.Cancel(throwOnFirstException: true); 25 | } 26 | catch (Exception cancelError) when (cancelError is ThreadAbortException == false) 27 | { 28 | DebugLogger.Instance.Warning("An error occurred while canceling token on source.", cancelError); 29 | } 30 | } 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Header/ByteArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Rfc6455 4 | { 5 | internal static class ByteArrayExtensions 6 | { 7 | internal static void ReversePortion(this byte[] buffer, int offset, int count) 8 | { 9 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 10 | if (offset < 0 || offset > buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); 11 | if (count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); 12 | 13 | if (count + offset > buffer.Length) 14 | throw new ArgumentException("The array is to small"); 15 | 16 | if (count < 1) 17 | return; 18 | 19 | byte pivot; 20 | var back = offset + count - 1; 21 | var half = (int)Math.Floor(count / 2f); 22 | for (var i = offset; i < offset + half; i++) 23 | { 24 | pivot = buffer[i]; 25 | buffer[i] = buffer[back]; 26 | buffer[back--] = pivot; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Async/AsyncQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System.Collections.Concurrent; 6 | using System.Threading; 7 | 8 | #pragma warning disable 420 9 | 10 | namespace vtortola.WebSockets.Async 11 | { 12 | internal sealed class AsyncQueue : AsyncCollection> 13 | { 14 | /// 15 | public AsyncQueue(int boundedCapacity = UNBOUND) 16 | : base(new ConcurrentQueue(), q => q.IsEmpty, boundedCapacity) 17 | { 18 | 19 | } 20 | 21 | public TakeResult DequeueAsync(CancellationToken cancellationToken = default(CancellationToken)) 22 | { 23 | return this.TakeAsync(cancellationToken); 24 | } 25 | public bool TryDequeue(out ItemT value) 26 | { 27 | return this.TryTake(out value); 28 | } 29 | public bool TryEnqueue(ItemT item) 30 | { 31 | return this.TryAdd(item); 32 | } 33 | 34 | /// 35 | public override string ToString() 36 | { 37 | return $"AsyncQueue, count: {this.Count}"; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | [assembly: InternalsVisibleTo("vtortola.WebSockets.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100DD799BC297D1586594801A2EE2E5FD9F8FB72002920C3E60C5C1A4681EC52CB4EA5BB90049B6649271B3CA2FDE367673C8F337D23CCF7F2FD407440B7EE251A9A82306C77259A3A3B445777DC290A00455A44C6590D9B802325376552D43DC99B4D10DEE52A5B1A0B18F113932B4550E7BA5E7DC1FB358D64B0B01A11BBA9BB7")] 4 | [assembly: InternalsVisibleTo("vtortola.WebSockets.Rfc6455, PublicKey=0024000004800000940000000602000000240000525341310004000001000100DD799BC297D1586594801A2EE2E5FD9F8FB72002920C3E60C5C1A4681EC52CB4EA5BB90049B6649271B3CA2FDE367673C8F337D23CCF7F2FD407440B7EE251A9A82306C77259A3A3B445777DC290A00455A44C6590D9B802325376552D43DC99B4D10DEE52A5B1A0B18F113932B4550E7BA5E7DC1FB358D64B0B01A11BBA9BB7")] 5 | [assembly: InternalsVisibleTo("vtortola.WebSockets.Deflate, PublicKey=0024000004800000940000000602000000240000525341310004000001000100DD799BC297D1586594801A2EE2E5FD9F8FB72002920C3E60C5C1A4681EC52CB4EA5BB90049B6649271B3CA2FDE367673C8F337D23CCF7F2FD407440B7EE251A9A82306C77259A3A3B445777DC290A00455A44C6590D9B802325376552D43DC99B4D10DEE52A5B1A0B18F113932B4550E7BA5E7DC1FB358D64B0B01A11BBA9BB7")] -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/NamedPipes/NamedPipeEndPoint.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | #if !NAMED_PIPES_DISABLE 6 | using System; 7 | using System.Net; 8 | using System.Text; 9 | 10 | namespace vtortola.WebSockets.Transports.NamedPipes 11 | { 12 | internal class NamedPipeEndPoint : EndPoint 13 | { 14 | public string PipeName { get; } 15 | 16 | public NamedPipeEndPoint(string pipeName) 17 | { 18 | this.PipeName = pipeName; 19 | } 20 | 21 | /// 22 | public override EndPoint Create(SocketAddress socketAddress) 23 | { 24 | if (socketAddress == null) throw new ArgumentNullException(nameof(socketAddress)); 25 | 26 | var pipeNameBytes = new byte[socketAddress.Size]; 27 | for (var i = 0; i < pipeNameBytes.Length; i++) 28 | pipeNameBytes[i] = socketAddress[i]; 29 | 30 | return new NamedPipeEndPoint(Encoding.UTF8.GetString(pipeNameBytes)); 31 | } 32 | 33 | /// 34 | public override string ToString() 35 | { 36 | return this.PipeName; 37 | } 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/NetworkConnection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.IO; 7 | using System.Net; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace vtortola.WebSockets.Transports 12 | { 13 | public abstract class NetworkConnection : IDisposable 14 | { 15 | public abstract EndPoint LocalEndPoint { get; } 16 | public abstract EndPoint RemoteEndPoint { get; } 17 | 18 | public abstract Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); 19 | public abstract Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken); 20 | public abstract Task FlushAsync(CancellationToken cancellationToken); 21 | 22 | public abstract Task CloseAsync(); 23 | public abstract void Dispose(bool disposed); 24 | 25 | public abstract Stream AsStream(); 26 | 27 | /// 28 | void IDisposable.Dispose() 29 | { 30 | this.Dispose(disposed: true); 31 | } 32 | 33 | /// 34 | public abstract override string ToString(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vtortola.WebSockets/WebSocketFactory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using JetBrains.Annotations; 3 | using vtortola.WebSockets.Transports; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | [PublicAPI] 8 | public abstract class WebSocketFactory 9 | { 10 | public abstract short Version { get; } 11 | 12 | public WebSocketMessageExtensionCollection MessageExtensions { get; private set; } 13 | 14 | protected WebSocketFactory() 15 | { 16 | MessageExtensions = new WebSocketMessageExtensionCollection(); 17 | } 18 | 19 | public abstract WebSocket CreateWebSocket(NetworkConnection networkConnection, WebSocketListenerOptions options, WebSocketHttpRequest httpRequest, WebSocketHttpResponse httpResponse, List negotiatedExtensions); 20 | 21 | /// 22 | public virtual WebSocketFactory Clone() 23 | { 24 | var clone = (WebSocketFactory)this.MemberwiseClone(); 25 | clone.MessageExtensions = new WebSocketMessageExtensionCollection(); 26 | foreach (var extension in this.MessageExtensions) 27 | clone.MessageExtensions.Add(extension.Clone()); 28 | return clone; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Samples/EchoServer/Log4NetLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using log4net; 3 | 4 | // ReSharper disable once CheckNamespace 5 | namespace vtortola.WebSockets 6 | { 7 | public sealed class Log4NetLogger : ILogger 8 | { 9 | private readonly ILog log; 10 | 11 | /// 12 | public bool IsDebugEnabled => this.log.IsDebugEnabled; 13 | /// 14 | public bool IsWarningEnabled => this.log.IsWarnEnabled; 15 | /// 16 | public bool IsErrorEnabled => this.log.IsErrorEnabled; 17 | /// 18 | public Log4NetLogger(Type loggerType = null) 19 | { 20 | this.log = LogManager.GetLogger(loggerType ?? typeof(WebSocketListener)); 21 | } 22 | 23 | public void Debug(string message, Exception error = null) 24 | { 25 | this.log.Debug(message, error); 26 | } 27 | /// 28 | public void Warning(string message, Exception error = null) 29 | { 30 | this.log.Warn(message, error); 31 | } 32 | /// 33 | public void Error(string message, Exception error = null) 34 | { 35 | this.log.Error(message, error); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketExtensionOption.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets 4 | { 5 | public class WebSocketExtensionOption 6 | { 7 | public readonly string Name; 8 | public readonly string Value; 9 | public readonly bool ClientAvailableOption; 10 | 11 | public WebSocketExtensionOption(string name) 12 | { 13 | if (name == null) throw new ArgumentNullException(nameof(name)); 14 | 15 | this.Name = name; 16 | } 17 | public WebSocketExtensionOption(string name, bool clientAvailableOption) 18 | { 19 | if (name == null) throw new ArgumentNullException(nameof(name)); 20 | 21 | this.Name = name; 22 | this.ClientAvailableOption = clientAvailableOption; 23 | } 24 | public WebSocketExtensionOption(string name, string value) 25 | { 26 | if (name == null) throw new ArgumentNullException(nameof(name)); 27 | 28 | this.Name = name; 29 | this.Value = value; 30 | } 31 | 32 | /// 33 | public override string ToString() 34 | { 35 | if (string.IsNullOrEmpty(this.Value)) 36 | return this.Name; 37 | else 38 | return $"{this.Name}={this.Value}"; 39 | } 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/WebSocketFactoryRfc6455.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using vtortola.WebSockets.Transports; 4 | 5 | namespace vtortola.WebSockets.Rfc6455 6 | { 7 | public class WebSocketFactoryRfc6455 : WebSocketFactory 8 | { 9 | public override short Version => 13; 10 | 11 | public override WebSocket CreateWebSocket(NetworkConnection networkConnection, WebSocketListenerOptions options, WebSocketHttpRequest httpRequest, WebSocketHttpResponse httpResponse, List negotiatedExtensions) 12 | { 13 | if (networkConnection == null) throw new ArgumentNullException(nameof(networkConnection)); 14 | if (options == null) throw new ArgumentNullException(nameof(options)); 15 | if (httpRequest == null) throw new ArgumentNullException(nameof(httpRequest)); 16 | if (httpResponse == null) throw new ArgumentNullException(nameof(httpResponse)); 17 | if (negotiatedExtensions == null) throw new ArgumentNullException(nameof(negotiatedExtensions)); 18 | 19 | return new WebSocketRfc6455(networkConnection, options, httpRequest, httpResponse, negotiatedExtensions); 20 | } 21 | 22 | /// 23 | public override string ToString() 24 | { 25 | return "Rfc6455, version: 13"; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | public sealed class WebSocketExtension 8 | { 9 | public static readonly ReadOnlyCollection Empty = new ReadOnlyCollection(new List()); 10 | private readonly string extensionString; 11 | 12 | public readonly string Name; 13 | public readonly ReadOnlyCollection Options; 14 | 15 | public WebSocketExtension(string name, IList options) 16 | { 17 | if (name == null) throw new ArgumentNullException(nameof(name)); 18 | if (options == null) throw new ArgumentNullException(nameof(options)); 19 | 20 | this.Name = name; 21 | this.Options = options as ReadOnlyCollection ?? new ReadOnlyCollection(options); 22 | this.extensionString = this.Options.Count > 0 ? this.Name + ";" + string.Join(";", this.Options) : this.Name; 23 | } 24 | public WebSocketExtension(string name) 25 | { 26 | this.Name = name; 27 | this.Options = Empty; 28 | } 29 | /// 30 | public override string ToString() 31 | { 32 | return this.extensionString; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/WebSocketDeflateExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Options = System.Collections.ObjectModel.ReadOnlyCollection; 3 | 4 | namespace vtortola.WebSockets.Deflate 5 | { 6 | public sealed class WebSocketDeflateExtension : IWebSocketMessageExtension 7 | { 8 | public const string EXTENSION_NAME = "permessage-deflate"; 9 | 10 | private static readonly Options DefaultOptions = new Options(new[] { new WebSocketExtensionOption("client_no_context_takeover") }); 11 | private static readonly WebSocketExtension DefaultResponse = new WebSocketExtension(EXTENSION_NAME, DefaultOptions); 12 | 13 | public string Name => EXTENSION_NAME; 14 | 15 | public bool TryNegotiate(WebSocketHttpRequest request, out WebSocketExtension extensionResponse, out IWebSocketMessageExtensionContext context) 16 | { 17 | if (request == null) throw new ArgumentNullException(nameof(request)); 18 | 19 | extensionResponse = DefaultResponse; 20 | context = new WebSocketDeflateContext(); 21 | return true; 22 | } 23 | 24 | public IWebSocketMessageExtension Clone() 25 | { 26 | var clone = (WebSocketDeflateExtension)this.MemberwiseClone(); 27 | return clone; 28 | } 29 | 30 | /// 31 | public override string ToString() 32 | { 33 | return DefaultResponse.ToString(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/SafeEnd.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using JetBrains.Annotations; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | internal static class SafeEnd 8 | { 9 | public static void Dispose([CanBeNull] T disposable, ILogger log = null) where T : class, IDisposable 10 | { 11 | if (log == null) 12 | { 13 | #if DEBUG 14 | log = DebugLogger.Instance; 15 | #else 16 | log = NullLogger.Instance; 17 | #endif 18 | } 19 | 20 | try 21 | { 22 | disposable?.Dispose(); 23 | } 24 | catch (Exception disposeError) 25 | { 26 | if (log.IsDebugEnabled) 27 | log.Debug($"{typeof(T)} dispose cause error.", disposeError); 28 | } 29 | } 30 | 31 | public static void ReleaseSemaphore(SemaphoreSlim semaphore, ILogger log = null) 32 | { 33 | try 34 | { 35 | semaphore.Release(); 36 | } 37 | catch (ObjectDisposedException) 38 | { 39 | // Held threads may try to release an already 40 | // disposed semaphore 41 | } 42 | catch (Exception releaseError) 43 | { 44 | if (log?.IsDebugEnabled ?? false) 45 | log.Debug("Semaphore release cause error.", releaseError); 46 | } 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/TimedQueueTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using vtortola.WebSockets.Async; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace vtortola.WebSockets.UnitTests 9 | { 10 | public sealed class TimedQueueTests 11 | { 12 | private readonly TestLogger logger; 13 | 14 | public TimedQueueTests(ITestOutputHelper output) 15 | { 16 | this.logger = new TestLogger(output); 17 | } 18 | 19 | [Theory] 20 | [InlineData(100)] 21 | [InlineData(1000)] 22 | [InlineData(2000)] 23 | [InlineData(3000)] 24 | public void SubscribeAndDispatch(int milliseconds) 25 | { 26 | var sw = Stopwatch.StartNew(); 27 | var timedQueue = new CancellationQueue(TimeSpan.FromMilliseconds(milliseconds / 2.0)); 28 | var subscriptions = 0; 29 | var hits = 0; 30 | while (sw.ElapsedMilliseconds < milliseconds) 31 | { 32 | timedQueue.GetSubscriptionList().Token.Register(() => Interlocked.Increment(ref hits)); 33 | subscriptions++; 34 | Thread.Sleep(10); 35 | } 36 | 37 | sw.Reset(); 38 | while (sw.ElapsedMilliseconds < milliseconds && subscriptions != hits) 39 | Thread.Sleep(10); 40 | 41 | this.logger.Debug($"[TEST] subscriptions: {subscriptions}, hits: {hits}."); 42 | 43 | Assert.Equal(subscriptions, hits); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ArraySegmentExtension.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets.Tools 8 | { 9 | internal static class ArraySegmentExtension 10 | { 11 | public static ArraySegment NextSegment(this ArraySegment segment, int segmentSize) 12 | { 13 | if (segment.Array == null) throw new ArgumentNullException(nameof(segment)); 14 | if (segmentSize < 0 || segment.Offset + segment.Count + segmentSize > segment.Array.Length) throw new ArgumentOutOfRangeException(nameof(segmentSize)); 15 | 16 | return new ArraySegment(segment.Array, segment.Offset + segment.Count, segmentSize); 17 | } 18 | public static ArraySegment Skip(this ArraySegment segment, int offset) 19 | { 20 | if (segment.Array == null) throw new ArgumentNullException(nameof(segment)); 21 | if (offset < 0 || offset > segment.Count) throw new ArgumentOutOfRangeException(nameof(offset)); 22 | 23 | return new ArraySegment(segment.Array, segment.Offset + offset, segment.Count - offset); 24 | } 25 | public static ArraySegment Limit(this ArraySegment segment, int size) 26 | { 27 | if (segment.Array == null) throw new ArgumentNullException(nameof(segment)); 28 | if (size < 0 || size > segment.Count) throw new ArgumentOutOfRangeException(nameof(size)); 29 | 30 | return new ArraySegment(segment.Array, segment.Offset, size); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/vtortola.WebSockets.UnitTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45 4 | True 5 | ..\..\WebSocketListener.snk 6 | False 7 | Unit tests for vtortola.WebSocketListener 8 | 0.0.0.0 9 | 0.0.0.0 10 | 1.0.0 11 | true 12 | true 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | all 25 | runtime; build; native; contentfiles; analyzers; buildtransitive 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/CookieParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net; 3 | using JetBrains.Annotations; 4 | 5 | namespace vtortola.WebSockets 6 | { 7 | public static class CookieParser 8 | { 9 | public static IEnumerable Parse([CanBeNull] string cookieString) 10 | { 11 | if (string.IsNullOrWhiteSpace(cookieString)) 12 | yield break; 13 | 14 | string part = string.Empty, name = string.Empty; 15 | for (int i = 0; i < cookieString.Length; i++) 16 | { 17 | char c = cookieString[i]; 18 | if (c == '=' && string.IsNullOrWhiteSpace(name)) 19 | { 20 | name = part; 21 | part = string.Empty; 22 | continue; 23 | } 24 | else if (c == ';') 25 | { 26 | if (!string.IsNullOrWhiteSpace(name)) 27 | yield return CreateCookie(name, part); 28 | else 29 | yield return CreateCookie(part, string.Empty); 30 | 31 | name = string.Empty; 32 | part = string.Empty; 33 | continue; 34 | } 35 | part += c; 36 | } 37 | if (!string.IsNullOrWhiteSpace(name) && !string.IsNullOrWhiteSpace(part)) 38 | { 39 | yield return CreateCookie(name, part); 40 | } 41 | } 42 | private static Cookie CreateCookie(string key, string value) 43 | { 44 | return new Cookie(key.Trim(), WebUtility.UrlDecode(value.Trim())); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Properties/RuntimeInformation.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace vtortola.WebSockets.Properties 5 | { 6 | internal class RuntimeInformation 7 | { 8 | public static readonly bool Is64BitOperatingSystem; 9 | public static readonly bool Is64BitProcess; 10 | public static readonly bool IsMono; 11 | public static readonly bool IsWindows; 12 | 13 | static RuntimeInformation() 14 | { 15 | IsMono = Type.GetType("Mono.Runtime") != null; 16 | 17 | #if !NETSTANDARD && !UAP 18 | switch (Environment.OSVersion.Platform) 19 | { 20 | case PlatformID.Win32S: 21 | case PlatformID.Win32Windows: 22 | case PlatformID.Win32NT: 23 | case PlatformID.Xbox: 24 | case PlatformID.WinCE: 25 | IsWindows = true; 26 | break; 27 | } 28 | 29 | Is64BitProcess = Environment.Is64BitProcess; 30 | Is64BitOperatingSystem = Environment.Is64BitOperatingSystem; 31 | #else 32 | Is64BitProcess = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == Architecture.X64 || 33 | System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture == Architecture.Arm64; 34 | Is64BitOperatingSystem = System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == Architecture.X64 || 35 | System.Runtime.InteropServices.RuntimeInformation.OSArchitecture == Architecture.Arm64; 36 | IsWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(OSPlatform.Windows); 37 | #endif 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /Samples/EchoServer/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Samples/MonoEchoServer/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /Samples/WebSocketClient/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ReflectionHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace vtortola.WebSockets.Tools 5 | { 6 | internal sealed class ReflectionHelper 7 | { 8 | public static readonly bool IsDynamicCompilationSupported; 9 | 10 | static ReflectionHelper() 11 | { 12 | try 13 | { 14 | IsDynamicCompilationSupported = Expression.Lambda>(Expression.Constant(true)).Compile().Invoke(); 15 | } 16 | catch 17 | { 18 | IsDynamicCompilationSupported = false; 19 | } 20 | } 21 | 22 | public static TypeCode GetTypeCode(Type type) 23 | { 24 | if (type == null) return TypeCode.Empty; 25 | if (type == typeof(bool)) return TypeCode.Boolean; 26 | if (type == typeof(char)) return TypeCode.Char; 27 | if (type == typeof(sbyte)) return TypeCode.SByte; 28 | if (type == typeof(byte)) return TypeCode.Byte; 29 | if (type == typeof(short)) return TypeCode.Int16; 30 | if (type == typeof(ushort)) return TypeCode.UInt16; 31 | if (type == typeof(int)) return TypeCode.Int32; 32 | if (type == typeof(uint)) return TypeCode.UInt32; 33 | if (type == typeof(long)) return TypeCode.Int64; 34 | if (type == typeof(ulong)) return TypeCode.UInt64; 35 | if (type == typeof(float)) return TypeCode.Single; 36 | if (type == typeof(double)) return TypeCode.Double; 37 | if (type == typeof(decimal)) return TypeCode.Decimal; 38 | if (type == typeof(DateTime)) return TypeCode.DateTime; 39 | if (type == typeof(string)) return TypeCode.String; 40 | 41 | return TypeCode.Object; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ThreadStaticRandom.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets.Tools 8 | { 9 | internal sealed class ThreadStaticRandom 10 | { 11 | [ThreadStatic] 12 | private static Random instance; 13 | 14 | public static Random Instance 15 | { 16 | get 17 | { 18 | if (instance == null) 19 | { 20 | var seed = unchecked((int)(17 * (DateTime.UtcNow.Ticks % int.MaxValue))); 21 | instance = new Random(seed); 22 | } 23 | return instance; 24 | } 25 | } 26 | 27 | public static int Next() 28 | { 29 | return Instance.Next(); 30 | } 31 | public static int NextNotZero() 32 | { 33 | var next = Instance.Next(); 34 | while (next == 0) 35 | { 36 | next = Instance.Next(); 37 | } 38 | 39 | return next; 40 | } 41 | public static double NextDouble() 42 | { 43 | return Instance.NextDouble(); 44 | } 45 | public static void NextBytes(ArraySegment arraySegment) 46 | { 47 | for (var i = 0; i < arraySegment.Count; i += 4) 48 | { 49 | var next = NextNotZero(); 50 | 51 | arraySegment.Array[arraySegment.Offset + i] = (byte)(next >> 24); 52 | if (i + 1 < arraySegment.Count) 53 | arraySegment.Array[arraySegment.Offset + i + 1] = (byte)(next >> 16); 54 | if (i + 2 < arraySegment.Count) 55 | arraySegment.Array[arraySegment.Offset + i + 2] = (byte)(next >> 8); 56 | if (i + 3 < arraySegment.Count) 57 | arraySegment.Array[arraySegment.Offset + i + 3] = (byte)next; 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/vtortola.WebSockets.Deflate.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45;netstandard1.3 4 | http://vtortola.github.io/WebSocketListener 5 | vtortola 6 | vtortola.WebSockets.Deflate 7 | 4.0.0 8 | Valeriano Tortola 9 | True 10 | ..\WebSocketListener.snk 11 | False 12 | 13 | false 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | $(DefineConstants);NETSTANDARD;NAMED_PIPES_DISABLE;THREADABORTDUMMY 33 | 34 | 35 | $(DefineConstants);UAP;NAMED_PIPES_DISABLE;THREADABORTDUMMY 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /vtortola.WebSockets/ConsoleLogger.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public sealed class ConsoleLogger : ILogger 10 | { 11 | public static ConsoleLogger Instance = new ConsoleLogger(); 12 | 13 | /// 14 | public bool IsDebugEnabled { get; set; } 15 | /// 16 | public bool IsWarningEnabled { get; set; } 17 | /// 18 | public bool IsErrorEnabled { get; set; } 19 | 20 | public ConsoleLogger() 21 | { 22 | this.IsDebugEnabled = true; 23 | this.IsWarningEnabled = true; 24 | this.IsErrorEnabled = true; 25 | } 26 | 27 | /// 28 | public void Debug(string message, Exception error = null) 29 | { 30 | if (this.IsDebugEnabled == false) 31 | return; 32 | 33 | if (string.IsNullOrEmpty(message) == false) 34 | Console.WriteLine(message); 35 | 36 | if (error != null) 37 | Console.WriteLine(error); 38 | } 39 | /// 40 | public void Warning(string message, Exception error = null) 41 | { 42 | if (this.IsWarningEnabled == false) 43 | return; 44 | 45 | if (string.IsNullOrEmpty(message) == false) 46 | System.Diagnostics.Debug.WriteLine("[WARN] " + message); 47 | 48 | if (error != null) 49 | Console.WriteLine(error); 50 | } 51 | /// 52 | public void Error(string message, Exception error = null) 53 | { 54 | if (this.IsErrorEnabled == false) 55 | return; 56 | 57 | if (string.IsNullOrEmpty(message) == false) 58 | System.Diagnostics.Debug.WriteLine("[ERROR] " + message); 59 | 60 | if (error != null) 61 | Console.WriteLine(error); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Async/PingQueue.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Threading; 7 | using vtortola.WebSockets.Tools; 8 | 9 | using PingSubscriptionList = System.Collections.Concurrent.ConcurrentBag; 10 | 11 | namespace vtortola.WebSockets.Async 12 | { 13 | internal class PingQueue : NotificationQueue 14 | { 15 | private readonly ObjectPool listPool; 16 | 17 | /// 18 | public PingQueue(TimeSpan period) : base(period) 19 | { 20 | this.listPool = new ObjectPool(() => new PingSubscriptionList(), 20); 21 | } 22 | 23 | /// 24 | protected override PingSubscriptionList CreateSubscriptionList() 25 | { 26 | return this.listPool.Take(); 27 | } 28 | /// 29 | protected override async void NotifySubscribers(PingSubscriptionList subscriptionList) 30 | { 31 | var webSocket = default(WebSocket); 32 | while (subscriptionList.TryTake(out webSocket)) 33 | { 34 | if (!webSocket.IsConnected) 35 | continue; 36 | 37 | try 38 | { 39 | await webSocket.SendPingAsync(null, 0, 0).ConfigureAwait(false); 40 | } 41 | catch (Exception pingError) 42 | { 43 | if (webSocket.IsConnected && pingError is ObjectDisposedException == false && pingError is ThreadAbortException == false) 44 | DebugLogger.Instance.Warning("An error occurred while sending ping.", pingError); 45 | } 46 | 47 | if (webSocket.IsConnected && this.IsDisposed == false) 48 | this.GetSubscriptionList().Add(webSocket); 49 | } 50 | this.listPool.Return(subscriptionList); 51 | } 52 | } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketHttpRequest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Net; 5 | using vtortola.WebSockets.Http; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public sealed class WebSocketHttpRequest : IHttpRequest 10 | { 11 | public static readonly IPEndPoint NoAddress = new IPEndPoint(IPAddress.None, 0); 12 | 13 | public EndPoint LocalEndPoint { get; internal set; } 14 | public EndPoint RemoteEndPoint { get; internal set; } 15 | public Uri RequestUri { get; internal set; } 16 | public Version HttpVersion { get; internal set; } 17 | public bool IsSecure { get; internal set; } 18 | public CookieCollection Cookies { get; } 19 | public Headers Headers { get; } 20 | public IDictionary Items { get; } 21 | public HttpRequestDirection Direction { get; } 22 | 23 | public IReadOnlyList WebSocketExtensions { get; private set; } 24 | 25 | public WebSocketHttpRequest(HttpRequestDirection direction) 26 | { 27 | this.Headers = new Headers(); 28 | this.Cookies = new CookieCollection(); 29 | this.Items = new Dictionary(); 30 | this.LocalEndPoint = NoAddress; 31 | this.RemoteEndPoint = NoAddress; 32 | this.Direction = direction; 33 | } 34 | 35 | internal void SetExtensions(List extensions) 36 | { 37 | if (extensions == null) throw new ArgumentNullException(nameof(extensions)); 38 | 39 | WebSocketExtensions = new ReadOnlyCollection(extensions); 40 | } 41 | 42 | /// 43 | public override string ToString() 44 | { 45 | if (this.RequestUri != null) 46 | return this.RequestUri.ToString(); 47 | else 48 | return $"{this.LocalEndPoint}->{this.RemoteEndPoint}"; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /vtortola.WebSockets/DefaultBufferManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using vtortola.WebSockets.Tools; 3 | 4 | namespace vtortola.WebSockets 5 | { 6 | internal sealed class DefaultBufferManager : BufferManager 7 | { 8 | private readonly ObjectPool smallPool; 9 | private readonly ObjectPool largePool; 10 | 11 | public override int SmallBufferSize { get; } 12 | public override int LargeBufferSize { get; } 13 | 14 | public DefaultBufferManager(int smallBufferSize, int smallPoolSizeLimit, int largeBufferSize, int largePoolSizeLimit) 15 | { 16 | this.SmallBufferSize = smallBufferSize; 17 | this.LargeBufferSize = largeBufferSize; 18 | this.smallPool = new ObjectPool(() => new byte[smallBufferSize], smallPoolSizeLimit); 19 | this.largePool = new ObjectPool(() => new byte[largeBufferSize], largePoolSizeLimit); 20 | 21 | } 22 | 23 | /// 24 | public override void Clear() 25 | { 26 | this.smallPool.Clear(); 27 | this.largePool.Clear(); 28 | } 29 | /// 30 | public override void ReturnBuffer(byte[] buffer) 31 | { 32 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 33 | 34 | if (buffer.Length >= this.LargeBufferSize) 35 | this.largePool.Return(buffer); 36 | else if (buffer.Length >= this.SmallBufferSize) 37 | this.smallPool.Return(buffer); 38 | else 39 | throw new ArgumentException("Length of buffer does not match the pool's buffer length property.", nameof(buffer)); 40 | } 41 | /// 42 | public override byte[] TakeBuffer(int bufferSize) 43 | { 44 | if (bufferSize < 0 || bufferSize > this.LargeBufferSize) throw new ArgumentOutOfRangeException(nameof(bufferSize)); 45 | 46 | if (bufferSize >= this.SmallBufferSize) 47 | return this.largePool.Take(); 48 | else 49 | return this.smallPool.Take(); 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/vtortola.WebSockets.Rfc6455.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45;netstandard1.3 4 | http://vtortola.github.io/WebSocketListener 5 | vtortola 6 | vtortola.WebSockets.Rfc6455 7 | 4.0.0 8 | Valeriano Tortola 9 | True 10 | ..\WebSocketListener.snk 11 | False 12 | 13 | 4.0.0.0 14 | 4.0.0.0 15 | true 16 | false 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | $(DefineConstants);NETSTANDARD;NAMED_PIPES_DISABLE;THREADABORTDUMMY 36 | 37 | 38 | $(DefineConstants);UAP;NAMED_PIPES_DISABLE;THREADABORTDUMMY 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/CombinedStream.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace vtortola.WebSockets.UnitTests 4 | { 5 | public sealed class CombinedStream : Stream 6 | { 7 | private readonly Stream readStream; 8 | private readonly Stream writeStream; 9 | /// 10 | public override bool CanRead => this.readStream.CanRead; 11 | /// 12 | public override bool CanSeek => this.readStream.CanSeek; 13 | /// 14 | public override bool CanWrite => this.writeStream.CanWrite; 15 | /// 16 | public override long Length => this.readStream.Length; 17 | /// 18 | public override long Position { get => this.readStream.Position; set => this.readStream.Position = value; } 19 | 20 | public CombinedStream(Stream readStream, Stream writeStream) 21 | { 22 | this.readStream = readStream; 23 | this.writeStream = writeStream; 24 | } 25 | 26 | /// 27 | public override void Flush() 28 | { 29 | this.writeStream.Flush(); 30 | } 31 | /// 32 | public override long Seek(long offset, SeekOrigin origin) 33 | { 34 | return this.readStream.Seek(offset, origin); 35 | } 36 | /// 37 | public override void SetLength(long value) 38 | { 39 | this.readStream.SetLength(value); 40 | } 41 | /// 42 | public override int Read(byte[] buffer, int offset, int count) 43 | { 44 | return this.readStream.Read(buffer, offset, count); 45 | } 46 | /// 47 | public override void Write(byte[] buffer, int offset, int count) 48 | { 49 | this.writeStream.Write(buffer, offset, count); 50 | } 51 | 52 | /// 53 | protected override void Dispose(bool disposing) 54 | { 55 | this.readStream.Dispose(); 56 | this.writeStream.Dispose(); 57 | base.Dispose(disposing); 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /vtortola.WebSockets/DebugLogger.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public sealed class DebugLogger : ILogger 10 | { 11 | public static DebugLogger Instance = new DebugLogger(); 12 | 13 | /// 14 | public bool IsDebugEnabled { get; set; } 15 | /// 16 | public bool IsWarningEnabled { get; set; } 17 | /// 18 | public bool IsErrorEnabled { get; set; } 19 | 20 | public DebugLogger() 21 | { 22 | this.IsDebugEnabled = true; 23 | this.IsWarningEnabled = true; 24 | this.IsErrorEnabled = true; 25 | } 26 | 27 | /// 28 | public void Debug(string message, Exception error = null) 29 | { 30 | if (this.IsDebugEnabled == false) 31 | return; 32 | 33 | if (string.IsNullOrEmpty(message) == false) 34 | System.Diagnostics.Debug.WriteLine(message); 35 | 36 | if (error != null) 37 | { 38 | System.Diagnostics.Debug.WriteLine(error); 39 | } 40 | } 41 | /// 42 | public void Warning(string message, Exception error = null) 43 | { 44 | if (this.IsWarningEnabled == false) 45 | return; 46 | 47 | if (string.IsNullOrEmpty(message) == false) 48 | System.Diagnostics.Debug.WriteLine("[WARN] " + message); 49 | 50 | if (error != null) 51 | { 52 | System.Diagnostics.Debug.WriteLine(error); 53 | } 54 | } 55 | /// 56 | public void Error(string message, Exception error = null) 57 | { 58 | if (this.IsErrorEnabled == false) 59 | return; 60 | 61 | if (string.IsNullOrEmpty(message) == false) 62 | System.Diagnostics.Debug.WriteLine("[ERROR] " + message); 63 | 64 | if (error != null) 65 | { 66 | System.Diagnostics.Debug.WriteLine(error); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/ResponseHeader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | namespace vtortola.WebSockets.Http 6 | { 7 | public enum ResponseHeader 8 | { 9 | [Header("Cache-Control")] 10 | CacheControl = 0, 11 | Connection = 1, 12 | [Header(Flags = HeaderFlags.Singleton)] 13 | Date = 2, 14 | [Header("Keep-Alive")] 15 | KeepAlive = 3, 16 | Pragma = 4, 17 | Trailer = 5, 18 | [Header("Transfer-Encoding")] 19 | TransferEncoding = 6, 20 | Upgrade = 7, 21 | Via = 8, 22 | Warning = 9, 23 | Allow = 10, 24 | [Header("Content-Length", Flags = HeaderFlags.Singleton)] 25 | ContentLength = 11, 26 | [Header("Content-Type", Flags = HeaderFlags.Singleton)] 27 | ContentType = 12, 28 | [Header("Content-Encoding")] 29 | ContentEncoding = 13, 30 | [Header("Content-Language")] 31 | ContentLanguage = 14, 32 | [Header("Content-Location")] 33 | ContentLocation = 15, 34 | [Header("Content-MD5")] 35 | ContentMd5 = 16, 36 | [Header("Content-Range")] 37 | ContentRange = 17, 38 | Expires = 18, 39 | [Header("Last-Modified")] 40 | LastModified = 19, 41 | [Header("Accept-Ranges")] 42 | AcceptRanges = 20, 43 | Age = 21, 44 | ETag = 22, 45 | [Header(Flags = HeaderFlags.Singleton)] 46 | Location = 23, 47 | [Header("Proxy-Authenticate")] 48 | ProxyAuthenticate = 24, 49 | [Header("Retry-After")] 50 | RetryAfter = 25, 51 | Server = 26, 52 | [Header("Set-Cookie", Flags = HeaderFlags.Singleton)] 53 | SetCookie = 27, 54 | Vary = 28, 55 | [Header("WWW-Authenticate")] 56 | WwwAuthenticate = 29, 57 | 58 | // extensions to HttpRequestHeaders 59 | [Header("Content-Disposition", Flags = HeaderFlags.Singleton)] 60 | ContentDisposition = 30, 61 | [Header("Sec-WebSocket-Extensions")] 62 | WebSocketExtensions = 31, 63 | [Header("Sec-WebSocket-Protocol")] 64 | WebSocketProtocol = 32, 65 | [Header("Sec-WebSocket-Accept")] 66 | WebSocketAccept = 33, 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/HttpHelperTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using vtortola.WebSockets.Tools; 3 | using Xunit; 4 | 5 | namespace vtortola.WebSockets.UnitTests 6 | { 7 | public class HttpHelperTest 8 | { 9 | [Theory, 10 | InlineData("HTTP/1.1 101 Web Socket Protocol Handshake", HttpStatusCode.SwitchingProtocols, "Web Socket Protocol Handshake"), 11 | InlineData("HTTP/1.0 200 OK", HttpStatusCode.OK, "OK"), 12 | InlineData("HTTP/1.1 200 OK", HttpStatusCode.OK, "OK"), 13 | InlineData("HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 14 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 15 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 16 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 17 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 18 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 19 | InlineData(" HTTP/1.1 404 Not Found", HttpStatusCode.NotFound, "Not Found"), 20 | InlineData(" HTTP/1.1 404 Not Found ", HttpStatusCode.NotFound, "Not Found"), 21 | InlineData(" HTTP/1.1 404 Not Found ", HttpStatusCode.NotFound, "Not Found"), 22 | InlineData("HTTP/1.1 200", HttpStatusCode.OK, ""), 23 | InlineData("HTTP/1.1 ", (HttpStatusCode)0, "Missing Response Code"), 24 | InlineData("HTTP/1.1 WRONG", (HttpStatusCode)0, "Missing Response Code"), 25 | InlineData("HTTP/1.1", (HttpStatusCode)0, "Missing Response Code"), 26 | InlineData("HTTP/1", (HttpStatusCode)0, "Malformed Response"), 27 | InlineData("", (HttpStatusCode)0, "Malformed Response"), 28 | InlineData("200 OK", (HttpStatusCode)0, "Malformed Response")] 29 | public void TryParseAndAddRequestHeaderTest(string headline, HttpStatusCode statusCode, string description) 30 | { 31 | var actualStatusCode = default(HttpStatusCode); 32 | var actualDescription = default(string); 33 | HttpHelper.TryParseHttpResponse(headline, out actualStatusCode, out actualDescription); 34 | 35 | Assert.Equal(statusCode, actualStatusCode); 36 | Assert.Equal(description, actualDescription); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ExceptionExtentions.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Reflection; 7 | using System.Runtime.ExceptionServices; 8 | 9 | namespace vtortola.WebSockets.Tools 10 | { 11 | internal static class ExceptionExtensions 12 | { 13 | #if !NETSTANDARD && !UAP 14 | private static readonly Action PreserveStackTrace; 15 | 16 | static ExceptionExtensions() 17 | { 18 | var internalPreserveStackTrace = typeof(Exception).GetMethod("InternalPreserveStackTrace", BindingFlags.Instance | BindingFlags.NonPublic); 19 | if (internalPreserveStackTrace != null) PreserveStackTrace = (Action)Delegate.CreateDelegate(typeof(Action), internalPreserveStackTrace, false); 20 | } 21 | #endif 22 | 23 | public static Exception Unwrap(this Exception exception) 24 | { 25 | while (true) 26 | { 27 | if (exception == null) return null; 28 | 29 | var aggregateException = exception as AggregateException; 30 | if (aggregateException != null && aggregateException.InnerExceptions.Count == 1) 31 | { 32 | exception = aggregateException.InnerExceptions[0]; 33 | continue; 34 | } 35 | 36 | var targetInvocationException = exception as TargetInvocationException; 37 | if (targetInvocationException != null) 38 | { 39 | exception = targetInvocationException.InnerException; 40 | continue; 41 | } 42 | 43 | return exception; 44 | } 45 | } 46 | 47 | public static void Rethrow(this Exception exception) 48 | { 49 | if (exception == null) throw new ArgumentNullException(nameof(exception), "exception != null"); 50 | 51 | #if !NETSTANDARD && !UAP 52 | if (PreserveStackTrace != null) 53 | { 54 | PreserveStackTrace(exception); 55 | throw exception; 56 | } 57 | #endif 58 | var exceptionDispatchInfo = ExceptionDispatchInfo.Capture(exception); 59 | exceptionDispatchInfo.Throw(); 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/DummyNetworkConnection.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Net; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using vtortola.WebSockets.Transports; 6 | 7 | namespace vtortola.WebSockets.UnitTests 8 | { 9 | public sealed class DummyNetworkConnection : NetworkConnection 10 | { 11 | private readonly Stream readStream; 12 | private readonly Stream writeStream; 13 | /// 14 | public override EndPoint LocalEndPoint => new IPEndPoint(IPAddress.Parse("127.0.0.1"), 1); 15 | /// 16 | public override EndPoint RemoteEndPoint => new IPEndPoint(IPAddress.Parse("127.0.0.1"), 2); 17 | 18 | public DummyNetworkConnection(Stream readStream, Stream writeStream) 19 | { 20 | this.readStream = readStream; 21 | this.writeStream = writeStream; 22 | } 23 | 24 | /// 25 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 26 | { 27 | return this.readStream.ReadAsync(buffer, offset, count, cancellationToken); 28 | } 29 | /// 30 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 31 | { 32 | return this.writeStream.WriteAsync(buffer, offset, count, cancellationToken); 33 | } 34 | /// 35 | public override Task FlushAsync(CancellationToken cancellationToken) 36 | { 37 | return this.writeStream.FlushAsync(cancellationToken); 38 | } 39 | /// 40 | public override Task CloseAsync() 41 | { 42 | return Task.FromResult(true); 43 | } 44 | /// 45 | public override void Dispose(bool disposed) 46 | { 47 | this.readStream.Dispose(); 48 | this.writeStream.Dispose(); 49 | } 50 | /// 51 | public override Stream AsStream() 52 | { 53 | return new CombinedStream(this.readStream, this.writeStream); 54 | } 55 | /// 56 | public override string ToString() 57 | { 58 | return this.GetType().Name; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketHttpResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Net; 5 | using vtortola.WebSockets.Http; 6 | 7 | namespace vtortola.WebSockets 8 | { 9 | public sealed class WebSocketHttpResponse 10 | { 11 | public readonly CookieCollection Cookies; 12 | public readonly Headers Headers; 13 | public HttpStatusCode Status; 14 | public string StatusDescription; 15 | public readonly List WebSocketExtensions; 16 | 17 | public WebSocketHttpResponse() 18 | { 19 | this.Headers = new Headers(); 20 | this.Cookies = new CookieCollection(); 21 | this.WebSocketExtensions = new List(); 22 | this.Status = HttpStatusCode.SwitchingProtocols; 23 | this.StatusDescription = "Web Socket Protocol Handshake"; 24 | } 25 | public void ThrowIfInvalid(string computedHandshake) 26 | { 27 | if (computedHandshake == null) throw new ArgumentNullException(nameof(computedHandshake)); 28 | 29 | var upgrade = this.Headers[ResponseHeader.Upgrade]; 30 | if (string.Equals("websocket", upgrade, StringComparison.OrdinalIgnoreCase) == false) 31 | throw new WebSocketException($"Missing or wrong {Headers.GetHeaderName(ResponseHeader.Upgrade)} header in response."); 32 | 33 | if (this.Headers.GetValues(ResponseHeader.Connection).Contains("Upgrade", StringComparison.OrdinalIgnoreCase) == false) 34 | throw new WebSocketException($"Missing or wrong {Headers.GetHeaderName(ResponseHeader.Connection)} header in response."); 35 | 36 | var acceptResult = this.Headers[ResponseHeader.WebSocketAccept]; 37 | if (string.Equals(computedHandshake, acceptResult, StringComparison.OrdinalIgnoreCase) == false) 38 | throw new WebSocketException( 39 | $"Missing or wrong {Headers.GetHeaderName(ResponseHeader.WebSocketAccept)} header in response."); 40 | } 41 | 42 | /// 43 | public override string ToString() 44 | { 45 | return $"{this.Status} {this.StatusDescription}"; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/Tcp/TcpListener.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using vtortola.WebSockets.Transports.Sockets; 9 | 10 | #pragma warning disable 420 11 | 12 | namespace vtortola.WebSockets.Transports.Tcp 13 | { 14 | internal sealed class TcpListener : SocketListener 15 | { 16 | private readonly TcpTransport transport; 17 | 18 | /// 19 | public TcpListener(TcpTransport transport, EndPoint[] endPointsToListen, WebSocketListenerOptions options) 20 | : base(transport, endPointsToListen, ProtocolType.Tcp, options) 21 | { 22 | if (transport == null) throw new ArgumentNullException(nameof(transport)); 23 | if (endPointsToListen == null) throw new ArgumentNullException(nameof(endPointsToListen)); 24 | if (options == null) throw new ArgumentNullException(nameof(options)); 25 | 26 | this.transport = transport; 27 | } 28 | 29 | /// 30 | protected override NetworkConnection CreateConnection(Socket socket, EndPoint localEndPoint) 31 | { 32 | if (this.transport.LingerState != null) 33 | socket.LingerState = this.transport.LingerState; 34 | socket.NoDelay = this.transport.NoDelay; 35 | socket.ReceiveBufferSize = this.transport.ReceiveBufferSize; 36 | socket.ReceiveTimeout = (int)this.transport.ReceiveTimeout.TotalMilliseconds + 1; 37 | socket.SendBufferSize = this.transport.SendBufferSize; 38 | socket.SendTimeout = (int)this.transport.SendTimeout.TotalMilliseconds + 1; 39 | #if !NETSTANDARD && !UAP 40 | if (this.transport.IpProtectionLevel != IPProtectionLevel.Unspecified) 41 | socket.SetIPProtectionLevel(this.transport.IpProtectionLevel); 42 | socket.UseOnlyOverlappedIO = this.transport.IsAsync; 43 | #endif 44 | return new TcpConnection(socket, localEndPoint); 45 | } 46 | 47 | /// 48 | public override string ToString() 49 | { 50 | // ReSharper disable once CoVariantArrayConversion 51 | return $"{nameof(TcpListener)}, {string.Join(", ", this.LocalEndpoints)}"; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/NamedPipes/NamedPipeTransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | #if !NAMED_PIPES_DISABLE 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO.Pipes; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace vtortola.WebSockets.Transports.NamedPipes 13 | { 14 | public sealed class NamedPipeTransport : WebSocketTransport 15 | { 16 | public const int DEFAULT_SEND_BUFFER_SIZE = 1024; 17 | public const int DEFAULT_RECEIVE_BUFFER_SIZE = 1024; 18 | 19 | private static readonly string[] SupportedSchemes = { "pipe" }; 20 | 21 | public int MaxConnections { get; set; } = NamedPipeServerStream.MaxAllowedServerInstances; 22 | public int SendBufferSize { get; set; } = DEFAULT_SEND_BUFFER_SIZE; 23 | public int ReceiveBufferSize { get; set; } = DEFAULT_RECEIVE_BUFFER_SIZE; 24 | 25 | /// 26 | public override IReadOnlyCollection Schemes => SupportedSchemes; 27 | /// 28 | internal override Task ListenAsync(Uri address, WebSocketListenerOptions options) 29 | { 30 | return Task.FromResult((Listener)new NamedPipeListener(this, address, options)); 31 | } 32 | /// 33 | internal override Task ConnectAsync(Uri address, WebSocketListenerOptions options, CancellationToken cancellation) 34 | { 35 | if (address == null) throw new ArgumentNullException(nameof(address)); 36 | if (options == null) throw new ArgumentNullException(nameof(options)); 37 | 38 | var pipeName = address.GetComponents(UriComponents.Host | UriComponents.Path, UriFormat.SafeUnescaped); 39 | var clientPipeStream = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.Asynchronous); 40 | 41 | clientPipeStream.Connect(); 42 | 43 | return Task.FromResult((NetworkConnection)new NamedPipeConnection(clientPipeStream, pipeName)); 44 | } 45 | /// 46 | internal override bool ShouldUseSsl(Uri requestUri) 47 | { 48 | if (requestUri == null) throw new ArgumentNullException(nameof(requestUri)); 49 | 50 | return false; 51 | } 52 | } 53 | } 54 | #endif 55 | -------------------------------------------------------------------------------- /vtortola.WebSockets/vtortola.WebSockets.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net45;netstandard1.3 4 | 5 | 6 | http://vtortola.github.io/WebSocketListener 7 | vtortola 8 | vtortola.WebSockets 9 | 4.0.0 10 | Valeriano Tortola, Denis Zykov 11 | https://opensource.org/licenses/MIT 12 | https://github.com/vtortola/WebSocketListener 13 | https://github.com/vtortola/WebSocketListener 14 | git 15 | websockets HTML5 realtime streaming sockets server async asynchronous 16 | True 17 | ..\WebSocketListener.snk 18 | False 19 | 4.0.0.0 20 | 4.0.0.0 21 | false 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | $(DefineConstants);NETSTANDARD;NAMED_PIPES_DISABLE;THREADABORTDUMMY 38 | 39 | 40 | $(DefineConstants);UAP;NAMED_PIPES_DISABLE;THREADABORTDUMMY 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/WebSocketDeflateReadStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using JetBrains.Annotations; 6 | using vtortola.WebSockets.Tools; 7 | 8 | namespace vtortola.WebSockets.Deflate 9 | { 10 | public sealed class WebSocketDeflateReadStream : WebSocketMessageReadStream 11 | { 12 | private const int STATE_OPEN = 0; 13 | private const int STATE_CLOSED = 1; 14 | private const int STATE_DISPOSED = 2; 15 | 16 | private readonly WebSocketMessageReadStream innerStream; 17 | readonly DeflateStream deflateStream; 18 | private volatile int state = STATE_OPEN; 19 | 20 | public override WebSocketMessageType MessageType => this.innerStream.MessageType; 21 | public override WebSocketExtensionFlags Flags => this.innerStream.Flags; 22 | 23 | /// 24 | internal override WebSocketListenerOptions Options => this.innerStream.Options; 25 | 26 | public WebSocketDeflateReadStream([NotNull] WebSocketMessageReadStream innerStream) 27 | { 28 | if (innerStream == null) throw new ArgumentNullException(nameof(innerStream)); 29 | 30 | this.innerStream = innerStream; 31 | this.deflateStream = new DeflateStream(innerStream, CompressionMode.Decompress, leaveOpen: true); 32 | } 33 | 34 | /// 35 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 36 | { 37 | return this.deflateStream.ReadAsync(buffer, offset, count, cancellationToken); 38 | } 39 | 40 | /// 41 | public override Task CloseAsync() 42 | { 43 | if (Interlocked.CompareExchange(ref this.state, STATE_CLOSED, STATE_OPEN) != STATE_OPEN) 44 | { 45 | return TaskHelper.CompletedTask; 46 | } 47 | 48 | return this.innerStream.CloseAsync(); 49 | } 50 | 51 | /// 52 | protected override void Dispose(bool disposing) 53 | { 54 | if (Interlocked.Exchange(ref this.state, STATE_DISPOSED) == STATE_DISPOSED) 55 | { 56 | return; 57 | } 58 | 59 | SafeEnd.Dispose(this.deflateStream); 60 | SafeEnd.Dispose(this.innerStream); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/TestLogger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit.Abstractions; 3 | 4 | namespace vtortola.WebSockets.UnitTests 5 | { 6 | public class TestLogger : ILogger 7 | { 8 | private readonly ITestOutputHelper output; 9 | /// 10 | public bool IsDebugEnabled { get; set; } 11 | /// 12 | public bool IsWarningEnabled { get; set; } 13 | /// 14 | public bool IsErrorEnabled { get; set; } 15 | 16 | public TestLogger(TestLogger other) 17 | : this(other.output) 18 | { 19 | if (other == null) throw new ArgumentNullException(nameof(other)); 20 | } 21 | public TestLogger(ITestOutputHelper output) 22 | { 23 | if (output == null) throw new ArgumentNullException(nameof(output)); 24 | 25 | this.IsDebugEnabled = true; 26 | this.IsWarningEnabled = true; 27 | this.IsErrorEnabled = true; 28 | 29 | this.output = output; 30 | } 31 | 32 | /// 33 | public void Debug(string message, Exception error = null) 34 | { 35 | if (!this.IsDebugEnabled) 36 | return; 37 | 38 | if (!string.IsNullOrEmpty(message)) 39 | this.WriteLine(message); 40 | 41 | if (error != null) 42 | this.WriteLine(error.ToString()); 43 | } 44 | /// 45 | public void Warning(string message, Exception error = null) 46 | { 47 | if (!this.IsWarningEnabled) 48 | return; 49 | 50 | if (!string.IsNullOrEmpty(message)) 51 | this.WriteLine("[WARN] " + message); 52 | if (error != null) 53 | this.WriteLine(error.ToString()); 54 | } 55 | /// 56 | public void Error(string message, Exception error = null) 57 | { 58 | if (!this.IsErrorEnabled) 59 | return; 60 | 61 | if (!string.IsNullOrEmpty("[ERROR] " + message)) 62 | this.WriteLine(message); 63 | if (error != null) 64 | this.WriteLine(error.ToString()); 65 | } 66 | private void WriteLine(string message) 67 | { 68 | this.output.WriteLine(message); 69 | System.Diagnostics.Debug.WriteLine(message); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/UnixSockets/UnixSocketConnection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using vtortola.WebSockets.Transports.Sockets; 9 | 10 | namespace vtortola.WebSockets.Transports.UnixSockets 11 | { 12 | internal sealed class UnixSocketConnection : SocketConnection 13 | { 14 | private readonly Socket socket; 15 | 16 | public int Available => this.socket.Available; 17 | public bool IsConnected => this.socket.Connected; 18 | 19 | public LingerOption LingerState 20 | { 21 | get { return this.socket.LingerState; } 22 | set { this.socket.LingerState = value; } 23 | } 24 | public int ReceiveBufferSize 25 | { 26 | get { return this.socket.ReceiveBufferSize; } 27 | set { this.socket.ReceiveBufferSize = value; } 28 | } 29 | public int ReceiveTimeout 30 | { 31 | get { return this.socket.ReceiveTimeout; } 32 | set { this.socket.ReceiveTimeout = value; } 33 | } 34 | public int SendBufferSize 35 | { 36 | get { return this.socket.SendBufferSize; } 37 | set { this.socket.SendBufferSize = value; } 38 | } 39 | public int SendTimeout 40 | { 41 | get { return this.socket.SendTimeout; } 42 | set { this.socket.SendTimeout = value; } 43 | } 44 | 45 | /// 46 | public UnixSocketConnection(Socket socket, EndPoint localEndPoint) : base(socket, localEndPoint) 47 | { 48 | if (socket == null) throw new ArgumentNullException(nameof(socket)); 49 | 50 | this.socket = socket; 51 | } 52 | 53 | /// 54 | public override string ToString() 55 | { 56 | // ReSharper disable HeapView.BoxingAllocation 57 | return $"{nameof(UnixSocketConnection)}, local: {this.LocalEndPoint}, remote: {this.RemoteEndPoint}, " + 58 | $"connected: {this.IsConnected}, available (bytes){this.Available}" + 59 | $"receive buffer: {this.ReceiveBufferSize}, receive timeout: {TimeSpan.FromMilliseconds(this.ReceiveTimeout)}, " + 60 | $"send buffer: {this.SendBufferSize}, send timeout: {TimeSpan.FromMilliseconds(this.SendTimeout)}"; 61 | // ReSharper restore HeapView.BoxingAllocation 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | os: Visual Studio 2017 2 | 3 | version: 3.0.0.{build} 4 | 5 | configuration: 6 | - Debug 7 | - Release 8 | 9 | platform: Any CPU 10 | 11 | init: 12 | - ps: $Env:LABEL = "CI" + $Env:APPVEYOR_BUILD_NUMBER.PadLeft(5, "0") 13 | 14 | install: 15 | - ps: Start-FileDownload 'https://download.microsoft.com/download/0/A/3/0A372822-205D-4A86-BFA7-084D2CBE9EDF/DotNetCore.1.0.1-SDK.1.0.0.Preview2-003133-x64.exe' 16 | - cmd: DotNetCore.1.0.1-SDK.1.0.0.Preview2-003133-x64 /quiet 17 | 18 | environment: 19 | PATH: $(PATH);$(PROGRAMFILES)\dotnet\ 20 | 21 | build_script: 22 | # Restore NuGet 23 | - appveyor-retry dotnet restore .\deniszykov.WebSocketListener\deniszykov.WebSocketListener.csproj -v Minimal 24 | - appveyor-retry dotnet restore .\vtortola.WebSockets\vtortola.WebSockets.csproj -v Minimal 25 | - appveyor-retry dotnet restore .\vtortola.WebSockets.Deflate\vtortola.WebSockets.Deflate.csproj -v Minimal 26 | - appveyor-retry dotnet restore .\vtortola.WebSockets.Rfc6455\vtortola.WebSockets.Rfc6455.csproj -v Minimal 27 | 28 | - appveyor-retry dotnet restore .\Tests\WebSocketListener.UnitTests\vtortola.WebSockets.UnitTests.csproj -v Minimal 29 | 30 | # Build dll (Core) 31 | - dotnet build .\deniszykov.WebSocketListener\deniszykov.WebSocketListener.csproj -c %CONFIGURATION% 32 | - dotnet build .\vtortola.WebSockets\vtortola.WebSockets.csproj -c %CONFIGURATION% 33 | - dotnet build .\vtortola.WebSockets.Deflate\vtortola.WebSockets.Deflate.csproj -c %CONFIGURATION% 34 | - dotnet build .\vtortola.WebSockets.Rfc6455\vtortola.WebSockets.Rfc6455.csproj -c %CONFIGURATION% 35 | 36 | # Build UnitTests 37 | - dotnet build .\Tests\WebSocketListener.UnitTests\vtortola.WebSockets.UnitTests.csproj -c %CONFIGURATION% 38 | 39 | 40 | # Pack NuGets 41 | - dotnet pack -c %CONFIGURATION% --no-build --version-suffix %LABEL% -o .\artifacts .\deniszykov.WebSocketListener\deniszykov.WebSocketListener.csproj 42 | - dotnet pack -c %CONFIGURATION% --no-build --version-suffix %LABEL% -o .\artifacts .\vtortola.WebSockets\vtortola.WebSockets.csproj 43 | - dotnet pack -c %CONFIGURATION% --no-build --version-suffix %LABEL% -o .\artifacts .\vtortola.WebSockets.Deflate\vtortola.WebSockets.Deflate.csproj 44 | - dotnet pack -c %CONFIGURATION% --no-build --version-suffix %LABEL% -o .\artifacts .\vtortola.WebSockets.Rfc6455\vtortola.WebSockets.Rfc6455.csproj 45 | 46 | # Run tests 47 | test_script: 48 | - dotnet test -c %CONFIGURATION% --no-build .\Tests\WebSocketListener.UnitTests\vtortola.WebSockets.UnitTests.csproj 49 | 50 | artifacts: 51 | - path: "**/artifacts/**/*.*" 52 | 53 | cache: 54 | - packages -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/Headers.Enumerator.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | 9 | namespace vtortola.WebSockets.Http 10 | { 11 | partial class Headers 12 | { 13 | public class Enumerator : IEnumerator> 14 | { 15 | private readonly Headers headers; 16 | private Dictionary.Enumerator customHeadersEnumerator; 17 | private int knownHeadersIndex; 18 | private bool useCustomHeadersEnumerator; 19 | private int version; 20 | 21 | public KeyValuePair Current { get; private set; } 22 | object IEnumerator.Current => this.Current; 23 | 24 | public Enumerator(Headers headers) 25 | { 26 | if (headers == null) throw new ArgumentNullException(nameof(headers), "headersDictionary != null"); 27 | 28 | this.headers = headers; 29 | this.Reset(); 30 | } 31 | 32 | public bool MoveNext() 33 | { 34 | if (this.version != this.headers.version) throw new InvalidOperationException("Collection was modified. Enumeration operation may not execute."); 35 | 36 | var knownHeaders = this.headers.knownHeaders; 37 | if (knownHeaders != null && this.knownHeadersIndex < knownHeaders.Length) 38 | { 39 | for (var i = this.knownHeadersIndex; i < knownHeaders.Length; i++) 40 | { 41 | this.knownHeadersIndex++; 42 | var value = knownHeaders[i]; 43 | if (value.IsEmpty) continue; 44 | 45 | this.Current = new KeyValuePair(GetKnownHeaderName(i), value); 46 | return true; 47 | } 48 | } 49 | 50 | if (!this.useCustomHeadersEnumerator) return false; 51 | 52 | while (this.customHeadersEnumerator.MoveNext()) 53 | { 54 | var value = this.customHeadersEnumerator.Current; 55 | if (value.Value.IsEmpty) continue; 56 | 57 | this.Current = value; 58 | return true; 59 | } 60 | 61 | return false; 62 | } 63 | public void Reset() 64 | { 65 | if (this.useCustomHeadersEnumerator) this.customHeadersEnumerator.Dispose(); 66 | 67 | this.knownHeadersIndex = 0; 68 | this.useCustomHeadersEnumerator = this.headers.customHeaders != null; 69 | this.customHeadersEnumerator = this.headers.customHeaders?.GetEnumerator() ?? default(Dictionary.Enumerator); 70 | this.version = this.headers.version; 71 | } 72 | 73 | public void Dispose() 74 | { 75 | if (this.useCustomHeadersEnumerator) this.customHeadersEnumerator.Dispose(); 76 | } 77 | } 78 | } 79 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/ObjectPool.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Concurrent; 7 | using System.Threading; 8 | using JetBrains.Annotations; 9 | 10 | #pragma warning disable 420 11 | 12 | namespace vtortola.WebSockets.Tools 13 | { 14 | internal sealed class ObjectPool where T : class 15 | { 16 | public readonly Func ConstructFn; 17 | 18 | private readonly ConcurrentQueue items; 19 | 20 | private readonly int sizeLimit; 21 | private volatile int count; 22 | public readonly Action ReturnFunction; 23 | public readonly Func TakeFunction; 24 | 25 | public ObjectPool([NotNull] Func constructor, int poolSizeLimit = 1000) 26 | { 27 | if (constructor == null) throw new ArgumentNullException(nameof(constructor), "constructor != null"); 28 | if (poolSizeLimit <= 0) throw new ArgumentOutOfRangeException(nameof(poolSizeLimit), "poolLimit > 0"); 29 | 30 | this.sizeLimit = poolSizeLimit; 31 | this.items = new ConcurrentQueue(); 32 | this.ConstructFn = constructor; 33 | this.TakeFunction = this.Take; 34 | this.ReturnFunction = this.Return; 35 | } 36 | 37 | public void Clear() 38 | { 39 | var item = default(T); 40 | while (this.items.TryDequeue(out item)) 41 | { 42 | Interlocked.Decrement(ref this.count); 43 | /* no body */ 44 | } 45 | 46 | } 47 | 48 | public T Take() 49 | { 50 | var item = default(T); 51 | if (this.TryTake(out item) == false) 52 | { 53 | item = this.ConstructFn(); 54 | } 55 | 56 | return item; 57 | } 58 | public bool TryTake(out T item) 59 | { 60 | while (this.items.TryDequeue(out item)) 61 | { 62 | Interlocked.Decrement(ref this.count); 63 | return true; 64 | } 65 | 66 | item = default(T); 67 | return false; 68 | } 69 | public void Return(T item) 70 | { 71 | if (item == null) throw new ArgumentNullException(nameof(item)); 72 | 73 | this.TryReturn(item); 74 | } 75 | public bool TryReturn(T item) 76 | { 77 | if (item == null) throw new ArgumentNullException(nameof(item)); 78 | 79 | if (this.count >= this.sizeLimit) return false; 80 | 81 | this.items.Enqueue(item); 82 | Interlocked.Increment(ref this.count); 83 | return true; 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/HttpHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Globalization; 7 | using System.Net; 8 | using JetBrains.Annotations; 9 | 10 | namespace vtortola.WebSockets.Tools 11 | { 12 | internal static class HttpHelper 13 | { 14 | public static readonly string WebSocketHttp10Version = "HTTP/1.0"; 15 | public static readonly string WebSocketHttp11Version = "HTTP/1.1"; 16 | 17 | public static bool TryParseHttpResponse([NotNull] string responseLine, out HttpStatusCode statusCode, out string statusCodeDescription) 18 | { 19 | if (responseLine == null) throw new ArgumentNullException(nameof(responseLine)); 20 | 21 | var responseLineStartIndex = 0; 22 | var responseLineLength = responseLine.Length; 23 | HeadersHelper.TrimInPlace(responseLine, ref responseLineStartIndex, ref responseLineLength); 24 | 25 | if (string.CompareOrdinal(responseLine, responseLineStartIndex, WebSocketHttp11Version, 0, WebSocketHttp11Version.Length) != 0 && 26 | string.CompareOrdinal(responseLine, responseLineStartIndex, WebSocketHttp10Version, 0, WebSocketHttp11Version.Length) != 0) 27 | { 28 | statusCode = 0; 29 | statusCodeDescription = "Malformed Response"; 30 | return false; 31 | } 32 | 33 | var responseCodeStartIndex = Math.Min(responseLineStartIndex + WebSocketHttp11Version.Length + 1, responseLine.Length); 34 | HeadersHelper.Skip(responseLine, ref responseCodeStartIndex, UnicodeCategory.SpaceSeparator); 35 | var responseCodeEndIndex = responseCodeStartIndex; 36 | HeadersHelper.Skip(responseLine, ref responseCodeEndIndex, UnicodeCategory.DecimalDigitNumber); 37 | 38 | if (responseCodeEndIndex == responseCodeStartIndex) 39 | { 40 | statusCode = 0; 41 | statusCodeDescription = "Missing Response Code"; 42 | return false; 43 | } 44 | var responseStatus = ushort.Parse(responseLine.Substring(responseCodeStartIndex, responseCodeEndIndex - responseCodeStartIndex)); 45 | 46 | var descriptionStartIndex = responseCodeEndIndex; 47 | var descriptionLength = responseLine.Length - descriptionStartIndex; 48 | 49 | HeadersHelper.TrimInPlace(responseLine, ref descriptionStartIndex, ref descriptionLength); 50 | 51 | statusCode = (HttpStatusCode)responseStatus; 52 | statusCodeDescription = descriptionLength > 0 ? responseLine.Substring(descriptionStartIndex, descriptionLength) : string.Empty; 53 | return true; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/UnixSockets/UnixSocketListener.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | 6 | using System; 7 | using System.IO; 8 | using System.Net; 9 | using System.Net.Sockets; 10 | using System.Threading; 11 | using vtortola.WebSockets.Transports.Sockets; 12 | 13 | namespace vtortola.WebSockets.Transports.UnixSockets 14 | { 15 | internal sealed class UnixSocketListener : SocketListener 16 | { 17 | private readonly UnixSocketTransport transport; 18 | 19 | /// 20 | public UnixSocketListener(UnixSocketTransport transport, EndPoint[] endPointsToListen, WebSocketListenerOptions options) 21 | : base(transport, endPointsToListen, ProtocolType.Unspecified, options) 22 | { 23 | this.transport = transport; 24 | } 25 | 26 | /// 27 | protected override NetworkConnection CreateConnection(Socket socket, EndPoint localEndPoint) 28 | { 29 | if (this.transport.LingerState != null) 30 | socket.LingerState = this.transport.LingerState; 31 | socket.ReceiveBufferSize = this.transport.ReceiveBufferSize; 32 | socket.ReceiveTimeout = (int)this.transport.ReceiveTimeout.TotalMilliseconds + 1; 33 | socket.SendBufferSize = this.transport.SendBufferSize; 34 | socket.SendTimeout = (int)this.transport.SendTimeout.TotalMilliseconds + 1; 35 | #if !NETSTANDARD && !UAP 36 | socket.UseOnlyOverlappedIO = this.transport.IsAsync; 37 | #endif 38 | return new UnixSocketConnection(socket, localEndPoint); 39 | } 40 | 41 | /// 42 | protected override void Dispose(bool disposed) 43 | { 44 | var endPoints = this.LocalEndpoints; 45 | base.Dispose(disposed); 46 | 47 | if (endPoints == null || endPoints.Count == 0) 48 | return; 49 | 50 | foreach (var endPoint in endPoints) 51 | { 52 | try 53 | { 54 | var fileName = UnixSocketTransport.GetEndPointFileName(endPoint); 55 | if (File.Exists(fileName)) 56 | File.Delete(fileName); 57 | } 58 | catch (Exception webSocketFileDeleteError) when (webSocketFileDeleteError is ThreadAbortException == false) 59 | { 60 | /* ignore delete exception */ 61 | } 62 | } 63 | } 64 | 65 | /// 66 | public override string ToString() 67 | { 68 | // ReSharper disable once CoVariantArrayConversion 69 | return $"{nameof(UnixSocketListener)}, {string.Join(", ", this.LocalEndpoints)}"; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/EndianBitConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using vtortola.WebSockets.Rfc6455.Header; 4 | using Xunit; 5 | 6 | namespace vtortola.WebSockets.UnitTests 7 | { 8 | public class EndianBitConverterTests 9 | { 10 | [Fact] 11 | public void UInt32LeTest() 12 | { 13 | var buffer = new byte[512]; 14 | var random = new Random(nameof(UInt32LeTest).Sum(c => c)); 15 | for (int i = 0; i < 1000; i++) 16 | { 17 | var expected = (uint)random.Next(); 18 | var offset = random.Next(0, 256); 19 | 20 | EndianBitConverter.UInt32CopyBytesLe(expected, buffer, offset); 21 | 22 | var actual = EndianBitConverter.ToUInt32Le(buffer, offset); 23 | 24 | Assert.Equal(expected, actual); 25 | } 26 | } 27 | 28 | [Fact] 29 | public void UInt64LeTest() 30 | { 31 | var buffer = new byte[512]; 32 | var random = new Random(nameof(UInt64LeTest).Sum(c => c)); 33 | for (int i = 0; i < 1000; i++) 34 | { 35 | var expected = (((ulong)random.Next()) << 32) | (ulong)random.Next(); 36 | var offset = random.Next(0, 256); 37 | 38 | EndianBitConverter.UInt64CopyBytesLe(expected, buffer, offset); 39 | 40 | var actual = EndianBitConverter.ToUInt64Le(buffer, offset); 41 | 42 | Assert.Equal(expected, actual); 43 | } 44 | } 45 | 46 | [Fact] 47 | public void UInt16BeTest() 48 | { 49 | var buffer = new byte[512]; 50 | var random = new Random(nameof(UInt16BeTest).Sum(c => c)); 51 | for (int i = 0; i < 1000; i++) 52 | { 53 | var expected = (ushort)random.Next(); 54 | var offset = random.Next(0, 256); 55 | 56 | EndianBitConverter.UInt16CopyBytesBe(expected, buffer, offset); 57 | 58 | var actual = EndianBitConverter.ToUInt16Be(buffer, offset); 59 | 60 | Assert.Equal(expected, actual); 61 | } 62 | } 63 | 64 | [Fact] 65 | public void UInt64BeTest() 66 | { 67 | var buffer = new byte[512]; 68 | var random = new Random(nameof(UInt64BeTest).Sum(c => c)); 69 | for (int i = 0; i < 1000; i++) 70 | { 71 | var expected = (((ulong)random.Next()) << 32) | (ulong)random.Next(); 72 | var offset = random.Next(0, 256); 73 | 74 | EndianBitConverter.UInt64CopyBytesBe(expected, buffer, offset); 75 | 76 | var actual = EndianBitConverter.ToUInt64Be(buffer, offset); 77 | 78 | Assert.Equal(expected, actual); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/WebSocketSecureConnectionExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net.Security; 3 | using System.Security.Authentication; 4 | using System.Security.Cryptography.X509Certificates; 5 | using System.Threading.Tasks; 6 | using vtortola.WebSockets.Extensibility; 7 | using vtortola.WebSockets.Transports; 8 | 9 | namespace vtortola.WebSockets 10 | { 11 | public sealed class WebSocketSecureConnectionExtension : IWebSocketConnectionExtension 12 | { 13 | private readonly X509Certificate2 _certificate; 14 | private readonly RemoteCertificateValidationCallback _validation; 15 | private readonly SslProtocols _protocols; 16 | 17 | public WebSocketSecureConnectionExtension(X509Certificate2 certificate) 18 | { 19 | if (certificate == null) throw new ArgumentNullException(nameof(certificate)); 20 | 21 | _certificate = certificate; 22 | _protocols = SslProtocols.Tls12; 23 | } 24 | public WebSocketSecureConnectionExtension(X509Certificate2 certificate, RemoteCertificateValidationCallback validation) 25 | { 26 | if (certificate == null) throw new ArgumentNullException(nameof(certificate)); 27 | 28 | _certificate = certificate; 29 | _validation = validation; 30 | _protocols = SslProtocols.Tls12; 31 | } 32 | public WebSocketSecureConnectionExtension(X509Certificate2 certificate, RemoteCertificateValidationCallback validation, SslProtocols supportedSslProtocols) 33 | { 34 | if (certificate == null) throw new ArgumentNullException(nameof(certificate)); 35 | 36 | _certificate = certificate; 37 | _validation = validation; 38 | _protocols = supportedSslProtocols; 39 | } 40 | 41 | public async Task ExtendConnectionAsync(NetworkConnection networkConnection) 42 | { 43 | if (networkConnection == null) throw new ArgumentNullException(nameof(networkConnection)); 44 | 45 | var ssl = new SslStream(networkConnection.AsStream(), false, _validation); 46 | try 47 | { 48 | await ssl.AuthenticateAsServerAsync(_certificate, _validation != null, _protocols, false).ConfigureAwait(false); 49 | return new SslNetworkConnection(ssl, networkConnection); 50 | } 51 | catch 52 | { 53 | SafeEnd.Dispose(ssl); 54 | throw; 55 | } 56 | } 57 | 58 | /// 59 | public IWebSocketConnectionExtension Clone() 60 | { 61 | return (IWebSocketConnectionExtension)this.MemberwiseClone(); 62 | } 63 | 64 | /// 65 | public override string ToString() 66 | { 67 | return $"Secure Connection: protocols: {_protocols}, certificate: {_certificate.SubjectName}"; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/SslNetworkConnection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Net; 4 | using System.Net.Security; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using JetBrains.Annotations; 8 | using vtortola.WebSockets.Transports; 9 | 10 | namespace vtortola.WebSockets.Extensibility 11 | { 12 | public sealed class SslNetworkConnection : NetworkConnection 13 | { 14 | private readonly SslStream sslStream; 15 | 16 | public readonly NetworkConnection UnderlyingConnection; 17 | 18 | /// 19 | public override EndPoint LocalEndPoint => this.UnderlyingConnection.LocalEndPoint; 20 | /// 21 | public override EndPoint RemoteEndPoint => this.UnderlyingConnection.RemoteEndPoint; 22 | 23 | public SslNetworkConnection([NotNull] SslStream sslStream, [NotNull] NetworkConnection underlyingConnection) 24 | { 25 | if (sslStream == null) throw new ArgumentNullException(nameof(sslStream)); 26 | if (underlyingConnection == null) throw new ArgumentNullException(nameof(underlyingConnection)); 27 | 28 | this.sslStream = sslStream; 29 | this.UnderlyingConnection = underlyingConnection; 30 | } 31 | 32 | /// 33 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 34 | { 35 | return this.sslStream.ReadAsync(buffer, offset, count, cancellationToken); 36 | } 37 | /// 38 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 39 | { 40 | return this.sslStream.WriteAsync(buffer, offset, count, cancellationToken); 41 | } 42 | /// 43 | public override async Task FlushAsync(CancellationToken cancellationToken) 44 | { 45 | await this.sslStream.FlushAsync(cancellationToken).ConfigureAwait(false); 46 | await this.UnderlyingConnection.FlushAsync(cancellationToken).ConfigureAwait(false); 47 | } 48 | /// 49 | public override async Task CloseAsync() 50 | { 51 | await this.sslStream.FlushAsync().ConfigureAwait(false); 52 | await this.UnderlyingConnection.CloseAsync().ConfigureAwait(false); 53 | } 54 | /// 55 | public override Stream AsStream() 56 | { 57 | return this.sslStream; 58 | } 59 | 60 | /// 61 | public override void Dispose(bool disposed) 62 | { 63 | this.sslStream.Dispose(); 64 | this.UnderlyingConnection.Dispose(disposed); 65 | } 66 | 67 | /// 68 | public override string ToString() 69 | { 70 | return "(SSL Protected) " + this.UnderlyingConnection.ToString(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/Tcp/TcpConnection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using vtortola.WebSockets.Transports.Sockets; 9 | 10 | namespace vtortola.WebSockets.Transports.Tcp 11 | { 12 | internal sealed class TcpConnection : SocketConnection 13 | { 14 | private readonly Socket socket; 15 | 16 | public int Available => this.socket.Available; 17 | public bool IsConnected => this.socket.Connected; 18 | 19 | public bool ExclusiveAddressUse 20 | { 21 | get { return this.socket.ExclusiveAddressUse; } 22 | set { this.socket.ExclusiveAddressUse = value; } 23 | } 24 | public LingerOption LingerState 25 | { 26 | get { return this.socket.LingerState; } 27 | set { this.socket.LingerState = value; } 28 | } 29 | public bool NoDelay 30 | { 31 | get { return this.socket.NoDelay; } 32 | set { this.socket.NoDelay = value; } 33 | } 34 | public int ReceiveBufferSize 35 | { 36 | get { return this.socket.ReceiveBufferSize; } 37 | set { this.socket.ReceiveBufferSize = value; } 38 | } 39 | public int ReceiveTimeout 40 | { 41 | get { return this.socket.ReceiveTimeout; } 42 | set { this.socket.ReceiveTimeout = value; } 43 | } 44 | public int SendBufferSize 45 | { 46 | get { return this.socket.SendBufferSize; } 47 | set { this.socket.SendBufferSize = value; } 48 | } 49 | public int SendTimeout 50 | { 51 | get { return this.socket.SendTimeout; } 52 | set { this.socket.SendTimeout = value; } 53 | } 54 | #if !NETSTANDARD && !UAP 55 | public bool IsAsync 56 | { 57 | get { return this.socket.UseOnlyOverlappedIO; } 58 | set { this.socket.UseOnlyOverlappedIO = value; } 59 | } 60 | #endif 61 | 62 | /// 63 | public TcpConnection(Socket socket, EndPoint localEndPoint) : base(socket, localEndPoint) 64 | { 65 | this.socket = socket; 66 | } 67 | 68 | /// 69 | public override string ToString() 70 | { 71 | // ReSharper disable HeapView.BoxingAllocation 72 | return $"{nameof(TcpConnection)}, local: {this.LocalEndPoint}, remote: {this.RemoteEndPoint}, " + 73 | $"connected: {this.IsConnected}, available (bytes){this.Available}, no delay: {this.NoDelay}, " + 74 | $"receive buffer: {this.ReceiveBufferSize}, receive timeout: {TimeSpan.FromMilliseconds(this.ReceiveTimeout)}, " + 75 | $"send buffer: {this.SendBufferSize}, send timeout: {TimeSpan.FromMilliseconds(this.SendTimeout)}"; 76 | // ReSharper restore HeapView.BoxingAllocation 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Ping/ManualPing.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Diagnostics; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using vtortola.WebSockets.Tools; 10 | 11 | namespace vtortola.WebSockets.Rfc6455 12 | { 13 | partial class WebSocketConnectionRfc6455 14 | { 15 | private sealed class ManualPing : PingHandler 16 | { 17 | private readonly TimeSpan pingTimeout; 18 | private readonly ArraySegment pingBuffer; 19 | private readonly WebSocketConnectionRfc6455 connection; 20 | private TimeSpan lastActivityTime; 21 | 22 | public ManualPing(WebSocketConnectionRfc6455 connection) 23 | { 24 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 25 | 26 | this.pingTimeout = connection.options.PingTimeout; 27 | if (this.pingTimeout < TimeSpan.Zero) 28 | this.pingTimeout = TimeSpan.MaxValue; 29 | 30 | this.pingBuffer = connection.outPingBuffer; 31 | this.connection = connection; 32 | this.NotifyActivity(); 33 | } 34 | 35 | public override async Task PingAsync() 36 | { 37 | if (this.connection.IsClosed) 38 | return; 39 | 40 | var elapsedTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()) - this.lastActivityTime; 41 | if (elapsedTime > this.pingTimeout) 42 | { 43 | SafeEnd.Dispose(this.connection); 44 | 45 | if (this.connection.log.IsDebugEnabled) 46 | this.connection.log.Debug($"WebSocket connection ({this.connection.GetHashCode():X}) has been closed due ping timeout. Time elapsed: {elapsedTime}, timeout: {this.pingTimeout}."); 47 | 48 | return; 49 | } 50 | 51 | var messageType = (WebSocketMessageType)WebSocketFrameOption.Ping; 52 | var count = this.pingBuffer.Array[this.pingBuffer.Offset]; 53 | var payload = this.pingBuffer.Skip(1); 54 | 55 | // manual pinging is always enforces sending ping frames 56 | var pingFrame = this.connection.PrepareFrame(payload, count, true, false, messageType, WebSocketExtensionFlags.None); 57 | await this.connection.SendFrameAsync(pingFrame, Timeout.InfiniteTimeSpan, SendOptions.NoErrors, CancellationToken.None).ConfigureAwait(false); 58 | } 59 | /// 60 | public override void NotifyActivity() 61 | { 62 | this.lastActivityTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()); 63 | } 64 | public override void NotifyPong(ArraySegment pongBuffer) 65 | { 66 | this.NotifyActivity(); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/WebSocketMessageExtensionCollection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | 10 | namespace vtortola.WebSockets 11 | { 12 | public sealed class WebSocketMessageExtensionCollection : IReadOnlyCollection 13 | { 14 | private readonly List extensions; 15 | private volatile int useCounter; 16 | 17 | public int Count => this.extensions.Count; 18 | public bool IsReadOnly => this.useCounter > 0; 19 | 20 | public WebSocketMessageExtensionCollection() 21 | { 22 | this.extensions = new List(); 23 | } 24 | 25 | public void Add(IWebSocketMessageExtension extension) 26 | { 27 | if (extension == null) throw new ArgumentNullException(nameof(extension)); 28 | 29 | if (this.IsReadOnly) 30 | throw new WebSocketException($"New entries cannot be added because this collection is used in running {nameof(WebSocketClient)} or {nameof(WebSocketListener)}."); 31 | 32 | if (this.extensions.Any(ext => ext.GetType() == extension.GetType())) 33 | throw new WebSocketException($"Can't add extension '{extension}' because another extension of type '{extension.GetType().Name}' is already exists in collection."); 34 | 35 | this.extensions.Add(extension); 36 | } 37 | 38 | IEnumerator IEnumerable.GetEnumerator() 39 | { 40 | return this.extensions.GetEnumerator(); 41 | } 42 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 43 | { 44 | return this.extensions.GetEnumerator(); 45 | } 46 | 47 | public List.Enumerator GetEnumerator() 48 | { 49 | return this.extensions.GetEnumerator(); 50 | } 51 | 52 | internal WebSocketMessageExtensionCollection Clone() 53 | { 54 | var cloned = new WebSocketMessageExtensionCollection(); 55 | foreach (var item in this.extensions) 56 | cloned.extensions.Add(item.Clone()); 57 | return cloned; 58 | } 59 | 60 | internal void SetUsed(bool isUsed) 61 | { 62 | #pragma warning disable 420 63 | var newValue = default(int); 64 | if (isUsed) 65 | newValue = Interlocked.Increment(ref this.useCounter); 66 | else 67 | newValue = Interlocked.Decrement(ref this.useCounter); 68 | if (newValue < 0) 69 | throw new InvalidOperationException("The collection is released more than once."); 70 | #pragma warning restore 420 71 | } 72 | 73 | /// 74 | public override string ToString() 75 | { 76 | return string.Join(", ", this.extensions); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/HttpStatusDescription.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Net; 6 | 7 | namespace vtortola.WebSockets.Http 8 | { 9 | internal static class HttpStatusDescription 10 | { 11 | internal static string Get(HttpStatusCode code) 12 | { 13 | return Get((int)code); 14 | } 15 | 16 | internal static string Get(int code) 17 | { 18 | switch (code) 19 | { 20 | case 100: return "Continue"; 21 | case 101: return "Switching Protocols"; 22 | case 102: return "Processing"; 23 | 24 | case 200: return "OK"; 25 | case 201: return "Created"; 26 | case 202: return "Accepted"; 27 | case 203: return "Non-Authoritative Information"; 28 | case 204: return "No Content"; 29 | case 205: return "Reset Content"; 30 | case 206: return "Partial Content"; 31 | case 207: return "Multi-Status"; 32 | 33 | case 300: return "Multiple Choices"; 34 | case 301: return "Moved Permanently"; 35 | case 302: return "Found"; 36 | case 303: return "See Other"; 37 | case 304: return "Not Modified"; 38 | case 305: return "Use Proxy"; 39 | case 307: return "Temporary Redirect"; 40 | 41 | case 400: return "Bad Request"; 42 | case 401: return "Unauthorized"; 43 | case 402: return "Payment Required"; 44 | case 403: return "Forbidden"; 45 | case 404: return "Not Found"; 46 | case 405: return "Method Not Allowed"; 47 | case 406: return "Not Acceptable"; 48 | case 407: return "Proxy Authentication Required"; 49 | case 408: return "Request Timeout"; 50 | case 409: return "Conflict"; 51 | case 410: return "Gone"; 52 | case 411: return "Length Required"; 53 | case 412: return "Precondition Failed"; 54 | case 413: return "Request Entity Too Large"; 55 | case 414: return "Request-Uri Too Long"; 56 | case 415: return "Unsupported Media Type"; 57 | case 416: return "Requested Range Not Satisfiable"; 58 | case 417: return "Expectation Failed"; 59 | case 422: return "Unprocessable Entity"; 60 | case 423: return "Locked"; 61 | case 424: return "Failed Dependency"; 62 | case 426: return "Upgrade Required"; // RFC 2817 63 | 64 | case 500: return "Internal Server Error"; 65 | case 501: return "Not Implemented"; 66 | case 502: return "Bad Gateway"; 67 | case 503: return "Service Unavailable"; 68 | case 504: return "Gateway Timeout"; 69 | case 505: return "Http Version Not Supported"; 70 | case 507: return "Insufficient Storage"; 71 | } 72 | return null; 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Ping/BandwidthSavingPing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | 6 | namespace vtortola.WebSockets.Rfc6455 7 | { 8 | partial class WebSocketConnectionRfc6455 9 | { 10 | private class BandwidthSavingPing : PingHandler 11 | { 12 | private readonly ArraySegment pingBuffer; 13 | private readonly TimeSpan pingTimeout; 14 | private readonly TimeSpan pingInterval; 15 | private readonly WebSocketConnectionRfc6455 connection; 16 | private TimeSpan lastActivityTime; 17 | 18 | public BandwidthSavingPing(WebSocketConnectionRfc6455 connection) 19 | { 20 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 21 | 22 | this.pingTimeout = connection.options.PingTimeout; 23 | if (this.pingTimeout < TimeSpan.Zero) 24 | this.pingTimeout = TimeSpan.MaxValue; 25 | this.pingInterval = connection.options.PingInterval; 26 | 27 | this.pingBuffer = connection.outPingBuffer; 28 | this.connection = connection; 29 | this.NotifyActivity(); 30 | } 31 | 32 | public sealed override async Task PingAsync() 33 | { 34 | if (this.connection.IsClosed) 35 | return; 36 | 37 | var elapsedTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()) - this.lastActivityTime; 38 | if (elapsedTime > this.pingTimeout) 39 | { 40 | SafeEnd.Dispose(this.connection); 41 | 42 | if (this.connection.log.IsDebugEnabled) 43 | this.connection.log.Debug($"WebSocket connection ({this.connection.GetHashCode():X}) has been closed due ping timeout. Time elapsed: {elapsedTime}, timeout: {this.pingTimeout}, interval: {this.pingInterval}."); 44 | 45 | return; 46 | } 47 | 48 | var messageType = (WebSocketMessageType)WebSocketFrameOption.Ping; 49 | var pingFrame = this.connection.PrepareFrame(this.pingBuffer, 0, true, false, messageType, WebSocketExtensionFlags.None); 50 | var pingFrameLockTimeout = elapsedTime < this.pingInterval ? TimeSpan.Zero : Timeout.InfiniteTimeSpan; 51 | 52 | // 53 | // ping_interval is 33% of ping_timeout time 54 | // 55 | // if elapsed_time < ping_interval then TRY to send ping frame 56 | // if elapsed_time > ping_interval then ENFORCE sending ping frame because ping_timeout is near 57 | // 58 | // pingFrameLockTimeout is controlling TRY/ENFORCE behaviour. Zero mean TRY to take write lock or skip. InfiniteTimeSpan mean wait indefinitely for write lock. 59 | // 60 | 61 | await this.connection.SendFrameAsync(pingFrame, pingFrameLockTimeout, SendOptions.NoErrors, CancellationToken.None).ConfigureAwait(false); 62 | } 63 | 64 | /// 65 | public sealed override void NotifyActivity() 66 | { 67 | this.lastActivityTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()); 68 | } 69 | public sealed override void NotifyPong(ArraySegment pongBuffer) 70 | { 71 | this.NotifyActivity(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Header/EndianBitConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace vtortola.WebSockets.Rfc6455.Header 4 | { 5 | public static class EndianBitConverter 6 | { 7 | public static void UInt32CopyBytesLe(uint value, byte[] buffer, int offset) 8 | { 9 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 10 | 11 | for (var i = 0; i < 4; i++) 12 | { 13 | buffer[offset + i] = (byte)value; 14 | value >>= 8; 15 | } 16 | } 17 | 18 | public static void UInt64CopyBytesLe(ulong value, byte[] buffer, int offset) 19 | { 20 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 21 | 22 | for (var i = 0; i < 8; i++) 23 | { 24 | buffer[offset + i] = (byte)value; 25 | value >>= 8; 26 | } 27 | } 28 | 29 | public static void UInt16CopyBytesBe(ushort value, byte[] buffer, int offset) 30 | { 31 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 32 | 33 | for (var i = offset + 1; i >= offset; i--) 34 | { 35 | buffer[i] = (byte)value; 36 | value >>= 8; 37 | } 38 | } 39 | 40 | public static void UInt64CopyBytesBe(ulong value, byte[] buffer, int offset) 41 | { 42 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 43 | 44 | for (var i = offset + 7; i >= offset; i--) 45 | { 46 | buffer[i] = (byte)value; 47 | value >>= 8; 48 | } 49 | } 50 | 51 | public static ushort ToUInt16Be(byte[] buffer, int offset) 52 | { 53 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 54 | 55 | long value = 0; 56 | for (var i = 0; i < sizeof(ushort); i++) 57 | { 58 | value = unchecked((value << 8) | buffer[offset + i]); 59 | } 60 | return (ushort)value; 61 | } 62 | 63 | public static ulong ToUInt64Be(byte[] buffer, int offset) 64 | { 65 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 66 | 67 | long value = 0; 68 | for (var i = 0; i < sizeof(ulong); i++) 69 | { 70 | value = unchecked((value << 8) | buffer[offset + i]); 71 | } 72 | return (ulong)value; 73 | } 74 | 75 | public static uint ToUInt32Le(byte[] buffer, int offset) 76 | { 77 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 78 | 79 | long value = 0; 80 | for (var i = 0; i < sizeof(uint); i++) 81 | { 82 | value = unchecked((value << 8) | buffer[offset + sizeof(uint) - 1 - i]); 83 | } 84 | return (uint)value; 85 | } 86 | 87 | public static ulong ToUInt64Le(byte[] buffer, int offset) 88 | { 89 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 90 | 91 | long value = 0; 92 | for (var i = 0; i < sizeof(ulong); i++) 93 | { 94 | value = unchecked((value << 8) | buffer[offset + sizeof(ulong) - 1 - i]); 95 | } 96 | return (ulong)value; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Samples/WebSocketClient/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using log4net.Config; 8 | using vtortola.WebSockets; 9 | using vtortola.WebSockets.Deflate; 10 | using vtortola.WebSockets.Rfc6455; 11 | 12 | namespace WebSocketClient 13 | { 14 | public sealed class Program 15 | { 16 | private static readonly Log4NetLogger Log = new Log4NetLogger(typeof(Program)); 17 | 18 | private static void Main(string[] args) 19 | { 20 | // configuring logging 21 | XmlConfigurator.Configure(); 22 | 23 | var cancellationTokenSource = new CancellationTokenSource(); 24 | Console.CancelKeyPress += (_, __) => cancellationTokenSource.Cancel(); 25 | 26 | Console.WriteLine("Press CTRL+C to stop client."); 27 | Console.WriteLine("Press ESC to gracefully close connection."); 28 | 29 | var bufferSize = 1024 * 8; // 8KiB 30 | var bufferPoolSize = 100 * bufferSize; // 800KiB pool 31 | 32 | var options = new WebSocketListenerOptions 33 | { 34 | // set send buffer size (optional but recommended) 35 | SendBufferSize = bufferSize, 36 | // set buffer manager for buffers re-use (optional but recommended) 37 | BufferManager = BufferManager.CreateBufferManager(bufferPoolSize, bufferSize), 38 | // set logger, leave default NullLogger if you don't want logging 39 | Logger = Log 40 | }; 41 | // register RFC6455 protocol implementation (required) 42 | options.Standards.RegisterRfc6455(); 43 | // configure tcp transport (optional) 44 | options.Transports.ConfigureTcp(tcp => 45 | { 46 | tcp.BacklogSize = 100; // max pending connections waiting to be accepted 47 | tcp.ReceiveBufferSize = bufferSize; 48 | tcp.SendBufferSize = bufferSize; 49 | }); 50 | 51 | var message = "Hello!"; 52 | var echoServerUrl = new Uri("ws://echo.websocket.org?encoding=text"); 53 | var client = new vtortola.WebSockets.WebSocketClient(options); 54 | 55 | Log.Warning("Connecting to " + echoServerUrl + "..."); 56 | var webSocket = client.ConnectAsync(echoServerUrl, cancellationTokenSource.Token).Result; 57 | Log.Warning("Connected to " + echoServerUrl + ". "); 58 | 59 | while (cancellationTokenSource.IsCancellationRequested == false) 60 | { 61 | Log.Warning("Sending text: " + message); 62 | webSocket.WriteStringAsync(message, cancellationTokenSource.Token).Wait(); 63 | 64 | var responseText = webSocket.ReadStringAsync(cancellationTokenSource.Token).Result; 65 | Log.Warning("Received message:" + responseText); 66 | 67 | if (Console.KeyAvailable && Console.ReadKey(intercept: true).Key == ConsoleKey.Escape) 68 | break; 69 | Thread.Sleep(400); 70 | } 71 | 72 | Log.Warning("Disconnecting from " + echoServerUrl + "..."); 73 | webSocket.CloseAsync().Wait(); 74 | Log.Warning("Disconnected from " + echoServerUrl + "."); 75 | 76 | Log.Warning("Disposing client..."); 77 | client.CloseAsync().Wait(); 78 | 79 | Console.WriteLine("Press any key to exit."); 80 | Console.ReadKey(); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/WebSocketFrameHeaderFlagsTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using vtortola.WebSockets.Rfc6455; 3 | using Xunit; 4 | 5 | namespace vtortola.WebSockets.UnitTests 6 | { 7 | public class WebSocketFrameHeaderFlagsTests 8 | { 9 | public static byte[] GenerateHeader(bool fin, bool rsv1, bool rsv2, bool rsv3, bool opt4, bool opt3, bool opt2, bool opt1, bool mask) 10 | { 11 | var header1 = new bool[8]; 12 | header1[7] = fin; 13 | header1[6] = rsv1; 14 | header1[5] = rsv2; 15 | header1[4] = rsv3; 16 | header1[3] = opt4; 17 | header1[2] = opt3; 18 | header1[1] = opt2; 19 | header1[0] = opt1; 20 | 21 | var header2 = new bool[8]; 22 | header2[7] = mask; 23 | header2[6] = false; 24 | header2[5] = false; 25 | header2[4] = false; 26 | header2[3] = false; 27 | header2[2] = false; 28 | header2[1] = false; 29 | header2[0] = false; 30 | 31 | var bitArray1 = new BitArray(header1); 32 | var byteHead = new byte[2]; 33 | bitArray1.CopyTo(byteHead, 0); 34 | 35 | var bitArray2 = new BitArray(header2); 36 | bitArray2.CopyTo(byteHead, 1); 37 | 38 | return byteHead; 39 | } 40 | 41 | [Fact] 42 | public void Parse() 43 | { 44 | var header = GenerateHeader(true, false, false, false, false, false, false, true, false); 45 | WebSocketFrameHeaderFlags flags; 46 | Assert.True(WebSocketFrameHeaderFlags.TryParse(header, 0, out flags)); 47 | Assert.True(flags.FIN); 48 | Assert.True(flags.OPT1); 49 | Assert.True(flags.Option == WebSocketFrameOption.Text); 50 | 51 | Assert.False(flags.Option == WebSocketFrameOption.Binary); 52 | Assert.False(flags.MASK); 53 | 54 | header = GenerateHeader(false, false, false, false, false, false, false, true, true); 55 | 56 | Assert.True(WebSocketFrameHeaderFlags.TryParse(header, 0, out flags)); 57 | Assert.False(flags.FIN); 58 | Assert.True(flags.OPT1); 59 | Assert.True(flags.Option == WebSocketFrameOption.Text); 60 | 61 | Assert.False(flags.Option == WebSocketFrameOption.Binary); 62 | Assert.True(flags.MASK); 63 | 64 | header = GenerateHeader(false, false, false, false, false, false, true, false, true); 65 | 66 | Assert.True(WebSocketFrameHeaderFlags.TryParse(header, 0, out flags)); 67 | Assert.False(flags.FIN); 68 | Assert.False(flags.OPT1); 69 | Assert.False(flags.Option == WebSocketFrameOption.Text); 70 | Assert.True(flags.OPT2); 71 | Assert.True(flags.Option == WebSocketFrameOption.Binary); 72 | Assert.True(flags.MASK); 73 | 74 | header = GenerateHeader(true, false, false, false, true, false, false, true, true); 75 | 76 | Assert.True(WebSocketFrameHeaderFlags.TryParse(header, 0, out flags)); 77 | Assert.True(flags.FIN); 78 | Assert.True(flags.OPT1); 79 | Assert.False(flags.Option == WebSocketFrameOption.Text); 80 | Assert.False(flags.OPT2); 81 | Assert.False(flags.Option == WebSocketFrameOption.Binary); 82 | Assert.True(flags.MASK); 83 | Assert.False(flags.OPT3); 84 | Assert.True(flags.OPT4); 85 | Assert.True(flags.Option == WebSocketFrameOption.Ping); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/RequestHeader.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | namespace vtortola.WebSockets.Http 6 | { 7 | /// 8 | /// Type is 1:1 convertible to except ContentDisposition and WebSocket headers. 9 | /// 10 | public enum RequestHeader 11 | { 12 | [Header("Cache-Control")] 13 | CacheControl = 0, 14 | Connection = 1, 15 | [Header(Flags = HeaderFlags.Singleton)] 16 | Date = 2, 17 | [Header("Keep-Alive")] 18 | KeepAlive = 3, 19 | Pragma = 4, 20 | Trailer = 5, 21 | [Header("Transfer-Encoding")] 22 | TransferEncoding = 6, 23 | Upgrade = 7, 24 | Via = 8, 25 | Warning = 9, 26 | Allow = 10, 27 | [Header("Content-Length", Flags = HeaderFlags.Singleton)] 28 | ContentLength = 11, 29 | [Header("Content-Type", Flags = HeaderFlags.Singleton)] 30 | ContentType = 12, 31 | [Header("Content-Encoding")] 32 | ContentEncoding = 13, 33 | [Header("Content-Language")] 34 | ContentLanguage = 14, 35 | [Header("Content-Location")] 36 | ContentLocation = 15, 37 | [Header("Content-MD5")] 38 | ContentMd5 = 16, 39 | [Header("Content-Range")] 40 | ContentRange = 17, 41 | Expires = 18, 42 | [Header("Last-Modified")] 43 | LastModified = 19, 44 | 45 | Accept = 20, 46 | [Header("Accept-Charset")] 47 | AcceptCharset = 21, 48 | [Header("Accept-Encoding")] 49 | AcceptEncoding = 22, 50 | [Header("Accept-Language")] 51 | AcceptLanguage = 23, 52 | [Header(Flags = HeaderFlags.Singleton)] 53 | Authorization = 24, 54 | [Header(Flags = HeaderFlags.Singleton)] 55 | Cookie = 25, 56 | Expect = 26, 57 | [Header(Flags = HeaderFlags.Singleton)] 58 | From = 27, 59 | [Header(Flags = HeaderFlags.Singleton)] 60 | Host = 28, 61 | [Header("If-Match")] 62 | IfMatch = 29, 63 | [Header("If-Modified-Since", Flags = HeaderFlags.Singleton)] 64 | IfModifiedSince = 30, 65 | [Header("If-None-Match")] 66 | IfNoneMatch = 31, 67 | [Header("If-Range")] 68 | IfRange = 32, 69 | [Header("If-Unmodified-Since", Flags = HeaderFlags.Singleton)] 70 | IfUnmodifiedSince = 33, 71 | [Header("Max-Forwards", Flags = HeaderFlags.Singleton)] 72 | MaxForwards = 34, 73 | [Header("Proxy-Authorization", Flags = HeaderFlags.Singleton)] 74 | ProxyAuthorization = 35, 75 | // ReSharper disable once IdentifierTypo 76 | [Header(Flags = HeaderFlags.Singleton)] 77 | Referer = 36, 78 | Range = 37, 79 | [Header("TE")] 80 | Te = 38, 81 | Translate = 39, 82 | [Header("User-Agent", Flags = HeaderFlags.Singleton)] 83 | UserAgent = 40, 84 | 85 | // extensions to HttpRequestHeaders 86 | [Header("Content-Disposition", Flags = HeaderFlags.Singleton)] 87 | ContentDisposition = 41, 88 | [Header(Flags = HeaderFlags.Singleton)] 89 | Origin = 42, 90 | [Header("Sec-WebSocket-Key", Flags = HeaderFlags.Singleton)] 91 | WebSocketKey = 43, 92 | [Header("Sec-WebSocket-Version")] 93 | WebSocketVersion = 44, 94 | [Header("Sec-WebSocket-Extensions")] 95 | WebSocketExtensions = 45, 96 | [Header("Sec-WebSocket-Protocol")] 97 | WebSocketProtocol = 46, 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Deflate/WebSocketDeflateWriteStream.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO.Compression; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using JetBrains.Annotations; 6 | 7 | namespace vtortola.WebSockets.Deflate 8 | { 9 | public sealed class WebSocketDeflateWriteStream : WebSocketMessageWriteStream 10 | { 11 | private static readonly byte[] FINAL_BYTE = new byte[] { 0 }; 12 | 13 | private const int STATE_OPEN = 0; 14 | private const int STATE_CLOSED = 1; 15 | private const int STATE_DISPOSED = 2; 16 | 17 | private readonly WebSocketMessageWriteStream innerStream; 18 | private readonly DeflateStream deflateStream; 19 | private volatile int state = STATE_OPEN; 20 | 21 | /// 22 | internal override WebSocketListenerOptions Options => this.innerStream.Options; 23 | 24 | public WebSocketDeflateWriteStream([NotNull]WebSocketMessageWriteStream innerStream) 25 | { 26 | if (innerStream == null) throw new ArgumentNullException(nameof(innerStream)); 27 | 28 | this.innerStream = innerStream; 29 | this.deflateStream = new DeflateStream(innerStream, CompressionLevel.Optimal, leaveOpen: true); 30 | } 31 | 32 | /// 33 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 34 | { 35 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 36 | if (offset < 0 || offset > buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); 37 | if (count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); 38 | 39 | if (count == 0) 40 | { 41 | return Task.FromResult(0); 42 | } 43 | 44 | return this.deflateStream.WriteAsync(buffer, offset, count, cancellationToken); 45 | } 46 | 47 | /// 48 | public override async Task WriteAndCloseAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 49 | { 50 | if (buffer == null) throw new ArgumentNullException(nameof(buffer)); 51 | if (offset < 0 || offset > buffer.Length) throw new ArgumentOutOfRangeException(nameof(offset)); 52 | if (count < 0 || offset + count > buffer.Length) throw new ArgumentOutOfRangeException(nameof(count)); 53 | 54 | if (count > 0) 55 | { 56 | await this.deflateStream.WriteAsync(buffer, offset, count, cancellationToken); 57 | } 58 | 59 | await this.CloseAsync().ConfigureAwait(false); 60 | } 61 | 62 | /// 63 | public override async Task CloseAsync() 64 | { 65 | if (Interlocked.CompareExchange(ref this.state, STATE_CLOSED, STATE_OPEN) != STATE_OPEN) 66 | return; 67 | 68 | // flush remaining data 69 | await this.deflateStream.FlushAsync(CancellationToken.None); 70 | this.deflateStream.Dispose(); // will flush deflate data 71 | // close inner stream 72 | await this.innerStream.WriteAndCloseAsync(FINAL_BYTE, 0, 1, CancellationToken.None).ConfigureAwait(false); 73 | } 74 | 75 | /// 76 | protected override void Dispose(bool disposing) 77 | { 78 | if (Interlocked.Exchange(ref this.state, STATE_DISPOSED) == STATE_DISPOSED) 79 | return; 80 | 81 | SafeEnd.Dispose(this.deflateStream); 82 | SafeEnd.Dispose(this.innerStream); 83 | base.Dispose(disposing); 84 | } 85 | 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Tools/HeadersHelper.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Globalization; 8 | using System.Linq; 9 | using System.Text; 10 | using JetBrains.Annotations; 11 | 12 | namespace vtortola.WebSockets.Tools 13 | { 14 | internal static class HeadersHelper 15 | { 16 | public static IEnumerable> SplitAndTrimKeyValue([CanBeNull] string valueString, char valuesSeparator = ';', char nameValueSeparator = '=', StringSplitOptions options = StringSplitOptions.None) 17 | { 18 | if (valueString == null) 19 | yield break; 20 | 21 | var startIndex = 0; 22 | do 23 | { 24 | var nextValueIndex = valueString.IndexOf(valuesSeparator, startIndex); 25 | if (nextValueIndex < 0) 26 | nextValueIndex = valueString.Length; 27 | 28 | var equalsIndex = valueString.IndexOf(nameValueSeparator, startIndex, nextValueIndex - startIndex); 29 | if (equalsIndex < 0) 30 | equalsIndex = startIndex - 1; 31 | 32 | var nameStart = startIndex; 33 | var nameLength = Math.Max(0, equalsIndex - startIndex); 34 | 35 | TrimInPlace(valueString, ref nameStart, ref nameLength); 36 | 37 | var name = string.Empty; 38 | if (nameLength > 0) 39 | name = valueString.Substring(nameStart, nameLength); 40 | 41 | var valueStart = equalsIndex + 1; 42 | var valueLength = nextValueIndex - equalsIndex - 1; 43 | var value = string.Empty; 44 | 45 | TrimInPlace(valueString, ref valueStart, ref valueLength); 46 | 47 | if (valueLength > 0) 48 | value = valueString.Substring(valueStart, valueLength); 49 | else 50 | value = string.Empty; 51 | 52 | if (options == StringSplitOptions.None || (string.IsNullOrWhiteSpace(value) == false || string.IsNullOrWhiteSpace(name) == false)) 53 | yield return new KeyValuePair(name, value); 54 | 55 | startIndex = nextValueIndex + 1; 56 | } while (startIndex < valueString.Length); 57 | } 58 | 59 | public static void TrimInPlace([NotNull] string value, ref int startIndex, ref int length) 60 | { 61 | if (value == null) throw new ArgumentNullException(nameof(value)); 62 | if (startIndex < 0 || startIndex > value.Length) throw new ArgumentOutOfRangeException(nameof(startIndex)); 63 | if (length < 0 || startIndex + length > value.Length) throw new ArgumentOutOfRangeException(nameof(length)); 64 | 65 | if (length == 0) 66 | return; 67 | 68 | while (length > 0 && char.IsWhiteSpace(value[startIndex])) 69 | { 70 | startIndex++; 71 | length--; 72 | } 73 | 74 | if (length == 0) return; 75 | var end = startIndex + length - 1; 76 | while (length > 0 && char.IsWhiteSpace(value[end])) 77 | { 78 | end--; 79 | length--; 80 | } 81 | } 82 | public static void Skip(string value, ref int startIndex, UnicodeCategory category) 83 | { 84 | if (value == null) throw new ArgumentNullException(nameof(value)); 85 | if (startIndex < 0 || startIndex > value.Length) throw new ArgumentOutOfRangeException(nameof(startIndex)); 86 | 87 | 88 | while (startIndex < value.Length && CharUnicodeInfo.GetUnicodeCategory(value[startIndex]) == category) 89 | startIndex++; 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/WebSocketConnectionExtensionCollection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Security; 9 | using System.Security.Authentication; 10 | using System.Security.Cryptography.X509Certificates; 11 | using System.Threading; 12 | using JetBrains.Annotations; 13 | 14 | namespace vtortola.WebSockets 15 | { 16 | [PublicAPI] 17 | public sealed class WebSocketConnectionExtensionCollection : IReadOnlyCollection 18 | { 19 | private readonly List extensions; 20 | private volatile int useCounter; 21 | 22 | public int Count => this.extensions.Count; 23 | public bool IsReadOnly => this.useCounter > 0; 24 | 25 | public WebSocketConnectionExtensionCollection() 26 | { 27 | this.extensions = new List(); 28 | } 29 | 30 | public void Add(IWebSocketConnectionExtension extension) 31 | { 32 | if (extension == null) throw new ArgumentNullException(nameof(extension)); 33 | 34 | if (this.IsReadOnly) 35 | throw new WebSocketException($"New entries cannot be added because this collection is used in running {nameof(WebSocketClient)} or {nameof(WebSocketListener)}."); 36 | 37 | if (this.extensions.Any(ext => ext.GetType() == extension.GetType())) 38 | throw new WebSocketException($"Can't add extension '{extension}' because another extension of type '{extension.GetType().Name}' is already exists in collection."); 39 | 40 | this.extensions.Add(extension); 41 | } 42 | 43 | public WebSocketConnectionExtensionCollection RegisterSecureConnection( 44 | X509Certificate2 certificate, 45 | RemoteCertificateValidationCallback validation = null, 46 | SslProtocols supportedSslProtocols = SslProtocols.Tls12) 47 | { 48 | if (certificate == null) throw new ArgumentNullException(nameof(certificate)); 49 | 50 | var extension = new WebSocketSecureConnectionExtension(certificate, validation, supportedSslProtocols); 51 | this.Add(extension); 52 | return this; 53 | } 54 | 55 | IEnumerator IEnumerable.GetEnumerator() 56 | { 57 | return this.extensions.GetEnumerator(); 58 | } 59 | System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() 60 | { 61 | return this.extensions.GetEnumerator(); 62 | } 63 | 64 | public List.Enumerator GetEnumerator() 65 | { 66 | return this.extensions.GetEnumerator(); 67 | } 68 | 69 | internal WebSocketConnectionExtensionCollection Clone() 70 | { 71 | var cloned = new WebSocketConnectionExtensionCollection(); 72 | foreach (var item in this.extensions) 73 | cloned.extensions.Add(item.Clone()); 74 | return cloned; 75 | } 76 | 77 | internal void SetUsed(bool isUsed) 78 | { 79 | #pragma warning disable 420 80 | var newValue = default(int); 81 | if (isUsed) 82 | newValue = Interlocked.Increment(ref this.useCounter); 83 | else 84 | newValue = Interlocked.Decrement(ref this.useCounter); 85 | if (newValue < 0) 86 | throw new InvalidOperationException("The collection is released more than once."); 87 | #pragma warning restore 420 88 | } 89 | 90 | /// 91 | public override string ToString() 92 | { 93 | return string.Join(", ", this.extensions); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Extensibility/WebSocketFactoryCollection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Linq; 9 | using System.Threading; 10 | using vtortola.WebSockets.Http; 11 | 12 | namespace vtortola.WebSockets 13 | { 14 | public sealed class WebSocketFactoryCollection : IReadOnlyCollection 15 | { 16 | private readonly Dictionary factoryByVersion; 17 | private volatile int useCounter; 18 | 19 | public IEnumerable SupportedVersions => this.factoryByVersion.Keys; 20 | public int Count => this.factoryByVersion.Count; 21 | public bool IsReadOnly => this.useCounter > 0; 22 | 23 | public WebSocketFactoryCollection() 24 | { 25 | this.factoryByVersion = new Dictionary(); 26 | } 27 | 28 | public void Add(WebSocketFactory factory) 29 | { 30 | if (factory == null) throw new ArgumentNullException(nameof(factory)); 31 | 32 | if (this.IsReadOnly) 33 | throw new WebSocketException($"New entries cannot be added because this collection is used in running {nameof(WebSocketClient)} or {nameof(WebSocketListener)}."); 34 | 35 | if (this.factoryByVersion.ContainsKey(factory.Version)) 36 | { 37 | throw new WebSocketException($"Can't add {nameof(WebSocketFactory)} '{factory}' because another {nameof(WebSocketFactory)} with " + 38 | $"version '{factory.Version}' is already exists in collection."); 39 | } 40 | 41 | this.factoryByVersion.Add(factory.Version, factory); 42 | } 43 | 44 | IEnumerator IEnumerable.GetEnumerator() 45 | { 46 | return this.factoryByVersion.Values.GetEnumerator(); 47 | } 48 | IEnumerator IEnumerable.GetEnumerator() 49 | { 50 | return this.factoryByVersion.Values.GetEnumerator(); 51 | } 52 | public Dictionary.ValueCollection.Enumerator GetEnumerator() 53 | { 54 | return this.factoryByVersion.Values.GetEnumerator(); 55 | } 56 | 57 | internal WebSocketFactoryCollection Clone() 58 | { 59 | var cloned = new WebSocketFactoryCollection(); 60 | foreach (var kv in this.factoryByVersion) 61 | cloned.factoryByVersion[kv.Key] = kv.Value.Clone(); 62 | return cloned; 63 | } 64 | 65 | internal void SetUsed(bool isUsed) 66 | { 67 | #pragma warning disable 420 68 | var newValue = default(int); 69 | if (isUsed) 70 | newValue = Interlocked.Increment(ref this.useCounter); 71 | else 72 | newValue = Interlocked.Decrement(ref this.useCounter); 73 | if (newValue < 0) 74 | throw new InvalidOperationException("The collection is released more than once."); 75 | #pragma warning restore 420 76 | } 77 | 78 | internal WebSocketFactory GetLast() 79 | { 80 | return this.factoryByVersion[this.factoryByVersion.Keys.Max()]; 81 | } 82 | 83 | public bool TryGetWebSocketFactory(WebSocketHttpRequest request, out WebSocketFactory factory) 84 | { 85 | if (request == null) throw new ArgumentNullException(nameof(request)); 86 | 87 | factory = default(WebSocketFactory); 88 | var webSocketsVersion = default(short); 89 | if (short.TryParse(request.Headers[RequestHeader.WebSocketVersion], out webSocketsVersion) && this.factoryByVersion.TryGetValue(webSocketsVersion, out factory)) 90 | return true; 91 | else 92 | return false; 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/Ping/LatencyControlPing.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using vtortola.WebSockets.Rfc6455.Header; 6 | 7 | namespace vtortola.WebSockets.Rfc6455 8 | { 9 | partial class WebSocketConnectionRfc6455 10 | { 11 | private sealed class LatencyControlPing : PingHandler 12 | { 13 | private readonly ArraySegment pingBuffer; 14 | private readonly TimeSpan pingTimeout; 15 | private readonly TimeSpan pingInterval; 16 | private readonly WebSocketConnectionRfc6455 connection; 17 | private TimeSpan lastActivityTime; 18 | 19 | public LatencyControlPing(WebSocketConnectionRfc6455 connection) 20 | { 21 | if (connection == null) throw new ArgumentNullException(nameof(connection)); 22 | 23 | this.pingTimeout = connection.options.PingTimeout; 24 | if (this.pingTimeout < TimeSpan.Zero) 25 | this.pingTimeout = TimeSpan.MaxValue; 26 | this.pingInterval = connection.options.PingInterval; 27 | 28 | this.pingBuffer = connection.outPingBuffer; 29 | this.connection = connection; 30 | this.NotifyActivity(); 31 | } 32 | 33 | public override async Task PingAsync() 34 | { 35 | if (this.connection.IsClosed) 36 | return; 37 | 38 | var elapsedTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()) - this.lastActivityTime; 39 | if (elapsedTime > this.pingTimeout) 40 | { 41 | this.connection.latency = Timeout.InfiniteTimeSpan; 42 | SafeEnd.Dispose(this.connection); 43 | 44 | if (this.connection.log.IsDebugEnabled) 45 | this.connection.log.Debug($"WebSocket connection ({this.connection.GetHashCode():X}) has been closed due ping timeout. Time elapsed: {elapsedTime}, timeout: {this.pingTimeout}, interval: {this.pingInterval}."); 46 | 47 | return; 48 | } 49 | 50 | EndianBitConverter.UInt64CopyBytesLe((ulong)Stopwatch.GetTimestamp(), this.pingBuffer.Array, this.pingBuffer.Offset); 51 | var messageType = (WebSocketMessageType)WebSocketFrameOption.Ping; 52 | 53 | var pingFrame = this.connection.PrepareFrame(this.pingBuffer, 8, true, false, messageType, WebSocketExtensionFlags.None); 54 | var pingFrameLockTimeout = elapsedTime < this.pingInterval ? TimeSpan.Zero : Timeout.InfiniteTimeSpan; 55 | 56 | // 57 | // ping_interval is 33% of ping_timeout time 58 | // 59 | // if elapsed_time < ping_interval then TRY to send ping frame 60 | // if elapsed_time > ping_interval then ENFORCE sending ping frame because ping_timeout is near 61 | // 62 | // pingFrameLockTimeout is controlling TRY/ENFORCE behaviour. Zero mean TRY to take write lock or skip. InfiniteTimeSpan mean wait indefinitely for write lock. 63 | // 64 | 65 | await this.connection.SendFrameAsync(pingFrame, pingFrameLockTimeout, SendOptions.NoErrors, CancellationToken.None).ConfigureAwait(false); 66 | } 67 | 68 | /// 69 | public override void NotifyActivity() 70 | { 71 | this.lastActivityTime = TimestampToTimeSpan(Stopwatch.GetTimestamp()); 72 | } 73 | public override void NotifyPong(ArraySegment pongBuffer) 74 | { 75 | this.NotifyActivity(); 76 | 77 | var timeDelta = TimestampToTimeSpan(Stopwatch.GetTimestamp() - (long)EndianBitConverter.ToUInt64Le(pongBuffer.Array, pongBuffer.Offset)); 78 | this.connection.latency = TimeSpan.FromMilliseconds(Math.Max(0, timeDelta.TotalMilliseconds / 2)); 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/NamedPipes/NamedPipeConnection.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | #if !NAMED_PIPES_DISABLE 6 | using System; 7 | using System.IO; 8 | using System.IO.Pipes; 9 | using System.Net; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | using vtortola.WebSockets.Tools; 13 | 14 | namespace vtortola.WebSockets.Transports.NamedPipes 15 | { 16 | internal class NamedPipeConnection : NetworkConnection 17 | { 18 | private readonly PipeStream pipeStream; 19 | 20 | public string PipeName { get; } 21 | 22 | public virtual int OutBufferSize => this.pipeStream.OutBufferSize; 23 | public virtual int InBufferSize => this.pipeStream.InBufferSize; 24 | public bool IsConnected => this.pipeStream.IsConnected; 25 | public bool CanWrite => this.pipeStream.CanWrite; 26 | public bool CanRead => this.pipeStream.CanRead; 27 | public bool IsAsync => this.pipeStream.IsAsync; 28 | 29 | /// 30 | public override EndPoint LocalEndPoint { get; } 31 | /// 32 | public override EndPoint RemoteEndPoint { get; } 33 | 34 | public NamedPipeConnection(PipeStream pipeStream, string pipeName) 35 | { 36 | if (pipeStream == null) throw new ArgumentNullException(nameof(pipeStream)); 37 | if (pipeName == null) throw new ArgumentNullException(nameof(pipeName)); 38 | 39 | this.pipeStream = pipeStream; 40 | this.PipeName = pipeName; 41 | this.RemoteEndPoint = this.LocalEndPoint = new NamedPipeEndPoint(pipeName); 42 | } 43 | 44 | /// 45 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 46 | { 47 | return this.pipeStream.ReadAsync(buffer, offset, count, cancellationToken); 48 | } 49 | /// 50 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 51 | { 52 | return this.pipeStream.WriteAsync(buffer, offset, count, cancellationToken); 53 | } 54 | /// 55 | public override Task FlushAsync(CancellationToken cancellationToken) 56 | { 57 | if (cancellationToken.IsCancellationRequested) 58 | return TaskHelper.CanceledTask; 59 | 60 | return this.pipeStream.FlushAsync(cancellationToken); 61 | } 62 | 63 | /// 64 | public override Stream AsStream() 65 | { 66 | return this.pipeStream; 67 | } 68 | 69 | /// 70 | public override Task CloseAsync() 71 | { 72 | try 73 | { 74 | this.pipeStream.Close(); 75 | return TaskHelper.CompletedTask; 76 | } 77 | catch (Exception closeError) when (closeError.Unwrap() is ThreadAbortException == false) 78 | { 79 | if (closeError is ObjectDisposedException) 80 | return TaskHelper.CompletedTask; 81 | 82 | return TaskHelper.FailedTask(closeError); 83 | } 84 | 85 | } 86 | 87 | /// 88 | public override void Dispose(bool disposed) 89 | { 90 | SafeEnd.Dispose(this.pipeStream); 91 | } 92 | 93 | /// 94 | public override string ToString() 95 | { 96 | // ReSharper disable HeapView.BoxingAllocation 97 | return $"{nameof(NamedPipeConnection)}, pipe: {this.PipeName}, connected: {this.IsConnected}, " + 98 | $"can write: {this.CanWrite}, can read: {this.CanRead} in buffer: {this.InBufferSize}, out buffer: {this.OutBufferSize}"; 99 | // ReSharper restore HeapView.BoxingAllocation 100 | } 101 | } 102 | } 103 | #endif 104 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Http/WebSocketHandshake.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Runtime.ExceptionServices; 5 | using System.Security.Cryptography; 6 | using System.Text; 7 | using System.Threading; 8 | using JetBrains.Annotations; 9 | using vtortola.WebSockets.Http; 10 | 11 | namespace vtortola.WebSockets 12 | { 13 | internal class WebSocketHandshake : IComparable, IEquatable 14 | { 15 | private static long LastId = 1; 16 | 17 | private bool _invalidated; 18 | 19 | public readonly long Id; 20 | 21 | [NotNull] 22 | public readonly WebSocketHttpRequest Request; 23 | public readonly WebSocketHttpResponse Response; 24 | public readonly List NegotiatedMessageExtensions; 25 | 26 | public bool IsWebSocketRequest { get; internal set; } 27 | public bool IsVersionSupported { get; internal set; } 28 | public bool IsResponseSent { get; internal set; } 29 | public WebSocketFactory Factory { get; internal set; } 30 | public ExceptionDispatchInfo Error { get; internal set; } 31 | 32 | public bool IsValidWebSocketRequest 33 | { 34 | get 35 | { 36 | return !_invalidated && Error == null && IsWebSocketRequest && IsVersionSupported && Response.Status == HttpStatusCode.SwitchingProtocols; 37 | } 38 | set { _invalidated = !value; } 39 | } 40 | public bool IsValidHttpRequest 41 | { 42 | get 43 | { 44 | return !_invalidated && Error == null; 45 | } 46 | set { _invalidated = !value; } 47 | } 48 | 49 | public WebSocketHandshake([NotNull] WebSocketHttpRequest request) 50 | { 51 | if (request == null) throw new ArgumentNullException(nameof(request)); 52 | 53 | this.Id = Interlocked.Increment(ref LastId); 54 | this.Request = request; 55 | this.Response = new WebSocketHttpResponse(); 56 | this.NegotiatedMessageExtensions = new List(); 57 | } 58 | 59 | public string ComputeHandshake() 60 | { 61 | var webSocketKey = this.Request.Headers[RequestHeader.WebSocketKey]; 62 | if (string.IsNullOrEmpty(webSocketKey)) throw new InvalidOperationException($"Missing or wrong {Headers.GetHeaderName(RequestHeader.WebSocketKey)} header in request."); 63 | 64 | using (var sha1 = SHA1.Create()) 65 | return Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(webSocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))); 66 | } 67 | public string GenerateClientNonce() 68 | { 69 | return Convert.ToBase64String(Guid.NewGuid().ToByteArray()); 70 | } 71 | 72 | /// 73 | public int CompareTo(WebSocketHandshake other) 74 | { 75 | if (other == null) return 1; 76 | return this.Id.CompareTo(other.Id); 77 | } 78 | /// 79 | public bool Equals(WebSocketHandshake other) 80 | { 81 | if (ReferenceEquals(other, null)) return false; 82 | if (ReferenceEquals(this, other)) return true; 83 | 84 | return this.Id == other.Id; 85 | } 86 | /// 87 | public override bool Equals(object obj) 88 | { 89 | return this.Equals(obj as WebSocketHandshake); 90 | } 91 | /// 92 | public override int GetHashCode() 93 | { 94 | return this.Id.GetHashCode(); 95 | } 96 | 97 | /// 98 | public override string ToString() 99 | { 100 | return $"Handshake, id: {this.Id}, request: {this.Request}, response: {this.Response}"; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/AsyncConditionSourceTests.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | using vtortola.WebSockets.Async; 5 | using Xunit; 6 | using Xunit.Abstractions; 7 | 8 | namespace vtortola.WebSockets.UnitTests 9 | { 10 | public sealed class AsyncConditionSourceTests 11 | { 12 | private readonly TestLogger logger; 13 | 14 | public AsyncConditionSourceTests(ITestOutputHelper output) 15 | { 16 | this.logger = new TestLogger(output); 17 | } 18 | 19 | [Theory] 20 | [InlineData(2)] 21 | [InlineData(5)] 22 | [InlineData(10)] 23 | [InlineData(40)] 24 | [InlineData(80)] 25 | [InlineData(100)] 26 | [InlineData(200)] 27 | [InlineData(500)] 28 | [InlineData(1000)] 29 | public async Task ParallelSetContinuationTest(int subscribers) 30 | { 31 | var condition = new AsyncConditionSource(); 32 | 33 | var hits = 0; 34 | var parallelLoopResult = Parallel.For(0, subscribers, i => 35 | { 36 | if (i == subscribers / 2) 37 | condition.Set(); 38 | condition.GetAwaiter().OnCompleted(() => Interlocked.Increment(ref hits)); 39 | }); 40 | 41 | while (parallelLoopResult.IsCompleted == false) 42 | await Task.Delay(10).ConfigureAwait(false); 43 | 44 | var sw = Stopwatch.StartNew(); 45 | while (sw.ElapsedMilliseconds < 1000 && subscribers != hits) 46 | Thread.Sleep(10); 47 | 48 | this.logger.Debug($"[TEST] subscribers: {subscribers}, hits: {hits}."); 49 | 50 | Assert.Equal(subscribers, hits); 51 | } 52 | 53 | [Theory] 54 | [InlineData(1)] 55 | [InlineData(2)] 56 | [InlineData(5)] 57 | [InlineData(10)] 58 | [InlineData(40)] 59 | [InlineData(80)] 60 | [InlineData(100)] 61 | [InlineData(200)] 62 | [InlineData(500)] 63 | [InlineData(1000)] 64 | public async Task BeforeSetContinuationTest(int subscribers) 65 | { 66 | var condition = new AsyncConditionSource(isSet: true); 67 | 68 | var hits = 0; 69 | var parallelLoopResult = Parallel.For(0, subscribers, i => 70 | { 71 | condition.GetAwaiter().OnCompleted(() => Interlocked.Increment(ref hits)); 72 | }); 73 | 74 | while (parallelLoopResult.IsCompleted == false) 75 | await Task.Delay(10).ConfigureAwait(false); 76 | 77 | var sw = Stopwatch.StartNew(); 78 | while (sw.ElapsedMilliseconds < 1000 && subscribers != hits) 79 | Thread.Sleep(10); 80 | 81 | this.logger.Debug($"[TEST] subscribers: {subscribers}, hits: {hits}."); 82 | 83 | Assert.Equal(subscribers, hits); 84 | } 85 | 86 | [Theory] 87 | [InlineData(1)] 88 | [InlineData(2)] 89 | [InlineData(5)] 90 | [InlineData(10)] 91 | [InlineData(40)] 92 | [InlineData(80)] 93 | [InlineData(100)] 94 | [InlineData(200)] 95 | [InlineData(500)] 96 | [InlineData(1000)] 97 | public async Task AfterSetContinuationTest(int subscribers) 98 | { 99 | var condition = new AsyncConditionSource(isSet: false); 100 | 101 | var hits = 0; 102 | var parallelLoopResult = Parallel.For(0, subscribers, i => 103 | { 104 | condition.GetAwaiter().OnCompleted(() => Interlocked.Increment(ref hits)); 105 | }); 106 | 107 | while (parallelLoopResult.IsCompleted == false) 108 | await Task.Delay(10).ConfigureAwait(false); 109 | 110 | condition.Set(); 111 | 112 | var sw = Stopwatch.StartNew(); 113 | while (sw.ElapsedMilliseconds < 1000 && subscribers != hits) 114 | Thread.Sleep(10); 115 | 116 | this.logger.Debug($"[TEST] subscribers: {subscribers}, hits: {hits}."); 117 | 118 | Assert.Equal(subscribers, hits); 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/EchoServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using vtortola.WebSockets.Tools; 6 | 7 | namespace vtortola.WebSockets.UnitTests 8 | { 9 | public sealed class EchoServer : IDisposable 10 | { 11 | private readonly ILogger log; 12 | private readonly WebSocketListener listener; 13 | private CancellationTokenSource startCancellation; 14 | 15 | public int ReceivedMessages; 16 | public int SentMessages; 17 | public int Errors; 18 | 19 | public List DetailedErrors; 20 | 21 | public EchoServer(Uri[] listenEndPoints, WebSocketListenerOptions options) 22 | { 23 | if (listenEndPoints == null) throw new ArgumentNullException(nameof(listenEndPoints)); 24 | if (options == null) throw new ArgumentNullException(nameof(options)); 25 | 26 | this.log = options.Logger; 27 | this.listener = new WebSocketListener(listenEndPoints, options); 28 | this.DetailedErrors = new List(); 29 | } 30 | 31 | public async Task StartAsync() 32 | { 33 | await this.listener.StartAsync().ConfigureAwait(false); 34 | this.startCancellation = new CancellationTokenSource(); 35 | this.StartAcceptingConnectionsAsync(this.startCancellation.Token).LogFault(this.log); 36 | } 37 | public Task StopAsync() 38 | { 39 | this.startCancellation?.Cancel(); 40 | return this.listener.StopAsync(); 41 | } 42 | 43 | private async Task StartAcceptingConnectionsAsync(CancellationToken cancellation) 44 | { 45 | await Task.Yield(); 46 | 47 | while (this.listener.IsStarted) 48 | { 49 | cancellation.ThrowIfCancellationRequested(); 50 | 51 | var webSocket = await this.listener.AcceptWebSocketAsync(cancellation).ConfigureAwait(false); 52 | if (webSocket == null) 53 | return; 54 | 55 | this.StartEchoingMessagesAsync(webSocket, cancellation).LogFault(this.log); 56 | } 57 | } 58 | 59 | private async Task StartEchoingMessagesAsync(WebSocket webSocket, CancellationToken cancellation) 60 | { 61 | await Task.Yield(); 62 | try 63 | { 64 | while (this.listener.IsStarted) 65 | { 66 | cancellation.ThrowIfCancellationRequested(); 67 | 68 | var message = await webSocket.ReadStringAsync(cancellation).ConfigureAwait(false); 69 | if (message == null) 70 | return; 71 | 72 | Interlocked.Increment(ref this.ReceivedMessages); 73 | 74 | if (webSocket.IsConnected == false) 75 | { 76 | Interlocked.Increment(ref this.Errors); 77 | return; 78 | } 79 | 80 | await webSocket.WriteStringAsync(message, cancellation).ConfigureAwait(false); 81 | Interlocked.Increment(ref this.SentMessages); 82 | } 83 | } 84 | catch (Exception error) 85 | { 86 | lock (this.DetailedErrors) 87 | this.DetailedErrors.Add(error.Unwrap()); 88 | 89 | Interlocked.Increment(ref this.Errors); 90 | } 91 | } 92 | 93 | public void PushErrorMessagesTo(SortedDictionary errorMessages) 94 | { 95 | if (errorMessages == null) throw new ArgumentNullException(nameof(errorMessages)); 96 | 97 | var ct = 0; 98 | lock (this.DetailedErrors) 99 | foreach (var error in this.DetailedErrors) 100 | if (errorMessages.TryGetValue(error.Message, out ct)) 101 | errorMessages[error.Message] = ct + 1; 102 | else 103 | errorMessages[error.Message] = 1; 104 | } 105 | 106 | /// 107 | public void Dispose() 108 | { 109 | this.listener?.Dispose(); 110 | } 111 | 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /vtortola.WebSockets/Transports/Sockets/SocketTransport.cs: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Denis Zykov 3 | License: https://opensource.org/licenses/MIT 4 | */ 5 | using System; 6 | using System.Net; 7 | using System.Net.Sockets; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using vtortola.WebSockets.Async; 11 | using vtortola.WebSockets.Transports.UnixSockets; 12 | 13 | namespace vtortola.WebSockets.Transports.Sockets 14 | { 15 | public abstract class SocketTransport : WebSocketTransport 16 | { 17 | public const int DEFAULT_BACKLOG_SIZE = 5; 18 | 19 | public int BacklogSize { get; set; } = DEFAULT_BACKLOG_SIZE; 20 | 21 | protected abstract EndPoint GetRemoteEndPoint(Uri address); 22 | protected abstract ProtocolType GetProtocolType(Uri address, EndPoint remoteEndPoint); 23 | protected virtual void SetupClientSocket(Socket socket, EndPoint remoteEndPoint) 24 | { 25 | } 26 | 27 | /// 28 | internal override async Task ConnectAsync(Uri address, WebSocketListenerOptions options, CancellationToken cancellation) 29 | { 30 | if (address == null) throw new ArgumentNullException(nameof(address)); 31 | if (options == null) throw new ArgumentNullException(nameof(options)); 32 | 33 | var remoteEndPoint = this.GetRemoteEndPoint(address); 34 | var protocolType = this.GetProtocolType(address, remoteEndPoint); 35 | // prepare socket 36 | var socket = new Socket(remoteEndPoint.AddressFamily, SocketType.Stream, protocolType); 37 | this.SetupClientSocket(socket, remoteEndPoint); 38 | try 39 | { 40 | // prepare connection 41 | var socketConnectedCondition = new AsyncConditionSource 42 | { 43 | ContinueOnCapturedContext = false 44 | }; 45 | var socketAsyncEventArgs = new SocketAsyncEventArgs 46 | { 47 | RemoteEndPoint = remoteEndPoint, 48 | UserToken = socketConnectedCondition 49 | }; 50 | 51 | // connect 52 | socketAsyncEventArgs.Completed += (_, e) => ((AsyncConditionSource)e.UserToken).Set(); 53 | 54 | // interrupt connection when cancellation token is set 55 | var connectInterruptRegistration = cancellation.CanBeCanceled ? 56 | cancellation.Register(s => ((AsyncConditionSource)s).Interrupt(new OperationCanceledException()), socketConnectedCondition) : 57 | default(CancellationTokenRegistration); 58 | using (connectInterruptRegistration) 59 | { 60 | if (socket.ConnectAsync(socketAsyncEventArgs) == false) 61 | socketConnectedCondition.Set(); 62 | 63 | await socketConnectedCondition; 64 | } 65 | cancellation.ThrowIfCancellationRequested(); 66 | 67 | // check connection result 68 | if (socketAsyncEventArgs.ConnectByNameError != null) 69 | throw socketAsyncEventArgs.ConnectByNameError; 70 | 71 | if (socketAsyncEventArgs.SocketError != SocketError.Success) 72 | throw new WebSocketException($"Failed to open socket to '{address}' due error '{socketAsyncEventArgs.SocketError}'.", 73 | new SocketException((int)socketAsyncEventArgs.SocketError)); 74 | 75 | var localEndPoint = default(EndPoint); 76 | try 77 | { 78 | localEndPoint = socket.LocalEndPoint; 79 | } 80 | catch 81 | { 82 | if (UnixSocketTransport.IsUnixEndPoint(remoteEndPoint)) 83 | { 84 | localEndPoint = remoteEndPoint; 85 | } 86 | } 87 | 88 | var connection = new SocketConnection(socket, localEndPoint); 89 | socket = null; 90 | return connection; 91 | } 92 | finally 93 | { 94 | if (socket != null) 95 | SafeEnd.Dispose(socket, options.Logger); 96 | } 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tests/WebSocketListener.UnitTests/BufferManagerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Xunit; 3 | 4 | namespace vtortola.WebSockets.UnitTests 5 | { 6 | public class BufferManagerTests 7 | { 8 | [Theory, 9 | InlineData(1), 10 | InlineData(2), 11 | InlineData(8), 12 | InlineData(64), 13 | InlineData(256), 14 | InlineData(333), 15 | InlineData(800), 16 | InlineData(1024), 17 | InlineData(2047), 18 | InlineData(2048), 19 | InlineData(2049)] 20 | public void TakeBuffer(int maxBufferSize) 21 | { 22 | var bufferManager = BufferManager.CreateBufferManager(maxBufferSize * 100, maxBufferSize); 23 | var buffer = bufferManager.TakeBuffer(maxBufferSize); 24 | 25 | Assert.NotNull(buffer); 26 | Assert.True(buffer.Length >= maxBufferSize, "buffer.Length >= maxBufferSize"); 27 | } 28 | 29 | [Theory, 30 | InlineData(1024, 1), 31 | InlineData(1024, 2), 32 | InlineData(1024, 8), 33 | InlineData(1024, 64), 34 | InlineData(1024, 256), 35 | InlineData(1024, 333), 36 | InlineData(1024, 800), 37 | InlineData(4096, 1), 38 | InlineData(4096, 2), 39 | InlineData(4096, 8), 40 | InlineData(4096, 64), 41 | InlineData(4096, 256), 42 | InlineData(4096, 333), 43 | InlineData(4096, 800), 44 | InlineData(4096, 1024), 45 | InlineData(4096, 2047), 46 | InlineData(4096, 2048), 47 | InlineData(4096, 2049)] 48 | public void TakeSmallBuffer(int maxBufferSize, int takeBufferSize) 49 | { 50 | var bufferManager = BufferManager.CreateBufferManager(maxBufferSize * 100, maxBufferSize); 51 | var buffer = bufferManager.TakeBuffer(takeBufferSize); 52 | 53 | Assert.NotNull(buffer); 54 | Assert.True(buffer.Length >= takeBufferSize, "buffer.Length >= maxBufferSize"); 55 | } 56 | 57 | [Theory, 58 | InlineData(1), 59 | InlineData(2), 60 | InlineData(8), 61 | InlineData(64), 62 | InlineData(256), 63 | InlineData(333), 64 | InlineData(800), 65 | InlineData(1024), 66 | InlineData(2047), 67 | InlineData(2048), 68 | InlineData(2049)] 69 | public void ReturnBuffer(int maxBufferSize) 70 | { 71 | var bufferManager = BufferManager.CreateBufferManager(maxBufferSize * 100, maxBufferSize); 72 | var buffer = bufferManager.TakeBuffer(maxBufferSize); 73 | 74 | Assert.NotNull(buffer); 75 | 76 | bufferManager.ReturnBuffer(buffer); 77 | } 78 | 79 | [Theory, 80 | InlineData(1024, 1), 81 | InlineData(1024, 2), 82 | InlineData(1024, 8), 83 | InlineData(1024, 64), 84 | InlineData(1024, 256), 85 | InlineData(1024, 333), 86 | InlineData(1024, 800), 87 | InlineData(4096, 1), 88 | InlineData(4096, 2), 89 | InlineData(4096, 8), 90 | InlineData(4096, 64), 91 | InlineData(4096, 256), 92 | InlineData(4096, 333), 93 | InlineData(4096, 800), 94 | InlineData(4096, 1024), 95 | InlineData(4096, 2047), 96 | InlineData(4096, 2048), 97 | InlineData(4096, 2049)] 98 | public void ReturnSmallBuffer(int maxBufferSize, int takeBufferSize) 99 | { 100 | var bufferManager = BufferManager.CreateBufferManager(maxBufferSize * 100, maxBufferSize); 101 | var buffer = bufferManager.TakeBuffer(takeBufferSize); 102 | 103 | Assert.NotNull(buffer); 104 | 105 | bufferManager.ReturnBuffer(buffer); 106 | } 107 | [Fact] 108 | public void Construct() 109 | { 110 | var bufferManager = BufferManager.CreateBufferManager(1, 1); 111 | 112 | Assert.NotNull(bufferManager); 113 | } 114 | 115 | [Fact] 116 | public void ConstructWithInvalidFirstParameter() 117 | { 118 | Assert.Throws(() => BufferManager.CreateBufferManager(-1, 1)); 119 | } 120 | 121 | [Fact] 122 | public void ConstructWithInvalidSecondParameters() 123 | { 124 | Assert.Throws(() => BufferManager.CreateBufferManager(1, -1)); 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /vtortola.WebSockets/WebSocketStringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using JetBrains.Annotations; 7 | 8 | namespace vtortola.WebSockets 9 | { 10 | public static class WebSocketStringExtensions 11 | { 12 | private static readonly UTF8Encoding Utf8NoBom = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: false); 13 | 14 | [NotNull, ItemCanBeNull] 15 | public static async Task ReadStringAsync([NotNull] this WebSocket webSocket, CancellationToken cancellationToken = default(CancellationToken)) 16 | { 17 | if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); 18 | 19 | using (var readStream = await webSocket.ReadMessageAsync(cancellationToken).ConfigureAwait(false)) 20 | { 21 | if (readStream == null) 22 | return null; 23 | 24 | using (var reader = new StreamReader(readStream, Utf8NoBom)) 25 | return await reader.ReadToEndAsync().ConfigureAwait(false); 26 | } 27 | } 28 | 29 | public static async Task WriteStringAsync([NotNull] this WebSocket webSocket, [NotNull] string data, CancellationToken cancellationToken = default(CancellationToken)) 30 | { 31 | if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); 32 | if (data == null) throw new ArgumentNullException(nameof(data)); 33 | 34 | cancellationToken.ThrowIfCancellationRequested(); 35 | 36 | using (var msg = webSocket.CreateMessageWriter(WebSocketMessageType.Text)) 37 | using (var writer = new StreamWriter(msg, Utf8NoBom)) 38 | { 39 | await writer.WriteAsync(data).ConfigureAwait(false); 40 | await writer.FlushAsync().ConfigureAwait(false); 41 | await msg.CloseAsync().ConfigureAwait(false); 42 | } 43 | } 44 | 45 | public static async Task WriteStringAsync([NotNull] this WebSocket webSocket, [NotNull] char[] data, int offset, int count, CancellationToken cancellationToken = default(CancellationToken)) 46 | { 47 | if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); 48 | if (data == null) throw new ArgumentNullException(nameof(data)); 49 | if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); 50 | if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); 51 | if (offset + count > data.Length) throw new ArgumentOutOfRangeException(nameof(count)); 52 | 53 | cancellationToken.ThrowIfCancellationRequested(); 54 | 55 | using (var msg = webSocket.CreateMessageWriter(WebSocketMessageType.Text)) 56 | using (var writer = new StreamWriter(msg, Utf8NoBom)) 57 | { 58 | await writer.WriteAsync(data, offset, count).ConfigureAwait(false); 59 | await writer.FlushAsync().ConfigureAwait(false); 60 | await msg.CloseAsync().ConfigureAwait(false); 61 | } 62 | } 63 | 64 | public static Task WriteBytesAsync([NotNull] this WebSocket webSocket, [NotNull] byte[] data, CancellationToken cancellationToken = default(CancellationToken)) 65 | { 66 | if (data == null) throw new ArgumentNullException(nameof(data)); 67 | 68 | return WriteBytesAsync(webSocket, data, 0, data.Length, cancellationToken); 69 | } 70 | 71 | public static async Task WriteBytesAsync([NotNull] this WebSocket webSocket, [NotNull] byte[] data, int offset, int count, CancellationToken cancellationToken = default(CancellationToken)) 72 | { 73 | if (webSocket == null) throw new ArgumentNullException(nameof(webSocket)); 74 | if (data == null) throw new ArgumentNullException(nameof(data)); 75 | if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset)); 76 | if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); 77 | if (offset + count > data.Length) throw new ArgumentOutOfRangeException(nameof(count)); 78 | 79 | cancellationToken.ThrowIfCancellationRequested(); 80 | 81 | using (var writer = webSocket.CreateMessageWriter(WebSocketMessageType.Binary)) 82 | { 83 | await writer.WriteAndCloseAsync(data, offset, count, cancellationToken).ConfigureAwait(false); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /vtortola.WebSockets.Rfc6455/WebSocketRfc6455.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using JetBrains.Annotations; 7 | using vtortola.WebSockets.Http; 8 | using vtortola.WebSockets.Transports; 9 | 10 | namespace vtortola.WebSockets.Rfc6455 11 | { 12 | public class WebSocketRfc6455 : WebSocket 13 | { 14 | private readonly ILogger log; 15 | private readonly IReadOnlyList extensions; 16 | 17 | internal WebSocketConnectionRfc6455 Connection { get; } 18 | 19 | public override EndPoint RemoteEndpoint { get; } 20 | public override EndPoint LocalEndpoint { get; } 21 | public override bool IsConnected => this.Connection.CanReceive || this.Connection.CanSend; 22 | public override TimeSpan Latency => this.Connection.Latency; 23 | public override string SubProtocol { get; } 24 | public override WebSocketCloseReason? CloseReason => this.Connection.CloseReason; 25 | 26 | public WebSocketRfc6455([NotNull] NetworkConnection networkConnection, [NotNull] WebSocketListenerOptions options, [NotNull] WebSocketHttpRequest httpRequest, [NotNull] WebSocketHttpResponse httpResponse, [NotNull] IReadOnlyList extensions) 27 | : base(httpRequest, httpResponse) 28 | { 29 | if (networkConnection == null) throw new ArgumentNullException(nameof(networkConnection)); 30 | if (options == null) throw new ArgumentNullException(nameof(options)); 31 | if (httpRequest == null) throw new ArgumentNullException(nameof(httpRequest)); 32 | if (httpResponse == null) throw new ArgumentNullException(nameof(httpResponse)); 33 | if (extensions == null) throw new ArgumentNullException(nameof(extensions)); 34 | 35 | this.log = options.Logger; 36 | 37 | this.RemoteEndpoint = httpRequest.RemoteEndPoint; 38 | this.LocalEndpoint = httpRequest.LocalEndPoint; 39 | 40 | this.Connection = new WebSocketConnectionRfc6455(networkConnection, httpRequest.Direction == HttpRequestDirection.Outgoing, options); 41 | this.extensions = extensions; 42 | this.SubProtocol = httpResponse.Headers.Contains(ResponseHeader.WebSocketProtocol) ? 43 | httpResponse.Headers[ResponseHeader.WebSocketProtocol] : default(string); 44 | } 45 | public override async Task ReadMessageAsync(CancellationToken token) 46 | { 47 | await this.Connection.AwaitHeaderAsync(token).ConfigureAwait(false); 48 | if (this.Connection.CanReceive && this.Connection.CurrentHeader != null) 49 | { 50 | WebSocketMessageReadStream reader = new WebSocketMessageReadRfc6455Stream(this); 51 | foreach (var extension in this.extensions) 52 | reader = extension.ExtendReader(reader); 53 | return reader; 54 | } 55 | return null; 56 | } 57 | 58 | public override WebSocketMessageWriteStream CreateMessageWriter(WebSocketMessageType messageType) 59 | { 60 | if (!this.Connection.CanSend) 61 | throw new WebSocketException("Unable to write new message because underlying connection is closed or close frame is sent."); 62 | 63 | this.Connection.BeginWriting(); 64 | WebSocketMessageWriteStream writer = new WebSocketMessageWriteRfc6455Stream(this, messageType); 65 | 66 | foreach (var extension in this.extensions) 67 | writer = extension.ExtendWriter(writer); 68 | 69 | return writer; 70 | } 71 | /// 72 | public override Task SendPingAsync(byte[] data, int offset, int count) 73 | { 74 | if (data != null) 75 | { 76 | if (offset < 0 || offset > data.Length) throw new ArgumentOutOfRangeException(nameof(offset)); 77 | if (count < 0 || count > 125 || offset + count > data.Length) throw new ArgumentOutOfRangeException(nameof(count)); 78 | } 79 | 80 | return this.Connection.PingAsync(data, offset, count); 81 | } 82 | 83 | public override Task CloseAsync() 84 | { 85 | return this.Connection.CloseAsync(WebSocketCloseReason.NormalClose); 86 | } 87 | 88 | public override Task CloseAsync(WebSocketCloseReason closeReason) 89 | { 90 | return this.Connection.CloseAsync(closeReason); 91 | } 92 | 93 | public override void Dispose() 94 | { 95 | SafeEnd.Dispose(this.Connection, this.log); 96 | } 97 | } 98 | } 99 | --------------------------------------------------------------------------------