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

  Shows how to use TSslHttpServer (HTTPS) and THttpServer (HTTP) to implement a web server,
  both serving the same document folder. /demo.html, /time.html and /redir.html are built in
  code, and a POST to /cgi-bin/cgifrm1.exe is saved to CgiFrm1.txt. Both servers start when
  the form opens. WARNING: for demonstration only; it serves every file under DocDir without
  any access check. OverbyteIcsSslMultiWebServ is a better demo.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The default document folder is found three levels up from the Win64x\<config> output
    folder (Delphi: four, from bin\<platform>\<config>).
  - The log file is a std::unique_ptr<TStreamWriter> (Delphi: a TextFile), written in the ANSI
    code page as before; the posted data is held in an AnsiString and a String on the connection
    (Delphi: GetMem buffers).
  - The empty Delphi handlers (FormDestroy, ToolsPanelClick, ButtonOSSLVersionClick) are not
    translated and the .dfm bindings of the first two are removed. RenegotiationIntervalEditChange
    and SslHttpServer1SslSetSessionIDContext are not translated: no control or event is bound to
    them, so they never run in Delphi.
  - The form destructor frees the components first, because the servers can still fire events
    that use the form while they are destroyed.
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include <System.TypInfo.hpp>
#include <cstring>

#include "OverbyteIcsSslWebServ1.h"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsSocketUtils.hpp"
#include "OverbyteIcsHtmlUtils.hpp"

#pragma package(smart_init)
#pragma resource "*.dfm"

TSslWebServForm *SslWebServForm;

namespace {

// Every ICS unit declares its own CopyRight, so the demo's is named AppCopyRight.
const String AppCopyRight = String(L"WebServ") + IcsVerLitCopyright;
const wchar_t *const Ssl_Session_ID_Context = L"WebServ_Test";

// IniFile layout for persistent data.
const wchar_t *const SectionWindow      = L"WindowMain";
const wchar_t *const KeyTop             = L"Top";
const wchar_t *const KeyLeft            = L"Left";
const wchar_t *const KeyWidth           = L"Width";
const wchar_t *const KeyHeight          = L"Height";
const wchar_t *const SectionData        = L"Data";
const wchar_t *const KeyDocDir          = L"DocDir";
const wchar_t *const KeyDefaultDoc      = L"DefaultDoc";
const wchar_t *const KeyPortHttps       = L"PortHttps";
const wchar_t *const KeyPortHttp        = L"PortHttp";
const wchar_t *const KeyDisplayHeader   = L"DisplayHeader";
const wchar_t *const KeyDisplaySslInfo  = L"DisplaySslInfo";
const wchar_t *const KeyLogToFile       = L"LogToFile";
const wchar_t *const KeyCertFile        = L"CertFile";
const wchar_t *const KeyPassPhrase      = L"PassPhrase";
const wchar_t *const KeyPrivKeyFile     = L"PrivKeyFile";
const wchar_t *const KeyVerifyPeer      = L"VerifyPeer";
const wchar_t *const KeyCAFile          = L"CAFile";
const wchar_t *const KeyCAPath          = L"CAPath";
const wchar_t *const KeyAcceptableHosts = L"AcceptableHosts";
const wchar_t *const KeyRenegInterval   = L"RenegotiationInterval";
const wchar_t *const KeySslCipherEdit   = L"SslCipherEdit";
const wchar_t *const KeySslCipherList   = L"SslCipherList";
const wchar_t *const KeyListenAddr      = L"ListenAddr";
const wchar_t *const KeySslMinVersion   = L"SslMinVersion";
const wchar_t *const KeySslMaxVersion   = L"SslMaxVersion";
const wchar_t *const KeyOldSsl          = L"OldSsl";
const wchar_t *const KeyWebDAV          = L"WebDAV";
const wchar_t *const KeySslSecLevel     = L"SslSecLevel";
const wchar_t *const KeyWellKnownPath   = L"WellKnownPath";
const wchar_t *const KeySslCipher13     = L"SslCipher13";
const wchar_t *const KeySslCryptoGroups = L"SslCryptoGroups";

// Sample Let's Encrypt challenge token and its key authorization (sample only).
const wchar_t *const AcmeToken = L"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0";
const wchar_t *const AcmeKeyAuth =
    L"LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0.9jg46WB3rR_AHD-EBXdN7cBkH1WOu0tA3M9fm21mqTI";

// The largest POST body the cgifrm1 form handler buffers.
const __int64 MaxPostedDataSize = 1024 * 1024;

String NowHms()
{
    return FormatDateTime("HH:NN:SS", Now());
}

// The binary session id packed into a String (two bytes per character), as the session cache key.
String SessionKey(const void *SessId, int Idlen)
{
    String Key;
    const int Chars = (Idlen + 1) / 2;
    if (Chars > 0) {
        Key.SetLength(Chars);
        WideChar *Dst = &Key[1];
        Dst[Chars - 1] = 0;   // an odd length leaves the last high byte defined (Delphi leaves it unset)
        std::memcpy(Dst, SessId, Idlen);
    }
    return Key + Ssl_Session_ID_Context;
}

// Adds a line to the memo, keeping the last 5000 lines, and scrolls to it. A separate function
// because bcc64x rejects __finally in a function that also has a C++ try.
void AddMemoLine(TMemo *Memo, const String &Msg)
{
    Memo->Lines->BeginUpdate();
    __try {
        while (Memo->Lines->Count > 5000)
            Memo->Lines->Delete(0);
        Memo->Lines->Add(Msg);
    }
    __finally {
        Memo->Lines->EndUpdate();
        SendMessage(Memo->Handle, EM_SCROLLCARET, 0, 0);
    }
}

// The /.well-known/ handler shared by both servers: answers the sample acme challenge.
void AnswerWellKnown(TSslWebServForm *Form, const String &ServerName, const String &Path, String &BodyStr)
{
    Form->Display(ServerName + " Server: Well-Known File Requested: " + Path);
    // acme challenges use HTTP only, since they precede the issuing of an SSL certificate,
    // but other services might use SSL.
    if (Path.Pos("/acme-challenge/") > 1 && Path.Pos(String("/") + AcmeToken) > 1) {
        BodyStr = AcmeKeyAuth;
        Form->Display(ServerName + " Server: acme-challenge response: " + BodyStr);
    }
}

} // namespace

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

