Exemple #1
0
        /// <summary>
        ///     Asynchronously connect to a Kubernetes WebSocket.
        /// </summary>
        /// <param name="uri">
        ///     The target URI.
        /// </param>
        /// <param name="options">
        ///     <see cref="KubernetesWebSocketOptions"/> that control the WebSocket's configuration and connection process.
        /// </param>
        /// <param name="cancellationToken">
        ///     An optional <see cref="CancellationToken"/> that can be used to cancel the operation.
        /// </param>
        /// <returns>
        ///     A <see cref="WebSocket"/> representing the connection.
        /// </returns>
        public static async Task <WebSocket> ConnectAsync(Uri uri, KubernetesWebSocketOptions options, CancellationToken cancellationToken = default(CancellationToken))
        {
            try
            {
                // Connect to the remote server
                Socket connectedSocket = await ConnectSocketAsync(uri.Host, uri.Port, cancellationToken).ConfigureAwait(false);

                Stream stream = new NetworkStream(connectedSocket, ownsSocket: true);

                // Upgrade to SSL if needed
                if (uri.Scheme == "wss")
                {
                    X509Certificate2Collection clientCertificates = new X509Certificate2Collection();
                    foreach (X509Certificate2 clientCertificate in options.ClientCertificates)
                    {
                        clientCertificates.Add(clientCertificate);
                    }

                    var sslStream = new SslStream(
                        innerStream: stream,
                        leaveInnerStreamOpen: false,
                        userCertificateValidationCallback: options.ServerCertificateCustomValidationCallback
                        );
                    await
                    sslStream.AuthenticateAsClientAsync(
                        uri.Host,
                        clientCertificates,
                        options.EnabledSslProtocols,
                        checkCertificateRevocation : false
                        )
                    .ConfigureAwait(false);

                    stream = sslStream;
                }

                // Create the security key and expected response, then build all of the request headers
                (string secKey, string webSocketAccept) = CreateSecKeyAndSecWebSocketAccept();
                byte[] requestHeader = BuildRequestHeader(uri, options, secKey);

                // Write out the header to the connection
                await stream.WriteAsync(requestHeader, 0, requestHeader.Length, cancellationToken).ConfigureAwait(false);

                // Parse the response and store our state for the remainder of the connection
                string subprotocol = await ParseAndValidateConnectResponseAsync(stream, options, webSocketAccept, cancellationToken).ConfigureAwait(false);

                return(WebSocket.CreateClientWebSocket(
                           stream,
                           subprotocol,
                           options.ReceiveBufferSize,
                           options.SendBufferSize,
                           options.KeepAliveInterval,
                           false,
                           WebSocket.CreateClientBuffer(options.ReceiveBufferSize, options.SendBufferSize)
                           ));
            }
            catch (Exception unexpectedError)
            {
                throw new WebSocketException("WebSocket connection failure.", unexpectedError);
            }
        }
Exemple #2
0
        /// <summary>Creates a byte[] containing the headers to send to the server.</summary>
        /// <param name="uri">The Uri of the server.</param>
        /// <param name="options">The options used to configure the websocket.</param>
        /// <param name="secKey">The generated security key to send in the Sec-WebSocket-Key header.</param>
        /// <returns>The byte[] containing the encoded headers ready to send to the network.</returns>
        private static byte[] BuildRequestHeader(Uri uri, KubernetesWebSocketOptions options, string secKey)
        {
            StringBuilder builder = new StringBuilder()
                                    .Append("GET ")
                                    .Append(uri.PathAndQuery)
                                    .Append(" HTTP/1.1\r\n");

            // Add all of the required headers, honoring Host header if set.
            string hostHeader;

            if (!options.RequestHeaders.TryGetValue(HttpKnownHeaderNames.Host, out hostHeader))
            {
                hostHeader = uri.Host;
            }

            builder.Append("Host: ");
            if (String.IsNullOrEmpty(hostHeader))
            {
                builder.Append(uri.IdnHost).Append(':').Append(uri.Port).Append("\r\n");
            }
            else
            {
                builder.Append(hostHeader).Append("\r\n");
            }

            builder.Append("Connection: Upgrade\r\n");
            builder.Append("Upgrade: websocket\r\n");
            builder.Append("Sec-WebSocket-Version: 13\r\n");
            builder.Append("Sec-WebSocket-Key: ").Append(secKey).Append("\r\n");

            // Add all of the additionally requested headers
            foreach (string key in options.RequestHeaders.Keys)
            {
                if (String.Equals(key, HttpKnownHeaderNames.Host, StringComparison.OrdinalIgnoreCase))
                {
                    // Host header handled above
                    continue;
                }

                builder.Append(key).Append(": ").Append(options.RequestHeaders[key]).Append("\r\n");
            }

            // Add the optional subprotocols header
            if (options.RequestedSubProtocols.Count > 0)
            {
                builder.Append(HttpKnownHeaderNames.SecWebSocketProtocol).Append(": ");
                builder.Append(options.RequestedSubProtocols[0]);
                for (int i = 1; i < options.RequestedSubProtocols.Count; i++)
                {
                    builder.Append(", ").Append(options.RequestedSubProtocols[i]);
                }
                builder.Append("\r\n");
            }

            // End the headers
            builder.Append("\r\n");

            // Return the bytes for the built up header
            return(Encoding.ASCII.GetBytes(builder.ToString()));
        }
