/*
  ICS IP Streaming Log demo - C++Builder translation of the Delphi demo OverbyteIcsIpStmLogTst1.pas
  (demos-delphi-vcl), ICS V9.8.

  A client and server test application for TIcsIpStrmLog, which sends and receives lines of text
  (or a file stream) over UDP or TCP, optionally with SSL/TLS, IPv4 or IPv6, to one or several remote
  hosts, with reconnection. Local mode runs a server and a client talking to each other. The SSL
  server can order and install its own certificates with TSslX509Certs. The components are created
  in code, so nothing needs installing on the palette. Settings are kept in the ICS ini file.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The client, server, file stream and log file stream are std::unique_ptr members created with no
    Owner (Delphi: unit-level variables, Create(Self), freed in OnDestroy and FormClose).
  - The TCP server host set-up shared by Local and Server is one function, SetupTcpServerHost.
  - The server's debug options are added to its own (Delphi builds them from the client's).
  - The server's Unicode sample lines carry the server's serial number (Delphi uses the client's).
  - Choosing Any IPv4 or Any IPv6 in Socket Family sets the local address again: SocketFamilyChange
    is connected in FormCreate (Delphi declares it but never connects it).
  - The application monitor stays disabled by a constant instead of an early exit.
  - The stop wait uses GetTickCount64 and waits for messages instead of sleeping.
  - Failures that the Delphi swallows with an empty except (local address list, log file write)
    are logged.
  - The file size of a file being sent is shown with the 64-bit formatter.
  - The unused Delphi variables and constants (certificate directory, log option sets, DNS
    challenge numbers, the deliberate memory leak) are not converted.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsIpStmLogTst1.h"

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

TIpLogForm *IpLogForm;

namespace {

const int ProtoUdp = 0;
const int ProtoTcp = 1;

// Change to true to report to the ICS Application Monitor sample (IcsAppMon.exe), which must be running.
const bool AppMonEnabled = false;

// '£ € Œ Ω ☼ ⅓' as UTF-16 code units, for the Unicode sample lines
const String UChars = L"£ € Œ Ω ☼ ⅓";

// Runs the given timer again when it goes out of scope, as the Delphi try/finally does.
class TTimerRestart
{
public:
    explicit TTimerRestart(TTimer *Timer) : FTimer(Timer) { FTimer->Enabled = false; }
    ~TTimerRestart() { FTimer->Enabled = true; }
    TTimerRestart(const TTimerRestart &) = delete;
    TTimerRestart &operator=(const TTimerRestart &) = delete;
private:
    TTimer *FTimer;
};

String BoolText(bool Value)
{
    return Value ? "True" : "False";
}

}  // namespace

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

// Done here rather than in OnDestroy, which in C++ runs after the members are gone. The component
// events that write to the log are disconnected first: the form's components are freed after this
// destructor, when the log buffers no longer exist.
__fastcall TIpLogForm::~TIpLogForm()
{
    FCertCheckTrigger = Trigger64Disabled;
    DataTimer->Enabled = false;
    TimerLog->Enabled = false;
    FIpLogClient.reset();
    FIpLogServer.reset();
    // FCliSend and FSrvSend are members, so their streams are freed after this body: after both
    // components, which may still hold a pointer to them, are gone.
    IcsAppMonCli->onLogEvent = nullptr;
    SslX509Certs->OnCertProg = nullptr;
    SslX509Certs->OnNewCert = nullptr;
    SslX509Certs->OnOAuthAuthUrl = nullptr;
    SslX509Certs->OnChallengeDNS = nullptr;
}

void __fastcall TIpLogForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    FIcsBuffLogStream.reset();   // write log file
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));   // PreserveAnsi: Delphi default
    const String Section = "Main";
    IniFile->WriteString(Section, "DataClient_Checked", BoolText(DataClient->Checked));
    IniFile->WriteString(Section, "DataGap_Text", DataGap->Text);
    IniFile->WriteString(Section, "DataServer_Checked", BoolText(DataServer->Checked));
    IniFile->WriteInteger(Section, "DataServer_State", static_cast<int>(DataServer->State));
    IniFile->WriteString(Section, "HeavyTraffic_Checked", BoolText(HeavyTraffic->Checked));
    IniFile->WriteInteger(Section, "HeavyTraffic_State", static_cast<int>(HeavyTraffic->State));
    IniFile->WriteString(Section, "LocalAddr_Text", LocalAddr->Text);
    IniFile->WriteString(Section, "LocalPort_Text", LocalPort->Text);
    IniFile->WriteString(Section, "MaxSockets_Text", MaxSockets->Text);
    IniFile->WriteString(Section, "PingRemote_Checked", BoolText(PingRemote->Checked));
    IniFile->WriteInteger(Section, "Protocol_ItemIndex", Protocol->ItemIndex);
    IniFile->WriteString(Section, "ProxyURL_Text", ProxyURL->Text);
    IniFile->WriteString(Section, "RawData_Checked", BoolText(RawData->Checked));
    IniFile->WriteString(Section, "RemoteHosts_Lines", RemoteHosts->Lines->CommaText);
    IniFile->WriteString(Section, "RemotePort_Text", RemotePort->Text);
    IniFile->WriteString(Section, "ReportChain_Checked", BoolText(ReportChain->Checked));
    IniFile->WriteString(Section, "RevokeCheck_Checked", BoolText(RevokeCheck->Checked));
    IniFile->WriteString(Section, "SendFileName_Text", SendFileName->Text);
    IniFile->WriteString(Section, "ServerPort_Text", ServerPort->Text);
    IniFile->WriteInteger(Section, "SocketFamily_ItemIndex", SocketFamily->ItemIndex);
    IniFile->WriteString(Section, "SrvTimeout_Text", SrvTimeout->Text);
    IniFile->WriteString(Section, "SslCACerts_Text", SslCACerts->Text);
    IniFile->WriteString(Section, "SslCertAuth_Text", SslCertAuth->Text);
    IniFile->WriteString(Section, "SslCertKey_Text", SslCertKey->Text);
    IniFile->WriteInteger(Section, "SslCliSec_ItemIndex", SslCliSec->ItemIndex);
    IniFile->WriteString(Section, "SslDomainName_Text", SslDomainName->Text);
    IniFile->WriteString(Section, "SslCertPassword_Text", SslCertPassword->Text);
    IniFile->WriteString(Section, "SslServCert_Text", SslServCert->Text);
    IniFile->WriteInteger(Section, "SslSrvSec_ItemIndex", SslSrvSec->ItemIndex);
    IniFile->WriteString(Section, "UseSSL_Checked", BoolText(UseSSL->Checked));
    IniFile->WriteInteger(Section, "VerifyCertMode_ItemIndex", VerifyCertMode->ItemIndex);
    IniFile->WriteString(Section, "SslCertAutoOrder_Checked", BoolText(SslCertAutoOrder->Checked));
    IniFile->WriteString(Section, "SslCertExpireDays_Text", SslCertExpireDays->Text);
    IniFile->WriteInteger(Section, "SslCertSupplierProto_ItemIndex", SslCertSupplierProto->ItemIndex);
    IniFile->WriteInteger(Section, "SslCertChallenge_ItemIndex", SslCertChallenge->ItemIndex);
    IniFile->WriteInteger(Section, "SslCertPKeyType_ItemIndex", SslCertPKeyType->ItemIndex);
    IniFile->WriteInteger(Section, "SslCertSignDigest_ItemIndex", SslCertSignDigest->ItemIndex);
    IniFile->WriteString(Section, "SslCertProduct_Text", SslCertProduct->Text);
    IniFile->WriteString(Section, "SslCertDirWork_Text", SslCertDirWork->Text);
    IniFile->WriteString(Section, "DirLogs_Text", DirLogs->Text);
    IniFile->WriteInteger(Section, "DnsChlgType_ItemIndex", DnsChlgType->ItemIndex);
    IniFile->WriteString(Section, "DataUnicode_Checked", BoolText(DataUnicode->Checked));
    IniFile->WriteString(Section, "UseUtf8_Checked", BoolText(UseUtf8->Checked));
    IniFile->WriteString(Section, "CliRetryAttempts_Text", CliRetryAttempts->Text);
    IniFile->WriteInteger("Window", "Top", Top);
    IniFile->WriteInteger("Window", "Left", Left);
    IniFile->WriteInteger("Window", "Width", Width);
    IniFile->WriteInteger("Window", "Height", Height);
    IniFile->UpdateFile();
}

void __fastcall TIpLogForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    ReportMemoryLeaksOnShutdown = (DebugHook != 0);

    // list socket families, security levels and the certificate ordering choices, in enum order
    SocketFamily->Items->Clear();
    for (size_t i = 0; i < SocketFamilyNames.size(); i++)
        SocketFamily->Items->Add(String(SocketFamilyNames[i]));
    SslSrvSec->Items->Clear();
    for (size_t i = 0; i < SslSrvSecurityNames.size(); i++)
        SslSrvSec->Items->Add(String(SslSrvSecurityNames[i]));
    SslCliSec->Items->Clear();
    for (size_t i = 0; i < SslCliSecurityNames.size(); i++)
        SslCliSec->Items->Add(String(SslCliSecurityNames[i]));
    SslCertSupplierProto->Items->Clear();
    for (size_t i = 0; i < SupplierProtoLits.size(); i++)
        SslCertSupplierProto->Items->Add(SupplierProtoLits[i]);
    SslCertSignDigest->Items->Clear();
    for (int i = 0; i <= DigestListLitsLast; i++)
        SslCertSignDigest->Items->Add(String(DigestListLits[i]));
    SslCertPKeyType->Items->Clear();
    for (int i = 0; i <= static_cast<int>(SslPrivKeyTypeLastCert); i++)
        SslCertPKeyType->Items->Add(String(SslPrivKeyTypeLits[i]));
    SslCertChallenge->Items->Clear();
    for (size_t i = 0; i < ChallengeTypeLits.size(); i++)
        SslCertChallenge->Items->Add(ChallengeTypeLits[i]);

    // set local IPs
    try {
        LocalAddr->Items->Assign(LocalIPList(sfIPv4, IPPROTO_TCP));
        if (IsIPv6APIAvailable())
            LocalAddr->Items->AddStrings(LocalIPList(sfIPv6, IPPROTO_TCP));
    }
    catch (Exception &E) {   // Delphi ignores this silently
        LogWin->Lines->Add("Could not list local IP addresses - " + E.Message);
    }
    if (IsIPv6APIAvailable())
        LocalAddr->Items->Insert(0, ICS_ANY_HOST_V6);
    LocalAddr->Items->Insert(0, ICS_ANY_HOST_V4);
    LocalAddr->Items->Insert(1, ICS_LOCAL_HOST_V4);

    // get old settings
    FIniFileName = GetIcsIniFileName();
    LogWin->Lines->Add("INI File: " + FIniFileName);
    {
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        const String Section = "Main";
        auto readBool = [&IniFile, &Section](const String &Key) {
            return IniFile->ReadString(Section, Key, "False") == "True";
        };
        DataClient->Checked = readBool("DataClient_Checked");
        DataGap->Text = IniFile->ReadString(Section, "DataGap_Text", "1000");
        DataServer->Checked = readBool("DataServer_Checked");
        DataServer->State = static_cast<TCheckBoxState>(IniFile->ReadInteger(Section, "DataServer_State",
                                                                             static_cast<int>(cbUnchecked)));
        HeavyTraffic->Checked = readBool("HeavyTraffic_Checked");
        HeavyTraffic->State = static_cast<TCheckBoxState>(IniFile->ReadInteger(Section, "HeavyTraffic_State",
                                                                               static_cast<int>(cbUnchecked)));
        LocalAddr->Text = IniFile->ReadString(Section, "LocalAddr_Text", "0.0.0.0");
        LocalPort->Text = IniFile->ReadString(Section, "LocalPort_Text", "25678");
        MaxSockets->Text = IniFile->ReadString(Section, "MaxSockets_Text", "4");
        PingRemote->Checked = readBool("PingRemote_Checked");
        Protocol->ItemIndex = IniFile->ReadInteger(Section, "Protocol_ItemIndex", 0);
        ProxyURL->Text = IniFile->ReadString(Section, "ProxyURL_Text", "");
        RawData->Checked = readBool("RawData_Checked");
        RemoteHosts->Lines->CommaText = IniFile->ReadString(Section, "RemoteHosts_Lines", "192.168.1.120");
        RemotePort->Text = IniFile->ReadString(Section, "RemotePort_Text", "514");
        ReportChain->Checked = readBool("ReportChain_Checked");
        RevokeCheck->Checked = readBool("RevokeCheck_Checked");
        SendFileName->Text = IniFile->ReadString(Section, "SendFileName_Text", "");
        ServerPort->Text = IniFile->ReadString(Section, "ServerPort_Text", "514");
        SocketFamily->ItemIndex = IniFile->ReadInteger(Section, "SocketFamily_ItemIndex", 0);
        SrvTimeout->Text = IniFile->ReadString(Section, "SrvTimeout_Text", "300");
        SslCACerts->Text = IniFile->ReadString(Section, "SslCACerts_Text", "");
        SslCertAuth->Text = IniFile->ReadString(Section, "SslCertAuth_Text", "");
        SslCertKey->Text = IniFile->ReadString(Section, "SslCertKey_Text", "");
        SslCliSec->ItemIndex = IniFile->ReadInteger(Section, "SslCliSec_ItemIndex", 0);
        SslDomainName->Text = IniFile->ReadString(Section, "SslDomainName_Text", "");
        SslCertPassword->Text = IniFile->ReadString(Section, "SslCertPassword_Text", "");
        SslServCert->Text = IniFile->ReadString(Section, "SslServCert_Text", "");
        SslSrvSec->ItemIndex = IniFile->ReadInteger(Section, "SslSrvSec_ItemIndex", 0);
        UseSSL->Checked = readBool("UseSSL_Checked");
        VerifyCertMode->ItemIndex = IniFile->ReadInteger(Section, "VerifyCertMode_ItemIndex", 0);
        SslCertAutoOrder->Checked = readBool("SslCertAutoOrder_Checked");
        SslCertExpireDays->Text = IniFile->ReadString(Section, "SslCertExpireDays_Text", "30");
        SslCertSupplierProto->ItemIndex = IniFile->ReadInteger(Section, "SslCertSupplierProto_ItemIndex", 2);
        SslCertChallenge->ItemIndex = IniFile->ReadInteger(Section, "SslCertChallenge_ItemIndex", 3);
        SslCertPKeyType->ItemIndex = IniFile->ReadInteger(Section, "SslCertPKeyType_ItemIndex", 1);
        SslCertSignDigest->ItemIndex = IniFile->ReadInteger(Section, "SslCertSignDigest_ItemIndex", 2);
        SslCertDirWork->Text = IniFile->ReadString(Section, "SslCertDirWork_Text", "");
        SslCertProduct->Text = IniFile->ReadString(Section, "SslCertProduct_Text", SslCertProduct->Text);
        DirLogs->Text = IniFile->ReadString(Section, "DirLogs_Text", "");
        DnsChlgType->ItemIndex = IniFile->ReadInteger(Section, "DnsChlgType_ItemIndex", 0);
        DataUnicode->Checked = readBool("DataUnicode_Checked");
        UseUtf8->Checked = readBool("UseUtf8_Checked");
        CliRetryAttempts->Text = IniFile->ReadString(Section, "CliRetryAttempts_Text", "3");
        Top = IniFile->ReadInteger("Window", "Top", (Screen->Height - Height) / 2);
        Left = IniFile->ReadInteger("Window", "Left", (Screen->Width - Width) / 2);
        Width = IniFile->ReadInteger("Window", "Width", Width);
        Height = IniFile->ReadInteger("Window", "Height", Height);
    }
    SocketFamily->OnClick = SocketFamilyChange;   // after the ini load, which sets both boxes itself

    SetButtons(false);
    PageControl1->ActivePage = SheetOperation;
    LabelSendClient->Caption = "Total Lines Sent to Server 0";
    LabelSendServer->Caption = "Total Lines Sent by Server 0";

    FIpLogServer.reset(new TIcsIpStrmLog(nullptr));
    FIpLogServer->onLogRecvEvent = IpLogServerRecvEvent;
    FIpLogServer->onLogProgEvent = IpLogServerProgEvent;
    FIpLogServer->onLogChangeEvent = IpLogServerChangeEvent;
    FIpLogServer->MaxSockets = 1;

    FIpLogClient.reset(new TIcsIpStrmLog(nullptr));
    FIpLogClient->onLogRecvEvent = IpLogClientRecvEvent;
    FIpLogClient->onLogProgEvent = IpLogClientProgEvent;
    FIpLogClient->onLogChangeEvent = IpLogClientChangeEvent;
    FIpLogClient->OnSslCertVerifyEvent = IpLogClientSslCertVerifyEvent;
    FIpLogClient->RetryWaitSecs = 10;
    FCertCheckTrigger = Trigger64Disabled;

    // load OpenSSL; its version is logged when the log file is opened
    IcsSslRootCAStore->Initialise();   // if OpenSSL and the internal root store are not loaded, do it
    IcsSslLoadProviders(true, false);  // need legacy provider

    if (SslSrvSec->ItemIndex <= 0)
        SslSrvSec->ItemIndex = static_cast<int>(sslSrvSecDefault);
    if (SslCliSec->ItemIndex <= 0)
        SslCliSec->ItemIndex = static_cast<int>(sslCliSecDefault);
}

void __fastcall TIpLogForm::FormResize(TObject *Sender)
{
    DataWin->Width = PanelSplitter->Width / 3;
    LogWin->Width = PanelSplitter->Width - DataWin->Width;
    DataWin->Left = LogWin->Width;
}

// Log lines are buffered here and written to the memo by TimerLog; also written to the log file.
void TIpLogForm::AddLog(const String &S)
{
    FBuffLogLines += S + IcsCRLF;
    if (DirLogs->Text == "" || !FIcsBuffLogStream)
        return;
    try {
        FIcsBuffLogStream->WriteLine(S);
    }
    catch (Exception &E) {   // Delphi ignores this silently
        FBuffLogLines += "Log file write failed - " + E.Message + IcsCRLF;
    }
}

void __fastcall TIpLogForm::TimerLogTimer(TObject *Sender)
{
    // check SSL certificates every two hours, may order expired certs
    if (IcsTestTrgTick64(FCertCheckTrigger)) {
        FCertCheckTrigger = IcsGetTrgMins64(120);
        try {
            // don't stop on first error, no exceptions
            String CertErrs;
            const bool NewFlag = FIpLogServer->SrvRecheckSslCerts(CertErrs, false, true);
            if (NewFlag || CertErrs != "") {
                if (NewFlag)
                    AddLog("Server Recheck Loaded New SSL Certificate(s)");
                AddLog(String("Recheck SSL Certificate Errors:") + IcsCRLF + CertErrs);
            }
        }
        catch (Exception &E) {
            AddLog("Recheck SSL Certificate Failed - " + E.Message);
        }
    }

    // tell the Application Monitor component we are running
    if (IcsAppMonCli->CliState == CliStateOK)
        IcsAppMonCli->Awake("", "");

    const int DispLen = FBuffLogLines.Length();
    if (DispLen > 0) {
        FBuffLogLines.SetLength(DispLen - 2);   // remove CRLF
        LogWin->Lines->Add(FBuffLogLines);
        SendMessage(LogWin->Handle, EM_LINESCROLL, 0, 999999);
        FBuffLogLines = "";
    }
}

// Opens the log file, or renames it if the log directory changed. The log is written as UTF-8.
void TIpLogForm::OpenLogFile()
{
    if (DirLogs->Text == "")
        return;   // no log
    // a FormatDateTime mask: the quoted parts are literal, yyyy-mm-dd is today's date
    const String FName = "\"" + IncludeTrailingPathDelimiter(DirLogs->Text) + "ics-ipstmlog-\"yyyy-mm-dd\".log\"";
    if (!FIcsBuffLogStream)
        FIcsBuffLogStream.reset(new TIcsBuffLogStream(nullptr, FName, Caption + IcsCRLF, FileCPUtf8));
    else {
        if (FName == FIcsBuffLogStream->NameMask)
            return;   // skip no change
        if (FIcsBuffLogStream->LogSize > 0)
            FIcsBuffLogStream->FlushFile(true);   // changing log path, write old log first
        FIcsBuffLogStream->NameMask = FName;
    }
    AddLog(String(IcsCRLF) + "Opened log file: " + FIcsBuffLogStream->FullName);
    AddLog(String(IcsVerLitRelDate) + ", SSL/TLS " + IcsReportOpenSSLVer(true));
}

void TIpLogForm::SetButtons(bool Started)
{
    doStop->Enabled = Started;
    doLocal->Enabled = !Started;
    doClient->Enabled = !Started;
    doServer->Enabled = !Started;
    doSrvSendFile->Enabled = Started;
    doCliSendFile->Enabled = Started;
}

void __fastcall TIpLogForm::doExitClick(TObject *Sender)
{
    doStopClick(this);
    Close();
}

// Starts the Application Monitor client in monitor mode (no restarting); disabled unless AppMonEnabled.
void TIpLogForm::StartAppMon()
{
    if (!AppMonEnabled)
        return;
    IcsAppMonCli->AppTitle = "OverbyteIcsIpStmLogTst";
    IcsAppMonCli->AppGeneral = Caption + ", " + IcsBuiltWithEx() + ", " + IcsReportOpenSSLVer(false);
    IcsAppMonCli->ServiceName = ServiceNone;
    IcsAppMonCli->LANWide = false;   // only listen for ServMon on this computer
    IcsAppMonCli->AppHandle = Handle;
    IcsAppMonCli->AppMonMode = AMModeMonitor;   // no restarting
    IcsAppMonCli->LogPackets = true;
    IcsAppMonCli->Start("OverbyteIcsIpStmLogTst Monitoring Starting");
}

void __fastcall TIpLogForm::DataTimerTimer(TObject *Sender)
{
    TTimerRestart Restart(DataTimer);

    // sending sample data
    DataTimer->Interval = Icsatoi(DataGap->Text);
    if (DataClient->Checked) {
        const int Tot = HeavyTraffic->Checked ? 50 : 1;
        if (FIpLogClient->AnyStateOK) {
            const int Old = FIpLogClient->GetSendWaiting(0);
            if (Old > FIpLogClient->MaxSendBuffer - 1000)
                LogWin->Lines->Add("Client already waiting to send (bytes) " + IcsIntToCStr(Old));
            for (int i = 1; i <= Tot; i++) {
                bool Flag;
                if (DataUnicode->Checked)
                    Flag = FIpLogClient->SendLogLine(TimeToStr(Time()) + " Unicode Data to Server - " + UChars +
                                                     ", Serial " + IntToStr(FCSerialNr));
                else   // an AnsiString line is sent as ANSI
                    Flag = FIpLogClient->SendLogLine(AnsiString(TimeToStr(Time()) +
                                                                " Test Line of Data to Server, Serial " +
                                                                IntToStr(FCSerialNr)));
                if (Flag) {
                    FCSerialNr++;
                    LabelSendClient->Caption = "Total Lines Sent to Server " + IcsIntToCStr(FCSerialNr);
                }
            }
        }
    }
    if (DataServer->Checked) {
        if (FIpLogServer->AnyStateOK) {
            bool Flag;
            if (DataUnicode->Checked)   // Delphi puts the client's serial number in this line
                Flag = FIpLogServer->SendLogLine(TimeToStr(Time()) + " Unicode Data from Server - " + UChars +
                                                 ", Serial " + IntToStr(FSSerialNr), 0);
            else
                Flag = FIpLogServer->SendLogLine(AnsiString(TimeToStr(Time()) +
                                                            " Test Line of Data from Server, Serial " +
                                                            IcsIntToCStr(FSSerialNr)), 0);   // 0: all clients
            if (Flag) {
                LabelSendServer->Caption = "Total Lines Sent by Server " + IcsIntToCStr(FSSerialNr);
                FSSerialNr++;
            }
        }
    }
}

// Sets up the single TCP server host on Port, with SSL and automatic certificate ordering when
// ForceSsl is set, and validates it. Returns false (with the reason logged) if the server must not start.
bool TIpLogForm::SetupTcpServerHost(const String &Port, const String &HostTag)
{
    FIpLogServer->SrvIcsHosts->Clear();
    FIpLogServer->SrvIcsHosts->Add();   // only one host supported at the moment
    TIcsHost *Host = FIpLogServer->SrvIcsHosts->Items[0];
    Host->HostEnabled = true;
    Host->BindIpAddr = LocalAddr->Text;
    if (!FIpLogServer->ForceSsl) {
        Host->BindNonPort = Icsatoi(Port);
        Host->BindSslPort = 0;
    }
    else {
        Host->BindSslPort = Icsatoi(Port);
        Host->BindNonPort = 0;
    }
    Host->HostTag = HostTag;
    Host->Descr = HostTag;
    Host->CertSupplierProto = SuppProtoNone;
    if (!FIpLogServer->ForceSsl)
        return true;

    if (SslDomainName->Text == "" || SslServCert->Text == "") {
        AddLog("SSL reqires certificate file name and domain name");
        return false;
    }
    FIpLogServer->LogSslReportChain = ReportChain->Checked;
    FIpLogServer->LogSslRevocation = RevokeCheck->Checked;
    Host->SslSrvSecurity = static_cast<TSslSrvSecurity>(SslSrvSec->ItemIndex);
    Host->HostNames->Text = IcsTrim(SslDomainName->Text);
    Host->SslCert = IcsTrim(SslServCert->Text);
    Host->SslKey = IcsTrim(SslCertKey->Text);
    Host->SslPassword = IcsTrim(SslCertPassword->Text);
    Host->SslInter = IcsTrim(SslCACerts->Text);

    // following are for automatic ordering and installation of SSL certificates
    FIpLogServer->CertExpireDays = Icsatoi(SslCertExpireDays->Text);
    FIpLogServer->SrvCertAutoOrder = SslCertAutoOrder->Checked;
    if (FIpLogServer->SrvCertAutoOrder) {
        Host->CertDirWork = IcsTrim(SslCertDirWork->Text);
        if (Host->CertDirWork != "" && DirectoryExists(Host->CertDirWork)) {
            SslX509Certs->ProxyURL = ProxyURL->Text;
            FIpLogServer->SrvX509Certs = SslX509Certs;
            Host->CertSupplierProto = static_cast<TSupplierProto>(SslCertSupplierProto->ItemIndex);
            Host->CertChallenge = static_cast<TChallengeType>(SslCertChallenge->ItemIndex);
            Host->CertPKeyType = static_cast<TSslPrivKeyType>(SslCertPKeyType->ItemIndex);
            Host->CertProduct = IcsTrim(SslCertProduct->Text);
            Host->CertSignDigest = static_cast<TEvpDigest>(SslCertSignDigest->ItemIndex);
        }
        else
            AddLog("Automatic Certificate Ordering Disabled, Work Directory Not Found");
    }

    // set up binding and SSL contexts, check certificates; validate hosts and keep site certificate information
    try {
        const String ErrStr = FIpLogServer->SrvValidateHosts(false, true);   // don't stop on first error, no exceptions
        if (Host->CertValRes == chainFail) {   // don't stop on warnings
            AddLog(String("Server Not Started, Host Validation Errors:") + IcsCRLF + ErrStr);
            return false;
        }
    }
    catch (Exception &E) {
        AddLog("Server Not Started, Host Validation Failed - " + E.Message);
        return false;
    }

    // validation may have changed SSL bundle name
    if (Host->SslCert != SslServCert->Text) {
        SslServCert->Text = Host->SslCert;
        AddLog("Server changed SSL certificate file name to default: " + SslServCert->Text);
    }
    return true;
}

// Local runs both a server and a client, which send to each other.
void __fastcall TIpLogForm::doLocalClick(TObject *Sender)
{
    OpenLogFile();
    StartAppMon();
    FIpLogClient->WSDebugOptions = FIpLogClient->WSDebugOptions << wsdlogState << wsdlogMsgs;
    FIpLogServer->WSDebugOptions = FIpLogServer->WSDebugOptions << wsdlogState << wsdlogMsgs;   // Delphi starts from the client's

    // server stuff
    FIpLogServer->MaxSockets = 1;
    FIpLogServer->RawData = RawData->Checked;
    FIpLogServer->UseUtf8 = UseUtf8->Checked;
    if (Protocol->ItemIndex == ProtoUdp) {
        FIpLogServer->ForceSsl = false;
        FIpLogClient->LogProtocol = logprotUdpClient;
        FIpLogServer->LogProtocol = logprotUdpServer;
        FIpLogServer->SocFamily = static_cast<TSocketFamily>(SocketFamily->ItemIndex);
        FIpLogServer->LocalIpAddr = LocalAddr->Text;
        FIpLogServer->LocalIpPort = LocalPort->Text;
    }
    else if (Protocol->ItemIndex == ProtoTcp) {
        FIpLogClient->LogProtocol = logprotTcpClient;
        FIpLogClient->ForceSsl = UseSSL->Checked;
        FIpLogServer->LogProtocol = logprotTcpServer;
        FIpLogServer->ForceSsl = UseSSL->Checked;
        FIpLogServer->LogSslVerMethod = logSslVerNone;
        FIpLogServer->ExternalSslSessCache = SslAvlSessionCache;
        FIpLogServer->SrvTimeoutSecs = Icsatoi(SrvTimeout->Text);
        if (!SetupTcpServerHost(LocalPort->Text, "LocalServer"))
            return;
    }
    else
        return;

    // client stuff common to UDP and TCP
    FIpLogClient->MaxSockets = 1;
    FIpLogClient->SocFamily = static_cast<TSocketFamily>(SocketFamily->ItemIndex);
    if (FIpLogClient->SocFamily == sfAnyIPv6)
        FIpLogClient->RemoteHost = ICS_LOCAL_HOST_V6;
    else if (FIpLogClient->SocFamily == sfAnyIPv4)
        FIpLogClient->RemoteHost = ICS_LOCAL_HOST_V4;
    else
        FIpLogClient->RemoteHost = LocalAddr->Text;
    FIpLogClient->RemoteIpPort = LocalPort->Text;
    FIpLogClient->CheckPing = PingRemote->Checked;
    FIpLogClient->RawData = RawData->Checked;
    FIpLogClient->UseUtf8 = UseUtf8->Checked;
    FIpLogClient->RetryAttempts = StrToIntDef(CliRetryAttempts->Text, 0);

    // client SSL
    if (Protocol->ItemIndex == ProtoTcp && FIpLogClient->ForceSsl) {
        FIpLogClient->LogSslVerMethod = static_cast<TStrmVerifyMethod>(VerifyCertMode->ItemIndex);
        FIpLogClient->LogSslRevocation = RevokeCheck->Checked;
        FIpLogClient->LogSslReportChain = ReportChain->Checked;
        FIpLogClient->ExternalSslSessCache = SslAvlSessionCache;
        FIpLogClient->LogSslCliSecurity = static_cast<TSslCliSecurity>(SslCliSec->ItemIndex);
        FIpLogClient->RemoteHost = IcsTrim(SslDomainName->Text);   // SSL uses host name not IP address
    }

    // start logging, the client only if the server started
    if (FIpLogServer->StartLogging()) {
        SetButtons(true);
        FCertCheckTrigger = IcsGetTrgSecs64(5);   // first check is early to order new certificates
        DataTimerTimer(this);
        FIpLogClient->StartLogging();
    }
}

void __fastcall TIpLogForm::doClearClick(TObject *Sender)
{
    DataWin->Lines->Clear();
    LogWin->Lines->Clear();
}

void __fastcall TIpLogForm::doClientClick(TObject *Sender)
{
    OpenLogFile();
    StartAppMon();
    FIpLogClient->WSDebugOptions = FIpLogClient->WSDebugOptions + DefWSDebugDevel;
    if (Protocol->ItemIndex == ProtoUdp) {
        FIpLogClient->LogProtocol = logprotUdpClient;
        FIpLogClient->ForceSsl = false;
    }
    else if (Protocol->ItemIndex == ProtoTcp) {
        FIpLogClient->LogProtocol = logprotTcpClient;
        FIpLogClient->ForceSsl = UseSSL->Checked;
    }
    else
        return;

    if (RemoteHosts->Lines->Count == 0)
        return;
    FIpLogClient->SocFamily = static_cast<TSocketFamily>(SocketFamily->ItemIndex);
    FIpLogClient->RemoteIpPort = RemotePort->Text;
    if (RemoteHosts->Lines->Count > 1) {
        FIpLogClient->MaxSockets = RemoteHosts->Lines->Count;
        int Tot = 0;
        for (int i = 0; i < RemoteHosts->Lines->Count; i++) {
            const String Host = Trim(RemoteHosts->Lines->Strings[i]);
            if (Host != "" && Host.Pos("*") != 1) {   // ignore empty lines and lines starting with *
                FIpLogClient->SetRemotes(Tot, Host, RemotePort->Text);
                Tot++;
            }
        }
        if (FIpLogClient->MaxSockets != Tot)
            FIpLogClient->MaxSockets = Tot;
    }
    else
        FIpLogClient->RemoteHost = RemoteHosts->Lines->Strings[0];

    FIpLogClient->LocalIpAddr = LocalAddr->Text;
    FIpLogClient->CheckPing = PingRemote->Checked;
    FIpLogClient->RawData = RawData->Checked;
    FIpLogClient->UseUtf8 = UseUtf8->Checked;
    FIpLogClient->RetryAttempts = StrToIntDef(CliRetryAttempts->Text, 0);
    if (Protocol->ItemIndex == ProtoTcp && FIpLogClient->ForceSsl) {
        FIpLogClient->LogSslVerMethod = static_cast<TStrmVerifyMethod>(VerifyCertMode->ItemIndex);
        FIpLogClient->LogSslRevocation = RevokeCheck->Checked;
        FIpLogClient->LogSslReportChain = ReportChain->Checked;
        FIpLogClient->ExternalSslSessCache = SslAvlSessionCache;
        FIpLogClient->LogSslCliSecurity = static_cast<TSslCliSecurity>(SslCliSec->ItemIndex);
    }
    SetButtons(true);
    FIpLogClient->StartLogging();
    DataTimerTimer(this);
}

void __fastcall TIpLogForm::doServerClick(TObject *Sender)
{
    OpenLogFile();
    StartAppMon();
    FIpLogServer->WSDebugOptions = FIpLogServer->WSDebugOptions << wsdlogState << wsdlogMsgs;   // Delphi starts from the client's
    FIpLogServer->RawData = RawData->Checked;
    FIpLogServer->MaxSockets = Icsatoi(MaxSockets->Text);
    FIpLogServer->UseUtf8 = UseUtf8->Checked;

    if (Protocol->ItemIndex == ProtoUdp) {
        FIpLogServer->LogProtocol = logprotUdpServer;
        FIpLogServer->SocFamily = static_cast<TSocketFamily>(SocketFamily->ItemIndex);   // must set bindings
        FIpLogServer->LocalIpAddr = LocalAddr->Text;
        FIpLogServer->LocalIpPort = ServerPort->Text;
        FIpLogServer->ForceSsl = false;
    }
    else if (Protocol->ItemIndex == ProtoTcp) {
        FIpLogServer->LogProtocol = logprotTcpServer;
        FIpLogServer->ForceSsl = UseSSL->Checked;
        FIpLogServer->SocFamily = static_cast<TSocketFamily>(SocketFamily->ItemIndex);
        FIpLogServer->LocalIpAddr = LocalAddr->Text;
        FIpLogServer->LocalIpPort = ServerPort->Text;
        FIpLogServer->LogSslVerMethod = logSslVerNone;
        FIpLogServer->ExternalSslSessCache = SslAvlSessionCache;
        FIpLogServer->SrvTimeoutSecs = Icsatoi(SrvTimeout->Text);
        if (!SetupTcpServerHost(ServerPort->Text, "TCPServer"))
            return;
    }
    else
        return;

    if (FIpLogServer->StartLogging()) {
        SetButtons(true);
        DataTimerTimer(this);
        FCertCheckTrigger = IcsGetTrgSecs64(5);   // first check is early to order new certificates
    }
}

void __fastcall TIpLogForm::doStopClick(TObject *Sender)
{
    FCertCheckTrigger = Trigger64Disabled;
    DataTimer->Enabled = false;
    AddLog("Stop Clicked");

    // stop Application Monitor component, if running
    if (IcsAppMonCli->CliState == CliStateOK)
        IcsAppMonCli->Stop("Stop Monitoring");

    // report traffic
    auto reportSession = [this](TIcsIpStrmLog *IpLog, const String &Who) {
        if (!IpLog->LogActive)
            return;
        TWSocket *MySocket = IpLog->Socket[0];
        if (MySocket && MySocket->Counter && MySocket->Counter->ConnectTick > 0)
            AddLog(Who + " session lasted " + IntToStr(IcsElapsedSecs64(MySocket->Counter->ConnectTick)) +
                   " secs, Xmit " + IntToStr(MySocket->WriteCount) + ", Recv " + IntToStr(MySocket->ReadCount));
    };
    reportSession(FIpLogServer.get(), "Server");
    reportSession(FIpLogClient.get(), "Client");

    FIpLogServer->StopLogging();
    FIpLogClient->StopLogging();

    // TCP does not close instantly, wait up to five seconds until both have stopped
    const ULONGLONG EndTick = GetTickCount64() + 5000;
    while (GetTickCount64() < EndTick) {
        if (FIpLogServer->CheckStopped() && FIpLogClient->CheckStopped())
            break;
        AddLog("Waiting for Streams to Stop");
        // handle messages for 250 ms, waiting for each one instead of sleeping
        const ULONGLONG NextCheck = GetTickCount64() + 250;
        while (GetTickCount64() < NextCheck) {
            MsgWaitForMultipleObjectsEx(0, nullptr, static_cast<DWORD>(NextCheck - GetTickCount64()) + 1,
                                        QS_ALLINPUT, MWMO_INPUTAVAILABLE);
            Application->ProcessMessages();
        }
    }
    SetButtons(false);
    // ICS keeps a raw pointer to the stream it sends, so a stream is freed here only once its
    // endpoint has stopped; otherwise its state-change events or the form destructor free it.
    if (FIpLogClient->CheckStopped())
        ReleaseFileSend(FCliSend);
    if (FIpLogServer->CheckStopped())
        ReleaseFileSend(FSrvSend);
}

void __fastcall TIpLogForm::SelDirLogsClick(TObject *Sender)
{
    OpenDialog->InitialDir = DirLogs->Text;
    if (OpenDialog->Execute())
        DirLogs->Text = ExtractFilePath(OpenDialog->FileName);
}

void __fastcall TIpLogForm::SelectFileClick(TObject *Sender)
{
    OpenDialog->FileName = SendFileName->Text;
    OpenDialog->InitialDir = ExtractFileDir(SendFileName->Text);
    if (OpenDialog->Execute())
        SendFileName->Text = OpenDialog->FileName;
}

// Sends the chosen file through Sender (client or server). Each endpoint owns its own stream, freed
// once every channel that started sending it has left logstateOKStream, on a Stop that confirmed the
// endpoint stopped, or after the components are destroyed (form destructor). The Delphi
// shares one stream and frees it on any state change of either endpoint, while it may still be read.
void TIpLogForm::SendFile(TIcsIpStrmLog *Sender, TFileSend &Send, const String &Who)
{
    if (SendFileName->Text == "" || !FileExists(SendFileName->Text)) {
        AddLog("Must Specify a File Name to Send");
        return;
    }
    if (Send.Stream) {
        AddLog("Last File Still Being Sent");
        return;
    }
    try {
        // Delphi passes MAX_BUFSIZE as the third (Rights) argument, which Windows ignores.
        Send.Stream.reset(new TFileStream(SendFileName->Text, fmOpenRead | fmShareDenyWrite));
        const __int64 FileSize = Send.Stream->Size;
        // SendStream starts each channel in turn and an empty file finishes inside the call, so the
        // stream must not be freed until SendStream has handed it to every channel.
        Send.Starting = true;
        const bool Started = Sender->SendStream(Send.Stream.get());
        Send.Starting = false;
        // A channel that reached logstateOKStream holds the stream even when SendStream reports
        // failure; its state change frees the stream later.
        if (Send.Channels.empty())
            Send.Stream.reset();
        if (Started)
            AddLog(Who + " Sending File: " + SendFileName->Text + ", Size " + IcsInt64ToCStr(FileSize));
        else
            AddLog("Failed to Send File");
    }
    catch (Exception &E) {
        Send.Starting = false;
        if (Send.Channels.empty())   // as above: a channel already sending keeps the stream
            Send.Stream.reset();
        AddLog("Failed to Open Send File - " + IcsGetExceptMess(&E));
    }
}

// Tracks which channels of one endpoint are reading its stream; frees the stream when the last one
// leaves logstateOKStream (finished, failed or disconnected).
void TIpLogForm::FileSendStateChange(TFileSend &Send, int Socnr, TStrmLogState LogState)
{
    if (!Send.Stream)
        return;
    if (LogState == logstateOKStream) {
        Send.Channels.insert(Socnr);
        return;
    }
    Send.Channels.erase(Socnr);
    if (Send.Channels.empty() && !Send.Starting)
        Send.Stream.reset();
}

void TIpLogForm::ReleaseFileSend(TFileSend &Send)
{
    Send.Stream.reset();
    Send.Channels.clear();
}

void __fastcall TIpLogForm::doCliSendFileClick(TObject *Sender)
{
    SendFile(FIpLogClient.get(), FCliSend, "Client");
}

void __fastcall TIpLogForm::doSrvSendFileClick(TObject *Sender)
{
    SendFile(FIpLogServer.get(), FSrvSend, "Server");
}

void __fastcall TIpLogForm::SocketFamilyChange(TObject *Sender)
{
    if (SocketFamily->ItemIndex == static_cast<int>(sfAnyIPv4))
        LocalAddr->Text = ICS_ANY_HOST_V4;
    else if (SocketFamily->ItemIndex == static_cast<int>(sfAnyIPv6))
        LocalAddr->Text = ICS_ANY_HOST_V6;
}

void __fastcall TIpLogForm::SslX509CertsCertProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    AddLog(TimeToStr(Time()) + " " + Msg);
}

void __fastcall TIpLogForm::SslX509CertsChallengeDNS(TObject *Sender, const TChallengeItem &ChallengeItem, bool &ChlgOK)
{
    AddLog("DNS Server Not Supported Yet");   // never tested here
}

void __fastcall TIpLogForm::SslX509CertsNewCert(TObject *Sender)
{
    AddLog(String("Server ordered new SSL cerrtificate.") + IcsCRLF + SslX509Certs->GetOrderResult());

    // if server not started yet due to no certificate, start it now
    if (!FIpLogServer->LogActive) {
        AddLog("Restarting Server");
        if (FIpLogServer->StartLogging()) {
            SetButtons(true);
            DataTimerTimer(this);
        }
        return;
    }

    // server running, so we can reload automatically
    String CertErrs;
    const bool NewFlag = FIpLogServer->SrvRecheckSslCerts(CertErrs, false, true);
    if (NewFlag)
        AddLog("Server Recheck Loaded New SSL Certificate(s)");
    if (CertErrs != "")
        AddLog(String("Server Recheck SSL Certificate Errors:") + IcsCRLF + CertErrs);
}

void __fastcall TIpLogForm::SslX509CertsOAuthAuthUrl(TObject *Sender, const String URL)
{
    AddLog("Demo needs OAuth authenfication for new certificate, Browse to this URL: " + URL + ", From PC: " +
           IcsGetCompName());
}

void __fastcall TIpLogForm::LocalAddrChange(TObject *Sender)
{
    TSocketFamily SF;
    if (!WSocketIsIPEx(LocalAddr->Text, SF))
        return;
    SocketFamily->ItemIndex = static_cast<int>(SF);
}

void __fastcall TIpLogForm::IcsAppMonCliLogEvent(TObject *Sender, const String Line)
{
    AddLog(Line);
}

void __fastcall TIpLogForm::IpLogClientChangeEvent(TObject *Sender, int Socnr, TStrmLogState LogState)
{
    FileSendStateChange(FCliSend, Socnr, LogState);
    AddLog(TimeToStr(Time()) + " C[" + IntToStr(Socnr) + "] Client State: " + String(StrmLogStateNames[LogState]));
}

void __fastcall TIpLogForm::IpLogClientProgEvent(TObject *Sender, int Socnr, TLogOption LogOption, const String Msg)
{
    AddLog(TimeToStr(Time()) + " C[" + IntToStr(Socnr) + "] " + Msg);
}

void __fastcall TIpLogForm::IpLogClientRecvEvent(TObject *Sender, int Socnr, const String Line)
{
    DataWin->Lines->Add("C[" + IntToStr(Socnr) + "] " + Line);
}

void __fastcall TIpLogForm::IpLogServerChangeEvent(TObject *Sender, int Socnr, TStrmLogState LogState)
{
    FileSendStateChange(FSrvSend, Socnr, LogState);
    AddLog(TimeToStr(Time()) + " S[" + IntToStr(Socnr) + "] Server State: " + String(StrmLogStateNames[LogState]));
}

void __fastcall TIpLogForm::IpLogServerProgEvent(TObject *Sender, int Socnr, TLogOption LogOption, const String Msg)
{
    AddLog(TimeToStr(Time()) + " S[" + IntToStr(Socnr) + "] " + Msg);
}

void __fastcall TIpLogForm::IpLogServerRecvEvent(TObject *Sender, int Socnr, const String Line)
{
    DataWin->Lines->Add("S[" + IntToStr(Socnr) + "] " + Line);
}

// Called during the SSL handshake when LogSslVerMethod is logSslVerOwnEvent: the application checks the
// chain itself. Here any certificate issued by "ICS" is accepted; a real check would also verify the root,
// or compare the public key with one found earlier.
void __fastcall TIpLogForm::IpLogClientSslCertVerifyEvent(TObject *Sender, TX509List *CertChain, int &VerifyCode,
                                                          String &VerifyInfo, bool &Disconnect)
{
    AddLog("SslCertVerifyEvent: " + VerifyInfo);
    if (!CertChain)
        return;
    for (int i = 0; i < CertChain->Count; i++) {
        if (CertChain->Items[i]->IssuerCName.Pos("ICS") > 0) {
            VerifyCode = X509_V_OK;
            VerifyInfo = "ICS Certificate Matched OK";
            return;
        }
    }
    VerifyCode = X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT;
    VerifyInfo = "Failed to Verify Public Key";
}
