[websocket] Implement HttpListenerContext.AcceptWebSocketAsync().
authorINADA Naoki <inada-n@klab.com>
Sat, 5 Apr 2014 01:56:24 +0000 (10:56 +0900)
committerINADA Naoki <inada-n@klab.com>
Mon, 7 Apr 2014 01:36:03 +0000 (10:36 +0900)
mcs/class/System/System.Net.WebSockets/HttpListenerWebSocketContext.cs
mcs/class/System/System.Net.WebSockets/StreamWebSocket.cs [new file with mode: 0644]
mcs/class/System/System.Net.WebSockets/WebSocket.cs
mcs/class/System/System.Net/HttpConnection.cs
mcs/class/System/System.Net/HttpListenerContext.cs
mcs/class/System/System.Net/HttpListenerRequest.cs
mcs/class/System/System.dll.sources

index 5da2a8dcb02b18f79e64cd5e1881cdd73afb398c..e12939cd2a2391e831151fa27e89c3ee7282e0c7 100644 (file)
@@ -29,7 +29,9 @@
 #if NET_4_5
 
 using System;
+using System.IO;
 using System.Net;
+using System.Net.Sockets;
 using System.Collections.Specialized;
 using System.Collections.Generic;
 using System.Security.Principal;
@@ -40,59 +42,62 @@ namespace System.Net.WebSockets
 {
        public class HttpListenerWebSocketContext : WebSocketContext
        {
-               [MonoTODO]
+               HttpListenerRequest request;
+               StreamWebSocket webSocket;
+               IPrincipal user;
+
+               internal HttpListenerWebSocketContext(RequestStream requestStream, Stream writeStream, HttpListenerRequest request, IPrincipal user, string subProtocol)
+               {
+                       this.request = request;
+                       this.user = user;
+                       webSocket = new StreamWebSocket (requestStream, writeStream, 16 * 1024, false, subProtocol);
+               }
+
                public override CookieCollection CookieCollection {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Cookies;
                        }
                }
 
-               [MonoTODO]
                public override NameValueCollection Headers {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Headers;
                        }
                }
 
-               [MonoTODO]
                public override bool IsAuthenticated {
                        get {
-                               throw new NotImplementedException ();
+                               return request.IsAuthenticated;
                        }
                }
 
-               [MonoTODO]
                public override bool IsLocal {
                        get {
-                               throw new NotImplementedException ();
+                               return request.IsLocal;
                        }
                }
 
-               [MonoTODO]
                public override bool IsSecureConnection {
                        get {
-                               throw new NotImplementedException ();
+                               return request.IsSecureConnection;
                        }
                }
 
-               [MonoTODO]
                public override string Origin {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Headers ["Origin"];
                        }
                }
 
-               [MonoTODO]
                public override Uri RequestUri {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Url;
                        }
                }
 
-               [MonoTODO]
                public override string SecWebSocketKey {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Headers ["Sec-WebSocket-Key"];
                        }
                }
 
@@ -103,24 +108,19 @@ namespace System.Net.WebSockets
                        }
                }
 
-               [MonoTODO]
                public override string SecWebSocketVersion {
                        get {
-                               throw new NotImplementedException ();
+                               return request.Headers ["Sec-WebSocket-Version"];
                        }
                }
 
-               [MonoTODO]
                public override IPrincipal User {
-                       get {
-                               throw new NotImplementedException ();
-                       }
+                       get { return user; }
                }
 
-               [MonoTODO]
                public override WebSocket WebSocket {
                        get {
-                               throw new NotImplementedException ();
+                               return webSocket;
                        }
                }
        }
