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

  The URL handlers (dynamic pages) of the OverbyteIcsSslMultiWebServ demo: login and
  session pages, a hit counter image, configuration form, HEAD request test, form and
  file uploads, an email form sent through TIcsMailQueue, a parameter display page and
  an IP address lookup page. Most pages are templates from the server's TemplateDir.

  How this C++ version differs from the Delphi original, apart from syntax:
  - TSslMultiWebDataModule is a plain C++ class instead of a TComponent created in the
    unit's initialization section and never freed: the server form owns the instance
    and points SslMultiWebDataModule at it. Its IcsGeoTools pointer is the server
    form's TIcsGeoTools, set when the server starts.
  - The USE_IcsGeoTools blocks are always compiled: TIcsGeoTools (IcsGeoUtils) is in the
    ICS C++ Common package.
  - Components the handlers create (socket, timer, HTTP client, decoders, streams) have
    no Owner and are held in std::unique_ptr.
  - AnswerPage's UserData is passed as a typed null: a bare nullptr is ambiguous between
    its TObject* (template file) and PWideChar (resource type) overloads.
  - Delphi demo bugs are fixed here; each fix has a one-line comment.
*/

#include <vcl.h>
#pragma hdrstop

#include <Vcl.Graphics.hpp>
#include <Vcl.Imaging.jpeg.hpp>
#include <algorithm>

#include "OverbyteIcsSslMultiWebPages.h"
#include "OverbyteIcsIniFiles.hpp"
#include "OverbyteIcsUtils.hpp"
#include "OverbyteIcsTicks64.hpp"
#include "OverbyteIcsFormDataDecoder.hpp"
#include "OverbyteIcsUrl.hpp"
#include "OverbyteIcsCharsetUtils.hpp"
#include "OverbyteIcsMD5.hpp"
#include "OverbyteIcsHtmlUtils.hpp"
#include "OverbyteIcsSmtpProt.hpp"

#pragma package(smart_init)

const wchar_t *const UrlLogin                   = L"/login/loginform.html";
const wchar_t *const UrlDoLoginSecure           = L"/DoLoginSecure.Html";
const wchar_t *const UrlHomePage                = L"/HomePage.html";
const wchar_t *const UrlCounter                 = L"/Counter.jpg";
const wchar_t *const UrlConfigForm              = L"/ConfigForm.html";
const wchar_t *const UrlDoConfigHtml            = L"/DoConfig.html";
const wchar_t *const UrlConfigLogoPng           = L"/ConfigLogo.png";
const wchar_t *const UrlDoConfigConfirmSaveHtml = L"/ConfigConfirmSave.html";
const wchar_t *const UrlCounterViewHtml         = L"/CounterView.html";
const wchar_t *const UrlJavascriptErrorHtml     = L"/JavascriptError.html";
const wchar_t *const UrlAjaxFetchCounter        = L"/Ajax/FetchCounter";
const wchar_t *const UrlHeadForm                = L"/HeadForm.html";
const wchar_t *const UrlIpAddrLookup            = L"/IpAddrLookup.html";
const wchar_t *const UrlPostinfo                = L"/postinfo.html";

TSslMultiWebDataModule *SslMultiWebDataModule = nullptr;

namespace {

const wchar_t *const CounterSection = L"Counter";
const wchar_t *const SectionConfig  = L"Config";
const wchar_t *const KeyPort        = L"Port";
const wchar_t *const DftPort        = L"20105";

/* The email host is deliberately hardcoded in this form, to prevent it being supplied
   as a form parameter, which is how spammers abuse email forms. The account may be
   passed as a parameter with the form, but sending the form will fail unless it's
   another valid account at ftptest.org. */
const wchar_t *const DefaultEmailDomain  = L"@ftptest.org";
const wchar_t *const DefaultEmailAccount = L"testing";

const wchar_t *const PleaseSelect = L"Please select";

// Typed null for AnswerPage's UserData, see the file header.
TObject *const NoUserData = nullptr;

// True when the first L characters of S are digits (fewer if S is shorter).
bool IsNumeric(const String &S, int L)
{
    for (int I = 1; I <= L && I <= S.Length(); I++) {
        if (S[I] < L'0' || S[I] > L'9')
            return false;
    }
    return true;
}

String GetTempLastMod(THttpAppSrvConnection *Client, const String &FName)
{
    TDateTime FileDT;
    __int64 FSize;
    if (IcsGetUAgeSizeFile(Client->TemplateDir + "/" + FName, FileDT, FSize))
        return FormatDateTime("dd mmm yyyy", FileDT);
    return "Page not found";
}

// Does a string contain any common HTML tags; spammers use HTML in email bodies.
bool IsHtmlTags(const String &S)
{
    const String S2 = LowerCase(S);
    for (const wchar_t *Tag : {L"<a", L"</a", L"href", L"<img", L"[/url]"}) {
        if (S2.Pos(Tag) > 0)
            return true;
    }
    return false;
}

bool IsValidEmail(const String &Value)
{
    // Only letters, digits, '_', '-' and '.' are accepted in either part.
    auto CheckAllowed = [](const String &S) {
        for (int I = 1; I <= S.Length(); I++) {
            const wchar_t C = S[I];
            if (!((C >= L'a' && C <= L'z') || (C >= L'A' && C <= L'Z') ||
                  (C >= L'0' && C <= L'9') || C == L'_' || C == L'-' || C == L'.'))
                return false;
        }
        return true;
    };

    const int At = Value.Pos("@");
    if (At == 0)
        return false;
    const String NamePart = Value.SubString(1, At - 1);
    const String ServerPart = Value.SubString(At + 1, MaxInt);
    if (NamePart.IsEmpty() || ServerPart.Length() < 5)
        return false;
    const int Dot = ServerPart.Pos(".");
    if (Dot == 0 || Dot > ServerPart.Length() - 2)
        return false;
    return CheckAllowed(NamePart) && CheckAllowed(ServerPart);
}

bool HasPathChars(const String &FileName)
{
    return FileName.Pos("/") > 0 || FileName.Pos("\\") > 0 || FileName.Pos(":") > 0;
}

// True when S is usable as one file or directory name: no path delimiters, drive separator,
// wildcards or control characters, and no trailing '.' or space (so not "." or "..").
bool IsPlainFileName(const String &S)
{
    if (S.IsEmpty() || S[S.Length()] == L'.' || S[S.Length()] == L' ')
        return false;
    for (int I = 1; I <= S.Length(); I++) {
        const wchar_t C = S[I];
        if (C < L' ' || wcschr(L"<>:\"/\\|?*", C) != nullptr)
            return false;
    }
    return true;
}

// True when S can be written as an INI value: no line breaks or other control characters.
bool IsIniValueSafe(const String &S)
{
    for (int I = 1; I <= S.Length(); I++)
        if (S[I] < L' ')
            return false;
    return true;
}

// True when a counter name can be an INI key: no line breaks or other control
// characters, and none of the '=', '[' or ']' that the INI format gives a meaning.
bool IsValidCounterName(const String &S)
{
    for (int I = 1; I <= S.Length(); I++) {
        const wchar_t C = S[I];
        if (C < L' ' || C == L'=' || C == L'[' || C == L']')
            return false;
    }
    return true;
}

} // namespace

/* ---- TSslMultiWebDataModule: settings shared between the form and the pages ---- */

int TSslMultiWebDataModule::CounterValue(const String &CounterName, int DefaultValue) const
{
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FCounterFileName, true));
    return IniFile->ReadInteger(CounterSection, CounterName, DefaultValue);
}

