|
|
|
@@ -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 <stdlib.h>
|
|
|
|
|
#include <stdarg.h>
|
|
|
|
|
#include <errno.h>
|
|
|
|
|
#include <limits.h>
|
|
|
|
|
#include <sys/stat.h>
|
|
|
|
|
#include <time.h>
|
|
|
|
|
|
|
|
|
|
#include <sqlite3.h>
|
|
|
|
|
#include <openssl/rand.h>
|
|
|
|
|
|
|
|
|
|
#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;
|
|
|
|
|