#include "sslitem.h"

#include <balancer/kernel/process/thread_info.h>

#include <util/system/thread.h>
#include <util/thread/singleton.h>
#include <util/system/env.h>

namespace NModSsl {
    TSslItem::TSslItem(
        const NFH::TFlatHashMap<ui64, TVector<TSslItem>>& items,
        ui64 section, const TString &name, const NSrvKernel::TModuleParams &mp, bool ja3Enabled, bool validateCertDate)
        : TSslItemBase(mp.Control->GetCountOfChildren() + 1)
        , Items_(items)
        , ItemsSection_(section)
        , Name_(name)
        , Ja3Enabled_(ja3Enabled)
        , ValidateCertDate_(validateCertDate)
        , Selected_(mp.Control->SharedStatsManager().MakeCounter("ssl_sni-ssl_item-" + Name_ + "-servername_selected").AllowDuplicate().Build())
    {
        // Preparse primary cert info
        PrimaryCertData_.Reset(
            ParseSslItemSettings(
                mp.Config,
                [this](const TString &name, const TString &regexp) { DoConsumeEventPrimary(name, regexp); },
                "primary",
                mp.Control->GetCountOfChildren()
            )
        );

        // Parse all other fields
        NSrvKernel::ParseMap(mp.Config, [this, mp](const auto &key, auto *value) {
            {
                ON_KEY("ciphers", Ciphers_) {
                    return;
                }

                ON_KEY("suites", Suites_) {
                    return;
                }

                ON_KEY("ecdh_curves", Curves_) {
                    return;
                }

                if (key == "timeout") {
                    Timeout_ = FromString<TDuration>(value->AsString());
                    return;
                }

                ON_KEY("log", LogName_) {
                    NeedLog_ = true;
                    mp.Control->CreateLog(LogName_);
                    return;
                }

                ON_KEY("log_ciphers_stats", LogCiphersStats_) {
                    return;
                }

                ON_KEY("secrets_log", SecretsLogName_) {
                    NeedSecretLog_= true;
                    return;
                }

                ON_KEY("secrets_log_freq", SecretsLogFreqDefault_) {
                    return;
                }

                ON_KEY("secrets_log_freq_file", SecretsLogFreqFileName_) {
                    return;
                }

                ON_KEY("disable_sslv3", DisableSslv3_) {
                    return;
                }

                ON_KEY("disable_tlsv1_3", DisableTlsv1_3_) {
                    return;
                }

                ON_KEY("priority", Priority_) {
                    return;
                }

                ON_KEY("ticket_keys_validate", SslTicketsValidate_) {
                    return;
                }

                if (key == "servername") {
                    Matcher_ = ConstructMatcher(value->AsSubConfig());
                    return;
                }

                if (key == "ticket_keys_list") {
                    Tickets = NModSsl::ParseTickets(value->AsSubConfig());
                    return;
                }

                if (key == "client") {
                    ClientCertSettings_ = NModSsl::ParseSettings(value->AsSubConfig());
                    return;
                }

                if (key == "secondary") {
                    SecondaryCertData_.Reset(
                        ParseSslItemSettings(
                            value->AsSubConfig(),
                            [this](const TString &name, const TString &regexp) {
                                DoConsumeEventSecondary(name, regexp);
                            },
                            "secondary",
                            mp.Control->GetCountOfChildren()
                        )
                    );
                    return;
                }

                if (key == "ssl_protocols") {
                    SslProtocols_ = 0;
                    NSrvKernel::ParseMap(value->AsSubConfig(), [this](const auto &key, auto *val) {
                        Y_UNUSED(key);
                        if (AsciiEqualsIgnoreCase(val->AsString(), "sslv2")) {
                            SslProtocols_ |= SSL_OP_NO_SSLv2;
                        } else if (AsciiEqualsIgnoreCase(val->AsString(), "sslv3")) {
                            SslProtocols_ |= SSL_OP_NO_SSLv3;
                        } else if (AsciiEqualsIgnoreCase(val->AsString(), "tlsv1")) {
                            SslProtocols_ |= SSL_OP_NO_TLSv1;
                        } else if (AsciiEqualsIgnoreCase(val->AsString(), "tlsv1.1")) {
                            SslProtocols_ |= SSL_OP_NO_TLSv1_1;
                        } else if (AsciiEqualsIgnoreCase(val->AsString(), "tlsv1.2")) {
                            SslProtocols_ |= SSL_OP_NO_TLSv1_2;
                        } else if (AsciiEqualsIgnoreCase(val->AsString(), "tlsv1.3")) {
                            SslProtocols_ |= SSL_OP_NO_TLSv1_3;
                        }
                    });
                    return;
                }

                ON_KEY("earlydata_enabled", ItemEarlyDataParams_.Enabled) {
                    return;
                }

                ON_KEY("earlydata_max", ItemEarlyDataParams_.MaxEarlyData) {
                    return;
                }

                ON_KEY("earlydata_recv_max", ItemEarlyDataParams_.RecvMaxEarlyData) {
                    return;
                }

                // This values skipped because they were processed in ParseSslItemSettings for primary cert
                // May be move them to primary = {} section as for secondary cert?
                if (key == "cert" || key == "priv" || key == "ca" || key == "ocsp" || key == "ocsp_file_switch" ||
                    key == "events") {
                    return;
                }
            }
        });

#ifndef NDEBUG
        auto envSecretLog = GetEnv("SSLKEYLOGFILE");
        if (envSecretLog) {
            NeedSecretLog_ = true;
            SecretsLogName_ = envSecretLog;
        }
#endif

        if (NeedSecretLog_) {
            mp.Control->CreateLog(SecretsLogName_);
        }

        NSrvKernel::InitSsl();

        if (Ciphers_.empty()) {
            ythrow NConfig::TConfigParseError() << "no ciphers set";
        }

        if (DisableSslv3_) {
            SslProtocols_ ^= SSL_OP_NO_SSLv3;
        }

        if (!DisableTlsv1_3_) {
            SslProtocols_ |= SSL_OP_NO_TLSv1_3;
        }

        if (!PrimaryCertData_) {
            ythrow NConfig::TConfigError()
                << "empty primary certificate";
        }

        if (!IsDefault() && !Matcher_) {
            ythrow NSrvKernel::TConfigParseError() << "no matcher configured";
        } else if (IsDefault() && Matcher_) {
            ythrow NSrvKernel::TConfigParseError() << "servername matcher for default";
        }

        // Here we set wildcard mask for "default" section
        // which will be selected in MatchCtx if no section was matched
        if (IsDefault()) {
            Priority_ = -1;
            Matcher_.Reset(new NModSsl::TAllServersMatcher());
        } else if (Priority_ < 0) {
            ythrow NConfig::TConfigParseError() << "negative priority";
        }

        // Since TSslItem is being created only in config parsing, it's not required to
        // propagate TError from this point.
        CtxInitParams_.Reset(MakeHolder<NSrvKernel::TSslServerCtxInitParams>(
            SslProtocols_,
            Ciphers_.data(),
            Suites_.data(),
            !Curves_.empty() ? Curves_.data() : nullptr,
            PrimaryCertData_->Priv.data(),
            PrimaryCertData_->Cert.data(),
            !!PrimaryCertData_->Ca ? PrimaryCertData_->Ca.data() : nullptr,
            Ja3Enabled_,
            ValidateCertDate_));

        THolder<NSrvKernel::TSslServerContext> ctx;
        NSrvKernel::TryRethrowError(
            NSrvKernel::CreateSslServerCtx(*CtxInitParams_).AssignTo(ctx)
        );

        if (SecondaryCertData_) {
            // Since TSslItem is being created only in config parsing, it's not required to
            // propagate TError from this point.
            AddSecondaryCert(ctx.Get());
        }

        if (ClientCertSettings_) {
            if (!PrimaryCertData_->Ca) {
                ythrow NConfig::TConfigError()
                    << "\"client\" settings require \"ca\" to be set for primary cert";
            }
            if (SecondaryCertData_ && SecondaryCertData_->Ca) {
                ythrow NConfig::TConfigError()
                    << "\"client\" settings require \"ca\" to be set for primary cert and not for the secondary";
            }
            // Since TSslItem is being created only in config parsing, it's not required to
            // propagate TError from this point.
            SetClientSettings(ctx.Get());
            HasClientCert_ = true;
        }

        if (ItemEarlyDataParams_.MaxEarlyData == 0 || ItemEarlyDataParams_.RecvMaxEarlyData == 0) {
            ItemEarlyDataParams_.Enabled = false;
        }
    }