int TSslMultiWebDataModule::CounterIncrement(const String &CounterRef) const
{
    // The ini file is created if it doesn't exist.
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(FCounterFileName, true));
    const int Result = IniFile->ReadInteger(CounterSection, CounterRef, 0) + 1;
    IniFile->WriteInteger(CounterSection, CounterRef, Result);
    IniFile->UpdateFile();
    return Result;
}

void TSslMultiWebDataModule::Display(const String &Msg) const
{
    if (OnDisplay)
        OnDisplay(nullptr, Msg);   // Delphi passes the data module as Sender; it is no TObject here
}

void TSslMultiWebDataModule::LoadConfig()
{
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(IniFileName, true));
    Port = IniFile->ReadString(SectionConfig, KeyPort, DftPort);
}

void TSslMultiWebDataModule::SaveConfig() const
{
    std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(IniFileName, true));
    IniFile->WriteString(SectionConfig, KeyPort, Port);
    IniFile->UpdateFile();
}

void TSslMultiWebDataModule::SetDataDir(const String &Value)
{
    FDataDir = IncludeTrailingPathDelimiter(Value);
    FCounterFileName = FDataDir + "Counters.ini";   // Delphi adds a second PathDelim here
}

/* ---- Session data, saved to and loaded from the sessions file by name ---- */

__fastcall TAppSrvSessionData::TAppSrvSessionData(TComponent *AOwner)
    : TWebSessionData(AOwner)
{
}

/* ---- Base class for pages that need a logged-in session ---- */

// Answers the "not logged" page when there is no valid session; the handler is then finished.
bool TUrlHandlerBase::NotLogged()
{
    const bool Result = !ValidateSession();
    if (Result) {
        AnswerPage("", NO_CACHE, "NotLogged.html", NoUserData,
                   ARRAYOFCONST((L"LOGIN", UrlLogin)));
        Finish();
    }
    return Result;
}

TAppSrvSessionData *TUrlHandlerBase::SessionData()
{
    return WSession ? dynamic_cast<TAppSrvSessionData *>(WSession->SessionData) : nullptr;
}

void TUrlHandlerBase::Relocate(const String &Location)
{
    AnswerPage("302 moved", "Location: " + Location + "\r\n" + NO_CACHE, "Moved.html",
               NoUserData, ARRAYOFCONST((L"LOCATION", Location)));
}

/* ---- Helpers for the temporary configuration directories ---- */

// Deletes a directory, all the files it contains and all its subdirectories.
void ForceRemoveDir(const String &Dir)
{
    TSearchRec F;
    int Status = FindFirst(IncludeTrailingPathDelimiter(Dir) + "*.*", faAnyFile, F);
    while (Status == 0) {
        if ((F.Attr & faDirectory) != 0) {
            if (F.Name != "." && F.Name != "..")
                ForceRemoveDir(Dir + PathDelim + F.Name);
        }
        else
            DeleteFile(Dir + PathDelim + F.Name);
        Status = FindNext(F);
    }
    FindClose(F);
    RemoveDir(Dir);
}

// Removes the subdirectories of Dir whose name starts with a YYYYMMDDHHNNSS time stamp
// that is now in the past.
void CleanupTimeStampedDir(const String &Dir)
{
    // Delphi scans the drive root ("\") when Dir is empty, which the housekeeping timer
    // passes if the server stopped starting before it set the data folder.
    if (Dir.IsEmpty())
        return;
    const String TimeStamp = FormatDateTime("YYYYMMDDHHNNSS", Now());
    TSearchRec F;
    int Status = FindFirst(IncludeTrailingPathDelimiter(Dir) + "*.*", faAnyFile, F);
    while (Status == 0) {
        if ((F.Attr & faDirectory) != 0 && F.Name != "." && F.Name != ".." &&
            F.Name.Length() >= 14 && IsNumeric(F.Name, 14) &&
            F.Name.SubString(1, 14) <= TimeStamp)
            ForceRemoveDir(Dir + PathDelim + F.Name);
        Status = FindNext(F);
    }
    FindClose(F);
}

/* ---- /mailer.html: email form, sent through the server's mail queue ---- */

void __fastcall TUrlHandlerMailer::Execute()
{
    FWSocket.reset(new TWSocket(nullptr));
    FWSocket->OnDnsLookupDone = DoneDnsLookup;
    FWSocket->OnBgException = HandleBackgroundExceptions;
    FAbortTimer.reset(new TIcsTimer(nullptr));   // Delphi makes the socket the timer's Owner
    FAbortTimer->OnTimer = TimerAbortTimer;
    FAbortTimer->Interval = 5000;                // five second timeout for DNS
    try {
        // Get the user's IP address and look up the host name.
        sIPAddr = Client->GetPeerAddr();
        sUserIPHost = "";
        FWSocket->ReverseDnsLookup(sIPAddr);
        FAbortTimer->Enabled = true;
    }
    catch (Exception &E) {
        Display("Exception Looking up DNS - " + IcsGetExceptMess(&E));
        DoneDnsLookup(this, 999);   // continue to use the form
    }
}

void __fastcall TUrlHandlerMailer::HandleBackgroundExceptions(TObject *Sender, Exception *E, bool &CanClose)
{
    Display("Exception processing page - " + E->ClassName() + ": " + E->Message);
    CanClose = true;
}

__fastcall TUrlHandlerMailer::~TUrlHandlerMailer()
{
    if (FAbortTimer)
        FAbortTimer->Enabled = false;
}

void __fastcall TUrlHandlerMailer::TimerAbortTimer(TObject *Sender)
{
    FAbortTimer->Enabled = false;
    Display("DNS Lookup Timed Out");
    FWSocket->CancelDnsLookup();
}

