payload: bounded webui server inside the helper

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RxCGb8Asp1A1AQYiv1kbgS
This commit is contained in:
phantomptrandClaude Opus 5.5 committed 2026-10-17 03:59:04 -07:00
1 parent 180309354f
commit 2803f01d19
8 files changed
+1073 -3

No files matched your search

+7
View File
@@ -895,6 +895,13 @@ test-payload: payload
-o /tmp/ps5upload-webui-auth-selftest \
$(PAYLOAD_DIR)/tests/webui_auth_selftest.c $(PAYLOAD_DIR)/src/webui_auth.c
@/tmp/ps5upload-webui-auth-selftest
@echo "Running webui server self-test (host build, ~20 s)..."
@cc -O2 -Wall -Wextra -Werror -pthread -I$(PAYLOAD_DIR)/include \
-o /tmp/ps5upload-webui-server-selftest \
$(PAYLOAD_DIR)/tests/webui_server_selftest.c $(PAYLOAD_DIR)/src/webui_server.c \
$(PAYLOAD_DIR)/src/webui_http.c $(PAYLOAD_DIR)/src/webui_auth.c
@/tmp/ps5upload-webui-server-selftest
@echo "✓ silent, half-sent and flooding web clients never stall the helper"
@echo "Running cheat title list self-test (host build)..."
@cc -O2 -Wall -Wextra -Werror -I$(PAYLOAD_DIR)/include \
-o /tmp/ps5upload-cheats-list-selftest \
@@ -78,12 +78,13 @@ fn c_payload_binds_only_9120() {
binders.sort();
assert_eq!(
binders,
vec!["ava1/ava1_server.c"],
"only the AVA1 server (9120) opens a listener"
vec!["ava1/ava1_server.c", "src/webui_server.c"],
"only the AVA1 server (9120) and the console web UI server (WEBUI_PORT) open a listener"
);
// The AVA1 server binds AVA1_DEFAULT_PORT and the config carries no other helper port.
// The AVA1 server binds AVA1_DEFAULT_PORT; the only other helper port is the web UI's.
let cfg = std::fs::read_to_string(payload().join("include/config.h")).unwrap();
assert!(!cfg.contains("9113") && !cfg.contains("9114"));
assert!(cfg.contains("#define WEBUI_PORT 9180"));
let gen = std::fs::read_to_string(payload().join("ava1/gen/ava1_gen.h")).unwrap();
assert!(gen.contains("AVA1_DEFAULT_PORT 9120"));
}
+1
View File
@@ -166,6 +166,7 @@ SRCS := src/main.c src/runtime.c src/shell_builtin.c src/takeover.c src/takeover
src/wake_watchdog.c src/icon_keeper.c src/ffpfsc_unpack.c src/cheats.c src/cheats_list.c src/xml_encoding.c src/activity.c src/sdk_changer.c src/fakelib_overlay.c \
src/tmdb.c src/fw_spoof.c src/focus_probe.c \
src/content_db.c src/mgmt_rpc.c src/fs_jobs.c src/net_probe.c src/net_ports.c src/mgmt_fs.c src/path_policy.c \
src/webui_http.c src/webui_auth.c src/webui_server.c \
third_party/tiny_aes/aes.c \
$(BLAKE3_SRCS)
+3
View File
@@ -33,4 +33,7 @@
* startup reconciliation to detect orphaned mounts. */
#define PS5UPLOAD2_MOUNTS_DIR "/data/ps5upload/mounts"
/* Console web UI (webui_server.c): loopback-only unless LAN access is on. */
#define WEBUI_PORT 9180
#endif
+51
View File
@@ -0,0 +1,51 @@
/* Bounded HTTP server for the console web UI, running inside the helper.
*
* One accept thread and WEBUI_WORKERS workers. Accepted sockets go through a
* bounded queue (depth 32; a connection that does not fit is closed at once).
* Every connection carries a deadline of accept time + 5 s: reads and writes
* stop there, and a worker that dequeues an already-expired connection closes
* it without reading. A slow, silent or hostile client therefore holds one
* worker for at most 5 s and can never stall the accept thread. */
#ifndef PS5UPLOAD_WEBUI_SERVER_H
#define PS5UPLOAD_WEBUI_SERVER_H
#include <stddef.h>
#include <stdint.h>
#include "webui_http.h"
/* Writes a complete response to fd (use webui_send / webui_send_json).
* Runs on a worker after the Host/Origin/token checks passed. Unknown paths
* answer 404 and wrong methods 405. The return value is ignored. */
typedef int (*webui_route_fn)(const webui_req_t *r, const uint8_t *body, size_t body_len, int fd);
typedef struct {
uint16_t port;
int lan; /* 0: bind 127.0.0.1; 1: bind 0.0.0.0 */
webui_route_fn route;
} webui_cfg_t;
/* New token, bind, start the threads. Returns 0, or -1 when already running or
* the threads cannot start. A bind failure is NOT an error: it is logged and
* retried every 5 s by the accept thread (a helper takeover means the old
* instance may still hold the port for a moment). */
int webui_start(const webui_cfg_t *cfg);
/* Close the listener and the queued connections, wake the workers and join
* them within 2 s. A worker still inside route() after that is detached and
* finishes on its own; it keeps the server state it needs alive. */
void webui_stop(void);
/* Rebind the listener for a LAN setting change. Safe to call from a route
* handler (it never waits on the workers). The token is kept. Returns 0 when
* the new listener is bound, -1 when it is not (yet): the accept thread keeps
* retrying every 5 s. */
int webui_restart(int lan);
/* The token for this start ("" when the server is not running). */
const char *webui_token(void);
/* Write a complete response (head + body) within the connection deadline. */
void webui_send(int fd, int status, const char *ctype, const void *body, size_t len);
void webui_send_json(int fd, int status, const char *json);
#endif
+21
View File
@@ -31,6 +31,7 @@
#include "sony_api_lock.h"
#include "proc_identity.h"
#include "webui_server.h"
/* Sony "debugger" / system-process authid. Setting our process's
* ucred authid to this value grants the credentials Sony's kernel
* stubs check before allowing sensor reads / launches / installs
@@ -490,6 +491,13 @@ static void redirect_stdio_to_file(void) {
/* Starts the AVA1 side; only ever reached through takeover_gate_start (no other helper holds the port). */
static int g_ava1_start_rc = 0;
/* Console web UI router: 404 for everything until the API lands (Task 4 replaces it). */
static int webui_route_none(const webui_req_t *r, const uint8_t *body, size_t body_len, int fd) {
(void)r; (void)body; (void)body_len;
webui_send_json(fd, 404, "{\"error\":\"not_found\"}");
return 0;
}
static void start_ava1_cb(void *ctx) {
(void)ctx;
int ava1_rc = ava1_payload_start();
@@ -791,6 +799,17 @@ int main(void) {
}
startup_trace("TOAST_DONE");
/* Console web UI, after AVA1 is up. A bind failure is not fatal: the server's accept thread
* retries every 5 s (a takeover's old instance may still hold the port for a moment). */
{
webui_cfg_t wc;
wc.port = WEBUI_PORT;
wc.lan = 0;
wc.route = webui_route_none;
if (webui_start(&wc) != 0) fprintf(stderr, "webui: server did not start\n");
startup_trace("WEBUI_STARTED");
}
/* The AVA1 server runs on its own threads; this thread has nothing left to serve. It waits for
* node.shutdown or the takeover flag (runtime_request_shutdown), then tears down. */
runtime_wait_for_shutdown(&state);
@@ -810,6 +829,8 @@ int main(void) {
/* The AVA1 half: stop accepting, wait for the sessions (2 s) and for any in-flight Sony call
* (this does NOT return while one runs: only the exit watchdog armed above (8 s, or up to 60 s while a Sony call runs) can end the
* process then), then stop the data layer so journals are closed and durable jobs resume. */
webui_stop(); /* bounded: joins its workers within 2 s, detaches a stuck one */
{
int sr = ava1_payload_stop(2000, 3000);
if (sr) fprintf(stderr, "ava1: stop was slow (0x%x): sessions or a Sony call outlived the soft wait\n", sr);
+682
View File
@@ -0,0 +1,682 @@
/* Bounded HTTP server for the console web UI (see webui_server.h).
*
* Shape: one accept thread, WEBUI_WORKERS workers, a fixed queue between
* them. Nothing here can block without a bound:
* - the listener is non-blocking and polled in 200 ms slices, so the accept
* thread notices stop/rebind requests and never sticks in accept();
* - a full queue closes the new connection instead of waiting;
* - every connection has a deadline of accept time + 5 s; a worker reads in
* poll slices up to that deadline and drops an expired connection unread;
* - writes carry SO_SNDTIMEO and a total budget, so a client that stops
* reading frees the worker.
*
* Lifetime: all state lives in one heap object (srv_t) with a reference per
* thread plus one for the running server. webui_stop() joins what finishes
* within 2 s and detaches the rest; a detached worker still inside route()
* keeps the object alive until it returns. A connection's fd is cleared from
* the worker slot under the lock BEFORE it is closed, so stop() can never
* shutdown() a descriptor number that was reused by another subsystem. */
#include "webui_server.h"
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <netinet/in.h>
#include <poll.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#include "proc_identity.h"
#include "webui_auth.h"
#define WEBUI_WORKERS 4
#define WEBUI_QUEUE_DEPTH 32
#define WEBUI_CONN_MS 5000u /* read timeout / idle-connection drop */
#define WEBUI_BIND_RETRY_MS 5000u
#define WEBUI_STOP_MS 2000u
#define WEBUI_SLICE_MS 200
#define WEBUI_MIN_SEND_MS 1000u /* a route that computed long still gets this to write */
#define WEBUI_DRAIN_MS 250u
#define WEBUI_STACK (256u * 1024u)
#define WEBUI_MAX_ADDRS 8
#define WEBUI_THREAD_NAME "ps5upload-web"
#define WEBUI_BUF_CAP (WEBUI_HEADER_CAP + WEBUI_BODY_CAP)
typedef struct {
int fd;
uint64_t deadline_ms;
} conn_t;
struct srv;
typedef struct {
struct srv *s;
int idx;
} worker_arg_t;
typedef struct srv {
pthread_mutex_t mu;
pthread_cond_t cv;
int refs;
int stop; /* read lock-free with __atomic in loops */
conn_t q[WEBUI_QUEUE_DEPTH];
int qhead, qlen;
uint16_t port;
webui_route_fn route;
int lan;
int rebind_req; /* accept thread: close and rebind with ->lan */
int bound;
unsigned bind_gen; /* bumped after every bind attempt */
int listen_fd; /* owned by the accept thread once started */
char token[65];
char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN];
size_t n_addrs;
pthread_t acc;
pthread_t w[WEBUI_WORKERS];
int w_started[WEBUI_WORKERS];
int w_done[WEBUI_WORKERS];
int w_fd[WEBUI_WORKERS]; /* connection being served, -1 if none */
worker_arg_t wargs[WEBUI_WORKERS];
char *wbuf[WEBUI_WORKERS];
} srv_t;
/* g_life serializes start/stop/restart; g_mu guards g_srv and g_token. */
static pthread_mutex_t g_life = PTHREAD_MUTEX_INITIALIZER;
static pthread_mutex_t g_mu = PTHREAD_MUTEX_INITIALIZER;
static srv_t *g_srv;
static char g_token[65];
static uint64_t mono_ms(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000u + (uint64_t)ts.tv_nsec / 1000000u;
}
static void nap_ms(int ms) { (void)poll(NULL, 0, ms); }
static int stopping(srv_t *s) { return __atomic_load_n(&s->stop, __ATOMIC_ACQUIRE); }
static void srv_release(srv_t *s) {
int last, i;
pthread_mutex_lock(&s->mu);
last = (--s->refs == 0);
pthread_mutex_unlock(&s->mu);
if (!last) return;
for (i = 0; i < s->qlen; i++) close(s->q[(s->qhead + i) % WEBUI_QUEUE_DEPTH].fd);
if (s->listen_fd >= 0) close(s->listen_fd);
for (i = 0; i < WEBUI_WORKERS; i++) free(s->wbuf[i]);
pthread_cond_destroy(&s->cv);
pthread_mutex_destroy(&s->mu);
free(s);
}
static int thread_start(pthread_t *t, void *(*fn)(void *), void *arg) {
pthread_attr_t attr;
int rc;
if (pthread_attr_init(&attr) != 0) return -1;
if (pthread_attr_setstacksize(&attr, WEBUI_STACK) != 0) {
pthread_attr_destroy(&attr);
return -1;
}
rc = pthread_create(t, &attr, fn, arg);
pthread_attr_destroy(&attr);
return rc == 0 ? 0 : -1;
}
/* The console's IPv4 addresses, for the LAN Host check. */
static size_t own_addrs(char out[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN]) {
struct ifaddrs *ifa = NULL, *p;
size_t n = 0;
if (getifaddrs(&ifa) != 0) return 0;
for (p = ifa; p && n < WEBUI_MAX_ADDRS; p = p->ifa_next) {
if (!p->ifa_addr || p->ifa_addr->sa_family != AF_INET) continue;
if (!inet_ntop(AF_INET, &((struct sockaddr_in *)(void *)p->ifa_addr)->sin_addr,
out[n], INET_ADDRSTRLEN))
continue;
n++;
}
freeifaddrs(ifa);
return n;
}
/* Non-blocking listener on 127.0.0.1 or 0.0.0.0. -1 with errno on failure. */
static int bind_listener(uint16_t port, int lan) {
struct sockaddr_in a;
int one = 1, fd, fl, e;
fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return -1;
(void)setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
memset(&a, 0, sizeof a);
a.sin_family = AF_INET;
a.sin_port = htons(port);
a.sin_addr.s_addr = htonl(lan ? INADDR_ANY : INADDR_LOOPBACK);
fl = fcntl(fd, F_GETFL, 0);
if (fl < 0 || fcntl(fd, F_SETFL, fl | O_NONBLOCK) != 0 ||
bind(fd, (struct sockaddr *)&a, sizeof a) != 0 || listen(fd, 64) != 0) {
e = errno;
close(fd);
errno = e;
return -1;
}
return fd;
}
static void set_timeo(int fd, int opt, uint64_t ms) {
struct timeval tv;
if (ms == 0) ms = 1; /* 0 would mean "block forever" */
tv.tv_sec = (time_t)(ms / 1000u);
tv.tv_usec = (suseconds_t)((ms % 1000u) * 1000u);
(void)setsockopt(fd, SOL_SOCKET, opt, &tv, sizeof tv);
}
/* ---- responses ---------------------------------------------------------- */
static uint64_t send_budget_ms(int fd) {
struct timeval tv;
socklen_t l = sizeof tv;
uint64_t ms;
if (getsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &tv, &l) != 0) return WEBUI_CONN_MS;
ms = (uint64_t)tv.tv_sec * 1000u + (uint64_t)tv.tv_usec / 1000u;
if (ms == 0 || ms > WEBUI_CONN_MS) ms = WEBUI_CONN_MS;
return ms;
}
/* Write all of buf before `end` (monotonic ms). 0 on success. */
static int send_all(int fd, const void *buf, size_t len, uint64_t end) {
const char *p = (const char *)buf;
while (len > 0) {
struct pollfd pf;
uint64_t now = mono_ms();
ssize_t n;
int r;
if (now >= end) return -1;
pf.fd = fd;
pf.events = POLLOUT;
pf.revents = 0;
r = poll(&pf, 1, (int)(end - now));
if (r < 0 && errno == EINTR) continue;
if (r <= 0) return -1;
n = send(fd, p, len, 0);
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (n <= 0) return -1;
p += n;
len -= (size_t)n;
}
return 0;
}
void webui_send(int fd, int status, const char *ctype, const void *body, size_t len) {
char head[512];
uint64_t end = mono_ms() + send_budget_ms(fd);
int hl = webui_http_head(head, sizeof head, status, ctype, len, NULL);
if (hl < 0) return;
if (send_all(fd, head, (size_t)hl, end) != 0) return;
if (len > 0 && body) (void)send_all(fd, body, len, end);
}
void webui_send_json(int fd, int status, const char *json) {
webui_send(fd, status, "application/json", json, json ? strlen(json) : 0);
}
/* ---- one connection ----------------------------------------------------- */
enum { RD_OK = 0, RD_DROP = -1 };
/* Read one request within the deadline. Returns RD_OK, RD_DROP (silent close),
* or an HTTP status (400/413) to answer with. */
static int read_request(srv_t *s, int fd, uint64_t deadline, char *buf, webui_req_t *req,
size_t *got_out) {
size_t got = 0, need = 0;
int parsed = 0;
for (;;) {
struct pollfd pf;
uint64_t now;
ssize_t n;
int r, wait;
if (got > 0 && !parsed) {
r = webui_http_parse(buf, got, req);
if (r == WEBUI_PARSE_BAD) return 400;
if (r == WEBUI_PARSE_TOO_BIG) return 413;
if (r == WEBUI_PARSE_OK) {
if (req->content_length > WEBUI_BODY_CAP) return 413;
parsed = 1;
need = req->header_len + req->content_length;
}
}
if (parsed && got >= need) {
*got_out = got;
return RD_OK;
}
if (got >= WEBUI_BUF_CAP) return 413;
if (stopping(s)) return RD_DROP;
now = mono_ms();
if (now >= deadline) return RD_DROP;
wait = (int)(deadline - now);
if (wait > WEBUI_SLICE_MS) wait = WEBUI_SLICE_MS;
pf.fd = fd;
pf.events = POLLIN;
pf.revents = 0;
r = poll(&pf, 1, wait);
if (r < 0 && errno == EINTR) continue;
if (r < 0) return RD_DROP;
if (r == 0) continue;
n = recv(fd, buf + got, WEBUI_BUF_CAP - got, 0);
if (n < 0 && (errno == EINTR || errno == EAGAIN || errno == EWOULDBLOCK)) continue;
if (n <= 0) return RD_DROP;
got += (size_t)n;
}
}
/* Half-close, then read off whatever the client still sends for a moment, so
* the close does not turn into a reset that discards the response. */
static void finish_conn(int fd, char *scratch, size_t cap) {
uint64_t end = mono_ms() + WEBUI_DRAIN_MS;
size_t total = 0;
(void)shutdown(fd, SHUT_WR);
while (total < 2 * WEBUI_BUF_CAP) {
struct pollfd pf;
uint64_t now = mono_ms();
ssize_t n;
if (now >= end) break;
pf.fd = fd;
pf.events = POLLIN;
pf.revents = 0;
if (poll(&pf, 1, (int)(end - now)) <= 0) break;
n = recv(fd, scratch, cap, 0);
if (n <= 0) break;
total += (size_t)n;
}
}
static void serve(srv_t *s, int idx, conn_t c) {
char *buf = s->wbuf[idx];
webui_req_t req;
webui_auth_cfg_t ac;
char token[65];
char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN];
const char *addr_ptrs[WEBUI_MAX_ADDRS];
size_t got = 0, i, n_addrs;
uint64_t now;
int rd, is_api, lan;
set_timeo(c.fd, SO_RCVTIMEO, WEBUI_CONN_MS);
set_timeo(c.fd, SO_SNDTIMEO, WEBUI_CONN_MS);
memset(&req, 0, sizeof req);
rd = read_request(s, c.fd, c.deadline_ms, buf, &req, &got);
if (rd == RD_DROP) return;
/* Writes get what is left of the deadline, but at least a short window. */
now = mono_ms();
set_timeo(c.fd, SO_SNDTIMEO,
c.deadline_ms > now + WEBUI_MIN_SEND_MS ? c.deadline_ms - now : WEBUI_MIN_SEND_MS);
if (rd == 400) {
webui_send_json(c.fd, 400, "{\"error\":\"bad_request\"}");
return;
}
if (rd == 413) {
webui_send_json(c.fd, 413, "{\"error\":\"too_big\"}");
return;
}
pthread_mutex_lock(&s->mu);
memcpy(token, s->token, sizeof token);
lan = s->lan;
n_addrs = s->n_addrs;
memcpy(addrs, s->addrs, sizeof addrs);
pthread_mutex_unlock(&s->mu);
for (i = 0; i < n_addrs; i++) addr_ptrs[i] = addrs[i];
ac.token = token;
ac.port = s->port;
ac.lan = lan;
ac.own_addrs = addr_ptrs;
ac.n_own = n_addrs;
is_api = strncmp(req.path, "/api/", 5) == 0 && strcmp(req.path, "/api/token") != 0;
switch (webui_check(&req, &ac, is_api)) {
case WEBUI_ALLOW:
break;
case WEBUI_DENY_TOKEN:
webui_send_json(c.fd, 401, "{\"error\":\"token\"}");
return;
default:
webui_send_json(c.fd, 403, "{\"error\":\"forbidden\"}");
return;
}
if (s->route)
(void)s->route(&req, (const uint8_t *)buf + req.header_len, req.content_length, c.fd);
else
webui_send_json(c.fd, 404, "{\"error\":\"not_found\"}");
}
static void *worker_main(void *arg) {
worker_arg_t *wa = (worker_arg_t *)arg;
srv_t *s = wa->s;
int idx = wa->idx;
proc_name_set_self(WEBUI_THREAD_NAME);
for (;;) {
conn_t c;
pthread_mutex_lock(&s->mu);
while (!s->stop && s->qlen == 0) pthread_cond_wait(&s->cv, &s->mu);
if (s->stop) {
pthread_mutex_unlock(&s->mu);
break;
}
c = s->q[s->qhead];
s->qhead = (s->qhead + 1) % WEBUI_QUEUE_DEPTH;
s->qlen--;
s->w_fd[idx] = c.fd;
pthread_mutex_unlock(&s->mu);
if (mono_ms() < c.deadline_ms) {
serve(s, idx, c);
finish_conn(c.fd, s->wbuf[idx], WEBUI_BUF_CAP);
}
pthread_mutex_lock(&s->mu);
s->w_fd[idx] = -1;
pthread_mutex_unlock(&s->mu);
close(c.fd);
}
pthread_mutex_lock(&s->mu);
s->w_done[idx] = 1;
pthread_mutex_unlock(&s->mu);
srv_release(s);
return NULL;
}
/* ---- accept thread ------------------------------------------------------ */
static void *accept_main(void *arg) {
srv_t *s = (srv_t *)arg;
unsigned fails = 0;
proc_name_set_self(WEBUI_THREAD_NAME);
while (!stopping(s)) {
struct pollfd pf;
int fd, r, lan, fl;
pthread_mutex_lock(&s->mu);
if (s->rebind_req) {
s->rebind_req = 0;
if (s->listen_fd >= 0) close(s->listen_fd);
s->listen_fd = -1;
s->bound = 0;
fails = 0;
}
lan = s->lan;
fd = s->listen_fd;
pthread_mutex_unlock(&s->mu);
if (fd < 0) {
uint64_t until;
fd = bind_listener(s->port, lan);
int e = errno;
pthread_mutex_lock(&s->mu);
s->listen_fd = fd;
s->bound = fd >= 0;
s->bind_gen++;
pthread_cond_broadcast(&s->cv);
pthread_mutex_unlock(&s->mu);
if (fd >= 0) {
if (fails) fprintf(stderr, "webui: bound port %u after %u retries\n", (unsigned)s->port, fails);
fails = 0;
continue;
}
/* First failure and then once a minute: the log must not grow every 5 s forever. */
if (fails % 12 == 0)
fprintf(stderr, "webui: bind port %u (%s) failed: errno %d; retrying every 5 s\n",
(unsigned)s->port, lan ? "lan" : "loopback", e);
fails++;
until = mono_ms() + WEBUI_BIND_RETRY_MS;
while (!stopping(s) && mono_ms() < until) {
int req;
pthread_mutex_lock(&s->mu);
req = s->rebind_req;
pthread_mutex_unlock(&s->mu);
if (req) break;
nap_ms(WEBUI_SLICE_MS);
}
continue;
}
pf.fd = fd;
pf.events = POLLIN;
pf.revents = 0;
r = poll(&pf, 1, WEBUI_SLICE_MS);
if (r <= 0) {
if (r < 0 && errno != EINTR) nap_ms(50);
continue;
}
r = accept(fd, NULL, NULL);
if (r < 0) {
/* Never leave the loop on an accept error (a stray errno once killed a helper's
* server); back off briefly on the ones that would otherwise spin. */
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) nap_ms(50);
continue;
}
/* BSD hands the listener's O_NONBLOCK to the accepted socket; workers expect blocking
* sockets bounded by timeouts and poll. */
fl = fcntl(r, F_GETFL, 0);
if (fl >= 0) (void)fcntl(r, F_SETFL, fl & ~O_NONBLOCK);
#ifdef SO_NOSIGPIPE
{
int one = 1;
(void)setsockopt(r, SOL_SOCKET, SO_NOSIGPIPE, &one, sizeof one);
}
#endif
pthread_mutex_lock(&s->mu);
if (!s->stop && s->qlen < WEBUI_QUEUE_DEPTH) {
int tail = (s->qhead + s->qlen) % WEBUI_QUEUE_DEPTH;
s->q[tail].fd = r;
s->q[tail].deadline_ms = mono_ms() + WEBUI_CONN_MS;
s->qlen++;
pthread_cond_signal(&s->cv);
r = -1;
}
pthread_mutex_unlock(&s->mu);
if (r >= 0) close(r); /* queue full: drop now, never wait */
}
pthread_mutex_lock(&s->mu);
if (s->listen_fd >= 0) close(s->listen_fd);
s->listen_fd = -1;
s->bound = 0;
pthread_mutex_unlock(&s->mu);
srv_release(s);
return NULL;
}
/* ---- lifecycle ---------------------------------------------------------- */
static void stop_locked(srv_t *s) {
uint64_t end;
int i, all;
pthread_mutex_lock(&s->mu);
__atomic_store_n(&s->stop, 1, __ATOMIC_RELEASE);
pthread_cond_broadcast(&s->cv);
/* Wake a worker blocked writing to a client that stopped reading. */
for (i = 0; i < WEBUI_WORKERS; i++)
if (s->w_fd[i] >= 0) (void)shutdown(s->w_fd[i], SHUT_RDWR);
/* Queued connections were never served: close them now. */
while (s->qlen > 0) {
close(s->q[s->qhead].fd);
s->qhead = (s->qhead + 1) % WEBUI_QUEUE_DEPTH;
s->qlen--;
}
pthread_mutex_unlock(&s->mu);
/* The accept thread polls in 200 ms slices; joining it frees the port. */
pthread_join(s->acc, NULL);
end = mono_ms() + WEBUI_STOP_MS;
for (;;) {
all = 1;
pthread_mutex_lock(&s->mu);
for (i = 0; i < WEBUI_WORKERS; i++)
if (s->w_started[i] && !s->w_done[i]) all = 0;
pthread_mutex_unlock(&s->mu);
if (all || mono_ms() >= end) break;
nap_ms(20);
}
for (i = 0; i < WEBUI_WORKERS; i++) {
int done;
if (!s->w_started[i]) continue;
pthread_mutex_lock(&s->mu);
done = s->w_done[i];
pthread_mutex_unlock(&s->mu);
if (done) {
pthread_join(s->w[i], NULL);
} else {
fprintf(stderr, "webui: worker %d still inside a request after 2 s; detached\n", i);
pthread_detach(s->w[i]);
}
}
srv_release(s); /* the running server's own reference */
}
int webui_start(const webui_cfg_t *cfg) {
srv_t *s;
int i, e;
if (!cfg) return -1;
pthread_mutex_lock(&g_life);
if (g_srv) {
pthread_mutex_unlock(&g_life);
return -1;
}
s = (srv_t *)calloc(1, sizeof *s);
if (!s) goto fail_nomem;
for (i = 0; i < WEBUI_WORKERS; i++) {
s->wbuf[i] = (char *)malloc(WEBUI_BUF_CAP);
s->w_fd[i] = -1;
if (!s->wbuf[i]) {
while (i >= 0) free(s->wbuf[i--]);
free(s);
goto fail_nomem;
}
}
pthread_mutex_init(&s->mu, NULL);
pthread_cond_init(&s->cv, NULL);
s->refs = 1;
s->port = cfg->port;
s->route = cfg->route;
s->lan = cfg->lan ? 1 : 0;
webui_token_new(s->token);
if (s->lan) s->n_addrs = own_addrs(s->addrs);
/* First bind here so the server answers as soon as start returns; on failure the accept
* thread retries (a takeover's old instance may still hold the port for a moment). */
s->listen_fd = bind_listener(s->port, s->lan);
e = errno;
s->bound = s->listen_fd >= 0;
if (s->listen_fd < 0)
fprintf(stderr, "webui: bind port %u failed: errno %d; retrying every 5 s\n", (unsigned)s->port, e);
s->refs++;
if (thread_start(&s->acc, accept_main, s) != 0) {
s->refs--;
srv_release(s);
fprintf(stderr, "webui: accept thread did not start\n");
pthread_mutex_unlock(&g_life);
return -1;
}
for (i = 0; i < WEBUI_WORKERS; i++) {
s->wargs[i].s = s;
s->wargs[i].idx = i;
pthread_mutex_lock(&s->mu);
s->refs++;
pthread_mutex_unlock(&s->mu);
if (thread_start(&s->w[i], worker_main, &s->wargs[i]) != 0) {
pthread_mutex_lock(&s->mu);
s->refs--;
pthread_mutex_unlock(&s->mu);
fprintf(stderr, "webui: worker %d did not start\n", i);
continue; /* fewer workers still serve; zero is handled below */
}
s->w_started[i] = 1;
}
for (i = 0; i < WEBUI_WORKERS && !s->w_started[i]; i++) {
}
if (i == WEBUI_WORKERS) {
stop_locked(s);
pthread_mutex_unlock(&g_life);
return -1;
}
pthread_mutex_lock(&g_mu);
g_srv = s;
memcpy(g_token, s->token, sizeof g_token);
pthread_mutex_unlock(&g_mu);
if (s->bound)
fprintf(stderr, "webui: serving on %s:%u\n", s->lan ? "0.0.0.0" : "127.0.0.1", (unsigned)s->port);
pthread_mutex_unlock(&g_life);
return 0;
fail_nomem:
fprintf(stderr, "webui: out of memory\n");
pthread_mutex_unlock(&g_life);
return -1;
}
void webui_stop(void) {
srv_t *s;
pthread_mutex_lock(&g_life);
pthread_mutex_lock(&g_mu);
s = g_srv;
g_srv = NULL;
memset(g_token, 0, sizeof g_token);
pthread_mutex_unlock(&g_mu);
if (s) stop_locked(s);
pthread_mutex_unlock(&g_life);
}
int webui_restart(int lan) {
srv_t *s;
char addrs[WEBUI_MAX_ADDRS][INET_ADDRSTRLEN];
size_t n = 0;
unsigned gen;
uint64_t end;
int bound = 0;
if (lan) n = own_addrs(addrs);
pthread_mutex_lock(&g_life);
s = g_srv;
if (!s) {
pthread_mutex_unlock(&g_life);
return -1;
}
pthread_mutex_lock(&s->mu);
s->lan = lan ? 1 : 0;
s->n_addrs = n;
if (n) memcpy(s->addrs, addrs, sizeof addrs);
s->rebind_req = 1;
gen = s->bind_gen;
s->refs++;
pthread_mutex_unlock(&s->mu);
pthread_mutex_unlock(&g_life);
/* Wait briefly for the accept thread's attempt; it keeps retrying on its own after. */
end = mono_ms() + 1500u;
for (;;) {
int done;
pthread_mutex_lock(&s->mu);
done = s->bind_gen != gen && !s->rebind_req;
bound = s->bound;
pthread_mutex_unlock(&s->mu);
if (done || stopping(s) || mono_ms() >= end) break;
nap_ms(20);
}
srv_release(s);
return bound ? 0 : -1;
}
const char *webui_token(void) { return g_token; }
+304
View File
@@ -0,0 +1,304 @@
/* Host-side tests for the console web UI server: real sockets on an
* ephemeral loopback port and a stub router answering {"ok":true}.
* Covers the properties that keep the helper alive: silent, half-sent and
* flooding clients never stall other requests, and a stop/start rebinds the
* port with a new token. */
#include <arpa/inet.h>
#include <errno.h>
#include <netinet/in.h>
#include <poll.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/socket.h>
#include <time.h>
#include <unistd.h>
#include "webui_server.h"
static int failures = 0;
#define CHECK(expr) \
do { \
if (!(expr)) { \
fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #expr); \
failures++; \
} \
} while (0)
static uint16_t g_port;
static int stub_route(const webui_req_t *r, const uint8_t *body, size_t body_len, int fd) {
(void)r; (void)body; (void)body_len;
webui_send_json(fd, 200, "{\"ok\":true}");
return 0;
}
static double now_s(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static void sleep_ms(int ms) {
struct timespec ts = { ms / 1000, (long)(ms % 1000) * 1000000L };
nanosleep(&ts, NULL);
}
/* An ephemeral port nobody holds right now. */
static uint16_t free_port(void) {
struct sockaddr_in a;
socklen_t al = sizeof a;
int s = socket(AF_INET, SOCK_STREAM, 0);
memset(&a, 0, sizeof a);
a.sin_family = AF_INET;
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (s < 0 || bind(s, (struct sockaddr *)&a, sizeof a) != 0 ||
getsockname(s, (struct sockaddr *)&a, &al) != 0) {
if (s >= 0) close(s);
return 0;
}
close(s);
return ntohs(a.sin_port);
}
static int dial(void) {
struct sockaddr_in a;
int s = socket(AF_INET, SOCK_STREAM, 0);
if (s < 0) return -1;
memset(&a, 0, sizeof a);
a.sin_family = AF_INET;
a.sin_port = htons(g_port);
a.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
if (connect(s, (struct sockaddr *)&a, sizeof a) != 0) {
close(s);
return -1;
}
return s;
}
/* Read until EOF or timeout. Returns bytes read, -1 on timeout. */
static int read_all(int s, char *out, size_t cap, int timeout_ms) {
size_t got = 0;
double end = now_s() + timeout_ms / 1000.0;
for (;;) {
struct pollfd p = { s, POLLIN, 0 };
int left = (int)((end - now_s()) * 1000.0);
ssize_t n;
if (left <= 0) return -1;
if (poll(&p, 1, left) <= 0) continue;
n = recv(s, out + got, cap - 1 - got, 0);
if (n <= 0) break;
got += (size_t)n;
if (got >= cap - 1) break;
}
out[got] = '\0';
return (int)got;
}
/* Send one request on a fresh connection; return the HTTP status or -1. */
static int request(const char *path, const char *token, char *resp, size_t cap) {
char req[512];
int s = dial(), n, status = -1;
if (s < 0) return -1;
n = snprintf(req, sizeof req,
"GET %s HTTP/1.1\r\nHost: 127.0.0.1:%u\r\n%s%s%s\r\n",
path, (unsigned)g_port, token ? "X-PS5U-Token: " : "",
token ? token : "", token ? "\r\n" : "");
if (send(s, req, (size_t)n, 0) == n && read_all(s, resp, cap, 7000) > 0)
(void)sscanf(resp, "HTTP/1.1 %d", &status);
close(s);
return status;
}
static int start_server(void) {
webui_cfg_t c;
c.port = g_port;
c.lan = 0;
c.route = stub_route;
return webui_start(&c);
}
static void serves_api_with_token(void) {
char resp[2048];
CHECK(request("/api/x", webui_token(), resp, sizeof resp) == 200);
CHECK(strstr(resp, "{\"ok\":true}") != NULL);
CHECK(strstr(resp, "Connection: close") != NULL);
}
static void api_without_token_401(void) {
char resp[2048];
CHECK(request("/api/x", NULL, resp, sizeof resp) == 401);
CHECK(strstr(resp, "{\"error\":\"token\"}") != NULL);
/* The page itself and the token endpoint need no token. */
CHECK(request("/", NULL, resp, sizeof resp) == 200);
CHECK(request("/api/token", NULL, resp, sizeof resp) == 200);
}
static void bad_host_403(void) {
char req[256], resp[2048];
int s = dial(), n, status = -1;
CHECK(s >= 0);
if (s < 0) return;
n = snprintf(req, sizeof req, "GET /api/x HTTP/1.1\r\nHost: evil.example:%u\r\nX-PS5U-Token: %s\r\n\r\n",
(unsigned)g_port, webui_token());
CHECK(send(s, req, (size_t)n, 0) == n);
if (read_all(s, resp, sizeof resp, 7000) > 0) (void)sscanf(resp, "HTTP/1.1 %d", &status);
close(s);
CHECK(status == 403);
CHECK(strstr(resp, "{\"error\":\"forbidden\"}") != NULL);
}
static void garbage_400(void) {
char resp[2048];
int s = dial(), status = -1;
CHECK(s >= 0);
if (s < 0) return;
CHECK(send(s, "NOT HTTP AT ALL\r\n\r\n", 19, 0) == 19);
if (read_all(s, resp, sizeof resp, 7000) > 0) (void)sscanf(resp, "HTTP/1.1 %d", &status);
close(s);
CHECK(status == 400);
}
static void too_big_413(void) {
char req[256], resp[2048];
int s = dial(), n, status = -1;
CHECK(s >= 0);
if (s < 0) return;
n = snprintf(req, sizeof req,
"POST /api/x HTTP/1.1\r\nHost: 127.0.0.1:%u\r\nContent-Length: 999999\r\n\r\n",
(unsigned)g_port);
CHECK(send(s, req, (size_t)n, 0) == n);
if (read_all(s, resp, sizeof resp, 7000) > 0) (void)sscanf(resp, "HTTP/1.1 %d", &status);
close(s);
CHECK(status == 413);
}
/* 4 idle connections occupy every worker; a 5th request is still served
* once their 5 s deadline drops them. */
static void silent_client_dropped_and_next_served(void) {
int idle[4], i, st;
char resp[2048];
double t0 = now_s();
for (i = 0; i < 4; i++) idle[i] = dial();
for (i = 0; i < 4; i++) CHECK(idle[i] >= 0);
/* Let the workers take the idle ones first, so the request below is
* queued behind them with its own (later) deadline. */
sleep_ms(1000);
st = request("/api/x", webui_token(), resp, sizeof resp);
CHECK(st == 200);
CHECK(now_s() - t0 < 7.5);
for (i = 0; i < 4; i++)
if (idle[i] >= 0) close(idle[i]);
}
/* A request that never finishes its headers is dropped within the timeout. */
static void half_request_dropped_within_timeout(void) {
char buf[256];
int s = dial(), n;
double t0 = now_s();
CHECK(s >= 0);
if (s < 0) return;
CHECK(send(s, "GET /api/x HTTP/1.1\r\nHost: 127.0.0.1", 36, 0) == 36);
n = read_all(s, buf, sizeof buf, 7000);
CHECK(n >= 0); /* closed (EOF), not still open at 7 s */
CHECK(now_s() - t0 < 7.0);
close(s);
}
/* 40 idle connections: more than queue depth + workers. The overflow is
* closed at once and a fresh request retried once per second is answered
* within 7 s; the accept thread never blocks on the full queue. */
static void queue_overflow_closes_not_blocks(void) {
int idle[40], i, ok = 0;
char resp[2048];
double t0 = now_s();
for (i = 0; i < 40; i++) idle[i] = dial();
for (i = 0; i < 40; i++) CHECK(idle[i] >= 0);
while (now_s() - t0 < 7.0) {
double t1 = now_s();
int s = dial();
if (s >= 0) {
char req[512];
int n = snprintf(req, sizeof req,
"GET /api/x HTTP/1.1\r\nHost: 127.0.0.1:%u\r\nX-PS5U-Token: %s\r\n\r\n",
(unsigned)g_port, webui_token());
int st = -1;
if (send(s, req, (size_t)n, 0) == n && read_all(s, resp, sizeof resp, 1000) > 0)
(void)sscanf(resp, "HTTP/1.1 %d", &st);
close(s);
if (st == 200) { ok = 1; break; }
}
if (now_s() - t1 < 1.0) sleep_ms((int)((1.0 - (now_s() - t1)) * 1000.0));
}
CHECK(ok);
CHECK(now_s() - t0 < 7.5);
for (i = 0; i < 40; i++)
if (idle[i] >= 0) close(idle[i]);
}
static void stop_then_start_rebinds_with_new_token(void) {
char t1[65], resp[2048];
double t0;
snprintf(t1, sizeof t1, "%s", webui_token());
CHECK(strlen(t1) == 64);
t0 = now_s();
webui_stop();
CHECK(now_s() - t0 < 2.5);
CHECK(webui_token()[0] == '\0');
CHECK(request("/api/x", t1, resp, sizeof resp) == -1); /* nothing listening */
CHECK(start_server() == 0);
CHECK(strlen(webui_token()) == 64);
CHECK(strcmp(t1, webui_token()) != 0);
CHECK(request("/api/x", webui_token(), resp, sizeof resp) == 200);
CHECK(request("/api/x", t1, resp, sizeof resp) == 401);
}
/* Stop while a worker is blocked on a silent client: still within 2 s. */
static void stop_while_workers_busy(void) {
int idle[6], i;
double t0;
for (i = 0; i < 6; i++) idle[i] = dial();
sleep_ms(200);
t0 = now_s();
webui_stop();
CHECK(now_s() - t0 < 2.5);
for (i = 0; i < 6; i++)
if (idle[i] >= 0) close(idle[i]);
CHECK(webui_start(NULL) == -1);
CHECK(start_server() == 0);
CHECK(start_server() == -1); /* already running */
}
static void restart_keeps_token_and_serves(void) {
char t1[65], resp[2048];
snprintf(t1, sizeof t1, "%s", webui_token());
CHECK(webui_restart(0) == 0);
CHECK(strcmp(t1, webui_token()) == 0);
CHECK(request("/api/x", webui_token(), resp, sizeof resp) == 200);
}
int main(void) {
signal(SIGPIPE, SIG_IGN);
g_port = free_port();
CHECK(g_port != 0);
CHECK(start_server() == 0);
serves_api_with_token();
api_without_token_401();
bad_host_403();
garbage_400();
too_big_413();
silent_client_dropped_and_next_served();
half_request_dropped_within_timeout();
queue_overflow_closes_not_blocks();
stop_then_start_rebinds_with_new_token();
stop_while_workers_busy();
restart_keeps_token_and_serves();
webui_stop();
if (failures) {
fprintf(stderr, "webui_server_selftest: %d failure(s)\n", failures);
return 1;
}
printf("webui_server_selftest: ok\n");
return 0;
}