__fastcall TSslWebServForm::TSslWebServForm(TComponent *Owner)
    : TForm(Owner)
{
}

__fastcall TSslWebServForm::~TSslWebServForm()
{
    // Free the servers and their clients while the log file and other members still exist.
    DestroyComponents();
}

void __fastcall TSslWebServForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    ReportMemoryLeaksOnShutdown = DebugHook != 0;
    FIniFileName = GetIcsIniFileName();
    FLogFileName = ChangeFileExt(FIniFileName, ".log");
    HttpServer2->OnPostedData = SslHttpServer1PostedData;   // the Delphi .dfm leaves it unbound, so an HTTP form POST is never answered
}

void __fastcall TSslWebServForm::FormShow(TObject *Sender)
{
    if (FInitialized)
        return;
    FInitialized = true;

    // Security level items, named from TSslSecLevel.
    for (int SL = sslSecLevelAny; SL <= sslSecLevel256bits; SL++)
        SslSecLevel->Items->Add(GetEnumName(__delphirtti(TSslSecLevel), SL));

    // Restore persistent data from the INI file.
    String OldIp;
    {
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        Width = IniFile->ReadInteger(SectionWindow, KeyWidth, Width);
        Height = IniFile->ReadInteger(SectionWindow, KeyHeight, Height);
        Top = IniFile->ReadInteger(SectionWindow, KeyTop, (Screen->Height - Height) / 2);
        Left = IniFile->ReadInteger(SectionWindow, KeyLeft, (Screen->Width - Width) / 2);
        DocDirEdit->Text = IniFile->ReadString(SectionData, KeyDocDir, "");
        DefaultDocEdit->Text = IniFile->ReadString(SectionData, KeyDefaultDoc, "index.html");
        PortHttpsEdit->Text = IniFile->ReadString(SectionData, KeyPortHttps, "443");
        PortHttpEdit->Text = IniFile->ReadString(SectionData, KeyPortHttp, "80");
        DisplayHeaderCheckBox->Checked = IniFile->ReadInteger(SectionData, KeyDisplayHeader, 0) != 0;
        WriteLogFileCheckBox->Checked = IniFile->ReadInteger(SectionData, KeyLogToFile, 0) != 0;
        DisplaySslInfoCheckBox->Checked = IniFile->ReadInteger(SectionData, KeyDisplaySslInfo, 0) != 0;
        CertFileEdit->Text = IniFile->ReadString(SectionData, KeyCertFile, GSSL_CERTS_DIR + GSSL_LOCALHOST_NAME);
        PrivKeyFileEdit->Text = IniFile->ReadString(SectionData, KeyPrivKeyFile, "");
        PassPhraseEdit->Text = IniFile->ReadString(SectionData, KeyPassPhrase, "password");
        CAFileEdit->Text = IniFile->ReadString(SectionData, KeyCAFile, "");
        CAPathEdit->Text = IniFile->ReadString(SectionData, KeyCAPath, "");
        AcceptableHostsEdit->Text = IniFile->ReadString(SectionData, KeyAcceptableHosts, "");
        VerifyPeerCheckBox->Checked = IniFile->ReadInteger(SectionData, KeyVerifyPeer, 0) != 0;
        FRenegotiationInterval = static_cast<unsigned>(IniFile->ReadInteger(SectionData, KeyRenegInterval, 0));
        SslMinVersion->ItemIndex = IniFile->ReadInteger(SectionData, KeySslMinVersion, 0);
        SslMaxVersion->ItemIndex = IniFile->ReadInteger(SectionData, KeySslMaxVersion, 5);
        SslCipherList->ItemIndex = IniFile->ReadInteger(SectionData, KeySslCipherList, 2);
        SslCipherEdit->Text = IniFile->ReadString(SectionData, KeySslCipherEdit, "");
        OldIp = IniFile->ReadString(SectionData, KeyListenAddr, "");
        OldSslCheckBox->Checked = IniFile->ReadBool(SectionData, KeyOldSsl, false);
        WebDAVCheckBox->Checked = IniFile->ReadBool(SectionData, KeyWebDAV, false);
        SslSecLevel->ItemIndex = IniFile->ReadInteger(SectionData, KeySslSecLevel, 1);
        WellKnownPathEdit->Text = IniFile->ReadString(SectionData, KeyWellKnownPath, "");
        SslCipher13Edit->Text = IniFile->ReadString(SectionData, KeySslCipher13, "");
        SslCryptoGroups->Text = IniFile->ReadString(SectionData, KeySslCryptoGroups, "");
    }

    if (CertFileEdit->Text.IsEmpty())
        CertFileEdit->Text = GSSL_CERTS_DIR + GSSL_LOCALHOST_NAME;
    // demos-data is three levels up from Win64x\<config> (Delphi: four, from bin\<platform>\<config>).
    if (DocDirEdit->Text.IsEmpty())
        DocDirEdit->Text = IcsAbsolutisePath(ExtractFilePath(ParamStr(0)) + "..\\..\\..\\demos-data\\WebServData\\wwwRoot\\");

    // The IP address to listen on: localhost, any, or one of the local addresses (sorted).
    {
        std::unique_ptr<TStringList> InterfaceList(new TStringList());
        InterfaceList->Sorted = true;
        IcsGetInterfaceList(InterfaceList.get());
        ListenAddr->Items->Text = "localhost\r\n0.0.0.0\r\n" + InterfaceList->Text;
    }
    const int I = ListenAddr->Items->IndexOf(OldIp);
    ListenAddr->ItemIndex = I >= 0 ? I : 0;

    if (WriteLogFileCheckBox->Checked) {
        OpenLogFile();
        WriteLogFileCheckBox->Checked = static_cast<bool>(FLogFile);
    }
    ClientHttpsCountLabel->Caption = "0";
    ClientHttpCountLabel->Caption = "0";

    // Version information for the program and the components it uses.
    const TWSAData wsi = WinsockInfo();
    DisplayMemo->Clear();
    Display(AppCopyRight);
    Display("Using:");
    Display("   " + Overbyteicswsocket::CopyRight);
    Display("   " + Overbyteicswsockets::CopyRight);
    Display("   " + Overbyteicshttpsrv::CopyRight);
    Display("    Winsock:");
    Display("        Version " + IntToStr(wsi.wHighVersion >> 8) + "." + IntToStr(wsi.wHighVersion & 15));
    Display("        " + String(AnsiString(wsi.szDescription)));
    Display("        " + String(AnsiString(wsi.szSystemStatus)));
    Display("INI File: " + FIniFileName);
    if (wsi.lpVendorInfo)
        Display("        " + String(AnsiString(wsi.lpVendorInfo)));

    // Load OpenSSL, then show the DLL name and version.
    IcsSslRootCAStore->Initialise();
    DisplayMemo->Lines->Add(String(IcsVerLitRelDate) + ", SSL/TLS DLL: " + IcsReportOpenSSLVer(true));

    if (SslCryptoGroups->Text.IsEmpty()) {
        if (ICS_OPENSSL_VERSION_NUMBER >= static_cast<unsigned>(OSSL_VER_3500))
            SslCryptoGroups->Text = sslCryptoGroups13;   // OpenSSL 3.5 and later have X25519MLKEM768
        else
            SslCryptoGroups->Text = sslCryptoGroupsDef;
    }
    if (SslCipher13Edit->Text.IsEmpty())
        SslCipher13Edit->Text = sslCipherSuitesTLS13;

    // Start both servers.
    StartHttpsButtonClick(this);
    StartHttpButtonClick(this);
}

