/*
  ICS SSL Multi Web Server - C++Builder translation of the Delphi demo
  OverbyteIcsSslMultiWebServ1.pas (demos-delphi-vcl), ICS V9.8.

  An SSL web application server with no real GUI; it is designed to become a Windows
  service. All settings come from an ini file (copied from the Delphi demo folder to
  the ICS ini directory on first run, and that copy is the one to edit). IcsHosts
  gives several hosts on several addresses and ports, each with its own SSL
  certificate and W3C log file; certificates can be ordered automatically (Let's
  Encrypt and others), hackers are blocked by filter lists and blacklists, and status
  and errors can be emailed to an administrator. The server starts when the form opens.

  Static pages come from the WebDocDir folder of each host; template pages and the
  dynamic pages in OverbyteIcsSslMultiWebPages come from WebTemplDir; a few pages are
  built in code here (CreateVirtualDocument_*); WebSocket echo and chat servers answer
  under /WebSocket/.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The default ini file and the demos-data folder are found relative to this demo's
    Win64x\<config> output folder: the ini is read from ..\demos-delphi-vcl.
  - The USE_IcsGeoTools blocks are always compiled: TIcsGeoTools (IcsGeoUtils) is in the
    ICS C++ Common package. It needs the GEO database files in
    C:\ProgramData\ICS-OpenSSL\ICS-Geodb\ (TIcsGeoTools defaults); without them it logs
    "GEO Database File Not Found" and the lookups return nothing.
    OpenSSL_OcspStaple is not defined, as in Delphi.
  - The unit globals are members of the form, and the form owns the settings the URL
    handlers share (FPagesData). The destructor frees the form's components first,
    because they can still fire logging events while they are destroyed.
  - Components created in code have no Owner and are held in std::unique_ptr; the
    diagnostic and W3C log files, the filter lists and the blacklists are freed at the
    same points as in Delphi.
  - The virtual documents are looked up in a table instead of an if/else chain.
  - FormDestroy is not translated: it is not bound in the .dfm and its body is
    commented out.
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include <System.TypInfo.hpp>
#include <algorithm>
#include <initializer_list>
#include <vector>

#include "OverbyteIcsSslMultiWebServ1.h"
#include "OverbyteIcsSslMultiWebPages.h"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsTicks64.hpp"
#include "OverbyteIcsSslUtils.hpp"
#include "OverbyteIcsHtmlUtils.hpp"
#include "OverbyteIcsSmtpProt.hpp"

#pragma package(smart_init)
#pragma link "OverbyteIcsWndControl"
#pragma resource "*.dfm"
#pragma resource "ICSMedia.res"

TWeblServerForm *WeblServerForm;

namespace {

const wchar_t *const SrvCopyRight = L" OverbyteIcsSslMultiWebServ" IcsVerLitCopyright;
const int MaxWinChars = 800000;
const wchar_t *const LogNameMask = L"\"webapp-\"yyyymmdd\".log\"";
const wchar_t *const WebLogFields =
    L"#Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query "
    L"s-port cs-username c-ip cs-version cs(User-Agent) cs(Referer) cs-host sc-status "
    L"sc-bytes cs-bytes time-taken country asn";
const wchar_t *const WebLogHdrMask = L"\"#Date: \"" ISODateMask L" " ISOTimeMask L"\"";
const int XmitBufSize = 65536;

const wchar_t *const SectionGeneral    = L"General";
const wchar_t *const KeyServLogDir     = L"ServLogDir";
const wchar_t *const KeyAdminEmailTo   = L"AdminEmailTo";
const wchar_t *const KeyAdminEmailFrom = L"AdminEmailFrom";
const wchar_t *const WebApp            = L"WEB-APP";

// Typed null for AnswerPage's UserData: a bare nullptr is ambiguous between its
// TObject* (template file) and PWideChar (resource type) overloads.
TObject *const NoUserData = nullptr;

String SpaceToPlus(const String &S)
{
    return StringReplace(S, " ", "+", TReplaceFlags() << rfReplaceAll);
}

TAuthenticationTypes AuthSet(std::initializer_list<TAuthenticationType> Types)
{
    TAuthenticationTypes Result;
    for (TAuthenticationType T : Types)
        Result << T;
    return Result;
}

String HostPort(int Port, int DefaultPort)
{
    return Port != DefaultPort ? ":" + IntToStr(Port) : String();
}

} // namespace

/* ---- WebSocket page and index page handlers ---- */

class TUrlHandlerWSClientEcho : public TUrlHandler
{
public:
    __fastcall virtual TUrlHandlerWSClientEcho(TComponent *AOwner) : TUrlHandler(AOwner) {}
    virtual void __fastcall Execute();
};

class TUrlHandlerIndexHtml : public TUrlHandler
{
public:
    __fastcall virtual TUrlHandlerIndexHtml(TComponent *AOwner) : TUrlHandler(AOwner) {}
    virtual void __fastcall Execute();
};

// websocketclient.html offers a WebSocket URL for each WEB-APP host binding.
void __fastcall TUrlHandlerWSClientEcho::Execute()
{
    TSslHttpAppSrv *Server = WeblServerForm->SslHttpAppSrv1;
    String Options;
    String Sel = " selected";
    for (int I = 0; I < Server->IcsHosts->Count; I++) {
        TIcsHost *Host = Server->IcsHosts->Items[I];
        if (!Host->HostEnabled || Host->HostTag != WebApp)
            continue;
        const String HostName = Host->HostNames->Strings[0];
        if (Host->BindNonPort != 0) {
            Options += "<option" + Sel + ">ws://" + HostName + HostPort(Host->BindNonPort, 80) +
                       "/WebSocket/Echo</option>\r\n";
            Sel = "";
        }
        if (Host->BindSslPort != 0 && !Server->NoSSL) {   // Delphi offers wss:// even with NoSSL
            const String Wss = "wss://" + HostName + HostPort(Host->BindSslPort, 443);
            Options += "<option" + Sel + ">" + Wss + "/WebSocket/Echo</option>\r\n";
            Sel = "";
            Options += "<option>" + Wss + "/WebSocket/EchoPing</option>\r\n";
            Options += "<option>" + Wss + "/WebSocket/Chat?MyName</option>\r\n";
        }
    }
    AnswerPage("", NO_CACHE, "websocketclient.html", NoUserData, ARRAYOFCONST((L"WSSURL", Options)));
    Finish();
}

void __fastcall TUrlHandlerIndexHtml::Execute()
{
    AnswerPage("", NO_CACHE, "Index.html", NoUserData,
               ARRAYOFCONST((L"serverinfo", WeblServerForm->ServerInfo())));
    Finish();
}

/* ---- Client connection class: per-request log counters and WebSocket events ---- */

__fastcall TMyHttpConnection::TMyHttpConnection(TComponent *AOwner)
    : THttpWSSrvConn(AOwner)
{
    // Keep-alive means a connection may carry several requests, so the data sent and
    // received is tracked per request.
    OnBgException = BgExceptionEvent;
    OnWSLogEvent = WeblServerForm->ClientWSLogEvent;
    OnWSHandshake = WeblServerForm->ClientWSHandshakeEvent;
    OnWSFrameRcvd = WeblServerForm->ClientWSFrameRcvdEvent;
    OnWSFrameSent = WeblServerForm->ClientWSFrameSentEvent;
    OnWSDisconnected = WeblServerForm->ClientWSDisconnectedEvent;
    OnWSReady = WeblServerForm->ClientWSReadyEvent;
    OnWSPingTimer = WeblServerForm->ClientWSPingTimerEvent;
    // DebugBody decodes all frames, DebugHdr only the main info, DebugConn only connections.
    DebugLevel = WeblServerForm->DisplayHeaderCheckBox->Checked ? DebugHdr : DebugConn;
}

void __fastcall TMyHttpConnection::BgExceptionEvent(TObject *Sender, Exception *E, bool &CanClose)
{
    WeblServerForm->Display("Client Error - " + IcsGetExceptMess(E));
}

// Also sets SessIpInfo.RemoteRDNS, a record property that can only be changed from
// inside the connection class (Delphi writes the protected field from the form).
void TMyHttpConnection::SetPeerHostName(const String &HostName)
{
    CPeerHostName = HostName;
    FSessIpInfo.RemoteRDNS = HostName;
}

/* ---- Form ---- */

__fastcall TWeblServerForm::TWeblServerForm(TComponent *Owner)
    : TForm(Owner)
{
    SslMultiWebDataModule = &FPagesData;
}

__fastcall TWeblServerForm::~TWeblServerForm()
{
    // Free the server and the other components while this form's C++ members still
    // exist: C++ destroys members before the VCL base destructor frees the components,
    // and destroying them can fire events that log.
    DestroyComponents();
    SslMultiWebDataModule = nullptr;
}

void __fastcall TWeblServerForm::IcsAppLog(TObject *Sender, const String Msg)
{
    Display(Msg);
}

// Called by the blacklists to get a country name from its ISO code.
void __fastcall TWeblServerForm::CountryNameEvent(TObject *Sender, const String ISOA2, String &CountryName)
{
    CountryName = FGeoTools ? FGeoTools->FindCountry(ISOA2) : String();   // null once FormClose freed it
}

// "Country, ASN name" for an IP address, or its ISO code when the country is unknown.
String TWeblServerForm::GeoForIp(const String &IpStr)
{
    if (!FGeoTools)
        return "";
    const String ISO2A = FGeoTools->FindISOA2Code(IpStr);
    const String CountryName = FGeoTools->FindCountry(ISO2A);
    __int64 AsnNum = 0;
    const String AsnName = FGeoTools->FindASNCode(IpStr, AsnNum);
    String Geo = CountryName.IsEmpty() ? ISO2A : CountryName;
    if (!AsnName.IsEmpty())
        Geo += ", " + AsnName;
    return Geo;
}

void __fastcall TWeblServerForm::IcsDomainNameCache1GeoEvent(TObject *Sender, const String IpStr, String &Geo)
{
    Geo = GeoForIp(IpStr);
}

// Lines are buffered here and added to the log window once a second by the timer.
void TWeblServerForm::Display(const String &Msg)
{
    FWinLinesBuff += Msg + "\r\n";
    if (FDiagLogBuffer) {
        try {
            FDiagLogBuffer->WriteLine(Msg);
        }
        catch (Exception &) {
            // Delphi ignores a failed write too; there is nowhere else to report it.
        }
    }
}

// Delphi calls FDiagLogBuffer.FlushFile unguarded, which fails when ServLogDir is empty.
void TWeblServerForm::FlushDiagLog()
{
    if (FDiagLogBuffer)
        FDiagLogBuffer->FlushFile(true);
}

