mirror of
https://github.com/phantomptr/ps5upload.git
synced 2026-10-19 02:20:02 -05:00
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:
1 parent
180309354f
commit
2803f01d19
8 files changed
+1073
-3
No files matched your search
@@ -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"));
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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; }
|
||||
@@ -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;
|
||||
}
|
||||
Reference in new issue
Block a user