diff --git a/doc/tasks.md b/doc/tasks.md index 0db5b25e..095a5cdb 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -4,6 +4,15 @@ ## Текущие задачи +[+] **chat headless: полный разбор JSON,целые TCP-кадры и результат публикации** — 27.10.2026. + SQLite проверяет весь объект до dispatch;параметры типизированы и копируются без усечения,дубли/NUL/JSON5 запрещены. + TX сохраняет FIFO при short write/EAGAIN и освобождает отправленный префикс;переполнение закрывает клиента с WARN. + Status/candidates/invite names используют checked JSON escaping;прежний event handler сохраняется/восстанавливается. + send проверяет результат,attach отвечает из нового media completion после commit/отказа;disconnect отсоединяет адресата. + Контекст completion освобождается после закрытия control;quit дописывает bye,init полностью убирает частичные ресурсы. + test_chat_headless_control:настоящие TCP/SQLite/media,Unicode/опросы,SQL finalize fault,short write/pressure, + async disconnect и handler restore PASS;services/chat_attachment/media_index_regressions PASS. + [+] **chat: атомарное глобальное имя и подписанный снимок** — 27.10.2026. Один commit сохраняет local_identity,nodes,NODEINFO и полные собственные профили каналов/Merkle. Память/LAN/уведомления меняются после commit;отказ сохраняет все представления,no-op не меняет версии. diff --git a/src/chat/chat_attachment.c b/src/chat/chat_attachment.c index a5694649..07f3528a 100644 --- a/src/chat/chat_attachment.c +++ b/src/chat/chat_attachment.c @@ -236,7 +236,7 @@ static void ak_finish_file(struct ak_file* job) { snprintf(request.media_src, sizeof(request.media_src), "%s", job->cipher); snprintf(request.media_dest, sizeof(request.media_dest), "%s", job->cipher); memcpy(request.reply_to, job->reply, 32); - chat_core_submit_media_message(inst, &request); + chat_core_submit_media_message(inst, &request, NULL, NULL); } else if (!success) { if (valid) DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "attachment keys: worker failed channel=%s encrypt=%d error=%d worker=%d", job->channel, job->encrypt, job->completion_error, job->error); diff --git a/src/chat/chat_core.h b/src/chat/chat_core.h index 780ade4f..5d6ee0f7 100644 --- a/src/chat/chat_core.h +++ b/src/chat/chat_core.h @@ -93,8 +93,12 @@ int chat_core_edit_message(struct UTUN_INSTANCE* inst, const char* channel, cons /* Подготовить файл в worker, затем сохранить и разослать сообщение; результат — CHAT_EVT_MSG_SUBMITTED при request_id!=0. * req и data копируются до возврата и остаются у вызывающего; файлы должны жить до завершения подготовки. - * media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. */ -void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); + * media_copy копирует src в dest, media_video запускает транскодирование. Ошибка также приходит событием. + * Необязательный complete(arg,success) вызывается ровно один раз после сохранения/отказа и освобождения задачи, + * в том числе синхронно при отказе запуска;arg принадлежит caller и живёт до callback. */ +typedef void (*chat_media_submitted_fn)(void* arg, int success); +void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req, + chat_media_submitted_fn complete, void* arg); /* Ручная загрузка по локальному номеру строки; внешняя SQL-транзакция запрещена. * Manifest ограничен общим DB_SYNC_MESSAGE_MAX. Зашифрованный индекс восстанавливается из истории до wanted/загрузки; diff --git a/src/chat/chat_headless_control.c b/src/chat/chat_headless_control.c index f3c134df..5349ddba 100644 --- a/src/chat/chat_headless_control.c +++ b/src/chat/chat_headless_control.c @@ -28,6 +28,8 @@ #include "../transport_layer/etcp_connections.h" #include "../utun_instance.h" #include "../ntp_time.h" +#include "../media_delivery/media_index.h" +#include "../../lib/file_io.h" #include "../../lib/ll_queue.h" #include "../../lib/u_async.h" #include "../../lib/mem.h" @@ -42,19 +44,34 @@ #include #include #include +#include #include #include #include +#include #define HC_ID "headless" #define RECV_BUF_SIZE 16384 #define SEND_BUF_SIZE 32768 #define MAX_CLIENTS 64 +#define MAX_FIELDS 32 + +struct hc_field { + char key[32]; + const char* text; /* Декодированная строка из arena запроса. */ + int integer, kind; /* kind:1=text,2=int/bool,0=array/object/прочее для feedback. */ +}; +struct hc_request { + struct hc_field fields[MAX_FIELDS]; + unsigned count; + size_t used; + char arena[RECV_BUF_SIZE]; /* Живёт до возврата синхронного handler. */ +}; #ifndef DEBUG_CATEGORY_HEADLESS -#define DEBUG_CATEGORY_HEADLESS DEBUG_CATEGORY_GENERAL +#define DEBUG_CATEGORY_HEADLESS DEBUG_CATEGORY_CONTROL #endif struct headless_client { @@ -67,7 +84,10 @@ struct headless_client { int send_offset; int subscribed; int closing; + int draining; + const struct hc_request* request; struct headless_invite* invites; + struct headless_submission* submissions; struct headless_control* hc; struct headless_client* next; }; @@ -79,6 +99,13 @@ struct headless_invite { int id; }; +struct headless_submission { + struct headless_submission* next; + struct headless_client* client; /* При disconnect обнуляется;последний media completion освобождает объект. */ + int id; + char destination[1024]; +}; + struct headless_control { struct UASYNC* ua; struct UTUN_INSTANCE* inst; @@ -88,6 +115,9 @@ struct headless_control { void* listen_sock_id; int running; void* cleanup_timer; + chat_event_handler_fn previous_handler; + char bind_ip[INET_ADDRSTRLEN]; + int configured_port; }; static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) { @@ -98,71 +128,109 @@ static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) { static void hc_handle_command(struct headless_client* cli, const char* json); static void hc_close_client(struct headless_client* cli); +static void client_write_callback(socket_t fd, void* arg); /* ── JSON helpers ── */ -static int json_get_int(const char* s, const char* key, int def) { - char pat[128]; snprintf(pat, sizeof(pat), "\"%s\"", key); - const char* p = strstr(s, pat); if (!p) return def; - p += strlen(pat); - while (*p == ' ' || *p == '\t' || *p == ':') p++; - return (int)strtol(p, NULL, 10); +/* Все значения уже проверены/скопированы,SQL cursor закрыт до выполнения команды. */ +static int json_get_int(struct headless_client* cli, const char* key, int def) { + for (unsigned i = 0; i < cli->request->count; i++) + if (!strcmp(cli->request->fields[i].key, key)) return cli->request->fields[i].integer; + return def; } -static int json_get_str(const char* s, const char* key, char* out, size_t out_sz) { - char pat[128]; snprintf(pat, sizeof(pat), "\"%s\"", key); - const char* p = strstr(s, pat); if (!p) return -1; - p += strlen(pat); - while (*p == ' ' || *p == '\t' || *p == ':') p++; - if (*p == '"') p++; - size_t i = 0; - while (*p && *p != '"' && i < out_sz - 1) { - if (*p == '\\' && *(p + 1)) { p++; out[i++] = *(p++); } - else out[i++] = *(p++); +static int json_get_str(struct headless_client* cli, const char* key, char* out, size_t size) { + if (size) out[0] = 0; + for (unsigned i = 0; i < cli->request->count; i++) { + const struct hc_field* field = &cli->request->fields[i]; + if (strcmp(field->key, key)) continue; + if (field->kind != 1 || strlen(field->text) >= size) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: invalid string parameter key=%s capacity=%zu", key, size); return -1; + } + memcpy(out, field->text, strlen(field->text) + 1); return 0; } - out[i] = '\0'; return 0; + return -1; } -static const char* json_get_cmd(const char* s) { - const char* p = strstr(s, "\"cmd\""); - if (!p) return NULL; - p += 5; - while (*p == ' ' || *p == '\t' || *p == ':') p++; - if (*p == '"') p++; - return p; -} -static const char* json_get_cmd_end(const char* cmd_start) { - const char* p = cmd_start; - while (*p && *p != '"') { if (*p == '\\' && *(p + 1)) p++; p++; } - return p; + +/* Один полный разбор объекта;у feedback остаётся исходный JSON с массивами/типами. */ +static int hc_parse_request(struct headless_client* cli, const char* json, struct hc_request* request) { + sqlite3* db = chat_core_get_db(cli->hc->inst); sqlite3_stmt* st = NULL; + int rc = sqlite3_prepare_v2(db, "SELECT CASE WHEN json_valid(?1) THEN json_type(?1) END,key,type,value FROM json_each(?1)", -1, &st, NULL); + if (rc == SQLITE_OK) rc = sqlite3_bind_text(st, 1, json, -1, SQLITE_STATIC); + while (rc == SQLITE_OK || rc == SQLITE_ROW) { + rc = sqlite3_step(st); if (rc != SQLITE_ROW) break; + const char *root = (const char*)sqlite3_column_text(st, 0), *key = (const char*)sqlite3_column_text(st, 1); + const char* type = (const char*)sqlite3_column_text(st, 2); + if (!root || strcmp(root, "object") || !key || !type || request->count == MAX_FIELDS || + strlen(key) != (size_t)sqlite3_column_bytes(st, 1) || strlen(key) >= sizeof(request->fields[0].key)) { + rc = SQLITE_MISMATCH; break; + } + int duplicate = 0; + for (unsigned i = 0; i < request->count; i++) duplicate |= !strcmp(request->fields[i].key, key); + if (duplicate) { rc = SQLITE_CONSTRAINT; break; } + struct hc_field* field = &request->fields[request->count++]; strcpy(field->key, key); + if (!strcmp(type, "text")) { + const char* value = (const char*)sqlite3_column_text(st, 3); int bytes = sqlite3_column_bytes(st, 3); + if (!value || strlen(value) != (size_t)bytes || (size_t)bytes + 1 > sizeof(request->arena) - request->used) { + rc = SQLITE_MISMATCH; break; + } + memcpy(request->arena + request->used, value, (size_t)bytes + 1); + field->kind = 1; field->text = request->arena + request->used; request->used += (size_t)bytes + 1; + } else if (!strcmp(type, "integer") || !strcmp(type, "true") || !strcmp(type, "false")) { + sqlite3_int64 value = sqlite3_column_int64(st, 3); + if (sqlite3_column_type(st, 3) != SQLITE_INTEGER || value < INT_MIN || value > INT_MAX) { + rc = SQLITE_RANGE; break; + } + field->kind = 2; field->integer = (int)value; + } + int integer = !strcmp(key, "id") || !strcmp(key, "count") || !strcmp(key, "offset") || + !strcmp(key, "as_video") || !strcmp(key, "proto") || !strcmp(key, "enable"); + if (integer && (field->kind != 2 || (!strcmp(type, "true") || !strcmp(type, "false")) && + strcmp(key, "enable") && strcmp(key, "as_video"))) { rc = SQLITE_MISMATCH; break; } + if ((!strcmp(key, "enable") || !strcmp(key, "as_video")) && field->integer != 0 && field->integer != 1) { + rc = SQLITE_RANGE; break; + } + if (!strcmp(key, "offset") && field->integer < 0) { rc = SQLITE_RANGE; break; } + const struct { const char* key; size_t size; } limits[] = { + {"cmd",32},{"ch",64},{"name",128},{"data",4096},{"content_type",32},{"path",1024}, + {"link",INVITE_LINK_MAX},{"node_id",32},{"call_id",32},{"conv_id",64},{"pubkey",65},{"addr",128} + }; + for (unsigned i = 0; i < sizeof(limits) / sizeof(limits[0]); i++) if (!strcmp(key, limits[i].key) && + (field->kind != 1 || strlen(field->text) >= limits[i].size)) { rc = SQLITE_MISMATCH; break; } + if (rc != SQLITE_ROW) break; + if ((!strcmp(key, "ch") && field->text[0]) || !strcmp(key, "conv_id")) { + const char* identity = !strcmp(key, "ch") && !strncmp(field->text, "dm:", 3) ? field->text + 3 : field->text; + if (!chat_channel_id(identity)) { rc = SQLITE_MISMATCH; break; } + } + if (!strcmp(key, "node_id") || !strcmp(key, "call_id")) { + const char* digits = field->text; + if (!strncmp(digits, "0x", 2) || !strncmp(digits, "0X", 2)) digits += 2; + size_t length = strlen(digits); + if (!length || length > 16 || strspn(digits, "0123456789abcdefABCDEF") != length || + (!strcmp(key, "call_id") && !strtoull(digits, NULL, 16))) { rc = SQLITE_MISMATCH; break; } + } + } + int final = sqlite3_finalize(st); + if (rc != SQLITE_DONE || final != SQLITE_OK) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: request rejected fields=%u rc=%d finalize=%d: %s", + request->count, rc, final, db ? sqlite3_errmsg(db) : "no chat DB"); return -1; + } + return 0; } /* ── Send helpers ── */ static void cli_send(struct headless_client* cli, const char* data, size_t len) { - struct headless_control* hc = cli->hc; if (!cli || cli->closing || cli->fd == SOCKET_INVALID || !data || len == 0) return; - if (cli->send_len > 0) { - size_t space = sizeof(cli->send_buf) - (size_t)cli->send_len; - if (len + 1 > space) return; - memcpy(cli->send_buf + cli->send_len, data, len); - cli->send_len += (int)len; - return; - } - ssize_t r = send(cli->fd, data, (int)len, 0); - if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) { - if (len <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data, len); cli->send_len = (int)len; cli->send_offset = 0; } - if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1); - return; - } - if (r >= 0 && (size_t)r < len) { - size_t remain = len - (size_t)r; - if (remain <= sizeof(cli->send_buf)) { memcpy(cli->send_buf, data + r, remain); cli->send_len = (int)remain; cli->send_offset = 0; } - if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 1); - } - if (r < 0) { - DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed err=%d, closing client", socket_get_error()); - hc_close_client(cli); + int pending = cli->send_len - cli->send_offset; + if (len > sizeof(cli->send_buf) - (size_t)pending) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: slow client closed fd=%d pending=%d incoming=%zu limit=%zu", + (int)cli->fd, pending, len, sizeof(cli->send_buf)); hc_close_client(cli); return; } + if (pending && cli->send_offset) memmove(cli->send_buf, cli->send_buf + cli->send_offset, (size_t)pending); + memcpy(cli->send_buf + pending, data, len); cli->send_len = pending + (int)len; cli->send_offset = 0; + uasync_set_socket_write(cli->hc->ua, cli->socket_id, 1); + client_write_callback(cli->fd, cli); } static void cli_send_str(struct headless_client* cli, const char* s) { @@ -172,11 +240,12 @@ static void cli_send_str(struct headless_client* cli, const char* s) { static void send_response(struct headless_client* cli, int id, const char* ok_data, const char* error) { struct strbuf out = strbuf_new(); int rc; if (error) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: command failed id=%d reason=%s", id, error); rc = strbuf_addf(&out, "{\"id\":%d,\"ok\":false,\"error\":", id); if (rc >= 0) rc = strbuf_add_json_string(&out, error); if (rc >= 0) rc = strbuf_addf(&out, "}\n"); } else rc = strbuf_addf(&out, "{\"id\":%d,\"ok\":true,\"data\":%s}\n", id, ok_data ? ok_data : "null"); - if (rc < 0 || out.len + (size_t)cli->send_len > SEND_BUF_SIZE) { + if (rc < 0 || out.len > SEND_BUF_SIZE) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: response rejected id=%d bytes=%zu queued=%d rc=%d", id, out.len, cli->send_len, rc); char failure[160]; int bytes = snprintf(failure, sizeof(failure), @@ -191,17 +260,31 @@ static void send_response(struct headless_client* cli, int id, const char* ok_da static void hc_broadcast_event(struct headless_control* hc, const char* json) { size_t jlen = strlen(json); char buf[4096]; int blen = snprintf(buf, sizeof(buf), "%s\n", json); - if (blen <= 0) return; + if (blen <= 0 || (size_t)blen >= sizeof(buf)) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: event exceeds limit bytes=%zu", jlen); return; } for (struct headless_client* c = hc->clients; c; c = c->next) - if (c->subscribed) cli_send(c, buf, (size_t)blen); + if (c->subscribed && !c->draining) cli_send(c, buf, (size_t)blen); } /* ── chat_event callback ── */ -static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { - struct headless_control* hc = hc_of(inst); - if (!hc) return; +static void hc_emit_chat_event(struct headless_control* hc, int type, const uint8_t* data, int len) { if (type == 10) return; /* skip status text */ + if (type == CHAT_EVT_MSG_RECEIVED || type == CHAT_EVT_MEMBERS_CHANGED || type == CHAT_EVT_CHANNEL_UPDATED || + type == CHAT_EVT_INVITE_RECEIVED || type == CHAT_EVT_DM_MSG_RECEIVED || type == CHAT_EVT_DM_CONV_UPDATED || + type == CHAT_EVT_CALL_INCOMING || type == CHAT_EVT_FEEDBACK_CHANGED) { + if (!data || len < 1 || !data[0] || data[0] > 20 || len < 1 + data[0]) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: invalid event prefix type=%d bytes=%d", type, len); return; + } + char channel[21]; memcpy(channel, data + 1, data[0]); channel[data[0]] = 0; + if (!chat_channel_id(channel) || strlen(channel) != data[0]) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: invalid event identity type=%d bytes=%d", type, len); return; + } + int expected = type == CHAT_EVT_MSG_RECEIVED ? data[0] + 10 : + type == CHAT_EVT_MEMBERS_CHANGED || type == CHAT_EVT_CHANNEL_UPDATED ? data[0] + 1 : + type == CHAT_EVT_DM_MSG_RECEIVED || type == CHAT_EVT_DM_CONV_UPDATED ? data[0] + 9 : + type == CHAT_EVT_CALL_INCOMING ? data[0] + 17 : type == CHAT_EVT_FEEDBACK_CHANGED ? data[0] + 34 : len; + if (len != expected) { DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: invalid event size type=%d bytes=%d expected=%d", type, len, expected); return; } + } char json[1024]; json[0] = '\0'; if (type == CHAT_EVT_FEEDBACK_CHANGED && data && len >= 34 && len == data[0] + 34) { uint8_t cl = data[0]; char message_id[65]; @@ -229,8 +312,17 @@ static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t uint8_t nl = data[1 + cl]; uint64_t inviter; int off = 2 + cl + nl; if (len >= off + 8) { memcpy(&inviter, data + off, 8); - snprintf(json, sizeof(json), "{\"event\":\"invite_received\",\"ch\":\"%.*s\",\"ch_name\":\"%.*s\",\"from_id\":\"0x%016llx\"}", - (int)cl, data + 1, (int)nl, data + 2 + cl, (unsigned long long)inviter); + if (len < off + 9 || len != off + 9 + data[off + 8] || memchr(data + 2 + cl, 0, nl)) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: malformed invite event bytes=%d", len); return; + } + char name[256]; memcpy(name, data + 2 + cl, nl); name[nl] = 0; + struct strbuf event = strbuf_new(); + int rc = strbuf_addf(&event, "{\"event\":\"invite_received\",\"ch\":\"%.*s\",\"ch_name\":", (int)cl, data + 1); + if (rc >= 0) rc = strbuf_add_json_string(&event, name); + if (rc >= 0) rc = strbuf_addf(&event, ",\"from_id\":\"0x%016llx\"}", (unsigned long long)inviter); + if (rc >= 0) hc_broadcast_event(hc, strbuf_str(&event)); + else DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite event allocation failed"); + strbuf_free(&event); return; } } } else if (type == 32 && data && len >= 10) { /* DM_MSG_RECEIVED */ @@ -261,10 +353,27 @@ static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t } else if (type == CHAT_EVT_CALL_DECLINED && data && len >= 9) { /* CALL_DECLINED */ uint64_t cid; memcpy(&cid, data, 8); snprintf(json, sizeof(json), "{\"event\":\"call_declined\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]); + } else if (type == CHAT_EVT_CALL_ERROR && data && len == 9) { + uint64_t cid; memcpy(&cid, data, 8); + snprintf(json, sizeof(json), "{\"event\":\"call_error\",\"call_id\":\"0x%016llx\",\"error\":%u}", (unsigned long long)cid, data[8]); + } else if (type == CHAT_EVT_CONNECT_RESULT && data && len == 28) { + uint64_t peer, channel, request; int32_t result; + memcpy(&peer, data, 8); memcpy(&result, data + 8, 4); memcpy(&channel, data + 12, 8); memcpy(&request, data + 20, 8); + snprintf(json, sizeof(json), "{\"event\":\"connect_result\",\"node_id\":\"0x%016llx\",\"ch\":\"%llu\",\"request\":%llu,\"result\":%d}", + (unsigned long long)peer, (unsigned long long)channel, (unsigned long long)request, (int)result); } if (json[0]) hc_broadcast_event(hc, json); } +/* Headless дополняет прежнего получателя;после его callback к hc больше не обращаемся. */ +static void hc_on_chat_event(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + struct headless_control* hc = hc_of(inst); + if (!hc) return; + chat_event_handler_fn previous = hc->previous_handler; + hc_emit_chat_event(hc, type, data, len); + if (previous) previous(inst, type, data, len); +} + /* ── Write callback ── */ static void client_write_callback(socket_t fd, void* arg) { @@ -272,7 +381,7 @@ static void client_write_callback(socket_t fd, void* arg) { struct headless_control* hc = cli ? cli->hc : NULL; if (!cli || cli->closing || fd != cli->fd) return; if (cli->send_len <= 0) { if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); return; } - ssize_t r = send(cli->fd, cli->send_buf + cli->send_offset, (int)(cli->send_len - cli->send_offset), 0); + ssize_t r = socket_send(cli->fd, cli->send_buf + cli->send_offset, (size_t)(cli->send_len - cli->send_offset)); if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return; if (r <= 0) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: send failed (r=%zd err=%d) — closing client", r, socket_get_error()); @@ -280,7 +389,11 @@ static void client_write_callback(socket_t fd, void* arg) { return; } cli->send_offset += (int)r; - if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); } + if (cli->send_offset >= cli->send_len) { + cli->send_len = 0; cli->send_offset = 0; + if (hc && hc->ua) uasync_set_socket_write(hc->ua, cli->socket_id, 0); + if (cli->draining) hc_close_client(cli); + } } /* ── Close client ── */ @@ -290,6 +403,9 @@ static void hc_close_client(struct headless_client* cli) { if (!cli || cli->closing) return; cli->closing = 1; while (cli->invites) chat_invite_build_cancel(cli->invites->pending); + while (cli->submissions) { + struct headless_submission* pending = cli->submissions; cli->submissions = pending->next; pending->client = NULL; + } if (hc && hc->ua && cli->socket_id) uasync_remove_socket_t(hc->ua, cli->fd); cli->socket_id = NULL; if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; } @@ -299,15 +415,22 @@ static void hc_close_client(struct headless_client* cli) { static void client_read_callback(socket_t fd, void* arg) { struct headless_client* cli = (struct headless_client*)arg; - if (!cli || cli->closing) return; + if (!cli || cli->closing || cli->draining || fd != cli->fd) return; char tmp[4096]; ssize_t r = recv(fd, tmp, (int)sizeof(tmp) - 1, 0); - if (r <= 0) { hc_close_client(cli); return; } + if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return; + if (r <= 0) { + if (r < 0) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: receive failed err=%d", socket_get_error()); + hc_close_client(cli); return; + } for (ssize_t i = 0; i < r; i++) { if (tmp[i] == '\n') { if (cli->recv_len > 0) { cli->recv_buf[cli->recv_len] = '\0'; hc_handle_command(cli, cli->recv_buf); cli->recv_len = 0; } - if (cli->closing) return; - } else if (cli->recv_len < RECV_BUF_SIZE - 1) { + if (cli->closing || cli->draining) return; + } else if (!tmp[i] || cli->recv_len == RECV_BUF_SIZE - 1) { + DEBUG_WARN(DEBUG_CATEGORY_HEADLESS, "headless: invalid/oversized input fd=%d bytes=%d", (int)fd, cli->recv_len); + hc_close_client(cli); return; + } else { cli->recv_buf[cli->recv_len++] = tmp[i]; } } @@ -324,38 +447,29 @@ static void hc_handle_ping(struct headless_client* cli, int id, const char* json static void hc_handle_status(struct headless_client* cli, int id, const char* json) { (void)json; struct headless_control* hc = cli->hc; - char buf[8192]; int off = 0; - off += snprintf(buf + off, sizeof(buf) - off, "\""); - if (!hc->inst) { - off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance"); - } else { - int conn_count = hc->inst->connections ? queue_entry_count(hc->inst->connections) : 0; - struct uasync_load load; - if (uasync_get_load(hc->ua, &load) == ERR_OK && load.valid) { - off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: %.1f%%, average %.1f%% (window %.0f ms, age %.0f ms)\\n", - load.current_percent, load.average_percent, load.window_us / 1000.0, - (get_time_us() - load.sample_time_us) / 1000.0); - } else off += snprintf(buf + off, sizeof(buf) - off, "uasync CPU: unavailable\\n"); - struct NTP_TIME* ntp = &hc->inst->ntp; - off += snprintf(buf + off, sizeof(buf) - off, "NTP: enabled=%s synced=%s offset=%.1fs\\n", - ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0); - off += snprintf(buf + off, sizeof(buf) - off, "Connections: %d active\\n", conn_count); - uint64_t my_id = hc->inst->node_id; - struct ll_entry* entry = hc->inst->connections ? hc->inst->connections->head : NULL; - while (entry) { - struct conn_queue_entry* ce = (struct conn_queue_entry*)entry->data; - if (ce) { - char name[64]; chat_core_get_node_name(hc->inst, ce->peer_node_id, name, sizeof(name)); - off += snprintf(buf + off, sizeof(buf) - off, " [%04llX]->[%04llX] %s ETCP:%s\\n", - (unsigned long long)(my_id & 0xFFFF), (unsigned long long)(ce->peer_node_id & 0xFFFF), - name[0] ? name : "?", ce->conn && ce->conn->links_up ? "UP" : "DOWN"); - } - entry = entry->next; - } + struct strbuf text = strbuf_new(), encoded = strbuf_new(); struct uasync_load load; + int rc; + if (uasync_get_load(hc->ua, &load) == ERR_OK && load.valid) + rc = strbuf_addf(&text, "uasync CPU: %.1f%%, average %.1f%% (window %.0f ms, age %.0f ms)\n", + load.current_percent, load.average_percent, load.window_us / 1000.0, (get_time_us() - load.sample_time_us) / 1000.0); + else rc = strbuf_addf(&text, "uasync CPU: unavailable\n"); + struct NTP_TIME* ntp = &hc->inst->ntp; + if (rc >= 0) rc = strbuf_addf(&text, "NTP: enabled=%s synced=%s offset=%.1fs\nConnections: %d active\n", + ntp->enabled ? "yes" : "no", ntp->synced ? "yes" : "no", ntp->offset_us / 1000000.0, + hc->inst->connections ? queue_entry_count(hc->inst->connections) : 0); + for (struct ll_entry* e = hc->inst->connections ? hc->inst->connections->head : NULL; e && rc >= 0; e = e->next) { + struct conn_queue_entry* conn = (struct conn_queue_entry*)e->data; + if (!conn) continue; + char name[64]; + if (chat_core_get_node_name(hc->inst, conn->peer_node_id, name, sizeof(name))) { rc = -1; break; } + rc = strbuf_addf(&text, " [%04llX]->[%04llX] %s ETCP:%s\n", (unsigned long long)(hc->inst->node_id & 0xffff), + (unsigned long long)(conn->peer_node_id & 0xffff), name[0] ? name : "?", conn->conn && conn->conn->links_up ? "UP" : "DOWN"); + if (text.len > SEND_BUF_SIZE) rc = -1; } - if (off >= (int)sizeof(buf) - 2) off = (int)sizeof(buf) - 2; - off += snprintf(buf + off, sizeof(buf) - off, "\""); - send_response(cli, id, buf, NULL); + if (rc >= 0) rc = strbuf_add_json_string(&encoded, strbuf_str(&text)); + if (rc < 0) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: status snapshot failed bytes=%zu", text.len); + send_response(cli, id, rc < 0 ? NULL : strbuf_str(&encoded), rc < 0 ? "status snapshot failed or too large" : NULL); + strbuf_free(&text); strbuf_free(&encoded); } static void hc_handle_channels(struct headless_client* cli, int id, const char* json) { @@ -369,7 +483,7 @@ static void hc_handle_channels(struct headless_client* cli, int id, const char* static void hc_handle_members(struct headless_client* cli, int id, const char* json) { char ch[64]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } char buf[16384]; size_t len = 0; if (chat_core_get_members_json(cli->hc->inst, ch, buf, sizeof(buf), &len) < 0) send_response(cli, id, NULL, "failed to query members"); @@ -379,9 +493,9 @@ static void hc_handle_members(struct headless_client* cli, int id, const char* j static void hc_handle_messages(struct headless_client* cli, int id, const char* json) { char ch[64]; int count = 20, offset = 0; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } - count = json_get_int(json, "count", 20); - offset = json_get_int(json, "offset", 0); + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + count = json_get_int(cli, "count", 20); + offset = json_get_int(cli, "offset", 0); char buf[16384]; size_t len = 0; if (chat_core_get_messages_json(cli->hc->inst, ch, count, offset, buf, sizeof(buf), &len) < 0) send_response(cli, id, NULL, "failed to query messages"); @@ -391,20 +505,19 @@ static void hc_handle_messages(struct headless_client* cli, int id, const char* static void hc_handle_send(struct headless_client* cli, int id, const char* json) { char ch[64], ct[32], data[4096]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } - if (json_get_str(json, "content_type", ct, sizeof(ct)) < 0) { ct[0] = 't'; ct[1] = 'e'; ct[2] = 'x'; ct[3] = 't'; ct[4] = '\0'; } - if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "content_type", ct, sizeof(ct)) < 0) { ct[0] = 't'; ct[1] = 'e'; ct[2] = 'x'; ct[3] = 't'; ct[4] = '\0'; } + if (json_get_str(cli, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; } struct chat_msg_submit* req = u_calloc(1, sizeof(*req)); if (!req) { send_response(cli, id, NULL, "out of memory"); return; } req->inst = cli->hc->inst; snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch); snprintf(req->content_type, sizeof(req->content_type), "%s", ct); - req->data = (uint8_t*)u_strdup(data); + req->data = (uint8_t*)data; req->data_len = (uint32_t)strlen(data); - chat_core_submit_message(cli->hc->inst, req); - if (req->data) u_free(req->data); + int result = chat_core_submit_message(cli->hc->inst, req); u_free(req); - send_response(cli, id, "{\"sent\":true}", NULL); + send_response(cli, id, result ? NULL : "{\"sent\":true}", result ? "message save failed" : NULL); } static int hc_mkdirs(const char* path) { @@ -420,53 +533,65 @@ static int hc_mkdirs(const char* path) { return 0; } +/* Callback не зависит от lifetime control-сервера после disconnect/stop. */ +static void hc_media_ready(void* arg, int success) { + struct headless_submission* pending = arg; struct headless_client* cli = pending->client; + if (!cli) { u_free(pending); return; } + struct headless_submission** slot = &cli->submissions; + while (*slot && *slot != pending) slot = &(*slot)->next; + if (*slot) *slot = pending->next; + if (cli->draining) { u_free(pending); return; } + struct strbuf response = strbuf_new(); int rc = -1, id = pending->id; + if (success) { + rc = strbuf_addf(&response, "{\"attached\":true,\"dest\":"); + if (rc >= 0) rc = strbuf_add_json_string(&response, pending->destination); + if (rc >= 0) rc = strbuf_addf(&response, "}"); + } + u_free(pending); + send_response(cli, id, rc >= 0 ? strbuf_str(&response) : NULL, rc >= 0 ? NULL : "attachment publication failed"); + strbuf_free(&response); +} + static void hc_handle_attach(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char ch[64], path[1024], ct[32]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } - if (json_get_str(json, "path", path, sizeof(path)) < 0) { send_response(cli, id, NULL, "missing 'path' param"); return; } - if (json_get_str(json, "content_type", ct, sizeof(ct)) < 0) + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "path", path, sizeof(path)) < 0) { send_response(cli, id, NULL, "missing 'path' param"); return; } + if (json_get_str(cli, "content_type", ct, sizeof(ct)) < 0) snprintf(ct, sizeof(ct), "application/octet-stream"); - int as_video = json_get_int(json, "as_video", 0); - - struct stat st; - if (stat(path, &st) != 0 || st.st_size <= 0) { send_response(cli, id, NULL, "file not found or empty"); return; } + int as_video = json_get_int(cli, "as_video", 0); + if (!chat_channel_id(ch) || !si_find(hc->inst, ch) || (as_video != 0 && as_video != 1)) { + send_response(cli, id, NULL, "invalid/unavailable channel or video flag"); return; + } + struct file_io_info info; + if (file_io_inspect(path, 1, &info) || info.type != FILE_IO_REGULAR || info.size <= 0) { + send_response(cli, id, NULL, "regular nonempty file required"); return; + } if (!hc->inst || !hc->inst->media_async) { send_response(cli, id, NULL, "media engine not initialized"); return; } - /* media_base из ui_state (как в блок-сервере media_delivery) */ sqlite3* db = chat_core_get_db(hc->inst); - char media_base[512] = "/tmp/utun_media"; - if (db) { - sqlite3_stmt* ms = NULL; - if (sqlite3_prepare_v2(db, "SELECT value FROM ui_state WHERE key='media_base'", -1, &ms, NULL) == SQLITE_OK) { - if (sqlite3_step(ms) == SQLITE_ROW) { - const char* mb = (const char*)sqlite3_column_text(ms, 0); - if (mb) snprintf(media_base, sizeof(media_base), "%s", mb); - } - sqlite3_finalize(ms); - } - } + char media_base[MEDIA_BASE_MAX]; + if (media_index_get_base(db, media_base)) { send_response(cli, id, NULL, "media base unavailable"); return; } const char* base = strrchr(path, '/'); base = base ? base + 1 : path; - char name[256]; - { size_t blen = strlen(base); if (blen >= sizeof(name)) blen = sizeof(name) - 1; - memcpy(name, base, blen); name[blen] = '\0'; } + if (!*base || strlen(base) >= 256) { send_response(cli, id, NULL, "invalid attachment name"); return; } + char name[256]; strcpy(name, base); - char dir[640]; snprintf(dir, sizeof(dir), "%s/%s", media_base, ch); + char dir[640]; snprintf(dir, sizeof(dir), "%s/media/%s", media_base, ch); if (hc_mkdirs(dir) != 0) { send_response(cli, id, NULL, "failed to create media dir"); return; } - char dest[1024]; - uint64_t ts = (uint64_t)time(NULL); - if (as_video) { - char* dot = strrchr(name, '.'); if (dot) *dot = '\0'; - snprintf(dest, sizeof(dest), "%s/%llu_%s.mp4", dir, (unsigned long long)ts, name); - } else { - snprintf(dest, sizeof(dest), "%s/%llu_%s", dir, (unsigned long long)ts, name); - } + uint8_t uuid[16]; char hex[33], dest[1024]; + if (RAND_bytes(uuid, sizeof(uuid)) != 1) { send_response(cli, id, NULL, "attachment UUID unavailable"); return; } + for (unsigned i = 0; i < 16; i++) snprintf(hex + i * 2, 3, "%02x", uuid[i]); + snprintf(dest, sizeof(dest), "%s/%s.%s", dir, hex, as_video ? "mp4" : "bin"); struct chat_msg_submit* req = u_calloc(1, sizeof(*req)); if (!req) { send_response(cli, id, NULL, "out of memory"); return; } + struct headless_submission* pending = u_calloc(1, sizeof(*pending)); + if (!pending) { u_free(req); send_response(cli, id, NULL, "out of memory"); return; } + pending->client = cli; pending->id = id; strcpy(pending->destination, dest); + pending->next = cli->submissions; cli->submissions = pending; req->inst = hc->inst; snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch); snprintf(req->content_type, sizeof(req->content_type), "%s", as_video ? "video/mp4" : ct); @@ -480,12 +605,8 @@ static void hc_handle_attach(struct headless_client* cli, int id, const char* js DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: attach ch=%s src=%s dst=%s video=%d", ch, path, dest, as_video); - chat_core_submit_media_message(hc->inst, req); + chat_core_submit_media_message(hc->inst, req, hc_media_ready, pending); u_free(req); - - char resp[1400]; snprintf(resp, sizeof(resp), "{\"attached\":true,\"dest\":\"%s\",\"video\":%s}", - dest, as_video ? "true" : "false"); - send_response(cli, id, resp, NULL); } static void hc_invite_ready(void* arg, int result, const char* link) { @@ -496,7 +617,7 @@ static void hc_invite_ready(void* arg, int result, const char* link) { if (*p) *p = req->next; int id = req->id; u_free(req); - if (cli->closing) return; + if (cli->closing || cli->draining) return; if (result != CHAT_JOIN_OK) { send_response(cli, id, NULL, result == CHAT_JOIN_TIMEOUT ? "invite registration timed out" : "invite registration failed"); return; @@ -519,15 +640,15 @@ static void hc_invite_ready(void* arg, int result, const char* link) { static void hc_handle_invite(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char ch[64], node_id_str[32]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; } - uint64_t channel_id = strtoull(ch, NULL, 10); + uint64_t channel_id = chat_channel_id(ch); if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; } /* опциональный выбор узла: "0" или отсутствует → auto (лучший узел) */ uint64_t target_node_id = 0; - if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) == 0) + if (json_get_str(cli, "node_id", node_id_str, sizeof(node_id_str)) == 0) target_node_id = strtoull(node_id_str, NULL, 16); struct headless_invite* req = u_calloc(1, sizeof(*req)); @@ -545,7 +666,7 @@ static void hc_handle_invite(struct headless_client* cli, int id, const char* js static void hc_handle_invite_nodes(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char ch[64]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } uint64_t channel_id = strtoull(ch, NULL, 10); if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; } @@ -554,25 +675,26 @@ static void hc_handle_invite_nodes(struct headless_client* cli, int id, const ch if (cnt < 0) { send_response(cli, id, NULL, "no candidates (see log)"); return; } uint64_t auto_id = 0; - chat_invite_best_node(hc->inst, channel_id, &auto_id); - - char resp[4096]; int off = 0; - off += snprintf(resp + off, sizeof(resp) - off, "{\"ch\":\"%s\",\"auto_node_id\":\"0x%016llx\",\"nodes\":[", - ch, (unsigned long long)auto_id); - for (int i = 0; i < cnt && off < (int)sizeof(resp) - 256; i++) { + if (chat_invite_best_node(hc->inst, channel_id, &auto_id)) { send_response(cli, id, NULL, "candidate selection failed"); return; } + struct strbuf resp = strbuf_new(); + int rc = strbuf_addf(&resp, "{\"ch\":\"%s\",\"auto_node_id\":\"0x%016llx\",\"nodes\":[", ch, (unsigned long long)auto_id); + for (int i = 0; i < cnt && rc >= 0; i++) { char name[64] = ""; - chat_core_get_node_name(hc->inst, ids[i], name, sizeof(name)); - off += snprintf(resp + off, sizeof(resp) - off, "%s{\"node_id\":\"0x%016llx\",\"name\":\"%s\"}", - i ? "," : "", (unsigned long long)ids[i], name); + if (chat_core_get_node_name(hc->inst, ids[i], name, sizeof(name))) { rc = -1; break; } + rc = strbuf_addf(&resp, "%s{\"node_id\":\"0x%016llx\",\"name\":", i ? "," : "", (unsigned long long)ids[i]); + if (rc >= 0) rc = strbuf_add_json_string(&resp, name); + if (rc >= 0) rc = strbuf_addf(&resp, "}"); } - off += snprintf(resp + off, sizeof(resp) - off, "]}"); - send_response(cli, id, resp, NULL); + if (rc >= 0) rc = strbuf_addf(&resp, "]}"); + if (rc < 0) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite candidates formatting failed"); + send_response(cli, id, rc < 0 ? NULL : strbuf_str(&resp), rc < 0 ? "candidate snapshot failed" : NULL); + strbuf_free(&resp); } static void hc_handle_connect(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char link[INVITE_LINK_MAX]; - if (json_get_str(json, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; } + if (json_get_str(cli, "link", link, sizeof(link)) < 0) { send_response(cli, id, NULL, "missing 'link' param"); return; } struct InviteData d; char err[256]; if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { send_response(cli, id, NULL, err); return; } if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; } @@ -591,7 +713,7 @@ static void hc_handle_connect(struct headless_client* cli, int id, const char* j static void hc_handle_create_channel(struct headless_client* cli, int id, const char* json) { char name[256]; - if (json_get_str(json, "name", name, sizeof(name)) < 0) { send_response(cli, id, NULL, "missing 'name' param"); return; } + if (json_get_str(cli, "name", name, sizeof(name)) < 0) { send_response(cli, id, NULL, "missing 'name' param"); return; } uint64_t channel = chat_core_create_channel_auto(cli->hc->inst, name); if (!channel) { send_response(cli, id, NULL, "channel creation failed (see log)"); return; } char result[96]; snprintf(result, sizeof(result), "{\"created\":true,\"channel_id\":\"%llu\"}", (unsigned long long)channel); @@ -601,11 +723,11 @@ static void hc_handle_create_channel(struct headless_client* cli, int id, const static void hc_handle_invite_to(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char ch[64], node_id_str[32], pubkey_hex[128], addr[128]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } - if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } - if (json_get_str(json, "pubkey", pubkey_hex, sizeof(pubkey_hex)) < 0) { send_response(cli, id, NULL, "missing 'pubkey' param"); return; } - if (json_get_str(json, "addr", addr, sizeof(addr)) < 0) { send_response(cli, id, NULL, "missing 'addr' param"); return; } - int proto = json_get_int(json, "proto", 1); + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } + if (json_get_str(cli, "pubkey", pubkey_hex, sizeof(pubkey_hex)) < 0) { send_response(cli, id, NULL, "missing 'pubkey' param"); return; } + if (json_get_str(cli, "addr", addr, sizeof(addr)) < 0) { send_response(cli, id, NULL, "missing 'addr' param"); return; } + int proto = json_get_int(cli, "proto", 1); if (!hc->inst) { send_response(cli, id, NULL, "no instance"); return; } uint64_t target_node_id = strtoull(node_id_str, NULL, 16); @@ -618,8 +740,10 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char* char* colon = strrchr(ip, ':'); if (!colon) { send_response(cli, id, NULL, "invalid addr (no port)"); return; } *colon = '\0'; - int port = (int)strtol(colon + 1, NULL, 10); - if (port <= 0 || port > 65535) { send_response(cli, id, NULL, "invalid port"); return; } + char* end = NULL; errno = 0; long port = strtol(colon + 1, &end, 10); + if (errno || end == colon + 1 || *end || port <= 0 || port > 65535 || (proto != 1 && proto != 2)) { + send_response(cli, id, NULL, "invalid port/protocol"); return; + } uint8_t addrs_data[22]; int addrs_len = 0; struct in_addr a4; struct in6_addr a6; @@ -658,7 +782,7 @@ static void hc_handle_invite_to(struct headless_client* cli, int id, const char* } static void hc_handle_subscribe(struct headless_client* cli, int id, const char* json) { - int enable = json_get_int(json, "enable", 1); + int enable = json_get_int(cli, "enable", 1); cli->subscribed = enable; char resp[64]; snprintf(resp, sizeof(resp), "{\"subscribed\":%s}", enable ? "true" : "false"); send_response(cli, id, resp, NULL); @@ -667,8 +791,9 @@ static void hc_handle_subscribe(struct headless_client* cli, int id, const char* static void hc_handle_quit(struct headless_client* cli, int id, const char* json) { (void)json; send_response(cli, id, "{\"bye\":true}", NULL); - cli_send(cli, "{\"status\":\"closing\"}\n", 20); - hc_close_client(cli); + if (cli->closing) return; + cli->draining = 1; uasync_set_socket_read(cli->hc->ua, cli->socket_id, 0); + if (!cli->send_len) hc_close_client(cli); } /* ── DM (прямой чат) ── */ @@ -676,31 +801,36 @@ static void hc_handle_quit(struct headless_client* cli, int id, const char* json static void hc_handle_dm_start(struct headless_client* cli, int id, const char* json) { struct headless_control* hc = cli->hc; char node_id_str[64], ch[64]; - if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) ch[0] = '\0'; + if (json_get_str(cli, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) ch[0] = '\0'; uint64_t target = strtoull(node_id_str, NULL, 16); if (!target || !hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; } sqlite3* db = chat_core_get_db(hc->inst); - uint8_t x25519[32], ed25519[32]; + uint8_t x25519[32], ed25519[32]; char name[MS_USERINFO_MAX + 1]; int ok = 0; sqlite3_stmt* st = NULL; if (db && sqlite3_prepare_v2(db, "SELECT x25519_pubkey, ed25519_pubkey, COALESCE(name,'') FROM nodes WHERE node_id=?", -1, &st, NULL) == SQLITE_OK) { - sqlite3_bind_int64(st, 1, (sqlite3_int64)target); - if (sqlite3_step(st) == SQLITE_ROW) { + int rc = sqlite3_bind_int64(st, 1, (sqlite3_int64)target); + if (rc == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW) { const uint8_t* x = (const uint8_t*)sqlite3_column_blob(st, 0); const uint8_t* e = (const uint8_t*)sqlite3_column_blob(st, 1); - if (x && e && sqlite3_column_bytes(st, 0) >= 32 && sqlite3_column_bytes(st, 1) >= 32) { + int type = sqlite3_column_type(st, 2), bytes = sqlite3_column_bytes(st, 2); + const char* text = (const char*)sqlite3_column_text(st, 2); + if (sqlite3_column_type(st, 0) == SQLITE_BLOB && sqlite3_column_type(st, 1) == SQLITE_BLOB && x && e && + sqlite3_column_bytes(st, 0) == 32 && sqlite3_column_bytes(st, 1) == 32 && type == SQLITE_TEXT && text && + bytes < (int)sizeof(name) && strlen(text) == (size_t)bytes) { memcpy(x25519, x, 32); memcpy(ed25519, e, 32); - const char* name = (const char*)sqlite3_column_text(st, 2); - if (dm_start(hc->inst, target, x25519, ed25519, name, ch[0] ? ch : NULL) == 0) ok = 1; + memcpy(name, text, (size_t)bytes + 1); ok = 1; } } } - if (st) sqlite3_finalize(st); + int final = sqlite3_finalize(st); + if (final != SQLITE_OK) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: DM identity finalize rc=%d", final); ok = 0; } if (!ok) { send_response(cli, id, NULL, "failed to start DM (node not found?)"); return; } + if (dm_start(hc->inst, target, x25519, ed25519, name, ch[0] ? ch : NULL)) { send_response(cli, id, NULL, "DM start failed"); return; } char conv[64]; uint64_t conv_num = dm_derive_conv_id(hc->inst->node_id, target); snprintf(conv, sizeof(conv), "%llu", (unsigned long long)conv_num); @@ -721,9 +851,9 @@ static void hc_handle_dm_list(struct headless_client* cli, int id, const char* j /* Сообщения DM-беседы (расшифрованные). */ static void hc_handle_dm_messages(struct headless_client* cli, int id, const char* json) { char conv[64]; int count = 20, offset = 0; - if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; } - count = json_get_int(json, "count", 20); - offset = json_get_int(json, "offset", 0); + if (json_get_str(cli, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; } + count = json_get_int(cli, "count", 20); + offset = json_get_int(cli, "offset", 0); char buf[16384]; size_t len = 0; if (dm_list_messages_json(cli->hc->inst, conv, count, offset, buf, sizeof(buf), &len) < 0) send_response(cli, id, NULL, "failed to query DM messages"); @@ -734,8 +864,8 @@ static void hc_handle_dm_messages(struct headless_client* cli, int id, const cha /* Отправить сообщение в DM-беседу. */ static void hc_handle_dm_send(struct headless_client* cli, int id, const char* json) { char conv[64], data[4096]; - if (json_get_str(json, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; } - if (json_get_str(json, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; } + if (json_get_str(cli, "conv_id", conv, sizeof(conv)) < 0) { send_response(cli, id, NULL, "missing 'conv_id' param"); return; } + if (json_get_str(cli, "data", data, sizeof(data)) < 0) { send_response(cli, id, NULL, "missing 'data' param"); return; } if (dm_send(cli->hc->inst, conv, "text", (const uint8_t*)data, (uint32_t)strlen(data)) != 0) send_response(cli, id, NULL, "failed to send DM"); else @@ -745,19 +875,21 @@ static void hc_handle_dm_send(struct headless_client* cli, int id, const char* j /* ── Звонки (P2P аудио) ── */ static uint64_t hc_gen_call_id(void) { - uint64_t id = ((uint64_t)rand() << 32) ^ (uint64_t)rand(); - return id ? id : 1; + uint64_t id; + if (RAND_bytes((uint8_t*)&id, sizeof(id)) != 1 || !id) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: call ID RNG failed"); return 0; } + return id; } static void hc_handle_call_start(struct headless_client* cli, int id, const char* json) { char ch[64], node_id_str[32], call_id_str[32]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } - if (json_get_str(json, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "node_id", node_id_str, sizeof(node_id_str)) < 0) { send_response(cli, id, NULL, "missing 'node_id' param"); return; } uint64_t peer = strtoull(node_id_str, NULL, 16); if (!peer || !cli->hc->inst) { send_response(cli, id, NULL, "invalid node_id"); return; } uint64_t call_id = 0; - if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) == 0) call_id = strtoull(call_id_str, NULL, 16); + if (json_get_str(cli, "call_id", call_id_str, sizeof(call_id_str)) == 0) call_id = strtoull(call_id_str, NULL, 16); if (!call_id) call_id = hc_gen_call_id(); + if (!call_id) { send_response(cli, id, NULL, "call ID unavailable"); return; } call_start(cli->hc->inst, ch, peer, call_id); char resp[256]; snprintf(resp, sizeof(resp), "{\"call_id\":\"0x%016llx\",\"calling\":true}", (unsigned long long)call_id); @@ -766,7 +898,7 @@ static void hc_handle_call_start(struct headless_client* cli, int id, const char static void hc_handle_call_accept(struct headless_client* cli, int id, const char* json) { char call_id_str[32]; - if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } + if (json_get_str(cli, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_accept(cli->hc->inst, call_id); send_response(cli, id, "{\"accepted\":true}", NULL); @@ -774,7 +906,7 @@ static void hc_handle_call_accept(struct headless_client* cli, int id, const cha static void hc_handle_call_decline(struct headless_client* cli, int id, const char* json) { char call_id_str[32]; - if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } + if (json_get_str(cli, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_decline(cli->hc->inst, call_id); send_response(cli, id, "{\"declined\":true}", NULL); @@ -782,7 +914,7 @@ static void hc_handle_call_decline(struct headless_client* cli, int id, const ch static void hc_handle_call_hangup(struct headless_client* cli, int id, const char* json) { char call_id_str[32]; - if (json_get_str(json, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } + if (json_get_str(cli, "call_id", call_id_str, sizeof(call_id_str)) < 0) { send_response(cli, id, NULL, "missing 'call_id' param"); return; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_hangup(cli->hc->inst, call_id); send_response(cli, id, "{\"hungup\":true}", NULL); @@ -792,7 +924,7 @@ static void hc_handle_call_hangup(struct headless_client* cli, int id, const cha static void hc_handle_radio_on(struct headless_client* cli, int id, const char* json) { char ch[64]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } uint64_t group_id = strtoull(ch, NULL, 10); if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; } radio_set_active(cli->hc->inst, group_id, 1); @@ -802,7 +934,7 @@ static void hc_handle_radio_on(struct headless_client* cli, int id, const char* static void hc_handle_radio_off(struct headless_client* cli, int id, const char* json) { char ch[64]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } uint64_t group_id = strtoull(ch, NULL, 10); if (!group_id || !cli->hc->inst) { send_response(cli, id, NULL, "invalid ch"); return; } radio_set_active(cli->hc->inst, group_id, 0); @@ -812,7 +944,7 @@ static void hc_handle_radio_off(struct headless_client* cli, int id, const char* static void hc_handle_radio_subscribers(struct headless_client* cli, int id, const char* json) { char ch[64]; - if (json_get_str(json, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } + if (json_get_str(cli, "ch", ch, sizeof(ch)) < 0) { send_response(cli, id, NULL, "missing 'ch' param"); return; } uint64_t group_id = strtoull(ch, NULL, 10); int n = radio_subscribers(cli->hc->inst, group_id); char resp[128]; snprintf(resp, sizeof(resp), "{\"subscribers\":%d,\"ch\":\"%llu\"}", n, (unsigned long long)group_id); @@ -841,21 +973,22 @@ static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_ static void hc_handle_command(struct headless_client* cli, const char* json) { if (!cli || cli->closing || !json) return; - int id = json_get_int(json, "id", 0); - const char* cs = json_get_cmd(json); - if (!cs) { send_response(cli, id, NULL, "missing cmd"); return; } - const char* ce = json_get_cmd_end(cs); - size_t clen = (size_t)(ce - cs); - if (clen == 0 || clen > 31) { send_response(cli, id, NULL, "invalid cmd"); return; } - char cmd[32]; memcpy(cmd, cs, clen); cmd[clen] = '\0'; + struct hc_request request = {0}; + if (hc_parse_request(cli, json, &request)) { send_response(cli, 0, NULL, "invalid request"); return; } + cli->request = &request; + int id = json_get_int(cli, "id", 0); + char cmd[32]; + if (json_get_str(cli, "cmd", cmd, sizeof(cmd)) || !*cmd) { send_response(cli, id, NULL, "missing/invalid cmd"); goto done; } if (!strcmp(cmd, "poll_create") || !strcmp(cmd, "vote") || !strcmp(cmd, "react") || !strcmp(cmd, "feedback") || !strcmp(cmd, "reaction_catalog")) { - hc_handle_feedback(cli, id, json); return; + hc_handle_feedback(cli, id, json); goto done; } for (int i = 0; cmd_names[i]; i++) { - if (strcmp(cmd, cmd_names[i]) == 0) { cmd_handlers[i](cli, id, json); return; } + if (strcmp(cmd, cmd_names[i]) == 0) { cmd_handlers[i](cli, id, json); goto done; } } send_response(cli, id, NULL, "unknown command"); +done: + cli->request = NULL; } /* ── Accept callback ── */ @@ -866,7 +999,10 @@ static void accept_callback(socket_t fd, void* arg) { socket_t cfd = accept(fd, (struct sockaddr*)&addr, &alen); if (cfd == SOCKET_INVALID) { int e = socket_get_error(); if (e != ERR_AGAIN && e != ERR_WOULDBLOCK) DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: accept failed: %s", socket_strerror(e)); return; } - socket_set_nonblocking(cfd); + if (socket_set_nonblocking(cfd)) { + DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: accepted socket nonblocking failed err=%d", socket_get_error()); + socket_close_wrapper(cfd); return; + } if (hc->client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; } @@ -897,47 +1033,55 @@ static void hc_cleanup(void* arg) { } else prev = &(*prev)->next; } hc->cleanup_timer = uasync_set_timeout(hc->ua, 5000, hc, hc_cleanup, "hc_cleanup"); - if (!hc->cleanup_timer) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed"); + if (!hc->cleanup_timer) { + DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed;requesting owner stop"); utun_instance_stop(hc->inst); + } } /* ── Init / Destroy ── */ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, const char* bind_ip, int port) { - if (!ua || !inst || !bind_ip || port < 0 || port > 65535) { + if (!ua || !inst || ua != inst->ua || !chat_core_is_initialized(inst) || !bind_ip || port < 0 || port > 65535) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invalid init arguments port=%d", port); return -1; } struct headless_control* hc = hc_of(inst); - if (hc && hc->running) return 0; + struct in_addr address; + if (inet_pton(AF_INET, bind_ip, &address) != 1) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invalid IPv4 bind=%s", bind_ip); return -1; } + if (hc && hc->running) { + if (hc->configured_port == port && !strcmp(hc->bind_ip, bind_ip)) return 0; + DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: listener already running with different binding"); return -1; + } if (!hc) { hc = u_calloc(1, sizeof(*hc)); - if (!hc) return -1; + if (!hc) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: context allocation failed"); return -1; } inst->headless = hc; } - hc->ua = ua; hc->inst = inst; + hc->ua = ua; hc->inst = inst; hc->listen_fd = SOCKET_INVALID; + strcpy(hc->bind_ip, bind_ip); hc->configured_port = port; hc->listen_fd = socket(AF_INET, SOCK_STREAM, 0); - if (hc->listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; } + if (hc->listen_fd == SOCKET_INVALID) goto fail; - socket_set_reuseaddr(hc->listen_fd, 1); - socket_set_nonblocking(hc->listen_fd); + if (socket_set_reuseaddr(hc->listen_fd, 1) || socket_set_nonblocking(hc->listen_fd)) goto fail; struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_port = htons((uint16_t)port); - if (inet_pton(AF_INET, bind_ip, &sin.sin_addr) != 1) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } + sin.sin_addr = address; if (bind(hc->listen_fd, (struct sockaddr*)&sin, sizeof(sin)) < 0) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: bind(%s:%d) failed: %s", bind_ip, port, socket_strerror(socket_get_error())); - socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; + goto fail; } - if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } + if (listen(hc->listen_fd, 5) < 0) goto fail; uint16_t bound_port = socket_get_bound_port(hc->listen_fd); - if (!bound_port) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } + if (!bound_port) goto fail; hc->listen_sock_id = uasync_add_socket_t(ua, hc->listen_fd, accept_callback, NULL, NULL, "chatctl_listen", hc); - if (!hc->listen_sock_id) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } + if (!hc->listen_sock_id) goto fail; + hc->previous_handler = inst->chat_event_handler; chat_event_set_handler(inst, hc_on_chat_event); hc->cleanup_timer = uasync_set_timeout(ua, 5000, hc, hc_cleanup, "hc_cleanup"); if (!hc->cleanup_timer) { @@ -948,6 +1092,9 @@ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, hc->running = 1; DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%u", bind_ip, bound_port); return 0; +fail: + DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: listener init failed bind=%s:%d socket_error=%d", bind_ip, port, socket_get_error()); + chat_headless_control_destroy(inst); return -1; } uint16_t chat_headless_control_get_port(struct UTUN_INSTANCE* inst) { @@ -961,7 +1108,7 @@ void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) { if (!hc) return; hc->running = 0; if (hc->cleanup_timer) uasync_cancel_timeout(hc->ua, hc->cleanup_timer); - if (inst->chat_event_handler == hc_on_chat_event) chat_event_set_handler(inst, NULL); + if (inst->chat_event_handler == hc_on_chat_event) chat_event_set_handler(inst, hc->previous_handler); if (hc->listen_sock_id && hc->ua) uasync_remove_socket_t(hc->ua, hc->listen_fd); if (hc->listen_fd != SOCKET_INVALID) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; } struct headless_client* c = hc->clients; diff --git a/src/chat/chat_headless_control.h b/src/chat/chat_headless_control.h index 168fec85..8eb38bed 100644 --- a/src/chat/chat_headless_control.h +++ b/src/chat/chat_headless_control.h @@ -3,13 +3,21 @@ * * JSON line protocol: * Request: {"id":N,"cmd":"",...params} - * Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."} + * Response: {"id":N,"ok":true,"data":} | {"id":N,"ok":false,"error":"..."} * Event: {"event":"",...} * - * Команды каналов, DM, звонков/рации и настроек — в tools/chatcli_commands.txt; + * Команды каналов, DM и взаимодействий — в tools/chatcli_commands.txt; * диспетчер реализации — chat_headless_control.c. Аудио PCM использует отдельные call/radio_headless сокеты. - * init регистрирует listener и события chat_event, destroy закрывает клиентов и снимает подписку. - * Все вызовы и команды исполняются в uasync-потоке экземпляра. + * init требует готовый chat_core и ua==inst->ua. Listener IPv4,до64 клиентов,строка до16383байт без LF. + * До команды проверяется весь строгий JSON-object:до32 уникальных ключей длиной<=31байта, + * id — int32,строковые параметры не обрезаются,Unicode escapes декодируются,скрытый NUL запрещён. + * Массивы опросов передаются общему feedback API;ошибка разбора/типов получает id=0,ошибка команды сохраняет id. + * Исходящая очередь до32768байт:короткие записи/EAGAIN сохраняют порядок,переполнение закрывает клиента с WARN. + * quit дописывает bye перед закрытием. attach отвечает после сохранения/отказа публикации,не после доставки пирам; + * его completion освобождается также после disconnect. connect/call подтверждают передачу запроса ядру; + * подписчикам приходят connect_result/call_error и состояния звонка,а не гарантия готовности связи из ответа команды. + * Прежний chat_event handler вызывается после headless;destroy восстанавливает его,если handler всё ещё принадлежит headless. + * Все вызовы/команды/completion — в uasync;stop/destroy вне callbacks этой службы. Диагностика — категория control. */ #ifndef CHAT_HEADLESS_CONTROL_H #define CHAT_HEADLESS_CONTROL_H @@ -20,10 +28,19 @@ struct UASYNC; struct UTUN_INSTANCE; +#ifdef __cplusplus +extern "C" { +#endif + +/* 0 — запущен/повтор с тем же bind,-1 — отказ с полной очисткой. port=0 выбирает ОС. */ int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, const char* bind_ip, int port); void chat_headless_control_destroy(struct UTUN_INSTANCE* inst); // port=0 при init выбирает ОС; getter возвращает 0 для остановленной службы или при ошибке. uint16_t chat_headless_control_get_port(struct UTUN_INSTANCE* inst); +#ifdef __cplusplus +} +#endif + #endif /* CHAT_HEADLESS_CONTROL_H */ diff --git a/src/chat/chat_msg.c b/src/chat/chat_msg.c index 8efdfaf4..9705f651 100644 --- a/src/chat/chat_msg.c +++ b/src/chat/chat_msg.c @@ -31,8 +31,6 @@ static void media_queue_channel_backfill(struct UTUN_INSTANCE* inst, const char* channel); -/* forward decl */ -void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req); static void wh_transcribe_done_cb(void* arg, const char* ch_id, const char* text, int err, const uint8_t reply_id[32]); static void chat_media_cache_cleanup_orphans(struct UTUN_INSTANCE* inst); @@ -196,7 +194,7 @@ void chat_core_submit_trampoline(void* arg) { req->expected_ver, (const char*)req->data, req->data_len); chat_event_submitted(req->inst, req->request_id, result == 0); } else if (req->media_src[0] != '\0') - chat_core_submit_media_message(req->inst, req); + chat_core_submit_media_message(req->inst, req, NULL, NULL); else { int result = chat_core_submit_message(req->inst, req); chat_event_submitted(req->inst, req->request_id, result == 0); @@ -222,12 +220,15 @@ struct media_submit_ctx { int height; char reply_hex[65]; uint64_t request_id; + chat_media_submitted_fn complete; + void* complete_arg; }; /* Завершение владеющей асинхронной отправки, включая все ошибки подготовки/регистрации. */ static void media_submit_finish(struct media_submit_ctx* ctx, int success) { struct UTUN_INSTANCE* inst = ctx->inst; struct chat_core_ctx* cc = CC(inst); + chat_media_submitted_fn complete = ctx->complete; void* complete_arg = ctx->complete_arg; char channel[64]; snprintf(channel, sizeof(channel), "%s", ctx->channel_id); /* Синхронный обработчик события может запросить удаление; ctx пока остаётся в списке владельца. */ chat_event_submitted(ctx->inst, ctx->request_id, success); @@ -239,11 +240,15 @@ static void media_submit_finish(struct media_submit_ctx* ctx, int success) { media_delivery_release(inst, ctx->destination); u_free(ctx); if (delete_pending) { + int waiting = 0; for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next) - if (!strcmp(pending->channel_id, channel)) return; - DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media submissions drained; continuing channel deletion ch=%s", channel); - chat_core_delete_channel(inst, channel); + if (!strcmp(pending->channel_id, channel)) { waiting = 1; break; } + if (!waiting) { + DEBUG_INFO(DEBUG_CATEGORY_MEDIA, "media submissions drained; continuing channel deletion ch=%s", channel); + chat_core_delete_channel(inst, channel); + } } + if (complete) complete(complete_arg, success); } int chat_media_defer_channel_delete(struct UTUN_INSTANCE* inst, const char* channel) { @@ -419,33 +424,38 @@ static void on_video_prepared(void* arg, int err, const struct video_info* vi) { on_media_registered, mctx); } -void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req) { +void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req, + chat_media_submitted_fn complete, void* arg) { struct chat_core_ctx* cc = inst ? CC(inst) : NULL; - if (!cc || !cc->initialized || !req || !req->media_src[0]) { + if (!cc || !cc->initialized || !req || !req->media_src[0] || !memchr(req->channel_id, 0, sizeof(req->channel_id)) || + !memchr(req->content_type, 0, sizeof(req->content_type)) || !memchr(req->media_src, 0, sizeof(req->media_src)) || + !memchr(req->media_dest, 0, sizeof(req->media_dest)) || !chat_channel_id(req->channel_id) || req->data_len >= 4096 || + (req->data_len && !req->data)) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: invalid media submit/stopped core", CC_ID); if (req) chat_event_submitted(inst, req->request_id, 0); + if (complete) complete(arg, 0); return; } char media_base[MEDIA_BASE_MAX], location[MEDIA_PATH_MAX], destination[MEDIA_PATH_MAX]; if (media_index_get_base(cc->db, media_base) || media_index_location(req->media_dest, media_base, location) || media_index_path(media_base, location, destination)) { - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } for (struct media_submit_ctx* pending = cc->media_submissions; pending; pending = pending->next) { if ((!strcmp(pending->channel_id, req->channel_id) && pending->delete_pending) || !strcmp(pending->media_dest, destination)) { DEBUG_WARN(DEBUG_CATEGORY_MEDIA, "media submit rejected: channel deleting/destination busy ch=%s path=%s", req->channel_id, req->media_dest); - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } } if (!si_find(inst, req->channel_id)) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit without channel ch=%s", req->channel_id); - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } char reply_hex[65]; if (msg_reply_hex(cc, req->channel_id, req->reply_to, reply_hex) < 0) { - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } /* лимит одного медиафайла применяется и к локальным отправкам */ @@ -454,12 +464,12 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_ int64_t sz = ma_file_size(req->media_src); if (sz < 0) { DEBUG_ERROR(DEBUG_CATEGORY_MEDIA, "media submit source size unavailable path=%s", req->media_src); - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } if (sz > 0 && (uint64_t)sz > unit) { DEBUG_WARN(DEBUG_CATEGORY_CHAT_SYNC, "%s: media too large for local send (%lld > %llu bytes), rejected ch=%s", CC_ID, (long long)sz, (unsigned long long)unit, req->channel_id); - chat_event_submitted(inst, req->request_id, 0); + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } } @@ -470,7 +480,7 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_ if (!inst->media_async) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media_async not initialized", CC_ID); - chat_event_submitted(inst, req->request_id, 0); + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } @@ -478,16 +488,17 @@ void chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_ struct media_submit_ctx* mctx = u_calloc(1, sizeof(*mctx)); if (!mctx) { DEBUG_ERROR(DEBUG_CATEGORY_CHAT_SYNC, "%s: media context allocation failed", CC_ID); - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } mctx->destination = media_delivery_reserve(inst, destination); - if (!mctx->destination) { u_free(mctx); chat_event_submitted(inst, req->request_id, 0); return; } + if (!mctx->destination) { u_free(mctx); chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } if (media_index_track_preparation(cc->db, req->channel_id, destination, media_base)) { media_delivery_release(inst, mctx->destination); u_free(mctx); - chat_event_submitted(inst, req->request_id, 0); return; + chat_event_submitted(inst, req->request_id, 0); if (complete) complete(arg, 0); return; } mctx->inst = inst; mctx->request_id = req->request_id; + mctx->complete = complete; mctx->complete_arg = arg; mctx->next = cc->media_submissions; cc->media_submissions = mctx; memcpy(mctx->reply_hex, reply_hex, strlen(reply_hex) + 1); snprintf(mctx->channel_id, sizeof(mctx->channel_id), "%s", req->channel_id); diff --git a/tests/Makefile.am b/tests/Makefile.am index 21c7cc4d..cd2bb652 100644 --- a/tests/Makefile.am +++ b/tests/Makefile.am @@ -308,6 +308,12 @@ check_PROGRAMS += test_chat_json check_PROGRAMS += test_chat_profile check_PROGRAMS += test_chat_preferences check_PROGRAMS += test_chat_whisper test_chat_whisper_disabled +check_PROGRAMS += test_chat_headless_control + +test_chat_headless_control_SOURCES = test_chat_headless_control.c +test_chat_headless_control_LDADD = $(LIBUTUN) $(COMMON_LIBS) +test_chat_headless_control_LDFLAGS = -Wl,--wrap=chat_core_submit_media_message -Wl,--wrap=u_calloc_impl \ + -Wl,--wrap=uasync_set_timeout -Wl,--wrap=sqlite3_finalize test_chat_whisper_SOURCES = test_chat_whisper.c whisper_stub/whisper.h ../src/chat/chat_whisper.c test_chat_whisper_CPPFLAGS = $(AM_CPPFLAGS) -I$(srcdir)/whisper_stub diff --git a/tests/test_chat_headless_control.c b/tests/test_chat_headless_control.c new file mode 100644 index 00000000..1e17d1d3 --- /dev/null +++ b/tests/test_chat_headless_control.c @@ -0,0 +1,164 @@ +/* Настоящий TCP/uasync/SQLite/media;управляем только короткий send/EAGAIN и отложенный media completion. */ +#define _GNU_SOURCE +#include "../lib/socket_compat.h" +static ssize_t hc_test_send(socket_t fd, const void* data, size_t size); +#define socket_send hc_test_send +#include "../src/chat/chat_headless_control.c" +#undef socket_send +#include + +static int blocked; +static size_t short_write; +static unsigned observed, fail_timer, fail_context, parser_fault; +static chat_media_submitted_fn deferred; +static void* deferred_arg; +static int delay_media; + +static ssize_t hc_test_send(socket_t fd, const void* data, size_t size) { + if (blocked) { +#ifdef _WIN32 + WSASetLastError(WSAEWOULDBLOCK); +#else + errno = EAGAIN; +#endif + return -1; + } + return socket_send(fd, data, short_write && size > short_write ? short_write : size); +} +extern void __real_chat_core_submit_media_message(struct UTUN_INSTANCE*, struct chat_msg_submit*, chat_media_submitted_fn, void*); +void __wrap_chat_core_submit_media_message(struct UTUN_INSTANCE* inst, struct chat_msg_submit* req, chat_media_submitted_fn fn, void* arg) { + if (delay_media) { assert(!deferred); deferred = fn; deferred_arg = arg; return; } + __real_chat_core_submit_media_message(inst, req, fn, arg); +} +extern void* __real_u_calloc_impl(uint32_t, uint32_t, const char*); +void* __wrap_u_calloc_impl(uint32_t count, uint32_t bytes, const char* site) { + if (fail_context && strstr(site, "chat_headless_control.c")) { fail_context = 0; return NULL; } + return __real_u_calloc_impl(count, bytes, site); +} +extern void* __real_uasync_set_timeout(struct UASYNC*, int, void*, void (*)(void*), const char*); +void* __wrap_uasync_set_timeout(struct UASYNC* ua, int tb, void* arg, void (*fn)(void*), const char* name) { + if (fail_timer && !strcmp(name, "hc_cleanup")) { fail_timer = 0; return NULL; } + return __real_uasync_set_timeout(ua, tb, arg, fn, name); +} +extern int __real_sqlite3_finalize(sqlite3_stmt*); +int __wrap_sqlite3_finalize(sqlite3_stmt* st) { + int fail = parser_fault && st && strstr(sqlite3_sql(st), "END,key,type,value FROM json_each(?1)"); + if (fail) parser_fault = 0; + int result = __real_sqlite3_finalize(st); return fail ? SQLITE_IOERR : result; +} + +static void hc_observe(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { + (void)inst; (void)type; (void)data; (void)len; observed++; +} +static socket_t hc_peer(struct UTUN_INSTANCE* inst) { + socket_t fd = socket(AF_INET, SOCK_STREAM, 0); assert(fd != SOCKET_INVALID); + struct sockaddr_in address = {.sin_family = AF_INET, .sin_port = htons(chat_headless_control_get_port(inst))}; + assert(inet_pton(AF_INET, "127.0.0.1", &address.sin_addr) == 1); + assert(!connect(fd, (struct sockaddr*)&address, sizeof(address)) && !socket_set_nonblocking(fd)); + uasync_poll(inst->ua, 0); return fd; +} +static size_t hc_receive(struct UASYNC* ua, socket_t fd, char* out, size_t capacity, size_t minimum) { + size_t used = 0; + for (unsigned i = 0; i < 1000; i++) { + uasync_poll(ua, 5); + ssize_t count = recv(fd, out + used, (int)(capacity - used - 1), 0); + if (count > 0) used += (size_t)count; + else if (count == 0) break; + else assert(socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK); + out[used] = 0; + if (minimum ? used >= minimum : strchr(out, '\n') != NULL) return used; + assert(used + 1 < capacity); + } + assert(!"headless response timeout/early EOF"); return 0; +} +static void hc_request_line(struct UTUN_INSTANCE* inst, socket_t fd, const char* request, char response[65536]) { + assert(socket_send(fd, request, strlen(request)) == (ssize_t)strlen(request)); + response[0] = 0; hc_receive(inst->ua, fd, response, 65536, 0); +} +static int hc_response_ok(sqlite3* db, const char* response, int id) { + sqlite3_stmt* st = NULL; + assert(sqlite3_prepare_v2(db, "SELECT json_valid(?1),json_extract(?1,'$.id'),json_extract(?1,'$.ok')", -1, &st, NULL) == SQLITE_OK); + assert(sqlite3_bind_text(st, 1, response, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(st) == SQLITE_ROW); + assert(sqlite3_column_int(st, 0) && sqlite3_column_int(st, 1) == id); + int ok = sqlite3_column_int(st, 2); assert(sqlite3_finalize(st) == SQLITE_OK); return ok; +} + +int main(void) { + debug_config_init(); debug_set_level(DEBUG_LEVEL_WARN); debug_set_category_level(DEBUG_CATEGORY_CONTROL, DEBUG_LEVEL_DEBUG); + size_t baseline = u_get_allocated_count(); utun_instance_set_tun_init_enabled(0); + struct UASYNC* ua = uasync_create(); assert(ua); + char directory[] = "/tmp/utun-headless-XXXXXX", config[700]; assert(mkdtemp(directory)); + snprintf(config, sizeof(config), "[global]\nmy_node_name=headless\ndb_path=%s\n[ntp]\nenabled=no\n" + "[chatserver]\ngroup_autoconnect=0\nstorage_autoload=0\n", directory); + struct UTUN_INSTANCE* inst = utun_instance_create_from_str(ua, config); assert(inst && !utun_core_start(inst) && !chat_service_start(inst)); + chat_event_set_handler(inst, hc_observe); size_t before = u_get_allocated_count(); + fail_context = 1; assert(chat_headless_control_init(ua, inst, "127.0.0.1", 0) < 0 && !fail_context && !inst->headless); + assert(u_get_allocated_count() == before && inst->chat_event_handler == hc_observe); + fail_timer = 1; assert(chat_headless_control_init(ua, inst, "127.0.0.1", 0) < 0 && !fail_timer && !inst->headless); + assert(u_get_allocated_count() == before && inst->chat_event_handler == hc_observe); + assert(!chat_headless_control_init(ua, inst, "127.0.0.1", 0) && !chat_headless_control_init(ua, inst, "127.0.0.1", 0)); + assert(chat_headless_control_init(ua, inst, "127.0.0.2", 0) < 0); + socket_t peer = hc_peer(inst); char response[65536]; + const char* ping = "{\"id\":7,\"cmd\":\"p\\u0069ng\"}\n"; + assert(socket_send(peer, ping, 5) == 5); uasync_poll(ua, 0); + assert(socket_send(peer, ping + 5, strlen(ping) - 5) == (ssize_t)strlen(ping) - 5); + response[0] = 0; hc_receive(ua, peer, response, sizeof(response), 0); assert(hc_response_ok(inst->topo_sqlite_db, response, 7)); + hc_request_line(inst, peer, "{\"id\":8,\"cmd\":\"create_channel\",\"name\":\"quote\\\"\\\\\\u0001\"}\n", response); + assert(hc_response_ok(inst->topo_sqlite_db, response, 8)); + const char* invalid[] = {"{\"id\":9,\"cmd\":\"ping\",\"cmd\":\"status\"}\n", "{\"id\":2147483648,\"cmd\":\"ping\"}\n", + "{\"id\":9,\"cmd\":\"ping\",}\n", "{\"id\":9,\"cmd\":\"ping\",\"data\":\"\\u0000\"}\n"}; + for (unsigned i = 0; i < sizeof(invalid) / sizeof(invalid[0]); i++) { + hc_request_line(inst, peer, invalid[i], response); assert(!hc_response_ok(inst->topo_sqlite_db, response, 0)); + } + parser_fault = 1; hc_request_line(inst, peer, "{\"id\":9,\"cmd\":\"create_channel\",\"name\":\"not committed\"}\n", response); + assert(!parser_fault && !hc_response_ok(inst->topo_sqlite_db, response, 0)); + hc_request_line(inst, peer, "{\"id\":10,\"cmd\":\"send\",\"ch\":\"999\",\"data\":\"text\"}\n", response); + assert(!hc_response_ok(inst->topo_sqlite_db, response, 10)); + unsigned old_observed = observed; chat_event_post(inst, CHAT_EVT_STATUS_REFRESH, NULL, 0); assert(observed == old_observed + 1); + hc_request_line(inst, peer, "{\"id\":11,\"cmd\":\"status\"}\n", response); assert(hc_response_ok(inst->topo_sqlite_db, response, 11)); + uint64_t channel = chat_core_create_channel_auto(inst, "media"); assert(channel); + char poll[1024]; snprintf(poll, sizeof(poll), "{\"id\":17,\"cmd\":\"poll_create\",\"ch\":\"%llu\",\"question\":\"Q?\"," + "\"mode\":\"single\",\"allow_custom_answer\":true,\"options\":[\"yes\",\"no\"]}\n", (unsigned long long)channel); + hc_request_line(inst, peer, poll, response); assert(hc_response_ok(inst->topo_sqlite_db, response, 17)); + hc_request_line(inst, peer, "{\"id\":18,\"cmd\":\"subscribe\",\"enable\":true}\n", response); + assert(hc_response_ok(inst->topo_sqlite_db, response, 18)); + char ch[32]; snprintf(ch, sizeof(ch), "%llu", (unsigned long long)channel); size_t cl = strlen(ch); + const char invite_name[] = "q\"\\\x01"; uint8_t event[128]; event[0] = (uint8_t)cl; memcpy(event + 1, ch, cl); + event[cl + 1] = sizeof(invite_name) - 1; memcpy(event + cl + 2, invite_name, sizeof(invite_name) - 1); + memcpy(event + cl + 1 + sizeof(invite_name), &inst->node_id, 8); event[cl + 9 + sizeof(invite_name)] = 0; + chat_event_post(inst, CHAT_EVT_INVITE_RECEIVED, event, (int)(cl + 10 + sizeof(invite_name))); + response[0] = 0; hc_receive(ua, peer, response, sizeof(response), 0); + sqlite3_stmt* decoded = NULL; + assert(sqlite3_prepare_v2(inst->topo_sqlite_db, "SELECT json_valid(?1),json_extract(?1,'$.ch_name')", -1, &decoded, NULL) == SQLITE_OK); + assert(sqlite3_bind_text(decoded, 1, response, -1, SQLITE_STATIC) == SQLITE_OK && sqlite3_step(decoded) == SQLITE_ROW); + assert(sqlite3_column_int(decoded, 0) && !strcmp((const char*)sqlite3_column_text(decoded, 1), invite_name)); + assert(sqlite3_finalize(decoded) == SQLITE_OK); + hc_request_line(inst, peer, "{\"id\":19,\"cmd\":\"subscribe\",\"enable\":false}\n", response); + assert(hc_response_ok(inst->topo_sqlite_db, response, 19)); + char source[600], attach[1000]; snprintf(source, sizeof(source), "%s/input.bin", directory); + FILE* file = fopen(source, "wb"); assert(file && fwrite("headless fixture", 1, 16, file) == 16 && !fclose(file)); + snprintf(attach, sizeof(attach), "{\"id\":12,\"cmd\":\"attach\",\"ch\":\"%llu\",\"path\":\"%s\"}\n", (unsigned long long)channel, source); + hc_request_line(inst, peer, attach, response); assert(hc_response_ok(inst->topo_sqlite_db, response, 12)); + struct headless_client* cli = hc_of(inst)->clients; + char first[20000], second[10000]; memset(first, 'a', sizeof(first)); memset(second, 'b', sizeof(second)); + short_write = 7; cli_send(cli, first, sizeof(first)); cli_send(cli, second, sizeof(second)); short_write = 0; + client_write_callback(cli->fd, cli); response[0] = 0; size_t count = hc_receive(ua, peer, response, sizeof(response), 30000); + assert(count == 30000 && !memcmp(response, first, sizeof(first)) && !memcmp(response + sizeof(first), second, sizeof(second))); + blocked = 1; cli_send(cli, first, sizeof(first)); cli_send(cli, first, sizeof(first)); + assert(cli->closing && cli->fd == SOCKET_INVALID); blocked = 0; socket_close_wrapper(peer); + peer = hc_peer(inst); delay_media = 1; + blocked = 1; const char* quit = "{\"id\":20,\"cmd\":\"quit\"}\n"; + assert(socket_send(peer, quit, strlen(quit)) == (ssize_t)strlen(quit)); + for (unsigned i = 0; i < 100 && !hc_of(inst)->clients->draining; i++) uasync_poll(ua, 1); + assert(hc_of(inst)->clients->draining && !hc_of(inst)->clients->closing); blocked = 0; + response[0] = 0; hc_receive(ua, peer, response, sizeof(response), 0); assert(hc_response_ok(inst->topo_sqlite_db, response, 20)); + socket_close_wrapper(peer); peer = hc_peer(inst); + assert(socket_send(peer, attach, strlen(attach)) == (ssize_t)strlen(attach)); + for (unsigned i = 0; i < 100 && !deferred; i++) uasync_poll(ua, 1); + assert(deferred); + chat_headless_control_destroy(inst); assert(inst->chat_event_handler == hc_observe); socket_close_wrapper(peer); + deferred(deferred_arg, 0); deferred = NULL; deferred_arg = NULL; delay_media = 0; + utun_instance_destroy(inst); uasync_poll(ua, 0); uasync_destroy(ua, 0); assert(u_get_allocated_count() == baseline); + DEBUG_INFO(DEBUG_CATEGORY_CONTROL, "PASS headless:JSON/Unicode/SQL admission,real TCP fragmentation/short writes/pressure,media commit/disconnect,handler restore"); + return 0; +} diff --git a/tests/test_media_index_regressions.c b/tests/test_media_index_regressions.c index 39293416..3999116f 100644 --- a/tests/test_media_index_regressions.c +++ b/tests/test_media_index_regressions.c @@ -227,13 +227,13 @@ static void pending_deletion(void) { assert(!media_index_path(root, relative, destinations[i])); snprintf(req.media_src, sizeof(req.media_src), "%s", source); snprintf(req.media_dest, sizeof(req.media_dest), "%s", destinations[i]); - req.request_id = i + 1; chat_core_submit_media_message(inst, &req); + req.request_id = i + 1; chat_core_submit_media_message(inst, &req, NULL, NULL); } assert(CC(inst)->media_submissions && !failed_submissions); assert(scalar(db, "SELECT count(*) FROM media_preparations") == 2); /* completions не обработаны */ chat_core_delete_channel(inst, channel); chat_core_delete_channel(inst, channel); assert(!deleted_events && scalar(db, "SELECT count(*) FROM channels WHERE name='pending'") == 1); - req.request_id = 3; chat_core_submit_media_message(inst, &req); assert(failed_submissions == 1); + req.request_id = 3; chat_core_submit_media_message(inst, &req, NULL, NULL); assert(failed_submissions == 1); for (int i = 0; i < 2000 && !deleted_events; i++) uasync_poll(ua, 100); assert(deleted_events == 1 && failed_submissions == 3 && !successful_submissions && !CC(inst)->media_submissions); assert(!scalar(db, "SELECT count(*) FROM channels WHERE name='pending'")); @@ -249,7 +249,7 @@ static void pending_deletion(void) { assert(sqlite3_step(st) == SQLITE_ROW); snprintf(req.channel_id, sizeof(req.channel_id), "%s", sqlite3_column_text(st, 0)); sqlite3_finalize(st); snprintf(delete_from_event, sizeof(delete_from_event), "%s", req.channel_id); - req.request_id = 4; chat_core_submit_media_message(inst, &req); + req.request_id = 4; chat_core_submit_media_message(inst, &req, NULL, NULL); for (int i = 0; i < 2000 && deleted_events < 2; i++) uasync_poll(ua, 100); assert(deleted_events == 2 && successful_submissions == 1 && failed_submissions == 3 && !CC(inst)->media_submissions); assert(!scalar(db, "SELECT count(*) FROM channels WHERE name='reentrant'")); diff --git a/tools/chatcli_commands.txt b/tools/chatcli_commands.txt index e0ceba9b..cd6d6788 100644 --- a/tools/chatcli_commands.txt +++ b/tools/chatcli_commands.txt @@ -56,6 +56,8 @@ send Пример: chatcli send 12345 "hello world" attach [--video] + Ответ приходит после публикации сообщения в локальной БД;ошибка подготовки/сохранения возвращает ok=false. + Это не подтверждение доставки пирам. При disconnect подготовка завершается без ответа закрытому клиенту. Прикрепить файл к сообщению. Файл копируется в медиа-каталог канала. С флагом --video — файл обрабатывается как видео: если ffmpeg доступен, перекодируется в H.264/MP4 (720p, ≤2 Мбит); если уже в нужном формате —