// SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 2020-2021, The Monero Project. #include "QtWebSockets/qwebsocketserver.h" #include "QtWebSockets/qwebsocket.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include "model/AddressBookModel.h" #include "model/TransactionHistoryModel.h" #include "libwalletqt/AddressBook.h" #include "libwalletqt/TransactionHistory.h" #include #include "wsserver.h" #include "appcontext.h" #include "utils/utils.h" WSServer::WSServer(AppContext *ctx, const QHostAddress &host, const quint16 port, const QString &password, bool debug, QObject *parent) : QObject(parent), m_debug(debug), m_ctx(ctx), m_password(password), m_pWebSocketServer( new QWebSocketServer(QStringLiteral("Feather Daemon WS"), QWebSocketServer::NonSecureMode, this)) { if (!m_pWebSocketServer->listen(QHostAddress::Any, port)) return; qDebug() << "websocket server listening on port" << port; connect(m_pWebSocketServer, &QWebSocketServer::newConnection, this, &WSServer::onNewConnection); connect(m_pWebSocketServer, &QWebSocketServer::closed, this, &WSServer::closed); connect(m_ctx, &AppContext::walletClosed, this, &WSServer::onWalletClosed); connect(m_ctx, &AppContext::balanceUpdated, this, &WSServer::onBalanceUpdated); connect(m_ctx, &AppContext::walletOpened, this, &WSServer::onWalletOpened); connect(m_ctx, &AppContext::walletOpenedError, this, &WSServer::onWalletOpenedError); connect(m_ctx, &AppContext::walletCreatedError, this, &WSServer::onWalletCreatedError); connect(m_ctx, &AppContext::walletCreated, this, &WSServer::onWalletCreated); connect(m_ctx, &AppContext::synchronized, this, &WSServer::onSynchronized); connect(m_ctx, &AppContext::blockchainSync, this, &WSServer::onBlockchainSync); connect(m_ctx, &AppContext::refreshSync, this, &WSServer::onRefreshSync); connect(m_ctx, &AppContext::createTransactionError, this, &WSServer::onCreateTransactionError); connect(m_ctx, &AppContext::createTransactionSuccess, this, &WSServer::onCreateTransactionSuccess); connect(m_ctx, &AppContext::transactionCommitted, this, &WSServer::onTransactionCommitted); connect(m_ctx, &AppContext::walletOpenPasswordNeeded, this, &WSServer::onWalletOpenPasswordRequired); connect(m_ctx, &AppContext::initiateTransaction, this, &WSServer::onInitiateTransaction); m_walletDir = m_ctx->defaultWalletDir; // Bootstrap Tor/websockets m_ctx->initTor(); m_ctx->initWS(); } QString WSServer::connectionId(QWebSocket *pSocket) { return QString("%1#%2").arg(pSocket->peerAddress().toString()).arg(pSocket->peerPort()); } void WSServer::onNewConnection() { QWebSocket *pSocket = m_pWebSocketServer->nextPendingConnection(); connect(pSocket, &QWebSocket::binaryMessageReceived, this, &WSServer::processBinaryMessage); connect(pSocket, &QWebSocket::disconnected, this, &WSServer::socketDisconnected); m_clients << pSocket; m_clients_auth[this->connectionId(pSocket)] = false; // blast wallet listing on connect QJsonArray arr; for(const WalletKeysFiles &wallet: m_ctx->listWallets()) arr << wallet.toJsonObject(); auto welcomeWalletMessage = WSServer::createWSMessage("walletList", arr); pSocket->sendBinaryMessage(welcomeWalletMessage); // and the current state of appcontext QJsonObject obj; if(this->m_ctx->currentWallet == nullptr) { obj["state"] = "walletClosed"; } else { obj["state"] = "walletOpened"; obj["walletPath"] = m_ctx->currentWallet->path(); } this->sendAll("state", obj); } void WSServer::processBinaryMessage(QByteArray buffer) { QWebSocket *pClient = qobject_cast(sender()); const QString cid = this->connectionId(pClient); if (m_debug) qDebug() << "Websocket (server) received:" << buffer; if (!pClient) return; QJsonDocument doc = QJsonDocument::fromJson(buffer); QJsonObject object = doc.object(); QString cmd = object.value("cmd").toString(); if(m_clients_auth.contains(cid) && !m_clients_auth[cid]) { if (cmd == "password") { auto data = object.value("data").toObject(); auto passwd = data.value("password").toString(); if(passwd != this->m_password) { this->sendAll("passwordIncorrect", "authentication failed."); return; } else { this->m_clients_auth[cid] = true; this->sendAll("passwordSuccess", "authentication OK."); return; } } else { this->sendAll("passwordIncorrect", "authentication failed."); return; } } if(cmd == "openWallet") { auto data = object.value("data").toObject(); auto path = data.value("path").toString(); auto passwd = data.value("password").toString(); m_ctx->onOpenWallet(path, passwd); } else if (cmd == "closeWallet") { if (m_ctx->currentWallet == nullptr) return; m_ctx->closeWallet(true, true); } else if(cmd == "addressList") { auto data = object.value("data").toObject(); auto accountIndex = data.value("accountIndex").toInt(); auto addressIndex = data.value("addressIndex").toInt(); auto limit = data.value("limit").toInt(50); auto offset = data.value("offset").toInt(0); QJsonArray arr; for(int i = offset; i != limit; i++) { arr << m_ctx->currentWallet->address((quint32) accountIndex, (quint32) addressIndex + i); } QJsonObject obj; obj["accountIndex"] = accountIndex; obj["addressIndex"] = addressIndex; obj["offset"] = offset; obj["limit"] = limit; obj["addresses"] = arr; this->sendAll("addressList", arr); } else if(cmd == "sendTransaction") { auto data = object.value("data").toObject(); auto address = data.value("address").toString(); auto amount = data.value("amount").toDouble(0); auto description = data.value("description").toString(); bool all = data.value("all").toBool(false); if(!WalletManager::addressValid(address, m_ctx->currentWallet->nettype())){ this->sendAll("transactionError", "Could not validate address"); return; } if(amount <= 0) { this->sendAll("transactionError", "y u send 0"); return; } m_ctx->onCreateTransaction(address, (quint64) amount, description, all); } else if(cmd == "createWallet") { auto data = object.value("data").toObject(); auto name = data.value("name").toString(); auto path = data.value("path").toString(); auto password = data.value("password").toString(); QString walletPath; if(name.isEmpty()){ this->sendAll("walletCreatedError", "Supply a name for your wallet"); return; } if(path.isEmpty()) { walletPath = QDir(m_walletDir).filePath(name + ".keys"); if(Utils::fileExists(walletPath)) { auto err = QString("Filepath already exists: %1").arg(walletPath); this->sendAll("walletCreatedError", err); return; } } FeatherSeed seed = FeatherSeed(m_ctx->restoreHeights[m_ctx->networkType], m_ctx->coinName, m_ctx->seedLanguage); m_ctx->createWallet(seed, walletPath, password); } else if(cmd == "transactionHistory") { m_ctx->currentWallet->history()->refresh(m_ctx->currentWallet->currentSubaddressAccount()); auto *model = m_ctx->currentWallet->history(); QJsonArray arr = model->toJsonArray(); this->sendAll("transactionHistory", arr); } else if (cmd == "addressBook") { QJsonArray arr = m_ctx->currentWallet->addressBookModel()->toJsonArray(); this->sendAll("addressBook", arr); } } void WSServer::socketDisconnected() { QWebSocket *pClient = qobject_cast(sender()); QString cid = connectionId(pClient); m_clients_auth[cid] = false; if (m_debug) qDebug() << "socketDisconnected:" << pClient; if (pClient) { m_clients.removeAll(pClient); pClient->deleteLater(); } } // templates are forbidden! QByteArray WSServer::createWSMessage(const QString &cmd, const QJsonArray &arr) { QJsonObject jsonObject = QJsonObject(); jsonObject["cmd"] = cmd; jsonObject["data"] = arr; QJsonDocument doc = QJsonDocument(jsonObject); return doc.toJson(QJsonDocument::Compact); } QByteArray WSServer::createWSMessage(const QString &cmd, const QJsonObject &obj) { QJsonObject jsonObject = QJsonObject(); jsonObject["cmd"] = cmd; jsonObject["data"] = obj; QJsonDocument doc = QJsonDocument(jsonObject); return doc.toJson(QJsonDocument::Compact); } QByteArray WSServer::createWSMessage(const QString &cmd, const int val) { QJsonObject jsonObject = QJsonObject(); jsonObject["cmd"] = cmd; jsonObject["data"] = val; QJsonDocument doc = QJsonDocument(jsonObject); return doc.toJson(QJsonDocument::Compact); } QByteArray WSServer::createWSMessage(const QString &cmd, const QString &val) { QJsonObject jsonObject = QJsonObject(); jsonObject["cmd"] = cmd; jsonObject["data"] = val; QJsonDocument doc = QJsonDocument(jsonObject); return doc.toJson(QJsonDocument::Compact); } WSServer::~WSServer() { m_pWebSocketServer->close(); qDeleteAll(m_clients.begin(), m_clients.end()); } void WSServer::sendAll(const QString &cmd, const QJsonObject &obj) { for(QWebSocket *pSocket: m_clients) { pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, obj)); } } void WSServer::sendAll(const QString &cmd, const QJsonArray &arr) { for(QWebSocket *pSocket: m_clients) { pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, arr)); } } void WSServer::sendAll(const QString &cmd, int val) { for(QWebSocket *pSocket: m_clients) { pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, val)); } } void WSServer::sendAll(const QString &cmd, const QString &val) { for(QWebSocket *pSocket: m_clients) { pSocket->sendBinaryMessage(WSServer::createWSMessage(cmd, val)); } } // ====================================================================== void WSServer::onWalletOpened(Wallet *wallet) { connect(m_ctx->currentWallet, &Wallet::connectionStatusChanged, this, &WSServer::onConnectionStatusChanged); auto obj = wallet->toJsonObject(); sendAll("walletOpened", obj); } void WSServer::onBlockchainSync(int height, int target) { QJsonObject obj; obj["height"] = height; obj["target"] = target; sendAll("blockchainSync", obj); } void WSServer::onRefreshSync(int height, int target) { QJsonObject obj; obj["height"] = height; obj["target"] = target; sendAll("refreshSync", obj); } void WSServer::onWalletClosed() { QJsonObject obj; sendAll("walletClosed", obj); } void WSServer::onBalanceUpdated(quint64 balance, quint64 spendable) { QJsonObject obj; obj["balance"] = balance / globals::cdiv; obj["spendable"] = spendable / globals::cdiv; sendAll("balanceUpdated", obj); } void WSServer::onWalletOpenedError(const QString &err) { sendAll("walletOpenedError", err); } void WSServer::onWalletCreatedError(const QString &err) { sendAll("walletCreatedError", err); } void WSServer::onWalletCreated(Wallet *wallet) { auto obj = wallet->toJsonObject(); sendAll("walletCreated", obj); // emit signal on behalf of walletManager m_ctx->walletManager->walletOpened(wallet); } void WSServer::onSynchronized() { QJsonObject obj; sendAll("synchronized", obj); } void WSServer::onWalletOpenPasswordRequired(bool invalidPassword, const QString &path) { QJsonObject obj; obj["invalidPassword"] = invalidPassword; obj["path"] = path; sendAll("synchronized", obj); } void WSServer::onConnectionStatusChanged(int status) { sendAll("connectionStatusChanged", status); } void WSServer::onInitiateTransaction() { QJsonObject obj; sendAll("transactionStarted", obj); } void WSServer::onCreateTransactionError(const QString &message) { sendAll("transactionError", message); } void WSServer::onCreateTransactionSuccess(PendingTransaction *tx, const QVector &address) { // auto-commit all tx's m_ctx->currentWallet->commitTransactionAsync(tx); } void WSServer::onTransactionCommitted(bool status, PendingTransaction *tx, const QStringList &txid) { QString preferredCur = config()->get(Config::preferredFiatCurrency).toString(); auto convert = [preferredCur](double amount){ return QString::number(AppContext::prices->convert("WOW", preferredCur, amount), 'f', 2); }; QJsonObject obj; QJsonArray txids; for(const QString &id: txid) txids << id; obj["txid"] = txids; obj["status"] = status; obj["amount"] = tx->amount() / globals::cdiv; obj["fee"] = tx->fee() / globals::cdiv; obj["total"] = (tx->amount() + tx->fee()) / globals::cdiv; obj["amount_fiat"] = convert(tx->amount() / globals::cdiv); obj["fee_fiat"] = convert(tx->fee() / globals::cdiv); obj["total_fiat"] = convert((tx->amount() + tx->fee()) / globals::cdiv); sendAll("transactionSent", obj); }