#include "module.h"
#include "metrika/core/libs/uahints/UAHintsHeaders.h"

#include <balancer/modules/http/http.cfgproto.pb.h>

#include <balancer/modules/http/cycles_checker.h>

#include <balancer/kernel/client_hints/client_hints_face.h>
#include <balancer/kernel/custom_io/concat.h>
#include <balancer/kernel/custom_io/countio.h>
#include <balancer/kernel/custom_io/iterator.h>
#include <balancer/kernel/custom_io/limitio.h>
#include <balancer/kernel/custom_io/stream.h>
#include <balancer/kernel/fs/shared_file_exists_checker.h>
#include <balancer/kernel/fs/shared_files.h>
#include <balancer/kernel/helpers/default_instance.h>
#include <balancer/kernel/helpers/errors.h>
#include <balancer/kernel/helpers/ranges.h>
#include <balancer/kernel/http/parser/common_headers.h>
#include <balancer/kernel/http/parser/httpdecoder.h>
#include <balancer/kernel/http/parser/output.h>
#include <balancer/kernel/http/parser/parse_options.h>
#include <balancer/kernel/http/parser/response_builder.h>
#include <balancer/kernel/log/errorlog.h>
#include <balancer/kernel/module/module.h>
#include <balancer/kernel/net/socket.h>
#include <balancer/kernel/process/thread_info.h>
#include <balancer/kernel/ssl/sslio.h>
#include <balancer/kernel/thread/threadedqueue.h>

#include <library/cpp/regex/pire/regexp.h>
#include <library/cpp/string_utils/base64/base64.h>

#include <util/folder/path.h>
#include <util/generic/scope.h>
#include <util/generic/yexception.h>
#include <util/generic/ylimits.h>
#include <util/string/cast.h>
#include <util/thread/singleton.h>

using namespace NModHttp;
using namespace NSrvKernel;


static const TString ALLOWED_METHODS("OPTIONS, GET, HEAD, POST, PUT, DELETE, CONNECT");

namespace {
    namespace Nhttp {
        struct TSharedStats {
            TSharedStats(const TString& uuid, TSharedStatsManager& statsManager)
                : BannedRequests(statsManager.MakeCounter("http-banned_requests").AllowDuplicate().Build())
                , ParseError(statsManager.MakeCounter("http-http_request_parse_error").AllowDuplicate().Build())
                , CompleteRequestParseError(statsManager.MakeCounter("http-http_complete_request_parse_error").AllowDuplicate().Build())
                , UnfinishedClientStreamError(statsManager.MakeCounter("http-http_unfinished_client_stream_error").AllowDuplicate().Build())
                , UnfinishedBackendStreamError(statsManager.MakeCounter("http-http_unfinished_backend_stream_error").AllowDuplicate().Build())
                , CyclesStopped(statsManager.MakeCounter("http-cycles_stopped").AllowDuplicate().Build())
                , CycleHeaderLength(statsManager.MakeCounter("cycle-header-length").AllowDuplicate().Build())
                , CycleHeaderFormatError(statsManager.MakeCounter("cycle-header-format-error").AllowDuplicate().Build())
            {
                if (!uuid.empty()) {
                    BadRequest = statsManager.MakeCounter("http-" + uuid + "-badhttp").AllowDuplicate().Build();
                    EmptyRequest = statsManager.MakeCounter("http-" + uuid + "-emptyrequest").AllowDuplicate().Build();
                    BigRequest = statsManager.MakeCounter("http-" + uuid + "-bigrequest").AllowDuplicate().Build();
                    CancelledSession = statsManager.MakeCounter("http-" + uuid + "-cancelled_session").AllowDuplicate().Build();
                    for (ui32 cnt : xrange(CancelledSessionHttp2.size())) {
                        CancelledSessionHttp2[cnt] = statsManager.MakeCounter(TStringBuilder() << "http-"
                            << uuid << "-cancelled_session_http2_" << (THTTP2StreamDetails::EFinalState)cnt).AllowDuplicate().Build();
                    }
                    ConnectionKeepAliveReqs = statsManager.MakeCounter("http-" + uuid + "-connection_keep_alive_reqs").AllowDuplicate().Build();
                    ConnectionCloseReqs = statsManager.MakeCounter("http-" + uuid + "-connection_close_reqs").AllowDuplicate().Build();
                }
            }

