Browse Source

chatgui: replace MsgClient/ChatPropagator/crypto with chat_core, update DB/messaging/layout

chatgui
Evgeny 3 months ago
parent
commit
62954ca0a9
  1. 9
      AGENTS.md
  2. 2
      doc/_plan.txt
  3. 4
      src/route_node.h
  4. 31
      tools/chatgui/CMakeLists.txt
  5. 167
      tools/chatgui/chat_tz.txt1
  6. 101
      tools/chatgui/db/db_manager.cpp
  7. 2
      tools/chatgui/src/emojipanel.cpp
  8. 21
      tools/chatgui/src/main.cpp
  9. 32
      tools/chatgui/src/mainwindow.cpp
  10. 5
      tools/chatgui/src/mainwindow.h
  11. 10
      tools/chatgui/src/messagedelegate.cpp
  12. 2
      tools/chatgui/src/messagedelegate.h
  13. 58
      tools/chatgui/src/messagelist.cpp
  14. 1
      tools/chatgui/src/messagelist.h
  15. 247
      tools/chatgui/tests/test_crypto.cpp
  16. 697
      tools/chatgui/transport/chat_core.c
  17. 73
      tools/chatgui/transport/chat_core.h
  18. 376
      tools/chatgui/transport/chat_propagator.cpp
  19. 70
      tools/chatgui/transport/chat_propagator.h
  20. 48
      tools/chatgui/transport/chat_proto.h
  21. 572
      tools/chatgui/transport/chat_sync.c
  22. 221
      tools/chatgui/transport/crypto.cpp
  23. 48
      tools/chatgui/transport/crypto.h
  24. 43
      tools/chatgui/transport/gui_bridge.h
  25. 220
      tools/chatgui/transport/gui_bridge_impl.cpp
  26. 174
      tools/chatgui/transport/msg_client.cpp
  27. 80
      tools/chatgui/transport/msg_client.h
  28. 2
      tools/chatgui/transport/node_config.cpp
  29. 14
      tools/chatgui/transport/utun_node.cpp
  30. 2
      tools/chatgui/transport/utun_node.h

9
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.

2
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% (обновляется каждый час)
Сделать подключение к группе

4
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]

31
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)

167
tools/chatgui/chat_tz.txt1

