Naut/plugins/webui/webui.c
ookami125 41ed172272 webui: real speeds, honest actions, and hardening
Address the review of the webui plugin:

- Live download rates. A background sampler polls the daemon once per
  second, derives per-torrent dlspeed from successive byte counts (EWMA
  smoothed), and computes a real ETA. dl_info_speed now aggregates the
  fleet instead of reporting a hardcoded 0.

- Single shared snapshot. The sampler publishes one cached snapshot that
  /api/snapshot, /api/torrents and every SSE stream serve, so N browser
  tabs no longer each poll the engine and race the speed table. SSE
  waiters block on a condition and wake promptly on shutdown.

- Honest /api/action. The engine has no pause/resume/recheck/queue verbs,
  so the endpoint returns 501 with an explanatory message instead of
  claiming success.

- Reject oversized uploads with 413 instead of silently truncating a
  torrent into garbage.

- Auth hardening: constant-time credential comparison, CSPRNG-only token
  generation via getrandom (fail closed, no weak fallback), oldest-session
  eviction instead of clobbering slot 0, and a warning when bound to a
  non-loopback address.

- Cap concurrent connections (503 beyond the limit) so a client can't
  spawn unbounded threads.

- nautd: tear down plugins (joining the webui's threads) before freeing
  torrent tasks, closing a shutdown-time use-after-free window where an
  in-flight request could touch freed state.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 00:29:52 -04:00

1418 lines
51 KiB
C

#include "naut/naut_plugin.h"
#include <jansson.h>
#include <arpa/inet.h>
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <netinet/in.h>
#include <pthread.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdatomic.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include <sys/random.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <time.h>
#include <unistd.h>
#define DEFAULT_HOST "127.0.0.1"
#define DEFAULT_PORT 8080
#define READ_LIMIT (8u << 20)
#define SESSION_COOKIE "naut_session"
#define SESSION_TTL_SECONDS (60 * 60 * 24 * 7)
#define MAX_SESSIONS 64
#define MAX_CONNECTIONS 128
#define SPEED_SLOTS 256
#define ETA_INFINITY 8640000 /* torrent-ui renders >= this as the infinity glyph */
typedef struct {
char token[96];
time_t expires;
bool used;
} webui_session;
/* Single-writer (sampler thread) running estimate of a torrent's download
* rate, derived from successive byte counts. */
typedef struct {
uint64_t id;
uint64_t last_bytes;
double last_time;
double dlspeed;
bool used;
} speed_slot;
typedef struct {
naut_host_api host;
char root[PATH_MAX];
char host_name[64];
char auth_user[64];
char auth_password[64];
int port;
int listener;
bool generated_password;
atomic_bool stopping;
bool thread_started;
pthread_t thread;
bool sampler_started;
pthread_t sampler;
pthread_mutex_t auth_lock;
pthread_mutex_t conn_lock;
pthread_cond_t conn_cond;
size_t active_connections;
/* Latest snapshot, published once per second by the sampler thread and
* shared by /api/snapshot, /api/torrents and every SSE stream. */
pthread_mutex_t snap_lock;
pthread_cond_t snap_cond;
char *snapshot_str;
char *torrents_str;
uint64_t snap_seq;
pthread_mutex_t speed_lock;
speed_slot speeds[SPEED_SLOTS];
webui_session sessions[MAX_SESSIONS];
} webui_state;
typedef struct {
int fd;
} conn_arg;
static webui_state g_webui;
static double monotonic_seconds(void) {
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
}
static void log_msg(int level, const char *message) {
if (g_webui.host.log)
g_webui.host.log(g_webui.host.host_context, level, message);
}
static bool send_all_fd(int fd, const char *buf, size_t len) {
while (len) {
ssize_t n = send(fd, buf, len, MSG_NOSIGNAL);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
if (n == 0) return false;
buf += n;
len -= (size_t)n;
}
return true;
}
static void http_head_extra(int fd, int code, const char *status,
const char *ctype, size_t len,
const char *extra) {
char h[512];
int n = snprintf(h, sizeof h,
"HTTP/1.1 %d %s\r\nContent-Type: %s\r\nContent-Length: %zu\r\n"
"Cache-Control: no-cache\r\n%sConnection: close\r\n\r\n",
code, status, ctype, len, extra ? extra : "");
if (n > 0) send_all_fd(fd, h, (size_t)n);
}
static void http_head(int fd, int code, const char *status,
const char *ctype, size_t len) {
http_head_extra(fd, code, status, ctype, len, NULL);
}
static void http_text(int fd, int code, const char *status, const char *body) {
if (!body) body = "";
http_head(fd, code, status, "text/plain; charset=utf-8", strlen(body));
send_all_fd(fd, body, strlen(body));
}
static void http_json(int fd, int code, json_t *json) {
char *txt = json ? json_dumps(json, JSON_COMPACT | JSON_ENCODE_ANY) : NULL;
if (!txt) {
http_text(fd, 500, "Internal Server Error", "json encode failed");
return;
}
http_head(fd, code, code == 200 ? "OK" : "Error",
"application/json; charset=utf-8", strlen(txt));
send_all_fd(fd, txt, strlen(txt));
free(txt);
}
static void http_json_extra(int fd, int code, json_t *json, const char *extra) {
char *txt = json ? json_dumps(json, JSON_COMPACT | JSON_ENCODE_ANY) : NULL;
if (!txt) {
http_text(fd, 500, "Internal Server Error", "json encode failed");
return;
}
http_head_extra(fd, code, code == 200 ? "OK" : "Error",
"application/json; charset=utf-8", strlen(txt), extra);
send_all_fd(fd, txt, strlen(txt));
free(txt);
}
/* Serve an already-serialized JSON string under one lock copy. */
static void http_json_str(int fd, const char *json, const char *fallback) {
const char *body = json ? json : fallback;
http_head(fd, 200, "OK", "application/json; charset=utf-8", strlen(body));
send_all_fd(fd, body, strlen(body));
}
static const char *mime_type(const char *path) {
const char *dot = strrchr(path, '.');
if (!dot) return "application/octet-stream";
if (strcmp(dot, ".html") == 0) return "text/html; charset=utf-8";
if (strcmp(dot, ".js") == 0) return "text/javascript; charset=utf-8";
if (strcmp(dot, ".css") == 0) return "text/css; charset=utf-8";
if (strcmp(dot, ".json") == 0) return "application/json; charset=utf-8";
if (strcmp(dot, ".svg") == 0) return "image/svg+xml";
if (strcmp(dot, ".ico") == 0) return "image/x-icon";
return "application/octet-stream";
}
static void strip_query(char *path) {
char *q = strchr(path, '?');
if (q) *q = 0;
char *hash = strchr(path, '#');
if (hash) *hash = 0;
}
/* Constant-time equality so credential checks don't leak length/content via
* timing. Returns true when both NUL-terminated strings match exactly. */
static bool constant_time_equal(const char *a, const char *b) {
if (!a || !b) return false;
size_t la = strlen(a), lb = strlen(b);
size_t n = la > lb ? la : lb;
unsigned diff = (unsigned)(la ^ lb);
for (size_t i = 0; i < n; i++) {
unsigned char ca = i < la ? (unsigned char)a[i] : 0;
unsigned char cb = i < lb ? (unsigned char)b[i] : 0;
diff |= (unsigned)(ca ^ cb);
}
return diff == 0;
}
/* Cryptographically strong hex. Fails closed: if the kernel CSPRNG is
* unavailable we refuse rather than fall back to predictable bytes (these
* feed session tokens). */
static bool random_hex(char *out, size_t out_size, size_t bytes) {
static const char hex[] = "0123456789abcdef";
if (out_size < bytes * 2 + 1) return false;
unsigned char buf[64];
if (bytes > sizeof buf) return false;
size_t got = 0;
while (got < bytes) {
ssize_t n = getrandom(buf + got, bytes - got, 0);
if (n < 0) {
if (errno == EINTR) continue;
return false;
}
got += (size_t)n;
}
for (size_t i = 0; i < bytes; i++) {
out[i * 2] = hex[buf[i] >> 4];
out[i * 2 + 1] = hex[buf[i] & 15];
}
out[bytes * 2] = 0;
return true;
}
static void init_auth(void) {
const char *user = getenv("NAUT_AUTH_USER");
if (!user || !*user) user = getenv("NAUT_USER");
if (!user || !*user) user = "admin";
snprintf(g_webui.auth_user, sizeof g_webui.auth_user, "%s", user);
const char *password = getenv("NAUT_AUTH_PASSWORD");
if (!password || !*password) password = getenv("NAUT_PASSWORD");
if (password && *password) {
snprintf(g_webui.auth_password, sizeof g_webui.auth_password, "%s",
password);
g_webui.generated_password = false;
return;
}
if (!random_hex(g_webui.auth_password, sizeof g_webui.auth_password, 9)) {
/* No CSPRNG: leave the password empty so login is impossible rather
* than guessable. The operator must set NAUT_AUTH_PASSWORD. */
g_webui.auth_password[0] = 0;
}
g_webui.generated_password = true;
}
static const char *header_value(const char *headers, const char *end,
const char *name) {
size_t name_len = strlen(name);
for (const char *p = headers; p && p < end;) {
const char *line_end = memmem(p, (size_t)(end - p), "\r\n", 2);
if (!line_end) line_end = end;
if ((size_t)(line_end - p) > name_len &&
strncasecmp(p, name, name_len) == 0 && p[name_len] == ':') {
const char *value = p + name_len + 1;
while (value < line_end && (*value == ' ' || *value == '\t'))
value++;
return value;
}
p = line_end + 2;
}
return NULL;
}
static bool cookie_token(const char *headers, const char *end,
char *out, size_t out_size) {
const char *cookie = header_value(headers, end, "cookie");
if (!cookie) return false;
const char *line_end = memmem(cookie, (size_t)(end - cookie), "\r\n", 2);
if (!line_end) line_end = end;
const char *p = cookie;
size_t key_len = strlen(SESSION_COOKIE);
while (p < line_end) {
while (p < line_end && (*p == ' ' || *p == ';')) p++;
if ((size_t)(line_end - p) > key_len &&
strncmp(p, SESSION_COOKIE, key_len) == 0 &&
p[key_len] == '=') {
p += key_len + 1;
size_t len = strcspn(p, "; \r\n");
if (len >= out_size) len = out_size - 1;
memcpy(out, p, len);
out[len] = 0;
return true;
}
p = memchr(p, ';', (size_t)(line_end - p));
if (!p) break;
}
return false;
}
static bool current_user(const char *headers, const char *end) {
char token[96];
if (!cookie_token(headers, end, token, sizeof token)) return false;
bool ok = false;
time_t now = time(NULL);
pthread_mutex_lock(&g_webui.auth_lock);
for (size_t i = 0; i < MAX_SESSIONS; i++) {
webui_session *session = &g_webui.sessions[i];
if (!session->used || strcmp(session->token, token) != 0) continue;
if (session->expires < now) {
session->used = false;
break;
}
session->expires = now + SESSION_TTL_SECONDS;
ok = true;
break;
}
pthread_mutex_unlock(&g_webui.auth_lock);
return ok;
}
static bool create_session(char *out, size_t out_size) {
char token[96];
if (!random_hex(token, sizeof token, 24)) return false;
time_t now = time(NULL);
time_t expires = now + SESSION_TTL_SECONDS;
pthread_mutex_lock(&g_webui.auth_lock);
webui_session *slot = NULL;
for (size_t i = 0; i < MAX_SESSIONS; i++) {
webui_session *s = &g_webui.sessions[i];
if (!s->used || s->expires < now) { slot = s; break; }
/* Otherwise track the session that expires soonest, so a full table
* evicts the oldest rather than always clobbering slot 0. */
if (!slot || s->expires < slot->expires) slot = s;
}
snprintf(slot->token, sizeof slot->token, "%s", token);
slot->expires = expires;
slot->used = true;
pthread_mutex_unlock(&g_webui.auth_lock);
snprintf(out, out_size, "%s", token);
return true;
}
static void clear_session(const char *headers, const char *end) {
char token[96];
if (!cookie_token(headers, end, token, sizeof token)) return;
pthread_mutex_lock(&g_webui.auth_lock);
for (size_t i = 0; i < MAX_SESSIONS; i++)
if (g_webui.sessions[i].used &&
strcmp(g_webui.sessions[i].token, token) == 0)
g_webui.sessions[i].used = false;
pthread_mutex_unlock(&g_webui.auth_lock);
}
static bool bad_static_path(const char *path) {
return strstr(path, "..") || strchr(path, '\\');
}
static bool join_root_path(char *out, size_t out_size, const char *suffix) {
int n = snprintf(out, out_size, "%s%s", g_webui.root, suffix);
return n > 0 && (size_t)n < out_size;
}
static bool serve_file(int fd, const char *request_path) {
char clean[PATH_MAX];
snprintf(clean, sizeof clean, "%s", request_path && *request_path
? request_path : "/");
strip_query(clean);
if (strcmp(clean, "/") == 0) snprintf(clean, sizeof clean, "/index.html");
if (bad_static_path(clean)) {
http_text(fd, 403, "Forbidden", "forbidden");
return true;
}
char path[PATH_MAX];
if (!join_root_path(path, sizeof path, clean)) {
http_text(fd, 414, "URI Too Long", "path too long");
return true;
}
int file = open(path, O_RDONLY);
if (file < 0 && strchr(clean + 1, '/') == NULL) {
if (!join_root_path(path, sizeof path, "/index.html")) {
http_text(fd, 500, "Internal Server Error", "root too long");
return true;
}
file = open(path, O_RDONLY);
}
if (file < 0) return false;
struct stat st;
if (fstat(file, &st) != 0 || st.st_size < 0) {
close(file);
http_text(fd, 500, "Internal Server Error", "stat failed");
return true;
}
http_head(fd, 200, "OK", mime_type(path), (size_t)st.st_size);
char buf[16384];
for (;;) {
ssize_t n = read(file, buf, sizeof buf);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
if (n == 0) break;
if (!send_all_fd(fd, buf, (size_t)n)) break;
}
close(file);
return true;
}
static json_t *rpc_call_json(const char *method, json_t *params) {
if (!g_webui.host.call_rpc) return NULL;
char *request = json_dumps(params ? params : json_null(),
JSON_COMPACT | JSON_ENCODE_ANY);
if (!request) return NULL;
char *response = NULL;
naut_err error = g_webui.host.call_rpc(g_webui.host.host_context,
method, request, &response);
free(request);
if (error != NAUT_OK || !response) {
free(response);
return NULL;
}
json_error_t json_error;
json_t *json = json_loads(response, JSON_REJECT_DUPLICATES |
JSON_DECODE_ANY, &json_error);
free(response);
return json;
}
static const char *json_string_or(const json_t *obj, const char *key,
const char *fallback) {
const char *value = json_string_value(json_object_get(obj, key));
return value ? value : fallback;
}
static uint64_t json_u64(const json_t *obj, const char *key) {
json_t *value = json_object_get(obj, key);
return json_is_integer(value) && json_integer_value(value) > 0
? (uint64_t)json_integer_value(value) : 0;
}
static const char *base_name(const char *path) {
if (!path || !*path) return "torrent";
const char *slash = strrchr(path, '/');
const char *name = slash ? slash + 1 : path;
return *name ? name : "torrent";
}
static char *torrent_name(const json_t *torrent) {
const char *source = json_string_or(torrent, "source", "torrent");
if (strncmp(source, "magnet:", 7) == 0) {
const char *dn = strstr(source, "dn=");
if (dn) {
dn += 3;
size_t len = strcspn(dn, "&");
char *name = malloc(len + 1);
if (!name) return NULL;
memcpy(name, dn, len);
name[len] = 0;
return name;
}
}
return strdup(base_name(source));
}
static const char *ui_state(const char *state, double progress) {
if (!state) return "stalledDL";
if (strcmp(state, "complete") == 0) return "uploading";
if (strcmp(state, "stopped") == 0)
return progress >= 1.0 ? "pausedUP" : "pausedDL";
if (strcmp(state, "stopping") == 0) return "pausedDL";
if (strcmp(state, "queued") == 0) return "queuedDL";
if (strcmp(state, "error") == 0) return "error";
return "downloading";
}
/* ---- single-writer download-rate estimate keyed by torrent id ---- */
static double speed_sample(uint64_t id, uint64_t bytes) {
double now = monotonic_seconds();
double result = 0.0;
pthread_mutex_lock(&g_webui.speed_lock);
speed_slot *slot = NULL, *spare = NULL;
for (size_t i = 0; i < SPEED_SLOTS; i++) {
speed_slot *s = &g_webui.speeds[i];
if (s->used && s->id == id) { slot = s; break; }
if (!s->used && !spare) spare = s;
}
if (!slot) {
if (!spare) {
/* table full: evict least-recently-updated */
spare = &g_webui.speeds[0];
for (size_t i = 1; i < SPEED_SLOTS; i++)
if (g_webui.speeds[i].last_time < spare->last_time)
spare = &g_webui.speeds[i];
}
slot = spare;
slot->used = true;
slot->id = id;
slot->last_bytes = bytes;
slot->last_time = now;
slot->dlspeed = 0.0;
pthread_mutex_unlock(&g_webui.speed_lock);
return 0.0;
}
double dt = now - slot->last_time;
if (dt > 0.0) {
double delta = bytes >= slot->last_bytes
? (double)(bytes - slot->last_bytes) : 0.0;
double inst = delta / dt;
slot->dlspeed = slot->dlspeed * 0.6 + inst * 0.4;
if (slot->dlspeed < 0.0) slot->dlspeed = 0.0;
slot->last_bytes = bytes;
slot->last_time = now;
}
result = slot->dlspeed;
pthread_mutex_unlock(&g_webui.speed_lock);
return result;
}
static double speed_peek(uint64_t id) {
double result = 0.0;
pthread_mutex_lock(&g_webui.speed_lock);
for (size_t i = 0; i < SPEED_SLOTS; i++)
if (g_webui.speeds[i].used && g_webui.speeds[i].id == id) {
result = g_webui.speeds[i].dlspeed;
break;
}
pthread_mutex_unlock(&g_webui.speed_lock);
return result;
}
/* Drop slots for ids no longer present so a long-lived server doesn't hand a
* stale rate to a recycled id. */
static void speed_retain(json_t *torrents) {
pthread_mutex_lock(&g_webui.speed_lock);
for (size_t i = 0; i < SPEED_SLOTS; i++) {
speed_slot *s = &g_webui.speeds[i];
if (!s->used) continue;
bool found = false;
size_t index;
json_t *torrent;
json_array_foreach(torrents, index, torrent)
if (json_u64(torrent, "torrent_id") == s->id) { found = true; break; }
if (!found) s->used = false;
}
pthread_mutex_unlock(&g_webui.speed_lock);
}
static json_t *tracker_hosts(json_t *trackers) {
json_t *hosts = json_array();
if (!hosts || !json_is_array(trackers)) return hosts;
size_t index;
json_t *tracker;
json_array_foreach(trackers, index, tracker) {
const char *url = json_string_value(json_object_get(tracker, "url"));
if (!url || strstr(url, "**")) continue;
const char *start = strstr(url, "://");
start = start ? start + 3 : url;
size_t len = strcspn(start, "/");
char host[256];
snprintf(host, sizeof host, "%.*s", (int)len, start);
json_array_append_new(hosts, json_string(host));
}
return hosts;
}
static double torrent_progress_ratio(json_t *torrent, uint64_t *done_out,
uint64_t *total_out) {
uint64_t done = json_u64(torrent, "bytes_done");
uint64_t total = json_u64(torrent, "total_bytes");
uint64_t pieces = json_u64(torrent, "total_pieces");
uint64_t pieces_done = json_u64(torrent, "pieces_done");
double progress = total ? (double)done / (double)total :
(pieces ? (double)pieces_done / (double)pieces : 0.0);
if (progress > 1.0) progress = 1.0;
if (done_out) *done_out = done;
if (total_out) *total_out = total;
return progress;
}
static json_int_t compute_eta(uint64_t done, uint64_t total, double dlspeed) {
if (total > done && dlspeed >= 1.0)
return (json_int_t)((double)(total - done) / dlspeed);
return ETA_INFINITY;
}
static json_t *map_torrent(json_t *torrent, bool detail, double dlspeed) {
uint64_t id = json_u64(torrent, "torrent_id");
uint64_t done = 0, total = 0;
double progress = torrent_progress_ratio(torrent, &done, &total);
uint64_t pieces = json_u64(torrent, "total_pieces");
uint64_t pieces_done = json_u64(torrent, "pieces_done");
char hash[32];
snprintf(hash, sizeof hash, "%llu", (unsigned long long)id);
char *name = torrent_name(torrent);
if (!name) return NULL;
const char *state = ui_state(json_string_value(json_object_get(torrent,
"state")),
progress);
json_t *trackers = NULL;
json_t *files = NULL;
json_t *peers_list = NULL;
json_t *hosts = NULL;
if (detail) {
trackers = json_array();
if (trackers)
json_array_append_new(trackers, json_pack(
"{s:s,s:i,s:s,s:i,s:i,s:i,s:i,s:s}",
"url", "** [DHT] **", "tier", -1, "status", "working",
"seeds", (int)json_u64(torrent, "peers_discovered"),
"peers", (int)json_u64(torrent, "peers"),
"leeches", -1, "downloaded", -1, "message", ""));
files = json_array();
if (files)
json_array_append_new(files, json_pack(
"{s:s,s:I,s:f,s:i,s:f}", "name", name,
"size", (json_int_t)total, "progress", progress,
"priority", 1, "availability", 1.0));
peers_list = json_array();
hosts = tracker_hosts(trackers);
}
json_t *out = json_pack(
"{s:s,s:s,s:I,s:f,s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:s,s:o,s:s,"
"s:I,s:I,s:I,s:I,s:f,s:i,s:o,s:b,s:b,s:b,s:I,s:I,s:s,s:s}",
"hash", hash,
"name", name,
"size", (json_int_t)total,
"progress", progress,
"dlspeed", (json_int_t)dlspeed,
"upspeed", 0,
"eta", compute_eta(done, total, dlspeed),
"seeds", (int)json_u64(torrent, "peers"),
"seedsTotal", (int)json_u64(torrent, "peers_discovered"),
"peers", (int)json_u64(torrent, "peers_connecting"),
"peersTotal", (int)json_u64(torrent, "peers_discovered"),
"ratio", 0.0,
"category", "",
"tags", json_array(),
"savePath", json_string_or(torrent, "output", ""),
"addedOn", (json_int_t)0,
"completionOn", progress >= 1.0 ? (json_int_t)0 : (json_int_t)-1,
"lastActivity", (json_int_t)0,
"downloaded", (json_int_t)done,
"uploaded", (json_int_t)0,
"availability", 1.0,
"priority", 1,
"trackerHosts", hosts ? hosts : json_array(),
"seqDl", false,
"superSeeding", false,
"forceStart", false,
"timeActive", (json_int_t)json_u64(torrent, "elapsed_seconds"),
"pieceSize", pieces ? (json_int_t)(total / pieces) : (json_int_t)0,
"state", state,
"contentPath", json_string_or(torrent, "output", ""));
if (out && detail) {
json_object_set_new(out, "comment", json_string(""));
json_object_set_new(out, "createdBy", json_string("Naut"));
json_object_set_new(out, "creationDate", json_integer(0));
json_object_set_new(out, "private", json_false());
json_object_set_new(out, "magnetUri",
json_string(json_string_or(torrent, "source", "")));
json_object_set_new(out, "pieceCount", json_integer((json_int_t)pieces));
json_object_set_new(out, "piecesDone", json_integer((json_int_t)pieces_done));
json_object_set_new(out, "trackers", trackers ? trackers : json_array());
json_object_set_new(out, "peersList", peers_list ? peers_list : json_array());
json_object_set_new(out, "files", files ? files : json_array());
} else if (detail) {
json_decref(trackers);
json_decref(files);
json_decref(peers_list);
json_decref(hosts);
}
free(name);
return out;
}
/* Build a fresh snapshot (grid + global stats) with live download rates. */
static json_t *build_snapshot(void) {
json_t *params = json_object();
json_t *torrents = rpc_call_json("torrents", params);
json_decref(params);
if (!json_is_array(torrents)) {
json_decref(torrents);
torrents = json_array();
}
speed_retain(torrents);
json_t *items = json_array();
uint64_t active = 0;
uint64_t total_rate = 0;
uint64_t total_data = 0;
size_t index;
json_t *torrent;
json_array_foreach(torrents, index, torrent) {
uint64_t id = json_u64(torrent, "torrent_id");
uint64_t done = json_u64(torrent, "bytes_done");
double dlspeed = speed_sample(id, done);
json_t *mapped = map_torrent(torrent, false, dlspeed);
if (!mapped) continue;
const char *state = json_string_value(json_object_get(mapped, "state"));
if (state && strcmp(state, "downloading") == 0) active++;
total_rate += (uint64_t)dlspeed;
total_data += done;
json_array_append_new(items, mapped);
}
json_decref(torrents);
json_t *server = json_pack(
"{s:I,s:i,s:I,s:i,s:i,s:i,s:f,s:i,s:s,s:i,s:I,s:i,s:i,s:s,s:i}",
"dl_info_speed", (json_int_t)total_rate,
"up_info_speed", 0,
"dl_info_data", (json_int_t)total_data,
"up_info_data", 0,
"dl_rate_limit", 0,
"up_rate_limit", 0,
"global_ratio", 0.0,
"dht_nodes", 0,
"connection_status", "connected",
"listen_port", g_webui.port,
"free_space", (json_int_t)0,
"active_torrents", (int)active,
"total_torrents", (int)json_array_size(items),
"read_cache_hits", "0.0",
"queued_io_jobs", 0);
return json_pack("{s:I,s:o,s:o}", "ts", (json_int_t)time(NULL) * 1000,
"server", server, "torrents", items);
}
/* Publish a newly built snapshot for all readers; wakes SSE waiters. */
static void publish_snapshot(void) {
json_t *snapshot = build_snapshot();
if (!snapshot) return;
char *full = json_dumps(snapshot, JSON_COMPACT | JSON_ENCODE_ANY);
json_t *torrents = json_object_get(snapshot, "torrents");
char *list = json_dumps(torrents ? torrents : json_array(),
JSON_COMPACT | JSON_ENCODE_ANY);
json_decref(snapshot);
if (!full || !list) {
free(full);
free(list);
return;
}
pthread_mutex_lock(&g_webui.snap_lock);
free(g_webui.snapshot_str);
free(g_webui.torrents_str);
g_webui.snapshot_str = full;
g_webui.torrents_str = list;
g_webui.snap_seq++;
pthread_cond_broadcast(&g_webui.snap_cond);
pthread_mutex_unlock(&g_webui.snap_lock);
}
static void *sampler_thread(void *arg) {
(void)arg;
while (!atomic_load(&g_webui.stopping)) {
publish_snapshot();
/* sleep ~1s but stay responsive to shutdown */
for (int i = 0; i < 10 && !atomic_load(&g_webui.stopping); i++) {
struct timespec ts = { .tv_sec = 0, .tv_nsec = 100 * 1000 * 1000 };
nanosleep(&ts, NULL);
}
}
return NULL;
}
static bool parse_id(const char *text, uint64_t *id) {
if (!text || !*text) return false;
char *end = NULL;
unsigned long long value = strtoull(text, &end, 10);
if (!end || (*end && *end != '/')) return false;
*id = (uint64_t)value;
return true;
}
static json_t *full_torrent_by_hash(const char *hash) {
uint64_t id = 0;
if (!parse_id(hash, &id)) return NULL;
json_t *params = json_pack("{s:I}", "torrent_id", (json_int_t)id);
json_t *torrent = rpc_call_json("torrent", params);
json_decref(params);
if (!torrent) return NULL;
json_t *mapped = map_torrent(torrent, true, speed_peek(id));
json_decref(torrent);
return mapped;
}
static void api_meta(int fd) {
json_t *json = json_object();
json_t *preferences = json_object();
if (!json || !preferences) {
json_decref(json);
json_decref(preferences);
http_text(fd, 500, "Internal Server Error", "oom");
return;
}
json_object_set_new(json, "categories", json_array());
json_object_set_new(json, "tags", json_array());
json_object_set_new(json, "trackers", json_array());
json_object_set_new(preferences, "save_path",
json_string(getenv("NAUT_WEBUI_SAVE_PATH")
? getenv("NAUT_WEBUI_SAVE_PATH") : "."));
json_object_set_new(preferences, "dl_limit", json_integer(0));
json_object_set_new(preferences, "up_limit", json_integer(0));
json_object_set_new(preferences, "alt_speed_enabled", json_false());
json_object_set_new(json, "preferences", preferences);
json_object_set_new(json, "searchPlugins", json_array());
http_json(fd, 200, json);
json_decref(json);
}
static void api_plugins(int fd) {
char path[PATH_MAX];
if (!join_root_path(path, sizeof path, "/plugins/plugins.json")) {
http_text(fd, 500, "Internal Server Error", "root too long");
return;
}
json_error_t error;
json_t *manifest = json_load_file(path, JSON_REJECT_DUPLICATES, &error);
json_t *modules = json_array();
if (json_is_object(manifest)) {
json_t *raw = json_object_get(manifest, "modules");
if (json_is_array(raw)) {
size_t index;
json_t *value;
json_array_foreach(raw, index, value) {
const char *module = json_string_value(value);
if (module && strncmp(module, "/plugins/", 9) == 0 &&
strstr(module, ".js"))
json_array_append_new(modules, json_string(module));
}
}
}
json_decref(manifest);
json_t *reply = json_pack("{s:o}", "modules", modules);
http_json(fd, 200, reply);
json_decref(reply);
}
static json_t *read_body_json(const char *body, size_t len) {
if (!body || len == 0) return json_object();
json_error_t error;
json_t *json = json_loadb(body, len, JSON_REJECT_DUPLICATES, &error);
return json ? json : json_object();
}
static const char *path_after(const char *path, const char *prefix) {
size_t len = strlen(prefix);
return strncmp(path, prefix, len) == 0 ? path + len : NULL;
}
static void api_torrent_detail(int fd, const char *tail) {
char hash[64];
size_t n = strcspn(tail, "/?");
snprintf(hash, sizeof hash, "%.*s", (int)n, tail);
json_t *torrent = full_torrent_by_hash(hash);
if (!torrent) {
http_text(fd, 404, "Not Found", "not found");
return;
}
char tab[64] = {0};
if (tail[n] == '/')
snprintf(tab, sizeof tab, "%s", tail + n + 1);
strip_query(tab);
if (strcmp(tab, "trackers") == 0) {
json_t *value = json_incref(json_object_get(torrent, "trackers"));
http_json(fd, 200, value);
json_decref(value);
} else if (strcmp(tab, "peers") == 0) {
json_t *value = json_incref(json_object_get(torrent, "peersList"));
http_json(fd, 200, value);
json_decref(value);
} else if (strcmp(tab, "files") == 0) {
json_t *value = json_incref(json_object_get(torrent, "files"));
http_json(fd, 200, value);
json_decref(value);
} else if (strcmp(tab, "pieces") == 0) {
uint64_t count = json_u64(torrent, "pieceCount");
uint64_t done = json_u64(torrent, "piecesDone");
json_t *pieces = json_array();
for (uint64_t i = 0; pieces && i < count && i < 4000; i++)
json_array_append_new(pieces, json_integer(i < done ? 2 : 0));
json_t *value = json_pack("{s:I,s:I,s:o}",
"pieceSize", json_u64(torrent, "pieceSize"),
"pieceCount", count, "pieces", pieces);
http_json(fd, 200, value);
json_decref(value);
} else {
http_json(fd, 200, torrent);
}
json_decref(torrent);
}
static void api_add(int fd, const char *body, size_t len) {
json_t *req = read_body_json(body, len);
const char *source = json_string_value(json_object_get(req, "source"));
const char *magnet = json_string_value(json_object_get(req, "magnet"));
const char *data = json_string_value(json_object_get(req, "data"));
const char *save_path = json_string_value(json_object_get(req, "savePath"));
if (!save_path || !*save_path) save_path = ".";
if (!source) source = magnet;
if ((!source || !*source) && (!data || !*data)) {
json_decref(req);
http_text(fd, 400, "Bad Request",
"torrent-ui must send a magnet, source, or torrent data");
return;
}
json_t *params = json_object();
json_object_set_new(params, "output", json_string(save_path));
if (source && *source) json_object_set_new(params, "source", json_string(source));
if (data && *data) json_object_set_new(params, "data", json_string(data));
json_t *result = rpc_call_json("add_torrent", params);
json_decref(params);
json_decref(req);
if (!result) {
http_text(fd, 502, "Bad Gateway", "add_torrent failed");
return;
}
char id[32];
snprintf(id, sizeof id, "%llu",
(unsigned long long)json_u64(result, "torrent_id"));
json_t *reply = json_pack("{s:b,s:s}", "ok", 1, "hash", id);
http_json(fd, 200, reply);
json_decref(reply);
json_decref(result);
/* refresh the shared snapshot so the new torrent shows up immediately */
publish_snapshot();
}
static void api_delete(int fd, const char *body, size_t len) {
json_t *req = read_body_json(body, len);
json_t *hashes = json_object_get(req, "hashes");
size_t removed = 0;
if (json_is_array(hashes)) {
size_t index;
json_t *hash;
json_array_foreach(hashes, index, hash) {
uint64_t id = 0;
if (!parse_id(json_string_value(hash), &id)) continue;
json_t *params = json_pack("{s:I}", "torrent_id", (json_int_t)id);
json_t *result = rpc_call_json("remove_torrent", params);
json_decref(params);
if (result) {
removed++;
json_decref(result);
}
}
}
json_decref(req);
json_t *reply = json_pack("{s:b,s:i}", "ok", 1, "removed", (int)removed);
http_json(fd, 200, reply);
json_decref(reply);
if (removed) publish_snapshot();
}
/* The engine has no pause/resume/recheck/queue/category/limit verbs yet, so
* rather than claim success we tell the UI the action is unsupported. The
* front end surfaces a non-2xx as an honest "Action failed" toast. */
static void api_action(int fd, const char *body, size_t len) {
json_t *req = read_body_json(body, len);
const char *action = json_string_value(json_object_get(req, "action"));
char message[128];
snprintf(message, sizeof message,
"action '%s' is not supported by the engine",
action ? action : "");
json_decref(req);
json_t *json = json_pack("{s:b,s:s}", "ok", 0, "error", message);
http_json(fd, 501, json);
json_decref(json);
}
static void api_stream(int fd) {
const char *head =
"HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n"
"Cache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n"
"retry: 2000\n\n";
if (!send_all_fd(fd, head, strlen(head))) return;
uint64_t seen = 0;
while (!atomic_load(&g_webui.stopping)) {
char *payload = NULL;
pthread_mutex_lock(&g_webui.snap_lock);
while (!atomic_load(&g_webui.stopping) && g_webui.snap_seq == seen) {
struct timespec ts;
clock_gettime(CLOCK_REALTIME, &ts);
ts.tv_nsec += 250 * 1000 * 1000;
if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; }
pthread_cond_timedwait(&g_webui.snap_cond, &g_webui.snap_lock, &ts);
}
if (!atomic_load(&g_webui.stopping) && g_webui.snapshot_str) {
payload = strdup(g_webui.snapshot_str);
seen = g_webui.snap_seq;
}
pthread_mutex_unlock(&g_webui.snap_lock);
if (!payload) break;
bool ok = send_all_fd(fd, "event: snapshot\ndata: ", 22) &&
send_all_fd(fd, payload, strlen(payload)) &&
send_all_fd(fd, "\n\n", 2);
free(payload);
if (!ok) break;
}
}
static void serve_cached_snapshot(int fd) {
pthread_mutex_lock(&g_webui.snap_lock);
char *copy = g_webui.snapshot_str ? strdup(g_webui.snapshot_str) : NULL;
pthread_mutex_unlock(&g_webui.snap_lock);
http_json_str(fd, copy, "{\"server\":{},\"torrents\":[]}");
free(copy);
}
static void serve_cached_torrents(int fd) {
pthread_mutex_lock(&g_webui.snap_lock);
char *copy = g_webui.torrents_str ? strdup(g_webui.torrents_str) : NULL;
pthread_mutex_unlock(&g_webui.snap_lock);
http_json_str(fd, copy, "[]");
free(copy);
}
static void handle_api(int fd, const char *method, char *path,
const char *headers, const char *headers_end,
const char *body, size_t body_len) {
strip_query(path);
if (strcmp(path, "/api/auth/status") == 0 && strcmp(method, "GET") == 0) {
json_t *json = json_pack("{s:b,s:s,s:b}",
"authenticated",
current_user(headers, headers_end),
"user", g_webui.auth_user,
"generatedPassword",
g_webui.generated_password);
http_json(fd, 200, json);
json_decref(json);
} else if (strcmp(path, "/api/login") == 0 && strcmp(method, "POST") == 0) {
json_t *req = read_body_json(body, body_len);
const char *user = json_string_value(json_object_get(req, "username"));
const char *password =
json_string_value(json_object_get(req, "password"));
bool user_ok = user && constant_time_equal(user, g_webui.auth_user);
bool pass_ok = password && g_webui.auth_password[0] &&
constant_time_equal(password, g_webui.auth_password);
if (!user_ok || !pass_ok) {
json_t *json = json_pack("{s:b,s:s}", "ok", 0,
"error", "invalid credentials");
http_json(fd, 401, json);
json_decref(json);
json_decref(req);
return;
}
char token[96];
if (!create_session(token, sizeof token)) {
json_decref(req);
http_text(fd, 500, "Internal Server Error", "session failed");
return;
}
char cookie[256];
snprintf(cookie, sizeof cookie,
"Set-Cookie: %s=%s; Path=/; HttpOnly; SameSite=Lax; "
"Max-Age=%d\r\n",
SESSION_COOKIE, token, SESSION_TTL_SECONDS);
json_t *json = json_pack("{s:b,s:s}", "ok", 1,
"user", g_webui.auth_user);
http_json_extra(fd, 200, json, cookie);
json_decref(json);
json_decref(req);
} else if (strcmp(path, "/api/logout") == 0 && strcmp(method, "POST") == 0) {
clear_session(headers, headers_end);
json_t *json = json_pack("{s:b}", "ok", 1);
http_json_extra(fd, 200, json,
"Set-Cookie: naut_session=; Path=/; HttpOnly; "
"SameSite=Lax; Max-Age=0\r\n");
json_decref(json);
} else if (!current_user(headers, headers_end)) {
json_t *json = json_pack("{s:s}", "error",
"authentication required");
http_json(fd, 401, json);
json_decref(json);
} else if (strcmp(path, "/api/plugins") == 0 && strcmp(method, "GET") == 0) {
api_plugins(fd);
} else if (strcmp(path, "/api/stream") == 0 && strcmp(method, "GET") == 0) {
api_stream(fd);
} else if (strcmp(path, "/api/snapshot") == 0 && strcmp(method, "GET") == 0) {
serve_cached_snapshot(fd);
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
api_meta(fd);
} else if (strcmp(path, "/api/preferences") == 0) {
api_meta(fd);
} else if (strcmp(path, "/api/altspeed") == 0 && strcmp(method, "POST") == 0) {
json_t *json = json_pack("{s:b}", "alt_speed_enabled", 0);
http_json(fd, 200, json);
json_decref(json);
} else if ((strcmp(path, "/api/categories") == 0 ||
strcmp(path, "/api/categories/delete") == 0) &&
strcmp(method, "POST") == 0) {
json_t *json = json_array();
http_json(fd, 200, json);
json_decref(json);
} else if ((strcmp(path, "/api/tags") == 0 ||
strcmp(path, "/api/tags/delete") == 0) &&
strcmp(method, "POST") == 0) {
json_t *json = json_array();
http_json(fd, 200, json);
json_decref(json);
} else if (strcmp(path, "/api/torrents") == 0 && strcmp(method, "GET") == 0) {
serve_cached_torrents(fd);
} else if (path_after(path, "/api/torrents/") && strcmp(method, "GET") == 0) {
api_torrent_detail(fd, path_after(path, "/api/torrents/"));
} else if (strcmp(path, "/api/add") == 0 && strcmp(method, "POST") == 0) {
api_add(fd, body, body_len);
} else if (strcmp(path, "/api/delete") == 0 && strcmp(method, "POST") == 0) {
api_delete(fd, body, body_len);
} else if (strcmp(path, "/api/action") == 0 && strcmp(method, "POST") == 0) {
api_action(fd, body, body_len);
} else if (strncmp(path, "/api/rss", 8) == 0 && strcmp(method, "GET") == 0) {
json_t *json = json_array();
http_json(fd, 200, json);
json_decref(json);
} else if (strncmp(path, "/api/search", 11) == 0 && strcmp(method, "GET") == 0) {
json_t *json = json_array();
http_json(fd, 200, json);
json_decref(json);
} else {
http_text(fd, 404, "Not Found", "not found");
}
}
static void handle_conn(int fd) {
char *request = malloc(READ_LIMIT + 1);
if (!request) {
http_text(fd, 500, "Internal Server Error", "oom");
return;
}
size_t len = 0;
char *hdrend = NULL;
while (len < READ_LIMIT) {
ssize_t n = recv(fd, request + len, READ_LIMIT - len, 0);
if (n < 0) {
if (errno == EINTR) continue;
free(request);
return;
}
if (n == 0) break;
len += (size_t)n;
request[len] = 0;
hdrend = memmem(request, len, "\r\n\r\n", 4);
if (hdrend) break;
}
if (!hdrend) {
free(request);
http_text(fd, 400, "Bad Request", "malformed request");
return;
}
char method[8] = {0};
char path[PATH_MAX] = {0};
if (sscanf(request, "%7s %4095s", method, path) != 2) {
free(request);
http_text(fd, 400, "Bad Request", "malformed request line");
return;
}
size_t header_len = (size_t)(hdrend - request) + 4;
size_t content_length = 0;
char *cl = strcasestr(request, "content-length:");
if (cl && cl < hdrend) content_length = strtoull(cl + 15, NULL, 10);
/* Reject bodies we can't buffer instead of silently truncating an upload
* into a corrupt torrent. */
if (content_length > READ_LIMIT - header_len) {
free(request);
http_text(fd, 413, "Payload Too Large",
"request body exceeds the 8 MiB limit");
return;
}
while (len - header_len < content_length && len < READ_LIMIT) {
ssize_t n = recv(fd, request + len, READ_LIMIT - len, 0);
if (n < 0) {
if (errno == EINTR) continue;
break;
}
if (n == 0) break;
len += (size_t)n;
request[len] = 0;
}
char *body = request + header_len;
size_t body_len = len > header_len ? len - header_len : 0;
if (strncmp(path, "/api/", 5) == 0)
handle_api(fd, method, path, request, hdrend, body, body_len);
else if (!serve_file(fd, path))
http_text(fd, 404, "Not Found", "not found");
free(request);
}
static void finish_connection(void) {
pthread_mutex_lock(&g_webui.conn_lock);
if (g_webui.active_connections > 0) g_webui.active_connections--;
pthread_cond_signal(&g_webui.conn_cond);
pthread_mutex_unlock(&g_webui.conn_lock);
}
static void *conn_thread(void *arg) {
conn_arg *conn = arg;
handle_conn(conn->fd);
close(conn->fd);
free(conn);
finish_connection();
return NULL;
}
static void *server_thread(void *arg) {
(void)arg;
for (;;) {
int fd = accept(g_webui.listener, NULL, NULL);
if (fd < 0) {
if (errno == EINTR) continue;
if (atomic_load(&g_webui.stopping)) break;
continue;
}
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout);
/* Bound concurrent connections so a client can't spawn unlimited
* threads (each SSE stream parks one). */
pthread_mutex_lock(&g_webui.conn_lock);
bool full = g_webui.active_connections >= MAX_CONNECTIONS;
if (!full) g_webui.active_connections++;
pthread_mutex_unlock(&g_webui.conn_lock);
if (full) {
http_text(fd, 503, "Service Unavailable", "too many connections");
close(fd);
continue;
}
conn_arg *conn = malloc(sizeof(*conn));
if (!conn) {
close(fd);
finish_connection();
continue;
}
conn->fd = fd;
pthread_t thread;
if (pthread_create(&thread, NULL, conn_thread, conn) != 0) {
close(fd);
free(conn);
finish_connection();
continue;
}
pthread_detach(thread);
}
return NULL;
}
static bool dir_exists(const char *path) {
struct stat st;
return path && stat(path, &st) == 0 && S_ISDIR(st.st_mode);
}
static const char *find_root(void) {
const char *env = getenv("NAUT_WEBUI_ROOT");
if (dir_exists(env)) return env;
static const char *candidates[] = {
"../torrent-ui/public",
"torrent-ui/public",
"./public",
"/usr/share/naut/torrent-ui/public",
};
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++)
if (dir_exists(candidates[i])) return candidates[i];
return NULL;
}
static int parse_port(void) {
const char *env = getenv("NAUT_WEBUI_PORT");
if (!env || !*env) return DEFAULT_PORT;
char *end = NULL;
long port = strtol(env, &end, 10);
return end && !*end && port > 0 && port <= 65535 ? (int)port : DEFAULT_PORT;
}
static naut_err start_server(void) {
const char *root = find_root();
if (!root) {
log_msg(0, "webui: could not find torrent-ui public assets; set NAUT_WEBUI_ROOT");
return NAUT_ERR_NOTFOUND;
}
snprintf(g_webui.root, sizeof g_webui.root, "%s", root);
const char *host = getenv("NAUT_WEBUI_HOST");
if (!host || !*host || strcmp(host, "localhost") == 0) host = DEFAULT_HOST;
snprintf(g_webui.host_name, sizeof g_webui.host_name, "%s", host);
g_webui.port = parse_port();
int fd = socket(AF_INET, SOCK_STREAM, 0);
if (fd < 0) return NAUT_ERR_IO;
int one = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof one);
struct sockaddr_in addr;
memset(&addr, 0, sizeof addr);
addr.sin_family = AF_INET;
addr.sin_port = htons((uint16_t)g_webui.port);
if (inet_pton(AF_INET, g_webui.host_name, &addr.sin_addr) != 1) {
close(fd);
return NAUT_ERR_INVAL;
}
if (bind(fd, (struct sockaddr *)&addr, sizeof addr) != 0 ||
listen(fd, 64) != 0) {
close(fd);
return NAUT_ERR_IO;
}
g_webui.listener = fd;
/* Prime the cache so the first request doesn't see an empty snapshot. */
publish_snapshot();
if (pthread_create(&g_webui.sampler, NULL, sampler_thread, NULL) != 0) {
close(fd);
g_webui.listener = -1;
return NAUT_ERR_NOMEM;
}
g_webui.sampler_started = true;
if (pthread_create(&g_webui.thread, NULL, server_thread, NULL) != 0) {
atomic_store(&g_webui.stopping, true);
pthread_join(g_webui.sampler, NULL);
g_webui.sampler_started = false;
close(fd);
g_webui.listener = -1;
return NAUT_ERR_NOMEM;
}
g_webui.thread_started = true;
char msg[PATH_MAX + 128];
snprintf(msg, sizeof msg, "webui: serving http://%s:%d from %s",
g_webui.host_name, g_webui.port, g_webui.root);
log_msg(2, msg);
if (strcmp(g_webui.host_name, DEFAULT_HOST) != 0)
log_msg(1, "webui: bound to a non-loopback address; credentials cross "
"the network in plaintext (set NAUT_AUTH_PASSWORD)");
snprintf(msg, sizeof msg, "webui: auth user %s", g_webui.auth_user);
log_msg(2, msg);
if (g_webui.generated_password && g_webui.auth_password[0]) {
snprintf(msg, sizeof msg, "webui: generated password %s",
g_webui.auth_password);
log_msg(1, msg);
} else if (!g_webui.auth_password[0]) {
log_msg(0, "webui: no password available (CSPRNG unavailable); set "
"NAUT_AUTH_PASSWORD to enable login");
}
return NAUT_OK;
}
naut_err naut_plugin_register(const naut_host_api *host) {
if (!host || host->abi_version != NAUT_PLUGIN_ABI_VERSION ||
host->struct_size < sizeof(*host) || !host->call_rpc)
return NAUT_ERR_INVAL;
naut_err error = NAUT_ERR_NOMEM;
memset(&g_webui, 0, sizeof g_webui);
g_webui.listener = -1;
g_webui.host = *host;
if (pthread_mutex_init(&g_webui.auth_lock, NULL) != 0)
return NAUT_ERR_NOMEM;
if (pthread_mutex_init(&g_webui.conn_lock, NULL) != 0)
goto fail_conn_lock;
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0)
goto fail_conn_cond;
if (pthread_mutex_init(&g_webui.snap_lock, NULL) != 0)
goto fail_snap_lock;
if (pthread_cond_init(&g_webui.snap_cond, NULL) != 0)
goto fail_snap_cond;
if (pthread_mutex_init(&g_webui.speed_lock, NULL) != 0)
goto fail_speed_lock;
init_auth();
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
"webui");
if (error != NAUT_OK) goto fail_named;
error = start_server();
if (error != NAUT_OK) goto fail_named;
return NAUT_OK;
fail_named:
pthread_mutex_destroy(&g_webui.speed_lock);
fail_speed_lock:
pthread_cond_destroy(&g_webui.snap_cond);
fail_snap_cond:
pthread_mutex_destroy(&g_webui.snap_lock);
fail_snap_lock:
pthread_cond_destroy(&g_webui.conn_cond);
fail_conn_cond:
pthread_mutex_destroy(&g_webui.conn_lock);
fail_conn_lock:
pthread_mutex_destroy(&g_webui.auth_lock);
return error;
}
naut_err naut_plugin_shutdown(void) {
atomic_store(&g_webui.stopping, true);
/* wake any SSE streams parked on the snapshot condition */
pthread_mutex_lock(&g_webui.snap_lock);
pthread_cond_broadcast(&g_webui.snap_cond);
pthread_mutex_unlock(&g_webui.snap_lock);
if (g_webui.listener >= 0) {
shutdown(g_webui.listener, SHUT_RDWR);
close(g_webui.listener);
g_webui.listener = -1;
}
if (g_webui.thread_started)
pthread_join(g_webui.thread, NULL);
g_webui.thread_started = false;
if (g_webui.sampler_started)
pthread_join(g_webui.sampler, NULL);
g_webui.sampler_started = false;
pthread_mutex_lock(&g_webui.conn_lock);
while (g_webui.active_connections > 0)
pthread_cond_wait(&g_webui.conn_cond, &g_webui.conn_lock);
pthread_mutex_unlock(&g_webui.conn_lock);
free(g_webui.snapshot_str);
free(g_webui.torrents_str);
g_webui.snapshot_str = NULL;
g_webui.torrents_str = NULL;
pthread_mutex_destroy(&g_webui.speed_lock);
pthread_cond_destroy(&g_webui.snap_cond);
pthread_mutex_destroy(&g_webui.snap_lock);
pthread_cond_destroy(&g_webui.conn_cond);
pthread_mutex_destroy(&g_webui.conn_lock);
pthread_mutex_destroy(&g_webui.auth_lock);
return NAUT_OK;
}