void __fastcall TUrlHandlerMailer::DoneDnsLookup(TObject *Sender, System::Word Error)
{
    FAbortTimer->Enabled = false;
    if (Error == 0) {
        // Delphi casts Sender to TWSocket, which raises when Execute passes Self after an exception.
        String sTemp;
        if (FWSocket->DnsResultList->Count != 0) {
            for (int I = 0; I < FWSocket->DnsResultList->Count; I++) {
                if (I != 0)
                    sTemp += ", ";
                sTemp += FWSocket->DnsResultList->Strings[I];
            }
        }
        else
            sTemp = FWSocket->DnsResult;
        sUserIPHost = sTemp + " (" + sIPAddr + ")";
    }
    else {
        Display("DNS Lookup Failed - " + WSocketErrorDesc(Error));
        sUserIPHost = sIPAddr;
    }
    // The POST URL uses the same protocol as the original page.
    sPageUrl = Client->RequestProtocol + "://" + Client->RequestHost + Client->Path;
    errorMsg = "";

    // A GET may pass the email account (no domain) as the query.
    if (Client->Method == "GET")
        sMailTo = Params;

    // The page POSTs to itself to send the email.
    if (Client->Method == "POST") {
        const String Posted = Client->PostedDataStr;
        ExtractURLEncodedValue(Posted, "MailZBody", sMailBody);
        if (IsHtmlTags(sMailBody)) {
            errorMsg = "Please specify valid content";   // spammers use HTML tags in the body
            Display("Email validation error: " + errorMsg + " - " + sMailBody);
        }
        else if (sMailBody.Length() < 40) {
            errorMsg = "Please specify your full message";
            Display("Email validation error: " + errorMsg + " - " + sMailBody);
        }
        ExtractURLEncodedValue(Posted, "MailZFrom", sMailFrom);
        ExtractURLEncodedValue(Posted, "MailZName", sMailName);
        ExtractURLEncodedValue(Posted, "MailZTo", sMailTo);
        if (sMailTo.Length() < 4)
            sMailTo = DefaultEmailAccount;   // no account passed, use the default
        if (!IsValidEmail(sMailFrom)) {
            errorMsg = "Please specify a valid email address";
            Display("Email validation error: " + errorMsg + " - " + sMailFrom);
        }
        // Stops spammers POSTing this page without a GET first: they must know the IP and host name.
        String sTemp;
        ExtractURLEncodedValue(Posted, "MailZIp", sTemp);
        if (sUserIPHost != sTemp) {
            errorMsg = "Please specify your message again, internal error";
            Display("Email validation error: " + errorMsg + " - IP Address " + sTemp);
        }
        if (sMailName.Length() < 5)
            errorMsg = "Please specify your full name";
        else if (!IsUsAscii(sMailName))
            errorMsg = "Your full name can not contain punctuation";

        TIcsMailQueue *MailQueue = SslMultiWebDataModule->IcsMailQueue;
        if (errorMsg.IsEmpty()) {
            if (!MailQueue->Active && !MailQueue->MailQuDir.IsEmpty() &&
                MailQueue->MailServers->Count > 0)
                MailQueue->Active = true;
            if (!MailQueue->Active)
                errorMsg = "Sorry, unable to send email at present, please try later";
        }

        // Build the form email and send it to the hardcoded host.
        if (errorMsg.IsEmpty()) {
            try {
                const String sMagEmail = sMailTo + DefaultEmailDomain;
                const String sTempFrom = "\"" + sMailName + "\" <" + sMailFrom + ">";
                THtmlSmtpCli *Smtp = MailQueue->QuHtmlSmtp;
                Smtp->PlainText->Clear();
                Smtp->PlainText->Text = RemoveHtmlSpecialChars(sMailBody) + "\r\n\r\n" +
                    "User Address: " + sUserIPHost + "\r\n" +
                    "Email Sent from Web Site Response Form: " + sPageUrl + "\r\n";
                if (!Client->AuthUserName.IsEmpty())
                    Smtp->PlainText->Add("User Account: " + Client->AuthUserName);
                Display("Sending Email Form to " + sMagEmail + " from " + sTempFrom);
                Smtp->RcptName->Clear();
                Smtp->RcptName->Add(sMagEmail);
                Smtp->RcptName->Add(sMailFrom);
                Smtp->FromName = sMailFrom;
                Smtp->HdrTo = sMagEmail;
                Smtp->HdrFrom = sTempFrom;
                Smtp->HdrReplyTo = sTempFrom;
                Smtp->HdrCc = sTempFrom;
                Smtp->HdrSubject = "ICS Demo Web Site Email Form - " + sMailTo;
                if (MailQueue->QueueMail() > 0) {
                    Display("Email Form Queued OK");
                    AnswerPage("", NO_CACHE, "maildone.html", NoUserData, nullptr, -1);
                    Finish();
                    return;
                }
                errorMsg = "Failed to Queue Email Form: " + Smtp->ErrorMessage;
                Display(errorMsg);
            }
            catch (Exception &E) {
                errorMsg = "Failed to Start Sending Email Form: " + IcsGetExceptMess(&E);
                Display(errorMsg);
            }
        }
    }

    AnswerPage("", NO_CACHE, "mailer.html", NoUserData,
               ARRAYOFCONST((L"PageLastMod", GetTempLastMod(Client, "mailer.html"),
                             L"sPageUrl", sPageUrl, L"sMailTo", sMailTo, L"sMailName", sMailName,
                             L"sUserIPHost", sUserIPHost, L"sMailFrom", sMailFrom,
                             L"sMailBody", sMailBody, L"errorMsg", errorMsg)));
    Finish();
}

/* ---- Login pages ---- */

void __fastcall TUrlHandlerLoginFormHtml::Execute()
{
    TAppSrvSessionData *MySessionData;
    String Headers;
    if (!ValidateSession()) {
        // The session takes ownership of its data.
        MySessionData = new TAppSrvSessionData(nullptr);
        MySessionData->AssignName();
        Headers = NO_CACHE + CreateSession("", 0, MySessionData);
    }
    else {
        MySessionData = SessionData();
        Headers = NO_CACHE;
    }

    MySessionData->LastRequest = Now();
    MySessionData->RequestCount = MySessionData->RequestCount + 1;
    MySessionData->LoginChallenge = StrMD5(IntToHex(static_cast<int>(GetTickCount()), 8));
    AnswerPage("", Headers, "LoginForm.html", NoUserData,
               ARRAYOFCONST((L"Challenge", MySessionData->LoginChallenge,
                             L"DoLoginSecure", UrlDoLoginSecure,
                             L"COUNTER", UrlCounter)));
    Finish();
}

void __fastcall TUrlHandlerDoLoginSecureHtml::Execute()
{
    if (NotLogged())
        return;

    const String Challenge = SessionData()->LoginChallenge;
    String PasswordHash, UserCode;
    ExtractURLEncodedValue(Params, "PasswordHash", PasswordHash);
    ExtractURLEncodedValue(Params, "UserCode", UserCode);
    // This demo uses a hardcoded password. A real application would look up the user
    // code, password and permissions in a database.
    const String Password = "admin";

    if (UserCode.IsEmpty() ||
        !SameText(PasswordHash, StrMD5(Challenge + Trim(UpperCase(Password))))) {
        SslMultiWebDataModule->CounterIncrement("LoginInvalid");
        DeleteSession();
        NotLogged();
        Display("Invalid password for " + UserCode);
        return;
    }

    SslMultiWebDataModule->CounterIncrement("LoginOK");
    SessionData()->LogonTime = Now();
    SessionData()->UserCode = UserCode;
    Display("Login OK for " + UserCode);
    Relocate(UrlHomePage);
    Finish();
}

void __fastcall TUrlHandlerDefaultDoc::Execute()
{
    if (NotLogged())
        return;
    Relocate(UrlHomePage);
    Finish();   // missing in Delphi, so every /appindex.html request left its handler alive
}

void __fastcall TUrlHandlerHomePageHtml::Execute()
{
    if (NotLogged())
        return;
    AnswerPage("", NO_CACHE, "HomePage.html", NoUserData,
               ARRAYOFCONST((L"LOGIN", UrlLogin,
                             L"COUNTER", UrlCounter,
                             L"CONFIG", UrlConfigForm,
                             L"COUNTERVIEW", UrlCounterViewHtml,
                             L"USERCODE", SessionData()->UserCode,
                             L"LOGINTIME", DateToStr(SessionData()->LogonTime))));
    Finish();
}

void __fastcall TUrlHandlerJavascriptErrorHtml::Execute()
{
    AnswerPage("", "", "JavascriptError.html", NoUserData,
               ARRAYOFCONST((L"COUNTER", UrlCounter)));
    Finish();
}

/* ---- /cgi-bin/FormHandler: appends the posted names to FormHandler.txt ---- */

