3 changed files with 271 additions and 1 deletions
@ -0,0 +1,219 @@
|
||||
#include <stdio.h> |
||||
#include <string.h> |
||||
|
||||
#include "../../lib/debug_config.h" |
||||
#include "../../lib/json_flat.h" |
||||
#include "../utun_instance.h" |
||||
#include "../routing_layer/topo_node_sqlite.h" |
||||
#include "stream_block.h" |
||||
#include "stream_member.h" |
||||
|
||||
/* Канонические IDs не проходят через JSON number и не теряют старшие биты. */ |
||||
static int member_hex(const char **text, uint64_t *value) { |
||||
const char *p = *text; |
||||
uint64_t result = 0; |
||||
for (unsigned i = 0; i < 16; i++) { |
||||
unsigned digit; |
||||
if (p[i] >= '0' && p[i] <= '9') digit = (unsigned)(p[i] - '0'); |
||||
else if (p[i] >= 'a' && p[i] <= 'f') digit = (unsigned)(p[i] - 'a') + 10; |
||||
else return STREAM_ERR_FORMAT; |
||||
result = (result << 4) | digit; |
||||
} |
||||
if (!result) return STREAM_ERR_FORMAT; |
||||
*text = p + 16; |
||||
*value = result; |
||||
return STREAM_OK; |
||||
} |
||||
|
||||
/* Десятичные параметры имеют ограничение ещё до сужения до uint8/uint16. */ |
||||
static int member_decimal(const char **text, unsigned maximum, unsigned *value) { |
||||
const char *p = *text; |
||||
unsigned result = 0; |
||||
if (*p < '0' || *p > '9' || (*p == '0' && p[1] >= '0' && p[1] <= '9')) return STREAM_ERR_FORMAT; |
||||
while (*p >= '0' && *p <= '9') { |
||||
unsigned digit = (unsigned)(*p++ - '0'); |
||||
if (result > (maximum - digit) / 10) return STREAM_ERR_FORMAT; |
||||
result = result * 10 + digit; |
||||
} |
||||
if (result > maximum) return STREAM_ERR_FORMAT; |
||||
*text = p; |
||||
*value = result; |
||||
return STREAM_OK; |
||||
} |
||||
|
||||
struct member_parse { |
||||
struct stream_member_state state; |
||||
unsigned seen; |
||||
}; |
||||
|
||||
/* Полностью проверить обе опции, в том числе повторы ключей и порядок списка. */ |
||||
static int member_option(const char *key, const char *value, void *arg) { |
||||
struct member_parse *parse = arg; |
||||
if (!strcmp(key, STREAM_MEMBER_SOURCE)) { |
||||
if (parse->seen & 1) return -1; |
||||
parse->seen |= 1; |
||||
if (!value[0]) return 0; |
||||
const char *p = value; |
||||
unsigned n, r, size; |
||||
if (member_hex(&p, &parse->state.source.source_epoch) != STREAM_OK || *p++ != ':' |
||||
|| member_decimal(&p, 255, &n) != STREAM_OK || *p++ != ':' |
||||
|| member_decimal(&p, 254, &r) != STREAM_OK || *p++ != ':' |
||||
|| member_decimal(&p, RS_MAX_BLOCK_SIZE, &size) != STREAM_OK || *p) return -1; |
||||
parse->state.source.n = (uint8_t)n; |
||||
parse->state.source.r = (uint8_t)r; |
||||
parse->state.source.block_size = (uint16_t)size; |
||||
if (stream_descriptor_validate(&parse->state.source) != STREAM_OK) return -1; |
||||
parse->state.published = 1; |
||||
} else if (!strcmp(key, STREAM_MEMBER_SUBSCRIPTIONS)) { |
||||
if (parse->seen & 2) return -1; |
||||
parse->seen |= 2; |
||||
const char *p = value; |
||||
while (*p) { |
||||
if (parse->state.count == STREAM_MEMBER_MAX_SUBSCRIPTIONS) return -1; |
||||
struct stream_subscription_key entry; |
||||
if (member_hex(&p, &entry.source_node_id) != STREAM_OK || *p++ != ':' |
||||
|| member_hex(&p, &entry.source_epoch) != STREAM_OK) return -1; |
||||
if (parse->state.count) { |
||||
const struct stream_subscription_key *previous = &parse->state.subscriptions[parse->state.count - 1]; |
||||
if (entry.source_node_id < previous->source_node_id |
||||
|| (entry.source_node_id == previous->source_node_id && entry.source_epoch <= previous->source_epoch)) return -1; |
||||
} |
||||
parse->state.subscriptions[parse->state.count++] = entry; |
||||
if (*p && (*p++ != ',' || !*p)) return -1; |
||||
} |
||||
} |
||||
return 0; |
||||
} |
||||
|
||||
int stream_member_has_subscription(const struct stream_member_state *state, uint64_t source_id, uint64_t epoch) { |
||||
if (!state || state->count > STREAM_MEMBER_MAX_SUBSCRIPTIONS) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member subscription lookup invalid state"); return 0; |
||||
} |
||||
for (size_t i = 0; i < state->count; i++) |
||||
if (state->subscriptions[i].source_node_id == source_id && state->subscriptions[i].source_epoch == epoch) return 1; |
||||
return 0; |
||||
} |
||||
|
||||
/* Отсутствие stream-ключей — обычный мембер, оно не создаёт экземпляр или буфер. */ |
||||
int stream_member_parse(uint64_t group_id, uint64_t member_id, const char *userinfo, struct stream_member_state *state) { |
||||
if (!group_id || !member_id || !userinfo || !state || strlen(userinfo) > MS_USERINFO_MAX) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member parse invalid arguments group=%llx member=%llx", |
||||
(unsigned long long)group_id, (unsigned long long)member_id); |
||||
return STREAM_ERR_ARGUMENT; |
||||
} |
||||
struct member_parse parse = { .state.source = { .group_id = group_id, .source_node_id = member_id } }; |
||||
if (json_flat_parse(userinfo, member_option, &parse) < 0 |
||||
|| (parse.state.published && !stream_member_has_subscription(&parse.state, member_id, parse.state.source.source_epoch))) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "member stream options malformed/inconsistent group=%llx member=%llx", |
||||
(unsigned long long)group_id, (unsigned long long)member_id); |
||||
return STREAM_ERR_FORMAT; |
||||
} |
||||
*state = parse.state; |
||||
return STREAM_OK; |
||||
} |
||||
|
||||
/* Сериализовать канонические строки до обновления подписанного JSON-профиля. */ |
||||
int stream_member_format(const struct stream_member_state *state, char source[MS_USERINFO_MAX + 1], |
||||
char subscriptions[MS_USERINFO_MAX + 1]) { |
||||
if (!state || !source || !subscriptions || state->count > STREAM_MEMBER_MAX_SUBSCRIPTIONS |
||||
|| (state->published && (stream_descriptor_validate(&state->source) != STREAM_OK |
||||
|| !stream_member_has_subscription(state, state->source.source_node_id, state->source.source_epoch)))) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member format invalid state"); return STREAM_ERR_ARGUMENT; |
||||
} |
||||
char encoded_source[MS_USERINFO_MAX + 1] = "", encoded_subscriptions[MS_USERINFO_MAX + 1] = ""; |
||||
if (state->published) snprintf(encoded_source, sizeof(encoded_source), "%016llx:%u:%u:%u", |
||||
(unsigned long long)state->source.source_epoch, state->source.n, state->source.r, state->source.block_size); |
||||
size_t used = 0; |
||||
for (size_t i = 0; i < state->count; i++) { |
||||
const struct stream_subscription_key *entry = &state->subscriptions[i]; |
||||
if (!entry->source_node_id || !entry->source_epoch || (i && |
||||
(entry->source_node_id < state->subscriptions[i - 1].source_node_id || |
||||
(entry->source_node_id == state->subscriptions[i - 1].source_node_id |
||||
&& entry->source_epoch <= state->subscriptions[i - 1].source_epoch)))) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member format invalid subscription index=%zu", i); return STREAM_ERR_ARGUMENT; |
||||
} |
||||
int length = snprintf(encoded_subscriptions + used, sizeof(encoded_subscriptions) - used, "%s%016llx:%016llx", |
||||
i ? "," : "", (unsigned long long)entry->source_node_id, (unsigned long long)entry->source_epoch); |
||||
if (length < 0 || (size_t)length >= sizeof(encoded_subscriptions) - used) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member subscriptions exceed limit count=%zu", state->count); return STREAM_ERR_ARGUMENT; |
||||
} |
||||
used += (size_t)length; |
||||
} |
||||
memcpy(source, encoded_source, strlen(encoded_source) + 1); |
||||
memcpy(subscriptions, encoded_subscriptions, used + 1); |
||||
return STREAM_OK; |
||||
} |
||||
|
||||
/* Прочитать именно подписанный блок данного канала, а не общий online/name из nodes. */ |
||||
int stream_member_load(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t member_id, |
||||
struct stream_member_state *state, uint8_t public_key[32]) { |
||||
struct TOPO_GROUP *group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL; |
||||
if (!group || group->stopping || group->group_type != TOPO_GROUP_TYPE_CHAT || !inst->topo_sqlite_db || !state || !member_id) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member load invalid group=%llx member=%llx", (unsigned long long)group_id, |
||||
(unsigned long long)member_id); return STREAM_ERR_ARGUMENT; |
||||
} |
||||
for (const char *p = group->channel_id; *p; p++) if (*p < '0' || *p > '9') { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member load invalid channel namespace group=%llx", (unsigned long long)group_id); |
||||
return STREAM_ERR_ARGUMENT; |
||||
} |
||||
char query[256]; |
||||
snprintf(query, sizeof(query), "SELECT source,x25519_pubkey,ed25519_pubkey,join_sig,join_ts,update_sig,update_ts," |
||||
"userinfo,signed_by,signature FROM \"peers_%s\" WHERE node_id=?", group->channel_id); |
||||
sqlite3_stmt *statement = NULL; |
||||
sqlite3 *db = inst->topo_sqlite_db; |
||||
if (sqlite3_prepare_v2(db, query, -1, &statement, NULL) != SQLITE_OK) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member load query failed group=%llx: %s", (unsigned long long)group_id, sqlite3_errmsg(db)); |
||||
return STREAM_ERR_STATE; |
||||
} |
||||
sqlite3_bind_int64(statement, 1, (sqlite3_int64)member_id); |
||||
int result = STREAM_WAIT, step = sqlite3_step(statement); |
||||
if (step == SQLITE_ROW && sqlite3_column_int(statement, 0) == 0 |
||||
&& sqlite3_column_bytes(statement, 1) == 32 && sqlite3_column_bytes(statement, 2) == 32 |
||||
&& sqlite3_column_bytes(statement, 3) == 64 && sqlite3_column_bytes(statement, 5) == 64 |
||||
&& sqlite3_column_bytes(statement, 9) == 64 && sqlite3_column_int64(statement, 6) > 0) { |
||||
const uint8_t *x25519 = sqlite3_column_blob(statement, 1), *ed25519 = sqlite3_column_blob(statement, 2); |
||||
const char *userinfo = (const char *)sqlite3_column_text(statement, 7); |
||||
const uint8_t *join = sqlite3_column_blob(statement, 3), *signature = sqlite3_column_blob(statement, 5); |
||||
uint64_t version = (uint64_t)sqlite3_column_int64(statement, 6); |
||||
uint8_t body[MS_USERINFO_MAX + 73]; |
||||
struct ms_member_rec record = { .node_id = member_id, .x25519 = x25519, .ed25519 = ed25519, |
||||
.join_sig = join, .join_ts = (uint64_t)sqlite3_column_int64(statement, 4), .update_sig = signature, |
||||
.update_ts = version, .userinfo = userinfo, .signed_by = (uint64_t)sqlite3_column_int64(statement, 8), |
||||
.signature = sqlite3_column_blob(statement, 9) }; |
||||
int tree = member_sync_verify_signature(db, group->channel_id, &record); |
||||
int bytes = userinfo && sqlite3_column_bytes(statement, 7) <= MS_USERINFO_MAX |
||||
&& (size_t)sqlite3_column_bytes(statement, 7) == strlen(userinfo) |
||||
? member_sync_build_update_msg(join, version, userinfo, body, sizeof(body)) : -1; |
||||
if (tree < 0) { |
||||
DEBUG_DEBUG(DEBUG_CATEGORY_STREAM, "member source trust pending group=%llx member=%llx", (unsigned long long)group_id, |
||||
(unsigned long long)member_id); |
||||
} else if (!tree || sc_derive_node_id_from_pubkey(x25519) != member_id || bytes < 0 |
||||
|| sc_ed25519_verify(ed25519, body, (size_t)bytes, signature) != SC_OK) { |
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "member profile signature/identity invalid group=%llx member=%llx version=%llu", |
||||
(unsigned long long)group_id, (unsigned long long)member_id, (unsigned long long)version); |
||||
result = STREAM_ERR_SIGNATURE; |
||||
} else { |
||||
struct stream_member_state parsed; |
||||
result = stream_member_parse(group_id, member_id, userinfo, &parsed); |
||||
if (result == STREAM_OK) { parsed.version = version; *state = parsed; if (public_key) memcpy(public_key, ed25519, 32); } |
||||
} |
||||
} else if (step == SQLITE_DONE) { |
||||
result = STREAM_ERR_NOT_MEMBER; |
||||
DEBUG_WARN(DEBUG_CATEGORY_STREAM, "member absent group=%llx member=%llx", (unsigned long long)group_id, |
||||
(unsigned long long)member_id); |
||||
} else if (step != SQLITE_ROW) { |
||||
DEBUG_ERROR(DEBUG_CATEGORY_STREAM, "member load failed group=%llx: %s", (unsigned long long)group_id, sqlite3_errmsg(db)); |
||||
result = STREAM_ERR_STATE; |
||||
} |
||||
sqlite3_finalize(statement); |
||||
return result; |
||||
} |
||||
|
||||
/* Online относится к пути этой группы; транспорт или запись в общей БД не заменяют его. */ |
||||
int stream_member_online(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t member_id) { |
||||
struct TOPO_GROUP *group = inst && inst->topo_groups ? topo_groups_find(inst->topo_groups, group_id) : NULL; |
||||
if (!group || group->stopping || group->group_type != TOPO_GROUP_TYPE_CHAT) return 0; |
||||
if (member_id == inst->node_id) return inst->core_started && group->local_node != NULL; |
||||
struct TOPO_GROUP_NODE *node = topo_node_find_by_id(group, member_id); |
||||
return node && topo_node_best_path(node) != NULL; |
||||
} |
||||
@ -0,0 +1,50 @@
|
||||
/* Строковые опции подписанного блока мембера, без отдельного протокола объявлений.
|
||||
* stream_source = epoch(16 hex):n:r:RS-size; отсутствие/пустое значение — не публикует. |
||||
* stream_subscriptions = source(16 hex):epoch(16 hex),...; сортировка по source/epoch. |
||||
* Указанная в stream_source собственная трансляция также должна быть в subscriptions. |
||||
* load проверяет подпись профиля и принадлежность ключа источника, BGP-доступность — отдельно. |
||||
* parse/format не меняют выход при ошибке. Все вызовы — один uasync-поток. */ |
||||
#ifndef STREAM_MEMBER_H |
||||
#define STREAM_MEMBER_H |
||||
|
||||
#include "stream_proto.h" |
||||
#include "../chat/member_sync.h" |
||||
|
||||
#ifdef __cplusplus |
||||
extern "C" { |
||||
#endif |
||||
|
||||
#define STREAM_MEMBER_SOURCE "stream_source" |
||||
#define STREAM_MEMBER_SUBSCRIPTIONS "stream_subscriptions" |
||||
#define STREAM_MEMBER_MAX_SUBSCRIPTIONS (MS_USERINFO_MAX / 34) |
||||
|
||||
struct UTUN_INSTANCE; |
||||
|
||||
struct stream_subscription_key { |
||||
uint64_t source_node_id; |
||||
uint64_t source_epoch; |
||||
}; |
||||
|
||||
struct stream_member_state { |
||||
struct stream_descriptor source; |
||||
struct stream_subscription_key subscriptions[STREAM_MEMBER_MAX_SUBSCRIPTIONS]; |
||||
size_t count; |
||||
uint64_t version; |
||||
int published; |
||||
}; |
||||
|
||||
int stream_member_parse(uint64_t group_id, uint64_t member_id, const char *userinfo, struct stream_member_state *state); |
||||
int stream_member_format(const struct stream_member_state *state, char source[MS_USERINFO_MAX + 1], |
||||
char subscriptions[MS_USERINFO_MAX + 1]); |
||||
int stream_member_has_subscription(const struct stream_member_state *state, uint64_t source_id, uint64_t epoch); |
||||
|
||||
/* WAIT — нет подписанного профиля/подтверждения родителя; ошибка подписи/формата <0.
|
||||
* NOT_MEMBER отличает удалённого участника от временно неполного профиля. */ |
||||
int stream_member_load(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t member_id, |
||||
struct stream_member_state *state, uint8_t public_key[SC_PUBKEY_SIZE]); |
||||
int stream_member_online(struct UTUN_INSTANCE *inst, uint64_t group_id, uint64_t member_id); |
||||
|
||||
#ifdef __cplusplus |
||||
} |
||||
#endif |
||||
#endif |
||||
Loading…
Reference in new issue