diff --git a/mcs/class/System/System.Net.WebSockets/StreamWebSocket.cs b/mcs/class/System/System.Net.WebSockets/StreamWebSocket.cs
new file mode 100644 (file)
index 0000000..d743ce7
--- /dev/null
@@ -0,0 +1,306 @@
+//
+// StreamWebSocket.cs
+//
+// Authors:
+//       Jérémie Laval <jeremie dot laval at xamarin dot com>
+//    INADA Naoki <songofacandy at gmail dot com>
+//
+// Copyright 2013-2014 Xamarin Inc (http://www.xamarin.com).
+//
+// Lightly inspired from WebSocket4Net distributed under the Apache License 2.0
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+#if NET_4_5
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Net.WebSockets;
+using System.Security.Cryptography;
+using System.Threading;
+using System.Threading.Tasks;
+using System.Text;
+
+namespace System.Net.WebSockets {
+       internal class StreamWebSocket : WebSocket, IDisposable {
+               const string Magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+               WebSocketState state;
+               Stream writeStream;
+               Stream readStream;
+               Random random;
+               string subProtocol;
+
+               bool maskSend;
+               const int HeaderMaxLength = 14;
+               byte[] headerBuffer;
+               int sendBufferSize;
+               byte[] sendBuffer;
+
+               public StreamWebSocket (Stream readStream, Stream writeStream, int sendBufferSize, bool maskSend, string subProtocol)
+               {
+                       this.readStream = readStream;
+                       this.writeStream = writeStream;
+                       this.maskSend = maskSend;
+                       if (maskSend) {
+                               random = new Random ();
+                       }
+                       this.sendBufferSize = sendBufferSize;
+                       state = WebSocketState.Open;
+                       headerBuffer = new byte[HeaderMaxLength];
+                       this.subProtocol = subProtocol;
+               }
+
+               public override void Dispose ()
+               {
+                       if (readStream != null) {
+                               readStream.Dispose ();
+                               readStream = null;
+                       }
+                       if (writeStream != null) {
+                               writeStream.Dispose ();
+                               writeStream = null;
+                       }
+                       state = WebSocketState.Aborted;
+               }
+
+               public override WebSocketState State {
+                       get { return state; }
+               }
+
+               public override string SubProtocol {
+                       get { return subProtocol; }
+               }
+
+               public override WebSocketCloseStatus? CloseStatus {
+                       get {
+                               if (state != WebSocketState.Closed)
+                                       return (WebSocketCloseStatus?)null;
+                               return WebSocketCloseStatus.Empty;
+                       }
+               }
+
+               public override string CloseStatusDescription {
+                       get {
+                               return null;
+                       }
+               }
+
+               public override void Abort ()
+               {
+                       this.Dispose ();
+               }
+
+               public override Task SendAsync (ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)
+               {
+                       EnsureWebSocketConnected ();
+                       ValidateArraySegment (buffer);
+                       if (writeStream == null)
+                               throw new WebSocketException (WebSocketError.Faulted);
+                       var count = Math.Max (sendBufferSize, buffer.Count) + HeaderMaxLength;
+                       if (sendBuffer == null || sendBuffer.Length < count)
+                               sendBuffer = new byte[count];
+                       EnsureWebSocketState (WebSocketState.Open, WebSocketState.CloseReceived);
+                       var maskOffset = WriteHeader (messageType, buffer, endOfMessage);
+                       int headerLength = maskOffset;
+                       if (buffer.Count > 0) {
+                               if (maskSend) {
+                                       headerLength += 4;
+                                       MaskData (buffer, maskOffset);
+                               } else {
+                                       Array.Copy (buffer.Array, buffer.Offset, sendBuffer, maskOffset, buffer.Count);
+                               }
+                       }
+                       Array.Copy (headerBuffer, sendBuffer, headerLength);
+                       return writeStream.WriteAsync (sendBuffer, 0, headerLength + buffer.Count, cancellationToken);
+               }
+
+               public override async Task<WebSocketReceiveResult> ReceiveAsync (ArraySegment<byte> buffer, CancellationToken cancellationToken)
+               {
+                       EnsureWebSocketConnected ();
+                       ValidateArraySegment (buffer);
+                       EnsureWebSocketState (WebSocketState.Open, WebSocketState.CloseSent);
+
+                       // First read the two first bytes to know what we are doing next
+                       await readStream.ReadAsync (headerBuffer, 0, 2, cancellationToken);
+                       var isLast = (headerBuffer[0] >> 7) > 0;
+                       var isMasked = (headerBuffer[1] >> 7) > 0;
+                       byte[] mask = new byte[4];
+                       var type = (WebSocketMessageType)(headerBuffer[0] & 0xF);
+                       long length = headerBuffer[1] & 0x7F;
+                       int offset = 0;
+                       if (length == 126) {
+                               offset = 2;
+                               await readStream.ReadAsync (headerBuffer, 2, offset, cancellationToken);
+                               length = (headerBuffer[2] << 8) | headerBuffer[3];
+                       } else if (length == 127) {
+                               offset = 8;
+                               await readStream.ReadAsync (headerBuffer, 2, offset, cancellationToken);
+                               length = 0;
+                               for (int i = 2; i <= 9; i++)
+                                       length = (length << 8) | headerBuffer[i];
+                       }
+
+                       if (isMasked) {
+                               await readStream.ReadAsync (headerBuffer, 2 + offset, 4, cancellationToken);
+                               for (int i = 0; i < 4; i++) {
+                                       var pos = i + offset + 2;
+                                       mask[i] = headerBuffer[pos];
+                               }
+                       }
+
+                       if (type == WebSocketMessageType.Close) {
+                               var tmpBuffer = new byte[length];
+                               await readStream.ReadAsync (tmpBuffer, 0, tmpBuffer.Length, cancellationToken);
+                               UnmaskInPlace (mask, tmpBuffer, 0, (int)length);
+                               var closeStatus = (WebSocketCloseStatus)(tmpBuffer[0] << 8 | tmpBuffer[1]);
+                               var closeDesc = tmpBuffer.Length > 2 ? Encoding.UTF8.GetString (tmpBuffer, 2, tmpBuffer.Length - 2) : string.Empty;
+                               if (state == WebSocketState.Open) {
+                                       await SendCloseFrame (WebSocketCloseStatus.NormalClosure, "Received Close", cancellationToken).ConfigureAwait (false);
+                               }
+                               state = WebSocketState.Closed;
+                               return new WebSocketReceiveResult ((int)length, type, isLast, closeStatus, closeDesc);
+                       } else {
+                               var readLength = (int)(buffer.Count < length ? buffer.Count : length);
+                               await readStream.ReadAsync (buffer.Array, buffer.Offset, readLength);
+                               UnmaskInPlace (mask, buffer.Array, buffer.Offset, readLength);
+                               // TODO: Skip remains if buffer is smaller than frame?
+                               return new WebSocketReceiveResult ((int)length, type, isLast);
+                       }
+               }
+
+               // The damn difference between those two methods is that CloseAsync will wait for server acknowledgement before completing
+               // while CloseOutputAsync will send the close packet and simply complete.
+
+               public override async Task CloseAsync (WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
+               {
+                       EnsureWebSocketConnected ();
+                       await SendCloseFrame (closeStatus, statusDescription, cancellationToken).ConfigureAwait (false);
+                       state = WebSocketState.CloseSent;
+                       // TODO: figure what's exceptions are thrown if the server returns something faulty here
+                       await ReceiveAsync (new ArraySegment<byte> (new byte[0]), cancellationToken).ConfigureAwait (false);
+                       state = WebSocketState.Closed;
+               }
+
+               public override async Task CloseOutputAsync (WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
+               {
+                       EnsureWebSocketConnected ();
+                       await SendCloseFrame (closeStatus, statusDescription, cancellationToken).ConfigureAwait (false);
+                       state = WebSocketState.CloseSent;
+               }
+
+               async Task SendCloseFrame (WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)
+               {
+                       var statusDescBuffer = string.IsNullOrEmpty (statusDescription) ? new byte[2] : new byte[2 + Encoding.UTF8.GetByteCount (statusDescription)];
+                       statusDescBuffer[0] = (byte)(((ushort)closeStatus) >> 8);
+                       statusDescBuffer[1] = (byte)(((ushort)closeStatus) & 0xFF);
+                       if (!string.IsNullOrEmpty (statusDescription))
+                               Encoding.UTF8.GetBytes (statusDescription, 0, statusDescription.Length, statusDescBuffer, 2);
+                       await SendAsync (new ArraySegment<byte> (statusDescBuffer), WebSocketMessageType.Close, true, cancellationToken).ConfigureAwait (false);
+               }
+
+               public static string CreateAcceptKey(string secKey)
+               {
+                       return Convert.ToBase64String (SHA1.Create ().ComputeHash (Encoding.ASCII.GetBytes (secKey + Magic)));
+               }
+
+               int WriteHeader (WebSocketMessageType type, ArraySegment<byte> buffer, bool endOfMessage)
+               {
+                       var opCode = (byte)type;
+                       var length = buffer.Count;
+
+                       headerBuffer[0] = (byte)(opCode | (endOfMessage ? 0x80 : 0x0));
+                       if (length < 126) {
+                               headerBuffer[1] = (byte)length;
+                       } else if (length <= ushort.MaxValue) {
+                               headerBuffer[1] = (byte)126;
+                               headerBuffer[2] = (byte)(length / 256);
+                               headerBuffer[3] = (byte)(length % 256);
+                       } else {
+                               headerBuffer[1] = (byte)127;
+
+                               int left = length;
+                               int unit = 256;
+
+                               for (int i = 9; i > 1; i--) {
+                                       headerBuffer[i] = (byte)(left % unit);
+                                       left = left / unit;
+                               }
+                       }
+
+                       var l = Math.Max (0, headerBuffer[1] - 125);
+                       var maskOffset = 2 + l * l * 2;
+                       if (maskSend) {
+                               GenerateMask (headerBuffer, maskOffset);
+                               headerBuffer[1] |= 0x80;
+                       }
+                       return maskOffset;
+               }
+
+               void GenerateMask (byte[] mask, int offset)
+               {
+                       mask[offset + 0] = (byte)random.Next (0, 255);
+                       mask[offset + 1] = (byte)random.Next (0, 255);
+                       mask[offset + 2] = (byte)random.Next (0, 255);
+                       mask[offset + 3] = (byte)random.Next (0, 255);
+               }
+
+               void MaskData (ArraySegment<byte> buffer, int maskOffset)
+               {
+                       var sendBufferOffset = maskOffset + 4;
+                       for (var i = 0; i < buffer.Count; i++)
+                               sendBuffer[i + sendBufferOffset] = (byte)(buffer.Array[buffer.Offset + i] ^ headerBuffer[maskOffset + (i % 4)]);
+               }
+
+               void UnmaskInPlace(byte[] mask, byte[] data, int offset, int length)
+               {
+                       for (int i = 0; i < length; i++) {
+                               data[i + offset] ^= mask[i % 4];
+                       }
+               }
+
+               void EnsureWebSocketConnected ()
+               {
+                       if (state < WebSocketState.Open)
+                               throw new InvalidOperationException ("The WebSocket is not connected");
+               }
+
+               void EnsureWebSocketState (params WebSocketState[] validStates)
+               {
+                       foreach (var validState in validStates)
+                               if (state == validState)
+                                       return;
+                       throw new WebSocketException ("The WebSocket is in an invalid state ('" + state + "') for this operation. Valid states are: " + string.Join (", ", validStates));
+               }
+
+               void ValidateArraySegment (ArraySegment<byte> segment)
+               {
+                       if (segment.Array == null)
+                               throw new ArgumentNullException ("buffer.Array");
+                       if (segment.Offset < 0)
+                               throw new ArgumentOutOfRangeException ("buffer.Offset");
+                       if (segment.Offset + segment.Count > segment.Array.Length)
+                               throw new ArgumentOutOfRangeException ("buffer.Count");
+               }
+       }
+}
+
+#endif
index c32c5714f7387197925b54a082afcb525b2e5062..e969049fe02c6a1a44e95066dbe421bd364d4b29 100644 (file)
@@ -48,10 +48,9 @@ namespace System.Net.WebSockets
                public abstract WebSocketState State { get; }
                public abstract string SubProtocol { get; }
 
-               [MonoTODO]
                public static TimeSpan DefaultKeepAliveInterval {
                        get {
-                               throw new NotImplementedException ();
+                               return TimeSpan.FromSeconds (30);
                        }
                }
 
index 33080c2d77b9a1d463c3e3f61bbb4a0ff4eef9fd..c08d3d58e32b2cebd8b2abcb1f0b953e747c3c56 100644 (file)
@@ -209,6 +209,12 @@ namespace System.Net {
                        return o_stream;
                }
 
+               internal Stream Hijack ()
+               {
+                       // TODO: disable normal request/response.
+                       return stream;
+               }
+
                static void OnRead (IAsyncResult ares)
                {
                        HttpConnection cnc = (HttpConnection) ares.AsyncState;
index cd0ef417b68d24fec6ae23bad7a388f2b75400b3..846b6750ff294382464aa98187f6c6af660a2e95 100644 (file)
 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 //
+using System.ComponentModel;
+using System.Net.Mime;
 
 #if SECURITY_DEP
 
+using System;
 using System.Collections.Specialized;
 using System.IO;
 using System.Security.Principal;
 using System.Text;
+
+#if NET_4_5
+using System.Net.WebSockets;
+using System.Threading.Tasks;
+#endif
 namespace System.Net {
        public sealed class HttpListenerContext {
                HttpListenerRequest request;
@@ -94,7 +102,7 @@ namespace System.Net {
                        }
                        // TODO: throw if malformed -> 400 bad request
                }
-       
+
                internal IPrincipal ParseBasicAuthentication (string authData) {
                        try {
                                // Basic AUTH Data is a formatted Base64 String
@@ -103,7 +111,7 @@ namespace System.Net {
                                string password = null;
                                int pos = -1;
                                string authString = System.Text.Encoding.Default.GetString (Convert.FromBase64String (authData));
-       
+
                                // The format is DOMAIN\username:password
                                // Domain is optional
 
@@ -111,7 +119,7 @@ namespace System.Net {
        
                                // parse the password off the end
                                password = authString.Substring (pos+1);
-                               
+
                                // discard the password
                                authString = authString.Substring (0, pos);
        
@@ -133,6 +141,54 @@ namespace System.Net {
                                return null;
                        } 
                }
+#if NET_4_5
+               public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync (string subProtocol)
+               {
+                       return AcceptWebSocketAsync (subProtocol, System.Net.WebSockets.WebSocket.DefaultKeepAliveInterval);
+               }
+
+               public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync (string subProtocol, TimeSpan keepAliveInterval)
+               {
+                       // Default receiveBuffersize is documented on MSDN Library.
+                       // http://msdn.microsoft.com/ja-jp/library/hh159274(v=vs.110).aspx
+                       return AcceptWebSocketAsync (subProtocol, 16385, keepAliveInterval);
+               }
+
+               public async Task<HttpListenerWebSocketContext> AcceptWebSocketAsync (string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval)
+               {
+                       if (subProtocol != null && subProtocol == "") {
+                               throw new ArgumentException ("subProtocol must not empty string");
+                       }
+                       if (receiveBufferSize < 16 || receiveBufferSize > 64 * 1024) {
+                               throw new ArgumentOutOfRangeException ("receiveBufferSize should be >=16 and <=64K bytes");
+                       }
+                       if (!request.IsWebSocketRequest) {
+                               throw new WebSocketException ("Request is not WebSocket Handshake");
+                       }
+                       string secKey = request.Headers ["Sec-WebSocket-Key"];
+                       if (secKey == null) {
+                               throw new WebSocketException ("Request doesn't contain Sec-WebSocket-Key header");
+                       }
+                       string origin = request.Headers ["Origin"];
+                       if (origin == null) {
+                               throw new WebSocketException ("Request doesn't contain Origin header");
+                       }
+                       string acceptKey = StreamWebSocket.CreateAcceptKey (secKey);
+                       var rstream = cnc.GetRequestStream (false, -1);
+                       var wstream = cnc.Hijack ();
+                       string header = "HTTP/1.1 101 Switching Protocols\r\n";
+                       header += "Upgrade: websocket\r\nConnection: Upgrade\r\n";
+                       header += "Sec-WebSocket-Accept: " + acceptKey + "\r\n\r\n";
+                       var headerBytes = Encoding.ASCII.GetBytes (header);
+                       await wstream.WriteAsync (headerBytes, 0, headerBytes.Length);
+                       return new HttpListenerWebSocketContext (rstream, wstream, request, user, subProtocol);
+               }
+
+               public Task<HttpListenerWebSocketContext> AcceptWebSocketAsync (string subProtocol, int receiveBufferSize, TimeSpan keepAliveInterval, ArraySegment<byte> internalBuffer)
+               {
+                       return AcceptWebSocketAsync (subProtocol, receiveBufferSize, keepAliveInterval);
+               }
+#endif
        }
 }
 #endif
index c72d21d1d82f3c44e19c7f9d381d434336cc33aa..14cc96e8a76fe22ffbda2d2ed349daead221d3c4 100644 (file)
@@ -525,10 +525,17 @@ namespace System.Net {
 #endif
                
 #if NET_4_5
-               [MonoTODO]
                public bool IsWebSocketRequest {
                        get {
-                               return false;
+                               string connection = headers.Get ("Connection");
+                               if (connection == null || String.Compare (connection, "upgrade", StringComparison.OrdinalIgnoreCase) != 0) {
+                                       return false;
+                               }
+                               string upgrade = headers.Get ("Upgrade");
+                               if (upgrade == null || String.Compare (upgrade, "websocket", StringComparison.OrdinalIgnoreCase) != 0) {
+                                       return false;
+                               }
+                               return true;
                        }
                }
 
index 55cb9d9fb685d5772b45b6d25c0958755bc6bfa8..30c9ddf225a3ce4c4f4d54b2b424ecbbbc40a12c 100644 (file)
@@ -888,6 +888,7 @@ System.Net/WebUtility.cs
 System.Net.WebSockets/ClientWebSocket.cs
 System.Net.WebSockets/ClientWebSocketOptions.cs
 System.Net.WebSockets/HttpListenerWebSocketContext.cs
+System.Net.WebSockets/StreamWebSocket.cs
 System.Net.WebSockets/WebSocket.cs
 System.Net.WebSockets/WebSocketCloseStatus.cs
 System.Net.WebSockets/WebSocketContext.cs