@ -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" (A<B) или "group:hash"
name TEXT NOT NULL,
owner_node_id INTEGER, -- создатель (NULL для DM)
is_dm INTEGER DEFAULT 0,
last_message TEXT,
last_msg_at INTEGER, -- unix timestamp ms
created_at INTEGER DEFAULT (unixepoch())
);
CREATE UNIQUE INDEX idx_channel_cid ON channels(channel_id);
-- 4. channel_members — участники канала
CREATE TABLE channel_members (
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
joined_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (channel_id, node_id)
);
CREATE INDEX idx_cm_node ON channel_members(node_id);
-- 5. messages — сообщения чата
CREATE TABLE messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_id TEXT NOT NULL REFERENCES channels(channel_id) ON DELETE CASCADE,
author_node_id INTEGER NOT NULL,
content TEXT NOT NULL CHECK(length(content) <= 4096),
content_type TEXT DEFAULT 'text/plain',
timestamp INTEGER NOT NULL, -- unix milliseconds
signature BLOB CHECK(signature IS NULL OR length(signature) == 64),
is_outgoing INTEGER DEFAULT 0,
is_read INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_msg_channel_time ON messages(channel_id, timestamp);
CREATE INDEX idx_msg_author ON messages(author_node_id);
-- дедупликация: один автор не может отправить два сообщения в один ms в один канал
CREATE UNIQUE INDEX idx_msg_dedup ON messages(channel_id, author_node_id, timestamp);
-- 6. reactions — реакции на сообщения
CREATE TABLE reactions (
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
node_id INTEGER NOT NULL,
emoji TEXT NOT NULL, -- unicode codepoint или :shortcode:
signature BLOB, -- Ed25519 (64 bytes)
created_at INTEGER DEFAULT (unixepoch()),
PRIMARY KEY (message_id, node_id, emoji)
);
-- 7. attachments — вложения к сообщениям
CREATE TABLE attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
filename TEXT NOT NULL,
content_type TEXT NOT NULL, -- MIME type
size INTEGER NOT NULL, -- bytes
sha256 BLOB NOT NULL, -- 32 bytes
file_path TEXT, -- локальный путь к сохранённому файлу
signature BLOB, -- Ed25519 (64 bytes)
is_upload INTEGER DEFAULT 0, -- 0=скачано, 1=наше
created_at INTEGER DEFAULT (unixepoch())
);
CREATE INDEX idx_att_msg ON attachments(message_id);
================================================================================
Основные запросы
================================================================================
-- Текущая нода
SELECT node_id, name, x25519_pubkey, ed25519_pubkey
FROM my_nodes WHERE is_current = 1;
-- Все свои ноды
SELECT node_id, name, is_current FROM my_nodes ORDER BY created_at;
-- Список каналов с последним сообщением (для текущего пользователя)
SELECT c.channel_id, c.name, c.is_dm, c.last_message, c.last_msg_at
FROM channels c
JOIN channel_members cm ON c.channel_id = cm.channel_id
JOIN my_nodes mn ON cm.node_id = mn.node_id
WHERE mn.is_current = 1
ORDER BY c.last_msg_at DESC;
-- Сообщения канала (последние 50)
SELECT id, author_node_id, content, content_type, timestamp,
signature, is_outgoing, is_read
FROM messages
WHERE channel_id = ?
ORDER BY timestamp DESC
LIMIT 50;
-- Кто в канале
SELECT cm.node_id, n.name
FROM channel_members cm LEFT JOIN nodes n ON cm.node_id = n.node_id
WHERE cm.channel_id = ?;
-- DM между двумя узлами
-- channel_id = "dm:" || min(a,b) || "_" || max(a,b)
-- Реакции сообщения
SELECT node_id, emoji, signature FROM reactions WHERE message_id = ? ORDER BY created_at;
-- Вложения сообщения
SELECT id, filename, content_type, size, sha256, file_path
FROM attachments WHERE message_id = ?;
================================================================================
Примечания
================================================================================
- Адреса и подсети (node_addresses, node_subnets) — зона ответственности utun.
Чат не занимается сетевым уровнем. При необходимости utun отдаёт эту информацию
через control_server (etcpmon протокол).
- Все timestamp: INTEGER (unix epoch). messages.timestamp — миллисекунды,
created_at/updated_at/joined_at — секунды.
- signature везде Ed25519 (64 bytes) — NULL если не подписано.
- channel_id для DM: "dm:nodeA_nodeB" где A < B (канонический порядок).
- channel_id для групп: "group:" + SHA256(owner_node_id || name || timestamp).
- local_seq (из предыдущей версии) убран — дедупликация через UNIQUE индекс
(channel_id, author_node_id, timestamp) достаточна.
- deleted_at / soft delete не добавлены — можно добавить позже при необходимости.

101
tools/chatgui/db/db_manager.cpp

