/// <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); } }
/// <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())); }
/// <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); }