void __fastcall TWeblServerForm::Timer1Timer(TObject *Sender)
{
    // Check the SSL certificates every two hours; may order expired certificates.
    if (IcsTestTrgTick64(FCertCheckTrigger)) {
        FCertCheckTrigger = IcsGetTrgMins64(120);
        try {
            if (!SslHttpAppSrv1->NoSSL) {
                Display("Regular Server Recheck for New SSL Certificates Starting");
                FlushDiagLog();
                String S1;
                // Don't stop on the first error, no exceptions.
                const bool NewFlag = SslHttpAppSrv1->RecheckSslCerts(S1, false, true);
                if (NewFlag || !S1.IsEmpty()) {
                    if (NewFlag)
                        Display("Server Recheck Loaded New SSL Certificate(s)");
                    Display("Server Recheck SSL Certificate Errors:\r\n" + S1);
                    if (!S1.IsEmpty() && FLastErrorEmail != S1)
                        SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server Recheck SSL Certificate Errors", S1);
                    FLastErrorEmail = S1;
                    ReportHosts();
                    Display("Listen Bindings:\r\n" + SslHttpAppSrv1->ListenStates());
                }
                else
                    Display("Server Recheck SSL Certificate Nothing New");
            }
            LoadHackLists();
        }
        catch (Exception &E) {
            Display("Server Recheck SSL Certificate Failed - " + E.Message);
        }
    }

    // At midnight: start new log files, recheck SSL certificates, log the configuration again.
    if (FLogDate != static_cast<int>(Date())) {
        FLogDate = static_cast<int>(Date());
        // Flush all the logs under the old date's file name.
        FlushDiagLog();
        for (auto &WebLog : FWebLogBuffers) {
            if (WebLog)
                WebLog->FlushFile(true);
        }
        Display("Nightly Server Recheck Starting");
        ReportHosts();
        Display("Listen Bindings:\r\n" + SslHttpAppSrv1->ListenStates());
        FCertCheckTrigger = Overbyteicsticks64::Trigger64Immediate;
        FlushDiagLog();
    }

    // Add the buffered lines to the log window, trimming it when it grows too large.
    const int DispLen = FWinLinesBuff.Length();
    if (DispLen > 0) {
        if (FWinDispCur + DispLen > MaxWinChars) {
            String S1 = DisplayMemo->Lines->Text;
            const int RemoveLen = MaxWinChars - DispLen - 20000;
            if (RemoveLen > 20000) {
                S1 = S1.SubString(RemoveLen, MaxInt);
                const int NewStart = S1.Pos("\r");   // start of the next line
                DisplayMemo->Text = S1.SubString(NewStart, MaxInt) + FWinLinesBuff;
                FWinDispCur = S1.Length() - NewStart + DispLen;
            }
            else {
                DisplayMemo->Lines->Text = FWinLinesBuff;
                FWinDispCur = DispLen;
            }
        }
        else {
            DisplayMemo->Lines->Add(FWinLinesBuff.SubString(1, DispLen - 2));   // without the last CRLF
            FWinDispCur += DispLen;
            SendMessage(DisplayMemo->Handle, WM_VSCROLL, SB_BOTTOM, 0);
        }
        FWinLinesBuff = "";
    }

    // Housekeeping every five minutes.
    if (IcsTestTrgTick64(FHouseKeepingTrg)) {
        FHouseKeepingTrg = IcsGetTrgSecs64(300);   // Delphi reschedules in 300 milliseconds, not the five minutes it intends
        CleanupTimeStampedDir(FPagesData.DataDir());
    }
}

// The hacker URL filters and the IP addresses never to block, both in the Data folder.
void TWeblServerForm::LoadHackLists()
{
    FFilterList->FileName = FDataDir + "hackfilterlist.txt";
    FFilterList->LoadList();
    FWhiteList->FileName = FDataDir + "whiteiplist.txt";
    FWhiteList->LoadList();
}

bool TWeblServerForm::TestDottedIp(const String &Value)
{
    const bool Result = IcsHostDottedIp(Value);
    if (Result)
        Display("Found Dotted Host: " + Value);
    return Result;
}

void __fastcall TWeblServerForm::ApplicationEventsException(TObject *Sender, Exception *E)
{
    Display("!!! Application Exception Event - " + IcsGetExceptMess(E));
}

void __fastcall TWeblServerForm::doClearClick(TObject *Sender)
{
    DisplayMemo->Lines->Clear();
    FWinDispCur = 0;
}

void __fastcall TWeblServerForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    ReportMemoryLeaksOnShutdown = (DebugHook != 0);
    FIniFileName = GetIcsIniFileName(true);
    // The OpenSSL DLLs must come from the program directory and exist.
    GSSLEAY_DLL_IgnoreOld = true;
    Application->OnException = ApplicationEventsException;
    GSSL_DLL_DIR = ExtractFileDir(IcsLowerCase(ParamStr(0))) + "\\";
    GSSL_SignTest_Check = true;
    GSSL_SignTest_Certificate = true;
    FLogDate = static_cast<int>(Date());
    FHouseKeepingTrg = IcsGetTrgSecs64(300);
    FCertCheckTrigger = Overbyteicsticks64::Trigger64Disabled;
    Timer1->Enabled = true;   // so the log window updates
    FSrvCompName = IcsGetCompName();
    doClear->OnClick = doClearClick;   // the Delphi .dfm leaves Clear Log unbound, so it did nothing
    RegisterMultiWebClasses();

    // Use the ini file in the ICS ini directory (usually C:\Users\(me)\AppData\Local\ICS);
    // if it is missing, copy the default one from the Delphi demo folder.
    if (!FileExists(FIniFileName)) {
        const String DefaultIni = IcsAbsolutisePath(ExtractFilePath(ParamStr(0)) +
            "..\\..\\..\\demos-delphi-vcl\\" + ChangeFileExt(ExtractFileName(ParamStr(0)), ".ini"));
        if (FileExists(DefaultIni)) {
            try {
                std::unique_ptr<TStringList> List(new TStringList());
                List->LoadFromFile(DefaultIni);
                List->SaveToFile(FIniFileName);
                Display("Copied default INI file to: " + FIniFileName + ", where it should be edited");
            }
            catch (Exception &E) {
                Display("Failed to copy default INI file - " + E.Message);   // Delphi ignores this
            }
        }
    }
    String LogDir;
    {
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        LogDir = IniFile->ReadString(SectionGeneral, KeyServLogDir, "");
        FAdminEmailTo = IniFile->ReadString(SectionGeneral, KeyAdminEmailTo, "");
        FAdminEmailFrom = IniFile->ReadString(SectionGeneral, KeyAdminEmailFrom, "");
        IcsLoadMailQuFromIni(IniFile.get(), IcsMailQueue, "MailQueue");
        IcsLoadRestEmailFromIni(IniFile.get(), IcsRestEmail, "RestEmail");   // OAuth2 secrets and token
        FOldRefrToken = IcsRestEmail->RefrToken;                             // to see if it changes
    }
    if (!LogDir.IsEmpty()) {
        ForceDirectories(ExtractFileDir(LogDir));
        // The file name is a mask that adds the date.
        const String NameMask = "\"" + IncludeTrailingPathDelimiter(LogDir) + "\"" + LogNameMask;
        const String Header = "\"" + String(SrvCopyRight) + "\r\n" +
            "Log File Created: \"dd mmm yyyy hh:mm:ss\"\r\n" +
            "Computer: " + FSrvCompName + "\"\r\n";
        FDiagLogBuffer.reset(new TIcsBuffLogStream(nullptr, NameMask, Header, FileCPUtf8));
        Display("Log File: " + FDiagLogBuffer->FullName);
    }
    Display("INI file: " + FIniFileName);

    // MMDB reader for country and ASN lookups; the databases are loaded when the server starts.
    FGeoTools.reset(new TIcsGeoTools(nullptr));
    FGeoTools->OnAppLog = IcsAppLog;
    FFilterList.reset(new TIcsFilterList(nullptr));
    FWhiteList.reset(new TIcsIpAddrList(nullptr));
    FFilterList->OnAppLog = IcsAppLog;
    FWhiteList->OnAppLog = IcsAppLog;
    // demos-data is three levels up from Win64x\<config> (Delphi: four, from bin\<platform>\<config>).
    FBaseDir = IcsAbsolutisePath(IncludeTrailingPathDelimiter(ExtractFilePath(Application->ExeName)) +
                                 "..\\..\\..\\demos-data\\");
    FDataDir = FBaseDir + "WebAppServerData\\Data\\";
    FUploadsDir = FBaseDir + "WebAppServerData\\Uploads\\";
    FSessionFile = FDataDir + "Sessions.dat";

    FlushDiagLog();
    PostMessage(Handle, WM_STARTUP, 0, 0);
}

void __fastcall TWeblServerForm::RecheckCertsButtonClick(TObject *Sender)
{
    FCertCheckTrigger = Overbyteicsticks64::Trigger64Immediate;
}

void __fastcall TWeblServerForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    if (FFinalized)
        return;   // only once
    FFinalized = true;
    try {
        Timer1->Enabled = false;
        FCertCheckTrigger = Overbyteicsticks64::Trigger64Disabled;
        FHouseKeepingTrg = Overbyteicsticks64::Trigger64Disabled;
        StopButtonClick(this);
        StopQueueMail();
    }
    catch (Exception &E) {
        ShowMessage("Exception Stopping in FormClose - " + E.Message);
    }

    FDiagLogBuffer.reset();
    FPagesData.IcsGeoTools = nullptr;   // the pages share it
    FGeoTools.reset();
    try {
        FFilterList.reset();
        FWhiteList.reset();
        FHackBlackList.reset();
        FAttemptsBlackList.reset();
    }
    catch (Exception &E) {
        ShowMessage("Exception Freeing in FormClose - " + E.Message);
    }
}

void __fastcall TWeblServerForm::WMStartup(TMessage &Msg)
{
    StartButtonClick(this);
}

void TWeblServerForm::ReportHosts()
{
    FStatSrvSslCert = "";
    TIcsHostCollection *Hosts = SslHttpAppSrv1->IcsHosts;
    Display("Total server hosts " + IntToStr(Hosts->Count));
    for (int I = 0; I < Hosts->Count; I++) {
        TIcsHost *Host = Hosts->Items[I];
        if (Host->CertValRes == chainFail)
            Display("Server SSL Certificate Errors - " + Host->DisplayName);   // might want to stop and warn here

        // Site info for the log and the demo menu page.
        String Info = "Site Host " + IntToStr(I + 1) + " [" + Host->DisplayName + "] " +
                      Host->HostNames->CommaText + "\r\n" +
                      "Bindings: " + Host->BindInfo + "\r\n" +
                      "Web Pages Root: " + Host->WebDocDir + "\r\n" +
                      "Templates Root: " + Host->WebTemplDir + "\r\n";
        if (Host->WebRedirectStat != 0)
            Info += "Redirection to: " + Host->WebRedirectURL + "\r\n";
        if (!Host->WellKnownPath.IsEmpty())
            Info += "Well-Known Root: " + Host->WellKnownPath + "\r\n";
        if (!SslHttpAppSrv1->NoSSL) {
            Info += "SSL Security Level: " +
                GetEnumName(__delphirtti(TSslSrvSecurity), static_cast<int>(Host->SslSrvSecurity)) + "\r\n";
            if (Host->CliCertMethod > sslCliCertNone)
                Info += "SSL Client Certificate: " +
                    GetEnumName(__delphirtti(TSslCliCertMethod), static_cast<int>(Host->CliCertMethod)) + "\r\n";
            String AutoOrder = "Disabled";
            if (Host->BindSslPort != 0) {
                if (SslHttpAppSrv1->SslCertAutoOrder) {
                    const TSupplierProto Proto = Host->CertSupplierProto;
                    if (Proto == SuppProtoAcmeV2)
                        AutoOrder = "Acme Supplier: " + String(AcmeSupplierLits[Host->AcmeSupplier]);
                    else if (Proto <= SuppProtoServtas)
                        AutoOrder = SupplierProtoLits[Proto];
                    else
                        AutoOrder = "Unknown " + IntToStr(static_cast<int>(Proto));
                    if (Proto > SuppProtoNone) {
                        const TChallengeType Chall = Host->CertChallenge;
                        if (Chall > ChallNone && Chall <= ChallManual)
                            AutoOrder += ",  Challenge: " + String(ChallengeTypeLits[Chall]) +
                                         ", Database: " + Host->CertDirWork;
                        else
                            AutoOrder += ",  Challenge: Unknown " + IntToStr(static_cast<int>(Chall));
                    }
                }
                Info += "SSL Certificate Auto Order: " + AutoOrder + "\r\n";
            }
            Info += "SSL Certificate: " + Host->CertErrs + "\r\n" + Host->CertInfo + "\r\n";
        }
        FStatSrvSslCert += Info + "\r\n";
    }
    FStatSrvSslCertWeb = StringReplace(FStatSrvSslCert, "\r\n", "<BR>\r\n", TReplaceFlags() << rfReplaceAll);
    Display(FStatSrvSslCert);
}