void __fastcall TSslWebServForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    StopButtonClick(this);
    // Save persistent data to the INI file.
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
    IniFile->WriteInteger(SectionWindow, KeyTop, Top);
    IniFile->WriteInteger(SectionWindow, KeyLeft, Left);
    IniFile->WriteInteger(SectionWindow, KeyWidth, Width);
    IniFile->WriteInteger(SectionWindow, KeyHeight, Height);
    IniFile->WriteString(SectionData, KeyDefaultDoc, SslHttpServer1->DefaultDoc);
    IniFile->WriteString(SectionData, KeyPortHttps, SslHttpServer1->Port);
    IniFile->WriteString(SectionData, KeyPortHttp, HttpServer2->Port);
    IniFile->WriteInteger(SectionData, KeyDisplayHeader, DisplayHeaderCheckBox->Checked ? 1 : 0);
    IniFile->WriteInteger(SectionData, KeyLogToFile, WriteLogFileCheckBox->Checked ? 1 : 0);
    IniFile->WriteInteger(SectionData, KeyDisplaySslInfo, DisplaySslInfoCheckBox->Checked ? 1 : 0);
    IniFile->WriteString(SectionData, KeyCertFile, CertFileEdit->Text);
    IniFile->WriteString(SectionData, KeyPrivKeyFile, PrivKeyFileEdit->Text);
    IniFile->WriteString(SectionData, KeyPassPhrase, PassPhraseEdit->Text);
    IniFile->WriteString(SectionData, KeyCAFile, CAFileEdit->Text);
    IniFile->WriteString(SectionData, KeyCAPath, CAPathEdit->Text);
    IniFile->WriteString(SectionData, KeyAcceptableHosts, AcceptableHostsEdit->Text);
    IniFile->WriteInteger(SectionData, KeyVerifyPeer, VerifyPeerCheckBox->Checked ? 1 : 0);
    IniFile->WriteInteger(SectionData, KeyRenegInterval, static_cast<int>(FRenegotiationInterval));
    IniFile->WriteInteger(SectionData, KeySslMinVersion, SslMinVersion->ItemIndex);
    IniFile->WriteInteger(SectionData, KeySslMaxVersion, SslMaxVersion->ItemIndex);
    IniFile->WriteInteger(SectionData, KeySslCipherList, SslCipherList->ItemIndex);
    IniFile->WriteString(SectionData, KeySslCipherEdit, SslCipherEdit->Text);
    IniFile->WriteString(SectionData, KeyListenAddr, ListenAddr->Items->Strings[ListenAddr->ItemIndex]);
    IniFile->WriteBool(SectionData, KeyOldSsl, OldSslCheckBox->Checked);
    IniFile->WriteBool(SectionData, KeyWebDAV, WebDAVCheckBox->Checked);
    IniFile->WriteInteger(SectionData, KeySslSecLevel, SslSecLevel->ItemIndex);
    IniFile->WriteString(SectionData, KeyWellKnownPath, WellKnownPathEdit->Text);
    // Delphi writes DocDir twice, first from the server; the edit box is the value that is kept.
    IniFile->WriteString(SectionData, KeyDocDir, DocDirEdit->Text);
    IniFile->WriteString(SectionData, KeySslCipher13, SslCipher13Edit->Text);
    IniFile->WriteString(SectionData, KeySslCryptoGroups, SslCryptoGroups->Text);
    IniFile->UpdateFile();
    CloseLogFile();
}