Exemple #3
0
        /// <summary>Read and validate the connect response headers from the server.</summary>
        /// <param name="stream">The stream from which to read the response headers.</param>
        /// <param name="options">The options used to configure the websocket.</param>
        /// <param name="expectedSecWebSocketAccept">The expected value of the Sec-WebSocket-Accept header.</param>
        /// <param name="cancellationToken">The CancellationToken to use to cancel the websocket.</param>
        /// <returns>The agreed upon subprotocol with the server, or null if there was none.</returns>
        static async Task <string> ParseAndValidateConnectResponseAsync(Stream stream, KubernetesWebSocketOptions options, string expectedSecWebSocketAccept, CancellationToken cancellationToken)
        {
            // Read the first line of the response
            string statusLine = await ReadResponseHeaderLineAsync(stream, cancellationToken).ConfigureAwait(false);

            // Depending on the underlying sockets implementation and timing, connecting to a server that then
            // immediately closes the connection may either result in an exception getting thrown from the connect
            // earlier, or it may result in getting to here but reading 0 bytes.  If we read 0 bytes and thus have
            // an empty status line, treat it as a connect failure.
            if (String.IsNullOrEmpty(statusLine))
            {
                throw new WebSocketException("Connection failure.");
            }

            const string ExpectedStatusStart        = "HTTP/1.1 ";
            const string ExpectedStatusStatWithCode = "HTTP/1.1 101"; // 101 == SwitchingProtocols

            // If the status line doesn't begin with "HTTP/1.1" or isn't long enough to contain a status code, fail.
            if (!statusLine.StartsWith(ExpectedStatusStart, StringComparison.Ordinal) || statusLine.Length < ExpectedStatusStatWithCode.Length)
            {
                throw new WebSocketException(WebSocketError.HeaderError);
            }

            // If the status line doesn't contain a status code 101, or if it's long enough to have a status description
            // but doesn't contain whitespace after the 101, fail.
            if (!statusLine.StartsWith(ExpectedStatusStatWithCode, StringComparison.Ordinal) ||
                (statusLine.Length > ExpectedStatusStatWithCode.Length && !char.IsWhiteSpace(statusLine[ExpectedStatusStatWithCode.Length])))
            {
                throw new WebSocketException(WebSocketError.HeaderError, $"Connection failure (status line = '{statusLine}').");
            }

            // Read each response header. Be liberal in parsing the response header, treating
            // everything to the left of the colon as the key and everything to the right as the value, trimming both.
            // For each header, validate that we got the expected value.
            bool   foundUpgrade = false, foundConnection = false, foundSecWebSocketAccept = false;
            string subprotocol = null;
            string line;

            while (!String.IsNullOrEmpty(line = await ReadResponseHeaderLineAsync(stream, cancellationToken).ConfigureAwait(false)))
            {
                int colonIndex = line.IndexOf(':');
                if (colonIndex == -1)
                {
                    throw new WebSocketException(WebSocketError.HeaderError);
                }

                string headerName  = line.SubstringTrim(0, colonIndex);
                string headerValue = line.SubstringTrim(colonIndex + 1);

                // The Connection, Upgrade, and SecWebSocketAccept headers are required and with specific values.
                ValidateAndTrackHeader(HttpKnownHeaderNames.Connection, "Upgrade", headerName, headerValue, ref foundConnection);
                ValidateAndTrackHeader(HttpKnownHeaderNames.Upgrade, "websocket", headerName, headerValue, ref foundUpgrade);
                ValidateAndTrackHeader(HttpKnownHeaderNames.SecWebSocketAccept, expectedSecWebSocketAccept, headerName, headerValue, ref foundSecWebSocketAccept);

                // The SecWebSocketProtocol header is optional.  We should only get it with a non-empty value if we requested subprotocols,
                // 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
                // already got one in a previous header), fail. Otherwise, track which one we got.
                if (String.Equals(HttpKnownHeaderNames.SecWebSocketProtocol, headerName, StringComparison.OrdinalIgnoreCase) &&
                    !String.IsNullOrWhiteSpace(headerValue))
                {
                    if (options.RequestedSubProtocols.Count > 0)
                    {
                        string newSubprotocol = options.RequestedSubProtocols.Find(requested => String.Equals(requested, headerValue, StringComparison.OrdinalIgnoreCase));
                        if (newSubprotocol == null || subprotocol != null)
                        {
                            throw new WebSocketException(
                                      String.Format("Unsupported sub-protocol '{0}' (expected one of [{1}]).",
                                                    newSubprotocol,
                                                    String.Join(", ", options.RequestedSubProtocols)
                                                    )
                                      );
                        }
                        subprotocol = newSubprotocol;
                    }
                }
            }
            if (!foundUpgrade || !foundConnection || !foundSecWebSocketAccept)
            {
                throw new WebSocketException("Connection failure.");
            }

            return(subprotocol);
        }