/*
 *  IXSocketOpenSSL.cpp
 *  Author: Benjamin Sergeant, Matt DeBoer, Max Weisel
 *  Copyright (c) 2017-2020 Machine Zone, Inc. All rights reserved.
 *
 *  Adapted from Satori SDK OpenSSL code.
 */
#ifdef IXWEBSOCKET_USE_OPEN_SSL

#include "IXSocketOpenSSL.h"

#include "IXSocketConnect.h"
#include "IXUniquePtr.h"
#include <cassert>
#include <errno.h>
#include <vector>
#ifdef _WIN32
#include <shlwapi.h>
#else
#include <fnmatch.h>
#endif
#if OPENSSL_VERSION_NUMBER < 0x10100000L
#include <openssl/x509v3.h>
#endif
#define socketerrno errno

#ifdef _WIN32
// For manipulating the certificate store
#include <windows.h>
#include <wincrypt.h>
#endif

#ifdef _WIN32
namespace
{
    bool loadWindowsSystemCertificates(SSL_CTX* ssl, std::string& errorMsg)
    {
        DWORD flags = CERT_STORE_READONLY_FLAG | CERT_STORE_OPEN_EXISTING_FLAG |
                      CERT_SYSTEM_STORE_CURRENT_USER;
        HCERTSTORE systemStore = CertOpenStore(CERT_STORE_PROV_SYSTEM, 0, 0, flags, L"Root");

        if (!systemStore)
        {
            errorMsg = "CertOpenStore failed with ";
            errorMsg += std::to_string(GetLastError());
            return false;
        }

        PCCERT_CONTEXT certificateIterator = NULL;
        X509_STORE* opensslStore = SSL_CTX_get_cert_store(ssl);

        int certificateCount = 0;
        while ((certificateIterator = CertEnumCertificatesInStore(systemStore, certificateIterator)))
        {
            X509* x509 = d2i_X509(NULL,
                                  (const unsigned char**) &certificateIterator->pbCertEncoded,
                                  certificateIterator->cbCertEncoded);

            if (x509)
            {
                if (X509_STORE_add_cert(opensslStore, x509) == 1)
                {
                    ++certificateCount;
                }

                X509_free(x509);
            }
        }

        CertFreeCertificateContext(certificateIterator);
        CertCloseStore(systemStore, 0);

        if (certificateCount == 0)
        {
            errorMsg = "No certificates found";
            return false;
        }

        return true;
    }
} // namespace
#endif

namespace ix
{
    const std::string kDefaultCiphers =
        "ECDHE-ECDSA-AES128-GCM-SHA256 ECDHE-ECDSA-AES256-GCM-SHA384 ECDHE-ECDSA-AES128-SHA "
        "ECDHE-ECDSA-AES256-SHA ECDHE-ECDSA-AES128-SHA256 ECDHE-ECDSA-AES256-SHA384 "
        "ECDHE-RSA-AES128-GCM-SHA256 ECDHE-RSA-AES256-GCM-SHA384 ECDHE-RSA-AES128-SHA "
        "ECDHE-RSA-AES256-SHA ECDHE-RSA-AES128-SHA256 ECDHE-RSA-AES256-SHA384 "
        "DHE-RSA-AES128-GCM-SHA256 DHE-RSA-AES256-GCM-SHA384 DHE-RSA-AES128-SHA "
        "DHE-RSA-AES256-SHA DHE-RSA-AES128-SHA256 DHE-RSA-AES256-SHA256 AES128-SHA";

    std::atomic<bool> SocketOpenSSL::_openSSLInitializationSuccessful(false);
    std::once_flag SocketOpenSSL::_openSSLInitFlag;
    std::vector<std::unique_ptr<std::mutex>> openSSLMutexes;

    SocketOpenSSL::SocketOpenSSL(const SocketTLSOptions& tlsOptions, int fd)
        : Socket(fd)
        , _ssl_connection(nullptr)
        , _ssl_context(nullptr)
        , _tlsOptions(tlsOptions)
    {
        std::call_once(_openSSLInitFlag, &SocketOpenSSL::openSSLInitialize, this);
    }

    SocketOpenSSL::~SocketOpenSSL()
    {
        SocketOpenSSL::close();
    }

    void SocketOpenSSL::openSSLInitialize()
    {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
        if (!OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, nullptr)) return;
#else
        (void) OPENSSL_config(nullptr);