// Displays a message in the memo and writes it to the log file.
void TSslWebServForm::Display(const String &Msg)
{
    AddMemoLine(DisplayMemo, Msg);
    if (FLogFile) {
        try {
            FLogFile->WriteLine(Msg);
        }
        catch (Exception &E) {
            DisplayMemo->Lines->Add("*** Exception" + E.ClassName() + ": " + E.Message + " writing to log file ***");
        }
    }
}

// The WebDAV commands (and a few newer ones); Lock/Unlock will never be supported.
void TSslWebServForm::SetWebDAVOptions(THttpServer *Server)
{
    const THttpOptions WebDAVOptions = THttpOptions() << hoAllowOptions << hoAllowPut << hoAllowDelete
        << hoAllowTrace << hoAllowPatch << hoAllowPropFind << hoAllowMkCol << hoAllowCopy << hoAllowMove
        << hoAllowPropPatch << hoAllowLock << hoAllowUnlock;
    // Delphi only adds them, so unticking WebDAV keeps them until the program restarts.
    Server->Options = WebDAVCheckBox->Checked ? Server->Options + WebDAVOptions : Server->Options - WebDAVOptions;
}

// Starts the HTTPS server (also called from FormShow). OnServerStarted fires once it listens.
// ClientClass makes the server create our own connection class, which holds per-client data.
void __fastcall TSslWebServForm::StartHttpsButtonClick(TObject *Sender)
{
    if (!FileExists(CertFileEdit->Text)) {
        Display("HTTPS Not Started, Can Not Find Server SSL/TLS Certificate: " + CertFileEdit->Text);
        return;
    }
    SslHttpServer1->DocDir = Trim(DocDirEdit->Text);
    SslHttpServer1->WellKnownPath = Trim(WellKnownPathEdit->Text);
    SslHttpServer1->DefaultDoc = Trim(DefaultDocEdit->Text);
    SslHttpServer1->Port = Trim(PortHttpsEdit->Text);
    SslHttpServer1->Addr = ListenAddr->Items->Strings[ListenAddr->ItemIndex];
    SslHttpServer1->ClientClass = __classid(TMyHttpConnection);
    SslHttpServer1->SetAcceptableHostsList(AcceptableHostsEdit->Text);
    FSrvSslCert = "";
    SetWebDAVOptions(SslHttpServer1);

    SslHttpServer1->WSDebugOptions = DefWSDebugServer;   // can be reduced for less logging
    if (DisplayHeaderCheckBox->Checked)
        SslHttpServer1->WSDebugOptions = SslHttpServer1->WSDebugOptions << wsdlogHdr;
    if (DisplaySslInfoCheckBox->Checked)
        SslHttpServer1->WSDebugOptions = SslHttpServer1->WSDebugOptions + DefWSDebugDevel;   // all development logging

    // PEM, DER, PKCS12 and PKCS8 are all accepted, and checked before the context is set up.
    TX509Base *Cert = SslContext1->SslCertX509;
    TChainResult ValRes;
    String ErrStr;
    try {   // Delphi does not catch a load error, so a bad file also stops FormShow starting HTTP
        Cert->LoadFromFile(CertFileEdit->Text, croTry, croTry, PassPhraseEdit->Text);   // try the key and intermediates too
        if (!Cert->IsPKeyLoaded && !PrivKeyFileEdit->Text.IsEmpty())
            Cert->PrivateKeyLoadFromPemFile(PrivKeyFileEdit->Text);
        if (!Cert->IsInterLoaded && !CAFileEdit->Text.IsEmpty())
            Cert->LoadIntersFromPemFile(CAFileEdit->Text);
        // Check the certificate chain for errors (a host name would be better than none).
        ValRes = Cert->ValidateCertChain("", IcsSslRootCAStore, FSrvSslCert, ErrStr);
    }
    catch (Exception &E) {
        Display("HTTPS Not Started, Can Not Load Server SSL/TLS Certificate: " + E.Message);
        return;
    }
    if (!ErrStr.IsEmpty())
        ErrStr = ", " + ErrStr;
    if (ValRes == chainOK)
        ErrStr = "Chain Validated OK" + ErrStr;
    else if (ValRes == chainWarn)
        ErrStr = "Chain Warning" + ErrStr;
    else
        ErrStr = "Chain Failed" + ErrStr;
    // Delphi says "HTTPS Not Started" here even when the chain is fine and the server starts.
    Display(String(ValRes == chainFail ? "HTTPS Not Started: " : "HTTPS Server Certificate: ") +
            FSrvSslCert + "\r\n" + ErrStr + "\r\n");
    if (ValRes == chainFail)
        return;

    SslContext1->SslVerifyPeer = VerifyPeerCheckBox->Checked;
    SslContext1->SslMinVersion = static_cast<TSslVerMethod>(SslMinVersion->ItemIndex);
    SslContext1->SslMaxVersion = static_cast<TSslVerMethod>(SslMaxVersion->ItemIndex);
    switch (SslCipherList->ItemIndex) {
        case 0: SslContext1->SslCipherList = sslCiphersServer; break;
        case 1: SslContext1->SslCipherList = sslCiphersMozillaSrvBack; break;
        case 2: SslContext1->SslCipherList = sslCiphersMozillaSrvTLS12; break;
        default: break;
    }
    if (!SslCipherEdit->Text.IsEmpty())
        SslContext1->SslCipherList = SslCipherEdit->Text;
    SslContext1->SslCipherList13 = SslCipher13Edit->Text.IsEmpty() ? String(sslCipherSuitesTLS13)
                                                                    : SslCipher13Edit->Text;
    SslContext1->SslCryptoGroups = SslCryptoGroups->Text;
    SslContext1->SslSecLevel = static_cast<TSslSecLevel>(SslSecLevel->ItemIndex);
    SslContext1->SslOptions2 = SslContext1->SslOptions2 << sslOpt2_CIPHER_SERVER_PREFERENCE;

    try {
        // Reset SSL when the parameters change, to force a new negotiation.
        if (SslContext1->IsCtxInitialized) {
            SslContext1->DeInitContext();
            SslHttpServer1->WSocketServer->ResetSSL();
        }
        SslContext1->InitContext();   // get any error now, before starting the server

        FSrvSslCert = StringReplace(FSrvSslCert, "\r\n", "<BR>\r\n", TReplaceFlags() << rfReplaceAll);

        // The SSL ciphers and curve groups accepted.
        FSrvCipherList = StringReplace(SslContext1->SslGetAllCiphers(), "\r\n", ", ", TReplaceFlags() << rfReplaceAll);
        FSrvGroupList = StringReplace(SslContext1->SslGetAllGroups(), "\r\n", ", ", TReplaceFlags() << rfReplaceAll);
        Display("SSL Ciphers Accepted: \r\n" + FSrvCipherList + "\r\n" +
                "SSL Groups Accepted: \r\n" + FSrvGroupList + "\r\n");
    }
    catch (Exception &E) {
        Display("Failed to initialize SSL Context: " + E.Message);
        return;
    }
    SslHttpServer1->Start();
}

