/*++

Module Name:

  Common.cpp

Notices:

  Dial Module common.

Author:

  Copyright (c) Prepodobny Alen

  mailto: alienufo@inbox.ru
  mailto: ufocomp@gmail.com

--*/

#include "Core.hpp"
#include "Common.hpp"
//----------------------------------------------------------------------------------------------------------------------

#ifdef  __cplusplus
extern "C++" {
#endif // __cplusplus

namespace Apostol {

    namespace Common {

        //--------------------------------------------------------------------------------------------------------------

        //-- CCustomModule ---------------------------------------------------------------------------------------------

        //--------------------------------------------------------------------------------------------------------------

        CCustomModule::CCustomModule(CModuleProcess *AProcess, const CString& ModuleName, const CString& SectionName):
                CApostolModule(AProcess, ModuleName, SectionName) {

            m_SyncPeriod = BPS_DEFAULT_SYNC_PERIOD;
            m_Status = psStopped;

            CCustomModule::InitMethods();
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::InitMethods() {
#if defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 9)
            m_Methods.AddObject(_T("GET")    , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoGet(Connection); }));
            m_Methods.AddObject(_T("POST")   , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoPost(Connection); }));
            m_Methods.AddObject(_T("HEAD")   , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoHead(Connection); }));
            m_Methods.AddObject(_T("OPTIONS"), (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoOptions(Connection); }));
            m_Methods.AddObject(_T("PUT")    , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); }));
            m_Methods.AddObject(_T("DELETE") , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); }));
            m_Methods.AddObject(_T("TRACE")  , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); }));
            m_Methods.AddObject(_T("PATCH")  , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); }));
            m_Methods.AddObject(_T("CONNECT"), (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); }));
#else
            m_Methods.AddObject(_T("GET"), (CObject *) new CMethodHandler(true, std::bind(&CCustomModule::DoGet, this, _1)));
            m_Methods.AddObject(_T("POST"), (CObject *) new CMethodHandler(true, std::bind(&CCustomModule::DoPost, this, _1)));
            m_Methods.AddObject(_T("HEAD"), (CObject *) new CMethodHandler(true, std::bind(&CCustomModule::DoHead, this, _1)));
            m_Methods.AddObject(_T("OPTIONS"), (CObject *) new CMethodHandler(true, std::bind(&CCustomModule::DoOptions, this, _1)));
            m_Methods.AddObject(_T("PUT"), (CObject *) new CMethodHandler(false, std::bind(&CCustomModule::MethodNotAllowed, this, _1)));
            m_Methods.AddObject(_T("DELETE"), (CObject *) new CMethodHandler(false, std::bind(&CCustomModule::MethodNotAllowed, this, _1)));
            m_Methods.AddObject(_T("TRACE"), (CObject *) new CMethodHandler(false, std::bind(&CCustomModule::MethodNotAllowed, this, _1)));
            m_Methods.AddObject(_T("PATCH"), (CObject *) new CMethodHandler(false, std::bind(&CCustomModule::MethodNotAllowed, this, _1)));
            m_Methods.AddObject(_T("CONNECT"), (CObject *) new CMethodHandler(false, std::bind(&CCustomModule::MethodNotAllowed, this, _1)));