void TWeblServerForm::StartQueueMail()
{
    try {
        if (!IcsMailQueue->Active &&
            (IcsMailQueue->MailQuDir.IsEmpty() || !ForceDirectories(IcsMailQueue->MailQuDir))) {
            Display("!! Failed to Start Mail Queue, No Directory");
            return;
        }
        if (IcsMailQueue->MailServers->Count == 0) {
            Display("!! Failed to Start Mail Queue, No Mail Servers");
            return;
        }
        // The account passwords should be stored encrypted and decrypted here.
        String S;
        for (int I = 0; I < IcsMailQueue->MailServers->Count; I++)
            S += IcsMailQueue->MailServers->Items[I]->Host + ", ";
        IcsMailQueue->Active = true;
        if (IcsMailQueue->Active)
            Display("Started Mail Queue OK, Servers: " + S);
    }
    catch (Exception &E) {
        Display("!! Failed to Start Mail Queue - " + IcsGetExceptMess(&E));
    }
}

void TWeblServerForm::StopQueueMail()
{
    if (IcsMailQueue->Active && IcsMailQueue->MailImmItems > 0) {
        Display("Waiting up to 30 seconds to send Queued Mail Items");
        IcsMailQueue->WaitSendandStop(30);
    }
}

void TWeblServerForm::SendAdminEmail(const String &EmailTo, const String &Subject, const String &Body)
{
    if (EmailTo.IsEmpty() || FAdminEmailFrom.IsEmpty())
        return;
    if (!IcsMailQueue->Active)
        StartQueueMail();   // restart if not running
    if (!IcsMailQueue->Active) {
        Display("Error Starting Mail Queue");
        return;
    }
    try {
        THtmlSmtpCli *Smtp = IcsMailQueue->QuHtmlSmtp;
        // The mail queue ini setting AdminEmailFrom is the domain, "@example.com".
        const String sTempFrom = "\"" + FSrvCompName + "\" <" + FSrvCompName + FAdminEmailFrom + ">";   // Delphi omits the closing '>'
        Smtp->EmailFiles->Clear();
        Smtp->RcptName->Clear();
        Smtp->Allow8bitChars = true;
        Smtp->ContentType = smtpPlainText;
        Smtp->WrapMessageText = false;
        Smtp->WrapMsgMaxLineLen = 76;
        Smtp->PlainText->Clear();
        Smtp->PlainText->Text = String(SrvCopyRight) + "\r\n\r\n" + Body;
        Display("Queuing Email to " + EmailTo + " from " + sTempFrom);
        Smtp->RcptName->Add(EmailTo);
        Smtp->FromName = sTempFrom;
        Smtp->HdrCc = "";
        Smtp->HdrTo = EmailTo;
        Smtp->HdrFrom = sTempFrom;
        Smtp->HdrReplyTo = sTempFrom;
        Smtp->HdrSubject = Subject;
        Smtp->XMailer = "";
        if (IcsMailQueue->QueueMail() > 0)
            Display("Email Form Queued OK");
        else
            Display("Failed to Queue Email: " + Smtp->ErrorMessage);
    }
    catch (Exception &E) {
        Display("Failed to Queue Email: " + IcsGetExceptMess(&E));
    }
}

// Logging of the HTTP request and response headers is done by the server itself.
void __fastcall TWeblServerForm::DisplayHeaderCheckBoxClick(TObject *Sender)
{
    if (DisplayHeaderCheckBox->Checked)
        SslHttpAppSrv1->WSDebugOptions = SslHttpAppSrv1->WSDebugOptions << wsdlogHdr;
    else
        SslHttpAppSrv1->WSDebugOptions = SslHttpAppSrv1->WSDebugOptions >> wsdlogHdr;
}

// The URLs that reach the default document of each enabled host.
String TWeblServerForm::BuildDemoURIs()
{
    String Result;
    for (int I = 0; I < SslHttpAppSrv1->IcsHosts->Count; I++) {
        TIcsHost *Host = SslHttpAppSrv1->IcsHosts->Items[I];
        if (!Host->HostEnabled)
            continue;
        if (Host->BindNonPort != 0)
            Result += "http://" + Host->HostNames->Strings[0] + HostPort(Host->BindNonPort, 80) +
                      "/" + Host->WebDefDoc + "\r\n";
        if (Host->BindSslPort != 0 && !SslHttpAppSrv1->NoSSL)
            Result += "https://" + Host->HostNames->Strings[0] + HostPort(Host->BindSslPort, 443) +
                      "/" + Host->WebDefDoc + "\r\n";
    }
    return Result;
}

