#include "sslio.h"

#include <balancer/kernel/helpers/errors.h>
#include <balancer/kernel/fs/fs_validations.h>

#include <util/memory/tempbuf.h>
#include <util/generic/array_size.h>
#include <util/generic/strbuf.h>
#include <util/string/hex.h>
#include <util/string/split.h>
#include <util/string/subst.h>
#include <util/stream/format.h>
#include <util/stream/direct_io.h>
#include <util/system/env.h>

#include <openssl/dh.h>
#include <openssl/ssl3.h>

#include <openssl/bio.h>
#include <openssl/err.h>
#include <openssl/ocsp.h>
#include <openssl/ossl_typ.h>
#include <openssl/pem.h>
#include <openssl/ssl.h>
#include <openssl/x509.h>


namespace {

using namespace NSrvKernel;

struct TInitSsl {
    TInitSsl() noexcept {
        SSL_library_init();
        SSL_load_error_strings();
        OpenSSL_add_all_algorithms();
        ERR_load_BIO_strings();
    }
};

TSslError MakeSslError() noexcept {
    const int status = GetLastSslError();
    return TSslError{status} << '(' << SslErrorText(status) << ") ";
}

void SslClearError() noexcept {
    ERR_clear_error();
}

struct TBioDeleter {
    static void Destroy(BIO* p) noexcept {
        BIO_free(p);
    }
};

using TBioHolder = THolder<BIO, TBioDeleter>;

TErrorOr<TBioHolder> CreateBioFromFile(const char* fileName) {
    Y_PROPAGATE_ERROR(CheckFileReadable(fileName));
    TBioHolder retval{BIO_new_file(fileName, "r")};
    Y_REQUIRE(retval != nullptr,
              MakeSslError() << "failed to open file \"" << fileName << '"');
    return retval;
}

#define DBG_PS(fname) \
    #fname " = " << ((fname) ? (fname) : "NULL")

#define SCTX_DBGARGS \
    DBG_PS(params.Ciphers) << ", " << DBG_PS(params.KeyFile) << ", " << DBG_PS(params.CertFile) << ", " << DBG_PS(params.CaFile)

#define SCTX_CERT_DBGARGS \
    DBG_PS(keyFile) << ", " << DBG_PS(certFile) << ", " << DBG_PS(caFile)

TError AddCert(TX509Holder& x509, ssl_ctx_st* ctx, const char* keyFile,
                const char* certFile, const char* caFile, bool validateCertDate)
{
    Y_ASSERT(ctx);

    // we can't use SSL_CTX_use_certificate_chain_file() as it doesn't
    // allow to access certificate later from SSL_CTX, so we reimplement
    // it here

    Y_REQUIRE(certFile != nullptr,
              yexception() << "no certfile and cafile present " << SCTX_CERT_DBGARGS);

    TBioHolder inFile;
    Y_PROPAGATE_ERROR(CreateBioFromFile(certFile).AssignTo(inFile));

    x509.Reset(PEM_read_bio_X509_AUX(inFile.Get(), nullptr, nullptr, nullptr));
    Y_REQUIRE(x509 != nullptr,
              MakeSslError() << "PEM_read_bio_X509_AUX(" << certFile << ") failed "
                             << SCTX_CERT_DBGARGS);

    if (validateCertDate) {
        const auto dateAfter = X509_get0_notAfter(x509.Get());
        const auto dateBefore = X509_get0_notBefore(x509.Get());

        int certDay = 0;
        int certSec = 0;

        // nullptr as from/to indicates the current time
        Y_REQUIRE(ASN1_TIME_diff(&certDay, &certSec, dateAfter, nullptr),
                  MakeSslError() << "Could not parse cert date for " << SCTX_CERT_DBGARGS);

        Y_REQUIRE(certDay > 0 || certSec > 0,
                  MakeSslError() << "Certificate Valid After is early than now  " << SCTX_CERT_DBGARGS);

        Y_REQUIRE(ASN1_TIME_diff(&certDay, &certSec, nullptr, dateBefore),
                  MakeSslError() << "Could not parse cert date for " << SCTX_CERT_DBGARGS);

        Y_REQUIRE(certDay > 0 || certSec > 0,
                  MakeSslError() << "Certificate is expired " << SCTX_CERT_DBGARGS);
    }

    Y_REQUIRE(SSL_CTX_use_certificate(ctx, x509.Get()) == 1,
              MakeSslError() << "SSL_CTX_use_certificate(" << certFile << ") failed "
                             << SCTX_CERT_DBGARGS);

    // the rest of the chain
    for (int i = 0; ; ++i) {
        TX509Holder x509_ca{ PEM_read_bio_X509(inFile.Get(), nullptr, nullptr, nullptr) };
        if (x509_ca) {
            Y_REQUIRE(SSL_CTX_add1_chain_cert(ctx, x509_ca.Get()) == 1,
                      MakeSslError() << "SSL_CTX_add1_chain_cert() failed for " << i
                                     << "th cert in chain " << SCTX_CERT_DBGARGS);
        } else {
            const unsigned long n = ERR_peek_last_error();

            Y_REQUIRE(ERR_GET_LIB(n) == ERR_LIB_PEM && ERR_GET_REASON(n) == PEM_R_NO_START_LINE,
                      MakeSslError() << "PEM_read_bio_X509(" << certFile << ") failed for " << i
                                     << "th cert in chain " << SCTX_CERT_DBGARGS)
            // end of file
            ERR_clear_error();
            if (caFile) {
                TBioHolder caInput;
                Y_PROPAGATE_ERROR(CreateBioFromFile(caFile).AssignTo(caInput));
                x509_ca.Reset(PEM_read_bio_X509_AUX(caInput.Get(), nullptr, nullptr, nullptr));
                Y_REQUIRE(x509_ca != nullptr,
                          MakeSslError() << "PEM-read_bio_X509_AUX(" << caFile << ") failed "
                                         << SCTX_CERT_DBGARGS);
                Y_REQUIRE(SSL_CTX_add1_chain_cert(ctx, x509_ca.Get()) == 1,
                          MakeSslError() << "SSL_CTX_add1_chain_cert() failed for ca: "
                                         << caFile << ' ' << SCTX_CERT_DBGARGS);
            }
            break;
        }
    }

    if (keyFile) {
        Y_PROPAGATE_ERROR(CheckFileReadable(keyFile));
        // BALANCER-2831: SSL_CTX_use_PrivateKey_file will check the private key against
        // the last added certificate of the same type. That's why this function
        // should be called *after* loading certificate.
        Y_REQUIRE(SSL_CTX_use_PrivateKey_file(ctx, keyFile, SSL_FILETYPE_PEM) == 1,
                  MakeSslError() << "SSL_CTX_use_PrivateKey_file " << SCTX_CERT_DBGARGS);
    }

    return {};
}

class TBioMethod {
private:
    static const int METHOD_INDEX;

    struct TBioMethodDelete {
        static void Destroy(BIO_METHOD* meth) noexcept {
            BIO_meth_free(meth);
        }
    };

    THolder<BIO_METHOD, TBioMethodDelete> Method_;

public:
    TBioMethod() noexcept
        : Method_(BIO_meth_new(METHOD_INDEX, "balancer_ssl_bio"))
    {
        Y_VERIFY(Method_ != nullptr);
        Y_VERIFY(BIO_meth_set_write(Method_.Get(), TBioMethod::Write) == 1);
        Y_VERIFY(BIO_meth_set_read(Method_.Get(), TBioMethod::Read) == 1);
        Y_VERIFY(BIO_meth_set_ctrl(Method_.Get(), TBioMethod::Ctrl) == 1);

        //if (Y_UNLIKELY(BIO_meth_set_create(Method_.Get(), TBioMethod::Create) != 1)) {
        //    ythrow MakeSslError() << "BIO_meth_set_create() failed";
        //}
    }

    BIO_METHOD* Method() noexcept {
        return Method_.Get();
    }

    static TBioMethod* Instance() noexcept {
        return SingletonWithPriority<TBioMethod, 0>();
    }

    static TSslIo* GetIo(bio_st* bio) noexcept {
        if (Y_LIKELY(bio)) {
            return reinterpret_cast<TSslIo*>(BIO_get_data(bio));
        }
        return nullptr;
    }

    static void SetIo(bio_st* bio, TSslIo* io) noexcept {
        if (Y_LIKELY(bio)) {
            BIO_set_data(bio, reinterpret_cast<void*>(io));
        }
    }

private:
    static int Write(bio_st* bio, const char* buf, int len) {
        TSslIo* const parent = GetIo(bio);

        if (parent->HasStoredError()) {
            return -1;
        }

        TChunkPtr chunk = NewChunkReserve(len);

        memcpy(chunk->Data(), buf, len);

        chunk->Shrink(len);

        TInstant deadline = parent->SendDeadline();

        Y_TRY(TError, error) {
            return parent->Output().Send(TChunkList(std::move(chunk)), deadline);
        } Y_CATCH {
            parent->StoreError(std::move(error));
            len = -1;
        }

        return len;
    }

    static int Read(bio_st* bio, char* buf, int len) {
        TSslIo* const parent = GetIo(bio);

        if (parent->HasStoredError()) {
            return -1;
        }

        TChunkList lst;

        TInstant deadline = parent->RecvDeadline();

        Y_TRY(TError, error) {
            return parent->Input().Recv(lst, deadline);
        } Y_CATCH {
            parent->StoreError(std::move(error));
            return -1;
        }

        size_t ret = 0;

        while (!lst.Empty()) {
            TChunkPtr chunk = lst.PopFront();
            size_t chunkLength = chunk->Length();

            if ((size_t)len < chunkLength) {
                TChunkList remains(chunk->SubChunk(len, chunkLength));
                remains.Append(std::move(lst));

                parent->Input().UnRecv(std::move(remains));

                chunkLength = len;
                chunk->Shrink(chunkLength);
            }

            memcpy(buf, chunk->Data(), chunkLength);

            ret += chunkLength;
            buf += chunkLength;
            len -= chunkLength;
        }

        return ret;
    }

    static long Ctrl(bio_st*, int cmd, long, void*) noexcept {
        switch (cmd) {
        case BIO_CTRL_FLUSH: return 1;
        default: return 0;
        }
    }

    static int Create(bio_st* bio) noexcept {
        BIO_set_init(bio, 1);
        return 1;
    }
};

const int TBioMethod::METHOD_INDEX = BIO_get_new_index();

TError SetEcdhCurve(ssl_ctx_st* ctx, const char* curves) {
    Y_ASSERT(ctx);

    // SSL_CTX_set_ecdh_auto is set to enabled for openssl 1.1.0
    // by default X25519 will be used
    if (curves) {
        Y_REQUIRE(SSL_CTX_set1_curves_list(ctx, curves) == 1,
            MakeSslError() << " failed to set ecdh " << curves);

    }

    // use key only once
    SSL_CTX_set_options(ctx, SSL_OP_SINGLE_ECDH_USE);
    return {};
}

TError SetDhParam(ssl_ctx_st* ctx) {
    Y_ASSERT(ctx);
    // see nginx ngx_ssl_dhparam
    //
    //  -----BEGIN DH PARAMETERS-----
    //  MIGHAoGBALu8LcrYRnSQfEP89YDpz9vZWKP1aLQtSwju1OsPs1BMbAMCducQgAxc
    //  y7qokiYUxb7spWWl/fHSh6K8BJvmd4Bg6RqSp1fjBI9osHb302zI8pul34HcLKcl
    //  7OZicMyaUDXYzs7vnqAnSmOrHlj6/UmI0PZdFGdX2gcd8EXP4WubAgEC
    //  -----END DH PARAMETERS-----
    //
    static unsigned char dh1024_p[] = {
        0xBB, 0xBC, 0x2D, 0xCA, 0xD8, 0x46, 0x74, 0x90, 0x7C, 0x43, 0xFC, 0xF5,
        0x80, 0xE9, 0xCF, 0xDB, 0xD9, 0x58, 0xA3, 0xF5, 0x68, 0xB4, 0x2D, 0x4B,
        0x08, 0xEE, 0xD4, 0xEB, 0x0F, 0xB3, 0x50, 0x4C, 0x6C, 0x03, 0x02, 0x76,
        0xE7, 0x10, 0x80, 0x0C, 0x5C, 0xCB, 0xBA, 0xA8, 0x92, 0x26, 0x14, 0xC5,
        0xBE, 0xEC, 0xA5, 0x65, 0xA5, 0xFD, 0xF1, 0xD2, 0x87, 0xA2, 0xBC, 0x04,
        0x9B, 0xE6, 0x77, 0x80, 0x60, 0xE9, 0x1A, 0x92, 0xA7, 0x57, 0xE3, 0x04,
        0x8F, 0x68, 0xB0, 0x76, 0xF7, 0xD3, 0x6C, 0xC8, 0xF2, 0x9B, 0xA5, 0xDF,
        0x81, 0xDC, 0x2C, 0xA7, 0x25, 0xEC, 0xE6, 0x62, 0x70, 0xCC, 0x9A, 0x50,
        0x35, 0xD8, 0xCE, 0xCE, 0xEF, 0x9E, 0xA0, 0x27, 0x4A, 0x63, 0xAB, 0x1E,
        0x58, 0xFA, 0xFD, 0x49, 0x88, 0xD0, 0xF6, 0x5D, 0x14, 0x67, 0x57, 0xDA,
        0x07, 0x1D, 0xF0, 0x45, 0xCF, 0xE1, 0x6B, 0x9B
    };

    static unsigned char dh1024_g[] = { 0x02 };

    struct TDhDelete {
        static void Destroy(DH* key) noexcept {
            DH_free(key);
        }
    };

    struct TBnDelete {
        static void Destroy(BIGNUM* n) noexcept {
            BN_free(n);
        }
    };

    const THolder<DH, TDhDelete> dh(DH_new());

    Y_REQUIRE(dh != nullptr,
              MakeSslError() << " DH_new() failed");

    THolder<BIGNUM, TBnDelete> dhp_bn(BN_bin2bn(dh1024_p, sizeof (dh1024_p), NULL));
    THolder<BIGNUM, TBnDelete> dhg_bn(BN_bin2bn(dh1024_g, sizeof (dh1024_g), NULL));
    Y_REQUIRE(dhp_bn != nullptr && dhg_bn != nullptr
              && DH_set0_pqg(dh.Get(), dhp_bn.Get(), NULL, dhg_bn.Get()),
              MakeSslError() << " failed to set up DH params");
    Y_UNUSED(dhp_bn.Release()); // owned by `dh` now
    Y_UNUSED(dhg_bn.Release());

    SSL_CTX_set_tmp_dh(ctx, dh.Get());
    return {};
}

TError SslCtxSetVerify(SSL_CTX* ctx, int mode) noexcept {
    Y_REQUIRE(SSL_CTX_set_session_id_context(ctx, BALANCER_SESSION_CTX_ID, sizeof(BALANCER_SESSION_CTX_ID)) != 0,
              MakeSslError() << "failed to set session id context");

    SSL_CTX_set_verify(ctx, mode, NULL);
    SslClearError();

    return {};
}

TMaybe<TString> X509CertName(const TX509Holder& cert) noexcept {
    if (!cert) {
        return TMaybe<TString>();
    }

    auto subject = X509_get_subject_name(cert.Get());
    if (!subject) {
        return TMaybe<TString>();
    }

    TBioHolder bio(BIO_new(BIO_s_mem()));
    if (!bio) {
        Y_FAIL("failed to allocate BIO for printing CertName");
    }

    if (X509_NAME_print_ex(bio.Get(), subject, 0, XN_FLAG_COMPAT) <= 0) {
        return TMaybe<TString>();
    }

    char* data;
    size_t len = BIO_get_mem_data(bio.Get(), &data);

    TString subjectStr(data, len);
    SubstGlobal(subjectStr, ", ", "/");
    if (!subjectStr.StartsWith('/')) {
        subjectStr.prepend('/');
    }

    return TMaybe<TString>(std::move(subjectStr));
}

TMaybe<TString> X509SerialNumber(const TX509Holder& cert) noexcept {
    if (!cert) {
        return TMaybe<TString>();
    }

    asn1_string_st* serialStruct = X509_get_serialNumber(cert.Get());
    if (serialStruct == nullptr) {
        return TMaybe<TString>();
    }

    TStringStream serialStream;
    if (serialStruct->type == V_ASN1_NEG_INTEGER) {
        serialStream << "-";
    }

    for (int i = 0; i < serialStruct->length; ++i) {
        serialStream << Hex(serialStruct->data[i], ENumberFormatFlag::HF_FULL);
    }

    return serialStream.Str();
}

}  // namespace

namespace NSrvKernel {

void InitSsl() noexcept {
    Singleton<TInitSsl>();
}

int GetLastSslError() noexcept {
    return ERR_peek_last_error();
}

const char* SslErrorText(int error) noexcept {
    return ERR_error_string(error, nullptr);
}

namespace NPrivate {

void TX509Deleter::Destroy(x509_st* p) noexcept {
    X509_free(p);
}

}  // namespace NPrivate


void TSslContext::TDestroy::Destroy(ssl_ctx_st* ctx) noexcept {
    SSL_CTX_free(ctx);
}

TErrorOr<TSslContext> NPrivate::TSslContextAccessor::CreateSslCtx(const ssl_method_st* method, ui64 protocols) {
    TSslContext ctx(SSL_CTX_new(method));

    Y_REQUIRE(ctx.Ctx() != nullptr,
              MakeSslError() << "SSL_CTX_new");

    // see nginx ngx_ssl_create
    //
    // common options
    SSL_CTX_set_options(ctx.Ctx(),
        SSL_OP_NO_SSLv2|SSL_OP_NO_SSLv3|SSL_OP_NO_TLSv1|SSL_OP_NO_TLSv1_1|SSL_OP_NO_TLSv1_2|SSL_OP_NO_TLSv1_3);
    SSL_CTX_clear_options(ctx.Ctx(), protocols);

    // client options
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_MICROSOFT_SESS_ID_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_NETSCAPE_CHALLENGE_BUG);

    // server options
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_SAFARI_ECDHE_ECDSA_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_SSLREF2_REUSE_CERT_TYPE_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_MICROSOFT_BIG_SSLV3_BUFFER);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_MSIE_SSLV2_RSA_PADDING);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_SSLEAY_080_CLIENT_DH_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_TLS_D5_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_TLS_BLOCK_PADDING_BUG);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_DONT_INSERT_EMPTY_FRAGMENTS);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_SINGLE_DH_USE);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_PRIORITIZE_CHACHA);
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_NO_RENEGOTIATION);
#ifdef SSL_OP_NO_COMPRESSION
    SSL_CTX_set_options(ctx.Ctx(), SSL_OP_NO_COMPRESSION);
#endif
#ifdef SSL_MODE_RELEASE_BUFFERS
    SSL_CTX_set_mode(ctx.Ctx(), SSL_MODE_RELEASE_BUFFERS);
#endif
    SSL_CTX_set_read_ahead(ctx.Ctx(), 1);

    return ctx;
}

TErrorOr<THolder<TSslServerContext>> NPrivate::TSslContextAccessor::CreateServerCtx(TSslServerCtxInitParams& params)
{
    TSslContext originalCtx;
    Y_PROPAGATE_ERROR(CreateSslCtx(SSLv23_server_method(), params.Protocols).AssignTo(originalCtx));
    THolder<TSslServerContext> ctx{new TSslServerContext(std::move(originalCtx))};

    if (params.Ciphers) {
        Y_REQUIRE(SSL_CTX_set_cipher_list(ctx->Ctx(), params.Ciphers) == 1,
                  MakeSslError() << "SSL_CTX_set_cipher_list " << SCTX_DBGARGS);
    }
    if (params.Suites) {
        Y_REQUIRE(SSL_CTX_set_ciphersuites(ctx->Ctx(), params.Suites) == 1,
                  MakeSslError() << "SSL_CTX_set_ciphersuites " << SCTX_DBGARGS);
    }

    Y_PROPAGATE_ERROR(AddCert(ctx->PrimaryCert_, ctx->Ctx(), params.KeyFile, params.CertFile, params.CaFile, params.ValidateCertDate));

    Y_PROPAGATE_ERROR(ctx->InitServerContext(params.Curves, params.Ja3Enabled));

    return ctx;
}

// TODO(tender-bum): return TErrorOr
TSslClientContext NPrivate::TSslContextAccessor::CreateClientCtx(const char* ciphers, const char* suites) {
    TSslContext originalCtx;
    ui64 protocols =
        SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 | SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2 | SSL_OP_NO_TLSv1_3;
    TryRethrowError(CreateSslCtx(SSLv23_client_method(), protocols).AssignTo(originalCtx));
    TSslClientContext ctx(std::move(originalCtx));

    if (ciphers) {
        Y_ENSURE_EX(SSL_CTX_set_cipher_list(ctx.Ctx(), ciphers) == 1,
                    MakeSslError() << "SSL_CTX_set_cipher_list " << DBG_PS(ciphers));
    }

    if (suites) {
        Y_ENSURE_EX(SSL_CTX_set_ciphersuites(ctx.Ctx(), suites) == 1,
                    MakeSslError() << "SSL_CTX_set_ciphersuites " << DBG_PS(ciphers));
    }
#ifndef NDEBUG
    if (GetEnv("SSLKEYLOGFILE")) {
        SSL_CTX_set_keylog_callback(ctx.Ctx(), [](const SSL* /*ssl*/, const char *line) {
            TBufferedFileOutputEx file{GetEnv("SSLKEYLOGFILE"), ForAppend};
            file << line << Endl;
        });
    }
#endif

    return ctx;
}

TError TSslServerContext::AddSecondaryCert(const char* keyFile, const char* certFile,
                                           const char* caFile, bool validateCertDate) {
    Y_REQUIRE(Ctx() != nullptr,
              yexception{} << "SSL_CTX is not set to add secondary cert");
    return AddCert(SecondaryCert(), Ctx(), keyFile, certFile, caFile, validateCertDate);
}

int TSslServerContext::AlpnCb(ssl_st* ssl, const unsigned char** out, unsigned char* outlen,
                              const unsigned char* in, unsigned int inlen, void* arg)
{
    const TSslServerContext* serverCtx = static_cast<TSslServerContext*>(arg);
    auto* rawSslIo = SSL_get_ex_data(ssl, Default<TSslIoIndex>().Idx);
    Y_VERIFY(rawSslIo != nullptr);

    const auto* sslIo = reinterpret_cast<const TSslIo*>(rawSslIo);
    const auto remoteAddr = sslIo ? sslIo->RemoteAddr() : nullptr;
    const bool expEnabled = sslIo ? sslIo->ExpEnabled() : false;
    const bool cpuLimiterEnabled = sslIo ? sslIo->CpuLimiterEnabled() : true;

    auto* current = in;
    auto left = inlen;
    ui8 itemLen = 0;

    while (current && left) {
        itemLen = ui8(*current);
        if ((itemLen + 1) <= left) {
            ++current;
            --left;

            TStringBuf protoName((const char*)current, itemLen);
            if (cpuLimiterEnabled
                && serverCtx->HasH2(remoteAddr, expEnabled)
                && protoName == "h2"
            ) {
                break;
            } else if (protoName == "http/1.1") {
                break;
            }

            current += itemLen;
            left -= itemLen;
        } else { // malformed ALPN value
            return SSL_TLSEXT_ERR_NOACK;
        }
    }

    if (left == 0) {
        return SSL_TLSEXT_ERR_NOACK;
    }

    if (SSL_select_next_proto(const_cast<unsigned char**>(out), outlen,
                              current - 1, itemLen + 1, in, inlen) != OPENSSL_NPN_NEGOTIATED)
    {
        return SSL_TLSEXT_ERR_NOACK;
    }

    return SSL_TLSEXT_ERR_OK;
}

int TSslServerContext::HelloCb(ssl_st *ssl, int *al, void *arg) {
    Y_UNUSED(al);
    Y_UNUSED(arg);

    auto* rawSslIo = SSL_get_ex_data(ssl, Default<TSslIoIndex>().Idx);
    // Skip ja3 if no TSslIo was found
    if (rawSslIo == nullptr) {
        return SSL_CLIENT_HELLO_SUCCESS;
    }
    auto* sslIo = reinterpret_cast<TSslIo*>(rawSslIo);
    Y_VERIFY(sslIo != nullptr);

    ui32 version = SSL_client_hello_get0_legacy_version(ssl);
    sslIo->SetLegacyVersion(version);

    /**
     * This is only one part that can be optimized because it's stored in the field of
     * SSL ctx. For the unification of methods, it's stored as vector too.
     */
    const ui8* charout = nullptr;
    size_t outlen = SSL_client_hello_get0_ciphers(ssl, &charout);
    if (outlen) {
        sslIo->SetCiphers(charout, outlen);
    }

    /**
     * There is no way to access extensions field from SSL ctx. It will be freed at the
     * end of the handshake. This field should be copied inside this callback.
     */
    int *intout = nullptr;
    int ret = SSL_client_hello_get1_extensions_present(ssl, &intout, &outlen);
    if (ret == 1 && intout != nullptr) {
        sslIo->SetClientExtensions(intout, outlen);
        OPENSSL_free(intout);
    }

    /**
     * Curves extension field could be accessed outside this callback but SSL_get1_curves
     * should be called twice: first time with nullptr as the second arg to get a length
     * and second time with a pointer to the allocated memory. For SSL_get1_curves resulting
     * array will contain a NIDs(number id) which should be converted to curve ids.
     * https://tools.ietf.org/html/rfc4492#section-5.1.1
     * Here we have raw curve ids.
     */
    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_elliptic_curves, &charout, &outlen) == 1) {
        sslIo->SetEllipticCurves(charout, outlen);
    }

    // The same as above but there is no need to convert values.
    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_ec_point_formats, &charout, &outlen) == 1) {
        sslIo->SetEllipticCurvesPointFormats(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_signature_algorithms, &charout, &outlen) == 1) {
        sslIo->SetSignatureAlgorithms(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_signature_algorithms_cert, &charout, &outlen) == 1) {
        sslIo->SetSignatureAlgorithmsCert(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_supported_versions, &charout, &outlen) == 1) {
        sslIo->SetSupportedVersions(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_application_layer_protocol_negotiation, &charout, &outlen) == 1) {
        sslIo->SetApplicationLayerProtocolNegotiation(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_key_share, &charout, &outlen) == 1) {
        sslIo->SetKeyShare(charout, outlen);
    }

    if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_psk_kex_modes, &charout, &outlen) == 1) {
        sslIo->SetPskKeyExchangeModes(charout, outlen);
    }

    return SSL_CLIENT_HELLO_SUCCESS;
}

TError TSslServerContext::InitServerContext(const char* curves, bool ja3Enabled) {
    ssl_ctx_st* const ctx = Ctx();
    Y_ASSERT(ctx);

    SSL_CTX_set_options(ctx, SSL_OP_CIPHER_SERVER_PREFERENCE);

    SSL_CTX_set_alpn_select_cb(ctx, AlpnCb, this);
    if (ja3Enabled) {
        SSL_CTX_set_client_hello_cb(ctx, HelloCb, nullptr);
    }

    Y_PROPAGATE_ERROR(SetEcdhCurve(ctx, curves));
    return SetDhParam(ctx);
}

TError TSslServerContext::SetOcspResponseCallbackImpl(TOcspResponseImplCallback callback, void* data) {
    Y_REQUIRE(SSL_CTX_set_tlsext_status_cb(Ctx(), callback) != 0,
              MakeSslError() << "SSL_CTX_set_tlsext_status_cb");
    Y_REQUIRE(SSL_CTX_set_tlsext_status_arg(Ctx(), data) != 0,
              MakeSslError() << "SSL_CTX_set_tlsext_status_arg");
    return {};
}

TError TSslServerContext::SetServernameCallbackImpl(TServernameImplCallback callback, void* data) {
    Y_REQUIRE(SSL_CTX_set_tlsext_servername_callback(Ctx(), callback) == 1,
              MakeSslError() << "SSL_CTX_set_tlsext_servername_callback");
    Y_REQUIRE(SSL_CTX_set_tlsext_servername_arg(Ctx(), data) == 1,
              MakeSslError() << "SSL_CTX_set_tlsext_servername_arg");
    return {};
}

void TSslServerContext::SetKeylogCallbackImpl(TSetKeylogImplCallback callback, void* data) {
    SSL_CTX_set_keylog_callback(Ctx(), callback);
    SetSslKeylogData(Ctx(), data);
}

int TSslServerContext::ServernameRoutine(ssl_st* ssl, ssl_ctx_st* ctx) noexcept {
    if (!ssl || !ctx) {
        return SSL_TLSEXT_ERR_NOACK;
    }

    // https://st.yandex-team.ru/BALANCER-1144
    if (ctx != SSL_get_SSL_CTX(ssl)) {
        // client_CA list is inherited from ctx in this case
        // but verify parameters are not, so making explicit copies
        // see SSL_get_client_CA_list
        SSL_set_verify(ssl, SSL_CTX_get_verify_mode(ctx), SSL_CTX_get_verify_callback(ctx));
        SSL_set_verify_depth(ssl, SSL_CTX_get_verify_depth(ctx));
    }

    SSL_set_SSL_CTX(ssl, ctx);

    return SSL_TLSEXT_ERR_OK;
}

TStringBuf TSslServerContext::GetServername(ssl_st* s) noexcept {
    if (s) {
        return {SSL_get_servername(s, TLSEXT_NAMETYPE_host_name)};
    }

    return {};
}

TErrorOr<int> TSslServerContext::OcspResponseRoutine(ssl_st* ssl, const TChunk* response) {
    if (!response || response->Length() <= 0) {
        return SSL_TLSEXT_ERR_NOACK;
    }

    void* const ptr = OPENSSL_malloc(response->Length()); // OPENSSL_free is calles on SSL_free

    Y_REQUIRE(ptr != nullptr,
              MakeSslError() << "OPENSSL_malloc");

    memcpy(ptr, response->Data(), response->Length());

    SSL_set_tlsext_status_ocsp_resp(ssl, ptr, response->Length());

    return SSL_TLSEXT_ERR_OK;
}

TError TSslServerContext::SetTicketKeysCallbackImpl(TTicketKeysImplCallback callback, void* data) {
    Y_REQUIRE(SSL_CTX_set_tlsext_ticket_key_cb(Ctx(), callback) != 0,
              MakeSslError() << "SSL_CTX_set_tlsext_ticket_key_cb");
    SetSslTicketKeysData(Ctx(), data);
    return {};
}

void TSslServerContext::SetTimeout(TDuration timeout) noexcept {
    SSL_CTX_set_timeout(Ctx(), timeout.Seconds());
}

void TSslServerContext::SetMaxSendFragment(long maxSendFragment) noexcept {
    // See man SSL_CTX_set_max_send_fragment
    Y_VERIFY(maxSendFragment >= SSL3_RT_MIN_PLAIN_LENGTH && maxSendFragment <= SSL3_RT_MAX_PLAIN_LENGTH);
    SSL_CTX_set_max_send_fragment(Ctx(), maxSendFragment);
}

void TSslContext::DisableSslTicketKeys() noexcept {
    SSL_CTX_set_options(Ctx_.Get(), SSL_OP_NO_TICKET);
}

void TSslContext::EnableSslTicketKeys() noexcept {
    SSL_CTX_clear_options(Ctx_.Get(), SSL_OP_NO_TICKET);
}

const x509_st* TSslContext::GetFirstCert() const noexcept {
    return PrimaryCert_.Get();
}

const x509_st* TSslContext::GetNextCert() const noexcept {
    return SecondaryCert_.Get();
}

TErrorOr<bool> TSslContext::LoadVerifyLocations(const char* caFile) {
    Y_REQUIRE(caFile != nullptr,
              yexception{} << "caFile is nullptr");

    auto ctx = Ctx();
    return SSL_CTX_load_verify_locations(ctx, caFile, nullptr) == 1;
}

TError TSslContext::SetVerifyMode(int mode) noexcept {
    return SslCtxSetVerify(Ctx(), mode);
}

TError TSslContext::SetVerifyPeer() noexcept {
    return SetVerifyMode(SSL_VERIFY_PEER);
}

TError TSslContext::SetVerifyNone() noexcept {
    return SetVerifyMode(SSL_VERIFY_NONE);
}

TError TSslContext::SetVerifyDepth(int depth) {
    Y_REQUIRE(depth >= 0,
              yexception{} << "verify depth is negative: " << depth);
    auto ctx = Ctx();
    SSL_CTX_set_verify_depth(ctx, depth);
    SslClearError();
    return {};
}

TError TSslContext::SetClientCaFile(const char* ca) {
    Y_PROPAGATE_ERROR(CheckFileReadable(ca));
    STACK_OF(X509_NAME)* list = SSL_load_client_CA_file(ca);
    Y_REQUIRE(list != nullptr,
              MakeSslError() << "failed to load client ca file from " << ca);
    ERR_clear_error(); // SSL_load_client_CA_file may leave an error in the error queue
    SSL_CTX_set_client_CA_list(Ctx(), list);
    return {};
}

TError TSslContext::SetClientCrlFile(const char* crlFileName) {
    Y_PROPAGATE_ERROR(CheckFileReadable(crlFileName));
    X509_STORE* st = SSL_CTX_get_cert_store(Ctx());
    Y_REQUIRE(st != nullptr,
              MakeSslError() << "no cert_store found in ctx");

    X509_LOOKUP* lookup = X509_STORE_add_lookup(st, X509_LOOKUP_file());
    Y_REQUIRE(lookup != nullptr,
              MakeSslError() << "no store lookup found");

    Y_REQUIRE(X509_LOOKUP_load_file(lookup, crlFileName, X509_FILETYPE_PEM) != 0,
              MakeSslError() << "failed to load CRL file");

    X509_STORE_set_flags(st, X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL);
    return {};
}

TSslIo::TSslIo(
    const TSslContext& ctx, IIoInput& input, IIoOutput& output,
    const NAddr::IRemoteAddr* remoteAddr, bool expEnabled, bool cpuLimiterEnabled,
    const TSslEarlyDataParams& earlyDataParams
)
    : Input_(&input)
    , Output_(&output)
    , RemoteAddr_(remoteAddr)
    , Ssl_(SSL_new(ctx.Ctx()))
    , Bio_(BIO_new(TBioMethod::Instance()->Method()))
    , ExpEnabled_(expEnabled)
    , CpuLimiterEnabled_(cpuLimiterEnabled)
{
    if (!Ssl_) {
        if (!!Bio_) {
            BIO_free_all(Bio_.Get());
        }
        ythrow MakeSslError() << "SSL_new";
    }

    if (!Bio_) {
        ythrow MakeSslError() << "BIO_new";
    }

    TBioMethod::SetIo(Bio_.Get(), this);

    SSL_set_bio(Ssl_.Get(), Bio_.Get(), Bio_.Get());

    RegisterSslIo();

    // Early Data is available only in TLS >= 1.3
    if (earlyDataParams.Enabled && SSL_version(Ssl_.Get()) >= TLS1_3_VERSION) {
        if (SSL_set_max_early_data(Ssl_.Get(), earlyDataParams.MaxEarlyData) == 1
            && SSL_set_recv_max_early_data(Ssl_.Get(), earlyDataParams.RecvMaxEarlyData) == 1) {

            // Early data is not compatible with the current realisation of an internal cache
            // For more details please read https://github.com/openssl/openssl/issues/6313
            SSL_set_options(Ssl_.Get(), SSL_OP_NO_ANTI_REPLAY);

            EarlyDataEnabled_ = true;
            EarlyDataProcessed_ = false;
        }
    }
}

TSslIo::~TSslIo() {
    if (Input_ && Output_) {
        Shutdown();
    }
}

TError TSslIo::Connect(TInstant deadline) {
    RecvDeadline_ = deadline;
    SendDeadline_ = deadline;
    if (SSL_connect(Ssl_.Get()) != 1) {
        Y_PROPAGATE_ERROR(std::move(StoredError));
        return Y_MAKE_ERROR(MakeSslError() << "SSL_connect");
    }
    Established_ = true;
    return {};
}

TError TSslIo::Accept() {
    if (EarlyDataEnabled_) {
        size_t nread = 0;

        // SSL_read_early_data has three states:
        //   SSL_READ_EARLY_DATA_ERROR - failure, break the loop
        //   SSL_READ_EARLY_DATA_FINISH - no new data, break the loop
        //   SSL_READ_EARLY_DATA_SUCCESS - more data is available
        // On SSL_READ_EARLY_DATA_FINISH connection should be completed
        // by SSL_accept or SSL_do_handshake.
        while (true) {
            TChunkPtr chunk = NewChunkReserve();
            const int ret = SSL_read_early_data(Ssl_.Get(), chunk->Data(), chunk->Length(), &nread);

            if (ret == SSL_READ_EARLY_DATA_ERROR) {
                switch (SSL_get_error(Ssl_.Get(), ret)) {
                    // handshake is not ready or we have more data
                    case SSL_ERROR_WANT_WRITE:
                    case SSL_ERROR_WANT_ASYNC:
                    case SSL_ERROR_WANT_READ:
                        continue;
                }
                // break the loop outside switch.
                // More readable if statement
                break;
            } else if (ret == SSL_READ_EARLY_DATA_FINISH) {
                // On initial connection browsers sends the early
                // data extension with a zero-length data.
                // Safari(<=13.1) doesn't send any data on the
                // next connections.
                if (EarlyDataChunkList_.ChunksCount() > 0) {
                    EarlyDataFinished_ = true;
                }
                break;
            }

            // On the first call of SSL_read_early_data state is
            // SSL_EARLY_DATA_REJECTED. Open SSL calls post-processing
            // routine to set the state to SSL_EARLY_DATA_ACCEPTED
            // in normal case. On the second call of SSL_read_early_data
            // will return SSL_READ_EARLY_DATA_FINISH
            if (nread > 0) {
                chunk->Shrink(nread);
                EarlyDataChunkList_.Push(std::move(chunk));
            }
        }
    }

    Y_REQUIRE(SSL_accept(Ssl_.Get()) == 1,
              MakeSslError() << "SSL_accept");
    Established_ = true;
    return {};
}

void TSslIo::Shutdown() noexcept {
    try {
        if (Established_ && Ssl_.Get()) {
            if (SSL_shutdown(Ssl_.Get()) == 0) {
                SSL_shutdown(Ssl_.Get());
            }
        }
    } catch (...) {
    }
    Established_ = false;
}

int TSslIo::GetShutdown() noexcept {
    Y_ASSERT(Ssl_.Get());
    return SSL_get_shutdown(Ssl_.Get());
}

TString TSslIo::CipherName() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    auto cipher = SSL_get_current_cipher(Ssl_.Get());
    if (cipher == nullptr) {
        return TString();
    }

    return TString(SSL_CIPHER_get_name(cipher));
}

ui16 TSslIo::CipherId() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    auto cipher = SSL_get_current_cipher(Ssl_.Get());
    if (cipher == nullptr) {
        return 0;
    }

    return SSL_CIPHER_get_id(cipher) & 0xFFFF;
}

TString TSslIo::ClientRandom() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    unsigned char buf[SSL3_RANDOM_SIZE];
    SSL_get_client_random(Ssl_.Get(), buf, Y_ARRAY_SIZE(buf));

    return HexEncode(buf, Y_ARRAY_SIZE(buf));
}

TString TSslIo::ServerRandom() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    unsigned char buf[SSL3_RANDOM_SIZE];
    SSL_get_server_random(Ssl_.Get(), buf, Y_ARRAY_SIZE(buf));

    return HexEncode(buf, Y_ARRAY_SIZE(buf));
}

TString TSslIo::MasterSecret() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    SSL_SESSION* sess = SSL_get_session(Ssl_.Get());
    if (sess == nullptr) {
        return TString();
    }

    const size_t keyLen = SSL_SESSION_get_master_key(sess, NULL, 0);
    if (Y_UNLIKELY(keyLen == 0)) {
        return TString();
    }

    TTempBuf masterKey(keyLen);

    SSL_SESSION_get_master_key(sess, reinterpret_cast<unsigned char*>(masterKey.Data()), keyLen);

    return HexEncode(masterKey.Data(), keyLen);
}

TString TSslIo::SessionId() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    SSL_SESSION* sess = SSL_get_session(Ssl_.Get());
    if (sess == nullptr) {
        return TString();
    }

    unsigned int len;
    const unsigned char* id = SSL_SESSION_get_id(sess, &len);
    if (!id || !len) {
        return TString();
    }

    return HexEncode(id, len);
}

TString TSslIo::SslVersion() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    return SSL_get_version(Ssl_.Get());
}

NSsl::EProtocolVersion TSslIo::GetProtocolVersion() const {
    const SSL_SESSION* sess = SSL_get_session(Ssl_.Get());
    return sess ? static_cast<NSsl::EProtocolVersion>(SSL_SESSION_get_protocol_version(sess)) : NSsl::EProtocolVersion::NoCipher;
}

EClientProto TSslIo::AlpnProto() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    const unsigned char* data = nullptr;
    unsigned len = 0;
    SSL_get0_alpn_selected(Ssl_.Get(), &data, &len);

    TStringBuf proto{(const char*)data, len};
    if ("h2" == proto) {
        return EClientProto::CP_HTTP2;
    } else {
        return EClientProto::CP_HTTP;
    }
}

TMaybe<TString> TSslIo::ClientCertField(TStringBuf fieldName) const {
    TMaybe<TString> subject = ClientCertSubject();
    if (!subject) {
        return subject;
    }

    for (auto& it : StringSplitter(*subject).Split('/').SkipEmpty()) {
        auto token = it.Token();
        if (token.StartsWith(fieldName)) {
            auto field = token.Tail(fieldName.size() + 1); // 1 for = in "fieldName="
            if (field) {
                return MakeMaybe(TString(field));
            }
        }
    }

    return TMaybe<TString>();
}

TMaybe<TString> TSslIo::ClientCertCN() const {
    return ClientCertField("CN");
}

TMaybe<TString> TSslIo::ClientCertSubject() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    const TX509Holder clientCert{ SSL_get_peer_certificate(Ssl_.Get()) };
    return X509CertName(clientCert);
}

TMaybe<TString> TSslIo::ClientCertSerialNumber() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    const TX509Holder clientCert{ SSL_get_peer_certificate(Ssl_.Get()) };
    return X509SerialNumber(clientCert);
}

TMaybe<long> TSslIo::ClientCertVerifyResult() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    return MakeMaybe(SSL_get_verify_result(Ssl_.Get()));
}

bool TSslIo::ClientCertPresent() const {
    Y_ASSERT(Established_);
    Y_ASSERT(Ssl_.Get());

    const TX509Holder clientCert{ SSL_get_peer_certificate(Ssl_.Get()) };
    return !!clientCert;
}

const ssl_ctx_st* TSslIo::CtxRaw() const noexcept {
    Y_ASSERT(!!Ssl_);
    return SSL_get_SSL_CTX(Ssl_.Get());
}

TError TSslIo::DoRecv(TChunkList& lst, TInstant deadline) noexcept {
    // After the handshake, we shouldn't call SSL_read early data.
    // Browsers send the full request in early data except for strange
    // state in Chrome when a browser sends only PRI in the first packet
    // without HEADERS frame in the second packet. For this case,
    // EarlyDataProcessed_ is used but I can't reproduce it.
    if (EarlyDataEnabled_ && EarlyDataFinished_ && !EarlyDataProcessed_) {
        lst.Append(std::move(EarlyDataChunkList_));
        EarlyDataProcessed_ = true;
        return {};
    }

    TChunkPtr chunk = NewChunkReserve();

    RecvDeadline_ = deadline;
    const int ret = SSL_read(Ssl_.Get(), chunk->Data(), chunk->Length());

    if (ret < 0) {
        Y_PROPAGATE_ERROR(std::move(StoredError));
        return Y_MAKE_ERROR(TSslError{MakeSslError() << "SSL_read"});
    }

    if (ret > 0) {
        chunk->Shrink(ret);
        lst.Push(std::move(chunk));
    }

    return {};
}

TError TSslIo::SetSniServername(const char* serverName) {
    Y_REQUIRE(serverName != nullptr,
              yexception{} << "setting sni servername to NULL is forbidden");
    Y_REQUIRE(SSL_set_tlsext_host_name(Ssl_.Get(), serverName),
              MakeSslError() << "SSL_set_tlsext_host_name(" << serverName << ") failed");
    return {};
}

TError TSslIo::SetClientAlpn(const ui8 *protocols, size_t sz) {
    Y_REQUIRE(SSL_set_alpn_protos(Ssl_.Get(), protocols, sz) == 0,
              MakeSslError() << "SSL_set_alpn_protos() failed");
    return {};
}

TError TSslIo::DoSend(TChunkList lst, TInstant deadline) noexcept {
    auto chunk = Union(lst);

    SendDeadline_ = deadline;
    const int ret = SSL_write(Ssl_.Get(), chunk->Data(), chunk->Length());

    if (ret < 0) {
        Y_PROPAGATE_ERROR(std::move(StoredError));
        return Y_MAKE_ERROR(TSslError{MakeSslError() << "SSL_write"});
    }
    return {};
}

void TSslIo::RegisterSslIo() noexcept {
    if (SSL_set_ex_data(Ssl_.Get(), Default<TSslIoIndex>().Idx, this) == 0) {
        Y_FAIL("SSL_CTX_set_ex_data");
    }
}

void TSslIo::TDestroy::Destroy(ssl_st* ssl) noexcept {
    SSL_free(ssl);
}

void TSslIo::TDestroy::Destroy(bio_st* bio) noexcept {
    // TODO: bio is freed with SSL_free, but it is exception-unsafe in between, which may lead to mem leak
    Y_UNUSED(bio);
    //BIO_free(bio);
}

bool CheckOcspResponse(const TChunkPtr& data) noexcept {
    if (!data || data->Length() == 0) {
        return false;
    }
    /*
     * Maybe BIO(memory), then - read response, then read and convert it all
     */
    struct TOcspDestroyer {
        static void Destroy(OCSP_RESPONSE* resp) noexcept {
            OCSP_RESPONSE_free(resp);
        }
    };

    const unsigned char* d = reinterpret_cast<const unsigned char*>(data->Data());

    const THolder<OCSP_RESPONSE, TOcspDestroyer> response(d2i_OCSP_RESPONSE(nullptr, &d, data->Length()));

    if (!response) {
        return false;
    }

    const size_t len = i2d_OCSP_RESPONSE(response.Get(), nullptr);

    return len > 0;
}

#undef SCTX_DBGARGS
#undef DBG_PS

}  // namespace NSrvKernel