void __fastcall TUrlHandlerUploadData::Execute()
{
    if (Client->Method != "POST")
        return;

    String FirstName, LastName;
    ExtractURLEncodedValue(Client->PostedDataStr, "FirstName", FirstName);
    ExtractURLEncodedValue(Client->PostedDataStr, "LastName", LastName);
    // 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 = IncludeTrailingPathDelimiter(SslMultiWebDataModule->UploadDir) + "FormHandler.txt";
    try {
        // unique_ptr closes the file if a write raises (Delphi: Destroy with no finally).
        std::unique_ptr<TFileStream> Stream;
        bool IsUtf8;
        Byte Bom[3];
        if (FileExists(FileName)) {
            Stream.reset(new TFileStream(FileName, fmOpenReadWrite));
            const int Len = Stream->Read(Bom, static_cast<int>(sizeof(Bom)));
            IsUtf8 = Len == 3 && Bom[0] == 0xEF && Bom[1] == 0xBB && Bom[2] == 0xBF;
            Stream->Seek(static_cast<__int64>(0), soEnd);
        }
        else {
            // New data files are UTF-8.
            Stream.reset(new TFileStream(FileName, fmCreate));
            IsUtf8 = true;
            Bom[0] = 0xEF; Bom[1] = 0xBB; Bom[2] = 0xBF;
            Stream->Write(Bom, static_cast<int>(sizeof(Bom)));
        }
        if (IsUtf8) {
            const RawByteString Utf8Str = StringToUtf8(Buf);
            Stream->Write(Utf8Str.c_str(), Utf8Str.Length());
        }
        else
            StreamWriteStrA(Stream.get(), Buf);
    }
    catch (Exception &E) {
        SslMultiWebDataModule->Display("Exception Saving Posted Data - " + E.Message);
    }

    // A real application would validate the data here and answer accordingly; this demo
    // always gives the same answer.
    AnswerString("", "", "",
        "<HTML><HEAD><TITLE>ICS WebServer Form Demo</TITLE></HEAD>\r\n"
        "<BODY><H2>Your data has been recorded:</H2>\r\n"
        "<P>" + TextToHtmlText(FirstName) + "." + TextToHtmlText(LastName) + "@" +
        TextToHtmlText(HostName) + "</P>" +
        "<P>Filename: " + TextToHtmlText(FileName) + "</P>" +
        "<A HREF=\"/form.html\">More data entry</A><BR>" +
        "<A HREF=\"/demo.html\">Back to demo menu</A><BR>" +
        "</BODY></HTML>");
    Finish();
}

/* ---- /uploadfile.html: upload a file with a form (multipart) or a simple POST/PUT ---- */

void __fastcall TUrlHandlerUploadFile::Execute()
{
    THttpAppSrvConnection *RemoteClient = Client;
    String sinfo1, sFileName, sfiletitle, newfilename;
    __int64 UploadTotTicks = IcsElapsedTicks64(RemoteClient->RequestStartTick) + 1;
    const __int64 MaxUploadSize = RemoteClient->AppServer->MaxUploadMB * IcsMBYTE;
    const String sPageUrl = Client->RequestProtocol + "://" + Client->RequestHost + Client->Path;

    // Builds the success report: new name, size, duration and speed.
    auto Logit = [&](__int64 FSize) {
        sinfo1 = "Saved Uploaded File OK\r\nNew File Name: " + newfilename +
                 "\r\nFile Size: " + IntToKbyte(FSize);
        const String Duration = UploadTotTicks >= 1000
            ? FloatToStrF(UploadTotTicks / 1000.0, ffFixed, 15, 2) + " secs"
            : IntToStr(UploadTotTicks) + " msecs";
        __int64 Speed = 0;
        // No speed for less than five seconds.
        if (UploadTotTicks > 5000 && UploadTotTicks < 60 * 60 * 1000 && FSize > 1000) {
            if (UploadTotTicks > 100000 && FSize > 1000000)
                Speed = FSize / (UploadTotTicks / 1000);
            else
                Speed = (FSize * 1000) / UploadTotTicks;
        }
        sinfo1 += "\r\nUpload Duration: " + Duration;
        if (Speed > 0)
            sinfo1 += "\r\nSpeed: " + IntToStr(Speed) + " chars/sec";
    };

    if (RemoteClient->Method == "GET" && !Params.IsEmpty()) {   // not really used
        ExtractURLEncodedValue(Params, "FileName", sFileName);
        ExtractURLEncodedValue(Params, "FileTitle", sfiletitle);
    }

    // The page POSTs (or PUTs) to itself to upload a file.
    if (RemoteClient->Method == "POST" || RemoteClient->Method == "PUT") {
        const String sContent = LowerCase(RemoteClient->RequestContentType);
        Display("Received Post/PUT Data File, Size " + IntToKbyte(RemoteClient->PostedDataLen) +
                ", Content Type: " + sContent);
        if (!RemoteClient->PostTempName.IsEmpty())
            Display("Temporary File Name: " + RemoteClient->PostTempName);

        if (RemoteClient->PostedDataLen > MaxUploadSize)
            sinfo1 = "Upload File (" + IntToKbyte(RemoteClient->PostedDataLen) + ") Exceeds Maximum Size";
        else {
            // First tell the component that all the data has arrived.
            RemoteClient->PostedDataReceived();
            try {
                if (sContent.Pos("multipart/form-data") > 0) {
                    std::unique_ptr<TFormDataAnalyser> Decoder(new TFormDataAnalyser(nullptr));
                    Decoder->DecodeStream(RemoteClient->PostedDataStream);
                    Display(Decoder->DecodeInfo);

                    // Extract the file, with a minimal validity check.
                    TFormDataItem *Field = Decoder->Part("FileName");
                    if (!Field)
                        sinfo1 = "Upload Form Error, Missing FileName Tag";
                    else {
                        sFileName = ExtractFileName(Field->ContentFileName());
                        if (sFileName.IsEmpty())
                            sinfo1 = "Upload Form Error, Empty FileName, ContentFileName: " + Field->ContentFileName();
                        else if (HasPathChars(sFileName))   // a zero length file is allowed
                            sinfo1 = "Illegal Upload File Name: " + sFileName;
                        else {
                            try {
                                // New file name with date and time.
                                newfilename = IncludeTrailingPathDelimiter(SslMultiWebDataModule->UploadDir) +
                                              FormatDateTime("yyyymmdd\"-\"hhnnss", Now()) + "_" + sFileName;
                                Display("Saving MIME Upload File as " + newfilename);
                                Field->SaveToFile(newfilename);
                                Logit(IcsGetFileSize(newfilename));
                            }
                            catch (Exception &E) {
                                sinfo1 = "Failed to Save MIME Uploaded File as " + newfilename + " - " + E.Message;
                            }
                        }
                    }
                    sfiletitle = Decoder->PartData(String("FileTitle"), CP_ACP, true, true);
                }

                // Simple POST/PUT binary upload: no form parameters, only the URL and headers.
                else if (sContent.Pos("application") > 0 || sContent.Pos("audio") > 0 ||
                         sContent.Pos("image") > 0) {
                    // Look for the non-standard Content-Disposition request header (sent by TSslHttpRest).
                    sFileName = "";
                    TStringList *Hdrs = RemoteClient->RequestHeader;
                    for (int I = 0; I < Hdrs->Count; I++) {
                        if (Hdrs->Strings[I].Pos("Content-Disposition:") == 1) {
                            Display("Custom Header Found - " + Hdrs->Strings[I]);
                            // filename*=UTF-8''file%20name.jpg is RFC5987 encoded
                            sFileName = IcsDecHttp2Params(Hdrs->Strings[I], "filename");
                        }
                    }
                    if (!sFileName.IsEmpty())
                        Display("Upload file name from Requst Header: " + sFileName);

                    // Then for the file name in the URL parameters.
                    if (!Params.IsEmpty()) {
                        ExtractURLEncodedValue(Params, "FileName", sFileName);
                        ExtractURLEncodedValue(Params, "FileTitle", sfiletitle);
                    }
                    if (sFileName.IsEmpty())
                        sinfo1 = "No Valid Upload Parameters, Upload Failed";
                    else if (HasPathChars(sFileName))
                        sinfo1 = "Illegal Upload File Name: " + sFileName;
                    else {
                        // Date and time in the name, since the same file may be uploaded repeatedly.
                        newfilename = IncludeTrailingPathDelimiter(SslMultiWebDataModule->UploadDir) +
                                      FormatDateTime("yyyymmdd\"-\"hhnnss", Now()) + "_" + sFileName;
                        Display("Saving Simple Upload File as " + newfilename);

                        // A large upload is already in a temporary file: rename it instead of copying.
                        if (!RemoteClient->PostTempName.IsEmpty() && FileExists(RemoteClient->PostTempName)) {
                            delete RemoteClient->PostedDataStream;
                            RemoteClient->PostedDataStream = nullptr;
                            if (IcsRenameFile(RemoteClient->PostTempName, newfilename, false, false) != 0)
                                Display("Failed to rename temporary file - " + RemoteClient->PostTempName);
                            else
                                RemoteClient->PostTempName = "";   // Delphi clears it after a failed rename too, so ICS never deleted the temp file
                        }
                        if (RemoteClient->PostedDataStream) {
                            try {
                                std::unique_ptr<TFileStream> FileStream(new TFileStream(newfilename, fmCreate));
                                FileStream->CopyFrom(RemoteClient->PostedDataStream, 0);
                            }
                            catch (Exception &E) {
                                sinfo1 = "Failed to Save Uploaded File as " + newfilename + " - " + E.Message;
                            }
                        }
                        if (FileExists(newfilename))
                            Logit(IcsGetFileSize(newfilename));
                    }
                }
                else
                    sinfo1 = "Unknown Post Data Content: " + RemoteClient->RequestContentType;
            }
            catch (Exception &E) {
                Display("Exception Saving Posted Data - " + E.Message);
            }
            sinfo1 += "\r\nUpload FileName: " + sFileName + "\r\n" +
                      "FileTitle: " + sfiletitle + "\r\n" +
                      "Post URL: " + sPageUrl + "\r\n" +
                      "From IP Address: " + RemoteClient->CPeerAddr + "\r\n" +
                      IcsVerLitRelDate + "\r\n";
        }
    }
    Display(sinfo1);
    sinfo1 = StringReplace(sinfo1, "\r\n", "<br>", TReplaceFlags() << rfReplaceAll);
    AnswerPage("", "", "uploadfile.html", NoUserData,
               ARRAYOFCONST((L"sinfo1", sinfo1, L"sPageUrl", sPageUrl,
                             L"sMaxFileSize", IntToKbyte(MaxUploadSize, true),
                             L"sFileName", TextToHtmlText(sFileName),
                             L"sFileTitle", TextToHtmlText(sfiletitle))),
               CP_ACP, CP_UTF8, Now());
    Finish();
}

