diff --git a/WebSockets.sln b/WebSockets.sln index a79504e978..8c2c5904a1 100644 --- a/WebSockets.sln +++ b/WebSockets.sln @@ -10,8 +10,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{9E55 EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Protocol", "src\Microsoft.AspNetCore.WebSockets.Protocol\Microsoft.AspNetCore.WebSockets.Protocol.xproj", "{E0C10DEC-3339-4A47-85BC-3100C5D34AD4}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Protocol.Test", "test\Microsoft.AspNetCore.WebSockets.Protocol.Test\Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj", "{62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Client", "src\Microsoft.AspNetCore.WebSockets.Client\Microsoft.AspNetCore.WebSockets.Client.xproj", "{4A1C4875-AE21-4A78-979A-F0E4DF5EB518}" EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Client.Test", "test\Microsoft.AspNetCore.WebSockets.Client.Test\Microsoft.AspNetCore.WebSockets.Client.Test.xproj", "{6604D154-817F-4BC5-BE95-FF7E851179D9}" @@ -27,14 +25,21 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "TestClient", "samples\TestC EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestServer", "samples\TestServer\TestServer.csproj", "{4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Server.Test", "test\Microsoft.AspNetCore.WebSockets.Server.Test\Microsoft.AspNetCore.WebSockets.Server.Test.xproj", "{E82D9F64-8AFA-4DCB-A842-2283FDA73BE8}" -EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AutobahnTestApp", "test\AutobahnTestApp\AutobahnTestApp.xproj", "{9755F612-A155-4BDD-9E20-37ADE0B4B3BA}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutobahnTestAppAspNet4", "samples\AutobahnTestAppAspNet4\AutobahnTestAppAspNet4.csproj", "{72E3AB32-682F-42AF-B7C7-0B777244FF11}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutobahnTestAppHttpListener", "samples\AutobahnTestAppHttpListener\AutobahnTestAppHttpListener.csproj", "{B7246F23-6A4B-492F-AB61-292AA1A9E9D5}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scripts", "scripts", "{19595D64-E42E-46FD-AB2E-BDC870724EE7}" + ProjectSection(SolutionItems) = preProject + scripts\UpdateCoreFxCode.ps1 = scripts\UpdateCoreFxCode.ps1 + EndProjectSection +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Server.ConformanceTest", "test\Microsoft.AspNetCore.WebSockets.Server.ConformanceTest\Microsoft.AspNetCore.WebSockets.Server.ConformanceTest.xproj", "{A722BB6C-9114-4F25-9BB0-2191D4405F3A}" +EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.WebSockets.Protocol.Test", "test\Microsoft.AspNetCore.WebSockets.Protocol.Test\Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj", "{AAF2DFCF-845E-4410-BBF0-0683AD60DD6A}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -45,10 +50,6 @@ Global {E0C10DEC-3339-4A47-85BC-3100C5D34AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {E0C10DEC-3339-4A47-85BC-3100C5D34AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {E0C10DEC-3339-4A47-85BC-3100C5D34AD4}.Release|Any CPU.Build.0 = Release|Any CPU - {62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32}.Debug|Any CPU.Build.0 = Debug|Any CPU - {62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32}.Release|Any CPU.ActiveCfg = Release|Any CPU - {62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32}.Release|Any CPU.Build.0 = Release|Any CPU {4A1C4875-AE21-4A78-979A-F0E4DF5EB518}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4A1C4875-AE21-4A78-979A-F0E4DF5EB518}.Debug|Any CPU.Build.0 = Debug|Any CPU {4A1C4875-AE21-4A78-979A-F0E4DF5EB518}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -69,10 +70,6 @@ Global {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Debug|Any CPU.Build.0 = Debug|Any CPU {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.ActiveCfg = Release|Any CPU {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B}.Release|Any CPU.Build.0 = Release|Any CPU - {E82D9F64-8AFA-4DCB-A842-2283FDA73BE8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {E82D9F64-8AFA-4DCB-A842-2283FDA73BE8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {E82D9F64-8AFA-4DCB-A842-2283FDA73BE8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {E82D9F64-8AFA-4DCB-A842-2283FDA73BE8}.Release|Any CPU.Build.0 = Release|Any CPU {9755F612-A155-4BDD-9E20-37ADE0B4B3BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9755F612-A155-4BDD-9E20-37ADE0B4B3BA}.Debug|Any CPU.Build.0 = Debug|Any CPU {9755F612-A155-4BDD-9E20-37ADE0B4B3BA}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -85,21 +82,29 @@ Global {B7246F23-6A4B-492F-AB61-292AA1A9E9D5}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7246F23-6A4B-492F-AB61-292AA1A9E9D5}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7246F23-6A4B-492F-AB61-292AA1A9E9D5}.Release|Any CPU.Build.0 = Release|Any CPU + {A722BB6C-9114-4F25-9BB0-2191D4405F3A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A722BB6C-9114-4F25-9BB0-2191D4405F3A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A722BB6C-9114-4F25-9BB0-2191D4405F3A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A722BB6C-9114-4F25-9BB0-2191D4405F3A}.Release|Any CPU.Build.0 = Release|Any CPU + {AAF2DFCF-845E-4410-BBF0-0683AD60DD6A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AAF2DFCF-845E-4410-BBF0-0683AD60DD6A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AAF2DFCF-845E-4410-BBF0-0683AD60DD6A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AAF2DFCF-845E-4410-BBF0-0683AD60DD6A}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {E0C10DEC-3339-4A47-85BC-3100C5D34AD4} = {2C7947A5-9FBD-4267-97C1-2D726D7B3BAF} - {62A07A24-4D06-4DDA-B6BF-02D0C9CB7D32} = {C45106D0-76C8-4776-A140-F7DD83CA2958} {4A1C4875-AE21-4A78-979A-F0E4DF5EB518} = {2C7947A5-9FBD-4267-97C1-2D726D7B3BAF} {6604D154-817F-4BC5-BE95-FF7E851179D9} = {C45106D0-76C8-4776-A140-F7DD83CA2958} {78A097D0-C0A4-4AED-93E2-84A65392FB52} = {2C7947A5-9FBD-4267-97C1-2D726D7B3BAF} {8C8EAC01-DC49-4C5E-B348-E4E46FE675F9} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} {4E5F5FCC-172C-44D9-BEA0-39098A79CD0B} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} - {E82D9F64-8AFA-4DCB-A842-2283FDA73BE8} = {C45106D0-76C8-4776-A140-F7DD83CA2958} {9755F612-A155-4BDD-9E20-37ADE0B4B3BA} = {C45106D0-76C8-4776-A140-F7DD83CA2958} {72E3AB32-682F-42AF-B7C7-0B777244FF11} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} {B7246F23-6A4B-492F-AB61-292AA1A9E9D5} = {9E55FC5B-FD9C-4266-AB24-F3AA649D7C8B} + {A722BB6C-9114-4F25-9BB0-2191D4405F3A} = {C45106D0-76C8-4776-A140-F7DD83CA2958} + {AAF2DFCF-845E-4410-BBF0-0683AD60DD6A} = {C45106D0-76C8-4776-A140-F7DD83CA2958} EndGlobalSection EndGlobal diff --git a/scripts/UpdateCoreFxCode.ps1 b/scripts/UpdateCoreFxCode.ps1 new file mode 100644 index 0000000000..bc1d772500 --- /dev/null +++ b/scripts/UpdateCoreFxCode.ps1 @@ -0,0 +1,44 @@ +param([string]$CoreFxRepoRoot) + +$RepoRoot = Split-Path -Parent $PSScriptRoot + +$FilesToCopy = @( + "src\System.Net.WebSockets.Client\src\System\Net\WebSockets\ManagedWebSocket.cs", + "src\Common\src\System\Net\WebSockets\WebSocketValidate.cs" +) + +if(!$CoreFxRepoRoot) { + $CoreFxRepoRoot = "$RepoRoot\..\..\dotnet\corefx" +} + +if(!(Test-Path $CoreFxRepoRoot)) { + throw "Could not find CoreFx repo at $CoreFxRepoRoot" +} +$CoreFxRepoRoot = Convert-Path $CoreFxRepoRoot + +$DestinationRoot = "$RepoRoot\src\Microsoft.AspNetCore.WebSockets.Protocol\ext" + +$FilesToCopy | foreach { + $Source = Join-Path $CoreFxRepoRoot $_ + $Destination = Join-Path $DestinationRoot $_ + $DestinationDir = Split-Path -Parent $Destination + + if(!(Test-Path $Source)) { + Write-Warning "Can't find source file: $Source" + } else { + if(!(Test-Path $DestinationDir)) { + mkdir $DestinationDir | Out-Null + } + if(Test-Path $Destination) { + del $Destination + } + Write-Host "Copying $_" + + $SourceCode = [IO.File]::ReadAllText($Source) + $SourceCode = $SourceCode.Replace("Task.FromException", "CompatHelpers.FromException") + $SourceCode = $SourceCode.Replace("Task.CompletedTask", "CompatHelpers.CompletedTask") + $SourceCode = $SourceCode.Replace("Array.Empty", "CompatHelpers.Empty") + $SourceCode = $SourceCode.Replace("nameof(ClientWebSocket)", "`"ClientWebSocket`"") + [IO.File]::WriteAllText($Destination, $SourceCode) + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.WebSockets.Client/WebSocketClient.cs b/src/Microsoft.AspNetCore.WebSockets.Client/WebSocketClient.cs index 8e924734f1..7445a80873 100644 --- a/src/Microsoft.AspNetCore.WebSockets.Client/WebSocketClient.cs +++ b/src/Microsoft.AspNetCore.WebSockets.Client/WebSocketClient.cs @@ -53,12 +53,6 @@ namespace Microsoft.AspNetCore.WebSockets.Client set; } - public bool UseZeroMask - { - get; - set; - } - public Action ConfigureRequest { get; @@ -114,7 +108,7 @@ namespace Microsoft.AspNetCore.WebSockets.Client Stream stream = response.GetResponseStream(); - return CommonWebSocket.CreateClientWebSocket(stream, subProtocol, KeepAliveInterval, ReceiveBufferSize, useZeroMask: UseZeroMask); + return CommonWebSocket.CreateClientWebSocket(stream, subProtocol, KeepAliveInterval, ReceiveBufferSize); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/CommonWebSocket.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/CommonWebSocket.cs index 56baf28d2f..78f29aa9ca 100644 --- a/src/Microsoft.AspNetCore.WebSockets.Protocol/CommonWebSocket.cs +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/CommonWebSocket.cs @@ -2,644 +2,31 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Diagnostics.Contracts; using System.IO; -using System.Linq; using System.Net.WebSockets; -using System.Security.Cryptography; -using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace Microsoft.AspNetCore.WebSockets.Protocol { - // https://tools.ietf.org/html/rfc6455 - public class CommonWebSocket : WebSocket + public static class CommonWebSocket { - private readonly static byte[] PingBuffer = Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz"); - private static readonly RandomNumberGenerator _rng = RandomNumberGenerator.Create(); - - private readonly Stream _stream; - private readonly string _subProtocl; - private readonly bool _maskOutput; - private readonly bool _unmaskInput; - private readonly bool _useZeroMask; - private readonly SemaphoreSlim _writeLock; - private readonly Timer _keepAliveTimer; - - private WebSocketState _state; - - private WebSocketCloseStatus? _closeStatus; - private string _closeStatusDescription; - - private bool _isOutgoingMessageInProgress; - - private byte[] _receiveBuffer; - private int _receiveBufferOffset; - private int _receiveBufferBytes; - - private FrameHeader _frameInProgress; - private long _frameBytesRemaining; - private int? _firstDataOpCode; - private int _dataUnmaskOffset; - private Utilities.Utf8MessageState _incomingUtf8MessageState = new Utilities.Utf8MessageState(); - - public CommonWebSocket(Stream stream, string subProtocol, TimeSpan keepAliveInterval, int receiveBufferSize, bool maskOutput, bool useZeroMask, bool unmaskInput) + public static WebSocket CreateClientWebSocket(Stream stream, string subProtocol, TimeSpan keepAliveInterval, int receiveBufferSize) { - _stream = stream; - _subProtocl = subProtocol; - _state = WebSocketState.Open; - _receiveBuffer = new byte[receiveBufferSize]; - _maskOutput = maskOutput; - _useZeroMask = useZeroMask; - _unmaskInput = unmaskInput; - _writeLock = new SemaphoreSlim(1); - if (keepAliveInterval != Timeout.InfiniteTimeSpan) - { - _keepAliveTimer = new Timer(SendKeepAlive, this, keepAliveInterval, keepAliveInterval); - } + return ManagedWebSocket.CreateFromConnectedStream( + stream, + isServer: false, + subprotocol: subProtocol, + keepAliveIntervalSeconds: (int)keepAliveInterval.TotalSeconds, + receiveBufferSize: receiveBufferSize); } - public static CommonWebSocket CreateClientWebSocket(Stream stream, string subProtocol, TimeSpan keepAliveInterval, int receiveBufferSize, bool useZeroMask) + public static WebSocket CreateServerWebSocket(Stream stream, string subProtocol, TimeSpan keepAliveInterval, int receiveBufferSize) { - return new CommonWebSocket(stream, subProtocol, keepAliveInterval, receiveBufferSize, maskOutput: true, useZeroMask: useZeroMask, unmaskInput: false); - } - - public static CommonWebSocket CreateServerWebSocket(Stream stream, string subProtocol, TimeSpan keepAliveInterval, int receiveBufferSize) - { - return new CommonWebSocket(stream, subProtocol, keepAliveInterval, receiveBufferSize, maskOutput: false, useZeroMask: false, unmaskInput: true); - } - - public override WebSocketCloseStatus? CloseStatus - { - get { return _closeStatus; } - } - - public override string CloseStatusDescription - { - get { return _closeStatusDescription; } - } - - public override WebSocketState State - { - get { return _state; } - } - - public override string SubProtocol - { - get { return _subProtocl; } - } - - // https://tools.ietf.org/html/rfc6455#section-5.3 - // The masking key is a 32-bit value chosen at random by the client. - // When preparing a masked frame, the client MUST pick a fresh masking - // key from the set of allowed 32-bit values. The masking key needs to - // be unpredictable; thus, the masking key MUST be derived from a strong - // source of entropy, and the masking key for a given frame MUST NOT - // make it simple for a server/proxy to predict the masking key for a - // subsequent frame. The unpredictability of the masking key is - // essential to prevent authors of malicious applications from selecting - // the bytes that appear on the wire. RFC 4086 [RFC4086] discusses what - // entails a suitable source of entropy for security-sensitive - // applications. - private int GetNextMask() - { - if (_useZeroMask) - { - return 0; - } - - // Get 32-bits of randomness and convert it to an int - var buffer = new byte[sizeof(int)]; - _rng.GetBytes(buffer); - return BitConverter.ToInt32(buffer, 0); - } - - public override async Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) - { - ValidateSegment(buffer); - if (messageType != WebSocketMessageType.Binary && messageType != WebSocketMessageType.Text) - { - // Block control frames - throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); - } - - // Check concurrent writes, pings & pongs, or closes - await _writeLock.WaitAsync(cancellationToken); - try - { - ThrowIfDisposed(); - ThrowIfOutputClosed(); - - int mask = GetNextMask(); - int opcode = _isOutgoingMessageInProgress ? Constants.OpCodes.ContinuationFrame : Utilities.GetOpCode(messageType); - FrameHeader frameHeader = new FrameHeader(endOfMessage, opcode, _maskOutput, mask, buffer.Count); - ArraySegment headerSegment = frameHeader.Buffer; - - if (_maskOutput && mask != 0) - { - // TODO: For larger messages consider using a limited size buffer and masking & sending in segments. - byte[] maskedFrame = Utilities.MergeAndMask(mask, headerSegment, buffer); - await _stream.WriteAsync(maskedFrame, 0, maskedFrame.Length, cancellationToken); - } - else - { - await _stream.WriteAsync(headerSegment.Array, headerSegment.Offset, headerSegment.Count, cancellationToken); - await _stream.WriteAsync(buffer.Array, buffer.Offset, buffer.Count, cancellationToken); - } - - _isOutgoingMessageInProgress = !endOfMessage; - } - finally - { - _writeLock.Release(); - } - } - - private static void SendKeepAlive(object state) - { - CommonWebSocket websocket = (CommonWebSocket)state; - websocket.SendKeepAliveAsync(); - } - - private async void SendKeepAliveAsync() - { - // Check concurrent writes, pings & pongs, or closes - if (!_writeLock.Wait(0)) - { - // Sending real data is better than a ping, discard it. - return; - } - try - { - if (State == WebSocketState.CloseSent || State >= WebSocketState.Closed) - { - _keepAliveTimer.Dispose(); - return; - } - - int mask = GetNextMask(); - FrameHeader frameHeader = new FrameHeader(true, Constants.OpCodes.PingFrame, _maskOutput, mask, PingBuffer.Length); - ArraySegment headerSegment = frameHeader.Buffer; - - // TODO: CancelationToken / timeout? - if (_maskOutput && mask != 0) - { - byte[] maskedFrame = Utilities.MergeAndMask(mask, headerSegment, new ArraySegment(PingBuffer)); - await _stream.WriteAsync(maskedFrame, 0, maskedFrame.Length); - } - else - { - await _stream.WriteAsync(headerSegment.Array, headerSegment.Offset, headerSegment.Count); - await _stream.WriteAsync(PingBuffer, 0, PingBuffer.Length); - } - } - catch (Exception) - { - // TODO: Log exception, this is a background thread. - - // Shut down, we must be in a faulted state; - Abort(); - } - finally - { - _writeLock.Release(); - } - } - - public async override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) - { - ThrowIfDisposed(); - ThrowIfInputClosed(); - ValidateSegment(buffer); - // TODO: InvalidOperationException if any receives are currently in progress. - - // No active frame. Loop because we may be discarding ping/pong frames. - while (_frameInProgress == null) - { - await ReadNextFrameAsync(cancellationToken); - } - - int opCode = _frameInProgress.OpCode; - - if (opCode == Constants.OpCodes.CloseFrame) - { - return await ProcessCloseFrameAsync(cancellationToken); - } - - // Handle fragmentation, remember the first frame type - if (opCode == Constants.OpCodes.ContinuationFrame) - { - if (!_firstDataOpCode.HasValue) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid continuation frame", cancellationToken); - } - opCode = _firstDataOpCode.Value; - } - else - { - _firstDataOpCode = opCode; - } - - // Make sure there's at least some data in the buffer - int bytesToBuffer = (int)Math.Min((long)_receiveBuffer.Length, _frameBytesRemaining); - await EnsureDataAvailableOrReadAsync(bytesToBuffer, cancellationToken); - - // Copy buffered data to the users buffer - int bytesToRead = (int)Math.Min((long)buffer.Count, _frameBytesRemaining); - int bytesToCopy = Math.Min(bytesToRead, _receiveBufferBytes); - Array.Copy(_receiveBuffer, _receiveBufferOffset, buffer.Array, buffer.Offset, bytesToCopy); - - if (_unmaskInput) - { - // _frameInProgress.Masked == _unmaskInput already verified - Utilities.MaskInPlace(_frameInProgress.MaskKey, ref _dataUnmaskOffset, new ArraySegment(buffer.Array, buffer.Offset, bytesToCopy)); - } - - WebSocketReceiveResult result; - WebSocketMessageType messageType = Utilities.GetMessageType(opCode); - - if (messageType == WebSocketMessageType.Text - && !Utilities.TryValidateUtf8(new ArraySegment(buffer.Array, buffer.Offset, bytesToCopy), _frameInProgress.Fin, _incomingUtf8MessageState)) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.InvalidPayloadData, "Invalid UTF-8", cancellationToken); - } - - if (bytesToCopy == _frameBytesRemaining) - { - result = new WebSocketReceiveResult(bytesToCopy, messageType, _frameInProgress.Fin); - if (_frameInProgress.Fin) - { - _firstDataOpCode = null; - } - _frameInProgress = null; - _dataUnmaskOffset = 0; - } - else - { - result = new WebSocketReceiveResult(bytesToCopy, messageType, false); - } - - _frameBytesRemaining -= bytesToCopy; - _receiveBufferBytes -= bytesToCopy; - _receiveBufferOffset += bytesToCopy; - - return result; - } - - private async Task ReadNextFrameAsync(CancellationToken cancellationToken) - { - await EnsureDataAvailableOrReadAsync(2, cancellationToken); - int frameHeaderSize = FrameHeader.CalculateFrameHeaderSize(_receiveBuffer[_receiveBufferOffset + 1]); - await EnsureDataAvailableOrReadAsync(frameHeaderSize, cancellationToken); - _frameInProgress = new FrameHeader(new ArraySegment(_receiveBuffer, _receiveBufferOffset, frameHeaderSize)); - _receiveBufferOffset += frameHeaderSize; - _receiveBufferBytes -= frameHeaderSize; - _frameBytesRemaining = _frameInProgress.DataLength; - - if (_frameInProgress.AreReservedSet()) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Unexpected reserved bits set", cancellationToken); - } - - if (_unmaskInput != _frameInProgress.Masked) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Incorrect masking", cancellationToken); - } - - if (!ValidateOpCode(_frameInProgress.OpCode)) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid opcode: " + _frameInProgress.OpCode, cancellationToken); - } - - if (_frameInProgress.IsControlFrame) - { - if (_frameBytesRemaining > 125) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid control frame size", cancellationToken); - } - - if (!_frameInProgress.Fin) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Fragmented control frame", cancellationToken); - } - - if (_frameInProgress.OpCode == Constants.OpCodes.PingFrame || _frameInProgress.OpCode == Constants.OpCodes.PongFrame) - { - // Drain it, should be less than 125 bytes - await EnsureDataAvailableOrReadAsync((int)_frameBytesRemaining, cancellationToken); - - if (_frameInProgress.OpCode == Constants.OpCodes.PingFrame) - { - await SendPongReplyAsync(cancellationToken); - } - - _receiveBufferOffset += (int)_frameBytesRemaining; - _receiveBufferBytes -= (int)_frameBytesRemaining; - _frameBytesRemaining = 0; - _frameInProgress = null; - } - } - else if (_firstDataOpCode.HasValue && _frameInProgress.OpCode != Constants.OpCodes.ContinuationFrame) - { - // A data frame is already in progress, but this new frame is not a continuation frame. - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Expected a continuation frame: " + _frameInProgress.OpCode, cancellationToken); - } - } - - private async Task EnsureDataAvailableOrReadAsync(int bytesNeeded, CancellationToken cancellationToken) - { - // Adequate buffer space? - Contract.Assert(bytesNeeded <= _receiveBuffer.Length); - - // Insufficient buffered data - while (_receiveBufferBytes < bytesNeeded) - { - cancellationToken.ThrowIfCancellationRequested(); - - int spaceRemaining = _receiveBuffer.Length - (_receiveBufferOffset + _receiveBufferBytes); - if (_receiveBufferOffset > 0 && bytesNeeded > spaceRemaining) - { - // Some data in the buffer, shift down to make room - Array.Copy(_receiveBuffer, _receiveBufferOffset, _receiveBuffer, 0, _receiveBufferBytes); - _receiveBufferOffset = 0; - spaceRemaining = _receiveBuffer.Length - _receiveBufferBytes; - } - // Add to the end - int read = await _stream.ReadAsync(_receiveBuffer, _receiveBufferOffset + _receiveBufferBytes, spaceRemaining, cancellationToken); - if (read == 0) - { - throw new IOException("Unexpected end of stream"); - } - _receiveBufferBytes += read; - } - } - - // We received a ping, send a pong in reply - private async Task SendPongReplyAsync(CancellationToken cancellationToken) - { - await _writeLock.WaitAsync(cancellationToken); - try - { - if (State != WebSocketState.Open) - { - // Output closed, discard the pong. - return; - } - - ArraySegment dataSegment = new ArraySegment(_receiveBuffer, _receiveBufferOffset, (int)_frameBytesRemaining); - if (_unmaskInput) - { - // _frameInProgress.Masked == _unmaskInput already verified - Utilities.MaskInPlace(_frameInProgress.MaskKey, dataSegment); - } - - int mask = GetNextMask(); - FrameHeader header = new FrameHeader(true, Constants.OpCodes.PongFrame, _maskOutput, mask, _frameBytesRemaining); - if (_maskOutput) - { - Utilities.MaskInPlace(mask, dataSegment); - } - - ArraySegment headerSegment = header.Buffer; - await _stream.WriteAsync(headerSegment.Array, headerSegment.Offset, headerSegment.Count, cancellationToken); - await _stream.WriteAsync(dataSegment.Array, dataSegment.Offset, dataSegment.Count, cancellationToken); - } - finally - { - _writeLock.Release(); - } - } - - private async Task ProcessCloseFrameAsync(CancellationToken cancellationToken) - { - // The close message should be less than 125 bytes and fit in the buffer. - await EnsureDataAvailableOrReadAsync((int)_frameBytesRemaining, CancellationToken.None); - - // Status code and message are optional - if (_frameBytesRemaining >= 2) - { - if (_unmaskInput) - { - Utilities.MaskInPlace(_frameInProgress.MaskKey, new ArraySegment(_receiveBuffer, _receiveBufferOffset, (int)_frameBytesRemaining)); - } - _closeStatus = (WebSocketCloseStatus)((_receiveBuffer[_receiveBufferOffset] << 8) | _receiveBuffer[_receiveBufferOffset + 1]); - if (!ValidateCloseStatus(_closeStatus.Value)) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid close status code.", cancellationToken); - } - try - { - var encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); - _closeStatusDescription = encoding.GetString(_receiveBuffer, _receiveBufferOffset + 2, (int)_frameBytesRemaining - 2) ?? string.Empty; - } - catch (DecoderFallbackException) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid UTF-8 close message.", cancellationToken); - } - } - else if (_frameBytesRemaining == 1) - { - await SendErrorAbortAndThrow(WebSocketCloseStatus.ProtocolError, "Invalid close body.", cancellationToken); - } - else - { - _closeStatus = _closeStatus ?? WebSocketCloseStatus.NormalClosure; - _closeStatusDescription = _closeStatusDescription ?? string.Empty; - } - - Contract.Assert(_frameInProgress.Fin); - WebSocketReceiveResult result = new WebSocketReceiveResult(0, WebSocketMessageType.Close, _frameInProgress.Fin, - _closeStatus.Value, _closeStatusDescription); - - if (State == WebSocketState.Open) - { - _state = WebSocketState.CloseReceived; - } - else if (State == WebSocketState.CloseSent) - { - _state = WebSocketState.Closed; - _stream.Dispose(); - } - - return result; - } - - public async override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) - { - ThrowIfDisposed(); - - if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) - { - // Send a close message. - await CloseOutputAsync(closeStatus, statusDescription, cancellationToken); - } - - if (State == WebSocketState.CloseSent) - { - // Do a receiving drain - byte[] data = new byte[_receiveBuffer.Length]; - WebSocketReceiveResult result; - do - { - result = await ReceiveAsync(new ArraySegment(data), cancellationToken); - } - while (result.MessageType != WebSocketMessageType.Close); - } - } - - public override async Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) - { - await _writeLock.WaitAsync(cancellationToken); - try - { - ThrowIfDisposed(); - ThrowIfOutputClosed(); - if (_keepAliveTimer != null) - { - _keepAliveTimer.Dispose(); - } - - byte[] descriptionBytes = Encoding.UTF8.GetBytes(statusDescription ?? string.Empty); - byte[] fullData = new byte[descriptionBytes.Length + 2]; - fullData[0] = (byte)((int)closeStatus >> 8); - fullData[1] = (byte)closeStatus; - Array.Copy(descriptionBytes, 0, fullData, 2, descriptionBytes.Length); - - int mask = GetNextMask(); - if (_maskOutput) - { - Utilities.MaskInPlace(mask, new ArraySegment(fullData)); - } - - FrameHeader frameHeader = new FrameHeader(true, Constants.OpCodes.CloseFrame, _maskOutput, mask, fullData.Length); - - ArraySegment segment = frameHeader.Buffer; - await _stream.WriteAsync(segment.Array, segment.Offset, segment.Count, cancellationToken); - await _stream.WriteAsync(fullData, 0, fullData.Length, cancellationToken); - - if (State == WebSocketState.Open) - { - _state = WebSocketState.CloseSent; - } - else if (State == WebSocketState.CloseReceived) - { - _state = WebSocketState.Closed; - _stream.Dispose(); - } - } - finally - { - _writeLock.Release(); - } - } - - public override void Abort() - { - if (_state >= WebSocketState.Closed) // or Aborted - { - return; - } - - _state = WebSocketState.Aborted; - if (_keepAliveTimer != null) - { - _keepAliveTimer.Dispose(); - } - _stream.Dispose(); - } - - public override void Dispose() - { - if (_state >= WebSocketState.Closed) // or Aborted - { - return; - } - - _state = WebSocketState.Closed; - if (_keepAliveTimer != null) - { - _keepAliveTimer.Dispose(); - } - _stream.Dispose(); - } - - private void ThrowIfDisposed() - { - if (_state >= WebSocketState.Closed) // or Aborted - { - throw new ObjectDisposedException(typeof(CommonWebSocket).FullName); - } - } - - private void ThrowIfOutputClosed() - { - if (State == WebSocketState.CloseSent) - { - throw new InvalidOperationException("Close already sent."); - } - } - - private void ThrowIfInputClosed() - { - if (State == WebSocketState.CloseReceived) - { - throw new InvalidOperationException("Close already received."); - } - } - - private void ValidateSegment(ArraySegment buffer) - { - if (buffer.Array == null) - { - throw new ArgumentNullException(nameof(buffer)); - } - if (buffer.Offset < 0 || buffer.Offset > buffer.Array.Length) - { - throw new ArgumentOutOfRangeException(nameof(buffer.Offset), buffer.Offset, string.Empty); - } - if (buffer.Count < 0 || buffer.Count > buffer.Array.Length - buffer.Offset) - { - throw new ArgumentOutOfRangeException(nameof(buffer.Count), buffer.Count, string.Empty); - } - } - - private bool ValidateOpCode(int opCode) - { - return Constants.OpCodes.ValidOpCodes.Contains(opCode); - } - - private static bool ValidateCloseStatus(WebSocketCloseStatus closeStatus) - { - if (closeStatus < (WebSocketCloseStatus)1000 || closeStatus >= (WebSocketCloseStatus)5000) - { - return false; - } - else if (closeStatus >= (WebSocketCloseStatus)3000) - { - // 3000-3999 - Reserved for frameworks - // 4000-4999 - Reserved for private usage - return true; - } - int[] validCodes = new[] { 1000, 1001, 1002, 1003, 1007, 1008, 1009, 1010, 1011 }; - foreach (var validCode in validCodes) - { - if (closeStatus == (WebSocketCloseStatus)validCode) - { - return true; - } - } - return false; - } - - private async Task SendErrorAbortAndThrow(WebSocketCloseStatus error, string message, CancellationToken cancellationToken) - { - if (State == WebSocketState.Open || State == WebSocketState.CloseReceived) - { - await CloseOutputAsync(error, message, cancellationToken); - } - Abort(); - throw new InvalidOperationException(message); // TODO: WebSocketException + return ManagedWebSocket.CreateFromConnectedStream( + stream, + isServer: true, + subprotocol: subProtocol, + keepAliveIntervalSeconds: (int)keepAliveInterval.TotalSeconds, + receiveBufferSize: receiveBufferSize); } } } diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/CompatHelpers.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/CompatHelpers.cs new file mode 100644 index 0000000000..53dc09608f --- /dev/null +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/CompatHelpers.cs @@ -0,0 +1,49 @@ +using System.Threading.Tasks; + +namespace System.Net.WebSockets +{ + // Needed to support the WebSockets code from CoreFX. + internal static class CompatHelpers + { + internal static readonly Task CompletedTask; + + static CompatHelpers() + { + var tcs = new TaskCompletionSource(); + tcs.SetResult(null); + CompletedTask = tcs.Task; + } + + public static Task FromException(Exception ex) + { +#if NET451 + return FromException(ex); +#else + return Task.FromException(ex); +#endif + } + + public static Task FromException(Exception ex) + { +#if NET451 + var tcs = new TaskCompletionSource(); + tcs.SetException(ex); + return tcs.Task; +#else + return Task.FromException(ex); +#endif + } + + internal static T[] Empty() + { +#if NET451 + return new T[0]; +#else + return Array.Empty(); +#endif + } + } + + // This is just here to be used by a nameof in the CoreFX code. + //internal static class ClientWebSocket { } +} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/FrameHeader.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/FrameHeader.cs deleted file mode 100644 index 50fe6060b4..0000000000 --- a/src/Microsoft.AspNetCore.WebSockets.Protocol/FrameHeader.cs +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net.WebSockets; -using System.Text; -using System.Threading.Tasks; - -namespace Microsoft.AspNetCore.WebSockets.Protocol -{ - public class FrameHeader - { - private byte[] _header; - - public FrameHeader(ArraySegment header) - { - _header = new byte[header.Count]; - Array.Copy(header.Array, header.Offset, _header, 0, _header.Length); - } - - public FrameHeader(bool final, int opCode, bool masked, int maskKey, long dataLength) - { - int headerLength = 2; - if (masked) - { - headerLength += 4; - } - - if (dataLength <= 125) - { - } - else if (125 < dataLength && dataLength <= 0xFFFF) - { - headerLength += 2; - } - else - { - headerLength += 8; - } - _header = new byte[headerLength]; - - Fin = final; - OpCode = opCode; - Masked = masked; - DataLength = dataLength; - if (masked) - { - MaskKey = maskKey; - } - } - - public bool Fin - { - get - { - return (_header[0] & 0x80) == 0x80; - } - private set - { - if (value) - { - _header[0] |= 0x80; - } - else - { - _header[0] &= 0x7F; - } - } - } - - public int OpCode - { - get - { - return (_header[0] & 0xF); - } - private set - { - // TODO: Clear out a prior value? - _header[0] |= (byte)(value & 0xF); - } - } - - public bool Masked - { - get - { - return (_header[1] & 0x80) == 0x80; - } - private set - { - if (value) - { - _header[1] |= 0x80; - } - else - { - _header[1] &= 0x7F; - } - } - } - - public int MaskKey - { - get - { - if (!Masked) - { - return 0; - } - int offset = ExtendedLengthFieldSize + 2; - return (_header[offset] << 24) + (_header[offset + 1] << 16) - + (_header[offset + 2] << 8) + _header[offset + 3]; - } - private set - { - int offset = ExtendedLengthFieldSize + 2; - _header[offset] = (byte)(value >> 24); - _header[offset + 1] = (byte)(value >> 16); - _header[offset + 2] = (byte)(value >> 8); - _header[offset + 3] = (byte)value; - } - } - - public int PayloadField - { - get - { - return (_header[1] & 0x7F); - } - private set - { - // TODO: Clear out a prior value? - _header[1] |= (byte)(value & 0x7F); - } - } - - public int ExtendedLengthFieldSize - { - get - { - int payloadField = PayloadField; - if (payloadField <= 125) - { - return 0; - } - if (payloadField == 126) - { - return 2; - } - return 8; - } - } - - public long DataLength - { - get - { - int extendedFieldSize = ExtendedLengthFieldSize; - if (extendedFieldSize == 0) - { - return PayloadField; - } - if (extendedFieldSize == 2) - { - return (_header[2] << 8) + _header[3]; - } - return (_header[2] << 56) + (_header[3] << 48) - + (_header[4] << 40) + (_header[5] << 32) - + (_header[6] << 24) + (_header[7] << 16) - + (_header[8] << 8) + _header[9]; - } - private set - { - if (value <= 125) - { - PayloadField = (int)value; - } - else if (125 < value && value <= 0xFFFF) - { - PayloadField = 0x7E; - - _header[2] = (byte)(value >> 8); - _header[3] = (byte)value; - } - else - { - PayloadField = 0x7F; - - _header[2] = (byte)(value >> 56); - _header[3] = (byte)(value >> 48); - _header[4] = (byte)(value >> 40); - _header[5] = (byte)(value >> 32); - _header[6] = (byte)(value >> 24); - _header[7] = (byte)(value >> 16); - _header[8] = (byte)(value >> 8); - _header[9] = (byte)value; - } - } - } - - public ArraySegment Buffer - { - get - { - return new ArraySegment(_header); - } - } - - public bool IsControlFrame - { - get - { - return OpCode >= Constants.OpCodes.CloseFrame; - } - } - - // bits 1-3. - internal bool AreReservedSet() - { - return (_header[0] & 0x70) != 0; - } - - // Given the second bytes of a frame, calculate how long the whole frame header should be. - // Range 2-12 bytes - public static int CalculateFrameHeaderSize(byte b2) - { - int headerLength = 2; - if ((b2 & 0x80) == 0x80) // Masked - { - headerLength += 4; - } - - int payloadField = (b2 & 0x7F); - if (payloadField <= 125) - { - // headerLength += 0 - } - else if (payloadField == 126) - { - headerLength += 2; - } - else - { - headerLength += 8; - } - return headerLength; - } - } -} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/SR.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/SR.cs new file mode 100644 index 0000000000..4466198e20 --- /dev/null +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/SR.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace System.Net.WebSockets +{ + // Needed to support the WebSockets code from CoreFX. + internal static class SR + { + internal static readonly string net_Websockets_AlreadyOneOutstandingOperation = nameof(net_Websockets_AlreadyOneOutstandingOperation); + internal static readonly string net_WebSockets_Argument_InvalidMessageType = nameof(net_WebSockets_Argument_InvalidMessageType); + internal static readonly string net_WebSockets_InvalidCharInProtocolString = nameof(net_WebSockets_InvalidCharInProtocolString); + internal static readonly string net_WebSockets_InvalidCloseStatusCode = nameof(net_WebSockets_InvalidCloseStatusCode); + internal static readonly string net_WebSockets_InvalidCloseStatusDescription = nameof(net_WebSockets_InvalidCloseStatusDescription); + internal static readonly string net_WebSockets_InvalidEmptySubProtocol = nameof(net_WebSockets_InvalidEmptySubProtocol); + internal static readonly string net_WebSockets_InvalidState = nameof(net_WebSockets_InvalidState); + internal static readonly string net_WebSockets_InvalidState_ClosedOrAborted = nameof(net_WebSockets_InvalidState_ClosedOrAborted); + internal static readonly string net_WebSockets_ReasonNotNull = nameof(net_WebSockets_ReasonNotNull); + internal static readonly string net_WebSockets_UnsupportedPlatform = nameof(net_WebSockets_UnsupportedPlatform); + + internal static string Format(string name, params object[] args) => $"TODO, RESX: {name}; ({string.Join(",", args)})"; + } +} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/Utilities.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/Utilities.cs deleted file mode 100644 index c8ec5768c2..0000000000 --- a/src/Microsoft.AspNetCore.WebSockets.Protocol/Utilities.cs +++ /dev/null @@ -1,170 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net.WebSockets; - -namespace Microsoft.AspNetCore.WebSockets.Protocol -{ - public static class Utilities - { - // Copies the header and data into a new buffer and masks the data. - public static byte[] MergeAndMask(int mask, ArraySegment header, ArraySegment data) - { - byte[] frame = new byte[header.Count + data.Count]; - Array.Copy(header.Array, header.Offset, frame, 0, header.Count); - Array.Copy(data.Array, data.Offset, frame, header.Count, data.Count); - - MaskInPlace(mask, new ArraySegment(frame, header.Count, data.Count)); - return frame; - } - - public static void MaskInPlace(int mask, ArraySegment data) - { - int maskOffset = 0; - MaskInPlace(mask, ref maskOffset, data); - } - - public static void MaskInPlace(int mask, ref int maskOffset, ArraySegment data) - { - if (mask == 0) - { - return; - } - - byte[] maskBytes = new byte[] - { - (byte)(mask >> 24), - (byte)(mask >> 16), - (byte)(mask >> 8), - (byte)mask, - }; - - int end = data.Offset + data.Count; - for (int i = data.Offset; i < end; i++) - { - data.Array[i] ^= maskBytes[maskOffset]; - maskOffset = (maskOffset + 1) & 0x3; // fast % 4; - } - } - - public static int GetOpCode(WebSocketMessageType messageType) - { - switch (messageType) - { - case WebSocketMessageType.Text: return Constants.OpCodes.TextFrame; - case WebSocketMessageType.Binary: return Constants.OpCodes.BinaryFrame; - case WebSocketMessageType.Close: return Constants.OpCodes.CloseFrame; - default: throw new NotImplementedException(messageType.ToString()); - } - } - - public static WebSocketMessageType GetMessageType(int opCode) - { - switch (opCode) - { - case Constants.OpCodes.TextFrame: return WebSocketMessageType.Text; - case Constants.OpCodes.BinaryFrame: return WebSocketMessageType.Binary; - case Constants.OpCodes.CloseFrame: return WebSocketMessageType.Close; - default: throw new NotImplementedException(opCode.ToString()); - } - } - - // Performs a stateful validation of UTF-8 bytes. - // It checks for valid formatting, overlong encodings, surrogates, and value ranges. - public static bool TryValidateUtf8(ArraySegment arraySegment, bool endOfMessage, Utf8MessageState state) - { - for (int i = arraySegment.Offset; i < arraySegment.Offset + arraySegment.Count; ) - { - // Have we started a character sequence yet? - if (!state.SequenceInProgress) - { - // The first byte tells us how many bytes are in the sequence. - state.SequenceInProgress = true; - byte b = arraySegment.Array[i]; - i++; - if ((b & 0x80) == 0) // 0bbbbbbb, single byte - { - state.AdditionalBytesExpected = 0; - state.CurrentDecodeBits = b & 0x7F; - state.ExpectedValueMin = 0; - } - else if ((b & 0xC0) == 0x80) - { - // Misplaced 10bbbbbb continuation byte. This cannot be the first byte. - return false; - } - else if ((b & 0xE0) == 0xC0) // 110bbbbb 10bbbbbb - { - state.AdditionalBytesExpected = 1; - state.CurrentDecodeBits = b & 0x1F; - state.ExpectedValueMin = 0x80; - } - else if ((b & 0xF0) == 0xE0) // 1110bbbb 10bbbbbb 10bbbbbb - { - state.AdditionalBytesExpected = 2; - state.CurrentDecodeBits = b & 0xF; - state.ExpectedValueMin = 0x800; - } - else if ((b & 0xF8) == 0xF0) // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - { - state.AdditionalBytesExpected = 3; - state.CurrentDecodeBits = b & 0x7; - state.ExpectedValueMin = 0x10000; - } - else // 111110bb & 1111110b & 11111110 && 11111111 are not valid - { - return false; - } - } - while (state.AdditionalBytesExpected > 0 && i < arraySegment.Offset + arraySegment.Count) - { - byte b = arraySegment.Array[i]; - if ((b & 0xC0) != 0x80) - { - return false; - } - - i++; - state.AdditionalBytesExpected--; - - // Each continuation byte carries 6 bits of data 0x10bbbbbb. - state.CurrentDecodeBits = (state.CurrentDecodeBits << 6) | (b & 0x3F); - - if (state.AdditionalBytesExpected == 1 && state.CurrentDecodeBits >= 0x360 && state.CurrentDecodeBits <= 0x37F) - { - // This is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that are not allowed in UTF-8; - return false; - } - if (state.AdditionalBytesExpected == 2 && state.CurrentDecodeBits >= 0x110) - { - // This is going to be out of the upper Unicode bound 0x10FFFF. - return false; - } - } - if (state.AdditionalBytesExpected == 0) - { - state.SequenceInProgress = false; - if (state.CurrentDecodeBits < state.ExpectedValueMin) - { - // Overlong encoding (e.g. using 2 bytes to encode something that only needed 1). - return false; - } - } - } - if (endOfMessage && state.SequenceInProgress) - { - return false; - } - return true; - } - - public class Utf8MessageState - { - public bool SequenceInProgress { get; set; } - public int AdditionalBytesExpected { get; set; } - public int ExpectedValueMin { get; set; } - public int CurrentDecodeBits { get; set; } - } - } -} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/README.md b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/README.md new file mode 100644 index 0000000000..4b8f84b7ed --- /dev/null +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/README.md @@ -0,0 +1,5 @@ +# External Code + +External code copied from CoreFX. Do not modify files in this directory, use the `scripts\UpdateCoreFxCore.ps1` script in the repo root. + +This folder structure is designed to exactly mirror the structure in the CoreFX repo (hence the deep nesting). \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/Common/src/System/Net/WebSockets/WebSocketValidate.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/Common/src/System/Net/WebSockets/WebSocketValidate.cs new file mode 100644 index 0000000000..06e07f29dd --- /dev/null +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/Common/src/System/Net/WebSockets/WebSocketValidate.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Globalization; +using System.Text; + +namespace System.Net.WebSockets +{ + internal static class WebSocketValidate + { + internal const int MaxControlFramePayloadLength = 123; + private const int CloseStatusCodeAbort = 1006; + private const int CloseStatusCodeFailedTLSHandshake = 1015; + private const int InvalidCloseStatusCodesFrom = 0; + private const int InvalidCloseStatusCodesTo = 999; + private const string Separators = "()<>@,;:\\\"/[]?={} "; + + internal static void ValidateSubprotocol(string subProtocol) + { + if (string.IsNullOrWhiteSpace(subProtocol)) + { + throw new ArgumentException(SR.net_WebSockets_InvalidEmptySubProtocol, nameof(subProtocol)); + } + + string invalidChar = null; + int i = 0; + while (i < subProtocol.Length) + { + char ch = subProtocol[i]; + if (ch < 0x21 || ch > 0x7e) + { + invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); + break; + } + + if (!char.IsLetterOrDigit(ch) && + Separators.IndexOf(ch) >= 0) + { + invalidChar = ch.ToString(); + break; + } + + i++; + } + + if (invalidChar != null) + { + throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCharInProtocolString, subProtocol, invalidChar), nameof(subProtocol)); + } + } + + internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, string statusDescription) + { + if (closeStatus == WebSocketCloseStatus.Empty && !string.IsNullOrEmpty(statusDescription)) + { + throw new ArgumentException(SR.Format(SR.net_WebSockets_ReasonNotNull, + statusDescription, + WebSocketCloseStatus.Empty), + nameof(statusDescription)); + } + + int closeStatusCode = (int)closeStatus; + + if ((closeStatusCode >= InvalidCloseStatusCodesFrom && + closeStatusCode <= InvalidCloseStatusCodesTo) || + closeStatusCode == CloseStatusCodeAbort || + closeStatusCode == CloseStatusCodeFailedTLSHandshake) + { + // CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort + throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusCode, + closeStatusCode), + nameof(closeStatus)); + } + + int length = 0; + if (!string.IsNullOrEmpty(statusDescription)) + { + length = Encoding.UTF8.GetByteCount(statusDescription); + } + + if (length > MaxControlFramePayloadLength) + { + throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusDescription, + statusDescription, + MaxControlFramePayloadLength), + nameof(statusDescription)); + } + } + + internal static void ThrowPlatformNotSupportedException() + { + throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform); + } + + internal static void ValidateArraySegment(ArraySegment arraySegment, string parameterName) + { + if (arraySegment.Array == null) + { + throw new ArgumentNullException(parameterName + ".Array"); + } + } + + internal static void ThrowIfInvalidState(WebSocketState currentState, bool isDisposed, WebSocketState[] validStates) + { + string validStatesText = string.Empty; + + if (validStates != null && validStates.Length > 0) + { + foreach (WebSocketState validState in validStates) + { + if (currentState == validState) + { + // Ordering is important to maintain .NET 4.5 WebSocket implementation exception behavior. + if (isDisposed) + { + throw new ObjectDisposedException("ClientWebSocket"); + } + + return; + } + } + + validStatesText = string.Join(", ", validStates); + } + + throw new WebSocketException( + WebSocketError.InvalidState, + SR.Format(SR.net_WebSockets_InvalidState, currentState, validStatesText)); + } + } +} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/System.Net.WebSockets.Client/src/System/Net/WebSockets/ManagedWebSocket.cs b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/System.Net.WebSockets.Client/src/System/Net/WebSockets/ManagedWebSocket.cs new file mode 100644 index 0000000000..738b0a7348 --- /dev/null +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/ext/src/System.Net.WebSockets.Client/src/System/Net/WebSockets/ManagedWebSocket.cs @@ -0,0 +1,1312 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Diagnostics; +using System.IO; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Security.Cryptography; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +// NOTE: This file is shared between CoreFX and ASP.NET. Be very thoughtful when changing it. + +namespace System.Net.WebSockets +{ + /// A managed implementation of a web socket that sends and receives data via a . + /// + /// Thread-safety: + /// - It's acceptable to call ReceiveAsync and SendAsync in parallel. One of each may run concurrently. + /// - It's acceptable to have a pending ReceiveAsync while CloseOutputAsync or CloseAsync is called. + /// - Attemping to invoke any other operations in parallel may corrupt the instance. Attempting to invoke + /// a send operation while another is in progress or a receive operation while another is in progress will + /// result in an exception. + /// + internal sealed class ManagedWebSocket : WebSocket + { + /// Creates a from a connected to a websocket endpoint. + /// The connected Stream. + /// true if this is the server-side of the connection; false if this is the client-side of the connection. + /// The agreed upon subprotocol for the connection. + /// The current state of the websocket connection. + /// The interval to use for keep-alive pings. + /// The buffer size to use for received data. + /// The created instance. + public static ManagedWebSocket CreateFromConnectedStream( + Stream stream, bool isServer, string subprotocol, + int keepAliveIntervalSeconds = 30, int receiveBufferSize = 0x1000) + { + return new ManagedWebSocket(stream, isServer, subprotocol, TimeSpan.FromSeconds(keepAliveIntervalSeconds), receiveBufferSize); + } + + /// Per-thread cached 4-byte mask byte array. + [ThreadStatic] + private static byte[] t_headerMask; + + /// Thread-safe random number generator used to generate masks for each send. + private static readonly RandomNumberGenerator s_random = RandomNumberGenerator.Create(); + /// Encoding for the payload of text messages: UTF8 encoding that throws if invalid bytes are discovered, per the RFC. + private static readonly UTF8Encoding s_textEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); + + /// Valid states to be in when calling SendAsync. + private static readonly WebSocketState[] s_validSendStates = { WebSocketState.Open, WebSocketState.CloseReceived }; + /// Valid states to be in when calling ReceiveAsync. + private static readonly WebSocketState[] s_validReceiveStates = { WebSocketState.Open, WebSocketState.CloseSent }; + /// Valid states to be in when calling CloseOutputAsync. + private static readonly WebSocketState[] s_validCloseOutputStates = { WebSocketState.Open, WebSocketState.CloseReceived }; + /// Valid states to be in when calling CloseAsync. + private static readonly WebSocketState[] s_validCloseStates = { WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent }; + + /// The maximum size in bytes of a message frame header that includes mask bytes. + private const int MaxMessageHeaderLength = 14; + /// The maximum size of a control message payload. + private const int MaxControlPayloadLength = 125; + /// Length of the mask XOR'd with the payload data. + private const int MaskLength = 4; + + /// The stream used to communicate with the remote server. + private readonly Stream _stream; + /// + /// true if this is the server-side of the connection; false if it's client. + /// This impacts masking behavior: clients always mask payloads they send and + /// expect to always receive unmasked payloads, whereas servers always send + /// unmasked payloads and expect to always receive masked payloads. + /// + private readonly bool _isServer = false; + /// The agreed upon subprotocol with the server. + private readonly string _subprotocol; + /// Timer used to send periodic pings to the server, at the interval specified + private readonly Timer _keepAliveTimer; + /// CancellationTokenSource used to abort all current and future operations when anything is canceled or any error occurs. + private readonly CancellationTokenSource _abortSource = new CancellationTokenSource(); + /// Buffer used for reading data from the network. + private readonly byte[] _receiveBuffer; + /// + /// Tracks the state of the validity of the UTF8 encoding of text payloads. Text may be split across fragments. + /// + private readonly Utf8MessageState _utf8TextState = new Utf8MessageState(); + /// + /// Semaphore used to ensure that calls to SendFrameAsync don't run concurrently. While + /// is used to fail if a caller tries to issue another SendAsync while a previous one is running, internally + /// we use SendFrameAsync as an implementation detail, and it should not cause user requests to SendAsync to fail, + /// nor should such internal usage be allowed to run concurrently with other internal usage or with SendAsync. + /// + private readonly SemaphoreSlim _sendFrameAsyncLock = new SemaphoreSlim(1, 1); + + // We maintain the current WebSocketState in _state. However, we separately maintain _sentCloseFrame and _receivedCloseFrame + // as there isn't a strict ordering between CloseSent and CloseReceived. If we receive a close frame from the server, we need to + // transition to CloseReceived even if we're currently in CloseSent, and if we send a close frame, we need to transition to + // CloseSent even if we're currently in CloseReceived. + + /// The current state of the web socket in the protocol. + private WebSocketState _state = WebSocketState.Open; + /// true if Dispose has been called; otherwise, false. + private bool _disposed; + /// Whether we've ever sent a close frame. + private bool _sentCloseFrame; + /// Whether we've ever received a close frame. + private bool _receivedCloseFrame; + /// The reason for the close, as sent by the server, or null if not yet closed. + private WebSocketCloseStatus? _closeStatus = null; + /// A description of the close reason as sent by the server, or null if not yet closed. + private string _closeStatusDescription = null; + + /// + /// The last header received in a ReceiveAsync. If ReceiveAsync got a header but then + /// returned fewer bytes than was indicated in the header, subsequent ReceiveAsync calls + /// will use the data from the header to construct the subsequent receive results, and + /// the payload length in this header will be decremented to indicate the number of bytes + /// remaining to be received for that header. As a result, between fragments, the payload + /// length in this header should be 0. + /// + private MessageHeader _lastReceiveHeader = new MessageHeader { Opcode = MessageOpcode.Text, Fin = true }; + /// The offset of the next available byte in the _receiveBuffer. + private int _receiveBufferOffset = 0; + /// The number of bytes available in the _receiveBuffer. + private int _receiveBufferCount = 0; + /// + /// When dealing with partially read fragments of binary/text messages, a mask previously received may still + /// apply, and the first new byte received may not correspond to the 0th position in the mask. This value is + /// the next offset into the mask that should be applied. + /// + private int _receivedMaskOffsetOffset = 0; + /// + /// Buffer used to store the complete message to be sent to the stream. This is needed + /// rather than just sending a header and then the user's buffer, as we need to mutate the + /// buffered data with the mask, and we don't want to change the data in the user's buffer. + /// + private byte[] _sendBuffer; + /// + /// Whether the last SendAsync had endOfMessage==false. We need to track this so that we + /// can send the subsequent message with a continuation opcode if the last message was a fragment. + /// + private bool _lastSendWasFragment; + /// + /// The task returned from the last SendAsync operation to not complete synchronously. + /// If this is not null and not completed when a subsequent SendAsync is issued, an exception occurs. + /// + private Task _lastSendAsync; + /// + /// The task returned from the last ReceiveAsync operation to not complete synchronously. + /// If this is not null and not completed when a subsequent ReceiveAsync is issued, an exception occurs. + /// + private Task _lastReceiveAsync; + + /// Lock used to protect update and check-and-update operations on _state. + private object StateUpdateLock => _abortSource; + /// + /// We need to coordinate between receives and close operations happening concurrently, as a ReceiveAsync may + /// be pending while a Close{Output}Async is issued, which itself needs to loop until a close frame is received. + /// As such, we need thread-safety in the management of . + /// + private object ReceiveAsyncLock => _utf8TextState; // some object, as we're simply lock'ing on it + + /// Initializes the websocket. + /// The connected Stream. + /// true if this is the server-side of the connection; false if this is the client-side of the connection. + /// The agreed upon subprotocol for the connection. + /// The interval to use for keep-alive pings. + /// The buffer size to use for received data. + private ManagedWebSocket(Stream stream, bool isServer, string subprotocol, TimeSpan keepAliveInterval, int receiveBufferSize) + { + Debug.Assert(StateUpdateLock != null, $"Expected {nameof(StateUpdateLock)} to be non-null"); + Debug.Assert(ReceiveAsyncLock != null, $"Expected {nameof(ReceiveAsyncLock)} to be non-null"); + Debug.Assert(StateUpdateLock != ReceiveAsyncLock, "Locks should be different objects"); + + Debug.Assert(stream != null, $"Expected non-null stream"); + Debug.Assert(stream.CanRead, $"Expected readable stream"); + Debug.Assert(stream.CanWrite, $"Expected writeable stream"); + Debug.Assert(keepAliveInterval == Timeout.InfiniteTimeSpan || keepAliveInterval >= TimeSpan.Zero, $"Invalid keepalive interval: {keepAliveInterval}"); + Debug.Assert(receiveBufferSize >= MaxMessageHeaderLength, $"Receive buffer size {receiveBufferSize} is too small"); + + _stream = stream; + _isServer = isServer; + _subprotocol = subprotocol; + _receiveBuffer = new byte[Math.Max(receiveBufferSize, MaxMessageHeaderLength)]; + + // Set up the abort source so that if it's triggered, we transition the instance appropriately. + _abortSource.Token.Register(s => + { + var thisRef = (ManagedWebSocket)s; + + lock (thisRef.StateUpdateLock) + { + WebSocketState state = thisRef._state; + if (state != WebSocketState.Closed && state != WebSocketState.Aborted) + { + thisRef._state = state != WebSocketState.None && state != WebSocketState.Connecting ? + WebSocketState.Aborted : + WebSocketState.Closed; + } + } + }, this); + + // Now that we're opened, initiate the keep alive timer to send periodic pings + if (keepAliveInterval > TimeSpan.Zero) + { + _keepAliveTimer = new Timer(s => ((ManagedWebSocket)s).SendKeepAliveFrameAsync(), this, keepAliveInterval, keepAliveInterval); + } + } + + public override void Dispose() + { + lock (StateUpdateLock) + { + DisposeCore(); + } + } + + private void DisposeCore() + { + Debug.Assert(Monitor.IsEntered(StateUpdateLock), $"Expected {nameof(StateUpdateLock)} to be held"); + if (!_disposed) + { + _disposed = true; + _keepAliveTimer?.Dispose(); + _stream?.Dispose(); + if (_state < WebSocketState.Aborted) + { + _state = WebSocketState.Closed; + } + } + } + + public override WebSocketCloseStatus? CloseStatus => _closeStatus; + + public override string CloseStatusDescription => _closeStatusDescription; + + public override WebSocketState State => _state; + + public override string SubProtocol => _subprotocol; + + public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) + { + if (messageType != WebSocketMessageType.Text && messageType != WebSocketMessageType.Binary) + { + throw new ArgumentException(SR.Format( + SR.net_WebSockets_Argument_InvalidMessageType, + nameof(WebSocketMessageType.Close), nameof(SendAsync), nameof(WebSocketMessageType.Binary), nameof(WebSocketMessageType.Text), nameof(CloseOutputAsync)), + nameof(messageType)); + } + WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer)); + + try + { + WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validSendStates); + ThrowIfOperationInProgress(_lastSendAsync); + } + catch (Exception exc) + { + return CompatHelpers.FromException(exc); + } + + MessageOpcode opcode = + _lastSendWasFragment ? MessageOpcode.Continuation : + messageType == WebSocketMessageType.Binary ? MessageOpcode.Binary : + MessageOpcode.Text; + + Task t = SendFrameAsync(opcode, endOfMessage, buffer, cancellationToken); + _lastSendWasFragment = !endOfMessage; + _lastSendAsync = t; + return t; + } + + public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) + { + WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer)); + + try + { + WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validReceiveStates); + + Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}"); + lock (ReceiveAsyncLock) // synchronize with receives in CloseAsync + { + ThrowIfOperationInProgress(_lastReceiveAsync); + Task t = ReceiveAsyncPrivate(buffer, cancellationToken); + _lastReceiveAsync = t; + return t; + } + } + catch (Exception exc) + { + return CompatHelpers.FromException(exc); + } + } + + public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription); + + try + { + WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validCloseStates); + } + catch (Exception exc) + { + return CompatHelpers.FromException(exc); + } + + return CloseAsyncPrivate(closeStatus, statusDescription, cancellationToken); + } + + public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription); + + try + { + WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validCloseOutputStates); + } + catch (Exception exc) + { + return CompatHelpers.FromException(exc); + } + + return SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken); + } + + public override void Abort() + { + _abortSource.Cancel(); + Dispose(); // forcibly tear down connection + } + + /// Sends a websocket frame to the network. + /// The opcode for the message. + /// The value of the FIN bit for the message. + /// The buffer containing the payload data fro the message. + /// The CancellationToken to use to cancel the websocket. + private Task SendFrameAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer, CancellationToken cancellationToken) + { + // TODO: #4900 SendFrameAsync should in theory typically complete synchronously, making it fast and allocation free. + // However, due to #4900, it almost always yields, resulting in all of the allocations involved in an async method + // yielding, e.g. the boxed state machine, the Action delegate, the MoveNextRunner, and the resulting Task, plus it's + // common that the awaited operation completes so fast after the await that we may end up allocating an AwaitTaskContinuation + // inside of the TaskAwaiter. Since SendFrameAsync is such a core code path, until that can be fixed, we put some + // optimizations in place to avoid a few of those expenses, at the expense of more complicated code; for the common case, + // this code has fewer than half the number and size of allocations. If/when that issue is fixed, this method should be deleted + // and replaced by SendFrameFallbackAsync, which is the same logic but in a much more easily understand flow. + + // If a cancelable cancellation token was provided, that would require registering with it, which means more state we have to + // pass around (the CancellationTokenRegistration), so if it is cancelable, just immediately go to the fallback path. + // Similarly, it should be rare that there are multiple outstanding calls to SendFrameAsync, but if there are, again + // fall back to the fallback path. + return cancellationToken.CanBeCanceled || !_sendFrameAsyncLock.Wait(0) ? + SendFrameFallbackAsync(opcode, endOfMessage, payloadBuffer, cancellationToken) : + SendFrameLockAcquiredNonCancelableAsync(opcode, endOfMessage, payloadBuffer); + } + + /// Sends a websocket frame to the network. The caller must hold the sending lock. + /// The opcode for the message. + /// The value of the FIN bit for the message. + /// The buffer containing the payload data fro the message. + private Task SendFrameLockAcquiredNonCancelableAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer) + { + Debug.Assert(_sendFrameAsyncLock.CurrentCount == 0, "Caller should hold the _sendFrameAsyncLock"); + + // If we get here, the cancellation token is not cancelable so we don't have to worry about it, + // and we own the semaphore, so we don't need to asynchronously wait for it. + Task writeTask = null; + bool releaseSemaphore = true; + try + { + // Write the payload synchronously to the buffer, then write that buffer out to the network. + int sendBytes = WriteFrameToSendBuffer(opcode, endOfMessage, payloadBuffer); + writeTask = _stream.WriteAsync(_sendBuffer, 0, sendBytes, CancellationToken.None); + + // If the operation happens to complete synchronously (or, more specifically, by + // the time we get from the previous line to here, release the semaphore, propagate + // exceptions, and we're done. + if (writeTask.IsCompleted) + { + writeTask.GetAwaiter().GetResult(); // propagate any exceptions + return CompatHelpers.CompletedTask; + } + + // Up until this point, if an exception occurred (such as when accessing _stream or when + // calling GetResult), we want to release the semaphore. After this point, the semaphore needs + // to remain held until writeTask completes. + releaseSemaphore = false; + } + catch (Exception exc) + { + return CompatHelpers.FromException(_state == WebSocketState.Aborted ? + CreateOperationCanceledException(exc) : + new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc)); + } + finally + { + if (releaseSemaphore) + { + _sendFrameAsyncLock.Release(); + } + } + + // The write was not yet completed. Create and return a continuation that will + // release the semaphore and translate any exception that occurred. + return writeTask.ContinueWith((t, s) => + { + var thisRef = (ManagedWebSocket)s; + thisRef._sendFrameAsyncLock.Release(); + + try { t.GetAwaiter().GetResult(); } + catch (Exception exc) + { + throw thisRef._state == WebSocketState.Aborted ? + CreateOperationCanceledException(exc) : + new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); + } + }, this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); + } + + private async Task SendFrameFallbackAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer, CancellationToken cancellationToken) + { + await _sendFrameAsyncLock.WaitAsync().ConfigureAwait(false); + try + { + int sendBytes = WriteFrameToSendBuffer(opcode, endOfMessage, payloadBuffer); + using (cancellationToken.Register(s => ((ManagedWebSocket)s).Abort(), this)) + { + await _stream.WriteAsync(_sendBuffer, 0, sendBytes, cancellationToken).ConfigureAwait(false); + } + } + catch (Exception exc) + { + throw _state == WebSocketState.Aborted ? + CreateOperationCanceledException(exc, cancellationToken) : + new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); + } + finally + { + _sendFrameAsyncLock.Release(); + } + } + + /// Writes a frame into the send buffer, which can then be sent over the network. + private int WriteFrameToSendBuffer(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer) + { + // Grow our send buffer as needed. We reuse the buffer for all messages, with it protected by the send frame lock. + EnsureBufferLength(ref _sendBuffer, payloadBuffer.Count + MaxMessageHeaderLength); + + // Write the message header data to the buffer. + int headerLength; + int? maskOffset = null; + if (_isServer) + { + // The server doesn't send a mask, so the mask offset returned by WriteHeader + // is actually the end of the header. + headerLength = WriteHeader(opcode, _sendBuffer, payloadBuffer, endOfMessage, useMask: false); + } + else + { + // We need to know where the mask starts so that we can use the mask to manipulate the payload data, + // and we need to know the total length for sending it on the wire. + maskOffset = WriteHeader(opcode, _sendBuffer, payloadBuffer, endOfMessage, useMask: true); + headerLength = maskOffset.GetValueOrDefault() + MaskLength; + } + + // Write the payload + if (payloadBuffer.Count > 0) + { + Buffer.BlockCopy(payloadBuffer.Array, payloadBuffer.Offset, _sendBuffer, headerLength, payloadBuffer.Count); + + // If we added a mask to the header, XOR the payload with the mask. We do the manipulation in the send buffer so as to avoid + // changing the data in the caller-supplied payload buffer. + if (maskOffset.HasValue) + { + ApplyMask(_sendBuffer, headerLength, _sendBuffer, maskOffset.Value, 0, payloadBuffer.Count); + } + } + + // Return the number of bytes in the send buffer + return headerLength + payloadBuffer.Count; + } + + private void SendKeepAliveFrameAsync() + { + bool acquiredLock = _sendFrameAsyncLock.Wait(0); + if (acquiredLock) + { + // This exists purely to keep the connection alive; don't wait for the result, and ignore any failures. + // The call will handle releasing the lock. + SendFrameLockAcquiredNonCancelableAsync(MessageOpcode.Ping, true, new ArraySegment(CompatHelpers.Empty())); + } + else + { + // If the lock is already held, something is already getting sent, + // so there's no need to send a keep-alive ping. + } + } + + private static int WriteHeader(MessageOpcode opcode, byte[] sendBuffer, ArraySegment payload, bool endOfMessage, bool useMask) + { + // Client header format: + // 1 bit - FIN - 1 if this is the final fragment in the message (it could be the only fragment), otherwise 0 + // 1 bit - RSV1 - Reserved - 0 + // 1 bit - RSV2 - Reserved - 0 + // 1 bit - RSV3 - Reserved - 0 + // 4 bits - Opcode - How to interpret the payload + // - 0x0 - continuation + // - 0x1 - text + // - 0x2 - binary + // - 0x8 - connection close + // - 0x9 - ping + // - 0xA - pong + // - (0x3 to 0x7, 0xB-0xF - reserved) + // 1 bit - Masked - 1 if the payload is masked, 0 if it's not. Must be 1 for the client + // 7 bits, 7+16 bits, or 7+64 bits - Payload length + // - For length 0 through 125, 7 bits storing the length + // - For lengths 126 through 2^16, 7 bits storing the value 126, followed by 16 bits storing the length + // - For lengths 2^16+1 through 2^64, 7 bits storing the value 127, followed by 64 bytes storing the length + // 0 or 4 bytes - Mask, if Masked is 1 - random value XOR'd with each 4 bytes of the payload, round-robin + // Length bytes - Payload data + + Debug.Assert(sendBuffer.Length >= MaxMessageHeaderLength, $"Expected sendBuffer to be at least {MaxMessageHeaderLength}, got {sendBuffer.Length}"); + + sendBuffer[0] = (byte)opcode; // 4 bits for the opcode + if (endOfMessage) + { + sendBuffer[0] |= 0x80; // 1 bit for FIN + } + + // Store the payload length. + int maskOffset; + if (payload.Count <= 125) + { + sendBuffer[1] = (byte)payload.Count; + maskOffset = 2; // no additional payload length + } + else if (payload.Count <= ushort.MaxValue) + { + sendBuffer[1] = 126; + sendBuffer[2] = (byte)(payload.Count / 256); + sendBuffer[3] = (byte)payload.Count; + maskOffset = 2 + sizeof(ushort); // additional 2 bytes for 16-bit length + } + else + { + sendBuffer[1] = 127; + int length = payload.Count; + for (int i = 9; i >= 2; i--) + { + sendBuffer[i] = (byte)length; + length = length / 256; + } + maskOffset = 2 + sizeof(ulong); // additional 8 bytes for 64-bit length + } + + if (useMask) + { + // Generate the mask. + sendBuffer[1] |= 0x80; + WriteRandomMask(sendBuffer, maskOffset); + } + + // Return the position of the mask. + return maskOffset; + } + + /// Writes a 4-byte random mask to the specified buffer at the specified offset. + /// The buffer to which to write the mask. + /// The offset into the buffer at which to write the mask. + private static void WriteRandomMask(byte[] buffer, int offset) + { + byte[] mask = t_headerMask ?? (t_headerMask = new byte[MaskLength]); + Debug.Assert(mask.Length == MaskLength, $"Expected mask of length {MaskLength}, got {mask.Length}"); + s_random.GetBytes(mask); + Buffer.BlockCopy(mask, 0, buffer, offset, MaskLength); + } + + /// + /// Receive the next text, binary, continuation, or close message, returning information about it and + /// writing its payload into the supplied buffer. Other control messages may be consumed and processed + /// as part of this operation, but data about them will not be returned. + /// + /// The buffer into which payload data should be written. + /// The CancellationToken used to cancel the websocket. + /// Information about the received message. + private async Task ReceiveAsyncPrivate(ArraySegment payloadBuffer, CancellationToken cancellationToken) + { + // This is a long method. While splitting it up into pieces would arguably help with readability, doing so would + // also result in more allocations, as each async method that yields ends up with multiple allocations. The impact + // of those allocations is amortized across all of the awaits in the method, and since we generally expect a receive + // operation to require at most a single yield (while waiting for data to arrive), it's more efficient to have + // everything in the one method. We do separate out pieces for handling close and ping/pong messages, as we expect + // those to be much less frequent (e.g. we should only get one close per websocket), and thus we can afford to pay + // a bit more for readability and maintainability. + + CancellationTokenRegistration registration = cancellationToken.Register(s => ((ManagedWebSocket)s).Abort(), this); + try + { + while (true) // in case we get control frames that should be ignored from the user's perspective + { + // Get the last received header. If its payload length is non-zero, that means we previously + // received the header but were only able to read a part of the fragment, so we should skip + // reading another header and just proceed to use that same header and read more data associated + // with it. If instead its payload length is zero, then we've completed the processing of + // thta message, and we should read the next header. + MessageHeader header = _lastReceiveHeader; + if (header.PayloadLength == 0) + { + if (_receiveBufferCount < (_isServer ? (MaxMessageHeaderLength - MaskLength) : MaxMessageHeaderLength)) + { + // Make sure we have the first two bytes, which includes the start of the payload length. + if (_receiveBufferCount < 2) + { + await EnsureBufferContainsAsync(2, cancellationToken, throwOnPrematureClosure: false).ConfigureAwait(false); + if (_receiveBufferCount < 2) + { + // The connection closed; nothing more to read. + return new WebSocketReceiveResult(0, WebSocketMessageType.Text, true); + } + } + + // Then make sure we have the full header based on the payload length. + // If this is the server, we also need room for the received mask. + long payloadLength = _receiveBuffer[_receiveBufferOffset + 1] & 0x7F; + if (_isServer || payloadLength > 125) + { + int minNeeded = + 2 + + (_isServer ? MaskLength : 0) + + (payloadLength <= 125 ? 0 : payloadLength == 126 ? sizeof(ushort) : sizeof(ulong)); // additional 2 or 8 bytes for 16-bit or 64-bit length + await EnsureBufferContainsAsync(minNeeded, cancellationToken).ConfigureAwait(false); + } + } + + if (!TryParseMessageHeaderFromReceiveBuffer(out header)) + { + await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); + } + _receivedMaskOffsetOffset = 0; + } + + // If the header represents a ping or a pong, it's a control message meant + // to be transparent to the user, so handle it and then loop around to read again. + // Alternatively, if it's a close message, handle it and exit. + if (header.Opcode == MessageOpcode.Ping || header.Opcode == MessageOpcode.Pong) + { + await HandleReceivedPingPongAsync(header, cancellationToken).ConfigureAwait(false); + continue; + } + else if (header.Opcode == MessageOpcode.Close) + { + return await HandleReceivedCloseAsync(header, cancellationToken).ConfigureAwait(false); + } + + // If this is a continuation, replace the opcode with the one of the message it's continuing + if (header.Opcode == MessageOpcode.Continuation) + { + header.Opcode = _lastReceiveHeader.Opcode; + } + + // The message should now be a binary or text message. Handle it by reading the payload and returning the contents. + Debug.Assert(header.Opcode == MessageOpcode.Binary || header.Opcode == MessageOpcode.Text, $"Unexpected opcode {header.Opcode}"); + + // If there's no data to read, return an appropriate result. + int bytesToRead = (int)Math.Min(payloadBuffer.Count, header.PayloadLength); + if (bytesToRead == 0) + { + _lastReceiveHeader = header; + return new WebSocketReceiveResult( + 0, + header.Opcode == MessageOpcode.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, + header.PayloadLength == 0 ? header.Fin : false); + } + + // Otherwise, read as much of the payload as we can efficiently, and upate the header to reflect how much data + // remains for future reads. + + if (_receiveBufferCount == 0) + { + await EnsureBufferContainsAsync(1, cancellationToken, throwOnPrematureClosure: false).ConfigureAwait(false); + } + + int bytesToCopy = Math.Min(bytesToRead, _receiveBufferCount); + if (_isServer) + { + _receivedMaskOffsetOffset = ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, _receivedMaskOffsetOffset, bytesToCopy); + } + Buffer.BlockCopy(_receiveBuffer, _receiveBufferOffset, payloadBuffer.Array, payloadBuffer.Offset, bytesToCopy); + ConsumeFromBuffer(bytesToCopy); + header.PayloadLength -= bytesToCopy; + + // If this a text message, validate that it contains valid UTF8. + if (header.Opcode == MessageOpcode.Text && + !TryValidateUtf8(new ArraySegment(payloadBuffer.Array, payloadBuffer.Offset, bytesToCopy), header.Fin, _utf8TextState)) + { + await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.InvalidPayloadData, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); + } + + _lastReceiveHeader = header; + return new WebSocketReceiveResult( + bytesToCopy, + header.Opcode == MessageOpcode.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, + bytesToCopy == 0 || (header.Fin && header.PayloadLength == 0)); + } + } + catch (Exception exc) + { + throw _state == WebSocketState.Aborted ? + new WebSocketException(WebSocketError.InvalidState, SR.Format(SR.net_WebSockets_InvalidState_ClosedOrAborted, "System.Net.WebSockets.InternalClientWebSocket", "Aborted"), exc) : + new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); + } + finally + { + registration.Dispose(); + } + } + + /// Processes a received close message. + /// The message header. + /// The cancellation token to use to cancel the websocket. + /// The received result message. + private async Task HandleReceivedCloseAsync( + MessageHeader header, CancellationToken cancellationToken) + { + lock (StateUpdateLock) + { + _receivedCloseFrame = true; + if (_state < WebSocketState.CloseReceived) + { + _state = WebSocketState.CloseReceived; + } + } + + WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure; + string closeStatusDescription = string.Empty; + + // Handle any payload by parsing it into the close status and description. + if (header.PayloadLength == 1) + { + // The close payload length can be 0 or >= 2, but not 1. + await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); + } + else if (header.PayloadLength >= 2) + { + if (_receiveBufferCount < header.PayloadLength) + { + await EnsureBufferContainsAsync((int)header.PayloadLength, cancellationToken).ConfigureAwait(false); + } + + if (_isServer) + { + ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, 0, header.PayloadLength); + } + + closeStatus = (WebSocketCloseStatus)(_receiveBuffer[_receiveBufferOffset] << 8 | _receiveBuffer[_receiveBufferOffset + 1]); + if (!IsValidCloseStatus(closeStatus)) + { + await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); + } + + if (header.PayloadLength > 2) + { + try + { + closeStatusDescription = s_textEncoding.GetString(_receiveBuffer, _receiveBufferOffset + 2, (int)header.PayloadLength - 2); + } + catch (DecoderFallbackException exc) + { + await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken, exc).ConfigureAwait(false); + } + } + ConsumeFromBuffer((int)header.PayloadLength); + } + + // Store the close status and description onto the instance. + _closeStatus = closeStatus; + _closeStatusDescription = closeStatusDescription; + + // And return them as part of the result message. + return new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, closeStatus, closeStatusDescription); + } + + /// Processes a received ping or pong message. + /// The message header. + /// The cancellation token to use to cancel the websocket. + private async Task HandleReceivedPingPongAsync(MessageHeader header, CancellationToken cancellationToken) + { + // Consume any (optional) payload associated with the ping/pong. + if (header.PayloadLength > 0 && _receiveBufferCount < header.PayloadLength) + { + await EnsureBufferContainsAsync((int)header.PayloadLength, cancellationToken).ConfigureAwait(false); + } + + // If this was a ping, send back a pong response. + if (header.Opcode == MessageOpcode.Ping) + { + if (_isServer) + { + ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, 0, header.PayloadLength); + } + + await SendFrameAsync( + MessageOpcode.Pong, true, + new ArraySegment(_receiveBuffer, _receiveBufferOffset, (int)header.PayloadLength), cancellationToken).ConfigureAwait(false); + } + + // Regardless of whether it was a ping or pong, we no longer need the payload. + if (header.PayloadLength > 0) + { + ConsumeFromBuffer((int)header.PayloadLength); + } + } + + /// Check whether a close status is valid according to the RFC. + /// The status to validate. + /// true if the status if valid; otherwise, false. + private static bool IsValidCloseStatus(WebSocketCloseStatus closeStatus) + { + // 0-999: "not used" + // 1000-2999: reserved for the protocol; we need to check individual codes manually + // 3000-3999: reserved for use by higher-level code + // 4000-4999: reserved for private use + // 5000-: not mentioned in RFC + + if (closeStatus < (WebSocketCloseStatus)1000 || closeStatus >= (WebSocketCloseStatus)5000) + { + return false; + } + + if (closeStatus >= (WebSocketCloseStatus)3000) + { + return true; + } + + switch (closeStatus) // check for the 1000-2999 range known codes + { + case WebSocketCloseStatus.EndpointUnavailable: + case WebSocketCloseStatus.InternalServerError: + case WebSocketCloseStatus.InvalidMessageType: + case WebSocketCloseStatus.InvalidPayloadData: + case WebSocketCloseStatus.MandatoryExtension: + case WebSocketCloseStatus.MessageTooBig: + case WebSocketCloseStatus.NormalClosure: + case WebSocketCloseStatus.PolicyViolation: + case WebSocketCloseStatus.ProtocolError: + return true; + + default: + return false; + } + } + + /// Send a close message to the server and throw an exception, in response to getting bad data from the server. + /// The close status code to use. + /// The error reason. + /// The CancellationToken used to cancel the websocket. + /// An optional inner exception to include in the thrown exception. + private async Task CloseWithReceiveErrorAndThrowAsync( + WebSocketCloseStatus closeStatus, WebSocketError error, CancellationToken cancellationToken, Exception innerException = null) + { + // Close the connection if it hasn't already been closed + if (!_sentCloseFrame) + { + await CloseOutputAsync(closeStatus, string.Empty, cancellationToken).ConfigureAwait(false); + } + + // Dump our receive buffer; we're in a bad state to do any further processing + _receiveBufferCount = 0; + + // Let the caller know we've failed + throw new WebSocketException(error, innerException); + } + + /// Parses a message header from the buffer. This assumes the header is in the buffer. + /// The read header. + /// true if a header was read; false if the header was invalid. + private bool TryParseMessageHeaderFromReceiveBuffer(out MessageHeader resultHeader) + { + Debug.Assert(_receiveBufferCount >= 2, $"Expected to at least have the first two bytes of the header."); + + var header = new MessageHeader(); + + header.Fin = (_receiveBuffer[_receiveBufferOffset] & 0x80) != 0; + bool reservedSet = (_receiveBuffer[_receiveBufferOffset] & 0x70) != 0; + header.Opcode = (MessageOpcode)(_receiveBuffer[_receiveBufferOffset] & 0xF); + + bool masked = (_receiveBuffer[_receiveBufferOffset + 1] & 0x80) != 0; + header.PayloadLength = _receiveBuffer[_receiveBufferOffset + 1] & 0x7F; + + ConsumeFromBuffer(2); + + // Read the remainder of the payload length, if necessary + if (header.PayloadLength == 126) + { + Debug.Assert(_receiveBufferCount >= 2, $"Expected to have two bytes for the payload length."); + header.PayloadLength = (_receiveBuffer[_receiveBufferOffset] << 8) | _receiveBuffer[_receiveBufferOffset + 1]; + ConsumeFromBuffer(2); + } + else if (header.PayloadLength == 127) + { + Debug.Assert(_receiveBufferCount >= 8, $"Expected to have eight bytes for the payload length."); + header.PayloadLength = 0; + for (int i = 0; i < 8; i++) + { + header.PayloadLength = (header.PayloadLength << 8) | _receiveBuffer[_receiveBufferOffset + i]; + } + ConsumeFromBuffer(8); + } + + bool shouldFail = reservedSet; + if (masked) + { + if (!_isServer) + { + shouldFail = true; + } + header.Mask = CombineMaskBytes(_receiveBuffer, _receiveBufferOffset); + + // Consume the mask bytes + ConsumeFromBuffer(4); + } + + // Do basic validation of the header + switch (header.Opcode) + { + case MessageOpcode.Continuation: + if (_lastReceiveHeader.Fin) + { + // Can't continue from a final message + shouldFail = true; + } + break; + + case MessageOpcode.Binary: + case MessageOpcode.Text: + if (!_lastReceiveHeader.Fin) + { + // Must continue from a non-final message + shouldFail = true; + } + break; + + case MessageOpcode.Close: + case MessageOpcode.Ping: + case MessageOpcode.Pong: + if (header.PayloadLength > MaxControlPayloadLength || !header.Fin) + { + // Invalid control messgae + shouldFail = true; + } + break; + + default: + // Unknown opcode + shouldFail = true; + break; + } + + // Return the read header + resultHeader = header; + return !shouldFail; + } + + /// Send a close message, then receive until we get a close response message. + /// The close status to send. + /// The close status description to send. + /// The CancellationToken to use to cancel the websocket. + private async Task CloseAsyncPrivate(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) + { + // Send the close message. Skip sending a close frame if we're currently in a CloseSent state, + // for example having just done a CloseOutputAsync. + if (!_sentCloseFrame) + { + await SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken).ConfigureAwait(false); + } + + // We should now either be in a CloseSent case (because we just sent one), or in a CloseReceived state, in case + // there was a concurrent receive that ended up handling an immediate close frame response from the server. + // Of course it could also be Aborted if something happened concurrently to cause things to blow up. + Debug.Assert( + State == WebSocketState.CloseSent || + State == WebSocketState.CloseReceived || + State == WebSocketState.Aborted, + $"Unexpected state {State}."); + + // Wait until we've received a close response + byte[] closeBuffer = new byte[MaxMessageHeaderLength + MaxControlPayloadLength]; + while (!_receivedCloseFrame) + { + Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}"); + Task receiveTask; + lock (ReceiveAsyncLock) + { + // Now that we're holding the ReceiveAsyncLock, double-check that we've not yet received the close frame. + // It could have been received between our check above and now due to a concurrent receive completing. + if (_receivedCloseFrame) + { + break; + } + + // We've not yet processed a received close frame, which means we need to wait for a received close to complete. + // There may already be one in flight, in which case we want to just wait for that one rather than kicking off + // another (we don't support concurrent receive operations). We need to kick off a new receive if either we've + // never issued a receive or if the last issued receive completed for reasons other than a close frame. There is + // a race condition here, e.g. if there's a in-flight receive that completes after we check, but that's fine: worst + // case is we then await it, find that it's not what we need, and try again. + receiveTask = _lastReceiveAsync; + if (receiveTask == null || + (receiveTask.Status == TaskStatus.RanToCompletion && receiveTask.Result.MessageType != WebSocketMessageType.Close)) + { + _lastReceiveAsync = receiveTask = ReceiveAsyncPrivate(new ArraySegment(closeBuffer), cancellationToken); + } + } + + // Wait for whatever receive task we have. We'll then loop around again to re-check our state. + Debug.Assert(receiveTask != null); + await receiveTask.ConfigureAwait(false); + } + + // We're closed. Close the connection and update the status. + lock (StateUpdateLock) + { + DisposeCore(); + if (_state < WebSocketState.Closed) + { + _state = WebSocketState.Closed; + } + } + } + + /// Sends a close message to the server. + /// The close status to send. + /// The close status description to send. + /// The CancellationToken to use to cancel the websocket. + private async Task SendCloseFrameAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) + { + // Close payload is two bytes containing the close status followed by a UTF8-encoding of the status description, if it exists. + + byte[] buffer; + if (string.IsNullOrEmpty(closeStatusDescription)) + { + buffer = new byte[2]; + } + else + { + buffer = new byte[2 + s_textEncoding.GetByteCount(closeStatusDescription)]; + int encodedLength = s_textEncoding.GetBytes(closeStatusDescription, 0, closeStatusDescription.Length, buffer, 2); + Debug.Assert(buffer.Length - 2 == encodedLength, $"GetByteCount and GetBytes encoded count didn't match"); + } + + ushort closeStatusValue = (ushort)closeStatus; + buffer[0] = (byte)(closeStatusValue >> 8); + buffer[1] = (byte)(closeStatusValue & 0xFF); + + await SendFrameAsync(MessageOpcode.Close, true, new ArraySegment(buffer), cancellationToken).ConfigureAwait(false); + + lock (StateUpdateLock) + { + _sentCloseFrame = true; + if (_state <= WebSocketState.CloseReceived) + { + _state = WebSocketState.CloseSent; + } + } + } + + private void ConsumeFromBuffer(int count) + { + Debug.Assert(count >= 0, $"Expected non-negative count, got {count}"); + Debug.Assert(count <= _receiveBufferCount, $"Trying to consume {count}, which is more than exists {_receiveBufferCount}"); + _receiveBufferCount -= count; + _receiveBufferOffset += count; + } + + private async Task EnsureBufferContainsAsync(int minimumRequiredBytes, CancellationToken cancellationToken, bool throwOnPrematureClosure = true) + { + Debug.Assert(minimumRequiredBytes <= _receiveBuffer.Length, $"Requested number of bytes {minimumRequiredBytes} must not exceed {_receiveBuffer.Length}"); + + // If we don't have enough data in the buffer to satisfy the minimum required, read some more. + if (_receiveBufferCount < minimumRequiredBytes) + { + // If there's any data in the buffer, shift it down. + if (_receiveBufferCount > 0) + { + Buffer.BlockCopy(_receiveBuffer, _receiveBufferOffset, _receiveBuffer, 0, _receiveBufferCount); + } + _receiveBufferOffset = 0; + + // While we don't have enough data, read more. + while (_receiveBufferCount < minimumRequiredBytes) + { + int numRead = await _stream.ReadAsync(_receiveBuffer, _receiveBufferCount, _receiveBuffer.Length - _receiveBufferCount, cancellationToken).ConfigureAwait(false); + Debug.Assert(numRead >= 0, $"Expected non-negative bytes read, got {numRead}"); + _receiveBufferCount += numRead; + if (numRead == 0) + { + // The connection closed before we were able to read everything we needed. + // If it was due to use being disposed, fail. If it was due to the connection + // being closed and it wasn't expected, fail. If it was due to the connection + // being closed and that was expected, exit gracefully. + if (_disposed) + { + throw new ObjectDisposedException("ClientWebSocket"); + } + else if (throwOnPrematureClosure) + { + throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely); + } + break; + } + } + } + } + + /// + /// Grows the specified buffer if it's not at least the specified minimum length. + /// Data is not copied if the buffer is grown. + /// + private static void EnsureBufferLength(ref byte[] buffer, int minLength) + { + if (buffer == null || buffer.Length < minLength) + { + buffer = new byte[minLength]; + } + } + + private static unsafe int CombineMaskBytes(byte[] buffer, int maskOffset) => + BitConverter.ToInt32(buffer, maskOffset); + + /// Applies a mask to a portion of a byte array. + /// The buffer to which the mask should be applied. + /// The offset into at which the mask should start to be applied. + /// The array containing the mask to apply. + /// The offset into of the mask to apply of length . + /// The next position offset from of which by to apply next from the mask. + /// The number of bytes starting from to which the mask should be applied. + /// The updated maskOffsetOffset value. + private static int ApplyMask(byte[] toMask, int toMaskOffset, byte[] mask, int maskOffset, int maskOffsetIndex, long count) + { + Debug.Assert(maskOffsetIndex < MaskLength, $"Unexpected {nameof(maskOffsetIndex)}: {maskOffsetIndex}"); + Debug.Assert(mask.Length >= MaskLength + maskOffset, $"Unexpected inputs: {mask.Length}, {maskOffset}"); + return ApplyMask(toMask, toMaskOffset, CombineMaskBytes(mask, maskOffset), maskOffsetIndex, count); + } + + /// Applies a mask to a portion of a byte array. + /// The buffer to which the mask should be applied. + /// The offset into at which the mask should start to be applied. + /// The four-byte mask, stored as an Int32. + /// The index into the mas + /// The number of bytes to mask. + /// + private static unsafe int ApplyMask(byte[] toMask, int toMaskOffset, int mask, int maskIndex, long count) + { + Debug.Assert(toMaskOffset <= toMask.Length - count, $"Unexpected inputs: {toMaskOffset}, {toMask.Length}, {count}"); + Debug.Assert(maskIndex < sizeof(int), $"Unexpected {nameof(maskIndex)}: {maskIndex}"); + + byte* maskPtr = (byte*)&mask; + fixed (byte* toMaskPtr = toMask) + { + byte* p = toMaskPtr + toMaskOffset; + byte* end = p + count; + while (p < end) + { + *p++ ^= maskPtr[maskIndex]; + maskIndex = (maskIndex + 1) & 3; // & 3 == faster % MaskLength + } + return maskIndex; + } + } + + /// Aborts the websocket and throws an exception if an existing operation is in progress. + private void ThrowIfOperationInProgress(Task operationTask, [CallerMemberName] string methodName = null) + { + if (operationTask != null && !operationTask.IsCompleted) + { + Abort(); + throw new InvalidOperationException(SR.Format(SR.net_Websockets_AlreadyOneOutstandingOperation, methodName)); + } + } + + /// Creates an OperationCanceledException instance, using a default message and the specified inner exception and token. + private static Exception CreateOperationCanceledException(Exception innerException, CancellationToken cancellationToken = default(CancellationToken)) + { + return new OperationCanceledException( + new OperationCanceledException().Message, + innerException, + cancellationToken); + } + + // From https://raw.githubusercontent.com/aspnet/WebSockets/dev/src/Microsoft.AspNetCore.WebSockets.Protocol/Utilities.cs + // Performs a stateful validation of UTF-8 bytes. + // It checks for valid formatting, overlong encodings, surrogates, and value ranges. + private static bool TryValidateUtf8(ArraySegment arraySegment, bool endOfMessage, Utf8MessageState state) + { + for (int i = arraySegment.Offset; i < arraySegment.Offset + arraySegment.Count;) + { + // Have we started a character sequence yet? + if (!state.SequenceInProgress) + { + // The first byte tells us how many bytes are in the sequence. + state.SequenceInProgress = true; + byte b = arraySegment.Array[i]; + i++; + if ((b & 0x80) == 0) // 0bbbbbbb, single byte + { + state.AdditionalBytesExpected = 0; + state.CurrentDecodeBits = b & 0x7F; + state.ExpectedValueMin = 0; + } + else if ((b & 0xC0) == 0x80) + { + // Misplaced 10bbbbbb continuation byte. This cannot be the first byte. + return false; + } + else if ((b & 0xE0) == 0xC0) // 110bbbbb 10bbbbbb + { + state.AdditionalBytesExpected = 1; + state.CurrentDecodeBits = b & 0x1F; + state.ExpectedValueMin = 0x80; + } + else if ((b & 0xF0) == 0xE0) // 1110bbbb 10bbbbbb 10bbbbbb + { + state.AdditionalBytesExpected = 2; + state.CurrentDecodeBits = b & 0xF; + state.ExpectedValueMin = 0x800; + } + else if ((b & 0xF8) == 0xF0) // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb + { + state.AdditionalBytesExpected = 3; + state.CurrentDecodeBits = b & 0x7; + state.ExpectedValueMin = 0x10000; + } + else // 111110bb & 1111110b & 11111110 && 11111111 are not valid + { + return false; + } + } + while (state.AdditionalBytesExpected > 0 && i < arraySegment.Offset + arraySegment.Count) + { + byte b = arraySegment.Array[i]; + if ((b & 0xC0) != 0x80) + { + return false; + } + + i++; + state.AdditionalBytesExpected--; + + // Each continuation byte carries 6 bits of data 0x10bbbbbb. + state.CurrentDecodeBits = (state.CurrentDecodeBits << 6) | (b & 0x3F); + + if (state.AdditionalBytesExpected == 1 && state.CurrentDecodeBits >= 0x360 && state.CurrentDecodeBits <= 0x37F) + { + // This is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that are not allowed in UTF-8; + return false; + } + if (state.AdditionalBytesExpected == 2 && state.CurrentDecodeBits >= 0x110) + { + // This is going to be out of the upper Unicode bound 0x10FFFF. + return false; + } + } + if (state.AdditionalBytesExpected == 0) + { + state.SequenceInProgress = false; + if (state.CurrentDecodeBits < state.ExpectedValueMin) + { + // Overlong encoding (e.g. using 2 bytes to encode something that only needed 1). + return false; + } + } + } + if (endOfMessage && state.SequenceInProgress) + { + return false; + } + return true; + } + + private sealed class Utf8MessageState + { + internal bool SequenceInProgress; + internal int AdditionalBytesExpected; + internal int ExpectedValueMin; + internal int CurrentDecodeBits; + } + + private enum MessageOpcode : byte + { + Continuation = 0x0, + Text = 0x1, + Binary = 0x2, + Close = 0x8, + Ping = 0x9, + Pong = 0xA + } + + [StructLayout(LayoutKind.Auto)] + private struct MessageHeader + { + internal MessageOpcode Opcode; + internal bool Fin; + internal long PayloadLength; + internal int Mask; + } + } +} diff --git a/src/Microsoft.AspNetCore.WebSockets.Protocol/project.json b/src/Microsoft.AspNetCore.WebSockets.Protocol/project.json index 8acb4508e4..c0ebe738b3 100644 --- a/src/Microsoft.AspNetCore.WebSockets.Protocol/project.json +++ b/src/Microsoft.AspNetCore.WebSockets.Protocol/project.json @@ -11,14 +11,18 @@ "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", "nowarn": [ - "CS1591" + "CS1591", + "CS1572", + "CS1573" ], - "xmlDoc": true + "xmlDoc": true, + "allowUnsafe": true }, "frameworks": { "net451": {}, "netstandard1.3": { "dependencies": { + "System.Diagnostics.Debug": "4.0.11-*", "System.Diagnostics.Contracts": "4.0.1-*", "System.Globalization": "4.0.11-*", "System.IO": "4.1.0-*", diff --git a/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 b/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 index 52f35e492f..d109182eac 100644 --- a/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 +++ b/test/AutobahnTestApp/scripts/RunAutobahnTests.ps1 @@ -1,7 +1,7 @@ # # RunAutobahnTests.ps1 # -param([Parameter(Mandatory=$true)][string]$ServerUrl, [string[]]$Cases = @("*"), [string]$OutputDir) +param([Parameter(Mandatory=$true)][string]$ServerUrl, [string[]]$Cases = @("*"), [string]$OutputDir, [int]$Iterations = 1) if(!(Get-Command wstest -ErrorAction SilentlyContinue)) { throw "Missing required command 'wstest'. See README.md in Microsoft.AspNetCore.WebSockets.Server.Test project for information on installing Autobahn Test Suite." @@ -12,19 +12,32 @@ if(!$OutputDir) { $OutputDir = Join-Path $OutputDir "autobahnreports" } -$Spec = Convert-Path (Join-Path $PSScriptRoot "autobahn.spec.json") +Write-Host "Launching Autobahn Test Suite ($Iterations iteration(s))..." -$CasesArray = [string]::Join(",", @($Cases | ForEach-Object { "`"$_`"" })) +0..($Iterations-1) | % { + $iteration = $_ -$SpecJson = [IO.File]::ReadAllText($Spec).Replace("OUTPUTDIR", $OutputDir.Replace("\", "\\")).Replace("WEBSOCKETURL", $ServerUrl).Replace("`"CASES`"", $CasesArray) + $Spec = Convert-Path (Join-Path $PSScriptRoot "autobahn.spec.json") -$TempFile = [IO.Path]::GetTempFileName() + $CasesArray = [string]::Join(",", @($Cases | ForEach-Object { "`"$_`"" })) -try { - [IO.File]::WriteAllText($TempFile, $SpecJson) - & wstest -m fuzzingclient -s $TempFile -} finally { - if(Test-Path $TempFile) { - rm $TempFile + $SpecJson = [IO.File]::ReadAllText($Spec).Replace("OUTPUTDIR", $OutputDir.Replace("\", "\\")).Replace("WEBSOCKETURL", $ServerUrl).Replace("`"CASES`"", $CasesArray) + + $TempFile = [IO.Path]::GetTempFileName() + + try { + [IO.File]::WriteAllText($TempFile, $SpecJson) + $wstestOutput = & wstest -m fuzzingclient -s $TempFile + } finally { + if(Test-Path $TempFile) { + rm $TempFile + } + } + + $report = ConvertFrom-Json ([IO.File]::ReadAllText((Convert-Path (Join-Path $OutputDir "index.json")))) + + $report.Server | gm | ? { $_.MemberType -eq "NoteProperty" } | % { + $case = $report.Server."$($_.Name)" + Write-Host "[#$($iteration.ToString().PadRight(2))] [$($case.behavior.PadRight(6))] Case $($_.Name)" } } \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Client.Test/WebSocketClientTests.cs b/test/Microsoft.AspNetCore.WebSockets.Client.Test/WebSocketClientTests.cs index fe3e0d6852..1b742cd8e8 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Client.Test/WebSocketClientTests.cs +++ b/test/Microsoft.AspNetCore.WebSockets.Client.Test/WebSocketClientTests.cs @@ -290,9 +290,11 @@ namespace Microsoft.AspNetCore.WebSockets.Client.Test } } - [ConditionalFact] + [ConditionalTheory] + [InlineData(1024 * 16)] + [InlineData(0xFFFFFF)] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ReceiveLongDataInSmallBuffer_Success() + public async Task ReceiveLongData(int receiveBufferSize) { var orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); using (var server = KestrelWebSocketHelpers.CreateServer(async context => @@ -303,7 +305,7 @@ namespace Microsoft.AspNetCore.WebSockets.Client.Test await webSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); })) { - var client = new WebSocketClient(); + var client = new WebSocketClient() { ReceiveBufferSize = receiveBufferSize }; using (var clientSocket = await client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None)) { var clientBuffer = new byte[orriginalData.Length]; @@ -324,32 +326,6 @@ namespace Microsoft.AspNetCore.WebSockets.Client.Test } } - [ConditionalFact] - [FrameworkSkipCondition(RuntimeFrameworks.Mono)] - public async Task ReceiveLongDataInLargeBuffer_Success() - { - var orriginalData = Encoding.UTF8.GetBytes(new string('a', 0x1FFFF)); - using (var server = KestrelWebSocketHelpers.CreateServer(async context => - { - Assert.True(context.WebSockets.IsWebSocketRequest); - var webSocket = await context.WebSockets.AcceptWebSocketAsync(); - - await webSocket.SendAsync(new ArraySegment(orriginalData), WebSocketMessageType.Binary, true, CancellationToken.None); - })) - { - var client = new WebSocketClient() { ReceiveBufferSize = 0xFFFFFF }; - using (var clientSocket = await client.ConnectAsync(new Uri(ClientAddress), CancellationToken.None)) - { - var clientBuffer = new byte[orriginalData.Length]; - var result = await clientSocket.ReceiveAsync(new ArraySegment(clientBuffer), CancellationToken.None); - Assert.True(result.EndOfMessage); - Assert.Equal(orriginalData.Length, result.Count); - Assert.Equal(WebSocketMessageType.Binary, result.MessageType); - Assert.Equal(orriginalData, clientBuffer); - } - } - } - [ConditionalFact] [FrameworkSkipCondition(RuntimeFrameworks.Mono)] public async Task ReceiveFragmentedData_Success() diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/BufferStream.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/BufferStream.cs index 2e900c2a5e..0d8454162f 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/BufferStream.cs +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/BufferStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. using System; using System.Collections.Concurrent; @@ -22,7 +22,7 @@ namespace Microsoft.AspNetCore.WebSockets.Protocol.Test private SemaphoreSlim _readLock; private SemaphoreSlim _writeLock; private TaskCompletionSource _readWaitingForData; - + internal BufferStream() { _readLock = new SemaphoreSlim(1, 1); diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexStream.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexStream.cs index 0355fa44aa..9d0c40bfc2 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexStream.cs +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexStream.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. +// Copyright (c) .NET Foundation. All rights reserved. See License.txt in the project root for license information. using System; using System.IO; @@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.WebSockets.Protocol.Test public DuplexStream() : this (new BufferStream(), new BufferStream()) - { + { } public DuplexStream(Stream readStream, Stream writeStream) diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexTests.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexTests.cs deleted file mode 100644 index c0fe5dc66c..0000000000 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/DuplexTests.cs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Net.WebSockets; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Xunit; - -namespace Microsoft.AspNetCore.WebSockets.Protocol.Test -{ - public class DuplexTests - { - [Fact] - public async Task SendAndReceive() - { - DuplexStream serverStream = new DuplexStream(); - DuplexStream clientStream = serverStream.CreateReverseDuplexStream(); - - WebSocket serverWebSocket = CommonWebSocket.CreateServerWebSocket(serverStream, null, TimeSpan.FromMinutes(2), 1024); - WebSocket clientWebSocket = CommonWebSocket.CreateClientWebSocket(clientStream, null, TimeSpan.FromMinutes(2), 1024, false); - - byte[] clientBuffer = Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz"); - byte[] serverBuffer = new byte[clientBuffer.Length]; - - await clientWebSocket.SendAsync(new ArraySegment(clientBuffer), WebSocketMessageType.Text, true, CancellationToken.None); - WebSocketReceiveResult serverResult = await serverWebSocket.ReceiveAsync(new ArraySegment(serverBuffer), CancellationToken.None); - Assert.True(serverResult.EndOfMessage); - Assert.Equal(clientBuffer.Length, serverResult.Count); - Assert.Equal(WebSocketMessageType.Text, serverResult.MessageType); - Assert.Equal(clientBuffer, serverBuffer); - } - - [Fact] - // Tests server unmasking with offset masks - public async Task ServerReceiveOffsetData() - { - DuplexStream serverStream = new DuplexStream(); - DuplexStream clientStream = serverStream.CreateReverseDuplexStream(); - - WebSocket serverWebSocket = CommonWebSocket.CreateServerWebSocket(serverStream, null, TimeSpan.FromMinutes(2), 1024); - WebSocket clientWebSocket = CommonWebSocket.CreateClientWebSocket(clientStream, null, TimeSpan.FromMinutes(2), 1024, false); - - byte[] clientBuffer = Encoding.ASCII.GetBytes("abcdefghijklmnopqrstuvwxyz"); - byte[] serverBuffer = new byte[clientBuffer.Length]; - - await clientWebSocket.SendAsync(new ArraySegment(clientBuffer), WebSocketMessageType.Text, true, CancellationToken.None); - WebSocketReceiveResult serverResult = await serverWebSocket.ReceiveAsync(new ArraySegment(serverBuffer, 0, 3), CancellationToken.None); - Assert.False(serverResult.EndOfMessage); - Assert.Equal(3, serverResult.Count); - Assert.Equal(WebSocketMessageType.Text, serverResult.MessageType); - - serverResult = await serverWebSocket.ReceiveAsync(new ArraySegment(serverBuffer, 3, 10), CancellationToken.None); - Assert.False(serverResult.EndOfMessage); - Assert.Equal(10, serverResult.Count); - Assert.Equal(WebSocketMessageType.Text, serverResult.MessageType); - - serverResult = await serverWebSocket.ReceiveAsync(new ArraySegment(serverBuffer, 13, 13), CancellationToken.None); - Assert.True(serverResult.EndOfMessage); - Assert.Equal(13, serverResult.Count); - Assert.Equal(WebSocketMessageType.Text, serverResult.MessageType); - Assert.Equal(clientBuffer, serverBuffer); - } - } -} diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj index 80b9e72b45..bbc4595fcc 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.AspNetCore.WebSockets.Protocol.Test.xproj @@ -4,11 +4,13 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - + - 62a07a24-4d06-4dda-b6bf-02d0c9cb7d32 + aaf2dfcf-845e-4410-bbf0-0683ad60dd6a + Microsoft.AspNetCore.WebSockets.Protocol.Test .\obj .\bin\ + v4.6.1 2.0 @@ -16,5 +18,5 @@ - + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.Net.WebSockets.Test.csproj b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.Net.WebSockets.Test.csproj deleted file mode 100644 index 70cae1a0ff..0000000000 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Microsoft.Net.WebSockets.Test.csproj +++ /dev/null @@ -1,69 +0,0 @@ - - - - - Debug - AnyCPU - {EF1FE910-6E0C-4DE8-8CC1-6118B726A59E} - Library - Properties - Microsoft.Net.WebSockets.Test - Microsoft.Net.WebSockets.Test - v4.5 - 512 - - - true - full - false - bin\Debug\ - DEBUG;TRACE - prompt - 4 - - - pdbonly - true - bin\Release\ - TRACE - prompt - 4 - - - - - - - - - - - ..\..\packages\xunit.1.9.2\lib\net20\xunit.dll - - - ..\..\packages\xunit.extensions.1.9.2\lib\net20\xunit.extensions.dll - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Properties/AssemblyInfo.cs index f30741c6b2..e1fe33792e 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Properties/AssemblyInfo.cs +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Properties/AssemblyInfo.cs @@ -2,35 +2,18 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices; -// General Information about an assembly is controlled through the following +// General Information about an assembly is controlled through the following // set of attributes. Change these attribute values to modify the information // associated with an assembly. -[assembly: AssemblyTitle("Microsoft.Net.WebSockets.Test")] -[assembly: AssemblyDescription("")] [assembly: AssemblyConfiguration("")] [assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("Microsoft.Net.WebSockets.Test")] -[assembly: AssemblyCopyright("Copyright © 2014")] +[assembly: AssemblyProduct("Microsoft.AspNetCore.WebSockets.Protocol.Test")] [assembly: AssemblyTrademark("")] -[assembly: AssemblyCulture("")] -// Setting ComVisible to false makes the types in this assembly not visible -// to COM components. If you need to access a type in this assembly from +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from // COM, set the ComVisible attribute to true on that type. [assembly: ComVisible(false)] // The following GUID is for the ID of the typelib if this project is exposed to COM -[assembly: Guid("237d6e8f-6e5e-4c3f-96b4-b19cf3bf4d80")] - -// Version information for an assembly consists of the following four values: -// -// Major Version -// Minor Version -// Build Number -// Revision -// -// You can specify all the values or you can default the Build and Revision Numbers -// by using the '*' as shown below: -// [assembly: AssemblyVersion("1.0.*")] -[assembly: AssemblyVersion("1.0.0.0")] -[assembly: AssemblyFileVersion("1.0.0.0")] +[assembly: Guid("aaf2dfcf-845e-4410-bbf0-0683ad60dd6a")] diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/SendReceiveTests.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/SendReceiveTests.cs new file mode 100644 index 0000000000..109c4b821a --- /dev/null +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/SendReceiveTests.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; +using System.Net.WebSockets; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.AspNetCore.WebSockets.Protocol.Test +{ + public class SendReceiveTests + { + [Fact] + public async Task ClientToServerTextMessage() + { + const string message = "Hello, World!"; + + var pair = WebSocketPair.Create(); + var sendBuffer = Encoding.UTF8.GetBytes(message); + + await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); + } + + [Fact] + public async Task ServerToClientTextMessage() + { + const string message = "Hello, World!"; + + var pair = WebSocketPair.Create(); + var sendBuffer = Encoding.UTF8.GetBytes(message); + + await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Text, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Text, result.MessageType); + Assert.Equal(message, Encoding.UTF8.GetString(receiveBuffer, 0, result.Count)); + } + + [Fact] + public async Task ClientToServerBinaryMessage() + { + var pair = WebSocketPair.Create(); + var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + await pair.ClientSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ServerSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); + } + + [Fact] + public async Task ServerToClientBinaryMessage() + { + var pair = WebSocketPair.Create(); + var sendBuffer = new byte[] { 0xde, 0xad, 0xbe, 0xef }; + + await pair.ServerSocket.SendAsync(new ArraySegment(sendBuffer), WebSocketMessageType.Binary, endOfMessage: true, cancellationToken: CancellationToken.None); + + var receiveBuffer = new byte[32]; + var result = await pair.ClientSocket.ReceiveAsync(new ArraySegment(receiveBuffer), CancellationToken.None); + + Assert.Equal(WebSocketMessageType.Binary, result.MessageType); + Assert.Equal(sendBuffer, receiveBuffer.Take(result.Count).ToArray()); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Utf8ValidationTests.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Utf8ValidationTests.cs deleted file mode 100644 index 10641f0dbd..0000000000 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/Utf8ValidationTests.cs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text; -using Xunit; - -namespace Microsoft.AspNetCore.WebSockets.Protocol.Test -{ - public class Utf8ValidationTests - { - [Theory] - [InlineData(new byte[] { })] - [InlineData(new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20, 0x57, 0x6F, 0x72, 0x6C, 0x64 })] // Hello World - [InlineData(new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2D, 0xC2, 0xB5, 0x40, 0xC3, 0x9F, 0xC3, 0xB6, 0xC3, 0xA4, 0xC3, 0xBC, 0xC3, 0xA0, 0xC3, 0xA1 })] // "Hello-µ@ßöäüàá"; - // [InlineData(new byte[] { 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0xf0, 0xa4, 0xad, 0xa2, 0x77, 0x6f, 0x72, 0x6c, 0x64 })] // "hello\U00024b62world" - [InlineData(new byte[] { 0xf0, 0xa4, 0xad, 0xa2 })] // "\U00024b62" - public void ValidateSingleValidSegments_Valid(byte[] data) - { - var state = new Utilities.Utf8MessageState(); - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data), endOfMessage: true, state: state)); - } - - [Theory] - [InlineData(new byte[] { }, new byte[] { }, new byte[] { })] - [InlineData(new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x20 }, new byte[] { }, new byte[] { 0x57, 0x6F, 0x72, 0x6C, 0x64 })] // Hello ,, World - [InlineData(new byte[] { 0x48, 0x65, 0x6C, 0x6C, 0x6F, 0x2D, 0xC2, }, new byte[] { 0xB5, 0x40, 0xC3, 0x9F, 0xC3, 0xB6, 0xC3, 0xA4, }, new byte[] { 0xC3, 0xBC, 0xC3, 0xA0, 0xC3, 0xA1 })] // "Hello-µ@ßöäüàá"; - public void ValidateMultipleValidSegments_Valid(byte[] data1, byte[] data2, byte[] data3) - { - var state = new Utilities.Utf8MessageState(); - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data1), endOfMessage: false, state: state)); - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data2), endOfMessage: false, state: state)); - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data3), endOfMessage: true, state: state)); - } - - [Theory] - [InlineData(new byte[] { 0xfe })] - [InlineData(new byte[] { 0xff })] - [InlineData(new byte[] { 0xfe, 0xfe, 0xff, 0xff })] - [InlineData(new byte[] { 0xc0, 0xb1 })] // Overlong Ascii - [InlineData(new byte[] { 0xc1, 0xb1 })] // Overlong Ascii - [InlineData(new byte[] { 0xe0, 0x80, 0xaf })] // Overlong - [InlineData(new byte[] { 0xf0, 0x80, 0x80, 0xaf })] // Overlong - [InlineData(new byte[] { 0xf8, 0x80, 0x80, 0x80, 0xaf })] // Overlong - [InlineData(new byte[] { 0xfc, 0x80, 0x80, 0x80, 0x80, 0xaf })] // Overlong - [InlineData(new byte[] { 0xed, 0xa0, 0x80, 0x65, 0x64, 0x69, 0x74, 0x65, 0x64 })] // 0xEDA080 decodes to 0xD800, which is a reserved high surrogate character. - public void ValidateSingleInvalidSegment_Invalid(byte[] data) - { - var state = new Utilities.Utf8MessageState(); - Assert.False(Utilities.TryValidateUtf8(new ArraySegment(data), endOfMessage: true, state: state)); - } - - [Fact] - public void ValidateIndividualInvalidSegments_Invalid() - { - var data = new byte[] { 0xce, 0xba, 0xe1, 0xbd, 0xb9, 0xcf, 0x83, 0xce, 0xbc, 0xce, 0xb5, 0xed, 0xa0, 0x80, 0x65, 0x64, 0x69, 0x74, 0x65, 0x64 }; - var state = new Utilities.Utf8MessageState(); - for (int i = 0; i < 12; i++) - { - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data, i, 1), endOfMessage: false, state: state), i.ToString()); - } - Assert.False(Utilities.TryValidateUtf8(new ArraySegment(data, 12, 1), endOfMessage: false, state: state), 12.ToString()); - } - - [Fact] - public void ValidateMultipleInvalidSegments_Invalid() - { - var data0 = new byte[] { 0xce, 0xba, 0xe1, 0xbd, 0xb9, 0xcf, 0x83, 0xce, 0xbc, 0xce, 0xb5, 0xf4 }; - var data1 = new byte[] { 0x90 }; - var state = new Utilities.Utf8MessageState(); - Assert.True(Utilities.TryValidateUtf8(new ArraySegment(data0), endOfMessage: false, state: state)); - Assert.False(Utilities.TryValidateUtf8(new ArraySegment(data1), endOfMessage: false, state: state)); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/UtilitiesTests.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/UtilitiesTests.cs deleted file mode 100644 index 0d69ed89f7..0000000000 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/UtilitiesTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Text; -using Xunit; - -namespace Microsoft.AspNetCore.WebSockets.Protocol.Test -{ - public class UtilitiesTests - { - [Fact] - public void MaskDataRoundTrips() - { - byte[] data = Encoding.UTF8.GetBytes("Hello World"); - byte[] orriginal = Encoding.UTF8.GetBytes("Hello World"); - Utilities.MaskInPlace(16843009, new ArraySegment(data)); - Utilities.MaskInPlace(16843009, new ArraySegment(data)); - Assert.Equal(orriginal, data); - } - } -} diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/WebSocketPair.cs b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/WebSocketPair.cs new file mode 100644 index 0000000000..1bf3512468 --- /dev/null +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/WebSocketPair.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.WebSockets; + +namespace Microsoft.AspNetCore.WebSockets.Protocol.Test +{ + internal class WebSocketPair + { + public WebSocket ClientSocket { get; } + public WebSocket ServerSocket { get; } + + public WebSocketPair(WebSocket clientSocket, WebSocket serverSocket) + { + ClientSocket = clientSocket; + ServerSocket = serverSocket; + } + + public static WebSocketPair Create() + { + // Create streams + var serverStream = new DuplexStream(); + var clientStream = serverStream.CreateReverseDuplexStream(); + + return new WebSocketPair( + clientSocket: CommonWebSocket.CreateClientWebSocket(clientStream, null, TimeSpan.FromMinutes(2), 1024), + serverSocket: CommonWebSocket.CreateServerWebSocket(serverStream, null, TimeSpan.FromMinutes(2), 1024)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/project.json b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/project.json index ff14cc11fb..9eb586b89b 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/project.json +++ b/test/Microsoft.AspNetCore.WebSockets.Protocol.Test/project.json @@ -1,4 +1,4 @@ -{ +{ "dependencies": { "dotnet-test-xunit": "2.2.0-*", "Microsoft.AspNetCore.WebSockets.Protocol": "0.2.0-*", @@ -13,7 +13,6 @@ "type": "platform" } } - }, - "net451": {} + } } -} \ No newline at end of file +} diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnCaseResult.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnCaseResult.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnCaseResult.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnCaseResult.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnExpectations.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnExpectations.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnExpectations.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnExpectations.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnResult.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnResult.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnResult.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnResult.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnServerResult.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnServerResult.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnServerResult.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnServerResult.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnSpec.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnSpec.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnSpec.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnSpec.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnTester.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnTester.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/AutobahnTester.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/AutobahnTester.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Executable.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Executable.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Executable.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Executable.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Expectation.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Expectation.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Expectation.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Expectation.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/ServerSpec.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/ServerSpec.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/ServerSpec.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/ServerSpec.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Wstest.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Wstest.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Autobahn/Wstest.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Autobahn/Wstest.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/AutobahnTests.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/AutobahnTests.cs similarity index 85% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/AutobahnTests.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/AutobahnTests.cs index 8e6d3ab191..0a55c8b0d1 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Server.Test/AutobahnTests.cs +++ b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/AutobahnTests.cs @@ -37,19 +37,22 @@ namespace Microsoft.AspNetCore.WebSockets.Server.Test .IncludeCase("*") .ExcludeCase("9.*", "12.*", "13.*"); - var loggerFactory = new LoggerFactory(); // No logging! It's very loud... + var loggerFactory = new LoggerFactory(); // No logging by default! It's very loud... + + if(string.Equals(Environment.GetEnvironmentVariable("AUTOBAHN_SUITES_LOG"), "1", StringComparison.Ordinal)) + { + loggerFactory.AddConsole(); + } AutobahnResult result; using (var tester = new AutobahnTester(loggerFactory, spec)) { - await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: false, environment: "ManagedSockets", expectationConfig: expect => expect - .NonStrict("6.4.3", "6.4.4")); // https://github.com/aspnet/WebSockets/issues/99 + await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: false, environment: "ManagedSockets"); // Windows-only IIS tests, and Kestrel SSL tests (due to: https://github.com/aspnet/WebSockets/issues/102) if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { - await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: true, environment: "ManagedSockets", expectationConfig: expect => expect - .NonStrict("6.4.3", "6.4.4")); // https://github.com/aspnet/WebSockets/issues/99 + await tester.DeployTestAndAddToSpec(ServerType.Kestrel, ssl: true, environment: "ManagedSockets"); if (IsWindows8OrHigher()) { @@ -62,9 +65,7 @@ namespace Microsoft.AspNetCore.WebSockets.Server.Test .OkOrNonStrict("3.2", "3.3", "3.4", "4.1.3", "4.1.4", "4.1.5", "4.2.3", "4.2.4", "4.2.5", "5.15")); // These occasionally get non-strict results } - await tester.DeployTestAndAddToSpec(ServerType.WebListener, ssl: false, environment: "ManagedSockets", expectationConfig: expect => expect - .Fail("6.1.2", "6.1.3") // https://github.com/aspnet/WebSockets/issues/97 - .NonStrict("6.4.3", "6.4.4")); // https://github.com/aspnet/WebSockets/issues/99 + await tester.DeployTestAndAddToSpec(ServerType.WebListener, ssl: false, environment: "ManagedSockets"); } } diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Helpers.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Helpers.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Helpers.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Helpers.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Microsoft.AspNetCore.WebSockets.Server.Test.xproj b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest.xproj similarity index 73% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Microsoft.AspNetCore.WebSockets.Server.Test.xproj rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest.xproj index 11f55fd48c..10e9e7c82d 100644 --- a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Microsoft.AspNetCore.WebSockets.Server.Test.xproj +++ b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest.xproj @@ -1,12 +1,13 @@  - + - 14.0 + 14.0.25420 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - e82d9f64-8afa-4dcb-a842-2283fda73be8 + a722bb6c-9114-4f25-9bb0-2191d4405f3a + Microsoft.AspNetCore.WebSockets.Server.ConformanceTest .\obj .\bin\ diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/Properties/AssemblyInfo.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Properties/AssemblyInfo.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/Properties/AssemblyInfo.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/Properties/AssemblyInfo.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/SkipIfWsTestNotPresentAttribute.cs b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/SkipIfWsTestNotPresentAttribute.cs rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/SkipIfWsTestNotPresentAttribute.cs diff --git a/test/Microsoft.AspNetCore.WebSockets.Server.Test/project.json b/test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/project.json similarity index 100% rename from test/Microsoft.AspNetCore.WebSockets.Server.Test/project.json rename to test/Microsoft.AspNetCore.WebSockets.Server.ConformanceTest/project.json