You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
652 lines
30 KiB
652 lines
30 KiB
/* |
|
* chat_headless_control.c — TCP control socket for headless chat CLI |
|
* |
|
* JSON line protocol over TCP. Integrated into uasync event loop. |
|
* Default: localhost-only, port from config. |
|
*/ |
|
#include "chat_headless_control.h" |
|
#include "invite_link.h" |
|
#include "invite_build.h" |
|
#include "chat_core.h" |
|
#include "chat_sync.h" |
|
#include "chat_event.h" |
|
#include "../utun_instance.h" |
|
#include "../ntp_time.h" |
|
#include "../transport_layer/etcp.h" |
|
#include "../transport_layer/etcp_connections.h" |
|
#include "../transport_layer/secure_channel.h" |
|
#include "../../lib/u_async.h" |
|
#include "../../lib/debug_config.h" |
|
#include "../../lib/mem.h" |
|
#include "../../lib/ll_queue.h" |
|
#include "../routing_layer/topo_node_sqlite.h" |
|
|
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <stdio.h> |
|
#include <time.h> |
|
#include <sys/stat.h> |
|
#include "../../lib/platform_compat.h" |
|
#include "../../lib/socket_compat.h" |
|
|
|
#define MAX_CLIENTS 16 |
|
#define RECV_BUF_SIZE 16384 |
|
#define SEND_BUF_SIZE 16384 |
|
|
|
#ifndef DEBUG_CATEGORY_HEADLESS |
|
#define DEBUG_CATEGORY_HEADLESS 27 |
|
#endif |
|
|
|
struct headless_client { |
|
socket_t fd; |
|
void* socket_id; |
|
char recv_buf[RECV_BUF_SIZE]; |
|
int recv_len; |
|
char send_buf[SEND_BUF_SIZE]; |
|
int send_len; |
|
int send_offset; |
|
int subscribed; |
|
int closing; |
|
struct headless_client* next; |
|
}; |
|
|
|
static struct { |
|
struct UASYNC* ua; |
|
struct UTUN_INSTANCE* inst; |
|
struct headless_client* clients; |
|
int client_count; |
|
socket_t listen_fd; |
|
void* listen_sock_id; |
|
int running; |
|
} g_hc; |
|
|
|
/* ── Forward declarations ── */ |
|
|
|
static void hc_handle_command(struct headless_client* cli, const char* json); |
|
|
|
/* ── 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); |
|
} |
|
|
|
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++); |
|
} |
|
out[i] = '\0'; return 0; |
|
} |
|
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; |
|
} |
|
|
|
/* ── Send helpers ── */ |
|
|
|
static void cli_send(struct headless_client* cli, const char* data, size_t len) { |
|
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 (g_hc.ua) uasync_set_socket_write(g_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 (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 1); |
|
} |
|
} |
|
|
|
static void cli_send_str(struct headless_client* cli, const char* s) { |
|
if (s) cli_send(cli, s, strlen(s)); |
|
} |
|
|
|
static void send_response(struct headless_client* cli, int id, const char* ok_data, const char* error) { |
|
char buf[4096]; int len; |
|
if (error) len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":false,\"error\":\"%s\"}\n", id, error); |
|
else if (ok_data) len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true,\"data\":%s}\n", id, ok_data); |
|
else len = snprintf(buf, sizeof(buf), "{\"id\":%d,\"ok\":true}\n", id); |
|
if (len > 0) cli_send(cli, buf, (size_t)len); |
|
} |
|
|
|
/* ── Broadcast event ── */ |
|
|
|
static void hc_broadcast_event(const char* json) { |
|
size_t jlen = strlen(json); |
|
char buf[4096]; int blen = snprintf(buf, sizeof(buf), "%s\n", json); |
|
if (blen <= 0) return; |
|
for (struct headless_client* c = g_hc.clients; c; c = c->next) |
|
if (c->subscribed) cli_send(c, buf, (size_t)blen); |
|
} |
|
|
|
/* ── chat_event callback ── */ |
|
|
|
static void hc_on_chat_event(int type, const uint8_t* data, int len) { |
|
if (type == 10) return; /* skip status text */ |
|
char json[1024]; json[0] = '\0'; |
|
if (type == 1 && data && len >= 10) { /* MSG_RECEIVED */ |
|
uint8_t cl = data[0]; |
|
uint64_t author; memcpy(&author, data + 1 + cl, 8); |
|
snprintf(json, sizeof(json), "{\"event\":\"msg\",\"ch\":\"%.*s\",\"author_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)author); |
|
} else if (type == 5 && data && len >= 2) { /* MEMBERS_CHANGED */ |
|
uint8_t cl = data[0]; |
|
snprintf(json, sizeof(json), "{\"event\":\"members_changed\",\"ch\":\"%.*s\"}", (int)cl, data + 1); |
|
} else if (type == 4 && data && len >= 2) { /* CHANNEL_UPDATED */ |
|
uint8_t cl = data[0]; |
|
snprintf(json, sizeof(json), "{\"event\":\"channel_updated\",\"ch\":\"%.*s\"}", (int)cl, data + 1); |
|
} else if (type == 18 && data && len >= 2) { /* INVITE_RECEIVED */ |
|
uint8_t cl = data[0]; |
|
if (len >= 2 + cl) { |
|
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 (json[0]) hc_broadcast_event(json); |
|
} |
|
|
|
/* ── Write callback ── */ |
|
|
|
static void client_write_callback(socket_t fd, void* arg) { |
|
struct headless_client* cli = (struct headless_client*)arg; |
|
if (!cli || cli->closing || fd != cli->fd) return; |
|
if (cli->send_len <= 0) { if (g_hc.ua) uasync_set_socket_write(g_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); |
|
if (r < 0 && (socket_get_error() == ERR_AGAIN || socket_get_error() == ERR_WOULDBLOCK)) return; |
|
if (r <= 0) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); return; } |
|
cli->send_offset += (int)r; |
|
if (cli->send_offset >= cli->send_len) { cli->send_len = 0; cli->send_offset = 0; if (g_hc.ua) uasync_set_socket_write(g_hc.ua, cli->socket_id, 0); } |
|
} |
|
|
|
/* ── Close client ── */ |
|
|
|
static void hc_close_client(struct headless_client* cli) { |
|
if (!cli || cli->closing) return; |
|
cli->closing = 1; |
|
if (g_hc.ua && cli->socket_id) uasync_remove_socket_t(g_hc.ua, cli->fd); |
|
if (cli->fd != SOCKET_INVALID) { socket_close_wrapper(cli->fd); cli->fd = SOCKET_INVALID; } |
|
} |
|
|
|
/* ── Read callback ── */ |
|
|
|
static void client_read_callback(socket_t fd, void* arg) { |
|
struct headless_client* cli = (struct headless_client*)arg; |
|
if (!cli || cli->closing) return; |
|
char tmp[4096]; |
|
ssize_t r = recv(fd, tmp, (int)sizeof(tmp) - 1, 0); |
|
if (r <= 0) { 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; } |
|
} else if (cli->recv_len < RECV_BUF_SIZE - 1) { |
|
cli->recv_buf[cli->recv_len++] = tmp[i]; |
|
} |
|
} |
|
} |
|
|
|
/* ── Command handlers ── */ |
|
|
|
static void hc_handle_ping(struct headless_client* cli, int id, const char* json) { |
|
(void)json; |
|
uint64_t uptime = 0; |
|
(void)uptime; |
|
char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":%llu}", (unsigned long long)uptime); |
|
send_response(cli, id, data, NULL); |
|
} |
|
|
|
static void hc_handle_status(struct headless_client* cli, int id, const char* json) { |
|
(void)json; |
|
char buf[8192]; int off = 0; |
|
off += snprintf(buf + off, sizeof(buf) - off, "\""); |
|
if (!g_hc.inst) { |
|
off += snprintf(buf + off, sizeof(buf) - off, "ERROR: no instance"); |
|
} else { |
|
int conn_count = g_hc.inst->connections ? queue_entry_count(g_hc.inst->connections) : 0; |
|
struct NTP_TIME* ntp = &g_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 = g_hc.inst->node_id; |
|
struct ll_entry* entry = g_hc.inst->connections ? g_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(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; |
|
} |
|
} |
|
if (off >= (int)sizeof(buf) - 2) off = (int)sizeof(buf) - 2; |
|
off += snprintf(buf + off, sizeof(buf) - off, "\""); |
|
send_response(cli, id, buf, NULL); |
|
} |
|
|
|
static void hc_handle_channels(struct headless_client* cli, int id, const char* json) { |
|
(void)json; |
|
char buf[16384]; size_t len = 0; |
|
if (chat_core_get_channels_json(buf, sizeof(buf), &len) < 0) |
|
send_response(cli, id, NULL, "failed to query channels"); |
|
else |
|
send_response(cli, id, buf, NULL); |
|
} |
|
|
|
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; } |
|
char buf[16384]; size_t len = 0; |
|
if (chat_core_get_members_json(ch, buf, sizeof(buf), &len) < 0) |
|
send_response(cli, id, NULL, "failed to query members"); |
|
else |
|
send_response(cli, id, buf, NULL); |
|
} |
|
|
|
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); |
|
char buf[16384]; size_t len = 0; |
|
if (chat_core_get_messages_json(ch, count, offset, buf, sizeof(buf), &len) < 0) |
|
send_response(cli, id, NULL, "failed to query messages"); |
|
else |
|
send_response(cli, id, buf, NULL); |
|
} |
|
|
|
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; } |
|
struct chat_msg_submit* req = u_calloc(1, sizeof(*req)); |
|
if (!req) { send_response(cli, id, NULL, "out of memory"); return; } |
|
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_len = (uint32_t)strlen(data); |
|
chat_core_submit_message(req); |
|
if (req->data) u_free(req->data); |
|
u_free(req); |
|
send_response(cli, id, "{\"sent\":true}", NULL); |
|
} |
|
|
|
static int hc_mkdirs(const char* path) { |
|
char tmp[1024]; snprintf(tmp, sizeof(tmp), "%s", path); |
|
size_t len = strlen(tmp); |
|
for (size_t i = 1; i < len; i++) { |
|
if (tmp[i] != '/') continue; |
|
tmp[i] = '\0'; |
|
if (utun_mkdir(tmp, 0755) != 0 && errno != EEXIST) { tmp[i] = '/'; return -1; } |
|
tmp[i] = '/'; |
|
} |
|
if (utun_mkdir(tmp, 0755) != 0 && errno != EEXIST) return -1; |
|
return 0; |
|
} |
|
|
|
static void hc_handle_attach(struct headless_client* cli, int id, const char* json) { |
|
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) |
|
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; } |
|
|
|
if (!g_hc.inst || !g_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(); |
|
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); |
|
} |
|
} |
|
|
|
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'; } |
|
|
|
char dir[640]; snprintf(dir, sizeof(dir), "%s/%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); |
|
} |
|
|
|
struct chat_msg_submit* req = u_calloc(1, sizeof(*req)); |
|
if (!req) { send_response(cli, id, NULL, "out of memory"); return; } |
|
snprintf(req->channel_id, sizeof(req->channel_id), "%s", ch); |
|
snprintf(req->content_type, sizeof(req->content_type), "%s", as_video ? "video/mp4" : ct); |
|
snprintf(req->media_src, sizeof(req->media_src), "%s", path); |
|
snprintf(req->media_dest, sizeof(req->media_dest), "%s", dest); |
|
req->media_copy = as_video ? 0 : 1; |
|
req->media_video = as_video ? 1 : 0; |
|
req->data = (uint8_t*)name; |
|
req->data_len = (uint32_t)strlen(name); |
|
|
|
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(req); |
|
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_handle_invite(struct headless_client* cli, int id, const char* json) { |
|
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 (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; } |
|
|
|
uint64_t channel_id = strtoull(ch, NULL, 10); |
|
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) |
|
target_node_id = strtoull(node_id_str, NULL, 16); |
|
|
|
char link[1024]; |
|
if (chat_invite_build_link(channel_id, target_node_id, NULL, link, sizeof(link)) < 0) { |
|
send_response(cli, id, NULL, "failed to build invite link (see log)"); |
|
return; |
|
} |
|
|
|
struct InviteData d; char err[128]; |
|
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { |
|
send_response(cli, id, NULL, "failed to decode built link"); |
|
return; |
|
} |
|
|
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite ch=%s target=0x%016llx node=0x%016llx addrs=%d", |
|
ch, (unsigned long long)target_node_id, (unsigned long long)d.nodeId, (int)d.addrCount); |
|
|
|
char resp[1280]; |
|
snprintf(resp, sizeof(resp), |
|
"{\"link\":\"%s\",\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d}", |
|
link, (unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount); |
|
send_response(cli, id, resp, NULL); |
|
} |
|
|
|
static void hc_handle_invite_nodes(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; } |
|
uint64_t channel_id = strtoull(ch, NULL, 10); |
|
if (channel_id == 0) { send_response(cli, id, NULL, "invalid channel_id"); return; } |
|
|
|
uint64_t ids[INVITE_ADDR_MAX]; |
|
int cnt = chat_invite_candidate_nodes(channel_id, ids, INVITE_ADDR_MAX); |
|
if (cnt < 0) { send_response(cli, id, NULL, "no candidates (see log)"); return; } |
|
|
|
uint64_t auto_id = 0; |
|
chat_invite_best_node(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++) { |
|
char name[64] = ""; |
|
chat_core_get_node_name(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); |
|
} |
|
off += snprintf(resp + off, sizeof(resp) - off, "]}"); |
|
send_response(cli, id, resp, NULL); |
|
} |
|
|
|
static void hc_handle_connect(struct headless_client* cli, int id, const char* json) { |
|
char link[1024]; |
|
if (json_get_str(json, "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 (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; } |
|
|
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: connect ch=%llu node=0x%016llx addrs=%d", |
|
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount); |
|
|
|
uint8_t addrs_buf[2048]; |
|
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf)); |
|
if (addrs_len <= 0) { send_response(cli, id, NULL, "serialize addrs failed"); return; } |
|
|
|
chat_core_ensure_channel_ready((const char*)(&(char[64]){0})); |
|
{ char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId); |
|
chat_core_ensure_channel_ready(chstr); } |
|
|
|
chat_core_set_my_node_id(g_hc.inst->node_id); |
|
|
|
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey, |
|
addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL); |
|
|
|
char resp[512]; snprintf(resp, sizeof(resp), |
|
"{\"channel_id\":%llu,\"node_id\":\"0x%016llx\",\"addrs\":%d,\"connecting\":true}", |
|
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount); |
|
send_response(cli, id, resp, NULL); |
|
} |
|
|
|
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; } |
|
chat_core_create_channel_auto(name); |
|
send_response(cli, id, "{\"created\":true}", NULL); |
|
} |
|
|
|
static void hc_handle_invite_to(struct headless_client* cli, int id, const char* json) { |
|
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 (!g_hc.inst) { send_response(cli, id, NULL, "no instance"); return; } |
|
|
|
uint64_t target_node_id = strtoull(node_id_str, NULL, 16); |
|
if (target_node_id == 0 || target_node_id == g_hc.inst->node_id) { send_response(cli, id, NULL, "invalid node_id"); return; } |
|
|
|
uint8_t pubkey_bin[SC_PUBKEY_SIZE]; |
|
if (sc_hex_to_binary(pubkey_hex, pubkey_bin, SC_PUBKEY_SIZE) != 0) { send_response(cli, id, NULL, "invalid pubkey hex"); return; } |
|
|
|
char ip[128]; snprintf(ip, sizeof(ip), "%s", addr); |
|
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; } |
|
|
|
uint8_t addrs_data[21]; int addrs_len = 0; |
|
struct in_addr a4; struct in6_addr a6; |
|
if (inet_pton(AF_INET, ip, &a4) == 1) { |
|
addrs_data[0] = 4; addrs_data[1] = 0; addrs_data[2] = (uint8_t)proto; |
|
memcpy(addrs_data + 3, &a4.s_addr, 4); |
|
addrs_data[7] = (uint8_t)(port >> 8); addrs_data[8] = (uint8_t)(port & 0xFF); |
|
addrs_len = 9; |
|
} else if (inet_pton(AF_INET6, ip, &a6) == 1) { |
|
addrs_data[0] = 6; addrs_data[1] = 0; addrs_data[2] = (uint8_t)proto; |
|
memcpy(addrs_data + 3, &a6, 16); |
|
addrs_data[19] = (uint8_t)(port >> 8); addrs_data[20] = (uint8_t)(port & 0xFF); |
|
addrs_len = 21; |
|
} else { send_response(cli, id, NULL, "invalid addr ip"); return; } |
|
|
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: invite_to ch=%s node=0x%016llx addr=%s:%d proto=%d", |
|
ch, (unsigned long long)target_node_id, ip, port, proto); |
|
|
|
chat_sync_invite_to_channel_with_addrs(g_hc.inst, ch, target_node_id, |
|
pubkey_bin, addrs_data, 1, addrs_len); |
|
|
|
char resp[256]; snprintf(resp, sizeof(resp), |
|
"{\"channel_id\":\"%s\",\"node_id\":\"0x%016llx\",\"inviting\":true}", |
|
ch, (unsigned long long)target_node_id); |
|
send_response(cli, id, resp, NULL); |
|
} |
|
|
|
static void hc_handle_subscribe(struct headless_client* cli, int id, const char* json) { |
|
int enable = json_get_int(json, "enable", 1); |
|
cli->subscribed = enable; |
|
char resp[64]; snprintf(resp, sizeof(resp), "{\"subscribed\":%s}", enable ? "true" : "false"); |
|
send_response(cli, id, resp, NULL); |
|
} |
|
|
|
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); |
|
} |
|
|
|
/* ── Command dispatch ── */ |
|
|
|
typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json); |
|
|
|
static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "quit", NULL }; |
|
static hc_cmd_fn cmd_handlers[] = { hc_handle_ping, hc_handle_status, hc_handle_channels, hc_handle_members, hc_handle_messages, hc_handle_send, hc_handle_attach, hc_handle_invite, hc_handle_invite_nodes, hc_handle_connect, hc_handle_create_channel, hc_handle_invite_to, hc_handle_subscribe, hc_handle_quit }; |
|
|
|
void hc_handle_command(struct headless_client* cli, const char* json) { |
|
if (!cli || !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'; |
|
for (int i = 0; cmd_names[i]; i++) { |
|
if (strcmp(cmd, cmd_names[i]) == 0) { cmd_handlers[i](cli, id, json); return; } |
|
} |
|
send_response(cli, id, NULL, "unknown command"); |
|
} |
|
|
|
/* ── Accept callback ── */ |
|
|
|
static void accept_callback(socket_t fd, void* arg) { |
|
(void)arg; |
|
struct sockaddr_storage addr; socklen_t alen = sizeof(addr); |
|
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 (g_hc.client_count >= MAX_CLIENTS) { socket_close_wrapper(cfd); DEBUG_WARN((int)DEBUG_CATEGORY_HEADLESS, "headless: max clients reached"); return; } |
|
|
|
struct headless_client* cli = u_calloc(1, sizeof(*cli)); |
|
if (!cli) { socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to allocate client"); return; } |
|
cli->fd = cfd; |
|
|
|
cli->socket_id = uasync_add_socket_t(g_hc.ua, cfd, client_read_callback, client_write_callback, NULL, cli); |
|
if (!cli->socket_id) { u_free(cli); socket_close_wrapper(cfd); DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: failed to register client fd"); return; } |
|
|
|
cli->next = g_hc.clients; g_hc.clients = cli; g_hc.client_count++; |
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, g_hc.client_count); |
|
} |
|
|
|
/* ── Periodic cleanup ── */ |
|
|
|
static void hc_cleanup(void* arg) { |
|
(void)arg; |
|
struct headless_client** prev = &g_hc.clients; |
|
while (*prev) { |
|
if ((*prev)->closing) { |
|
struct headless_client* dead = *prev; *prev = dead->next; |
|
if (dead->socket_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, dead->fd); |
|
if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd); |
|
u_free(dead); g_hc.client_count--; |
|
} else prev = &(*prev)->next; |
|
} |
|
if (g_hc.ua) uasync_set_timeout(g_hc.ua, 5000, NULL, hc_cleanup, "hc_cleanup"); |
|
} |
|
|
|
/* ── Init / Destroy ── */ |
|
|
|
int chat_headless_control_init(struct UASYNC* ua, struct UTUN_INSTANCE* inst, |
|
const char* bind_ip, int port) { |
|
if (!ua || !bind_ip || port <= 0 || port > 65535) return -1; |
|
if (g_hc.running) return 0; |
|
|
|
memset(&g_hc, 0, sizeof(g_hc)); |
|
g_hc.ua = ua; g_hc.inst = inst; |
|
|
|
g_hc.listen_fd = socket(AF_INET, SOCK_STREAM, 0); |
|
if (g_hc.listen_fd == SOCKET_INVALID) { DEBUG_ERROR((int)DEBUG_CATEGORY_HEADLESS, "headless: socket() failed: %s", socket_strerror(socket_get_error())); return -1; } |
|
|
|
socket_set_reuseaddr(g_hc.listen_fd, 1); |
|
socket_set_nonblocking(g_hc.listen_fd); |
|
|
|
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(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; } |
|
|
|
if (bind(g_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(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; |
|
} |
|
if (listen(g_hc.listen_fd, 5) < 0) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; } |
|
|
|
g_hc.listen_sock_id = uasync_add_socket_t(ua, g_hc.listen_fd, accept_callback, NULL, NULL, NULL); |
|
if (!g_hc.listen_sock_id) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; return -1; } |
|
|
|
chat_event_set_handler(hc_on_chat_event); |
|
uasync_set_timeout(ua, 5000, NULL, hc_cleanup, "hc_cleanup"); |
|
|
|
g_hc.running = 1; |
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port); |
|
return 0; |
|
} |
|
|
|
void chat_headless_control_destroy(void) { |
|
if (!g_hc.running) return; |
|
g_hc.running = 0; |
|
chat_event_set_handler(NULL); |
|
if (g_hc.listen_sock_id && g_hc.ua) uasync_remove_socket_t(g_hc.ua, g_hc.listen_fd); |
|
if (g_hc.listen_fd != SOCKET_INVALID) { socket_close_wrapper(g_hc.listen_fd); g_hc.listen_fd = SOCKET_INVALID; } |
|
struct headless_client* c = g_hc.clients; |
|
while (c) { struct headless_client* n = c->next; if (c->fd != SOCKET_INVALID) socket_close_wrapper(c->fd); u_free(c); c = n; } |
|
g_hc.clients = NULL; g_hc.client_count = 0; |
|
DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed"); |
|
}
|
|
|