// A quick test of plain HTTP without SSL, on its own port.
void __fastcall TSslWebServForm::StartHttpButtonClick(TObject *Sender)
{
    HttpServer2->DocDir = Trim(DocDirEdit->Text);
    HttpServer2->WellKnownPath = Trim(WellKnownPathEdit->Text);
    HttpServer2->DefaultDoc = Trim(DefaultDocEdit->Text);
    HttpServer2->Port = Trim(PortHttpEdit->Text);
    HttpServer2->Addr = ListenAddr->Items->Strings[ListenAddr->ItemIndex];
    HttpServer2->ClientClass = __classid(TMyHttpConnection);
    SetWebDAVOptions(HttpServer2);

    HttpServer2->WSDebugOptions = DefWSDebugServer;
    if (DisplaySslInfoCheckBox->Checked)
        HttpServer2->WSDebugOptions = HttpServer2->WSDebugOptions + DefWSDebugDevel;
    if (DisplayHeaderCheckBox->Checked)
        HttpServer2->WSDebugOptions = HttpServer2->WSDebugOptions << wsdlogHdr;

    HttpServer2->Start();
}

// Stops both servers; OnServerStopped fires for each.
void __fastcall TSslWebServForm::StopButtonClick(TObject *Sender)
{
    SslHttpServer1->Stop();
    HttpServer2->Stop();
}

void __fastcall TSslWebServForm::ClearButtonClick(TObject *Sender)
{
    DisplayMemo->Clear();
}

void __fastcall TSslWebServForm::SslHttpServer1ServerStarted(TObject *Sender)
{
    DocDirEdit->Enabled = false;
    DefaultDocEdit->Enabled = false;
    PortHttpsEdit->Enabled = false;
    PortHttpEdit->Enabled = false;
    StartHttpsButton->Enabled = false;
    StopButton->Enabled = true;
    Display("HTTPS Server is waiting for connections");
    String S = IcsFmtIpv6Addr(SslHttpServer1->Addr);   // browser friendly IPv6
    if (S == "0.0.0.0")
        S = "localhost";
    Display("https://" + S + "/demo.html");
}

void __fastcall TSslWebServForm::HttpServer2ServerStarted(TObject *Sender)
{
    StartHttpButton->Enabled = false;
    Display("HTTP Server is waiting for connections");
    String S = IcsFmtIpv6Addr(HttpServer2->Addr);   // Delphi shows the HTTPS server's address, unset if HTTPS did not start
    if (S == "0.0.0.0")
        S = "localhost";
    Display("http://" + S + "/demo.html");
}

void __fastcall TSslWebServForm::SslHttpServer1ServerStopped(TObject *Sender)
{
    DocDirEdit->Enabled = true;
    DefaultDocEdit->Enabled = true;
    PortHttpsEdit->Enabled = true;
    StartHttpsButton->Enabled = true;
    Display("HTTPS Server stopped");
}

void __fastcall TSslWebServForm::HttpServer2ServerStopped(TObject *Sender)
{
    PortHttpEdit->Enabled = true;
    StartHttpButton->Enabled = true;
    Display("HTTP Server stopped");
}

void __fastcall TSslWebServForm::HttpServer2WellKnownDir(TObject *Sender, TObject *Client, const String Path, String &BodyStr)
{
    AnswerWellKnown(this, "HTTP", Path, BodyStr);
}

void __fastcall TSslWebServForm::SslHttpServer1WellKnownDir(TObject *Sender, TObject *Client, const String Path, String &BodyStr)
{
    AnswerWellKnown(this, "HTTPS", Path, BodyStr);
}

