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.
385 lines
13 KiB
385 lines
13 KiB
/* |
|
* headless_control.c — JSON control socket implementation |
|
*/ |
|
#include "headless_control.h" |
|
#include "../libutun_lite/utun_config_api.h" |
|
#include "../libutun_lite/invite_link_c.h" |
|
#include "../../../src/chat/chat_setting.h" |
|
#include "../../../src/chat/chat_sync.h" |
|
#include "../../../lib/debug_config.h" |
|
|
|
#include <stdint.h> |
|
#include <stdio.h> |
|
#include <stdlib.h> |
|
#include <string.h> |
|
#include <errno.h> |
|
|
|
#ifdef __linux__ |
|
#include <unistd.h> |
|
#include <sys/socket.h> |
|
#include <netinet/in.h> |
|
#include <arpa/inet.h> |
|
#include <fcntl.h> |
|
#include <sys/select.h> |
|
#endif |
|
|
|
/* ── Simple JSON helpers (no library dependency) ── */ |
|
|
|
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++; |
|
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); |
|
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; |
|
return p + 7; |
|
} |
|
|
|
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; |
|
} |
|
|
|
/* ── Client state ── */ |
|
|
|
#define MAX_CLIENTS 16 |
|
#define BUF_SIZE 16384 |
|
|
|
struct control_client { |
|
int fd; |
|
char recv_buf[BUF_SIZE]; |
|
int recv_len; |
|
int subscribed; |
|
struct control_client* next; |
|
}; |
|
|
|
static struct control_client* g_clients = NULL; |
|
static int g_listen_fd = -1; |
|
static int g_running = 0; |
|
|
|
/* ── Forward declarations ── */ |
|
|
|
static void send_response(int fd, int id, const char* ok_data, const char* error); |
|
static void handle_command(struct control_client* cli, const char* json); |
|
static void send_event_to_client(struct control_client* cli, const char* event_json); |
|
|
|
/* ── Init / Destroy ── */ |
|
|
|
int headless_control_init(struct UASYNC* ua, const char* bind_ip, int port) { |
|
(void)ua; |
|
if (port <= 0 || port > 65535) return -1; |
|
if (!bind_ip) bind_ip = "127.0.0.1"; |
|
|
|
g_listen_fd = socket(AF_INET, SOCK_STREAM, 0); |
|
if (g_listen_fd < 0) { fprintf(stderr, "control: socket() failed: %s\n", strerror(errno)); return -1; } |
|
|
|
int reuse = 1; |
|
setsockopt(g_listen_fd, SOL_SOCKET, SO_REUSEADDR, &reuse, sizeof(reuse)); |
|
|
|
struct sockaddr_in addr; |
|
memset(&addr, 0, sizeof(addr)); |
|
addr.sin_family = AF_INET; |
|
addr.sin_port = htons((uint16_t)port); |
|
inet_pton(AF_INET, bind_ip, &addr.sin_addr); |
|
|
|
if (bind(g_listen_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { |
|
fprintf(stderr, "control: bind(%s:%d) failed: %s\n", bind_ip, port, strerror(errno)); |
|
close(g_listen_fd); g_listen_fd = -1; return -1; |
|
} |
|
|
|
if (listen(g_listen_fd, 5) < 0) { |
|
fprintf(stderr, "control: listen() failed: %s\n", strerror(errno)); |
|
close(g_listen_fd); g_listen_fd = -1; return -1; |
|
} |
|
|
|
int flags = fcntl(g_listen_fd, F_GETFL, 0); |
|
if (flags >= 0) fcntl(g_listen_fd, F_SETFL, flags | O_NONBLOCK); |
|
|
|
g_running = 1; |
|
printf("control: listening on %s:%d\n", bind_ip, port); |
|
return 0; |
|
} |
|
|
|
void headless_control_destroy(void) { |
|
struct control_client* c = g_clients; |
|
while (c) { struct control_client* next = c->next; if (c->fd >= 0) close(c->fd); free(c); c = next; } |
|
g_clients = NULL; |
|
if (g_listen_fd >= 0) { close(g_listen_fd); g_listen_fd = -1; } |
|
g_running = 0; |
|
} |
|
|
|
/* ── Process (call from main loop) ── */ |
|
|
|
void headless_control_poll(void) { |
|
if (!g_running || g_listen_fd < 0) return; |
|
|
|
fd_set rfds; |
|
FD_ZERO(&rfds); |
|
FD_SET(g_listen_fd, &rfds); |
|
int maxfd = g_listen_fd; |
|
|
|
for (struct control_client* c = g_clients; c; c = c->next) { |
|
FD_SET(c->fd, &rfds); |
|
if (c->fd > maxfd) maxfd = c->fd; |
|
} |
|
|
|
struct timeval tv = {0, 100000}; /* 100ms */ |
|
int n = select(maxfd + 1, &rfds, NULL, NULL, &tv); |
|
if (n <= 0) return; |
|
|
|
/* Accept new connections */ |
|
if (FD_ISSET(g_listen_fd, &rfds)) { |
|
int cfd = accept(g_listen_fd, NULL, NULL); |
|
if (cfd >= 0) { |
|
int flags = fcntl(cfd, F_GETFL, 0); |
|
if (flags >= 0) fcntl(cfd, F_SETFL, flags | O_NONBLOCK); |
|
struct control_client* cli = calloc(1, sizeof(*cli)); |
|
if (cli) { cli->fd = cfd; cli->next = g_clients; g_clients = cli; } |
|
else close(cfd); |
|
} |
|
} |
|
|
|
/* Read from clients */ |
|
for (struct control_client* c = g_clients; c; c = c->next) { |
|
if (!FD_ISSET(c->fd, &rfds)) continue; |
|
char tmp[4096]; |
|
ssize_t r = read(c->fd, tmp, sizeof(tmp) - 1); |
|
if (r <= 0) { close(c->fd); c->fd = -1; continue; } |
|
for (ssize_t i = 0; i < r; i++) { |
|
if (tmp[i] == '\n') { |
|
if (c->recv_len > 0) { |
|
c->recv_buf[c->recv_len] = '\0'; |
|
handle_command(c, c->recv_buf); |
|
c->recv_len = 0; |
|
} |
|
} else if (c->recv_len < (int)(BUF_SIZE - 1)) { |
|
c->recv_buf[c->recv_len++] = tmp[i]; |
|
} |
|
} |
|
} |
|
|
|
/* Clean up closed clients */ |
|
struct control_client** prev = &g_clients; |
|
while (*prev) { |
|
if ((*prev)->fd < 0) { |
|
struct control_client* dead = *prev; |
|
*prev = dead->next; |
|
free(dead); |
|
} else prev = &(*prev)->next; |
|
} |
|
} |
|
|
|
/* ── Send helpers ── */ |
|
|
|
static void send_raw(int fd, const char* data, size_t len) { |
|
if (fd < 0 || !data || len == 0) return; |
|
size_t sent = 0; |
|
while (sent < len) { |
|
ssize_t r = write(fd, data + sent, len - sent); |
|
if (r <= 0) break; |
|
sent += (size_t)r; |
|
} |
|
} |
|
|
|
static void send_response(int fd, 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) send_raw(fd, buf, (size_t)len); |
|
} |
|
|
|
static void send_event_to_client(struct control_client* cli, const char* event_json) { |
|
if (!cli || !cli->subscribed || cli->fd < 0) return; |
|
send_raw(cli->fd, event_json, strlen(event_json)); |
|
/* Append \n if not present */ |
|
size_t elen = strlen(event_json); |
|
if (elen == 0 || event_json[elen - 1] != '\n') |
|
send_raw(cli->fd, "\n", 1); |
|
} |
|
|
|
/* ── Event broadcasting ── */ |
|
|
|
void headless_control_event(const char* json_event) { |
|
for (struct control_client* c = g_clients; c; c = c->next) |
|
send_event_to_client(c, json_event); |
|
} |
|
|
|
void headless_control_broadcast_log(const char* level, const char* category, const char* msg) { |
|
char buf[1024]; |
|
snprintf(buf, sizeof(buf), "{\"event\":\"log\",\"level\":\"%s\",\"cat\":\"%s\",\"msg\":\"%s\"}", |
|
level ? level : "info", category ? category : "", msg ? msg : ""); |
|
headless_control_event(buf); |
|
} |
|
|
|
/* ── Command handlers ── */ |
|
|
|
static int handle_ping(int fd, int id, const char* json) { |
|
(void)json; |
|
send_response(fd, id, "{\"pong\":true,\"uptime\":0}", NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_status(int fd, int id, const char* json) { |
|
(void)json; |
|
send_response(fd, id, "{\"node_id\":\"0\",\"uptime\":0,\"peers\":0,\"channels\":0}", NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_connections(int fd, int id, const char* json) { |
|
(void)json; |
|
send_response(fd, id, "{\"connections\":[]}", NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_subscribe(int fd, int id, const char* json) { |
|
int enable = json_get_int(json, "enable", 1); |
|
for (struct control_client* c = g_clients; c; c = c->next) |
|
if (c->fd == fd) { c->subscribed = enable; break; } |
|
send_response(fd, id, enable ? "{\"subscribed\":true}" : "{\"subscribed\":false}", NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_debug_level(int fd, int id, const char* json) { |
|
(void)json; |
|
send_response(fd, id, "{\"global\":\"info\"}", NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_quit(int fd, int id, const char* json) { |
|
(void)json; |
|
send_response(fd, id, "{\"bye\":true}", NULL); |
|
return -1; |
|
} |
|
|
|
static int handle_join(int fd, int id, const char* json) { |
|
char link[1024]; |
|
if (json_get_str(json, "link", link, sizeof(link)) < 0) { |
|
send_response(fd, id, NULL, "missing link"); |
|
return 0; |
|
} |
|
struct InviteDataC d; |
|
char err[256]; |
|
if (invite_link_decode(link, strlen(link), &d, err, sizeof(err)) < 0) { |
|
send_response(fd, id, NULL, err); |
|
return 0; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "join: ch=%llu node=0x%016llx pubkey=%02x%02x%02x%02x... addrs=%d", |
|
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, |
|
d.pubkey[0], d.pubkey[1], d.pubkey[2], d.pubkey[3], (int)d.addrCount); |
|
|
|
for (int i = 0; i < (int)d.addrCount; i++) { |
|
struct InviteAddrC* a = &d.addrs[i]; |
|
char ip[64]; snprintf(ip, sizeof(ip), a->family == 6 ? "v6" : "%d.%d.%d.%d", |
|
a->address[0], a->address[1], a->address[2], a->address[3]); |
|
const char* proto = a->proto == 1 ? "UDP" : a->proto == 2 ? "TCP" : a->proto == 3 ? "UDP+TCP" : "?"; |
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, " addr[%d]: %s:%d %s sock=%d", i, ip, (int)a->port, proto, (int)a->socketId); |
|
} |
|
|
|
uint8_t addrs_buf[512]; |
|
int addrs_len = invite_serialize_addrs(&d, addrs_buf, sizeof(addrs_buf)); |
|
if (addrs_len <= 0) { |
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "join: invite_serialize_addrs failed"); |
|
send_response(fd, id, NULL, "internal error: serialize failed"); |
|
return 0; |
|
} |
|
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "join: calling chat_sync_connect_from_invite..."); |
|
chat_sync_connect_from_invite(d.channelId, d.nodeId, d.pubkey, addrs_buf, d.addrCount, addrs_len); |
|
|
|
char resp[512]; |
|
snprintf(resp, sizeof(resp), |
|
"{\"channel_id\":%llu,\"node_id\":\"0x%llx\",\"addrs\":%d,\"connecting\":true}", |
|
(unsigned long long)d.channelId, (unsigned long long)d.nodeId, (int)d.addrCount); |
|
send_response(fd, id, resp, NULL); |
|
return 0; |
|
} |
|
|
|
static int handle_chat_setting(int fd, int id, const char* json) { |
|
char name[128], value[128]; |
|
if (json_get_str(json, "name", name, sizeof(name)) < 0 || json_get_str(json, "value", value, sizeof(value)) < 0) { |
|
send_response(fd, id, NULL, "missing name or value"); |
|
return 0; |
|
} |
|
int rc = chat_setting_set(name, value); |
|
if (rc == -1) { send_response(fd, id, NULL, "unknown setting"); return 0; } |
|
if (rc == -2) { send_response(fd, id, NULL, "invalid value"); return 0; } |
|
printf("control: chat_setting %s=%s\n", name, value); |
|
send_response(fd, id, "{\"set\":true}", NULL); |
|
return 0; |
|
} |
|
|
|
/* ── Command dispatch ── */ |
|
|
|
typedef int (*cmd_handler_fn)(int fd, int id, const char* json); |
|
|
|
static const char* const g_cmd_names[] = { |
|
"ping", "status", "connect", "disconnect", "connections", |
|
"create_channel", "channels", "send", "messages", |
|
"debug_level", "subscribe", "join", "chat_setting", "quit", NULL |
|
}; |
|
|
|
static cmd_handler_fn g_cmd_handlers[] = { |
|
handle_ping, handle_status, NULL, NULL, handle_connections, |
|
NULL, NULL, NULL, NULL, |
|
handle_debug_level, handle_subscribe, handle_join, handle_chat_setting, handle_quit |
|
}; |
|
|
|
static void handle_command(struct control_client* cli, const char* json) { |
|
if (!cli || !json) return; |
|
|
|
int id = json_get_int(json, "id", 0); |
|
const char* cmd_start = json_get_cmd(json); |
|
if (!cmd_start) { send_response(cli->fd, id, NULL, "missing cmd"); return; } |
|
|
|
const char* cmd_end = json_get_cmd_end(cmd_start); |
|
size_t cmd_len = (size_t)(cmd_end - cmd_start); |
|
if (cmd_len == 0 || cmd_len > 31) { send_response(cli->fd, id, NULL, "invalid cmd"); return; } |
|
|
|
char cmd[32]; |
|
memcpy(cmd, cmd_start, cmd_len); |
|
cmd[cmd_len] = '\0'; |
|
|
|
for (int i = 0; g_cmd_names[i]; i++) { |
|
if (strcmp(cmd, g_cmd_names[i]) == 0) { |
|
if (g_cmd_handlers[i]) { |
|
int rc = g_cmd_handlers[i](cli->fd, id, json); |
|
if (rc < 0) { close(cli->fd); cli->fd = -1; } |
|
} else { |
|
send_response(cli->fd, id, NULL, "not implemented"); |
|
} |
|
return; |
|
} |
|
} |
|
send_response(cli->fd, id, NULL, "unknown command"); |
|
}
|
|
|