webui: persist login sessions so they survive restarts
Sessions lived in an in-memory array, so every daemon restart wiped them and forced a re-login. Move them into the webui DB: - New sessions table storing a SHA-256 of the bearer token (never the raw token, so a DB read can't be replayed), the user, role, and an absolute expiry. - create/lookup/touch/delete + per-user delete + prune in webui_store. - Login persists the session; auth checks validate against the DB with a throttled sliding expiry (re-extended at most hourly to avoid a write per request); logout and admin reset/delete drop the rows. Expired rows are reaped lazily on lookup and pruned at startup. - TTL is configurable via NAUT_SESSION_TTL (default 7 days) and drives the cookie Max-Age. Removes the in-memory session array + auth_lock. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
096535292d
commit
067b62c23a
3 changed files with 190 additions and 70 deletions
|
|
@ -29,20 +29,11 @@
|
|||
#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 SESSION_TTL_SECONDS (60 * 60 * 24 * 7) /* default; NAUT_SESSION_TTL */
|
||||
#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];
|
||||
char user[64];
|
||||
char role[16];
|
||||
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 {
|
||||
|
|
@ -70,8 +61,6 @@ typedef struct {
|
|||
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;
|
||||
|
|
@ -103,8 +92,6 @@ typedef struct {
|
|||
bool rss_thread_started;
|
||||
pthread_cond_t rss_cond; /* wakes the poller for an immediate refresh */
|
||||
bool rss_wake; /* set with rss_cond to force an early re-poll */
|
||||
|
||||
webui_session sessions[MAX_SESSIONS];
|
||||
} webui_state;
|
||||
|
||||
typedef struct {
|
||||
|
|
@ -381,52 +368,52 @@ static bool cookie_token(const char *headers, const char *end,
|
|||
/* Look up the session for this request. On a live session, refreshes its TTL
|
||||
* and (optionally) copies the account's username and role. Returns true if a
|
||||
* valid session was found. */
|
||||
/* Session lifetime in seconds (sliding). Override with NAUT_SESSION_TTL. */
|
||||
static long session_ttl(void) {
|
||||
const char *env = getenv("NAUT_SESSION_TTL");
|
||||
if (env && *env) {
|
||||
char *e = NULL;
|
||||
long v = strtol(env, &e, 10);
|
||||
if (e && e != env && !*e && v > 0) return v;
|
||||
}
|
||||
return SESSION_TTL_SECONDS;
|
||||
}
|
||||
|
||||
/* Don't rewrite the session row on every request; only re-extend the sliding
|
||||
* expiry once it has advanced by more than this. */
|
||||
#define SESSION_REFRESH_THRESHOLD 3600
|
||||
|
||||
static bool current_identity(const char *headers, const char *end,
|
||||
char *user, size_t user_sz,
|
||||
char *role, size_t role_sz) {
|
||||
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;
|
||||
if (user) snprintf(user, user_sz, "%s", session->user);
|
||||
if (role) snprintf(role, role_sz, "%s", session->role);
|
||||
ok = true;
|
||||
break;
|
||||
if (!g_webui.store) return false;
|
||||
char u[64] = {0}, r[16] = {0};
|
||||
long expires = 0;
|
||||
if (!webui_store_session_lookup(g_webui.store, token, u, sizeof u,
|
||||
r, sizeof r, &expires))
|
||||
return false;
|
||||
long now = (long)time(NULL);
|
||||
if (expires <= now) { /* expired: clean it up */
|
||||
webui_store_session_delete(g_webui.store, token);
|
||||
return false;
|
||||
}
|
||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
||||
return ok;
|
||||
long fresh = now + session_ttl(); /* sliding window, throttled */
|
||||
if (fresh - expires > SESSION_REFRESH_THRESHOLD)
|
||||
webui_store_session_touch(g_webui.store, token, fresh);
|
||||
if (user) snprintf(user, user_sz, "%s", u);
|
||||
if (role) snprintf(role, role_sz, "%s", r);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool create_session(const char *user, const char *role,
|
||||
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);
|
||||
snprintf(slot->user, sizeof slot->user, "%s", user ? user : "");
|
||||
snprintf(slot->role, sizeof slot->role, "%s", role ? role : "user");
|
||||
slot->expires = expires;
|
||||
slot->used = true;
|
||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
||||
if (!g_webui.store || !random_hex(token, sizeof token, 24)) return false;
|
||||
long expires = (long)time(NULL) + session_ttl();
|
||||
if (!webui_store_session_create(g_webui.store, token, user, role, expires))
|
||||
return false;
|
||||
snprintf(out, out_size, "%s", token);
|
||||
return true;
|
||||
}
|
||||
|
|
@ -434,23 +421,13 @@ static bool create_session(const char *user, const char *role,
|
|||
/* Invalidate every session belonging to `user` (after delete / password reset
|
||||
* by an admin). */
|
||||
static void drop_user_sessions(const char *user) {
|
||||
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].user, user) == 0)
|
||||
g_webui.sessions[i].used = false;
|
||||
pthread_mutex_unlock(&g_webui.auth_lock);
|
||||
if (g_webui.store) webui_store_sessions_delete_user(g_webui.store, user);
|
||||
}
|
||||
|
||||
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);
|
||||
if (g_webui.store && cookie_token(headers, end, token, sizeof token))
|
||||
webui_store_session_delete(g_webui.store, token);
|
||||
}
|
||||
|
||||
static bool bad_static_path(const char *path) {
|
||||
|
|
@ -2764,8 +2741,8 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
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);
|
||||
"Max-Age=%ld\r\n",
|
||||
SESSION_COOKIE, token, session_ttl());
|
||||
json_t *json = json_pack("{s:b,s:s,s:s}", "ok", 1,
|
||||
"user", user, "role", role);
|
||||
http_json_extra(fd, 200, json, cookie);
|
||||
|
|
@ -3110,10 +3087,8 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
|||
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;
|
||||
return NAUT_ERR_NOMEM;
|
||||
if (pthread_cond_init(&g_webui.conn_cond, NULL) != 0)
|
||||
goto fail_conn_cond;
|
||||
if (pthread_mutex_init(&g_webui.snap_lock, NULL) != 0)
|
||||
|
|
@ -3132,6 +3107,8 @@ naut_err naut_plugin_register(const naut_host_api *host) {
|
|||
pthread_mutex_init(&g_webui.rss_lock, NULL);
|
||||
pthread_cond_init(&g_webui.rss_cond, NULL);
|
||||
init_auth();
|
||||
if (g_webui.store) /* drop sessions that expired while we were down */
|
||||
webui_store_sessions_prune(g_webui.store, (long)time(NULL));
|
||||
error = g_webui.host.set_plugin_name(g_webui.host.host_context,
|
||||
"webui");
|
||||
if (error != NAUT_OK) goto fail_store;
|
||||
|
|
@ -3158,8 +3135,6 @@ 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;
|
||||
}
|
||||
|
||||
|
|
@ -3212,7 +3187,6 @@ naut_err naut_plugin_shutdown(void) {
|
|||
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);
|
||||
webui_store_close(g_webui.store);
|
||||
g_webui.store = NULL;
|
||||
return NAUT_OK;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue