From 7374b59c03f0b9ab6c56718bd74ecefcddbe9ef9 Mon Sep 17 00:00:00 2001 From: evgeny Date: Wed, 28 Oct 2026 10:58:25 +0100 Subject: [PATCH] Authorize server join requests and run ordinary channel JOIN --- src/chat/chat_join_port.c | 3 + src/chat/chat_sync.c | 158 ++++++++++++++++++++++++++++++++++++-- src/chat/chat_sync.h | 7 +- 3 files changed, 159 insertions(+), 9 deletions(-) diff --git a/src/chat/chat_join_port.c b/src/chat/chat_join_port.c index 7f439e67..d0432c40 100644 --- a/src/chat/chat_join_port.c +++ b/src/chat/chat_join_port.c @@ -68,6 +68,9 @@ static int submit(struct chat_join_port* p, uint64_t request, const char* link, } struct InviteData invite; if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID; + if (!offer && invite.kind != INVITE_KIND_CHANNEL) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server link requires selecting a local channel to add"); return CHAT_CONNECT_INVALID; + } struct join_command* command = u_calloc(1, sizeof(*command) + len); if (!command) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "join command allocation failed"); return CHAT_CONNECT_ERROR; } command->post.callback = execute; command->post.arg = command; command->port = p; diff --git a/src/chat/chat_sync.c b/src/chat/chat_sync.c index baf0ad17..f153ecc3 100644 --- a/src/chat/chat_sync.c +++ b/src/chat/chat_sync.c @@ -8,6 +8,8 @@ #include "chat_join.h" #include "chat_join_port.h" #include "invite_link.h" +#include "invite_build.h" +#include #include "group_avatar.h" #include "../routing_layer/topo_node_sqlite.h" #include "../routing_layer/topo_group.h" @@ -87,6 +89,12 @@ struct chat_sync { char password[INVITE_PASS_MAX]; struct NODE_CONN_DIRECT* handle; void* transport_timer; + uint8_t server_offer; /* отправляем серверу обычное приглашение после проверки add_key */ + uint8_t add_key[INVITE_ADD_KEY_SIZE]; /* только исходящий SERVER_JOIN_REQ */ + char offer_link[INVITE_LINK_MAX]; /* подтверждённое обычное приглашение, одинаковое при повторах */ + struct chat_invite_build* offer_build; + void* operation_timer; /* общий deadline серверной операции, сохраняется при reconnect */ + uint64_t origin_operation; /* входящий запрос сервера: ответить peer после обычного JOIN */ } join; struct join_pending* join_pending; /* connection-узел: ожидающие ready джойнеры */ /* Pending incoming invites (для ask/deny) */ @@ -104,6 +112,7 @@ struct chat_sync { static void cs_info_req_timeout_cb(void* arg); static void cs_join_finish(struct chat_sync* cs, int result); +static int cs_server_join_reply(struct chat_sync* cs, const char* channel, uint64_t peer, uint64_t operation, int result); static void cs_join_connect_timeout(void* arg) { struct chat_sync* cs = arg; @@ -156,6 +165,8 @@ static int cs_join_hold_transport(struct chat_sync* cs, uint64_t node_id, struct static const char* cs_msg_name(uint8_t type) { switch (type) { + case CS_MSG_SERVER_JOIN_REQ: return "SERVER_JOIN_REQ"; + case CS_MSG_SERVER_JOIN_RESULT: return "SERVER_JOIN_RESULT"; case CS_MSG_ERROR: return "ERROR"; case CS_MSG_CHANNEL_INVITE: return "CHANNEL_INVITE"; case CS_MSG_JOIN_INFO_REQ: return "JOIN_INFO_REQ"; @@ -186,10 +197,18 @@ static void cs_join_end(struct chat_sync* cs, int result, int notify) { struct UTUN_INSTANCE* inst = cs->inst; uint64_t channel = cs->join.channel, peer = cs->join.peer, request = cs->join.request; struct NODE_CONN_DIRECT* handle = cs->join.handle; + struct chat_invite_build* build = cs->join.offer_build; + uint64_t origin = cs->join.origin_operation; cs_cancel_proto_timers(cs); if (cs->join.transport_timer) uasync_cancel_timeout(inst->ua, cs->join.transport_timer); - memset(&cs->join, 0, sizeof(cs->join)); + if (cs->join.operation_timer) uasync_cancel_timeout(inst->ua, cs->join.operation_timer); + OPENSSL_cleanse(&cs->join, sizeof(cs->join)); chat_join_port_complete(cs->port, request); + if (build) chat_invite_build_cancel(build); + if (origin && notify) { + char name[64]; snprintf(name, sizeof(name), "%llu", (unsigned long long)channel); + cs_server_join_reply(cs, name, peer, origin, result); + } if (handle) node_conn_direct_close(handle); if (notify) chat_sync_join_result(inst, request, channel, peer, result); } @@ -268,6 +287,69 @@ static int cs_send_attempt(struct chat_sync* cs, const char* channel, uint64_t p /* ── Channel cache ── */ +static int cs_server_join_reply(struct chat_sync* cs, const char* channel, uint64_t peer, uint64_t operation, int result) { + uint8_t data[5] = {CS_MSG_SERVER_JOIN_RESULT}; + uint32_t code = htonl((uint32_t)result); memcpy(data + 1, &code, sizeof(code)); + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "server join reply operation=%016llx channel=%s peer=%016llx result=%d", + (unsigned long long)operation, channel, (unsigned long long)peer, result); + int rc = cs_send_attempt(cs, channel, peer, data, sizeof(data), operation); + if (rc) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "server join reply failed operation=%016llx rc=%d", (unsigned long long)operation, rc); + return rc; +} + +static void cs_handle_server_join(struct chat_sync* cs, uint64_t peer, const char* channel, + const uint8_t* data, size_t len, uint64_t operation) { + const struct global_config* cfg = &cs->inst->config->global; + int result = CHAT_CONNECT_INVALID; + if (len < INVITE_ADD_KEY_SIZE + 2 || !cfg->chatserver_enabled || !cfg->chatserver_has_add_key || + CRYPTO_memcmp(data, cfg->chatserver_add_key, INVITE_ADD_KEY_SIZE)) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server join authorization rejected channel=%s peer=%016llx key_configured=%d bytes=%zu", + channel, (unsigned long long)peer, cfg->chatserver_has_add_key, len); + cs_server_join_reply(cs, channel, peer, operation, CHAT_CONNECT_AUTH); return; + } + size_t size = ((size_t)data[32] << 8) | data[33]; + struct InviteData invite; + if (size != len - 34 || invite_link_decode((const char*)data + 34, size, &invite, NULL, 0) || + invite.kind != INVITE_KIND_CHANNEL || invite.channelId != chat_channel_id(channel) || invite.nodeId != peer) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server join invitation rejected channel=%s peer=%016llx bytes=%zu", + channel, (unsigned long long)peer, size); + } else if (cs->join.channel) { + if (cs->join.origin_operation == operation && cs->join.peer == peer && cs->join.channel == invite.channelId && + cs->join.key == invite.join_key) { + DEBUG_DEBUG(DEBUG_CATEGORY_CHAT_SYNC, "server join duplicate active operation=%016llx", (unsigned long long)operation); return; + } + result = CHAT_CONNECT_BUSY; + } else if (topo_node_sqlite_member_allowed(cs->inst->topo_sqlite_db, channel, cs->inst->node_id) && + topo_node_sqlite_member_allowed(cs->inst->topo_sqlite_db, channel, peer)) { + result = 0; + } else { + result = chat_sync_join_link(cs->inst, 0, (const char*)data + 34, size); + if (!result) { + cs->join.origin_operation = operation; + DEBUG_INFO(DEBUG_CATEGORY_CHAT_SYNC, "server join authorized; ordinary JOIN started operation=%016llx channel=%s peer=%016llx", + (unsigned long long)operation, channel, (unsigned long long)peer); return; + } + } + cs_server_join_reply(cs, channel, peer, operation, result); +} + +static void cs_handle_server_join_result(struct chat_sync* cs, uint64_t peer, const char* channel, + const uint8_t* data, size_t len, uint64_t operation) { + if (!cs->join.server_offer || cs->join.phase != 4 || cs->join.peer != peer || + cs->join.channel != chat_channel_id(channel) || cs->join.attempt != operation) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "ignore stale server join result operation=%016llx channel=%s peer=%016llx", + (unsigned long long)operation, channel, (unsigned long long)peer); return; + } + if (len != 4) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "invalid server join result bytes=%zu", len); return; } + uint32_t code; memcpy(&code, data, sizeof(code)); + int result = (int32_t)ntohl(code); + if (result != 0 && result != CHAT_CONNECT_ERROR && result != CHAT_CONNECT_TIMEOUT && result != CHAT_CONNECT_INVALID && + result != CHAT_CONNECT_BUSY && result != CHAT_CONNECT_CANCELLED && result != CHAT_CONNECT_AUTH) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "unknown server join result=%d", result); return; + } + cs_join_finish(cs, result); +} + /* ── Forward declarations for new message handlers ── */ static void cs_handle_join_info_req(struct chat_sync* cs, uint64_t peer, @@ -319,7 +401,8 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { /* CHANNEL_INVITE/JOIN_INFO_REQ приходят для ещё не созданного у нас канала: group_id == channel_id (numeric), поэтому выводим ch_id напрямую. */ if (type == CS_MSG_CHANNEL_INVITE || type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP || - type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) { + type == CS_MSG_JOIN_REQUEST || type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR || + type == CS_MSG_SERVER_JOIN_REQ || type == CS_MSG_SERVER_JOIN_RESULT) { snprintf(ch_id, sizeof(ch_id), "%llu", (unsigned long long)group_id); } else { u_free(entry->dgram); queue_entry_free(entry); return; @@ -335,7 +418,7 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { uint64_t attempt = 0; if (type == CS_MSG_JOIN_INFO_REQ || type == CS_MSG_JOIN_INFO_RESP || type == CS_MSG_JOIN_REQUEST || - type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR) { + type == CS_MSG_JOIN_READY || type == CS_MSG_ERROR || type == CS_MSG_SERVER_JOIN_REQ || type == CS_MSG_SERVER_JOIN_RESULT) { if (plen < 8) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "join missing attempt type=%u", type); goto received; } memcpy(&attempt, pl, 8); attempt = be64toh(attempt); pl += 8; plen -= 8; if (!attempt) { DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "join zero attempt type=%u", type); goto received; } @@ -359,6 +442,8 @@ static void chat_sync_recv_cb(struct ETCP_CONN* conn, struct ll_entry* entry) { break; } case CS_MSG_CHANNEL_INVITE: cs_handle_channel_invite(cs, peer, ch_id, pl, plen); break; + case CS_MSG_SERVER_JOIN_REQ: cs_handle_server_join(cs, peer, ch_id, pl, plen, attempt); break; + case CS_MSG_SERVER_JOIN_RESULT: cs_handle_server_join_result(cs, peer, ch_id, pl, plen, attempt); break; case CS_MSG_SOUND_SETTING: if (plen == 1 && pl[0] <= 1) db_sync_set_peer_sound(cs->inst, group_id, peer, pl[0]); else DEBUG_WARN(DEBUG_CATEGORY_MEMBER_SYNC, "invalid SOUND_SETTING size=%zu", plen); @@ -662,7 +747,49 @@ static int cs_start_channel_join(struct chat_sync* cs, uint64_t peer) { return 0; } +static int cs_send_server_offer(struct chat_sync* cs) { + char channel[64]; snprintf(channel, sizeof(channel), "%llu", (unsigned long long)cs->join.channel); + size_t len = strlen(cs->join.offer_link); + uint8_t data[35 + INVITE_LINK_MAX] = {CS_MSG_SERVER_JOIN_REQ}; + memcpy(data + 1, cs->join.add_key, INVITE_ADD_KEY_SIZE); + data[33] = (uint8_t)(len >> 8); data[34] = (uint8_t)len; memcpy(data + 35, cs->join.offer_link, len); + cs->join.phase = 4; + int rc = cs_send_attempt(cs, channel, cs->join.peer, data, len + 35, cs->join.attempt); + OPENSSL_cleanse(data, sizeof(data)); + if (rc) DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "server join request send failed channel=%s rc=%d", channel, rc); + return rc; +} + +static void cs_server_invite_ready(void* arg, int result, const char* link) { + struct chat_sync* cs = arg; + if (!cs->join.server_offer) return; /* отмена обнулила join до cancel регистрации */ + cs->join.offer_build = NULL; + if (result != CHAT_JOIN_OK) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server invitation registration failed result=%d", result); + cs_join_finish(cs, result == CHAT_JOIN_TIMEOUT ? CHAT_CONNECT_TIMEOUT : CHAT_CONNECT_ERROR); return; + } + struct InviteData invitation; + if (!link || invite_link_decode(link, strlen(link), &invitation, NULL, 0) || + invitation.kind != INVITE_KIND_CHANNEL || invitation.channelId != cs->join.channel || invitation.nodeId != cs->inst->node_id) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "server invitation builder returned unexpected target"); + cs_join_finish(cs, CHAT_CONNECT_ERROR); return; + } + strcpy(cs->join.offer_link, link); cs->join.key = invitation.join_key; + struct ETCP_CONN* conn = node_conn_direct_get_conn(cs->join.handle); + if (conn && conn->initialized && conn->links_up) { + if (cs_send_server_offer(cs)) cs_join_finish(cs, CHAT_CONNECT_ERROR); + } else cs->join.phase = 1; +} + static int cs_start_channel_offer(struct chat_sync* cs, uint64_t peer) { + if (cs->join.server_offer) { + if (cs->join.offer_link[0]) return cs_send_server_offer(cs); + if (cs->join.offer_build) return 0; + cs->join.phase = 6; + cs->join.offer_build = chat_invite_build_link(cs->inst, cs->join.channel, cs->inst->node_id, NULL, cs_server_invite_ready, cs); + if (!cs->join.offer_build) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "cannot build ordinary invitation for server"); return -1; } + return 0; + } char channel[64], name[128]; uint64_t owner; uint8_t x[32], ed[32], signature[64]; snprintf(channel, sizeof(channel), "%llu", (unsigned long long)cs->join.channel); @@ -990,6 +1117,15 @@ static int cs_connect_invite(struct UTUN_INSTANCE* inst, uint64_t request, const const struct InviteData invite = *data; int rc = cs_join_reserve(cs, offer_channel ? offer_channel : invite.channelId, invite.nodeId, request, offer_channel != 0); if (rc) return rc; + cs->join.server_offer = offer_channel && invite.kind == INVITE_KIND_SERVER; + if (cs->join.server_offer) { + memcpy(cs->join.add_key, invite.add_key, sizeof(cs->join.add_key)); + cs->join.operation_timer = uasync_set_timeout(inst->ua, 300000, cs, cs_info_req_timeout_cb, "server_join_deadline"); + if (!cs->join.operation_timer) { + DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "server join deadline allocation failed"); + cs_join_end(cs, CHAT_CONNECT_ERROR, 0); return CHAT_CONNECT_ERROR; + } + } struct TOPO_NODE node = { .node_id = invite.nodeId }; memcpy(node.public_key, invite.pubkey, 32); for (unsigned i = 0; i < invite.addrCount; i++) { @@ -1035,7 +1171,9 @@ failure: int chat_sync_join_link(struct UTUN_INSTANCE* inst, uint64_t request, const char* link, size_t len) { struct InviteData invite; - if (invite_link_decode(link, len, &invite, NULL, 0)) return CHAT_CONNECT_INVALID; + if (invite_link_decode(link, len, &invite, NULL, 0) || invite.kind != INVITE_KIND_CHANNEL) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "ordinary JOIN requires a channel invitation"); return CHAT_CONNECT_INVALID; + } return cs_connect_invite(inst, request, &invite, 0); } int chat_sync_offer_link(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, const char* link, size_t len) { @@ -1390,7 +1528,8 @@ static void cs_handle_join_request(struct chat_sync* cs, uint64_t peer, отправляем JOIN_READY напрямую, если мембер реально добавлен. */ if (topo_node_sqlite_member_allowed(cs->inst->topo_sqlite_db, ch_id, m.node_id)) { cs_send_join_ready(cs, ch_id, m.node_id); - if (cs->join.inviter && cs->join.channel == channel_id && cs->join.peer == m.node_id && cs->join.key == join_key) + if (cs->join.inviter && !cs->join.server_offer && cs->join.channel == channel_id && + cs->join.peer == m.node_id && cs->join.key == join_key) cs_join_finish(cs, 0); } else { uint8_t error = CS_MSG_ERROR; cs_send_attempt(cs, ch_id, peer, &error, 1, attempt); @@ -1442,6 +1581,10 @@ static void cs_handle_join_ready(struct chat_sync* cs, uint64_t peer, static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer, const char* ch_id, const uint8_t* pl, size_t len) { + if (cs->inst->config->global.chatserver_enabled) { + DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "server rejects unauthenticated CHANNEL_INVITE channel=%s peer=%016llx; use SERVER_JOIN_REQ", + ch_id, (unsigned long long)peer); return; + } DEBUG_TRACE(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE ch=%s from=%016llx len=%zu", CS_ID, ch_id, (unsigned long long)peer, len); if (len < 1 + 8 + 32 + 32 + 64 + 1 + 64 + 8 + 2 + 8) { @@ -1489,11 +1632,10 @@ static void cs_handle_channel_invite(struct chat_sync* cs, uint64_t peer, return; } - /* Check join_policy: server always autojoin, GUI uses chat_setting */ + /* Обычные узлы используют настройку GUI; сервер принимает только авторизованный SERVER_JOIN_REQ. */ int policy; { - struct global_config *gcfg = &cs->inst->config->global; - policy = gcfg->chatserver_enabled ? 0 : chat_setting_get_int(cs->inst, "join_policy", 1); + policy = chat_setting_get_int(cs->inst, "join_policy", 1); } DEBUG_DEBUG(DEBUG_CATEGORY_MEMBER_SYNC, "%s: CHANNEL_INVITE policy=%d (chatserver=%d) ch=%s from=%016llx", CS_ID, policy, cs->inst->config->global.chatserver_enabled, ch_id, (unsigned long long)peer); diff --git a/src/chat/chat_sync.h b/src/chat/chat_sync.h index 0661ee22..7caac427 100644 --- a/src/chat/chat_sync.h +++ b/src/chat/chat_sync.h @@ -35,6 +35,8 @@ struct UTUN_INSTANCE; #define CS_MSG_JOIN_REQUEST 0x12 /* joiner → connection: {join_key:8, member...} */ #define CS_MSG_JOIN_READY 0x13 /* connection → joiner: {group_id:8} */ #define CS_MSG_SOUND_SETTING 0x14 /* peer → server: {sound_on:1} — per-chat play sound on message */ +#define CS_MSG_SERVER_JOIN_REQ 0x15 /* client → server: operation:8BE, add_key:32, link_len:2BE, CHANNEL-link */ +#define CS_MSG_SERVER_JOIN_RESULT 0x16 /* server → client: operation:8BE, result:4BE (CHAT_CONNECT_*) */ /* Управление JOIN (INFO_REQ/RESP, REQUEST, READY, ERROR): после type идёт attempt_id:8BE, * затем payload, описанный выше. Ненулевой случайный ID новый для каждой локальной попытки; @@ -62,11 +64,14 @@ void chat_sync_destroy(struct UTUN_INSTANCE* inst); #define CHAT_CONNECT_INVALID -7 #define CHAT_CONNECT_BUSY -8 #define CHAT_CONNECT_CANCELLED -9 +#define CHAT_CONNECT_AUTH -10 int chat_sync_join_link(struct UTUN_INSTANCE* inst, uint64_t request, const char* link, size_t len); /* Единый формат результата: peer:8,result:4,channel:8,request:8, native; только uasync. */ void chat_sync_join_result(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, uint64_t peer, int result); -/* uasync: пригласить адресата ссылки в свой channel; те же правила запуска/результата. */ +/* uasync: пригласить адресата в свой channel. SERVER-ссылка запускает запрос с add_key и обычным + * приглашением через self; успех возвращает сервер после своего JOIN. CHANNEL — обычный push-invite. + * Те же правила запуска/результата; серверная ссылка не является приглашением в канал. */ int chat_sync_offer_link(struct UTUN_INSTANCE* inst, uint64_t request, uint64_t channel, const char* link, size_t len); /* Пригласить target_node_id в канал ch_id (inviter → joiner).