/*
  ICS Proxy Server - C++Builder translation of the Delphi demo OverbyteIcsProxySslServer1.pas
  (demos-delphi-vcl), ICS V9.8.

  An HTTP/HTTPS reverse and forward proxy built on TIcsHttpProxy. All of its settings (listening
  hosts, SSL certificates, remote targets, automatic certificate ordering) come from the ini file
  OverbyteIcsProxySslServer.ini, which is copied from demos-delphi-vcl into the ICS ini directory
  (usually C:\Users\(me)\AppData\Local\ICS) on first run and has to be edited there.

  How this C++ version differs from the Delphi original, apart from syntax:
  - The ini file is released on every path out of Start (Delphi never freed it).
  - The Delphi handlers that do nothing (OnDestroy, OnDataSendTar, OnDataRecvTar, OnLogHeader) are
    not converted and not bound in the .dfm.
  - The default ini file is read from the Delphi demo folder, three levels up from Win64x\<config>.
*/

#include <vcl.h>
#pragma hdrstop

#include "OverbyteIcsProxySslServer1.h"

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

#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsTicks64.hpp"

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

TProxySslServerForm *ProxySslServerForm;

namespace {

const String SectionWindow = "Window";
const String KeyTop = "Top";
const String KeyLeft = "Left";
const String KeyWidth = "Width";
const String KeyHeight = "Height";
const int MaxWinChars = 800000;

}  // namespace

__fastcall TProxySslServerForm::TProxySslServerForm(TComponent *Owner)
    : TForm(Owner), FInitialized(false), FWinDispCur(0), FLogDate(0), FCertCheckTrigger(0)
{
}

// Lines are buffered here and added to the log window once a second by the timer.
void TProxySslServerForm::Display(const String &Msg)
{
    FWinLinesBuff += Msg + IcsCRLF;
}

void __fastcall TProxySslServerForm::Timer1Timer(TObject *Sender)
{
    // Check the SSL certificates every two hours; may order expired certificates.
    if (IcsTestTrgTick64(FCertCheckTrigger)) {
        FCertCheckTrigger = IcsGetTrgMins64(120);
        try {
            // Don't stop on the first error, no exceptions.
            String S1;
            const bool NewFlag = IcsHttpProxy1->RecheckSslCerts(S1, false, true);
            if (NewFlag || !S1.IsEmpty()) {
                if (NewFlag)
                    Display("Server Recheck Loaded New SSL Certificate(s)");
                Display(String("Proxy Recheck SSL Certificate Errors:") + IcsCRLF + S1);
                ReportHosts();   // report everything again
                Display(String("Listen Bindings:") + IcsCRLF + IcsHttpProxy1->ListenStates());
            }
        }
        catch (Exception &E) {
            Display("Proxy Recheck SSL Certificate Failed - " + E.Message);
        }
    }

    // At midnight: recheck SSL certificates and log the configuration again.
    if (FLogDate != static_cast<int>(Date())) {
        FLogDate = static_cast<int>(Date());
        Display("Nightly Server Recheck Starting");
        FCertCheckTrigger = Overbyteicstypes::Trigger64Immediate;
        ReportHosts();   // report everything again
        Display(String("Listen Bindings:") + IcsCRLF + IcsHttpProxy1->ListenStates());
    }

    // 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 = "";
    }
}

void __fastcall TProxySslServerForm::FormCreate(TObject *Sender)
{
    Caption = Caption + IcsVerLitSample;   // common ICS sample caption: version and date
    FIniFileName = GetIcsIniFileName(true);
    // The OpenSSL DLLs must come from the program directory and exist.
    GSSL_DLL_DIR = ExtractFileDir(LowerCase(ParamStr(0))) + "\\";
    GSSL_SignTest_Check = true;
    GSSL_SignTest_Certificate = true;
    FLogDate = static_cast<int>(Date());
    FCertCheckTrigger = Overbyteicstypes::Trigger64Disabled;
    PostMessage(Handle, WM_STARTUP, 0, 0);
}

