private bool ValidateServerCert(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
        {
            if (certificate is X509Certificate2 c2)
            {
                PeerCertificateReceived?.Invoke(this, new TlsCertificateReceivedEventArgs(c2));
            }

            if (_options.PinnedServerCertificate != null)
            {
                var retVal = certificate.Equals(_options.PinnedServerCertificate);
                if (!retVal)
                {
                    WriteLog.To.Sync.W(Tag, "Server certificate did not match the pinned one!");
                    _validationException = new TlsCertificateException("The certificate does not match the pinned certificate",
                                                                       C4NetworkErrorCode.TLSCertUnknownRoot, X509ChainStatusFlags.ExplicitDistrust);
                }

                return(retVal);
            }

            // Mono doesn't pass chain information?
            if (chain?.ChainElements?.Count == 0 && certificate is X509Certificate2 cert2)
            {
                chain = X509Chain.Create();
                chain.Build(cert2);
            }

#if COUCHBASE_ENTERPRISE
            var onlySelfSigned = _options.AcceptOnlySelfSignedServerCertificate;
            #else
            var onlySelfSigned = false;
            #endif

            if (!onlySelfSigned && sslPolicyErrors != SslPolicyErrors.None)
            {
                WriteLog.To.Sync.W(Tag, $"Error validating TLS chain: {sslPolicyErrors}");
                if (chain.ChainElements != null)
                {
                    foreach (var element in chain.ChainElements)
                    {
                        if (element.ChainElementStatus != null)
                        {
                            foreach (var status in element.ChainElementStatus)
                            {
                                if (status.Status != X509ChainStatusFlags.NoError)
                                {
                                    WriteLog.To.Sync.V(Tag,
                                                       $"Error {status.Status} ({status.StatusInformation}) for certificate:{Environment.NewLine}{element.Certificate}");
                                    if (status.Status == X509ChainStatusFlags.UntrustedRoot)
                                    {
                                        _validationException = new TlsCertificateException("The certificate does not terminate in a trusted root CA.",
                                                                                           C4NetworkErrorCode.TLSCertUnknownRoot, X509ChainStatusFlags.UntrustedRoot);
                                        return(false);
                                    }
                                }
                            }
                        }
                    }
                }

                if (chain.ChainStatus != null)
                {
                    foreach (var status in chain.ChainStatus)
                    {
                        if (status.Status == X509ChainStatusFlags.PartialChain)
                        {
                            _validationException = new TlsCertificateException("The certificate does not terminate in a trusted root CA.",
                                                                               C4NetworkErrorCode.TLSCertUnknownRoot, X509ChainStatusFlags.PartialChain);
                            return(false);
                        }
                    }
                }
            }
            else if (onlySelfSigned)
            {
                if (chain.ChainElements.Count != 1)
                {
                    _validationException = new TlsCertificateException("A non self-signed certificate was received in self-signed mode.",
                                                                       C4NetworkErrorCode.TLSCertUnknownRoot, X509ChainStatusFlags.ExplicitDistrust);
                    return(false);
                }

                if (chain.ChainStatus[0].Status != X509ChainStatusFlags.UntrustedRoot &&
                    chain.ChainStatus[0].Status != X509ChainStatusFlags.PartialChain &&
                    chain.ChainStatus[0].Status != X509ChainStatusFlags.NoError)
                {
                    _validationException = new TlsCertificateException("Certificate verification failed",
                                                                       C4NetworkErrorCode.TLSCertUnknownRoot, chain.ChainStatus[0].Status);
                    return(false);
                }

                if (chain.ChainElements[0].Certificate.IssuerName.Name
                    != chain.ChainElements[0].Certificate.SubjectName.Name)
                {
                    _validationException = new TlsCertificateException("A non self-signed certificate was received in self-signed mode.",
                                                                       C4NetworkErrorCode.TLSCertUnknownRoot, X509ChainStatusFlags.ExplicitDistrust);
                    return(false);
                }

                return(true);
            }

            if (sslPolicyErrors == SslPolicyErrors.None)
            {
                return(true);
            }

            _validationException = new TlsCertificateException("Certificate verification failed",
                                                               C4NetworkErrorCode.TLSCertUntrusted, sslPolicyErrors);
            return(false);
        }
        private void StartInternal()
        {
            // STEP 3: Create the WebSocket Upgrade HTTP request
            WriteLog.To.Sync.I(Tag, $"WebSocket connecting to {_logic.UrlRequest?.Host}:{_logic.UrlRequest?.Port}");
            var rng        = RandomNumberGenerator.Create() ?? throw new RuntimeException("Failed to create RandomNumberGenerator");
            var nonceBytes = new byte[16];

            rng.GetBytes(nonceBytes);
            var nonceKey = Convert.ToBase64String(nonceBytes);

            _expectedAcceptHeader = Base64Digest(String.Concat(nonceKey, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"));

            foreach (var header in _options.Headers)
            {
                _logic[header.Key] = header.Value;
            }

            var cookieString = _options.CookieString;

            if (cookieString != null)
            {
                // https://github.com/couchbase/couchbase-lite-net/issues/974
                // Don't overwrite a possible entry in the above headers unless there is
                // actually a value
                _logic["Cookie"] = cookieString;
            }

            // These ones should be overwritten.  The user has no business setting them.
            _logic["Connection"]            = "Upgrade";
            _logic["Upgrade"]               = "websocket";
            _logic["Sec-WebSocket-Version"] = "13";
            _logic["Sec-WebSocket-Key"]     = nonceKey;
            var protocols = _options.Protocols;

            if (protocols != null)
            {
                _logic["Sec-WebSocket-Protocol"] = protocols;
            }

            if (_logic.UseTls)
            {
                var baseStream = _client?.GetStream();
                if (baseStream == null)
                {
                    WriteLog.To.Sync.W(Tag, "Failed to get network stream (already closed?).  Aborting start...");
                    DidClose(C4WebSocketCloseCode.WebSocketCloseAbnormal, "Unexpected error in client logic");
                    return;
                }

                var stream = new SslStream(baseStream, false, ValidateServerCert);
                X509CertificateCollection clientCerts = null;
                if (_options.ClientCert != null)
                {
                    clientCerts = new X509CertificateCollection(new[] { _options.ClientCert as X509Certificate });
                }

                try {
                    // STEP 3A: TLS handshake
                    stream.AuthenticateAsClientAsync(_logic.UrlRequest?.Host, clientCerts, SslProtocols.Tls12, false)
                    .ContinueWith(
                        t =>
                    {
                        if (_validationException != null)
                        {
                            DidClose(_validationException);
                            _validationException = null;
                            return;
                        }

                        if (t.Exception?.InnerException != null)
                        {
                            // Failed before the server validation step, probably client certificate error
                            DidClose(new TlsCertificateException(
                                         "Authentication failed prior to server certificate (bad / missing client cert?)",
                                         C4NetworkErrorCode.TLSHandshakeFailed, t.Exception.InnerException));
                            return;
                        }

                        if (!NetworkTaskSuccessful(t))
                        {
                            return;
                        }

                        _queue.DispatchAsync(OnSocketReady);
                    });
                } catch (Exception e) {
                    // I can't believe I have to do this, but .NET Core will throw the exception
                    // here on Linux instead of passing it to the continuation
                    var toThrow = _validationException ?? new TlsCertificateException(
                        "Authentication failed prior to server certificate (bad / missing client cert?)",
                        C4NetworkErrorCode.TLSHandshakeFailed, e);
                    DidClose(toThrow);
                    _validationException = null;
                    return;
                }

                NetworkStream = stream;
            }
            else
            {
                NetworkStream = _client?.GetStream();
                OnSocketReady();
            }
        }