void __fastcall TWeblServerForm::StartButtonClick(TObject *Sender)
{
    try {
        // Main web server settings from the ini file; the built-in root CA bundle if none is given.
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        IcsLoadTHttpAppSrvFromIni(IniFile.get(), SslHttpAppSrv1, "WebAppServer");

        FServerInfo = SrvCopyRight;
        if (!SslHttpAppSrv1->NoSSL) {
            IcsSslRootCAStore->Initialise();   // loads OpenSSL and the internal root CAs if needed
            Display(String(SrvCopyRight) + "\r\nSSL/TLS " + IcsReportOpenSSLVer(true));
            FServerInfo += "<br>SSL/TLS " + IcsReportOpenSSLVer(false);
            // Ordering X509 certificates may need a proxy server.
            IcsSslX509Certs->ProxyURL = IniFile->ReadString("WebAppServer", "X509ProxyURL", "");
        }
        FServerInfo += "<br>Running on: " + FSrvCompName;

        // The server hosts; takes defaults from IcsLoadTHttpAppSrvFromIni and checks the SSL files exist.
        TIcsHostCollection *Hosts = SslHttpAppSrv1->IcsHosts;
        IcsLoadIcsHostsFromIni(IniFile.get(), Hosts, "Host", SslHttpAppSrv1->WSocketServer);
        if (Hosts->Count <= 0) {
            Display("Can Not Start Server - No Source Server Hosts Configured");
            return;
        }
        Display("Number of Hosts Configured: " + IntToStr(Hosts->Count));

        for (int J = 0; J < Hosts->Count; J++) {
            TIcsHost *Host = Hosts->Items[J];
            if (!Host->HostEnabled)
                continue;
            // Directories without a drive letter are relative to the ICS demos-data folder.
            if (Host->WebDocDir.Pos("WebAppServerData\\") == 1)
                Host->WebDocDir = FBaseDir + Host->WebDocDir;
            if (Host->WebTemplDir.Pos("WebAppServerData\\") == 1)
                Host->WebTemplDir = FBaseDir + Host->WebTemplDir;

            // The W3C log goes in the ini file's folder unless the host names one.
            if (Host->WebLogDir.IsEmpty())
                Host->WebLogDir = IncludeTrailingPathDelimiter(ExtractFileDir(FIniFileName)) + Host->HostTag;
            try {
                ForceDirectories(ExtractFileDir(Host->WebLogDir));
            }
            catch (Exception &) {
                Display("Failed to Create Directory: " + Host->WebLogDir);
            }
            // The file name is a mask that adds the date.
            Host->WebLogDir = "\"" + IncludeTrailingPathDelimiter(Host->WebLogDir) + "\"" + LogNameMask;
        }

        // Validate the hosts and keep the site certificate information.
        try {
            const String Errs = SslHttpAppSrv1->ValidateHosts(false, true);   // don't stop on the first error, no exceptions
            if (!Errs.IsEmpty()) {
                Display("Server Validation Errors:\r\n" + Errs);
                if (!FAdminEmailTo.IsEmpty())
                    SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server Validation Errors", Errs);
            }
            ReportHosts();
            Display("Required Listen Bindings:\r\n" + SslHttpAppSrv1->ListenStates());
        }
        catch (Exception &E) {
            Display("Host Validation Failed, Server Stopped - " + E.Message);
            return;
        }

        // Server defaults; most were set by IcsLoadTHttpAppSrvFromIni.
        SslHttpAppSrv1->TemplateDir = Hosts->Items[0]->WebTemplDir;
        SslHttpAppSrv1->DocDir = Hosts->Items[0]->WebDocDir;
        SslHttpAppSrv1->DefaultDoc = Hosts->Items[0]->WebDefDoc;
        SslHttpAppSrv1->ServerHeader = DefServerHeader;   // the latest version
        SslHttpAppSrv1->ClientClass = __classid(TMyHttpConnection);
        SslHttpAppSrv1->Options = SslHttpAppSrv1->Options << hoContentEncoding;   // compress replies
        SslHttpAppSrv1->MaxBlkSize = XmitBufSize;
        SslHttpAppSrv1->SocketErrs = wsErrFriendly;
        SslHttpAppSrv1->ExclusiveAddr = true;
        if (SslHttpAppSrv1->SessionTimeout < 30)
            SslHttpAppSrv1->SessionTimeout = 300;   // sanity check
        SslHttpAppSrv1->WSDebugOptions = DefWSDebugServer;   // can be reduced for less logging

        // WebDAV and the newer methods; Lock and Unlock are not planned.
        SslHttpAppSrv1->Options = SslHttpAppSrv1->Options << hoAllowOptions << hoAllowPut << hoAllowDelete
            << hoAllowTrace << hoAllowPatch << hoAllowPropFind << hoAllowMkCol << hoAllowCopy
            << hoAllowMove << hoAllowPropPatch << hoAllowLock << hoAllowUnlock << hoAllowQuery;

        // The server can log the HTTP request and response headers itself.
        if (DisplayHeaderCheckBox->Checked)
            SslHttpAppSrv1->WSDebugOptions = SslHttpAppSrv1->WSDebugOptions << wsdlogHdr;

        try {
            ForceDirectories(FDataDir);
            ForceDirectories(FUploadsDir);
            if (!SslHttpAppSrv1->TemplateDir.IsEmpty())
                ForceDirectories(SslHttpAppSrv1->TemplateDir);
            ForceDirectories(SslHttpAppSrv1->DocDir);
            ForceDirectories(SslHttpAppSrv1->DocDir + "\\Js");
            ForceDirectories(SslHttpAppSrv1->DocDir + "\\Styles");
            ForceDirectories(SslHttpAppSrv1->DocDir + "\\Images");
        }
        catch (Exception &) {
            Display("Failed to Create Directory: " + SslHttpAppSrv1->DocDir);
        }

        // Settings the URL handlers share.
        FPagesData.IniFileName = FIniFileName;
        FPagesData.OnDisplay = SslHttpAppSrv1Display;
        FPagesData.SetDataDir(FDataDir);
        FPagesData.ImagesDir = SslHttpAppSrv1->DocDir + "\\Images";
        FPagesData.LoadConfig();
        FPagesData.UploadDir = FUploadsDir;
        SslHttpAppSrv1->UploadDir = FUploadsDir;
        FPagesData.IcsMailQueue = IcsMailQueue;
        if (!IcsMailQueue->MailQuDir.IsEmpty())
            ForceDirectories(IcsMailQueue->MailQuDir);

        // The allowed paths and handlers must match a HostTag of a host in the ini file.
        // Simple web server, no handlers: all static files are allowed.
        SslHttpAppSrv1->AddGetAllowedPath("/", afBeginBy, "HTTP-WEB");

        // Application web server: static documents only from these folders.
        SslHttpAppSrv1->AddGetAllowedPath("/", afBeginBy, WebApp);
        SslHttpAppSrv1->AddGetAllowedPath("/js/", afBeginBy, WebApp);
        SslHttpAppSrv1->AddGetAllowedPath("/styles/", afBeginBy, WebApp);
        SslHttpAppSrv1->AddGetAllowedPath("/images/", afBeginBy, WebApp);

        // The dynamic page handlers.
        SslHttpAppSrv1->AddGetHandler("/", __classid(TUrlHandlerIndexHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler("/index.html", __classid(TUrlHandlerIndexHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler("/appindex.html", __classid(TUrlHandlerDefaultDoc), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlLogin, __classid(TUrlHandlerLoginFormHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlDoLoginSecure, __classid(TUrlHandlerDoLoginSecureHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlCounter, __classid(TUrlHandlerCounterJpg), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlHomePage, __classid(TUrlHandlerHomePageHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlConfigForm, __classid(TUrlHandlerConfigFormHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlConfigLogoPng, __classid(TUrlHandlerConfigLogoPng), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlDoConfigConfirmSaveHtml, __classid(TUrlHandlerDoConfigConfirmSaveHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler(UrlDoConfigHtml, __classid(TUrlHandlerDoConfigHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlCounterViewHtml, __classid(TUrlHandlerCounterViewHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlAjaxFetchCounter, __classid(TUrlHandlerAjaxFetchCounter), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlJavascriptErrorHtml, __classid(TUrlHandlerJavascriptErrorHtml), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler(UrlHeadForm, __classid(TUrlHandlerHead), hgWillSendMySelf, WebApp);

        // The simplest possible handler.
        SslHttpAppSrv1->AddGetHandler("/HelloWorld.html", __classid(TUrlHandlerHelloWorld), hgWillSendMySelf, WebApp);

        // These replace the POST code of OverbyteIcsWebServ1, which does not work for HttpAppSrv.
        SslHttpAppSrv1->AddPostHandler("/cgi-bin/FormHandler", __classid(TUrlHandlerUploadData), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddGetHandler("/mailer.html", __classid(TUrlHandlerMailer), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler("/mailer.html", __classid(TUrlHandlerMailer), hgWillSendMySelf, WebApp);

        SslHttpAppSrv1->AddGetHandler("/uploadfile.html", __classid(TUrlHandlerUploadFile), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler("/uploadfile.html", __classid(TUrlHandlerUploadFile), hgWillSendMySelf, WebApp);

        SslHttpAppSrv1->AddGetHandler("/websocketclient.html", __classid(TUrlHandlerWSClientEcho), hgWillSendMySelf, WebApp);

        SslHttpAppSrv1->AddGetHandler(UrlPostinfo, __classid(TUrlHandlerPostInfo), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler(UrlPostinfo, __classid(TUrlHandlerPostInfo), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPutHandler(UrlPostinfo, __classid(TUrlHandlerPostInfo), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddDeleteHandler(UrlPostinfo, __classid(TUrlHandlerPostInfo), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddQueryHandler(UrlPostinfo, __classid(TUrlHandlerPostInfo), hgWillSendMySelf, WebApp);

        SslHttpAppSrv1->AddGetHandler("/DemoAuthAll.html", __classid(TUrlHandlerDemoAuthAll), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler("/DemoAuthAll.html", __classid(TUrlHandlerDemoAuthAll), hgWillSendMySelf, WebApp);

        SslHttpAppSrv1->AddGetHandler(UrlIpAddrLookup, __classid(TUrlIpAddrLookup), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddQueryHandler(UrlIpAddrLookup, __classid(TUrlIpAddrLookup), hgWillSendMySelf, WebApp);
        SslHttpAppSrv1->AddPostHandler(UrlIpAddrLookup, __classid(TUrlIpAddrLookup), hgWillSendMySelf, WebApp);

        // Sessions saved when the server last stopped.
        if (FileExists(FSessionFile)) {
            try {
                SslHttpAppSrv1->WSessions->LoadFromFile(FSessionFile);
                Display(IntToStr(SslHttpAppSrv1->SessionsCount) + " sessions loaded");
            }
            catch (Exception &) {
                // Clear anything partly loaded and delete the (corrupted) file.
                SslHttpAppSrv1->WSessions->Clear();
                DeleteFile(FSessionFile);
                Display("Unable to load existing sessions");
            }
        }

        // Hacker filters and blacklists.
        LoadHackLists();
        if (!FAttemptsBlackList)
            FAttemptsBlackList.reset(new TIcsBlacklist(nullptr));
        FAttemptsBlackList->SaveAscii = true;
        FAttemptsBlackList->ListName = "Attempts";
        FAttemptsBlackList->OnBlackLogEvent = IcsAppLog;
        FAttemptsBlackList->OnCountryNameEvent = CountryNameEvent;
        FAttemptsBlackList->BlockAfterMins = 1440;
        FAttemptsBlackList->BlockAttempts = 1000;
        FAttemptsBlackList->BlockForMins = 720;
        FAttemptsBlackList->BlackFile = FDataDir + "AttemptsBlacklist.lst";
        if (!FHackBlackList)
            FHackBlackList.reset(new TIcsBlacklist(nullptr));
        FHackBlackList->ListName = "Hackers";
        FHackBlackList->OnBlackLogEvent = IcsAppLog;
        FHackBlackList->OnCountryNameEvent = CountryNameEvent;
        FHackBlackList->BlockAfterMins = 1;
        FHackBlackList->BlockAttempts = 1;
        FHackBlackList->BlockForMins = 1440;
        FHackBlackList->BlackFile = FDataDir + "HackBlacklist.lst";

        // GEO databases; DBCountryFile and DBASBFile default to C:\ProgramData\ICS-OpenSSL\ICS-Geodb\.
        if (FileExists(FGeoTools->DBCountryFile)) {
            FGeoTools->UseResources = true;
            FGeoTools->LoadDBCountry();
            FGeoTools->LoadDBASN();
            FGeoTools->LoadIcsCountries();
        }
        else
            Display("GEO Database File Not Found: " + FGeoTools->DBCountryFile);
        FPagesData.IcsGeoTools = FGeoTools.get();   // shared with the pages
        FChallengeTestingActive = false;

        // One W3C log file per host; hosts with the same file name share it.
        // Delphi indexes its array from 1, past the end when every host has its own log,
        // and matches shared logs by host number rather than by log number.
        FWebLogBuffers.clear();
        for (int J = 0; J < Hosts->Count; J++) {
            TIcsHost *Host = Hosts->Items[J];
            Host->WebLogIdx = -1;
            if (!Host->HostEnabled)
                continue;
            Display("W3C logging for host: " + Host->HostTag + " to " + FormatDateTime(Host->WebLogDir, Date()));
            for (int K = 0; K < J; K++) {
                TIcsHost *Earlier = Hosts->Items[K];
                if (Earlier->WebLogIdx >= 0 && Earlier->WebLogDir == Host->WebLogDir) {
                    Host->WebLogIdx = Earlier->WebLogIdx;
                    break;
                }
            }
            if (Host->WebLogIdx < 0) {
                Host->WebLogIdx = static_cast<int>(FWebLogBuffers.size());
                FWebLogBuffers.emplace_back(new TIcsBuffLogStream(nullptr, Host->WebLogDir, WebLogHdrMask, FileCPUtf8));
                FWebLogBuffers.back()->WriteLine(WebLogFields);
            }
        }

        // Start as many hosts as possible.
        const String Errs = SslHttpAppSrv1->Start(true);
        if (!Errs.IsEmpty())
            Display("Start Web Server Error - " + Errs);
        const String S = String(SslHttpAppSrv1->ListenAllOK() ? "Started OK, " : "Failed to Start, ") +
                         "Listen Bindings:\r\n" + SslHttpAppSrv1->ListenStates();
        Display(S);
        FCertCheckTrigger = IcsGetTrgSecs64(15);   // the first check is early, to order new certificates
        StartButton->Enabled = false;
        StopButton->Enabled = true;
        if (!FAdminEmailTo.IsEmpty())
            SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server Started",
                           "ICS Multi Web Server\r\n\r\n" + S + "\r\n" + Errs);

        Display("Now browse to one of these URLs:\r\n" + BuildDemoURIs());
        FlushDiagLog();
    }
    catch (Exception &E) {
        Display("Failed to start web server - " + E.Message);
    }
}

void __fastcall TWeblServerForm::StopButtonClick(TObject *Sender)
{
    if (!StopButton->Enabled)
        return;
    IcsSslX509Certs->CloseAccount();
    FCertCheckTrigger = Overbyteicsticks64::Trigger64Disabled;
    StartButton->Enabled = true;
    StopButton->Enabled = false;
    FlushDiagLog();
    SslHttpAppSrv1->Stop();

    // Report the blacklisted remote addresses and save the lists.
    String S;
    if (FAttemptsBlackList)
        S += FAttemptsBlackList->ListName + " Report:\r\n" + FAttemptsBlackList->ReportBlackList(true) + "\r\n";
    if (FHackBlackList)
        S += FHackBlackList->ListName + " Report:\r\n" + FHackBlackList->ReportBlackList(true) + "\r\n";
    Display(S);
    if (FHackBlackList)
        FHackBlackList->FlushToFiles();
    if (FAttemptsBlackList)
        FAttemptsBlackList->FlushToFiles();

    FWebLogBuffers.clear();   // closes every web log; Delphi missed the last one
    if (!FAdminEmailTo.IsEmpty()) {
        IcsMailQueue->QuStartDelay = 0;   // no delay sending the last email
        SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server Stopped", "ICS Multi Web Server Stopped\r\n");
    }
    Display("Server stopped");
    FlushDiagLog();
}

/* Writes the W3C log, similar to Microsoft IIS; a log line looks something like this:
   2009-07-03 12:34:48 WebAppDemo PC19 0.0.0.0 GET /login/loginform.html - 20105 - 192.168.1.119 HTTP/1.1
   Mozilla/5.0+(Windows;...) http://pc19:20105/ pc19:20105 200 2101 1138 31 */
void __fastcall TWeblServerForm::SslHttpAppSrv1AfterAnswer(TObject *Sender, TObject *Client)
{
    TMyHttpConnection *RemoteClient = static_cast<TMyHttpConnection *>(Client);
    const int LogIdx = RemoteClient->WebLogIdx;
    if (LogIdx < 0)
        return;   // no logging
    String Info = FormatDateTime(ISODateMask L" " ISOTimeMask, Now());
    try {
        const TIcsSessIpInfo IpInfo = RemoteClient->SessIpInfo;
        // The remote host name instead of the IP address if there is one.
        const String Host = RemoteClient->CPeerHostName.IsEmpty() ? RemoteClient->CPeerAddr
                                                                  : RemoteClient->CPeerHostName;
        const String NewParams = RemoteClient->Params.IsEmpty() ? String("-") : RemoteClient->Params;
        String NewUser = SpaceToPlus(RemoteClient->AuthUserName);
        if (NewUser.IsEmpty())
            NewUser = "-";
        const __int64 CurRead = RemoteClient->ReadCount - RemoteClient->CLastRead;
        const __int64 CurWrite = RemoteClient->WriteCount - RemoteClient->CLastWrite;
        const __int64 Duration = IcsElapsedMsecs64(RemoteClient->CStartTick);
        // #Fields: date time s-sitename s-computername s-ip cs-method cs-uri-stem cs-uri-query
        //   s-port cs-username c-ip cs-version cs(User-Agent) cs(Referer) cs-host sc-status
        //   sc-bytes cs-bytes time-taken country asn
        Info += " " + RemoteClient->HostTag + " " + FSrvCompName + " " + RemoteClient->CServerAddr + " " +
                RemoteClient->Method + " " + SpaceToPlus(RemoteClient->Path) + " " + SpaceToPlus(NewParams) +
                " " + RemoteClient->CServerPort + " " + NewUser + " " + Host + " " + RemoteClient->Version +
                " " + SpaceToPlus(RemoteClient->RequestUserAgent) + " " + SpaceToPlus(RemoteClient->RequestReferer) +
                " " + RemoteClient->RequestHost + " " + IntToStr(RemoteClient->AnswerStatus) +
                " " + IntToStr(CurWrite) + " " + IntToStr(CurRead) + " " + IntToStr(Duration) +
                " " + SpaceToPlus(IpInfo.CountryName) + " " + SpaceToPlus(IpInfo.AsnName);
        if (LogIdx < static_cast<int>(FWebLogBuffers.size()) && FWebLogBuffers[LogIdx])
            FWebLogBuffers[LogIdx]->WriteLine(Info);   // a separate file for each host
        else
            Display("No Web Log Assigned");
    }
    catch (Exception &E) {
        Display("AfterAnswer Error - " + Info + " - " + IcsGetExceptMess(&E));
    }
    Display("Answer Log - " + Info);
    RemoteClient->CLastRead = RemoteClient->ReadCount;   // ready for the next request
    FlushDiagLog();
}

void __fastcall TWeblServerForm::SslHttpAppSrv1AuthGetPassword(TObject *Sender, TObject *Client, String &Password)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("AuthGetPassword for \"" + ClientCnx->Path + "\" AuthType is \"" +
            AuthTypesToString(ClientCnx->AuthTypes) + "\" for Username: " + ClientCnx->AuthUserName);
    if (ClientCnx->AuthTypes == AuthSet({atNtlm}))
        return;   // Windows validates NTLM credentials
    // A real server would look the user name up in a database to get the password.
    // Hardcoded for testing; the password is the same for every method.
    if (ClientCnx->AuthUserName == "test")
        Password = "password";
    if (ClientCnx->AuthUserName == "webdav")
        Password = "password";
}

void __fastcall TWeblServerForm::SslHttpAppSrv1AuthGetType(TObject *Sender, TObject *Client)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    // Each demo page and the authentication methods it accepts.
    const struct {
        const wchar_t *Path;
        TAuthenticationTypes Types;
        const wchar_t *Realm;
    } AuthPages[] = {
        {L"/DemoBasicAuth.html",   AuthSet({atBasic}),                         L"DemoBasicAuth"},
        {L"/DemoDigestAuth.html",  AuthSet({atDigest}),                        L"DemoDigestAuth"},
        {L"/DemoDigest2Auth.html", AuthSet({atDigestSha2}),                    L"DemoDigest2Auth"},
        {L"/DemoDigestsAll.html",  AuthSet({atDigest, atDigestSha2}),          L"DemoDigestsAll"},
        {L"/DemoNtlmAuth.html",    AuthSet({atNtlm}),                          L"DemoNtlmAuth"},
        {L"/DemoAuthAll.html",     AuthSet({atBasic, atDigest, atDigestSha2}), L"DemoAuthAll"},
    };
    ClientCnx->AuthTypes = TAuthenticationTypes();
    for (const auto &Page : AuthPages) {
        if (CompareText(ClientCnx->Path, Page.Path) == 0) {
            ClientCnx->AuthTypes = Page.Types;
            ClientCnx->AuthRealm = Page.Realm;
            break;
        }
    }

    // The WebDAV commands are always authenticated.
    switch (ClientCnx->RequestMethod) {
        case httpMethodPropFind: case httpMethodMkCol: case httpMethodCopy: case httpMethodMove:
        case httpMethodPropPatch: case httpMethodLock: case httpMethodUnlock:
            ClientCnx->AuthTypes = AuthSet({atBasic, atDigest, atDigestSha2});
            ClientCnx->AuthRealm = "WebDAV";
            break;
        default:
            break;
    }

    if (ClientCnx->AuthTypes != TAuthenticationTypes())
        Display("Request requires authentication \"" + ClientCnx->Path + "\" AuthType is \"" +
                AuthTypesToString(ClientCnx->AuthTypes) + "\", ReqAuth: " + ClientCnx->RequestAuth);
}

void __fastcall TWeblServerForm::SslHttpAppSrv1AuthNtlmBeforeValidate(TObject *Sender, TObject *Client, bool &Allow)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Allow = !ClientCnx->AuthNtlmSession->Username.IsEmpty() &&
            ClientCnx->AuthNtlmSession->Domain != "SomeDomain";
}

void __fastcall TWeblServerForm::SslHttpAppSrv1AuthResult(TObject *Sender, TObject *Client, bool Success)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    // To always pop up the browser's login dialog with Digest authentication when the
    // nonce is stale, FAuthDigestStale can be set back to false here (never to true).
    // A nonce is stale once AuthDigestNonceLifeTimeMin has expired.

    if (Success && ClientCnx->AuthUserName == "webdav")   // hardcoded for testing, enables WebDAV commands
        ClientCnx->WebDAVAcc = webdavWrite;

    Display("Authentication result " + String(Success ? "OK" : "failed") + " with type " +
            HttpAuthTypeNames[ClientCnx->AuthGetMethod()] + ", Username: " + ClientCnx->AuthUserName +
            " for " + ClientCnx->Path);

    if (!Success && ClientCnx->AuthTypes == AuthSet({atNtlm}) && ClientCnx->AuthNtlmSession)
        Display(ClientCnx->AuthNtlmSession->AuthErrorDesc());   // just for debugging
}

void __fastcall TWeblServerForm::SslHttpAppSrv1BgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Exception processing page - " + E->ClassName() + ": " + E->Message);
    CanClose = false;   // let the server continue to accept connections
}

// Called before OnClientConnect, so a connection can be refused before it is accepted.
void __fastcall TWeblServerForm::SslHttpAppSrv1HttpAcceptFilter(TObject *Sender, TObject *Client,
                                                                TIcsSessIpInfo &SessIpInfo, bool &Allowed)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    String LogISO, AsnInfo, Geo;
    TIcsSessIpInfo &S = SessIpInfo;

    // Country, region and ASN of the remote address; the results are kept in SessIpInfo.
    if (FGeoTools) {
        S.ISOA2 = FGeoTools->FindISOA2Code(S.RemoteAddr);
        const TIcsCountry Country = FGeoTools->FindNameRec(S.ISOA2);
        S.CountryName = Country.Country;
        S.RegionName = FGeoTools->FindRegion(Country.SubRegion);
        LogISO = " ISO=" + S.ISOA2 + " " + S.CountryName + ", Region " + S.RegionName;
        S.AsnName = FGeoTools->FindASNCode(S.RemoteAddr, S.AsnNum);
        if (S.AsnNum > 0)
            AsnInfo = S.AsnName + " (" + IntToStr(S.AsnNum) + ")";
        Geo = S.CountryName;
        if (!S.AsnName.IsEmpty())
            Geo += ", " + S.AsnName;

        // Block some regions. Let's Encrypt and Google Trust Services check challenges from
        // several countries, so all addresses are allowed while challenges are being checked.
        if (Country.Region == IcsRegionAsia || Country.Region == IcsRegionAfrica ||
            Country.SubRegion == IcsRegionEastEurope || Country.SubRegion == IcsRegionLatinAmerica) {
            if (!FWhiteList->CheckIpAddr(S.RemoteAddr, true) && !FChallengeTestingActive) {
                FHackBlackList->AddBlackListSoc(S.SocRemoteAddr, 9999, S.ISOA2, AsnInfo);
                Display("Region Blocked, Connection Refused: " + S.RemoteAddr + LogISO);
                Allowed = false;
                return;
            }
            Display("Region Blocked, But IP Address Whitelisted: " + S.RemoteAddr + LogISO);
        }

        // Block some ASNs (ISPs) listed in the filter file.
        if (FFilterList->TestFilters("asn", AsnInfo)) {
            if (!FWhiteList->CheckIpAddr(S.RemoteAddr, true) && !FChallengeTestingActive) {
                FHackBlackList->AddBlackListSoc(S.SocRemoteAddr, 9999, S.ISOA2, AsnInfo);
                Display("ASN/ISP Blocked, Connection Refused - " + AsnInfo + ": " + S.RemoteAddr);
                Allowed = false;
                return;
            }
            Display("ASN/ISP Blocked, But IP Address Whitelisted: " + AsnInfo + ": " + S.RemoteAddr);
        }
    }

    // Reverse DNS for the remote host. LookupIPOne may find the address in its cache; if
    // not it returns the IP and starts a lookup that calls IcsDomainNameCache1DNUpdateEvent.
    ClientCnx->CPeerHostName = IcsDomainNameCache1->LookupIPOne(SessIpInfo.RemoteAddr, 1, sfAny,
                                                                IcsDomainNameCache1DNUpdateEvent, Geo);
    SessIpInfo.RemoteRDNS = ClientCnx->CPeerHostName;

    Display(FormatDateTime("HH:NN:SS", Now()) + " New Remote Client: " + SessIpInfo.RemoteAddr + LogISO +
            " " + AsnInfo + ", Host: " + IcsFmtIpv6AddrPort(SessIpInfo.LocalAddr, SessIpInfo.LocalPort));

    // Check the blacklist before starting SSL negotiation.
    if (FFilterList->TestFilters("remaddr", SessIpInfo.RemoteAddr, true) ||
        FFilterList->TestFilters("remhost", SessIpInfo.RemoteRDNS) ||
        FHackBlackList->CheckBlackListSoc(SessIpInfo.SocRemoteAddr)) {
        if (!FWhiteList->CheckIpAddr(SessIpInfo.RemoteAddr, true)) {
            FHackBlackList->AddBlackListSoc(SessIpInfo.SocRemoteAddr, 9999, SessIpInfo.ISOA2, AsnInfo);
            Display("Hacker Aleady Blacklisted, Connection Refused: " + SessIpInfo.RemoteAddr);
            Allowed = false;   // the server does not accept the connection, so it needs no close
        }
    }
}

void __fastcall TWeblServerForm::SslHttpAppSrv1ClientConnect(TObject *Sender, TObject *Client, System::Word Error)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    ClientCnx->WSessionCookie = "OverbyteIcsWebAppServer" + SslHttpAppSrv1->Port;
    ClientCnx->OnBgException = HttpAppSrvClientBgException;
    ClientCnx->OnClientAlpnChallg = IcsSslX509Certs->WebSrvAlpn;
}
// Logs each request, with its headers, before authentication and the method events.
void __fastcall TWeblServerForm::SslHttpAppSrv1BeforeProcessRequest(TObject *Sender, TObject *Client)
{
    TMyHttpConnection *RemoteClient = static_cast<TMyHttpConnection *>(Client);
    const String CProtocol = RemoteClient->RequestProtocol + "://";
    RemoteClient->CStartTick = IcsGetTickCount64();
    RemoteClient->CLastWrite = RemoteClient->WriteCount;

    String S = "[" + FormatDateTime("HH:NN:SS", Now()) + " " + RemoteClient->CPeerAddr + "] " +
               RemoteClient->HostTag + " " + RemoteClient->Method + " " + RemoteClient->Path;
    if (!RemoteClient->Params.IsEmpty())
        S += "?" + RemoteClient->Params;
    Display(S);

    // An absolute URL: strip off the protocol and host.
    const String Path = RemoteClient->Path;
    if (Path.Pos(CProtocol) == 1) {
        for (int I = CProtocol.Length() + 1; I <= Path.Length(); I++) {
            if (Path[I] == L'/') {
                Display("Found and Trimmed Absolute URL: " + Path);
                RemoteClient->Path = Path.SubString(I, 999);
                break;
            }
        }
    }
}

void __fastcall TWeblServerForm::SslHttpAppSrv1DeleteDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    // Not authenticated yet: possibly still in an authentication exchange with no content
    // (NTLM uses challenge/response).
    if (Flags == hg401)
        return;
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    // Ignored when content is uploaded, for example a list of files to delete.
    if (ClientCnx->RequestContentLength > 0)
        return;

    // The path may name a file, or just a name, to delete; this demo only reports it.
    Flags = hgWillSendMySelf;
    THttpGetFlag Dummy;
    ClientCnx->AnswerString(Dummy, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer DELETE Demo</TITLE></HEAD>\r\n"
        "<BODY><H2>Your DELETE request has been noted:</H2>\r\n"
        "<P>Command: " + ClientCnx->Path + "</P>\r\n" +
        "<A HREF=\"/demo.html\">Back to demo menu</A><BR></BODY></HTML>");
}

void __fastcall TWeblServerForm::SslHttpAppSrv1DeleteSession(TObject *Sender, TIcsWebSession *Session)
{
    TAppSrvSessionData *MySessionData = dynamic_cast<TAppSrvSessionData *>(Session->SessionData);
    Display("Session for user \"" + (MySessionData ? MySessionData->UserCode : String()) + "\" timed out");
}

void __fastcall TWeblServerForm::SslHttpAppSrv1Display(TObject *Sender, const String Msg)
{
    Display(Msg);
}

void __fastcall TWeblServerForm::SslHttpAppSrv1GetDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    String Path = LowerCase(ClientCnx->Path);

    // Too many attempts to access the web site.
    bool AbandonFlag = false;
    if (FAttemptsBlackList->FailedBlackList(ClientCnx->CPeerAddr, 9999)) {
        AbandonFlag = true;
        Display("Maximum Attempts Exceeded, Blacklisted: " + ClientCnx->CPeerAddr);
    }
    // PHP hacking attempts and illegal URLs, using the URL filters. Checking the SSL server
    // name stops https://1.2.3.4/ (an IP instead of a host name).
    else if (FFilterList->TestFilters("path", Path) ||
             FFilterList->TestFilters("useragent", ClientCnx->RequestUserAgent) ||
             FFilterList->TestFilters("remhost", ClientCnx->CPeerHostName) ||
             TestDottedIp(ClientCnx->SslServerName) ||
             FHackBlackList->CheckBlackList(ClientCnx->CPeerAddr)) {
        AbandonFlag = true;
        Display("Hacker Blacklisted: " + ClientCnx->CPeerAddr + " - " + Path);
        const TIcsSessIpInfo IpInfo = ClientCnx->SessIpInfo;
        FHackBlackList->AddBlackList(ClientCnx->CPeerAddr, 9999, IpInfo.ISOA2, IpInfo.AsnName);
    }

    // The white list stops us locking ourselves out; otherwise redirect hackers back to themselves.
    if (AbandonFlag && !FWhiteList->CheckIpAddr(ClientCnx->CPeerAddr, true)) {
        ClientCnx->KeepAlive = false;
        Flags = hgWillSendMySelf;
        ClientCnx->AuthUserName = "";
        ClientCnx->AuthPassword = "";
        ClientCnx->PostedDataLen = 0;
        ClientCnx->Path = "";
        Path = ClientCnx->CPeerAddr;
        if (Path.Pos(":") > 1)
            Path = "[" + Path + "]";
        Path = "http://" + Path + "/";
        Display("Hacker Redirected to: " + Path);
        ClientCnx->AnswerRedirect(301, Path);
        return;
    }

    // Authentication is starting: no pages yet.
    if (Flags == hg401)
        return;

    // Some hosts only redirect.
    if (ClientCnx->WebRedirectStat != 0) {
        Flags = hgWillSendMySelf;
        ClientCnx->AnswerRedirect(ClientCnx->WebRedirectStat, ClientCnx->WebRedirectURL);
        return;
    }

    // Most special pages of the WEB-APP host are URL handlers using templates; these
    // pages are built in code.
    if (ClientCnx->HostTag == WebApp) {
        typedef void (TWeblServerForm::*TVirtualDocument)(TMyHttpConnection *, THttpGetFlag &);
        static const struct {
            const wchar_t *Path;
            TVirtualDocument Create;
        } VirtualDocs[] = {
            {L"/demo.html",            &TWeblServerForm::CreateVirtualDocument_Demo},
            {L"/demoBasicAuth.html",   &TWeblServerForm::CreateVirtualDocument_DemoBasicAuth},
            {L"/demoDigestAuth.html",  &TWeblServerForm::CreateVirtualDocument_DemoDigestAuth},
            {L"/demoDigest2Auth.html", &TWeblServerForm::CreateVirtualDocument_DemoDigest2Auth},
            {L"/demoDigestsAll.html",  &TWeblServerForm::CreateVirtualDocument_DemoDigestsAll},
            {L"/demoNtlmAuth.html",    &TWeblServerForm::CreateVirtualDocument_DemoNtlmAuth},
            {L"/time.html",            &TWeblServerForm::CreateVirtualDocument_Time},
            {L"/bruno.html",           &TWeblServerForm::CreateVirtualDocument_Bruno},
            {L"/myip.html",            &TWeblServerForm::CreateVirtualDocument_MyIP},
            {L"/HeaderBug.html",       &TWeblServerForm::CreateVirtualDocument_HeaderBug},
            {L"/redir.html",           &TWeblServerForm::CreateVirtualDocument_Redir},
            {L"/template.html",        &TWeblServerForm::CreateVirtualDocument_Template},
        };
        bool Found = false;
        for (const auto &Doc : VirtualDocs) {
            if (CompareText(ClientCnx->Path, Doc.Path) == 0) {
                (this->*Doc.Create)(ClientCnx, Flags);
                Found = true;
                break;
            }
        }
        if (!Found && CompareText(ClientCnx->Path, "/favicon.ico") == 0) {
            const TBytes ImageTB = IcsResourceGetTB("ICSICON16", RT_RCDATA);
            if (ImageTB.Length > 0)
                ClientCnx->AnswerBodyTB(Flags, "200 OK", "image/icon", "", ImageTB);
            else
                ClientCnx->Answer404();
        }
    }
    FlushDiagLog();
}

void __fastcall TWeblServerForm::SslHttpAppSrv1HeadDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    String S = ClientCnx->CPeerAddr + " - " + ClientCnx->HostTag + " " + ClientCnx->Path;
    if (!ClientCnx->Params.IsEmpty())
        S += "?" + ClientCnx->Params;
    Display(S);
}

// Customises the body of the server's standard error responses (404 only here).
void __fastcall TWeblServerForm::SslHttpAppSrv1HttpCustomError(TObject *Sender, TObject *Client, int ErrNr,
                                                               const String Info, String &Body)
{
    if (DisplayHeaderCheckBox->Checked)
        Display("CustomError Event: ErrNr" + IntToStr(ErrNr) + ", Param: " + Info);
    if (ErrNr != 404)
        return;
    Body = "<HTML><HEAD><TITLE>Custom 404 Not Found</TITLE></HEAD>"
           "<BODY><H1>Custom 404 Not Found</H1>The requested URL " + TextToHtmlText(Info) +
           " was not found on this server.<P></BODY></HTML>\r\n";
}

void __fastcall TWeblServerForm::SslHttpAppSrv1HttpMimeContentType(TObject *Sender, TObject *Client,
                                                                   const String FileName, String &ContentType)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("[" + FormatDateTime("HH:NN:SS", Now()) + " " + ClientCnx->CPeerAddr + "] : Document: " +
            FileName + ", Content-Type: " + ContentType);
}

void __fastcall TWeblServerForm::SslHttpAppSrv1HttpRequestDone(TObject *Sender, TObject *Client)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("[" + FormatDateTime("HH:NN:SS", Now()) + " " + ClientCnx->CPeerAddr +
            "] Request Completed, Status " + IntToStr(ClientCnx->AnswerStatus) + "\r\n");
}

// The response headers are logged by the server when Display HTTP Protocol is ticked.// Left empty so the server sends its general OPTIONS response.void __fastcall TWeblServerForm::SslHttpAppSrv1PatchDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    if (Flags == hg401)
        return;   // see DeleteDocument
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    const __int64 MaxUpload = SslHttpAppSrv1->MaxUploadMB * IcsMBYTE;
    if (ClientCnx->RequestContentLength > MaxUpload) {
        Display("Upload size exceeded limit (" + IntToKbyte(MaxUpload) + ")");
        Flags = hg403;
    }
}

void __fastcall TWeblServerForm::SslHttpAppSrv1PostDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    if (Flags == hg401)
        return;   // see DeleteDocument
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("PostDocument Event reached, Content Len=" + IntToStr(ClientCnx->RequestContentLength));

    // Most POSTs on the WEB-APP host go to URL handlers; this page is built in code.
    if (ClientCnx->HostTag == WebApp && CompareText(ClientCnx->Path, "/DemoBasicAuth.html") == 0) {
        CreateVirtualDocument_DemoBasicAuth(ClientCnx, Flags);
        return;
    }

    const __int64 MaxUpload = SslHttpAppSrv1->MaxUploadMB * IcsMBYTE;
    if (ClientCnx->RequestContentLength > MaxUpload) {
        Display("Upload size exceeded limit (" + IntToKbyte(MaxUpload) + ")");
        Flags = hg403;
    }
}

// Where posted data for virtual pages would be accepted; this demo only POSTs to templates.
void __fastcall TWeblServerForm::SslHttpAppSrv1PostedData(TObject *Sender, TObject *Client, System::Word Error)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("PostedData Event reached");
    ClientCnx->PostedDataReceived();   // got all the data expected, that is none
}

void __fastcall TWeblServerForm::SslHttpAppSrv1PutDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    if (Flags == hg401)
        return;   // see DeleteDocument
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("PutDocument Event reached, Content Len=" + IntToStr(ClientCnx->RequestContentLength));
    const __int64 MaxUpload = SslHttpAppSrv1->MaxUploadMB * IcsMBYTE;
    if (ClientCnx->RequestContentLength > MaxUpload) {
        Display("Upload size exceeded limit (" + IntToKbyte(MaxUpload) + ")");
        Flags = hg403;
    }
    // The URL may name a file, or just a name; this demo does not store PUT data.
}

void __fastcall TWeblServerForm::SslHttpAppSrv1QueryDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    if (Flags == hg401)
        return;
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display("QueryDocument Event reached, Content Len=" + IntToStr(ClientCnx->RequestContentLength));
}

void __fastcall TWeblServerForm::SslHttpAppSrv1ServerStarted(TObject *Sender)
{
    Display("Server has started");
}

void __fastcall TWeblServerForm::SslHttpAppSrv1ServerStopped(TObject *Sender)
{
    SslHttpAppSrv1->WSessions->SaveToFile(FSessionFile);
    CleanupTimeStampedDir(FPagesData.DataDir());
    Display("Server is now stopped");
}

void __fastcall TWeblServerForm::SslHttpAppSrv1SslHandshakeDone(TObject *Sender, System::Word ErrCode,
                                                                TX509Base *PeerCert, bool &Disconnect)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Sender);
    // Log the client hello if there was no SNI, then the ciphers.
    if (DisplaySslInfo->Checked) {
        if (ClientCnx->SslServerName.IsEmpty())
            Display(ClientCnx->CPeerAddr + " - Client Hello: " + Trim(WSocketGetCliHelloStr(ClientCnx->CliHelloData)));
        String Ciphers = Trim(ClientCnx->SslGetSupportedCiphers(true, true));
        if (!Ciphers.IsEmpty()) {
            Ciphers = StringReplace(Ciphers, "\r\n", ", ", TReplaceFlags() << rfReplaceAll);
            Display("SSL Ciphers from Client: " + Ciphers);
        }
    }
    Display(ClientCnx->CPeerAddr + " - " + ClientCnx->HostTag + " " + ClientCnx->SslHandshakeRespMsg);

    // A client certificate needs CliCertMethod sslCliCertRequire or sslCliCertOptional in
    // IcsHosts. A real server would check its common name or issuer and maybe set Disconnect.
    if (PeerCert && PeerCert->IsCertLoaded)
        Display(ClientCnx->CPeerAddr + " - " + ClientCnx->HostTag +
                " Client certificate received, should we trust client?\r\n" + PeerCert->CertMainInfo());
}

void __fastcall TWeblServerForm::SslHttpAppSrv1SslServerName(TObject *Sender, TSslContext *&Ctx, TTlsExtError &ErrCode)
{
    if (DisplaySslInfo->Checked) {
        TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Sender);
        Display("Client Hello from " + ClientCnx->CPeerAddr + ", " +
                Trim(WSocketGetCliHelloStr(ClientCnx->CliHelloData)));
    }
}

// Selects HTTP/1.1 when the client offers it (HTTP/2 is not supported yet).
void __fastcall TWeblServerForm::SslHttpAppSrv1SslAlpnSelect(TObject *Sender, TStrings *ProtoList,
                                                             String &SelProto, TTlsExtError &ErrCode)
{
    for (int I = 0; I < ProtoList->Count; I++) {
        if (ProtoList->Strings[I] == ALPN_ID_HTTP11) {
            SelProto = ALPN_ID_HTTP11;
            ErrCode = teeOk;
            return;
        }
    }
}
void __fastcall TWeblServerForm::SslHttpAppSrv1VirtualException(TObject *Sender, Exception *E,
                                                                THttpMethod Method, const String Path)
{
    Display("Exception creating virtual page: " + Path + " - " + E->Message);
}

// Called for several WebDAV commands; only PROPFIND is handled, by the server.
void __fastcall TWeblServerForm::SslHttpAppSrv1WebDAVDocument(TObject *Sender, TObject *Client,
                                                              THttpMethod HttpMethod, THttpGetFlag &Flags)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    Display("WebDAV Request: " + Remote->Method + " - " + Remote->Path);
    if (HttpMethod != httpMethodPropFind)
        Flags = hg400;
}