    void TSslItem::AddSecondaryCert(NSrvKernel::TSslServerContext* ctx) const {
        NSrvKernel::TryRethrowError(ctx->AddSecondaryCert(
            SecondaryCertData_->Priv.data(),
            SecondaryCertData_->Cert.data(),
            !!SecondaryCertData_->Ca ? SecondaryCertData_->Ca.data() : nullptr,
            ValidateCertDate_
        ));
    }

    void TSslItem::SetClientSettings(NSrvKernel::TSslServerContext* ctx) const {
        NSrvKernel::TryRethrowError(ctx->LoadVerifyLocations(PrimaryCertData_->Ca.data()).ReleaseError());
        NSrvKernel::TryRethrowError(ctx->SetVerifyMode(ClientCertSettings_->VerifyMode));
        NSrvKernel::TryRethrowError(ctx->SetVerifyDepth(ClientCertSettings_->VerifyDepth));
        NSrvKernel::TryRethrowError(ctx->SetClientCaFile(PrimaryCertData_->Ca.data()));
        if (ClientCertSettings_->Crl) {
            NSrvKernel::TryRethrowError(ctx->SetClientCrlFile(ClientCertSettings_->Crl.data()));
        }
    }

    THolder<TSslItemTls> TSslItem::DoInit(NSrvKernel::IWorkerCtl& process) noexcept {
        auto tls = MakeHolder<TSslItemTls>(NSrvKernel::TSharedCounter(Selected_, process.WorkerId()));
        if (process.WorkerType() != NSrvKernel::NProcessCore::TChildProcessType::Default) {
            return tls;
        }

        NSrvKernel::TryRethrowError(
            NSrvKernel::CreateSslServerCtx(*CtxInitParams_).AssignTo(tls->Ctx)
        );

        PrimaryCertData_->GetTls(process).RawCert = tls->Ctx->GetFirstCert();
        if (SecondaryCertData_) {
            AddSecondaryCert(tls->Ctx.Get());
            SecondaryCertData_->GetTls(process).RawCert = tls->Ctx->GetNextCert();
        }

        if (ClientCertSettings_) {
            SetClientSettings(tls->Ctx.Get());
        }

        if (Timeout_ != TDuration::Zero()) {
            tls->Ctx->SetTimeout(Timeout_);
        }

        if (MaxSendFragment_.Defined()) {
            tls->Ctx->SetMaxSendFragment(*MaxSendFragment_);
        }

        if (Protos_) {
            tls->Ctx->EnableH2();
            tls->Ctx->SetAlpnProtos(Protos_);
        }

        if (NeedLog_) {
            tls->Log = process.GetLog(LogName_);
        }
        if (NeedSecretLog_) {
            tls->SecretsLog = process.GetLog(SecretsLogName_);
        }
        tls->Queue = process.ThreadedQueue(Name_);
        if (!!PrimaryCertData_->Ocsp || (SecondaryCertData_ && SecondaryCertData_->Ocsp)) {
            // Since TSslItem is being created only in config parsing, it's not required to
            // propagate TError from this point.
            NSrvKernel::TryRethrowError(tls->Ctx->SetOcspResponseCallback<OcspResponseCallback>(this));
        }

        if (!Tickets.empty()) {
            // Since TSslItem is being created only in config parsing, it's not required to
            // propagate TError from this point.
            NSrvKernel::TryRethrowError(tls->Ctx->SetTicketKeysCallback<SslTicketKeysCallback>(this));
        }

        // When client uses SNI we check if there is any other TSslItem with proper servername.
        // If context was found we replace current ssl ctx with proper context from
        // another TSslItem in ServernameCallback.
        NSrvKernel::TryRethrowError(tls->Ctx->SetServernameCallback<ServernameCallback>(this));

        if (NeedSecretLog_) {
            // Use generic way to get NSS keylog line
            tls->Ctx->SetKeylogCallback<KeylogCallback>(this);
        }

        if (PrimaryCertData_ && PrimaryCertData_->Ocsp) {
            PrimaryCertData_->GetTls(process).OcspResponseLoader = NSrvKernel::TCoroutine{
                NSrvKernel::ECoroType::Service, "ocsp_response_loader", &process.Executor(), [this, &process] {
                    DoReloadOcspResponse(*PrimaryCertData_, process);
            }};
            if (PrimaryCertData_->OcspFileSwitch) {
                PrimaryCertData_->GetTls(process).OcspFileSwitchChecker = process.SharedFiles()->FileChecker(
                    PrimaryCertData_->OcspFileSwitch, TDuration::Seconds(1));
            }
        }

        if (SecondaryCertData_ && SecondaryCertData_->Ocsp) {
            SecondaryCertData_->GetTls(process).OcspResponseLoader = NSrvKernel::TCoroutine{
                NSrvKernel::ECoroType::Service, "ocsp_response_loader", &process.Executor(), [this, &process] {
                    auto cert = SecondaryCertData();
                    if (cert) {
                        DoReloadOcspResponse(*cert, process);
                    }
            }};
            if (SecondaryCertData_->OcspFileSwitch) {
                SecondaryCertData_->GetTls(process).OcspFileSwitchChecker = process.SharedFiles()->FileChecker(
                    SecondaryCertData_->OcspFileSwitch, TDuration::Seconds(1));
            }
        }

        if (!Tickets.empty()) {
            tls->TicketKeysLoader = NSrvKernel::TCoroutine{NSrvKernel::ECoroType::Service, "ticket_keys_loader", &process.Executor(), [this] {
                DoReloadTicketKeys();
            }};
        }

        tls->SecretsLogFreq = NSrvKernel::TWatchedState<double>(
            SecretsLogFreqDefault_, SecretsLogFreqFileName_, *process.SharedFiles());

        return tls;
    }

    void TSslItem::DoConsumeEventPrimary(const TString& handler, const TString& regexp) {
        if (handler == "reload_ticket_keys") {
            // Replace this with macro?
            void (TSslItem::*fn)(NSrvKernel::TEventData&) noexcept = &TSslItem::ReloadTicketKeys;
            Events_.emplace_back(regexp, "ssl_item.ReloadTicketKeys", fn);
            return;
        }

        if (handler == "force_reload_ticket_keys") {
            void (TSslItem::*fn)(NSrvKernel::TEventData&) noexcept = &TSslItem::ForceReloadTicketKeys;
            Events_.emplace_back(regexp, "ssl_item.ForceReloadTicketKeys", fn);
            return;
        }

        if (handler == "reload_ocsp_response") {
            void (TSslItem::*fn)(NSrvKernel::TEventData&) noexcept = &TSslItem::ReloadPrimaryOcspResponse;
            Events_.emplace_back(regexp, "ssl_item.ReloadOcspResponse", fn);
            return;
        }

        ythrow NConfig::TConfigParseError() << "unsupported handler(" << handler.Quote() << ')';
    }

    void TSslItem::DoConsumeEventSecondary(const TString& handler, const TString& regexp) {
        if (handler == "reload_ocsp_response") {
            void (TSslItem::*fn)(NSrvKernel::TEventData&) noexcept = &TSslItem::ReloadSecondaryOcspResponse;
            Events_.emplace_back(regexp, "ssl_item.ReloadSecondaryOcspResponse", fn);
            return;
        }

        ythrow NConfig::TConfigParseError() << "unsupported handler(" << handler.Quote() << ')';
    }


    /* Tickets reload */

    void TSslItem::ReloadTicketKeys(IOutputStream& out, bool forceReload) noexcept {
        Y_ASSERT(FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);
        auto& tls = GetTls(*FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);

        TSslTicketKeyStorage newKeys(tls.SslTicketKeys.Ctx ? tls.SslTicketKeys.Ctx : tls.Ctx->Ctx());

        if (forceReload && tls.Log) {
            *tls.Log << TLOG_ERR << Now().ToStringLocal()
                  << " forcing reload tls tickets\n";
        }

        // For validation mode, we copy old values for restoration
        // For backward compatibility leave newKeys empty to treat it as a new key list.
        if (SslTicketsValidate_ && !forceReload) {
            newKeys = tls.SslTicketKeys;
        }

        // Tickets are already sorted by priority field
        for (size_t idx = 0; idx < Tickets.size(); ++idx) {
            out << "Reloading " << Tickets[idx].FileName << "...";

            Y_TRY(NSrvKernel::TError, error) {
                TList<TSslTicketKey> keys{};

                Y_PROPAGATE_ERROR(ThreadedReadTicketsKey(
                    Tickets[idx].FileName.data(), tls.Queue, TInstant::Max()).AssignTo(keys));

                // If Keys list is empty than just append new values
                if (newKeys.Keys.size() > idx) {
                    newKeys.Keys[idx] = std::move(keys);
                } else {
                    newKeys.Keys.push_back(std::move(keys));
                }

                out << " OK\r\n";
                return {};
            } Y_CATCH {
                out << " failed\r\n";
                if (tls.Log) {
                    *tls.Log << TLOG_ERR << Now().ToStringLocal()
                          << " failed to read tls key " << Tickets[idx].FileName
                          << " with error " << error->what() << "\n";
                }
            }
        }

        bool isValid = true;
        if (SslTicketsValidate_) {
            isValid = ValidateReloadTicketKeys(out, newKeys, forceReload, tls);
        }

        if (isValid) {
            std::swap(newKeys, tls.SslTicketKeys);

            if (SslTicketsValidate_ && tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << " validatation of tls session keys was successfull\n";
            }
        }

        // Use old keys if validation failed
        if (tls.SslTicketKeys.Default()) {
            tls.Ctx->EnableSslTicketKeys();
            ++tls.SslTicketsReloadOkCount;
            if (tls.Log) {
                // For new keys log message will be "reloaded"
                // For old "restored"
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << " tls session ticket key was " << (isValid ? "reloaded" : "restored") << " successfully, enabling tls session ticket extension\n";
            }
        } else {
            tls.Ctx->DisableSslTicketKeys();
            ++tls.SslTicketsReloadFailCount;
            if (tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << " fail in reloading tls session ticket key: disabling tls session ticket extension\n";
            }
        }
    }

    bool TSslItem::ValidateReloadTicketKeys(IOutputStream &out, const TSslTicketKeyStorage& newKeys, bool forceReload, TSslItemTls& tls) noexcept {
        bool isValid = true;

        // First check if all keys were loaded
        if (newKeys.Keys.size() != Tickets.size()) {
            isValid = false;
            ++tls.SslTicketsValidationFailCount;
            out << "Validation failed. Updated " << newKeys.Keys.size() << " tickets of " << Tickets.size() << "\r\n";
            if (tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << " failed to validate ticket keys updated " << newKeys.Keys.size() << " keys of " << Tickets.size() << "\n";
            }
        }

        // We do not validate shift by one on force reload and first load(empty SslTicketKeys_.Keys)
        if (isValid && !forceReload && !tls.SslTicketKeys.Keys.empty()) {
            if (!newKeys.Default() || !tls.SslTicketKeys.Default()) {
                // Should never happen
                isValid = false;
                ++tls.SslTicketsValidationFailCount;
                if (tls.Log) {
                    *tls.Log << TLOG_ERR << Now().ToStringLocal()
                          << " empty pointers for tls default keys\n";
                }
            } else if (*newKeys.Default() != *tls.SslTicketKeys.Default()) {
                // Always treat the first value as valid
                out << "Validating " << Tickets[0].FileName << "... OK\r\n";

                // Check shift by one
                size_t iterEnd = tls.SslTicketKeys.Keys.size() - 1;
                for (size_t i = 0; i < iterEnd; ++i) {
                    out << "Validating " << Tickets[i + 1].FileName << "...";

                    bool eq = std::equal(
                        newKeys.Keys[i + 1].begin(), newKeys.Keys[i + 1].end(),
                        tls.SslTicketKeys.Keys[i].begin(), tls.SslTicketKeys.Keys[i].end());
                    // If validation is failed we continue iteration
                    // to output do other values are valid
                    if (!eq) {
                        isValid = false;
                        out << " failed\r\n";
                        if (tls.Log) {
                            *tls.Log << TLOG_ERR << Now().ToStringLocal()
                                  << " keys shifted incorrectly. Key "
                                  << Tickets[i + 1].FileName
                                  << " is broken\n";
                        }
                    } else {
                        out << " OK\r\n";
                    }
                }

                if (!isValid) {
                    ++tls.SslTicketsValidationFailCount;
                }
            } else {
                // When the first value is not changed we can continue
                // because if client ticket was not found or outdate
                // we should renew ticket with first key
                out << "Validation ... OK\r\n";
            }
        }

        return isValid;
    }

    void TSslItem::ReloadTicketKeys(NSrvKernel::TEventData& event) noexcept {
        ReloadTicketKeys(event.RawOut(), false);
    }

    void TSslItem::ForceReloadTicketKeys(NSrvKernel::TEventData& event) noexcept {
        ReloadTicketKeys(event.RawOut(), true);
    }

    void TSslItem::DoReloadTicketKeys() noexcept {
        ReloadTicketKeys(Cnull, false);
    }

    /* End tickets reload */

    /* OCSP reload */

    void TSslItem::ReloadPrimaryOcspResponse(NSrvKernel::TEventData& event) noexcept {
        ReloadOcspResponseFor(*PrimaryCertData_.Get(), event.RawOut());
    }

    void TSslItem::ReloadSecondaryOcspResponse(NSrvKernel::TEventData& event) noexcept {
        if (SecondaryCertData_) {
            ReloadOcspResponseFor(*SecondaryCertData_.Get(), event.RawOut());
        }
    }

    void TSslItem::ReloadOcspResponseFor(TSslItemSettings& data, IOutputStream& out) noexcept {
        out << TThread::CurrentThreadNumericId() << " Reloading " << data.Ocsp << "...";
        Y_ASSERT(FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);
        if (DoReloadOcspResponse(data, *FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess)) {
            out << " OK\n";
        } else {
            out << " failed\n";
        }
    }

    bool TSslItem::DoReloadOcspResponse(TSslItemSettings& data, const NSrvKernel::IWorkerCtl& process) noexcept {
        NSrvKernel::TChunkPtr ocspResponse;
        auto& settingsTls = data.GetTls(process);
        auto& tls = GetTls(process);

        bool ret = false;
        Y_TRY(NSrvKernel::TError, error) {
            Y_PROPAGATE_ERROR(NSrvKernel::ThreadedFileReadAll(
                data.Ocsp, tls.Queue, MaxOcspFileSize_, TInstant::Max()).AssignTo(ocspResponse));
            if (CheckOcspResponse(ocspResponse)) {
                settingsTls.OcspResponse.Swap(ocspResponse);
                if (tls.Log) {
                    *tls.Log << TLOG_ERR << Now().ToStringLocal()
                          << ' ' << TThread::CurrentThreadNumericId() << " ocsp response check ok, storing ocsp response"
                          << ((!settingsTls.OcspResponse || settingsTls.OcspResponse->Length() == 0) ? " (empty)\n" : "\n");
                }
                ret = true;
                return NSrvKernel::TError{};
            } else {
                if (tls.Log) {
                    *tls.Log << TLOG_ERR << Now().ToStringLocal()
                          << ' ' << TThread::CurrentThreadNumericId() << " ocsp response check failed, using the previous response"
                          << ((!settingsTls.OcspResponse || settingsTls.OcspResponse->Length() == 0) ? " (empty)\n" : "\n");
                }
            }
            return {};
        } Y_CATCH {
            if (tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << ' ' << TThread::CurrentThreadNumericId() << " ocsp response reload failed, error reading file \""
                      << data.Ocsp << "\": " << error->what() << '\n';
            }
        }
        return ret;
    }