// Called for several WebDAV commands; only PROPFIND is handled, by the server.
void __fastcall TSslWebServForm::SslHttpServer1WebDAVDocument(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 TSslWebServForm::SslHttpServer1ClientConnect(TObject *Sender, TObject *Client, System::Word Error)
{
    ClientHttpsCountLabel->Caption = IntToStr(static_cast<THttpServer *>(Sender)->ClientCount);
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    Remote->OnBgException = BackgroundException;
    // Allows the WebDAV commands; a real server would only do so after authentication.
    if (WebDAVCheckBox->Checked)
        Remote->WebDAVAcc = webdavWrite;
}

void __fastcall TSslWebServForm::SslHttpServer1SslServerName(TObject *Sender, TSslContext *&Ctx, TTlsExtError &ErrCode)
{
    // Tells SSL the server can handle SslServerName; teeAlertWarning (the old default) stopped
    // Java clients connecting, teeAlertFatal rejects the connection.
    ErrCode = teeOk;

    TSslWSocketClient *Cli = static_cast<TSslWSocketClient *>(Sender);
    Display("[" + NowHms() + " " + Cli->GetPeerAddr() + "] SNI \"" + Cli->SslServerName + "\" received");
    // SNI, ALPN, version, extensions and more from the client hello.
    Display("Client Hello: " + WSocketGetCliHelloStr(Cli->CliHelloData));

    // A different SslContext could be given in Ctx here for each server name, allowing several
    // hosts and certificates on the same IP address; IcsHosts in TSslWSocketServer does it better.
}

// Selects HTTP/1.1 when the client offers it (HTTP/2 is not supported yet).
void __fastcall TSslWebServForm::SslHttpServer1SslAlpnSelect(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;
        }
    }
}

// Called just before the client component is closed.
void __fastcall TSslWebServForm::SslHttpServer1ClientDisconnect(TObject *Sender, TObject *Client, System::Word Error)
{
    ClientHttpsCountLabel->Caption = IntToStr(static_cast<THttpServer *>(Sender)->ClientCount - 1);
}

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

void __fastcall TSslWebServForm::HttpServer2ClientConnect(TObject *Sender, TObject *Client, System::Word Error)
{
    ClientHttpCountLabel->Caption = IntToStr(static_cast<THttpServer *>(Sender)->ClientCount);
}

void __fastcall TSslWebServForm::HttpServer2ClientDisconnect(TObject *Sender, TObject *Client, System::Word Error)
{
    ClientHttpCountLabel->Caption = IntToStr(static_cast<THttpServer *>(Sender)->ClientCount - 1);
}

// HEAD: count the request and let the server do the rest.
void __fastcall TSslWebServForm::SslHttpServer1HeadDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    FCountRequests++;
    Display(IntToStr(FCountRequests) + ": HEAD " + static_cast<TMyHttpConnection *>(Client)->Path);
}

// GET: count the request and build the pages that do not exist on disk. With a classic web
// server this would need a CGI or ISAPI module; here it is plain code.
void __fastcall TSslWebServForm::SslHttpServer1GetDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    FCountRequests++;
    Display("[" + NowHms() + " " + Remote->GetPeerAddr() + "] " + IntToStr(FCountRequests) +
            ": GET " + Remote->Path);

    if (CompareText(Remote->Path, "/demo.html") == 0)
        CreateVirtualDocument_Demo(Remote, Flags);
    else if (CompareText(Remote->Path, "/time.html") == 0)
        CreateVirtualDocument_time_htm(Remote, Flags);
    else if (CompareText(Remote->Path, "/redir.html") == 0)
        CreateVirtualDocument_redir_htm(Remote, Flags);
}

// Sends a complete answer (status line, headers and an HTML body) as the document stream.
void TSslWebServForm::SendHtmlStream(TMyHttpConnection *Client, const String &Status, const String &ExtraHeader,
                                     const String &Body)
{
    // Content-Length counts the ANSI bytes written (Delphi counts characters).
    const AnsiString BodyA = Body;
    const String Header = Client->Version + " " + Status + "\r\n" +
                          "Content-Type: text/html\r\n" + ExtraHeader +
                          "Content-Length: " + IntToStr(BodyA.Length()) + "\r\n\r\n";
    TMemoryStream *Stream = new TMemoryStream();   // the connection owns and frees the document stream
    Client->DocStream = Stream;
    StreamWriteStrA(Stream, Header);
    Stream->WriteBuffer(BodyA.c_str(), BodyA.Length());
    Stream->Seek(static_cast<__int64>(0), soBeginning);
    Client->SendStream();
}

// /redir.html?url redirects to url (default /time.html).
void TSslWebServForm::CreateVirtualDocument_redir_htm(TMyHttpConnection *Client, THttpGetFlag &Flags)
{
    String Location = Client->Params;
    if (Location.IsEmpty())
        Location = "/time.html";
    Flags = hgWillSendMySelf;   // we send the answer ourselves
    SendHtmlStream(Client, "302 OK", "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>\r\n");
}

void TSslWebServForm::CreateVirtualDocument_Demo(TMyHttpConnection *Client, THttpGetFlag &Flags)
{
    Client->AnswerString(Flags, "", "", "",
        "<HTML><HEAD><TITLE>ICS-SSL WebServer Demo - Menu</TITLE></HEAD><BODY>"
        "<H2>ICS-SSL WebServer Demo Menu</H2>\r\n"
        "<H3>" + Client->SslHandshakeRespMsg + "</H3>\r\n" +
        "<A HREF=\"/time.html\">Server time</A><BR>\r\n"
        "<A HREF=\"/redir.html\">Redirection</A><BR>\r\n"
        "<A HREF=\"/\">Default document</A><BR>\r\n"
        "<A HREF=\"https://www.overbyte.be\">ICS Home page</A><P>\r\n"
        "<P>OpenSSL Version: " + IcsReportOpenSSLVer() + "<BR>\r\n\r\n" +
        "Server SSL Certificates: <BR>\r\n" +
        FSrvSslCert + "<BR><BR>\r\n" +
        "OpenSSL Server Ciphers: " + FSrvCipherList + "<P>\r\n" +
        "SSL Ciphers from Client: " + FCliCipherList + "<P>\r\n" +
        "Note: There is a better demo OverbyteIcsSslMultiWebServ.<P>\r\n"
        "</BODY>\r\n</HTML>\r\n");
}

