546 lines
34 KiB
C
546 lines
34 KiB
C
/*
|
|
* chat_profile.c — профиль узла: имя, адреса, ui_state
|
|
*
|
|
* Вынесено из chat_core.c для уменьшения размера модуля.
|
|
*/
|
|
|
|
#include "chat_core_priv.h"
|
|
#include "chat_event.h"
|
|
#include "chat_member.h"
|
|
#include "chat_local.h"
|
|
#include "../../lib/json_flat.h"
|
|
#include "../routing_layer/topo_node_sqlite.h"
|
|
#include "../routing_layer/topo_node.h"
|
|
#include "member_sync.h"
|
|
#include "../member_files/member_files.h"
|
|
|
|
#include "../utun_instance.h"
|
|
#include "../transport_layer/etcp.h"
|
|
#include "../transport_layer/etcp_connections.h"
|
|
#include "../transport_layer/secure_channel.h"
|
|
#include "../ntp_time.h"
|
|
#include "../../lib/u_async.h"
|
|
#include "../../lib/mem.h"
|
|
#include "../../lib/platform_compat.h"
|
|
|
|
#include <openssl/evp.h>
|
|
#include <limits.h>
|
|
|
|
void chat_core_update_my_name_trampoline(void* arg) {
|
|
struct update_my_name_arg* a = (struct update_my_name_arg*)arg;
|
|
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: null request"); return; }
|
|
if (!memchr(a->name, 0, sizeof(a->name))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: unterminated request");
|
|
else chat_core_update_my_name(a->inst, a->name);
|
|
u_free(arg);
|
|
}
|
|
|
|
/* ── Обновление своего мембера (userinfo + update_ts + подпись + синк).
|
|
Адреса больше не здесь — их синхронизирует BGP/topo_group. ── */
|
|
|
|
struct profile_json {
|
|
char text[MS_USERINFO_MAX + 1];
|
|
size_t used;
|
|
int avatar;
|
|
const struct chat_profile_option* options;
|
|
size_t option_count;
|
|
};
|
|
|
|
static int profile_string(struct profile_json* p, const char* text) {
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = '"';
|
|
for (const unsigned char* c = (const unsigned char*)text; *c; c++) {
|
|
char encoded[7]; size_t bytes = 1; encoded[0] = (char)*c;
|
|
if (*c == '"' || *c == '\\') { encoded[0] = '\\'; encoded[1] = (char)*c; bytes = 2; }
|
|
else if (*c < 32) { snprintf(encoded, sizeof(encoded), "\\u%04x", *c); bytes = 6; }
|
|
if (p->used + bytes >= MS_USERINFO_MAX) return -1;
|
|
memcpy(p->text + p->used, encoded, bytes); p->used += bytes;
|
|
}
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = '"'; p->text[p->used] = 0; return 0;
|
|
}
|
|
|
|
static int profile_pair(struct profile_json* p, const char* key, const char* value) {
|
|
if (p->used >= MS_USERINFO_MAX) return -1;
|
|
if (p->used > 1) p->text[p->used++] = ',';
|
|
if (profile_string(p, key) < 0 || p->used >= MS_USERINFO_MAX) return -1;
|
|
p->text[p->used++] = ':';
|
|
return profile_string(p, value);
|
|
}
|
|
|
|
static int profile_copy_pair(const char* key, const char* value, void* arg) {
|
|
struct profile_json* p = arg;
|
|
if (!strcmp(key, "name") || (p->avatar && (!strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")))) return 0;
|
|
for (size_t i = 0; i < p->option_count; i++) if (!strcmp(key, p->options[i].key)) return 0;
|
|
return profile_pair(p, key, value);
|
|
}
|
|
|
|
int chat_core_name_userinfo(const char* name, char* out, size_t capacity) {
|
|
struct profile_json p = { .text = "{", .used = 1 };
|
|
if (!name || !out || profile_pair(&p, "name", name) < 0 || p.used + 2 > capacity || p.used >= MS_USERINFO_MAX) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot encode display name capacity=%zu", capacity); return -1;
|
|
}
|
|
p.text[p.used++] = '}'; p.text[p.used] = 0; memcpy(out, p.text, p.used + 1); return 0;
|
|
}
|
|
|
|
/* Локальная запись member_sync доверяет caller:перед правкой проверяем прежний JOIN явно. */
|
|
static int profile_verify_join(struct UTUN_INSTANCE* inst, const char* channel, const uint8_t join[64], uint64_t timestamp) {
|
|
uint8_t x[32], ed[32], body[112];
|
|
if (topo_node_sqlite_channel_get(CC(inst)->db, channel, NULL, 0, NULL, x, ed, NULL)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: channel keys unavailable ch=%s", channel); return -1;
|
|
}
|
|
int bytes = member_sync_build_join_msg(x, ed, inst->node_id, inst->my_keys.public_key, timestamp, body, sizeof(body));
|
|
if (bytes < 0 || sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)bytes, join) != SC_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored JOIN invalid ch=%s", channel); return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int chat_core_get_my_profile(struct UTUN_INSTANCE* inst, const char* channel, char* out, size_t capacity,
|
|
uint8_t signature[64], uint64_t* timestamp) {
|
|
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
|
if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !chat_channel_id(channel) ||
|
|
!out || !capacity || !signature || !timestamp) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid read request"); return -1;
|
|
}
|
|
char table[80], sql[256]; peers_table_name(channel, table, sizeof(table));
|
|
snprintf(sql, sizeof(sql), "SELECT userinfo,update_sig,update_ts,join_sig,ed25519_pubkey,source,join_ts FROM \"%s\" WHERE node_id=?", table);
|
|
sqlite3_stmt* st = NULL; const char* stage = "prepare";
|
|
char profile[MS_USERINFO_MAX + 1]; uint8_t update[64], join[64], body[MS_USERINFO_MAX + 73]; uint64_t version, join_ts;
|
|
int rc = sqlite3_prepare_v2(cc->db, sql, -1, &st, NULL);
|
|
if (rc == SQLITE_OK) { stage = "bind"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)cc->my_node_id); }
|
|
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
|
|
if (rc != SQLITE_ROW) goto error;
|
|
stage = "fields"; rc = SQLITE_CORRUPT;
|
|
int json_type = sqlite3_column_type(st, 0);
|
|
int bytes = sqlite3_column_bytes(st, 0); const char* json = (const char*)sqlite3_column_text(st, 0);
|
|
const void *sig = sqlite3_column_blob(st, 1), *joined = sqlite3_column_blob(st, 3), *key = sqlite3_column_blob(st, 4);
|
|
if (json_type != SQLITE_TEXT || !json || bytes > MS_USERINFO_MAX || strlen(json) != (size_t)bytes ||
|
|
sqlite3_column_type(st, 1) != SQLITE_BLOB || sqlite3_column_bytes(st, 1) != 64 || !sig ||
|
|
sqlite3_column_type(st, 3) != SQLITE_BLOB || sqlite3_column_bytes(st, 3) != 64 || !joined ||
|
|
sqlite3_column_type(st, 4) != SQLITE_BLOB || sqlite3_column_bytes(st, 4) != 32 || !key || memcmp(key, inst->my_ed25519_pubkey, 32) ||
|
|
sqlite3_column_type(st, 2) != SQLITE_INTEGER || sqlite3_column_int64(st, 2) <= 0 || sqlite3_column_int(st, 5) ||
|
|
sqlite3_column_type(st, 6) != SQLITE_INTEGER || sqlite3_column_int64(st, 6) <= 0) goto error;
|
|
if ((size_t)bytes >= capacity) { stage = "capacity"; rc = SQLITE_TOOBIG; goto error; }
|
|
memcpy(profile, json, (size_t)bytes + 1); memcpy(update, sig, 64); memcpy(join, joined, 64);
|
|
version = (uint64_t)sqlite3_column_int64(st, 2); join_ts = (uint64_t)sqlite3_column_int64(st, 6);
|
|
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
|
|
int length = member_sync_build_update_msg(join, version, profile, body, sizeof(body));
|
|
if (length < 0 || profile_verify_join(inst, channel, join, join_ts) ||
|
|
sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)length, update) != SC_OK ||
|
|
chat_json_strings(cc->db, profile, MS_USERINFO_MAX, NULL, NULL)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored signature/JSON invalid channel=%s", channel); return -1;
|
|
}
|
|
memcpy(out, profile, (size_t)bytes + 1); memcpy(signature, update, 64); *timestamp = version; return 0;
|
|
error:
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read channel=%s stage=%s rc=%d capacity=%zu: %s", channel, stage, rc, capacity, sqlite3_errmsg(cc->db));
|
|
rc = sqlite3_finalize(st);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: read cleanup finalize rc=%d", rc);
|
|
return -1;
|
|
}
|
|
|
|
struct profile_current {
|
|
uint8_t join[64], signature[64];
|
|
uint64_t join_ts, version;
|
|
char json[MS_USERINFO_MAX + 1];
|
|
int signed_profile;
|
|
};
|
|
|
|
/* 1 — свой полный member,0 — отсутствует/placeholder,-1 — ошибка. Cursor закрыт до подписи/изменения. */
|
|
static int profile_read_current(struct UTUN_INSTANCE* inst, const char* channel, struct profile_current* out) {
|
|
sqlite3* db = CC(inst)->db; char table[80], sql[384]; peers_table_name(channel, table, sizeof(table));
|
|
snprintf(sql, sizeof(sql), "SELECT join_sig,join_ts,update_ts,userinfo,update_sig,x25519_pubkey,ed25519_pubkey,source"
|
|
" FROM \"%s\" WHERE node_id=?", table);
|
|
sqlite3_stmt* st = NULL; const char* stage = "prepare"; int found = 0;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
|
|
if (rc == SQLITE_OK) { stage = "bind"; rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id); }
|
|
if (rc == SQLITE_OK) { stage = "step"; rc = sqlite3_step(st); }
|
|
if (rc == SQLITE_DONE) goto done;
|
|
if (rc != SQLITE_ROW) goto error;
|
|
if (sqlite3_column_int(st, 7)) goto done;
|
|
stage = "fields"; rc = SQLITE_CORRUPT;
|
|
int json_type = sqlite3_column_type(st, 3);
|
|
int bytes = sqlite3_column_bytes(st, 3), sig_bytes = sqlite3_column_bytes(st, 4);
|
|
const char* json = (const char*)sqlite3_column_text(st, 3);
|
|
const void *join = sqlite3_column_blob(st, 0), *signature = sqlite3_column_blob(st, 4);
|
|
const void *x = sqlite3_column_blob(st, 5), *ed = sqlite3_column_blob(st, 6);
|
|
if (sqlite3_column_type(st, 0) != SQLITE_BLOB || sqlite3_column_bytes(st, 0) != 64 || !join ||
|
|
sqlite3_column_type(st, 1) != SQLITE_INTEGER || sqlite3_column_int64(st, 1) <= 0 ||
|
|
sqlite3_column_type(st, 2) != SQLITE_INTEGER || sqlite3_column_int64(st, 2) < 0 ||
|
|
json_type != SQLITE_TEXT || !json || bytes > MS_USERINFO_MAX || strlen(json) != (size_t)bytes ||
|
|
sqlite3_column_type(st, 5) != SQLITE_BLOB || sqlite3_column_bytes(st, 5) != 32 || !x ||
|
|
sqlite3_column_type(st, 6) != SQLITE_BLOB || sqlite3_column_bytes(st, 6) != 32 || !ed ||
|
|
memcmp(x, inst->my_keys.public_key, 32) || memcmp(ed, inst->my_ed25519_pubkey, 32)) goto error;
|
|
out->join_ts = (uint64_t)sqlite3_column_int64(st, 1); out->version = (uint64_t)sqlite3_column_int64(st, 2);
|
|
if (out->version) {
|
|
if (sqlite3_column_type(st, 4) != SQLITE_BLOB || sig_bytes != 64 || !signature) goto error;
|
|
memcpy(out->signature, signature, 64); out->signed_profile = 1;
|
|
} else if (sig_bytes && (sig_bytes != 64 || !signature || memcmp(signature, (uint8_t[64]){0}, 64))) goto error;
|
|
memcpy(out->json, json, (size_t)bytes + 1); memcpy(out->join, join, 64); found = 1;
|
|
done:
|
|
stage = "finalize"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
|
|
if (found && profile_verify_join(inst, channel, out->join, out->join_ts)) return -1;
|
|
if (found && out->signed_profile) {
|
|
uint8_t body[MS_USERINFO_MAX + 73];
|
|
int bytes = member_sync_build_update_msg(out->join, out->version, out->json, body, sizeof(body));
|
|
if (bytes < 0 || sc_ed25519_verify(inst->my_ed25519_pubkey, body, (size_t)bytes, out->signature) != SC_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: stored signature invalid channel=%s", channel); return -1;
|
|
}
|
|
}
|
|
return found;
|
|
error:
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: current record ch=%s stage=%s rc=%d: %s", channel, stage, rc, sqlite3_errmsg(db));
|
|
rc = sqlite3_finalize(st);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: current record cleanup finalize rc=%d", rc);
|
|
return -1;
|
|
}
|
|
|
|
/* Одинаковые поля не меняют версию из-за порядка ключей или способа JSON escaping. */
|
|
static int profile_equal(sqlite3* db, const char* a, const char* b) {
|
|
sqlite3_stmt* st = NULL;
|
|
int rc = sqlite3_prepare_v2(db, "SELECT NOT EXISTS(SELECT key,value FROM json_each(?1) EXCEPT SELECT key,value FROM json_each(?2))"
|
|
" AND NOT EXISTS(SELECT key,value FROM json_each(?2) EXCEPT SELECT key,value FROM json_each(?1))", -1, &st, NULL);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, a, -1, SQLITE_STATIC);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 2, b, -1, SQLITE_STATIC);
|
|
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
|
int same = rc == SQLITE_ROW && sqlite3_column_int(st, 0), final = sqlite3_finalize(st);
|
|
if (rc != SQLITE_ROW || final != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: comparison rc=%d finalize=%d: %s", rc, final, sqlite3_errmsg(db)); return -1;
|
|
}
|
|
return same;
|
|
}
|
|
|
|
/* Под транзакцией caller:commit-флаги member_sync,-2 — нет полного member,-1 — ошибка;без callbacks. */
|
|
static int profile_update_locked(struct UTUN_INSTANCE* inst, const char* ch, const char* name, int avatar, uint64_t server, const char* file,
|
|
const struct chat_profile_option* options, size_t option_count) {
|
|
struct profile_current current = {0};
|
|
int found = profile_read_current(inst, ch, ¤t);
|
|
if (found <= 0) {
|
|
if (!found) DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: own full member unavailable channel=%s", ch);
|
|
return found ? -1 : -2;
|
|
}
|
|
if (avatar && server && !member_files_member(inst, strtoull(ch, NULL, 10), server, 1)) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: avatar server has no storage role channel=%s", ch); return -1;
|
|
}
|
|
struct profile_json profile = {.text = "{", .used = 1, .avatar = avatar, .options = options, .option_count = option_count};
|
|
if (profile_pair(&profile, "name", name) < 0 ||
|
|
(current.signed_profile && chat_json_strings(CC(inst)->db, current.json, MS_USERINFO_MAX, profile_copy_pair, &profile))) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: cannot preserve/encode signed attributes channel=%s", ch); return -1;
|
|
}
|
|
for (size_t i = 0; i < option_count; i++) if (options[i].value && profile_pair(&profile, options[i].key, options[i].value) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: option exceeds limit channel=%s index=%zu", ch, i); return -1;
|
|
}
|
|
if (avatar && server) {
|
|
char id[17]; snprintf(id, sizeof(id), "%016llx", (unsigned long long)server);
|
|
if (profile_pair(&profile, "avatar_server", id) < 0 || profile_pair(&profile, "avatar_file", file) < 0) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: avatar exceeds limit channel=%s", ch); return -1;
|
|
}
|
|
}
|
|
if (profile.used >= MS_USERINFO_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: attributes exceed limit channel=%s", ch); return -1; }
|
|
profile.text[profile.used++] = '}'; profile.text[profile.used] = 0;
|
|
if (current.signed_profile) {
|
|
int equal = profile_equal(CC(inst)->db, current.json, profile.text);
|
|
if (equal < 0) return -1;
|
|
if (equal) { DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: unchanged channel=%s ts=%llu", ch, (unsigned long long)current.version); return 0; }
|
|
}
|
|
if (current.version == INT64_MAX) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: version exhausted channel=%s", ch); return -1; }
|
|
time_t seconds = ntp_time_get_seconds(inst); uint64_t now = seconds > 0 ? (uint64_t)seconds : 0;
|
|
uint64_t version = now > current.version ? now : current.version + 1;
|
|
uint8_t body[MS_USERINFO_MAX + 73], signature[64];
|
|
int bytes = member_sync_build_update_msg(current.join, version, profile.text, body, sizeof(body));
|
|
if (bytes < 0 || sc_ed25519_sign(inst->my_ed25519_privkey, body, (size_t)bytes, signature) != SC_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: signing failed channel=%s", ch); return -1;
|
|
}
|
|
int changes = member_sync_put(inst, ch, inst->node_id, inst->my_keys.public_key, inst->my_ed25519_pubkey,
|
|
current.join, current.join_ts, signature, version, profile.text, NULL, NULL, 0, 0, NULL);
|
|
if (changes < 0 || !(changes & MS_APPLY_CHANGED)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: new version not saved channel=%s flags=%d", ch, changes); return -1;
|
|
}
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "profile: staged channel=%s ts=%llu bytes=%zu", ch, (unsigned long long)version, profile.used);
|
|
return changes;
|
|
}
|
|
|
|
static int profile_update_channel(struct UTUN_INSTANCE* inst, const char* channel, int avatar, uint64_t server, const char* file,
|
|
const struct chat_profile_option* options, size_t option_count) {
|
|
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
|
if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !chat_channel_id(channel) ||
|
|
!sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: inactive core/invalid channel/external transaction"); return -1;
|
|
}
|
|
char ch[20]; memcpy(ch, channel, strlen(channel) + 1);
|
|
int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* profile */", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: begin ch=%s rc=%d: %s", ch, rc, sqlite3_errmsg(cc->db)); return -1; }
|
|
int changes = profile_update_locked(inst, ch, inst->name, avatar, server, file, options, option_count);
|
|
if (changes > 0) {
|
|
rc = sqlite3_exec(cc->db, "COMMIT /* profile */", NULL, NULL, NULL);
|
|
if (rc == SQLITE_OK) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_MEMBER_SYNC, "profile: committed ch=%s node=%016llx avatar=%d",
|
|
ch, (unsigned long long)inst->node_id, avatar);
|
|
member_sync_record_committed(inst, ch, inst->node_id, (unsigned)changes); return 0;
|
|
}
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: commit ch=%s rc=%d: %s", ch, rc, sqlite3_errmsg(cc->db)); changes = -1;
|
|
}
|
|
if (!sqlite3_get_autocommit(cc->db)) {
|
|
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: rollback ch=%s rc=%d: %s", ch, rc, sqlite3_errmsg(cc->db)); changes = -1; }
|
|
if (!sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: transaction remains open ch=%s;requesting owner stop/DB close", ch);
|
|
utun_instance_stop(inst); changes = -1;
|
|
}
|
|
}
|
|
return changes < 0 ? -1 : 0;
|
|
}
|
|
|
|
/* Прочитать оба локальных представления;bit0/bit1 означают необходимость записи identity/nodes. */
|
|
static int profile_name_changes(struct UTUN_INSTANCE* inst, const char* name) {
|
|
sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL; int result = 0;
|
|
int rc = sqlite3_prepare_v2(db, "SELECT local_identity.name,nodes.name FROM local_identity"
|
|
" JOIN nodes ON nodes.node_id=local_identity.node_id WHERE id=1 AND local_identity.node_id=?", -1, &st, NULL);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)inst->node_id);
|
|
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
|
if (rc == SQLITE_ROW) for (unsigned i = 0; i < 2; i++) {
|
|
int type = sqlite3_column_type(st, i), length = sqlite3_column_bytes(st, i);
|
|
const char* saved = (const char*)sqlite3_column_text(st, i);
|
|
if (type != SQLITE_TEXT || !saved || length >= (int)sizeof(inst->name) || strlen(saved) != (size_t)length) {
|
|
rc = SQLITE_CORRUPT; break;
|
|
}
|
|
if (strcmp(saved, name)) result |= 1 << i;
|
|
}
|
|
int final = sqlite3_finalize(st);
|
|
if (rc != SQLITE_ROW || final != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: stored names rc=%d finalize=%d: %s", rc, final, sqlite3_errmsg(db)); return -1;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/* Запись под транзакцией rename;changes/finalize проверяются до общего commit. */
|
|
static int profile_name_write(struct UTUN_INSTANCE* inst, const char* sql, const char* name, sqlite3_int64 now) {
|
|
sqlite3* db = CC(inst)->db; sqlite3_stmt* st = NULL;
|
|
int rc = sqlite3_prepare_v2(db, sql, -1, &st, NULL);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, name, -1, SQLITE_STATIC);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 2, now);
|
|
if (rc == SQLITE_OK) rc = sqlite3_bind_int64(st, 3, (sqlite3_int64)inst->node_id);
|
|
if (rc == SQLITE_OK) rc = sqlite3_step(st);
|
|
if (rc == SQLITE_DONE && sqlite3_changes(db) != 1) rc = SQLITE_CONSTRAINT;
|
|
int final = sqlite3_finalize(st);
|
|
if (rc != SQLITE_DONE || final != SQLITE_OK) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: write rc=%d finalize=%d sql=%.80s: %s", rc, final, sql, sqlite3_errmsg(db)); return -1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/* Имя identity,подписанный NODEINFO и все свои полные member-блоки фиксируются одним commit. */
|
|
int chat_core_update_my_name(struct UTUN_INSTANCE* inst, const char* name) {
|
|
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
|
if (!cc || !cc->initialized || !cc->db || cc->my_node_id != inst->node_id || !name ||
|
|
strlen(name) >= sizeof(inst->name) || !sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: inactive context/invalid name/external transaction"); return -1;
|
|
}
|
|
char desired[sizeof(inst->name)]; memcpy(desired, name, strlen(name) + 1);
|
|
struct TOPO_NODE* node = inst->topo_groups ? topo_node_registry_find(inst->topo_groups, inst->node_id) : NULL;
|
|
if (!node || topo_node_verify(node)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: own signed node unavailable"); return -1; }
|
|
int snapshot_changed = strcmp(node->node_name ? node->node_name : "", desired) != 0;
|
|
struct TOPO_NODE candidate = *node; char* prepared_name = snapshot_changed ? u_strdup(desired) : NULL;
|
|
if (snapshot_changed && !prepared_name) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: name allocation failed"); return -1; }
|
|
char** channels = NULL; int rows = 0, columns = 0, rc, committed = 0;
|
|
unsigned* changes = NULL; const char* stage = "begin";
|
|
rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* name */", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) goto done;
|
|
stage = "stored names"; int names_changed = profile_name_changes(inst, desired);
|
|
if (names_changed < 0) goto rollback;
|
|
stage = "channel catalog";
|
|
rc = sqlite3_get_table(cc->db, "SELECT channel_id,length(CAST(channel_id AS BLOB)) FROM channels ORDER BY channel_id",
|
|
&channels, &rows, &columns, NULL);
|
|
if (rc != SQLITE_OK || (rows && columns != 2) || (size_t)rows > UINT32_MAX / sizeof(*changes)) goto rollback;
|
|
if (rows) {
|
|
changes = u_calloc((uint32_t)rows, sizeof(*changes));
|
|
if (!changes) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: channel snapshot allocation failed rows=%d", rows); goto rollback; }
|
|
}
|
|
int changed = names_changed || snapshot_changed;
|
|
stage = "member profiles";
|
|
for (int i = 0; i < rows; i++) {
|
|
const char *channel = channels[(i + 1) * columns], *length = channels[(i + 1) * columns + 1];
|
|
if (!chat_channel_id(channel) || !length || strlen(channel) != strtoul(length, NULL, 10)) goto rollback;
|
|
int flags = profile_update_locked(inst, channel, desired, 0, 0, NULL, NULL, 0);
|
|
if (flags == -2) continue; /* JOIN ещё не создал собственного полного member. */
|
|
if (flags < 0) goto rollback;
|
|
changes[i] = (unsigned)flags; if (flags) changed = 1;
|
|
}
|
|
if (changed) {
|
|
sqlite3_int64 now = (sqlite3_int64)ntp_time_get_seconds(inst);
|
|
stage = "identity";
|
|
if (now <= 0 || ((names_changed & 1) && profile_name_write(inst,
|
|
"UPDATE local_identity SET name=?1,updated_at=?2 WHERE id=1 AND node_id=?3", desired, now))) goto rollback;
|
|
stage = "node projection";
|
|
if ((names_changed & 2) && profile_name_write(inst, "UPDATE nodes SET name=?1,"
|
|
"update_ts=CASE WHEN update_ts IS NULL OR update_ts<?2 THEN ?2 ELSE update_ts END WHERE node_id=?3", desired, now)) goto rollback;
|
|
stage = "signed node";
|
|
if (snapshot_changed) {
|
|
candidate.node_name = prepared_name; candidate.ver = (node->ver % 255) + 1;
|
|
if (topo_node_sign_self(inst, &candidate)) goto rollback;
|
|
}
|
|
stage = "commit"; rc = sqlite3_exec(cc->db, "COMMIT /* name */", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) goto rollback;
|
|
} else {
|
|
stage = "no-op rollback"; rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) goto rollback;
|
|
}
|
|
committed = 1;
|
|
if (snapshot_changed) { char* old = node->node_name; *node = candidate; prepared_name = NULL; u_free(old); }
|
|
memcpy(inst->name, desired, strlen(desired) + 1);
|
|
if (inst->local_transport) local_transport_set_name(inst->local_transport, inst->name);
|
|
DEBUG_INFO(DEBUG_CATEGORY_CHAT, "name update: %s channels=%d snapshot=%d", changed ? "committed" : "unchanged", rows, snapshot_changed);
|
|
/* NODEINFO отправляет актуальный реестр;групповые local_node/подсети не требуют замены. */
|
|
if (snapshot_changed) for (struct ll_entry* e = inst->topo_groups->group_list->head; e; e = e->next) {
|
|
struct TOPO_GROUP* group = (struct TOPO_GROUP*)e;
|
|
if (!group->local_node || !group->senders_list) continue;
|
|
for (struct ll_entry* s = group->senders_list->head; s; s = s->next) {
|
|
struct TOPO_GROUP_CONN_ITEM* peer = (struct TOPO_GROUP_CONN_ITEM*)s->data;
|
|
if (peer && peer->conn) topo_group_send_nodeinfo(group, group->local_node, peer->conn);
|
|
}
|
|
}
|
|
for (int i = 0; i < rows; i++) if (changes[i])
|
|
member_sync_record_committed(inst, channels[(i + 1) * columns], inst->node_id, changes[i]);
|
|
goto done;
|
|
rollback:
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: failed stage=%s rc=%d: %s", stage, rc, sqlite3_errmsg(cc->db));
|
|
if (!sqlite3_get_autocommit(cc->db)) {
|
|
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db));
|
|
if (!sqlite3_get_autocommit(cc->db)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: rollback incomplete;requesting owner stop"); utun_instance_stop(inst); }
|
|
}
|
|
done:
|
|
if (!committed && !strcmp(stage, "begin")) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "name update: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db));
|
|
sqlite3_free_table(channels); u_free(changes); u_free(prepared_name); return committed ? 0 : -1;
|
|
}
|
|
|
|
/* Проверить заменяемые ключи до построения/подписи нового блока мембера. */
|
|
int chat_core_set_my_profile_options(struct UTUN_INSTANCE* inst, const char* channel,
|
|
const struct chat_profile_option* options, size_t count) {
|
|
if (!inst || !channel || !channel[0] || !options || !count || count > MS_USERINFO_MAX / 4) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid options update count=%zu", count); return -1;
|
|
}
|
|
if (!chat_core_get_db(inst) || !sqlite3_get_autocommit(chat_core_get_db(inst))) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: options update requires initialized DB without outer transaction"); return -1;
|
|
}
|
|
for (size_t i = 0; i < count; i++) {
|
|
const char* key = options[i].key;
|
|
if (!key || !key[0] || strlen(key) > MS_USERINFO_MAX || (options[i].value && strlen(options[i].value) > MS_USERINFO_MAX)
|
|
|| !strcmp(key, "name") || !strcmp(key, "avatar_server") || !strcmp(key, "avatar_file")) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid/reserved option index=%zu key=%s", i, key ? key : "null"); return -1;
|
|
}
|
|
for (size_t j = 0; j < i; j++) if (!strcmp(key, options[j].key)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: duplicate option key=%s", key); return -1;
|
|
}
|
|
}
|
|
return profile_update_channel(inst, channel, 0, 0, NULL, options, count);
|
|
}
|
|
|
|
void chat_core_update_my_member(struct UTUN_INSTANCE* inst) {
|
|
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
|
if (!cc || !cc->initialized || !cc->db || !sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: refresh requires active chat without external transaction"); return;
|
|
}
|
|
char** rows = NULL; int count = 0, columns = 0;
|
|
int rc = sqlite3_get_table(cc->db, "SELECT channel_id,length(CAST(channel_id AS BLOB)) FROM channels ORDER BY channel_id",
|
|
&rows, &count, &columns, NULL);
|
|
if (rc != SQLITE_OK || (count && columns != 2)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: channel scan rc=%d: %s", rc, sqlite3_errmsg(cc->db)); sqlite3_free_table(rows); return;
|
|
}
|
|
for (int i = 0; i < count; i++) {
|
|
const char *ch = rows[(i + 1) * columns], *bytes = rows[(i + 1) * columns + 1];
|
|
if (!chat_channel_id(ch) || !bytes || strlen(ch) != strtoul(bytes, NULL, 10)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: invalid stored channel index=%d", i); continue;
|
|
}
|
|
profile_update_channel(inst, ch, 0, 0, NULL, NULL, 0);
|
|
}
|
|
sqlite3_free_table(rows);
|
|
}
|
|
|
|
int chat_core_set_my_avatar(struct UTUN_INSTANCE* inst, const char* ch_id, uint64_t server, const char* file) {
|
|
if (!inst || !ch_id || !file || (server && !mf_avatar_manifest_valid(file))) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: invalid avatar reference"); return -1;
|
|
}
|
|
if (server) {
|
|
for (int i = 0; i < 64; i++) if (!((file[i] >= '0' && file[i] <= '9') || (file[i] >= 'a' && file[i] <= 'f'))) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_CHAT, "profile: invalid avatar hash"); return -1;
|
|
}
|
|
}
|
|
return profile_update_channel(inst, ch_id, 1, server, file, NULL, 0);
|
|
}
|
|
|
|
void chat_core_set_my_avatar_trampoline(void* arg) {
|
|
struct chat_avatar_req* q = arg;
|
|
if (!q) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: null avatar request"); return; }
|
|
char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)q->group);
|
|
int rc = -1;
|
|
if (!memchr(q->file, 0, sizeof(q->file))) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "profile: unterminated avatar request");
|
|
else rc = chat_core_set_my_avatar(q->inst, ch, q->server, q->file);
|
|
uint8_t data[17]; mf_put64(data, q->id); mf_put64(data + 8, q->group); data[16] = rc ? 1 : 0;
|
|
chat_event_post(q->inst, CHAT_EVT_AVATAR_RESULT, data, sizeof(data));
|
|
u_free(q);
|
|
}
|
|
|
|
void chat_core_sync_my_addresses(struct UTUN_INSTANCE* inst) {
|
|
if (inst)
|
|
topo_node_update_my_addresses(inst);
|
|
chat_core_update_my_member(inst);
|
|
}
|
|
|
|
int chat_core_save_ui_state(struct UTUN_INSTANCE* inst, const char* key, const char* value) {
|
|
struct chat_core_ctx* cc = inst ? CC(inst) : NULL;
|
|
if (!cc || !cc->initialized || !cc->db || !key || !key[0] || !value || strlen(key) > INT_MAX || strlen(value) > INT_MAX ||
|
|
!sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: invalid context/key/value or outer transaction"); return -1;
|
|
}
|
|
int rc = sqlite3_exec(cc->db, "BEGIN IMMEDIATE /* ui_state */", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: begin rc=%d: %s", rc, sqlite3_errmsg(cc->db)); return -1; }
|
|
sqlite3_stmt* st = NULL; const char* stage = "prepare read"; int result = -1, same = 0;
|
|
rc = sqlite3_prepare_v2(cc->db, "SELECT value FROM ui_state WHERE key=?", -1, &st, NULL);
|
|
if (rc == SQLITE_OK) { stage = "bind read"; rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); }
|
|
if (rc == SQLITE_OK) { stage = "step read"; rc = sqlite3_step(st); }
|
|
if (rc == SQLITE_ROW) {
|
|
int type = sqlite3_column_type(st, 0), bytes = sqlite3_column_bytes(st, 0);
|
|
const char* text = (const char*)sqlite3_column_text(st, 0);
|
|
if (type != SQLITE_NULL && (type != SQLITE_TEXT || !text || strlen(text) != (size_t)bytes)) {
|
|
stage = "stored value"; rc = SQLITE_CORRUPT; goto error;
|
|
}
|
|
same = text && !strcmp(text, value);
|
|
} else if (rc != SQLITE_DONE) goto error;
|
|
stage = "finalize read"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
|
|
if (same) { DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "UI state: unchanged key=%s", key); result = 0; goto done; }
|
|
stage = "prepare write";
|
|
rc = sqlite3_prepare_v2(cc->db, "INSERT INTO ui_state(key,value) VALUES(?1,?2) ON CONFLICT(key) DO UPDATE SET value=excluded.value", -1, &st, NULL);
|
|
if (rc == SQLITE_OK) { stage = "bind key"; rc = sqlite3_bind_text(st, 1, key, -1, SQLITE_STATIC); }
|
|
if (rc == SQLITE_OK) { stage = "bind value"; rc = sqlite3_bind_text(st, 2, value, -1, SQLITE_STATIC); }
|
|
if (rc == SQLITE_OK) { stage = "step write"; rc = sqlite3_step(st); }
|
|
if (rc != SQLITE_DONE) goto error;
|
|
if (sqlite3_changes(cc->db) != 1) { stage = "changes != 1"; rc = SQLITE_CONSTRAINT; goto error; }
|
|
stage = "finalize write"; rc = sqlite3_finalize(st); st = NULL; if (rc != SQLITE_OK) goto error;
|
|
stage = "commit"; rc = sqlite3_exec(cc->db, "COMMIT /* ui_state */", NULL, NULL, NULL); if (rc != SQLITE_OK) goto error;
|
|
DEBUG_DEBUG(DEBUG_CATEGORY_CHAT, "UI state: committed key=%s bytes=%zu", key, strlen(value)); return 0;
|
|
error:
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: key=%s stage=%s rc=%d: %s", key, stage, rc, sqlite3_errmsg(cc->db));
|
|
done:
|
|
rc = sqlite3_finalize(st);
|
|
if (rc != SQLITE_OK) DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: cleanup finalize rc=%d", rc);
|
|
if (!sqlite3_get_autocommit(cc->db)) {
|
|
rc = sqlite3_exec(cc->db, "ROLLBACK", NULL, NULL, NULL);
|
|
if (rc != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: rollback rc=%d: %s", rc, sqlite3_errmsg(cc->db)); result = -1; }
|
|
if (!sqlite3_get_autocommit(cc->db)) {
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: transaction remains open;requesting owner stop/DB close");
|
|
utun_instance_stop(inst); result = -1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void chat_core_save_ui_state_trampoline(void* arg) {
|
|
struct save_ui_state_arg* a = (struct save_ui_state_arg*)arg;
|
|
if (!a) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: null request"); return; }
|
|
const char* end = memchr(a->data, 0, sizeof(a->data));
|
|
if (!end || end == a->data + sizeof(a->data) - 1 || !memchr(end + 1, 0, sizeof(a->data) - (size_t)(end + 1 - a->data)))
|
|
DEBUG_ERROR(DEBUG_CATEGORY_CHAT, "UI state: unterminated key/value request");
|
|
else chat_core_save_ui_state(a->inst, a->data, end + 1);
|
|
u_free(arg);
|
|
}
|