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>
This commit is contained in:
parent
8dde48c05a
commit
41ed172272
2 changed files with 402 additions and 97 deletions
|
|
@ -957,6 +957,11 @@ int main(int argc, char **argv) {
|
||||||
|
|
||||||
close(listener);
|
close(listener);
|
||||||
unlink(socket_path);
|
unlink(socket_path);
|
||||||
|
/* Tear down plugins first: a plugin like webui runs its own threads that
|
||||||
|
* call back into the daemon via RPC, so it must be stopped (and its
|
||||||
|
* threads joined) before we free the torrent tasks those calls touch. */
|
||||||
|
naut_plugin_manager_destroy(state.plugins);
|
||||||
|
state.plugins = NULL;
|
||||||
naut_script_destroy(state.script);
|
naut_script_destroy(state.script);
|
||||||
state.script = NULL;
|
state.script = NULL;
|
||||||
stop_torrents(&state);
|
stop_torrents(&state);
|
||||||
|
|
@ -966,7 +971,6 @@ int main(int argc, char **argv) {
|
||||||
for (size_t i = 0; i < state.subscriber_count; i++)
|
for (size_t i = 0; i < state.subscriber_count; i++)
|
||||||
close(state.subscribers[i]);
|
close(state.subscribers[i]);
|
||||||
pthread_mutex_unlock(&state.subscriber_lock);
|
pthread_mutex_unlock(&state.subscriber_lock);
|
||||||
naut_plugin_manager_destroy(state.plugins);
|
|
||||||
naut_rpc_registry_destroy(state.rpc);
|
naut_rpc_registry_destroy(state.rpc);
|
||||||
naut_event_bus_destroy(state.events);
|
naut_event_bus_destroy(state.events);
|
||||||
pthread_mutex_destroy(&state.subscriber_lock);
|
pthread_mutex_destroy(&state.subscriber_lock);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <strings.h>
|
#include <strings.h>
|
||||||
|
#include <sys/random.h>
|
||||||
#include <sys/socket.h>
|
#include <sys/socket.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <sys/time.h>
|
#include <sys/time.h>
|
||||||
|
|
@ -27,6 +28,9 @@
|
||||||
#define SESSION_COOKIE "naut_session"
|
#define SESSION_COOKIE "naut_session"
|
||||||
#define SESSION_TTL_SECONDS (60 * 60 * 24 * 7)
|
#define SESSION_TTL_SECONDS (60 * 60 * 24 * 7)
|
||||||
#define MAX_SESSIONS 64
|
#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 {
|
typedef struct {
|
||||||
char token[96];
|
char token[96];
|
||||||
|
|
@ -34,6 +38,16 @@ typedef struct {
|
||||||
bool used;
|
bool used;
|
||||||
} webui_session;
|
} 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 {
|
typedef struct {
|
||||||
naut_host_api host;
|
naut_host_api host;
|
||||||
char root[PATH_MAX];
|
char root[PATH_MAX];
|
||||||
|
|
@ -46,10 +60,27 @@ typedef struct {
|
||||||
atomic_bool stopping;
|
atomic_bool stopping;
|
||||||
bool thread_started;
|
bool thread_started;
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
|
|
||||||
|
bool sampler_started;
|
||||||
|
pthread_t sampler;
|
||||||
|
|
||||||
pthread_mutex_t auth_lock;
|
pthread_mutex_t auth_lock;
|
||||||
|
|
||||||
pthread_mutex_t conn_lock;
|
pthread_mutex_t conn_lock;
|
||||||
pthread_cond_t conn_cond;
|
pthread_cond_t conn_cond;
|
||||||
size_t active_connections;
|
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_session sessions[MAX_SESSIONS];
|
||||||
} webui_state;
|
} webui_state;
|
||||||
|
|
||||||
|
|
@ -59,6 +90,12 @@ typedef struct {
|
||||||
|
|
||||||
static webui_state g_webui;
|
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) {
|
static void log_msg(int level, const char *message) {
|
||||||
if (g_webui.host.log)
|
if (g_webui.host.log)
|
||||||
g_webui.host.log(g_webui.host.host_context, level, message);
|
g_webui.host.log(g_webui.host.host_context, level, message);
|
||||||
|
|
@ -124,6 +161,13 @@ static void http_json_extra(int fd, int code, json_t *json, const char *extra) {
|
||||||
free(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) {
|
static const char *mime_type(const char *path) {
|
||||||
const char *dot = strrchr(path, '.');
|
const char *dot = strrchr(path, '.');
|
||||||
if (!dot) return "application/octet-stream";
|
if (!dot) return "application/octet-stream";
|
||||||
|
|
@ -143,20 +187,37 @@ static void strip_query(char *path) {
|
||||||
if (hash) *hash = 0;
|
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 bool random_hex(char *out, size_t out_size, size_t bytes) {
|
||||||
static const char hex[] = "0123456789abcdef";
|
static const char hex[] = "0123456789abcdef";
|
||||||
if (out_size < bytes * 2 + 1) return false;
|
if (out_size < bytes * 2 + 1) return false;
|
||||||
unsigned char buf[64];
|
unsigned char buf[64];
|
||||||
if (bytes > sizeof buf) return false;
|
if (bytes > sizeof buf) return false;
|
||||||
int fd = open("/dev/urandom", O_RDONLY);
|
size_t got = 0;
|
||||||
ssize_t got = fd >= 0 ? read(fd, buf, bytes) : -1;
|
while (got < bytes) {
|
||||||
if (fd >= 0) close(fd);
|
ssize_t n = getrandom(buf + got, bytes - got, 0);
|
||||||
if (got != (ssize_t)bytes) {
|
if (n < 0) {
|
||||||
unsigned seed = (unsigned)time(NULL) ^ (unsigned)getpid();
|
if (errno == EINTR) continue;
|
||||||
for (size_t i = 0; i < bytes; i++) {
|
return false;
|
||||||
seed = seed * 1103515245u + 12345u;
|
|
||||||
buf[i] = (unsigned char)(seed >> 16);
|
|
||||||
}
|
}
|
||||||
|
got += (size_t)n;
|
||||||
}
|
}
|
||||||
for (size_t i = 0; i < bytes; i++) {
|
for (size_t i = 0; i < bytes; i++) {
|
||||||
out[i * 2] = hex[buf[i] >> 4];
|
out[i * 2] = hex[buf[i] >> 4];
|
||||||
|
|
@ -180,7 +241,11 @@ static void init_auth(void) {
|
||||||
g_webui.generated_password = false;
|
g_webui.generated_password = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
random_hex(g_webui.auth_password, sizeof g_webui.auth_password, 9);
|
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;
|
g_webui.generated_password = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -252,16 +317,17 @@ static bool current_user(const char *headers, const char *end) {
|
||||||
static bool create_session(char *out, size_t out_size) {
|
static bool create_session(char *out, size_t out_size) {
|
||||||
char token[96];
|
char token[96];
|
||||||
if (!random_hex(token, sizeof token, 24)) return false;
|
if (!random_hex(token, sizeof token, 24)) return false;
|
||||||
time_t expires = time(NULL) + SESSION_TTL_SECONDS;
|
time_t now = time(NULL);
|
||||||
|
time_t expires = now + SESSION_TTL_SECONDS;
|
||||||
pthread_mutex_lock(&g_webui.auth_lock);
|
pthread_mutex_lock(&g_webui.auth_lock);
|
||||||
webui_session *slot = NULL;
|
webui_session *slot = NULL;
|
||||||
for (size_t i = 0; i < MAX_SESSIONS; i++) {
|
for (size_t i = 0; i < MAX_SESSIONS; i++) {
|
||||||
if (!g_webui.sessions[i].used) {
|
webui_session *s = &g_webui.sessions[i];
|
||||||
slot = &g_webui.sessions[i];
|
if (!s->used || s->expires < now) { slot = s; break; }
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (!slot) slot = &g_webui.sessions[0];
|
|
||||||
snprintf(slot->token, sizeof slot->token, "%s", token);
|
snprintf(slot->token, sizeof slot->token, "%s", token);
|
||||||
slot->expires = expires;
|
slot->expires = expires;
|
||||||
slot->used = true;
|
slot->used = true;
|
||||||
|
|
@ -403,6 +469,79 @@ static const char *ui_state(const char *state, double progress) {
|
||||||
return "downloading";
|
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) {
|
static json_t *tracker_hosts(json_t *trackers) {
|
||||||
json_t *hosts = json_array();
|
json_t *hosts = json_array();
|
||||||
if (!hosts || !json_is_array(trackers)) return hosts;
|
if (!hosts || !json_is_array(trackers)) return hosts;
|
||||||
|
|
@ -421,8 +560,8 @@ static json_t *tracker_hosts(json_t *trackers) {
|
||||||
return hosts;
|
return hosts;
|
||||||
}
|
}
|
||||||
|
|
||||||
static json_t *map_torrent(json_t *torrent, bool detail) {
|
static double torrent_progress_ratio(json_t *torrent, uint64_t *done_out,
|
||||||
uint64_t id = json_u64(torrent, "torrent_id");
|
uint64_t *total_out) {
|
||||||
uint64_t done = json_u64(torrent, "bytes_done");
|
uint64_t done = json_u64(torrent, "bytes_done");
|
||||||
uint64_t total = json_u64(torrent, "total_bytes");
|
uint64_t total = json_u64(torrent, "total_bytes");
|
||||||
uint64_t pieces = json_u64(torrent, "total_pieces");
|
uint64_t pieces = json_u64(torrent, "total_pieces");
|
||||||
|
|
@ -430,6 +569,23 @@ static json_t *map_torrent(json_t *torrent, bool detail) {
|
||||||
double progress = total ? (double)done / (double)total :
|
double progress = total ? (double)done / (double)total :
|
||||||
(pieces ? (double)pieces_done / (double)pieces : 0.0);
|
(pieces ? (double)pieces_done / (double)pieces : 0.0);
|
||||||
if (progress > 1.0) progress = 1.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];
|
char hash[32];
|
||||||
snprintf(hash, sizeof hash, "%llu", (unsigned long long)id);
|
snprintf(hash, sizeof hash, "%llu", (unsigned long long)id);
|
||||||
|
|
@ -439,43 +595,46 @@ static json_t *map_torrent(json_t *torrent, bool detail) {
|
||||||
"state")),
|
"state")),
|
||||||
progress);
|
progress);
|
||||||
|
|
||||||
json_t *trackers = json_array();
|
json_t *trackers = NULL;
|
||||||
if (trackers) {
|
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(
|
json_array_append_new(trackers, json_pack(
|
||||||
"{s:s,s:i,s:s,s:i,s:i,s:i,s:i,s:s}",
|
"{s:s,s:i,s:s,s:i,s:i,s:i,s:i,s:s}",
|
||||||
"url", "** [DHT] **", "tier", -1, "status", "working",
|
"url", "** [DHT] **", "tier", -1, "status", "working",
|
||||||
"seeds", (int)json_u64(torrent, "peers_discovered"),
|
"seeds", (int)json_u64(torrent, "peers_discovered"),
|
||||||
"peers", (int)json_u64(torrent, "peers"),
|
"peers", (int)json_u64(torrent, "peers"),
|
||||||
"leeches", -1, "downloaded", -1, "message", ""));
|
"leeches", -1, "downloaded", -1, "message", ""));
|
||||||
}
|
files = json_array();
|
||||||
json_t *files = json_array();
|
if (files)
|
||||||
if (files) {
|
|
||||||
json_array_append_new(files, json_pack(
|
json_array_append_new(files, json_pack(
|
||||||
"{s:s,s:I,s:f,s:i,s:f}", "name", name,
|
"{s:s,s:I,s:f,s:i,s:f}", "name", name,
|
||||||
"size", (json_int_t)total, "progress", progress,
|
"size", (json_int_t)total, "progress", progress,
|
||||||
"priority", 1, "availability", 1.0));
|
"priority", 1, "availability", 1.0));
|
||||||
|
peers_list = json_array();
|
||||||
|
hosts = tracker_hosts(trackers);
|
||||||
}
|
}
|
||||||
json_t *peers_list = json_array();
|
|
||||||
json_t *tags = json_array();
|
|
||||||
json_t *hosts = tracker_hosts(trackers);
|
|
||||||
|
|
||||||
json_t *out = json_pack(
|
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: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}",
|
"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,
|
"hash", hash,
|
||||||
"name", name,
|
"name", name,
|
||||||
"size", (json_int_t)total,
|
"size", (json_int_t)total,
|
||||||
"progress", progress,
|
"progress", progress,
|
||||||
"dlspeed", 0,
|
"dlspeed", (json_int_t)dlspeed,
|
||||||
"upspeed", 0,
|
"upspeed", 0,
|
||||||
"eta", progress > 0.0 && progress < 1.0 ? 8640000 : 0,
|
"eta", compute_eta(done, total, dlspeed),
|
||||||
"seeds", (int)json_u64(torrent, "peers"),
|
"seeds", (int)json_u64(torrent, "peers"),
|
||||||
"seedsTotal", (int)json_u64(torrent, "peers_discovered"),
|
"seedsTotal", (int)json_u64(torrent, "peers_discovered"),
|
||||||
"peers", (int)json_u64(torrent, "peers_connecting"),
|
"peers", (int)json_u64(torrent, "peers_connecting"),
|
||||||
"peersTotal", (int)json_u64(torrent, "peers_discovered"),
|
"peersTotal", (int)json_u64(torrent, "peers_discovered"),
|
||||||
"ratio", 0.0,
|
"ratio", 0.0,
|
||||||
"category", "",
|
"category", "",
|
||||||
"tags", tags ? tags : json_array(),
|
"tags", json_array(),
|
||||||
"savePath", json_string_or(torrent, "output", ""),
|
"savePath", json_string_or(torrent, "output", ""),
|
||||||
"addedOn", (json_int_t)0,
|
"addedOn", (json_int_t)0,
|
||||||
"completionOn", progress >= 1.0 ? (json_int_t)0 : (json_int_t)-1,
|
"completionOn", progress >= 1.0 ? (json_int_t)0 : (json_int_t)-1,
|
||||||
|
|
@ -504,16 +663,18 @@ static json_t *map_torrent(json_t *torrent, bool detail) {
|
||||||
json_object_set_new(out, "trackers", trackers ? trackers : json_array());
|
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, "peersList", peers_list ? peers_list : json_array());
|
||||||
json_object_set_new(out, "files", files ? files : json_array());
|
json_object_set_new(out, "files", files ? files : json_array());
|
||||||
} else {
|
} else if (detail) {
|
||||||
json_decref(trackers);
|
json_decref(trackers);
|
||||||
json_decref(files);
|
json_decref(files);
|
||||||
json_decref(peers_list);
|
json_decref(peers_list);
|
||||||
|
json_decref(hosts);
|
||||||
}
|
}
|
||||||
free(name);
|
free(name);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
static json_t *snapshot_json(void) {
|
/* Build a fresh snapshot (grid + global stats) with live download rates. */
|
||||||
|
static json_t *build_snapshot(void) {
|
||||||
json_t *params = json_object();
|
json_t *params = json_object();
|
||||||
json_t *torrents = rpc_call_json("torrents", params);
|
json_t *torrents = rpc_call_json("torrents", params);
|
||||||
json_decref(params);
|
json_decref(params);
|
||||||
|
|
@ -521,24 +682,32 @@ static json_t *snapshot_json(void) {
|
||||||
json_decref(torrents);
|
json_decref(torrents);
|
||||||
torrents = json_array();
|
torrents = json_array();
|
||||||
}
|
}
|
||||||
|
speed_retain(torrents);
|
||||||
|
|
||||||
json_t *items = json_array();
|
json_t *items = json_array();
|
||||||
uint64_t active = 0;
|
uint64_t active = 0;
|
||||||
|
uint64_t total_rate = 0;
|
||||||
|
uint64_t total_data = 0;
|
||||||
size_t index;
|
size_t index;
|
||||||
json_t *torrent;
|
json_t *torrent;
|
||||||
json_array_foreach(torrents, index, torrent) {
|
json_array_foreach(torrents, index, torrent) {
|
||||||
json_t *mapped = map_torrent(torrent, false);
|
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;
|
if (!mapped) continue;
|
||||||
const char *state = json_string_value(json_object_get(mapped, "state"));
|
const char *state = json_string_value(json_object_get(mapped, "state"));
|
||||||
if (state && strcmp(state, "downloading") == 0) active++;
|
if (state && strcmp(state, "downloading") == 0) active++;
|
||||||
|
total_rate += (uint64_t)dlspeed;
|
||||||
|
total_data += done;
|
||||||
json_array_append_new(items, mapped);
|
json_array_append_new(items, mapped);
|
||||||
}
|
}
|
||||||
json_decref(torrents);
|
json_decref(torrents);
|
||||||
json_t *server = json_pack(
|
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}",
|
"{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", 0,
|
"dl_info_speed", (json_int_t)total_rate,
|
||||||
"up_info_speed", 0,
|
"up_info_speed", 0,
|
||||||
"dl_info_data", 0,
|
"dl_info_data", (json_int_t)total_data,
|
||||||
"up_info_data", 0,
|
"up_info_data", 0,
|
||||||
"dl_rate_limit", 0,
|
"dl_rate_limit", 0,
|
||||||
"up_rate_limit", 0,
|
"up_rate_limit", 0,
|
||||||
|
|
@ -555,6 +724,43 @@ static json_t *snapshot_json(void) {
|
||||||
"server", server, "torrents", items);
|
"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) {
|
static bool parse_id(const char *text, uint64_t *id) {
|
||||||
if (!text || !*text) return false;
|
if (!text || !*text) return false;
|
||||||
char *end = NULL;
|
char *end = NULL;
|
||||||
|
|
@ -571,7 +777,7 @@ static json_t *full_torrent_by_hash(const char *hash) {
|
||||||
json_t *torrent = rpc_call_json("torrent", params);
|
json_t *torrent = rpc_call_json("torrent", params);
|
||||||
json_decref(params);
|
json_decref(params);
|
||||||
if (!torrent) return NULL;
|
if (!torrent) return NULL;
|
||||||
json_t *mapped = map_torrent(torrent, true);
|
json_t *mapped = map_torrent(torrent, true, speed_peek(id));
|
||||||
json_decref(torrent);
|
json_decref(torrent);
|
||||||
return mapped;
|
return mapped;
|
||||||
}
|
}
|
||||||
|
|
@ -714,6 +920,8 @@ static void api_add(int fd, const char *body, size_t len) {
|
||||||
http_json(fd, 200, reply);
|
http_json(fd, 200, reply);
|
||||||
json_decref(reply);
|
json_decref(reply);
|
||||||
json_decref(result);
|
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) {
|
static void api_delete(int fd, const char *body, size_t len) {
|
||||||
|
|
@ -739,12 +947,23 @@ static void api_delete(int fd, const char *body, size_t len) {
|
||||||
json_t *reply = json_pack("{s:b,s:i}", "ok", 1, "removed", (int)removed);
|
json_t *reply = json_pack("{s:b,s:i}", "ok", 1, "removed", (int)removed);
|
||||||
http_json(fd, 200, reply);
|
http_json(fd, 200, reply);
|
||||||
json_decref(reply);
|
json_decref(reply);
|
||||||
|
if (removed) publish_snapshot();
|
||||||
}
|
}
|
||||||
|
|
||||||
static void api_noop(int fd) {
|
/* The engine has no pause/resume/recheck/queue/category/limit verbs yet, so
|
||||||
json_t *reply = json_pack("{s:b,s:i}", "ok", 1, "affected", 0);
|
* rather than claim success we tell the UI the action is unsupported. The
|
||||||
http_json(fd, 200, reply);
|
* front end surfaces a non-2xx as an honest "Action failed" toast. */
|
||||||
json_decref(reply);
|
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) {
|
static void api_stream(int fd) {
|
||||||
|
|
@ -753,18 +972,45 @@ static void api_stream(int fd) {
|
||||||
"Cache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n"
|
"Cache-Control: no-cache\r\nConnection: keep-alive\r\n\r\n"
|
||||||
"retry: 2000\n\n";
|
"retry: 2000\n\n";
|
||||||
if (!send_all_fd(fd, head, strlen(head))) return;
|
if (!send_all_fd(fd, head, strlen(head))) return;
|
||||||
|
uint64_t seen = 0;
|
||||||
while (!atomic_load(&g_webui.stopping)) {
|
while (!atomic_load(&g_webui.stopping)) {
|
||||||
json_t *snapshot = snapshot_json();
|
char *payload = NULL;
|
||||||
char *text = json_dumps(snapshot, JSON_COMPACT | JSON_ENCODE_ANY);
|
pthread_mutex_lock(&g_webui.snap_lock);
|
||||||
json_decref(snapshot);
|
while (!atomic_load(&g_webui.stopping) && g_webui.snap_seq == seen) {
|
||||||
if (!text) break;
|
struct timespec ts;
|
||||||
bool ok = send_all_fd(fd, "event: snapshot\ndata: ", 22) &&
|
clock_gettime(CLOCK_REALTIME, &ts);
|
||||||
send_all_fd(fd, text, strlen(text)) &&
|
ts.tv_nsec += 250 * 1000 * 1000;
|
||||||
send_all_fd(fd, "\n\n", 2);
|
if (ts.tv_nsec >= 1000000000) { ts.tv_sec++; ts.tv_nsec -= 1000000000; }
|
||||||
free(text);
|
pthread_cond_timedwait(&g_webui.snap_cond, &g_webui.snap_lock, &ts);
|
||||||
if (!ok) break;
|
|
||||||
sleep(1);
|
|
||||||
}
|
}
|
||||||
|
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,
|
static void handle_api(int fd, const char *method, char *path,
|
||||||
|
|
@ -785,8 +1031,10 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
const char *user = json_string_value(json_object_get(req, "username"));
|
const char *user = json_string_value(json_object_get(req, "username"));
|
||||||
const char *password =
|
const char *password =
|
||||||
json_string_value(json_object_get(req, "password"));
|
json_string_value(json_object_get(req, "password"));
|
||||||
if (!user || !password || strcmp(user, g_webui.auth_user) != 0 ||
|
bool user_ok = user && constant_time_equal(user, g_webui.auth_user);
|
||||||
strcmp(password, g_webui.auth_password) != 0) {
|
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,
|
json_t *json = json_pack("{s:b,s:s}", "ok", 0,
|
||||||
"error", "invalid credentials");
|
"error", "invalid credentials");
|
||||||
http_json(fd, 401, json);
|
http_json(fd, 401, json);
|
||||||
|
|
@ -827,9 +1075,7 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
} else if (strcmp(path, "/api/stream") == 0 && strcmp(method, "GET") == 0) {
|
} else if (strcmp(path, "/api/stream") == 0 && strcmp(method, "GET") == 0) {
|
||||||
api_stream(fd);
|
api_stream(fd);
|
||||||
} else if (strcmp(path, "/api/snapshot") == 0 && strcmp(method, "GET") == 0) {
|
} else if (strcmp(path, "/api/snapshot") == 0 && strcmp(method, "GET") == 0) {
|
||||||
json_t *json = snapshot_json();
|
serve_cached_snapshot(fd);
|
||||||
http_json(fd, 200, json);
|
|
||||||
json_decref(json);
|
|
||||||
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
|
} else if (strcmp(path, "/api/meta") == 0 && strcmp(method, "GET") == 0) {
|
||||||
api_meta(fd);
|
api_meta(fd);
|
||||||
} else if (strcmp(path, "/api/preferences") == 0) {
|
} else if (strcmp(path, "/api/preferences") == 0) {
|
||||||
|
|
@ -851,11 +1097,7 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
http_json(fd, 200, json);
|
http_json(fd, 200, json);
|
||||||
json_decref(json);
|
json_decref(json);
|
||||||
} else if (strcmp(path, "/api/torrents") == 0 && strcmp(method, "GET") == 0) {
|
} else if (strcmp(path, "/api/torrents") == 0 && strcmp(method, "GET") == 0) {
|
||||||
json_t *snapshot = snapshot_json();
|
serve_cached_torrents(fd);
|
||||||
json_t *torrents = json_incref(json_object_get(snapshot, "torrents"));
|
|
||||||
http_json(fd, 200, torrents);
|
|
||||||
json_decref(torrents);
|
|
||||||
json_decref(snapshot);
|
|
||||||
} else if (path_after(path, "/api/torrents/") && strcmp(method, "GET") == 0) {
|
} else if (path_after(path, "/api/torrents/") && strcmp(method, "GET") == 0) {
|
||||||
api_torrent_detail(fd, path_after(path, "/api/torrents/"));
|
api_torrent_detail(fd, path_after(path, "/api/torrents/"));
|
||||||
} else if (strcmp(path, "/api/add") == 0 && strcmp(method, "POST") == 0) {
|
} else if (strcmp(path, "/api/add") == 0 && strcmp(method, "POST") == 0) {
|
||||||
|
|
@ -863,7 +1105,7 @@ static void handle_api(int fd, const char *method, char *path,
|
||||||
} else if (strcmp(path, "/api/delete") == 0 && strcmp(method, "POST") == 0) {
|
} else if (strcmp(path, "/api/delete") == 0 && strcmp(method, "POST") == 0) {
|
||||||
api_delete(fd, body, body_len);
|
api_delete(fd, body, body_len);
|
||||||
} else if (strcmp(path, "/api/action") == 0 && strcmp(method, "POST") == 0) {
|
} else if (strcmp(path, "/api/action") == 0 && strcmp(method, "POST") == 0) {
|
||||||
api_noop(fd);
|
api_action(fd, body, body_len);
|
||||||
} else if (strncmp(path, "/api/rss", 8) == 0 && strcmp(method, "GET") == 0) {
|
} else if (strncmp(path, "/api/rss", 8) == 0 && strcmp(method, "GET") == 0) {
|
||||||
json_t *json = json_array();
|
json_t *json = json_array();
|
||||||
http_json(fd, 200, json);
|
http_json(fd, 200, json);
|
||||||
|
|
@ -914,7 +1156,14 @@ static void handle_conn(int fd) {
|
||||||
size_t content_length = 0;
|
size_t content_length = 0;
|
||||||
char *cl = strcasestr(request, "content-length:");
|
char *cl = strcasestr(request, "content-length:");
|
||||||
if (cl && cl < hdrend) content_length = strtoull(cl + 15, NULL, 10);
|
if (cl && cl < hdrend) content_length = strtoull(cl + 15, NULL, 10);
|
||||||
if (content_length > READ_LIMIT - header_len) content_length = READ_LIMIT - header_len;
|
/* 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) {
|
while (len - header_len < content_length && len < READ_LIMIT) {
|
||||||
ssize_t n = recv(fd, request + len, READ_LIMIT - len, 0);
|
ssize_t n = recv(fd, request + len, READ_LIMIT - len, 0);
|
||||||
if (n < 0) {
|
if (n < 0) {
|
||||||
|
|
@ -962,15 +1211,26 @@ static void *server_thread(void *arg) {
|
||||||
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
|
struct timeval timeout = { .tv_sec = 5, .tv_usec = 0 };
|
||||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
|
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof timeout);
|
||||||
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout);
|
setsockopt(fd, SOL_SOCKET, SO_SNDTIMEO, &timeout, sizeof timeout);
|
||||||
conn_arg *conn = malloc(sizeof(*conn));
|
|
||||||
if (!conn) {
|
/* 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);
|
close(fd);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn_arg *conn = malloc(sizeof(*conn));
|
||||||
|
if (!conn) {
|
||||||
|
close(fd);
|
||||||
|
finish_connection();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
conn->fd = fd;
|
conn->fd = fd;
|
||||||
pthread_mutex_lock(&g_webui.conn_lock);
|
|
||||||
g_webui.active_connections++;
|
|
||||||
pthread_mutex_unlock(&g_webui.conn_lock);
|
|
||||||
pthread_t thread;
|
pthread_t thread;
|
||||||
if (pthread_create(&thread, NULL, conn_thread, conn) != 0) {
|
if (pthread_create(&thread, NULL, conn_thread, conn) != 0) {
|
||||||
close(fd);
|
close(fd);
|
||||||
|
|
@ -1040,7 +1300,19 @@ static naut_err start_server(void) {
|
||||||
return NAUT_ERR_IO;
|
return NAUT_ERR_IO;
|
||||||
}
|
}
|
||||||
g_webui.listener = fd;
|
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) {
|
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);
|
close(fd);
|
||||||
g_webui.listener = -1;
|
g_webui.listener = -1;
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
|
|
@ -1050,12 +1322,18 @@ static naut_err start_server(void) {
|
||||||
snprintf(msg, sizeof msg, "webui: serving http://%s:%d from %s",
|
snprintf(msg, sizeof msg, "webui: serving http://%s:%d from %s",
|
||||||
g_webui.host_name, g_webui.port, g_webui.root);
|
g_webui.host_name, g_webui.port, g_webui.root);
|
||||||
log_msg(2, msg);
|
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);
|
snprintf(msg, sizeof msg, "webui: auth user %s", g_webui.auth_user);
|
||||||
log_msg(2, msg);
|
log_msg(2, msg);
|
||||||
if (g_webui.generated_password) {
|
if (g_webui.generated_password && g_webui.auth_password[0]) {
|
||||||
snprintf(msg, sizeof msg, "webui: generated password %s",
|
snprintf(msg, sizeof msg, "webui: generated password %s",
|
||||||
g_webui.auth_password);
|
g_webui.auth_password);
|
||||||
log_msg(1, msg);
|
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;
|
return NAUT_OK;
|
||||||
}
|
}
|
||||||
|
|
@ -1064,40 +1342,51 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
||||||
if (!host || host->abi_version != NAUT_PLUGIN_ABI_VERSION ||
|
if (!host || host->abi_version != NAUT_PLUGIN_ABI_VERSION ||
|
||||||
host->struct_size < sizeof(*host) || !host->call_rpc)
|
host->struct_size < sizeof(*host) || !host->call_rpc)
|
||||||
return NAUT_ERR_INVAL;
|
return NAUT_ERR_INVAL;
|
||||||
|
naut_err error = NAUT_ERR_NOMEM;
|
||||||
memset(&g_webui, 0, sizeof g_webui);
|
memset(&g_webui, 0, sizeof g_webui);
|
||||||
g_webui.listener = -1;
|
g_webui.listener = -1;
|
||||||
g_webui.host = *host;
|
g_webui.host = *host;
|
||||||
if (pthread_mutex_init(&g_webui.auth_lock, NULL) != 0)
|
if (pthread_mutex_init(&g_webui.auth_lock, NULL) != 0)
|
||||||
return NAUT_ERR_NOMEM;
|
return NAUT_ERR_NOMEM;
|
||||||
if (pthread_mutex_init(&g_webui.conn_lock, NULL) != 0) {
|
if (pthread_mutex_init(&g_webui.conn_lock, NULL) != 0)
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
goto fail_conn_lock;
|
||||||
return NAUT_ERR_NOMEM;
|
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0)
|
||||||
}
|
goto fail_conn_cond;
|
||||||
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0) {
|
if (pthread_mutex_init(&g_webui.snap_lock, NULL) != 0)
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
goto fail_snap_lock;
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
if (pthread_cond_init(&g_webui.snap_cond, NULL) != 0)
|
||||||
return NAUT_ERR_NOMEM;
|
goto fail_snap_cond;
|
||||||
}
|
if (pthread_mutex_init(&g_webui.speed_lock, NULL) != 0)
|
||||||
|
goto fail_speed_lock;
|
||||||
init_auth();
|
init_auth();
|
||||||
naut_err error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
||||||
"webui");
|
"webui");
|
||||||
if (error != NAUT_OK) {
|
if (error != NAUT_OK) goto fail_named;
|
||||||
pthread_cond_destroy(&g_webui.conn_cond);
|
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
error = start_server();
|
error = start_server();
|
||||||
if (error != NAUT_OK) {
|
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);
|
pthread_cond_destroy(&g_webui.conn_cond);
|
||||||
|
fail_conn_cond:
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
pthread_mutex_destroy(&g_webui.conn_lock);
|
||||||
|
fail_conn_lock:
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
pthread_mutex_destroy(&g_webui.auth_lock);
|
||||||
}
|
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
naut_err naut_plugin_shutdown(void) {
|
naut_err naut_plugin_shutdown(void) {
|
||||||
atomic_store(&g_webui.stopping, true);
|
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) {
|
if (g_webui.listener >= 0) {
|
||||||
shutdown(g_webui.listener, SHUT_RDWR);
|
shutdown(g_webui.listener, SHUT_RDWR);
|
||||||
close(g_webui.listener);
|
close(g_webui.listener);
|
||||||
|
|
@ -1106,10 +1395,22 @@ naut_err naut_plugin_shutdown(void) {
|
||||||
if (g_webui.thread_started)
|
if (g_webui.thread_started)
|
||||||
pthread_join(g_webui.thread, NULL);
|
pthread_join(g_webui.thread, NULL);
|
||||||
g_webui.thread_started = false;
|
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);
|
pthread_mutex_lock(&g_webui.conn_lock);
|
||||||
while (g_webui.active_connections > 0)
|
while (g_webui.active_connections > 0)
|
||||||
pthread_cond_wait(&g_webui.conn_cond, &g_webui.conn_lock);
|
pthread_cond_wait(&g_webui.conn_cond, &g_webui.conn_lock);
|
||||||
pthread_mutex_unlock(&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_cond_destroy(&g_webui.conn_cond);
|
||||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
pthread_mutex_destroy(&g_webui.conn_lock);
|
||||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
pthread_mutex_destroy(&g_webui.auth_lock);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue