using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Net; using System.Net.Http; using System.Net.Security; using System.Security.Cryptography.X509Certificates; using k8s.Exceptions; using k8s.Models; using Microsoft.Rest; namespace k8s { public partial class Kubernetes { /// /// Initializes a new instance of the class. /// /// /// The kube config to use. /// /// /// The to use for all requests. /// public Kubernetes(KubernetesClientConfiguration config, HttpClient httpClient) : this(config, httpClient, false) { } /// /// Initializes a new instance of the class. /// /// /// The kube config to use. /// /// /// The to use for all requests. /// /// /// Whether or not the object should own the lifetime of . /// public Kubernetes(KubernetesClientConfiguration config, HttpClient httpClient, bool disposeHttpClient) : this(httpClient, disposeHttpClient) { ValidateConfig(config); CaCerts = config.SslCaCerts; SkipTlsVerify = config.SkipTlsVerify; SetCredentials(config); } /// /// Initializes a new instance of the class. /// /// /// The kube config to use. /// /// /// Optional. The delegating handlers to add to the http client pipeline. /// public Kubernetes(KubernetesClientConfiguration config, params DelegatingHandler[] handlers) { Initialize(); ValidateConfig(config); CreateHttpClient(handlers); CaCerts = config.SslCaCerts; SkipTlsVerify = config.SkipTlsVerify; InitializeFromConfig(config); } private void ValidateConfig(KubernetesClientConfiguration config) { if (config == null) { throw new KubeConfigException("KubeConfig must be provided"); } if (string.IsNullOrWhiteSpace(config.Host)) { throw new KubeConfigException("Host url must be set"); } try { BaseUri = new Uri(config.Host); } catch (UriFormatException e) { throw new KubeConfigException("Bad host url", e); } } private void InitializeFromConfig(KubernetesClientConfiguration config) { if (BaseUri.Scheme == "https") { if (config.SkipTlsVerify) { #if NET452 ((WebRequestHandler) HttpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; #elif XAMARINIOS1_0 || MONOANDROID8_1 System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { return true; }; #else HttpClientHandler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => true; #endif } else { if (CaCerts == null) { throw new KubeConfigException("A CA must be set when SkipTlsVerify === false"); } #if NET452 ((WebRequestHandler) HttpClientHandler).ServerCertificateValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors); }; #elif XAMARINIOS1_0 System.Net.ServicePointManager.ServerCertificateValidationCallback += (sender, certificate, chain, sslPolicyErrors) => { var cert = new X509Certificate2(certificate); return Kubernetes.CertificateValidationCallBack(sender, CaCerts, cert, chain, sslPolicyErrors); }; #elif MONOANDROID8_1 var certList = new System.Collections.Generic.List(); foreach (X509Certificate2 caCert in CaCerts) { using (var certStream = new System.IO.MemoryStream(caCert.RawData)) { Java.Security.Cert.Certificate cert = Java.Security.Cert.CertificateFactory.GetInstance("X509").GenerateCertificate(certStream); certList.Add(cert); } } var handler = (Xamarin.Android.Net.AndroidClientHandler)this.HttpClientHandler; handler.TrustedCerts = certList; #else HttpClientHandler.ServerCertificateCustomValidationCallback = (sender, certificate, chain, sslPolicyErrors) => { return Kubernetes.CertificateValidationCallBack(sender, CaCerts, certificate, chain, sslPolicyErrors); }; #endif } } // set credentails for the kubernetes client SetCredentials(config); config.AddCertificates(HttpClientHandler); } private X509Certificate2Collection CaCerts { get; } private bool SkipTlsVerify { get; } partial void CustomInitialize() { #if NET452 ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls12; #endif DeserializationSettings.Converters.Add(new V1Status.V1StatusObjectViewConverter()); } /// A that simply forwards a request with no further processing. private sealed class ForwardingHandler : DelegatingHandler { public ForwardingHandler(HttpMessageHandler handler) : base(handler) { } } private void AppendDelegatingHandler() where T : DelegatingHandler, new() { var cur = FirstMessageHandler as DelegatingHandler; while (cur != null) { var next = cur.InnerHandler as DelegatingHandler; if (next == null) { // last one // append watcher handler between to last handler cur.InnerHandler = new T { InnerHandler = cur.InnerHandler }; break; } cur = next; } } // NOTE: this method replicates the logic that the base ServiceClient uses except that it doesn't insert the RetryDelegatingHandler // and it does insert the WatcherDelegatingHandler. we don't want the RetryDelegatingHandler because it has a very broad definition // of what requests have failed. it considers everything outside 2xx to be failed, including 1xx (e.g. 101 Switching Protocols) and // 3xx. in particular, this prevents upgraded connections and certain generic/custom requests from working. private void CreateHttpClient(DelegatingHandler[] handlers) { FirstMessageHandler = HttpClientHandler = CreateRootHandler(); if (handlers == null || handlers.Length == 0) { // ensure we have at least one DelegatingHandler so AppendDelegatingHandler will work FirstMessageHandler = new ForwardingHandler(HttpClientHandler); } else { for (int i = handlers.Length - 1; i >= 0; i--) { DelegatingHandler handler = handlers[i]; while (handler.InnerHandler is DelegatingHandler d) handler = d; handler.InnerHandler = FirstMessageHandler; FirstMessageHandler = handlers[i]; } } AppendDelegatingHandler(); HttpClient = new HttpClient(FirstMessageHandler, false); } /// /// Set credentials for the Client /// /// k8s client configuration private void SetCredentials(KubernetesClientConfiguration config) { // set the Credentails for token based auth if (!string.IsNullOrWhiteSpace(config.AccessToken)) { Credentials = new TokenCredentials(config.AccessToken); } else if (!string.IsNullOrWhiteSpace(config.Username) && !string.IsNullOrWhiteSpace(config.Password)) { Credentials = new BasicAuthenticationCredentials { UserName = config.Username, Password = config.Password }; } } /// /// SSl Cert Validation Callback /// /// sender /// client certificate /// chain /// ssl policy errors /// true if valid cert [SuppressMessage("Microsoft.Usage", "CA1801:ReviewUnusedParameters", Justification = "Unused by design")] public static bool CertificateValidationCallBack( object sender, X509Certificate2Collection caCerts, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors) { // If the certificate is a valid, signed certificate, return true. if (sslPolicyErrors == SslPolicyErrors.None) { return true; } // If there are errors in the certificate chain, look at each error to determine the cause. if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0) { chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // Added our trusted certificates to the chain // chain.ChainPolicy.ExtraStore.AddRange(caCerts); chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority; var isValid = chain.Build((X509Certificate2)certificate); var isTrusted = false; var rootCert = chain.ChainElements[chain.ChainElements.Count - 1].Certificate; // Make sure that one of our trusted certs exists in the chain provided by the server. // foreach (var cert in caCerts) { if (rootCert.RawData.SequenceEqual(cert.RawData)) { isTrusted = true; break; } } return isValid && isTrusted; } // In all other cases, return false. return false; } } }