Files
utun2/src/dm/dm_core.h
T

210 lines
18 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* dm_core.h — прямой p2p чат между двумя пользователями (DM)
*
* Личная беседа не создаёт отдельную TOPO_GROUP и не синхронизируется через member_sync/Merkle.
* Для доставки использует маршрут из общей CHAT-группы участников.
* - conv_id и content_key детерминированно выводятся обеими сторонами
* из своих ключей и pubkey пира (см. dm_crypto.h) — без переговоров.
* - сообщения идут через etcp_router (прямое соединение или релей),
* при недоступности пира — через dm_mailbox (суперузлы).
* - содержимое шифруется E2E (AES-256-CCM), запись подписывается Ed25519.
*
* Два независимых направленных потока (я→пир и пир→я) с монотонным исходящим seq.
* Все API и трамплины выполняются в uasync-потоке с живым instance/SQLite/router.
* Входные строки/тела заимствуются на время вызова; функции не забирают их владение.
* Запросы трамплинов — один блок u_malloc/u_calloc с NUL-строками; обработчик освобождает блок.
* Instance должен пережить запрос и его отложенное завершение; остановка сериализуется через uasync.
*/
#ifndef DM_CORE_H
#define DM_CORE_H
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include "dm_media.h"
struct UTUN_INSTANCE;
#ifdef __cplusplus
extern "C" {
#endif
/* ── Каноническое тело DM-сообщения (общий wire-формат dm_core и dm_mailbox) ──
* [conv_id:8][seq:8][ts:8][author:8][ct_len:1][ct:ct_len][data_len:2][data_enc:data_len][media?:56][sig:64]
* media присутствует только для ct="file", подписан вместе с сообщением.
* data_enc — AES-256-CCM (шифротекст + tag), sig — Ed25519 автора по всем полям до sig.
* Числа native-endian; seq=1..INT64_MAX, ts — миллисекунды NTP-времени instance.
* ETCP_RT_ID_DM=0x34: [MSG=1][тело] либо [ACK=2][квитанция:136]; router-пакет подписан. */
#define DM_MSG_FIXED_HDR 33 /* conv_id+seq+ts+author(32) + ct_len(1) — до ct */
#define DM_MSG_SIG_SIZE 64
#define DM_DATA_MAX 1024
#define DM_RECEIPT_SIZE 136
#define DM_PEER_NAME_MAX 63 /* UTF-8 байты без NUL; сохраняются только целые code points. */
/* Вычислить полную длину канонического тела из его заголовка.
* Возвращает 1 и *out_len, либо 0 если буфер мал/повреждён (выход не меняется).
* Допускает хвост после тела; для отдельного сообщения вызывающий требует *out_len==avail.
* Многобайтные числа имеют порядок байтов хоста; подпись/seq/идентичность здесь не проверяются. */
static inline int dm_msg_len(const uint8_t* msg, size_t avail, size_t* out_len) {
if (!msg || !out_len || avail < DM_MSG_FIXED_HDR) return 0;
uint8_t ct_len = msg[32];
if (avail < (size_t)DM_MSG_FIXED_HDR + ct_len + 2) return 0;
uint16_t data_len;
memcpy(&data_len, msg + DM_MSG_FIXED_HDR + ct_len, 2);
if (data_len < 16 || data_len > DM_DATA_MAX + 16) return 0;
size_t media_len = ct_len == 4 && !memcmp(msg + DM_MSG_FIXED_HDR, "file", 4) ? DM_MEDIA_DESC_SIZE : 0;
size_t total = (size_t)DM_MSG_FIXED_HDR + ct_len + 2 + data_len + media_len + DM_MSG_SIG_SIZE;
if (avail < total) return 0;
*out_len = total;
return 1;
}
/* Выбрать общую CHAT-группу с путём router к peer, сначала preferred (0=без предпочтения).
* 1 и *group_id при успехе; 0 при отсутствии пути/ошибке, выход не меняется.
* group_id обязателен; наличие пути не гарантирует успешной последующей отправки. */
int dm_route_group(struct UTUN_INSTANCE* inst, uint64_t peer, uint64_t preferred, uint64_t* group_id);
/* Проверить точную длину msg/len, conv/author/recipient/seq, descriptor и подпись автора из nodes.
* 0=корректно, -1=отказ. Нужна SQLite, владелец dm_core не требуется.
* Не расшифровывает тело и не проверяет членство/роль router-отправителя: это обязанность вызывающего. */
int dm_verify_message(struct UTUN_INSTANCE* inst, uint64_t recipient, const uint8_t* msg, size_t len);
/* Проверить и расшифровать входящее тело для локального узла, атомарно сохранить историю/беседу/media.
* 0 и receipt после commit либо точного повтора; -1 при отказе. Для file нужен живой dm_media.
* Точный удалённый повтор подтверждается по отпечатку без восстановления истории; конфликт отклоняется.
* receipt — обязательные 136 байт; при -1 буфер не использовать. Ошибка подписи квитанции возможна после commit. */
int dm_accept_message(struct UTUN_INSTANCE* inst, const uint8_t* msg, size_t len, uint8_t receipt[DM_RECEIPT_SIZE]);
/* receipt — ровно 136 доступных байт: ["DMACK001":8][conv:8][seq:8][author:8][recipient:8][SHA256(msg):32][sig:64].
* Проверяет идентичность и подпись B по первым 72 байтам; при msg!=NULL также точное тело msg/len.
* 0=корректно, -1=отказ. Не проверяет наличие outbox и ничего не записывает. */
int dm_verify_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE], const uint8_t* msg, size_t len);
/* Для локального автора проверить receipt и удалить только точный outbox; история/file receipt сохраняются.
* 0=удалено либо outbox уже отсутствует, -1=ошибка/конфликт. Требуется запущенный dm_core. */
int dm_accept_receipt(struct UTUN_INSTANCE* inst, const uint8_t receipt[DM_RECEIPT_SIZE]);
/* Жизненный цикл в uasync: init после chat_core_init с SQLite/router/ua ядра; 0=успех, -1=ошибка.
* Повторный init запущенного владельца возвращает 0. Для file-send/accept нужен также dm_media_init.
* destroy отменяет invite/таймер и снимает bind; БД/outbox сохраняются. Вызывать до chat_core_destroy. */
int dm_core_init(struct UTUN_INSTANCE* inst);
void dm_core_destroy(struct UTUN_INSTANCE* inst);
/* Начать беседу с пользователем (peer). source_ch_id — канал, где нашли target
* (общая группа; нужен для маршрутизации и подключения). Создаёт беседу если её
* ещё нет, выводит conv_id/ключи. Подключение для передачи медиа принадлежит media.
* Снимает hidden без смены поколения; публикует CONV_UPDATED с текущим поколением.
* Ключи обязательны, peer_name/source_ch_id могут быть NULL; переданные ключи пира проверяет вызывающий.
* Имя ограничено DM_PEER_NAME_MAX байтами по границе UTF-8; некорректный суффикс отбрасывается с WARN.
* Возвращает 0 при успехе, <0 при ошибке.
* Отказ чтения/проверки ключей не создаёт беседу, не меняет её поля/hidden и не выдаёт CONV_UPDATED.
* Создание разрешено только после успешного SELECT без строки; существующие seq сохраняются. */
int dm_start(struct UTUN_INSTANCE* inst, uint64_t peer_node_id,
const uint8_t peer_x25519[32], const uint8_t peer_ed25519[32],
const char* peer_name, const char* source_ch_id);
/* Отправить сообщение в беседу. Шифрует, подписывает, сохраняет локально и
* доставляет (прямое соединение при доступности пира, иначе mailbox).
* conv_id — строковое представление (как в dm_conversations.conv_id).
* Для ct="file" data=[descriptor:56][attachment_info в формате attachment_encode()], после подготовки dm_media.
* Descriptor подписывается открыто, метаданные вложения шифруются; формат — media_async/attachment.h.
* Для ct="invite" data=[group_name:utf8][NUL][полная ссылка:utf8], без завершающего NUL.
* Имя и ссылка шифруются и подписываются вместе; GUI получает data=ссылка, tags.group_name=имя.
* 0 после атомарного commit истории/seq/outbox, -1 при ошибке; доставка подтверждается отдельно.
* content_type до 255 байт, data_len до 1024 (для file вместе с descriptor); скрытая беседа отклоняется. */
int dm_send(struct UTUN_INSTANCE* inst, const char* conv_id, const char* content_type,
const uint8_t* data, uint32_t data_len);
/* Локальное удаление по dm_history_delete; generation защищает повторно открытую беседу.
* CHAT_EVT_DM_DELETE_RESULT: [request_id:8][conv:u64][generation:8][success:u8], всего 25 байт native-endian. */
struct dm_delete_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро до завершения запроса. */
char conv_id[64]; /* Десятичный ID беседы с NUL. */
uint64_t generation, /* Ненулевое ожидаемое поколение. */
request_id; /* Корреляция результата с запросом UI. */
};
void dm_delete_trampoline(void* arg);
/* После commit удаления отменить ожидающий invite этой беседы; квитанции/другие беседы не меняет. */
void dm_cancel_pending_history(struct UTUN_INSTANCE* inst, const char* conv);
/* До 200 видимых бесед по last_ts DESC: [{conv_id,peer_node_id,peer_name,last_ts,out_seq,in_seq,generation}].
* buf вмещает JSON и NUL; 0 и *out_len без NUL при успехе, -1 при ошибке. Все вызовы в uasync.
* Имя из БД либо nodes нормализуется по правилам dm_start; снимок не меняет сохранённые строки. */
int dm_list_conversations_json(struct UTUN_INSTANCE* inst, char* buf, size_t buf_size, size_t* out_len);
/* Сообщения беседы в JSON: расшифрованное содержимое, статусы доставки и медиа.
* count=1..200 задаёт предел; count<=0 или count>200 означает 200.
* offset>=0 отсчитывается от новых сообщений; offset=0 выбирает последние count.
* Выбранная страница возвращается по возрастанию ts, seq, dir.
* buf — выходной буфер с NUL; 0 и *out_len байт без NUL при успехе, -1 при ошибке.
* pending относится к квитанции сообщения. media_status: delivered/ready/downloading/expired/pending.
* hidden здесь не фильтруется; интерфейс выбирает видимую беседу по списку/поколению. */
int dm_list_messages_json(struct UTUN_INSTANCE* inst, const char* conv_id, int count, int offset,
char* buf, size_t buf_size, size_t* out_len);
/* ── Трамплины для gui_bridge (GUI → uasync) ── */
/* Начать беседу с узлом из общей группы. Трамплин сам находит x25519/ed25519
* пира в таблице nodes (как headless hc_handle_dm_start), после чего вызывает
* dm_start(). Результат — событие CHAT_EVT_DM_CONV_UPDATED. */
struct dm_start_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */
uint64_t peer_node_id; /* Другой участник общей группы; ключи читаются из nodes. */
char source_ch_id[64]; /* Десятичный ID общей группы либо пустая строка. */
char peer_name[128]; /* UTF-8 имя с NUL; dm_start ограничит до 63 байт. */
};
void dm_start_trampoline(void* arg);
/* Отправить текстовое сообщение в беседу (content_type="text"). */
struct dm_send_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */
char conv_id[64]; /* Десятичный ID беседы с NUL. */
uint64_t generation; /* 0: захватить при входе в uasync, до любой фоновой работы. */
uint32_t data_len; /* Число байт текста, до DM_DATA_MAX. */
uint8_t* data; /* Указывает на data_len байт в том же блоке после структуры. */
};
void dm_send_trampoline(void* arg);
/* Приглашение в выбранный канал через PM: свежая ссылка после ACK регистрации,
* затем dm_send("invite") с названием группы. Получатель фиксирован независимо от текущего UI.
* Один ожидающий запрос на instance; результат CHAT_EVT_DM_INVITE_RESULT.
* Результат: [request_id:8][result:4][peer:8][conv:8][generation:8], всего 36 байт native-endian.
* cancel=1 отменяет только ожидающий request_id; отправленное PM не отзывается.
* arg=u_calloc(dm_invite_req); обработчик/финальный callback освобождает arg. */
enum dm_invite_result {
DM_INVITE_OK = 0, DM_INVITE_START_ERROR = -1, DM_INVITE_BUILD_ERROR = -2,
DM_INVITE_SEND_ERROR = -3, DM_INVITE_CANCELLED = -4, DM_INVITE_BUSY = -5
};
struct dm_invite_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро до итогового callback. */
uint64_t request_id, /* Ненулевой ID; cancel использует тот же ID. */
peer_node_id, /* Фиксированный получатель, не локальный узел. */
channel_id, /* Выбранная активная CHAT-группа отправителя. */
generation; /* Заполняет uasync до регистрации; проверяет callback. */
char source_ch_id[64]; /* общая группа для новой PM; из шапки PM может быть пустой */
char peer_name[128]; /* UTF-8 имя для новой беседы с NUL. */
int cancel; /* 0=создать, 1=отменить ожидающий request_id. */
struct chat_invite_build* pending; /* Внутренний handle; запрос создаётся обнулённым. */
};
void dm_invite_trampoline(void* arg);
/* Запросить последние 200 сообщений со статусами. CHAT_EVT_DM_MESSAGES:
* [conv_id_len:1][conv_id:var][generation:8][JSON:utf8]. Все чтения в uasync.
* Ненулевое generation проверяется до чтения; 0 захватывает текущее поколение. GUI отбрасывает старый снимок. */
struct dm_messages_req {
struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */
char conv_id[64]; /* Десятичный ID с NUL, также для запроса маршрута звонка. */
uint64_t generation; /* Поколение выбранного PM; для запроса маршрута не используется. */
};
void dm_messages_trampoline(void* arg);
/* 1 при живом BGP-маршруте общей группы, 0 при недоступности/ошибке. peer/group_id обязательны.
* При корректных аргументах выходы сначала обнуляются; peer может быть известен и без маршрута.
* Трамплин выдаёт CHAT_EVT_DM_CALL_ROUTE: [conv_len:1][conv][peer:8][group:8]; group=0 без маршрута. */
int dm_call_route(struct UTUN_INSTANCE* inst, const char* conv_id, uint64_t* peer, uint64_t* group_id);
void dm_call_route_trampoline(void* arg); /* аргумент: dm_messages_req */
/* Снимок бесед публикуется как CHAT_EVT_DM_CONVERSATIONS с JSON без NUL. */
struct dm_conversations_req { struct UTUN_INSTANCE* inst; /* Заимствованное ядро. */ };
void dm_conversations_trampoline(void* arg);
#ifdef __cplusplus
}
#endif
#endif /* DM_CORE_H */