    void TSslItem::LogCipherStat(const NSrvKernel::TConnDescr& descr, const NSrvKernel::TSslIo& io) const {
        auto& tls = GetTls(descr.Process());

        ++(tls.CiphersStat[io.CipherName()]);
        if (LogCiphersStats_) {
            TStringStream ss;
            ss.Reserve(1024);
            for (const auto& [key, value]: tls.CiphersStat) {
                ss << "cipher name = " << (key.empty() ? "unknown" : key) << " " << value << "\n";
            }
            *tls.Log << ss.Str();
        }
    }

    bool TSslItem::Match(const TStringBuf name) const noexcept {
        return Matcher_->Match(name);
    }

    ssl_ctx_st* TSslItem::MatchCtx(const TStringBuf servername) const {
        // TODO: change ownership of the contexts
        const TSslItem* slave = nullptr;
        int prio = -1;

        auto* process = FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess;
        Y_ASSERT(process);
        auto& tls = GetTls(*process);

        // Choose context. It could be exp_id or default contexts section
        const auto& items = Items_.find(ItemsSection_);
        if (items == Items_.end()) {
            if (tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << ' ' << TThread::CurrentThreadNumericId() << " could not find section for server name "
                      << servername << "\n";
            }

            return nullptr;
        }
        for (const auto& it : items->second) {
            // Iterate through all items in section and check all regexps
            if ((it.Priority() > prio) && it.Match(servername)) {
                slave = &it;
                prio = it.Priority();
            }
        }

        if (!slave) {
            if (tls.Log) {
                *tls.Log << TLOG_ERR << Now().ToStringLocal()
                      << ' ' << TThread::CurrentThreadNumericId() << " could not find context for server name "
                      << servername << "\n";
            }
            return nullptr;
        }
        slave->IncSelected(*process);

        if (tls.Log) {
            *tls.Log << TLOG_ERR << Now().ToStringLocal()
                  << ' ' << TThread::CurrentThreadNumericId() << " found context " << ItemsSection_ << '['
                  << slave->Name_ << "] for server name " << servername << "\n";
        }

        return slave->Ctx(*process).Ctx();
    }

    /* End OCSP reload */

    void TSslItem::SetMaxSendFragment(long maxSendFragment) noexcept {
        MaxSendFragment_ = maxSendFragment;
    }

    void TSslItem::TryUpdateMaxSendFragment(long maxSendFragment) noexcept {
        if (!MaxSendFragment_.Defined()) {
            MaxSendFragment_ = maxSendFragment;
        }
    }

    void TSslItem::EnableH2(const NSrvKernel::IAlpnProtos* protos) noexcept {
        Protos_ = protos;
    }

    void TSslItem::SetKeylogLine(const TStringBuf line) noexcept {
        auto& tls = GetTls(*FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);

        if (!tls.SecretsLog || RandomNumber<double>() >= tls.SecretsLogFreq.Get()) {
            return;
        }

        *tls.SecretsLog << line << "\n";
    }

    /* Callbacks */

    static const NSrvKernel::TChunk* OcspResponseCallback(void* data, ssl_st* ssl) {
        const TSslItem* const parent = reinterpret_cast<TSslItem*>(data);
        Y_ASSERT(FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);
        const auto* process = FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess;
        if (parent && ssl) {
            const X509* primaryCert = SSL_get_certificate(ssl);
            if (primaryCert) {
                auto& primaryTls = parent->PrimaryCertData()->GetTls(*process);
                if (primaryCert == primaryTls.RawCert) {
                    if (primaryTls.OcspFileSwitchChecker.Exists()) {
                        return nullptr;
                    }
                    return primaryTls.OcspResponse.Get();
                } else if (parent->SecondaryCertData() != nullptr && primaryCert == parent->SecondaryCertData()->GetTls(*process).RawCert) {
                    auto& secondaryTls = parent->SecondaryCertData()->GetTls(*process);
                    if (secondaryTls.OcspFileSwitchChecker.Exists()) {
                        return nullptr;
                    }
                    return secondaryTls.OcspResponse.Get();
                }
            }
        }
        return nullptr;
    }