            TMaybe<TSharedCounter> BadRequest;
            TMaybe<TSharedCounter> EmptyRequest;
            TMaybe<TSharedCounter> BigRequest;
            TMaybe<TSharedCounter> CancelledSession;
            std::array<TMaybe<TSharedCounter>, (ui32)THTTP2StreamDetails::EFinalState::Count> CancelledSessionHttp2;
            TMaybe<TSharedCounter> ConnectionKeepAliveReqs;
            TMaybe<TSharedCounter> ConnectionCloseReqs;
            TSharedCounter BannedRequests;
            TSharedCounter ParseError;
            TSharedCounter CompleteRequestParseError;
            TSharedCounter UnfinishedClientStreamError;
            TSharedCounter UnfinishedBackendStreamError;
            TSharedCounter CyclesStopped;
            TSharedCounter CycleHeaderLength;
            TSharedCounter CycleHeaderFormatError;
        };
    }

    enum EReadOperationStatus {
        OK,
        INTERRUPTED,
        EMPTY_REQUEST,
    };

    class TGracefulShutdownHandler : public IGracefulShutdownHandler {
    public:
        TGracefulShutdownHandler(TCont* cont) : Cont_(cont) {}

        void OnShutdown() noexcept override {
            Interrupted_ = true;
            if (!Cont_->Scheduled()) {
                Cont_->ReSchedule();
            }
        }

        bool Interrupted() const noexcept {
            return Interrupted_;
        }

    private:
        TCont* Cont_ = nullptr;
        bool Interrupted_ = false;
    };

    enum class EBanRuleType {
        Ip,
        Path,
        CGI,
        Url,
        Header
    };

    struct TBanRule {
        TFsm RegExp;
        double Probability;
        EBanRuleType Type;
        ui16 Code;
    };
}

Y_TLS(http) {
    TTls(const TString& uuid, TSharedStats& sharedStats, size_t workerId)
        : Stats(uuid, sharedStats, workerId)
    {}

    struct TStats {
        TStats(const TString& uuid, TSharedStats& sharedStats, size_t workerId) {
            if (!uuid.empty()) {
                BadRequest_.SetSharedCounter(TSharedCounter(*sharedStats.BadRequest, workerId));
                EmptyRequest_.SetSharedCounter(TSharedCounter(*sharedStats.EmptyRequest, workerId));
                BigRequest_.SetSharedCounter(TSharedCounter(*sharedStats.BigRequest, workerId));
                CancelledSession_.SetSharedCounter(TSharedCounter(*sharedStats.CancelledSession, workerId));
                for (ui32 cnt : xrange(CancelledSessionHttp2_.size())) {
                    CancelledSessionHttp2_[cnt].SetSharedCounter(TSharedCounter(*sharedStats.CancelledSessionHttp2[cnt], workerId));
                }
                ConnectionKeepAliveReqs_.SetSharedCounter(TSharedCounter(*sharedStats.ConnectionKeepAliveReqs, workerId));
                ConnectionCloseReqs_.SetSharedCounter(TSharedCounter(*sharedStats.ConnectionCloseReqs, workerId));
            }
            BannedRequests_.SetSharedCounter(TSharedCounter(sharedStats.BannedRequests, workerId));
            ParseError_.SetSharedCounter(TSharedCounter(sharedStats.ParseError, workerId));
            CompleteRequestParseError_.SetSharedCounter(TSharedCounter(sharedStats.CompleteRequestParseError, workerId));
            UnfinishedClientStreamError_.SetSharedCounter(TSharedCounter(sharedStats.UnfinishedClientStreamError, workerId));
            UnfinishedBackendStreamError_.SetSharedCounter(TSharedCounter(sharedStats.UnfinishedBackendStreamError, workerId));
            CyclesStopped_.SetSharedCounter(TSharedCounter(sharedStats.CyclesStopped, workerId));
            CycleHeaderLength_.SetSharedCounter(TSharedCounter(sharedStats.CycleHeaderLength, workerId));
            CycleHeaderFormatError_.SetSharedCounter(TSharedCounter(sharedStats.CycleHeaderFormatError, workerId));
        }

        void IncCancelledSession() noexcept {
            ++CancelledSession_;
        }

        void IncBadRequest() noexcept {
            ++BadRequest_;
        }

        void IncEmptyRequest() noexcept {
            ++EmptyRequest_;
        }

        void IncBigRequest() noexcept {
            ++BigRequest_;
        }

        void IncCancelledSessionHttp2(THTTP2StreamDetails::EFinalState state) noexcept {
            if (ui32(state) >= CancelledSessionHttp2_.size()) {
                state = THTTP2StreamDetails::EFinalState::Unknown;
            }
            ++CancelledSessionHttp2_[(ui32)state];

            if (THTTP2StreamDetails::EFinalState::ConnCancel == state) {
                IncCancelledSession();
            }
        }

        void IncConnectionKeepAliveReqs() noexcept {
            ++ConnectionKeepAliveReqs_;
        }

        void IncConnectionCloseReqs() noexcept {
            ++ConnectionCloseReqs_;
        }

        void IncBannedRequests() noexcept {
            ++BannedRequests_;
        }

        void IncParseError() noexcept {
            ++ParseError_;
        }

        void IncCompleteRequestParseError() noexcept {
            ++CompleteRequestParseError_;
        }

        void IncUnfinishedClientStreamError() noexcept {
            ++UnfinishedClientStreamError_;
        }

        void IncUnfinishedBackendStreamError() noexcept {
            ++UnfinishedBackendStreamError_;
        }

        void IncCyclesStopped() noexcept {
            ++CyclesStopped_;
        }

        void IncCycleHeaderLength() noexcept {
            ++CycleHeaderLength_;
        }

        void IncCycleHeaderFormatError() noexcept {
            ++CycleHeaderFormatError_;
        }

    private:
        TCombinedCounter BadRequest_;
        TCombinedCounter EmptyRequest_;
        TCombinedCounter BigRequest_;
        TCombinedCounter CancelledSession_;
        std::array<TCombinedCounter, (ui32)THTTP2StreamDetails::EFinalState::Count> CancelledSessionHttp2_;
        TCombinedCounter ConnectionKeepAliveReqs_;
        TCombinedCounter ConnectionCloseReqs_;
        TCombinedCounter BannedRequests_;
        TCombinedCounter ParseError_;
        TCombinedCounter CompleteRequestParseError_;
        TCombinedCounter UnfinishedClientStreamError_;
        TCombinedCounter UnfinishedBackendStreamError_;
        TCombinedCounter CyclesStopped_;
        TCombinedCounter CycleHeaderLength_;
        TCombinedCounter CycleHeaderFormatError_;
    };

    bool NoKeepAliveFileExists() const noexcept {
        return NoKeepAliveFileChecker.Exists();
    }

    TSharedFileExistsChecker NoKeepAliveFileChecker;
    TSharedFileExistsChecker AllowWebDavChecker;
    TSharedFileExistsChecker DisableCHRestoreChecker;
    TSharedFileExistsChecker DisableCyclesProtectionChecker;

    TSharedFileReReader BanRequestsFileReReader;
    TSharedFileReReader::TData BanRequestsFileData;
    TVector<TBanRule> BanRules;

    TStats Stats;
};