/* ---- /postinfo.html: displays the URL parameters and content of any request ---- */

void __fastcall TUrlHandlerPostInfo::Execute()
{
    THttpAppSrvConnection *RemoteClient = Client;
    String sinfo1 = "Request Method: " + RemoteClient->Method + "\r\n" +
                    "Content Size " + IntToKbyte(RemoteClient->PostedDataLen) + "\r\n" +
                    "Content Type: " + RemoteClient->RequestContentType + "\r\n\r\n\r\n";
    const String sPageUrl = RemoteClient->RequestProtocol + "://" + RemoteClient->RequestHost + Client->Path;

    // Lists URL-encoded name=value pairs, one per line.
    auto AddParamList = [&sinfo1](const String &Encoded) {
        std::unique_ptr<TStringList> ParamList(new TStringList());
        const int ParamTot = IcsExtractURLEncodedParamList(Encoded, ParamList.get(), true);
        for (int I = 0; I < ParamTot; I++)
            sinfo1 += "Param " + IntToStr(I + 1) + ": " + ParamList->Strings[I] + "\r\n";
    };

    if (!Params.IsEmpty()) {
        sinfo1 += "Raw URL Params:\r\n" + IcsStrBeakup(Params, 132) + "\r\n";
        AddParamList(Params);
        sinfo1 += "\r\n";
    }

    // GET, DELETE, POST, PATCH and PUT may all have content.
    if (RemoteClient->PostedDataLen > 0) {
        if (RemoteClient->PostedDataLen < 9000) {
            const String rawdata = IcsStrBeakup(IcsStrRemCntlsTB(RemoteClient->PostedDataTB, true), 132);
            sinfo1 += "Raw Upload Content:\r\n" + rawdata + "\r\n\r\n";
        }

        // First tell the component that all the data has arrived.
        RemoteClient->PostedDataReceived();
        try {
            const String ContType = RemoteClient->RequestContentType;
            if (ContType.Pos("multipart/form-data") > 0) {
                std::unique_ptr<TFormDataAnalyser> Decoder(new TFormDataAnalyser(nullptr));
                LongWord ACodePage;
                MimeCharsetToCodePage(ContType, ACodePage);
                Decoder->FormCodePage = ACodePage;
                RemoteClient->PostedDataStream->Position = 0;
                Decoder->DecodeStream(RemoteClient->PostedDataStream);
                sinfo1 += Decoder->DecodeInfo;
            }
            else if (RemoteClient->PostedDataLen > 1000 ||
                     SameText(ContType, "application/binary") ||
                     SameText(ContType, "application/octet-stream") ||
                     SameText(ContType, "application/zip"))
                sinfo1 += "Unable to process content";
            else if (ContType.Pos("json") == 0)   // Json is not parsed
                AddParamList(Client->PostedDataStr);
        }
        catch (Exception &E) {
            Display("Exception Saving Content Data - " + IcsGetExceptMess(&E));
        }
    }
    sinfo1 += "\r\n\r\nPost URL: " + sPageUrl + "\r\n" +
              "From IP Address: " + RemoteClient->CPeerAddr + "\r\n" +
              IcsVerLitRelDate + "\r\n";
    Display(sinfo1);
    sinfo1 = TextToHtmlText(sinfo1);   // also converts CRLF
    AnswerPage("", "", "postinfo.html", NoUserData,
               ARRAYOFCONST((L"sinfo1", sinfo1, L"sPageUrl", sPageUrl)), CP_ACP, CP_UTF8, Now());
    Finish();
}

/* ---- /DemoAuthAll.html: authentication test template for GET and POST ---- */

void __fastcall TUrlHandlerDemoAuthAll::Execute()
{
    THttpAppSrvConnection *RemoteClient = Client;
    String sPageUrl = RemoteClient->Method + " " + RemoteClient->RequestProtocol + "://" +
                      RemoteClient->RequestHost + Client->Path;
    if (Client->PostedDataLen > 0)
        sPageUrl += ", PostParams: " + Client->PostedDataStr;
    AnswerPage("", "", "DemoAuthAll.html", NoUserData,
               ARRAYOFCONST((L"sPageUrl", sPageUrl,
                             L"sAuthType", String(HttpAuthTypeNames[RemoteClient->AuthType]))),
               CP_ACP, CP_UTF8, Now());
    Finish();
}

void __fastcall TUrlHandlerHelloWorld::Execute()
{
    AnswerString("", "", "", "<HTML><BODY>Hello World !</BODY></HTML>");
    Finish();
}

/* ---- /HeadForm.html: sends a HEAD request to a URL once a sum is answered ---- */

namespace {
const wchar_t *const sDefaultUrl         = L"https://www.google.com";
const wchar_t *const sDoCalc             = L"Please try to calculate the correct value";
const wchar_t *const sHeadUrl            = L"HeadUrl";
const wchar_t *const sEquals             = L"Equals";
const wchar_t *const sResponse           = L"Response";
const wchar_t *const sAnswerThisQuestion = L"Please answer this question: <br>";

// Calls the handler's Finish when the scope ends, however it ends (Delphi: try/finally),
// unless Release was called because an async request will finish it later.
class TFinishOnExit
{
public:
    explicit TFinishOnExit(TUrlHandler *Handler) : FHandler(Handler) {}
    ~TFinishOnExit() { if (FHandler) FHandler->Finish(); }
    void Release() { FHandler = nullptr; }
    TFinishOnExit(const TFinishOnExit &) = delete;
    TFinishOnExit &operator=(const TFinishOnExit &) = delete;
private:
    TUrlHandler *FHandler;
};
}

