/*++ Program name: dm Module Name: WebSocket.cpp Notices: Module: WebSocket Author: Copyright (c) Prepodobny Alen mailto: alienufo@inbox.ru mailto: ufocomp@gmail.com --*/ //---------------------------------------------------------------------------------------------------------------------- #include "Core.hpp" #include "WebSocket.hpp" //---------------------------------------------------------------------------------------------------------------------- #include //---------------------------------------------------------------------------------------------------------------------- #define BPS_PGP_HASH "SHA512" #define BM_PREFIX "BM-" //---------------------------------------------------------------------------------------------------------------------- #define SYSTEM_PROVIDER_NAME "system" #define SERVICE_APPLICATION_NAME "service" #define CONFIG_SECTION_NAME "module" //---------------------------------------------------------------------------------------------------------------------- #define NOT_FOUND_ACTIVE_CONNECTION "Not found active connection. Try again later." //---------------------------------------------------------------------------------------------------------------------- extern "C++" { namespace Apostol { namespace Module { //-------------------------------------------------------------------------------------------------------------- //-- CWebSocketModule ------------------------------------------------------------------------------------------ //-------------------------------------------------------------------------------------------------------------- CWebSocketModule::CWebSocketModule(CModuleProcess *AProcess) : CApostolModule(AProcess, "web socket", "module/WebSocket") { m_SyncPeriod = BPS_DEFAULT_SYNC_PERIOD; m_Headers.Add("Authorization"); CWebSocketModule::InitMethods(); InitServerList(); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::InitMethods() { #if defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 9) m_pMethods->AddObject(_T("GET") , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoGet(Connection); })); m_pMethods->AddObject(_T("POST") , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoPost(Connection); })); m_pMethods->AddObject(_T("HEAD") , (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoHead(Connection); })); m_pMethods->AddObject(_T("OPTIONS"), (CObject *) new CMethodHandler(true , [this](auto && Connection) { DoOptions(Connection); })); m_pMethods->AddObject(_T("PUT") , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); })); m_pMethods->AddObject(_T("DELETE") , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); })); m_pMethods->AddObject(_T("TRACE") , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); })); m_pMethods->AddObject(_T("PATCH") , (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); })); m_pMethods->AddObject(_T("CONNECT"), (CObject *) new CMethodHandler(false, [this](auto && Connection) { MethodNotAllowed(Connection); })); #else m_pMethods->AddObject(_T("GET") , (CObject *) new CMethodHandler(true, std::bind(&CWebSocketModule::DoGet, this, _1))); m_pMethods->AddObject(_T("POST") , (CObject *) new CMethodHandler(true, std::bind(&CWebSocketModule::DoPost, this, _1))); m_pMethods->AddObject(_T("HEAD") , (CObject *) new CMethodHandler(true, std::bind(&CWebSocketModule::DoHead, this, _1))); m_pMethods->AddObject(_T("OPTIONS"), (CObject *) new CMethodHandler(true, std::bind(&CWebSocketModule::DoOptions, this, _1))); m_pMethods->AddObject(_T("PUT") , (CObject *) new CMethodHandler(false, std::bind(&CWebSocketModule::MethodNotAllowed, this, _1))); m_pMethods->AddObject(_T("DELETE") , (CObject *) new CMethodHandler(false, std::bind(&CWebSocketModule::MethodNotAllowed, this, _1))); m_pMethods->AddObject(_T("TRACE") , (CObject *) new CMethodHandler(false, std::bind(&CWebSocketModule::MethodNotAllowed, this, _1))); m_pMethods->AddObject(_T("PATCH") , (CObject *) new CMethodHandler(false, std::bind(&CWebSocketModule::MethodNotAllowed, this, _1))); m_pMethods->AddObject(_T("CONNECT"), (CObject *) new CMethodHandler(false, std::bind(&CWebSocketModule::MethodNotAllowed, this, _1))); #endif } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::InitServerList() { m_DefaultServer.Value().Name() = m_DefaultServer.Name(); m_DefaultServer.Value().PGP().Name = "PUBLIC"; if (m_Servers.Count() == 0) { #ifdef _DEBUG int index = m_Servers.AddPair(BPS_BM_DEBUG_ADDRESS, CClientContext(CLocation(BPS_SERVER_URL))); m_Servers[index].Value().Name() = m_Servers[index].Name(); m_Servers[index].Value().PGP().Name = "PUBLIC"; #else m_Servers.Add(m_DefaultServer); #endif } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::UpdateServerList(const CString &Key) { CStringPairs ServerList; CStringList Keys; int index; ParsePGPKey(Key, ServerList, Keys); if (ServerList.Count() != 0) { CStringPairs::ConstEnumerator em(ServerList); while (em.MoveNext()) { const auto &caCurrent = em.Current(); index = m_Servers.IndexOfName(caCurrent.Name()); if (index == -1) { index = m_Servers.AddPair(caCurrent.Name(), CClientContext(CLocation(caCurrent.Value()))); } auto &Context = m_Servers[index].Value(); Context.Name() = caCurrent.Name(); Context.PGP().Name = "PUBLIC"; Context.PGP().Key = Key; Context.BTCKeys() = Keys; UpdateOAuth2(Context, m_OAuth2.Object()); } } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::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 *ARequest) { Context.SetStatus(Context::csInProgress); const auto &caModuleAddress = m_Module["address"]; ARequest->ContentType = CHTTPRequest::text; ARequest->Content.Format(R"({"address": "%s"})", caModuleAddress.c_str()); CHTTPRequest::Prepare(ARequest, "POST", "/api/v1/dm/status"); ARequest->AddHeader("Authorization", "Bearer " + Context.Tokens()["access_token"]); if (!caModuleAddress.IsEmpty()) ARequest->AddHeader("Module-Address", caModuleAddress); DebugRequest(ARequest); }; auto OnExecute = [this, &Context](CTCPConnection *AConnection) { auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pReply = pConnection->Reply(); DebugReply(pReply); if (pReply->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 (AConnection); if (pConnection != nullptr) { auto pClient = dynamic_cast (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 CWebSocketModule::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 *ARequest) { Context.SetStatus(Context::csPreparing); ARequest->ContentType = CHTTPRequest::text; Apostol::PGP::CleartextSignature( m_pgpModuleKey, m_pgpPassphrase, BPS_PGP_HASH, Context.Name(), ARequest->Content); CHTTPRequest::Prepare(ARequest, "POST", "/api/v1/dm/service"); const auto &caModuleAddress = m_Module["address"]; if (!caModuleAddress.IsEmpty()) ARequest->AddHeader("Module-Address", caModuleAddress); DebugRequest(ARequest); }; auto OnExecute = [&Context](CTCPConnection *AConnection) { Context.SetStatus(Context::csInitialization); auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pReply = pConnection->Reply(); DebugReply(pReply); if (pReply->Status == CHTTPReply::ok) { const CJSON OAuth2(pReply->Content); UpdateOAuth2(Context, OAuth2.Object()); } pConnection->CloseConnection(true); } return true; }; auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) { auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pClient = dynamic_cast (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 CWebSocketModule::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 *ARequest) { Context.SetStatus(Context::csInProgress); ARequest->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)); ARequest->Content = Json.ToString(); CHTTPRequest::Prepare(ARequest, "POST", "/api/v1/client/new"); ARequest->AddHeader("Authorization", "Bearer " + Context.Tokens()["access_token"]); if (!caModuleAddress.IsEmpty()) ARequest->AddHeader("Module-Address", caModuleAddress); DebugRequest(ARequest); }; auto OnExecute = [this, &Context](CTCPConnection *AConnection) { Context.SetStatus(Context::csInitialized); auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pReply = pConnection->Reply(); DebugReply(pReply); if (pReply->Status == CHTTPReply::ok) { const CJSON Json(pReply->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 (AConnection); if (pConnection != nullptr) { auto pClient = dynamic_cast (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 CWebSocketModule::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 *ARequest) { Context.SetStatus(Context::csAuthorization); ARequest->ContentType = CHTTPRequest::text; Apostol::PGP::CleartextSignature( m_pgpModuleKey, m_pgpPassphrase, BPS_PGP_HASH, Context.Name(), ARequest->Content); CHTTPRequest::Prepare(ARequest, "POST", "/api/v1/dm/authorize"); ARequest->AddHeader("Authorization", "Bearer " + Context.Tokens()["access_token"]); const auto &caModuleAddress = m_Module["address"]; if (!caModuleAddress.IsEmpty()) ARequest->AddHeader("Module-Address", caModuleAddress); DebugRequest(ARequest); }; auto OnExecute = [&Context](CTCPConnection *AConnection) { Context.SetStatus(Context::csInitialized); auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pReply = pConnection->Reply(); DebugReply(pReply); if (pReply->Status == CHTTPReply::ok) { const CJSON Json(pReply->Content); Context.Session() = Json["session"].AsString(); Context.Secret() = Json["secret"].AsString(); Context.Tokens().Values("access_token", Json["access_token"].AsString()); Context.SetFixedDate(0); Context.SetCheckDate(Now() + (CDateTime) 55 / MinsPerDay); // 55 min Context.SetStatus(csAuthorized); } pConnection->CloseConnection(true); } return true; }; auto OnException = [&Context](CTCPConnection *AConnection, const Delphi::Exception::Exception &E) { auto pConnection = dynamic_cast (AConnection); if (pConnection != nullptr) { auto pClient = dynamic_cast (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 CWebSocketModule::UpdateOAuth2(CContext &Context, const CJSONObject &OAuth2) { if (OAuth2["type"].AsString() == "service_account") { UpdateProviders(Context.Providers(), OAuth2); Context.SetCheckDate(0); Context.SetStatus(Context::csInitialized); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::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); } //-------------------------------------------------------------------------------------------------------------- bool CWebSocketModule::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; } //-------------------------------------------------------------------------------------------------------------- CWebSocketClient *CWebSocketModule::GetWebSocketClient(CClientContext &Context) { auto pClient = Context.ClientManager().Add(&Context, CLocation(Context.URL().Origin() + "/session/" + Context.Session())); pClient->Session() = Context.Session(); pClient->ClientName() = GApplication->Title(); pClient->AutoConnect(false); pClient->AllocateEventHandlers(Server()); #if defined(_GLIBCXX_RELEASE) && (_GLIBCXX_RELEASE >= 9) pClient->OnVerbose([this](auto && Sender, auto && AConnection, auto && AFormat, auto && args) { DoVerbose(Sender, AConnection, AFormat, args); }); pClient->OnException([this](auto && AConnection, auto && AException) { DoException(AConnection, AException); }); pClient->OnEventHandlerException([this](auto && AHandler, auto && AException) { DoEventHandlerException(AHandler, AException); }); pClient->OnNoCommandHandler([this](auto && Sender, auto && AData, auto && AConnection) { DoNoCommandHandler(Sender, AData, AConnection); }); pClient->OnWebSocketError([this](auto && AConnection) { DoWebSocketError(AConnection); }); pClient->OnConnected([this](auto && Sender) { DoClientConnected(Sender); }); pClient->OnDisconnected([this](auto && Sender) { DoClientDisconnected(Sender); }); pClient->OnMessage([this](auto && Sender, auto && Message) { DoClientMessage(Sender, Message); }); pClient->OnError([this](auto && Sender, int Code, auto && Message) { DoClientError(Sender, Code, Message); }); pClient->OnHeartbeat([this](auto && Sender) { DoClientHeartbeat(Sender); }); pClient->OnTimeOut([this](auto && Sender) { DoClientTimeOut(Sender); }); pClient->OnAuthorize([this](auto && Sender, auto && Request, auto && Response) { DoAuthorizeEvent(Sender, Request, Response); }); pClient->OnSubscribe([this](auto && Sender, auto && Request, auto && Response) { DoSubscribeEvent(Sender, Request, Response); }); pClient->OnKey([this](auto && Sender, auto && Request, auto && Response) { DoKeyEvent(Sender, Request, Response); }); pClient->OnWebSocketEvent([this](auto && Sender, auto && Request, auto && Response) { DoWebSocketClientEvent(Sender, Request, Response); }); #else pClient->OnVerbose(std::bind(&CWebSocketModule::DoVerbose, this, _1, _2, _3, _4)); pClient->OnException(std::bind(&CWebSocketModule::DoException, this, _1, _2)); pClient->OnEventHandlerException(std::bind(&CWebSocketModule::DoEventHandlerException, this, _1, _2)); pClient->OnNoCommandHandler(std::bind(&CWebSocketModule::DoNoCommandHandler, this, _1, _2, _3)); pClient->OnWebSocketError(std::bind(&CWebSocketModule::DoWebSocketError, this, _1)); pClient->OnConnected(std::bind(&CWebSocketModule::DoClientConnected, this, _1)); pClient->OnDisconnected(std::bind(&CWebSocketModule::DoClientDisconnected, this, _1)); pClient->OnMessage(std::bind(&CWebSocketModule::DoClientMessage, this, _1, _2)); pClient->OnError(std::bind(&CWebSocketModule::DoClientError, this, _1, _2, _3)); pClient->OnHeartbeat(std::bind(&CWebSocketModule::DoClientHeartbeat, this, _1)); pClient->OnTimeOut(std::bind(&CWebSocketModule::DoClientTimeOut, this, _1)); pClient->OnAuthorize(std::bind(&CWebSocketModule::DoAuthorizeEvent, this, _1, _2, _3)); pClient->OnSubscribe(std::bind(&CWebSocketModule::DoSubscribeEvent, this, _1, _2, _3)); pClient->OnKey(std::bind(&CWebSocketModule::DoKeyEvent, this, _1, _2, _3)); pClient->OnWebSocketEvent(std::bind(&CWebSocketModule::DoWebSocketClientEvent, this, _1, _2, _3)); #endif return pClient; } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::CreateWebSocketClient(CClientContext &Context) { auto pClient = GetWebSocketClient(Context); try { pClient->Active(true); Context.SetFixedDate(0); Context.SetStatus(csRunning); } catch (std::exception &e) { Log()->Error(APP_LOG_ERR, 0, e.what()); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::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) { 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 CWebSocketModule::CreateAccessToken(const CProvider &Provider, const CString &Application, CClientContext &Context) { auto OnDone = [this, &Context](CTCPConnection *Sender) { auto pConnection = dynamic_cast (Sender); auto pReply = pConnection->Reply(); DebugReply(pReply); if (pReply->Status == CHTTPReply::ok) { const CJSON Json(pReply->Content); Context.Session() = Json["session"].AsString(); Context.Secret() = Json["secret"].AsString(); Context.Tokens().Values("access_token", Json["access_token"].AsString()); ModuleStatus(Context); } return true; }; 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); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::FetchProviders(CDateTime Now, CClientContext &Context) { 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 &app = Provider.Applications().Members(j); if (app["type"].AsString() == "service_account") { if (Provider.KeyStatus() == ksUnknown) { CreateAccessToken(Provider, app.String(), Context); Provider.KeyStatusTime(Now); Provider.KeyStatus(ksSuccess); } } } } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::CheckProviders(CDateTime Now, CClientContext &Context) { for (int i = 0; i < Context.Providers().Count(); i++) { auto& Provider = Context.Providers()[i].Value(); if (Provider.KeyStatus() != ksUnknown) { Provider.KeyStatusTime(Now); Provider.KeyStatus(ksUnknown); } } } //-------------------------------------------------------------------------------------------------------------- CWebSocketClient *CWebSocketModule::GetConnectedClient(const CClientContext &Context) { for (int i = 0; i < Context.ClientManager().Count(); i++) { auto pClient = Context.ClientManager()[i]; if (pClient->Connected()) return pClient; } return nullptr; } //-------------------------------------------------------------------------------------------------------------- int CWebSocketModule::CurrentContextIndex(const CString &Params) { int index = 0; while (index < m_Servers.Count()) { const auto &caContext = m_Servers[index].Value(); if (caContext.Status() == Context::csRunning) { if (Params.IsEmpty() || Params == caContext.URL().Origin()) { auto pClient = GetConnectedClient(caContext); if (pClient != nullptr) return index; } } index++; } return -1; } //-------------------------------------------------------------------------------------------------------------- CWebSocketClient *CWebSocketModule::GetConnectedClient(const CString &Params) { int index = 0; while (index < m_Servers.Count()) { const auto &caContext = m_Servers[index].Value(); if (caContext.Status() == Context::csRunning) { if (Params.IsEmpty() || Params == caContext.URL().Origin()) { auto pClient = GetConnectedClient(caContext); if (pClient != nullptr) return pClient; } } index++; } return nullptr; } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientConnected(CObject *Sender) { auto pConnection = dynamic_cast(Sender); if (pConnection != nullptr) { auto pBinding = pConnection->Socket()->Binding(); if (pBinding != nullptr) { Log()->Notice(_T("[%s:%d] [%s] WebSocket client connected."), pConnection->Socket()->Binding()->IP(), pConnection->Socket()->Binding()->Port(), pConnection->Session().c_str()); } else { Log()->Notice(_T("[%s] WebSocket client connected."), pConnection->Session().c_str()); } } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientDisconnected(CObject *Sender) { auto pConnection = dynamic_cast(Sender); if (pConnection != nullptr) { auto pBinding = pConnection->Socket()->Binding(); if (pBinding != nullptr) { Log()->Notice(_T("[%s:%d] [%s] WebSocket client disconnected."), pConnection->Socket()->Binding()->IP(), pConnection->Socket()->Binding()->Port(), pConnection->Session().c_str()); } else { Log()->Notice(_T("[%s] WebSocket client disconnected."), pConnection->Session().c_str()); } } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientHeartbeat(CObject *Sender) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientTimeOut(CObject *Sender) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); pClient->SwitchConnection(nullptr); pClient->Reload(); pContext->SetFixedDate(0); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientMessage(CObject *Sender, const CWSMessage &Message) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); Log()->Message("[%s] [%s] [%s] [%s] %s", pClient->Session().c_str(), Message.UniqueId.c_str(), Message.Action.IsEmpty() ? "Unknown" : Message.Action.c_str(), CWSMessage::MessageTypeIdToString(Message.MessageTypeId).c_str(), Message.Payload.IsNull() ? "{}" : Message.Payload.ToString().c_str()); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoClientError(CObject *Sender, int Code, const CString &Message) { Log()->Error(APP_LOG_ERR, 0, "[%d] %s", Code, Message.c_str()); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoAuthorizeEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoSubscribeEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoKeyEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); pContext->PGP().StatusTime = Now(); pContext->PGP().Status = CKeyContext::ksSuccess; pContext->PGP().RunTime = GetRandomDate(10 * 60, m_SyncPeriod * 60, pContext->PGP().StatusTime); // 10..m_SyncPeriod min if (Response.Payload.HasOwnProperty("data")) { UpdateServerList(Response.Payload["data"].AsString()); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoWebSocketClientEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) { auto pClient = dynamic_cast (Sender); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoWebSocketError(CTCPConnection *AConnection) { auto pConnection = dynamic_cast (AConnection); auto pClient = dynamic_cast (pConnection->Client()); chASSERT(pClient); auto pContext = pClient->Context(); chASSERT(pContext); auto pReply = pConnection->Reply(); if (pReply->Status == CHTTPReply::moved_permanently || pReply->Status == CHTTPReply::moved_temporarily) { const auto &caLocation = pReply->Headers["Location"]; if (!caLocation.IsEmpty()) { pClient->SetURI(CLocation(caLocation)); Log()->Notice(_T("[%s] Redirect to %s."), pClient->Session().c_str(), pClient->URI().href().c_str()); } pContext->SetFixedDate(0); } else { auto pBinding = pConnection->Socket()->Binding(); if (pBinding != nullptr) { Log()->Warning(_T("[%s:%d] [%s] WebSocket client failed to establish connection"), pConnection->Socket()->Binding()->IP(), pConnection->Socket()->Binding()->Port(), pConnection->Session().c_str()); } else { Log()->Warning(_T("[%s] WebSocket client failed to establish connection."), pConnection->Session().c_str()); } pContext->SetFixedDate(Now() + (CDateTime) 1 / MinsPerDay); // 1 min } pConnection->CloseConnection(true); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoAccount(CHTTPServerConnection *AConnection, const CString &Method, const CString &URI) { auto OnRequest = [AConnection](CWebSocketMessageHandler *AHandler, CWebSocketConnection *AWSConnection) { auto pReply = AConnection->Reply(); const auto &wsMessage = CCustomWebSocketClient::RequestToMessage(AWSConnection); pReply->ContentType = CHTTPReply::json; if (wsMessage.MessageTypeId == mtCallResult) { pReply->Content = wsMessage.Payload.ToString(); AConnection->SendReply(CHTTPReply::ok, nullptr, true); } else { ReplyError(AConnection, CHTTPReply::bad_request, wsMessage.ErrorMessage); } }; auto pServerRequest = AConnection->Request(); const auto &caModuleAddress = m_Module["address"]; const auto &caHost = pServerRequest->Headers["host"]; const auto &caOrigin = pServerRequest->Headers["origin"]; const auto &caUserAddress = pServerRequest->Params["address"]; const auto &pgpValue = pServerRequest->Params["pgp"]; const auto &caServerParam = pServerRequest->Params["server"]; CStringList caClearText; CString sPayload; if (!pServerRequest->Content.IsEmpty()) { const auto &ContentType = pServerRequest->Headers.Values(_T("content-type")); if (ContentType.Find("application/x-www-form-urlencoded") == 0) { const CStringList &FormData = pServerRequest->FormData; const auto &formDate = FormData["date"]; const auto &formAddress = FormData["address"]; const auto &formBitmessage = FormData["bitmessage"]; const auto &formKey = FormData["key"]; const auto &formPGP = FormData["pgp"]; const auto &formURL = FormData["url"]; const auto &formFlags = FormData["flags"]; const auto &formSign = FormData["sign"]; if (!formDate.IsEmpty()) { caClearText << formDate; } if (!formAddress.IsEmpty()) { caClearText << formAddress; } if (!formFlags.IsEmpty()) { caClearText << formFlags; } if (!formBitmessage.IsEmpty()) { caClearText << formBitmessage; } if (!formKey.IsEmpty()) { caClearText << formKey; } if (!formPGP.IsEmpty()) { caClearText << formPGP; } if (!formURL.IsEmpty()) { caClearText << formURL; } if (!formSign.IsEmpty()) { caClearText << formSign; } } else if (ContentType.Find("multipart/form-data") == 0) { CFormData FormData; CHTTPRequestParser::ParseFormData(pServerRequest, FormData); const auto &formDate = FormData.Data("date"); const auto &formAddress = FormData.Data("address"); const auto &formBitmessage = FormData.Data("bitmessage"); const auto &formKey = FormData.Data("key"); const auto &formPGP = FormData.Data("pgp"); const auto &formURL = FormData.Data("url"); const auto &formFlags = FormData.Data("flags"); const auto &formSign = FormData.Data("sign"); if (!formDate.IsEmpty()) { caClearText << formDate; } if (!formAddress.IsEmpty()) { caClearText << formAddress; } if (!formFlags.IsEmpty()) { caClearText << formFlags; } if (!formBitmessage.IsEmpty()) { caClearText << formBitmessage; } if (!formKey.IsEmpty()) { caClearText << formKey; } if (!formPGP.IsEmpty()) { caClearText << formPGP; } if (!formURL.IsEmpty()) { caClearText << formURL; } if (!formSign.IsEmpty()) { caClearText << formSign; } } else if (ContentType.Find("application/json") == 0) { const CJSON contextJson(pServerRequest->Content); const auto &jsonDate = contextJson["date"].AsString(); const auto &jsonAddress = contextJson["address"].AsString(); const auto &jsonBitmessage = contextJson["bitmessage"].AsString(); const auto &jsonKey = contextJson["key"].AsString(); const auto &jsonPGP = contextJson["pgp"].AsString(); const auto &jsonFlags = contextJson["flags"].AsString(); const auto &jsonSign = contextJson["sign"].AsString(); if (!jsonDate.IsEmpty()) { caClearText << jsonDate; } if (!jsonAddress.IsEmpty()) { caClearText << jsonAddress; } if (!jsonFlags.IsEmpty()) { caClearText << jsonFlags; } if (!jsonBitmessage.IsEmpty()) { caClearText << jsonBitmessage; } if (!jsonKey.IsEmpty()) { caClearText << jsonKey; } if (!jsonPGP.IsEmpty()) { caClearText << jsonPGP; } const CJSONValue &jsonURL = contextJson["url"]; if (jsonURL.IsArray()) { const CJSONArray &arrayURL = jsonURL.Array(); for (int i = 0; i < arrayURL.Count(); i++) { caClearText << arrayURL[i].AsString(); } } if (!jsonSign.IsEmpty()) { caClearText << jsonSign; } } else { caClearText = pServerRequest->Content; } if (pgpValue == "off" || pgpValue == "false") { sPayload = caClearText.Text(); } else { Apostol::PGP::CleartextSignature( m_pgpModuleKey, m_pgpPassphrase, BPS_PGP_HASH, caClearText.Text(), sPayload); } } CJSON Json(jvtObject); CJSONValue Module(jvtObject); Module.Object().AddPair("address", caModuleAddress); Json.Object().AddPair("id", GetUID(16).Lower()); Json.Object().AddPair("host", caHost); if (!caOrigin.IsEmpty()) { Json.Object().AddPair("origin", caOrigin); } Json.Object().AddPair("module", Module); Json.Object().AddPair("address", caUserAddress.IsEmpty() ? caModuleAddress : caUserAddress); if (!sPayload.IsEmpty()) Json.Object().AddPair("payload", base64_encode(sPayload)); auto pClient = GetConnectedClient(caServerParam); if (pClient == nullptr) { throw Delphi::Exception::Exception(NOT_FOUND_ACTIVE_CONNECTION); } pClient->Send(URI, Json, OnRequest); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoDeal(CHTTPServerConnection *AConnection, const CString &Method, const CString &URI, const CString &Action) { auto OnRequest = [AConnection](CWebSocketMessageHandler *AHandler, CWebSocketConnection *AWSConnection) { auto pReply = AConnection->Reply(); const auto &wsMessage = CCustomWebSocketClient::RequestToMessage(AWSConnection); pReply->ContentType = CHTTPReply::json; if (wsMessage.MessageTypeId == mtCallResult) { pReply->Content = wsMessage.Payload.ToString(); AConnection->SendReply(CHTTPReply::ok, nullptr, true); } else { ReplyError(AConnection, CHTTPReply::bad_request, wsMessage.ErrorMessage); } }; auto pServerRequest = AConnection->Request(); const auto &caModuleAddress = m_Module["address"]; const auto &caModuleFee = m_Module["fee"]; const auto checkFee = CheckFee(caModuleFee); if (checkFee == -1) throw ExceptionFrm("Invalid module fee value: %s", caModuleFee.c_str()); const auto &caHost = pServerRequest->Headers["host"]; const auto &caOrigin = pServerRequest->Headers["origin"]; const auto &address = pServerRequest->Params["address"]; const auto &code = pServerRequest->Params["code"]; const auto &caUserAddress = address.length() == 40 ? CString() : address; const auto &caDealCode = !code.empty() ? code : address.length() == 40 ? address : CString(); const auto &pgpValue = pServerRequest->Params["pgp"]; const auto &caServerParam = pServerRequest->Params["server"]; const auto index = CurrentContextIndex(caServerParam); if (index == -1) { throw Delphi::Exception::Exception(NOT_FOUND_ACTIVE_CONNECTION); } const auto &caContext = m_Servers[index].Value(); YAML::Node Node; CString sPayload; if (!pServerRequest->Content.IsEmpty()) { const auto &ContentType = pServerRequest->Headers.Values(_T("content-type")); if (ContentType.Find("application/x-www-form-urlencoded") == 0) { const CStringList &FormData = pServerRequest->FormData; const auto &formType = FormData["type"]; const auto &formAt = FormData["at"]; const auto &formDate = FormData["date"]; const auto &formSellerAddress = FormData["seller_address"]; const auto &formSellerRating = FormData["seller_rating"]; const auto &formSellerSignature = FormData["seller_signature"]; const auto &formCustomerAddress = FormData["customer_address"]; const auto &formCustomerRating = FormData["customer_rating"]; const auto &formCustomerSignature = FormData["customer_signature"]; const auto &formPaymentAddress = FormData["payment_address"]; const auto &formPaymentUntil = FormData["payment_until"]; const auto &formPaymentSum = FormData["payment_sum"]; const auto &formFeedbackLeaveBefore = FormData["feedback_leave_before"]; const auto &formFeedbackStatus = FormData["feedback_status"]; const auto &formFeedbackComments = FormData["feedback_comments"]; CheckKeyForNull("order", Action.c_str()); CheckKeyForNull("type", formType.c_str()); CheckKeyForNull("at", formAt.c_str()); CheckKeyForNull("date", formDate.c_str()); CheckKeyForNull("seller_address", formSellerAddress.c_str()); CheckKeyForNull("customer_address", formCustomerAddress.c_str()); CheckKeyForNull("payment_sum", formPaymentSum.c_str()); if (Action == "cancel") { CheckKeyForNull("seller_signature", formSellerSignature.c_str()); } if (Action == "feedback") { CheckKeyForNull("customer_signature", formCustomerSignature.c_str()); } YAML::Node Deal = Node["deal"]; Deal["order"] = Action.c_str(); Deal["type"] = formType.c_str(); Deal["at"] = formAt.c_str(); Deal["date"] = formDate.c_str(); YAML::Node Seller = Deal["seller"]; Seller["address"] = formSellerAddress.c_str(); if (!formSellerRating.IsEmpty()) Seller["rating"] = formSellerRating.c_str(); if (!formSellerSignature.IsEmpty()) Seller["signature"] = formSellerSignature.c_str(); YAML::Node Customer = Deal["customer"]; Customer["address"] = formCustomerAddress.c_str(); if (!formCustomerRating.IsEmpty()) Customer["rating"] = formCustomerRating.c_str(); if (!formCustomerSignature.IsEmpty()) Customer["signature"] = formCustomerSignature.c_str(); YAML::Node Payment = Deal["payment"]; if (!formPaymentAddress.IsEmpty()) Payment["address"] = formPaymentAddress.c_str(); if (!formPaymentUntil.IsEmpty()) Payment["until"] = formPaymentUntil.c_str(); Payment["sum"] = formPaymentSum.c_str(); if (!formFeedbackLeaveBefore.IsEmpty()) { YAML::Node Feedback = Deal["feedback"]; Feedback["leave-before"] = formFeedbackLeaveBefore.c_str(); if (!formFeedbackStatus.IsEmpty()) Feedback["status"] = formFeedbackStatus.c_str(); if (!formFeedbackComments.IsEmpty()) Feedback["comments"] = formFeedbackComments.c_str(); } } else if (ContentType.Find("multipart/form-data") == 0) { CFormData FormData; CHTTPRequestParser::ParseFormData(pServerRequest, FormData); const auto &formType = FormData.Data("type"); const auto &formAt = FormData.Data("at"); const auto &formDate = FormData.Data("date"); const auto &formSellerAddress = FormData.Data("seller_address"); const auto &formSellerRating = FormData.Data("seller_rating"); const auto &formSellerSignature = FormData.Data("seller_signature"); const auto &formCustomerAddress = FormData.Data("customer_address"); const auto &formCustomerRating = FormData.Data("customer_rating"); const auto &formCustomerSignature = FormData.Data("customer_signature"); const auto &formPaymentAddress = FormData.Data("payment_address"); const auto &formPaymentUntil = FormData.Data("payment_until"); const auto &formPaymentSum = FormData.Data("payment_sum"); const auto &formFeedbackLeaveBefore = FormData.Data("feedback_leave_before"); const auto &formFeedbackStatus = FormData.Data("feedback_status"); const auto &formFeedbackComments = FormData.Data("feedback_comments"); CheckKeyForNull("order", Action.c_str()); CheckKeyForNull("type", formType.c_str()); CheckKeyForNull("at", formAt.c_str()); CheckKeyForNull("date", formDate.c_str()); CheckKeyForNull("seller_address", formSellerAddress.c_str()); CheckKeyForNull("customer_address", formCustomerAddress.c_str()); CheckKeyForNull("payment_sum", formPaymentSum.c_str()); if (Action == "cancel") { CheckKeyForNull("seller_signature", formSellerSignature.c_str()); } if (Action == "feedback") { CheckKeyForNull("customer_signature", formCustomerSignature.c_str()); } YAML::Node Deal = Node["deal"]; Deal["order"] = Action.c_str(); Deal["type"] = formType.c_str(); Deal["at"] = formAt.c_str(); Deal["date"] = formDate.c_str(); YAML::Node Seller = Deal["seller"]; Seller["address"] = formSellerAddress.c_str(); if (!formSellerRating.IsEmpty()) Seller["rating"] = formSellerRating.c_str(); if (!formSellerSignature.IsEmpty()) Seller["signature"] = formSellerSignature.c_str(); YAML::Node Customer = Deal["customer"]; Customer["address"] = formCustomerAddress.c_str(); if (!formSellerRating.IsEmpty()) Customer["rating"] = formCustomerRating.c_str(); if (!formCustomerSignature.IsEmpty()) Customer["signature"] = formCustomerSignature.c_str(); YAML::Node Payment = Deal["payment"]; if (!formPaymentAddress.IsEmpty()) Payment["address"] = formPaymentAddress.c_str(); if (!formPaymentUntil.IsEmpty()) Payment["until"] = formPaymentUntil.c_str(); Payment["sum"] = formPaymentSum.c_str(); if (!formFeedbackLeaveBefore.IsEmpty()) { YAML::Node Feedback = Deal["feedback"]; Feedback["leave-before"] = formFeedbackLeaveBefore.c_str(); if (!formFeedbackStatus.IsEmpty()) Feedback["status"] = formFeedbackStatus.c_str(); if (!formFeedbackComments.IsEmpty()) Feedback["comments"] = formFeedbackComments.c_str(); } } else if (ContentType.Find("application/json") == 0) { const CJSON jsonData(pServerRequest->Content); const auto &formOrder = jsonData["order"].AsString().Lower(); const auto &formType = jsonData["type"].AsString(); const auto &formAt = jsonData["at"].AsString(); const auto &formDate = jsonData["date"].AsString(); const CJSONValue &jsonSeller = jsonData["seller"]; const auto &formSellerAddress = jsonSeller["address"].AsString(); const auto &formSellerRating = jsonSeller["rating"].AsString(); const auto &formSellerSignature = jsonSeller["signature"].AsString(); const CJSONValue &jsonCustomer = jsonData["customer"]; const auto &formCustomerAddress = jsonCustomer["address"].AsString(); const auto &formCustomerRating = jsonCustomer["rating"].AsString(); const auto &formCustomerSignature = jsonCustomer["signature"].AsString(); const CJSONValue &jsonPayment = jsonData["payment"]; const auto &formPaymentAddress = jsonPayment["address"].AsString(); const auto &formPaymentUntil = jsonPayment["until"].AsString(); const auto &formPaymentSum = jsonPayment["sum"].AsString(); const CJSONValue &jsonFeedback = jsonData["feedback"]; const auto &formFeedbackLeaveBefore = jsonFeedback["leave-before"].AsString(); const auto &formFeedbackStatus = jsonFeedback["status"].AsString(); const auto &formFeedbackComments = jsonFeedback["comments"].AsString(); const auto &action = Action.IsEmpty() ? formOrder : Action; CheckKeyForNull("order", action); CheckKeyForNull("type", formType); CheckKeyForNull("at", formAt); CheckKeyForNull("date", formDate); CheckKeyForNull("seller.address", formSellerAddress); CheckKeyForNull("customer.address", formCustomerAddress); CheckKeyForNull("payment.sum", formPaymentSum); if (action == "cancel") { CheckKeyForNull("seller.signature", formSellerSignature); } if (action == "feedback") { CheckKeyForNull("customer.signature", formCustomerSignature); } YAML::Node Deal = Node["deal"]; Deal["order"] = action.c_str(); Deal["type"] = formType.c_str(); Deal["at"] = formAt.c_str(); Deal["date"] = formDate.c_str(); YAML::Node Seller = Deal["seller"]; Seller["address"] = formSellerAddress.c_str(); if (!formSellerRating.IsEmpty()) Seller["rating"] = formSellerRating.c_str(); YAML::Node Customer = Deal["customer"]; Customer["address"] = formCustomerAddress.c_str(); if (!formSellerRating.IsEmpty()) Customer["rating"] = formCustomerRating.c_str(); YAML::Node Payment = Deal["payment"]; if (!formPaymentAddress.IsEmpty()) Payment["address"] = formPaymentAddress.c_str(); if (!formPaymentUntil.IsEmpty()) Payment["until"] = formPaymentUntil.c_str(); Payment["sum"] = formPaymentSum.c_str(); if (!formFeedbackLeaveBefore.IsEmpty()) { YAML::Node Feedback = Deal["feedback"]; Feedback["leave-before"] = formFeedbackLeaveBefore.c_str(); if (!formFeedbackStatus.IsEmpty()) Feedback["status"] = formFeedbackStatus.c_str(); if (!formFeedbackComments.IsEmpty()) Feedback["comments"] = formFeedbackComments.c_str(); } } else { Node = YAML::Load(pServerRequest->Content.c_str()); } const auto &BTCKeys = caContext.BTCKeys(); if (BTCKeys.Count() < 2) throw ExceptionFrm("Bitcoin keys cannot be empty."); for (int i = 0; i < BTCKeys.Count(); ++i) { if (BTCKeys[i].IsEmpty()) throw ExceptionFrm("Bitcoin KEY%d cannot be empty.", i); } CDeal Deal(Node); auto& Data = Deal.Data(); if (Data.Order == doCreate) { Data.Payment.Address = Deal.GetPaymentHD(BTCKeys.Names(0), BTCKeys.Names(1), Deal.Data().Transaction.Key, BitcoinConfig.version_hd, BitcoinConfig.version_script); Node["deal"]["date"] = Data.Date.c_str(); YAML::Node Payment = Node["deal"]["payment"]; Payment.remove("sum"); Payment["address"] = Data.Payment.Address.c_str(); Payment["until"] = Data.Payment.Until.c_str(); Payment["sum"] = Data.Payment.Sum.c_str(); Node["deal"]["feedback"]["leave-before"] = Data.FeedBack.LeaveBefore.c_str(); } CheckDeal(Deal); const CString caClearText(YAML::Dump(Node)); if (pgpValue == "off" || pgpValue == "false") { sPayload = caClearText; } else { Apostol::PGP::CleartextSignature( m_pgpModuleKey, m_pgpPassphrase, BPS_PGP_HASH, caClearText, sPayload); } } CJSON Json(jvtObject); CJSONValue Module(jvtObject); Module.Object().AddPair("address", caModuleAddress); Module.Object().AddPair("fee", caModuleFee); Json.Object().AddPair("id", GetUID(16).Lower()); Json.Object().AddPair("host", caHost); if (!caOrigin.IsEmpty()) { Json.Object().AddPair("origin", caOrigin); } Json.Object().AddPair("module", Module); Json.Object().AddPair("address", caUserAddress.IsEmpty() ? caModuleAddress : caUserAddress); if (!caDealCode.IsEmpty()) { CJSONValue Deal(jvtObject); Deal.Object().AddPair("code", caDealCode); Json.Object().AddPair("deal", Deal); } if (!sPayload.IsEmpty()) Json.Object().AddPair("payload", base64_encode(sPayload)); auto pClient = GetConnectedClient(caContext); if (pClient == nullptr) { throw Delphi::Exception::Exception(NOT_FOUND_ACTIVE_CONNECTION); } pClient->Send(URI, Json, OnRequest); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoSignature(CHTTPServerConnection *AConnection) { } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoAPI(CHTTPServerConnection *AConnection) { auto pRequest = AConnection->Request(); auto pReply = AConnection->Reply(); pReply->ContentType = CHTTPReply::json; CStringList slRouts; SplitColumns(pRequest->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") { pReply->Content << "{\"serverTime\": " << ToString(MsEpoch()) << "}"; AConnection->SendReply(CHTTPReply::ok); } else if (caCommand == "help") { pRequest->Content.Clear(); DoAccount(AConnection, "GET", sRoute); } else if (caCommand == "account" && caAction == "status") { pRequest->Content.Clear(); DoAccount(AConnection, "GET", sRoute); } else if (caCommand == "deal" && caAction == "status") { pRequest->Content.Clear(); DoDeal(AConnection, "GET", sRoute, caAction); } else if (caCommand == "bc" && caAction == "history") { const auto &caAccount = pRequest->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); pReply->Content = history.ToString(); } catch (Delphi::Exception::Exception &E) { ExceptionToJson(CHTTPReply::bad_request, E, pReply->Content); Log()->Error(APP_LOG_EMERG, 0, E.what()); } AConnection->SendReply(CHTTPReply::ok, nullptr, true); } else if (caCommand == "bc" && caAction == "header") { const auto &caHeight = pRequest->Params["height"]; const auto &caHash = pRequest->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); } pReply->Content = header.ToString(); } catch (Delphi::Exception::Exception &E) { ExceptionToJson(CHTTPReply::bad_request, E, pReply->Content); Log()->Error(APP_LOG_EMERG, 0, E.what()); } AConnection->SendReply(CHTTPReply::ok, nullptr, true); } else { AConnection->SendStockReply(CHTTPReply::not_found); } } catch (std::exception &e) { CHTTPReply::CStatusType status = CHTTPReply::internal_server_error; ExceptionToJson(0, e, pReply->Content); AConnection->SendReply(status); Log()->Error(APP_LOG_EMERG, 0, e.what()); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoGet(CHTTPServerConnection *AConnection) { auto pRequest = AConnection->Request(); CString sPath(pRequest->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; } SendResource(AConnection, sPath); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::DoPost(CHTTPServerConnection *AConnection) { auto pRequest = AConnection->Request(); auto pReply = AConnection->Reply(); pReply->ContentType = CHTTPReply::json; CStringList slRouts; SplitColumns(pRequest->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") { DoAccount(AConnection, "POST", sRoute); } else if (caCommand == "deal") { DoDeal(AConnection, "POST", sRoute, caAction); } else if (caCommand == "signature") { DoSignature(AConnection); } else { AConnection->SendStockReply(CHTTPReply::not_found); } } catch (std::exception &e) { ReplyError(AConnection, CHTTPReply::bad_request, e.what()); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::Heartbeat(CDateTime Now) { for (int i = 0; i < m_Servers.Count(); i++) { auto &Context = m_Servers[i].Value(); if (Now >= Context.CheckDate()) { Context.SetCheckDate(Now + (CDateTime) 30 / SecsPerDay); // 30 sec if (Context.Status() == Context::csInitialization) { ModuleService(Context); } if (Context.Status() >= Context::csInitialized) { CheckProviders(Now, Context); FetchProviders(Now, Context); } } if (Now >= Context.FixedDate()) { Context.SetFixedDate(Now + (CDateTime) 30 / SecsPerDay); // 30 sec if (Context.Status() == Context::csAuthorized) { CreateWebSocketClient(Context); } if (Context.Status() == Context::csRunning) { for (int j = 0; j < Context.ClientManager().Count(); ++j) { auto pClient = Context.ClientManager()[j]; if (!pClient->Active()) pClient->Active(true); if (!pClient->Connected() && !pClient->Session().IsEmpty()) { Log()->Notice(_T("[%s] [%s] [%s] Trying to establish a connection."), Context.Name().c_str(), Context.URL().Origin().c_str(), pClient->Session().c_str()); pClient->ConnectStart(); } } } } } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::Reload() { Config()->IniFile().ReadSectionValues("module", &m_Module); CString FileName; FileName = Config()->IniFile().ReadString(CONFIG_SECTION_NAME, "oauth2", "oauth2/service.json"); if (!path_separator(FileName.front())) { FileName = Config()->Prefix() + FileName; } if (FileExists(FileName.c_str())) { m_OAuth2.LoadFromFile(FileName.c_str()); } const auto &caPrivateKey = Config()->IniFile().ReadString("pgp", "private", "module.sec"); const auto &caPublicKey = Config()->IniFile().ReadString("pgp", "public", "dm.pub"); m_pgpPassphrase = Config()->IniFile().ReadString("pgp", "passphrase", ""); if (FileExists(caPrivateKey.c_str())) { m_pgpModuleKey.LoadFromFile(caPrivateKey.c_str()); if (FileExists(caPublicKey.c_str())) { m_pgpPublicKey.LoadFromFile(caPublicKey.c_str()); UpdateServerList(m_pgpPublicKey); } else { Log()->Error(APP_LOG_WARN, 0, APP_FILE_NOT_FOUND, caPublicKey.c_str()); } } else { Log()->Error(APP_LOG_WARN, 0, APP_FILE_NOT_FOUND, caPrivateKey.c_str()); } } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::Initialization(CModuleProcess *AProcess) { CApostolModule::Initialization(AProcess); if (Enabled()) { Reload(); } } //-------------------------------------------------------------------------------------------------------------- bool CWebSocketModule::Enabled() { if (m_ModuleStatus == msUnknown) m_ModuleStatus = Config()->IniFile().ReadBool(SectionName().c_str(), "enable", false) ? msEnabled: msDisabled; return m_ModuleStatus == msEnabled; } //-------------------------------------------------------------------------------------------------------------- int CWebSocketModule::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; } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::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) { const CDateTime Until = StringToDate(Data.Payment.Until); if (DateTime > Until) throw ExceptionFrm("Deal cancellation expired."); 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()); } //-------------------------------------------------------------------------------------------------------------- void CWebSocketModule::CheckKeyForNull(LPCTSTR key, const CString &Value) { if (Value.IsEmpty()) throw ExceptionFrm("Invalid format: key \"%s\" cannot be empty.", key); } //-------------------------------------------------------------------------------------------------------------- CString CWebSocketModule::ToString(unsigned long Value) { TCHAR szString[_INT_T_LEN + 1] = {0}; sprintf(szString, "%lu", Value); return { szString }; } //-------------------------------------------------------------------------------------------------------------- } } }