{"action":"created","comment":{"url":"https://api.github.com/repos/dotnet/corefx/pulls/comments/66356267","id":66356267,"diff_hunk":"@@ -2,108 +2,1037 @@\n // The .NET Foundation licenses this file to you under the MIT license.\n // See the LICENSE file in the project root for more information.\n \n+using System.Collections.Generic;\n+using System.Diagnostics;\n+using System.IO;\n+using System.Net.Security;\n+using System.Net.Sockets;\n+using System.Runtime.CompilerServices;\n+using System.Runtime.InteropServices;\n+using System.Security.Cryptography;\n+using System.Text;\n using System.Threading;\n using System.Threading.Tasks;\n-using System.Runtime.InteropServices;\n \n namespace System.Net.WebSockets\n {\n     internal struct WebSocketHandle\n     {\n-        #region Properties\n-        public bool IsValid\n+        // WebSocketHandle is the PAL abstraction used by ClientWebSocket.  The implementation\n+        // is a veneer over the real implementation here in ManagedClientWebSocket.\n+\n+        private ManagedClientWebSocket _webSocket;\n+\n+        public bool IsValid => _webSocket != null;\n+\n+        public WebSocketCloseStatus? CloseStatus => _webSocket.CloseStatus;\n+\n+        public string CloseStatusDescription => _webSocket.CloseStatusDescription;\n+\n+        public WebSocketState State => _webSocket.State;\n+\n+        public string SubProtocol => _webSocket.SubProtocol;\n+\n+        public static WebSocketHandle Create() => new WebSocketHandle { _webSocket = new ManagedClientWebSocket() };\n+\n+        public static void CheckPlatformSupport() { /* nop */ }\n+\n+        public Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options) =>\n+            _webSocket.ConnectAsync(uri, cancellationToken, options);\n+\n+        public Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken) =>\n+            _webSocket.SendAsync(buffer, messageType, endOfMessage, cancellationToken);\n+\n+        public Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken) =>\n+            _webSocket.ReceiveAsync(buffer, cancellationToken);\n+\n+        public Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) =>\n+            _webSocket.CloseAsync(closeStatus, statusDescription, cancellationToken);\n+\n+        public Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken) =>\n+            _webSocket.CloseOutputAsync(closeStatus, statusDescription, cancellationToken);\n+\n+        public void Dispose() => _webSocket.Dispose();\n+\n+        public void Abort() => _webSocket.Abort();\n+\n+        /// <summary>A managed implementation of a client web socket.</summary>\n+        private sealed class ManagedClientWebSocket : WebSocket\n         {\n-            get\n+            /// <summary>Per-thread cached StringBuilder for building of strings to send on the connection.</summary>\n+            [ThreadStatic]\n+            private static StringBuilder t_cachedStringBuilder;\n+            /// <summary>Per-thread cached 4-byte mask byte array.</summary>\n+            [ThreadStatic]\n+            private static byte[] t_headerMask;\n+\n+            /// <summary>Thread-safe random number generator used to generate masks for each send.</summary>\n+            private static readonly RandomNumberGenerator s_random = RandomNumberGenerator.Create();\n+            /// <summary>Default encoding for HTTP requests. Latin alphabeta no 1, ISO/IEC 8859-1.</summary>\n+            private static readonly Encoding s_defaultHttpEncoding = Encoding.GetEncoding(28591);\n+            /// <summary>Encoding for the payload of text messages: UTF8 encoding that throws if invalid bytes are discovered, per the RFC.</summary>\n+            private static readonly UTF8Encoding s_textEncoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);\n+            /// <summary>Buffers used while reading header responses line-by-line.</summary>\n+            [ThreadStatic]\n+            private static KeyValuePair<StringBuilder, byte[]> t_readLineBuffers;\n+\n+            /// <summary>Valid states to be in when calling Connect.</summary>\n+            private static readonly WebSocketState[] s_validConnectStates = { WebSocketState.None };\n+            /// <summary>Valid states to be in when calling SendAsync.</summary>\n+            private static readonly WebSocketState[] s_validSendStates = { WebSocketState.Open, WebSocketState.CloseReceived };\n+            /// <summary>Valid states to be in when calling ReceiveAsync.</summary>\n+            private static readonly WebSocketState[] s_validReceiveStates = { WebSocketState.Open, WebSocketState.CloseSent };\n+            /// <summary>Valid states to be in when calling CloseOutputAsync.</summary>\n+            private static readonly WebSocketState[] s_validCloseOutputStates = { WebSocketState.Open, WebSocketState.CloseReceived };\n+            /// <summary>Valid states to be in when calling CloseAsync.</summary>\n+            private static readonly WebSocketState[] s_validCloseStates = { WebSocketState.Open, WebSocketState.CloseReceived, WebSocketState.CloseSent };\n+\n+            /// <summary>GUID appended by the server as part of the security key response.  Defined in the RFC.</summary>\n+            private const string WSServerGuid = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\n+            /// <summary>The maximum size in bytes of a message frame header.</summary>\n+            private const int MaxMessageHeaderLength = 14;\n+            /// <summary>Length of the mask XOR'd with the payload data.</summary>\n+            private const int MaskLength = 4;\n+            /// <summary>Default keep-alive interval to use if one wasn't supplied in the options.</summary>\n+            private const int DefaultKeepAliveIntervalSeconds = 30;\n+\n+            /// <summary>The TcpClient managing the underlying socket.</summary>\n+            private TcpClient _client;\n+            /// <summary>The stream used to communicate with the remote server.</summary>\n+            private Stream _stream;\n+\n+            /// <summary>CancellationTokenSource used to abort all current and future operations when anything is canceled or any error occurs.</summary>\n+            private readonly CancellationTokenSource _abortSource = new CancellationTokenSource();\n+            /// <summary>Timer used to send periodic pings to the server, at the interval specified</summary>\n+            private readonly Timer _keepAliveTimer;\n+\n+            /// <summary>The current state of the web socket in the protocol.</summary>\n+            private WebSocketState _state;\n+            /// <summary>The agreed upon subprotocol with the server.</summary>\n+            private string _subprotocol;\n+            /// <summary>The reason for the close, as sent by the server, or null if not yet closed.</summary>\n+            private WebSocketCloseStatus? _closeStatus = null;\n+            /// <summary>A description of the close reason as sent by the server, or null if not yet closed.</summary>\n+            private string _closeStatusDescription = null;\n+            /// <summary>true if Dispose has been called; otherwise, false.</summary>\n+            private bool _disposed;\n+\n+            /// <summary>\n+            /// The last header received in a ReceiveAsync.  If ReceiveAsync got a header but then\n+            /// returned fewer bytes than was indicated in the header, subsequent ReceiveAsync calls\n+            /// will use the data from the header to construct the subsequent receive results, and\n+            /// the payload length in this header will be decremented to indicate the number of bytes\n+            /// remaining to be received for that header.  As a result, between fragments, the payload\n+            /// length in this header should be 0.\n+            /// </summary>\n+            private MessageHeader _lastReceivedHeader;\n+            /// <summary>Temporary buffer used to store the header for received messages.</summary>\n+            private byte[] _receiveHeaderBuffer = new byte[MaxMessageHeaderLength];\n+            /// <summary>\n+            /// Buffer used to store the complete message to be sent to the stream.  This is needed\n+            /// rather than just sending a header and then the user's buffer, as we need to mutate the\n+            /// buffered data with the mask, and we don't want to change the data in the user's buffer.\n+            /// </summary>\n+            private byte[] _sendBuffer;\n+            /// <summary>\n+            /// Whether the last SendAsync had endOfMessage==false. We need to track this so that we\n+            /// can send the subsequent message with a continuation opcode if the last message was a fragment.\n+            /// </summary>\n+            private bool _lastSendWasFragment;\n+\n+            // Thread-safety:\n+            // It's acceptable to call ReceiveAsync and SendAsync in parallel.  One of each may run concurrently.\n+            // Attemping to invoke any other operations in parallel may corrupt the instance.  Attempting to invoke\n+            // a send operation while another is in progress or a receive operation while another is in progress will\n+            // result in an exception.\n+\n+            /// <summary>\n+            /// The task returned from the last SendAsync operation to not complete synchronously.\n+            /// If this is not null and not completed when a subsequent SendAsync is issued, an exception occurs.\n+            /// </summary>\n+            private Task _lastSendAsync;\n+            /// <summary>\n+            /// The task returned from the last ReceiveAsync operation to not complete synchronously.\n+            /// If this is not null and not completed when a subsequent ReceiveAsync is issued, an exception occurs.\n+            /// </summary>\n+            private Task<WebSocketReceiveResult> _lastReceiveAsync;\n+            /// <summary>\n+            /// Semaphore used to ensure that calls to SendFrameAsync don't run concurrently.  While <see cref=\"_lastSendAsync\"/>\n+            /// is used to fail if a caller tries to issue another SendAsync while a previous one is running, internally\n+            /// we use SendFrameAsync as an implementation detail, and it should not cause user requests to SendAsync to fail,\n+            /// nor should such internal usage be allowed to run concurrently with other internal usage or with SendAsync.\n+            /// </summary>\n+            private readonly SemaphoreSlim _sendFrameAsyncLock = new SemaphoreSlim(1, 1);\n+\n+            public ManagedClientWebSocket()\n             {\n-                throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n+                // Initialize the keep alive timer.  When active, it'll send periodic ping frames to the server.\n+                _keepAliveTimer = new Timer(\n+                    s => ((ManagedClientWebSocket)s).SendKeepAliveFrameAsync(), this, \n+                    Timeout.Infinite, Timeout.Infinite);\n+\n+                // Set up the abort source so that if it's triggered, we transition the instance appropriately\n+                _abortSource.Token.Register(s =>\n+                {\n+                    var thisRef = (ManagedClientWebSocket)s;\n+\n+                    WebSocketState state = thisRef._state;\n+                    if (state != WebSocketState.Closed && state != WebSocketState.Aborted)\n+                    {\n+                        thisRef._state = state != WebSocketState.None && state != WebSocketState.Connecting ?\n+                            WebSocketState.Aborted :\n+                            WebSocketState.Closed;\n+                    }\n+                }, this);\n             }\n-        }\n \n-        public WebSocketCloseStatus? CloseStatus\n-        {\n-            get\n+            public override void Dispose()\n             {\n-                throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n+                if (!_disposed)\n+                {\n+                    _disposed = true;\n+                    _keepAliveTimer.Dispose();\n+                    _stream?.Dispose();\n+                    _client?.Dispose();\n+                }\n             }\n-        }\n \n-        public string CloseStatusDescription\n-        {\n-            get\n+            public override WebSocketCloseStatus? CloseStatus => _closeStatus;\n+\n+            public override string CloseStatusDescription => _closeStatusDescription;\n+\n+            public override WebSocketState State => _state;\n+\n+            public override string SubProtocol => _subprotocol;\n+\n+            public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)\n             {\n-                throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n+                // Not currently used:\n+                // - ClientWebSocketOptions.Credentials\n+                // - ClientWebSocketOptions.Proxy\n+\n+                ClientWebSocket.ThrowIfInvalidState(_state, _disposed, s_validConnectStates);\n+                _state = WebSocketState.Connecting;\n+\n+                // Establish connection to the server\n+                Stream stream = null;\n+                var client = new TcpClient();\n+                try\n+                {\n+                    // Connect over TCP to the remote server\n+                    await client.ConnectAsync(uri.Host, uri.Port).ConfigureAwait(false);\n+                    stream = client.GetStream();\n+\n+                    // Upgrade to SSL if needed\n+                    if (uri.Scheme == UriScheme.Wss)\n+                    {\n+                        var sslStream = new SslStream(stream);\n+                        await sslStream.AuthenticateAsClientAsync(\n+                            uri.Host, \n+                            options.ClientCertificates, \n+                            SecurityProtocol.AllowedSecurityProtocols, \n+                            checkCertificateRevocation: false).ConfigureAwait(false);\n+                        stream = sslStream;\n+                    }\n+\n+                    // Create the security key and expected response, then build all of the request headers\n+                    KeyValuePair<string, string> secKeyAndSecWebSocketAccept = CreateSecKeyAndSecWebSocketAccept();\n+                    byte[] requestHeader = BuildRequestHeader(uri, options, secKeyAndSecWebSocketAccept.Key);\n+\n+                    // Write out the header to the connection\n+                    await stream.WriteAsync(requestHeader, 0, requestHeader.Length, cancellationToken).ConfigureAwait(false);\n+\n+                    // Parse the response and store our state for the remainder of the connection\n+                    _subprotocol = await ParseAndValidateConnectResponseAsync(stream, options, secKeyAndSecWebSocketAccept.Value, cancellationToken).ConfigureAwait(false);\n+                    _client = client;\n+                    _stream = stream;\n+                    _state = WebSocketState.Open;\n+\n+                    // Initiate the keep alive timer\n+                    if (options.KeepAliveInterval != default(TimeSpan))\n+                    {\n+                        _keepAliveTimer.Change(options.KeepAliveInterval, options.KeepAliveInterval);\n+                    }\n+                }\n+                catch (WebSocketException)\n+                {\n+                    _state = WebSocketState.Closed;\n+                    Abort();\n+                    throw;\n+                }\n+                catch (Exception exc)\n+                {\n+                    _state = WebSocketState.Closed;\n+                    Abort();\n+                    throw new WebSocketException(SR.net_webstatus_ConnectFailure, exc);\n+                }\n             }\n-        }\n \n-        public WebSocketState State\n-        {\n-            get\n+            public override Task SendAsync(ArraySegment<byte> buffer, WebSocketMessageType messageType, bool endOfMessage, CancellationToken cancellationToken)\n             {\n-                throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n+                try\n+                {\n+                    ClientWebSocket.ThrowIfInvalidState(_state, _disposed, s_validSendStates);\n+                    ThrowIfOperationInProgress(_lastSendAsync);\n+                }\n+                catch (Exception e)\n+                {\n+                    return Task.FromException(e);\n+                }\n+\n+                Task t = SendFrameAsync(buffer, _lastSendWasFragment ? MessageOpcode.Continuation : ToMessageOpcode(messageType), endOfMessage, cancellationToken);\n+                _lastSendWasFragment = !endOfMessage;\n+\n+                return t.Status == TaskStatus.RanToCompletion ?\n+                    t :\n+                    (_lastSendAsync = WithAbortAsync<int>(t, AbortErrorType.OperationCanceledException, cancellationToken));\n             }\n-        }\n \n-        public string SubProtocol\n-        {\n-            get\n+            public override Task<WebSocketReceiveResult> ReceiveAsync(ArraySegment<byte> buffer, CancellationToken cancellationToken)\n             {\n-                throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n+                try\n+                {\n+                    ClientWebSocket.ThrowIfInvalidState(_state, _disposed, s_validReceiveStates);\n+                    ThrowIfOperationInProgress(_lastReceiveAsync);\n+                }\n+                catch (Exception e)\n+                {\n+                    return Task.FromException<WebSocketReceiveResult>(e);\n+                }\n+\n+                Task<WebSocketReceiveResult> t = ReceiveAsyncPrivate(buffer, cancellationToken);\n+                return t.Status == TaskStatus.RanToCompletion ?\n+                    t :\n+                    (_lastReceiveAsync = WithAbortAsync<WebSocketReceiveResult>(t, AbortErrorType.ClosedOrAbortedWebSocketException, cancellationToken));\n             }\n-        }\n-        #endregion\n \n-        public static WebSocketHandle Create()\n-        {\n-            return default(WebSocketHandle);\n-        }\n+            public override Task CloseAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)\n+            {\n+                ClientWebSocket.ThrowIfInvalidState(_state, _disposed, s_validCloseStates);\n+                Task t = CloseAsyncPrivate(closeStatus, statusDescription, cancellationToken);\n+                return t.Status == TaskStatus.RanToCompletion ? t : WithAbortAsync<WebSocketReceiveResult>(t, AbortErrorType.ClosedOrAbortedWebSocketException, cancellationToken);\n+            }\n \n-        public static void CheckPlatformSupport()\n-        {\n-        }\n-        \n-        public Task ConnectAsyncCore(Uri uri, CancellationToken cancellationToken, ClientWebSocketOptions options)\n-        {\n-            throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n-        }\n+            public override Task CloseOutputAsync(WebSocketCloseStatus closeStatus, string statusDescription, CancellationToken cancellationToken)\n+            {\n+                ClientWebSocket.ThrowIfInvalidState(_state, _disposed, s_validCloseOutputStates);\n+                Task t = SendCloseFrameAsync(closeStatus, statusDescription, cancellationToken);\n+                return t.Status == TaskStatus.RanToCompletion ? t : WithAbortAsync<int>(t, AbortErrorType.ClosedOrAbortedWebSocketException, cancellationToken);\n+            }\n \n-        public Task SendAsync(\n-            ArraySegment<byte> buffer,\n-            WebSocketMessageType messageType,\n-            bool endOfMessage,\n-            CancellationToken cancellationToken)\n-        {\n-            throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n-        }\n+            public override void Abort()\n+            {\n+                _abortSource.Cancel();\n+            }\n \n-        public Task<WebSocketReceiveResult> ReceiveAsync(\n-            ArraySegment<byte> buffer,\n-            CancellationToken cancellationToken)\n-        {\n-            throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n-        }\n+            private static byte[] BuildRequestHeader(Uri uri, ClientWebSocketOptions options, string secKey)\n+            {\n+                StringBuilder builder = t_cachedStringBuilder ?? (t_cachedStringBuilder = new StringBuilder());\n+                Debug.Assert(builder.Length == 0, $\"Expected builder to be empty, got one of length {builder.Length}\");\n+                try\n+                {\n+                    builder.Append(\"GET \").Append(uri.PathAndQuery).Append(\" HTTP/1.1\\r\\n\");\n \n-        public Task CloseAsync(\n-            WebSocketCloseStatus closeStatus,\n-            string statusDescription,\n-            CancellationToken cancellationToken)\n-        {\n-            throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n-        }\n+                    // Add all of the required headers\n+                    builder.Append(\"Host: \").Append(uri.Host).Append(\"\\r\\n\");\n+                    builder.Append(\"Connection: Upgrade\\r\\n\");\n+                    builder.Append(\"Upgrade: websocket\\r\\n\");\n+                    builder.Append(\"Sec-WebSocket-Version: 13\\r\\n\");\n+                    builder.Append(\"Sec-WebSocket-Key: \").Append(secKey).Append(\"\\r\\n\");\n \n-        public Task CloseOutputAsync(\n-            WebSocketCloseStatus closeStatus,\n-            string statusDescription,\n-            CancellationToken cancellationToken)\n-        {\n-            throw new PlatformNotSupportedException(SR.net_WebSockets_UnsupportedPlatform);\n-        }\n+                    // Add all of the additionally requested headers\n+                    foreach (string key in options.RequestHeaders.AllKeys)\n+                    {\n+                        builder.Append(key).Append(\": \").Append(options.RequestHeaders[key]).Append(\"\\r\\n\");\n+                    }\n \n-        public void Dispose()\n-        {\n-        }\n+                    // Add the optional subprotocols header\n+                    if (options.RequestedSubProtocols.Count > 0)\n+                    {\n+                        builder.Append(HttpKnownHeaderNames.SecWebSocketProtocol).Append(\": \");\n+                        builder.Append(options.RequestedSubProtocols[0]);\n+                        for (int i = 1; i < options.RequestedSubProtocols.Count; i++)\n+                        {\n+                            builder.Append(\", \").Append(options.RequestedSubProtocols[i]);\n+                        }\n+                        builder.Append(\"\\r\\n\");\n+                    }\n \n-        public void Abort()\n-        {\n+                    // Add an optional cookies header\n+                    if (options.Cookies != null)\n+                    {\n+                        string header = options.Cookies.GetCookieHeader(uri);\n+                        if (!string.IsNullOrWhiteSpace(header))\n+                        {\n+                            builder.Append(HttpKnownHeaderNames.SetCookie).Append(\": \").Append(header).Append(\"\\r\\n\");\n+                        }\n+                    }\n+\n+                    // End the headers\n+                    builder.Append(\"\\r\\n\");\n+\n+                    // Return the bytes for the built up header\n+                    return s_defaultHttpEncoding.GetBytes(builder.ToString());\n+                }\n+                finally\n+                {\n+                    // Make sure we clear the builder\n+                    builder.Clear();\n+                }\n+            }\n+\n+            private static KeyValuePair<string, string> CreateSecKeyAndSecWebSocketAccept()\n+            {\n+                string secKey = Convert.ToBase64String(Guid.NewGuid().ToByteArray());\n+                using (SHA1 sha = SHA1.Create())\n+                {\n+                    return new KeyValuePair<string, string>(\n+                        secKey,\n+                        Convert.ToBase64String(sha.ComputeHash(Encoding.ASCII.GetBytes(secKey + WSServerGuid))));\n+                }\n+            }\n+\n+            private async Task<string> ParseAndValidateConnectResponseAsync(\n+                Stream stream, ClientWebSocketOptions options, string expectedSecWebSocketAccept, CancellationToken cancellationToken)\n+            {\n+                // Read the first line of the response\n+                string statusLine = await ReadLineNoBufferingAsync(stream, cancellationToken).ConfigureAwait(false);\n+\n+                // Depending on the underlying sockets implementation and timing, connecting to a server that then\n+                // immediately closes the connection may either result in an exception getting thrown from the connect\n+                // earlier, or it may result in getting to here but reading 0 bytes.  If we read 0 bytes and thus have\n+                // an empty status line, treat it as a connect failure.\n+                if (string.IsNullOrEmpty(statusLine))\n+                {\n+                    throw new WebSocketException(SR.Format(SR.net_webstatus_ConnectFailure));\n+                }\n+\n+                const string ExpectedStatusStart = \"HTTP/1.1 \";\n+                const string ExpectedStatusStatWithCode = \"HTTP/1.1 101\"; // 101 == SwitchingProtocols\n+\n+                // If the status line doesn't begin with \"HTTP/1.1\" or isn't long enough to contain a status code, fail.\n+                if (!statusLine.StartsWith(ExpectedStatusStart, StringComparison.Ordinal) || statusLine.Length < ExpectedStatusStatWithCode.Length)\n+                {\n+                    throw new WebSocketException(WebSocketError.HeaderError, SR.net_WebSockets_HeaderError_Generic);\n+                }\n+\n+                // If the status line doesn't contain a status code 101, or if it's long enough to have a status description\n+                // but doesn't contain whitespace after the 101, fail.\n+                if (!statusLine.StartsWith(ExpectedStatusStatWithCode, StringComparison.Ordinal) ||\n+                    (statusLine.Length > ExpectedStatusStatWithCode.Length && !char.IsWhiteSpace(statusLine[ExpectedStatusStatWithCode.Length])))\n+                {\n+                    throw new WebSocketException(SR.net_webstatus_ConnectFailure);\n+                }\n+\n+                // Read each response header. Be liberal in parsing the response header, treating\n+                // everything to the left of the colon as the key and everything to the right as the value, trimming both.\n+                // For each header, validate that we got the expected value.\n+                bool foundUpgrade = false, foundConnection = false, foundSecWebSocketAccept = false;\n+                string subprotocol = null;\n+                string line;\n+                while (!string.IsNullOrEmpty(line = await ReadLineNoBufferingAsync(stream, cancellationToken).ConfigureAwait(false)))\n+                {\n+                    int colonIndex = line.IndexOf(':');\n+                    if (colonIndex == -1)\n+                    {\n+                        throw new WebSocketException(WebSocketError.HeaderError, SR.net_WebSockets_HeaderError_Generic);\n+                    }\n+\n+                    string headerName = line.SubstringTrim(0, colonIndex);\n+                    string headerValue = line.SubstringTrim(colonIndex + 1);\n+\n+                    // The Connection, Upgrade, and SecWebSocketAccept headers are required and with specific values.\n+                    ValidateAndTrackHeader(HttpKnownHeaderNames.Connection, \"Upgrade\", headerName, headerValue, ref foundConnection);\n+                    ValidateAndTrackHeader(HttpKnownHeaderNames.Upgrade, \"websocket\", headerName, headerValue, ref foundUpgrade);\n+                    ValidateAndTrackHeader(HttpKnownHeaderNames.SecWebSocketAccept, expectedSecWebSocketAccept, headerName, headerValue, ref foundSecWebSocketAccept);\n+\n+                    // The SecWebSocketProtocol header is optional.  We should only get it with a non-empty value if we requested subprotocols,\n+                    // 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\n+                    // already got one in a previous header), fail. Otherwise, track which one we got.\n+                    if (string.Equals(HttpKnownHeaderNames.SecWebSocketProtocol, headerName, StringComparison.OrdinalIgnoreCase) &&\n+                        !string.IsNullOrWhiteSpace(headerValue))\n+                    {\n+                        string newSubprotocol = options.RequestedSubProtocols.Find(requested => string.Equals(requested, headerValue, StringComparison.OrdinalIgnoreCase));\n+                        if (newSubprotocol == null || subprotocol != null)\n+                        {\n+                            throw new WebSocketException(\n+                                WebSocketError.UnsupportedProtocol,\n+                                SR.Format(SR.net_WebSockets_AcceptUnsupportedProtocol, string.Join(\", \", options.RequestedSubProtocols), subprotocol));\n+                        }\n+                        subprotocol = newSubprotocol;\n+                    }\n+                }\n+                if (!foundUpgrade || !foundConnection || !foundSecWebSocketAccept)\n+                {\n+                    throw new WebSocketException(SR.net_webstatus_ConnectFailure);\n+                }\n+\n+                return subprotocol;\n+            }\n+\n+            private static void ValidateAndTrackHeader(\n+                string targetHeaderName, string targetHeaderValue, \n+                string foundHeaderName, string foundHeaderValue, \n+                ref bool foundHeader)\n+            {\n+                bool isTargetHeader = string.Equals(targetHeaderName, foundHeaderName, StringComparison.OrdinalIgnoreCase);\n+                if (!foundHeader)\n+                {\n+                    if (isTargetHeader)\n+                    {\n+                        if (!string.Equals(targetHeaderValue, foundHeaderValue, StringComparison.OrdinalIgnoreCase))\n+                        {\n+                            throw new WebSocketException(SR.Format(SR.net_WebSockets_InvalidResponseHeader, targetHeaderName, foundHeaderValue));\n+                        }\n+                        foundHeader = true;\n+                    }\n+                }\n+                else\n+                {\n+                    if (isTargetHeader)\n+                    {\n+                        throw new WebSocketException(SR.Format(SR.net_webstatus_ConnectFailure));\n+                    }\n+                }\n+            }\n+\n+            private Task<string> ReadLineNoBufferingAsync(Stream stream, CancellationToken cancellationToken)\n+            {\n+                Task<string> t = ReadLineNoBufferingAsyncPrivate(stream, cancellationToken);\n+                return t.Status == TaskStatus.RanToCompletion ? t : WithAbortAsync<string>(t, AbortErrorType.ConnectFailureWebSocketException, cancellationToken);\n+            }\n+\n+            private async Task<string> ReadLineNoBufferingAsyncPrivate(Stream stream, CancellationToken cancellationToken)\n+            {\n+                // We don't want to use StreamReader.ReadLineAsync or do any other kind of read-ahead buffering, as we don't\n+                // want to accidentally consume bytes that are part of subsequent messages.  For now, as this is only used\n+                // as part of reading headers, which represents only a small portion of the websocket's lifecycle, we do the\n+                // (terribly) inefficient but easy solution of reading one byte at a time.  If this proves problematic, we can\n+                // update the implementation to do more buffering, e.g. by having wrapping the underlying stream in a BufferedStream.\n+                // As a small concession, we cache the StringBuilder and byte[] used per line.\n+\n+                KeyValuePair<StringBuilder, byte[]> readLineBuffers = t_readLineBuffers;\n+                t_readLineBuffers = default(KeyValuePair<StringBuilder, byte[]>);\n+                if (readLineBuffers.Key == null)\n+                {\n+                    readLineBuffers = new KeyValuePair<StringBuilder, byte[]>(new StringBuilder(), new byte[1]);\n+                }\n+\n+                StringBuilder sb = readLineBuffers.Key;\n+                byte[] oneByte = readLineBuffers.Value;\n+                Debug.Assert(sb.Length == 0, $\"Expected empty StringBuilder\");\n+                try\n+                {\n+                    while (true)\n+                    {\n+                        int numRead = await stream.ReadAsync(oneByte, 0, 1, cancellationToken).ConfigureAwait(false);\n+                        byte b = oneByte[0];\n+                        if (numRead == 0 || b == '\\n') break;\n+                        sb.Append((char)b);\n+                    }\n+\n+                    if (sb.Length > 0 && sb[sb.Length - 1] == '\\r')\n+                    {\n+                        sb.Length = sb.Length - 1;\n+                    }\n+\n+                    return sb.ToString();\n+                }\n+                finally\n+                {\n+                    sb.Clear();\n+                    t_readLineBuffers = readLineBuffers;\n+                }\n+            }\n+\n+            private async Task SendFrameAsync(ArraySegment<byte> buffer, MessageOpcode opcode, bool endOfMessage, CancellationToken cancellationToken)\n+            {\n+                await _sendFrameAsyncLock.WaitAsync().ConfigureAwait(false);\n+                try\n+                {\n+                    EnsureBufferLength(ref _sendBuffer, buffer.Count + MaxMessageHeaderLength);\n+\n+                    int maskOffset = WriteHeader(opcode, _sendBuffer, buffer, endOfMessage);\n+                    int headerLength = maskOffset + MaskLength;\n+\n+                    if (buffer.Count > 0)\n+                    {\n+                        for (int i = 0; i < buffer.Count; i++)\n+                        {\n+                            _sendBuffer[i + headerLength] = (byte)(buffer.Array[buffer.Offset + i] ^ _sendBuffer[maskOffset + (i % MaskLength)]);\n+                        }\n+                    }\n+\n+                    await _stream.WriteAsync(_sendBuffer, 0, headerLength + buffer.Count, cancellationToken).ConfigureAwait(false);\n+                }\n+                catch (ObjectDisposedException ode)\n+                {\n+                    throw new WebSocketException(WebSocketError.ConnectionClosedPrematurely, ode);\n+                }\n+                finally\n+                {\n+                    _sendFrameAsyncLock.Release();\n+                }\n+            }\n+\n+            private void SendKeepAliveFrameAsync()\n+            {\n+                Task pingTask = SendFrameAsync(new ArraySegment<byte>(Array.Empty<byte>()), MessageOpcode.Ping, true, CancellationToken.None);\n+                // This is exists purely to keep the connection alive; ignore any failures.","path":"src/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Unix.cs","position":652,"original_position":652,"commit_id":"b65762052589009332599af9d8a4585436f6d6fc","original_commit_id":"b65762052589009332599af9d8a4585436f6d6fc","user":{"login":"stephentoub","id":2642209,"avatar_url":"https://avatars.githubusercontent.com/u/2642209?v=3","gravatar_id":"","url":"https://api.github.com/users/stephentoub","html_url":"https://github.com/stephentoub","followers_url":"https://api.github.com/users/stephentoub/followers","following_url":"https://api.github.com/users/stephentoub/following{/other_user}","gists_url":"https://api.github.com/users/stephentoub/gists{/gist_id}","starred_url":"https://api.github.com/users/stephentoub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephentoub/subscriptions","organizations_url":"https://api.github.com/users/stephentoub/orgs","repos_url":"https://api.github.com/users/stephentoub/repos","events_url":"https://api.github.com/users/stephentoub/events{/privacy}","received_events_url":"https://api.github.com/users/stephentoub/received_events","type":"User","site_admin":false},"body":"I explicitly don't want to (that's what the comment was trying to get at, but I guess I didn't explain enough).  I simply want to initiate the operation, and there's no need to block this thread waiting for it to complete. The only reason to do so would be if we wanted to propagate any failures on this thread, but I want to ignore any failures.","created_at":"2016-06-08T23:05:38Z","updated_at":"2016-06-08T23:05:39Z","html_url":"https://github.com/dotnet/corefx/pull/9258#discussion_r66356267","pull_request_url":"https://api.github.com/repos/dotnet/corefx/pulls/9258","_links":{"self":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/comments/66356267"},"html":{"href":"https://github.com/dotnet/corefx/pull/9258#discussion_r66356267"},"pull_request":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/9258"}}},"pull_request":{"url":"https://api.github.com/repos/dotnet/corefx/pulls/9258","id":73108784,"html_url":"https://github.com/dotnet/corefx/pull/9258","diff_url":"https://github.com/dotnet/corefx/pull/9258.diff","patch_url":"https://github.com/dotnet/corefx/pull/9258.patch","issue_url":"https://api.github.com/repos/dotnet/corefx/issues/9258","number":9258,"state":"open","locked":false,"title":"Initial System.Net.WebSockets.Client implementation on Unix","user":{"login":"stephentoub","id":2642209,"avatar_url":"https://avatars.githubusercontent.com/u/2642209?v=3","gravatar_id":"","url":"https://api.github.com/users/stephentoub","html_url":"https://github.com/stephentoub","followers_url":"https://api.github.com/users/stephentoub/followers","following_url":"https://api.github.com/users/stephentoub/following{/other_user}","gists_url":"https://api.github.com/users/stephentoub/gists{/gist_id}","starred_url":"https://api.github.com/users/stephentoub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephentoub/subscriptions","organizations_url":"https://api.github.com/users/stephentoub/orgs","repos_url":"https://api.github.com/users/stephentoub/repos","events_url":"https://api.github.com/users/stephentoub/events{/privacy}","received_events_url":"https://api.github.com/users/stephentoub/received_events","type":"User","site_admin":false},"body":"This PR provides an initial implementation of the client WebSockets library for Unix.  It's actually not Unix-specific, and rather is a managed implementation on top of System.Net.Sockets and System.Net.Security.\r\n\r\nI opted to start with a managed implementation as there does not appear to be a universally-adopted native implementation we can utilize.  I looked to see if we could just adopt the Mono implementation, but it's based on HttpWebRequest, which in corefx is built on top of HttpClient, which isn't particularly amenable to being used for websockets (and libcurl does not have websockets support).\r\n\r\nThis is passing all of the unit/functional tests, including a few more I added, though I'm fairly confident that there are additional bugs lurking (i.e. I assume there's going to be a bug tail here).  I'm opting to check this in as an initial implementation now in hopes that others will be able to help with finding and fixing issues.  Along with that, I've spent close to zero time looking at performance; I'm sure there are opportunities to make meaningful improvements, but for now I've opted for simplicity and functionality.  There are also two features I didn't implement: proxies and credentials... but it doesn't look like either the WinHttp or WinRT implementations pay attention to those either.\r\n\r\ncc: @ericeil, @cipop, @davidsh, @bartonjs\r\nFix https://github.com/dotnet/corefx/issues/2486","created_at":"2016-06-08T19:49:46Z","updated_at":"2016-06-08T23:05:39Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4db5ba62647fcc1f2aa00ce77ecea89b7734d11d","assignee":null,"milestone":{"url":"https://api.github.com/repos/dotnet/corefx/milestones/4","html_url":"https://github.com/dotnet/corefx/milestones/1.1.0","labels_url":"https://api.github.com/repos/dotnet/corefx/milestones/4/labels","id":1248719,"number":4,"title":"1.1.0","description":"Features associated with 1.1.0 unfortunately won't make it into the initial 1.0.0 RTM release.  We are tentatively planning on shipping them in the initial feature release after 1.0.0 RTM.\r\n\r\nNo calendar date is set.\r\n\r\nhttps://github.com/aspnet/Home/wiki/Roadmap","creator":{"login":"joshfree","id":1302850,"avatar_url":"https://avatars.githubusercontent.com/u/1302850?v=3","gravatar_id":"","url":"https://api.github.com/users/joshfree","html_url":"https://github.com/joshfree","followers_url":"https://api.github.com/users/joshfree/followers","following_url":"https://api.github.com/users/joshfree/following{/other_user}","gists_url":"https://api.github.com/users/joshfree/gists{/gist_id}","starred_url":"https://api.github.com/users/joshfree/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/joshfree/subscriptions","organizations_url":"https://api.github.com/users/joshfree/orgs","repos_url":"https://api.github.com/users/joshfree/repos","events_url":"https://api.github.com/users/joshfree/events{/privacy}","received_events_url":"https://api.github.com/users/joshfree/received_events","type":"User","site_admin":false},"open_issues":416,"closed_issues":757,"state":"open","created_at":"2015-08-10T18:04:00Z","updated_at":"2016-06-08T20:01:56Z","due_on":"2016-11-30T08:00:00Z","closed_at":null},"commits_url":"https://api.github.com/repos/dotnet/corefx/pulls/9258/commits","review_comments_url":"https://api.github.com/repos/dotnet/corefx/pulls/9258/comments","review_comment_url":"https://api.github.com/repos/dotnet/corefx/pulls/comments{/number}","comments_url":"https://api.github.com/repos/dotnet/corefx/issues/9258/comments","statuses_url":"https://api.github.com/repos/dotnet/corefx/statuses/b65762052589009332599af9d8a4585436f6d6fc","head":{"label":"stephentoub:unix_websockets","ref":"unix_websockets","sha":"b65762052589009332599af9d8a4585436f6d6fc","user":{"login":"stephentoub","id":2642209,"avatar_url":"https://avatars.githubusercontent.com/u/2642209?v=3","gravatar_id":"","url":"https://api.github.com/users/stephentoub","html_url":"https://github.com/stephentoub","followers_url":"https://api.github.com/users/stephentoub/followers","following_url":"https://api.github.com/users/stephentoub/following{/other_user}","gists_url":"https://api.github.com/users/stephentoub/gists{/gist_id}","starred_url":"https://api.github.com/users/stephentoub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephentoub/subscriptions","organizations_url":"https://api.github.com/users/stephentoub/orgs","repos_url":"https://api.github.com/users/stephentoub/repos","events_url":"https://api.github.com/users/stephentoub/events{/privacy}","received_events_url":"https://api.github.com/users/stephentoub/received_events","type":"User","site_admin":false},"repo":{"id":26551529,"name":"corefx","full_name":"stephentoub/corefx","owner":{"login":"stephentoub","id":2642209,"avatar_url":"https://avatars.githubusercontent.com/u/2642209?v=3","gravatar_id":"","url":"https://api.github.com/users/stephentoub","html_url":"https://github.com/stephentoub","followers_url":"https://api.github.com/users/stephentoub/followers","following_url":"https://api.github.com/users/stephentoub/following{/other_user}","gists_url":"https://api.github.com/users/stephentoub/gists{/gist_id}","starred_url":"https://api.github.com/users/stephentoub/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/stephentoub/subscriptions","organizations_url":"https://api.github.com/users/stephentoub/orgs","repos_url":"https://api.github.com/users/stephentoub/repos","events_url":"https://api.github.com/users/stephentoub/events{/privacy}","received_events_url":"https://api.github.com/users/stephentoub/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/stephentoub/corefx","description":"This repository contains the foundational libraries that make up the .NET Core development stack.","fork":true,"url":"https://api.github.com/repos/stephentoub/corefx","forks_url":"https://api.github.com/repos/stephentoub/corefx/forks","keys_url":"https://api.github.com/repos/stephentoub/corefx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/stephentoub/corefx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/stephentoub/corefx/teams","hooks_url":"https://api.github.com/repos/stephentoub/corefx/hooks","issue_events_url":"https://api.github.com/repos/stephentoub/corefx/issues/events{/number}","events_url":"https://api.github.com/repos/stephentoub/corefx/events","assignees_url":"https://api.github.com/repos/stephentoub/corefx/assignees{/user}","branches_url":"https://api.github.com/repos/stephentoub/corefx/branches{/branch}","tags_url":"https://api.github.com/repos/stephentoub/corefx/tags","blobs_url":"https://api.github.com/repos/stephentoub/corefx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/stephentoub/corefx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/stephentoub/corefx/git/refs{/sha}","trees_url":"https://api.github.com/repos/stephentoub/corefx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/stephentoub/corefx/statuses/{sha}","languages_url":"https://api.github.com/repos/stephentoub/corefx/languages","stargazers_url":"https://api.github.com/repos/stephentoub/corefx/stargazers","contributors_url":"https://api.github.com/repos/stephentoub/corefx/contributors","subscribers_url":"https://api.github.com/repos/stephentoub/corefx/subscribers","subscription_url":"https://api.github.com/repos/stephentoub/corefx/subscription","commits_url":"https://api.github.com/repos/stephentoub/corefx/commits{/sha}","git_commits_url":"https://api.github.com/repos/stephentoub/corefx/git/commits{/sha}","comments_url":"https://api.github.com/repos/stephentoub/corefx/comments{/number}","issue_comment_url":"https://api.github.com/repos/stephentoub/corefx/issues/comments{/number}","contents_url":"https://api.github.com/repos/stephentoub/corefx/contents/{+path}","compare_url":"https://api.github.com/repos/stephentoub/corefx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/stephentoub/corefx/merges","archive_url":"https://api.github.com/repos/stephentoub/corefx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/stephentoub/corefx/downloads","issues_url":"https://api.github.com/repos/stephentoub/corefx/issues{/number}","pulls_url":"https://api.github.com/repos/stephentoub/corefx/pulls{/number}","milestones_url":"https://api.github.com/repos/stephentoub/corefx/milestones{/number}","notifications_url":"https://api.github.com/repos/stephentoub/corefx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/stephentoub/corefx/labels{/name}","releases_url":"https://api.github.com/repos/stephentoub/corefx/releases{/id}","deployments_url":"https://api.github.com/repos/stephentoub/corefx/deployments","created_at":"2014-11-12T19:06:24Z","updated_at":"2016-01-11T15:13:21Z","pushed_at":"2016-06-08T22:30:33Z","git_url":"git://github.com/stephentoub/corefx.git","ssh_url":"git@github.com:stephentoub/corefx.git","clone_url":"https://github.com/stephentoub/corefx.git","svn_url":"https://github.com/stephentoub/corefx","homepage":"","size":73430,"stargazers_count":1,"watchers_count":1,"language":"C#","has_issues":false,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":1,"default_branch":"master"}},"base":{"label":"dotnet:master","ref":"master","sha":"fc88bf49cf8cdaa6c5484b3e19f8f17cccd50fbb","user":{"login":"dotnet","id":9141961,"avatar_url":"https://avatars.githubusercontent.com/u/9141961?v=3","gravatar_id":"","url":"https://api.github.com/users/dotnet","html_url":"https://github.com/dotnet","followers_url":"https://api.github.com/users/dotnet/followers","following_url":"https://api.github.com/users/dotnet/following{/other_user}","gists_url":"https://api.github.com/users/dotnet/gists{/gist_id}","starred_url":"https://api.github.com/users/dotnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dotnet/subscriptions","organizations_url":"https://api.github.com/users/dotnet/orgs","repos_url":"https://api.github.com/users/dotnet/repos","events_url":"https://api.github.com/users/dotnet/events{/privacy}","received_events_url":"https://api.github.com/users/dotnet/received_events","type":"Organization","site_admin":false},"repo":{"id":26295345,"name":"corefx","full_name":"dotnet/corefx","owner":{"login":"dotnet","id":9141961,"avatar_url":"https://avatars.githubusercontent.com/u/9141961?v=3","gravatar_id":"","url":"https://api.github.com/users/dotnet","html_url":"https://github.com/dotnet","followers_url":"https://api.github.com/users/dotnet/followers","following_url":"https://api.github.com/users/dotnet/following{/other_user}","gists_url":"https://api.github.com/users/dotnet/gists{/gist_id}","starred_url":"https://api.github.com/users/dotnet/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/dotnet/subscriptions","organizations_url":"https://api.github.com/users/dotnet/orgs","repos_url":"https://api.github.com/users/dotnet/repos","events_url":"https://api.github.com/users/dotnet/events{/privacy}","received_events_url":"https://api.github.com/users/dotnet/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/dotnet/corefx","description":"This repo contains the .NET Core foundational libraries, called CoreFX. It includes classes for collections, file systems, console, XML, async and many others.","fork":false,"url":"https://api.github.com/repos/dotnet/corefx","forks_url":"https://api.github.com/repos/dotnet/corefx/forks","keys_url":"https://api.github.com/repos/dotnet/corefx/keys{/key_id}","collaborators_url":"https://api.github.com/repos/dotnet/corefx/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/dotnet/corefx/teams","hooks_url":"https://api.github.com/repos/dotnet/corefx/hooks","issue_events_url":"https://api.github.com/repos/dotnet/corefx/issues/events{/number}","events_url":"https://api.github.com/repos/dotnet/corefx/events","assignees_url":"https://api.github.com/repos/dotnet/corefx/assignees{/user}","branches_url":"https://api.github.com/repos/dotnet/corefx/branches{/branch}","tags_url":"https://api.github.com/repos/dotnet/corefx/tags","blobs_url":"https://api.github.com/repos/dotnet/corefx/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/dotnet/corefx/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/dotnet/corefx/git/refs{/sha}","trees_url":"https://api.github.com/repos/dotnet/corefx/git/trees{/sha}","statuses_url":"https://api.github.com/repos/dotnet/corefx/statuses/{sha}","languages_url":"https://api.github.com/repos/dotnet/corefx/languages","stargazers_url":"https://api.github.com/repos/dotnet/corefx/stargazers","contributors_url":"https://api.github.com/repos/dotnet/corefx/contributors","subscribers_url":"https://api.github.com/repos/dotnet/corefx/subscribers","subscription_url":"https://api.github.com/repos/dotnet/corefx/subscription","commits_url":"https://api.github.com/repos/dotnet/corefx/commits{/sha}","git_commits_url":"https://api.github.com/repos/dotnet/corefx/git/commits{/sha}","comments_url":"https://api.github.com/repos/dotnet/corefx/comments{/number}","issue_comment_url":"https://api.github.com/repos/dotnet/corefx/issues/comments{/number}","contents_url":"https://api.github.com/repos/dotnet/corefx/contents/{+path}","compare_url":"https://api.github.com/repos/dotnet/corefx/compare/{base}...{head}","merges_url":"https://api.github.com/repos/dotnet/corefx/merges","archive_url":"https://api.github.com/repos/dotnet/corefx/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/dotnet/corefx/downloads","issues_url":"https://api.github.com/repos/dotnet/corefx/issues{/number}","pulls_url":"https://api.github.com/repos/dotnet/corefx/pulls{/number}","milestones_url":"https://api.github.com/repos/dotnet/corefx/milestones{/number}","notifications_url":"https://api.github.com/repos/dotnet/corefx/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/dotnet/corefx/labels{/name}","releases_url":"https://api.github.com/repos/dotnet/corefx/releases{/id}","deployments_url":"https://api.github.com/repos/dotnet/corefx/deployments","created_at":"2014-11-06T23:42:48Z","updated_at":"2016-06-08T15:12:27Z","pushed_at":"2016-06-08T22:30:31Z","git_url":"git://github.com/dotnet/corefx.git","ssh_url":"git@github.com:dotnet/corefx.git","clone_url":"https://github.com/dotnet/corefx.git","svn_url":"https://github.com/dotnet/corefx","homepage":"http://dotnet.github.io/","size":104078,"stargazers_count":9837,"watchers_count":9837,"language":"C#","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":2379,"mirror_url":null,"open_issues_count":1036,"forks":2379,"open_issues":1036,"watchers":9837,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/9258"},"html":{"href":"https://github.com/dotnet/corefx/pull/9258"},"issue":{"href":"https://api.github.com/repos/dotnet/corefx/issues/9258"},"comments":{"href":"https://api.github.com/repos/dotnet/corefx/issues/9258/comments"},"review_comments":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/9258/comments"},"review_comment":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/dotnet/corefx/pulls/9258/commits"},"statuses":{"href":"https://api.github.com/repos/dotnet/corefx/statuses/b65762052589009332599af9d8a4585436f6d6fc"}}}}
{"actor":{"display_login":"stephentoub"}}