├── .gitignore ├── LICENSE ├── README.md ├── System.Net.WebSockets.Client.Managed.sln ├── System.Net.WebSockets.Client.Managed ├── ClientWebSocket.cs ├── ClientWebSocketOptions.cs ├── HttpKnownHeaderNames.cs ├── ManagedWebSocket.cs ├── NET45Shims.cs ├── NetEventSource.WebSockets.cs ├── SR.cs ├── SecurityProtocol.cs ├── StringExtensions.cs ├── Strings.Designer.cs ├── Strings.resx ├── System.Net.WebSockets.Client.Managed.csproj ├── SystemClientWebSocket.cs ├── UriScheme.cs ├── WebSocketHandle.Managed.cs ├── WebSocketValidate.cs └── packages.config └── TestApp ├── Program.cs └── TestApp.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | artifacts/ 46 | 47 | *_i.c 48 | *_p.c 49 | *_i.h 50 | *.ilk 51 | *.meta 52 | *.obj 53 | *.pch 54 | *.pdb 55 | *.pgc 56 | *.pgd 57 | *.rsp 58 | *.sbr 59 | *.tlb 60 | *.tli 61 | *.tlh 62 | *.tmp 63 | *.tmp_proj 64 | *.log 65 | *.vspscc 66 | *.vssscc 67 | .builds 68 | *.pidb 69 | *.svclog 70 | *.scc 71 | 72 | # Chutzpah Test files 73 | _Chutzpah* 74 | 75 | # Visual C++ cache files 76 | ipch/ 77 | *.aps 78 | *.ncb 79 | *.opendb 80 | *.opensdf 81 | *.sdf 82 | *.cachefile 83 | *.VC.db 84 | *.VC.VC.opendb 85 | 86 | # Visual Studio profiler 87 | *.psess 88 | *.vsp 89 | *.vspx 90 | *.sap 91 | 92 | # TFS 2012 Local Workspace 93 | $tf/ 94 | 95 | # Guidance Automation Toolkit 96 | *.gpState 97 | 98 | # ReSharper is a .NET coding add-in 99 | _ReSharper*/ 100 | *.[Rr]e[Ss]harper 101 | *.DotSettings.user 102 | 103 | # JustCode is a .NET coding add-in 104 | .JustCode 105 | 106 | # TeamCity is a build add-in 107 | _TeamCity* 108 | 109 | # DotCover is a Code Coverage Tool 110 | *.dotCover 111 | 112 | # NCrunch 113 | _NCrunch_* 114 | .*crunch*.local.xml 115 | nCrunchTemp_* 116 | 117 | # MightyMoose 118 | *.mm.* 119 | AutoTest.Net/ 120 | 121 | # Web workbench (sass) 122 | .sass-cache/ 123 | 124 | # Installshield output folder 125 | [Ee]xpress/ 126 | 127 | # DocProject is a documentation generator add-in 128 | DocProject/buildhelp/ 129 | DocProject/Help/*.HxT 130 | DocProject/Help/*.HxC 131 | DocProject/Help/*.hhc 132 | DocProject/Help/*.hhk 133 | DocProject/Help/*.hhp 134 | DocProject/Help/Html2 135 | DocProject/Help/html 136 | 137 | # Click-Once directory 138 | publish/ 139 | 140 | # Publish Web Output 141 | *.[Pp]ublish.xml 142 | *.azurePubxml 143 | # TODO: Comment the next line if you want to checkin your web deploy settings 144 | # but database connection strings (with potential passwords) will be unencrypted 145 | *.pubxml 146 | *.publishproj 147 | 148 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 149 | # checkin your Azure Web App publish settings, but sensitive information contained 150 | # in these scripts will be unencrypted 151 | PublishScripts/ 152 | 153 | # NuGet Packages 154 | *.nupkg 155 | # The packages folder can be ignored because of Package Restore 156 | **/packages/* 157 | # except build/, which is used as an MSBuild target. 158 | !**/packages/build/ 159 | # Uncomment if necessary however generally it will be regenerated when needed 160 | #!**/packages/repositories.config 161 | # NuGet v3's project.json files produces more ignoreable files 162 | *.nuget.props 163 | *.nuget.targets 164 | 165 | # Microsoft Azure Build Output 166 | csx/ 167 | *.build.csdef 168 | 169 | # Microsoft Azure Emulator 170 | ecf/ 171 | rcf/ 172 | 173 | # Windows Store app package directories and files 174 | AppPackages/ 175 | BundleArtifacts/ 176 | Package.StoreAssociation.xml 177 | _pkginfo.txt 178 | 179 | # Visual Studio cache files 180 | # files ending in .cache can be ignored 181 | *.[Cc]ache 182 | # but keep track of directories ending in .cache 183 | !*.[Cc]ache/ 184 | 185 | # Others 186 | ClientBin/ 187 | ~$* 188 | *~ 189 | *.dbmdl 190 | *.dbproj.schemaview 191 | *.pfx 192 | *.publishsettings 193 | node_modules/ 194 | orleans.codegen.cs 195 | 196 | # Since there are multiple workflows, uncomment next line to ignore bower_components 197 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 198 | #bower_components/ 199 | 200 | # RIA/Silverlight projects 201 | Generated_Code/ 202 | 203 | # Backup & report files from converting an old project file 204 | # to a newer Visual Studio version. Backup files are not needed, 205 | # because we have git ;-) 206 | _UpgradeReport_Files/ 207 | Backup*/ 208 | UpgradeLog*.XML 209 | UpgradeLog*.htm 210 | 211 | # SQL Server files 212 | *.mdf 213 | *.ldf 214 | 215 | # Business Intelligence projects 216 | *.rdl.data 217 | *.bim.layout 218 | *.bim_*.settings 219 | 220 | # Microsoft Fakes 221 | FakesAssemblies/ 222 | 223 | # GhostDoc plugin setting file 224 | *.GhostDoc.xml 225 | 226 | # Node.js Tools for Visual Studio 227 | .ntvs_analysis.dat 228 | 229 | # Visual Studio 6 build log 230 | *.plg 231 | 232 | # Visual Studio 6 workspace options file 233 | *.opt 234 | 235 | # Visual Studio LightSwitch build output 236 | **/*.HTMLClient/GeneratedArtifacts 237 | **/*.DesktopClient/GeneratedArtifacts 238 | **/*.DesktopClient/ModelManifest.xml 239 | **/*.Server/GeneratedArtifacts 240 | **/*.Server/ModelManifest.xml 241 | _Pvt_Extensions 242 | 243 | # Paket dependency manager 244 | .paket/paket.exe 245 | paket-files/ 246 | 247 | # FAKE - F# Make 248 | .fake/ 249 | 250 | # JetBrains Rider 251 | .idea/ 252 | *.sln.iml 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Pingman Tools 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # System.Net.WebSockets.Client.Managed 2 | Microsoft's managed implementation of System.Net.WebSockets.ClientWebSocket tweaked for use on Windows 7 and .NET 4.5 3 | 4 | --- 5 | 6 | From Microsoft's [ClientWebSocket documentation](https://msdn.microsoft.com/en-us/library/system.net.websockets.clientwebsocket(v=vs.110).aspx) 7 | > Some of the classes and class elements in the System.Net.WebSockets namespace are supported on Windows 7, Windows Vista SP2, and Windows Server 2008. **However, the only public implementations of client and server WebSockets are supported on Windows 8 and Windows Server 2012.** The class elements in the System.Net.WebSockets namespace that are supported on Windows 7, Windows Vista SP2, and Windows Server 2008 are abstract class elements. This allows an application developer to inherit and extend these abstract class classes and class elements with an actual implementation of client WebSockets. 8 | 9 | In other words: on a Windows 7 machine calling `new System.Net.WebSockets.ClientWebSocket()` throws a `PlatformNotSupportedException`. 10 | 11 | Thankfully Microsoft did implement that abstract class in managed code for use on non-Windows systems! But its only available for .NET 4.6+ 12 | 13 | This project is the managed System.Net.WebSockets.Client code with a few tweaks to work on .NET 4.5. 14 | 15 | The code was taken from the CoreFX `release/2.0.0` branch on Nov 28th, 2017: 16 | * [System.Net.WebSockets.Client/src/System/Net/WebSockets](https://github.com/dotnet/corefx/tree/17c427343d7f2e9321f96a5615e4f0687878cfcf/src/System.Net.WebSockets.Client/src/System/Net/WebSockets) 17 | * [System.Net.WebSockets/src/System/Net/WebSockets](https://github.com/dotnet/corefx/tree/17c427343d7f2e9321f96a5615e4f0687878cfcf/src/System.Net.WebSockets/src/System/Net/WebSockets) 18 | * [System.Net.WebSockets/src/Common/src/System/Net/WebSockets](https://github.com/dotnet/corefx/tree/17c427343d7f2e9321f96a5615e4f0687878cfcf/src/Common/src/System/Net/WebSockets) 19 | 20 | --- 21 | 22 | Most the tweaks required are in the added files `NET45Shims.cs` and `SR.cs`, with a few changes to the original source when extensions methods could not be leveraged (NET46-only properties and statics). 23 | 24 | The only actual NET 4.6+ features used were some Task helpers (Task.FromException, Task.FromCanceled, Task.CompletedTask) and the Socket.ConnectAsync task. Microsoft could easily fix these and provide an official nuget package like this to support Win7 and .NET 4.5. 25 | 26 | ## Install 27 | 28 | Nuget package as [System.Net.WebSockets.Client.Managed](https://www.nuget.org/packages/System.Net.WebSockets.Client.Managed/) 29 | 30 | `PM> Install-Package System.Net.WebSockets.Client.Managed` 31 | 32 | ## Usage 33 | 34 | `System.Net.WebSockets.SystemClientWebSocket` class has some helpers for easily creating a ClientWebSocket that will work on the current system. 35 | 36 | ```cs 37 | // Creates a ClientWebSocket that works for this platform. Uses System.Net.WebSockets.ClientWebSocket if supported or System.Net.WebSockets.Managed.ClientWebSocket if not. 38 | public static WebSocket SystemClientWebSocket.CreateClientWebSocket() { ... } 39 | 40 | // Creates and connects a ClientWebSocket that works for this platform. Uses System.Net.WebSockets.ClientWebSocket if supported or System.Net.WebSockets.Managed.ClientWebSocket if not. 41 | public static async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) 42 | ``` 43 | 44 | If you know you want a managed instance than use `new System.Net.WebSockets.Managed.ClientWebSocket()` rather than `new System.Net.WebSockets.ClientWebSocket()` 45 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2010 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "System.Net.WebSockets.Client.Managed", "System.Net.WebSockets.Client.Managed\System.Net.WebSockets.Client.Managed.csproj", "{855237CC-239C-44A8-8818-E4B5D3BC8D6D}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1313B191-B79C-4D06-9132-244991019943}" 9 | ProjectSection(SolutionItems) = preProject 10 | README.md = README.md 11 | EndProjectSection 12 | EndProject 13 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "TestApp\TestApp.csproj", "{6C21369F-5AC3-4AB8-9E43-7B350FD8F999}" 14 | EndProject 15 | Global 16 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 17 | Debug|Any CPU = Debug|Any CPU 18 | Release|Any CPU = Release|Any CPU 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {855237CC-239C-44A8-8818-E4B5D3BC8D6D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {855237CC-239C-44A8-8818-E4B5D3BC8D6D}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {855237CC-239C-44A8-8818-E4B5D3BC8D6D}.Release|Any CPU.ActiveCfg = Release|Any CPU 24 | {855237CC-239C-44A8-8818-E4B5D3BC8D6D}.Release|Any CPU.Build.0 = Release|Any CPU 25 | {6C21369F-5AC3-4AB8-9E43-7B350FD8F999}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 26 | {6C21369F-5AC3-4AB8-9E43-7B350FD8F999}.Debug|Any CPU.Build.0 = Debug|Any CPU 27 | {6C21369F-5AC3-4AB8-9E43-7B350FD8F999}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {6C21369F-5AC3-4AB8-9E43-7B350FD8F999}.Release|Any CPU.Build.0 = Release|Any CPU 29 | EndGlobalSection 30 | GlobalSection(SolutionProperties) = preSolution 31 | HideSolutionNode = FALSE 32 | EndGlobalSection 33 | GlobalSection(ExtensibilityGlobals) = postSolution 34 | SolutionGuid = {5744BD7B-1B46-4CB3-ADE3-8C4E1E6DD508} 35 | EndGlobalSection 36 | EndGlobal 37 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/ClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace System.Net.WebSockets.Managed 10 | { 11 | public sealed partial class ClientWebSocket : WebSocket 12 | { 13 | private enum InternalState 14 | { 15 | Created = 0, 16 | Connecting = 1, 17 | Connected = 2, 18 | Disposed = 3 19 | } 20 | 21 | private readonly ClientWebSocketOptions _options; 22 | private WebSocketHandle _innerWebSocket; // may be mutable struct; do not make readonly 23 | 24 | // NOTE: this is really an InternalState value, but Interlocked doesn't support 25 | // operations on values of enum types. 26 | private int _state; 27 | 28 | public ClientWebSocket() 29 | { 30 | if (NetEventSource.IsEnabled) NetEventSource.Enter(this); 31 | WebSocketHandle.CheckPlatformSupport(); 32 | 33 | _state = (int)InternalState.Created; 34 | _options = new ClientWebSocketOptions(); 35 | 36 | if (NetEventSource.IsEnabled) NetEventSource.Exit(this); 37 | } 38 | 39 | #region Properties 40 | 41 | public ClientWebSocketOptions Options 42 | { 43 | get 44 | { 45 | return _options; 46 | } 47 | } 48 | 49 | public override WebSocketCloseStatus? CloseStatus 50 | { 51 | get 52 | { 53 | if (WebSocketHandle.IsValid(_innerWebSocket)) 54 | { 55 | return _innerWebSocket.CloseStatus; 56 | } 57 | return null; 58 | } 59 | } 60 | 61 | public override string CloseStatusDescription 62 | { 63 | get 64 | { 65 | if (WebSocketHandle.IsValid(_innerWebSocket)) 66 | { 67 | return _innerWebSocket.CloseStatusDescription; 68 | } 69 | return null; 70 | } 71 | } 72 | 73 | public override string SubProtocol 74 | { 75 | get 76 | { 77 | if (WebSocketHandle.IsValid(_innerWebSocket)) 78 | { 79 | return _innerWebSocket.SubProtocol; 80 | } 81 | return null; 82 | } 83 | } 84 | 85 | public override WebSocketState State 86 | { 87 | get 88 | { 89 | // state == Connected or Disposed 90 | if (WebSocketHandle.IsValid(_innerWebSocket)) 91 | { 92 | return _innerWebSocket.State; 93 | } 94 | switch ((InternalState)_state) 95 | { 96 | case InternalState.Created: 97 | return WebSocketState.None; 98 | case InternalState.Connecting: 99 | return WebSocketState.Connecting; 100 | default: // We only get here if disposed before connecting 101 | Debug.Assert((InternalState)_state == InternalState.Disposed); 102 | return WebSocketState.Closed; 103 | } 104 | } 105 | } 106 | 107 | #endregion Properties 108 | 109 | public Task ConnectAsync(Uri uri, CancellationToken cancellationToken) 110 | { 111 | if (uri == null) 112 | { 113 | throw new ArgumentNullException(nameof(uri)); 114 | } 115 | if (!uri.IsAbsoluteUri) 116 | { 117 | throw new ArgumentException(SR.net_uri_NotAbsolute, nameof(uri)); 118 | } 119 | if (uri.Scheme != UriScheme.Ws && uri.Scheme != UriScheme.Wss) 120 | { 121 | throw new ArgumentException(SR.net_WebSockets_Scheme, nameof(uri)); 122 | } 123 | 124 | // Check that we have not started already 125 | var priorState = (InternalState)Interlocked.CompareExchange(ref _state, (int)InternalState.Connecting, (int)InternalState.Created); 126 | if (priorState == InternalState.Disposed) 127 | { 128 | throw new ObjectDisposedException(GetType().FullName); 129 | } 130 | else if (priorState != InternalState.Created) 131 | { 132 | throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted); 133 | } 134 | _options.SetToReadOnly(); 135 | 136 | return ConnectAsyncCore(uri, cancellationToken); 137 | } 138 | 139 | private async Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken) 140 | { 141 | _innerWebSocket = WebSocketHandle.Create(); 142 | 143 | try 144 | { 145 | // Change internal state to 'connected' to enable the other methods 146 | if ((InternalState)Interlocked.CompareExchange(ref _state, (int)InternalState.Connected, (int)InternalState.Connecting) != InternalState.Connecting) 147 | { 148 | // Aborted/Disposed during connect. 149 | throw new ObjectDisposedException(GetType().FullName); 150 | } 151 | 152 | await _innerWebSocket.ConnectAsyncCore(uri, cancellationToken, _options).ConfigureAwait(false); 153 | } 154 | catch (Exception ex) 155 | { 156 | if (NetEventSource.IsEnabled) NetEventSource.Error(this, ex); 157 | throw; 158 | } 159 | } 160 | 161 | public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, 162 | CancellationToken cancellationToken) 163 | { 164 | ThrowIfNotConnected(); 165 | return _innerWebSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); 166 | } 167 | 168 | public override Task ReceiveAsync(ArraySegment buffer, 169 | CancellationToken cancellationToken) 170 | { 171 | ThrowIfNotConnected(); 172 | return _innerWebSocket.ReceiveAsync(buffer, cancellationToken); 173 | } 174 | 175 | public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, 176 | CancellationToken cancellationToken) 177 | { 178 | ThrowIfNotConnected(); 179 | return _innerWebSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); 180 | } 181 | 182 | public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, 183 | CancellationToken cancellationToken) 184 | { 185 | ThrowIfNotConnected(); 186 | return _innerWebSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken); 187 | } 188 | 189 | public override void Abort() 190 | { 191 | if ((InternalState)_state == InternalState.Disposed) 192 | { 193 | return; 194 | } 195 | if (WebSocketHandle.IsValid(_innerWebSocket)) 196 | { 197 | _innerWebSocket.Abort(); 198 | } 199 | Dispose(); 200 | } 201 | 202 | public override void Dispose() 203 | { 204 | var priorState = (InternalState)Interlocked.Exchange(ref _state, (int)InternalState.Disposed); 205 | if (priorState == InternalState.Disposed) 206 | { 207 | // No cleanup required. 208 | return; 209 | } 210 | if (WebSocketHandle.IsValid(_innerWebSocket)) 211 | { 212 | _innerWebSocket.Dispose(); 213 | } 214 | } 215 | 216 | private void ThrowIfNotConnected() 217 | { 218 | if ((InternalState)_state == InternalState.Disposed) 219 | { 220 | throw new ObjectDisposedException(GetType().FullName); 221 | } 222 | else if ((InternalState)_state != InternalState.Connected) 223 | { 224 | throw new InvalidOperationException(SR.net_WebSockets_NotConnected); 225 | } 226 | } 227 | } 228 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/ClientWebSocketOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.Diagnostics.CodeAnalysis; 8 | using System.Security.Cryptography.X509Certificates; 9 | using System.Threading; 10 | 11 | namespace System.Net.WebSockets.Managed 12 | { 13 | public sealed class ClientWebSocketOptions 14 | { 15 | private bool _isReadOnly; // After ConnectAsync is called the options cannot be modified. 16 | private readonly List _requestedSubProtocols; 17 | private readonly WebHeaderCollection _requestHeaders; 18 | private TimeSpan _keepAliveInterval = WebSocket.DefaultKeepAliveInterval; 19 | private bool _useDefaultCredentials; 20 | private ICredentials _credentials; 21 | private IWebProxy _proxy; 22 | private X509CertificateCollection _clientCertificates; 23 | private CookieContainer _cookies; 24 | private int _receiveBufferSize = 0x1000; 25 | private int _sendBufferSize = 0x1000; 26 | private ArraySegment? _buffer; 27 | 28 | internal ClientWebSocketOptions() 29 | { 30 | _requestedSubProtocols = new List(); 31 | _requestHeaders = new WebHeaderCollection(); 32 | } 33 | 34 | #region HTTP Settings 35 | 36 | // Note that some headers are restricted like Host. 37 | public void SetRequestHeader(string headerName, string headerValue) 38 | { 39 | ThrowIfReadOnly(); 40 | 41 | // WebHeaderCollection performs validation of headerName/headerValue. 42 | _requestHeaders[headerName] = headerValue; 43 | } 44 | 45 | internal WebHeaderCollection RequestHeaders { get { return _requestHeaders; } } 46 | 47 | internal List RequestedSubProtocols { get { return _requestedSubProtocols; } } 48 | 49 | public bool UseDefaultCredentials 50 | { 51 | get 52 | { 53 | return _useDefaultCredentials; 54 | } 55 | set 56 | { 57 | ThrowIfReadOnly(); 58 | _useDefaultCredentials = value; 59 | } 60 | } 61 | 62 | public ICredentials Credentials 63 | { 64 | get 65 | { 66 | return _credentials; 67 | } 68 | set 69 | { 70 | ThrowIfReadOnly(); 71 | _credentials = value; 72 | } 73 | } 74 | 75 | public IWebProxy Proxy 76 | { 77 | get 78 | { 79 | return _proxy; 80 | } 81 | set 82 | { 83 | ThrowIfReadOnly(); 84 | _proxy = value; 85 | } 86 | } 87 | 88 | [SuppressMessage("Microsoft.Usage", "CA2227:CollectionPropertiesShouldBeReadOnly", 89 | Justification = "This collection will be handed off directly to HttpWebRequest.")] 90 | public X509CertificateCollection ClientCertificates 91 | { 92 | get 93 | { 94 | if (_clientCertificates == null) 95 | { 96 | _clientCertificates = new X509CertificateCollection(); 97 | } 98 | return _clientCertificates; 99 | } 100 | set 101 | { 102 | ThrowIfReadOnly(); 103 | if (value == null) 104 | { 105 | throw new ArgumentNullException(nameof(value)); 106 | } 107 | _clientCertificates = value; 108 | } 109 | } 110 | 111 | public CookieContainer Cookies 112 | { 113 | get 114 | { 115 | return _cookies; 116 | } 117 | set 118 | { 119 | ThrowIfReadOnly(); 120 | _cookies = value; 121 | } 122 | } 123 | 124 | #endregion HTTP Settings 125 | 126 | #region WebSocket Settings 127 | 128 | public void AddSubProtocol(string subProtocol) 129 | { 130 | ThrowIfReadOnly(); 131 | WebSocketValidate.ValidateSubprotocol(subProtocol); 132 | 133 | // Duplicates not allowed. 134 | foreach (string item in _requestedSubProtocols) 135 | { 136 | if (string.Equals(item, subProtocol, StringComparison.OrdinalIgnoreCase)) 137 | { 138 | throw new ArgumentException(SR.Format(SR.net_WebSockets_NoDuplicateProtocol, subProtocol), nameof(subProtocol)); 139 | } 140 | } 141 | _requestedSubProtocols.Add(subProtocol); 142 | } 143 | 144 | public TimeSpan KeepAliveInterval 145 | { 146 | get 147 | { 148 | return _keepAliveInterval; 149 | } 150 | set 151 | { 152 | ThrowIfReadOnly(); 153 | if (value != Timeout.InfiniteTimeSpan && value < TimeSpan.Zero) 154 | { 155 | throw new ArgumentOutOfRangeException(nameof(value), value, 156 | SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 157 | Timeout.InfiniteTimeSpan.ToString())); 158 | } 159 | _keepAliveInterval = value; 160 | } 161 | } 162 | 163 | internal int ReceiveBufferSize => _receiveBufferSize; 164 | internal int SendBufferSize => _sendBufferSize; 165 | internal ArraySegment? Buffer => _buffer; 166 | 167 | public void SetBuffer(int receiveBufferSize, int sendBufferSize) 168 | { 169 | ThrowIfReadOnly(); 170 | 171 | if (receiveBufferSize <= 0) 172 | { 173 | throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), receiveBufferSize, SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 1)); 174 | } 175 | if (sendBufferSize <= 0) 176 | { 177 | throw new ArgumentOutOfRangeException(nameof(sendBufferSize), sendBufferSize, SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 1)); 178 | } 179 | 180 | _receiveBufferSize = receiveBufferSize; 181 | _sendBufferSize = sendBufferSize; 182 | _buffer = null; 183 | } 184 | 185 | public void SetBuffer(int receiveBufferSize, int sendBufferSize, ArraySegment buffer) 186 | { 187 | ThrowIfReadOnly(); 188 | 189 | if (receiveBufferSize <= 0) 190 | { 191 | throw new ArgumentOutOfRangeException(nameof(receiveBufferSize), receiveBufferSize, SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 1)); 192 | } 193 | if (sendBufferSize <= 0) 194 | { 195 | throw new ArgumentOutOfRangeException(nameof(sendBufferSize), sendBufferSize, SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 1)); 196 | } 197 | 198 | WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer)); 199 | if (buffer.Count == 0) 200 | { 201 | throw new ArgumentOutOfRangeException(nameof(buffer)); 202 | } 203 | 204 | _receiveBufferSize = receiveBufferSize; 205 | _sendBufferSize = sendBufferSize; 206 | _buffer = buffer; 207 | } 208 | 209 | #endregion WebSocket settings 210 | 211 | #region Helpers 212 | 213 | internal void SetToReadOnly() 214 | { 215 | Debug.Assert(!_isReadOnly, "Already set"); 216 | _isReadOnly = true; 217 | } 218 | 219 | private void ThrowIfReadOnly() 220 | { 221 | if (_isReadOnly) 222 | { 223 | throw new InvalidOperationException(SR.net_WebSockets_AlreadyStarted); 224 | } 225 | } 226 | 227 | #endregion Helpers 228 | } 229 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/HttpKnownHeaderNames.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace System.Net 6 | { 7 | internal static partial class HttpKnownHeaderNames 8 | { 9 | // When adding a new constant, add it to HttpKnownHeaderNames.TryGetHeaderName.cs as well. 10 | 11 | public const string Accept = "Accept"; 12 | public const string AcceptCharset = "Accept-Charset"; 13 | public const string AcceptEncoding = "Accept-Encoding"; 14 | public const string AcceptLanguage = "Accept-Language"; 15 | public const string AcceptPatch = "Accept-Patch"; 16 | public const string AcceptRanges = "Accept-Ranges"; 17 | public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials"; 18 | public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers"; 19 | public const string AccessControlAllowMethods = "Access-Control-Allow-Methods"; 20 | public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin"; 21 | public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers"; 22 | public const string AccessControlMaxAge = "Access-Control-Max-Age"; 23 | public const string Age = "Age"; 24 | public const string Allow = "Allow"; 25 | public const string AltSvc = "Alt-Svc"; 26 | public const string Authorization = "Authorization"; 27 | public const string CacheControl = "Cache-Control"; 28 | public const string Connection = "Connection"; 29 | public const string ContentDisposition = "Content-Disposition"; 30 | public const string ContentEncoding = "Content-Encoding"; 31 | public const string ContentLanguage = "Content-Language"; 32 | public const string ContentLength = "Content-Length"; 33 | public const string ContentLocation = "Content-Location"; 34 | public const string ContentMD5 = "Content-MD5"; 35 | public const string ContentRange = "Content-Range"; 36 | public const string ContentSecurityPolicy = "Content-Security-Policy"; 37 | public const string ContentType = "Content-Type"; 38 | public const string Cookie = "Cookie"; 39 | public const string Cookie2 = "Cookie2"; 40 | public const string Date = "Date"; 41 | public const string ETag = "ETag"; 42 | public const string Expect = "Expect"; 43 | public const string Expires = "Expires"; 44 | public const string From = "From"; 45 | public const string Host = "Host"; 46 | public const string IfMatch = "If-Match"; 47 | public const string IfModifiedSince = "If-Modified-Since"; 48 | public const string IfNoneMatch = "If-None-Match"; 49 | public const string IfRange = "If-Range"; 50 | public const string IfUnmodifiedSince = "If-Unmodified-Since"; 51 | public const string KeepAlive = "Keep-Alive"; 52 | public const string LastModified = "Last-Modified"; 53 | public const string Link = "Link"; 54 | public const string Location = "Location"; 55 | public const string MaxForwards = "Max-Forwards"; 56 | public const string Origin = "Origin"; 57 | public const string P3P = "P3P"; 58 | public const string Pragma = "Pragma"; 59 | public const string ProxyAuthenticate = "Proxy-Authenticate"; 60 | public const string ProxyAuthorization = "Proxy-Authorization"; 61 | public const string ProxyConnection = "Proxy-Connection"; 62 | public const string PublicKeyPins = "Public-Key-Pins"; 63 | public const string Range = "Range"; 64 | public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched. 65 | public const string RetryAfter = "Retry-After"; 66 | public const string SecWebSocketAccept = "Sec-WebSocket-Accept"; 67 | public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions"; 68 | public const string SecWebSocketKey = "Sec-WebSocket-Key"; 69 | public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol"; 70 | public const string SecWebSocketVersion = "Sec-WebSocket-Version"; 71 | public const string Server = "Server"; 72 | public const string SetCookie = "Set-Cookie"; 73 | public const string SetCookie2 = "Set-Cookie2"; 74 | public const string StrictTransportSecurity = "Strict-Transport-Security"; 75 | public const string TE = "TE"; 76 | public const string TSV = "TSV"; 77 | public const string Trailer = "Trailer"; 78 | public const string TransferEncoding = "Transfer-Encoding"; 79 | public const string Upgrade = "Upgrade"; 80 | public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests"; 81 | public const string UserAgent = "User-Agent"; 82 | public const string Vary = "Vary"; 83 | public const string Via = "Via"; 84 | public const string WWWAuthenticate = "WWW-Authenticate"; 85 | public const string Warning = "Warning"; 86 | public const string XAspNetVersion = "X-AspNet-Version"; 87 | public const string XContentDuration = "X-Content-Duration"; 88 | public const string XContentTypeOptions = "X-Content-Type-Options"; 89 | public const string XFrameOptions = "X-Frame-Options"; 90 | public const string XMSEdgeRef = "X-MSEdge-Ref"; 91 | public const string XPoweredBy = "X-Powered-By"; 92 | public const string XRequestID = "X-Request-ID"; 93 | public const string XUACompatible = "X-UA-Compatible"; 94 | } 95 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/ManagedWebSocket.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Buffers; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Numerics; 9 | using System.Runtime.CompilerServices; 10 | using System.Runtime.InteropServices; 11 | using System.Security.Cryptography; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | // NOTE: This file is shared between CoreFX and ASP.NET. Be very thoughtful when changing it. 17 | 18 | namespace System.Net.WebSockets.Managed 19 | { 20 | /// A managed implementation of a web socket that sends and receives data via a . 21 | /// 22 | /// Thread-safety: 23 | /// - It's acceptable to call ReceiveAsync and SendAsync in parallel. One of each may run concurrently. 24 | /// - It's acceptable to have a pending ReceiveAsync while CloseOutputAsync or CloseAsync is called. 25 | /// - Attemping to invoke any other operations in parallel may corrupt the instance. Attempting to invoke 26 | /// a send operation while another is in progress or a receive operation while another is in progress will 27 | /// result in an exception. 28 | /// 29 | internal sealed class ManagedWebSocket : WebSocket 30 | { 31 | /// Creates a from a connected to a websocket endpoint. 32 | /// The connected Stream. 33 | /// true if this is the server-side of the connection; false if this is the client-side of the connection. 34 | /// The agreed upon subprotocol for the connection. 35 | /// The interval to use for keep-alive pings. 36 | /// The buffer size to use for received data. 37 | /// Optional buffer to use for receives. 38 | /// The created instance. 39 | public static ManagedWebSocket CreateFromConnectedStream( 40 | Stream stream, bool isServer, string subprotocol, TimeSpan keepAliveInterval, int receiveBufferSize, ArraySegment? receiveBuffer = null) 41 | { 42 | return new ManagedWebSocket(stream, isServer, subprotocol, keepAliveInterval, receiveBufferSize, receiveBuffer); 43 | } 44 | 45 | /// Per-thread cached 4-byte mask byte array. 46 | [ThreadStatic] 47 | private static byte[] t_headerMask; 48 | 49 | /// Thread-safe random number generator used to generate masks for each send. 50 | private static readonly RandomNumberGenerator s_random = RandomNumberGenerator.Create(); 51 | /// Encoding for the payload of text messages: UTF8 encoding that throws if invalid bytes are discovered, per the RFC. 52 | private static readonly UTF8Encoding s_textEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true); 53 | 54 | /// Valid states to be in when calling SendAsync. 55 | private static readonly WebSocketState[] s_validSendStates = { WebSocketState.Open, WebSocketState.CloseReceived }; 56 | /// Valid states to be in when calling ReceiveAsync. 57 | private static readonly WebSocketState[] s_validReceiveStates = { WebSocketState.Open, WebSocketState.CloseSent }; 58 | /// Valid states to be in when calling CloseOutputAsync. 59 | private static readonly WebSocketState[] s_validCloseOutputStates = { WebSocketState.Open, WebSocketState.CloseReceived }; 60 | /// Valid states to be in when calling CloseAsync. 61 | private static readonly WebSocketState[] s_validCloseStates = { WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent }; 62 | 63 | /// The maximum size in bytes of a message frame header that includes mask bytes. 64 | private const int MaxMessageHeaderLength = 14; 65 | /// The maximum size of a control message payload. 66 | private const int MaxControlPayloadLength = 125; 67 | /// Length of the mask XOR'd with the payload data. 68 | private const int MaskLength = 4; 69 | 70 | /// The stream used to communicate with the remote server. 71 | private readonly Stream _stream; 72 | /// 73 | /// true if this is the server-side of the connection; false if it's client. 74 | /// This impacts masking behavior: clients always mask payloads they send and 75 | /// expect to always receive unmasked payloads, whereas servers always send 76 | /// unmasked payloads and expect to always receive masked payloads. 77 | /// 78 | private readonly bool _isServer = false; 79 | /// The agreed upon subprotocol with the server. 80 | private readonly string _subprotocol; 81 | /// Timer used to send periodic pings to the server, at the interval specified 82 | private readonly Timer _keepAliveTimer; 83 | /// CancellationTokenSource used to abort all current and future operations when anything is canceled or any error occurs. 84 | private readonly CancellationTokenSource _abortSource = new CancellationTokenSource(); 85 | /// Buffer used for reading data from the network. 86 | private byte[] _receiveBuffer; 87 | /// Gets whether the receive buffer came from the ArrayPool. 88 | private readonly bool _receiveBufferFromPool; 89 | /// 90 | /// Tracks the state of the validity of the UTF8 encoding of text payloads. Text may be split across fragments. 91 | /// 92 | private readonly Utf8MessageState _utf8TextState = new Utf8MessageState(); 93 | /// 94 | /// Semaphore used to ensure that calls to SendFrameAsync don't run concurrently. While 95 | /// is used to fail if a caller tries to issue another SendAsync while a previous one is running, internally 96 | /// we use SendFrameAsync as an implementation detail, and it should not cause user requests to SendAsync to fail, 97 | /// nor should such internal usage be allowed to run concurrently with other internal usage or with SendAsync. 98 | /// 99 | private readonly SemaphoreSlim _sendFrameAsyncLock = new SemaphoreSlim(1, 1); 100 | 101 | // We maintain the current WebSocketState in _state. However, we separately maintain _sentCloseFrame and _receivedCloseFrame 102 | // as there isn't a strict ordering between CloseSent and CloseReceived. If we receive a close frame from the server, we need to 103 | // transition to CloseReceived even if we're currently in CloseSent, and if we send a close frame, we need to transition to 104 | // CloseSent even if we're currently in CloseReceived. 105 | 106 | /// The current state of the web socket in the protocol. 107 | private WebSocketState _state = WebSocketState.Open; 108 | /// true if Dispose has been called; otherwise, false. 109 | private bool _disposed; 110 | /// Whether we've ever sent a close frame. 111 | private bool _sentCloseFrame; 112 | /// Whether we've ever received a close frame. 113 | private bool _receivedCloseFrame; 114 | /// The reason for the close, as sent by the server, or null if not yet closed. 115 | private WebSocketCloseStatus? _closeStatus = null; 116 | /// A description of the close reason as sent by the server, or null if not yet closed. 117 | private string _closeStatusDescription = null; 118 | 119 | /// 120 | /// The last header received in a ReceiveAsync. If ReceiveAsync got a header but then 121 | /// returned fewer bytes than was indicated in the header, subsequent ReceiveAsync calls 122 | /// will use the data from the header to construct the subsequent receive results, and 123 | /// the payload length in this header will be decremented to indicate the number of bytes 124 | /// remaining to be received for that header. As a result, between fragments, the payload 125 | /// length in this header should be 0. 126 | /// 127 | private MessageHeader _lastReceiveHeader = new MessageHeader { Opcode = MessageOpcode.Text, Fin = true }; 128 | /// The offset of the next available byte in the _receiveBuffer. 129 | private int _receiveBufferOffset = 0; 130 | /// The number of bytes available in the _receiveBuffer. 131 | private int _receiveBufferCount = 0; 132 | /// 133 | /// When dealing with partially read fragments of binary/text messages, a mask previously received may still 134 | /// apply, and the first new byte received may not correspond to the 0th position in the mask. This value is 135 | /// the next offset into the mask that should be applied. 136 | /// 137 | private int _receivedMaskOffsetOffset = 0; 138 | /// 139 | /// Temporary send buffer. This should be released back to the ArrayPool once it's 140 | /// no longer needed for the current send operation. It is stored as an instance 141 | /// field to minimize needing to pass it around and to avoid it becoming a field on 142 | /// various async state machine objects. 143 | /// 144 | private byte[] _sendBuffer; 145 | /// 146 | /// Whether the last SendAsync had endOfMessage==false. We need to track this so that we 147 | /// can send the subsequent message with a continuation opcode if the last message was a fragment. 148 | /// 149 | private bool _lastSendWasFragment; 150 | /// 151 | /// The task returned from the last SendAsync operation to not complete synchronously. 152 | /// If this is not null and not completed when a subsequent SendAsync is issued, an exception occurs. 153 | /// 154 | private Task _lastSendAsync; 155 | /// 156 | /// The task returned from the last ReceiveAsync operation to not complete synchronously. 157 | /// If this is not null and not completed when a subsequent ReceiveAsync is issued, an exception occurs. 158 | /// 159 | private Task _lastReceiveAsync; 160 | 161 | /// Lock used to protect update and check-and-update operations on _state. 162 | private object StateUpdateLock => _abortSource; 163 | /// 164 | /// We need to coordinate between receives and close operations happening concurrently, as a ReceiveAsync may 165 | /// be pending while a Close{Output}Async is issued, which itself needs to loop until a close frame is received. 166 | /// As such, we need thread-safety in the management of . 167 | /// 168 | private object ReceiveAsyncLock => _utf8TextState; // some object, as we're simply lock'ing on it 169 | 170 | /// Initializes the websocket. 171 | /// The connected Stream. 172 | /// true if this is the server-side of the connection; false if this is the client-side of the connection. 173 | /// The agreed upon subprotocol for the connection. 174 | /// The interval to use for keep-alive pings. 175 | /// The buffer size to use for received data. 176 | /// Optional buffer to use for receives 177 | private ManagedWebSocket(Stream stream, bool isServer, string subprotocol, TimeSpan keepAliveInterval, int receiveBufferSize, ArraySegment? receiveBuffer) 178 | { 179 | Debug.Assert(StateUpdateLock != null, $"Expected {nameof(StateUpdateLock)} to be non-null"); 180 | Debug.Assert(ReceiveAsyncLock != null, $"Expected {nameof(ReceiveAsyncLock)} to be non-null"); 181 | Debug.Assert(StateUpdateLock != ReceiveAsyncLock, "Locks should be different objects"); 182 | 183 | Debug.Assert(stream != null, $"Expected non-null stream"); 184 | Debug.Assert(stream.CanRead, $"Expected readable stream"); 185 | Debug.Assert(stream.CanWrite, $"Expected writeable stream"); 186 | Debug.Assert(keepAliveInterval == Timeout.InfiniteTimeSpan || keepAliveInterval >= TimeSpan.Zero, $"Invalid keepalive interval: {keepAliveInterval}"); 187 | Debug.Assert(receiveBufferSize >= MaxMessageHeaderLength, $"Receive buffer size {receiveBufferSize} is too small"); 188 | 189 | _stream = stream; 190 | _isServer = isServer; 191 | _subprotocol = subprotocol; 192 | 193 | // If we were provided with a buffer to use, use it, as long as it's big enough for our needs, and for simplicity 194 | // as long as we're not supposed to use only a portion of it. If it doesn't meet our criteria, just create a new one. 195 | if (receiveBuffer.HasValue && 196 | receiveBuffer.Value.Offset == 0 && receiveBuffer.Value.Count == receiveBuffer.Value.Array.Length && 197 | receiveBuffer.Value.Count >= MaxMessageHeaderLength) 198 | { 199 | _receiveBuffer = receiveBuffer.Value.Array; 200 | } 201 | else 202 | { 203 | _receiveBufferFromPool = true; 204 | _receiveBuffer = ArrayPool.Shared.Rent(Math.Max(receiveBufferSize, MaxMessageHeaderLength)); 205 | } 206 | 207 | // Set up the abort source so that if it's triggered, we transition the instance appropriately. 208 | _abortSource.Token.Register(s => 209 | { 210 | var thisRef = (ManagedWebSocket)s; 211 | 212 | lock (thisRef.StateUpdateLock) 213 | { 214 | WebSocketState state = thisRef._state; 215 | if (state != WebSocketState.Closed && state != WebSocketState.Aborted) 216 | { 217 | thisRef._state = state != WebSocketState.None && state != WebSocketState.Connecting ? 218 | WebSocketState.Aborted : 219 | WebSocketState.Closed; 220 | } 221 | } 222 | }, this); 223 | 224 | // Now that we're opened, initiate the keep alive timer to send periodic pings 225 | if (keepAliveInterval > TimeSpan.Zero) 226 | { 227 | _keepAliveTimer = new Timer(s => ((ManagedWebSocket)s).SendKeepAliveFrameAsync(), this, keepAliveInterval, keepAliveInterval); 228 | } 229 | } 230 | 231 | public override void Dispose() 232 | { 233 | lock (StateUpdateLock) 234 | { 235 | DisposeCore(); 236 | } 237 | } 238 | 239 | private void DisposeCore() 240 | { 241 | Debug.Assert(Monitor.IsEntered(StateUpdateLock), $"Expected {nameof(StateUpdateLock)} to be held"); 242 | if (!_disposed) 243 | { 244 | _disposed = true; 245 | _keepAliveTimer?.Dispose(); 246 | _stream?.Dispose(); 247 | if (_receiveBufferFromPool) 248 | { 249 | byte[] old = _receiveBuffer; 250 | _receiveBuffer = null; 251 | ArrayPool.Shared.Return(old); 252 | } 253 | if (_state < WebSocketState.Aborted) 254 | { 255 | _state = WebSocketState.Closed; 256 | } 257 | } 258 | } 259 | 260 | public override WebSocketCloseStatus? CloseStatus => _closeStatus; 261 | 262 | public override string CloseStatusDescription => _closeStatusDescription; 263 | 264 | public override WebSocketState State => _state; 265 | 266 | public override string SubProtocol => _subprotocol; 267 | 268 | public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) 269 | { 270 | if (messageType != WebSocketMessageType.Text && messageType != WebSocketMessageType.Binary) 271 | { 272 | throw new ArgumentException(SR.Format( 273 | SR.net_WebSockets_Argument_InvalidMessageType, 274 | nameof(WebSocketMessageType.Close), nameof(SendAsync), nameof(WebSocketMessageType.Binary), nameof(WebSocketMessageType.Text), nameof(CloseOutputAsync)), 275 | nameof(messageType)); 276 | } 277 | WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer)); 278 | 279 | try 280 | { 281 | WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validSendStates); 282 | ThrowIfOperationInProgress(_lastSendAsync); 283 | } 284 | catch (Exception exc) 285 | { 286 | var tcs = new TaskCompletionSource(); 287 | tcs.SetException(exc); 288 | return tcs.Task; 289 | } 290 | 291 | MessageOpcode opcode = 292 | _lastSendWasFragment ? MessageOpcode.Continuation : 293 | messageType == WebSocketMessageType.Binary ? MessageOpcode.Binary : 294 | MessageOpcode.Text; 295 | 296 | Task t = SendFrameAsync(opcode, endOfMessage, buffer, cancellationToken); 297 | _lastSendWasFragment = !endOfMessage; 298 | _lastSendAsync = t; 299 | return t; 300 | } 301 | 302 | public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) 303 | { 304 | WebSocketValidate.ValidateArraySegment(buffer, nameof(buffer)); 305 | 306 | try 307 | { 308 | WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validReceiveStates); 309 | 310 | Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}"); 311 | lock (ReceiveAsyncLock) // synchronize with receives in CloseAsync 312 | { 313 | ThrowIfOperationInProgress(_lastReceiveAsync); 314 | Task t = ReceiveAsyncPrivate(buffer, cancellationToken); 315 | _lastReceiveAsync = t; 316 | return t; 317 | } 318 | } 319 | catch (Exception exc) 320 | { 321 | var tcs = new TaskCompletionSource(); 322 | tcs.SetException(exc); 323 | return tcs.Task; 324 | } 325 | } 326 | 327 | public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 328 | { 329 | WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription); 330 | 331 | try 332 | { 333 | WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validCloseStates); 334 | } 335 | catch (Exception exc) 336 | { 337 | var tcs = new TaskCompletionSource(); 338 | tcs.SetException(exc); 339 | return tcs.Task; 340 | } 341 | 342 | return CloseAsyncPrivate(closeStatus, statusDescription, cancellationToken); 343 | } 344 | 345 | public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 346 | { 347 | WebSocketValidate.ValidateCloseStatus(closeStatus, statusDescription); 348 | 349 | try 350 | { 351 | WebSocketValidate.ThrowIfInvalidState(_state, _disposed, s_validCloseOutputStates); 352 | } 353 | catch (Exception exc) 354 | { 355 | var tcs = new TaskCompletionSource(); 356 | tcs.SetException(exc); 357 | return tcs.Task; 358 | } 359 | 360 | return SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken); 361 | } 362 | 363 | public override void Abort() 364 | { 365 | _abortSource.Cancel(); 366 | Dispose(); // forcibly tear down connection 367 | } 368 | 369 | /// Sends a websocket frame to the network. 370 | /// The opcode for the message. 371 | /// The value of the FIN bit for the message. 372 | /// The buffer containing the payload data fro the message. 373 | /// The CancellationToken to use to cancel the websocket. 374 | private Task SendFrameAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer, CancellationToken cancellationToken) 375 | { 376 | // TODO: #4900 SendFrameAsync should in theory typically complete synchronously, making it fast and allocation free. 377 | // However, due to #4900, it almost always yields, resulting in all of the allocations involved in an async method 378 | // yielding, e.g. the boxed state machine, the Action delegate, the MoveNextRunner, and the resulting Task, plus it's 379 | // common that the awaited operation completes so fast after the await that we may end up allocating an AwaitTaskContinuation 380 | // inside of the TaskAwaiter. Since SendFrameAsync is such a core code path, until that can be fixed, we put some 381 | // optimizations in place to avoid a few of those expenses, at the expense of more complicated code; for the common case, 382 | // this code has fewer than half the number and size of allocations. If/when that issue is fixed, this method should be deleted 383 | // and replaced by SendFrameFallbackAsync, which is the same logic but in a much more easily understand flow. 384 | 385 | // If a cancelable cancellation token was provided, that would require registering with it, which means more state we have to 386 | // pass around (the CancellationTokenRegistration), so if it is cancelable, just immediately go to the fallback path. 387 | // Similarly, it should be rare that there are multiple outstanding calls to SendFrameAsync, but if there are, again 388 | // fall back to the fallback path. 389 | return cancellationToken.CanBeCanceled || !_sendFrameAsyncLock.Wait(0) ? 390 | SendFrameFallbackAsync(opcode, endOfMessage, payloadBuffer, cancellationToken) : 391 | SendFrameLockAcquiredNonCancelableAsync(opcode, endOfMessage, payloadBuffer); 392 | } 393 | 394 | /// Sends a websocket frame to the network. The caller must hold the sending lock. 395 | /// The opcode for the message. 396 | /// The value of the FIN bit for the message. 397 | /// The buffer containing the payload data fro the message. 398 | private Task SendFrameLockAcquiredNonCancelableAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer) 399 | { 400 | Debug.Assert(_sendFrameAsyncLock.CurrentCount == 0, "Caller should hold the _sendFrameAsyncLock"); 401 | 402 | // If we get here, the cancellation token is not cancelable so we don't have to worry about it, 403 | // and we own the semaphore, so we don't need to asynchronously wait for it. 404 | Task writeTask = null; 405 | bool releaseSemaphoreAndSendBuffer = true; 406 | try 407 | { 408 | // Write the payload synchronously to the buffer, then write that buffer out to the network. 409 | int sendBytes = WriteFrameToSendBuffer(opcode, endOfMessage, payloadBuffer); 410 | writeTask = _stream.WriteAsync(_sendBuffer, 0, sendBytes, CancellationToken.None); 411 | 412 | // If the operation happens to complete synchronously (or, more specifically, by 413 | // the time we get from the previous line to here, release the semaphore, propagate 414 | // exceptions, and we're done. 415 | if (writeTask.IsCompleted) 416 | { 417 | writeTask.GetAwaiter().GetResult(); // propagate any exceptions 418 | return Task.FromResult(true); 419 | } 420 | 421 | // Up until this point, if an exception occurred (such as when accessing _stream or when 422 | // calling GetResult), we want to release the semaphore and the send buffer. After this point, 423 | // both need to be held until writeTask completes. 424 | releaseSemaphoreAndSendBuffer = false; 425 | } 426 | catch (Exception exc) 427 | { 428 | var tcs = new TaskCompletionSource(); 429 | tcs.SetException(_state == WebSocketState.Aborted ? 430 | CreateOperationCanceledException(exc) : 431 | new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc)); 432 | return tcs.Task; 433 | } 434 | finally 435 | { 436 | if (releaseSemaphoreAndSendBuffer) 437 | { 438 | _sendFrameAsyncLock.Release(); 439 | ReleaseSendBuffer(); 440 | } 441 | } 442 | 443 | // The write was not yet completed. Create and return a continuation that will 444 | // release the semaphore and translate any exception that occurred. 445 | return writeTask.ContinueWith((t, s) => 446 | { 447 | var thisRef = (ManagedWebSocket)s; 448 | thisRef._sendFrameAsyncLock.Release(); 449 | thisRef.ReleaseSendBuffer(); 450 | 451 | try { t.GetAwaiter().GetResult(); } 452 | catch (Exception exc) 453 | { 454 | throw thisRef._state == WebSocketState.Aborted ? 455 | CreateOperationCanceledException(exc) : 456 | new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); 457 | } 458 | }, this, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default); 459 | } 460 | 461 | private async Task SendFrameFallbackAsync(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer, CancellationToken cancellationToken) 462 | { 463 | await _sendFrameAsyncLock.WaitAsync().ConfigureAwait(false); 464 | try 465 | { 466 | int sendBytes = WriteFrameToSendBuffer(opcode, endOfMessage, payloadBuffer); 467 | using (cancellationToken.Register(s => ((ManagedWebSocket)s).Abort(), this)) 468 | { 469 | await _stream.WriteAsync(_sendBuffer, 0, sendBytes, cancellationToken).ConfigureAwait(false); 470 | } 471 | } 472 | catch (Exception exc) 473 | { 474 | throw _state == WebSocketState.Aborted ? 475 | CreateOperationCanceledException(exc, cancellationToken) : 476 | new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); 477 | } 478 | finally 479 | { 480 | _sendFrameAsyncLock.Release(); 481 | ReleaseSendBuffer(); 482 | } 483 | } 484 | 485 | /// Writes a frame into the send buffer, which can then be sent over the network. 486 | private int WriteFrameToSendBuffer(MessageOpcode opcode, bool endOfMessage, ArraySegment payloadBuffer) 487 | { 488 | // Ensure we have a _sendBuffer. 489 | AllocateSendBuffer(payloadBuffer.Count + MaxMessageHeaderLength); 490 | 491 | // Write the message header data to the buffer. 492 | int headerLength; 493 | int? maskOffset = null; 494 | if (_isServer) 495 | { 496 | // The server doesn't send a mask, so the mask offset returned by WriteHeader 497 | // is actually the end of the header. 498 | headerLength = WriteHeader(opcode, _sendBuffer, payloadBuffer, endOfMessage, useMask: false); 499 | } 500 | else 501 | { 502 | // We need to know where the mask starts so that we can use the mask to manipulate the payload data, 503 | // and we need to know the total length for sending it on the wire. 504 | maskOffset = WriteHeader(opcode, _sendBuffer, payloadBuffer, endOfMessage, useMask: true); 505 | headerLength = maskOffset.GetValueOrDefault() + MaskLength; 506 | } 507 | 508 | // Write the payload 509 | if (payloadBuffer.Count > 0) 510 | { 511 | Buffer.BlockCopy(payloadBuffer.Array, payloadBuffer.Offset, _sendBuffer, headerLength, payloadBuffer.Count); 512 | 513 | // 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 514 | // changing the data in the caller-supplied payload buffer. 515 | if (maskOffset.HasValue) 516 | { 517 | ApplyMask(_sendBuffer, headerLength, _sendBuffer, maskOffset.Value, 0, payloadBuffer.Count); 518 | } 519 | } 520 | 521 | // Return the number of bytes in the send buffer 522 | return headerLength + payloadBuffer.Count; 523 | } 524 | 525 | private void SendKeepAliveFrameAsync() 526 | { 527 | bool acquiredLock = _sendFrameAsyncLock.Wait(0); 528 | if (acquiredLock) 529 | { 530 | // This exists purely to keep the connection alive; don't wait for the result, and ignore any failures. 531 | // The call will handle releasing the lock. 532 | SendFrameLockAcquiredNonCancelableAsync(MessageOpcode.Ping, true, new ArraySegment(new byte[0])); 533 | } 534 | else 535 | { 536 | // If the lock is already held, something is already getting sent, 537 | // so there's no need to send a keep-alive ping. 538 | } 539 | } 540 | 541 | private static int WriteHeader(MessageOpcode opcode, byte[] sendBuffer, ArraySegment payload, bool endOfMessage, bool useMask) 542 | { 543 | // Client header format: 544 | // 1 bit - FIN - 1 if this is the final fragment in the message (it could be the only fragment), otherwise 0 545 | // 1 bit - RSV1 - Reserved - 0 546 | // 1 bit - RSV2 - Reserved - 0 547 | // 1 bit - RSV3 - Reserved - 0 548 | // 4 bits - Opcode - How to interpret the payload 549 | // - 0x0 - continuation 550 | // - 0x1 - text 551 | // - 0x2 - binary 552 | // - 0x8 - connection close 553 | // - 0x9 - ping 554 | // - 0xA - pong 555 | // - (0x3 to 0x7, 0xB-0xF - reserved) 556 | // 1 bit - Masked - 1 if the payload is masked, 0 if it's not. Must be 1 for the client 557 | // 7 bits, 7+16 bits, or 7+64 bits - Payload length 558 | // - For length 0 through 125, 7 bits storing the length 559 | // - For lengths 126 through 2^16, 7 bits storing the value 126, followed by 16 bits storing the length 560 | // - For lengths 2^16+1 through 2^64, 7 bits storing the value 127, followed by 64 bytes storing the length 561 | // 0 or 4 bytes - Mask, if Masked is 1 - random value XOR'd with each 4 bytes of the payload, round-robin 562 | // Length bytes - Payload data 563 | 564 | Debug.Assert(sendBuffer.Length >= MaxMessageHeaderLength, $"Expected sendBuffer to be at least {MaxMessageHeaderLength}, got {sendBuffer.Length}"); 565 | 566 | sendBuffer[0] = (byte)opcode; // 4 bits for the opcode 567 | if (endOfMessage) 568 | { 569 | sendBuffer[0] |= 0x80; // 1 bit for FIN 570 | } 571 | 572 | // Store the payload length. 573 | int maskOffset; 574 | if (payload.Count <= 125) 575 | { 576 | sendBuffer[1] = (byte)payload.Count; 577 | maskOffset = 2; // no additional payload length 578 | } 579 | else if (payload.Count <= ushort.MaxValue) 580 | { 581 | sendBuffer[1] = 126; 582 | sendBuffer[2] = (byte)(payload.Count / 256); 583 | sendBuffer[3] = unchecked((byte)payload.Count); 584 | maskOffset = 2 + sizeof(ushort); // additional 2 bytes for 16-bit length 585 | } 586 | else 587 | { 588 | sendBuffer[1] = 127; 589 | int length = payload.Count; 590 | for (int i = 9; i >= 2; i--) 591 | { 592 | sendBuffer[i] = unchecked((byte)length); 593 | length = length / 256; 594 | } 595 | maskOffset = 2 + sizeof(ulong); // additional 8 bytes for 64-bit length 596 | } 597 | 598 | if (useMask) 599 | { 600 | // Generate the mask. 601 | sendBuffer[1] |= 0x80; 602 | WriteRandomMask(sendBuffer, maskOffset); 603 | } 604 | 605 | // Return the position of the mask. 606 | return maskOffset; 607 | } 608 | 609 | /// Writes a 4-byte random mask to the specified buffer at the specified offset. 610 | /// The buffer to which to write the mask. 611 | /// The offset into the buffer at which to write the mask. 612 | private static void WriteRandomMask(byte[] buffer, int offset) 613 | { 614 | byte[] mask = t_headerMask ?? (t_headerMask = new byte[MaskLength]); 615 | Debug.Assert(mask.Length == MaskLength, $"Expected mask of length {MaskLength}, got {mask.Length}"); 616 | s_random.GetBytes(mask); 617 | Buffer.BlockCopy(mask, 0, buffer, offset, MaskLength); 618 | } 619 | 620 | /// 621 | /// Receive the next text, binary, continuation, or close message, returning information about it and 622 | /// writing its payload into the supplied buffer. Other control messages may be consumed and processed 623 | /// as part of this operation, but data about them will not be returned. 624 | /// 625 | /// The buffer into which payload data should be written. 626 | /// The CancellationToken used to cancel the websocket. 627 | /// Information about the received message. 628 | private async Task ReceiveAsyncPrivate(ArraySegment payloadBuffer, CancellationToken cancellationToken) 629 | { 630 | // This is a long method. While splitting it up into pieces would arguably help with readability, doing so would 631 | // also result in more allocations, as each async method that yields ends up with multiple allocations. The impact 632 | // of those allocations is amortized across all of the awaits in the method, and since we generally expect a receive 633 | // operation to require at most a single yield (while waiting for data to arrive), it's more efficient to have 634 | // everything in the one method. We do separate out pieces for handling close and ping/pong messages, as we expect 635 | // those to be much less frequent (e.g. we should only get one close per websocket), and thus we can afford to pay 636 | // a bit more for readability and maintainability. 637 | 638 | CancellationTokenRegistration registration = cancellationToken.Register(s => ((ManagedWebSocket)s).Abort(), this); 639 | try 640 | { 641 | while (true) // in case we get control frames that should be ignored from the user's perspective 642 | { 643 | // Get the last received header. If its payload length is non-zero, that means we previously 644 | // received the header but were only able to read a part of the fragment, so we should skip 645 | // reading another header and just proceed to use that same header and read more data associated 646 | // with it. If instead its payload length is zero, then we've completed the processing of 647 | // thta message, and we should read the next header. 648 | MessageHeader header = _lastReceiveHeader; 649 | if (header.PayloadLength == 0) 650 | { 651 | if (_receiveBufferCount < (_isServer ? (MaxMessageHeaderLength - MaskLength) : MaxMessageHeaderLength)) 652 | { 653 | // Make sure we have the first two bytes, which includes the start of the payload length. 654 | if (_receiveBufferCount < 2) 655 | { 656 | await EnsureBufferContainsAsync(2, cancellationToken, throwOnPrematureClosure: true).ConfigureAwait(false); 657 | } 658 | 659 | // Then make sure we have the full header based on the payload length. 660 | // If this is the server, we also need room for the received mask. 661 | long payloadLength = _receiveBuffer[_receiveBufferOffset + 1] & 0x7F; 662 | if (_isServer || payloadLength > 125) 663 | { 664 | int minNeeded = 665 | 2 + 666 | (_isServer ? MaskLength : 0) + 667 | (payloadLength <= 125 ? 0 : payloadLength == 126 ? sizeof(ushort) : sizeof(ulong)); // additional 2 or 8 bytes for 16-bit or 64-bit length 668 | await EnsureBufferContainsAsync(minNeeded, cancellationToken).ConfigureAwait(false); 669 | } 670 | } 671 | 672 | if (!TryParseMessageHeaderFromReceiveBuffer(out header)) 673 | { 674 | await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); 675 | } 676 | _receivedMaskOffsetOffset = 0; 677 | } 678 | 679 | // If the header represents a ping or a pong, it's a control message meant 680 | // to be transparent to the user, so handle it and then loop around to read again. 681 | // Alternatively, if it's a close message, handle it and exit. 682 | if (header.Opcode == MessageOpcode.Ping || header.Opcode == MessageOpcode.Pong) 683 | { 684 | await HandleReceivedPingPongAsync(header, cancellationToken).ConfigureAwait(false); 685 | continue; 686 | } 687 | else if (header.Opcode == MessageOpcode.Close) 688 | { 689 | return await HandleReceivedCloseAsync(header, cancellationToken).ConfigureAwait(false); 690 | } 691 | 692 | // If this is a continuation, replace the opcode with the one of the message it's continuing 693 | if (header.Opcode == MessageOpcode.Continuation) 694 | { 695 | header.Opcode = _lastReceiveHeader.Opcode; 696 | } 697 | 698 | // The message should now be a binary or text message. Handle it by reading the payload and returning the contents. 699 | Debug.Assert(header.Opcode == MessageOpcode.Binary || header.Opcode == MessageOpcode.Text, $"Unexpected opcode {header.Opcode}"); 700 | 701 | // If there's no data to read, return an appropriate result. 702 | int bytesToRead = (int)Math.Min(payloadBuffer.Count, header.PayloadLength); 703 | if (bytesToRead == 0) 704 | { 705 | _lastReceiveHeader = header; 706 | return new WebSocketReceiveResult( 707 | 0, 708 | header.Opcode == MessageOpcode.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, 709 | header.PayloadLength == 0 ? header.Fin : false); 710 | } 711 | 712 | // Otherwise, read as much of the payload as we can efficiently, and upate the header to reflect how much data 713 | // remains for future reads. 714 | 715 | if (_receiveBufferCount == 0) 716 | { 717 | await EnsureBufferContainsAsync(1, cancellationToken, throwOnPrematureClosure: false).ConfigureAwait(false); 718 | } 719 | 720 | int bytesToCopy = Math.Min(bytesToRead, _receiveBufferCount); 721 | if (_isServer) 722 | { 723 | _receivedMaskOffsetOffset = ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, _receivedMaskOffsetOffset, bytesToCopy); 724 | } 725 | Buffer.BlockCopy(_receiveBuffer, _receiveBufferOffset, payloadBuffer.Array, payloadBuffer.Offset, bytesToCopy); 726 | ConsumeFromBuffer(bytesToCopy); 727 | header.PayloadLength -= bytesToCopy; 728 | 729 | // If this a text message, validate that it contains valid UTF8. 730 | if (header.Opcode == MessageOpcode.Text && 731 | !TryValidateUtf8(new ArraySegment(payloadBuffer.Array, payloadBuffer.Offset, bytesToCopy), header.Fin, _utf8TextState)) 732 | { 733 | await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.InvalidPayloadData, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); 734 | } 735 | 736 | _lastReceiveHeader = header; 737 | return new WebSocketReceiveResult( 738 | bytesToCopy, 739 | header.Opcode == MessageOpcode.Text ? WebSocketMessageType.Text : WebSocketMessageType.Binary, 740 | bytesToCopy == 0 || (header.Fin && header.PayloadLength == 0)); 741 | } 742 | } 743 | catch (Exception exc) 744 | { 745 | if (_state == WebSocketState.Aborted) 746 | { 747 | throw new OperationCanceledException(nameof(WebSocketState.Aborted), exc); 748 | } 749 | throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely, exc); 750 | } 751 | finally 752 | { 753 | registration.Dispose(); 754 | } 755 | } 756 | 757 | /// Processes a received close message. 758 | /// The message header. 759 | /// The cancellation token to use to cancel the websocket. 760 | /// The received result message. 761 | private async Task HandleReceivedCloseAsync( 762 | MessageHeader header, CancellationToken cancellationToken) 763 | { 764 | lock (StateUpdateLock) 765 | { 766 | _receivedCloseFrame = true; 767 | if (_state < WebSocketState.CloseReceived) 768 | { 769 | _state = WebSocketState.CloseReceived; 770 | } 771 | } 772 | 773 | WebSocketCloseStatus closeStatus = WebSocketCloseStatus.NormalClosure; 774 | string closeStatusDescription = string.Empty; 775 | 776 | // Handle any payload by parsing it into the close status and description. 777 | if (header.PayloadLength == 1) 778 | { 779 | // The close payload length can be 0 or >= 2, but not 1. 780 | await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); 781 | } 782 | else if (header.PayloadLength >= 2) 783 | { 784 | if (_receiveBufferCount < header.PayloadLength) 785 | { 786 | await EnsureBufferContainsAsync((int)header.PayloadLength, cancellationToken).ConfigureAwait(false); 787 | } 788 | 789 | if (_isServer) 790 | { 791 | ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, 0, header.PayloadLength); 792 | } 793 | 794 | closeStatus = (WebSocketCloseStatus)(_receiveBuffer[_receiveBufferOffset] << 8 | _receiveBuffer[_receiveBufferOffset + 1]); 795 | if (!IsValidCloseStatus(closeStatus)) 796 | { 797 | await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken).ConfigureAwait(false); 798 | } 799 | 800 | if (header.PayloadLength > 2) 801 | { 802 | try 803 | { 804 | closeStatusDescription = s_textEncoding.GetString(_receiveBuffer, _receiveBufferOffset + 2, (int)header.PayloadLength - 2); 805 | } 806 | catch (DecoderFallbackException exc) 807 | { 808 | await CloseWithReceiveErrorAndThrowAsync(WebSocketCloseStatus.ProtocolError, WebSocketError.Faulted, cancellationToken, exc).ConfigureAwait(false); 809 | } 810 | } 811 | ConsumeFromBuffer((int)header.PayloadLength); 812 | } 813 | 814 | // Store the close status and description onto the instance. 815 | _closeStatus = closeStatus; 816 | _closeStatusDescription = closeStatusDescription; 817 | 818 | // And return them as part of the result message. 819 | return new WebSocketReceiveResult(0, WebSocketMessageType.Close, true, closeStatus, closeStatusDescription); 820 | } 821 | 822 | /// Processes a received ping or pong message. 823 | /// The message header. 824 | /// The cancellation token to use to cancel the websocket. 825 | private async Task HandleReceivedPingPongAsync(MessageHeader header, CancellationToken cancellationToken) 826 | { 827 | // Consume any (optional) payload associated with the ping/pong. 828 | if (header.PayloadLength > 0 && _receiveBufferCount < header.PayloadLength) 829 | { 830 | await EnsureBufferContainsAsync((int)header.PayloadLength, cancellationToken).ConfigureAwait(false); 831 | } 832 | 833 | // If this was a ping, send back a pong response. 834 | if (header.Opcode == MessageOpcode.Ping) 835 | { 836 | if (_isServer) 837 | { 838 | ApplyMask(_receiveBuffer, _receiveBufferOffset, header.Mask, 0, header.PayloadLength); 839 | } 840 | 841 | await SendFrameAsync( 842 | MessageOpcode.Pong, true, 843 | new ArraySegment(_receiveBuffer, _receiveBufferOffset, (int)header.PayloadLength), cancellationToken).ConfigureAwait(false); 844 | } 845 | 846 | // Regardless of whether it was a ping or pong, we no longer need the payload. 847 | if (header.PayloadLength > 0) 848 | { 849 | ConsumeFromBuffer((int)header.PayloadLength); 850 | } 851 | } 852 | 853 | /// Check whether a close status is valid according to the RFC. 854 | /// The status to validate. 855 | /// true if the status if valid; otherwise, false. 856 | private static bool IsValidCloseStatus(WebSocketCloseStatus closeStatus) 857 | { 858 | // 0-999: "not used" 859 | // 1000-2999: reserved for the protocol; we need to check individual codes manually 860 | // 3000-3999: reserved for use by higher-level code 861 | // 4000-4999: reserved for private use 862 | // 5000-: not mentioned in RFC 863 | 864 | if (closeStatus < (WebSocketCloseStatus)1000 || closeStatus >= (WebSocketCloseStatus)5000) 865 | { 866 | return false; 867 | } 868 | 869 | if (closeStatus >= (WebSocketCloseStatus)3000) 870 | { 871 | return true; 872 | } 873 | 874 | switch (closeStatus) // check for the 1000-2999 range known codes 875 | { 876 | case WebSocketCloseStatus.EndpointUnavailable: 877 | case WebSocketCloseStatus.InternalServerError: 878 | case WebSocketCloseStatus.InvalidMessageType: 879 | case WebSocketCloseStatus.InvalidPayloadData: 880 | case WebSocketCloseStatus.MandatoryExtension: 881 | case WebSocketCloseStatus.MessageTooBig: 882 | case WebSocketCloseStatus.NormalClosure: 883 | case WebSocketCloseStatus.PolicyViolation: 884 | case WebSocketCloseStatus.ProtocolError: 885 | return true; 886 | 887 | default: 888 | return false; 889 | } 890 | } 891 | 892 | /// Send a close message to the server and throw an exception, in response to getting bad data from the server. 893 | /// The close status code to use. 894 | /// The error reason. 895 | /// The CancellationToken used to cancel the websocket. 896 | /// An optional inner exception to include in the thrown exception. 897 | private async Task CloseWithReceiveErrorAndThrowAsync( 898 | WebSocketCloseStatus closeStatus, WebSocketError error, CancellationToken cancellationToken, Exception innerException = null) 899 | { 900 | // Close the connection if it hasn't already been closed 901 | if (!_sentCloseFrame) 902 | { 903 | await CloseOutputAsync(closeStatus, string.Empty, cancellationToken).ConfigureAwait(false); 904 | } 905 | 906 | // Dump our receive buffer; we're in a bad state to do any further processing 907 | _receiveBufferCount = 0; 908 | 909 | // Let the caller know we've failed 910 | throw new WebSocketException(error, innerException); 911 | } 912 | 913 | /// Parses a message header from the buffer. This assumes the header is in the buffer. 914 | /// The read header. 915 | /// true if a header was read; false if the header was invalid. 916 | private bool TryParseMessageHeaderFromReceiveBuffer(out MessageHeader resultHeader) 917 | { 918 | Debug.Assert(_receiveBufferCount >= 2, $"Expected to at least have the first two bytes of the header."); 919 | 920 | var header = new MessageHeader(); 921 | 922 | header.Fin = (_receiveBuffer[_receiveBufferOffset] & 0x80) != 0; 923 | bool reservedSet = (_receiveBuffer[_receiveBufferOffset] & 0x70) != 0; 924 | header.Opcode = (MessageOpcode)(_receiveBuffer[_receiveBufferOffset] & 0xF); 925 | 926 | bool masked = (_receiveBuffer[_receiveBufferOffset + 1] & 0x80) != 0; 927 | header.PayloadLength = _receiveBuffer[_receiveBufferOffset + 1] & 0x7F; 928 | 929 | ConsumeFromBuffer(2); 930 | 931 | // Read the remainder of the payload length, if necessary 932 | if (header.PayloadLength == 126) 933 | { 934 | Debug.Assert(_receiveBufferCount >= 2, $"Expected to have two bytes for the payload length."); 935 | header.PayloadLength = (_receiveBuffer[_receiveBufferOffset] << 8) | _receiveBuffer[_receiveBufferOffset + 1]; 936 | ConsumeFromBuffer(2); 937 | } 938 | else if (header.PayloadLength == 127) 939 | { 940 | Debug.Assert(_receiveBufferCount >= 8, $"Expected to have eight bytes for the payload length."); 941 | header.PayloadLength = 0; 942 | for (int i = 0; i < 8; i++) 943 | { 944 | header.PayloadLength = (header.PayloadLength << 8) | _receiveBuffer[_receiveBufferOffset + i]; 945 | } 946 | ConsumeFromBuffer(8); 947 | } 948 | 949 | bool shouldFail = reservedSet; 950 | if (masked) 951 | { 952 | if (!_isServer) 953 | { 954 | shouldFail = true; 955 | } 956 | header.Mask = CombineMaskBytes(_receiveBuffer, _receiveBufferOffset); 957 | 958 | // Consume the mask bytes 959 | ConsumeFromBuffer(4); 960 | } 961 | 962 | // Do basic validation of the header 963 | switch (header.Opcode) 964 | { 965 | case MessageOpcode.Continuation: 966 | if (_lastReceiveHeader.Fin) 967 | { 968 | // Can't continue from a final message 969 | shouldFail = true; 970 | } 971 | break; 972 | 973 | case MessageOpcode.Binary: 974 | case MessageOpcode.Text: 975 | if (!_lastReceiveHeader.Fin) 976 | { 977 | // Must continue from a non-final message 978 | shouldFail = true; 979 | } 980 | break; 981 | 982 | case MessageOpcode.Close: 983 | case MessageOpcode.Ping: 984 | case MessageOpcode.Pong: 985 | if (header.PayloadLength > MaxControlPayloadLength || !header.Fin) 986 | { 987 | // Invalid control messgae 988 | shouldFail = true; 989 | } 990 | break; 991 | 992 | default: 993 | // Unknown opcode 994 | shouldFail = true; 995 | break; 996 | } 997 | 998 | // Return the read header 999 | resultHeader = header; 1000 | return !shouldFail; 1001 | } 1002 | 1003 | /// Send a close message, then receive until we get a close response message. 1004 | /// The close status to send. 1005 | /// The close status description to send. 1006 | /// The CancellationToken to use to cancel the websocket. 1007 | private async Task CloseAsyncPrivate(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) 1008 | { 1009 | // Send the close message. Skip sending a close frame if we're currently in a CloseSent state, 1010 | // for example having just done a CloseOutputAsync. 1011 | if (!_sentCloseFrame) 1012 | { 1013 | await SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken).ConfigureAwait(false); 1014 | } 1015 | 1016 | // We should now either be in a CloseSent case (because we just sent one), or in a CloseReceived state, in case 1017 | // there was a concurrent receive that ended up handling an immediate close frame response from the server. 1018 | // Of course it could also be Aborted if something happened concurrently to cause things to blow up. 1019 | Debug.Assert( 1020 | State == WebSocketState.CloseSent || 1021 | State == WebSocketState.CloseReceived || 1022 | State == WebSocketState.Aborted, 1023 | $"Unexpected state {State}."); 1024 | 1025 | // Wait until we've received a close response 1026 | byte[] closeBuffer = ArrayPool.Shared.Rent(MaxMessageHeaderLength + MaxControlPayloadLength); 1027 | try 1028 | { 1029 | while (!_receivedCloseFrame) 1030 | { 1031 | Debug.Assert(!Monitor.IsEntered(StateUpdateLock), $"{nameof(StateUpdateLock)} must never be held when acquiring {nameof(ReceiveAsyncLock)}"); 1032 | Task receiveTask; 1033 | lock (ReceiveAsyncLock) 1034 | { 1035 | // Now that we're holding the ReceiveAsyncLock, double-check that we've not yet received the close frame. 1036 | // It could have been received between our check above and now due to a concurrent receive completing. 1037 | if (_receivedCloseFrame) 1038 | { 1039 | break; 1040 | } 1041 | 1042 | // We've not yet processed a received close frame, which means we need to wait for a received close to complete. 1043 | // There may already be one in flight, in which case we want to just wait for that one rather than kicking off 1044 | // another (we don't support concurrent receive operations). We need to kick off a new receive if either we've 1045 | // never issued a receive or if the last issued receive completed for reasons other than a close frame. There is 1046 | // a race condition here, e.g. if there's a in-flight receive that completes after we check, but that's fine: worst 1047 | // case is we then await it, find that it's not what we need, and try again. 1048 | receiveTask = _lastReceiveAsync; 1049 | if (receiveTask == null || 1050 | (receiveTask.Status == TaskStatus.RanToCompletion && receiveTask.Result.MessageType != WebSocketMessageType.Close)) 1051 | { 1052 | _lastReceiveAsync = receiveTask = ReceiveAsyncPrivate(new ArraySegment(closeBuffer), cancellationToken); 1053 | } 1054 | } 1055 | 1056 | // Wait for whatever receive task we have. We'll then loop around again to re-check our state. 1057 | Debug.Assert(receiveTask != null); 1058 | await receiveTask.ConfigureAwait(false); 1059 | } 1060 | } 1061 | finally 1062 | { 1063 | ArrayPool.Shared.Return(closeBuffer); 1064 | } 1065 | 1066 | // We're closed. Close the connection and update the status. 1067 | lock (StateUpdateLock) 1068 | { 1069 | DisposeCore(); 1070 | if (_state < WebSocketState.Closed) 1071 | { 1072 | _state = WebSocketState.Closed; 1073 | } 1074 | } 1075 | } 1076 | 1077 | /// Sends a close message to the server. 1078 | /// The close status to send. 1079 | /// The close status description to send. 1080 | /// The CancellationToken to use to cancel the websocket. 1081 | private async Task SendCloseFrameAsync(WebSocketCloseStatus closeStatus, string closeStatusDescription, CancellationToken cancellationToken) 1082 | { 1083 | // Close payload is two bytes containing the close status followed by a UTF8-encoding of the status description, if it exists. 1084 | 1085 | byte[] buffer = null; 1086 | try 1087 | { 1088 | int count = 2; 1089 | if (string.IsNullOrEmpty(closeStatusDescription)) 1090 | { 1091 | buffer = ArrayPool.Shared.Rent(count); 1092 | } 1093 | else 1094 | { 1095 | count += s_textEncoding.GetByteCount(closeStatusDescription); 1096 | buffer = ArrayPool.Shared.Rent(count); 1097 | int encodedLength = s_textEncoding.GetBytes(closeStatusDescription, 0, closeStatusDescription.Length, buffer, 2); 1098 | Debug.Assert(count - 2 == encodedLength, $"GetByteCount and GetBytes encoded count didn't match"); 1099 | } 1100 | 1101 | ushort closeStatusValue = (ushort)closeStatus; 1102 | buffer[0] = (byte)(closeStatusValue >> 8); 1103 | buffer[1] = (byte)(closeStatusValue & 0xFF); 1104 | 1105 | await SendFrameAsync(MessageOpcode.Close, true, new ArraySegment(buffer, 0, count), cancellationToken).ConfigureAwait(false); 1106 | } 1107 | finally 1108 | { 1109 | if (buffer != null) 1110 | { 1111 | ArrayPool.Shared.Return(buffer); 1112 | } 1113 | } 1114 | 1115 | lock (StateUpdateLock) 1116 | { 1117 | _sentCloseFrame = true; 1118 | if (_state <= WebSocketState.CloseReceived) 1119 | { 1120 | _state = WebSocketState.CloseSent; 1121 | } 1122 | } 1123 | } 1124 | 1125 | private void ConsumeFromBuffer(int count) 1126 | { 1127 | Debug.Assert(count >= 0, $"Expected non-negative count, got {count}"); 1128 | Debug.Assert(count <= _receiveBufferCount, $"Trying to consume {count}, which is more than exists {_receiveBufferCount}"); 1129 | _receiveBufferCount -= count; 1130 | _receiveBufferOffset += count; 1131 | } 1132 | 1133 | private async Task EnsureBufferContainsAsync(int minimumRequiredBytes, CancellationToken cancellationToken, bool throwOnPrematureClosure = true) 1134 | { 1135 | Debug.Assert(minimumRequiredBytes <= _receiveBuffer.Length, $"Requested number of bytes {minimumRequiredBytes} must not exceed {_receiveBuffer.Length}"); 1136 | 1137 | // If we don't have enough data in the buffer to satisfy the minimum required, read some more. 1138 | if (_receiveBufferCount < minimumRequiredBytes) 1139 | { 1140 | // If there's any data in the buffer, shift it down. 1141 | if (_receiveBufferCount > 0) 1142 | { 1143 | Buffer.BlockCopy(_receiveBuffer, _receiveBufferOffset, _receiveBuffer, 0, _receiveBufferCount); 1144 | } 1145 | _receiveBufferOffset = 0; 1146 | 1147 | // While we don't have enough data, read more. 1148 | while (_receiveBufferCount < minimumRequiredBytes) 1149 | { 1150 | int numRead = await _stream.ReadAsync(_receiveBuffer, _receiveBufferCount, _receiveBuffer.Length - _receiveBufferCount, cancellationToken).ConfigureAwait(false); 1151 | Debug.Assert(numRead >= 0, $"Expected non-negative bytes read, got {numRead}"); 1152 | _receiveBufferCount += numRead; 1153 | if (numRead == 0) 1154 | { 1155 | // The connection closed before we were able to read everything we needed. 1156 | // If it was due to use being disposed, fail. If it was due to the connection 1157 | // being closed and it wasn't expected, fail. If it was due to the connection 1158 | // being closed and that was expected, exit gracefully. 1159 | if (_disposed) 1160 | { 1161 | throw new ObjectDisposedException(nameof(ClientWebSocket)); 1162 | } 1163 | else if (throwOnPrematureClosure) 1164 | { 1165 | throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely); 1166 | } 1167 | break; 1168 | } 1169 | } 1170 | } 1171 | } 1172 | 1173 | /// Gets a send buffer from the pool. 1174 | private void AllocateSendBuffer(int minLength) 1175 | { 1176 | Debug.Assert(_sendBuffer == null); // would only fail if had some catastrophic error previously that prevented cleaning up 1177 | _sendBuffer = ArrayPool.Shared.Rent(minLength); 1178 | } 1179 | 1180 | /// Releases the send buffer to the pool. 1181 | private void ReleaseSendBuffer() 1182 | { 1183 | byte[] old = _sendBuffer; 1184 | if (old != null) 1185 | { 1186 | _sendBuffer = null; 1187 | ArrayPool.Shared.Return(old); 1188 | } 1189 | } 1190 | 1191 | private static unsafe int CombineMaskBytes(byte[] buffer, int maskOffset) => 1192 | BitConverter.ToInt32(buffer, maskOffset); 1193 | 1194 | /// Applies a mask to a portion of a byte array. 1195 | /// The buffer to which the mask should be applied. 1196 | /// The offset into at which the mask should start to be applied. 1197 | /// The array containing the mask to apply. 1198 | /// The offset into of the mask to apply of length . 1199 | /// The next position offset from of which by to apply next from the mask. 1200 | /// The number of bytes starting from to which the mask should be applied. 1201 | /// The updated maskOffsetOffset value. 1202 | private static int ApplyMask(byte[] toMask, int toMaskOffset, byte[] mask, int maskOffset, int maskOffsetIndex, long count) 1203 | { 1204 | Debug.Assert(maskOffsetIndex < MaskLength, $"Unexpected {nameof(maskOffsetIndex)}: {maskOffsetIndex}"); 1205 | Debug.Assert(mask.Length >= MaskLength + maskOffset, $"Unexpected inputs: {mask.Length}, {maskOffset}"); 1206 | return ApplyMask(toMask, toMaskOffset, CombineMaskBytes(mask, maskOffset), maskOffsetIndex, count); 1207 | } 1208 | 1209 | /// Applies a mask to a portion of a byte array. 1210 | /// The buffer to which the mask should be applied. 1211 | /// The offset into at which the mask should start to be applied. 1212 | /// The four-byte mask, stored as an Int32. 1213 | /// The index into the mask. 1214 | /// The number of bytes to mask. 1215 | /// The next index into the mask to be used for future applications of the mask. 1216 | private static unsafe int ApplyMask(byte[] toMask, int toMaskOffset, int mask, int maskIndex, long count) 1217 | { 1218 | int maskShift = maskIndex * 8; 1219 | int shiftedMask = (int)(((uint)mask >> maskShift) | ((uint)mask << (32 - maskShift))); 1220 | 1221 | // Try to use SIMD. We can if the number of bytes we're trying to mask is at least as much 1222 | // as the width of a vector and if the width is an even multiple of the mask. 1223 | if (Vector.IsHardwareAccelerated && 1224 | Vector.Count % sizeof(int) == 0 && 1225 | count >= Vector.Count) 1226 | { 1227 | // Mask bytes a vector at a time. 1228 | Vector maskVector = Vector.AsVectorByte(new Vector(shiftedMask)); 1229 | while (count >= Vector.Count) 1230 | { 1231 | count -= Vector.Count; 1232 | (maskVector ^ new Vector(toMask, toMaskOffset)).CopyTo(toMask, toMaskOffset); 1233 | toMaskOffset += Vector.Count; 1234 | } 1235 | 1236 | // Fall through to processing any remaining bytes that were less than a vector width. 1237 | // Since we processed full masks at a time, we don't need to update maskIndex, and 1238 | // toMaskOffset has already been updated to point to the correct location. 1239 | } 1240 | 1241 | // If there are any bytes remaining (either we couldn't use vectors, or the count wasn't 1242 | // an even multiple of the vector width), process them without vectors. 1243 | if (count > 0) 1244 | { 1245 | fixed (byte* toMaskPtr = toMask) 1246 | { 1247 | // Get the location in the target array to continue processing. 1248 | byte* p = toMaskPtr + toMaskOffset; 1249 | 1250 | // Try to go an int at a time if the remaining data is 4-byte aligned and there's enough remaining. 1251 | if (((long)p % sizeof(int)) == 0) 1252 | { 1253 | while (count >= sizeof(int)) 1254 | { 1255 | count -= sizeof(int); 1256 | *((int*)p) ^= shiftedMask; 1257 | p += sizeof(int); 1258 | } 1259 | 1260 | // We don't need to update the maskIndex, as its mod-4 value won't have changed. 1261 | // `p` points to the remainder. 1262 | } 1263 | 1264 | // Process any remaining data a byte at a time. 1265 | if (count > 0) 1266 | { 1267 | byte* maskPtr = (byte*)&mask; 1268 | byte* end = p + count; 1269 | while (p < end) 1270 | { 1271 | *p++ ^= maskPtr[maskIndex]; 1272 | maskIndex = (maskIndex + 1) & 3; 1273 | } 1274 | } 1275 | } 1276 | } 1277 | 1278 | // Return the updated index. 1279 | return maskIndex; 1280 | } 1281 | 1282 | /// Aborts the websocket and throws an exception if an existing operation is in progress. 1283 | private void ThrowIfOperationInProgress(Task operationTask, [CallerMemberName] string methodName = null) 1284 | { 1285 | if (operationTask != null && !operationTask.IsCompleted) 1286 | { 1287 | Abort(); 1288 | throw new InvalidOperationException(SR.Format(SR.net_Websockets_AlreadyOneOutstandingOperation, methodName)); 1289 | } 1290 | } 1291 | 1292 | /// Creates an OperationCanceledException instance, using a default message and the specified inner exception and token. 1293 | private static Exception CreateOperationCanceledException(Exception innerException, CancellationToken cancellationToken = default(CancellationToken)) 1294 | { 1295 | return new OperationCanceledException( 1296 | new OperationCanceledException().Message, 1297 | innerException, 1298 | cancellationToken); 1299 | } 1300 | 1301 | // From https://raw.githubusercontent.com/aspnet/WebSockets/dev/src/Microsoft.AspNetCore.WebSockets.Protocol/Utilities.cs 1302 | // Performs a stateful validation of UTF-8 bytes. 1303 | // It checks for valid formatting, overlong encodings, surrogates, and value ranges. 1304 | private static bool TryValidateUtf8(ArraySegment arraySegment, bool endOfMessage, Utf8MessageState state) 1305 | { 1306 | for (int i = arraySegment.Offset; i < arraySegment.Offset + arraySegment.Count;) 1307 | { 1308 | // Have we started a character sequence yet? 1309 | if (!state.SequenceInProgress) 1310 | { 1311 | // The first byte tells us how many bytes are in the sequence. 1312 | state.SequenceInProgress = true; 1313 | byte b = arraySegment.Array[i]; 1314 | i++; 1315 | if ((b & 0x80) == 0) // 0bbbbbbb, single byte 1316 | { 1317 | state.AdditionalBytesExpected = 0; 1318 | state.CurrentDecodeBits = b & 0x7F; 1319 | state.ExpectedValueMin = 0; 1320 | } 1321 | else if ((b & 0xC0) == 0x80) 1322 | { 1323 | // Misplaced 10bbbbbb continuation byte. This cannot be the first byte. 1324 | return false; 1325 | } 1326 | else if ((b & 0xE0) == 0xC0) // 110bbbbb 10bbbbbb 1327 | { 1328 | state.AdditionalBytesExpected = 1; 1329 | state.CurrentDecodeBits = b & 0x1F; 1330 | state.ExpectedValueMin = 0x80; 1331 | } 1332 | else if ((b & 0xF0) == 0xE0) // 1110bbbb 10bbbbbb 10bbbbbb 1333 | { 1334 | state.AdditionalBytesExpected = 2; 1335 | state.CurrentDecodeBits = b & 0xF; 1336 | state.ExpectedValueMin = 0x800; 1337 | } 1338 | else if ((b & 0xF8) == 0xF0) // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb 1339 | { 1340 | state.AdditionalBytesExpected = 3; 1341 | state.CurrentDecodeBits = b & 0x7; 1342 | state.ExpectedValueMin = 0x10000; 1343 | } 1344 | else // 111110bb & 1111110b & 11111110 && 11111111 are not valid 1345 | { 1346 | return false; 1347 | } 1348 | } 1349 | while (state.AdditionalBytesExpected > 0 && i < arraySegment.Offset + arraySegment.Count) 1350 | { 1351 | byte b = arraySegment.Array[i]; 1352 | if ((b & 0xC0) != 0x80) 1353 | { 1354 | return false; 1355 | } 1356 | 1357 | i++; 1358 | state.AdditionalBytesExpected--; 1359 | 1360 | // Each continuation byte carries 6 bits of data 0x10bbbbbb. 1361 | state.CurrentDecodeBits = (state.CurrentDecodeBits << 6) | (b & 0x3F); 1362 | 1363 | if (state.AdditionalBytesExpected == 1 && state.CurrentDecodeBits >= 0x360 && state.CurrentDecodeBits <= 0x37F) 1364 | { 1365 | // This is going to end up in the range of 0xD800-0xDFFF UTF-16 surrogates that are not allowed in UTF-8; 1366 | return false; 1367 | } 1368 | if (state.AdditionalBytesExpected == 2 && state.CurrentDecodeBits >= 0x110) 1369 | { 1370 | // This is going to be out of the upper Unicode bound 0x10FFFF. 1371 | return false; 1372 | } 1373 | } 1374 | if (state.AdditionalBytesExpected == 0) 1375 | { 1376 | state.SequenceInProgress = false; 1377 | if (state.CurrentDecodeBits < state.ExpectedValueMin) 1378 | { 1379 | // Overlong encoding (e.g. using 2 bytes to encode something that only needed 1). 1380 | return false; 1381 | } 1382 | } 1383 | } 1384 | if (endOfMessage && state.SequenceInProgress) 1385 | { 1386 | return false; 1387 | } 1388 | return true; 1389 | } 1390 | 1391 | private sealed class Utf8MessageState 1392 | { 1393 | internal bool SequenceInProgress; 1394 | internal int AdditionalBytesExpected; 1395 | internal int ExpectedValueMin; 1396 | internal int CurrentDecodeBits; 1397 | } 1398 | 1399 | private enum MessageOpcode : byte 1400 | { 1401 | Continuation = 0x0, 1402 | Text = 0x1, 1403 | Binary = 0x2, 1404 | Close = 0x8, 1405 | Ping = 0x9, 1406 | Pong = 0xA 1407 | } 1408 | 1409 | [StructLayout(LayoutKind.Auto)] 1410 | private struct MessageHeader 1411 | { 1412 | internal MessageOpcode Opcode; 1413 | internal bool Fin; 1414 | internal long PayloadLength; 1415 | internal int Mask; 1416 | } 1417 | } 1418 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/NET45Shims.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using System.Net.Sockets; 7 | using System.Net.WebSockets.Managed; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | 12 | namespace System 13 | { 14 | static class SocketExtensions 15 | { 16 | public static Task ConnectAsync(this Socket socket, IPAddress address, int port) 17 | { 18 | return Task.Factory.FromAsync( 19 | (targetAddress, targetPort, callback, state) => ((Socket)state).BeginConnect(targetAddress, targetPort, callback, state), 20 | asyncResult => ((Socket)asyncResult.AsyncState).EndConnect(asyncResult), 21 | address, 22 | port, 23 | state: socket); 24 | } 25 | } 26 | 27 | static class WebSocketUtil 28 | { 29 | public static ManagedWebSocket CreateClientWebSocket(Stream innerStream, 30 | string subProtocol, int receiveBufferSize, int sendBufferSize, 31 | TimeSpan keepAliveInterval, bool useZeroMaskingKey, ArraySegment internalBuffer) 32 | { 33 | if (innerStream == null) 34 | { 35 | throw new ArgumentNullException(nameof(innerStream)); 36 | } 37 | 38 | if (!innerStream.CanRead || !innerStream.CanWrite) 39 | { 40 | throw new ArgumentException(!innerStream.CanRead ? SR.NotReadableStream : SR.NotWriteableStream, nameof(innerStream)); 41 | } 42 | 43 | if (subProtocol != null) 44 | { 45 | WebSocketValidate.ValidateSubprotocol(subProtocol); 46 | } 47 | 48 | if (keepAliveInterval != Timeout.InfiniteTimeSpan && keepAliveInterval < TimeSpan.Zero) 49 | { 50 | throw new ArgumentOutOfRangeException(nameof(keepAliveInterval), keepAliveInterval, 51 | SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 52 | 0)); 53 | } 54 | 55 | if (receiveBufferSize <= 0 || sendBufferSize <= 0) 56 | { 57 | throw new ArgumentOutOfRangeException( 58 | receiveBufferSize <= 0 ? nameof(receiveBufferSize) : nameof(sendBufferSize), 59 | receiveBufferSize <= 0 ? receiveBufferSize : sendBufferSize, 60 | SR.Format(SR.net_WebSockets_ArgumentOutOfRange_TooSmall, 0)); 61 | } 62 | 63 | return ManagedWebSocket.CreateFromConnectedStream( 64 | innerStream, false, subProtocol, keepAliveInterval, 65 | receiveBufferSize, internalBuffer); 66 | } 67 | } 68 | 69 | static class UriExtensions 70 | { 71 | public static string GetIdnHost(this Uri uri) 72 | { 73 | return new Globalization.IdnMapping().GetAscii(uri.Host); 74 | } 75 | } 76 | 77 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/NetEventSource.WebSockets.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics.Tracing; 6 | 7 | namespace System.Net 8 | { 9 | [EventSource(Name = "Microsoft-System-Net-WebSockets-Client")] 10 | internal sealed partial class NetEventSource 11 | { 12 | public static bool IsEnabled = false; 13 | public static void Enter(object obj) { } 14 | public static void Exit(object obj) { } 15 | public static void Error(object obj, Exception ex) { } 16 | } 17 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/SR.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets.Client.Managed; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace System 9 | { 10 | public static class SR 11 | { 12 | public static readonly string net_WebSockets_InvalidCloseStatusDescription = Strings.net_WebSockets_InvalidCloseStatusDescription; 13 | public static readonly string net_WebSockets_InvalidState = Strings.net_WebSockets_InvalidState; 14 | public static readonly string net_WebSockets_InvalidEmptySubProtocol = Strings.net_WebSockets_InvalidEmptySubProtocol; 15 | public static readonly string net_WebSockets_InvalidCharInProtocolString = Strings.net_WebSockets_InvalidCharInProtocolString; 16 | public static readonly string net_WebSockets_ReasonNotNull = Strings.net_WebSockets_ReasonNotNull; 17 | public static readonly string net_WebSockets_InvalidCloseStatusCode = Strings.net_WebSockets_InvalidCloseStatusCode; 18 | public static readonly string net_WebSockets_UnsupportedPlatform = Strings.net_WebSockets_UnsupportedPlatform; 19 | public static readonly string net_uri_NotAbsolute = Strings.net_uri_NotAbsolute; 20 | public static readonly string net_WebSockets_Scheme = Strings.net_WebSockets_Scheme; 21 | public static readonly string net_WebSockets_AlreadyStarted = Strings.net_WebSockets_AlreadyStarted; 22 | public static readonly string net_WebSockets_NotConnected = Strings.net_WebSockets_NotConnected; 23 | public static readonly string net_webstatus_ConnectFailure = Strings.net_webstatus_ConnectFailure; 24 | public static readonly string net_WebSockets_AcceptUnsupportedProtocol = Strings.net_WebSockets_AcceptUnsupportedProtocol; 25 | public static readonly string net_securityprotocolnotsupported = Strings.net_securityprotocolnotsupported; 26 | public static readonly string net_Websockets_AlreadyOneOutstandingOperation = Strings.net_Websockets_AlreadyOneOutstandingOperation; 27 | public static readonly string net_WebSockets_ArgumentOutOfRange_TooSmall = Strings.net_WebSockets_ArgumentOutOfRange_TooSmall; 28 | public static readonly string net_WebSockets_Generic = Strings.net_WebSockets_Generic; 29 | public static readonly string net_WebSockets_Argument_InvalidMessageType = Strings.net_WebSockets_Argument_InvalidMessageType; 30 | public static readonly string net_WebSockets_InvalidResponseHeader = Strings.net_WebSockets_InvalidResponseHeader; 31 | public static readonly string net_WebSockets_InvalidState_ClosedOrAborted = Strings.net_WebSockets_InvalidState_ClosedOrAborted; 32 | public static readonly string net_WebSockets_NoDuplicateProtocol = Strings.net_WebSockets_NoDuplicateProtocol; 33 | public static readonly string NotReadableStream = Strings.NotReadableStream; 34 | public static readonly string NotWriteableStream = Strings.NotWriteableStream; 35 | 36 | public static string Format(string str, params object[] p) 37 | { 38 | return string.Format(str, p); 39 | } 40 | 41 | } 42 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/SecurityProtocol.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Security.Authentication; 6 | 7 | namespace System.Net 8 | { 9 | internal static class SecurityProtocol 10 | { 11 | // SSLv2 and SSLv3 are considered insecure and will not be supported by the underlying implementations. 12 | public const SslProtocols AllowedSecurityProtocols = 13 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; 14 | 15 | public const SslProtocols DefaultSecurityProtocols = 16 | SslProtocols.Tls | SslProtocols.Tls11 | SslProtocols.Tls12; 17 | 18 | public const SslProtocols SystemDefaultSecurityProtocols = SslProtocols.None; 19 | 20 | public static void ThrowOnNotAllowed(SslProtocols protocols, bool allowNone = true) 21 | { 22 | if ((!allowNone && (protocols == SslProtocols.None)) || ((protocols & ~AllowedSecurityProtocols) != 0)) 23 | { 24 | throw new NotSupportedException(SR.net_securityprotocolnotsupported); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | 7 | namespace System 8 | { 9 | internal static class StringExtensions 10 | { 11 | internal static string SubstringTrim(this string value, int startIndex) 12 | { 13 | return SubstringTrim(value, startIndex, value.Length - startIndex); 14 | } 15 | 16 | internal static string SubstringTrim(this string value, int startIndex, int length) 17 | { 18 | Debug.Assert(value != null, "string must be non-null"); 19 | Debug.Assert(startIndex >= 0, "startIndex must be non-negative"); 20 | Debug.Assert(length >= 0, "length must be non-negative"); 21 | Debug.Assert(startIndex <= value.Length - length, "startIndex + length must be <= value.Length"); 22 | 23 | if (length == 0) 24 | { 25 | return string.Empty; 26 | } 27 | 28 | int endIndex = startIndex + length - 1; 29 | 30 | while (startIndex <= endIndex && char.IsWhiteSpace(value[startIndex])) 31 | { 32 | startIndex++; 33 | } 34 | 35 | while (endIndex >= startIndex && char.IsWhiteSpace(value[endIndex])) 36 | { 37 | endIndex--; 38 | } 39 | 40 | int newLength = endIndex - startIndex + 1; 41 | Debug.Assert(newLength >= 0 && newLength <= value.Length, "Expected resulting length to be within value's length"); 42 | 43 | return 44 | newLength == 0 ? string.Empty : 45 | newLength == value.Length ? value : 46 | value.Substring(startIndex, newLength); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/Strings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace System.Net.WebSockets.Client.Managed { 12 | using System; 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Strings { 26 | 27 | private static global::System.Resources.ResourceManager resourceMan; 28 | 29 | private static global::System.Globalization.CultureInfo resourceCulture; 30 | 31 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 32 | internal Strings() { 33 | } 34 | 35 | /// 36 | /// Returns the cached ResourceManager instance used by this class. 37 | /// 38 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 39 | internal static global::System.Resources.ResourceManager ResourceManager { 40 | get { 41 | if (object.ReferenceEquals(resourceMan, null)) { 42 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("System.Net.WebSockets.Client.Managed.Strings", typeof(Strings).Assembly); 43 | resourceMan = temp; 44 | } 45 | return resourceMan; 46 | } 47 | } 48 | 49 | /// 50 | /// Overrides the current thread's CurrentUICulture property for all 51 | /// resource lookups using this strongly typed resource class. 52 | /// 53 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 54 | internal static global::System.Globalization.CultureInfo Culture { 55 | get { 56 | return resourceCulture; 57 | } 58 | set { 59 | resourceCulture = value; 60 | } 61 | } 62 | 63 | /// 64 | /// Looks up a localized string similar to The requested security protocol is not supported.. 65 | /// 66 | internal static string net_securityprotocolnotsupported { 67 | get { 68 | return ResourceManager.GetString("net_securityprotocolnotsupported", resourceCulture); 69 | } 70 | } 71 | 72 | /// 73 | /// Looks up a localized string similar to This operation is not supported for a relative URI.. 74 | /// 75 | internal static string net_uri_NotAbsolute { 76 | get { 77 | return ResourceManager.GetString("net_uri_NotAbsolute", resourceCulture); 78 | } 79 | } 80 | 81 | /// 82 | /// Looks up a localized string similar to The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s).. 83 | /// 84 | internal static string net_WebSockets_AcceptUnsupportedProtocol { 85 | get { 86 | return ResourceManager.GetString("net_WebSockets_AcceptUnsupportedProtocol", resourceCulture); 87 | } 88 | } 89 | 90 | /// 91 | /// Looks up a localized string similar to There is already one outstanding '{0}' call for this WebSocket instance. ReceiveAsync and SendAsync can be called simultaneously, but at most one outstanding operation for each of them is allowed at the same time.. 92 | /// 93 | internal static string net_Websockets_AlreadyOneOutstandingOperation { 94 | get { 95 | return ResourceManager.GetString("net_Websockets_AlreadyOneOutstandingOperation", resourceCulture); 96 | } 97 | } 98 | 99 | /// 100 | /// Looks up a localized string similar to The WebSocket has already been started.. 101 | /// 102 | internal static string net_WebSockets_AlreadyStarted { 103 | get { 104 | return ResourceManager.GetString("net_WebSockets_AlreadyStarted", resourceCulture); 105 | } 106 | } 107 | 108 | /// 109 | /// Looks up a localized string similar to The message type '{0}' is not allowed for the '{1}' operation. Valid message types are: '{2}, {3}'. To close the WebSocket, use the '{4}' operation instead. . 110 | /// 111 | internal static string net_WebSockets_Argument_InvalidMessageType { 112 | get { 113 | return ResourceManager.GetString("net_WebSockets_Argument_InvalidMessageType", resourceCulture); 114 | } 115 | } 116 | 117 | /// 118 | /// Looks up a localized string similar to The argument must be a value greater than {0}.. 119 | /// 120 | internal static string net_WebSockets_ArgumentOutOfRange_TooSmall { 121 | get { 122 | return ResourceManager.GetString("net_WebSockets_ArgumentOutOfRange_TooSmall", resourceCulture); 123 | } 124 | } 125 | 126 | /// 127 | /// Looks up a localized string similar to An internal WebSocket error occurred. Please see the innerException, if present, for more details. . 128 | /// 129 | internal static string net_WebSockets_Generic { 130 | get { 131 | return ResourceManager.GetString("net_WebSockets_Generic", resourceCulture); 132 | } 133 | } 134 | 135 | /// 136 | /// Looks up a localized string similar to The WebSocket protocol '{0}' is invalid because it contains the invalid character '{1}'.. 137 | /// 138 | internal static string net_WebSockets_InvalidCharInProtocolString { 139 | get { 140 | return ResourceManager.GetString("net_WebSockets_InvalidCharInProtocolString", resourceCulture); 141 | } 142 | } 143 | 144 | /// 145 | /// Looks up a localized string similar to The close status code '{0}' is reserved for system use only and cannot be specified when calling this method.. 146 | /// 147 | internal static string net_WebSockets_InvalidCloseStatusCode { 148 | get { 149 | return ResourceManager.GetString("net_WebSockets_InvalidCloseStatusCode", resourceCulture); 150 | } 151 | } 152 | 153 | /// 154 | /// Looks up a localized string similar to The close status description '{0}' is too long. The UTF8-representation of the status description must not be longer than {1} bytes.. 155 | /// 156 | internal static string net_WebSockets_InvalidCloseStatusDescription { 157 | get { 158 | return ResourceManager.GetString("net_WebSockets_InvalidCloseStatusDescription", resourceCulture); 159 | } 160 | } 161 | 162 | /// 163 | /// Looks up a localized string similar to Empty string is not a valid subprotocol value. Please use \"null\" to specify no value.. 164 | /// 165 | internal static string net_WebSockets_InvalidEmptySubProtocol { 166 | get { 167 | return ResourceManager.GetString("net_WebSockets_InvalidEmptySubProtocol", resourceCulture); 168 | } 169 | } 170 | 171 | /// 172 | /// Looks up a localized string similar to The '{0}' header value '{1}' is invalid.. 173 | /// 174 | internal static string net_WebSockets_InvalidResponseHeader { 175 | get { 176 | return ResourceManager.GetString("net_WebSockets_InvalidResponseHeader", resourceCulture); 177 | } 178 | } 179 | 180 | /// 181 | /// Looks up a localized string similar to The WebSocket is in an invalid state ('{0}') for this operation. Valid states are: '{1}'. 182 | /// 183 | internal static string net_WebSockets_InvalidState { 184 | get { 185 | return ResourceManager.GetString("net_WebSockets_InvalidState", resourceCulture); 186 | } 187 | } 188 | 189 | /// 190 | /// Looks up a localized string similar to The '{0}' instance cannot be used for communication because it has been transitioned into the '{1}' state.. 191 | /// 192 | internal static string net_WebSockets_InvalidState_ClosedOrAborted { 193 | get { 194 | return ResourceManager.GetString("net_WebSockets_InvalidState_ClosedOrAborted", resourceCulture); 195 | } 196 | } 197 | 198 | /// 199 | /// Looks up a localized string similar to Duplicate protocols are not allowed: '{0}'.. 200 | /// 201 | internal static string net_WebSockets_NoDuplicateProtocol { 202 | get { 203 | return ResourceManager.GetString("net_WebSockets_NoDuplicateProtocol", resourceCulture); 204 | } 205 | } 206 | 207 | /// 208 | /// Looks up a localized string similar to The WebSocket is not connected.. 209 | /// 210 | internal static string net_WebSockets_NotConnected { 211 | get { 212 | return ResourceManager.GetString("net_WebSockets_NotConnected", resourceCulture); 213 | } 214 | } 215 | 216 | /// 217 | /// Looks up a localized string similar to The close status description '{0}' is invalid. When using close status code '{1}' the description must be null.. 218 | /// 219 | internal static string net_WebSockets_ReasonNotNull { 220 | get { 221 | return ResourceManager.GetString("net_WebSockets_ReasonNotNull", resourceCulture); 222 | } 223 | } 224 | 225 | /// 226 | /// Looks up a localized string similar to Only Uris starting with 'ws://' or 'wss://' are supported.. 227 | /// 228 | internal static string net_WebSockets_Scheme { 229 | get { 230 | return ResourceManager.GetString("net_WebSockets_Scheme", resourceCulture); 231 | } 232 | } 233 | 234 | /// 235 | /// Looks up a localized string similar to The WebSocket protocol is not supported on this platform.. 236 | /// 237 | internal static string net_WebSockets_UnsupportedPlatform { 238 | get { 239 | return ResourceManager.GetString("net_WebSockets_UnsupportedPlatform", resourceCulture); 240 | } 241 | } 242 | 243 | /// 244 | /// Looks up a localized string similar to Unable to connect to the remote server. 245 | /// 246 | internal static string net_webstatus_ConnectFailure { 247 | get { 248 | return ResourceManager.GetString("net_webstatus_ConnectFailure", resourceCulture); 249 | } 250 | } 251 | 252 | /// 253 | /// Looks up a localized string similar to The base stream is not readable.. 254 | /// 255 | internal static string NotReadableStream { 256 | get { 257 | return ResourceManager.GetString("NotReadableStream", resourceCulture); 258 | } 259 | } 260 | 261 | /// 262 | /// Looks up a localized string similar to The base stream is not writeable.. 263 | /// 264 | internal static string NotWriteableStream { 265 | get { 266 | return ResourceManager.GetString("NotWriteableStream", resourceCulture); 267 | } 268 | } 269 | } 270 | } 271 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/Strings.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | text/microsoft-resx 51 | 52 | 53 | 2.0 54 | 55 | 56 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 57 | 58 | 59 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 60 | 61 | 62 | Unable to connect to the remote server 63 | 64 | 65 | This operation is not supported for a relative URI. 66 | 67 | 68 | There is already one outstanding '{0}' call for this WebSocket instance. ReceiveAsync and SendAsync can be called simultaneously, but at most one outstanding operation for each of them is allowed at the same time. 69 | 70 | 71 | An internal WebSocket error occurred. Please see the innerException, if present, for more details. 72 | 73 | 74 | The WebSocket protocol is not supported on this platform. 75 | 76 | 77 | The WebSocket client request requested '{0}' protocol(s), but server is only accepting '{1}' protocol(s). 78 | 79 | 80 | The argument must be a value greater than {0}. 81 | 82 | 83 | The '{0}' instance cannot be used for communication because it has been transitioned into the '{1}' state. 84 | 85 | 86 | The WebSocket is in an invalid state ('{0}') for this operation. Valid states are: '{1}' 87 | 88 | 89 | The message type '{0}' is not allowed for the '{1}' operation. Valid message types are: '{2}, {3}'. To close the WebSocket, use the '{4}' operation instead. 90 | 91 | 92 | The WebSocket protocol '{0}' is invalid because it contains the invalid character '{1}'. 93 | 94 | 95 | Empty string is not a valid subprotocol value. Please use \"null\" to specify no value. 96 | 97 | 98 | The close status description '{0}' is invalid. When using close status code '{1}' the description must be null. 99 | 100 | 101 | The close status code '{0}' is reserved for system use only and cannot be specified when calling this method. 102 | 103 | 104 | The close status description '{0}' is too long. The UTF8-representation of the status description must not be longer than {1} bytes. 105 | 106 | 107 | Only Uris starting with 'ws://' or 'wss://' are supported. 108 | 109 | 110 | The WebSocket has already been started. 111 | 112 | 113 | The '{0}' header value '{1}' is invalid. 114 | 115 | 116 | The WebSocket is not connected. 117 | 118 | 119 | Duplicate protocols are not allowed: '{0}'. 120 | 121 | 122 | The requested security protocol is not supported. 123 | 124 | 125 | The base stream is not readable. 126 | 127 | 128 | The base stream is not writeable. 129 | 130 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/System.Net.WebSockets.Client.Managed.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0;net45 5 | true 6 | true 7 | 1.0.1 8 | Microsoft, Matthew Little 9 | Microsoft, Pingman Tools 10 | https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed/blob/master/LICENSE 11 | https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed/ 12 | https://github.com/PingmanTools/System.Net.WebSockets.Client.Managed/ 13 | websockets websocket-client websocketsharp clientwebsocket websocket4net 14 | Microsoft's managed implementation of System.Net.WebSockets.ClientWebSocket tweaked for use on Windows 7 and .NET 4.5 15 | latest 16 | 17 | 18 | 19 | bin\$(Configuration)\$(TargetFramework)\System.Net.WebSockets.Client.Managed.xml 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/SystemClientWebSocket.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace System.Net.WebSockets 9 | { 10 | public static class SystemClientWebSocket 11 | { 12 | /// 13 | /// False if System.Net.WebSockets.ClientWebSocket is available on this platform, true if System.Net.WebSockets.Managed.ClientWebSocket is required. 14 | /// 15 | public static bool ManagedWebSocketRequired => _managedWebSocketRequired.Value; 16 | 17 | static Lazy _managedWebSocketRequired => new Lazy(CheckManagedWebSocketRequired); 18 | 19 | static bool CheckManagedWebSocketRequired() 20 | { 21 | try 22 | { 23 | using (var clientWebSocket = new ClientWebSocket()) 24 | { 25 | return false; 26 | } 27 | } 28 | catch (PlatformNotSupportedException) 29 | { 30 | return true; 31 | } 32 | } 33 | 34 | /// 35 | /// Creates a ClientWebSocket that works for this platform. Uses System.Net.WebSockets.ClientWebSocket if supported or System.Net.WebSockets.Managed.ClientWebSocket if not. 36 | /// 37 | public static WebSocket CreateClientWebSocket() 38 | { 39 | if (ManagedWebSocketRequired) 40 | { 41 | return new Managed.ClientWebSocket(); 42 | } 43 | else 44 | { 45 | return new ClientWebSocket(); 46 | } 47 | } 48 | 49 | /// 50 | /// Creates and connects a ClientWebSocket that works for this platform. Uses System.Net.WebSockets.ClientWebSocket if supported or System.Net.WebSockets.Managed.ClientWebSocket if not. 51 | /// 52 | public static async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) 53 | { 54 | var clientWebSocket = CreateClientWebSocket(); 55 | await clientWebSocket.ConnectAsync(uri, cancellationToken); 56 | return clientWebSocket; 57 | } 58 | 59 | public static Task ConnectAsync(this WebSocket clientWebSocket, Uri uri, CancellationToken cancellationToken) 60 | { 61 | if (clientWebSocket is ClientWebSocket) 62 | { 63 | return (clientWebSocket as ClientWebSocket).ConnectAsync(uri, cancellationToken); 64 | } 65 | else if (clientWebSocket is Managed.ClientWebSocket) 66 | { 67 | return (clientWebSocket as Managed.ClientWebSocket).ConnectAsync(uri, cancellationToken); 68 | } 69 | 70 | throw new ArgumentException("WebSocket must be an instance of System.Net.WebSockets.ClientWebSocket or System.Net.WebSockets.Managed.ClientWebSocket", nameof(clientWebSocket)); 71 | } 72 | 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/UriScheme.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | namespace System.Net 6 | { 7 | internal static class UriScheme 8 | { 9 | public const string File = "file"; 10 | public const string Ftp = "ftp"; 11 | public const string Gopher = "gopher"; 12 | public const string Http = "http"; 13 | public const string Https = "https"; 14 | public const string News = "news"; 15 | public const string NetPipe = "net.pipe"; 16 | public const string NetTcp = "net.tcp"; 17 | public const string Nntp = "nntp"; 18 | public const string Mailto = "mailto"; 19 | public const string Ws = "ws"; 20 | public const string Wss = "wss"; 21 | 22 | public const string SchemeDelimiter = "://"; 23 | } 24 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/WebSocketHandle.Managed.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Collections.Generic; 6 | using System.Diagnostics; 7 | using System.IO; 8 | using System.Net.Security; 9 | using System.Net.Sockets; 10 | using System.Runtime.ExceptionServices; 11 | using System.Security.Cryptography; 12 | using System.Text; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | namespace System.Net.WebSockets.Managed 17 | { 18 | internal sealed class WebSocketHandle 19 | { 20 | /// Per-thread cached StringBuilder for building of strings to send on the connection. 21 | [ThreadStatic] 22 | private static StringBuilder t_cachedStringBuilder; 23 | 24 | /// Default encoding for HTTP requests. Latin alphabeta no 1, ISO/IEC 8859-1. 25 | private static readonly Encoding s_defaultHttpEncoding = Encoding.GetEncoding(28591); 26 | 27 | /// Size of the receive buffer to use. 28 | private const int DefaultReceiveBufferSize = 0x1000; 29 | /// GUID appended by the server as part of the security key response. Defined in the RFC. 30 | private const string WSServerGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 31 | 32 | private readonly CancellationTokenSource _abortSource = new CancellationTokenSource(); 33 | private WebSocketState _state = WebSocketState.Connecting; 34 | private ManagedWebSocket _webSocket; 35 | 36 | public static WebSocketHandle Create() => new WebSocketHandle(); 37 | 38 | public static bool IsValid(WebSocketHandle handle) => handle != null; 39 | 40 | public WebSocketCloseStatus? CloseStatus => _webSocket?.CloseStatus; 41 | 42 | public string CloseStatusDescription => _webSocket?.CloseStatusDescription; 43 | 44 | public WebSocketState State => _webSocket?.State ?? _state; 45 | 46 | public string SubProtocol => _webSocket?.SubProtocol; 47 | 48 | public static void CheckPlatformSupport() { /* nop */ } 49 | 50 | public void Dispose() 51 | { 52 | _state = WebSocketState.Closed; 53 | _webSocket?.Dispose(); 54 | } 55 | 56 | public void Abort() 57 | { 58 | _abortSource.Cancel(); 59 | _webSocket?.Abort(); 60 | } 61 | 62 | public Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) => 63 | _webSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken); 64 | 65 | public Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) => 66 | _webSocket.ReceiveAsync(buffer, cancellationToken); 67 | 68 | public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => 69 | _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken); 70 | 71 | public Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) => 72 | _webSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken); 73 | 74 | public async Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options) 75 | { 76 | // TODO #14480 : Not currently implemented, or explicitly ignored: 77 | // - ClientWebSocketOptions.UseDefaultCredentials 78 | // - ClientWebSocketOptions.Credentials 79 | // - ClientWebSocketOptions.Proxy 80 | // - ClientWebSocketOptions._sendBufferSize 81 | 82 | // Establish connection to the server 83 | CancellationTokenRegistration registration = cancellationToken.Register(s => ((WebSocketHandle)s).Abort(), this); 84 | try 85 | { 86 | // Connect to the remote server 87 | Socket connectedSocket = await ConnectSocketAsync(uri.Host, uri.Port, cancellationToken).ConfigureAwait(false); 88 | Stream stream = new NetworkStream(connectedSocket, ownsSocket: true); 89 | 90 | // Upgrade to SSL if needed 91 | if (uri.Scheme == UriScheme.Wss) 92 | { 93 | var sslStream = new SslStream(stream); 94 | await sslStream.AuthenticateAsClientAsync( 95 | uri.Host, 96 | options.ClientCertificates, 97 | SecurityProtocol.AllowedSecurityProtocols, 98 | checkCertificateRevocation: false).ConfigureAwait(false); 99 | stream = sslStream; 100 | } 101 | 102 | // Create the security key and expected response, then build all of the request headers 103 | KeyValuePair secKeyAndSecWebSocketAccept = CreateSecKeyAndSecWebSocketAccept(); 104 | byte[] requestHeader = BuildRequestHeader(uri, options, secKeyAndSecWebSocketAccept.Key); 105 | 106 | // Write out the header to the connection 107 | await stream.WriteAsync(requestHeader, 0, requestHeader.Length, cancellationToken).ConfigureAwait(false); 108 | 109 | // Parse the response and store our state for the remainder of the connection 110 | string subprotocol = await ParseAndValidateConnectResponseAsync(stream, options, secKeyAndSecWebSocketAccept.Value, cancellationToken).ConfigureAwait(false); 111 | 112 | _webSocket = ManagedWebSocket.CreateFromConnectedStream( 113 | stream, false, subprotocol, options.KeepAliveInterval, options.ReceiveBufferSize, options.Buffer); 114 | 115 | // If a concurrent Abort or Dispose came in before we set _webSocket, make sure to update it appropriately 116 | if (_state == WebSocketState.Aborted) 117 | { 118 | _webSocket.Abort(); 119 | } 120 | else if (_state == WebSocketState.Closed) 121 | { 122 | _webSocket.Dispose(); 123 | } 124 | } 125 | catch (Exception exc) 126 | { 127 | if (_state < WebSocketState.Closed) 128 | { 129 | _state = WebSocketState.Closed; 130 | } 131 | 132 | Abort(); 133 | 134 | if (exc is WebSocketException) 135 | { 136 | throw; 137 | } 138 | throw new WebSocketException(SR.net_webstatus_ConnectFailure, exc); 139 | } 140 | finally 141 | { 142 | registration.Dispose(); 143 | } 144 | } 145 | 146 | /// Connects a socket to the specified host and port, subject to cancellation and aborting. 147 | /// The host to which to connect. 148 | /// The port to which to connect on the host. 149 | /// The CancellationToken to use to cancel the websocket. 150 | /// The connected Socket. 151 | private async Task ConnectSocketAsync(string host, int port, CancellationToken cancellationToken) 152 | { 153 | IPAddress[] addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false); 154 | 155 | ExceptionDispatchInfo lastException = null; 156 | foreach (IPAddress address in addresses) 157 | { 158 | var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp); 159 | try 160 | { 161 | using (cancellationToken.Register(s => ((Socket)s).Dispose(), socket)) 162 | using (_abortSource.Token.Register(s => ((Socket)s).Dispose(), socket)) 163 | { 164 | try 165 | { 166 | await socket.ConnectAsync(address, port).ConfigureAwait(false); 167 | } 168 | catch (ObjectDisposedException ode) 169 | { 170 | // If the socket was disposed because cancellation was requested, translate the exception 171 | // into a new OperationCanceledException. Otherwise, let the original ObjectDisposedexception propagate. 172 | CancellationToken token = cancellationToken.IsCancellationRequested ? cancellationToken : _abortSource.Token; 173 | if (token.IsCancellationRequested) 174 | { 175 | throw new OperationCanceledException(new OperationCanceledException().Message, ode, token); 176 | } 177 | } 178 | } 179 | cancellationToken.ThrowIfCancellationRequested(); // in case of a race and socket was disposed after the await 180 | _abortSource.Token.ThrowIfCancellationRequested(); 181 | return socket; 182 | } 183 | catch (Exception exc) 184 | { 185 | socket.Dispose(); 186 | lastException = ExceptionDispatchInfo.Capture(exc); 187 | } 188 | } 189 | 190 | lastException?.Throw(); 191 | 192 | Debug.Fail("We should never get here. We should have already returned or an exception should have been thrown."); 193 | throw new WebSocketException(SR.net_webstatus_ConnectFailure); 194 | } 195 | 196 | /// Creates a byte[] containing the headers to send to the server. 197 | /// The Uri of the server. 198 | /// The options used to configure the websocket. 199 | /// The generated security key to send in the Sec-WebSocket-Key header. 200 | /// The byte[] containing the encoded headers ready to send to the network. 201 | private static byte[] BuildRequestHeader(Uri uri, ClientWebSocketOptions options, string secKey) 202 | { 203 | StringBuilder builder = t_cachedStringBuilder ?? (t_cachedStringBuilder = new StringBuilder()); 204 | Debug.Assert(builder.Length == 0, $"Expected builder to be empty, got one of length {builder.Length}"); 205 | try 206 | { 207 | builder.Append("GET ").Append(uri.PathAndQuery).Append(" HTTP/1.1\r\n"); 208 | 209 | // Add all of the required headers, honoring Host header if set. 210 | string hostHeader = options.RequestHeaders[HttpKnownHeaderNames.Host]; 211 | builder.Append("Host: "); 212 | if (string.IsNullOrEmpty(hostHeader)) 213 | { 214 | builder.Append(uri.DnsSafeHost).Append(':').Append(uri.Port).Append("\r\n"); 215 | } 216 | else 217 | { 218 | builder.Append(hostHeader).Append("\r\n"); 219 | } 220 | 221 | builder.Append("Connection: Upgrade\r\n"); 222 | builder.Append("Upgrade: websocket\r\n"); 223 | builder.Append("Sec-WebSocket-Version: 13\r\n"); 224 | builder.Append("Sec-WebSocket-Key: ").Append(secKey).Append("\r\n"); 225 | 226 | // Add all of the additionally requested headers 227 | foreach (string key in options.RequestHeaders.AllKeys) 228 | { 229 | if (string.Equals(key, HttpKnownHeaderNames.Host, StringComparison.OrdinalIgnoreCase)) 230 | { 231 | // Host header handled above 232 | continue; 233 | } 234 | 235 | builder.Append(key).Append(": ").Append(options.RequestHeaders[key]).Append("\r\n"); 236 | } 237 | 238 | // Add the optional subprotocols header 239 | if (options.RequestedSubProtocols.Count > 0) 240 | { 241 | builder.Append(HttpKnownHeaderNames.SecWebSocketProtocol).Append(": "); 242 | builder.Append(options.RequestedSubProtocols[0]); 243 | for (int i = 1; i < options.RequestedSubProtocols.Count; i++) 244 | { 245 | builder.Append(", ").Append(options.RequestedSubProtocols[i]); 246 | } 247 | builder.Append("\r\n"); 248 | } 249 | 250 | // Add an optional cookies header 251 | if (options.Cookies != null) 252 | { 253 | string header = options.Cookies.GetCookieHeader(uri); 254 | if (!string.IsNullOrWhiteSpace(header)) 255 | { 256 | builder.Append(HttpKnownHeaderNames.Cookie).Append(": ").Append(header).Append("\r\n"); 257 | } 258 | } 259 | 260 | // End the headers 261 | builder.Append("\r\n"); 262 | 263 | // Return the bytes for the built up header 264 | return s_defaultHttpEncoding.GetBytes(builder.ToString()); 265 | } 266 | finally 267 | { 268 | // Make sure we clear the builder 269 | builder.Clear(); 270 | } 271 | } 272 | 273 | /// 274 | /// Creates a pair of a security key for sending in the Sec-WebSocket-Key header and 275 | /// the associated response we expect to receive as the Sec-WebSocket-Accept header value. 276 | /// 277 | /// A key-value pair of the request header security key and expected response header value. 278 | [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "Required by RFC6455")] 279 | private static KeyValuePair CreateSecKeyAndSecWebSocketAccept() 280 | { 281 | string secKey = Convert.ToBase64String(Guid.NewGuid().ToByteArray()); 282 | using (SHA1 sha = SHA1.Create()) 283 | { 284 | return new KeyValuePair( 285 | secKey, 286 | Convert.ToBase64String(sha.ComputeHash(Encoding.ASCII.GetBytes(secKey + WSServerGuid)))); 287 | } 288 | } 289 | 290 | /// Read and validate the connect response headers from the server. 291 | /// The stream from which to read the response headers. 292 | /// The options used to configure the websocket. 293 | /// The expected value of the Sec-WebSocket-Accept header. 294 | /// The CancellationToken to use to cancel the websocket. 295 | /// The agreed upon subprotocol with the server, or null if there was none. 296 | private async Task ParseAndValidateConnectResponseAsync( 297 | Stream stream, ClientWebSocketOptions options, string expectedSecWebSocketAccept, CancellationToken cancellationToken) 298 | { 299 | // Read the first line of the response 300 | string statusLine = await ReadResponseHeaderLineAsync(stream, cancellationToken).ConfigureAwait(false); 301 | 302 | // Depending on the underlying sockets implementation and timing, connecting to a server that then 303 | // immediately closes the connection may either result in an exception getting thrown from the connect 304 | // earlier, or it may result in getting to here but reading 0 bytes. If we read 0 bytes and thus have 305 | // an empty status line, treat it as a connect failure. 306 | if (string.IsNullOrEmpty(statusLine)) 307 | { 308 | throw new WebSocketException(SR.Format(SR.net_webstatus_ConnectFailure)); 309 | } 310 | 311 | const string ExpectedStatusStart = "HTTP/1.1 "; 312 | const string ExpectedStatusStatWithCode = "HTTP/1.1 101"; // 101 == SwitchingProtocols 313 | 314 | // If the status line doesn't begin with "HTTP/1.1" or isn't long enough to contain a status code, fail. 315 | if (!statusLine.StartsWith(ExpectedStatusStart, StringComparison.Ordinal) || statusLine.Length < ExpectedStatusStatWithCode.Length) 316 | { 317 | throw new WebSocketException(WebSocketError.HeaderError); 318 | } 319 | 320 | // If the status line doesn't contain a status code 101, or if it's long enough to have a status description 321 | // but doesn't contain whitespace after the 101, fail. 322 | if (!statusLine.StartsWith(ExpectedStatusStatWithCode, StringComparison.Ordinal) || 323 | (statusLine.Length > ExpectedStatusStatWithCode.Length && !char.IsWhiteSpace(statusLine[ExpectedStatusStatWithCode.Length]))) 324 | { 325 | throw new WebSocketException(SR.net_webstatus_ConnectFailure); 326 | } 327 | 328 | // Read each response header. Be liberal in parsing the response header, treating 329 | // everything to the left of the colon as the key and everything to the right as the value, trimming both. 330 | // For each header, validate that we got the expected value. 331 | bool foundUpgrade = false, foundConnection = false, foundSecWebSocketAccept = false; 332 | string subprotocol = null; 333 | string line; 334 | while (!string.IsNullOrEmpty(line = await ReadResponseHeaderLineAsync(stream, cancellationToken).ConfigureAwait(false))) 335 | { 336 | int colonIndex = line.IndexOf(':'); 337 | if (colonIndex == -1) 338 | { 339 | throw new WebSocketException(WebSocketError.HeaderError); 340 | } 341 | 342 | string headerName = line.SubstringTrim(0, colonIndex); 343 | string headerValue = line.SubstringTrim(colonIndex + 1); 344 | 345 | // The Connection, Upgrade, and SecWebSocketAccept headers are required and with specific values. 346 | ValidateAndTrackHeader(HttpKnownHeaderNames.Connection, "Upgrade", headerName, headerValue, ref foundConnection); 347 | ValidateAndTrackHeader(HttpKnownHeaderNames.Upgrade, "websocket", headerName, headerValue, ref foundUpgrade); 348 | ValidateAndTrackHeader(HttpKnownHeaderNames.SecWebSocketAccept, expectedSecWebSocketAccept, headerName, headerValue, ref foundSecWebSocketAccept); 349 | 350 | // The SecWebSocketProtocol header is optional. We should only get it with a non-empty value if we requested subprotocols, 351 | // and then it must only be one of the ones we requested. If we got a subprotocol other than one we requested (or if we 352 | // already got one in a previous header), fail. Otherwise, track which one we got. 353 | if (string.Equals(HttpKnownHeaderNames.SecWebSocketProtocol, headerName, StringComparison.OrdinalIgnoreCase) && 354 | !string.IsNullOrWhiteSpace(headerValue)) 355 | { 356 | string newSubprotocol = options.RequestedSubProtocols.Find(requested => string.Equals(requested, headerValue, StringComparison.OrdinalIgnoreCase)); 357 | if (newSubprotocol == null || subprotocol != null) 358 | { 359 | throw new WebSocketException( 360 | WebSocketError.UnsupportedProtocol, 361 | SR.Format(SR.net_WebSockets_AcceptUnsupportedProtocol, string.Join(", ", options.RequestedSubProtocols), subprotocol)); 362 | } 363 | subprotocol = newSubprotocol; 364 | } 365 | } 366 | if (!foundUpgrade || !foundConnection || !foundSecWebSocketAccept) 367 | { 368 | throw new WebSocketException(SR.net_webstatus_ConnectFailure); 369 | } 370 | 371 | return subprotocol; 372 | } 373 | 374 | /// Validates a received header against expected values and tracks that we've received it. 375 | /// The header name against which we're comparing. 376 | /// The header value against which we're comparing. 377 | /// The actual header name received. 378 | /// The actual header value received. 379 | /// A bool tracking whether this header has been seen. 380 | private static void ValidateAndTrackHeader( 381 | string targetHeaderName, string targetHeaderValue, 382 | string foundHeaderName, string foundHeaderValue, 383 | ref bool foundHeader) 384 | { 385 | bool isTargetHeader = string.Equals(targetHeaderName, foundHeaderName, StringComparison.OrdinalIgnoreCase); 386 | if (!foundHeader) 387 | { 388 | if (isTargetHeader) 389 | { 390 | if (!string.Equals(targetHeaderValue, foundHeaderValue, StringComparison.OrdinalIgnoreCase)) 391 | { 392 | throw new WebSocketException(SR.Format(SR.net_WebSockets_InvalidResponseHeader, targetHeaderName, foundHeaderValue)); 393 | } 394 | foundHeader = true; 395 | } 396 | } 397 | else 398 | { 399 | if (isTargetHeader) 400 | { 401 | throw new WebSocketException(SR.Format(SR.net_webstatus_ConnectFailure)); 402 | } 403 | } 404 | } 405 | 406 | /// Reads a line from the stream. 407 | /// The stream from which to read. 408 | /// The CancellationToken used to cancel the websocket. 409 | /// The read line, or null if none could be read. 410 | private static async Task ReadResponseHeaderLineAsync(Stream stream, CancellationToken cancellationToken) 411 | { 412 | StringBuilder sb = t_cachedStringBuilder; 413 | if (sb != null) 414 | { 415 | t_cachedStringBuilder = null; 416 | Debug.Assert(sb.Length == 0, $"Expected empty StringBuilder"); 417 | } 418 | else 419 | { 420 | sb = new StringBuilder(); 421 | } 422 | 423 | var arr = new byte[1]; 424 | char prevChar = '\0'; 425 | try 426 | { 427 | // TODO: Reading one byte is extremely inefficient. The problem, however, 428 | // is that if we read multiple bytes, we could end up reading bytes post-headers 429 | // that are part of messages meant to be read by the managed websocket after 430 | // the connection. The likely solution here is to wrap the stream in a BufferedStream, 431 | // though a) that comes at the expense of an extra set of virtual calls, b) 432 | // it adds a buffer when the managed websocket will already be using a buffer, and 433 | // c) it's not exposed on the version of the System.IO contract we're currently using. 434 | while (await stream.ReadAsync(arr, 0, 1, cancellationToken).ConfigureAwait(false) == 1) 435 | { 436 | // Process the next char 437 | char curChar = (char)arr[0]; 438 | if (prevChar == '\r' && curChar == '\n') 439 | { 440 | break; 441 | } 442 | sb.Append(curChar); 443 | prevChar = curChar; 444 | } 445 | 446 | if (sb.Length > 0 && sb[sb.Length - 1] == '\r') 447 | { 448 | sb.Length = sb.Length - 1; 449 | } 450 | 451 | return sb.ToString(); 452 | } 453 | finally 454 | { 455 | sb.Clear(); 456 | t_cachedStringBuilder = sb; 457 | } 458 | } 459 | } 460 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/WebSocketValidate.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | // See the LICENSE file in the project root for more information. 4 | 5 | using System.Diagnostics; 6 | using System.Globalization; 7 | using System.Text; 8 | 9 | namespace System.Net.WebSockets.Managed 10 | { 11 | internal static partial class WebSocketValidate 12 | { 13 | internal const int MaxControlFramePayloadLength = 123; 14 | private const int CloseStatusCodeAbort = 1006; 15 | private const int CloseStatusCodeFailedTLSHandshake = 1015; 16 | private const int InvalidCloseStatusCodesFrom = 0; 17 | private const int InvalidCloseStatusCodesTo = 999; 18 | private const string Separators = "()<>@,;:\\\"/[]?={} "; 19 | 20 | internal static void ThrowIfInvalidState(WebSocketState currentState, bool isDisposed, WebSocketState[] validStates) 21 | { 22 | string validStatesText = string.Empty; 23 | 24 | if (validStates != null && validStates.Length > 0) 25 | { 26 | foreach (WebSocketState validState in validStates) 27 | { 28 | if (currentState == validState) 29 | { 30 | // Ordering is important to maintain .NET 4.5 WebSocket implementation exception behavior. 31 | if (isDisposed) 32 | { 33 | throw new ObjectDisposedException(nameof(ClientWebSocket)); 34 | } 35 | 36 | return; 37 | } 38 | } 39 | 40 | validStatesText = string.Join(", ", validStates); 41 | } 42 | 43 | throw new WebSocketException( 44 | WebSocketError.InvalidState, 45 | SR.Format(SR.net_WebSockets_InvalidState, currentState, validStatesText)); 46 | } 47 | 48 | internal static void ValidateSubprotocol(string subProtocol) 49 | { 50 | if (string.IsNullOrWhiteSpace(subProtocol)) 51 | { 52 | throw new ArgumentException(SR.net_WebSockets_InvalidEmptySubProtocol, nameof(subProtocol)); 53 | } 54 | 55 | string invalidChar = null; 56 | int i = 0; 57 | while (i < subProtocol.Length) 58 | { 59 | char ch = subProtocol[i]; 60 | if (ch < 0x21 || ch > 0x7e) 61 | { 62 | invalidChar = string.Format(CultureInfo.InvariantCulture, "[{0}]", (int)ch); 63 | break; 64 | } 65 | 66 | if (!char.IsLetterOrDigit(ch) && 67 | Separators.IndexOf(ch) >= 0) 68 | { 69 | invalidChar = ch.ToString(); 70 | break; 71 | } 72 | 73 | i++; 74 | } 75 | 76 | if (invalidChar != null) 77 | { 78 | throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCharInProtocolString, subProtocol, invalidChar), nameof(subProtocol)); 79 | } 80 | } 81 | 82 | internal static void ValidateCloseStatus(WebSocketCloseStatus closeStatus, string statusDescription) 83 | { 84 | if (closeStatus == WebSocketCloseStatus.Empty && !string.IsNullOrEmpty(statusDescription)) 85 | { 86 | throw new ArgumentException(SR.Format(SR.net_WebSockets_ReasonNotNull, 87 | statusDescription, 88 | WebSocketCloseStatus.Empty), 89 | nameof(statusDescription)); 90 | } 91 | 92 | int closeStatusCode = (int)closeStatus; 93 | 94 | if ((closeStatusCode >= InvalidCloseStatusCodesFrom && 95 | closeStatusCode <= InvalidCloseStatusCodesTo) || 96 | closeStatusCode == CloseStatusCodeAbort || 97 | closeStatusCode == CloseStatusCodeFailedTLSHandshake) 98 | { 99 | // CloseStatus 1006 means Aborted - this will never appear on the wire and is reflected by calling WebSocket.Abort 100 | throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusCode, 101 | closeStatusCode), 102 | nameof(closeStatus)); 103 | } 104 | 105 | int length = 0; 106 | if (!string.IsNullOrEmpty(statusDescription)) 107 | { 108 | length = Encoding.UTF8.GetByteCount(statusDescription); 109 | } 110 | 111 | if (length > MaxControlFramePayloadLength) 112 | { 113 | throw new ArgumentException(SR.Format(SR.net_WebSockets_InvalidCloseStatusDescription, 114 | statusDescription, 115 | MaxControlFramePayloadLength), 116 | nameof(statusDescription)); 117 | } 118 | } 119 | 120 | internal static void ThrowPlatformNotSupportedException() 121 | { 122 | throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform); 123 | } 124 | 125 | internal static void ValidateArraySegment(ArraySegment arraySegment, string parameterName) 126 | { 127 | Debug.Assert(!string.IsNullOrEmpty(parameterName), "'parameterName' MUST NOT be NULL or string.Empty"); 128 | 129 | if (arraySegment.Array == null) 130 | { 131 | throw new ArgumentNullException(parameterName + "." + nameof(arraySegment.Array)); 132 | } 133 | if (arraySegment.Offset < 0 || arraySegment.Offset > arraySegment.Array.Length) 134 | { 135 | throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Offset)); 136 | } 137 | if (arraySegment.Count < 0 || arraySegment.Count > (arraySegment.Array.Length - arraySegment.Offset)) 138 | { 139 | throw new ArgumentOutOfRangeException(parameterName + "." + nameof(arraySegment.Count)); 140 | } 141 | } 142 | 143 | internal static void ValidateBuffer(byte[] buffer, int offset, int count) 144 | { 145 | if (buffer == null) 146 | { 147 | throw new ArgumentNullException(nameof(buffer)); 148 | } 149 | 150 | if (offset < 0 || offset > buffer.Length) 151 | { 152 | throw new ArgumentOutOfRangeException(nameof(offset)); 153 | } 154 | 155 | if (count < 0 || count > (buffer.Length - offset)) 156 | { 157 | throw new ArgumentOutOfRangeException(nameof(count)); 158 | } 159 | } 160 | } 161 | } -------------------------------------------------------------------------------- /System.Net.WebSockets.Client.Managed/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.WebSockets; 5 | using System.Net.WebSockets.Managed; 6 | using System.Text; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace TestApp 11 | { 12 | class Program 13 | { 14 | const string WS_TEST_SERVER = "ws://echo.websocket.org"; 15 | const string WSS_TEST_SERVER = "wss://echo.websocket.org"; 16 | 17 | static void Main(string[] args) 18 | { 19 | TestConnection(WS_TEST_SERVER).GetAwaiter().GetResult(); 20 | TestConnection(WSS_TEST_SERVER).GetAwaiter().GetResult(); 21 | } 22 | 23 | static async Task TestConnection(string server) 24 | { 25 | using (var ws = new System.Net.WebSockets.Managed.ClientWebSocket()) 26 | { 27 | await ws.ConnectAsync(new Uri(server), CancellationToken.None); 28 | 29 | var buffer = new ArraySegment(new byte[1024]); 30 | var readTask = ws.ReceiveAsync(buffer, CancellationToken.None); 31 | 32 | const string msg = "hello"; 33 | var testMsg = new ArraySegment(Encoding.UTF8.GetBytes(msg)); 34 | await ws.SendAsync(testMsg, WebSocketMessageType.Text, true, CancellationToken.None); 35 | 36 | var read = await readTask; 37 | var reply = Encoding.UTF8.GetString(buffer.Array, 0, read.Count); 38 | 39 | if (reply != msg) 40 | { 41 | throw new Exception($"Expected to read back '{msg}' but got '{reply}' for server {server}"); 42 | } 43 | Console.WriteLine("Success connecting to server " + server); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /TestApp/TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net4.5 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | --------------------------------------------------------------------------------