/* * chat_headless_control.c — TCP control socket for headless chat CLI * * JSON line protocol: * Request: {"id":N,"cmd":"",...params} * Response: {"id":N,"ok":true,"data":{...}} | {"id":N,"ok":false,"error":"..."} * Event: {"event":"",...} * * Commands: ping, status, channels, members, messages, send, invite, * invite_nodes, connect, create_channel, subscribe, quit */ #include "chat_headless_control.h" #include "chat_core.h" #include "chat_core_priv.h" #include "chat_event.h" #include "chat_poll.h" #include "chat_sync.h" #include "chat_join.h" #include "invite_link.h" #include "invite_build.h" #include "../dm/dm_core.h" #include "../dm/dm_crypto.h" #include "../call/call.h" #include "../call/call_proto.h" #include "../radio/radio.h" #include "../transport_layer/secure_channel.h" #include "../transport_layer/etcp.h" #include "../transport_layer/etcp_connections.h" #include "../utun_instance.h" #include "../ntp_time.h" #include "../../lib/ll_queue.h" #include "../../lib/u_async.h" #include "../../lib/mem.h" #include "../../lib/strbuf.h" #include "../../lib/json_flat.h" #include "../../lib/socket_compat.h" #include "../../lib/debug_config.h" #include "../../lib/platform_compat.h" #include #include #include #include #include #include #include #include #define HC_ID "headless" #define RECV_BUF_SIZE 16384 #define SEND_BUF_SIZE 32768 #define MAX_CLIENTS 64 #ifndef DEBUG_CATEGORY_HEADLESS #define DEBUG_CATEGORY_HEADLESS DEBUG_CATEGORY_GENERAL #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_invite* invites; struct headless_control* hc; struct headless_client* next; }; struct headless_invite { struct headless_invite* next; struct headless_client* client; struct chat_invite_build* pending; int id; }; struct headless_control { struct UASYNC* ua; struct UTUN_INSTANCE* inst; struct headless_client* clients; int client_count; socket_t listen_fd; void* listen_sock_id; int running; void* cleanup_timer; }; static struct headless_control* hc_of(struct UTUN_INSTANCE* inst) { return inst ? (struct headless_control*)inst->headless : NULL; } /* ── Forward declarations ── */ static void hc_handle_command(struct headless_client* cli, const char* json); static void hc_close_client(struct headless_client* cli); /* ── 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) { 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); } } 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) { struct strbuf out = strbuf_new(); int rc; if (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) { 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), "{\"id\":%d,\"ok\":false,\"error\":\"Response too large or out of memory; request fewer records\"}\n", id); cli_send(cli, failure, bytes); } else cli_send(cli, strbuf_str(&out), out.len); strbuf_free(&out); } /* ── Broadcast event ── */ 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; for (struct headless_client* c = 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(struct UTUN_INSTANCE* inst, int type, const uint8_t* data, int len) { struct headless_control* hc = hc_of(inst); if (!hc) return; if (type == 10) return; /* skip status text */ 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]; for (unsigned i = 0; i < 32; i++) snprintf(message_id + i * 2, 3, "%02x", data[1 + cl + i]); snprintf(json, sizeof(json), "{\"event\":\"feedback\",\"ch\":\"%.*s\",\"message_id\":\"%s\",\"kind\":%u}", (int)cl, data + 1, message_id, data[33 + cl]); } else if (type == CHAT_EVT_MSG_RECEIVED) { if (!data || len < 11 || !data[0] || len != data[0] + 10 || data[9 + data[0]] > 1) { DEBUG_WARN(DEBUG_CATEGORY_CONTROL, "headless: invalid MSG_RECEIVED length=%d", len); return; } 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\",\"initial_sync\":%u}", (int)cl, data + 1, (unsigned long long)author, data[9 + cl]); } 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); } } } else if (type == 32 && data && len >= 10) { /* DM_MSG_RECEIVED */ uint8_t cl = data[0]; uint64_t author; memcpy(&author, data + 1 + cl, 8); snprintf(json, sizeof(json), "{\"event\":\"dm_msg\",\"conv\":\"%.*s\",\"author_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)author); } else if (type == 33 && data && len >= 2) { /* DM_CONV_UPDATED */ uint8_t cl = data[0]; snprintf(json, sizeof(json), "{\"event\":\"dm_conv\",\"conv\":\"%.*s\"}", (int)cl, data + 1); } else if (type == CHAT_EVT_CALL_INCOMING && data && len >= 1 + 8 + 8) { /* CALL_INCOMING */ uint8_t cl = data[0]; if (len >= 1 + (int)cl + 8 + 8) { uint64_t caller; memcpy(&caller, data + 1 + cl, 8); uint64_t cid; memcpy(&cid, data + 1 + cl + 8, 8); snprintf(json, sizeof(json), "{\"event\":\"call_incoming\",\"ch\":\"%.*s\",\"caller_id\":\"0x%016llx\",\"call_id\":\"0x%016llx\"}", (int)cl, data + 1, (unsigned long long)caller, (unsigned long long)cid); } } else if (type == CHAT_EVT_CALL_RINGING && data && len >= 8) { /* CALL_RINGING */ uint64_t cid; memcpy(&cid, data, 8); snprintf(json, sizeof(json), "{\"event\":\"call_ringing\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid); } else if (type == CHAT_EVT_CALL_ACCEPTED && data && len >= 8) { /* CALL_ACCEPTED */ uint64_t cid; memcpy(&cid, data, 8); snprintf(json, sizeof(json), "{\"event\":\"call_accepted\",\"call_id\":\"0x%016llx\"}", (unsigned long long)cid); } else if (type == CHAT_EVT_CALL_ENDED && data && len >= 9) { /* CALL_ENDED */ uint64_t cid; memcpy(&cid, data, 8); snprintf(json, sizeof(json), "{\"event\":\"call_ended\",\"call_id\":\"0x%016llx\",\"reason\":%d}", (unsigned long long)cid, data[8]); } 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]); } if (json[0]) hc_broadcast_event(hc, json); } /* ── Write callback ── */ static void client_write_callback(socket_t fd, void* arg) { struct headless_client* cli = (struct headless_client*)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); 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()); hc_close_client(cli); 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); } } /* ── Close client ── */ static void hc_close_client(struct headless_client* cli) { struct headless_control* hc = cli ? cli->hc : NULL; if (!cli || cli->closing) return; cli->closing = 1; while (cli->invites) chat_invite_build_cancel(cli->invites->pending); 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; } } /* ── 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; } if (cli->closing) return; } 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; char data[128]; snprintf(data, sizeof(data), "{\"pong\":true,\"uptime_sec\":0}"); send_response(cli, id, data, NULL); } 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 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; } } 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(cli->hc->inst, 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(cli->hc->inst, 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(cli->hc->inst, 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; } 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_len = (uint32_t)strlen(data); chat_core_submit_message(cli->hc->inst, 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) { 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) 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 (!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); } } 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; } 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); 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(hc->inst, 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_invite_ready(void* arg, int result, const char* link) { struct headless_invite* req = arg; struct headless_client* cli = req->client; struct headless_invite** p = &cli->invites; while (*p && *p != req) p = &(*p)->next; if (*p) *p = req->next; int id = req->id; u_free(req); if (cli->closing) return; if (result != CHAT_JOIN_OK) { send_response(cli, id, NULL, result == CHAT_JOIN_TIMEOUT ? "invite registration timed out" : "invite registration failed"); return; } struct InviteData d; char err[128]; if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cannot decode confirmed invite: %s", err); send_response(cli, id, NULL, "failed to decode built link"); return; } DEBUG_INFO(DEBUG_CATEGORY_HEADLESS, "headless: invite confirmed ch=%llu node=%016llx addrs=%d", (unsigned long long)d.channelId, (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(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 (!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); struct headless_invite* req = u_calloc(1, sizeof(*req)); if (!req) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invite allocation failed"); send_response(cli, id, NULL, "out of memory"); return; } req->client = cli; req->id = id; req->pending = chat_invite_build_link(hc->inst, channel_id, target_node_id, NULL, hc_invite_ready, req); if (!req->pending) { u_free(req); send_response(cli, id, NULL, "failed to build invite link (see log)"); return; } req->next = cli->invites; cli->invites = req; } 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; } 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(hc->inst, 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(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++) { 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); } 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) { struct headless_control* hc = cli->hc; 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 (!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; } { char chstr[64]; snprintf(chstr, sizeof(chstr), "%llu", (unsigned long long)d.channelId); chat_core_ensure_channel_ready(hc->inst, chstr); } chat_core_set_my_node_id(hc->inst, hc->inst->node_id); chat_sync_connect_from_invite(hc->inst, d.channelId, d.nodeId, d.pubkey, addrs_buf, d.addrCount, addrs_len, d.password_len ? d.password : NULL, d.join_key); 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(cli->hc->inst, name); send_response(cli, id, "{\"created\":true}", NULL); } 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 (!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 == 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[22]; int addrs_len = 0; struct in_addr a4; struct in6_addr a6; if (inet_pton(AF_INET, ip, &a4) == 1) { addrs_data[0] = 0; /* reality-префикс: has=0 */ addrs_data[1] = 4; addrs_data[2] = 0; addrs_data[3] = (uint8_t)proto; memcpy(addrs_data + 4, &a4.s_addr, 4); addrs_data[8] = (uint8_t)(port >> 8); addrs_data[9] = (uint8_t)(port & 0xFF); addrs_len = 10; } else if (inet_pton(AF_INET6, ip, &a6) == 1) { addrs_data[0] = 0; /* reality-префикс: has=0 */ addrs_data[1] = 6; addrs_data[2] = 0; addrs_data[3] = (uint8_t)proto; memcpy(addrs_data + 4, &a6, 16); addrs_data[20] = (uint8_t)(port >> 8); addrs_data[21] = (uint8_t)(port & 0xFF); addrs_len = 22; } 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(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); } /* ── DM (прямой чат) ── */ 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'; 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]; 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) { 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) { 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; } } } if (st) sqlite3_finalize(st); if (!ok) { send_response(cli, id, NULL, "failed to start DM (node not found?)"); 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); char resp[256]; snprintf(resp, sizeof(resp), "{\"conv_id\":\"%s\",\"started\":true}", conv); send_response(cli, id, resp, NULL); } /* Список DM-бесед. */ static void hc_handle_dm_list(struct headless_client* cli, int id, const char* json) { (void)json; char buf[16384]; size_t len = 0; if (dm_list_conversations_json(cli->hc->inst, buf, sizeof(buf), &len) < 0) send_response(cli, id, NULL, "failed to query conversations"); else send_response(cli, id, buf, NULL); } /* Сообщения 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); 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"); else send_response(cli, id, buf, NULL); } /* Отправить сообщение в 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 (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 send_response(cli, id, "{\"sent\":true}", NULL); } /* ── Звонки (P2P аудио) ── */ static uint64_t hc_gen_call_id(void) { uint64_t id = ((uint64_t)rand() << 32) ^ (uint64_t)rand(); return id ? id : 1; } 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; } 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 (!call_id) call_id = hc_gen_call_id(); 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); send_response(cli, id, resp, NULL); } 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; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_accept(cli->hc->inst, call_id); send_response(cli, id, "{\"accepted\":true}", NULL); } 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; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_decline(cli->hc->inst, call_id); send_response(cli, id, "{\"declined\":true}", NULL); } 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; } uint64_t call_id = strtoull(call_id_str, NULL, 16); call_hangup(cli->hc->inst, call_id); send_response(cli, id, "{\"hungup\":true}", NULL); } /* ── Рация (PTT walkie-talkie) ── */ 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; } 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); char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":true,\"ch\":\"%llu\"}", (unsigned long long)group_id); send_response(cli, id, resp, NULL); } 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; } 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); char resp[128]; snprintf(resp, sizeof(resp), "{\"radio\":false,\"ch\":\"%llu\"}", (unsigned long long)group_id); send_response(cli, id, resp, NULL); } 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; } 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); send_response(cli, id, resp, NULL); } /* ── Command dispatch ── */ typedef void (*hc_cmd_fn)(struct headless_client* cli, int id, const char* json); /* Опросы и реакции используют общий командный API ядра. */ static void hc_handle_feedback(struct headless_client* cli, int id, const char* json) { char* result = NULL; int rc = chat_feedback_command(cli->hc->inst, json, &result); if (rc == 0) send_response(cli, id, result, NULL); else { char error[256] = "Interaction failed"; if (result && json_flat_get(result, "error", error, sizeof(error)) < 0) DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: invalid interaction error JSON"); send_response(cli, id, NULL, error); } u_free(result); } static const char* cmd_names[] = { "ping", "status", "channels", "members", "messages", "send", "attach", "invite", "invite_nodes", "connect", "create_channel", "invite_to", "subscribe", "dm_start", "dm_list", "dm_messages", "dm_send", "call_start", "call_accept", "call_decline", "call_hangup", "radio_on", "radio_off", "radio_subscribers", "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_dm_start, hc_handle_dm_list, hc_handle_dm_messages, hc_handle_dm_send, hc_handle_call_start, hc_handle_call_accept, hc_handle_call_decline, hc_handle_call_hangup, hc_handle_radio_on, hc_handle_radio_off, hc_handle_radio_subscribers, hc_handle_quit }; 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'; 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; } 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) { struct headless_control* hc = (struct headless_control*)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 (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->hc = hc; cli->socket_id = uasync_add_socket_t(hc->ua, cfd, client_read_callback, client_write_callback, NULL, "chatctl_client", 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 = hc->clients; hc->clients = cli; hc->client_count++; DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: client connected fd=%d total=%d", (int)cfd, hc->client_count); } /* ── Periodic cleanup ── */ static void hc_cleanup(void* arg) { struct headless_control* hc = (struct headless_control*)arg; hc->cleanup_timer = NULL; struct headless_client** prev = &hc->clients; while (*prev) { if ((*prev)->closing) { struct headless_client* dead = *prev; *prev = dead->next; if (dead->socket_id && hc->ua) uasync_remove_socket_t(hc->ua, dead->fd); if (dead->fd != SOCKET_INVALID) socket_close_wrapper(dead->fd); u_free(dead); hc->client_count--; } 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"); } /* ── 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) return -1; struct headless_control* hc = hc_of(inst); if (hc && hc->running) return 0; if (!hc) { hc = u_calloc(1, sizeof(*hc)); if (!hc) return -1; inst->headless = hc; } hc->ua = ua; hc->inst = inst; 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; } socket_set_reuseaddr(hc->listen_fd, 1); socket_set_nonblocking(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(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } 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; } if (listen(hc->listen_fd, 5) < 0) { socket_close_wrapper(hc->listen_fd); hc->listen_fd = SOCKET_INVALID; return -1; } 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; } 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) { DEBUG_ERROR(DEBUG_CATEGORY_HEADLESS, "headless: cleanup timer allocation failed"); chat_headless_control_destroy(inst); return -1; } hc->running = 1; DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: listening on %s:%d", bind_ip, port); return 0; } void chat_headless_control_destroy(struct UTUN_INSTANCE* inst) { struct headless_control* hc = hc_of(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 (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; while (c) { struct headless_client* n = c->next; hc_close_client(c); u_free(c); c = n; } inst->headless = NULL; u_free(hc); DEBUG_INFO((int)DEBUG_CATEGORY_HEADLESS, "headless: destroyed"); }