    static int SslTicketKeysCallback(void* data, ssl_st* ssl, unsigned char* name, unsigned char* iv,
        evp_cipher_ctx_st* cipherCtx, hmac_ctx_st* hmacCtx, int init)
    {
        const TSslItem* const sslItem = reinterpret_cast<TSslItem*>(data);
        Y_VERIFY(sslItem != nullptr);

        if (init == 0) {
            auto* rawSslIo = SSL_get_ex_data(ssl, Default<NSrvKernel::TSslIoIndex>().Idx);
            auto* io = static_cast<NSrvKernel::TSslIo*>(rawSslIo);

            if (io) {
                // Set flag for handshake info function in headers module
                io->SetUsedTlsTickets();
                io->SetTicketName(name);
                io->SetTicketIV(iv);
            }
        }

        Y_ASSERT(FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);
        auto tickets = sslItem->SslTicketKeys(*FastTlsSingleton<NSrvKernel::NProcessCore::TThreadInfo>()->WorkerProcess);
        Y_VERIFY(tickets != nullptr);

        return SslTicketKeysRoutine(name, iv, cipherCtx, hmacCtx, init, tickets);
    }

    static ssl_ctx_st* ServernameCallback(void* data, const TStringBuf& servername) {
        const TSslItem* const parent = reinterpret_cast<TSslItem*>(data);
        return parent->MatchCtx(servername);
    }

    static void KeylogCallback(void* data, const TStringBuf line) {
        TSslItem* parent = reinterpret_cast<TSslItem*>(data);
        parent->SetKeylogLine(line);
    }

    /* End callbacks */

    THolder<TSslItemSettings> ParseSslItemSettings(NConfig::IConfig *conf, std::function<void(TString, TString)> func,
        TStringBuf suffix, size_t countOfWorkers) {
        struct TSslItemSettingsParser final : public NConfig::IConfig::IFunc {
            TSslItemSettingsParser(NConfig::IConfig *config, std::function<void(TString, TString)> func)
                : Func_(std::move(func))
            {
                config->ForEach(this);
            }

            // DoConsume instead of START_PARSE because we need to skip all keys from ssl_sni context
            void DoConsume(const TString& key, NConfig::IConfig::IValue* value) override {
                {
                    ON_KEY("cert", Cert) {
                        return;
                    }

                    ON_KEY("priv", Priv) {
                        return;
                    }

                    ON_KEY("ca", Ca) {
                        return;
                    }

                    ON_KEY("ocsp", Ocsp) {
                        return;
                    }

                    ON_KEY("ocsp_file_switch", OcspFileSwitch) {
                        return;
                    }

                    if (key == "events") {
                        NSrvKernel::ParseMap(value->AsSubConfig(), [this](const auto &key, auto *val) {
                            Func_(key, val->AsString());
                        });
                        return;
                    }
                }
            }

            TString Cert;
            TString Priv;
            TString Ca;
            TString Ocsp;
            TString OcspFileSwitch;

            std::function<void(TString, TString)> Func_;
        } parser(conf, std::move(func));

        if (parser.Cert.empty()) {
            ythrow NConfig::TConfigParseError() << "no public keys loaded " << suffix;
        }

        if (parser.Priv.empty()) {
            ythrow NConfig::TConfigParseError() << "no private keys loaded " << suffix;
        }

        return MakeHolder<TSslItemSettings>(countOfWorkers, parser.Cert, parser.Priv, parser.Ca, parser.Ocsp,
            parser.OcspFileSwitch);
    }

} // namespace NModSsl