void TSslWebServForm::CreateVirtualDocument_time_htm(TMyHttpConnection *Client, THttpGetFlag &Flags)
{
    Flags = hgWillSendMySelf;   // we send the answer ourselves
    SendHtmlStream(Client, "200 OK", "",
        "<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</BODY></HTML>\r\n");
}

/* POST: count the request and accept data only for /cgi-bin/cgifrm1.exe, for example from:
     <FORM METHOD="POST" ACTION="/cgi-bin/cgifrm1.exe">
       <INPUT TYPE="TEXT" NAME="FirstName" MAXLENGTH="25" VALUE="YourFirstName">
       <INPUT TYPE="TEXT" NAME="LastName" MAXLENGTH="25" VALUE="YourLastName">
       <INPUT TYPE="SUBMIT" NAME="Submit" VALUE="Button">
     </FORM>  */
void __fastcall TSslWebServForm::SslHttpServer1PostDocument(TObject *Sender, TObject *Client, THttpGetFlag &Flags)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    FCountRequests++;
    Display(IntToStr(FCountRequests) + ": POST " + Remote->Path);

    if (CompareText(Remote->Path, "/cgi-bin/cgifrm1.exe") != 0) {
        Flags = hg404;
        return;
    }

    // Delphi allocates whatever Content-Length the client sends and never answers a zero length.
    const __int64 ContentLength = Remote->RequestContentLength;
    if (ContentLength < 0) {
        Display("Invalid Content-Length " + IntToStr(ContentLength));
        Flags = hg400;
        return;
    }
    if (ContentLength > MaxPostedDataSize) {
        Display("Content-Length " + IntToStr(ContentLength) + " exceeds the " +
                IntToStr(MaxPostedDataSize) + " byte limit");
        // Header-only answer, then close the connection instead of reading the oversized body.
        Remote->KeepAlive = false;
        Remote->SendHeader(Remote->Version + " 413 Payload Too Large\r\n"
                           "Content-Length: 0\r\nConnection: close\r\n\r\n");
        Remote->CloseDelayed();
        Flags = hgWillSendMySelf;
        return;
    }
    if (ContentLength == 0) {
        // No OnPostedData event follows an empty body, so the request completes here.
        Flags = hgWillSendMySelf;
        Remote->PostedDataBuffer = "";
        Remote->PostedDataReceived();
        ProcessPostedData_CgiFrm1(Remote);
        return;
    }

    // Accept the posted data: OnPostedData fires as it arrives, in any format, so line
    // mode is off. The buffer holds the whole content.
    Flags = hgAcceptData;
    Remote->LineMode = false;
    Remote->PostedRawData.SetLength(static_cast<int>(ContentLength));
    Remote->DataLen = 0;
}

// Called for each packet of posted data once the POST was accepted. All the data the client
// sends must be received, even beyond Content-Length.
void __fastcall TSslWebServForm::SslHttpServer1PostedData(TObject *Sender, TObject *Client, System::Word Error)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    const int Remains = static_cast<int>(Remote->RequestContentLength) - Remote->DataLen;
    if (Remains <= 0) {
        // We have all our data: junk anything else.
        char Junk[256];
        Remote->Receive(Junk, static_cast<int>(sizeof(Junk)) - 1);
        return;
    }
    // The data may come in several packets, assembled in the buffer.
    const int Len = Remote->Receive(&Remote->PostedRawData[Remote->DataLen + 1], Remains);
    if (Len <= 0)
        return;
    Remote->DataLen += Len;
    Display("Data: '" + String(Remote->PostedRawData.SubString(1, Remote->DataLen)) + "'");

    if (Remote->DataLen == Remote->RequestContentLength) {
        // Delphi keeps a pointer into a temporary string here (freed at once) and later FreeMems it.
        Remote->PostedDataBuffer = String(Remote->PostedRawData);
        // First tell the component that all the data has arrived.
        Remote->PostedDataReceived();
        if (CompareText(Remote->Path, "/cgi-bin/cgifrm1.exe") == 0)
            ProcessPostedData_CgiFrm1(Remote);
        else
            Remote->Answer404();
    }
}

// Appends the posted names to CgiFrm1.txt and answers with what was recorded.
void TSslWebServForm::ProcessPostedData_CgiFrm1(TMyHttpConnection *Client)
{
    String FirstName, LastName;
    ExtractURLEncodedValue(Client->PostedDataBuffer, "FirstName", FirstName);
    ExtractURLEncodedValue(Client->PostedDataBuffer, "LastName", LastName);
    // The client IP address; a reverse DNS lookup could give the host name.
    const String HostName = Client->PeerAddr;
    const String Buf = FormatDateTime("YYYYMMDD HHNNSS ", Now()) +
                       FirstName + "." + LastName + "@" + HostName + "\r\n";

    const String FileName = ExtractFilePath(Application->ExeName) + "CgiFrm1.txt";
    {
        // unique_ptr closes the file if a write raises (Delphi: Destroy with no finally).
        std::unique_ptr<TFileStream> Stream(new TFileStream(FileName, FileExists(FileName) ? fmOpenWrite : fmCreate));
        Stream->Seek(static_cast<__int64>(0), soEnd);
        StreamWriteStrA(Stream.get(), Buf);
    }

    // The posted values are HTML-encoded (Delphi echoes them raw into the page).
    SendHtmlStream(Client, "200 OK", "",
        "<HTML><HEAD><TITLE>ICS WebServer Demo</TITLE></HEAD>\r\n"
        "<BODY><H2>Your data has been recorded:</H2>\r\n"
        "<P>" + TextToHtmlText(FirstName) + "." + TextToHtmlText(LastName) + "@" + HostName + "</P>" +
        "</BODY></HTML>\r\n");
}