#endif
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::Initialization(CModuleProcess *AProcess) {
            CApostolModule::Initialization(AProcess);

            if (Enabled()) {
                Reload();
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        bool CCustomModule::Enabled() {
            if (m_ModuleStatus == msUnknown)
                m_ModuleStatus = Config()->IniFile().ReadBool(SectionName().c_str(), "enable", true) ? msEnabled : msDisabled;
            return m_ModuleStatus == msEnabled;
        }
        //--------------------------------------------------------------------------------------------------------------

        CHTTPRequest::CContentType CCustomModule::StringToContentType(const CString &ContentType) {
            if (ContentType == "application/json") {
                return CHTTPRequest::json;
            } else if (ContentType == "text/html") {
                return CHTTPRequest::html;
            } else if (ContentType == "application/xml") {
                return CHTTPRequest::xml;
            } else if (ContentType == "application/octet-stream") {
                return CHTTPRequest::sbin;
            }
            return CHTTPRequest::text;
        }
        //--------------------------------------------------------------------------------------------------------------

        CString CCustomModule::ToString(unsigned long Value) {
            TCHAR szString[_INT_T_LEN + 1] = {0};
            sprintf(szString, "%lu", Value);
            return { szString };
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::JsonStringToKey(const CString &String, CString &Code, CString& Key) {
            const CJSON Json(String);

            if (Json.HasOwnProperty("error")) {
                const auto &error = Json["error"];
                if (error.ValueType() == jvtObject)
                    throw Delphi::Exception::Exception(error["message"].AsString().c_str());
            }

            if (Json.HasOwnProperty("code")) {
                Code = Json["code"].AsString();
            }

            if (Json.HasOwnProperty("data")) {
                Key = Json["data"].AsString();
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::CheckKeyForNull(LPCTSTR Key, LPCTSTR Value) {
            if (Value == nullptr)
                throw ExceptionFrm("Invalid format: key \"%s\" cannot be empty.", Key);
        }
        //--------------------------------------------------------------------------------------------------------------

        bool CCustomModule::FindURLInLine(const CString &Line, CStringList &List) {
            CString URL;

            TCHAR ch;
            int length = 0;
            size_t startPos, pos;

            pos = 0;
            while ((startPos = Line.Find(HTTP_PREFIX, pos)) != CString::npos) {

                URL.Clear();

                pos = startPos + HTTP_PREFIX_SIZE;
                if (Line.Length() < 5)
                    return false;

                URL.Append(HTTP_PREFIX);
                ch = Line.at(pos);
                if (ch == 's') {
                    URL.Append(ch);
                    pos++;
                }

                if (Line.Length() < 7 || Line.at(pos++) != ':' || Line.at(pos++) != '/' || Line.at(pos++) != '/')
                    return false;

                URL.Append("://");

                length = 0;
                ch = Line.at(pos);
                while (ch != 0 && (IsChar(ch) || IsNumeral(ch) || ch == ':' || ch == '.' || ch == '-')) {
                    URL.Append(ch);
                    length++;
                    ch = Line.at(++pos);
                }

                if (length < 3) {
                    return false;
                }

                if (startPos == 0) {
                    List.Add(URL);
                } else {
                    ch = Line.at(startPos - 1);
                    switch (ch) {
                        case ' ':
                        case ',':
                        case ';':
                            List.Add(URL);
                            break;
                        default:
                            return false;
                    }
                }
            }

            return true;
        }
        //--------------------------------------------------------------------------------------------------------------

        int CCustomModule::CheckFee(const CString &Fee) {
            if (!Fee.IsEmpty()) {

                if (Fee.Length() >= 10)
                    return -1;

                size_t numbers = 0;
                size_t delimiter = 0;
                size_t percent = 0;

                size_t pos = 0;
                TCHAR ch;

                ch = Fee.at(pos);
                while (ch != 0) {
                    if (IsNumeral(ch))
                        numbers++;
                    if (ch == '.')
                        delimiter++;
                    if (ch == '%')
                        percent++;
                    ch = Fee.at(++pos);
                }

                if (numbers == 0 || delimiter > 1 || percent > 1 || ((numbers + percent + delimiter) != Fee.Length()))
                    return -1;

                return 1;
            }

            return 0;
        }
        //--------------------------------------------------------------------------------------------------------------

        bool CCustomModule::CheckVerifyPGPSignature(int VerifyPGPSignature, CString &Message) {
            if (VerifyPGPSignature == -2) {
                Message = "PGP public key is not meaningful.";
            } else if (VerifyPGPSignature == -1) {
                Message = "PGP signature not valid.";
            } else if (VerifyPGPSignature == 0) {
                Message = "PGP signature not found.";
            } else if (VerifyPGPSignature > 1) {
                Message = "PGP signature status: Unknown.";
            }
            return VerifyPGPSignature == 1;
        }
        //--------------------------------------------------------------------------------------------------------------

        int CCustomModule::VerifyPGPSignature(const CString &ClearText, const CString &Key, CString &Message) {
            const OpenPGP::Key signer(Key.c_str());
            const OpenPGP::CleartextSignature cleartext(ClearText.c_str());

            if (!cleartext.meaningful())
                return -2;

            const int verified = OpenPGP::Verify::cleartext_signature(signer, cleartext);

            if (verified == 1) {
                Message = cleartext.get_message().c_str();
            }

            return verified;
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::ParsePGPKey(const CString &Key, CStringPairs& ServerList, CStringList& BTCKeys) {
            if (Key.IsEmpty())
                return;

            const Apostol::PGP::Key pgp(Key.c_str());
            if (!pgp.meaningful())
                return;

            CStringList Data;
            CPGPUserIdList List;
            CStringList KeyList;

            pgp.ExportUID(List);

            for (int i = 0; i < List.Count(); i++) {

                const auto &uid = List[i];

                DebugMessage("%s (%s)\n", uid.Name.c_str(), uid.Desc.c_str());

                const auto &name = uid.Name.Lower();
                const auto &data = uid.Desc.Lower();

                if (name == "technical_data") {
                    SplitColumns(data, Data, ';');

                    if (Data.Count() == 3) {
                        m_SyncPeriod = StrToIntDef(Data[1].Trim().c_str(), BPS_DEFAULT_SYNC_PERIOD);
                    } else if (Data.Count() == 2) {
                        m_SyncPeriod = StrToIntDef(Data[0].Trim().c_str(), BPS_DEFAULT_SYNC_PERIOD);
                    } else if (Data.Count() == 1) {
                        m_SyncPeriod = StrToIntDef(Data[0].Trim().c_str(), BPS_DEFAULT_SYNC_PERIOD);
                    }
                } if (uid.Name.Length() >= 35 && uid.Name.SubString(0, 3) == BM_PREFIX) {
#ifdef _DEBUG
                    ServerList.AddPair(BPS_DEBUG_SERVER_URL, BPS_DEBUG_SERVER_URL);
#endif
                    CStringList urlList;
                    if (FindURLInLine(uid.Desc, urlList)) {
                        for (int l = 0; l < urlList.Count(); l++) {
                            ServerList.AddPair(uid.Name, urlList[l]);
                        }
                    }
                } else if (name.Find("bitcoin_key") != CString::npos) {
                    const auto &key = wallet::ec_public(data.c_str());
                    if (verify(key))
                        KeyList.AddPair(name, key.encoded());
                }
            }

            CString Name;
            for (int i = 1; i <= KeyList.Count(); i++) {
                Name = "bitcoin_key";
                Name << i;
                const auto &key = KeyList[Name];
                if (!key.IsEmpty()) {
                    BTCKeys.Add(key);
                }
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::UpdateOAuth2(CContext &Context, const CJSONObject &OAuth2) {
            if (OAuth2["type"].AsString() == "service_account") {
                UpdateProviders(Context.Providers(), OAuth2);

                Context.SetCheckDate(0);
                Context.SetStatus(Context::csInitialized);
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::UpdateProviders(CProviders &Providers, const CJSONObject &Data) {
            const auto &caProviderName = CString(SYSTEM_PROVIDER_NAME);
            const auto &caApplicationName = CString(SERVICE_APPLICATION_NAME);

            int index = Providers.IndexOfName(caProviderName);
            if (index == -1)
                index = Providers.AddPair(caProviderName, CProvider(caProviderName));
            auto& Provider = Providers[index].Value();
            Provider.Applications().AddPair(caApplicationName, Data);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::CreateAccessToken(CContext &Context, CProvider &Provider, const CString &Application) {

            auto OnDone = [this, &Context, &Provider](CTCPConnection *Sender) {

                const auto pConnection = dynamic_cast<CHTTPClientConnection *> (Sender);
                const auto &Reply = pConnection->Reply();

                DebugReply(Reply);

                if (Reply.Status == CHTTPReply::ok) {
                    const CJSON Json(Reply.Content);

                    Context.Session() = Json["session"].AsString();
                    Context.Secret() = Json["secret"].AsString();

                    const auto expires_in = Json["expires_in"].AsInteger();

                    Context.Tokens().Values("module_token", Json["access_token"].AsString());

                    Provider.KeyStatus(ksSuccess);

                    Context.SetFixedDate(0);
                    Context.SetCheckDate(Now() + (CDateTime) (expires_in - 300) / SecsPerDay);

                    ModuleStatus(Context);
                } else {
                    Provider.KeyStatus(ksFailed);
                }

                return true;
            };

            auto OnFail = [&Context, &Provider](CTCPConnection *Sender, const Delphi::Exception::Exception &E) {
                Context.SetStatus(csInitialized);

                auto pConnection = dynamic_cast<CHTTPClientConnection *> (Sender);
                auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());

                Provider.KeyStatus(ksFailed);

                DebugReply(pConnection->Reply());

                Log()->Error(APP_LOG_ERR, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
            };

            auto OnHTTPClient = [this](const CLocation &URI) {
                return GetClient(URI.hostname, URI.port);
            };

            CString server_uri(Context.URL().Origin());

            const auto &token_uri = Provider.TokenURI(Application);
            const auto &service_token = CToken::CreateToken(Provider, Application);

            Context.Tokens().Values("service_token", service_token);

            if (!token_uri.IsEmpty()) {
                CToken::FetchAccessToken(token_uri.front() == '/' ? server_uri + token_uri : token_uri, service_token, OnHTTPClient, OnDone, OnFail);
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::UpdateAccessToken(CContext &Context, CProvider &Provider, const CString &Application) {

            auto OnDone = [this, &Context, &Provider](CTCPConnection *Sender) {

                const auto pConnection = dynamic_cast<CHTTPClientConnection *> (Sender);
                const auto &Reply = pConnection->Reply();

                DebugReply(Reply);

                if (Reply.Status == CHTTPReply::ok) {
                    const CJSON Json(Reply.Content);

                    Context.Session() = Json["session"].AsString();
                    Context.Secret() = Json["secret"].AsString();

                    const auto expires_in = Json["expires_in"].AsInteger();

                    Context.Tokens().Values("module_token", Json["access_token"].AsString());

                    Provider.KeyStatus(ksSuccess);

                    Context.SetFixedDate(0);
                    Context.SetCheckDate(Now() + (CDateTime) (expires_in - 300) / SecsPerDay);

                    ModuleStatus(Context);
                } else {
                    Provider.KeyStatus(ksFailed);
                }

                return true;
            };

            auto OnFail = [&Context, &Provider](CTCPConnection *Sender, const Delphi::Exception::Exception &E) {
                Context.SetStatus(csInitialized);

                auto pConnection = dynamic_cast<CHTTPClientConnection *> (Sender);
                auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());

                Provider.KeyStatus(ksFailed);

                DebugReply(pConnection->Reply());

                Log()->Error(APP_LOG_ERR, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
            };

            auto OnHTTPClient = [this](const CLocation &URI) {
                return GetClient(URI.hostname, URI.port);
            };
            //----------------------------------------------------------------------------------------------------------

            CString server_uri(Context.URL().Origin());

            const auto &client_id = Provider.ClientId(Application);
            const auto &secret = Provider.Secret(Application);
            const auto &token_uri = Provider.TokenURI(Application);

            if (!token_uri.IsEmpty()) {
                CToken::ExchangeAccessToken(token_uri.front() == '/' ? server_uri + token_uri : token_uri, client_id, secret, Context.Tokens()["module_token"], OnHTTPClient, OnDone, OnFail);
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::CheckProviders(CContext &Context, CDateTime Now) {
            for (int i = 0; i < Context.Providers().Count(); i++) {
                auto &Provider = Context.Providers()[i].Value();
                for (int j = 0; j < Provider.Applications().Count(); ++j) {
                    const auto &caApplication = Provider.Applications().Members(j);
                    if (caApplication["type"].AsString() == "service_account") {
                        if (Provider.KeyStatus() != ksFetching) {
                            Provider.KeyStatusTime(Now);
                            Provider.KeyStatus(ksFetching);
                            const auto &module_token = Context.Tokens()["module_token"];
                            if (module_token.empty()) {
                                CreateAccessToken(Context, Provider, caApplication.String());
                            } else {
                                UpdateAccessToken(Context, Provider, caApplication.String());
                            }
                        }
                    }
                }
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::CheckDeal(const CDeal &Deal) {

            const auto &Data = Deal.Data();

            const auto DateTime = UTC();
            const auto Date = StringToDate(Data.Date);
            //const auto Sum = BTCToDouble(Data.Payment.Sum);

            if (Data.Order == doCreate) {
                if (DateTime < Date)
                    throw ExceptionFrm("Invalid deal date.");

                if ((DateTime - Date) > (CDateTime) 180 / SecsPerDay)
                    throw ExceptionFrm("Deal date expired.");
            }

            if (Data.Order == doComplete) {
                const CDateTime LeaveBefore = StringToDate(Data.FeedBack.LeaveBefore);
                if (DateTime > LeaveBefore)
                    throw ExceptionFrm("Date feedback expired.");
            }

            if (Odd(int(Data.Order)) || Data.Order == doExecute || Data.Order == doDelete)
                throw ExceptionFrm("Invalid \"order\" value for deal module.");

            if (Data.Order == doCancel) {
                CString message(Data.Payment.Address);
                if (!Data.FeedBack.Comments.IsEmpty()) {
                    message += LINEFEED;
                    message += Data.FeedBack.Comments;
                }

                if (Data.Seller.Signature.IsEmpty() || !VerifyMessage(message, Data.Seller.Address, Data.Seller.Signature))
                    throw ExceptionFrm("The deal is not signed by the seller.");
            }

            if (Data.Order == doFeedback) {
                CString message(Data.Payment.Address);
                if (!Data.FeedBack.Comments.IsEmpty()) {
                    message += LINEFEED;
                    message += Data.FeedBack.Comments;
                }

                if (Data.Customer.Signature.IsEmpty() || !VerifyMessage(message, Data.Customer.Address, Data.Customer.Signature))
                    throw ExceptionFrm("The deal is not signed by the customer.");
            }

            if (!valid_address(Data.Seller.Address))
                throw ExceptionFrm("Invalid Seller address: %s.", Data.Seller.Address.c_str());

            if (!valid_address(Data.Customer.Address))
                throw ExceptionFrm("Invalid Customer address: %s.", Data.Customer.Address.c_str());

            if (!valid_address(Data.Payment.Address))
                throw ExceptionFrm("Invalid Payment address: %s.", Data.Payment.Address.c_str());
        }
        //--------------------------------------------------------------------------------------------------------------

        CString CCustomModule::GetParam(const CHTTPRequest &Request, const CString &Key) {
            CString Result;

            Result = Request.Params[Key];

            if (Result.IsEmpty() && !Request.Content.IsEmpty()) {
                const auto &contentType = Request.Headers.Values(_T("content-type"));
                if (contentType.Find("application/x-www-form-urlencoded") == 0) {
                    const CStringList &FormData = Request.FormData;
                    Result = FormData[Key];
                } else if (contentType.Find("multipart/form-data") == 0) {
                    CFormData FormData;
                    CHTTPRequestParser::ParseFormData(Request, FormData);
                    Result = FormData.Data(Key);
                } else if (contentType.Find("application/json") == 0) {
                    const CJSON contextJson(Request.Content);
                    Result = contextJson[Key].AsString();
                }
            }

            return Result;
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::DoGet(CHTTPServerConnection *AConnection) {

            const auto &caRequest = AConnection->Request();

            CString sPath(caRequest.Location.pathname);

            // Request sPath must be absolute and not contain "..".
            if (sPath.empty() || sPath.front() != '/' || sPath.find(_T("..")) != CString::npos) {
                AConnection->SendStockReply(CHTTPReply::bad_request);
                return;
            }

            if (sPath.SubString(0, 5) == "/api/") {
                DoAPI(AConnection);
                return;
            }

            CStringList TryFiles;

            TryFiles.Add("/index.html");

            SendResource(AConnection, sPath, nullptr, false, TryFiles);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::DoPost(CHTTPServerConnection *AConnection) {

            const auto &caRequest = AConnection->Request();
            auto &Reply = AConnection->Reply();

            Reply.ContentType = CHTTPReply::json;

            CStringList slRouts;
            SplitColumns(caRequest.Location.pathname, slRouts, '/');

            if (slRouts.Count() < 3) {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            const auto &caService = slRouts[0].Lower();
            const auto &caVersion = slRouts[1].Lower();
            const auto &caCommand = slRouts[2].Lower();
            const auto &caAction = slRouts.Count() == 4 ? slRouts[3].Lower() : "";

            if (caService != "api") {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            if (caVersion != "v1") {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            CString sRoute;
            for (int i = 0; i < slRouts.Count(); ++i) {
                sRoute.Append('/');
                sRoute.Append(slRouts[i]);
            }

            try {
                if (caCommand == "account" && (caAction == "status" || caAction == "new" || caAction == "add" || caAction == "update" || caAction == "delete")) {
                    DoAccount(AConnection, "POST", sRoute);
                } else if (caCommand == "deal" && slRouts.Count() == 4 && (caAction.IsEmpty() || caAction == "status" || caAction == "create" || caAction == "pay" || caAction == "complete" || caAction == "cancel" || caAction == "feedback")) {
                    DoDeal(AConnection, "POST", sRoute, caAction);
                } else if (caCommand == "signature") {
                    DoSignature(AConnection);
                } else {
                    DoProxy(AConnection, "POST", sRoute);
                }
            } catch (std::exception &e) {
                ReplyError(AConnection, CHTTPReply::bad_request, e.what());
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::DoAPI(CHTTPServerConnection *AConnection) {

            auto &Request = AConnection->Request();
            auto &Reply = AConnection->Reply();

            Reply.ContentType = CHTTPReply::json;

            CStringList slRouts;
            SplitColumns(Request.Location.pathname, slRouts, '/');

            if (slRouts.Count() < 3) {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            const auto &caService = slRouts[0].Lower();
            const auto &caVersion = slRouts[1].Lower();
            const auto &caCommand = slRouts[2].Lower();
            const auto &caAction = slRouts.Count() == 4 ? slRouts[3].Lower() : "";

            if (caService != "api") {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            if (caVersion != "v1") {
                AConnection->SendStockReply(CHTTPReply::not_found);
                return;
            }

            CString sRoute;
            for (int i = 0; i < slRouts.Count(); ++i) {
                sRoute.Append('/');
                sRoute.Append(slRouts[i]);
            }

            try {
                if (caCommand == "ping") {

                    AConnection->SendStockReply(CHTTPReply::ok);

                } else if (caCommand == "time") {

                    Reply.Content << "{\"serverTime\": " << ToString(MsEpoch()) << "}";

                    AConnection->SendReply(CHTTPReply::ok);

                } else if (caCommand == "help") {

                    Request.Content.Clear();

                    DoAccount(AConnection, "GET", sRoute);

                } else if (caCommand == "account" && caAction == "status") {

                    Request.Content.Clear();

                    DoAccount(AConnection, "GET", sRoute);

                } else if (caCommand == "deal" && caAction == "status") {

                    Request.Content.Clear();

                    DoDeal(AConnection, "GET", sRoute, caAction);

                } else if (caCommand == "bc" && caAction == "history") {

                    const auto &caAccount = Request.Params["account"];

                    if (caAccount.IsEmpty()) {
                        AConnection->SendStockReply(CHTTPReply::bad_request);
                        return;
                    }

                    try {
                        const wallet::payment_address address(std::string(caAccount.c_str()));

                        CJSON history;
                        fetch_history(address, history);

                        Reply.Content = history.ToString();
                    } catch (Delphi::Exception::Exception &E) {
                        ExceptionToJson(CHTTPReply::bad_request, E, Reply.Content);
                        Log()->Error(APP_LOG_EMERG, 0, E.what());
                    }

                    AConnection->SendReply(CHTTPReply::ok, nullptr, true);

                } else if (caCommand == "bc" && caAction == "header") {

                    const auto &caHeight = Request.Params["height"];
                    const auto &caHash = Request.Params["hash"];

                    if (caHeight.IsEmpty() && caHash.IsEmpty()) {
                        AConnection->SendStockReply(CHTTPReply::bad_request);
                        return;
                    }

                    try {
                        CJSON header;

                        if (!caHash.IsEmpty()) {
                            fetch_header(hash256(std::string(caHash.c_str())), header);
                        } else {
                            uint32_t height = StrToInt(caHeight.c_str());
                            fetch_header(height, header);
                        }

                        Reply.Content = header.ToString();
                    } catch (Delphi::Exception::Exception &E) {
                        ExceptionToJson(CHTTPReply::bad_request, E, Reply.Content);
                        Log()->Error(APP_LOG_EMERG, 0, E.what());
                    }

                    AConnection->SendReply(CHTTPReply::ok, nullptr, true);

                } else {

                    DoProxy(AConnection, "GET", sRoute);

                }

            } catch (std::exception &e) {
                ReplyError(AConnection, CHTTPReply::bad_request, e.what());
            }
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::ModuleService(CContext &Context) {

            Log()->Notice("[%s] [%s] Trying to fetch a OAuth2 configuration file.", Context.Name().c_str(), Context.URL().Origin().c_str());

            auto OnRequest = [this, &Context](CHTTPClient *Sender, CHTTPRequest &Request) {

                Context.SetStatus(Context::csPreparing);

                Request.ContentType = CHTTPRequest::text;

                Apostol::PGP::CleartextSignature(
                        m_pgpModuleKey,
                        m_pgpPassphrase,
                        BPS_PGP_HASH,
                        Context.Name(),
                        Request.Content);

                CHTTPRequest::Prepare(Request, "POST", "/api/v1/dm/service");

                const auto &caModuleAddress = m_Module["address"];
                if (!caModuleAddress.IsEmpty())
                    Request.AddHeader("Module-Address", caModuleAddress);

                DebugRequest(Request);
            };

            auto OnExecute = [&Context](CTCPConnection *AConnection) {

                Context.SetStatus(Context::csInitialization);

                const auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);

                if (pConnection != nullptr) {
                    auto &Reply = pConnection->Reply();

                    DebugReply(Reply);

                    if (Reply.Status == CHTTPReply::ok) {
                        const CJSON OAuth2(Reply.Content);
                        UpdateOAuth2(Context, OAuth2.Object());
                    }

                    pConnection->CloseConnection(true);
                }

                return true;
            };

            auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
                if (pConnection != nullptr) {
                    auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    if (pClient != nullptr) {
                        Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
                    }
                    DebugReply(pConnection->Reply());
                }

                Context.SetStatus(Context::csInitialization);
            };

            auto pClient = GetClient(Context.URL().hostname, Context.URL().port == 0 ? BPS_SERVER_PORT : Context.URL().port);

            pClient->OnRequest(OnRequest);
            pClient->OnExecute(OnExecute);
            pClient->OnException(OnException);

            pClient->AutoFree(true);
            pClient->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::ModuleStatus(CContext &Context) {

            Log()->Notice("[%s] [%s] Trying get module status.", Context.Name().c_str(), Context.URL().Origin().c_str());

            auto OnRequest = [this, &Context](CHTTPClient *Sender, CHTTPRequest &Request) {

                Context.SetStatus(Context::csInProgress);

                const auto &caModuleAddress = m_Module["address"];

                Request.ContentType = CHTTPRequest::text;

                Request.Content.Format(R"({"address": "%s"})", caModuleAddress.c_str());

                CHTTPRequest::Prepare(Request, "POST", "/api/v1/dm/status");

                Request.AddHeader("Authorization", "Bearer " + Context.Tokens()["module_token"]);

                if (!caModuleAddress.IsEmpty())
                    Request.AddHeader("Module-Address", caModuleAddress);

                DebugRequest(Request);
            };

            auto OnExecute = [this, &Context](CTCPConnection *AConnection) {

                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);

                if (pConnection != nullptr) {
                    auto &Reply = pConnection->Reply();

                    DebugReply(Reply);

                    if (Reply.Status == CHTTPReply::ok) {
                        ModuleAuthorize(Context);
                    } else {
                        ModuleNew(Context);
                    }

                    pConnection->CloseConnection(true);
                }

                return true;
            };

            auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
                if (pConnection != nullptr) {
                    auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    if (pClient != nullptr) {
                        Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
                    }
                    DebugReply(pConnection->Reply());
                }

                Context.SetStatus(Context::csInitialized);
            };

            auto pClient = GetClient(Context.URL().hostname, Context.URL().port == 0 ? BPS_SERVER_PORT : Context.URL().port);

            pClient->OnRequest(OnRequest);
            pClient->OnExecute(OnExecute);
            pClient->OnException(OnException);

            pClient->AutoFree(true);
            pClient->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::ModuleNew(CContext &Context) {

            Log()->Notice("[%s] [%s] Trying create new module.", Context.Name().c_str(), Context.URL().Origin().c_str());

            auto OnRequest = [this, &Context](CHTTPClient *Sender, CHTTPRequest &Request) {

                Context.SetStatus(Context::csInProgress);

                Request.ContentType = CHTTPRequest::json;

                const auto &caModuleAddress = m_Module["address"];

                CJSON Json(jvtObject);

                Json.Object().AddPair("address", caModuleAddress);
                Json.Object().AddPair("bitmessage", Context.Name());

                const OpenPGP::SecretKey pgpSecret(m_pgpModuleKey.c_str());
                const auto &public_key = pgpSecret.get_public();

                Json.Object().AddPair("pgp", public_key.write(OpenPGP::PGP::Armored::YES));

                Request.Content = Json.ToString();

                CHTTPRequest::Prepare(Request, "POST", "/api/v1/client/new");

                Request.AddHeader("Authorization", "Bearer " + Context.Tokens()["module_token"]);

                if (!caModuleAddress.IsEmpty())
                    Request.AddHeader("Module-Address", caModuleAddress);

                DebugRequest(Request);
            };

            auto OnExecute = [this, &Context](CTCPConnection *AConnection) {

                Context.SetStatus(Context::csInitialized);

                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);

                if (pConnection != nullptr) {
                    auto &Reply = pConnection->Reply();

                    DebugReply(Reply);

                    if (Reply.Status == CHTTPReply::ok) {
                        const CJSON Json(Reply.Content);

                        if (Json.HasOwnProperty("result")) {
                            const auto &caResult = Json["result"];
                            if (caResult.HasOwnProperty("success")) {
                                if (caResult["success"].AsBoolean()) {
                                    ModuleAuthorize(Context);
                                } else {
                                    if (caResult.HasOwnProperty("message")) {
                                        Log()->Error(APP_LOG_EMERG, 0, "[%s] [%s] %s", Context.Name().c_str(),
                                                     Context.URL().Origin().c_str(), caResult["message"].AsString().c_str());
                                    }
                                }
                            }
                        }
                    }

                    pConnection->CloseConnection(true);
                }

                return true;
            };

            auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
                if (pConnection != nullptr) {
                    auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    if (pClient != nullptr) {
                        Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
                    }
                    DebugReply(pConnection->Reply());
                }

                Context.SetStatus(Context::csInitialized);
            };

            auto pClient = GetClient(Context.URL().hostname, Context.URL().port == 0 ? BPS_SERVER_PORT : Context.URL().port);

            pClient->OnRequest(OnRequest);
            pClient->OnExecute(OnExecute);
            pClient->OnException(OnException);

            pClient->AutoFree(true);
            pClient->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

        void CCustomModule::ModuleAuthorize(CContext &Context) {

            Log()->Notice("[%s] [%s] Trying authorize module.", Context.Name().c_str(), Context.URL().Origin().c_str());

            auto OnRequest = [this, &Context](CHTTPClient *Sender, CHTTPRequest &Request) {

                Context.SetStatus(Context::csAuthorization);

                Request.ContentType = CHTTPRequest::text;

                Apostol::PGP::CleartextSignature(
                        m_pgpModuleKey,
                        m_pgpPassphrase,
                        BPS_PGP_HASH,
                        Context.Name(),
                        Request.Content);

                CHTTPRequest::Prepare(Request, "POST", "/api/v1/dm/authorize");

                Request.AddHeader("Authorization", "Bearer " + Context.Tokens()["module_token"]);

                const auto &access_token = Context.Tokens()["access_token"];
                if (!access_token.IsEmpty())
                    Request.AddHeader("Subject-Token", access_token);

                const auto &caModuleAddress = m_Module["address"];
                if (!caModuleAddress.IsEmpty())
                    Request.AddHeader("Module-Address", caModuleAddress);

                DebugRequest(Request);
            };

            auto OnExecute = [&Context](CTCPConnection *AConnection) {

                Context.SetStatus(Context::csInitialized);

                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);

                if (pConnection != nullptr) {
                    auto &Reply = pConnection->Reply();

                    DebugReply(Reply);

                    const CJSON Json(Reply.Content);

                    if (Reply.Status == CHTTPReply::ok) {
                        Context.Session() = Json["session"].AsString();
                        Context.Secret() = Json["secret"].AsString();

                        Context.Tokens().Values("access_token", Json["access_token"].AsString());

                        Context.SetStatus(csAuthorized);
                    } else {
                        Context.SetCheckDate(Now() + (CDateTime) 5 / MinsPerDay); // 5 min
                        if (Json.HasOwnProperty("error")) {
                            const auto &error = Json["error"];
                            if (error.HasOwnProperty("message")) {
                                Log()->Error(APP_LOG_EMERG, 0, "[%s] [%s] %s", Context.Name().c_str(),
                                             Context.URL().Origin().c_str(), error["message"].AsString().c_str());
                            }
                        }
                    }

                    pConnection->CloseConnection(true);
                }

                return true;
            };

            auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) {
                auto pConnection = dynamic_cast<CHTTPClientConnection *> (AConnection);
                if (pConnection != nullptr) {
                    auto pClient = dynamic_cast<CHTTPClient *> (pConnection->Client());
                    if (pClient != nullptr) {
                        Log()->Error(APP_LOG_EMERG, 0, "[%s:%d] %s", pClient->Host().c_str(), pClient->Port(), E.what());
                    }
                    DebugReply(pConnection->Reply());
                }

                Context.SetStatus(Context::csInitialized);
            };

            auto pClient = GetClient(Context.URL().hostname, Context.URL().port == 0 ? BPS_SERVER_PORT : Context.URL().port);

            pClient->OnRequest(OnRequest);
            pClient->OnExecute(OnExecute);
            pClient->OnException(OnException);

            pClient->AutoFree(true);
            pClient->Active(true);
        }
        //--------------------------------------------------------------------------------------------------------------

    } // namespace Common

} // namespace Apostol

#ifdef  __cplusplus
}
#endif // __cplusplus