void __fastcall TProxySslServerForm::FormShow(TObject *Sender)
{
    if (!FInitialized) {
        FInitialized = true;
        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);
        DisplayMemo->Clear();
    }
}

void __fastcall TProxySslServerForm::IcsHttpProxy1ConfReqBody(TObject *Sender, THttpProxyClient *ProxyClient, bool &Process)
{
    Display("Event: Confirm Process Request POST Body, Length " + IntToStr(ProxyClient->HtmlReqBodyLen) +
            ", Content: " + ProxyClient->RequestContentType);
    Process = ProxyClient->CheckTextualContent(ProxyClient->RequestContentType);
}

void __fastcall TProxySslServerForm::IcsHttpProxy1HttpReqBody(TObject *Sender, THttpProxyClient *ProxyClient, String &Arg)
{
    Display("Event: Request Body, Length " + IntToStr(Arg.Length()));
}

void __fastcall TProxySslServerForm::IcsHttpProxy1ConfRespBody(TObject *Sender, THttpProxyClient *ProxyClient, bool &Process)
{
    Display("Event: Confirm Process Response Body, Length " + IntToStr(ProxyClient->HtmlRespBodyLen) +
            ", Content: " + ProxyClient->RespContentType);
    Process = ProxyClient->CheckTextualContent(ProxyClient->RespContentType);
}

void __fastcall TProxySslServerForm::IcsHttpProxy1HttpRespBody(TObject *Sender, THttpProxyClient *ProxyClient, String &Arg)
{
    Display("Event: Response Body, Length " + IntToStr(Arg.Length()));
}

void __fastcall TProxySslServerForm::IcsHttpProxy1HttpWellKnown(TObject *Sender, THttpProxyClient *ProxyClient, String &Arg)
{
    Display("Event: Well-Known File Requested: " + ProxyClient->RequestPath);
    // Ask the X509Certs component for the response to a certificate ordering challenge.
    if (ProxyClient->RequestPath.Pos("/acme-challenge/") > 1 || ProxyClient->RequestPath.Pos("/ics-validation/") > 1) {
        // Check the challenge token received from Let's Encrypt and return the key authorization.
        IcsSslX509Certs->WebSrvHttp(this, ProxyClient->RequestHost, ProxyClient->RequestPath, Arg);
        if (!Arg.IsEmpty())
            Display(ProxyClient->CPeerAddr + " - " + ProxyClient->HostTag + " acme-challenge response: " + Arg);
    }
}

void __fastcall TProxySslServerForm::IcsHttpProxy1SrcConnect(TObject *Sender, TWSocketClient *Client, System::Word Error)
{
    // Every source client can answer a TLS-ALPN certificate ordering challenge.
    TSslWSocketClient *SslClient = dynamic_cast<TSslWSocketClient *>(Client);
    if (SslClient)
        SslClient->OnClientAlpnChallg = IcsSslX509Certs->WebSrvAlpn;
}

void __fastcall TProxySslServerForm::IcsHttpProxy1HttpPxyAuth(TObject *Sender, THttpProxyClient *ProxyClient, String &Arg)
{
    // A real proxy would check Username:Password here and clear Arg to block the user.
    Display("Event: Forward Proxy Authentication Check, Usename:Password: " + Arg);
}

void __fastcall TProxySslServerForm::IcsHttpProxy1ProxyForward(TObject *Sender, THttpProxyClient *ProxyClient,
                                                               const String TarPort, bool &Allowed)
{
    Display("Event: Forward Proxy Requesting Port: " + TarPort);
    // Only ports 80 and 443 can be forwarded.
    if (TarPort != "80" && TarPort != "443")
        Allowed = false;
}

void __fastcall TProxySslServerForm::IcsHttpProxy1ProxyProg(TObject *Sender, TLogOption LogOption, const String Msg)
{
    Display(Msg);
}

void __fastcall TProxySslServerForm::IcsHttpProxy1SetTarget(TObject *Sender, TProxyClient *ProxyClient)
{
    Display("Do we want to change target??? - " + ProxyClient->TarHost);
}

