// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; using System.Collections.Generic; using System.Net.WebSockets; using System.Text; using System.Threading; using System.Threading.Tasks; namespace Microsoft.AspNet.Owin { using WebSocketCloseAsync = Func; using WebSocketReceiveAsync = Func /* data */, CancellationToken /* cancel */, Task>>; using WebSocketReceiveTuple = Tuple; using WebSocketSendAsync = Func /* data */, int /* messageType */, bool /* endOfMessage */, CancellationToken /* cancel */, Task>; public class WebSocketAdapter { private readonly WebSocket _webSocket; private readonly IDictionary _environment; private readonly CancellationToken _cancellationToken; internal WebSocketAdapter(WebSocket webSocket, CancellationToken ct) { _webSocket = webSocket; _cancellationToken = ct; _environment = new Dictionary(); _environment[OwinConstants.WebSocket.SendAsync] = new WebSocketSendAsync(SendAsync); _environment[OwinConstants.WebSocket.ReceiveAsync] = new WebSocketReceiveAsync(ReceiveAsync); _environment[OwinConstants.WebSocket.CloseAsync] = new WebSocketCloseAsync(CloseAsync); _environment[OwinConstants.WebSocket.CallCancelled] = ct; _environment[OwinConstants.WebSocket.Version] = OwinConstants.WebSocket.VersionValue; _environment[typeof(WebSocket).FullName] = webSocket; } internal IDictionary Environment { get { return _environment; } } internal Task SendAsync(ArraySegment buffer, int messageType, bool endOfMessage, CancellationToken cancel) { // Remap close messages to CloseAsync. System.Net.WebSockets.WebSocket.SendAsync does not allow close messages. if (messageType == 0x8) { return RedirectSendToCloseAsync(buffer, cancel); } else if (messageType == 0x9 || messageType == 0xA) { // Ping & Pong, not allowed by the underlying APIs, silently discard. return Task.FromResult(0); } return _webSocket.SendAsync(buffer, OpCodeToEnum(messageType), endOfMessage, cancel); } internal async Task ReceiveAsync(ArraySegment buffer, CancellationToken cancel) { WebSocketReceiveResult nativeResult = await _webSocket.ReceiveAsync(buffer, cancel); if (nativeResult.MessageType == WebSocketMessageType.Close) { _environment[OwinConstants.WebSocket.ClientCloseStatus] = (int)(nativeResult.CloseStatus ?? WebSocketCloseStatus.NormalClosure); _environment[OwinConstants.WebSocket.ClientCloseDescription] = nativeResult.CloseStatusDescription ?? string.Empty; } return new WebSocketReceiveTuple( EnumToOpCode(nativeResult.MessageType), nativeResult.EndOfMessage, nativeResult.Count); } internal Task CloseAsync(int status, string description, CancellationToken cancel) { return _webSocket.CloseOutputAsync((WebSocketCloseStatus)status, description, cancel); } private Task RedirectSendToCloseAsync(ArraySegment buffer, CancellationToken cancel) { if (buffer.Array == null || buffer.Count == 0) { return CloseAsync(1000, string.Empty, cancel); } else if (buffer.Count >= 2) { // Unpack the close message. int statusCode = (buffer.Array[buffer.Offset] << 8) | buffer.Array[buffer.Offset + 1]; string description = Encoding.UTF8.GetString(buffer.Array, buffer.Offset + 2, buffer.Count - 2); return CloseAsync(statusCode, description, cancel); } else { throw new ArgumentOutOfRangeException(nameof(buffer)); } } internal async Task CleanupAsync() { switch (_webSocket.State) { case WebSocketState.Closed: // Closed gracefully, no action needed. case WebSocketState.Aborted: // Closed abortively, no action needed. break; case WebSocketState.CloseReceived: // Echo what the client said, if anything. await _webSocket.CloseAsync(_webSocket.CloseStatus ?? WebSocketCloseStatus.NormalClosure, _webSocket.CloseStatusDescription ?? string.Empty, _cancellationToken); break; case WebSocketState.Open: case WebSocketState.CloseSent: // No close received, abort so we don't have to drain the pipe. _webSocket.Abort(); break; default: throw new ArgumentOutOfRangeException("state", _webSocket.State, string.Empty); } } private static WebSocketMessageType OpCodeToEnum(int messageType) { switch (messageType) { case 0x1: return WebSocketMessageType.Text; case 0x2: return WebSocketMessageType.Binary; case 0x8: return WebSocketMessageType.Close; default: throw new ArgumentOutOfRangeException(nameof(messageType), messageType, string.Empty); } } private static int EnumToOpCode(WebSocketMessageType webSocketMessageType) { switch (webSocketMessageType) { case WebSocketMessageType.Text: return 0x1; case WebSocketMessageType.Binary: return 0x2; case WebSocketMessageType.Close: return 0x8; default: throw new ArgumentOutOfRangeException(nameof(webSocketMessageType), webSocketMessageType, string.Empty); } } } }