diff --git a/src/chat/chat_channel.c b/src/chat/chat_channel.c index 6f68b30b..6dcd5e7e 100644 --- a/src/chat/chat_channel.c +++ b/src/chat/chat_channel.c @@ -146,7 +146,7 @@ void chat_core_create_channel(struct chat_channel_create* req) { g_cc.inst->name[0] ? g_cc.inst->name : ""); int mrc = member_sync_put(g_cc.inst, req->channel_id, myid, g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, - join_sig, join_ts, NULL, 0, juser3, my_addrs, my_addr_cnt); + join_sig, join_ts, NULL, 0, juser3, my_addrs, my_addr_cnt, NULL, NULL, 0); if (mrc != 0) { DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_sync_put(self) FAILED ch=%s rc=%d", CC_ID, req->channel_id, mrc); diff --git a/src/chat/chat_profile.c b/src/chat/chat_profile.c index 16f96ee6..5c9a47d5 100644 --- a/src/chat/chat_profile.c +++ b/src/chat/chat_profile.c @@ -67,10 +67,10 @@ void chat_core_update_my_name(const char* name) { } char juser[256]; snprintf(juser, sizeof(juser), "{\"name\":\"%s\"}", name); int r1 = topo_node_sqlite_member_put(g_cc.db, ch, myid, new_sig, join_ts, NULL, 0, - g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, juser, NULL); + g_cc.inst->my_keys.public_key, g_cc.inst->my_ed25519_pubkey, juser, NULL, NULL, NULL, 0); if (r1 != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r1); int r2 = member_sync_put(g_cc.inst, ch, myid, g_cc.inst->my_keys.public_key, - g_cc.inst->my_ed25519_pubkey, new_sig, join_ts, NULL, 0, juser, NULL, 0); + g_cc.inst->my_ed25519_pubkey, new_sig, join_ts, NULL, 0, juser, NULL, 0, NULL, NULL, 0); if (r2 != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_sync_put(my_name) FAILED ch=%s rc=%d", CC_ID, ch, r2); uint8_t evt[65]; uint8_t cl = (uint8_t)strlen(ch); evt[0] = cl; memcpy(evt + 1, ch, cl); diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index 7f795d9e..b333608a 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -928,7 +928,7 @@ static void cs_handle_channel_info_resp(struct chat_sync* cs, uint64_t peer, DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: [ADDR_SYNC] INFO_RESP DONE peer=%016llx: %d links, %d written", CS_ID, (unsigned long long)peer, lk_count, lk_written); } - int rc = topo_node_sqlite_member_put(vdb, ch_id, peer, inviter_join_sig, inviter_join_ts, inviter_update_sig, inviter_update_ts, inv_x25519, inv_ed, inv_userinfo, inviter_join_sig); + int rc = topo_node_sqlite_member_put(vdb, ch_id, peer, inviter_join_sig, inviter_join_ts, inviter_update_sig, inviter_update_ts, inv_x25519, inv_ed, inv_userinfo, inviter_join_sig, NULL, NULL, 0); if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(inviter) FAILED ch=%s peer=%016llx rc=%d", CS_ID, ch_id, (unsigned long long)peer, rc); char inv_name[128]; json_flat_get(inv_userinfo, "name", inv_name, sizeof(inv_name)); topo_node_sqlite_node_update_verified(vdb, peer, inv_name, inv_x25519, inv_ed, inviter_join_ts, ntp_time_get_seconds(cs->inst)); @@ -1038,7 +1038,7 @@ static void cs_handle_channel_join(struct chat_sync* cs, uint64_t peer, } sqlite3* db = cs->inst->topo_sqlite_db; - int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, joiner_userinfo, NULL); + int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, joiner_userinfo, NULL, NULL, NULL, 0); if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(joiner) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc); { char j_name[128]; json_flat_get(joiner_userinfo, "name", j_name, sizeof(j_name)); topo_node_sqlite_node_update_verified(db, node_id, j_name, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); } @@ -1193,7 +1193,7 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer, } } - int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL); + int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL, NULL, NULL, 0); if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(welcome) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc); { char pn[256]; json_flat_get(peer_userinfo, "name", pn, sizeof(pn)); topo_node_sqlite_node_update_verified(db, node_id, pn, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); } @@ -1240,7 +1240,7 @@ static void cs_handle_welcome(struct chat_sync* cs, uint64_t peer, const uint8_t* my_x25 = cs->inst->my_keys.public_key; const uint8_t* my_ed = cs->inst->my_ed25519_pubkey; const char* my_name = cs->inst->name[0] ? cs->inst->name : ""; - int src = topo_node_sqlite_member_put(db, ch_id, myid, NULL, 0, NULL, 0, my_x25, my_ed, my_name, NULL); + int src = topo_node_sqlite_member_put(db, ch_id, myid, NULL, 0, NULL, 0, my_x25, my_ed, my_name, NULL, NULL, NULL, 0); if (src != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(self) FAILED in WELCOME ch=%s rc=%d", CS_ID, ch_id, src); else @@ -1306,7 +1306,7 @@ static void cs_handle_peer_upsert(struct chat_sync* cs, uint64_t peer, sqlite3* db = cs->inst->topo_sqlite_db; - int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL); + int rc = topo_node_sqlite_member_put(db, ch_id, node_id, join_sig, join_ts, NULL, 0, x25519, ed_pub, peer_userinfo, NULL, NULL, NULL, 0); if (rc != 0) DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put(peer_upsert) FAILED ch=%s node=0x%016llx rc=%d", CS_ID, ch_id, (unsigned long long)node_id, rc); { char pn2[128]; json_flat_get(peer_userinfo, "name", pn2, sizeof(pn2)); topo_node_sqlite_node_update_verified(db, node_id, pn2, x25519, ed_pub, join_ts, ntp_time_get_seconds(cs->inst)); } diff --git a/src/chat/member_sync.c b/src/chat/member_sync.c index 0c82c9e5..c05a6d82 100644 --- a/src/chat/member_sync.c +++ b/src/chat/member_sync.c @@ -49,6 +49,7 @@ static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const uint8_t* addrs_data, int addr_count, + const char* adm_tags, const uint8_t* adm_tags_sig, uint8_t hash_out[MT_HASH_SIZE]) { EVP_MD_CTX* ctx = EVP_MD_CTX_new(); EVP_DigestInit_ex(ctx, EVP_sha256(), NULL); @@ -81,6 +82,11 @@ static void _compute_member_hash(uint64_t node_id, const uint8_t* x25519, } } + uint8_t atl = adm_tags ? (uint8_t)strnlen(adm_tags, 255) : 0; + EVP_DigestUpdate(ctx, &atl, 1); + if (atl) EVP_DigestUpdate(ctx, adm_tags, atl); + if (adm_tags_sig) EVP_DigestUpdate(ctx, adm_tags_sig, 64); else { static const uint8_t z64[64]; EVP_DigestUpdate(ctx, z64, 64); } + EVP_DigestFinal_ex(ctx, hash_out, NULL); EVP_MD_CTX_free(ctx); } @@ -100,7 +106,7 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts" + " join_sig, join_ts, update_sig, update_ts, adm_tags, adm_tags_sig" " FROM \"%s\"" " WHERE (node_id & %lld) == %lld ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix64); @@ -117,9 +123,11 @@ static int _member_update_bucket_hash(void* ctx, const char* ns, uint8_t level, uint64_t jts = (uint64_t)sqlite3_column_int64(stmt, 4); const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); + const char* atags = (const char*)sqlite3_column_text(stmt, 7); + const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 8); if (!x25 || !ed) continue; uint8_t mh[MT_HASH_SIZE]; - _compute_member_hash(nid, x25, ed, sig, jts, usig, uts, NULL, 0, mh); + _compute_member_hash(nid, x25, ed, sig, jts, usig, uts, NULL, 0, atags, atsig, mh); EVP_DigestUpdate(sha_ctx, mh, MT_HASH_SIZE); count++; } @@ -141,7 +149,7 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts, userinfo" + " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig" " FROM \"%s\"" " WHERE (node_id & %lld) == %lld ORDER BY node_id ASC", peers_tbl, (long long)mask, (long long)prefix); @@ -162,8 +170,11 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, const uint8_t* usig = (const uint8_t*)sqlite3_column_blob(stmt, 5); uint64_t uts = (uint64_t)sqlite3_column_int64(stmt, 6); const char* nm = (const char*)sqlite3_column_text(stmt, 7); + const char* atags = (const char*)sqlite3_column_text(stmt, 8); + const uint8_t* atsig = (const uint8_t*)sqlite3_column_blob(stmt, 9); if (!x25 || !ed) continue; uint8_t nl = nm ? (uint8_t)strnlen(nm, 255) : 0; + uint8_t atl = atags ? (uint8_t)strnlen(atags, 255) : 0; sqlite3_stmt* as = NULL; sqlite3_prepare_v2(db, @@ -189,7 +200,7 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, } uint8_t flags = (sig && jts) ? PEERS_FLAG_HAS_JOIN : 0; - size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)addr_off; + size_t need = 8 + 32 + 32 + 1 + (flags ? 72ULL : 0ULL) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 1 + (size_t)addr_off; if (off + need > *len) { sqlite3_finalize(stmt); return -2; } memcpy(buf + off, &nid, 8); off += 8; memcpy(buf + off, x25, 32); off += 32; @@ -208,6 +219,10 @@ static int _member_get_items(void* ctx, const char* ns, uint8_t level, } buf[off++] = nl; if (nl) { memcpy(buf + off, nm, nl); off += nl; } + buf[off++] = atl; + if (atl) { memcpy(buf + off, atags, atl); off += atl; } + if (atsig && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, atsig, 64); off += 64; } + else { memset(buf + off, 0, 64); off += 64; } buf[off++] = (uint8_t)addr_count; memcpy(buf + off, addrs, (size_t)addr_off); off += (size_t)addr_off; (*cnt)++; @@ -227,6 +242,7 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, uint16_t count; memcpy(&count, data, 2); DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: apply_items ns=%s count=%u from=%016llx", MS_ID, ns, count, (unsigned long long)from_peer); const uint8_t* mp = data + 2; size_t mrem = len - 2; + sqlite3* db = _db(inst); for (uint16_t i = 0; i < count && mrem >= 148; i++) { uint64_t nid; memcpy(&nid, mp, 8); mp += 8; mrem -= 8; @@ -239,13 +255,19 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, } else { /* lookup join from local DB */ uint8_t buf_jsig[64]; uint64_t buf_jts; - if (topo_node_sqlite_member_get_join(_db(inst), ns, nid, buf_jsig, &buf_jts) == 0) { jsig = buf_jsig; jts = buf_jts; } + if (db && topo_node_sqlite_member_get_join(db, ns, nid, buf_jsig, &buf_jts) == 0) { jsig = buf_jsig; jts = buf_jts; } } if (mrem < 72) break; const uint8_t* usig = mp; mp += 64; mrem -= 64; uint64_t uts; memcpy(&uts, mp, 8); mp += 8; mrem -= 8; uint8_t nl = *mp++; mrem--; char nm[256] = ""; if (nl && mrem >= nl) { memcpy(nm, mp, nl); nm[nl] = '\0'; mp += nl; mrem -= nl; } + if (mrem < 1) break; + uint8_t atl = *mp++; mrem--; + char atags[256] = ""; + const uint8_t* atsig = NULL; + if (atl && mrem >= atl) { memcpy(atags, mp, atl); atags[atl] = '\0'; mp += atl; mrem -= atl; } + if (mrem >= 64) { atsig = mp; mp += 64; mrem -= 64; } uint8_t ac = *mp++; mrem--; const uint8_t* addrs = mp; int consumed = 0; @@ -276,8 +298,64 @@ static int _member_apply_items(void* ctx, const char* ns, uint64_t from_peer, } } } - member_sync_put(inst, ns, nid, x25, ed, jsig, jts, usig, uts, nm, addrs, (int)ac); - DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: [ADDR_SYNC] member_sync recv ns=%s nid=0x%016llx ac=%d", MS_ID, ns, (unsigned long long)nid, ac); + /* verify adm_tags_sig and version check */ + int adm_ver = 0, adm_storage = 0; + if (atags[0] && atsig) { + const char* ver_s = strstr(atags, "ver="); + if (ver_s) adm_ver = atoi(ver_s + 4); + adm_storage = strstr(atags, "storage=yes") ? 1 : 0; + uint8_t ch_ed_pub[32] = {0}; + if (db && topo_node_sqlite_channel_get(db, ns, NULL, 0, NULL, NULL, ch_ed_pub, NULL) == 0) { + uint8_t amsg[264]; size_t aoff = 0; + size_t atl2 = strlen(atags); + if (atl2 > 191) atl2 = 191; + memcpy(amsg + aoff, atags, atl2); aoff += atl2; + memcpy(amsg + aoff, &nid, 8); aoff += 8; + EVP_PKEY* pkey = EVP_PKEY_new_raw_public_key(EVP_PKEY_ED25519, NULL, ch_ed_pub, 32); + if (pkey) { + EVP_MD_CTX* vctx = EVP_MD_CTX_new(); + if (vctx) { + int ok = (EVP_DigestVerifyInit(vctx, NULL, NULL, NULL, pkey) == 1) + && (EVP_DigestVerify(vctx, atsig, 64, amsg, aoff) == 1); + if (!ok) { + DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_sync invalid adm_tags_sig node=0x%016llx ns=%s tags=%s", + MS_ID, (unsigned long long)nid, ns, atags); + adm_ver = -1; /* skip this record */ + } + EVP_MD_CTX_free(vctx); + } + EVP_PKEY_free(pkey); + } + } else { + DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: cannot verify adm_tags_sig — no channel key for ns=%s", MS_ID, ns); + adm_ver = -1; + } + /* check version is strictly greater than local */ + if (adm_ver > 0 && db) { + char peers_tbl[128]; _peers_table(ns, peers_tbl, sizeof(peers_tbl)); + sqlite3_stmt* av = NULL; + char asql[256]; snprintf(asql, sizeof(asql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", peers_tbl); + if (sqlite3_prepare_v2(db, asql, -1, &av, NULL) == SQLITE_OK) { + sqlite3_bind_int64(av, 1, (sqlite3_int64)nid); + if (sqlite3_step(av) == SQLITE_ROW) { + const char* local_tags = (const char*)sqlite3_column_text(av, 0); + if (local_tags) { + int local_ver = 0; const char* lv = strstr(local_tags, "ver="); + if (lv) local_ver = atoi(lv + 4); + if (adm_ver <= local_ver) { + DEBUG_WARN(DEBUG_CATEGORY_DB_SYNC, "%s: stale adm_tags node=0x%016llx ns=%s ver=%d <= local=%d — skipping", + MS_ID, (unsigned long long)nid, ns, adm_ver, local_ver); + adm_ver = -1; + } + } + } + sqlite3_finalize(av); + } + } + } + if (adm_ver < 0) { mp += consumed; mrem -= (size_t)consumed; continue; } + member_sync_put(inst, ns, nid, x25, ed, jsig, jts, usig, uts, nm, addrs, (int)ac, atags[0] ? atags : NULL, atsig, adm_storage); + DEBUG_INFO(DEBUG_CATEGORY_DEBUG, "%s: [ADDR_SYNC] member_sync recv ns=%s nid=0x%016llx ac=%d adm_ver=%d storage=%d", MS_ID, ns, (unsigned long long)nid, ac, adm_ver, adm_storage); mp += consumed; mrem -= (size_t)consumed; } if (count > 0) @@ -362,14 +440,16 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const char* userinfo, - const uint8_t* addrs_data, int addr_count) { + const uint8_t* addrs_data, int addr_count, + const char* adm_tags, const uint8_t* adm_tags_sig, int storage) { if (!inst || !ch_id) return -1; - DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s ac=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", addr_count); + DEBUG_TRACE(DEBUG_CATEGORY_DB_SYNC, "%s: put ch=%s nid=%016llx userinfo=%s ac=%d adm_tags=%s storage=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", addr_count, adm_tags ? adm_tags : "", storage); sqlite3* db = _db(inst); if (!db) return -1; int rc = topo_node_sqlite_member_put(db, ch_id, member_id, join_sig, join_ts, update_sig, update_ts, - x25519, ed25519, userinfo, NULL); + x25519, ed25519, userinfo, NULL, + adm_tags, adm_tags_sig, storage); if (rc != 0) { DEBUG_ERROR(DEBUG_CATEGORY_DB_SYNC, "%s: member_put FAILED ch=%s id=0x%016llx userinfo=%s rc=%d", MS_ID, ch_id, (unsigned long long)member_id, userinfo ? userinfo : "", rc); diff --git a/src/chat/member_sync.h b/src/chat/member_sync.h index 01ea6b06..7420313f 100644 --- a/src/chat/member_sync.h +++ b/src/chat/member_sync.h @@ -102,7 +102,8 @@ int member_sync_put(struct UTUN_INSTANCE* inst, const char* ch_id, const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const char* userinfo, - const uint8_t* addrs_data, int addr_count); + const uint8_t* addrs_data, int addr_count, + const char* adm_tags, const uint8_t* adm_tags_sig, int storage); /* Удалить мембера из канала и пересчитать дерево. */ int member_sync_del(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t member_id); diff --git a/src/routing_layer/topo_group.c b/src/routing_layer/topo_group.c index a7e7abfe..026e464a 100644 --- a/src/routing_layer/topo_group.c +++ b/src/routing_layer/topo_group.c @@ -669,7 +669,7 @@ int topo_group_process_nodeinfo(struct TOPO_GROUP* group, struct ETCP_CONN* from if (sdb) { topo_node_sqlite_node_put(sdb, nodeinfo1, ntp_time_get_seconds(group->instance)); if (group->group_type == TOPO_GROUP_TYPE_CHAT && group->channel_id[0]) { - topo_node_sqlite_member_put(sdb, group->channel_id, node_id, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL); + topo_node_sqlite_member_put(sdb, group->channel_id, node_id, NULL, 0, NULL, 0, NULL, NULL, NULL, NULL, NULL, NULL, 0); DEBUG_DEBUG(DEBUG_CATEGORY_DEBUG, "SQLite member_put: ch=%s node=%016llx", group->channel_id, (unsigned long long)node_id); } topo_node_sqlite_nodeinfo_updated(sdb, node_id); diff --git a/src/routing_layer/topo_node_sqlite.c b/src/routing_layer/topo_node_sqlite.c index 4e4f7d31..852260f5 100644 --- a/src/routing_layer/topo_node_sqlite.c +++ b/src/routing_layer/topo_node_sqlite.c @@ -280,6 +280,7 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, 2=EIM NAT 3=(не исп.) 4=суперузел(DIRECT+"supernode=yes") */ " node_RTT INTEGER," /* RTT до узла в мс, часть индекса (node_type, node_RTT) */ " connected INTEGER NOT NULL DEFAULT 0," /* 1 = было активное подключение к узлу */ + " storage INTEGER NOT NULL DEFAULT 0," /* 1 = приоритетный хранитель аттачей */ " local_nick TEXT" /* локальный ник (GUI only, не синхронизируется) */ ")"; @@ -303,6 +304,16 @@ int topo_node_sqlite_channel_put(sqlite3* db, const char* channel_id, u_free(idx_sql); } + int sidx_sz = snprintf(NULL, 0, + "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); + char* sidx_sql = u_malloc(sidx_sz + 1); + if (sidx_sql) { + snprintf(sidx_sql, sidx_sz + 1, + "CREATE INDEX IF NOT EXISTS \"idx_%s_storage\" ON \"%s\" (storage)", peers_tbl, peers_tbl); + sqlite3_exec(db, sidx_sql, NULL, NULL, NULL); + u_free(sidx_sql); + } + sqlite3_stmt* stmt = NULL; if (sqlite3_prepare_v2(db, "INSERT OR REPLACE INTO channels(channel_id, name, owner_node_id," @@ -326,7 +337,8 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* userinfo, - const uint8_t* creator_sig) { + const uint8_t* creator_sig, + const char* adm_tags, const uint8_t* adm_tags_sig, int storage) { if (!db || !channel_id || !x25519_pubkey || !ed25519_pubkey) return -1; char peers_tbl[80]; peers_table_name(channel_id, peers_tbl, sizeof(peers_tbl)); char sql[256]; @@ -349,7 +361,23 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no if (creator_sig) sqlite3_bind_blob(stmt, 9, creator_sig, PEERS_JOIN_SIG_SIZE, SQLITE_STATIC); else sqlite3_bind_null(stmt, 9); int rc = sqlite3_step(stmt); sqlite3_finalize(stmt); - return rc == SQLITE_DONE ? 0 : -1; + if (rc != SQLITE_DONE) return -1; + + if (adm_tags || adm_tags_sig || storage > 0) { + snprintf(sql, sizeof(sql), + "UPDATE \"%s\" SET adm_tags=?, adm_tags_sig=?, storage=? WHERE node_id=?", peers_tbl); + sqlite3_stmt* u = NULL; + if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { + if (adm_tags && adm_tags[0]) sqlite3_bind_text(u, 1, adm_tags, -1, SQLITE_STATIC); + else sqlite3_bind_null(u, 1); + if (adm_tags_sig) sqlite3_bind_blob(u, 2, adm_tags_sig, 64, SQLITE_STATIC); + else sqlite3_bind_null(u, 2); + sqlite3_bind_int(u, 3, storage); + sqlite3_bind_int64(u, 4, (sqlite3_int64)node_id); + sqlite3_step(u); sqlite3_finalize(u); + } + } + return 0; } int topo_node_sqlite_node_update_verified(sqlite3* db, uint64_t node_id, @@ -440,7 +468,7 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, char sql[512]; snprintf(sql, sizeof(sql), "SELECT node_id, x25519_pubkey, ed25519_pubkey," - " join_sig, join_ts, update_sig, update_ts, userinfo" + " join_sig, join_ts, update_sig, update_ts, userinfo, adm_tags, adm_tags_sig" " FROM \"%s\" ORDER BY node_id ASC", peers_tbl); if (sqlite3_prepare_v2(db, sql, -1, &stmt, NULL) != SQLITE_OK) return -1; @@ -458,8 +486,11 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, const uint8_t* update_sig = (const uint8_t*)sqlite3_column_blob(stmt, 5); uint64_t update_ts = (uint64_t)sqlite3_column_int64(stmt, 6); const char* puserinfo = (const char*)sqlite3_column_text(stmt, 7); + const char* adm_tags_s = (const char*)sqlite3_column_text(stmt, 8); + const uint8_t* adm_tags_sig_b = (const uint8_t*)sqlite3_column_blob(stmt, 9); if (!x25519 || !ed_pub) continue; uint8_t nl = puserinfo ? (uint8_t)strnlen(puserinfo, 255) : 0; + uint8_t atl = adm_tags_s ? (uint8_t)strnlen(adm_tags_s, 255) : 0; sqlite3_stmt* ast = NULL; snprintf(sql, sizeof(sql), @@ -488,7 +519,7 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, sqlite3_finalize(ast); uint8_t flags = (join_sig && join_ts) ? PEERS_FLAG_HAS_JOIN : 0; - size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1; + size_t peer_needed = 8 + 32 + 32 + 1 + (flags & PEERS_FLAG_HAS_JOIN ? 64+8 : 0) + 64 + 8 + 1 + (size_t)nl + 1 + (size_t)atl + 64 + 1; size_t addr_sz = off - addr_start; if (addr_cnt > 0) { if (off + peer_needed > buf_sz) { sqlite3_finalize(stmt); return -2; } @@ -516,6 +547,10 @@ int topo_node_sqlite_channel_peers_all(sqlite3* db, const char* channel_id, buf[off++] = nl; if (nl) { memcpy(buf + off, puserinfo, nl); off += nl; } buf[off++] = addr_cnt; + buf[off++] = atl; + if (atl) { memcpy(buf + off, adm_tags_s, atl); off += atl; } + if (adm_tags_sig_b && sqlite3_column_bytes(stmt, 9) >= 64) { memcpy(buf + off, adm_tags_sig_b, 64); off += 64; } + else { memset(buf + off, 0, 64); off += 64; } off += addr_sz; (*cnt_ptr)++; } @@ -593,7 +628,7 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) { while (sqlite3_step(s) == SQLITE_ROW) { const char* tbl = (const char*)sqlite3_column_text(s, 0); - int node_type = base_type; + int node_type = base_type; int is_storage = 0; if (best_at == ADDR_TYPE_DIRECT && base_type == 1) { snprintf(sql, sizeof(sql), "SELECT adm_tags FROM \"%s\" WHERE node_id=?", tbl); @@ -602,18 +637,22 @@ int topo_node_sqlite_nodeinfo_updated(sqlite3* db, uint64_t node_id) { sqlite3_bind_int64(a, 1, (sqlite3_int64)node_id); if (sqlite3_step(a) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(a, 0); - if (tags && strstr(tags, "supernode=yes")) node_type = 4; + if (tags) { + if (strstr(tags, "supernode=yes")) node_type = 4; + if (strstr(tags, "storage=yes")) is_storage = 1; + } } sqlite3_finalize(a); } } snprintf(sql, sizeof(sql), - "UPDATE \"%s\" SET node_type=? WHERE node_id=?", tbl); + "UPDATE \"%s\" SET node_type=?, storage=? WHERE node_id=?", tbl); sqlite3_stmt* u = NULL; if (sqlite3_prepare_v2(db, sql, -1, &u, NULL) == SQLITE_OK) { sqlite3_bind_int(u, 1, node_type); - sqlite3_bind_int64(u, 2, (sqlite3_int64)node_id); + sqlite3_bind_int(u, 2, is_storage); + sqlite3_bind_int64(u, 3, (sqlite3_int64)node_id); sqlite3_step(u); sqlite3_finalize(u); } } diff --git a/src/routing_layer/topo_node_sqlite.h b/src/routing_layer/topo_node_sqlite.h index 6a20082f..b4c21d2a 100644 --- a/src/routing_layer/topo_node_sqlite.h +++ b/src/routing_layer/topo_node_sqlite.h @@ -30,7 +30,8 @@ int topo_node_sqlite_member_put(sqlite3* db, const char* channel_id, uint64_t no const uint8_t* join_sig, uint64_t join_ts, const uint8_t* update_sig, uint64_t update_ts, const uint8_t* x25519_pubkey, const uint8_t* ed25519_pubkey, const char* userinfo, - const uint8_t* creator_sig); + const uint8_t* creator_sig, + const char* adm_tags, const uint8_t* adm_tags_sig, int storage); int topo_node_sqlite_member_get_join(sqlite3* db, const char* channel_id, uint64_t node_id, uint8_t* join_sig_out, uint64_t* join_ts_out); diff --git a/tools/chatgui/CMakeLists.txt b/tools/chatgui/CMakeLists.txt index 1eac1646..f42701e1 100644 --- a/tools/chatgui/CMakeLists.txt +++ b/tools/chatgui/CMakeLists.txt @@ -70,9 +70,12 @@ if(NOT FFMPEG_FOUND) endif() endif() if(NOT FFMPEG_FOUND) - message(FATAL_ERROR "FFmpeg not found at ${FFMPEG_INSTALL_DIR}. Run: cd ffmpeg && ./build_ffmpeg.sh") + message(WARNING "FFmpeg not found at ${FFMPEG_INSTALL_DIR}. Video transcoding disabled. Run ./ffmpeg/build_ffmpeg.sh to enable.") + set(FFMPEG_TARGET "") + set(FFMPEG_LIBRARIES "") + set(FFMPEG_INCLUDE_DIRS "") endif() -if(UNIX AND NOT APPLE) +if(UNIX AND NOT APPLE AND FFMPEG_FOUND) set(CMAKE_BUILD_RPATH "${FFMPEG_INSTALL_DIR}/lib") set(CMAKE_INSTALL_RPATH "${FFMPEG_INSTALL_DIR}/lib") endif() diff --git a/tools/chatgui/ffmpeg/.gitignore b/tools/chatgui/ffmpeg/.gitignore new file mode 100644 index 00000000..5ea79f89 --- /dev/null +++ b/tools/chatgui/ffmpeg/.gitignore @@ -0,0 +1 @@ +/ffmpeg_build/ diff --git a/tools/chatgui/ffmpeg/build_ffmpeg.sh b/tools/chatgui/ffmpeg/build_ffmpeg.sh new file mode 100755 index 00000000..c64b5bdf --- /dev/null +++ b/tools/chatgui/ffmpeg/build_ffmpeg.sh @@ -0,0 +1,269 @@ +#!/bin/bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +BUILD_DIR="$SCRIPT_DIR/ffmpeg_build" +SRC_DIR="$BUILD_DIR/src" +INSTALL_DIR="$BUILD_DIR/install" +JOBS=$(nproc 2>/dev/null || echo 4) + +case "$(uname -s)" in + Linux) PLATFORM=linux ;; + MINGW*|MSYS*|CYGWIN*) PLATFORM=windows ;; + *) echo "Unsupported platform: $(uname -s)"; exit 1 ;; +esac + +log() { echo "[FFmpeg] $1"; } +die() { echo "[FFmpeg] ERROR: $1" >&2; exit 1; } +check_cmd() { command -v "$1" >/dev/null 2>&1 || die "$1 not found. Install it first."; } + +do_clone() { + local name="$1" url="$2" tag="$3" + if [ ! -d "$SRC_DIR/$name" ]; then + log "Clone $name..." + for i in 1 2 3; do + git clone --depth 1 --branch "$tag" "$url" "$SRC_DIR/$name" 2>&1 && break + log "Clone attempt $i failed, retrying..." + rm -rf "$SRC_DIR/$name" + sleep 2 + done + [ -d "$SRC_DIR/$name/.git" ] || die "Failed to clone $name after 3 attempts" + else + log "Skip clone $name: already exists" + fi +} + +# ============================================================================ +# Step 0: System dependencies +# ============================================================================ +if [ "$PLATFORM" = "linux" ]; then + MISSING="" + for pkg in build-essential cmake nasm pkg-config meson ninja-build git; do + dpkg -s "$pkg" >/dev/null 2>&1 || MISSING="$MISSING $pkg" + done + if [ -n "$MISSING" ]; then + log "Installing build tools:$MISSING" + sudo apt update -qq && sudo apt install -y $MISSING + fi + HW_DEPS="libva-dev libvdpau-dev" + for pkg in $HW_DEPS; do + dpkg -s "$pkg" >/dev/null 2>&1 || { log "Installing $pkg (HW accel)"; sudo apt install -y $pkg; } + done +else + check_cmd pacman + for pkg in mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninja mingw-w64-ucrt-x86_64-nasm \ + mingw-w64-ucrt-x86_64-pkg-config mingw-w64-ucrt-x86_64-meson; do + pacman -Qi "$pkg" >/dev/null 2>&1 || { log "Installing $pkg"; pacman -S --noconfirm "$pkg"; } + done +fi + +check_cmd cmake +check_cmd nasm +check_cmd pkg-config +check_cmd meson +check_cmd git + +# ============================================================================ +# Step 1: Codec libraries (static .a, built with -fPIC) +# ============================================================================ +mkdir -p "$SRC_DIR" "$INSTALL_DIR/lib/pkgconfig" + +export PKG_CONFIG_PATH="$INSTALL_DIR/lib/pkgconfig${PKG_CONFIG_PATH:+:$PKG_CONFIG_PATH}" + +# --- x264 (H.264 encoder) --- +if [ ! -f "$INSTALL_DIR/lib/pkgconfig/x264.pc" ]; then + do_clone x264 https://code.videolan.org/videolan/x264.git stable + cd "$SRC_DIR/x264" + log "Configure x264..." + ./configure --prefix="$INSTALL_DIR" --enable-static --disable-shared \ + --disable-opencl --disable-avs --disable-swscale --disable-lavf \ + --disable-ffms --disable-gpac --disable-lsmash \ + --extra-cflags="-fPIC" --disable-cli + log "Build x264..." + make -j"$JOBS" + make install + log "x264 OK" +else + log "Skip x264: already built" +fi + +# --- x265 (H.265/HEVC encoder) --- +if [ ! -f "$INSTALL_DIR/lib/pkgconfig/x265.pc" ]; then + do_clone x265 https://bitbucket.org/multicoreware/x265_git.git master + cd "$SRC_DIR/x265" + mkdir -p build && cd build + log "Configure x265..." + cmake ../source -G "Unix Makefiles" \ + -DCMAKE_INSTALL_PREFIX="$INSTALL_DIR" \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_POSITION_INDEPENDENT_CODE=ON \ + -DENABLE_CLI=OFF -DENABLE_SHARED=OFF \ + -DHIGH_BIT_DEPTH=OFF -DENABLE_HDR10_PLUS=OFF + log "Build x265..." + cmake --build . -j"$JOBS" + cmake --install . + cat > "$INSTALL_DIR/lib/pkgconfig/x265.pc" <&1 && break + log "Download from $url failed, trying next..." + rm -f "$FFMPEG_TARBALL" + done + [ -f "$FFMPEG_TARBALL" ] || die "Failed to download FFmpeg tarball" + log "Download OK ($(du -h "$FFMPEG_TARBALL" | cut -f1))" + fi + log "Extract FFmpeg..." + mkdir -p "$FFMPEG_SRC" + tar -xf "$FFMPEG_TARBALL" -C "$FFMPEG_SRC" --strip-components=1 + rm -f "$FFMPEG_TARBALL" +fi + +cd "$SRC_DIR/ffmpeg" +log "Configure FFmpeg..." + +CONFIGURE_ARGS=( + --prefix="$INSTALL_DIR" + --enable-shared --disable-static + --enable-gpl --enable-version3 + --pkg-config-flags=--static + --extra-cflags="-I$INSTALL_DIR/include" + --extra-ldflags="-L$INSTALL_DIR/lib" + --extra-libs="-lpthread -lm -ldl" + --enable-libx264 --enable-libx265 + --enable-libvpx --enable-libsvtav1 --enable-libaom + --enable-libdav1d + --enable-nvenc --enable-cuda --enable-cuvid + --enable-vaapi --enable-vdpau + --disable-programs --disable-doc --disable-avdevice + --disable-ffprobe --disable-ffplay --disable-ffmpeg + --disable-debug --enable-optimizations +) + +if [ "$PLATFORM" = "windows" ]; then + CONFIGURE_ARGS+=( + --enable-d3d11va --enable-dxva2 + ) +fi + +"./configure" "${CONFIGURE_ARGS[@]}" + +log "Build FFmpeg..." +make -j"$JOBS" +make install + +log "===============================================================" +log "FFmpeg build SUCCESS" +log "Install prefix: $INSTALL_DIR" +log "Libraries: $(ls "$INSTALL_DIR/lib/"*.so* "$INSTALL_DIR/lib/"*.dll.a "$INSTALL_DIR/bin/"*.dll 2>/dev/null | tr '\n' ' ')" +log "===============================================================" diff --git a/tools/chatgui/src/memberpropsdialog.cpp b/tools/chatgui/src/memberpropsdialog.cpp index f8973880..f51f7ea3 100644 --- a/tools/chatgui/src/memberpropsdialog.cpp +++ b/tools/chatgui/src/memberpropsdialog.cpp @@ -31,8 +31,9 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, bool haveKey = (m_edPrivkey.size() == 32); bool isOwn = (m_nodeId == m_db->myNodeId()); - bool isAdmin = false, isSuper = false; + bool isAdmin = false, isSuper = false, isStorage = false; QString curNick; + m_currentVer = 0; if (m_db->m_db) { QString tbl = m_db->peersTableName(channelId); QByteArray sql = QString( @@ -44,8 +45,11 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, if (sqlite3_step(st) == SQLITE_ROW) { const char* tags = (const char*)sqlite3_column_text(st, 0); if (tags) { + const char* vp = strstr(tags, "ver="); + if (vp) m_currentVer = atoi(vp + 4); if (strstr(tags, "admin=yes")) isAdmin = true; if (strstr(tags, "supernode=yes")) isSuper = true; + if (strstr(tags, "storage=yes")) isStorage = true; } curNick = DbManager::colText(st, 1); } @@ -88,6 +92,11 @@ MemberPropsDialog::MemberPropsDialog(DbManager* db, const QString& channelId, m_superCb->setEnabled(haveKey); layout->addWidget(m_superCb); + m_storageCb = new QCheckBox("Storage (priority attachment keeper)", this); + m_storageCb->setChecked(isStorage); + m_storageCb->setEnabled(haveKey); + layout->addWidget(m_storageCb); + if (!haveKey) { auto* hint = new QLabel("(Admin/Super require channel private key)", this); hint->setStyleSheet("color: gray; font-size: 11px;"); @@ -144,12 +153,11 @@ void MemberPropsDialog::onSave() { /* signed fields — only if key available */ if (m_edPrivkey.size() != 32) { accept(); return; } - QString tags; - if (m_adminCb->isChecked()) tags += "admin=yes"; - if (m_superCb->isChecked()) { - if (!tags.isEmpty()) tags += ","; - tags += "supernode=yes"; - } + int ver = m_currentVer + 1; + QString tags = QString("ver=%1").arg(ver); + if (m_adminCb->isChecked()) tags += ",admin=yes"; + if (m_superCb->isChecked()) tags += ",supernode=yes"; + if (m_storageCb->isChecked()) tags += ",storage=yes"; QByteArray msg = tags.toUtf8(); uint64_t le = htole64(m_nodeId); @@ -170,7 +178,7 @@ void MemberPropsDialog::onSave() { } QByteArray sql = QString( - "UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=? WHERE node_id=?" + "UPDATE \"%1\" SET adm_tags=?, adm_tags_sig=?, node_type=?, storage=? WHERE node_id=?" ).arg(tbl).toUtf8(); sqlite3_stmt* st = nullptr; if (sqlite3_prepare_v2(m_db->m_db, sql.constData(), -1, &st, nullptr) == SQLITE_OK) { @@ -179,7 +187,9 @@ void MemberPropsDialog::onSave() { sqlite3_bind_blob(st, 2, sig, 64, SQLITE_TRANSIENT); int nt = m_superCb->isChecked() ? 4 : 1; sqlite3_bind_int(st, 3, nt); - sqlite3_bind_int64(st, 4, (sqlite3_int64)m_nodeId); + int stg = m_storageCb->isChecked() ? 1 : 0; + sqlite3_bind_int(st, 4, stg); + sqlite3_bind_int64(st, 5, (sqlite3_int64)m_nodeId); sqlite3_step(st); sqlite3_finalize(st); } diff --git a/tools/chatgui/src/memberpropsdialog.h b/tools/chatgui/src/memberpropsdialog.h index bd19518b..b3dce3fe 100644 --- a/tools/chatgui/src/memberpropsdialog.h +++ b/tools/chatgui/src/memberpropsdialog.h @@ -22,8 +22,10 @@ private: QString m_channelId; quint64 m_nodeId; QByteArray m_edPrivkey; + int m_currentVer; QLineEdit* m_nickEdit; QCheckBox* m_adminCb; QCheckBox* m_superCb; + QCheckBox* m_storageCb; };