        if (CRYPTO_get_locking_callback() == nullptr)
        {
            openSSLMutexes.clear();
            for (int i = 0; i < CRYPTO_num_locks(); ++i)
            {
                openSSLMutexes.push_back(ix::make_unique<std::mutex>());
            }
            CRYPTO_set_locking_callback(SocketOpenSSL::openSSLLockingCallback);
        }
#endif

        (void) OpenSSL_add_ssl_algorithms();
        (void) SSL_load_error_strings();

        _openSSLInitializationSuccessful = true;
    }

    void SocketOpenSSL::openSSLLockingCallback(int mode,
                                               int type,
                                               const char* /*file*/,
                                               int /*line*/)
    {
        if (mode & CRYPTO_LOCK)
        {
            openSSLMutexes[type]->lock();
        }
        else
        {
            openSSLMutexes[type]->unlock();
        }
    }

    std::string SocketOpenSSL::getSSLError(int ret)
    {
        unsigned long e;

        int err = SSL_get_error(_ssl_connection, ret);

        if (err == SSL_ERROR_WANT_CONNECT || err == SSL_ERROR_WANT_ACCEPT)
        {
            return "OpenSSL failed - connection failure";
        }
        else if (err == SSL_ERROR_WANT_X509_LOOKUP)
        {
            return "OpenSSL failed - x509 error";
        }
        else if (err == SSL_ERROR_SYSCALL)
        {
            e = ERR_get_error();
            if (e > 0)
            {
                std::string errMsg("OpenSSL failed - ");
                errMsg += ERR_error_string(e, nullptr);
                return errMsg;
            }
            else if (e == 0 && ret == 0)
            {
                return "OpenSSL failed - received early EOF";
            }
            else
            {
                return "OpenSSL failed - underlying BIO reported an I/O error";
            }
        }
        else if (err == SSL_ERROR_SSL)
        {
            e = ERR_get_error();
            std::string errMsg("OpenSSL failed - ");
            errMsg += ERR_error_string(e, nullptr);
            return errMsg;
        }
        else if (err == SSL_ERROR_NONE)
        {
            return "OpenSSL failed - err none";
        }
        else if (err == SSL_ERROR_ZERO_RETURN)
        {
            return "OpenSSL failed - err zero return";
        }
        else
        {
            return "OpenSSL failed - unknown error";
        }
    }

    SSL_CTX* SocketOpenSSL::openSSLCreateContext(std::string& errMsg)
    {
        const SSL_METHOD* method = SSLv23_client_method();
        if (method == nullptr)
        {
            errMsg = "SSLv23_client_method failure";
            return nullptr;
        }
        _ssl_method = method;

        SSL_CTX* ctx = SSL_CTX_new(_ssl_method);
        if (ctx)
        {
            SSL_CTX_set_mode(ctx,
                             SSL_MODE_ENABLE_PARTIAL_WRITE | SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);

            int options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_CIPHER_SERVER_PREFERENCE;

            SSL_CTX_set_options(ctx, options);
        }
        return ctx;
    }

    bool SocketOpenSSL::openSSLAddCARootsFromString(const std::string roots)
    {
        // Create certificate store
        X509_STORE* certificate_store = SSL_CTX_get_cert_store(_ssl_context);
        if (certificate_store == nullptr) return false;

        // Configure to allow intermediate certs
        X509_STORE_set_flags(certificate_store,
                             X509_V_FLAG_TRUSTED_FIRST | X509_V_FLAG_PARTIAL_CHAIN);

        // Create a new buffer and populate it with the roots
        BIO* buffer = BIO_new_mem_buf((void*) roots.c_str(), static_cast<int>(roots.length()));
        if (buffer == nullptr) return false;

        // Read each root in the buffer and add to the certificate store
        bool success = true;
        size_t number_of_roots = 0;

        while (true)
        {
            // Read the next root in the buffer
            X509* root = PEM_read_bio_X509_AUX(buffer, nullptr, nullptr, (void*) "");
            if (root == nullptr)
            {
                // No more certs left in the buffer, we're done.
                ERR_clear_error();
                break;
            }

            // Try adding the root to the certificate store
            ERR_clear_error();
            if (!X509_STORE_add_cert(certificate_store, root))
            {
                // Failed to add. If the error is unrelated to the x509 lib or the cert already
                // exists, we're safe to continue.
                unsigned long error = ERR_get_error();
                if (ERR_GET_LIB(error) != ERR_LIB_X509 ||
                    ERR_GET_REASON(error) != X509_R_CERT_ALREADY_IN_HASH_TABLE)
                {
                    // Failed. Clean up and bail.
                    success = false;
                    X509_free(root);
                    break;
                }
            }

            // Clean up and loop
            X509_free(root);
            number_of_roots++;
        }

        // Clean up buffer
        BIO_free(buffer);

        // Make sure we loaded at least one certificate.
        if (number_of_roots == 0) success = false;

        return success;
    }

    /**
     * Check whether a hostname matches a pattern
     */
    bool SocketOpenSSL::checkHost(const std::string& host, const char* pattern)
    {
#if OPENSSL_VERSION_NUMBER >= 0x10100000L
        return true;
#else

#ifdef _WIN32
        return PathMatchSpecA(host.c_str(), pattern);
#else
        return fnmatch(pattern, host.c_str(), 0) != FNM_NOMATCH;
#endif

#endif
    }

    bool SocketOpenSSL::openSSLCheckServerCert(SSL* ssl,
#if OPENSSL_VERSION_NUMBER < 0x10100000L
                                               const std::string& hostname,
#else
                                               const std::string& /* hostname */,
#endif
                                               std::string& errMsg)
    {
        X509* server_cert = SSL_get_peer_certificate(ssl);
        if (server_cert == nullptr)
        {
            errMsg = "OpenSSL failed - peer didn't present a X509 certificate.";
            return false;
        }

#if OPENSSL_VERSION_NUMBER < 0x10100000L
        // Check server name
        bool hostname_verifies_ok = false;
        STACK_OF(GENERAL_NAME)* san_names = (STACK_OF(GENERAL_NAME)*) X509_get_ext_d2i(
            (X509*) server_cert, NID_subject_alt_name, NULL, NULL);
        if (san_names)
        {
            for (int i = 0; i < sk_GENERAL_NAME_num(san_names); i++)
            {
                const GENERAL_NAME* sk_name = sk_GENERAL_NAME_value(san_names, i);
                if (sk_name->type == GEN_DNS)
                {
                    char* name = (char*) ASN1_STRING_data(sk_name->d.dNSName);
                    if ((size_t) ASN1_STRING_length(sk_name->d.dNSName) == strlen(name) &&
                        checkHost(hostname, name))
                    {
                        hostname_verifies_ok = true;
                        break;
                    }
                }
            }
        }
        sk_GENERAL_NAME_pop_free(san_names, GENERAL_NAME_free);

        if (!hostname_verifies_ok)
        {
            int cn_pos = X509_NAME_get_index_by_NID(
                X509_get_subject_name((X509*) server_cert), NID_commonName, -1);
            if (cn_pos >= 0)
            {
                X509_NAME_ENTRY* cn_entry =
                    X509_NAME_get_entry(X509_get_subject_name((X509*) server_cert), cn_pos);

                if (cn_entry != nullptr)
                {
                    ASN1_STRING* cn_asn1 = X509_NAME_ENTRY_get_data(cn_entry);
                    char* cn = (char*) ASN1_STRING_data(cn_asn1);

                    if ((size_t) ASN1_STRING_length(cn_asn1) == strlen(cn) &&
                        checkHost(hostname, cn))
                    {
                        hostname_verifies_ok = true;
                    }
                }
            }
        }

        if (!hostname_verifies_ok)
        {
            errMsg = "OpenSSL failed - certificate was issued for a different domain.";
            return false;
        }
#endif

        X509_free(server_cert);
        return true;
    }

    bool SocketOpenSSL::openSSLClientHandshake(const std::string& host,
                                               std::string& errMsg,
                                               const CancellationRequest& isCancellationRequested)
    {
        while (true)
        {
            if (_ssl_connection == nullptr || _ssl_context == nullptr)
            {
                return false;
            }

            if (isCancellationRequested())
            {
                errMsg = "Cancellation requested";
                return false;
            }

            ERR_clear_error();
            int connect_result = SSL_connect(_ssl_connection);
            if (connect_result == 1)
            {
                if (_tlsOptions.disable_hostname_validation)
                {
                    return true;
                }

                return openSSLCheckServerCert(_ssl_connection, host, errMsg);
            }
            int reason = SSL_get_error(_ssl_connection, connect_result);

            bool rc = false;
            if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
            {
                rc = true;
            }
            else
            {
                errMsg = getSSLError(connect_result);
                rc = false;
            }

            if (!rc)
            {
                return false;
            }
        }
    }

    bool SocketOpenSSL::openSSLServerHandshake(std::string& errMsg)
    {
        while (true)
        {
            if (_ssl_connection == nullptr || _ssl_context == nullptr)
            {
                return false;
            }

            ERR_clear_error();
            int accept_result = SSL_accept(_ssl_connection);
            if (accept_result == 1)
            {
                return true;
            }
            int reason = SSL_get_error(_ssl_connection, accept_result);

            bool rc = false;
            if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
            {
                rc = true;
            }
            else
            {
                errMsg = getSSLError(accept_result);
                rc = false;
            }

            if (!rc)
            {
                return false;
            }
        }
    }

    bool SocketOpenSSL::handleTLSOptions(std::string& errMsg)
    {
        ERR_clear_error();
        if (_tlsOptions.hasCertAndKey())
        {
            if (SSL_CTX_use_certificate_chain_file(_ssl_context, _tlsOptions.certFile.c_str()) != 1)
            {
                auto sslErr = ERR_get_error();
                errMsg = "OpenSSL failed - SSL_CTX_use_certificate_chain_file(\"" +
                         _tlsOptions.certFile + "\") failed: ";
                errMsg += ERR_error_string(sslErr, nullptr);
            }
            else if (SSL_CTX_use_PrivateKey_file(
                         _ssl_context, _tlsOptions.keyFile.c_str(), SSL_FILETYPE_PEM) != 1)
            {
                auto sslErr = ERR_get_error();
                errMsg = "OpenSSL failed - SSL_CTX_use_PrivateKey_file(\"" + _tlsOptions.keyFile +
                         "\") failed: ";
                errMsg += ERR_error_string(sslErr, nullptr);
            }
            else if (!SSL_CTX_check_private_key(_ssl_context))
            {
                auto sslErr = ERR_get_error();
                errMsg = "OpenSSL failed - cert/key mismatch(\"" + _tlsOptions.certFile + ", " +
                         _tlsOptions.keyFile + "\")";
                errMsg += ERR_error_string(sslErr, nullptr);
            }
        }

        ERR_clear_error();
        if (!_tlsOptions.isPeerVerifyDisabled())
        {
            if (_tlsOptions.isUsingSystemDefaults())
            {
#ifdef _WIN32
                if (!loadWindowsSystemCertificates(_ssl_context, errMsg))
                {
                    return false;
                }
#else
                if (SSL_CTX_set_default_verify_paths(_ssl_context) == 0)
                {
                    auto sslErr = ERR_get_error();
                    errMsg = "OpenSSL failed - SSL_CTX_default_verify_paths loading failed: ";
                    errMsg += ERR_error_string(sslErr, nullptr);
                    return false;
                }
#endif
            }
            else
            {
                if (_tlsOptions.isUsingInMemoryCAs())
                {
                    // Load from memory
                    openSSLAddCARootsFromString(_tlsOptions.caFile);
                }
                else
                {
                    if (SSL_CTX_load_verify_locations(
                            _ssl_context, _tlsOptions.caFile.c_str(), NULL) != 1)
                    {
                        auto sslErr = ERR_get_error();
                        errMsg = "OpenSSL failed - SSL_CTX_load_verify_locations(\"" +
                                 _tlsOptions.caFile + "\") failed: ";
                        errMsg += ERR_error_string(sslErr, nullptr);
                        return false;
                    }
                }
            }

            SSL_CTX_set_verify(_ssl_context,
                               SSL_VERIFY_PEER,
                               [](int preverify, X509_STORE_CTX*) -> int { return preverify; });
            SSL_CTX_set_verify_depth(_ssl_context, 4);
        }
        else
        {
            SSL_CTX_set_verify(_ssl_context, SSL_VERIFY_NONE, nullptr);
        }

        if (_tlsOptions.isUsingDefaultCiphers())
        {
            if (SSL_CTX_set_cipher_list(_ssl_context, kDefaultCiphers.c_str()) != 1)
            {
                auto sslErr = ERR_get_error();
                errMsg = "OpenSSL failed - SSL_CTX_set_cipher_list(\"" + kDefaultCiphers +
                         "\") failed: ";
                errMsg += ERR_error_string(sslErr, nullptr);
                return false;
            }
        }
        else if (SSL_CTX_set_cipher_list(_ssl_context, _tlsOptions.ciphers.c_str()) != 1)
        {
            auto sslErr = ERR_get_error();
            errMsg = "OpenSSL failed - SSL_CTX_set_cipher_list(\"" + _tlsOptions.ciphers +
                     "\") failed: ";
            errMsg += ERR_error_string(sslErr, nullptr);
            return false;
        }

        return true;
    }

    bool SocketOpenSSL::accept(std::string& errMsg)
    {
        bool handshakeSuccessful = false;
        {
            std::lock_guard<std::mutex> lock(_mutex);

            if (!_openSSLInitializationSuccessful)
            {
                errMsg = "OPENSSL_init_ssl failure";
                return false;
            }

            if (_sockfd == -1)
            {
                return false;
            }

            {
                const SSL_METHOD* method = SSLv23_server_method();
                if (method == nullptr)
                {
                    errMsg = "SSLv23_server_method failure";
                    _ssl_context = nullptr;
                }
                else
                {
                    _ssl_method = method;

                    _ssl_context = SSL_CTX_new(_ssl_method);
                    if (_ssl_context)
                    {
                        SSL_CTX_set_mode(_ssl_context, SSL_MODE_ENABLE_PARTIAL_WRITE);
                        SSL_CTX_set_mode(_ssl_context, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
                        SSL_CTX_set_options(_ssl_context,
                                            SSL_OP_ALL | SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3);
                    }
                }
            }

            if (_ssl_context == nullptr)
            {
                return false;
            }

            ERR_clear_error();
            if (_tlsOptions.hasCertAndKey())
            {
                if (SSL_CTX_use_certificate_chain_file(_ssl_context,
                                                       _tlsOptions.certFile.c_str()) != 1)
                {
                    auto sslErr = ERR_get_error();
                    errMsg = "OpenSSL failed - SSL_CTX_use_certificate_chain_file(\"" +
                             _tlsOptions.certFile + "\") failed: ";
                    errMsg += ERR_error_string(sslErr, nullptr);
                }
                else if (SSL_CTX_use_PrivateKey_file(
                             _ssl_context, _tlsOptions.keyFile.c_str(), SSL_FILETYPE_PEM) != 1)
                {
                    auto sslErr = ERR_get_error();
                    errMsg = "OpenSSL failed - SSL_CTX_use_PrivateKey_file(\"" +
                             _tlsOptions.keyFile + "\") failed: ";
                    errMsg += ERR_error_string(sslErr, nullptr);
                }
            }


            ERR_clear_error();
            if (!_tlsOptions.isPeerVerifyDisabled())
            {
                if (_tlsOptions.isUsingSystemDefaults())
                {
                    if (SSL_CTX_set_default_verify_paths(_ssl_context) == 0)
                    {
                        auto sslErr = ERR_get_error();
                        errMsg = "OpenSSL failed - SSL_CTX_default_verify_paths loading failed: ";
                        errMsg += ERR_error_string(sslErr, nullptr);
                    }
                }
                else
                {
                    if (_tlsOptions.isUsingInMemoryCAs())
                    {
                        // Load from memory
                        openSSLAddCARootsFromString(_tlsOptions.caFile);
                    }
                    else
                    {
                        const char* root_ca_file = _tlsOptions.caFile.c_str();
                        STACK_OF(X509_NAME) * rootCAs;
                        rootCAs = SSL_load_client_CA_file(root_ca_file);
                        if (rootCAs == NULL)
                        {
                            auto sslErr = ERR_get_error();
                            errMsg = "OpenSSL failed - SSL_load_client_CA_file('" +
                                     _tlsOptions.caFile + "') failed: ";
                            errMsg += ERR_error_string(sslErr, nullptr);
                        }
                        else
                        {
                            SSL_CTX_set_client_CA_list(_ssl_context, rootCAs);
                            if (SSL_CTX_load_verify_locations(
                                    _ssl_context, root_ca_file, nullptr) != 1)
                            {
                                auto sslErr = ERR_get_error();
                                errMsg = "OpenSSL failed - SSL_CTX_load_verify_locations(\"" +
                                         _tlsOptions.caFile + "\") failed: ";
                                errMsg += ERR_error_string(sslErr, nullptr);
                            }
                        }
                    }
                }

                SSL_CTX_set_verify(
                    _ssl_context, SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT, nullptr);
                SSL_CTX_set_verify_depth(_ssl_context, 4);
            }
            else
            {
                SSL_CTX_set_verify(_ssl_context, SSL_VERIFY_NONE, nullptr);
            }
            if (_tlsOptions.isUsingDefaultCiphers())
            {
                if (SSL_CTX_set_cipher_list(_ssl_context, kDefaultCiphers.c_str()) != 1)
                {
                    return false;
                }
            }
            else if (SSL_CTX_set_cipher_list(_ssl_context, _tlsOptions.ciphers.c_str()) != 1)
            {
                return false;
            }

            _ssl_connection = SSL_new(_ssl_context);
            if (_ssl_connection == nullptr)
            {
                errMsg = "OpenSSL failed to connect";
                SSL_CTX_free(_ssl_context);
                _ssl_context = nullptr;
                return false;
            }

            SSL_set_ecdh_auto(_ssl_connection, 1);

            SSL_set_fd(_ssl_connection, _sockfd);

            handshakeSuccessful = openSSLServerHandshake(errMsg);
        }

        if (!handshakeSuccessful)
        {
            close();
            return false;
        }

        return true;
    }

    bool SocketOpenSSL::connect(const std::string& host,
                                int port,
                                std::string& errMsg,
                                const CancellationRequest& isCancellationRequested)
    {
        bool handshakeSuccessful = false;
        {
            std::lock_guard<std::mutex> lock(_mutex);

            if (!_openSSLInitializationSuccessful)
            {
                errMsg = "OPENSSL_init_ssl failure";
                return false;
            }

            _sockfd = SocketConnect::connect(host, port, errMsg, isCancellationRequested);
            if (_sockfd == -1) return false;

            _ssl_context = openSSLCreateContext(errMsg);
            if (_ssl_context == nullptr)
            {
                return false;
            }

            if (!handleTLSOptions(errMsg))
            {
                return false;
            }

            _ssl_connection = SSL_new(_ssl_context);
            if (_ssl_connection == nullptr)
            {
                errMsg = "OpenSSL failed to connect";
                SSL_CTX_free(_ssl_context);
                _ssl_context = nullptr;
                return false;
            }
            SSL_set_fd(_ssl_connection, _sockfd);

            // SNI support
            SSL_set_tlsext_host_name(_ssl_connection, host.c_str());

#if OPENSSL_VERSION_NUMBER >= 0x10002000L
            // Support for server name verification
            // (The docs say that this should work from 1.0.2, and is the default from
            // 1.1.0, but it does not. To be on the safe side, the manual test
            // below is enabled for all versions prior to 1.1.0.)
            if (!_tlsOptions.disable_hostname_validation)
            {
                X509_VERIFY_PARAM* param = SSL_get0_param(_ssl_connection);
                X509_VERIFY_PARAM_set1_host(param, host.c_str(), host.size());
            }
#endif
            handshakeSuccessful = openSSLClientHandshake(host, errMsg, isCancellationRequested);
        }

        if (!handshakeSuccessful)
        {
            close();
            return false;
        }

        return true;
    }

    void SocketOpenSSL::close()
    {
        std::lock_guard<std::mutex> lock(_mutex);

        if (_ssl_connection != nullptr)
        {
            SSL_free(_ssl_connection);
            _ssl_connection = nullptr;
        }
        if (_ssl_context != nullptr)
        {
            SSL_CTX_free(_ssl_context);
            _ssl_context = nullptr;
        }

        Socket::close();
    }

    ssize_t SocketOpenSSL::send(char* buf, size_t nbyte)
    {
        std::lock_guard<std::mutex> lock(_mutex);

        if (_ssl_connection == nullptr || _ssl_context == nullptr)
        {
            return 0;
        }

        ERR_clear_error();
        ssize_t write_result = SSL_write(_ssl_connection, buf, (int) nbyte);
        int reason = SSL_get_error(_ssl_connection, (int) write_result);

        if (reason == SSL_ERROR_NONE)
        {
            return write_result;
        }
        else if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
        {
            errno = EWOULDBLOCK;
            return -1;
        }
        else
        {
            return -1;
        }
    }

    ssize_t SocketOpenSSL::recv(void* buf, size_t nbyte)
    {
        while (true)
        {
            std::lock_guard<std::mutex> lock(_mutex);

            if (_ssl_connection == nullptr || _ssl_context == nullptr)
            {
                return 0;
            }

            ERR_clear_error();
            ssize_t read_result = SSL_read(_ssl_connection, buf, (int) nbyte);

            if (read_result > 0)
            {
                return read_result;
            }

            int reason = SSL_get_error(_ssl_connection, (int) read_result);

            if (reason == SSL_ERROR_WANT_READ || reason == SSL_ERROR_WANT_WRITE)
            {
                errno = EWOULDBLOCK;
            }
            return -1;
        }
    }

} // namespace ix

#endif // IXWEBSOCKET_USE_OPEN_SSL
