Files
apostol-dm/src/modules/Workers/WebSocket/WebSocket.cpp

1944 lines
83 KiB
C++

/*++
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 <random>
//----------------------------------------------------------------------------------------------------------------------
#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<CHTTPClientConnection *> (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<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 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<CHTTPClientConnection *> (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<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 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<CHTTPClientConnection *> (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<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 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<CHTTPClientConnection *> (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<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 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<CHTTPClientConnection *> (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<CWebSocketClientConnection *>(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<CWebSocketClientConnection *>(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<CWebSocketClient *> (Sender);
chASSERT(pClient);
}
//--------------------------------------------------------------------------------------------------------------
void CWebSocketModule::DoClientTimeOut(CObject *Sender) {
auto pClient = dynamic_cast<CWebSocketClient *> (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<CWebSocketClient *> (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<CWebSocketClient *> (Sender);
chASSERT(pClient);
auto pContext = pClient->Context();
chASSERT(pContext);
}
//--------------------------------------------------------------------------------------------------------------
void CWebSocketModule::DoSubscribeEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) {
auto pClient = dynamic_cast<CWebSocketClient *> (Sender);
chASSERT(pClient);
auto pContext = pClient->Context();
chASSERT(pContext);
}
//--------------------------------------------------------------------------------------------------------------
void CWebSocketModule::DoKeyEvent(CObject *Sender, const CWSMessage &Request, const CWSMessage &Response) {
auto pClient = dynamic_cast<CWebSocketClient *> (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<CWebSocketClient *> (Sender);
chASSERT(pClient);
auto pContext = pClient->Context();
chASSERT(pContext);
}
//--------------------------------------------------------------------------------------------------------------
void CWebSocketModule::DoWebSocketError(CTCPConnection *AConnection) {
auto pConnection = dynamic_cast<CWebSocketClientConnection *> (AConnection);
auto pClient = dynamic_cast<CWebSocketClient *> (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 };
}
//--------------------------------------------------------------------------------------------------------------
}
}
}