void __fastcall TWeblServerForm::SslHttpAppSrv1WellKnownDir(TObject *Sender, TObject *Client,
                                                            const String Path, String &BodyStr)
{
    TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(Client);
    Display(ClientCnx->CPeerAddr + " - " + ClientCnx->HostTag + " Well-Known File Requested: " + Path);
    if (Path.Pos("/acme-challenge/") > 1 || Path.Pos("/ics-validation/") > 1) {
        // Check the challenge token from Let's Encrypt and return the key authorization.
        IcsSslX509Certs->WebSrvHttp(this, ClientCnx->RequestHost, Path, BodyStr);
        if (!BodyStr.IsEmpty())
            Display(ClientCnx->CPeerAddr + " - " + ClientCnx->HostTag + " acme-challenge response: " + BodyStr);
    }
}

// The DNS cache finished a remote host name lookup: update the client with that address,
// for the web log.
void __fastcall TWeblServerForm::IcsDomainNameCache1DNUpdateEvent(TObject *Sender, int ItemNr)
{
    if (ItemNr < 0)
        return;
    const String Request = IcsDomainNameCache1->GetDNItem(ItemNr).Request;
    for (int I = 0; I < SslHttpAppSrv1->ClientCount; I++) {
        TMyHttpConnection *ClientCnx = static_cast<TMyHttpConnection *>(SslHttpAppSrv1->Client[I]);
        if (ClientCnx->CPeerAddr == Request) {
            ClientCnx->SetPeerHostName(IcsDomainNameCache1->BuildRespList(ItemNr));
            Display(ClientCnx->CPeerAddr + " - Client DNS Lookup: " + ClientCnx->CPeerHostName);
            break;
        }
    }
}