// Reports a client certificate when the host asks for one (CliCertMethod in the ini file).
void __fastcall TProxySslServerForm::IcsHttpProxy1SrvSslHandshakeDone(TObject *Sender, System::Word ErrCode,
                                                                      TX509Base *PeerCert, bool &Disconnect)
{
    if (PeerCert != nullptr && PeerCert->IsCertLoaded) {
        // A real proxy would now check the common name and/or issuer, and set Disconnect to refuse the client.
        Display(String("SSL Client certificate received, should we trust client?") + IcsCRLF + PeerCert->CertMainInfo());
    }
}

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

void __fastcall TProxySslServerForm::IcsSslX509CertsChallgRefresh(TObject *Sender)
{
    TSslX509Certs *Certs = static_cast<TSslX509Certs *>(Sender);   // only wired to IcsSslX509Certs
    if (Certs->IssueState > IssStateNone)
        Display("X509 Certificate Order State Change: " + Certs->CertCommonName + " - New State: " +
                IssueStateLits[Certs->IssueState]);
}

void __fastcall TProxySslServerForm::IcsSslX509CertsNewCert(TObject *Sender)
{
    FCertCheckTrigger = Overbyteicstypes::Trigger64Immediate;   // force a certificate check to load the new ones
    Display("Trigger Recheck Certificates");
    Display(String("Proxy server ordered new SSL cerrtificate.") + IcsCRLF + IcsSslX509Certs->GetOrderResult());
}

void __fastcall TProxySslServerForm::IcsSslX509CertsOAuthAuthUrl(TObject *Sender, const String URL)
{
    // Ideally this would be emailed to the user, when run as a service.
    Display("Proxy server demo needs OAuth authenfication for new certificate. Browse to this URL: " + URL +
            ", From PC: " + IcsGetCompName());
}

void __fastcall TProxySslServerForm::RecheckCertsButtonClick(TObject *Sender)
{
    FCertCheckTrigger = Overbyteicstypes::Trigger64Immediate;
}

void __fastcall TProxySslServerForm::FormClose(TObject *Sender, TCloseAction &Action)
{
    FCertCheckTrigger = Overbyteicstypes::Trigger64Disabled;
    StopButtonClick(this);
    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->UpdateFile();
}

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

// Reports the source hosts (with their SSL certificates) and the remote targets.
void TProxySslServerForm::ReportHosts()
{
    TIcsHostCollection *Hosts = IcsHttpProxy1->IcsHosts;
    Display("Total source server hosts " + IntToStr(Hosts->Count));
    for (int J = 0; J < Hosts->Count; J++) {
        TIcsHost *Host = Hosts->Items[J];
        if (Host->CertValRes > chainOK) {
            Display("Server SSL Certificate Errors: Server: " + Host->DisplayName + IcsCRLF +
                    "SSL Certificate chain: " + IcsCRLF + Host->CertInfo + IcsCRLF + Host->CertErrs);
        }
        if (Host->CertValRes == chainFail) {
            Display("Server SSL Certificate Errors");
            return;   // as in Delphi, the hosts after this one and the targets are not reported
        }
        String S = "Source #" + IntToStr(J + 1) + " - " + Host->HostTag + ": " + Host->Descr + IcsCRLF +
                   "Host: " + Host->HostNames->Strings[0] + ", Bindings: " + Host->BindInfo + IcsCRLF;
        if (Host->BindSslPort != 0) {
            S += "SSL Security Level " +
                 GetEnumName(__delphirtti(TSslSrvSecurity), static_cast<int>(Host->SslSrvSecurity)) + IcsCRLF;
            if (Host->CliCertMethod > sslCliCertNone)
                S += "SSL Client Certificate: " +
                     GetEnumName(__delphirtti(TSslCliCertMethod), static_cast<int>(Host->CliCertMethod)) + IcsCRLF;
            S += Host->CertErrs + IcsCRLF + Host->CertInfo + IcsCRLF;
            String AutoOrder;
            if (IcsHttpProxy1->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));
                }
            }
            S += "SSL Certificate Auto Order: " + AutoOrder + IcsCRLF;
        }
        else
            S += String("No SSL") + IcsCRLF;
        if (Host->ForwardProxy)
            S += String("Forward Proxy") + IcsCRLF;
        if (Host->WebRedirectStat != 0)
            S += "Redirection to: " + Host->WebRedirectURL + IcsCRLF;
        if (!Host->WellKnownPath.IsEmpty())
            S += "Well-Known path root: " + Host->WellKnownPath + IcsCRLF;
        Display(S);
    }

    TProxyTargets *Targets = IcsHttpProxy1->ProxyTargets;
    Display("Total remote targets " + IntToStr(Targets->Count));
    for (int J = 0; J < Targets->Count; J++) {
        TProxyTarget *Target = Targets->Items[J];
        String S = "Target #" + IntToStr(J + 1) + " - " + Target->HostTag + ": " + Target->Descr + IcsCRLF +
                   "Remote " + Target->TarHost + ":" + IntToStr(Target->TarPort);
        if (Target->TarSsl)
            S += " with SSL";
        if (!Target->SrcPath.IsEmpty())
            S += ", Path " + Target->SrcPath;
        S += IcsCRLF;
        Display(S);
    }
}

