diff --git a/src/KubernetesClient/CoreFX.cs b/src/KubernetesClient/CoreFX.cs
deleted file mode 100644
index 828862f..0000000
--- a/src/KubernetesClient/CoreFX.cs
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
- * This (temporary) code has been adapted from Microsoft's .NET Core 2.0.4 codebase. Original code copyright (c) .NET Foundation and Contributors.
- * Hopefully, once .NET Core 2.1 lands, we can drop it in favour of the built-in ManagedWebSocket and SocketHttpHandler classes (providing they support custom validation of server certificates).
- *
- * Original code: https://github.com/dotnet/corefx/blob/v2.0.4/src/System.Net.WebSockets.Client/src/System/Net/WebSockets/WebSocketHandle.Managed.cs#L74
- * License: https://github.com/dotnet/corefx/blob/v2.0.4/LICENSE.TXT
- *
- */
-
-#if NETCOREAPP2_1
-
-using k8s;
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.IO;
-using System.Net;
-using System.Net.Http.Headers;
-using System.Net.Security;
-using System.Net.Sockets;
-using System.Net.WebSockets;
-using System.Runtime.ExceptionServices;
-using System.Security.Cryptography;
-using System.Security.Cryptography.X509Certificates;
-using System.Text;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace CoreFX
-{
- ///
- /// Connection factory for Kubernetes web sockets.
- ///
- internal static class K8sWebSocket
- {
- ///
- /// GUID appended by the server as part of the security key response.
- ///
- /// Defined in the RFC.
- ///
- const string WSServerGuid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
-
- ///
- /// Asynchronously connect to a Kubernetes WebSocket.
- ///
- ///
- /// The target URI.
- ///
- ///
- /// that control the WebSocket's configuration and connection process.
- ///
- ///
- /// An optional that can be used to cancel the operation.
- ///
- ///
- /// A representing the connection.
- ///
- public static async Task 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);
- }
- }
-
- /// Connects a socket to the specified host and port, subject to cancellation and aborting.
- /// The host to which to connect.
- /// The port to which to connect on the host.
- /// The CancellationToken to use to cancel the websocket.
- /// The connected Socket.
- private static async Task ConnectSocketAsync(string host, int port, CancellationToken cancellationToken)
- {
- IPAddress[] addresses = await Dns.GetHostAddressesAsync(host).ConfigureAwait(false);
-
- ExceptionDispatchInfo lastException = null;
- foreach (IPAddress address in addresses)
- {
- var socket = new Socket(address.AddressFamily, SocketType.Stream, ProtocolType.Tcp);
- try
- {
- using (cancellationToken.Register(() => socket.Dispose()))
- {
- try
- {
- await socket.ConnectAsync(address, port).ConfigureAwait(false);
- }
- catch (ObjectDisposedException objectDisposed)
- {
- // If the socket was disposed because cancellation was requested, translate the exception
- // into a new OperationCanceledException. Otherwise, let the original ObjectDisposedexception propagate.
- if (cancellationToken.IsCancellationRequested)
- {
- throw new OperationCanceledException(new OperationCanceledException().Message, objectDisposed, cancellationToken);
- }
- }
- }
- cancellationToken.ThrowIfCancellationRequested(); // in case of a race and socket was disposed after the await
-
- return socket;
- }
- catch (Exception exc)
- {
- socket.Dispose();
- lastException = ExceptionDispatchInfo.Capture(exc);
- }
- }
-
- lastException?.Throw();
-
- Debug.Fail("We should never get here. We should have already returned or an exception should have been thrown.");
- throw new WebSocketException("WebSocket connection failure.");
- }
-
- /// Creates a byte[] containing the headers to send to the server.
- /// The Uri of the server.
- /// The options used to configure the websocket.
- /// The generated security key to send in the Sec-WebSocket-Key header.
- /// The byte[] containing the encoded headers ready to send to the network.
- 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());
- }
-
- /// Read and validate the connect response headers from the server.
- /// The stream from which to read the response headers.
- /// The options used to configure the websocket.
- /// The expected value of the Sec-WebSocket-Accept header.
- /// The CancellationToken to use to cancel the websocket.
- /// The agreed upon subprotocol with the server, or null if there was none.
- static async Task 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;
- }
-
- /// Validates a received header against expected values and tracks that we've received it.
- /// The header name against which we're comparing.
- /// The header value against which we're comparing.
- /// The actual header name received.
- /// The actual header value received.
- /// A bool tracking whether this header has been seen.
- private static void ValidateAndTrackHeader(
- string targetHeaderName, string targetHeaderValue,
- string foundHeaderName, string foundHeaderValue,
- ref bool foundHeader)
- {
- bool isTargetHeader = String.Equals(targetHeaderName, foundHeaderName, StringComparison.OrdinalIgnoreCase);
- if (!foundHeader)
- {
- if (isTargetHeader)
- {
- if (!String.Equals(targetHeaderValue, foundHeaderValue, StringComparison.OrdinalIgnoreCase))
- {
- throw new WebSocketException(
- $"Invalid value for '{foundHeaderName}' header: '{foundHeaderValue}' (expected '{targetHeaderValue}')."
- );
- }
- foundHeader = true;
- }
- }
- else
- {
- if (isTargetHeader)
- {
- throw new WebSocketException("Connection failure.");
- }
- }
- }
-
- /// Reads a line from the stream.
- /// The stream from which to read.
- /// The CancellationToken used to cancel the websocket.
- /// The read line, or null if none could be read.
- private static async Task ReadResponseHeaderLineAsync(Stream stream, CancellationToken cancellationToken)
- {
- StringBuilder sb = new StringBuilder();
-
- var arr = new byte[1];
- char prevChar = '\0';
- try
- {
- // TODO: Reading one byte is extremely inefficient. The problem, however,
- // is that if we read multiple bytes, we could end up reading bytes post-headers
- // that are part of messages meant to be read by the managed websocket after
- // the connection. The likely solution here is to wrap the stream in a BufferedStream,
- // though a) that comes at the expense of an extra set of virtual calls, b)
- // it adds a buffer when the managed websocket will already be using a buffer, and
- // c) it's not exposed on the version of the System.IO contract we're currently using.
- while (await stream.ReadAsync(arr, 0, 1, cancellationToken).ConfigureAwait(false) == 1)
- {
- // Process the next char
- char curChar = (char)arr[0];
- if (prevChar == '\r' && curChar == '\n')
- {
- break;
- }
- sb.Append(curChar);
- prevChar = curChar;
- }
-
- if (sb.Length > 0 && sb[sb.Length - 1] == '\r')
- {
- sb.Length = sb.Length - 1;
- }
-
- return sb.ToString();
- }
- finally
- {
- sb.Clear();
- }
- }
-
- ///
- /// Create a security key for sending in the Sec-WebSocket-Key header and the associated response we expect to receive as the Sec-WebSocket-Accept header value.
- ///
- /// A key-value pair of the request header security key and expected response header value.
- [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA5350", Justification = "Required by RFC6455")]
- static (string secKey, string expectedResponse) CreateSecKeyAndSecWebSocketAccept()
- {
- string secKey = Convert.ToBase64String(Guid.NewGuid().ToByteArray());
- using (SHA1 sha = SHA1.Create())
- {
- return (
- secKey,
- Convert.ToBase64String(
- sha.ComputeHash(Encoding.ASCII.GetBytes(secKey + WSServerGuid))
- )
- );
- }
- }
-
- static void ValidateHeader(HttpHeaders headers, string name, string expectedValue)
- {
- if (!headers.TryGetValues(name, out IEnumerable values))
- ThrowConnectFailure();
-
- Debug.Assert(values is string[]);
- string[] array = (string[])values;
- if (array.Length != 1 || !String.Equals(array[0], expectedValue, StringComparison.OrdinalIgnoreCase))
- {
- throw new WebSocketException(
- $"Invalid WebSocker response header '{name}': [{String.Join(", ", array)}]"
- );
- }
- }
-
- static void ThrowConnectFailure() => throw new WebSocketException("Connection failure.");
- }
-
- ///
- /// Well-known HTTP header names from CoreFX used by .
- ///
- static class HttpKnownHeaderNames
- {
- public const string Accept = "Accept";
- public const string AcceptCharset = "Accept-Charset";
- public const string AcceptEncoding = "Accept-Encoding";
- public const string AcceptLanguage = "Accept-Language";
- public const string AcceptPatch = "Accept-Patch";
- public const string AcceptRanges = "Accept-Ranges";
- public const string AccessControlAllowCredentials = "Access-Control-Allow-Credentials";
- public const string AccessControlAllowHeaders = "Access-Control-Allow-Headers";
- public const string AccessControlAllowMethods = "Access-Control-Allow-Methods";
- public const string AccessControlAllowOrigin = "Access-Control-Allow-Origin";
- public const string AccessControlExposeHeaders = "Access-Control-Expose-Headers";
- public const string AccessControlMaxAge = "Access-Control-Max-Age";
- public const string Age = "Age";
- public const string Allow = "Allow";
- public const string AltSvc = "Alt-Svc";
- public const string Authorization = "Authorization";
- public const string CacheControl = "Cache-Control";
- public const string Connection = "Connection";
- public const string ContentDisposition = "Content-Disposition";
- public const string ContentEncoding = "Content-Encoding";
- public const string ContentLanguage = "Content-Language";
- public const string ContentLength = "Content-Length";
- public const string ContentLocation = "Content-Location";
- public const string ContentMD5 = "Content-MD5";
- public const string ContentRange = "Content-Range";
- public const string ContentSecurityPolicy = "Content-Security-Policy";
- public const string ContentType = "Content-Type";
- public const string Cookie = "Cookie";
- public const string Cookie2 = "Cookie2";
- public const string Date = "Date";
- public const string ETag = "ETag";
- public const string Expect = "Expect";
- public const string Expires = "Expires";
- public const string From = "From";
- public const string Host = "Host";
- public const string IfMatch = "If-Match";
- public const string IfModifiedSince = "If-Modified-Since";
- public const string IfNoneMatch = "If-None-Match";
- public const string IfRange = "If-Range";
- public const string IfUnmodifiedSince = "If-Unmodified-Since";
- public const string KeepAlive = "Keep-Alive";
- public const string LastModified = "Last-Modified";
- public const string Link = "Link";
- public const string Location = "Location";
- public const string MaxForwards = "Max-Forwards";
- public const string Origin = "Origin";
- public const string P3P = "P3P";
- public const string Pragma = "Pragma";
- public const string ProxyAuthenticate = "Proxy-Authenticate";
- public const string ProxyAuthorization = "Proxy-Authorization";
- public const string ProxyConnection = "Proxy-Connection";
- public const string PublicKeyPins = "Public-Key-Pins";
- public const string Range = "Range";
- public const string Referer = "Referer"; // NB: The spelling-mistake "Referer" for "Referrer" must be matched.
- public const string RetryAfter = "Retry-After";
- public const string SecWebSocketAccept = "Sec-WebSocket-Accept";
- public const string SecWebSocketExtensions = "Sec-WebSocket-Extensions";
- public const string SecWebSocketKey = "Sec-WebSocket-Key";
- public const string SecWebSocketProtocol = "Sec-WebSocket-Protocol";
- public const string SecWebSocketVersion = "Sec-WebSocket-Version";
- public const string Server = "Server";
- public const string SetCookie = "Set-Cookie";
- public const string SetCookie2 = "Set-Cookie2";
- public const string StrictTransportSecurity = "Strict-Transport-Security";
- public const string TE = "TE";
- public const string TSV = "TSV";
- public const string Trailer = "Trailer";
- public const string TransferEncoding = "Transfer-Encoding";
- public const string Upgrade = "Upgrade";
- public const string UpgradeInsecureRequests = "Upgrade-Insecure-Requests";
- public const string UserAgent = "User-Agent";
- public const string Vary = "Vary";
- public const string Via = "Via";
- public const string WWWAuthenticate = "WWW-Authenticate";
- public const string Warning = "Warning";
- public const string XAspNetVersion = "X-AspNet-Version";
- public const string XContentDuration = "X-Content-Duration";
- public const string XContentTypeOptions = "X-Content-Type-Options";
- public const string XFrameOptions = "X-Frame-Options";
- public const string XMSEdgeRef = "X-MSEdge-Ref";
- public const string XPoweredBy = "X-Powered-By";
- public const string XRequestID = "X-Request-ID";
- public const string XUACompatible = "X-UA-Compatible";
- }
-
- ///
- /// Extension methods for s from the CoreFX codebase (used by ).
- ///
- static class CoreFXStringExtensions
- {
- public static string SubstringTrim(this string value, int startIndex)
- {
- return SubstringTrim(value, startIndex, value.Length - startIndex);
- }
-
- public static string SubstringTrim(this string value, int startIndex, int length)
- {
- Debug.Assert(value != null, "string must be non-null");
- Debug.Assert(startIndex >= 0, "startIndex must be non-negative");
- Debug.Assert(length >= 0, "length must be non-negative");
- Debug.Assert(startIndex <= value.Length - length, "startIndex + length must be <= value.Length");
-
- if (length == 0)
- {
- return String.Empty;
- }
-
- int endIndex = startIndex + length - 1;
-
- while (startIndex <= endIndex && char.IsWhiteSpace(value[startIndex]))
- {
- startIndex++;
- }
-
- while (endIndex >= startIndex && char.IsWhiteSpace(value[endIndex]))
- {
- endIndex--;
- }
-
- int newLength = endIndex - startIndex + 1;
- Debug.Assert(newLength >= 0 && newLength <= value.Length, "Expected resulting length to be within value's length");
-
- return
- newLength == 0 ? String.Empty :
- newLength == value.Length ? value :
- value.Substring(startIndex, newLength);
- }
- }
-}
-
-#endif // NETCOREAPP2_1
diff --git a/src/KubernetesClient/Kubernetes.WebSocket.cs b/src/KubernetesClient/Kubernetes.WebSocket.cs
index f27f857..7b24676 100644
--- a/src/KubernetesClient/Kubernetes.WebSocket.cs
+++ b/src/KubernetesClient/Kubernetes.WebSocket.cs
@@ -269,7 +269,7 @@ namespace k8s
if (webSocketSubProtocol != null)
{
- webSocketBuilder.Options.RequestedSubProtocols.Add(webSocketSubProtocol);
+ webSocketBuilder.Options.AddSubProtocol(webSocketSubProtocol);
}
#endif // NETCOREAPP2_1
diff --git a/src/KubernetesClient/WebSocketBuilder.NetCoreApp2.1.cs b/src/KubernetesClient/WebSocketBuilder.NetCoreApp2.1.cs
deleted file mode 100644
index fceaffe..0000000
--- a/src/KubernetesClient/WebSocketBuilder.NetCoreApp2.1.cs
+++ /dev/null
@@ -1,124 +0,0 @@
-#if NETCOREAPP2_1
-
-using System;
-using System.Collections.Generic;
-using System.Diagnostics;
-using System.Net.Security;
-using System.Net.WebSockets;
-using System.Security.Authentication;
-using System.Security.Cryptography.X509Certificates;
-using System.Threading;
-using System.Threading.Tasks;
-
-namespace k8s
-{
- ///
- /// The creates a new object which connects to a remote WebSocket.
- ///
- public sealed class WebSocketBuilder
- {
- public KubernetesWebSocketOptions Options { get; } = new KubernetesWebSocketOptions();
-
- public WebSocketBuilder()
- {
- }
-
- public WebSocketBuilder SetRequestHeader(string headerName, string headerValue)
- {
- Options.RequestHeaders[headerName] = headerValue;
-
- return this;
- }
-
- public WebSocketBuilder AddClientCertificate(X509Certificate2 certificate)
- {
- Options.ClientCertificates.Add(certificate);
-
- return this;
- }
-
- public WebSocketBuilder ExpectServerCertificate(X509Certificate2 serverCertificate)
- {
- Options.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
- {
- return Kubernetes.CertificateValidationCallBack(sender, serverCertificate, certificate, chain, sslPolicyErrors);
- };
- return this;
- }
-
- public WebSocketBuilder SkipServerCertificateValidation()
- {
- Options.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
-
- return this;
- }
-
- public async Task BuildAndConnectAsync(Uri uri, CancellationToken cancellationToken)
- {
- return await CoreFX.K8sWebSocket.ConnectAsync(uri, Options, cancellationToken).ConfigureAwait(false);
- }
- }
-
- ///
- /// Options for connecting to Kubernetes web sockets.
- ///
- public class KubernetesWebSocketOptions
- {
- ///
- /// The default size (in bytes) for WebSocket send / receive buffers.
- ///
- public static readonly int DefaultBufferSize = 2048;
-
- ///
- /// Create new .
- ///
- public KubernetesWebSocketOptions()
- {
- }
-
- ///
- /// The requested size (in bytes) of the WebSocket send buffer.
- ///
- public int SendBufferSize { get; set; } = 2048;
-
- ///
- /// The requested size (in bytes) of the WebSocket receive buffer.
- ///
- public int ReceiveBufferSize { get; set; } = 2048;
-
- ///
- /// Custom request headers (if any).
- ///
- public Dictionary RequestHeaders { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase);
-
- ///
- /// Requested sub-protocols (if any).
- ///
- public List RequestedSubProtocols { get; } = new List();
-
- ///
- /// Client certificates (if any) to use for authentication.
- ///
- public List ClientCertificates = new List();
-
- ///
- /// An optional delegate to use for authenticating the remote server certificate.
- ///
- public RemoteCertificateValidationCallback ServerCertificateCustomValidationCallback { get; set; }
-
- ///
- /// An value representing the SSL protocols that the client supports.
- ///
- ///
- /// Defaults to , which lets the platform select the most appropriate protocol.
- ///
- public SslProtocols EnabledSslProtocols { get; set; } = SslProtocols.None;
-
- ///
- /// The WebSocket keep-alive interval.
- ///
- public TimeSpan KeepAliveInterval { get; set; } = TimeSpan.FromSeconds(5);
- }
-}
-
-#endif // NETCOREAPP2_1
diff --git a/src/KubernetesClient/WebSocketBuilder.cs b/src/KubernetesClient/WebSocketBuilder.cs
index b618369..94d8d71 100644
--- a/src/KubernetesClient/WebSocketBuilder.cs
+++ b/src/KubernetesClient/WebSocketBuilder.cs
@@ -1,5 +1,3 @@
-#if !NETCOREAPP2_1
-
using System;
using System.Net.WebSockets;
using System.Security.Cryptography.X509Certificates;
@@ -23,6 +21,8 @@ namespace k8s
{
}
+ public ClientWebSocketOptions Options => WebSocket.Options;
+
public virtual WebSocketBuilder SetRequestHeader(string headerName, string headerValue)
{
this.WebSocket.Options.SetRequestHeader(headerName, headerValue);
@@ -35,6 +35,27 @@ namespace k8s
return this;
}
+#if NETCOREAPP2_1
+
+ public WebSocketBuilder ExpectServerCertificate(X509Certificate2 serverCertificate)
+ {
+ Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) =>
+ {
+ return Kubernetes.CertificateValidationCallBack(sender, serverCertificate, certificate, chain, sslPolicyErrors);
+ };
+
+ return this;
+ }
+
+ public WebSocketBuilder SkipServerCertificateValidation()
+ {
+ Options.RemoteCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true;
+
+ return this;
+ }
+
+#endif // NETCOREAPP2_1
+
public virtual async Task BuildAndConnectAsync(Uri uri, CancellationToken cancellationToken)
{
await this.WebSocket.ConnectAsync(uri, cancellationToken).ConfigureAwait(false);
@@ -42,5 +63,3 @@ namespace k8s
}
}
}
-
-#endif // !NETCOREAPP2_1
diff --git a/tests/KubernetesClient.Tests/AuthTests.cs b/tests/KubernetesClient.Tests/AuthTests.cs
index ee4b865..5543692 100644
--- a/tests/KubernetesClient.Tests/AuthTests.cs
+++ b/tests/KubernetesClient.Tests/AuthTests.cs
@@ -3,7 +3,7 @@ using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Headers;
-using System.Security.Cryptography;
+using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks;
@@ -12,21 +12,21 @@ using k8s.Tests.Mock;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Rest;
-using Org.BouncyCastle.Crypto.Parameters;
-using Org.BouncyCastle.Pkcs;
-using Org.BouncyCastle.Security;
+using Org.BouncyCastle.Crypto.Parameters;
+using Org.BouncyCastle.Pkcs;
+using Org.BouncyCastle.Security;
using Xunit;
using Xunit.Abstractions;
namespace k8s.Tests
{
public class AuthTests
- {
- private readonly ITestOutputHelper testOutput;
-
- public AuthTests(ITestOutputHelper testOutput)
- {
- this.testOutput = testOutput;
+ {
+ private readonly ITestOutputHelper testOutput;
+
+ public AuthTests(ITestOutputHelper testOutput)
+ {
+ this.testOutput = testOutput;
}
private static HttpOperationResponse ExecuteListPods(IKubernetes client)
@@ -164,8 +164,10 @@ namespace k8s.Tests
Assert.Equal(HttpStatusCode.Unauthorized, listTask.Response.StatusCode);
}
}
- }
-
+ }
+
+#if NETCOREAPP2_1 // The functionality under test, here, is dependent on managed HTTP / WebSocket functionality in .NET Core 2.1 or newer.
+
[Fact]
public void Cert()
{
@@ -173,12 +175,12 @@ namespace k8s.Tests
var clientCertificateKeyData = File.ReadAllText("assets/client-key-data.txt");
var clientCertificateData = File.ReadAllText("assets/client-certificate-data.txt");
-
- X509Certificate2 serverCertificate = null;
- using (MemoryStream serverCertificateStream = new MemoryStream(Convert.FromBase64String(serverCertificateData)))
- {
- serverCertificate = OpenCertificateStore(serverCertificateStream);
- }
+
+ X509Certificate2 serverCertificate = null;
+ using (MemoryStream serverCertificateStream = new MemoryStream(Convert.FromBase64String(serverCertificateData)))
+ {
+ serverCertificate = OpenCertificateStore(serverCertificateStream);
+ }
var clientCertificate = new X509Certificate2(Convert.FromBase64String(clientCertificateData), "");
@@ -259,7 +261,9 @@ namespace k8s.Tests
Assert.False(clientCertificateValidationCalled);
}
}
- }
+ }
+
+#endif // NETCOREAPP2_1
[Fact]
public void Token()
@@ -330,27 +334,27 @@ namespace k8s.Tests
Assert.Equal(HttpStatusCode.Unauthorized, listTask.Response.StatusCode);
}
}
- }
-
- private X509Certificate2 OpenCertificateStore(Stream stream)
- {
- Pkcs12Store store = new Pkcs12Store();
- store.Load(stream, new char[] { });
-
- var keyAlias = store.Aliases.Cast().SingleOrDefault(a => store.IsKeyEntry(a));
-
- var key = (RsaPrivateCrtKeyParameters)store.GetKey(keyAlias).Key;
- var bouncyCertificate = store.GetCertificate(keyAlias).Certificate;
-
- var certificate = new X509Certificate2(DotNetUtilities.ToX509Certificate(bouncyCertificate));
- var parameters = DotNetUtilities.ToRSAParameters(key);
-
- RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
- rsa.ImportParameters(parameters);
-
- certificate = RSACertificateExtensions.CopyWithPrivateKey(certificate, rsa);
-
- return certificate;
+ }
+
+ private X509Certificate2 OpenCertificateStore(Stream stream)
+ {
+ Pkcs12Store store = new Pkcs12Store();
+ store.Load(stream, new char[] { });
+
+ var keyAlias = store.Aliases.Cast().SingleOrDefault(a => store.IsKeyEntry(a));
+
+ var key = (RsaPrivateCrtKeyParameters)store.GetKey(keyAlias).Key;
+ var bouncyCertificate = store.GetCertificate(keyAlias).Certificate;
+
+ var certificate = new X509Certificate2(DotNetUtilities.ToX509Certificate(bouncyCertificate));
+ var parameters = DotNetUtilities.ToRSAParameters(key);
+
+ RSACryptoServiceProvider rsa = new RSACryptoServiceProvider();
+ rsa.ImportParameters(parameters);
+
+ certificate = RSACertificateExtensions.CopyWithPrivateKey(certificate, rsa);
+
+ return certificate;
}
}
}
diff --git a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
index d8a1ad2..0d5f23b 100755
--- a/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
+++ b/tests/KubernetesClient.Tests/KubernetesClient.Tests.csproj
@@ -2,7 +2,7 @@
false
k8s.tests
- netcoreapp2.0;netcoreapp2.1
+ netcoreapp2.1;netcoreapp2.0