summaryrefslogtreecommitdiff
path: root/src/client.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/client.c')
-rw-r--r--src/client.c320
1 files changed, 320 insertions, 0 deletions
diff --git a/src/client.c b/src/client.c
new file mode 100644
index 0000000..034ea74
--- /dev/null
+++ b/src/client.c
@@ -0,0 +1,320 @@
+#include "client.h"
+#include "proto.h"
+#include "unit.h"
+#include "log.h"
+#include "umbrella.h"
+#include "console/rcon.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+#include <sys/epoll.h>
+#include <sys/stat.h>
+
+/* ── Socket setup ────────────────────────────────────────────────────────── */
+
+int client_listen(void) {
+ /* Remove stale socket if present */
+ unlink(UMBRELLA_SOCK_PATH);
+
+ int fd = socket(AF_UNIX, SOCK_STREAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0);
+ if (fd < 0) {
+ log_error("socket: %s", strerror(errno));
+ return -1;
+ }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ strncpy(addr.sun_path, UMBRELLA_SOCK_PATH, sizeof(addr.sun_path) - 1);
+
+ if (bind(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ log_error("bind %s: %s", UMBRELLA_SOCK_PATH, strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ /* Owner read/write, group read/write, others nothing */
+ chmod(UMBRELLA_SOCK_PATH, 0660);
+
+ if (listen(fd, 16) != 0) {
+ log_error("listen: %s", strerror(errno));
+ close(fd);
+ return -1;
+ }
+
+ log_info("Listening on %s", UMBRELLA_SOCK_PATH);
+ return fd;
+}
+
+/* ── Client lifecycle ────────────────────────────────────────────────────── */
+
+int client_accept(int listen_fd) {
+ if (g.client_count >= MAX_CLIENTS) {
+ log_warn("Max clients reached, rejecting connection");
+ /* Accept and immediately close */
+ int tmp = accept(listen_fd, NULL, NULL);
+ if (tmp >= 0) close(tmp);
+ return -1;
+ }
+
+ int fd = accept4(listen_fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
+ if (fd < 0) {
+ if (errno != EAGAIN && errno != EWOULDBLOCK)
+ log_error("accept: %s", strerror(errno));
+ return -1;
+ }
+
+ /* Get peer credentials for access control */
+ struct ucred cred;
+ socklen_t cred_len = sizeof(cred);
+ int uid = -1;
+ if (getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &cred, &cred_len) == 0)
+ uid = (int)cred.uid;
+
+ /* Find a free client slot */
+ int slot = -1;
+ for (int i = 0; i < MAX_CLIENTS; i++) {
+ if (g.clients[i].fd == 0) { slot = i; break; }
+ }
+ if (slot < 0) {
+ close(fd);
+ return -1;
+ }
+
+ g.clients[slot].fd = fd;
+ g.clients[slot].uid = uid;
+ g.clients[slot].attached[0] = '\0';
+ g.client_count++;
+
+ /* Register with epoll */
+ struct epoll_event ev;
+ ev.events = EPOLLIN | EPOLLRDHUP;
+ ev.data.fd = fd;
+ epoll_ctl(g.epoll_fd, EPOLL_CTL_ADD, fd, &ev);
+
+ log_debug("Client connected (fd=%d uid=%d)", fd, uid);
+ return 0;
+}
+
+Client *client_find(int fd) {
+ for (int i = 0; i < MAX_CLIENTS; i++) {
+ if (g.clients[i].fd == fd)
+ return &g.clients[i];
+ }
+ return NULL;
+}
+
+void client_remove(int fd) {
+ for (int i = 0; i < MAX_CLIENTS; i++) {
+ if (g.clients[i].fd == fd) {
+ log_debug("Client disconnected (fd=%d)", fd);
+ epoll_ctl(g.epoll_fd, EPOLL_CTL_DEL, fd, NULL);
+ close(fd);
+ memset(&g.clients[i], 0, sizeof(Client));
+ g.client_count--;
+ return;
+ }
+ }
+}
+
+void client_broadcast_output(const char *unit_name,
+ const char *data, int history) {
+ for (int i = 0; i < MAX_CLIENTS; i++) {
+ if (g.clients[i].fd == 0) continue;
+ if (strcmp(g.clients[i].attached, unit_name) == 0)
+ proto_send_output(g.clients[i].fd, unit_name, data, history);
+ }
+}
+
+/* ── Command handlers ────────────────────────────────────────────────────── */
+
+static void cmd_list(Client *c) {
+ /* Build a JSON array of unit summaries */
+ char buf[PROTO_MAX_MSG];
+ int pos = 0;
+
+ pos += snprintf(buf + pos, sizeof(buf) - pos,
+ "{\"type\":\"list\",\"units\":[");
+
+ for (int i = 0; i < g.unit_count; i++) {
+ Unit *u = &g.units[i];
+ if (i > 0)
+ pos += snprintf(buf + pos, sizeof(buf) - pos, ",");
+ pos += snprintf(buf + pos, sizeof(buf) - pos,
+ "{\"name\":\"%s\",\"display\":\"%s\","
+ "\"state\":\"%s\"}",
+ u->name, u->display, unit_state_str(u->state));
+ }
+
+ snprintf(buf + pos, sizeof(buf) - pos, "]}");
+ proto_send(c->fd, buf);
+}
+
+static const char *probe_rcon_state(Unit *u) {
+ if (u->console.type != CONSOLE_RCON)
+ return unit_state_str(u->state);
+
+ /* Check systemd service state first -- cheap, no network */
+ if (u->service[0]) {
+ char cmd[256];
+ snprintf(cmd, sizeof(cmd),
+ "systemctl is-active --quiet %s 2>/dev/null", u->service);
+ if (system(cmd) != 0)
+ return "stopped";
+ }
+
+ /* Service is active -- probe RCON to confirm reachability */
+ char response[64] = {0};
+ int r = rcon_exec(u->console.host, u->console.port,
+ u->console.password, "echo umbrella_probe",
+ response, sizeof(response));
+ return (r == 0) ? "running" : "unreachable";
+}
+
+static void cmd_status(Client *c, const char *unit_name) {
+ Unit *u = unit_find(unit_name);
+ if (!u) { proto_send_error(c->fd, "unit not found"); return; }
+
+ const char *state = probe_rcon_state(u);
+
+ char buf[512];
+ snprintf(buf, sizeof(buf),
+ "{\"type\":\"status\",\"unit\":\"%s\",\"display\":\"%s\","
+ "\"state\":\"%s\",\"pid\":%d}",
+ u->name, u->display, state, (int)u->pid);
+ proto_send(c->fd, buf);
+}
+static void cmd_attach(Client *c, const char *unit_name) {
+ Unit *u = unit_find(unit_name);
+ if (!u) { proto_send_error(c->fd, "unit not found"); return; }
+
+ strncpy(c->attached, unit_name, MAX_NAME - 1);
+ log_info("Client fd=%d attached to %s", c->fd, unit_name);
+
+ /* Send ring buffer history first */
+ RingBuffer *rb = u->output;
+ if (rb && rb->count > 0) {
+ int start = (rb->head - rb->count + RING_BUF_LINES) % RING_BUF_LINES;
+ for (int i = 0; i < rb->count; i++) {
+ int idx = (start + i) % RING_BUF_LINES;
+ proto_send_output(c->fd, unit_name, rb->lines[idx], 1);
+ }
+ }
+
+ proto_send_ok(c->fd);
+}
+
+static void cmd_detach(Client *c) {
+ if (c->attached[0])
+ log_info("Client fd=%d detached from %s", c->fd, c->attached);
+ c->attached[0] = '\0';
+ proto_send_ok(c->fd);
+}
+
+static void cmd_input(Client *c, const char *unit_name, const char *data) {
+ Unit *u = unit_find(unit_name);
+ if (!u) { proto_send_error(c->fd, "unit not found"); return; }
+
+ if (u->console.type == CONSOLE_RCON) {
+ /* Send via RCON — open a connection, send, close */
+ char response[4096] = {0};
+ int r = rcon_exec(u->console.host, u->console.port,
+ u->console.password, data,
+ response, sizeof(response));
+ if (r != 0) {
+ proto_send_error(c->fd, "RCON command failed");
+ return;
+ }
+ /* Echo the response back as output */
+ if (response[0])
+ proto_send_output(c->fd, unit_name, response, 0);
+ proto_send_ok(c->fd);
+
+ } else if (u->console.type == CONSOLE_STDIN) {
+ if (u->stdin_fd < 0) {
+ proto_send_error(c->fd, "process not running");
+ return;
+ }
+ ssize_t n = write(u->stdin_fd, data, strlen(data));
+ if (n < 0) {
+ proto_send_error(c->fd, "write to stdin failed");
+ return;
+ }
+ proto_send_ok(c->fd);
+ }
+}
+
+static void cmd_action(Client *c, const char *unit_name,
+ const char *action_name) {
+ Unit *u = unit_find(unit_name);
+ if (!u) { proto_send_error(c->fd, "unit not found"); return; }
+
+ /* Find the action */
+ const char *script = NULL;
+ for (int i = 0; i < u->action_count; i++) {
+ if (strcmp(u->actions[i].name, action_name) == 0) {
+ script = u->actions[i].script;
+ break;
+ }
+ }
+ if (!script) { proto_send_error(c->fd, "action not found"); return; }
+
+ log_info("Running action '%s' for unit %s: %s",
+ action_name, unit_name, script);
+
+ /* Fork a child to run the script */
+ pid_t pid = fork();
+ if (pid < 0) {
+ proto_send_error(c->fd, "fork failed");
+ return;
+ }
+ if (pid == 0) {
+ /* Child: exec the script */
+ execl("/bin/bash", "bash", script, NULL);
+ _exit(127);
+ }
+
+ /* Parent: don't wait — SIGCHLD handler will reap it */
+ proto_send_ok(c->fd);
+}
+
+/* ── Main dispatch ───────────────────────────────────────────────────────── */
+
+int client_handle(int fd) {
+ Client *c = client_find(fd);
+ if (!c) return -1;
+
+ char buf[PROTO_MAX_MSG];
+ int n = proto_recv(fd, buf, sizeof(buf));
+
+ if (n <= 0) return -1; /* 0 = disconnect, -1 = error */
+
+ /* Extract command and fields */
+ char cmd[32] = {0};
+ char unit[MAX_NAME] = {0};
+ char action[MAX_NAME] = {0};
+ char data[RING_BUF_LINE_MAX] = {0};
+
+ json_get_str(buf, "cmd", cmd, sizeof(cmd));
+ json_get_str(buf, "unit", unit, sizeof(unit));
+ json_get_str(buf, "action", action, sizeof(action));
+ json_get_str(buf, "data", data, sizeof(data));
+
+ log_debug("Client fd=%d cmd=%s unit=%s", fd, cmd, unit);
+
+ if (strcmp(cmd, "list") == 0) cmd_list(c);
+ else if (strcmp(cmd, "status") == 0) cmd_status(c, unit);
+ else if (strcmp(cmd, "attach") == 0) cmd_attach(c, unit);
+ else if (strcmp(cmd, "detach") == 0) cmd_detach(c);
+ else if (strcmp(cmd, "input") == 0) cmd_input(c, unit, data);
+ else if (strcmp(cmd, "action") == 0) cmd_action(c, unit, action);
+ else proto_send_error(fd, "unknown command");
+
+ return 0;
+}