void __fastcall TWeblServerForm::IcsMailQueueLogEvent(TMailLogLevel LogLevel, const String Info)
{
    if (LogLevel <= MLogLevelInfo)
        Display(Info);
}

void __fastcall TWeblServerForm::IcsMailQueueOATokenEvent(int ServNr, String &Token, String &TokAccount,
                                                          TDateTime &TokExpireDT)
{
    Display("Starting to get OAuth2 Bearer Token");
    // Allows interaction: waits for a browser window. A real service must not, since it
    // has no desktop for the browser.
    if (!IcsRestEmail->GetNewToken(true))
        Display("Failed to get OAuth2 Bearer Token");
    else {
        // The access token lives a few hours and can be refreshed, so it is not saved.
        Token = IcsRestEmail->AccToken;
        TokExpireDT = IcsRestEmail->AccExpireDT;
        TokAccount = IcsRestEmail->NewAccEmail;
    }
}

// The refresh token only comes after a login window, so it should be saved.
void __fastcall TWeblServerForm::IcsRestEmailEmailNewToken(TObject *Sender)
{
    if (!IcsRestEmail->RefrToken.IsEmpty() && IcsRestEmail->RefrToken != FOldRefrToken) {
        const String Info = "New OAuth2 Refresh Token for Account: " + IcsRestEmail->NewAccEmail +
                            ", Should be Saved in INI File: " + FIniFileName + "\r\n" +
                            "RefrToken=" + IcsRestEmail->RefrToken;
        Display(Info);
        if (!FAdminEmailTo.IsEmpty())
            SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server New OAuth2 Refresh Token",
                           "Web server New OAuth2 Refresh Token.\r\n\r\n" + Info);
    }
}

void __fastcall TWeblServerForm::IcsRestEmailEmailProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    Display(Msg);
}
void __fastcall TWeblServerForm::IcsSslX509CertsCertProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    Display(Msg);
}

// DNS challenges would update a DNS server with the TXT record; not done for a web server.
void __fastcall TWeblServerForm::IcsSslX509CertsChallengeDNS(TObject *Sender, const TChallengeItem &ChallengeItem,
                                                             bool &ChlgOK)
{
    ChlgOK = false;
}

// While challenges are being tested, connections may come from several continents.
void __fastcall TWeblServerForm::IcsSslX509CertsChallgRefresh(TObject *Sender)
{
    TSslX509Certs *Certs = static_cast<TSslX509Certs *>(Sender);
    if (Certs->IssueState > IssStateNone)
        Display("X509 Certificate Order State Change: " + Certs->CertCommonName + " - New State: " +
                IssueStateLits[Certs->IssueState]);
    FChallengeTestingActive = Certs->IssueState >= IssStateChallgTest && Certs->IssueState <= IssStateChallgOK;
}

void __fastcall TWeblServerForm::IcsSslX509CertsGeoEvent(TObject *Sender, const String IpStr, String &Geo)
{
    Geo = GeoForIp(IpStr);   // Delphi repeats the lookup of IcsDomainNameCache1GeoEvent here
}

void __fastcall TWeblServerForm::IcsSslX509CertsNewCert(TObject *Sender)
{
    FCertCheckTrigger = Overbyteicsticks64::Trigger64Immediate;   // load the new certificates
    Display("Trigger Recheck Certificates");
    Display("Web server ordered new SSL cerrtificate.\r\n" + IcsSslX509Certs->GetOrderResult());
    if (!FAdminEmailTo.IsEmpty())
        SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server SSL Certificate Order Completed",
                       "Web server ordered new SSL cerrtificate.\r\n\r\n" + IcsSslX509Certs->GetOrderResult() + "\r\n");
}

void __fastcall TWeblServerForm::IcsSslX509CertsOAuthAuthUrl(TObject *Sender, const String URL)
{
    Display("Web server demo needs OAuth authenfication for new certificate, Browse to this URL: " + URL +
            ", From PC: " + IcsGetCompName());
    if (!FAdminEmailTo.IsEmpty())
        SendAdminEmail(FAdminEmailTo, "ICS Multi Web Server needs OAuth authenfication for new certificate",
                       "Browse to this URL: " + URL + "\r\n\r\nFrom Browser on PC: " + FSrvCompName + "\r\n");
}

void __fastcall TWeblServerForm::HttpAppSrvClientBgException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Exception processing page - " + E->ClassName() + ": " + E->Message);
    CanClose = true;   // shut the client down
}

/* ---- Pages built in code ---- */

void TWeblServerForm::CreateVirtualDocument_Demo(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Menu</TITLE></HEAD><BODY>"
        "<H2>ICS WebAppServer Demo Menu</H2>"
        "<H3>" + Caption + "</H3>" +
        "<p>" + IcsReportOpenSSLVer() + "</p>" +
        "<p>" IcsVerLitRelDate "</p>"
        "<p></p><p>"
        "<A HREF=\"/IpAddrLookup.html\">IP Address Database Lookup</A><BR>"
        "<A HREF=\"/time.html\">Server time</A><BR>"
        "<A HREF=\"/template.html\">Template demo</A><BR>"
        "<A HREF=\"/appindex.html\">Application Web Server session demo</A><BR>"
        "<A HREF=\"/form.html\">Data entry</A><BR>"
        "<A HREF=\"/uploadfile.html\">Upload a file using POST</A><BR>"
        "<A HREF=\"/postinfo.html?GetParam=Hello\">Parameters Display</A><BR>"
        "<A HREF=\"/mailer.html\">Send Email Form</A><BR>"
        "<A HREF=\"/websocketclient.html\">WebSocket Client Echo Test</A><BR>"
        "<A HREF=\"/redir.html\">Redirection</A><BR>"
        "<A HREF=\"/myip.html\">Show client IP and Host Name</A><BR>"
        "<A HREF=\"/DemoBasicAuth.html\">Password protected page</A> (Basic method)<BR>"
        "<A HREF=\"/DemoDigestAuth.html\">Password protected page</A> (Digest MD5 method)<BR>"
        "<A HREF=\"/DemoDigest2Auth.html\">Password protected page</A> (Digest SHA-256 method)<BR>"
        "<A HREF=\"/DemoDigestsAll.html\">Password protected page</A> (Digest MD5 or SHA-256 methods))<BR>"
        "<A HREF=\"/DemoAuthAll.html\">Password protected page</A> (method is selected by the browser)<BR>"
        "<A HREF=\"/DemoNtlmAuth.html\">Password protected page</A> (NTLM method, usercode=(Windows) user, password=(Windows) password)<BR>"
        "<A HREF=\"/\">Default document</A><BR>"
        "<A HREF=\"https://www.overbyte.be\">ICS Home page</A><BR>"
        "<A HREF=\"https://wiki.overbyte.eu/wiki/index.php/Main_Page\">ICS Wiki and Downloads</A><BR>"
        "</p>"
        "<P><form action=\"/postinfo.html\" method=\"POST\"><input type=\"submit\" "
        "value=\"Parameters Display (POST)\"><input type=\"hidden\" name=\"PostParam\" value=\"Hello\"></form></P>"
        "<P><input type=\"button\" value=\"Password protected page (GET button)\" onclick=\"window.location.href='/DemoAuthAll.html';\"></P>"
        "<P><form action=\"/DemoAuthAll.html\" method=\"GET\"><input type=\"submit\" "
        "value=\"Password protected page (GET template)\"></form></P>"
        "<P><form action=\"/DemoAuthAll.html\" method=\"POST\"><input type=\"submit\" "
        "value=\"Password protected page (POST template)\"><input type=\"hidden\" name=\"PostParam\" value=\"Hello\"></form></P>"
        "<P><form action=\"/DemoBasicAuth.html\" method=\"POST\"><input type=\"submit\" "
        "value=\"Password protected page (POST page)\"><input type=\"hidden\" name=\"PostParam\" value=\"Hello\"></form></P>"
        "<P><form action=\"/DemoAuthAll.html\" method=\"PUT\"><input type=\"submit\" "
        "value=\"Password protected page (PUT template)\"><input type=\"hidden\" name=\"PutParam\" value=\"Hello\"></form></P>"
        "<p></p>"
        "<p>Login for protected pages (except NTLM) is \"test\" with password of \"password\"</p>"
        "<p></p>"
        "<p>" + FStatSrvSslCertWeb + "</p>" +
        "</BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_DemoBasicAuth(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    String sPageUrl = ClientCnx->Method + " " + ClientCnx->RequestProtocol + "://" + ClientCnx->RequestHost +
                      ClientCnx->Path;
    if (ClientCnx->PostedDataLen > 0)
        sPageUrl += ", PostParams: " + ClientCnx->PostedDataStr;
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Password Protected Page - Basic</TITLE></HEAD><BODY>"
        "<H2>ICS WebServer Password Protected Page</H2>"
        "This page is protected by a username and password.<BR>"
        "The authentication is done using Basic mode.<BR>"
        "Request Type: " + sPageUrl + "<BR>" +
        "<A HREF=\"/Demo.html\">Demo menu</A><BR></BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_DemoDigestAuth(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Password Protected Page - Digest MD5</TITLE></HEAD><BODY>"
        "<H2>ICS WebServer Password Protected Page</H2>"
        "This page is protected by a username and password.<BR>"
        "The authentication is done using Digest mode with MD5 Algorithm.<BR>"
        "<A HREF=\"/Demo.html\">Demo menu</A><BR></BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_DemoDigest2Auth(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Password Protected Page - Digest SHA-256</TITLE></HEAD><BODY>"
        "<H2>ICS WebServer Password Protected Page</H2>"
        "This page is protected by a username and password.<BR>"
        "The authentication is done using Digest mode with SHA-256 Algorithm.<BR>"
        "<A HREF=\"/Demo.html\">Demo menu</A><BR></BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_DemoDigestsAll(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Password Protected Page - Multiple Digests</TITLE></HEAD><BODY>"
        "<H2>ICS WebServer Password Protected Page</H2>"
        "<p>This page is protected by a username and password.</p>"
        "<p>Your browser selected the <B>" + String(HttpAuthTypeNames[ClientCnx->AuthType]) +
        "</B> authentication mode.</p>" +
        "<p><A HREF=\"/Demo.html\">Demo menu</A></p></BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_DemoNtlmAuth(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Password Protected Page - NTLM</TITLE></HEAD><BODY>"
        "<H2>ICS WebServer Password Protected Page</H2>"
        "This page is protected by a username and password.<BR>"
        "The authentication is done using NTLM mode.<BR>"
        "<A HREF=\"/Demo.html\">Demo menu</A><BR></BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_Template(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerPage(Flags, "", NO_CACHE, "TemplateDemo.html", NoUserData,
                          ARRAYOFCONST((L"TIME", DateTimeToStr(Now()),
                                        L"PROGVER", SrvCopyRight,
                                        L"SOURCE", TextToHtmlText(ClientCnx->TemplateDir + "TemplateDemo.html"))));
}

