30 changed files with 1170 additions and 2162 deletions
@ -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 не добавлены — можно добавить позже при необходимости. |
||||
@ -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" |
||||
@ -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); |
||||
} |
||||
@ -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 */ |
||||
@ -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; |
||||
} |
||||
@ -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); |
||||
}; |
||||
@ -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 |
||||
@ -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
|
||||
@ -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
|
||||
@ -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; |
||||
} |
||||
@ -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
|
||||
Loading…
Reference in new issue