MODULE_WITH_TLS(http) {
public:
    TModule(const TModuleParams& mp)
        : TModuleBase(mp)
    {
        ParseConfig();

        if (!Submodule_) {
            ythrow TConfigParseError() << "no module configured";
        }

        Y_ASSERT(Config_->maxlen() > 0);

        SharedStats_ = MakeHolder<TSharedStats>(Config_->stats_attr(), mp.Control->SharedStatsManager());

        if (Config_->enable_cycles_protection()) {
            CyclesChecker_ = MakeHolder<TCyclesChecker>(Control->GetConfigHash(),
                                                        Config_->max_cycles(),
                                                        Config_->cycles_header_len_alert());
        }

        if (Config_->allow_client_hints_restore()) {
            GetUARestorer()->Init(Config_->has_client_hints_ua_header(), Config_->has_client_hints_ua_proto_header());
        }

        Y_UNUSED(TTrueFsm::Instance());
    }
private:
    void ParseConfig() {
        auto config = ParseProtoConfig<TModuleConfig>(
            [&](const TString& key, NConfig::IConfig::IValue* value) {
                if (key == "allow_client_hints_restore_file") {
                    return;
                }
                Submodule_.Reset(Loader->MustLoad(key, Copy(value->AsSubConfig())).Release());
            }
        );

        if (!config.has_maxlen()) {
            Y_WARN_ONCE("maxlen is not set. Both maxlen and maxreq are required.");
            config.set_maxlen(HTTP_MAX_HEADERS_SIZE_DEFAULT);
        }

        if (config.maxlen() == 0) {
            config.set_maxlen(Max<decltype(Config_->maxlen())>());
        }

        if (!config.has_maxreq()) {
            Y_WARN_ONCE("maxreq is not set. Both maxlen and maxreq are required.");
            config.set_maxreq(HTTP_MAX_REQUEST_LINE_SIZE_DEFAULT);
        }

        if (config.keepalive_timeout() == TDuration::Zero()) {
            config.set_keepalive(false);
            config.set_keepalive_timeout(TDuration::Max());
        }

        if (config.allow_client_hints_restore()) {
            Y_ENSURE_EX(GetUARestorer(), TConfigParseError() << "client hints support is disabled");
        }

        Config_ = config;
    }

    bool CheckKeepAliveConstraints(const TConnDescr& descr, TInstant deadline, size_t keepAliveRequests) const noexcept {
        return
            Now() >= deadline ||
            keepAliveRequests + 1 >= Config_->keepalive_requests() ||
            descr.CpuLimiter() && descr.CpuLimiter()->CheckKeepAliveClosed() ||
            RandomNumber<double>() < Config_->keepalive_drop_probability();
        // We dont use epsilon for double comparison because closing connection is not the worst idea
    }

    THolder<TTls> DoInitTls(IWorkerCtl* process) override {
        auto tls = MakeHolder<TTls>(Config_->stats_attr(), *SharedStats_, process->WorkerId());
        if (Config_->no_keepalive_file()) {
            tls->NoKeepAliveFileChecker = process->SharedFiles()->FileChecker(Config_->no_keepalive_file(), TDuration::Seconds(1));
        }
        if (Config_->allow_webdav_file()) {
            tls->AllowWebDavChecker = process->SharedFiles()->FileChecker(
                Config_->allow_webdav_file(), TDuration::Seconds(1));
        }
        if (Config_->ban_requests_file()) {
            tls->BanRequestsFileReReader = process->SharedFiles()->FileReReader(Config_->ban_requests_file(), TDuration::Seconds(1));
        }
        if (Config_->disable_client_hints_restore_file()) {
            tls->DisableCHRestoreChecker = process->SharedFiles()->FileChecker(
                    Config_->disable_client_hints_restore_file(), TDuration::Seconds(1));
        }
        if (Config_->disable_cycles_protection_file()) {
            tls->DisableCyclesProtectionChecker = process->SharedFiles()->FileChecker(
                    Config_->disable_cycles_protection_file(), TDuration::Seconds(1));
        }

        return tls;
    }

    void RestoreUserAgent(TConnDescr& descr, THeaders& headers) const noexcept {
        Y_TRY(NSrvKernel::TError, error) {
            auto hintsHeaders = GetHintsHeaders(headers);
            auto oldUAHeader = GetUAHeader(headers);

            TUAAndUATraits uaAndUATraits;
            Y_PROPAGATE_ERROR(GetUARestorer()->GetUAAndUATraits(hintsHeaders, oldUAHeader).AssignTo(uaAndUATraits));

            headers.Replace(Config_->client_hints_ua_header(), uaAndUATraits.RestoredUA.GetOrElse(TString(oldUAHeader)));
            headers.Replace(Config_->client_hints_ua_proto_header(), Base64Encode(uaAndUATraits.SerializedUATraits.GetOrElse("")));

            return {};
        } Y_CATCH {
            LOG_ERROR(TLOG_ERR, descr, "User Agent restore failed: " << error->what());
        }
    }

    TError ParseRequest(TRequest*& request, TRequest& decodedRequest, TFromClientDecoder& decoder,
                     const size_t keepAliveRequests, TLengthLimitInput& limitInput, TConnDescr& descr,
                     const TInstant deadline, TTls& tls) const noexcept
    {
        if (!request) {
            request = &decodedRequest;

            Y_TRY(TError, error) {
                return decoder.ReadRequest(*request, TInstant::Max());
            } Y_CATCH {
                if (error.GetAs<THttpParseIncompleteInputError>()) {
                    if (request->RequestLine().UriParsed()) {
                        descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "entity too large");
                        return Y_MAKE_ERROR(THttpParseError{HTTP_REQUEST_ENTITY_TOO_LARGE});
                    } else { // even request line is not parsed
                        descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "uri too large");
                        return Y_MAKE_ERROR(THttpParseError{HTTP_REQUEST_URI_TOO_LARGE});
                    }
                } else {
                    descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client read error");
                }
                return error;
            }
            request->Props().Reused = keepAliveRequests > 0;
        } else {
            if (request->EncodedSize() > limitInput.GetLimit()) {
                if (request->RequestLine().EncodedSize() > limitInput.GetLimit()) {
                    descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "uri too large");
                    return Y_MAKE_ERROR(THttpParseError{HTTP_REQUEST_URI_TOO_LARGE});
                }
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "entity too large");
                return Y_MAKE_ERROR(THttpParseError{HTTP_REQUEST_ENTITY_TOO_LARGE});
            }
            decoder.Init(*request);
        }

        limitInput.SetLimit(Max<ui64>());

        descr.Properties->Start = Now();
        descr.Properties->Random = RandomNumber<ui64>();

        // FIXME: do not parse entire request
        if (Y_LIKELY(Config_->maxreq())) {
            const size_t pathLen = request->RequestLine().Path.size();
            if (pathLen + request->RequestLine().CGI.size() > Config_->maxreq()) {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "uri too large");
                return Y_MAKE_ERROR(THttpParseError{HTTP_REQUEST_URI_TOO_LARGE});
            }
        }

        if (request->Props().KeepAlive) {
            tls.Stats.IncConnectionKeepAliveReqs();
        } else {
            tls.Stats.IncConnectionCloseReqs();
        }

        if (!Config_->keepalive()
            || tls.NoKeepAliveFileExists()
            || descr.Process().IsShuttingDown()
            || CheckKeepAliveConstraints(descr, deadline, keepAliveRequests))
        {
            request->Props().KeepAlive = false;
        }

        if (!Config_->allow_trace() && request->RequestLine().Method == EMethod::TRACE) {
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "TRACE method not allowed");
            return Y_MAKE_ERROR(THttpError{HTTP_METHOD_NOT_ALLOWED});
        }

        if (request->RequestLine().Method & HttpWebDavMethods) {
            if (!Config_->allow_webdav() && !tls.AllowWebDavChecker.Exists()) {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "webdav method not allowed");
                return Y_MAKE_ERROR(THttpError{HTTP_METHOD_NOT_ALLOWED});
            }
        }

        if (Config_->allow_client_hints_restore() && !tls.DisableCHRestoreChecker.Exists()) {
            RestoreUserAgent(descr, request->Headers());
        }

        return {};
    }

    void OnECanceled(TConnDescr& descr, TTls& tls) const noexcept {
        if (IsHTTP2(descr.Request)) {
            tls.Stats.IncCancelledSessionHttp2(descr.Request->Props().HTTP2->FinalState);
            LOG_ERROR(TLOG_ERR, descr,
                      "cancelled session on http2: " << descr.Request->Props().HTTP2->FinalState << ", "
                                                     << descr.Request->Props().HTTP2->ErrorCode);
        } else {
            tls.Stats.IncCancelledSession();
            LOG_ERROR(TLOG_ERR, descr, "cancelled session: " << TErrno(ECANCELED));
        }
    }

    void UpdateBanRules(const TConnDescr& descr, TTls& tls) const {
        const auto& data = tls.BanRequestsFileReReader.Data();
        if (data.Id() == tls.BanRequestsFileData.Id()) {
            return;
        }
        tls.BanRequestsFileData = data;

        tls.BanRules.clear();
        for (TStringBuf line : StringSplitter(data.Data()).Split('\n').SkipEmpty()) {
            try {
                const double probability = FromString<double>(line.NextTok(','));
                if (!(probability >= 0)) {
                    continue;
                }

                const ui16 code = FromString<ui16>(line.NextTok(','));
                if (code < 100 || code >= 600) {
                    continue;
                }

                EBanRuleType type;
                TString typeStr = TString(line.NextTok(','));
                typeStr.to_lower();
                if (typeStr == "ip") {
                    type = EBanRuleType::Ip;
                } else if (typeStr == "path") {
                    type = EBanRuleType::Path;
                } else if (typeStr == "cgi") {
                    type = EBanRuleType::CGI;
                } else if (typeStr == "url") {
                    type = EBanRuleType::Url;
                } else if (typeStr == "header") {
                    type = EBanRuleType::Header;
                } else {
                    LOG_ERROR(TLOG_ERR, descr, "UpdateBanRules: Bad rule line: " << line << ": invalid rule type");
                    continue;
                }

                tls.BanRules.push_back(TBanRule{
                    .RegExp = TFsm(line),
                    .Probability = probability,
                    .Type = type,
                    .Code = code,
                });
            } catch (...) {
                LOG_ERROR(TLOG_ERR, descr, "UpdateBanRules: Bad rule line: " << line << ":" << CurrentExceptionMessage());
            }
        }
    }

    ui16 IsBannedRequest(const TConnDescr& descr, TTls& tls) const {
        for (const auto& rule : tls.BanRules) {
            bool match = false;

            switch (rule.Type) {
                case EBanRuleType::Ip:
                {
                    const TString addr = NAddr::PrintHost(descr.RemoteAddr());
                    match = TMatcher(rule.RegExp).Match(addr).Final();
                    break;
                }
                case EBanRuleType::Path:
                    match = TMatcher(rule.RegExp).Match(descr.Request->RequestLine().Path.AsStringBuf()).Final();
                    break;
                case EBanRuleType::CGI:
                    match = TMatcher(rule.RegExp).Match(descr.Request->RequestLine().CGI.AsStringBuf()).Final();
                    break;
                case EBanRuleType::Url:
                    match = descr.Request->RequestLine().MatchUrl(rule.RegExp);
                    break;
                case EBanRuleType::Header:
                    for (auto& headerKey : descr.Request->Headers()) {
                        for (auto& headerValue : headerKey.second) {
                            const TString header = TString(headerKey.first.AsStringBuf()) +
                                ":" + headerValue.AsStringBuf();
                            if (TMatcher(rule.RegExp).Match(header).Final()) {
                                match = true;
                                break;
                            }
                        }

                        if (match) {
                            break;
                        }
                    }
                    break;
            }

            if (match && (rule.Probability >= 1 || RandomNumber<double>() <= rule.Probability)) {
                return rule.Code;
            }
        }

        return 0;
    }

    TError CheckCycles(const TConnDescr& descr, TTls& tls) const {
        if (descr.Request) {
            Y_TRY(TError, error) {
                TString lengthAlert;
                TString formatAlert;
                Y_PROPAGATE_ERROR(CyclesChecker_->Check(*descr.Request, lengthAlert, formatAlert));
                if (!lengthAlert.empty()) {
                    LOG_ERROR(TLOG_ERR, descr, "[Cycles protection] " << lengthAlert);
                    tls.Stats.IncCycleHeaderLength();
                }
                if (!formatAlert.empty()) {
                    LOG_ERROR(TLOG_ERR, descr, "[Cycles protection] " << formatAlert);
                    tls.Stats.IncCycleHeaderFormatError();
                }
                return {};
            } Y_CATCH {
                LOG_ERROR(TLOG_ERR, descr, "Request cycle detected");
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "Request cycle detected");
                tls.Stats.IncCyclesStopped();
                // Return HTTP_TOO_MANY_REQUESTS to avoid cascade retries. This case, actually, should never happen,
                // so, we can skip special handling before writing answer to external client.
                TResponse response = BuildResponse().Version11().Code(HTTP_TOO_MANY_REQUESTS).ContentLength(0);
                Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), false, TInstant::Max()));
                Y_PROPAGATE_ERROR(descr.Output->SendEof(TInstant::Max()));
                return error;
            };
        }
        return {};
    }

    TError RunSubmodule(const TConnDescr& descr, TTls& tls) const {
        UpdateBanRules(descr, tls);
        ui16 banCode = IsBannedRequest(descr, tls);
        if (banCode > 0) {
            tls.Stats.IncBannedRequests();
            TResponse response = BuildResponse().Version11().Code(banCode).ContentLength(0);
            Y_TRY(TError, error) {
                Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), false, TInstant::Max()));
                return descr.Output->SendEof(TInstant::Max());
            } Y_CATCH {
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                return error;
            };
            descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "banned request");
            return {};
        }

        if (CyclesChecker_ && !tls.DisableCyclesProtectionChecker.Exists()) {
            Y_PROPAGATE_ERROR(CheckCycles(descr, tls));
        }

        return Submodule_->Run(descr);
    }

    TErrorOr<bool> RunSubmoduleHttp(TConnDescr& descr, TTls& tls) const {
        const auto& props = descr.Request->Props();
        THttpOutput httpOut{descr.Output, props.Version,
                            EMethod::HEAD == descr.Request->RequestLine().Method,
                            props.HTTP2 != nullptr, props.KeepAlive};
        Y_PROPAGATE_ERROR(RunSubmodule(descr.CopyOut(httpOut), tls));
        return httpOut.NeedForceClose();
    }

    // true - break main cycle, false - continue
    TErrorOr<bool> RespondToClient(
        TConnDescr& descr, const bool isHttp2, TRequest* request,
        TResponse&& response, bool forceClose
    ) const {
        if (isHttp2) {
            Y_PROPAGATE_ERROR(descr.Output->SendHead(std::move(response), forceClose, TInstant::Max()));
            Y_PROPAGATE_ERROR(descr.Output->SendEof(TInstant::Max()));
            return false;
        } else {
            THttpOutput httpOut{descr.Output, 1, false, false, request->Props().KeepAlive};
            Y_PROPAGATE_ERROR(httpOut.SendHead(std::move(response), forceClose, TInstant::Max()));
            Y_PROPAGATE_ERROR(httpOut.SendEof(TInstant::Max()));
            return httpOut.NeedForceClose();
        }
    }

    // true - break main cycle, false - continue
    TErrorOr<bool> ProcessRequest(TConnDescr& descr, const bool isHttp2, TTls& tls) const {
        bool forceClose = false;
        Y_TRY(TError, error) {
            if (isHttp2) {
                return RunSubmodule(descr, tls);
            } else {
                return RunSubmoduleHttp(descr, tls).AssignTo(forceClose);
            }
        } Y_CATCH {
            if (const auto* eSys = error.GetAs<TSystemError>()) {
                if (eSys->Status() == ECANCELED) {
                    OnECanceled(descr, tls);
                }
            } else if (error.GetAs<TForceStreamClose>()) {
                return true;
            }
            return error;
        }
        return forceClose;
    }

    TErrorOr<EReadOperationStatus> WaitForRequest(TConnDescr& descr, const TInstant& deadline) const noexcept {
        while (true) {
            TChunkList lst;

            {
                TGracefulShutdownHandler gracefulShutdownHandler(descr.Process().Executor().Running());
                descr.Process().AddGracefulShutdownHandler(&gracefulShutdownHandler, false);
                Y_TRY(TError, error) {
                    return descr.Input->Recv(lst, deadline);
                } Y_CATCH {
                    if (const auto* e = error.GetAs<TSystemError>()) {
                        if (e->Status() == EINPROGRESS && gracefulShutdownHandler.Interrupted()) {
                            return EReadOperationStatus::INTERRUPTED;
                        }
                    }
                    return error;
                }
            }

            if (lst.Empty()) {
                return EReadOperationStatus::EMPTY_REQUEST;
            }

            while (!lst.Empty()) {
                const TChunk* chunk = lst.Front();
                const TStringBuf data = chunk->AsStringBuf();
                const size_t nonspacePos = data.find_first_not_of("\r\n");
                if (nonspacePos != TString::npos) {
                    lst.Skip(nonspacePos);
                    descr.Input->UnRecv(std::move(lst));
                    return EReadOperationStatus::OK;
                }
                lst.PopFront();
            }
        }
    }

    TError DoRun(const TConnDescr& originalDescr, TTls& tls) const noexcept override {
        // If we should stop after processing the first request.
        auto descr = originalDescr.Copy();
        const TInstant deadline = Config_->keepalive_timeout().ToDeadLine();
        const bool isHttp2 = nullptr != descr.Request;
        size_t keepAliveRequests = 0;

        while (true) {
            Y_DEFER {
                ++keepAliveRequests;
            };

            if (!isHttp2) {
                EReadOperationStatus status;
                Y_TRY(TError, error) {
                    return WaitForRequest(descr, deadline).AssignTo(status);
                } Y_CATCH {
                    if (const auto* e = error.GetAs<TSystemError>()) {
                        tls.Stats.IncEmptyRequest();
                        auto logLevel = TLOG_ERR;
                        if (e->Status() == ETIMEDOUT) {
                            logLevel = TLOG_DEBUG;
                        }
                        LOG_ERROR(logLevel, descr, "empty request: " << TErrno(e->Status()));
                    }
                    descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client read error");
                    return error;
                }
                if (status != EReadOperationStatus::OK) {
                    if (status == EReadOperationStatus::EMPTY_REQUEST && keepAliveRequests == 0) {
                        tls.Stats.IncEmptyRequest();
                    }
                    break;
                }
            }

            Y_ASSERT(Config_->maxlen() > 0);
            TLengthLimitInput limitInput{ descr.Input, Config_->maxlen() };
            TFromClientDecoder decoder{&limitInput, {Config_->maxheaders(), Config_->multiple_hosts_enabled(), /*KeepAllHeaders*/Config_->server_mode()}};
            descr.HttpDecoder = &decoder;

            TRequest decodedRequest;
            TRequest* request = descr.Request;

            TError catchedError;
            Y_TRY(TError, error) {
                return ParseRequest(request, decodedRequest, decoder,
                                    keepAliveRequests, limitInput, descr, deadline, tls);
            } Y_CATCH {
                if (const auto* httpError = error.GetAs<THttpError>()) {
                    if (const auto* parseError = error.GetAs<THttpParseError>()) {
                        if (!error.GetAs<THttpParseIncompleteInputError>()) {
                            tls.Stats.IncCompleteRequestParseError();
                        }
                        tls.Stats.IncParseError();
                        LOG_ERROR(TLOG_ERR, descr, "http request parse error: " << parseError->Code() <<
                            " " << ToString(parseError->Chunks()));
                        if (IsIn({HTTP_REQUEST_ENTITY_TOO_LARGE, HTTP_REQUEST_URI_TOO_LARGE}, httpError->Code())) {
                            // TODO(velavokr): why is a big request not also a bad request?
                            tls.Stats.IncBigRequest();
                        } else {
                            tls.Stats.IncBadRequest();
                        }
                    } else {
                        tls.Stats.IncBadRequest();
                        LOG_ERROR(TLOG_ERR, descr, "http request error: " << httpError->Code());
                    }
                    if (IsHttpCode(httpError->Code())) {
                        catchedError = std::move(error);
                    } else {
                        return error;
                    }
                } else {
                    if (const auto* e = error.GetAs<TSystemError>()) {
                        if (e->Status() == ECANCELED) {
                            OnECanceled(descr, tls);
                        } else {
                            tls.Stats.IncBadRequest();
                            LOG_ERROR(TLOG_ERR, descr, "system error: " << TErrno(e->Status()));
                        }
                    } else {
                        tls.Stats.IncBadRequest();
                        LOG_ERROR(TLOG_ERR, descr, "unknown error: " << GetErrorMessage(error));
                    }
                    return error;
                }
            }

            bool forceClose = false;

            if (!catchedError) {
                TConnDescr copyDescr = descr.CopyIn(decoder);
                copyDescr.Request = request;
                copyDescr.Properties->UserConnIsSsl = copyDescr.Ssl().ThisConnIsSsl; // TODO (smalukav): remove

                Y_PROPAGATE_ERROR(ProcessRequest(copyDescr, isHttp2, tls).AssignTo(forceClose));
            } else {
                if (const auto* e = catchedError.GetAs<THttpParseError>()) {
                    Y_ASSERT(IsHttpCode(e->Code()));
                    TResponse response = BuildResponse().Version11()
                                                        .Code(e->Code())
                                                        .ContentLength(0);
                    if (TError error = RespondToClient(descr, isHttp2, request, std::move(response), true).AssignTo(forceClose)) {
                        descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                        return error;
                    }
                } else if (const auto* e = catchedError.GetAs<THttpError>()) {
                    Y_ASSERT(IsHttpCode(e->Code()));

                    TResponse response = BuildResponse().Version11()
                                                        .Code(e->Code())
                                                        .ContentLength(0);
                    if (e->Code() == HTTP_METHOD_NOT_ALLOWED) {
                        response.Headers().Add("Allow", ALLOWED_METHODS);
                    }

                    if (TError error = RespondToClient(descr, isHttp2, request, std::move(response), false).AssignTo(forceClose)) {
                        descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "client write error");
                        return error;
                    }
                }
            }

            if (isHttp2 || forceClose) {
                break;
            }

            if (decoder.StreamState() != EStreamState::Close) {
                tls.Stats.IncUnfinishedClientStreamError();
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "unfinished client stream");
                return Y_MAKE_ERROR(yexception{} << "reading from client hasn't finished");
            }
            if (descr.Output->StreamState() != EStreamState::Close) {
                tls.Stats.IncUnfinishedBackendStreamError();
                descr.ExtraAccessLog.SetSummary(GetHandle()->Name(), "unfinished backend stream");
                return Y_MAKE_ERROR(yexception{} << "reading from backend hasn't finished");
            }

            decoder.ForceNullInput();
            TChunkList lst = limitInput.RecvBuffered();
            descr.Input->UnRecv(std::move(lst));
        }
        return {};
    }

    bool DoCanWorkWithoutHTTP() const noexcept override {
        return true;
    }

private:
    TConfigHolder<TModuleConfig> Config_;

    THolder<TSharedStats> SharedStats_;
    THolder<IModule> Submodule_;
    THolder<TCyclesChecker> CyclesChecker_;
};

IModuleHandle* NModHttp::Handle() {
    return TModule::Handle();
}