void TUrlHandlerHead::AnswerHeadForm(const String &Url, const String &Response)
{
    AnswerPage("", NO_CACHE, UrlHeadForm, NoUserData,
               ARRAYOFCONST((sHeadUrl, Url, sEquals, GenerateMath(), sResponse, Response)));
}

void __fastcall TUrlHandlerHead::HeadRequestDone(TObject *Sender, THttpRequest RqType, System::Word ErrCode)
{
    TFinishOnExit FinishGuard(this);
    if (FCli->RcvdHeader->Count > 0) {
        if (!FAllHdrs)
            FResponse += FCli->RcvdHeader->Strings[0];
        else {
            for (int I = 0; I < FCli->RcvdHeader->Count; I++)
                FResponse += FCli->RcvdHeader->Strings[I] + "<br>\r\n";
        }
    }
    else if (ErrCode != 0)
        FResponse += "error #" + IntToStr(ErrCode);
    else
        FResponse += "Unknown error";
    AnswerHeadForm(sDefaultUrl, FResponse);
}

void __fastcall TUrlHandlerHead::HeadRequestTimeout(TObject *Sender, TTimeoutReason Reason)
{
    TFinishOnExit FinishGuard(this);
    FCli->OnRequestDone = nullptr;
    FCli->Abort();
    FResponse += " Request timeout";
    AnswerHeadForm(sDefaultUrl, FResponse);
}

void __fastcall TUrlHandlerHead::Execute()
{
    if (NotLogged())   // also finishes this handler
        return;
    TFinishOnExit FinishGuard(this);
    String Equals, S;
    FResponse = "";
    IcsExtractURLEncodedValue(Params, sHeadUrl, FUrl);
    IcsExtractURLEncodedValue(Params, sEquals, Equals);
    IcsExtractURLEncodedValue(Params, "Submit", S);
    const bool ButtonPressed = !S.IsEmpty();
    IcsExtractURLEncodedValue(Params, "AllHeaders", S);
    FAllHdrs = !S.IsEmpty();
    if (FUrl.IsEmpty())
        FUrl = sDefaultUrl;
    if (SessionData()->TempVar >= 0 && ButtonPressed && !Equals.IsEmpty()) {
        if (!VerifyMath(Equals))
            AnswerHeadForm(FUrl, sDoCalc);
        else {
            try {
                FCli.reset(new TSslHttpRest(nullptr));   // Delphi: Create(Self)
                FResponse = "Response from \"" + FUrl + "\":<br>";
                FCli->SocketFamily = sfAnyIPv4;          // IPv6 and IPv4, prefer IPv4
                FCli->RequestVer = "1.1";
                FCli->CtrlSocket->TimeoutConnect = 5 * 1000;
                FCli->CtrlSocket->TimeoutIdle = 10 * 1000;
                FCli->CtrlSocket->OnTimeout = HeadRequestTimeout;
                FCli->OnRequestDone = HeadRequestDone;
                if (FCli->RestRequest(httpHEAD, FUrl, true, "") >= 0) {
                    FinishGuard.Release();   // the request events finish this handler
                    return;
                }
                // Delphi waits for events after -1 (request never started, e.g. a non-HTTP URL); none come, so this answers now.
                FResponse += " Request not started: " + TextToHtmlText(FCli->ReasonPhrase);
            }
            catch (Exception &) {
                FResponse += " Internal server error";
            }
            AnswerHeadForm(sDefaultUrl, FResponse);
        }
    }
    else {
        if (ButtonPressed && Equals.IsEmpty())
            FResponse = sDoCalc;
        AnswerHeadForm(FUrl, FResponse);
    }
}

String TUrlHandlerHead::GenerateMath()
{
    TAppSrvSessionData *Session = SessionData();
    if (!Session)
        return "";
    const int N1 = Random(10);
    const int N2 = Random(10);
    if (Random(2) == 0) {
        Session->TempVar = N1 + N2;
        return String(sAnswerThisQuestion) + IntToStr(N1) + " + " + IntToStr(N2) + " equals ?";
    }
    const int Hi = std::max(N1, N2);
    const int Lo = std::min(N1, N2);
    Session->TempVar = Hi - Lo;
    return String(sAnswerThisQuestion) + IntToStr(Hi) + " - " + IntToStr(Lo) + " equals ?";
}

bool TUrlHandlerHead::VerifyMath(const String &S)
{
    return SessionData() && StrToIntDef(S, 0) == SessionData()->TempVar;
}

/* ---- /CounterView.html and its Ajax counter fetch ---- */

void __fastcall TUrlHandlerCounterViewHtml::Execute()
{
    if (NotLogged())
        return;

    std::unique_ptr<TStringList> Names(new TStringList());
    ExtractURLEncodedParamList(Params, Names.get());

    std::unique_ptr<TArrayOfConstBuilder> Tags(new TArrayOfConstBuilder());
    Tags->Add("LOGIN", UrlLogin);
    Tags->Add("COUNTER", UrlCounter);
    Tags->Add("USERCODE", SessionData()->UserCode);
    Tags->Add("LOGINTIME", DateToStr(SessionData()->LogonTime));
    for (int I = 0; I < Names->Count; I++) {
        String CounterName;
        ExtractURLEncodedValue(Params, Names->Strings[I], CounterName);
        FCountersSelected->Add(CounterName);
        Tags->Add("CounterValue" + IntToStr(I + 1), SslMultiWebDataModule->CounterValue(CounterName, 0));
    }

    ArrayOfTVarRec TagArray = Tags->Value;   // holds a reference to the builder's array
    OnGetRowData = GetRowData;
    AnswerPage("", NO_CACHE, "/CounterView.html", NoUserData, &TagArray[0], TagArray.High);
    OnGetRowData = nullptr;
    Finish();
}

// Fills the two counter drop-downs (tables 1 and 2): row 1 is "Please select", then
// one row per counter in Counters.ini.
void __fastcall TUrlHandlerCounterViewHtml::GetRowData(TObject *Sender, const String TableName, int Row,
                                                      TStringIndex *TagData, bool &More, TObject *UserData)
{
    const int NoTable = StrToIntDef(TableName, 0);
    if (Row == 1) {
        std::unique_ptr<TIcsIniFile> IniFile(new TIcsIniFile(SslMultiWebDataModule->CounterFileName(), true));
        FCounters->Clear();
        IniFile->ReadSection(CounterSection, FCounters.get());
        FCounters->Sort();
        TagData->Add("CounterItem", PleaseSelect);
        if (FCountersSelected->Count == 0)
            TagData->Add("CounterSelected", "SELECTED");
        More = true;
        return;
    }

    More = Row <= FCounters->Count + 1;   // Delphi stops at Row <= Count, so the last counter was never listed
    if (More) {
        TagData->Add("CounterItem", FCounters->Strings[Row - 2]);
        if (NoTable <= FCountersSelected->Count &&
            SameText(FCountersSelected->Strings[NoTable - 1], FCounters->Strings[Row - 2]))
            TagData->Add("CounterSelected", "SELECTED");
    }
}

void __fastcall TUrlHandlerAjaxFetchCounter::Execute()
{
    if (!ValidateSession()) {
        AnswerString("500", "text/plain", NO_CACHE, "Invalid login");
        Finish();
        return;   // Delphi carries on and sends a second answer after Finish
    }

    String CounterName;
    ExtractURLEncodedValue(Params, "counter", CounterName);
    CounterName = Trim(CounterName);
    const int CounterValue = (CounterName == PleaseSelect || CounterName.IsEmpty())
        ? 0 : SslMultiWebDataModule->CounterValue(CounterName, 0);
    AnswerString("", "text/plain", NO_CACHE, IntToStr(CounterValue));
    Finish();
}

