//------------------------------------------------------------------------------ // // Copyright (c) Microsoft Corporation. All rights reserved. // //------------------------------------------------------------------------------ namespace System.Web.WebSockets { using System; using System.Diagnostics.CodeAnalysis; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Web.Util; // Used to send and receive messages over a WebSocket connection // // SYNCHRONIZATION AND THREAD SAFETY: // // This class is not generally thread-safe, but the following exceptions are made: // // - We'll detect multiple calls to SendAsync (e.g. a second call takes place while the first // call isn't yet complete) and throw an appropriate exception. CloseOutputAsync is // an equivalent of SendAsync for this purpose. // // - There can be a pending call to SendAsync (or CloseOutputAsync) at the same time as a // call to ReceiveAsync, and we'll handle synchronization properly. // // - CloseAsync is the equivalent of starting parallel sends and receives. If there's already // a send (SendAsync / CloseOutputAsync) or receive (ReceiveAsync) in progress, we'll // detect this and throw an appropriate exception. // // - Any thread can call Abort at any time. Any pending sends / receives will result in // failure. Continuations hanging off the failed tasks will execute, but they may do // so before or after the call to Abort returns. // // - Dispose is *not* thread-safe and should not be called while the socket has a // pending operation. // // TAP asks that if exceptions are thrown due to precondition violations (e.g. bad parameters // or the object is in an invalid state), these be thrown synchronously from the Async method // rather than shoved into the resulting Task. Hence in the design below, the AsyncImpl methods // themselves are synchronous and perform any necessary initialization, then they return // delegates that can be used to kick off the asynchronous operations. This allows the // AsyncImpl callers to provide coarser locking around multiple methods if necessary. In all // other cases, the locks are held for the absolute minimum time necessary to guarantee thread- // safety. Importantly, locks must never be held while awaiting an asynchronous method. // // VISIBILITY: // // Aside from the ctor, internal members of this type are not meant to be called from outside // the type itself. Any internal members are only internal to ease unit testing. We have the // goal of allowing third-party developers to create the same higher-level abstractions over // this type that we can, and having ASP.NET call internal members runs counter to that goal. // // TERMINOLOGY: // // The WebSocket protocol is defined at: // http://tools.ietf.org/html/rfc6455 // // "WSPC" is the WebSocket Protocol Component (websocket.dll), which provides WebSocket- // parsing services to WCF, IIS, IE, and others. ASP.NET doesn't call into WSPC directly; // rather, IIS calls into WSPC on our behalf. public sealed class AspNetWebSocket : WebSocket, IAsyncAbortableWebSocket { // RFC 6455, Sec. 5.5: // All control frames MUST be 125 bytes or less in length and MUST NOT be fragmented. // // When a CLOSE frame is sent, it has an optional payload that consists of a 2-byte status code followed by a // UTF8-encoded string. This string must therefore be no greater than 123 bytes (after UTF8-encoding) in length. private const int _maxCloseMessageByteCount = 123; // represents no-op tasks or asynchronous operations private static readonly Task _completedTask = Task.FromResult(null); private static readonly Func _completedTaskFunc = () => _completedTask; // machine-readable / human-readable reasons this WebSocket was closed private const WebSocketCloseStatus CLOSE_STATUS_NOT_SET = (WebSocketCloseStatus)(-1); // used as a 'null' placeholder for _closeStatus since Nullable access not guaranteed atomic private WebSocketCloseStatus _closeStatus = CLOSE_STATUS_NOT_SET; private string _closeStatusDescription; // for determining whether the socket has been disposed of private bool _disposed; // the pipe used for low-level communication with the remote endpoint private readonly IWebSocketPipe _pipe; private readonly string _subProtocol; // the current state of the RECEIVE (remote to local) and SEND (local to remote) channels private int _abortAsyncCalled; private CountdownTask _pendingOperationCounter = new CountdownTask(1); internal ChannelState _receiveState; internal ChannelState _sendState; // the current state (observable externally) of this socket internal WebSocketState _state = WebSocketState.Open; // for synchronization around controlling state transitions private readonly object _stateLockObj = new object(); internal AspNetWebSocket(IWebSocketPipe pipe, string subProtocol) { _pipe = pipe; _subProtocol = subProtocol; // It is possible that a pipe couldn't be established, e.g. if the client disconnects or there is // a server error before the handshake is complete. If this is the case, we just immediately // mark this AspNetWebSocket instance as aborted. if (_pipe == null) { Abort(); } } public override WebSocketCloseStatus? CloseStatus { get { ThrowIfDisposed(); WebSocketCloseStatus closeStatus = _closeStatus; if (closeStatus == CLOSE_STATUS_NOT_SET) { // no close status (not even Unspecified) to report to the caller; most likely reason is connection hasn't been closed return null; } else { return closeStatus; } } } public override string CloseStatusDescription { get { ThrowIfDisposed(); return _closeStatusDescription; } } public override WebSocketState State { get { ThrowIfDisposed(); return _state; } } public override string SubProtocol { get { ThrowIfDisposed(); return _subProtocol; } } // for unit testing internal CountdownTask PendingOperationCounter { get { return _pendingOperationCounter; } } // Rudely closes a connection and cancels all pending I/O. // This is a fire-and-forget method which never blocks. // Both this and AbortAsync() are thread-safe. public override void Abort() { Abort(throwIfDisposed: true); // throws if disposed, per spec } [SuppressMessage("Microsoft.Security", "CA2122:DoNotIndirectlyExposeMethodsWithLinkDemands", Justification = "CloseTcpConnection() is not a dangerous method.")] private void Abort(bool throwIfDisposed, bool isDisposing = false) { lock (_stateLockObj) { // State validation if (throwIfDisposed) { ThrowIfDisposed(); } try { if (IsStateTerminal(_state)) { // If we're already in a terminal state, do nothing. return; } else { // Allowed transitions: // - Open -> Aborted // - CloseSent -> Aborted // - CloseReceived -> Aborted _state = WebSocketState.Aborted; } } finally { // Is this being called from Dispose()? if (isDisposing) { _disposed = true; } } } // -- CORE LOGIC -- // Everything after this point will be executed only once. if (_pipe != null) { _pipe.CloseTcpConnection(); } } // Similar to Abort(), but returns a Task which allows a caller to know when // the Abort operation is complete. When the returned Task is complete, this // means that there will be no more calls to or from the _pipe object. // This method is always safe to call, even if the socket has been disposed. // This method must be called at the end of WebSocket processing in order // to release resources associated with this socket. internal Task AbortAsync() { Abort(throwIfDisposed: false); if (Interlocked.Exchange(ref _abortAsyncCalled, 1) == 0) { // The CountdownTask was seeded with an initial value of 1 so that the // associated Task wouldn't be marked as complete until this call to // MarkOperationCompleted. If there is pending IO, the current value // might still be > 1. _pendingOperationCounter.MarkOperationCompleted(); } return _pendingOperationCounter.Task; } // Asynchronously sends a CLOSE frame and waits for the remote endpoint to send a CLOSE/ACK. public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { return CloseAsyncImpl(closeStatus, statusDescription, cancellationToken)(); } private Func CloseAsyncImpl(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken, bool performValidation = true) { Func sendCloseTaskFunc = null; Func> receiveCloseTaskFunc = null; // -- PARAMETER VALIDATION -- // Performed outside lock since doesn't affect state if (performValidation) { ValidateCloseStatusCodeAndDescription(closeStatus, ref statusDescription); } lock (_stateLockObj) { // -- STATE VALIDATION -- // Performed within lock if (performValidation) { ThrowIfDisposed(); ThrowIfAborted(); ThrowIfSendUnavailable(allowClosed: true); ThrowIfReceiveUnavailable(allowClosed: true); } // -- STATE INITIALIZATION -- // Performed within lock // State transitions are handled automatically by the CloseOutputAsyncImpl / ReceiveAsyncImpl methods // These methods don't need to perform validation since this method has already performed it (if necessary) if (_sendState != ChannelState.Closed) { sendCloseTaskFunc = CloseOutputAsyncImpl(closeStatus, statusDescription, cancellationToken, performValidation: false); } if (_receiveState != ChannelState.Closed) { ArraySegment buffer = new ArraySegment(new byte[_maxCloseMessageByteCount]); receiveCloseTaskFunc = ReceiveAsyncImpl(buffer, cancellationToken, performValidation: false); } // special-case no outstanding work to perform if (sendCloseTaskFunc == null && receiveCloseTaskFunc == null) { return _completedTaskFunc; } } // once initialization is complete, the implementation can run truly asynchronously return async () => { // By kicking off both tasks in parallel before awaiting either one, we have full-duplex communication Task sendCloseTask = (sendCloseTaskFunc != null) ? sendCloseTaskFunc() : null; Task receiveCloseTask = (receiveCloseTaskFunc != null) ? receiveCloseTaskFunc() : null; if (sendCloseTask != null) { await sendCloseTask.ConfigureAwait(continueOnCapturedContext: false); // -- ASYNC POINT -- // Any code after this which requires synchronization must reacquire the lock } if (receiveCloseTask != null) { WebSocketReceiveResult result = await receiveCloseTask.ConfigureAwait(continueOnCapturedContext: false); // -- ASYNC POINT -- // Any code after this which requires synchronization must reacquire the lock // Throw if this wasn't a CLOSE frame if (result.MessageType != WebSocketMessageType.Close) { Abort(throwIfDisposed: false); throw new WebSocketException(WebSocketError.InvalidMessageType); } } }; } // Sends a CLOSE frame asynchronously. public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) { return CloseOutputAsyncImpl(closeStatus, statusDescription, cancellationToken)(); } private Func CloseOutputAsyncImpl(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken, bool performValidation = true) { // -- PARAMETER VALIDATION -- // Performed outside lock since doesn't affect state if (performValidation) { ValidateCloseStatusCodeAndDescription(closeStatus, ref statusDescription); } lock (_stateLockObj) { // -- STATE VALIDATION -- // Performed within lock if (performValidation) { ThrowIfDisposed(); ThrowIfAborted(); ThrowIfSendUnavailable(allowClosed: true); } // -- STATE INITIALIZATION -- // Performed within lock if (_sendState == ChannelState.Closed) { // already closed; nothing to do return _completedTaskFunc; } // Mark channel as in use _sendState = ChannelState.Busy; _pendingOperationCounter.MarkOperationPending(); } // once initialization is complete, the implementation can run truly asynchronously return async () => { // Send the fragment await DoWork(() => _pipe.WriteCloseFragmentAsync(closeStatus, statusDescription), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); // -- ASYNC POINT -- // Any code after this which requires synchronization must reacquire the lock lock (_stateLockObj) { _sendState = ChannelState.Closed; // State transition: // - Open -> CloseSent // - CloseReceived -> Closed switch (_state) { case WebSocketState.Open: _state = WebSocketState.CloseSent; break; case WebSocketState.CloseReceived: _state = WebSocketState.Closed; break; } } }; } // Releases resources associated with this object; similar to calling Abort public override void Dispose() { throw new NotSupportedException(SR.GetString(SR.AspNetWebSocket_DisposeNotSupported)); } internal void DisposeInternal() { Abort(throwIfDisposed: false, isDisposing: true); } // Receives a single fragment from the remote endpoint public override Task ReceiveAsync(ArraySegment buffer, CancellationToken cancellationToken) { return ReceiveAsyncImpl(buffer, cancellationToken)(); } private Func> ReceiveAsyncImpl(ArraySegment buffer, CancellationToken cancellationToken, bool performValidation = true) { lock (_stateLockObj) { // -- STATE VALIDATION -- // Performed within lock if (performValidation) { ThrowIfDisposed(); ThrowIfAborted(); ThrowIfReceiveUnavailable(); } // -- STATE INITIALIZATION -- // Performed within lock // Mark channel as in use _receiveState = ChannelState.Busy; _pendingOperationCounter.MarkOperationPending(); } // once initialization is complete, the implementation can run truly asynchronously return async () => { // Receive a single fragment WebSocketReceiveResult result = await DoWork(() => _pipe.ReadFragmentAsync(buffer), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); // -- ASYNC POINT -- // Any code after this which requires synchronization must reacquire the lock lock (_stateLockObj) { if (result.MessageType == WebSocketMessageType.Close) { // received a CLOSE frame _receiveState = ChannelState.Closed; Debug.Assert(result.CloseStatus.HasValue, "The CloseStatus property should be non-null when a CLOSE frame is received."); _closeStatus = result.CloseStatus.Value; _closeStatusDescription = result.CloseStatusDescription; // State transition: // - Open -> CloseReceived // - CloseSent -> Closed switch (_state) { case WebSocketState.Open: _state = WebSocketState.CloseReceived; break; case WebSocketState.CloseSent: _state = WebSocketState.Closed; break; } } else { _receiveState = ChannelState.Ready; } } return result; }; } // Sends a single fragment to the remote endpoint public override Task SendAsync(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) { return SendAsyncImpl(buffer, messageType, endOfMessage, cancellationToken)(); } private Func SendAsyncImpl(ArraySegment buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken, bool performValidation = true) { // -- PARAMETER VALIDATION -- // Performed outside lock since doesn't affect state if (performValidation) { ValidateSendMessageType(messageType); } lock (_stateLockObj) { // -- STATE VALIDATION -- // Performed within lock if (performValidation) { ThrowIfDisposed(); ThrowIfAborted(); ThrowIfSendUnavailable(); } // -- STATE INITIALIZATION -- // Performed within lock // Mark channel as in use _sendState = ChannelState.Busy; _pendingOperationCounter.MarkOperationPending(); } // once initialization is complete, the implementation can run truly asynchronously return async () => { // Send the fragment await DoWork(() => _pipe.WriteFragmentAsync(buffer, (messageType == WebSocketMessageType.Text), endOfMessage), cancellationToken).ConfigureAwait(continueOnCapturedContext: false); // -- ASYNC POINT -- // Any code after this which requires synchronization must reacquire the lock lock (_stateLockObj) { _sendState = ChannelState.Ready; } }; } #region Validation and helper methods // Validates that the provided 'closeStatus' and 'statusDescription' parameters are sane. // The 'statusDescription' parameter is taken by reference since it might be normalized. internal static void ValidateCloseStatusCodeAndDescription(WebSocketCloseStatus closeStatus, ref string statusDescription) { // We do three checks here: // - RFC 6455, Sec. 5.5.1: Close status is 16-bit unsigned integer, so we need to make sure it is within the range 0x0000 - 0xffff. // - RFC 6455, Sec. 7.4.2: 0 - 999 is an invalid value for the close status. // - RFC 6455, Sec. 7.4.1: 1004, 1006, 1010, 1015 are invalid status codes for the server to send to the client. if (closeStatus < (WebSocketCloseStatus)1000 || closeStatus > (WebSocketCloseStatus)UInt16.MaxValue || closeStatus == (WebSocketCloseStatus)1004 || closeStatus == (WebSocketCloseStatus)1006 || closeStatus == (WebSocketCloseStatus)1010 || closeStatus == (WebSocketCloseStatus)1015) { throw new ArgumentOutOfRangeException("closeStatus"); } if (closeStatus == WebSocketCloseStatus.Empty) { // Fix Bug : 312472, we would like to allow empty strings to be passed to our APIs when status code is 1005. // Since WSPC requires the statusDescription to be null, we convert. if (statusDescription == String.Empty) { statusDescription = null; } // If the status code is 1005 (Empty), the statusDescription string MUST be null. // This behavior is required by WSPC and matches WCF. if (statusDescription != null) { throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_CloseStatusEmptyButCloseDescriptionNonNull), "statusDescription"); } } else if (statusDescription != null) { // Need to make sure the provided status description fits within a single WebSocket control frame. int byteCount = Encoding.UTF8.GetByteCount(statusDescription); if (byteCount > _maxCloseMessageByteCount) { throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_CloseDescriptionTooLong, _maxCloseMessageByteCount), "statusDescription"); } } else { // WSPC requires that null status descriptions be normalized to empty strings. statusDescription = String.Empty; } } private static void ValidateSendMessageType(WebSocketMessageType messageType) { switch (messageType) { case WebSocketMessageType.Text: case WebSocketMessageType.Binary: return; // these are OK default: throw new ArgumentException(SR.GetString(SR.AspNetWebSocket_SendMessageTypeInvalid), "messageType"); } } private void ThrowIfDisposed() { if (_disposed) { throw new ObjectDisposedException(objectName: GetType().FullName); } } private void ThrowIfAborted() { if (_state == WebSocketState.Aborted) { throw new WebSocketException(WebSocketError.InvalidState); } } private void ThrowIfSendUnavailable(bool allowClosed = false) { switch (_sendState) { case ChannelState.Busy: throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_SendInProgress)); case ChannelState.Closed: if (allowClosed) { break; } throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_CloseAlreadySent)); } } private void ThrowIfReceiveUnavailable(bool allowClosed = false) { switch (_receiveState) { case ChannelState.Busy: throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_ReceiveInProgress)); case ChannelState.Closed: if (allowClosed) { break; } throw new InvalidOperationException(SR.GetString(SR.AspNetWebSocket_CloseAlreadyReceived)); } } // Helper method that wraps performing some work and aborting on exceptions private async Task DoWork(Func> taskDelegate, CancellationToken cancellationToken) { // Used for timing out operations IDisposable cancellationTokenRegistration = null; try { try { // If cancellation is requested, honor it immediately. This is the same kind of optimization done throughout // the rest of the framework, e.g. as in FileStream.ReadAsync. Our finally block will be responsible for // throwing the actual exception which results in the Task being canceled. if (cancellationToken.IsCancellationRequested) { return default(T); } Task task = taskDelegate(); if (!task.IsCompleted && cancellationToken.CanBeCanceled) { // If the task didn't complete synchronously, let the CancellationToken abort the operation // The callback may complete inline, so we need to make sure it doesn't throw cancellationTokenRegistration = cancellationToken.Register(() => Abort(throwIfDisposed: false)); } // The 'await' keyword may cause an exception to be observed (rethrown) // We call only thread-safe methods so don't need to spend the time to come back to the SynchronizationContext return await task.ConfigureAwait(continueOnCapturedContext: false); } finally { // called for both normal and exceptional completion _pendingOperationCounter.MarkOperationCompleted(); // failed due to cancellation? if (cancellationTokenRegistration != null) { cancellationTokenRegistration.Dispose(); } cancellationToken.ThrowIfCancellationRequested(); } } catch { // Something went wrong while communicating on the pipe - mark faulted and observe exception. // Benign ---- - Abort might be called both by the CancellationTokenRegistration and by the line below. Abort(throwIfDisposed: false); throw; } } // Helper method that wraps performing some work and aborting on exceptions internal Task DoWork(Func taskDelegate, CancellationToken cancellationToken) { return DoWork(async () => { await taskDelegate().ConfigureAwait(continueOnCapturedContext: false); return null; }, cancellationToken); } #endregion Task IAsyncAbortableWebSocket.AbortAsync() { return AbortAsync(); } // Represents the state of a single channel; send and receive have their own states internal enum ChannelState { Ready, // this channel is available for transmitting new frames Busy, // the channel is already busy transmitting frames Closed // this channel has been closed } } }