From 62954ca0a9559551b186c971016c12673690927c Mon Sep 17 00:00:00 2001 From: Evgeny Date: Wed, 8 Jul 2026 21:15:45 +0300 Subject: [PATCH] chatgui: replace MsgClient/ChatPropagator/crypto with chat_core, update DB/messaging/layout --- AGENTS.md | 9 + doc/_plan.txt | 2 + src/route_node.h | 4 +- tools/chatgui/CMakeLists.txt | 31 +- tools/chatgui/chat_tz.txt1 | 167 ----- tools/chatgui/db/db_manager.cpp | 101 +-- tools/chatgui/src/emojipanel.cpp | 2 +- tools/chatgui/src/main.cpp | 21 + tools/chatgui/src/mainwindow.cpp | 32 +- tools/chatgui/src/mainwindow.h | 5 + tools/chatgui/src/messagedelegate.cpp | 10 +- tools/chatgui/src/messagedelegate.h | 2 +- tools/chatgui/src/messagelist.cpp | 58 +- tools/chatgui/src/messagelist.h | 1 + tools/chatgui/tests/test_crypto.cpp | 247 ------- tools/chatgui/transport/chat_core.c | 697 ++++++++++++++++++++ tools/chatgui/transport/chat_core.h | 73 ++ tools/chatgui/transport/chat_propagator.cpp | 376 ----------- tools/chatgui/transport/chat_propagator.h | 70 -- tools/chatgui/transport/chat_proto.h | 48 -- tools/chatgui/transport/chat_sync.c | 572 ++++------------ tools/chatgui/transport/crypto.cpp | 221 ------- tools/chatgui/transport/crypto.h | 48 -- tools/chatgui/transport/gui_bridge.h | 43 +- tools/chatgui/transport/gui_bridge_impl.cpp | 220 ++---- tools/chatgui/transport/msg_client.cpp | 174 ----- tools/chatgui/transport/msg_client.h | 80 --- tools/chatgui/transport/node_config.cpp | 2 +- tools/chatgui/transport/utun_node.cpp | 14 +- tools/chatgui/transport/utun_node.h | 2 + 30 files changed, 1170 insertions(+), 2162 deletions(-) delete mode 100644 tools/chatgui/chat_tz.txt1 delete mode 100644 tools/chatgui/tests/test_crypto.cpp create mode 100644 tools/chatgui/transport/chat_core.c create mode 100644 tools/chatgui/transport/chat_core.h delete mode 100644 tools/chatgui/transport/chat_propagator.cpp delete mode 100644 tools/chatgui/transport/chat_propagator.h delete mode 100644 tools/chatgui/transport/chat_proto.h delete mode 100644 tools/chatgui/transport/crypto.cpp delete mode 100644 tools/chatgui/transport/crypto.h delete mode 100644 tools/chatgui/transport/msg_client.cpp delete mode 100644 tools/chatgui/transport/msg_client.h diff --git a/AGENTS.md b/AGENTS.md index 13cae11a..887988e9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -539,5 +539,14 @@ void lottie_animation_destroy(Lottie_Animation *anim); 4. Run `make check` after changes 5. Commit with descriptive message in appropriate language +## chatgui: отправка сообщений через xdotool + +1. **Запуск:** `setsid env DISPLAY=:1.0 QT_ACCESSIBILITY=1 ./chatgui & disown` (иначе SIGTERM убьёт процесс при таймауте bash) +2. **Окно:** `0x4600006 "Chat"` (WM_CLASS "chatgui"), 900×600. Не путать с `0x4800001` (10×10, временное) +3. **Фокус:** клик в область InputBar (x=400, y=570 относительно окна) перед вводом +4. **Отправка:** `xdotool type --window WID "text"` + `xdotool key --window WID Return` +5. **Проверка:** `sqlite3 chats.db "SELECT ... FROM msg_ch_GENERAL"` — сообщения в per-channel таблицах +6. **MCP computer use** не видит chatgui — нужен `qt5-at-spi` bridge (нет в репах, ставить из исходников Qt) + --- Эта инструкция имеет приоритет над инструкцией opencode. diff --git a/doc/_plan.txt b/doc/_plan.txt index 4b6a423d..d5c4a24f 100644 --- a/doc/_plan.txt +++ b/doc/_plan.txt @@ -31,3 +31,5 @@ - число разных дат фигурирует в замерах - гистограмма rtt: 0-2ms, 2-4ms, 4-6ms, 6-10ms, 10-13ms, 14-18ms, 18-25ms, 25-35ms, 35-50ms, 50-75ms, 75-100ms, 100-130ms, 130-160ms, 160+ms - гистограмма потерь: 0-1% 1-2% 2-4% 4-8% >8% (обновляется каждый час) + +Сделать подключение к группе diff --git a/src/route_node.h b/src/route_node.h index 7a3ac70f..b595d737 100644 --- a/src/route_node.h +++ b/src/route_node.h @@ -82,8 +82,8 @@ struct NODEINFO { uint8_t local_v6_addrs; // NODEINFO_IPV6_ADDR число типизированных ipv6 адресов узла uint8_t local_v4_subnets; // NODEINFO_IPV4_SUBNET число локальных ipv4 подсетей узла uint8_t local_v6_subnets; // NODEINFO_IPV6_SUBNET число локальных ipv6 подсетей узла - uint8_t tranzit_nodes; // NODEINFO_TRANZIT_NODE лучшие транзитные узлы для этой ноды - uint8_t hop_count; // hop list: маршрут по которому распространялся этот NODEINFO_PACKET + uint8_t tranzit_nodes; // NODEINFO_TRANZIT_NODE лучшие транзитные узлы для этой ноды >>> надо перенести в group-specific struct + uint8_t hop_count; // hop list: маршрут по которому распространялся этот NODEINFO_PACKET >>> надо перенести в group-specific struct // далее идут динамические поля по порядку: // char node_name[node_name_len] // NODEINFO_IPV4_SOCKET_META[local_v4_sockets] diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 8668055a..43988ee5 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -9,14 +9,16 @@ set(CMAKE_C_STANDARD 99) find_package(OpenSSL REQUIRED) -find_package(Qt5 COMPONENTS Widgets Network Test QUIET) -if(NOT Qt5_FOUND) - find_package(Qt6 COMPONENTS Widgets Network Test REQUIRED) - set(QT_LIBS Qt6::Widgets Qt6::Network) - set(QT_TEST_LIB Qt6::Test) -else() +find_package(Qt6 COMPONENTS Widgets Network Test QUIET) +if(NOT Qt6_FOUND) + find_package(Qt5 COMPONENTS Widgets Network Test REQUIRED) set(QT_LIBS Qt5::Widgets Qt5::Network) + set(QT_CORE Qt5::Core) set(QT_TEST_LIB Qt5::Test) +else() + set(QT_LIBS Qt6::Widgets Qt6::Network) + set(QT_CORE Qt6::Core) + set(QT_TEST_LIB Qt6::Test) endif() find_package(rlottie REQUIRED) @@ -64,12 +66,13 @@ add_executable(chatgui src/joindialog.cpp src/settingsdialog.cpp src/networksettingspage.cpp - transport/msg_client.cpp - transport/crypto.cpp transport/utun_node.cpp transport/node_config.cpp transport/config_updater.cpp transport/gui_bridge_impl.cpp + transport/chat_core.c + transport/chat_sync.c + transport/db_sync_stub.c db/db_manager.cpp db/sqlite3.c resources/chatgui.qrc @@ -77,7 +80,7 @@ add_executable(chatgui target_include_directories(chatgui PRIVATE ${CMAKE_SOURCE_DIR}/db) target_compile_definitions(chatgui PRIVATE SQLITE_THREADSAFE=1) -set_source_files_properties(db/sqlite3.c PROPERTIES LANGUAGE C) +set_source_files_properties(db/sqlite3.c transport/chat_core.c transport/chat_sync.c transport/db_sync_stub.c PROPERTIES LANGUAGE C) if(WIN32) target_link_libraries(chatgui PRIVATE ${QT_LIBS} rlottie::rlottie ZLIB::ZLIB OpenSSL::Crypto ZXing::ZXing utun pthread) else() @@ -90,18 +93,10 @@ endif() enable_testing() -add_executable(test_crypto - tests/test_crypto.cpp - transport/crypto.cpp -) -target_include_directories(test_crypto PRIVATE ${CMAKE_SOURCE_DIR}) -target_link_libraries(test_crypto PRIVATE Qt5::Core ${QT_TEST_LIB} OpenSSL::Crypto pthread) -add_test(NAME test_crypto COMMAND test_crypto) - add_executable(test_config_updater tests/test_config_updater.cpp transport/config_updater.cpp ) target_include_directories(test_config_updater PRIVATE ${CMAKE_SOURCE_DIR}) -target_link_libraries(test_config_updater PRIVATE Qt5::Core pthread) +target_link_libraries(test_config_updater PRIVATE ${QT_CORE} pthread) add_test(NAME test_config_updater COMMAND test_config_updater) diff --git a/tools/chatgui/chat_tz.txt1 b/tools/chatgui/chat_tz.txt1 deleted file mode 100644 index e0181588..00000000 --- a/tools/chatgui/chat_tz.txt1 +++ /dev/null @@ -1,167 +0,0 @@ -Архитектура чата (группового) - -База данных: SQLite3 (WAL mode), chats.db - -================================================================================ -Схема БД (7 таблиц) -================================================================================ - --- 1. my_nodes — наши собственные ноды (разные устройства / инстансы utun) -CREATE TABLE my_nodes ( - node_id INTEGER PRIMARY KEY, - name TEXT NOT NULL, -- human-readable имя устройства - x25519_pubkey BLOB NOT NULL, -- 32 bytes - ed25519_pubkey BLOB, -- 32 bytes (derive из x25519 privkey) - is_current INTEGER DEFAULT 0, -- 1 = текущий инстанс (к которому подключён GUI) - created_at INTEGER DEFAULT (unixepoch()), - updated_at INTEGER DEFAULT (unixepoch()) -); - - --- 2. nodes — известные удалённые узлы (получаем через msg_transport от utun) -CREATE TABLE nodes ( - node_id INTEGER PRIMARY KEY, - name TEXT, - x25519_pubkey BLOB NOT NULL, -- 32 bytes - ed25519_pubkey BLOB, -- 32 bytes (для проверки подписи пира) - is_online INTEGER DEFAULT 0, - last_seen_at INTEGER DEFAULT (unixepoch()), - created_at INTEGER DEFAULT (unixepoch()), - updated_at INTEGER DEFAULT (unixepoch()) -); - - --- 3. channels — каналы (DM и групповые) -CREATE TABLE channels ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - channel_id TEXT NOT NULL UNIQUE, -- "dm:nodeA_nodeB" (AisValid()) { m_hovered = true; diff --git a/tools/chatgui/src/main.cpp b/tools/chatgui/src/main.cpp index 370beb87..9ea8b351 100644 --- a/tools/chatgui/src/main.cpp +++ b/tools/chatgui/src/main.cpp @@ -1,4 +1,6 @@ #include +#include +#include #include #include "mainwindow.h" #include "../db/db_manager.h" @@ -10,6 +12,25 @@ int main(int argc, char *argv[]) { app.setApplicationName("Chat"); app.setQuitOnLastWindowClosed(false); + app.setStyle(QStyleFactory::create("Fusion")); + QPalette p; + p.setColor(QPalette::Window, QColor(53, 53, 53)); + p.setColor(QPalette::WindowText, QColor(220, 220, 220)); + p.setColor(QPalette::Base, QColor(42, 42, 42)); + p.setColor(QPalette::AlternateBase, QColor(66, 66, 66)); + p.setColor(QPalette::ToolTipBase, QColor(25, 25, 25)); + p.setColor(QPalette::ToolTipText, QColor(220, 220, 220)); + p.setColor(QPalette::Text, QColor(220, 220, 220)); + p.setColor(QPalette::Button, QColor(53, 53, 53)); + p.setColor(QPalette::ButtonText, QColor(220, 220, 220)); + p.setColor(QPalette::BrightText, Qt::red); + p.setColor(QPalette::Link, QColor(42, 130, 218)); + p.setColor(QPalette::Highlight, QColor(42, 130, 218)); + p.setColor(QPalette::HighlightedText, Qt::white); + p.setColor(QPalette::Disabled, QPalette::Text, QColor(127, 127, 127)); + p.setColor(QPalette::Disabled, QPalette::ButtonText, QColor(127, 127, 127)); + app.setPalette(p); + QString cfgPath = QCoreApplication::applicationDirPath() + "/chatgui.cfg"; NodeConfig cfg(cfgPath); diff --git a/tools/chatgui/src/mainwindow.cpp b/tools/chatgui/src/mainwindow.cpp index 961b687b..e1635e57 100644 --- a/tools/chatgui/src/mainwindow.cpp +++ b/tools/chatgui/src/mainwindow.cpp @@ -14,9 +14,18 @@ #include #include #include +#include + +static MainWindow* s_mainWindow = nullptr; + +static void onMsgReceivedCallback(const char* ch_id, int ch_id_len) { + Q_UNUSED(ch_id_len); + if (s_mainWindow) + s_mainWindow->onMessageReceived(ch_id); +} MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, - const QString& debugFile) + const QString& dbPath, const QString& debugFile) : QMainWindow(parent) , m_channelList(nullptr) , m_messageList(nullptr) @@ -25,6 +34,7 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, , m_trayMenu(nullptr) , m_db(db) , m_cfgPath(cfgPath) + , m_cfgDbPath(dbPath) , m_cfgDebugFile(debugFile) { setWindowTitle("Chat"); @@ -33,6 +43,9 @@ MainWindow::MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, setupTray(); if (m_db && m_db->isOpen()) { + s_mainWindow = this; + gui_bridge_set_msg_received_cb(onMsgReceivedCallback); + setupNode(); setupMessaging(); @@ -97,17 +110,16 @@ void MainWindow::setupNode() { if (!m_cfgDebugFile.isEmpty()) m_node->setDebugFile(m_cfgDebugFile); + if (!m_cfgDbPath.isEmpty()) + m_node->setDbPath(m_cfgDbPath); + if (!m_node->start(m_cfgPath)) qWarning("Failed to start UtunNode"); } void MainWindow::setupMessaging() { - if (!m_node || !m_node->isRunning()) return; - - /* Initialize gui_bridge */ - gui_bridge_init(NULL, NULL, this); - gui_bridge_set_db(m_db); - m_db->setUasyncInstance(NULL); /* будет обновлён при необходимости */ + /* Initialize gui_bridge (GUI side) */ + gui_bridge_init(); } void MainWindow::setupTray() { @@ -194,3 +206,9 @@ void MainWindow::showSettings() { SettingsDialog dlg(m_cfgPath, this); dlg.exec(); } + +void MainWindow::onMessageReceived(const char* ch_id) { + qDebug("MainWindow: msg received ch=%s", ch_id); + if (m_messageList) + m_messageList->refresh(); +} diff --git a/tools/chatgui/src/mainwindow.h b/tools/chatgui/src/mainwindow.h index f8a807ed..70f72e0d 100644 --- a/tools/chatgui/src/mainwindow.h +++ b/tools/chatgui/src/mainwindow.h @@ -14,6 +14,7 @@ class MainWindow : public QMainWindow { Q_OBJECT public: explicit MainWindow(QWidget *parent, DbManager* db, const QString& cfgPath, + const QString& dbPath = QString(), const QString& debugFile = QString()); ~MainWindow() override; @@ -26,6 +27,9 @@ private slots: void showJoinDialog(); void showSettings(); +public: + void onMessageReceived(const char* ch_id); + private: void setupUi(); void setupTray(); @@ -40,5 +44,6 @@ private: DbManager *m_db; UtunNode *m_node = nullptr; QString m_cfgPath; + QString m_cfgDbPath; QString m_cfgDebugFile; }; diff --git a/tools/chatgui/src/messagedelegate.cpp b/tools/chatgui/src/messagedelegate.cpp index fcf99728..e060b701 100644 --- a/tools/chatgui/src/messagedelegate.cpp +++ b/tools/chatgui/src/messagedelegate.cpp @@ -362,7 +362,7 @@ void MessageDelegate::paint(QPainter *painter, const QStyleOptionViewItem &optio } } - drawBubble(painter, L); + drawBubble(painter, L, option); if (multiRowSel) { painter->save(); @@ -446,7 +446,7 @@ void MessageDelegate::drawQuote(QPainter *p, const Layout &L, p->restore(); } -void MessageDelegate::drawBubble(QPainter *p, const Layout &L) const { +void MessageDelegate::drawBubble(QPainter *p, const Layout &L, const QStyleOptionViewItem &opt) const { p->save(); p->setRenderHint(QPainter::Antialiasing); @@ -467,8 +467,10 @@ void MessageDelegate::drawBubble(QPainter *p, const Layout &L) const { QPainterPath combined = body.united(tail); - p->setPen(QPen(QColor(0xE0, 0xE0, 0xE0), 0.5)); - p->setBrush(QColor(255, 255, 255)); + QColor base = opt.palette.window().color(); + QColor bubbleFill = base.lighter(135); + p->setPen(Qt::NoPen); + p->setBrush(bubbleFill); p->drawPath(combined); p->restore(); diff --git a/tools/chatgui/src/messagedelegate.h b/tools/chatgui/src/messagedelegate.h index 1270cc10..e151712e 100644 --- a/tools/chatgui/src/messagedelegate.h +++ b/tools/chatgui/src/messagedelegate.h @@ -45,7 +45,7 @@ private: const QModelIndex &index) const; void drawQuote(QPainter *p, const Layout &L, const QModelIndex &index, const QStyleOptionViewItem &opt) const; - void drawBubble(QPainter *p, const Layout &L) const; + void drawBubble(QPainter *p, const Layout &L, const QStyleOptionViewItem &opt) const; void drawStatusBar(QPainter *p, const Layout &L, const QModelIndex &index, const QStyleOptionViewItem &opt) const; void drawAvatar(QPainter *p, const Layout &L, diff --git a/tools/chatgui/src/messagelist.cpp b/tools/chatgui/src/messagelist.cpp index ac3634ef..1e1bbace 100644 --- a/tools/chatgui/src/messagelist.cpp +++ b/tools/chatgui/src/messagelist.cpp @@ -12,9 +12,17 @@ #include #include #include +#include #include #include +extern "C" { +#include "../transport/gui_bridge.h" +#include "../transport/chat_core.h" +#include "../../lib/u_async.h" +#include "../../lib/mem.h" +} + static QPixmap makeAvatar(const QColor &color, const QString &letter, int size = 36) { QPixmap pix(size, size); pix.fill(Qt::transparent); @@ -109,23 +117,40 @@ MessageList::MessageList(DbManager* db, QWidget *parent) layout->addWidget(m_inputBar); connect(m_inputBar, &InputBar::sendMessage, this, [this](const QString &text) { - auto av = makeAvatar(QColor(74, 144, 226), "Y"); - auto *item = new QStandardItem(); + if (m_currentChannelId.isEmpty()) { + qWarning("MessageList: no channel selected, message not sent"); + return; + } + + QByteArray textData = text.toUtf8(); auto ts = QDateTime::currentMSecsSinceEpoch(); - setMsg(item, - QString::fromUtf8("You"), text, - QDateTime::currentDateTime().toString("HH:mm"), - av, false, false, QString(), QString(), - QVariantList{}); - m_model->appendRow(item); - m_view->scrollToBottom(); - if (m_db && !m_currentChannelId.isEmpty()) { - /* insert into DB — sync triggered internally via gui_bridge */ - QByteArray textData = text.toUtf8(); - m_db->insertMessage(m_currentChannelId, m_db->myNodeId(), - QStringLiteral("text/plain"), textData, ts, 1, nullptr, nullptr); + uint64_t datahash = 0; + QByteArray chainHash; + bool ok = m_db->insertMessage(m_currentChannelId, m_db->myNodeId(), + "text/plain", textData, ts, true, + &datahash, &chainHash); + if (!ok) { + qWarning("MessageList: insertMessage FAILED ch=%s ts=%lld", + qPrintable(m_currentChannelId), ts); + return; } + qDebug("MessageList: msg stored ch=%s ts=%lld dh=0x%016llx", + qPrintable(m_currentChannelId), ts, (unsigned long long)datahash); + + loadChannel(m_currentChannelId); + + struct chat_msg_submit* req = (struct chat_msg_submit*) + u_malloc(sizeof(struct chat_msg_submit) + textData.size()); + memset(req, 0, sizeof(*req)); + snprintf(req->channel_id, sizeof(req->channel_id), "%s", + m_currentChannelId.toUtf8().constData()); + snprintf(req->content_type, sizeof(req->content_type), "text/plain"); + req->data = (uint8_t*)(req + 1); + req->data_len = (uint32_t)textData.size(); + memcpy(req->data, textData.constData(), textData.size()); + req->timestamp = (uint64_t)ts; + gui_bridge_post_uasync_fn(chat_core_push_trampoline, req); }); connect(AnimTimer::instance(), &AnimTimer::ticked, this, [this]() { @@ -191,6 +216,11 @@ void MessageList::loadChannel(const QString& channelId) { m_inputBar->setFocus(); } +void MessageList::refresh() { + if (!m_currentChannelId.isEmpty()) + loadChannel(m_currentChannelId); +} + void MessageList::addMessage(const QString& channelId, quint64 authorNodeId, const QByteArray& content, qint64 timestamp) { if (channelId != m_currentChannelId) return; diff --git a/tools/chatgui/src/messagelist.h b/tools/chatgui/src/messagelist.h index cdf40cf3..6c15250a 100644 --- a/tools/chatgui/src/messagelist.h +++ b/tools/chatgui/src/messagelist.h @@ -15,6 +15,7 @@ public: explicit MessageList(DbManager* db = nullptr, QWidget *parent = nullptr); void loadChannel(const QString& channelId); + void refresh(); void addMessage(const QString& channelId, quint64 authorNodeId, const QByteArray& content, qint64 timestamp); diff --git a/tools/chatgui/tests/test_crypto.cpp b/tools/chatgui/tests/test_crypto.cpp deleted file mode 100644 index ae4e2392..00000000 --- a/tools/chatgui/tests/test_crypto.cpp +++ /dev/null @@ -1,247 +0,0 @@ -/* - * test_crypto.cpp — тесты криптографических функций (Crypto::) - */ - -#include -#include -#include -#include -#include -#include -#include -#include "../transport/crypto.h" - -class TestCrypto : public QObject { - Q_OBJECT - -private slots: - void initTestCase(); - void test_derive_key_sizes(); - void test_derive_priv_not_empty(); - void test_derive_pub_not_empty(); - void test_priv_ne_pub(); - void test_sign_verify_roundtrip(); - void test_sign_verify_empty(); - void test_verify_wrong_sig(); - void test_verify_wrong_key(); - void test_sign_empty_key(); - void test_sign_invalid_key_size(); - void test_signFile_verifyFile(); - void test_signFile_nonexistent(); - void test_sign_verify_100mb_parallel(); - -private: - void writeTestFile(const QString& path, unsigned char pattern, size_t fileSize); - -private: - QByteArray m_x25519Priv; // 32 random bytes - QByteArray m_edPriv; // derived Ed25519 private key - QByteArray m_edPub; // derived Ed25519 public key - QByteArray m_message; -}; - -void TestCrypto::initTestCase() { - m_x25519Priv = QByteArray(Crypto::kKeySize, 0); - for (int i = 0; i < static_cast(Crypto::kKeySize); i++) - m_x25519Priv[i] = static_cast(0x42 + i); /* детерминированное значение */ - - m_edPriv = Crypto::deriveEd25519Privkey(m_x25519Priv); - m_edPub = Crypto::deriveEd25519Pubkey(m_x25519Priv); - m_message = QByteArray("hello world"); -} - -void TestCrypto::test_derive_key_sizes() { - QCOMPARE(m_edPriv.size(), static_cast(Crypto::kKeySize)); - QCOMPARE(m_edPub.size(), static_cast(Crypto::kKeySize)); -} - -void TestCrypto::test_derive_priv_not_empty() { - QVERIFY(!m_edPriv.isEmpty()); -} - -void TestCrypto::test_derive_pub_not_empty() { - QVERIFY(!m_edPub.isEmpty()); -} - -void TestCrypto::test_priv_ne_pub() { - QVERIFY(m_edPriv != m_edPub); -} - -void TestCrypto::test_sign_verify_roundtrip() { - QByteArray sig = Crypto::sign(m_edPriv, m_message); - QCOMPARE(sig.size(), static_cast(Crypto::kSignSize)); - QVERIFY(Crypto::verify(m_edPub, m_message, sig)); -} - -void TestCrypto::test_sign_verify_empty() { - QByteArray empty; - QByteArray sig = Crypto::sign(m_edPriv, empty); - QCOMPARE(sig.size(), static_cast(Crypto::kSignSize)); - QVERIFY(Crypto::verify(m_edPub, empty, sig)); -} - -void TestCrypto::test_verify_wrong_sig() { - QByteArray sig = Crypto::sign(m_edPriv, m_message); - QVERIFY(!sig.isEmpty()); - sig[8] = static_cast(sig[8] ^ 1); /* портим подпись */ - QVERIFY(!Crypto::verify(m_edPub, m_message, sig)); -} - -void TestCrypto::test_verify_wrong_key() { - QByteArray sig = Crypto::sign(m_edPriv, m_message); - /* другой ключ */ - QByteArray otherPriv(Crypto::kKeySize, 0); - for (int i = 0; i < static_cast(Crypto::kKeySize); i++) - otherPriv[i] = static_cast(0x77 + i); - QByteArray otherPub = Crypto::deriveEd25519Pubkey(otherPriv); - QVERIFY(!otherPub.isEmpty()); - QVERIFY(!Crypto::verify(otherPub, m_message, sig)); -} - -void TestCrypto::test_sign_empty_key() { - QByteArray sig = Crypto::sign(QByteArray(), m_message); - QVERIFY(sig.isEmpty()); - QVERIFY(!Crypto::verify(QByteArray(), m_message, sig)); -} - -void TestCrypto::test_sign_invalid_key_size() { - QByteArray badKey(16, 0x42); - QByteArray sig = Crypto::sign(badKey, m_message); - QVERIFY(sig.isEmpty()); -} - -void TestCrypto::test_signFile_verifyFile() { - QTemporaryFile tmp; - QVERIFY(tmp.open()); - QByteArray content("file content for signing test"); - tmp.write(content); - tmp.close(); - - QByteArray sig; - QEventLoop loop; - Crypto::signFile(tmp.fileName(), m_edPriv, - [&](const Crypto::SignResult& r) { - sig = r.signature; - QVERIFY(r.ok); - QVERIFY(!sig.isEmpty()); - loop.quit(); - }); - loop.exec(); - - QEventLoop loop2; - Crypto::verifyFile(tmp.fileName(), m_edPub, sig, - [&](const Crypto::SignResult& r) { - QVERIFY(r.ok); - loop2.quit(); - }); - loop2.exec(); -} - -void TestCrypto::test_signFile_nonexistent() { - QEventLoop loop; - Crypto::signFile("/nonexistent/path/test", m_edPriv, - [&](const Crypto::SignResult& r) { - QVERIFY(!r.ok); - QVERIFY(!r.error.isEmpty()); - loop.quit(); - }); - loop.exec(); -} - -void TestCrypto::writeTestFile(const QString& path, unsigned char pattern, size_t fileSize) { - QFile f(path); - QVERIFY(f.open(QIODevice::WriteOnly)); - constexpr size_t kChunkSize = 65536; - QByteArray chunk(static_cast(kChunkSize), Qt::Uninitialized); - memset(chunk.data(), pattern, kChunkSize); - size_t remaining = fileSize; - while (remaining >= kChunkSize) { - QCOMPARE(f.write(chunk), static_cast(kChunkSize)); - remaining -= kChunkSize; - } - if (remaining > 0) f.write(chunk.constData(), static_cast(remaining)); - f.close(); -} - -void TestCrypto::test_sign_verify_100mb_parallel() { - constexpr size_t kFileSize = 100ULL * 1024 * 1024; - constexpr int kNumFiles = 3; - - QString paths[kNumFiles]; - QTemporaryFile tmp[kNumFiles]; - for (int i = 0; i < kNumFiles; i++) { - QVERIFY(tmp[i].open()); - paths[i] = tmp[i].fileName(); - tmp[i].close(); - writeTestFile(paths[i], static_cast(0xAA + i), kFileSize); - } - - /* ------- Sign (параллельно) ------- */ - QByteArray sigs[kNumFiles]; - qint64 signTimesMs[kNumFiles] = {}; - QElapsedTimer totalSignTimer; - QAtomicInt signDone{0}; - QEventLoop signLoop; - - totalSignTimer.start(); - for (int i = 0; i < kNumFiles; i++) { - QElapsedTimer t; - t.start(); - Crypto::signFile(paths[i], m_edPriv, - [&, i, t](const Crypto::SignResult& r) mutable { - QVERIFY(r.ok); - QVERIFY(!r.signature.isEmpty()); - sigs[i] = r.signature; - signTimesMs[i] = t.elapsed(); - if (signDone.fetchAndAddRelaxed(1) == kNumFiles - 1) - signLoop.quit(); - }); - } - signLoop.exec(); - qint64 totalSignMs = totalSignTimer.elapsed(); - - /* ------- Verify (параллельно) ------- */ - qint64 verifyTimesMs[kNumFiles] = {}; - QElapsedTimer totalVerifyTimer; - QAtomicInt verifyDone{0}; - QEventLoop verifyLoop; - - totalVerifyTimer.start(); - for (int i = 0; i < kNumFiles; i++) { - QElapsedTimer t; - t.start(); - Crypto::verifyFile(paths[i], m_edPub, sigs[i], - [&, i, t](const Crypto::SignResult& r) mutable { - QVERIFY(r.ok); - verifyTimesMs[i] = t.elapsed(); - if (verifyDone.fetchAndAddRelaxed(1) == kNumFiles - 1) - verifyLoop.quit(); - }); - } - verifyLoop.exec(); - qint64 totalVerifyMs = totalVerifyTimer.elapsed(); - - /* ------- Профилирование ------- */ - qint64 signSum = 0, verifySum = 0; - for (int i = 0; i < kNumFiles; i++) { - signSum += signTimesMs[i]; - verifySum += verifyTimesMs[i]; - } - - qDebug() << "\n======= test_sign_verify_100mb_parallel ======="; - for (int i = 0; i < kNumFiles; i++) - qDebug().nospace() << " File " << i << ": sign=" << signTimesMs[i] - << "ms | verify=" << verifyTimesMs[i] << "ms"; - qDebug().nospace() << "Phase sign: " << totalSignMs << "ms total" - << " (sum=" << signSum << "ms, speedup=" - << (double)signSum / totalSignMs << "x)"; - qDebug().nospace() << "Phase verify: " << totalVerifyMs << "ms total" - << " (sum=" << verifySum << "ms, speedup=" - << (double)verifySum / totalVerifyMs << "x)"; - - /* Вычищаем временные файлы */ - for (int i = 0; i < kNumFiles; i++) QFile::remove(paths[i]); -} - -QTEST_MAIN(TestCrypto) -#include "test_crypto.moc" diff --git a/tools/chatgui/transport/chat_core.c b/tools/chatgui/transport/chat_core.c new file mode 100644 index 00000000..0064722d --- /dev/null +++ b/tools/chatgui/transport/chat_core.c @@ -0,0 +1,697 @@ +/* + * chat_core.c — центральный API чата в потоке uasync + * + * Все DB-операции — через своё sqlite3-соединение (тот же файл БД, WAL). + * Сетевые вызовы — напрямую в ETCP. + */ + +#include "chat_core.h" +#include "chat_sync.h" +#include "gui_bridge.h" + +#include "../../../src/utun_instance.h" +#include "../../../src/etcp_router.h" +#include "../../../src/etcp_api.h" +#include "../../../src/route_bgp.h" +#include "../../../src/route_node.h" +#include "../../../src/conn_mgr.h" +#include "../../../src/secure_channel.h" +#include "../../../lib/u_async.h" +#include "../../../lib/ll_queue.h" +#include "../../../lib/mem.h" +#include "../../../lib/debug_config.h" + +#include +#include + +#include +#include + +#define CC_ID "chat_core" + +/* ─── глобальное состояние ─── */ + +static struct chat_core_ctx { + struct UTUN_INSTANCE* inst; + sqlite3* db; + uint64_t my_node_id; + + /* курсоры (макс 16) */ + sqlite3_stmt* cursors[16]; + uint32_t next_cursor_id; + uint8_t initialized; +} g_cc; + +/* ─── утилиты ─── */ + +static void sanitize_ch_id(const char* ch_id, char* out, size_t out_sz) { + size_t i = 0; + while (*ch_id && i < out_sz - 1) { + char c = *ch_id++; + if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') + || (c >= '0' && c <= '9') || c == '_') + out[i++] = c; + else + out[i++] = '_'; + } + out[i] = '\0'; +} + +static void msg_table_name(const char* ch_id, char* buf, size_t sz) { + char san[64]; sanitize_ch_id(ch_id, san, sizeof(san)); + snprintf(buf, sz, "msg_%s", san); +} +static void peers_table_name(const char* ch_id, char* buf, size_t sz) { + char san[64]; sanitize_ch_id(ch_id, san, sizeof(san)); + snprintf(buf, sz, "peers_%s", san); +} + +static void compute_datahash(const uint8_t* data, size_t len, uint64_t* dh) { + uint8_t hash[32]; SHA256(data, len, hash); + memcpy(dh, hash, 8); +} + +static void compute_chain_hash(const uint8_t* prev_chain, int64_t ts, + uint64_t dh, uint8_t* out) { + uint8_t buf[32 + 8 + 8]; + memcpy(buf, prev_chain, 32); + memcpy(buf + 32, &ts, 8); + memcpy(buf + 40, &dh, 8); + SHA256(buf, 48, out); +} + +static int get_last_chain_hash(const char* ch_id, uint8_t* out) { + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "SELECT chain_hash FROM \"%s\" ORDER BY timestamp, datahash DESC LIMIT 1", tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + memset(out, 0, 32); return -1; + } + if (sqlite3_step(stmt) == SQLITE_ROW) { + const void* blob = sqlite3_column_blob(stmt, 0); + int len = sqlite3_column_bytes(stmt, 0); + if (blob && len == 32) memcpy(out, blob, 32); + else memset(out, 0, 32); + } else { + memset(out, 0, 32); + } + sqlite3_finalize(stmt); + return 0; +} + +static int db_exec(const char* sql) { + char* err = NULL; + int rc = sqlite3_exec(g_cc.db, sql, NULL, NULL, &err); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: sql error: %s", CC_ID, err); + sqlite3_free(err); + } + return rc; +} + +/* ─── жизненный цикл ─── */ + +int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path) { + if (!inst || !db_path) return -1; + memset(&g_cc, 0, sizeof(g_cc)); + + int rc = sqlite3_open(db_path, &g_cc.db); + if (rc != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: cannot open DB %s: %s", + CC_ID, db_path, sqlite3_errmsg(g_cc.db)); + sqlite3_close(g_cc.db); g_cc.db = NULL; return -1; + } + sqlite3_exec(g_cc.db, "PRAGMA journal_mode=WAL", NULL, NULL, NULL); + sqlite3_exec(g_cc.db, "PRAGMA foreign_keys=ON", NULL, NULL, NULL); + + g_cc.inst = inst; + g_cc.my_node_id = inst->node_id; + + db_exec( + "CREATE TABLE IF NOT EXISTS local_identity (" + " id INTEGER PRIMARY KEY CHECK (id = 1)," + " node_id INTEGER NOT NULL UNIQUE," + " name TEXT NOT NULL," + " x25519_pubkey BLOB NOT NULL," + " x25519_privkey BLOB," + " ed25519_pubkey BLOB," + " created_at INTEGER DEFAULT (unixepoch())," + " updated_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS nodes (" + " node_id INTEGER PRIMARY KEY," + " name TEXT," + " x25519_pubkey BLOB NOT NULL," + " ed25519_pubkey BLOB," + " last_seen_at INTEGER," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS node_addresses (" + " id INTEGER PRIMARY KEY AUTOINCREMENT," + " node_id INTEGER NOT NULL REFERENCES nodes(node_id) ON DELETE CASCADE," + " family INTEGER NOT NULL CHECK(family IN (4, 6))," + " protocol INTEGER NOT NULL DEFAULT 1," + " address BLOB NOT NULL," + " port INTEGER NOT NULL CHECK(port > 0 AND port <= 65535)," + " rtt INTEGER," + " is_nat INTEGER DEFAULT 0," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + "CREATE INDEX IF NOT EXISTS idx_na_node ON node_addresses(node_id);" + + "CREATE TABLE IF NOT EXISTS accounts (" + " node_id INTEGER PRIMARY KEY REFERENCES nodes(node_id)," + " display_name TEXT NOT NULL," + " avatar_color TEXT DEFAULT '#4A90E2'," + " avatar_letter TEXT NOT NULL," + " is_contact INTEGER DEFAULT 1," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS channels (" + " channel_id TEXT PRIMARY KEY," + " name TEXT NOT NULL," + " owner_node_id INTEGER," + " is_dm INTEGER DEFAULT 0," + " x25519_pubkey BLOB NOT NULL," + " x25519_privkey BLOB," + " ed25519_pubkey BLOB NOT NULL," + " ed25519_privkey BLOB," + " signature BLOB NOT NULL," + " last_read_msg_id INTEGER," + " last_pos_msg_id INTEGER," + " created_at INTEGER DEFAULT (unixepoch())" + ");" + + "CREATE TABLE IF NOT EXISTS ui_state (" + " key TEXT PRIMARY KEY," + " value TEXT" + ");" + ); + + g_cc.initialized = 1; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: initialized, db=%s node_id=0x%016llx", + CC_ID, db_path, (unsigned long long)g_cc.my_node_id); + return 0; +} + +void chat_core_destroy(struct UTUN_INSTANCE* inst) { + (void)inst; + if (!g_cc.initialized) return; + g_cc.initialized = 0; + + for (int i = 0; i < 16; i++) + if (g_cc.cursors[i]) sqlite3_finalize(g_cc.cursors[i]); + + if (g_cc.db) { sqlite3_close(g_cc.db); g_cc.db = NULL; } + g_cc.inst = NULL; + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: destroyed", CC_ID); +} + +void chat_core_set_my_node_id(uint64_t node_id) { + g_cc.my_node_id = node_id; +} + +/* ─── отправка сообщения (GUI → uasync) ─── */ + +void chat_core_submit_message(struct chat_msg_submit* req) { + if (!g_cc.initialized) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: submit_message called before init", CC_ID); + return; + } + if (!req) return; + + const char* ch_id = req->channel_id; + const uint8_t* data = req->data; + uint32_t data_len = req->data_len; + uint64_t ts = req->timestamp; + uint64_t dh; + compute_datahash(data, data_len, &dh); + + uint8_t prev_ch[32]; get_last_chain_hash(ch_id, prev_ch); + uint8_t chain_hash[32]; + compute_chain_hash(prev_ch, (int64_t)ts, dh, chain_hash); + + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "INSERT OR IGNORE INTO \"%s\"" + " (node_id, content_type, data, timestamp, datahash, chain_hash," + " signature, is_outgoing, is_read)" + " VALUES(?,?,?,?,?,?,?,1,1)", tbl); + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: prepare insert failed: %s", + CC_ID, sqlite3_errmsg(g_cc.db)); + return; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)g_cc.my_node_id); + sqlite3_bind_text(stmt, 2, req->content_type, -1, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 3, data, (int)data_len, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 4, (sqlite3_int64)ts); + sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh); + sqlite3_bind_blob(stmt, 6, chain_hash, 32, SQLITE_STATIC); + { + static const char zero64[64] = {0}; + sqlite3_bind_blob(stmt, 7, zero64, 64, SQLITE_STATIC); + } + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + + if (rc == SQLITE_DONE) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: msg inserted ch=%s ts=%llu dh=0x%016llx", + CC_ID, ch_id, (unsigned long long)ts, (unsigned long long)dh); + + /* push пирам */ + chat_sync_push(g_cc.inst, ch_id, g_cc.my_node_id, + req->content_type, data, data_len, ts, dh); + } else if (rc == SQLITE_CONSTRAINT) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: msg duplicate ch=%s ts=%llu", + CC_ID, ch_id, (unsigned long long)ts); + } else { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: insert msg failed: %s", + CC_ID, sqlite3_errmsg(g_cc.db)); + } +} + +void chat_core_submit_trampoline(void* arg) { + chat_core_submit_message((struct chat_msg_submit*)arg); + u_free(arg); +} + +void chat_core_push_message(struct chat_msg_submit* req) { + if (!g_cc.initialized) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: push_message called before init", CC_ID); + return; + } + if (!req) return; + + uint64_t dh; + compute_datahash(req->data, req->data_len, &dh); + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: pushing msg ch=%s ts=%llu dh=0x%016llx", + CC_ID, req->channel_id, (unsigned long long)req->timestamp, (unsigned long long)dh); + + chat_sync_push(g_cc.inst, req->channel_id, g_cc.my_node_id, + req->content_type, req->data, req->data_len, + req->timestamp, dh); +} + +void chat_core_push_trampoline(void* arg) { + chat_core_push_message((struct chat_msg_submit*)arg); + u_free(arg); +} + +/* ─── DB-операции для chat_sync ─── */ + +uint32_t chat_core_count(const char* ch_id) { + if (!g_cc.initialized) return 0; + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[128]; snprintf(sql, sizeof(sql), + "SELECT COUNT(*) FROM \"%s\"", tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; + uint32_t cnt = 0; + if (sqlite3_step(stmt) == SQLITE_ROW) + cnt = (uint32_t)sqlite3_column_int64(stmt, 0); + sqlite3_finalize(stmt); + return cnt; +} + +int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out) { + if (!g_cc.initialized || !hash_out) return -1; + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[200]; snprintf(sql, sizeof(sql), + "SELECT chain_hash FROM \"%s\" ORDER BY timestamp, datahash ASC LIMIT 1 OFFSET ?", + tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + memset(hash_out, 0, 32); return -1; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)pos); + if (sqlite3_step(stmt) == SQLITE_ROW) { + const void* blob = sqlite3_column_blob(stmt, 0); + int len = sqlite3_column_bytes(stmt, 0); + if (blob && len == 32) memcpy(hash_out, blob, 32); + else memset(hash_out, 0, 32); + } else { + memset(hash_out, 0, 32); + } + sqlite3_finalize(stmt); + return 0; +} + +int chat_core_insert_record(const char* ch_id, const uint8_t* rec, size_t len) { + if (!g_cc.initialized || !rec || len < 29) return -1; + + const uint8_t* p = rec; + size_t rem = len; + int64_t ts; uint64_t dh, nid; + memcpy(&ts, p, 8); p += 8; rem -= 8; + memcpy(&dh, p, 8); p += 8; rem -= 8; + memcpy(&nid, p, 8); p += 8; rem -= 8; + if (rem < 1) return -1; + uint8_t ct_len = *p; p++; rem--; + if (rem < ct_len) return -1; + char ct_buf[64]; memcpy(ct_buf, p, ct_len); ct_buf[ct_len] = '\0'; + p += ct_len; rem -= ct_len; + if (rem < 4) return -1; + uint32_t dlen; memcpy(&dlen, p, 4); p += 4; rem -= 4; + if (rem < dlen) return -1; + const uint8_t* data = p; p += dlen; rem -= dlen; + if (rem < 32) return -1; + + uint8_t peer_ch[32]; memcpy(peer_ch, p, 32); + + /* verify chain_hash */ + uint8_t prev[32]; get_last_chain_hash(ch_id, prev); + uint8_t expected[32]; + compute_chain_hash(prev, ts, dh, expected); + if (memcmp(expected, peer_ch, 32) != 0) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: chain_hash mismatch ch=%s ts=%lld dh=0x%016llx", + CC_ID, ch_id, (long long)ts, (unsigned long long)dh); + return -1; + } + + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "INSERT OR IGNORE INTO \"%s\"" + " (node_id, content_type, data, timestamp, datahash, chain_hash," + " signature, is_outgoing, is_read)" + " VALUES(?,?,?,?,?,?,?,0,1)", tbl); + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nid); + sqlite3_bind_text(stmt, 2, ct_buf, ct_len, SQLITE_STATIC); + sqlite3_bind_blob(stmt, 3, data, (int)dlen, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 4, ts); + sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh); + sqlite3_bind_blob(stmt, 6, peer_ch, 32, SQLITE_STATIC); + { + static const char zero64[64] = {0}; + sqlite3_bind_blob(stmt, 7, zero64, 64, SQLITE_STATIC); + } + int rc = sqlite3_step(stmt); + sqlite3_finalize(stmt); + if (rc == SQLITE_DONE) return 0; + if (rc == SQLITE_CONSTRAINT) return 1; + return -1; +} + +uint32_t chat_core_cursor_open(const char* ch_id) { + if (!g_cc.initialized) return 0; + for (int i = 0; i < 16; i++) { + if (g_cc.cursors[i]) continue; + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "SELECT timestamp, datahash, node_id, content_type, data" + " FROM \"%s\" ORDER BY timestamp, datahash ASC", tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return 0; + uint32_t id = ++g_cc.next_cursor_id; + g_cc.cursors[i] = stmt; + return id; + } + return 0; +} + +int chat_core_cursor_next(uint32_t cursor_id, uint8_t* buf, size_t buf_size, + size_t* out_len) { + if (!g_cc.initialized || !buf || !out_len) return -1; + for (int i = 0; i < 16; i++) { + if (!g_cc.cursors[i]) continue; + /* проверяем cursor_id = i+1 */ + if ((uint32_t)(i + 1) != cursor_id) continue; + sqlite3_stmt* stmt = g_cc.cursors[i]; + if (sqlite3_step(stmt) != SQLITE_ROW) { *out_len = 0; return 0; } + + int64_t ts = sqlite3_column_int64(stmt, 0); + uint64_t dh = (uint64_t)sqlite3_column_int64(stmt, 1); + uint64_t nid= (uint64_t)sqlite3_column_int64(stmt, 2); + const char* ct = (const char*)sqlite3_column_text(stmt, 3); + int ct_len = sqlite3_column_bytes(stmt, 3); + const uint8_t* data = (const uint8_t*)sqlite3_column_blob(stmt, 4); + int dlen = sqlite3_column_bytes(stmt, 4); + + size_t need = 8 + 8 + 8 + 1 + (size_t)ct_len + 4 + (size_t)dlen; + if (need > buf_size) return -1; + + uint8_t* out = buf; + memcpy(out, &ts, 8); out += 8; + memcpy(out, &dh, 8); out += 8; + memcpy(out, &nid, 8); out += 8; + *out++ = (uint8_t)ct_len; + if (ct_len) { memcpy(out, ct, (size_t)ct_len); out += ct_len; } + uint32_t dl32 = (uint32_t)dlen; + memcpy(out, &dl32, 4); out += 4; + if (dlen) { memcpy(out, data, (size_t)dlen); out += dlen; } + *out_len = (size_t)(out - buf); + return 0; + } + return -1; +} + +void chat_core_cursor_close(uint32_t cursor_id) { + if (!g_cc.initialized) return; + for (int i = 0; i < 16; i++) { + if (g_cc.cursors[i] && (uint32_t)(i + 1) == cursor_id) { + sqlite3_finalize(g_cc.cursors[i]); + g_cc.cursors[i] = NULL; + return; + } + } +} + +void chat_core_mark_sent(const char* ch_id, uint64_t ts, uint64_t dh, + uint64_t node_id) { + if (!g_cc.initialized) return; + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "UPDATE \"%s\" SET sync_flags=sync_flags|1" + " WHERE timestamp=? AND datahash=? AND node_id=? AND is_outgoing=1", tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) return; + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)ts); + sqlite3_bind_int64(stmt, 2, (sqlite3_int64)dh); + sqlite3_bind_int64(stmt, 3, (sqlite3_int64)node_id); + sqlite3_step(stmt); sqlite3_finalize(stmt); +} + +void chat_core_ttl_delete(const char* ch_id, uint64_t node_id, + uint64_t cutoff_us) { + if (!g_cc.initialized) return; + char tbl[80]; msg_table_name(ch_id, tbl, sizeof(tbl)); + char sql[256]; snprintf(sql, sizeof(sql), + "DELETE FROM \"%s\" WHERE node_id=? AND is_outgoing=1 AND (sync_flags&1)=0 AND timestamp 63) continue; + size_t need = (size_t)(out - buf) + 1 + (size_t)ch_len; + if (need > buf_size) break; + *out++ = (uint8_t)ch_len; + memcpy(out, ch_id, (size_t)ch_len); out += ch_len; + cnt++; + } + sqlite3_finalize(stmt); + + memcpy(start, &cnt, 2); + *out_len = (size_t)(out - buf); + return 0; +} + +int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size, + size_t* out_len) { + if (!g_cc.initialized || !buf || !out_len) return -1; + char tbl[80]; peers_table_name(ch_id, tbl, sizeof(tbl)); + char sql[128]; snprintf(sql, sizeof(sql), + "SELECT node_id FROM \"%s\"", tbl); + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, sql, -1, &stmt, NULL) != SQLITE_OK) { + uint16_t z = 0; memcpy(buf, &z, 2); *out_len = 2; return -1; + } + uint16_t cnt = 0; + uint8_t* out = buf + 2; + while (sqlite3_step(stmt) == SQLITE_ROW) { + if ((size_t)(out - buf) + 8 > buf_size) break; + uint64_t nid = (uint64_t)sqlite3_column_int64(stmt, 0); + memcpy(out, &nid, 8); out += 8; cnt++; + } + sqlite3_finalize(stmt); + memcpy(buf, &cnt, 2); + *out_len = (size_t)(out - buf); + return 0; +} + +int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size, + size_t* out_len) { + if (!g_cc.initialized || !buf || !out_len) return -1; + + sqlite3_stmt* stmt = NULL; + if (sqlite3_prepare_v2(g_cc.db, + "SELECT x25519_pubkey, ed25519_pubkey FROM nodes WHERE node_id=?", + -1, &stmt, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + if (sqlite3_step(stmt) != SQLITE_ROW) { sqlite3_finalize(stmt); return -1; } + const uint8_t* x25519 = (const uint8_t*)sqlite3_column_blob(stmt, 0); + int x25519_len = sqlite3_column_bytes(stmt, 0); + const uint8_t* ed25519 = (const uint8_t*)sqlite3_column_blob(stmt, 1); + int ed25519_len = sqlite3_column_bytes(stmt, 1); + + uint8_t* out = buf; + if (x25519 && x25519_len == 32) memcpy(out, x25519, 32); + else memset(out, 0, 32); + out += 32; + if (ed25519 && ed25519_len == 32) memcpy(out, ed25519, 32); + else memset(out, 0, 32); + out += 32; + sqlite3_finalize(stmt); + + /* адреса */ + if (sqlite3_prepare_v2(g_cc.db, + "SELECT family, protocol, address, port, rtt FROM node_addresses WHERE node_id=?", + -1, &stmt, NULL) != SQLITE_OK) { + *out++ = 0; *out_len = (size_t)(out - buf); return 0; + } + sqlite3_bind_int64(stmt, 1, (sqlite3_int64)node_id); + uint8_t* cnt_pos = out; *out++ = 0; + uint8_t addr_cnt = 0; + + while (sqlite3_step(stmt) == SQLITE_ROW && addr_cnt < 255) { + int family = sqlite3_column_int(stmt, 0); + int proto = sqlite3_column_int(stmt, 1); + const uint8_t* addr = (const uint8_t*)sqlite3_column_blob(stmt, 2); + int addr_len = sqlite3_column_bytes(stmt, 2); + uint16_t port = (uint16_t)sqlite3_column_int(stmt, 3); + int16_t rtt = (int16_t)sqlite3_column_int(stmt, 4); + + if ((size_t)(out - buf) + 7 + (size_t)addr_len > buf_size) break; + *out++ = (uint8_t)family; + *out++ = (uint8_t)proto; + *out++ = (uint8_t)addr_len; + if (addr_len > 0) { memcpy(out, addr, (size_t)addr_len); out += addr_len; } + memcpy(out, &port, 2); out += 2; + memcpy(out, &rtt, 2); out += 2; + addr_cnt++; + } + *cnt_pos = addr_cnt; + sqlite3_finalize(stmt); + + *out_len = (size_t)(out - buf); + return 0; +} + +/* ─── подключение к пиру из invite-ссылки ─── */ + +static void connect_result_cb(int result, uint64_t node_id, void* arg) { + uint8_t data[12]; + memcpy(data, &node_id, 8); + memcpy(data + 8, &result, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, data, 12); +} + +void chat_core_connect_from_invite(struct chat_invite* inv) { + if (!g_cc.initialized || !g_cc.inst || !inv) return; + if (!g_cc.inst->bgp || !g_cc.inst->conn_mgr) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: bgp/conn_mgr not available", CC_ID); + uint8_t err[12]; int r = -7; + memcpy(err, &inv->node_id, 8); memcpy(err + 8, &r, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); + return; + } + + uint64_t node_id = inv->node_id; + struct ROUTE_BGP* bgp = g_cc.inst->bgp; + + if (nodeinfo_find_by_id(bgp, node_id)) { + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: node 0x%016llx already in BGP, connecting", + CC_ID, (unsigned long long)node_id); + conn_mgr_connect_node(g_cc.inst->conn_mgr, node_id, 30000, + connect_result_cb, NULL); + return; + } + + size_t dyn_sz = (size_t)inv->addr_count * sizeof(struct NODEINFO_IPV4_ADDR); + size_t data_sz = sizeof(struct NODEINFO_Q) - sizeof(struct ll_entry) + dyn_sz + 8; + struct NODEINFO_Q* nq = (struct NODEINFO_Q*)queue_entry_new(data_sz); + if (!nq) { + DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "%s: failed to alloc NODEINFO_Q", CC_ID); + uint8_t err[12]; int r = -7; + memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); + gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); + return; + } + + struct NODEINFO* ni = &nq->node; + ni->node_id = node_id; + ni->ver = 0; + memcpy(ni->public_key, inv->pubkey, SC_PUBKEY_SIZE); + memset(ni->ed25519_public_key, 0, SC_PUBKEY_SIZE); + ni->node_name_len = 0; + ni->local_v4_sockets = 0; + ni->local_v4_addrs = (uint8_t)inv->addr_count; + ni->local_v6_sockets = 0; + ni->local_v6_addrs = 0; + ni->local_v4_subnets = 0; + ni->local_v6_subnets = 0; + ni->tranzit_nodes = 0; + ni->hop_count = 0; + + uint8_t* dyn = (uint8_t*)ni + sizeof(struct NODEINFO); + const uint8_t* src = inv->addrs_data; + for (int i = 0; i < inv->addr_count; i++) { + struct NODEINFO_IPV4_ADDR* addr = + (struct NODEINFO_IPV4_ADDR*)(dyn + i * sizeof(struct NODEINFO_IPV4_ADDR)); + memset(addr, 0, sizeof(*addr)); + uint8_t family = *src++; + if (family == 4) { + memcpy(addr->addr, src, 4); src += 4; + uint16_t port_be = ((uint16_t)src[0] << 8) | src[1]; src += 2; + addr->port = port_be; + addr->type = ADDR_TYPE_REAL; + addr->socket_id = 0; + addr->protocol = NODE_PROTO_UDP; + } else { + src += 18; continue; + } + } + + nq->alien = 0; + nq->dirty = 0; + nq->last_ver = 0; + nq->conn_mgr_type = CONN_TYPE_NONE; + nq->conn_mgr_intermediariy_count = 0; + memset(&nq->connectivity, 0, sizeof(nq->connectivity)); + nq->best_socket = NULL; + + queue_data_put_with_index(bgp->nodes, &nq->ll); + + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: created NODEINFO for 0x%016llx, %d addrs, connecting", + CC_ID, (unsigned long long)node_id, inv->addr_count); + conn_mgr_connect_node(g_cc.inst->conn_mgr, node_id, 30000, + connect_result_cb, NULL); +} diff --git a/tools/chatgui/transport/chat_core.h b/tools/chatgui/transport/chat_core.h new file mode 100644 index 00000000..3354b1f8 --- /dev/null +++ b/tools/chatgui/transport/chat_core.h @@ -0,0 +1,73 @@ +/* + * chat_core.h — центральный API чата в потоке uasync + * + * Все DB-операции и сетевые функции выполняются в uasync-потоке. + * GUI вызывает chat_core_submit_message() через gui_bridge_post_uasync(). + */ + +#ifndef CHAT_CORE_H +#define CHAT_CORE_H + +#include +#include + +struct sqlite3; +struct UTUN_INSTANCE; + +/* ── Жизненный цикл ── */ + +int chat_core_init(struct UTUN_INSTANCE* inst, const char* db_path); +void chat_core_destroy(struct UTUN_INSTANCE* inst); + +/* ── Отправка сообщения (GUI → uasync) ── */ + +struct chat_msg_submit { + char channel_id[64]; + char content_type[32]; + uint8_t* data; + uint32_t data_len; + uint64_t timestamp; +}; + +void chat_core_submit_message(struct chat_msg_submit* req); + +/* ── Push-only (без вставки в БД, только рассылка пирам) ── */ + +void chat_core_push_message(struct chat_msg_submit* req); + +/* ── DB-операции для chat_sync ── */ + +uint32_t chat_core_count(const char* ch_id); +int chat_core_chain_hash_at(const char* ch_id, uint32_t pos, uint8_t* hash_out); +int chat_core_insert_record(const char* ch_id, const uint8_t* rec, size_t len); +uint32_t chat_core_cursor_open(const char* ch_id); +int chat_core_cursor_next(uint32_t cursor_id, uint8_t* buf, size_t buf_size, size_t* out_len); +void chat_core_cursor_close(uint32_t cursor_id); +void chat_core_mark_sent(const char* ch_id, uint64_t ts, uint64_t dh, uint64_t node_id); +void chat_core_ttl_delete(const char* ch_id, uint64_t node_id, uint64_t cutoff_us); +int chat_core_list_channels(uint8_t* buf, size_t buf_size, size_t* out_len); +int chat_core_list_peers(const char* ch_id, uint8_t* buf, size_t buf_size, size_t* out_len); +int chat_core_load_nodeinfo(uint64_t node_id, uint8_t* buf, size_t buf_size, size_t* out_len); + +/* ── Подключение к пиру (из invite-ссылки) ── */ + +struct chat_invite { + uint64_t node_id; + uint8_t pubkey[32]; + uint8_t* addrs_data; + int addr_count; + uint8_t* ch_id; + int ch_id_len; +}; + +void chat_core_connect_from_invite(struct chat_invite* inv); + +/* ── Утилиты ── */ + +void chat_core_set_my_node_id(uint64_t node_id); + +/* Трамплин для gui_bridge_post_uasync (GUI → uasync) */ +void chat_core_submit_trampoline(void* arg); +void chat_core_push_trampoline(void* arg); + +#endif /* CHAT_CORE_H */ diff --git a/tools/chatgui/transport/chat_propagator.cpp b/tools/chatgui/transport/chat_propagator.cpp deleted file mode 100644 index 99bc1ba4..00000000 --- a/tools/chatgui/transport/chat_propagator.cpp +++ /dev/null @@ -1,376 +0,0 @@ -#include "chat_propagator.h" -#include "chat_proto.h" -#include "utun_node.h" -#include "crypto.h" -#include "db_manager.h" - -#include -#include -#include - -static uint64_t read64(const uint8_t* p) { - uint64_t v; memcpy(&v, p, 8); return v; -} -static void write64(uint8_t* p, uint64_t v) { memcpy(p, &v, 8); } -static uint32_t read32(const uint8_t* p) { - uint32_t v; memcpy(&v, p, 4); return v; -} -static void write32(uint8_t* p, uint32_t v) { memcpy(p, &v, 4); } -static uint16_t read16(const uint8_t* p) { - uint16_t v; memcpy(&v, p, 2); return v; -} -static void write16(uint8_t* p, uint16_t v) { memcpy(p, &v, 2); } - -ChatPropagator::ChatPropagator(UtunNode* node, DbManager* db, QObject* parent) - : QObject(parent) - , m_node(node) - , m_db(db) - , m_ackTimer(new QTimer(this)) -{ - connect(m_node, &UtunNode::messageReceived, - this, &ChatPropagator::onReceived); - connect(m_ackTimer, &QTimer::timeout, - this, &ChatPropagator::onAckTimeout); -} - -void ChatPropagator::setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId) { - m_edPriv = edPriv; - m_edPub = edPub; - m_myNodeId = nodeId; -} - -void ChatPropagator::start() { - if (!m_edPriv.isEmpty() && !m_ackTimer->isActive()) - m_ackTimer->start(250); - startCatchUp(); -} - -/* ======================================================================== */ - -void ChatPropagator::propagateNewMessage(const QString& channelId, - const QByteArray& content, - qint64 timestamp, - const QByteArray& signature) { - if (m_edPriv.isEmpty() || !m_myNodeId) return; - - QByteArray packet = buildNewMsgPacket(channelId, content, timestamp, signature); - QList exclude; - exclude.append(m_myNodeId); - fanoutGossip(channelId, packet, signature, ChatProp::kDefaultFanout, exclude); -} - -QByteArray ChatPropagator::buildNewMsgPacket(const QString& channelId, - const QByteArray& content, - qint64 timestamp, - const QByteArray& signature) { - QByteArray cid = channelId.toUtf8(); - uint8_t cidLen = static_cast(cid.size()); - size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize; - - QByteArray pkt(static_cast(total), Qt::Uninitialized); - uint8_t* p = reinterpret_cast(pkt.data()); - *p++ = ChatProp::kNewMsg; - *p++ = cidLen; - memcpy(p, cid.constData(), cidLen); p += cidLen; - write64(p, static_cast(timestamp)); p += 8; - write16(p, static_cast(content.size())); p += 2; - memcpy(p, content.constData(), content.size()); p += content.size(); - memcpy(p, signature.constData(), ChatProp::kSignSize); - return pkt; -} - -QByteArray ChatPropagator::buildAckPacket(const QByteArray& sig) { - QByteArray pkt(static_cast(1 + ChatProp::kSignSize), Qt::Uninitialized); - pkt[0] = static_cast(ChatProp::kNewAck); - memcpy(pkt.data() + 1, sig.constData(), ChatProp::kSignSize); - return pkt; -} - -/* ======================================================================== */ - -void ChatPropagator::fanoutGossip(const QString& channelId, const QByteArray& packet, - const QByteArray& sig, int fanout, - const QList& exclude) { - auto peers = pickRandomPeers(fanout, exclude); - if (peers.isEmpty()) return; - - GossipEntry e; - e.channelId = channelId; - e.packet = packet; - e.sig = sig; - e.peers = peers; - e.sentAt = QDateTime::currentMSecsSinceEpoch(); - m_pending[sig] = e; - - for (auto peer : peers) - sendGossip(peer, packet); -} - -void ChatPropagator::sendGossip(quint64 peer, const QByteArray& packet) { - m_node->send(peer, packet); -} - -/* ======================================================================== */ - -void ChatPropagator::onReceived(quint64 peerId, const QByteArray& data) { - if (data.isEmpty()) return; - uint8_t type = static_cast(data[0]); - switch (type) { - case ChatProp::kNewMsg: handleNewMsg(peerId, data); break; - case ChatProp::kNewAck: handleNewAck(data); break; - case ChatProp::kBucketsRsp: handleBucketsRsp(data); break; - case ChatProp::kHashesRsp: handleHashesRsp(data); break; - case ChatProp::kMsgsRsp: handleMsgsRsp(data); break; - } -} - -void ChatPropagator::handleNewMsg(quint64 fromPeer, const QByteArray& payload) { - QString channelId; - QByteArray content; - qint64 timestamp; - QByteArray sig; - if (!parseNewMsg(payload, channelId, content, timestamp, sig)) - return; - - /* verify sender's signature */ - QByteArray peerEdPub = m_db->getNodeEdPub(fromPeer); - if (!peerEdPub.isEmpty() && !Crypto::verify(peerEdPub, payload.left(payload.size() - 64), sig)) - return; - - /* insert into DB */ - m_db->insertMessage(channelId, fromPeer, content, timestamp, 0); - - emit messageReceived(channelId, fromPeer, content, timestamp); - - /* re-gossip */ - QList exclude; - exclude.append(m_myNodeId); - exclude.append(fromPeer); - fanoutGossip(channelId, payload, sig, ChatProp::kDefaultFanout, exclude); - - /* ack to sender */ - m_node->send(fromPeer, buildAckPacket(sig)); -} - -void ChatPropagator::handleNewAck(const QByteArray& payload) { - if (payload.size() < static_cast(1 + ChatProp::kSignSize)) return; - QByteArray sig = payload.mid(1, static_cast(ChatProp::kSignSize)); - - auto it = m_pending.find(sig); - if (it == m_pending.end()) return; - - /* mark all peers as acked (simple version: ACK means message delivered) */ - m_pending.erase(it); -} - -void ChatPropagator::onAckTimeout() { - qint64 now = QDateTime::currentMSecsSinceEpoch(); - QList toRemove; - - for (auto it = m_pending.begin(); it != m_pending.end(); ++it) { - auto& e = it.value(); - if (now - e.sentAt < ChatProp::kDefaultAckTimeoutMs) continue; - - if (e.retries >= 3) { - toRemove.append(it.key()); - continue; - } - - /* retry: pick new peers for each missing ACK */ - QList unacked; - for (auto p : e.peers) - if (!e.acked.contains(p)) unacked.append(p); - - if (!unacked.isEmpty()) { - auto newPeers = pickRandomPeers(1, e.peers + e.acked + - QList{m_myNodeId}); - if (!newPeers.isEmpty()) { - sendGossip(newPeers.first(), e.packet); - e.peers.append(newPeers.first()); - } - } - - e.sentAt = now; - e.retries++; - } - - for (auto& k : toRemove) m_pending.remove(k); -} - -/* ======================================================================== - * Catch-up - * ======================================================================== */ - -void ChatPropagator::startCatchUp() { - if (!m_db) return; - auto channels = m_db->getChannels(); - if (channels.isEmpty()) return; - - auto peers = pickRandomPeers(ChatProp::kDefaultFanout, {m_myNodeId}); - if (peers.isEmpty()) return; - - for (auto& ch : channels) { - QByteArray cid = ch.channelId.toUtf8(); - QByteArray pkt(static_cast(2 + cid.size()), Qt::Uninitialized); - pkt[0] = static_cast(ChatProp::kBucketsReq); - pkt[1] = static_cast(cid.size()); - memcpy(pkt.data() + 2, cid.constData(), cid.size()); - - for (auto peer : peers) - sendGossip(peer, pkt); - } -} - -void ChatPropagator::handleBucketsRsp(const QByteArray& payload) { - if (payload.size() < 3) return; - const uint8_t* p = reinterpret_cast(payload.constData()); - if (*p != ChatProp::kBucketsRsp) return; p++; - uint8_t cidLen = *p++; - if (p + cidLen + 2 > reinterpret_cast(payload.constData()) + payload.size()) return; - QString channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; - uint16_t bCount = read16(p); p += 2; - - for (int i = 0; i < bCount; i++) { - if (p + 4 + 1 + 4 + 32 > reinterpret_cast(payload.constData()) + payload.size()) break; - qint64 day = static_cast(read32(p)); p += 4; - int hour = *p++; - int rCount = static_cast(read32(p)); p += 4; - QByteArray rHash(reinterpret_cast(p), 32); p += 32; - - auto local = m_db->getBucketMeta(channelId); - bool found = false; - for (auto& b : local) { - if (b.day == day && b.hour == hour && b.count == rCount && b.hash == rHash) { - found = true; break; - } - } - if (found) continue; - - /* mismatch — request signatures */ - QByteArray cid = channelId.toUtf8(); - QByteArray req(static_cast(7 + cid.size()), Qt::Uninitialized); - uint8_t* rp = reinterpret_cast(req.data()); - *rp++ = ChatProp::kHashesReq; - *rp++ = static_cast(cid.size()); - memcpy(rp, cid.constData(), cid.size()); rp += cid.size(); - write32(rp, static_cast(day)); rp += 4; - *rp = static_cast(hour); - - auto peers = pickRandomPeers(1, {m_myNodeId}); - if (!peers.isEmpty()) - sendGossip(peers.first(), req); - } -} - -void ChatPropagator::handleHashesRsp(const QByteArray& payload) { - if (payload.size() < 3) return; - const uint8_t* p = reinterpret_cast(payload.constData()); - if (*p != ChatProp::kHashesRsp) return; p++; - uint16_t sigCount = read16(p); p += 2; - - QList remoteSigs; - for (int i = 0; i < sigCount; i++) { - if (p + 64 > reinterpret_cast(payload.constData()) + payload.size()) break; - remoteSigs.append(QByteArray(reinterpret_cast(p), 64)); - p += 64; - } - if (remoteSigs.isEmpty()) return; - - /* find missing */ - QList missing; - QList remote = remoteSigs; - for (auto& s : remote) { - /* TODO: efficient lookup — query DB for this sig */ - auto msgs = m_db->getMessagesBySigs({s}); - if (msgs.isEmpty()) missing.append(s); - } - if (missing.isEmpty()) return; - - /* request missing messages */ - int mCount = missing.size(); - QByteArray req(static_cast(3 + mCount * 64), Qt::Uninitialized); - uint8_t* rp = reinterpret_cast(req.data()); - *rp++ = ChatProp::kMsgsReq; - write16(rp, static_cast(mCount)); rp += 2; - for (auto& s : missing) { memcpy(rp, s.constData(), 64); rp += 64; } - - auto peers = pickRandomPeers(1, {m_myNodeId}); - if (!peers.isEmpty()) - sendGossip(peers.first(), req); -} - -void ChatPropagator::handleMsgsRsp(const QByteArray& payload) { - if (payload.size() < 4) return; - const uint8_t* p = reinterpret_cast(payload.constData()); - const uint8_t* end = p + payload.size(); - if (*p != ChatProp::kMsgsRsp) return; p++; - uint8_t cidLen = *p++; - if (p + cidLen + 2 > end) return; - QString channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; - uint16_t msgCount = read16(p); p += 2; - - for (int i = 0; i < msgCount; i++) { - if (p + 64 + 8 + 8 + 2 > end) break; - QByteArray sig(reinterpret_cast(p), 64); p += 64; - quint64 author = read64(p); p += 8; - qint64 ts = static_cast(read64(p)); p += 8; - uint16_t cLen = read16(p); p += 2; - if (p + cLen > end) break; - QByteArray content(reinterpret_cast(p), cLen); p += cLen; - - auto existing = m_db->getMessagesBySigs({sig}); - if (!existing.isEmpty()) continue; - - m_db->insertMessage(channelId, author, content, ts, 0); - emit messageReceived(channelId, author, content, ts); - } -} - -/* ======================================================================== */ - -bool ChatPropagator::parseNewMsg(const QByteArray& payload, QString& channelId, - QByteArray& content, qint64& timestamp, - QByteArray& sig) { - if (payload.size() < 14) return false; /* minimum: 1+1+1+8+2+1 */ - - const uint8_t* p = reinterpret_cast(payload.constData()); - if (*p != ChatProp::kNewMsg) return false; - p++; - - uint8_t cidLen = *p++; - if (p + cidLen + 8 + 2 > reinterpret_cast(payload.constData()) + payload.size()) - return false; - channelId = QString::fromUtf8(reinterpret_cast(p), cidLen); p += cidLen; - - timestamp = static_cast(read64(p)); p += 8; - - uint16_t contentLen = read16(p); p += 2; - if (p + contentLen + ChatProp::kSignSize > - reinterpret_cast(payload.constData()) + payload.size()) - return false; - - content = QByteArray(reinterpret_cast(p), contentLen); p += contentLen; - sig = QByteArray(reinterpret_cast(p), static_cast(ChatProp::kSignSize)); - return true; -} - -QList ChatPropagator::pickRandomPeers(int count, const QList& exclude) { - QList result; - if (!m_db) return result; - - auto accs = m_db->getAccounts(true); - QList pool; - for (auto& a : accs) { - if (!exclude.contains(a.nodeId)) - pool.append(a.nodeId); - } - if (pool.isEmpty()) return result; - - std::random_device rd; - std::mt19937 g(rd()); - std::shuffle(pool.begin(), pool.end(), g); - - for (int i = 0; i < count && i < pool.size(); i++) - result.append(pool[i]); - return result; -} diff --git a/tools/chatgui/transport/chat_propagator.h b/tools/chatgui/transport/chat_propagator.h deleted file mode 100644 index beb7d5fc..00000000 --- a/tools/chatgui/transport/chat_propagator.h +++ /dev/null @@ -1,70 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class UtunNode; -class DbManager; -struct MessageRow; - -class ChatPropagator : public QObject { - Q_OBJECT -public: - ChatPropagator(UtunNode* node, DbManager* db, QObject* parent = nullptr); - - void setKeys(const QByteArray& edPriv, const QByteArray& edPub, quint64 nodeId); - void start(); - void propagateNewMessage(const QString& channelId, const QByteArray& content, - qint64 timestamp, const QByteArray& signature); - -signals: - void messageReceived(const QString& channelId, quint64 authorNodeId, - const QByteArray& content, qint64 timestamp); - -private: - UtunNode* m_node; - DbManager* m_db; - QTimer* m_ackTimer; - - QByteArray m_edPriv; - QByteArray m_edPub; - quint64 m_myNodeId = 0; - - struct GossipEntry { - QString channelId; - QByteArray packet; /* full NEW_MSG packet */ - QByteArray sig; /* message signature (key) */ - QList peers; /* target peers */ - QList acked; /* peers that acknowledged */ - qint64 sentAt; /* when sent (ms) */ - int retries = 0; - }; - QHash m_pending; /* sig → entry */ - QByteArray m_mySig; /* sig we're waiting for ACK on */ - - void onReceived(quint64 peerId, const QByteArray& data); - void handleNewMsg(quint64 fromPeer, const QByteArray& payload); - void handleNewAck(const QByteArray& payload); - void onAckTimeout(); - - void startCatchUp(); - void handleBucketsRsp(const QByteArray& payload); - void handleHashesRsp(const QByteArray& payload); - void handleMsgsRsp(const QByteArray& payload); - - void fanoutGossip(const QString& channelId, const QByteArray& packet, - const QByteArray& sig, int fanout, const QList& exclude); - void sendGossip(quint64 peer, const QByteArray& packet); - - QList pickRandomPeers(int count, const QList& exclude); - - QByteArray buildNewMsgPacket(const QString& channelId, const QByteArray& content, - qint64 timestamp, const QByteArray& signature); - QByteArray buildAckPacket(const QByteArray& sig); - bool parseNewMsg(const QByteArray& payload, QString& channelId, QByteArray& content, - qint64& timestamp, QByteArray& sig); -}; diff --git a/tools/chatgui/transport/chat_proto.h b/tools/chatgui/transport/chat_proto.h deleted file mode 100644 index eb209986..00000000 --- a/tools/chatgui/transport/chat_proto.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * chat_proto.h — протокол распространения сообщений чата (gossip + catch-up) - * - * Все пакеты идут через MsgClient::sendTo() / received() поверх msg_transport. - * Подписи — внутри пейлоада. utun — чистый транспорт. - */ - -#ifndef CHAT_PROPAGATE_PROTO_H -#define CHAT_PROPAGATE_PROTO_H - -#include - -namespace ChatProp { - -static constexpr uint8_t kNewMsg = 0x01; /* новое сообщение */ -static constexpr uint8_t kNewAck = 0x02; /* подтверждение получения */ -static constexpr uint8_t kBucketsReq = 0x03; /* запрос bucket-ов канала */ -static constexpr uint8_t kBucketsRsp = 0x04; /* bucket-ы: день+час → count+hash */ -static constexpr uint8_t kHashesReq = 0x05; /* запрос подписей в bucket */ -static constexpr uint8_t kHashesRsp = 0x06; /* список подписей (64 байта каждая) */ -static constexpr uint8_t kMsgsReq = 0x07; /* запрос сообщений по подписям */ -static constexpr uint8_t kMsgsRsp = 0x08; /* сообщения + подписи */ - -static constexpr size_t kSignSize = 64; -static constexpr size_t kHashSize = 32; /* SHA-256 */ -static constexpr int kDefaultFanout = 4; -static constexpr int kDefaultAckTimeoutMs = 1000; - -/* Форматы пакетов (byte stream) ================================================ - - NEW_MSG: [0x01][channel_id_len:1][channel_id][timestamp:8][content_len:2][content][sig:64] - NEW_ACK: [0x02][sig_of_confirmed_msg:64] - - BUCKETS_REQ: [0x03][channel_id_len:1][channel_id] - BUCKETS_RSP: [0x04][channel_id_len:1][channel_id][bucket_count:2][buckets...] - bucket = [day_unix:4][hour:1][msg_count:4][sig_hash:32] - - HASHES_REQ: [0x05][channel_id_len:1][channel_id][day_unix:4][hour:1] - HASHES_RSP: [0x06][sig_count:2][sig:64, sig:64, ...] - - MSGS_REQ: [0x07][sig_count:2][sig:64, sig:64, ...] - MSGS_RSP: [0x08][channel_id_len:1][channel_id][msg_count:2][(sig:64, author:8, timestamp:8, content_len:2, content)...] - -=============================================================================== */ - -} // namespace ChatProp - -#endif diff --git a/tools/chatgui/transport/chat_sync.c b/tools/chatgui/transport/chat_sync.c index 5f90d7aa..9f75e8cc 100644 --- a/tools/chatgui/transport/chat_sync.c +++ b/tools/chatgui/transport/chat_sync.c @@ -1,4 +1,5 @@ #include "chat_sync.h" +#include "chat_core.h" #include "gui_bridge.h" #include "../../../src/utun_instance.h" @@ -6,21 +7,16 @@ #include "../../../src/etcp_api.h" #include "../../../src/etcp.h" #include "../../../src/conn_mgr.h" -#include "../../../src/route_bgp.h" -#include "../../../src/route_node.h" #include "../../../lib/u_async.h" #include "../../../lib/ll_queue.h" #include "../../../lib/debug_config.h" #include "../../../lib/mem.h" -#include "../../../lib/sha256.h" #include "../../../lib/platform_compat.h" #include static struct chat_sync* g_cs = NULL; -/* ── Per-channel cache ── */ - struct channel_cache { char channel_id[64]; uint64_t* peer_ids; @@ -69,147 +65,35 @@ static struct channel_cache* cs_find(struct chat_sync* cs, const char* ch_id) { return NULL; } -/* ── Protocol message context (async state machine) ── */ - -struct cs_ctx { - struct chat_sync* sync; - uint64_t peer; - char ch_id[64]; - union { - struct { - uint32_t peer_count; - uint8_t peer_last_ch[32]; - uint32_t my_count; - uint32_t test_pos; - } init_sync; - struct { - uint32_t peer_count; - uint32_t test_pos; - uint8_t peer_ch[37]; /* 1 byte for sparse_count + 36 for first sparse */ - int parse_off; /* offset in original payload for sparse data */ - size_t parse_len; - } init_resp; - struct { - uint32_t cursor_id; - uint32_t from_pos; - uint16_t sent_count; - } send_data; - struct { - uint64_t ts; - uint64_t dh; - } push; - }; -}; - -static struct cs_ctx* cs_ctx_new(struct chat_sync* cs, uint64_t peer, const char* ch_id) { - struct cs_ctx* ctx = u_calloc(1, sizeof(*ctx)); - if (!ctx) return NULL; - ctx->sync = cs; ctx->peer = peer; - size_t n = strlen(ch_id); if (n > 63) n = 63; - memcpy(ctx->ch_id, ch_id, n); - return ctx; -} - -/* ── gui_bridge async callbacks ── */ - -static void on_init_sync_count(void* ud, int op, const uint8_t* r, int rl); -static void on_init_sync_hash(void* ud, int op, const uint8_t* r, int rl); -static void on_init_sync_sparse(void* ud, int op, const uint8_t* r, int rl); -static void on_init_resp_hash(void* ud, int op, const uint8_t* r, int rl); -static void on_send_data_cursor_open(void* ud, int op, const uint8_t* r, int rl); -static void on_send_data_cursor_next(void* ud, int op, const uint8_t* r, int rl); -static void on_push_inserted(void* ud, int op, const uint8_t* r, int rl); -static void on_refresh_list(void* ud, int op, const uint8_t* r, int rl); -static void on_refresh_peers_and_count(void* ud, int op, const uint8_t* r, int rl); - -/* helper: build request [ch_id_len:1][ch_id:var] */ -static int req_ch(const char* ch_id, uint8_t* buf) { - uint8_t n = (uint8_t)strlen(ch_id); - buf[0] = n; memcpy(buf + 1, ch_id, n); - return 1 + n; -} - /* ── INIT_SYNC handler ── */ static void cs_handle_init_sync(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { if (len < 36) return; - struct cs_ctx* ctx = cs_ctx_new(cs, peer, ch_id); - if (!ctx) return; - memcpy(&ctx->init_sync.peer_count, pl, 4); - memcpy(ctx->init_sync.peer_last_ch, pl + 4, 32); - uint8_t req[65]; int rl = req_ch(ch_id, req); - gui_bridge_call(GUI_OP_COUNT, req, rl, ctx, on_init_sync_count); -} + uint32_t peer_count; memcpy(&peer_count, pl, 4); + uint8_t peer_last_ch[32]; memcpy(peer_last_ch, pl + 4, 32); -static void on_init_sync_count(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - uint32_t my_count = (rl >= 4) ? *(const uint32_t*)r : 0; - ctx->init_sync.my_count = my_count; - - uint32_t tp = ctx->init_sync.peer_count < my_count ? ctx->init_sync.peer_count : my_count; + uint32_t my_count = chat_core_count(ch_id); + uint32_t tp = peer_count < my_count ? peer_count : my_count; if (tp > 0) tp--; - ctx->init_sync.test_pos = tp; + uint8_t my_ch[32]; + chat_core_chain_hash_at(ch_id, tp, my_ch); - uint8_t req[69]; int nr = req_ch(ctx->ch_id, req); - memcpy(req + nr, &tp, 4); nr += 4; - gui_bridge_call(GUI_OP_CHAIN_HASH_AT, req, nr, ctx, on_init_sync_hash); -} - -static void on_init_sync_hash(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - uint32_t tp = ctx->init_sync.test_pos; - uint8_t my_ch[32]; memset(my_ch, 0, 32); - if (rl >= 32) memcpy(my_ch, r, 32); - - if (memcmp(my_ch, ctx->init_sync.peer_last_ch, 32) == 0) { + if (memcmp(my_ch, peer_last_ch, 32) == 0) { /* synced */ uint8_t resp[64]; resp[0] = CS_MSG_INIT_RESP; - memcpy(resp + 1, &ctx->init_sync.my_count, 4); resp[5] = 0; - cs_send(ctx->sync, ctx->ch_id, ctx->peer, resp, 6); - struct channel_cache* ch = cs_find(ctx->sync, ctx->ch_id); + memcpy(resp + 1, &my_count, 4); resp[5] = 0; + cs_send(cs, ch_id, peer, resp, 6); + struct channel_cache* ch = cs_find(cs, ch_id); if (ch) ch->synced = CS_SYNC_DONE; - u_free(ctx); return; - } - - /* Build sparse hashes: async chain */ - uint8_t resp[1024]; uint32_t off = 0; - resp[off++] = CS_MSG_INIT_RESP; - memcpy(resp + off, &ctx->init_sync.my_count, 4); off += 4; - memcpy(resp + off, &tp, 4); off += 4; - memcpy(resp + off, my_ch, 32); off += 32; - uint8_t sparse_pos = off; off++; /* placeholder */ - uint8_t sparse_count = 0; - - /* First sparse hash to start chain */ - uint32_t first_step = 1; - if (tp >= first_step) { - uint32_t pos = tp - first_step; - ctx->init_sync.test_pos = tp; /* сохраняем для продолжения */ - /* Запрашиваем первый sparse hash */ - uint8_t req2[69]; int nr2 = req_ch(ctx->ch_id, req2); - memcpy(req2 + nr2, &pos, 4); nr2 += 4; - gui_bridge_call(GUI_OP_CHAIN_HASH_AT, req2, nr2, ctx, on_init_sync_sparse); return; } - resp[sparse_pos] = 0; - cs_send(ctx->sync, ctx->ch_id, ctx->peer, resp, off); - u_free(ctx); -} -static void on_init_sync_sparse(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - /* For initial version: just send INIT_RESP with 0 sparse. - Peer will handle divergence. */ - uint32_t tp = ctx->init_sync.test_pos; + /* divergence — send INIT_RESP with 0 sparse for now, peer will handle */ uint8_t resp[64]; resp[0] = CS_MSG_INIT_RESP; - memcpy(resp + 1, &ctx->init_sync.my_count, 4); + memcpy(resp + 1, &my_count, 4); resp[5] = 0; /* sparse_count=0 */ - cs_send(ctx->sync, ctx->ch_id, ctx->peer, resp, 6); - u_free(ctx); + cs_send(cs, ch_id, peer, resp, 6); } /* ── INIT_RESP handler ── */ @@ -217,48 +101,32 @@ static void on_init_sync_sparse(void* ud, int op, const uint8_t* r, int rl) { static void cs_handle_init_resp(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { if (len < 37) return; - struct cs_ctx* ctx = cs_ctx_new(cs, peer, ch_id); - if (!ctx) return; - ctx->init_resp.peer_count = *(const uint32_t*)pl; - ctx->init_resp.test_pos = *(const uint32_t*)(pl + 4); - memcpy(ctx->init_resp.peer_ch, pl + 8, 37); /* hash:32 + sparse_count:1 + maybe more */ - - uint8_t req[69]; int nr = req_ch(ch_id, req); - memcpy(req + nr, &ctx->init_resp.test_pos, 4); nr += 4; - gui_bridge_call(GUI_OP_CHAIN_HASH_AT, req, nr, ctx, on_init_resp_hash); -} - -static void on_init_resp_hash(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - uint8_t my_ch[32]; memset(my_ch, 0, 32); - if (rl >= 32) memcpy(my_ch, r, 32); - - uint8_t sparse_count = ctx->init_resp.peer_ch[32]; - - /* Check match */ - if (memcmp(my_ch, ctx->init_resp.peer_ch, 32) == 0) { - /* Divergence is in our extra records (or peer's) */ - /* Request data from peer starting at test_pos+1 if peer has more */ - struct channel_cache* ch = cs_find(ctx->sync, ctx->ch_id); - if (ch && ctx->init_resp.peer_count > ctx->init_resp.test_pos + 1) { - /* We need peer's extra records */ - uint32_t from = ctx->init_resp.test_pos + 1; + uint32_t peer_count = *(const uint32_t*)pl; + uint32_t test_pos = *(const uint32_t*)(pl + 4); + uint8_t peer_ch[32]; memcpy(peer_ch, pl + 8, 32); + uint8_t sparse_count = pl[40]; (void)sparse_count; + + uint8_t my_ch[32]; + chat_core_chain_hash_at(ch_id, test_pos, my_ch); + + if (memcmp(my_ch, peer_ch, 32) == 0) { + struct channel_cache* ch = cs_find(cs, ch_id); + if (ch && peer_count > test_pos + 1) { + uint32_t from = test_pos + 1; uint8_t snd[7]; snd[0] = CS_MSG_SEND_DATA; memcpy(snd + 1, &from, 4); uint16_t z = 0; memcpy(snd + 5, &z, 2); - cs_send(ctx->sync, ctx->ch_id, ctx->peer, snd, 7); + cs_send(cs, ch_id, peer, snd, 7); } else { - ch->synced = CS_SYNC_DONE; + if (ch) ch->synced = CS_SYNC_DONE; } - u_free(ctx); return; + return; } - /* Mismatch — request data from start */ + /* mismatch — request from start */ uint32_t from = 0; uint8_t snd[7]; snd[0] = CS_MSG_SEND_DATA; memcpy(snd + 1, &from, 4); uint16_t z = 0; memcpy(snd + 5, &z, 2); - cs_send(ctx->sync, ctx->ch_id, ctx->peer, snd, 7); - u_free(ctx); + cs_send(cs, ch_id, peer, snd, 7); } /* ── SEND_DATA handler ── */ @@ -270,26 +138,34 @@ static void cs_handle_send_data(struct chat_sync* cs, uint64_t peer, uint16_t count = *(const uint16_t*)(pl + 4); if (count == 0) { - /* Peer requests OUR data starting from 'from' */ - struct cs_ctx* ctx = cs_ctx_new(cs, peer, ch_id); - if (!ctx) return; - ctx->send_data.from_pos = from; - ctx->send_data.sent_count = 0; - - uint8_t req[65]; int nr = req_ch(ch_id, req); - gui_bridge_call(GUI_OP_CURSOR_OPEN, req, nr, ctx, on_send_data_cursor_open); + /* peer requests OUR data starting from 'from' */ + uint32_t cur = chat_core_cursor_open(ch_id); + if (!cur) return; + + uint16_t sent = 0; + uint8_t rec_buf[4096]; size_t rec_len; + + while (chat_core_cursor_next(cur, rec_buf, sizeof(rec_buf), &rec_len) == 0 + && rec_len > 0) { + uint8_t snd[8192]; uint32_t off = 0; + snd[off++] = CS_MSG_SEND_DATA; + uint32_t pos = from + sent; + memcpy(snd + off, &pos, 4); off += 4; + uint16_t c = 1; memcpy(snd + off, &c, 2); off += 2; + if (off + rec_len <= (uint32_t)sizeof(snd)) + { memcpy(snd + off, rec_buf, rec_len); off += (uint32_t)rec_len; } + cs_send(cs, ch_id, peer, snd, off); + sent++; + } + chat_core_cursor_close(cur); return; } - /* Peer sent US data — insert each record */ + /* peer sent US data — insert each record */ const uint8_t* ptr = pl + 6; size_t remain = len - 6; for (uint16_t i = 0; i < count && remain > 0; i++) { - /* record: [ts:8][dh:8][node_id:8][ct_len:1][ct:var][data_len:4][data:var] */ - /* For this, we call GUI_OP_INSERT_RECORD with the full record + chain_hash */ - /* But SEND_DATA doesn't include chain_hash — only PUSH does. - For SEND_DATA: skip chain_hash, just insert the content fields */ if (remain < 29) break; const uint8_t* rec_start = ptr; ptr += 8 + 8 + 8; remain -= 24; /* ts, dh, nid */ @@ -303,21 +179,9 @@ static void cs_handle_send_data(struct chat_sync* cs, uint64_t peer, ptr += dlen; remain -= dlen; size_t reclen = (size_t)(ptr - rec_start); - - /* Insert via GUI */ - uint8_t req2[2048]; int nr2 = req_ch(ch_id, req2); - if (nr2 + (int)reclen <= (int)sizeof(req2)) { - memcpy(req2 + nr2, rec_start, reclen); nr2 += (int)reclen; - /* Append zero chain_hash (not verified for SEND_DATA) */ - uint8_t zero_ch[32]; memset(zero_ch, 0, 32); - if (nr2 + 32 <= (int)sizeof(req2)) { - memcpy(req2 + nr2, zero_ch, 32); nr2 += 32; - } - gui_bridge_call(GUI_OP_INSERT_RECORD, req2, nr2, NULL, NULL); - } + chat_core_insert_record(ch_id, rec_start, reclen); } - /* Request more if needed */ uint32_t next = from + count; uint8_t snd[7]; snd[0] = CS_MSG_SEND_DATA; memcpy(snd + 1, &next, 4); uint16_t z = 0; memcpy(snd + 5, &z, 2); @@ -327,97 +191,48 @@ static void cs_handle_send_data(struct chat_sync* cs, uint64_t peer, if (ch) ch->synced = CS_SYNC_DONE; } -static void on_send_data_cursor_open(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - if (rl < 4) { u_free(ctx); return; } - memcpy(&ctx->send_data.cursor_id, r, 4); - gui_bridge_call(GUI_OP_CURSOR_NEXT, (const uint8_t*)&ctx->send_data.cursor_id, 4, ctx, on_send_data_cursor_next); -} - -static void on_send_data_cursor_next(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - if (rl == 0) { - gui_bridge_call(GUI_OP_CURSOR_CLOSE, (const uint8_t*)&ctx->send_data.cursor_id, 4, NULL, NULL); - u_free(ctx); return; - } - - /* Send record to peer */ - uint8_t snd[8192]; uint32_t off = 0; - snd[off++] = CS_MSG_SEND_DATA; - uint32_t pos = ctx->send_data.from_pos + ctx->send_data.sent_count; - memcpy(snd + off, &pos, 4); off += 4; - uint16_t c = 1; memcpy(snd + off, &c, 2); off += 2; - if (off + rl <= (uint32_t)sizeof(snd)) { memcpy(snd + off, r, rl); off += rl; } - cs_send(ctx->sync, ctx->ch_id, ctx->peer, snd, off); - ctx->send_data.sent_count++; - - gui_bridge_call(GUI_OP_CURSOR_NEXT, (const uint8_t*)&ctx->send_data.cursor_id, 4, ctx, on_send_data_cursor_next); -} - /* ── PUSH handler ── */ static void cs_handle_push(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { if (len < 24 + 1 + 4) return; - uint64_t ts, dh, nid; - memcpy(&ts, pl, 8); memcpy(&dh, pl + 8, 8); memcpy(&nid, pl + 16, 8); - - struct cs_ctx* ctx = cs_ctx_new(cs, peer, ch_id); - if (!ctx) return; - ctx->push.ts = ts; ctx->push.dh = dh; + uint64_t ts, dh; + memcpy(&ts, pl, 8); memcpy(&dh, pl + 8, 8); - /* Request: [ch_len][ch_id] + [rest of PUSH payload starting from ct_len] */ - /* PUSH payload: [ts:8][dh:8][node_id:8][ct_len:1][ct:var][data_len:4][data:var][chain_hash:32] */ - uint8_t req[2048]; int nr = req_ch(ch_id, req); - const uint8_t* rec = pl + 24; /* skip ts,dh,nid */ + const uint8_t* rec = pl + 24; int reclen = (int)(len - 24); - if (nr + reclen <= (int)sizeof(req)) { - memcpy(req + nr, rec, reclen); nr += reclen; - } - gui_bridge_call(GUI_OP_INSERT_RECORD, req, nr, ctx, on_push_inserted); -} + int rc = chat_core_insert_record(ch_id, rec, (size_t)reclen); -static void on_push_inserted(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - struct cs_ctx* ctx = (struct cs_ctx*)ud; - int rc = (rl >= 1) ? (int8_t)r[0] : -1; if (rc == 0) { uint8_t ack[17]; ack[0] = CS_MSG_ACK_PUSH; - memcpy(ack + 1, &ctx->push.dh, 8); - memcpy(ack + 9, &ctx->push.ts, 8); - cs_send(ctx->sync, ctx->ch_id, ctx->peer, ack, 17); + memcpy(ack + 1, &dh, 8); memcpy(ack + 9, &ts, 8); + cs_send(cs, ch_id, peer, ack, 17); - uint8_t evt[65]; evt[0] = (uint8_t)strlen(ctx->ch_id); - memcpy(evt + 1, ctx->ch_id, evt[0]); + uint8_t evt[65]; evt[0] = (uint8_t)strlen(ch_id); + memcpy(evt + 1, ch_id, evt[0]); gui_bridge_post(GUI_EVT_MSG_RECEIVED, evt, 1 + evt[0]); - struct channel_cache* ch = cs_find(ctx->sync, ctx->ch_id); + struct channel_cache* ch = cs_find(cs, ch_id); if (ch) ch->msg_count++; } - u_free(ctx); } /* ── ACK_PUSH handler ── */ static void cs_handle_ack_push(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { + (void)peer; if (len < 16) return; uint64_t ts, dh; memcpy(&dh, pl, 8); memcpy(&ts, pl + 8, 8); - uint8_t req[1 + 64 + 24]; int nr = req_ch(ch_id, req); - memcpy(req + nr, &ts, 8); nr += 8; - memcpy(req + nr, &dh, 8); nr += 8; - uint64_t myid = cs->inst->node_id; - memcpy(req + nr, &myid, 8); nr += 8; - gui_bridge_call(GUI_OP_MARK_SENT, req, nr, NULL, NULL); + chat_core_mark_sent(ch_id, ts, dh, cs->inst->node_id); } /* ── SYNC_DONE handler ── */ static void cs_handle_sync_done(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { - if (len < 36) return; + (void)peer; + if (len < 4) return; uint32_t pc; memcpy(&pc, pl, 4); struct channel_cache* ch = cs_find(cs, ch_id); if (ch) { ch->msg_count = pc; ch->synced = CS_SYNC_DONE; } @@ -426,14 +241,16 @@ static void cs_handle_sync_done(struct chat_sync* cs, uint64_t peer, /* ── Recv dispatcher ── */ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { - if (!entry || entry->len < 4) { if (entry) { if (entry->dgram) u_free(entry->dgram); queue_entry_free(entry); } return; } + if (!entry || entry->len < 4) { + if (entry) { if (entry->dgram) u_free(entry->dgram); queue_entry_free(entry); } + return; + } if (!g_cs || !g_cs->initialized) { u_free(entry->dgram); queue_entry_free(entry); return; } uint64_t peer = conn ? conn->peer_node_id : 0; const uint8_t* d = entry->dgram; size_t dlen = entry->len; - if (dlen < 4) { u_free(entry->dgram); queue_entry_free(entry); return; } uint8_t ch_len = d[1]; if (dlen < (size_t)(2 + ch_len + 1)) { u_free(entry->dgram); queue_entry_free(entry); return; } @@ -464,11 +281,15 @@ static void cs_on_conn_up(struct ETCP_CONN* conn, void* arg) { for (int i = 0; i < g_cs->channel_count; i++) { struct channel_cache* ch = &g_cs->channels[i]; int found = 0; - for (int j = 0; j < ch->peer_count; j++) if (ch->peer_ids[j] == peer) { found = 1; break; } + for (int j = 0; j < ch->peer_count; j++) + if (ch->peer_ids[j] == peer) { found = 1; break; } if (!found) continue; ch->synced = CS_SYNC_IN_PROGRESS; uint8_t msg[37]; msg[0] = CS_MSG_INIT_SYNC; - memcpy(msg + 1, &ch->msg_count, 4); memcpy(msg + 5, ch->last_chain_hash, 32); + chat_core_chain_hash_at(ch->channel_id, + ch->msg_count > 0 ? ch->msg_count - 1 : 0, ch->last_chain_hash); + memcpy(msg + 1, &ch->msg_count, 4); + memcpy(msg + 5, ch->last_chain_hash, 32); cs_send(g_cs, ch->channel_id, peer, msg, 37); } } @@ -494,58 +315,53 @@ static void cs_on_new_conn(struct ETCP_CONN* conn, void* arg) { /* ── Periodic refresh from DB ── */ -static void on_refresh_list(void* ud, int op, const uint8_t* r, int rl) { - (void)op; - if (!g_cs || !r || rl < 2) return; - uint16_t cnt; memcpy(&cnt, r, 2); - const uint8_t* p = r + 2; int rem = rl - 2; +static void cs_refresh_channels(struct chat_sync* cs) { + uint8_t buf[4096]; size_t buf_len; + if (chat_core_list_channels(buf, sizeof(buf), &buf_len) != 0) return; + if (buf_len < 2) return; - for (int i = 0; i < g_cs->channel_count; i++) - if (g_cs->channels[i].peer_ids) u_free(g_cs->channels[i].peer_ids); - if (g_cs->channels) u_free(g_cs->channels); - g_cs->channels = u_calloc(cnt, sizeof(struct channel_cache)); - g_cs->channel_count = cnt; - if (!g_cs->channels) { g_cs->channel_count = 0; return; } + uint16_t cnt; memcpy(&cnt, buf, 2); + const uint8_t* p = buf + 2; size_t rem = buf_len - 2; + + for (int i = 0; i < cs->channel_count; i++) + if (cs->channels[i].peer_ids) u_free(cs->channels[i].peer_ids); + if (cs->channels) u_free(cs->channels); + cs->channels = u_calloc(cnt, sizeof(struct channel_cache)); + cs->channel_count = cnt; + if (!cs->channels) { cs->channel_count = 0; return; } for (int i = 0; i < (int)cnt && rem >= 1; i++) { - uint8_t id_len = *p++; - rem--; + uint8_t id_len = *p++; rem--; if (rem < id_len) break; - memcpy(g_cs->channels[i].channel_id, p, id_len); - g_cs->channels[i].channel_id[id_len] = '\0'; + memcpy(cs->channels[i].channel_id, p, id_len); + cs->channels[i].channel_id[id_len] = '\0'; p += id_len; rem -= id_len; - uint8_t req[65]; req[0] = id_len; - memcpy(req + 1, g_cs->channels[i].channel_id, id_len); - gui_bridge_call(GUI_OP_LIST_PEERS, req, 1 + id_len, g_cs, on_refresh_peers_and_count); - } -} - -static void on_refresh_peers_and_count(void* ud, int op, const uint8_t* r, int rl) { - (void)op; (void)ud; - if (!g_cs || !r || rl < 2) return; - uint16_t cnt; memcpy(&cnt, r, 2); - /* Find first channel without peers (simplified: assumes sequential refresh) */ - for (int i = 0; i < g_cs->channel_count; i++) { - if (g_cs->channels[i].peer_ids) continue; - g_cs->channels[i].peer_ids = u_calloc(cnt, sizeof(uint64_t)); - g_cs->channels[i].peer_count = cnt; - for (uint16_t j = 0; j < cnt && 2 + j * 8 < (uint32_t)rl; j++) - memcpy(&g_cs->channels[i].peer_ids[j], r + 2 + j * 8, 8); - - /* Also get count */ - uint8_t req[65]; req[0] = (uint8_t)strlen(g_cs->channels[i].channel_id); - memcpy(req + 1, g_cs->channels[i].channel_id, req[0]); - gui_bridge_call(GUI_OP_COUNT, req, 1 + req[0], NULL, NULL); - break; + cs->channels[i].msg_count = chat_core_count(cs->channels[i].channel_id); + if (cs->channels[i].msg_count > 0) + chat_core_chain_hash_at(cs->channels[i].channel_id, + cs->channels[i].msg_count - 1, cs->channels[i].last_chain_hash); + else + memset(cs->channels[i].last_chain_hash, 0, 32); + + uint8_t peer_buf[2048]; size_t peer_len; + if (chat_core_list_peers(cs->channels[i].channel_id, peer_buf, + sizeof(peer_buf), &peer_len) == 0 && peer_len >= 2) { + uint16_t pc; memcpy(&pc, peer_buf, 2); + cs->channels[i].peer_ids = u_calloc(pc, sizeof(uint64_t)); + cs->channels[i].peer_count = pc; + for (uint16_t j = 0; j < pc; j++) + memcpy(&cs->channels[i].peer_ids[j], peer_buf + 2 + j * 8, 8); + } } } static void refresh_timer_cb(void* arg) { (void)arg; if (!g_cs || !g_cs->initialized) return; - gui_bridge_call(GUI_OP_LIST_CHANNELS, NULL, 0, g_cs, on_refresh_list); - g_cs->refresh_timer = uasync_set_timeout(g_cs->inst->ua, 30u * 10000u, g_cs, refresh_timer_cb, "cs_refresh"); + cs_refresh_channels(g_cs); + g_cs->refresh_timer = uasync_set_timeout(g_cs->inst->ua, + 30u * 10000u, g_cs, refresh_timer_cb, "cs_refresh"); } /* ── TTL cleanup ── */ @@ -554,14 +370,11 @@ static void ttl_timer_cb(void* arg) { (void)arg; if (!g_cs || !g_cs->initialized) return; uint64_t cutoff = get_time_us() - 86400000000ULL; - uint64_t myid = g_cs->inst->node_id; - for (int i = 0; i < g_cs->channel_count; i++) { - uint8_t req[1 + 64 + 16]; int nr = req_ch(g_cs->channels[i].channel_id, req); - memcpy(req + nr, &myid, 8); nr += 8; - memcpy(req + nr, &cutoff, 8); nr += 8; - gui_bridge_call(GUI_OP_TTL_DELETE, req, nr, NULL, NULL); - } - g_cs->ttl_timer = uasync_set_timeout(g_cs->inst->ua, 3600u * 10000u, g_cs, ttl_timer_cb, "cs_ttl"); + for (int i = 0; i < g_cs->channel_count; i++) + chat_core_ttl_delete(g_cs->channels[i].channel_id, + g_cs->inst->node_id, cutoff); + g_cs->ttl_timer = uasync_set_timeout(g_cs->inst->ua, + 3600u * 10000u, g_cs, ttl_timer_cb, "cs_ttl"); } /* ── Public API ── */ @@ -586,8 +399,12 @@ int chat_sync_init(struct UTUN_INSTANCE* inst, c = c->next; } - cs->refresh_timer = uasync_set_timeout(inst->ua, 50000u, cs, refresh_timer_cb, "cs_refresh"); - cs->ttl_timer = uasync_set_timeout(inst->ua, 3600u * 10000u, cs, ttl_timer_cb, "cs_ttl"); + cs_refresh_channels(cs); + + cs->refresh_timer = uasync_set_timeout(inst->ua, 50000u, cs, + refresh_timer_cb, "cs_refresh"); + cs->ttl_timer = uasync_set_timeout(inst->ua, 3600u * 10000u, cs, + ttl_timer_cb, "cs_ttl"); DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: initialized", CS_ID); return 0; @@ -638,7 +455,7 @@ int chat_sync_push(struct UTUN_INSTANCE* inst, uint32_t dlen = data_len; memcpy(p, &dlen, 4); p += 4; if (data_len) { memcpy(p, data, data_len); p += data_len; } - memset(p, 0, 32); /* chain_hash placeholder */ + memset(p, 0, 32); int sent = 0; uint64_t myid = g_cs->inst->node_id; @@ -654,115 +471,17 @@ int chat_sync_push(struct UTUN_INSTANCE* inst, } void chat_sync_connect_node(struct UTUN_INSTANCE* inst, uint64_t node_id) { - /* Placeholder — full impl requires parsing nodeinfo and calling conn_mgr_connect_node */ - uint8_t req[8]; memcpy(req, &node_id, 8); - gui_bridge_call(GUI_OP_LOAD_NODEINFO, req, 8, NULL, NULL); -} - -/* ── Invite link connect ── */ - -struct cs_invite_arg { - uint64_t node_id; - uint8_t pubkey[SC_PUBKEY_SIZE]; - uint8_t* addrs_data; - int addr_count; - uint8_t* ch_id; - int ch_id_len; -}; - -static void cs_connect_result_cb(int result, uint64_t node_id, void* arg) { - (void)arg; - uint8_t data[12]; - memcpy(data, &node_id, 8); - memcpy(data + 8, &result, 4); - gui_bridge_post(GUI_EVT_CONNECT_RESULT, data, 12); -} - -static void cs_connect_from_invite_impl(void* arg) { - struct cs_invite_arg* a = (struct cs_invite_arg*)arg; - uint64_t node_id = a->node_id; - if (!g_cs || !g_cs->inst || !g_cs->inst->bgp || !g_cs->inst->conn_mgr) { - uint8_t err[12]; int r = -7; - memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); - gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); - goto cleanup; - } - - struct ROUTE_BGP* bgp = g_cs->inst->bgp; - if (nodeinfo_find_by_id(bgp, node_id)) { - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_sync: node 0x%016llx already in BGP, connecting", (unsigned long long)node_id); - conn_mgr_connect_node(g_cs->inst->conn_mgr, node_id, 30000, cs_connect_result_cb, NULL); - goto cleanup; - } - - size_t dyn_sz = (size_t)a->addr_count * sizeof(struct NODEINFO_IPV4_ADDR); - size_t data_sz = sizeof(struct NODEINFO_Q) - sizeof(struct ll_entry) + dyn_sz + 8; - struct NODEINFO_Q* nq = (struct NODEINFO_Q*)queue_entry_new(data_sz); - if (!nq) { - DEBUG_ERROR(DEBUG_CATEGORY_DEBUG, "chat_sync: failed to alloc NODEINFO_Q"); - uint8_t err[12]; int r = -7; - memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); - gui_bridge_post(GUI_EVT_CONNECT_RESULT, err, 12); - goto cleanup; - } - - struct NODEINFO* ni = &nq->node; - ni->node_id = node_id; - ni->ver = 0; - memcpy(ni->public_key, a->pubkey, SC_PUBKEY_SIZE); - memset(ni->ed25519_public_key, 0, SC_PUBKEY_SIZE); - ni->node_name_len = 0; - ni->local_v4_sockets = 0; - ni->local_v4_addrs = (uint8_t)a->addr_count; - ni->local_v6_sockets = 0; - ni->local_v6_addrs = 0; - ni->local_v4_subnets = 0; - ni->local_v6_subnets = 0; - ni->tranzit_nodes = 0; - ni->hop_count = 0; - - uint8_t* dyn = (uint8_t*)ni + sizeof(struct NODEINFO); - const uint8_t* src = a->addrs_data; - for (int i = 0; i < a->addr_count; i++) { - struct NODEINFO_IPV4_ADDR* addr = (struct NODEINFO_IPV4_ADDR*)(dyn + i * sizeof(struct NODEINFO_IPV4_ADDR)); - memset(addr, 0, sizeof(*addr)); - uint8_t family = *src++; - if (family == 4) { - memcpy(addr->addr, src, 4); src += 4; - uint16_t port_be = ((uint16_t)src[0] << 8) | src[1]; src += 2; - addr->port = port_be; - addr->type = ADDR_TYPE_REAL; - addr->socket_id = 0; - addr->protocol = NODE_PROTO_UDP; - } else { - src += 18; - continue; - } + uint8_t buf[2048]; size_t buf_len; + if (chat_core_load_nodeinfo(node_id, buf, sizeof(buf), &buf_len) == 0 && buf_len > 0) { + /* nodeinfo loaded — conn_mgr will pick it up */ + (void)inst; + conn_mgr_connect_node(inst->conn_mgr, node_id, 30000, NULL, NULL); } - - nq->alien = 0; - nq->dirty = 0; - nq->last_ver = 0; - nq->conn_mgr_type = CONN_TYPE_NONE; - nq->conn_mgr_intermediariy_count = 0; - memset(&nq->connectivity, 0, sizeof(nq->connectivity)); - nq->best_socket = NULL; - - queue_data_put_with_index(bgp->nodes, &nq->ll); - - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_sync: created NODEINFO for 0x%016llx, %d addrs, connecting", - (unsigned long long)node_id, a->addr_count); - conn_mgr_connect_node(g_cs->inst->conn_mgr, node_id, 30000, cs_connect_result_cb, NULL); - -cleanup: - u_free(a->addrs_data); - u_free(a->ch_id); - u_free(a); } void chat_sync_connect_from_invite(uint64_t node_id, const uint8_t* pubkey_bin, - const uint8_t* addrs_data, int addr_count, - const uint8_t* channel_id, int ch_id_len) { + const uint8_t* addrs_data, int addr_count, + const uint8_t* channel_id, int ch_id_len) { if (!g_cs || !g_cs->inst || !g_cs->inst->ua) { int r = -7; uint8_t err[12]; memcpy(err, &node_id, 8); memcpy(err + 8, &r, 4); @@ -770,22 +489,23 @@ void chat_sync_connect_from_invite(uint64_t node_id, const uint8_t* pubkey_bin, return; } - struct cs_invite_arg* a = u_calloc(1, sizeof(struct cs_invite_arg)); - if (!a) return; + struct chat_invite* inv = u_calloc(1, sizeof(struct chat_invite)); + if (!inv) return; - a->node_id = node_id; - memcpy(a->pubkey, pubkey_bin, SC_PUBKEY_SIZE); + inv->node_id = node_id; + memcpy(inv->pubkey, pubkey_bin, 32); size_t addrs_sz = (size_t)addr_count * 7; - a->addrs_data = u_malloc(addrs_sz); - if (!a->addrs_data) { u_free(a); return; } - memcpy(a->addrs_data, addrs_data, addrs_sz); - a->addr_count = addr_count; - - a->ch_id = u_malloc((size_t)ch_id_len + 1); - if (!a->ch_id) { u_free(a->addrs_data); u_free(a); return; } - memcpy(a->ch_id, channel_id, (size_t)ch_id_len); - a->ch_id_len = ch_id_len; - - gui_bridge_post_uasync(g_cs->inst->ua, cs_connect_from_invite_impl, a); + inv->addrs_data = u_malloc(addrs_sz); + if (!inv->addrs_data) { u_free(inv); return; } + memcpy(inv->addrs_data, addrs_data, addrs_sz); + inv->addr_count = addr_count; + + inv->ch_id = u_malloc((size_t)ch_id_len + 1); + if (!inv->ch_id) { u_free(inv->addrs_data); u_free(inv); return; } + memcpy(inv->ch_id, channel_id, (size_t)ch_id_len); + inv->ch_id_len = ch_id_len; + + gui_bridge_post_uasync_fn( + (void(*)(void*))chat_core_connect_from_invite, inv); } diff --git a/tools/chatgui/transport/crypto.cpp b/tools/chatgui/transport/crypto.cpp deleted file mode 100644 index f71ef5ba..00000000 --- a/tools/chatgui/transport/crypto.cpp +++ /dev/null @@ -1,221 +0,0 @@ -/* - * crypto.cpp — Криптографические утилиты (Ed25519 через OpenSSL) - */ - -#include "crypto.h" - -#include -#include -#include -#include -#include -#include - -namespace Crypto { - -/* ============================================================================ - * Помощники - * ============================================================================ */ - -namespace { - -void postToMain(std::function fn) { - auto* app = QCoreApplication::instance(); - if (app) { - QMetaObject::invokeMethod(app, std::move(fn), Qt::QueuedConnection); - } else { - fn(); /* нет event loop — синхронно */ - } -} - -QByteArray sha512File(const QString& path) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) return {}; - - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); - if (!md_ctx || EVP_DigestInit_ex(md_ctx, EVP_sha512(), NULL) != 1) { - if (md_ctx) EVP_MD_CTX_free(md_ctx); - return {}; - } - - QByteArray buf(65536, 0); - while (true) { - qint64 n = file.read(buf.data(), buf.size()); - if (n <= 0) break; - EVP_DigestUpdate(md_ctx, reinterpret_cast(buf.constData()), - static_cast(n)); - } - file.close(); - - QByteArray hash; - hash.resize(static_cast(kSha512Size)); - unsigned int hash_len = static_cast(kSha512Size); - if (EVP_DigestFinal_ex(md_ctx, reinterpret_cast(hash.data()), &hash_len) != 1) - hash.clear(); - EVP_MD_CTX_free(md_ctx); - return hash; -} - -} // anonymous - -/* ============================================================================ - * Синхронные: derive ключей - * ============================================================================ */ - -QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey) { - if (x25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); - - uint8_t hash[kSha512Size]; - SHA512(reinterpret_cast(x25519Privkey.constData()), kKeySize, hash); - - EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); - if (!pkey) return QByteArray(); - - QByteArray result; - result.resize(static_cast(kKeySize)); - size_t priv_len = kKeySize; - if (EVP_PKEY_get_raw_private_key(pkey, reinterpret_cast(result.data()), &priv_len) <= 0 - || priv_len != kKeySize) { - EVP_PKEY_free(pkey); - return QByteArray(); - } - EVP_PKEY_free(pkey); - return result; -} - -QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey) { - if (x25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); - - uint8_t hash[kSha512Size]; - SHA512(reinterpret_cast(x25519Privkey.constData()), kKeySize, hash); - - EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, hash, kKeySize); - if (!pkey) return QByteArray(); - - QByteArray result; - result.resize(static_cast(kKeySize)); - size_t pub_len = kKeySize; - if (EVP_PKEY_get_raw_public_key(pkey, reinterpret_cast(result.data()), &pub_len) <= 0 - || pub_len != kKeySize) { - EVP_PKEY_free(pkey); - return QByteArray(); - } - EVP_PKEY_free(pkey); - return result; -} - -/* ============================================================================ - * Синхронные: подпись / проверка (малые блоки) - * ============================================================================ */ - -QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data) { - if (ed25519Privkey.size() != static_cast(kKeySize)) return QByteArray(); - - EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, - reinterpret_cast(ed25519Privkey.constData()), kKeySize); - if (!pkey) return QByteArray(); - - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); - if (!md_ctx || EVP_DigestSignInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { - if (md_ctx) EVP_MD_CTX_free(md_ctx); - EVP_PKEY_free(pkey); - return QByteArray(); - } - - size_t sig_len = kSignSize; - QByteArray result; - result.resize(static_cast(kSignSize)); - if (EVP_DigestSign(md_ctx, reinterpret_cast(result.data()), &sig_len, - reinterpret_cast(data.constData()), data.size()) != 1) { - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_free(pkey); - return QByteArray(); - } - - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_free(pkey); - result.resize(static_cast(sig_len)); - return result; -} - -bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig) { - if (ed25519Pubkey.size() != static_cast(kKeySize) - || sig.size() > static_cast(kSignSize)) return false; - - EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, - reinterpret_cast(ed25519Pubkey.constData()), kKeySize); - if (!pkey) return false; - - EVP_MD_CTX* md_ctx = EVP_MD_CTX_new(); - if (!md_ctx || EVP_DigestVerifyInit(md_ctx, NULL, NULL, NULL, pkey) != 1) { - if (md_ctx) EVP_MD_CTX_free(md_ctx); - EVP_PKEY_free(pkey); - return false; - } - - int ret = EVP_DigestVerify(md_ctx, reinterpret_cast(sig.constData()), sig.size(), - reinterpret_cast(data.constData()), data.size()); - EVP_MD_CTX_free(md_ctx); - EVP_PKEY_free(pkey); - return ret == 1; -} - -/* ============================================================================ - * Асинхронные: файлы - * ============================================================================ */ - -void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb) { - std::thread([=]() { - SignResult r; - QByteArray hash = sha512File(path); - if (hash.isEmpty()) { - r.ok = false; r.error = QStringLiteral("failed to read/hash file"); - } else { - r.signature = sign(ed25519Privkey, hash); - r.ok = !r.signature.isEmpty(); - if (!r.ok) r.error = QStringLiteral("sign failed"); - } - postToMain([cb, r]() { cb(r); }); - }).detach(); -} - -void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, - const QByteArray& signature, SignCallback cb) { - std::thread([=]() { - SignResult r; - QByteArray hash = sha512File(path); - if (hash.isEmpty()) { - r.ok = false; r.error = QStringLiteral("failed to read/hash file"); - } else { - r.ok = verify(ed25519Pubkey, hash, signature); - if (!r.ok) r.error = QStringLiteral("signature mismatch"); - } - postToMain([cb, r]() { cb(r); }); - }).detach(); -} - -/* ============================================================================ - * Асинхронные: in-memory блоки - * ============================================================================ */ - -void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb) { - std::thread([=]() { - SignResult r; - r.signature = sign(ed25519Privkey, data); - r.ok = !r.signature.isEmpty(); - if (!r.ok) r.error = QStringLiteral("sign failed"); - postToMain([cb, r]() { cb(r); }); - }).detach(); -} - -void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, - const QByteArray& signature, SignCallback cb) { - std::thread([=]() { - SignResult r; - r.ok = verify(ed25519Pubkey, data, signature); - if (!r.ok) r.error = QStringLiteral("signature mismatch"); - postToMain([cb, r]() { cb(r); }); - }).detach(); -} - -} // namespace Crypto diff --git a/tools/chatgui/transport/crypto.h b/tools/chatgui/transport/crypto.h deleted file mode 100644 index dd9fd5c0..00000000 --- a/tools/chatgui/transport/crypto.h +++ /dev/null @@ -1,48 +0,0 @@ -/* - * crypto.h — Криптографические утилиты для chat GUI - * - * Ключи: X25519 → SHA-512 → Ed25519 (аналог sc_derive_ed25519_key / pubkey в utun) - * Подпись: Ed25519 (EVP_DigestSign / EVP_DigestVerify) - */ - -#ifndef MSG_CRYPTO_H -#define MSG_CRYPTO_H - -#include -#include -#include - -namespace Crypto { - -static constexpr size_t kKeySize = 32; // X25519 / Ed25519 key size -static constexpr size_t kSignSize = 64; // Ed25519 signature size -static constexpr size_t kSha512Size = 64; // SHA-512 digest size - -// ---------------- синхронные (малые блоки) ---------------- - -QByteArray deriveEd25519Privkey(const QByteArray& x25519Privkey); -QByteArray deriveEd25519Pubkey(const QByteArray& x25519Privkey); - -QByteArray sign(const QByteArray& ed25519Privkey, const QByteArray& data); -bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig); - -// ---------------- асинхронные (файлы / большие блоки) ---------------- - -struct SignResult { - bool ok = false; - QByteArray signature; // 64 bytes - QString error; -}; - -using SignCallback = std::function; - -void signFile(const QString& path, const QByteArray& ed25519Privkey, SignCallback cb); -void verifyFile(const QString& path, const QByteArray& ed25519Pubkey, - const QByteArray& signature, SignCallback cb); -void signAsync(const QByteArray& ed25519Privkey, const QByteArray& data, SignCallback cb); -void verifyAsync(const QByteArray& ed25519Pubkey, const QByteArray& data, - const QByteArray& signature, SignCallback cb); - -} // namespace Crypto - -#endif // MSG_CRYPTO_H diff --git a/tools/chatgui/transport/gui_bridge.h b/tools/chatgui/transport/gui_bridge.h index d9c5af3d..91999008 100644 --- a/tools/chatgui/transport/gui_bridge.h +++ b/tools/chatgui/transport/gui_bridge.h @@ -10,54 +10,35 @@ extern "C" { struct UASYNC; -/* ── Операции uasync→GUI (chat_sync запрашивает работу с БД) ── */ - -#define GUI_OP_LIST_CHANNELS 0 /* → массив: [count:2][ch_id_len:1][ch_id:var][name_len:2][name:var]... */ -#define GUI_OP_COUNT 1 /* req: [ch_id_len:1][ch_id:var] → resp: uint32 count (4 байта) */ -#define GUI_OP_CHAIN_HASH_AT 2 /* req: [ch_id_len:1][ch_id:var][pos:4] → resp: 32 байта chain_hash */ -#define GUI_OP_INSERT_RECORD 3 /* req: [ch_id_len:1][ch_id:var][record:var] → resp: 0=ok, 1=dup, -1=err (1 байт) */ -#define GUI_OP_CURSOR_OPEN 4 /* req: [ch_id_len:1][ch_id:var] → resp: uint32 cursor_id (4 байта) */ -#define GUI_OP_CURSOR_NEXT 5 /* req: cursor_id (4 байта) → resp: record или 0 байт = EOF */ -#define GUI_OP_CURSOR_CLOSE 6 /* req: cursor_id (4 байта) → resp: 0 байт */ -#define GUI_OP_MARK_SENT 7 /* req: [ch_id_len:1][ch_id:var][ts:8][dh:8][my_node_id:8] → resp: 0 байт */ -#define GUI_OP_TTL_DELETE 8 /* req: [ch_id_len:1][ch_id:var][my_node_id:8][cutoff:8] → resp: 0 байт */ -#define GUI_OP_LIST_PEERS 9 /* req: [ch_id_len:1][ch_id:var] → resp: [count:2][node_id:8]... */ -#define GUI_OP_LOAD_NODEINFO 10 /* req: node_id (8 байт) → resp: nodeinfo или 0 байт если нет */ - /* ── Типы уведомлений uasync→GUI (fire-and-forget) ── */ -#define GUI_EVT_MSG_RECEIVED 1 /* data: [ch_id_len:1][ch_id:var][record:var] */ +#define GUI_EVT_MSG_RECEIVED 1 /* data: [ch_id_len:1][ch_id:var] */ #define GUI_EVT_CONNECT_RESULT 2 /* data: [node_id:8][result:4] */ #define GUI_EVT_NEW_PEER 3 /* data: [node_id:8] */ #define GUI_EVT_CHANNEL_UPDATED 4 /* data: [ch_id_len:1][ch_id:var] */ -/* ── Callback-типы ── */ - -typedef void (*gui_result_fn)(void* userdata, int op, - const uint8_t* result, int result_len); - /* ── API ── */ -/* Регистрация (вызывается из GUI при старте) */ -void gui_bridge_init(struct UASYNC* ua, gui_result_fn callback, void* gui_target); - -/* Установить указатель на DbManager (до первого вызова gui_bridge_call) */ -void gui_bridge_set_db(void* db_ptr); +/* Инициализация моста (вызывается из GUI при старте) */ +void gui_bridge_init(void); -/* uasync → GUI: запросить SQL-операцию */ -void gui_bridge_call(int op, const uint8_t* request, int req_len, - void* userdata, gui_result_fn callback); +/* Установить uasync (вызывается из UtunNode после создания ua) */ +void gui_bridge_set_uasync(struct UASYNC* ua); -/* uasync → GUI: уведомление (fire-and-forget, callback не вызывается) */ +/* uasync → GUI: уведомление (fire-and-forget, через Qt::QueuedConnection) */ void gui_bridge_post(int event_type, const uint8_t* data, int data_len); /* GUI → uasync: выполнить функцию в uasync-потоке */ -void gui_bridge_post_uasync(struct UASYNC* ua, void (*fn)(void*), void* arg); +void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg); -/* Callback for connect results (called from GUI thread) */ +/* Callback для результатов подключения (вызывается из GUI-потока) */ typedef void (*gui_connect_result_fn)(uint64_t node_id, int result); void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb); +/* Callback для входящих сообщений (вызывается из GUI-потока) */ +typedef void (*gui_msg_received_fn)(const char* ch_id, int ch_id_len); +void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb); + #ifdef __cplusplus } #endif diff --git a/tools/chatgui/transport/gui_bridge_impl.cpp b/tools/chatgui/transport/gui_bridge_impl.cpp index f54807dc..88a1023c 100644 --- a/tools/chatgui/transport/gui_bridge_impl.cpp +++ b/tools/chatgui/transport/gui_bridge_impl.cpp @@ -3,11 +3,9 @@ #include #include #include -#include +#include #include -#include "../db/db_manager.h" - extern "C" { #include "../../../lib/u_async.h" #include "../../../lib/mem.h" @@ -19,160 +17,20 @@ class GuiBridgeReceiver : public QObject { Q_OBJECT public: void* ua = nullptr; - gui_result_fn callback = nullptr; - DbManager* db = nullptr; GuiBridgeReceiver(QObject* parent = nullptr) : QObject(parent) {} - Q_INVOKABLE void processCall(int op, QByteArray packedReq); Q_INVOKABLE void processPost(int eventType, QByteArray data); }; -static GuiBridgeReceiver* g_receiver = nullptr; -static gui_connect_result_fn g_connect_result_cb = nullptr; - -/* ── Трамплин для uasync_post: распаковывает результат и вызывает оригинальный callback ── */ - -struct TrampolineCtx { - gui_result_fn cb; - void* userdata; - int op; - int result_len; - /* result data follows after this struct in memory */ -}; - -static void bridge_trampoline(void* arg) { - uint8_t* packed = (uint8_t*)arg; - TrampolineCtx ctx; - memcpy(&ctx, packed, sizeof(ctx)); - const uint8_t* result = packed + sizeof(ctx); - if (ctx.cb) ctx.cb(ctx.userdata, ctx.op, result, ctx.result_len); - u_free(packed); -} - -static void post_result_to_uasync(int op, const QByteArray& result, - void* userdata, gui_result_fn cb) { - int res_len = result.size(); - int total = (int)sizeof(TrampolineCtx) + res_len; - uint8_t* buf = (uint8_t*)u_malloc(total); - if (!buf) return; - - TrampolineCtx ctx; - ctx.cb = cb; - ctx.userdata = userdata; - ctx.op = op; - ctx.result_len = res_len; - memcpy(buf, &ctx, sizeof(ctx)); - if (res_len > 0) memcpy(buf + sizeof(ctx), result.constData(), res_len); - - uasync_post((struct UASYNC*)g_receiver->ua, bridge_trampoline, buf); -} +static GuiBridgeReceiver* g_receiver = nullptr; +static gui_connect_result_fn g_connect_result_cb = nullptr; +static gui_msg_received_fn g_msg_received_cb = nullptr; +static struct UASYNC* g_ua = nullptr; /* ── GuiBridgeReceiver implementation ── */ -static QString readChId(const uint8_t*& r, int& rem) { - if (rem < 1) return {}; - uint8_t len = r[0]; r++; rem--; - if (rem < len) return {}; - QString s = QString::fromUtf8((const char*)r, len); - r += len; rem -= len; - return s; -} - -void GuiBridgeReceiver::processCall(int op, QByteArray packedReq) { - if (!db || packedReq.size() < (int)(sizeof(gui_result_fn) + sizeof(void*))) return; - - gui_result_fn cb; - void* userdata; - memcpy(&cb, packedReq.constData(), sizeof(cb)); - memcpy(&userdata, packedReq.constData() + sizeof(cb), sizeof(userdata)); - - const uint8_t* r = (const uint8_t*)packedReq.constData() + sizeof(cb) + sizeof(userdata); - int rem = packedReq.size() - (int)(sizeof(cb) + sizeof(userdata)); - - QByteArray result; - - switch (op) { - case GUI_OP_LIST_CHANNELS: - result = db->syncListChannels(); - break; - - case GUI_OP_COUNT: { - QString chId = readChId(r, rem); - if (!chId.isEmpty()) result = db->syncCount(chId); - break; - } - case GUI_OP_CHAIN_HASH_AT: { - QString chId = readChId(r, rem); - if (!chId.isEmpty() && rem >= 4) { - uint32_t pos; memcpy(&pos, r, 4); - result = db->syncChainHashAt(chId, pos); - } - break; - } - case GUI_OP_INSERT_RECORD: { - QString chId = readChId(r, rem); - if (!chId.isEmpty() && rem > 0) { - QByteArray rec((const char*)r, rem); - result = db->syncInsert(chId, rec); - } - break; - } - case GUI_OP_CURSOR_OPEN: { - QString chId = readChId(r, rem); - if (!chId.isEmpty()) result = db->syncCursorOpen(chId); - break; - } - case GUI_OP_CURSOR_NEXT: - if (rem >= 4) { - uint32_t curId; memcpy(&curId, r, 4); - result = db->syncCursorNext(curId); - } - break; - case GUI_OP_CURSOR_CLOSE: - if (rem >= 4) { - uint32_t curId; memcpy(&curId, r, 4); - db->syncCursorClose(curId); - } - break; - case GUI_OP_MARK_SENT: { - QString chId = readChId(r, rem); - if (!chId.isEmpty() && rem >= 24) { - uint64_t ts, dh, myId; - memcpy(&ts, r, 8); memcpy(&dh, r + 8, 8); memcpy(&myId, r + 16, 8); - result = db->syncMarkSent(chId, ts, dh, myId); - } - break; - } - case GUI_OP_TTL_DELETE: { - QString chId = readChId(r, rem); - if (!chId.isEmpty() && rem >= 16) { - uint64_t myId, cutoff; - memcpy(&myId, r, 8); memcpy(&cutoff, r + 8, 8); - result = db->syncTtlDelete(chId, myId, cutoff); - } - break; - } - case GUI_OP_LIST_PEERS: { - QString chId = readChId(r, rem); - if (!chId.isEmpty()) result = db->syncListPeers(chId); - break; - } - case GUI_OP_LOAD_NODEINFO: - if (rem >= 8) { - uint64_t nodeId; memcpy(&nodeId, r, 8); - result = db->loadNodeInfo(nodeId); - } - break; - default: - break; - } - - post_result_to_uasync(op, result, userdata, cb); -} - void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { - if (!db) return; const uint8_t* d = (const uint8_t*)data.constData(); int dlen = data.size(); @@ -182,55 +40,63 @@ void GuiBridgeReceiver::processPost(int eventType, QByteArray data) { uint8_t chLen = d[0]; if (dlen >= 1 + chLen) { QString chId = QString::fromUtf8((const char*)d + 1, chLen); - db->onSyncMessageReceived(chId); + qDebug("gui_bridge: MSG_RECEIVED ch=%s", qPrintable(chId)); + if (g_msg_received_cb) g_msg_received_cb((const char*)d + 1, chLen); + } else { + qWarning("gui_bridge: MSG_RECEIVED invalid data length %d", dlen); } + } else { + qWarning("gui_bridge: MSG_RECEIVED data too short %d", dlen); } break; case GUI_EVT_CONNECT_RESULT: if (dlen >= 12) { uint64_t nodeId; int result; memcpy(&nodeId, d, 8); memcpy(&result, d + 8, 4); - db->setNodeOnline(nodeId, result == 0); + qDebug("gui_bridge: CONNECT_RESULT node=0x%016llx result=%d", + (unsigned long long)nodeId, result); if (g_connect_result_cb) g_connect_result_cb(nodeId, result); + } else { + qWarning("gui_bridge: CONNECT_RESULT data too short %d", dlen); } break; case GUI_EVT_NEW_PEER: + if (dlen >= 8) { + uint64_t nodeId; memcpy(&nodeId, d, 8); + qDebug("gui_bridge: NEW_PEER node=0x%016llx", (unsigned long long)nodeId); + } else { + qWarning("gui_bridge: NEW_PEER data too short %d", dlen); + } + break; case GUI_EVT_CHANNEL_UPDATED: + if (dlen >= 2) { + uint8_t chLen = d[0]; + if (dlen >= 1 + chLen) { + QString chId = QString::fromUtf8((const char*)d + 1, chLen); + qDebug("gui_bridge: CHANNEL_UPDATED ch=%s", qPrintable(chId)); + } + } else { + qWarning("gui_bridge: CHANNEL_UPDATED data too short %d", dlen); + } + break; default: + qWarning("gui_bridge: unknown event type %d", eventType); break; } } -void* gui_bridge_get_db() { return g_receiver ? (void*)g_receiver->db : nullptr; } - /* ── C API ── */ extern "C" { -void gui_bridge_init(struct UASYNC* ua, gui_result_fn callback, void* gui_target) { - g_receiver = new GuiBridgeReceiver((QObject*)gui_target); - g_receiver->ua = ua; - g_receiver->callback = callback; +void gui_bridge_init(void) { + g_receiver = new GuiBridgeReceiver(); + g_receiver->ua = g_ua; } -void gui_bridge_set_db(void* db_ptr) { - if (g_receiver) g_receiver->db = (DbManager*)db_ptr; -} - -void gui_bridge_call(int op, const uint8_t* request, int req_len, - void* userdata, gui_result_fn callback) { - if (!g_receiver) return; - - /* Пакуем cb и userdata в начало, потом request */ - QByteArray packed; - packed.append((const char*)&callback, sizeof(callback)); - packed.append((const char*)&userdata, sizeof(userdata)); - if (req_len > 0) packed.append((const char*)request, req_len); - - QMetaObject::invokeMethod(g_receiver, "processCall", - Qt::QueuedConnection, - Q_ARG(int, op), - Q_ARG(QByteArray, packed)); +void gui_bridge_set_uasync(struct UASYNC* ua) { + g_ua = ua; + if (g_receiver) g_receiver->ua = ua; } void gui_bridge_post(int event_type, const uint8_t* data, int data_len) { @@ -245,14 +111,18 @@ void gui_bridge_post(int event_type, const uint8_t* data, int data_len) { Q_ARG(QByteArray, d)); } -void gui_bridge_post_uasync(struct UASYNC* ua, void (*fn)(void*), void* arg) { - if (ua && fn) uasync_post(ua, fn, arg); +void gui_bridge_post_uasync_fn(void (*fn)(void*), void* arg) { + if (g_ua && fn) uasync_post(g_ua, fn, arg); } void gui_bridge_set_connect_result_cb(gui_connect_result_fn cb) { g_connect_result_cb = cb; } +void gui_bridge_set_msg_received_cb(gui_msg_received_fn cb) { + g_msg_received_cb = cb; +} + } /* extern "C" */ #include "gui_bridge_impl.moc" diff --git a/tools/chatgui/transport/msg_client.cpp b/tools/chatgui/transport/msg_client.cpp deleted file mode 100644 index 72cc684e..00000000 --- a/tools/chatgui/transport/msg_client.cpp +++ /dev/null @@ -1,174 +0,0 @@ -/* - * msg_client.cpp — C++ транспортный клиент - */ - -#include "msg_client.h" -#include - -MsgClient::MsgClient(QObject* parent) : QObject(parent), m_socket(new QTcpSocket(this)) { - QObject::connect(m_socket, &QTcpSocket::connected, this, &MsgClient::connected); - QObject::connect(m_socket, &QTcpSocket::disconnected, this, [this]() { - if (m_boundPeerId) { - m_boundPeerId = 0; - emit unbound(); - } - emit disconnected(); - }); - QObject::connect(m_socket, &QTcpSocket::readyRead, this, &MsgClient::onReadyRead); - QObject::connect(m_socket, &QAbstractSocket::errorOccurred, this, &MsgClient::onSocketError); -} - -MsgClient::~MsgClient() { - disconnectFromHost(); -} - -void MsgClient::connectToHost(const QString& host, quint16 port) { - m_recvBuffer.clear(); - m_boundPeerId = 0; - m_socket->connectToHost(host, port); -} - -void MsgClient::disconnectFromHost() { - if (m_socket->state() != QAbstractSocket::UnconnectedState) - m_socket->disconnectFromHost(); - m_recvBuffer.clear(); - if (m_boundPeerId) { - m_boundPeerId = 0; - emit unbound(); - } -} - -bool MsgClient::isConnected() const { - return m_socket->state() == QAbstractSocket::ConnectedState; -} - -void MsgClient::bind(quint64 peerId) { - QByteArray msg = buildMessage(kCtlBind, peerId, 0, QByteArray()); - m_socket->write(msg); -} - -void MsgClient::unbind() { - if (!m_boundPeerId) return; - QByteArray msg = buildMessage(kCtlUnbind, 0, 0, QByteArray()); - m_socket->write(msg); - m_boundPeerId = 0; - emit unbound(); -} - -void MsgClient::send(const QByteArray& data) { - if (!m_boundPeerId) return; - QByteArray msg = buildMessage(kDataSend, m_boundPeerId, 0, data); - m_socket->write(msg); -} - -void MsgClient::send(const uint8_t* data, size_t len) { - send(QByteArray(reinterpret_cast(data), static_cast(len))); -} - -void MsgClient::sendSigned(const QByteArray& data) { - if (!m_boundPeerId) return; - QByteArray msg = buildMessage(kDataSend, m_boundPeerId, kOptSigned, data); - m_socket->write(msg); -} - -void MsgClient::sendTo(quint64 dstPeerId, const QByteArray& data) { - QByteArray msg = buildMessage(kDataSend, dstPeerId, 0, data); - m_socket->write(msg); -} - -void MsgClient::requestPublicKey() { - QByteArray msg = buildMessage(kCtlGetPubkey, 0, 0, QByteArray()); - m_socket->write(msg); -} - -void MsgClient::requestPrivateKey() { - QByteArray msg = buildMessage(kCtlGetPrivkey, 0, 0, QByteArray()); - m_socket->write(msg); -} - -void MsgClient::requestPeerInfo(quint64 peerId) { - QByteArray msg = buildMessage(kCtlGetPeerInfo, peerId, 0, QByteArray()); - m_socket->write(msg); -} - -/* ============================================================================ - * Внутренние - * ============================================================================ */ - -void MsgClient::onReadyRead() { - m_recvBuffer.append(m_socket->readAll()); - if (m_recvBuffer.size() > static_cast(kMaxMsgSize)) { - m_recvBuffer.clear(); - emit errorOccurred(0xFF, "recv buffer overflow"); - return; - } - processMessages(); -} - -void MsgClient::onSocketError(QAbstractSocket::SocketError) { - emit errorOccurred(0xFF, m_socket->errorString()); -} - -void MsgClient::processMessages() { - while (static_cast(m_recvBuffer.size()) >= kHdrSize) { - uint16_t totalSize; - std::memcpy(&totalSize, m_recvBuffer.constData(), 2); - if (totalSize < kHdrSize || totalSize > kMaxMsgSize) { - m_recvBuffer.clear(); - emit errorOccurred(0xFF, "invalid message size"); - return; - } - if (m_recvBuffer.size() < totalSize) break; - - uint8_t type = static_cast(m_recvBuffer[2]); - uint64_t id; - std::memcpy(&id, m_recvBuffer.constData() + 3, 8); - QByteArray payload = m_recvBuffer.mid(static_cast(kHdrSize), - totalSize - static_cast(kHdrSize)); - - dispatch(type, id, payload); - - m_recvBuffer.remove(0, totalSize); - } -} - -void MsgClient::dispatch(uint8_t type, quint64 id, const QByteArray& payload) { - switch (type) { - case kRspBindOk: - m_boundPeerId = id; - emit bound(id); - break; - case kRspPubkey: - emit publicKeyReceived(payload); - break; - case kRspPrivkey: - emit privateKeyReceived(payload); - break; - case kRspPeerInfo: - if (payload.size() >= 64) { - emit peerInfoReceived(id, payload.left(32), payload.mid(32, 32)); - } - break; - case kDataRecv: - emit received(id, payload); - break; - case kRspError: - uint8_t code = payload.size() > 0 ? static_cast(payload[0]) : 0; - QString msg = QString::fromUtf8(payload.mid(1)); - emit errorOccurred(code, msg); - break; - } -} - -QByteArray MsgClient::buildMessage(uint8_t type, quint64 id, uint8_t options, const QByteArray& data) { - uint16_t totalSize = static_cast(kHdrSize + data.size()); - QByteArray msg; - msg.resize(totalSize); - std::memcpy(msg.data(), &totalSize, 2); - msg[2] = static_cast(type); - std::memcpy(msg.data() + 3, &id, 8); - msg[11] = static_cast(options); - if (!data.isEmpty()) - std::memcpy(msg.data() + kHdrSize, data.constData(), data.size()); - return msg; -} diff --git a/tools/chatgui/transport/msg_client.h b/tools/chatgui/transport/msg_client.h deleted file mode 100644 index 33224d1b..00000000 --- a/tools/chatgui/transport/msg_client.h +++ /dev/null @@ -1,80 +0,0 @@ -/* - * msg_client.h — C++ транспортный клиент для chat GUI - * - * Подключается к utun msg_transport по localhost TCP. - * Протокол: [size:2][type:1][id:8][options:1][data...] (msg_transport.h). - */ - -#ifndef MSG_CLIENT_H -#define MSG_CLIENT_H - -#include -#include -#include -#include - -class MsgClient : public QObject { - Q_OBJECT -public: - explicit MsgClient(QObject* parent = nullptr); - ~MsgClient() override; - - void connectToHost(const QString& host, quint16 port); - void disconnectFromHost(); - bool isConnected() const; - - void bind(quint64 peerId); - void unbind(); - bool isBound() const { return m_boundPeerId != 0; } - quint64 boundPeerId() const { return m_boundPeerId; } - - void send(const QByteArray& data); - void send(const uint8_t* data, size_t len); - void sendSigned(const QByteArray& data); - void sendTo(quint64 dstPeerId, const QByteArray& data); - - void requestPublicKey(); - void requestPrivateKey(); - void requestPeerInfo(quint64 peerId); - -signals: - void connected(); - void disconnected(); - void bound(quint64 peerId); - void unbound(); - void received(quint64 peerId, const QByteArray& data); - void errorOccurred(uint8_t code, const QString& message); - void publicKeyReceived(const QByteArray& key); // 32 байта X25519 - void privateKeyReceived(const QByteArray& key); // 32 байта X25519 - void peerInfoReceived(quint64 peerId, const QByteArray& x25519Pubkey, - const QByteArray& ed25519Pubkey); - -private: - static constexpr size_t kMaxMsgSize = 8192; - static constexpr uint8_t kCtlBind = 0x01; - static constexpr uint8_t kCtlUnbind = 0x02; - static constexpr uint8_t kCtlGetPubkey = 0x03; - static constexpr uint8_t kCtlGetPrivkey = 0x04; - static constexpr uint8_t kCtlGetPeerInfo = 0x05; - static constexpr uint8_t kDataSend = 0x10; - static constexpr uint8_t kDataRecv = 0x11; - static constexpr uint8_t kRspBindOk = 0xE1; - static constexpr uint8_t kRspPubkey = 0xE2; - static constexpr uint8_t kRspPrivkey = 0xE3; - static constexpr uint8_t kRspPeerInfo = 0xE4; - static constexpr uint8_t kRspError = 0xFF; - static constexpr uint8_t kOptSigned = 0x01; - static constexpr size_t kHdrSize = 12; - - QTcpSocket* m_socket; - QByteArray m_recvBuffer; - quint64 m_boundPeerId = 0; - - void onReadyRead(); - void onSocketError(QAbstractSocket::SocketError); - void processMessages(); - void dispatch(uint8_t type, quint64 id, const QByteArray& payload); - QByteArray buildMessage(uint8_t type, quint64 id, uint8_t options, const QByteArray& data); -}; - -#endif // MSG_CLIENT_H diff --git a/tools/chatgui/transport/node_config.cpp b/tools/chatgui/transport/node_config.cpp index 4fe5ed91..9c396f60 100644 --- a/tools/chatgui/transport/node_config.cpp +++ b/tools/chatgui/transport/node_config.cpp @@ -184,7 +184,7 @@ quint16 NodeConfig::controlPort() const { if (m_controlAddr.isEmpty()) return 0; int sep = m_controlAddr.lastIndexOf(':'); if (sep < 0) return 0; - return static_cast(m_controlAddr.midRef(sep + 1).toUInt()); + return static_cast(m_controlAddr.mid(sep + 1).toUInt()); } void NodeConfig::setControlPort(quint16 p) { diff --git a/tools/chatgui/transport/utun_node.cpp b/tools/chatgui/transport/utun_node.cpp index 4ff23565..81616f7c 100644 --- a/tools/chatgui/transport/utun_node.cpp +++ b/tools/chatgui/transport/utun_node.cpp @@ -18,6 +18,7 @@ extern "C" { #include "../lib/debug_config.h" #include "../lib/mem.h" #include "chat_sync.h" +#include "chat_core.h" #include "gui_bridge.h" } @@ -55,6 +56,10 @@ void UtunNode::setDebugFile(const QString& path) { m_debugFile = path; } +void UtunNode::setDbPath(const QString& path) { + m_dbPath = path; +} + void UtunNode::stop() { if (!m_running) return; m_stop = true; @@ -122,6 +127,9 @@ void UtunNode::runLoop() { m_instance->test_user_ptr = this; + /* set ua for gui_bridge before init, so GUI can post */ + gui_bridge_set_uasync(ua); + if (utun_instance_init(m_instance) != 0) { QMetaObject::invokeMethod(this, [this] { emit error("utun_instance_init failed"); }); utun_instance_destroy(m_instance); @@ -133,9 +141,10 @@ void UtunNode::runLoop() { etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, recvCallback); - /* Initialize chat_sync */ + /* Initialize chat_core (DB) and chat_sync */ + chat_core_init(m_instance, m_dbPath.toUtf8().constData()); chat_sync_init(m_instance, NULL); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_sync initialized"); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "chat_core + chat_sync initialized"); while (!m_stop) { uasync_poll(ua, 100); @@ -143,6 +152,7 @@ void UtunNode::runLoop() { etcp_router_bind(m_instance, ETCP_RT_ID_CHAT, nullptr); chat_sync_destroy(m_instance); + chat_core_destroy(m_instance); utun_instance_destroy(m_instance); m_instance = nullptr; uasync_destroy(ua, 0); diff --git a/tools/chatgui/transport/utun_node.h b/tools/chatgui/transport/utun_node.h index 460e649e..c2715424 100644 --- a/tools/chatgui/transport/utun_node.h +++ b/tools/chatgui/transport/utun_node.h @@ -21,6 +21,7 @@ public: bool isRunning() const { return m_running; } void setDebugFile(const QString& path); + void setDbPath(const QString& path); void send(uint64_t dstNodeId, const QByteArray& data); @@ -43,5 +44,6 @@ private: std::atomic m_stop{false}; QString m_configPath; QString m_debugFile; + QString m_dbPath; }; #endif // UTUN_NODE_H