void TSslWebServForm::OpenLogFile()
{
    if (FLogFile)
        return;
    try {
        // Appends to an existing file, in the ANSI code page like Delphi's TextFile.
        FLogFile.reset(new TStreamWriter(FLogFileName, true, TEncoding::Default, 1024));
        FLogFile->WriteLine("[" + FormatDateTime("HH:NN:SS YYYY/MM/DD", Now()) + " Log file opened.]");
    }
    catch (Exception &) {
        FLogFile.reset();
        Display("*** Unable to open log file ***");
    }
}

void TSslWebServForm::CloseLogFile()
{
    if (!FLogFile)
        return;
    std::unique_ptr<TStreamWriter> LogFile(std::move(FLogFile));   // Display stops writing to it first
    LogFile->WriteLine("[" + FormatDateTime("HH:NN:SS YYYY/MM/DD", Now()) + " Log file Closed.]");
}

void __fastcall TSslWebServForm::WriteLogFileCheckBoxClick(TObject *Sender)
{
    if (WriteLogFileCheckBox->Checked)
        OpenLogFile();
    else
        CloseLogFile();
    WriteLogFileCheckBox->Checked = static_cast<bool>(FLogFile);
}

// In this example any client certificate is accepted.
void __fastcall TSslWebServForm::SslHttpServer1SslVerifyPeer(TObject *Sender, int &Ok, TX509Base *Cert)
{
    if (DisplaySslInfoCheckBox->Checked)
        Display("Received certificate\r\nSubject: " + Cert->SubjectOneLine + "\r\nIssuer: " + Cert->IssuerOneLine);
    if (Ok != 1) {
        if (DisplaySslInfoCheckBox->Checked)
            Display("Error msg: " + Cert->VerifyErrMsg + "\r\nIn this example we accept any cert");
        Ok = 1;
    }
}

// External SSL session cache: look a session up by its id.
void __fastcall TSslWebServForm::SslHttpServer1SslSvrGetSession(TObject *Sender, void *&SslSession, void *SessId,
                                                                int Idlen, bool &IncRefCount)
{
    SslSession = SslAvlSessionCache1->GetSvrSession(SessionKey(SessId, Idlen), IncRefCount);
}

// External SSL session cache: store a new session under its id.
void __fastcall TSslWebServForm::SslHttpServer1SslSvrNewSession(TObject *Sender, void *SslSession, void *SessId,
                                                                int Idlen, bool &AddToInternalCache)
{
    SslAvlSessionCache1->CacheSvrSession(SslSession, SessionKey(SessId, Idlen), AddToInternalCache);
    if (DisplaySslInfoCheckBox->Checked)
        Display("[" + NowHms() + " " + static_cast<TCustomWSocket *>(Sender)->GetPeerAddr() +
                "] New SSL session created and cached in external cache class.");
}

// Requests an SSL renegotiation once RenegotiationInterval (ms, ini file only) has passed;
// renegotiation is no longer supported by modern TLS, so this usually fails.
void __fastcall TSslWebServForm::SslHttpServer1BeforeProcessRequest(TObject *Sender, TObject *Client)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Client);
    if (!Remote->SslEnable || FRenegotiationInterval == 0)
        return;
    const unsigned Ticks = GetTickCount();
    if (Remote->LastHandshake + FRenegotiationInterval >= Ticks)
        return;
    const bool Started = Remote->SslStartRenegotiation();
    if (DisplaySslInfoCheckBox->Checked)
        Display("[" + NowHms() + " " + Remote->GetPeerAddr() + "] " +
                (Started ? "SSL renegotiation flag set." : "SslStartRenegotiation failed "));
}

void __fastcall TSslWebServForm::SslHttpServer1SslHandshakeDone(TObject *Sender, System::Word ErrCode,
                                                                TX509Base *PeerCert, bool &Disconnect)
{
    TMyHttpConnection *Remote = static_cast<TMyHttpConnection *>(Sender);
    FCliCipherList = StringReplace(Remote->SslGetSupportedCiphers(true, true), "\r\n", ", ",
                                   TReplaceFlags() << rfReplaceAll);

    // The client hello is normally listed in the server name event; list it here when there is no name.
    if (Remote->SslServerName.IsEmpty())
        Display("Client Hello: " + WSocketGetCliHelloStr(Remote->CliHelloData));

    if (ErrCode == 0)
        Remote->LastHandshake = GetTickCount();
    if (!DisplaySslInfoCheckBox->Checked)
        return;
    if (ErrCode == 0)
        Display("[" + NowHms() + " " + Remote->GetPeerAddr() + "] " + Remote->SslHandshakeRespMsg +
                ", SessionReused " + IntToStr(Remote->SslSessionReused() ? 1 : 0));
    else
        Display("[" + NowHms() + " " + Remote->GetPeerAddr() + "] SslHandshake failed, error #" +
                IntToStr(ErrCode) + " - " + Remote->SslHandshakeRespMsg);
    Display("SSL Ciphers from Client: \r\n" + FCliCipherList + "\r\n");
}

// Delphi raises a new exception here as a test and never sets CanClose; the C++ logs and closes the client.
void __fastcall TSslWebServForm::BackgroundException(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("BgException: " + E->ClassName() + ": " + E->Message);
    CanClose = true;
}