@ -213,31 +213,12 @@ bool DbManager::createChannel(const QString& chId, const QString& name,
quint64 ownerNodeId,
const QByteArray& x25519pub, const QByteArray& x25519priv,
const QByteArray& ed25519pub, const QByteArray& ed25519priv) {
GUI_INFO("createChannel: chId='%s' name='%s'", chId.toUtf8().constData(), name.toUtf8().constData());
QByteArray sig = kZero64; Q_UNUSED(sig);
sqlite3_stmt* stmt = prepareOrNull(
"INSERT INTO channels(channel_id,name,owner_node_id,is_dm,"
" x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature)"
" VALUES(?,?,?,0,?,?,?,?,?)");
if (!stmt) return false;
sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)ownerNodeId);
sqlite3_bind_blob(stmt, 4, x25519pub.isEmpty() ? kZero32 : x25519pub.constData(),
x25519pub.isEmpty() ? 32 : x25519pub.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 5, x25519priv.isEmpty() ? nullptr : x25519priv.constData(),
x25519priv.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 6, ed25519pub.isEmpty() ? kZero32 : ed25519pub.constData(),
ed25519pub.isEmpty() ? 32 : ed25519pub.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 7, ed25519priv.isEmpty() ? nullptr : ed25519priv.constData(),
ed25519priv.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 8, sig.constData(), sig.size(), SQLITE_STATIC);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
if (!ok) { GUI_ERROR("createChannel %s failed: %s", chId.toUtf8().constData(), sqlite3_errmsg(m_db)); return false; }
/* per-channel message table */
QString msgt = msgTableName(chId), pt = peersTableName(chId);
/* per-channel message table (DDL — до INSERT, чтобы не ломать транзакцию) */
QString csql = QStringLiteral(
"CREATE TABLE IF NOT EXISTS \"%1\" ("
" id INTEGER PRIMARY KEY AUTOINCREMENT,"
@ -267,6 +248,28 @@ bool DbManager::createChannel(const QString& chId, const QString& name,
" PRIMARY KEY (node_id))").arg(pt);
sqlite3_exec(m_db, csql.toUtf8().constData(), nullptr, nullptr, nullptr);
/* вставка записи channels (после DDL, корректно внутри транзакции) */
sqlite3_stmt* stmt = prepareOrNull(
"INSERT INTO channels(channel_id,name,owner_node_id,is_dm,"
" x25519_pubkey,x25519_privkey,ed25519_pubkey,ed25519_privkey,signature)"
" VALUES(?,?,?,0,?,?,?,?,?)");
if (!stmt) return false;
sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 3, (sqlite3_int64)ownerNodeId);
sqlite3_bind_blob(stmt, 4, x25519pub.isEmpty() ? kZero32 : x25519pub.constData(),
x25519pub.isEmpty() ? 32 : x25519pub.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 5, x25519priv.isEmpty() ? nullptr : x25519priv.constData(),
x25519priv.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 6, ed25519pub.isEmpty() ? kZero32 : ed25519pub.constData(),
ed25519pub.isEmpty() ? 32 : ed25519pub.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 7, ed25519priv.isEmpty() ? nullptr : ed25519priv.constData(),
ed25519priv.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 8, sig.constData(), sig.size(), SQLITE_TRANSIENT);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
if (!ok) { GUI_ERROR("createChannel %s failed: %s", chId.toUtf8().constData(), sqlite3_errmsg(m_db)); return false; }
GUI_INFO("Channel created: %s (msg=%s, peers=%s)",
chId.toUtf8().constData(), msgt.toUtf8().constData(), pt.toUtf8().constData());
return true;
@ -278,7 +281,7 @@ bool DbManager::deleteChannel(const QString& chId) {
sqlite3_exec(m_db, QStringLiteral("DROP TABLE IF EXISTS \"%1\"").arg(pt).toUtf8().constData(), nullptr, nullptr, nullptr);
sqlite3_stmt* stmt = prepareOrNull("DELETE FROM channels WHERE channel_id=?");
if (!stmt) return false;
sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 1, chId.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt);
sqlite3_finalize(stmt);
return true;
@ -329,12 +332,12 @@ bool DbManager::insertMessage(const QString& chId, quint64 nodeId,
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return false;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
sqlite3_bind_text(stmt, 2, contentType.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, contentType.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 4, timestamp);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh);
sqlite3_bind_blob(stmt, 6, ch.constData(), 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 6, ch.constData(), 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 8, isOutgoing ? 1 : 0);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
@ -357,11 +360,11 @@ bool DbManager::addPeer(const QString& chId, quint64 nodeId,
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return false;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
sqlite3_bind_blob(stmt, 2, joinSig.constData(), joinSig.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 2, joinSig.constData(), joinSig.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, creatorSig.isEmpty() ? nullptr : creatorSig.constData(),
creatorSig.size(), SQLITE_STATIC);
creatorSig.size(), SQLITE_TRANSIENT);
if (!comment.isEmpty())
sqlite3_bind_text(stmt, 4, comment.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 4, comment.toUtf8().constData(), -1, SQLITE_TRANSIENT);
else
sqlite3_bind_null(stmt, 4);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
@ -389,10 +392,10 @@ bool DbManager::upsertNode(quint64 nodeId, const QString& name,
" VALUES(?,?,?,?,unixepoch())");
if (!stmt) return false;
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, x25519pub.constData(), x25519pub.size(), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, name.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, x25519pub.constData(), x25519pub.size(), SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 4, ed25519pub.isEmpty() ? nullptr : ed25519pub.constData(),
ed25519pub.size(), SQLITE_STATIC);
ed25519pub.size(), SQLITE_TRANSIENT);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
sqlite3_finalize(stmt);
return ok;
@ -407,7 +410,7 @@ bool DbManager::addNodeAddress(quint64 nodeId, int family, int protocol,
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nodeId);
sqlite3_bind_int(stmt, 2, family);
sqlite3_bind_int(stmt, 3, protocol);
sqlite3_bind_blob(stmt, 4, addr.constData(), addr.size(), SQLITE_STATIC);
sqlite3_bind_blob(stmt, 4, addr.constData(), addr.size(), SQLITE_TRANSIENT);
sqlite3_bind_int(stmt, 5, port);
sqlite3_bind_int(stmt, 6, isNat ? 1 : 0);
bool ok = sqlite3_step(stmt) == SQLITE_DONE;
@ -550,7 +553,7 @@ AccountRow DbManager::getAccount(quint64 nodeId) const {
QString DbManager::getUiState(const QString& key) const {
sqlite3_stmt* stmt = prepareOrNull("SELECT value FROM ui_state WHERE key=?");
if (!stmt) return {};
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT);
QString val;
if (sqlite3_step(stmt) == SQLITE_ROW) val = colText(stmt, 0);
sqlite3_finalize(stmt);
@ -561,8 +564,8 @@ void DbManager::setUiState(const QString& key, const QString& value) {
sqlite3_stmt* stmt = prepareOrNull(
"INSERT OR REPLACE INTO ui_state(key,value) VALUES(?,?)");
if (!stmt) return;
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_text(stmt, 1, key.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, value.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_step(stmt); sqlite3_finalize(stmt);
}
@ -641,12 +644,12 @@ QByteArray DbManager::syncInsert(const QString& chId, const QByteArray& recData)
sqlite3_stmt* stmt = prepareOrNull(sql.toUtf8().constData());
if (!stmt) return QByteArray(1, (char)-1);
sqlite3_bind_int64(stmt, 1, (sqlite3_int64)nid);
sqlite3_bind_text(stmt, 2, ct.toUtf8().constData(), -1, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_STATIC);
sqlite3_bind_text(stmt, 2, ct.toUtf8().constData(), -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 3, data.constData(), data.size(), SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 4, ts);
sqlite3_bind_int64(stmt, 5, (sqlite3_int64)dh);
sqlite3_bind_blob(stmt, 6, peerCH.constData(), 32, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_STATIC);
sqlite3_bind_blob(stmt, 6, peerCH.constData(), 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(stmt, 7, kZero64, 64, SQLITE_TRANSIENT);
int rc = sqlite3_step(stmt);
sqlite3_finalize(stmt);
if (rc == SQLITE_DONE) return QByteArray(1, '\0');
@ -760,8 +763,8 @@ void DbManager::seedStubData() {
"INSERT OR IGNORE INTO local_identity(id,node_id,name,x25519_pubkey,ed25519_pubkey)"
" VALUES(1,?,'MyDevice',?,?)");
sqlite3_bind_int64(ins, 1, m_myNodeId);
sqlite3_bind_blob(ins, 2, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_blob(ins, 2, pub_dummy, 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_TRANSIENT);
sqlite3_step(ins); sqlite3_finalize(ins);
struct { quint64 id; const char* name; const char* letter; const char* color; } accs[] = {
@ -773,17 +776,17 @@ void DbManager::seedStubData() {
ins = prepareOrNull("INSERT OR IGNORE INTO nodes(node_id,name,x25519_pubkey,ed25519_pubkey,last_seen_at)"
" VALUES(?,?,?,?,unixepoch())");
sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id);
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_STATIC);
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_TRANSIENT);
sqlite3_bind_blob(ins, 3, pub_dummy, 32, SQLITE_TRANSIENT);
sqlite3_bind_blob(ins, 4, pub_dummy, 32, SQLITE_TRANSIENT);
sqlite3_step(ins); sqlite3_finalize(ins);
ins = prepareOrNull("INSERT OR IGNORE INTO accounts(node_id,display_name,avatar_color,avatar_letter)"
" VALUES(?,?,?,?)");
sqlite3_bind_int64(ins, 1, (sqlite3_int64)a.id);
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_STATIC);
sqlite3_bind_text(ins, 2, a.name, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(ins, 3, a.color, -1, SQLITE_TRANSIENT);
sqlite3_bind_text(ins, 4, a.letter, -1, SQLITE_TRANSIENT);
sqlite3_step(ins); sqlite3_finalize(ins);
}

2
tools/chatgui/src/emojipanel.cpp

@ -46,7 +46,7 @@ public:
const QString &unicode() const { return m_data.unicode; }
protected:
void enterEvent(QEvent *e) override {
void enterEvent(QEnterEvent *e) override {
QPushButton::enterEvent(e);
if (m_icon && m_icon->isValid()) {
m_hovered = true;

21
tools/chatgui/src/main.cpp

@ -1,4 +1,6 @@
#include <QApplication>
#include <QPalette>
#include <QStyleFactory>
#include <QDir>
#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);

32
tools/chatgui/src/mainwindow.cpp

@ -14,9 +14,18 @@
#include <QApplication>
#include <QStyle>
#include <QMessageBox>
#include <QDebug>
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();
}

5
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;
};

10
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();

2
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,

58
tools/chatgui/src/messagelist.cpp

@ -12,9 +12,17 @@
#include <QPixmap>
#include <QDateTime>
#include <QFile>
#include <QDebug>
#include <vector>
#include <memory>
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;

1
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);

247
tools/chatgui/tests/test_crypto.cpp

@ -1,247 +0,0 @@
/*
* test_crypto.cpp — тесты криптографических функций (Crypto::)
*/
#include <QTest>
#include <QTemporaryFile>
#include <QByteArray>
#include <QElapsedTimer>
#include <QAtomicInt>
#include <QDebug>
#include <cstring>
#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<int>(Crypto::kKeySize); i++)
m_x25519Priv[i] = static_cast<char>(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<int>(Crypto::kKeySize));
QCOMPARE(m_edPub.size(), static_cast<int>(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<int>(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<int>(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<char>(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<int>(Crypto::kKeySize); i++)
otherPriv[i] = static_cast<char>(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<int>(kChunkSize), Qt::Uninitialized);
memset(chunk.data(), pattern, kChunkSize);
size_t remaining = fileSize;
while (remaining >= kChunkSize) {
QCOMPARE(f.write(chunk), static_cast<qint64>(kChunkSize));
remaining -= kChunkSize;
}
if (remaining > 0) f.write(chunk.constData(), static_cast<qint64>(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<unsigned char>(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"

697
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 <sqlite3.h>
#include <openssl/sha.h>
#include <string.h>
#include <stdio.h>
#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<?",
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)node_id);
sqlite3_bind_int64(stmt, 2, (sqlite3_int64)cutoff_us);
sqlite3_step(stmt); sqlite3_finalize(stmt);
}
int chat_core_list_channels(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 channel_id FROM channels ORDER BY created_at ASC",
-1, &stmt, NULL) != SQLITE_OK) return -1;
uint8_t* out = buf;
uint8_t* start = out;
out += 2; /* placeholder for count */
uint16_t cnt = 0;
while (sqlite3_step(stmt) == SQLITE_ROW) {
const char* ch_id = (const char*)sqlite3_column_text(stmt, 0);
int ch_len = sqlite3_column_bytes(stmt, 0);
if (!ch_id || ch_len <= 0 || ch_len > 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);
}

73
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 <stdint.h>
#include <stddef.h>
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 */

376
tools/chatgui/transport/chat_propagator.cpp

@ -1,376 +0,0 @@
#include "chat_propagator.h"
#include "chat_proto.h"
#include "utun_node.h"
#include "crypto.h"
#include "db_manager.h"
#include <QDateTime>
#include <algorithm>
#include <random>
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<quint64> 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<uint8_t>(cid.size());
size_t total = 1 + 1 + cidLen + 8 + 2 + content.size() + ChatProp::kSignSize;
QByteArray pkt(static_cast<int>(total), Qt::Uninitialized);
uint8_t* p = reinterpret_cast<uint8_t*>(pkt.data());
*p++ = ChatProp::kNewMsg;
*p++ = cidLen;
memcpy(p, cid.constData(), cidLen); p += cidLen;
write64(p, static_cast<uint64_t>(timestamp)); p += 8;
write16(p, static_cast<uint16_t>(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<int>(1 + ChatProp::kSignSize), Qt::Uninitialized);
pkt[0] = static_cast<char>(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<quint64>& 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<uint8_t>(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<quint64> 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<int>(1 + ChatProp::kSignSize)) return;
QByteArray sig = payload.mid(1, static_cast<int>(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<QByteArray> 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<quint64> 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<quint64>{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<int>(2 + cid.size()), Qt::Uninitialized);
pkt[0] = static_cast<char>(ChatProp::kBucketsReq);
pkt[1] = static_cast<char>(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<const uint8_t*>(payload.constData());
if (*p != ChatProp::kBucketsRsp) return; p++;
uint8_t cidLen = *p++;
if (p + cidLen + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) return;
QString channelId = QString::fromUtf8(reinterpret_cast<const char*>(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<const uint8_t*>(payload.constData()) + payload.size()) break;
qint64 day = static_cast<qint64>(read32(p)); p += 4;
int hour = *p++;
int rCount = static_cast<int>(read32(p)); p += 4;
QByteArray rHash(reinterpret_cast<const char*>(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<int>(7 + cid.size()), Qt::Uninitialized);
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data());
*rp++ = ChatProp::kHashesReq;
*rp++ = static_cast<uint8_t>(cid.size());
memcpy(rp, cid.constData(), cid.size()); rp += cid.size();
write32(rp, static_cast<uint32_t>(day)); rp += 4;
*rp = static_cast<uint8_t>(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<const uint8_t*>(payload.constData());
if (*p != ChatProp::kHashesRsp) return; p++;
uint16_t sigCount = read16(p); p += 2;
QList<QByteArray> remoteSigs;
for (int i = 0; i < sigCount; i++) {
if (p + 64 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size()) break;
remoteSigs.append(QByteArray(reinterpret_cast<const char*>(p), 64));
p += 64;
}
if (remoteSigs.isEmpty()) return;
/* find missing */
QList<QByteArray> missing;
QList<QByteArray> 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<int>(3 + mCount * 64), Qt::Uninitialized);
uint8_t* rp = reinterpret_cast<uint8_t*>(req.data());
*rp++ = ChatProp::kMsgsReq;
write16(rp, static_cast<uint16_t>(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<const uint8_t*>(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<const char*>(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<const char*>(p), 64); p += 64;
quint64 author = read64(p); p += 8;
qint64 ts = static_cast<qint64>(read64(p)); p += 8;
uint16_t cLen = read16(p); p += 2;
if (p + cLen > end) break;
QByteArray content(reinterpret_cast<const char*>(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<const uint8_t*>(payload.constData());
if (*p != ChatProp::kNewMsg) return false;
p++;
uint8_t cidLen = *p++;
if (p + cidLen + 8 + 2 > reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size())
return false;
channelId = QString::fromUtf8(reinterpret_cast<const char*>(p), cidLen); p += cidLen;
timestamp = static_cast<qint64>(read64(p)); p += 8;
uint16_t contentLen = read16(p); p += 2;
if (p + contentLen + ChatProp::kSignSize >
reinterpret_cast<const uint8_t*>(payload.constData()) + payload.size())
return false;
content = QByteArray(reinterpret_cast<const char*>(p), contentLen); p += contentLen;
sig = QByteArray(reinterpret_cast<const char*>(p), static_cast<int>(ChatProp::kSignSize));
return true;
}
QList<quint64> ChatPropagator::pickRandomPeers(int count, const QList<quint64>& exclude) {
QList<quint64> result;
if (!m_db) return result;
auto accs = m_db->getAccounts(true);
QList<quint64> 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;
}

70
tools/chatgui/transport/chat_propagator.h

@ -1,70 +0,0 @@
#pragma once
#include <QObject>
#include <QByteArray>
#include <QHash>
#include <QList>
#include <QTimer>
#include <cstdint>
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<quint64> peers; /* target peers */
QList<quint64> acked; /* peers that acknowledged */
qint64 sentAt; /* when sent (ms) */
int retries = 0;
};
QHash<QByteArray, GossipEntry> 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<quint64>& exclude);
void sendGossip(quint64 peer, const QByteArray& packet);
QList<quint64> pickRandomPeers(int count, const QList<quint64>& 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);
};

48
tools/chatgui/transport/chat_proto.h

@ -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 <cstdint>
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

572
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 <string.h>
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);
}

221
tools/chatgui/transport/crypto.cpp

@ -1,221 +0,0 @@
/*
* crypto.cpp — Криптографические утилиты (Ed25519 через OpenSSL)
*/
#include "crypto.h"
#include <openssl/sha.h>
#include <openssl/evp.h>
#include <QFile>
#include <QCoreApplication>
#include <thread>
#include <cstring>
namespace Crypto {
/* ============================================================================
* Помощники
* ============================================================================ */
namespace {
void postToMain(std::function<void()> 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<const uint8_t*>(buf.constData()),
static_cast<size_t>(n));
}
file.close();
QByteArray hash;
hash.resize(static_cast<int>(kSha512Size));
unsigned int hash_len = static_cast<unsigned int>(kSha512Size);
if (EVP_DigestFinal_ex(md_ctx, reinterpret_cast<uint8_t*>(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<int>(kKeySize)) return QByteArray();
uint8_t hash[kSha512Size];
SHA512(reinterpret_cast<const uint8_t*>(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<int>(kKeySize));
size_t priv_len = kKeySize;
if (EVP_PKEY_get_raw_private_key(pkey, reinterpret_cast<uint8_t*>(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<int>(kKeySize)) return QByteArray();
uint8_t hash[kSha512Size];
SHA512(reinterpret_cast<const uint8_t*>(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<int>(kKeySize));
size_t pub_len = kKeySize;
if (EVP_PKEY_get_raw_public_key(pkey, reinterpret_cast<uint8_t*>(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<int>(kKeySize)) return QByteArray();
EVP_PKEY* pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL,
reinterpret_cast<const uint8_t*>(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<int>(kSignSize));
if (EVP_DigestSign(md_ctx, reinterpret_cast<uint8_t*>(result.data()), &sig_len,
reinterpret_cast<const uint8_t*>(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<int>(sig_len));
return result;
}
bool verify(const QByteArray& ed25519Pubkey, const QByteArray& data, const QByteArray& sig) {
if (ed25519Pubkey.size() != static_cast<int>(kKeySize)
|| sig.size() > static_cast<int>(kSignSize)) return false;
EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL,
reinterpret_cast<const uint8_t*>(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<const uint8_t*>(sig.constData()), sig.size(),
reinterpret_cast<const uint8_t*>(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

48
tools/chatgui/transport/crypto.h

@ -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 <QByteArray>
#include <QString>
#include <functional>
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(const SignResult&)>;
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

43
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

220
tools/chatgui/transport/gui_bridge_impl.cpp

@ -3,11 +3,9 @@
#include <QObject>
#include <QMetaObject>
#include <QByteArray>
#include <QString>
#include <QDebug>
#include <cstring>
#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"

174
tools/chatgui/transport/msg_client.cpp

@ -1,174 +0,0 @@
/*
* msg_client.cpp — C++ транспортный клиент
*/
#include "msg_client.h"
#include <cstring>
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<const char*>(data), static_cast<int>(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<int>(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<size_t>(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<uint8_t>(m_recvBuffer[2]);
uint64_t id;
std::memcpy(&id, m_recvBuffer.constData() + 3, 8);
QByteArray payload = m_recvBuffer.mid(static_cast<int>(kHdrSize),
totalSize - static_cast<uint16_t>(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<uint8_t>(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<uint16_t>(kHdrSize + data.size());
QByteArray msg;
msg.resize(totalSize);
std::memcpy(msg.data(), &totalSize, 2);
msg[2] = static_cast<char>(type);
std::memcpy(msg.data() + 3, &id, 8);
msg[11] = static_cast<char>(options);
if (!data.isEmpty())
std::memcpy(msg.data() + kHdrSize, data.constData(), data.size());
return msg;
}

80
tools/chatgui/transport/msg_client.h

@ -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 <QObject>
#include <QByteArray>
#include <QTcpSocket>
#include <cstdint>
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

2
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<quint16>(m_controlAddr.midRef(sep + 1).toUInt());
return static_cast<quint16>(m_controlAddr.mid(sep + 1).toUInt());
}
void NodeConfig::setControlPort(quint16 p) {

14
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);

2
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<bool> m_stop{false};
QString m_configPath;
QString m_debugFile;
QString m_dbPath;
};
#endif // UTUN_NODE_H

Loading…
Cancel
Save