/* ---- /Counter.jpg: a hit counter drawn as a JPEG image ---- */

void __fastcall TUrlHandlerCounterJpg::Execute()
{
    String CounterRef;
    ExtractURLEncodedValue(Params, "Ref", CounterRef);
    if (CounterRef.IsEmpty())
        CounterRef = "Counter";
    // Delphi writes any name as an INI key, so a CR/LF in it injected lines into Counters.ini.
    if (!IsValidCounterName(CounterRef)) {
        AnswerString("400", "text/plain", NO_CACHE, "Invalid counter name");
        Finish();
        return;
    }
    // Separate counter for access without a login.
    if (!ValidateSession())
        CounterRef = "NotLogged_" + CounterRef;

    const String CounterString = IntToStr(SslMultiWebDataModule->CounterIncrement(CounterRef));

    delete DocStream;
    DocStream = new TMemoryStream();   // the connection owns and frees the document stream
    std::unique_ptr<TJPEGImage> JpegImg(new TJPEGImage());
    std::unique_ptr<Vcl::Graphics::TBitmap> BitMapImg(new Vcl::Graphics::TBitmap());
    BitMapImg->Width = 64;
    BitMapImg->Height = 32;
    TCanvas *Canvas = BitMapImg->Canvas;
    Canvas->Pen->Color = clBlack;
    Canvas->Brush->Color = clGray;
    Canvas->RoundRect(0, 0, BitMapImg->Width - 1, BitMapImg->Height - 1, 16, 16);
    Canvas->Font->Name = "arial";
    Canvas->Font->Size = 14;
    Canvas->Font->Color = clWhite;
    Canvas->TextOut((BitMapImg->Width - Canvas->TextWidth(CounterString)) / 2 - 1,
                    (BitMapImg->Height - Canvas->TextHeight(CounterString)) / 2 - 1,
                    CounterString);
    JpegImg->Assign(BitMapImg.get());
    JpegImg->SaveToStream(DocStream);
    AnswerStream("", "image/jpeg", NO_CACHE);
    Finish();
}

/* ---- Configuration: form, posted changes with a logo upload, confirmation ---- */

void __fastcall TUrlHandlerConfigFormHtml::Execute()
{
    if (NotLogged())
        return;
    AnswerPage("", NO_CACHE, "Config.html", NoUserData,
               ARRAYOFCONST((L"LOGIN", UrlLogin,
                             L"COUNTER", UrlCounter,
                             L"USERCODE", SessionData()->UserCode,
                             L"DOCONFIG", UrlDoConfigHtml,
                             L"PORT", SslMultiWebDataModule->Port,
                             L"LOGINTIME", DateToStr(SessionData()->LogonTime))));
    Finish();
}

void __fastcall TUrlHandlerDoConfigHtml::Execute()
{
    if (NotLogged())
        return;
    String ErrMsg;
    TAppSrvSessionData *Session = SessionData();
    Session->ConfigPort = "";
    Session->ConfigTempDir = "";

    std::unique_ptr<TMemoryStream> Stream(new TMemoryStream());
    Stream->WriteBuffer(Client->PostedData, Client->PostedDataLen);
    Stream->Seek(static_cast<__int64>(0), soBeginning);
    std::unique_ptr<TFormDataAnalyser> Decoder(new TFormDataAnalyser(nullptr));
    Decoder->DecodeStream(Stream.get());

    // Missing form parts are treated as empty; Delphi dereferences a nil Part().
    if (!SameText(Decoder->PartData(String("submit")), "Save"))
        ErrMsg = "canceled";
    else {
        // Port: a minimal check. A port is a positive 16 bit decimal number or a well
        // known service name such as "http".
        TFormDataItem *Field = Decoder->Part("port");
        if (Field && Field->DataLength > 0 && Field->DataLength < 100) {
            // Delphi checks only the length, so a CR/LF in the value injected lines into the INI file.
            const String Port = Trim(Field->AsString());
            if (IsIniValueSafe(Port))
                Session->ConfigPort = Port;
            else
                ErrMsg = "Invalid port";
        }

        // Logo image file: a minimal validity check.
        Field = Decoder->Part("logo");
        const String FileName = Field ? ExtractFileName(Field->ContentFileName()) : String();
        Session->ConfigHasLogo = !FileName.IsEmpty();
        if (Session->ConfigHasLogo) {
            const String FileExt = ExtractFileExt(FileName);
            if (!SameText(FileExt, ".png") ||
                !(SameText(Field->ContentType(), "image/png") || SameText(Field->ContentType(), "image/x-png")))
                ErrMsg = "Only PNG file accepted for logo";
            else if (Field->DataLength > 50 * 1024)
                ErrMsg = "Logo image file must be < 50KB";
            // Delphi uses the user code as a folder name unchecked, so "..\" escaped the data folder.
            else if (!IsPlainFileName(Session->UserCode))
                ErrMsg = "User code cannot be used as a folder name";
            else {
                // A temporary directory; the server deletes it once the time in its name has passed.
                Session->ConfigTempDir = String(PathDelim) +
                    FormatDateTime("YYYYMMDDHHNNSSZZZ", Now() + EncodeTime(0, 15, 0, 0));
                const String TempDir = Session->ConfigTempDir + PathDelim + Session->UserCode;
                ForceDirectories(SslMultiWebDataModule->DataDir() + TempDir);
                // Never use the original file name.
                Field->SaveToFile(SslMultiWebDataModule->DataDir() + TempDir + PathDelim + "Logo.png");
            }
        }
    }

    if (!ErrMsg.IsEmpty())
        AnswerString("", "", "", "<html><body><a href=\"" + String(UrlConfigForm) + "\">" +
                                 ErrMsg + "</a></body></html>");
    else
        AnswerPage("", NO_CACHE, "ConfigConfirm.html", NoUserData,
                   ARRAYOFCONST((L"PORT", Session->ConfigPort, L"LOGO", L"ConfigLogo.png",
                                 L"ACTION", UrlDoConfigConfirmSaveHtml)));
    Finish();
}

void __fastcall TUrlHandlerConfigLogoPng::Execute()
{
    if (NotLogged())
        return;
    TAppSrvSessionData *Session = SessionData();
    // The user code is checked again here and at confirm, because sessions are restored from Sessions.dat.
    const String FileName = (Session->ConfigHasLogo && IsPlainFileName(Session->UserCode))
        ? SslMultiWebDataModule->DataDir() + Session->ConfigTempDir + PathDelim + Session->UserCode + PathDelim + "Logo.png"
        : SslMultiWebDataModule->ImagesDir + PathDelim + "Logo.png";

    delete DocStream;
    DocStream = new TFileStream(FileName, fmOpenRead);
    AnswerStream("", "image/png", NO_CACHE);
    Finish();
}

void __fastcall TUrlHandlerDoConfigConfirmSaveHtml::Execute()
{
    if (NotLogged())
        return;
    TAppSrvSessionData *Session = SessionData();
    String Submit;
    ExtractURLEncodedValue(Params, "submit", Submit);
    if (SameText(Submit, "OK")) {
        // A new configuration has been confirmed.
        if (!Session->ConfigPort.IsEmpty() && IsIniValueSafe(Session->ConfigPort)) {
            SslMultiWebDataModule->Port = Session->ConfigPort;
            SslMultiWebDataModule->SaveConfig();
        }
        if (Session->ConfigHasLogo && IsPlainFileName(Session->UserCode)) {
            const String FileName = SslMultiWebDataModule->DataDir() + Session->ConfigTempDir +
                                    PathDelim + Session->UserCode + PathDelim + "Logo.png";
            if (!Session->ConfigTempDir.IsEmpty() && FileExists(FileName)) {
                // Replace the existing logo image with the new one.
                const String Logo = SslMultiWebDataModule->ImagesDir + PathDelim + "Logo.png";
                // Delphi deletes the old logo then renames unchecked, so a failed rename lost both; this replaces in one call.
                if (MoveFileEx(FileName.c_str(), Logo.c_str(), MOVEFILE_REPLACE_EXISTING | MOVEFILE_COPY_ALLOWED))
                    ForceRemoveDir(SslMultiWebDataModule->DataDir() + Session->ConfigTempDir);
                else
                    Display("Failed to replace logo - " + SysErrorMessage(GetLastError()));
            }
        }
    }
    Relocate("/");
    Finish();
}