// Redirects to the URL given as the query, for example /redir.html?https://www.overbyte.be
void TWeblServerForm::CreateVirtualDocument_Redir(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    const String Location = ClientCnx->Params;
    ClientCnx->AnswerString(Flags, "302 Moved", "", "Location: " + Location + "\r\n",
        "<HTML><HEAD><TITLE>ICS WebServer Demo - Redir</TITLE></HEAD>\r\n"
        "<BODY>You should be redirected automatically !<BR>\r\n"
        "<A HREF=\"" + Location + "\">Click Here</A><BR>\r\n</BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_Time(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "Pragma: no-cache\r\nExpires: -1\r\n",   // no client caching
        "<HTML><HEAD><TITLE>ICS WebServer Demo</TITLE></HEAD>\r\n"
        "<BODY><H2>Time at server side:</H2>\r\n"
        "<P>" + DateTimeToStr(Now()) + "</P>\r\n" +
        "<A HREF=\"/demo.html\">Demo menu</A>\r\n</BODY></HTML>");
}

// A long page with cookies: /bruno.html?N gives N lines (default 5000, at most 20000).
void TWeblServerForm::CreateVirtualDocument_Bruno(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    const int Count = std::min(StrToIntDef(ClientCnx->Params, 5000), 20000);
    String Body = "<HTML><HEAD><TITLE>Test Page from Bruno :-)</TITLE></HEAD>\r\n<BODY>";
    for (int I = 1; I <= Count; I++)
        Body += "<br>This is line number " + IntToStr(I);
    Body += "</BODY></HTML>";
    const String Header = "Pragma: no-cache\r\n"
                          "Expires: -1\r\n"
                          "Set-Cookie: Usuario=a; path=/\r\n"
                          "Set-Cookie: Senha=a; path=/\r\n";
    ClientCnx->AnswerString(Flags, "", "", Header, Body);
}

// Used to check client behaviour.
void TWeblServerForm::CreateVirtualDocument_HeaderBug(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "Pragma: no-cache\r\nExpires: -1\r\n",
        "<HTML><HEAD><TITLE>ICS WebServer Demo</TITLE></HEAD>\r\n"
        "<BODY>Congratulations !\r\n</BODY></HTML>");
}

void TWeblServerForm::CreateVirtualDocument_MyIP(TMyHttpConnection *ClientCnx, THttpGetFlag &Flags)
{
    ClientCnx->AnswerString(Flags, "", "", "Pragma: no-cache\r\nExpires: -1\r\n",
        "<HTML><HEAD><TITLE>ICS WebServer Demo</TITLE></HEAD>\r\n"
        "<BODY><P>Your IP is: " + ClientCnx->CPeerAddr + "</P>\r\n" +
        "<P>Your Host Name is: " + ClientCnx->CPeerHostName + "</P>\r\n" +
        "</BODY></HTML>");
}

/* ---- WebSocket servers: /WebSocket/Echo, /WebSocket/EchoPing and /WebSocket/Chat ---- */

void __fastcall TWeblServerForm::ClientWSLogEvent(TObject *Sender, const String Msg)
{
    Display(Msg);
}

void __fastcall TWeblServerForm::ClientWSHandshakeEvent(THttpWSSrvConn *Client, bool &OK, String &WelcomeMsg)
{
    OK = false;
    if (CompareText(Client->WSReqPage, "/WebSocket/Echo") == 0) {
        OK = true;
        WelcomeMsg = "Welcome to ICS WebSocket echo server, all messages will be echoed.";
    }
    else if (CompareText(Client->WSReqPage, "/WebSocket/EchoPing") == 0) {
        OK = true;
        WelcomeMsg = "Welcome to ICS WebSocket echo/ping server, all messages will be echoed.";
        Client->WSPingEnabled = true;
    }
    else if (CompareText(Client->WSReqPage, "/WebSocket/Chat") == 0) {
        OK = true;
        if (Client->WSReqParams.IsEmpty())   // ideally they passed a chat name
            Client->WSReqParams = Client->CPeerAddr;
    }
    else if (CompareText(Client->WSReqPage, "/WebSocket/Serverinfo") == 0) {
        OK = true;
        Client->WSPingSecs = 10;   // used for server push
        Client->WSPingEnabled = true;
    }
    Display("WebSocket " + Client->CPeerAddr + ": Request for " + Client->WSReqPage +
            (OK ? " Accepted" : " Rejected"));
}

// Called once, when the WebSocket is ready for traffic.
void __fastcall TWeblServerForm::ClientWSReadyEvent(THttpWSSrvConn *Client)
{
    Display("Websocket " + Client->CPeerAddr + ": Ready for " + Client->WSReqPage);
    if (CompareText(Client->WSReqPage, "/WebSocket/Chat") == 0) {
        String Users;
        WSGetChatUsers(Client->WSReqPage, Users);
        Client->WSSendText(nullptr, "Welcome to ICS WebSocket chat server, Users: " + Users);
        WSSendChatResp(Client, "!! Has Joined Chat", true);
    }
}

// With server pings enabled, called just before each ping so a page could be updated;
// Serverinfo sends nothing in this demo.
void __fastcall TWeblServerForm::ClientWSPingTimerEvent(THttpWSSrvConn *Client)
{
}

void TWeblServerForm::WSSendChatResp(THttpWSSrvConn *SendCli, const String &APacket, bool ToMe)
{
    // Edge and Firefox ignore text in angle brackets, probably taking it for HTML.
    const String Msg = "[" + SendCli->WSReqParams + "] " + APacket;
    int Sent = 0;
    try {
        for (int I = SslHttpAppSrv1->ClientCount - 1; I >= 0; I--) {
            THttpWSSrvConn *RecvCli = static_cast<THttpWSSrvConn *>(SslHttpAppSrv1->Client[I]);
            if (SendCli->WSReqPage == RecvCli->WSReqPage &&
                (ToMe || SendCli->CPeerAddr != RecvCli->CPeerAddr)) {
                RecvCli->WSSendText(nullptr, Msg);
                Sent++;
            }
        }
        Display("Send Chat message to " + IntToStr(Sent) + " users: " + Msg);
    }
    catch (Exception &) {
        Display("Exception sending chat message");
    }
}

int TWeblServerForm::WSGetChatUsers(const String &ChatPage, String &UList)
{
    UList = "";
    int Result = 0;
    try {
        for (int I = SslHttpAppSrv1->ClientCount - 1; I >= 0; I--) {
            THttpWSSrvConn *RecvCli = static_cast<THttpWSSrvConn *>(SslHttpAppSrv1->Client[I]);
            if (RecvCli->WSReqPage == ChatPage) {
                if (!UList.IsEmpty())
                    UList += ", ";
                UList += RecvCli->WSReqParams;
                Result++;
            }
        }
        Display("Chat  users line, total: " + IntToStr(Result) + " - " + UList);
    }
    catch (Exception &) {
        Display("Exception getting chat users");
    }
    return Result;
}

void __fastcall TWeblServerForm::ClientWSFrameRcvdEvent(THttpWSSrvConn *Client, const String APacket,
                                                        TWebSocketReceivedFrame *&AFrame)
{
    if (!AFrame || AFrame->Kind == wsfkPong)
        return;
    Display("Websocket " + Client->CPeerAddr + ": " + Client->WSReqPage + " " + GetWSFrameKind(AFrame->Kind) +
            " received, length: " + IntToStr(AFrame->DataBytes));
    const bool Echo = CompareText(Client->WSReqPage, "/WebSocket/Echo") == 0 ||
                      CompareText(Client->WSReqPage, "/WebSocket/EchoPing") == 0;
    const bool Chat = CompareText(Client->WSReqPage, "/WebSocket/Chat") == 0;
    switch (AFrame->Kind) {
        case wsfkBin:
            if (Echo) {
                AFrame->Data->Position = 0;
                Client->WSSendBinaryStream(nullptr, AFrame->Data);
            }
            break;
        case wsfkText:
            if (Echo)
                Client->WSSendText(nullptr, APacket);
            if (Chat)
                WSSendChatResp(Client, APacket, true);
            break;
        case wsfkClose:
            if (Chat)
                WSSendChatResp(Client, "!! Left Chat", true);
            break;
        default:
            break;
    }
}

void __fastcall TWeblServerForm::ClientWSFrameSentEvent(THttpWSSrvConn *Client, TWebSocketOutgoingFrame *&AFrame)
{
    if (!AFrame || AFrame->Kind == wsfkPing)
        return;
    Display("Websocket " + Client->CPeerAddr + ": " + Client->WSReqPage + " " + GetWSFrameKind(AFrame->Kind) + " sent");
}

void __fastcall TWeblServerForm::ClientWSDisconnectedEvent(TObject *Sender)
{
    THttpWSSrvConn *Client = static_cast<THttpWSSrvConn *>(Sender);
    Display("Websocket " + Client->CPeerAddr + ": " + Client->WSReqPage + " Closing, Total Frames " +
            IntToStr(Client->WSFrameCounter));
    SslHttpAppSrv1AfterAnswer(this, Client);   // the main web log
}