void __fastcall TProxySslServerForm::StartButtonClick(TObject *Sender)
{
    try {
        // 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
                }
            }
        }
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FIniFileName, true));
        Display("INI file: " + FIniFileName);

        // Tell them who we are.
        IcsSslRootCAStore->Initialise();   // if OpenSSL and the internal root store are not loaded, do it
        IcsSslLoadProviders(true, false);  // this sample needs the OpenSSL legacy provider
        Display(Overbyteicsproxy::CopyRight + IcsCRLF + IcsReportOpenSSLVer(true));

        // Main proxy settings from the ini file, the built-in CA store if no file is found.
        IcsLoadTIcsHttpProxyFromIni(IniFile.get(), IcsHttpProxy1, "Proxy");

        // Ordering X509 certificates may need a proxy server.
        IcsSslX509Certs->ProxyURL = IniFile->ReadString("Proxy", "X509ProxyURL", "");

        // Read the source server hosts from the ini file and check their SSL files exist.
        IcsLoadIcsHostsFromIni(IniFile.get(), IcsHttpProxy1->IcsHosts, "Source", IcsHttpProxy1->SourceServer);
        if (IcsHttpProxy1->IcsHosts->Count <= 0) {
            Display("Can Not Start Server - No Source Server Hosts Configured");
            return;
        }

        // Read the remote proxy targets.
        IcsLoadProxyTargetsFromIni(IniFile.get(), IcsHttpProxy1->ProxyTargets, "Target");
        if (IcsHttpProxy1->ProxyTargets->Count <= 0) {
            Display("Can Not Start Server - No Proxy Targets Configured");
            return;
        }

        // Validate the hosts and keep the site certificate information.
        try {
            const String S = IcsHttpProxy1->ValidateHosts(false, true);   // don't stop on the first error, no exceptions
            if (!S.IsEmpty())
                Display(String("Proxy Validation Errors:") + IcsCRLF + S);
            ReportHosts();
            Display(String("Required Listen Bindings:") + IcsCRLF + IcsHttpProxy1->ListenStates());
        }
        catch (Exception &E) {
            Display("Host Validation Failed, Server Stopped - " + E.Message);
            return;
        }
        IcsHttpProxy1->Start();
        if (!IcsHttpProxy1->ListenAllOK())
            Display(String("Failed to Start, Listen Bindings:") + IcsCRLF + IcsHttpProxy1->ListenStates());
        FCertCheckTrigger = IcsGetTrgSecs64(15);   // the first check is early, to order new certificates
        StartButton->Enabled = false;
        StopButton->Enabled = true;
    }
    catch (Exception &E) {
        Display("Failed to start proxy - " + E.Message);
    }
}

void __fastcall TProxySslServerForm::StopButtonClick(TObject *Sender)
{
    if (!StopButton->Enabled)
        return;
    FCertCheckTrigger = Overbyteicstypes::Trigger64Disabled;
    StartButton->Enabled = true;
    StopButton->Enabled = false;
    IcsHttpProxy1->Stop();
    Display("Server stopped");
}