/* ---- /IpAddrLookup.html: IP address lookup (page or Json) ----
   Examples:  GET IpAddrLookup.html?217.146.102.142, ?2a00:1940:2:2::142,
   ?IpAddr=217.146.102.142 or ?IpAddr=217.146.102.142&mode=json (Json response).
   QUERY and POST take the same parameters as content. */
void __fastcall TUrlIpAddrLookup::Execute()
{
    THttpAppSrvConnection *RemoteClient = Client;
    String sinfo1 = "Request Method: " + RemoteClient->Method + "\r\n" +
                    "Content Size " + IntToKbyte(RemoteClient->PostedDataLen) + "\r\n";
    const String sPageUrl = RemoteClient->RequestProtocol + "://" + RemoteClient->RequestHost + Client->Path;
    String sIpAddr, sDispTable;
    TSocketFamily ASocketFamily;
    bool JsonFlag = RemoteClient->Method == "QUERY";   // no page for QUERY

    if (!Params.IsEmpty()) {
        sinfo1 += "Raw URL Params:" + Params + "\r\n";
        ExtractURLEncodedValue(Params, "IpAddr", sIpAddr);
        if (IcsLowerCase(Params).Pos("json") > 0)
            JsonFlag = true;
        // A raw IP address with no name.
        if (sIpAddr.IsEmpty() && WSocketIsIP(Trim(Params), ASocketFamily))
            sIpAddr = Trim(Params);
    }

    // POSTed or QUERY content.
    if (RemoteClient->PostedDataLen > 0) {
        const String Posted = RemoteClient->PostedDataStr;
        sinfo1 += "Raw Content Params:" + Posted + "\r\n";
        ExtractURLEncodedValue(Posted, "IpAddr", sIpAddr);
        if (IcsLowerCase(Posted).Pos("json") > 0)
            JsonFlag = true;
        if (sIpAddr.IsEmpty() && WSocketIsIP(Trim(Posted), ASocketFamily))
            sIpAddr = Trim(Posted);
    }
    Display(sinfo1);

    AnsiString RecJStr;   // the lookup record as a Json array, when one was found
    if (sIpAddr.IsEmpty())
        sIpAddr = RemoteClient->GetPeerAddr();
    else {
        try {
            sIpAddr = Trim(sIpAddr);
            TIcsGeoTools *Geo = SslMultiWebDataModule->IcsGeoTools;
            if (!Geo) {
                sDispTable = "Sorry, GEO databas not installed";
                Display(sDispTable);
            }
            else if (!WSocketIsIP(sIpAddr, ASocketFamily)) {
                sDispTable = "Sorry, not a valid IP Address: " + sIpAddr;
                Display(sDispTable);
            }
            else {
                const String ISO2A = Geo->FindISOA2Code(sIpAddr);
                const TIcsCountry CountryRec = Geo->FindNameRec(ISO2A);
                __int64 AsnNum = 0;
                const String AsnName = Geo->FindASNCode(sIpAddr, AsnNum);
                String AsnInfo;
                if (AsnNum > 0)
                    AsnInfo = AsnName + " (" + IntToStr(AsnNum) + ")";

                if (JsonFlag) {
                    std::unique_ptr<TRestParams> JsonRec(new TRestParams(nullptr));
                    JsonRec->PContent = PContJson;
                    JsonRec->AddItem("ipaddr", sIpAddr);
                    JsonRec->AddItem("iso2a", ISO2A);
                    JsonRec->AddItem("cntyname", CountryRec.Country);
                    JsonRec->AddItem("akacntyname", CountryRec.AkaCountry);   // Delphi sends Country again
                    JsonRec->AddItem("region", CountryRec.Region);
                    JsonRec->AddItem("subregion", CountryRec.SubRegion);
                    JsonRec->AddItem("dialcode", CountryRec.DialCode);
                    JsonRec->AddItem("internet", CountryRec.Internet);
                    JsonRec->AddItem("numiso", CountryRec.NumISO);
                    JsonRec->AddItem("asnname", AsnName);
                    // AddItem has no Int64 overload; ASNs in the public databases fit in an int.
                    if (AsnNum <= MaxInt)
                        JsonRec->AddItem("asnnum", static_cast<int>(AsnNum));
                    else
                        JsonRec->AddItem("asnnum", static_cast<double>(AsnNum));
                    RecJStr = "[" + JsonRec->GetParameters() + "]";
                }
                else {
                    sDispTable = "IP Address: " + sIpAddr + "<BR>\r\n" +
                        "Country ISO: " + ISO2A + ", Name: " + CountryRec.Country +
                            ", Aka: " + CountryRec.AkaCountry + "<BR>\r\n" +
                        "Region: " + Geo->FindRegion(CountryRec.Region) + ", Sub: " +
                            Geo->FindRegion(CountryRec.SubRegion) + "<BR>\r\n" +
                        "Country DialCode: " + CountryRec.DialCode + ", Internet: " + CountryRec.Internet +
                            ", Num ISO:" + IntToStr(CountryRec.NumISO) + "<BR>\r\n" +
                        "ASN: " + AsnInfo + "<BR>\r\n";
                }
            }
        }
        catch (Exception &E) {
            Display("Exception Reading GEO Database - " + IcsGetExceptMess(&E));
            sDispTable = "Sorry, error reading GEO database";
        }
        catch (...) {   // Delphi's bare except also catches non-VCL exceptions; the page is still answered
            Display("Unknown Exception Reading GEO Database");
            sDispTable = "Sorry, error reading GEO database";
        }
    }

    if (JsonFlag) {
        std::unique_ptr<TRestParams> JsonResult(new TRestParams(nullptr));
        JsonResult->PContent = PContJson;
        if (!RecJStr.IsEmpty()) {
            JsonResult->AddItem("success", true);
            JsonResult->AddItem("reccount", 1);
            JsonResult->AddItemA("records", RecJStr, true);
        }
        else {
            JsonResult->AddItem("success", false);
            JsonResult->AddItem("reccount", 0);
            JsonResult->AddItem("errno", 8);
            JsonResult->AddItem("errdesc", sDispTable);
        }
        const AnsiString ResultJStr = JsonResult->GetParameters();
        Flags = hgWillSendMySelf;
        AnswerString("", "application/json", "Pragma: no-cache\r\nExpires: -1\r\n",
                     String(ResultJStr), CP_UTF8, Now());
    }
    else
        AnswerPage("", "", "IpAddrLookup.html", NoUserData,
                   ARRAYOFCONST((L"sIpAddr", sIpAddr, L"sPageUrl", sPageUrl, L"sDispTable", sDispTable)),
                   CP_ACP, CP_UTF8, Now());
    Finish();
}

// The sessions file names its session data class, so it must be registered before the
// file is loaded (Delphi: RegisterClass in this unit's initialization section).
void RegisterMultiWebClasses()
{
    RegisterClass(__classid(TAppSrvSessionData));
}
