webui: real multi-user accounts (SQLite + PBKDF2)
Replace the single env/generated password with a proper account system, owned entirely by the webui plugin: - auth_store: SQLite users table, PBKDF2-HMAC-SHA256 password hashing (per-user salt, 210k iterations) via OpenSSL. DB at NAUT_WEBUI_DB or an XDG default. Thread-safe (serialized connection). - Login verifies against the DB; sessions now carry the username + role. First run bootstraps an admin from NAUT_AUTH_USER/PASSWORD or a generated password (logged once). - Admin-only user management: GET/POST /api/users, /api/users/delete, /api/users/password, /api/users/role. Self-service POST /api/account/password. Guards the last admin and invalidates a user's sessions on delete or password reset. - /api/auth/status and /api/login now return the role. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ab733cb573
commit
a4ec585aed
4 changed files with 604 additions and 51 deletions
|
|
@ -1,5 +1,6 @@
|
|||
#include "naut/naut_plugin.h"
|
||||
#include "naut/http_client.h"
|
||||
#include "auth_store.h"
|
||||
|
||||
#include <jansson.h>
|
||||
|
||||
|
|
@ -36,6 +37,8 @@
|
|||
|
||||
typedef struct {
|
||||
char token[96];
|
||||
char user[64];
|
||||
char role[16];
|
||||
time_t expires;
|
||||
bool used;
|
||||
} webui_session;
|
||||
|
|
@ -54,8 +57,9 @@ typedef struct {
|
|||
naut_host_api host;
|
||||
char root[PATH_MAX];
|
||||
char host_name[64];
|
||||
char auth_user[64];
|
||||
char auth_password[64];
|
||||
char auth_user[64]; /* bootstrap admin name (for startup banner) */
|
||||
char auth_password[64]; /* generated bootstrap password (banner only) */
|
||||
auth_store *auth; /* SQLite-backed account store */
|
||||
int port;
|
||||
int listener;
|
||||
bool generated_password;
|
||||
|
|
@ -240,21 +244,6 @@ static bool query_get(const char *query, const char *key, char *out, size_t outs
|
|||
return false;
|
||||
}
|
||||
|
||||
/* 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). */
|
||||
|
|
@ -280,26 +269,71 @@ static bool random_hex(char *out, size_t out_size, size_t bytes) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/* mkdir -p for the account DB's parent directory (0700). */
|
||||
static int mkdir_p(const char *path, mode_t mode) {
|
||||
char tmp[PATH_MAX];
|
||||
size_t len = snprintf(tmp, sizeof tmp, "%s", path);
|
||||
if (len == 0 || len >= sizeof tmp) return -1;
|
||||
for (char *p = tmp + 1; *p; p++) {
|
||||
if (*p == '/') {
|
||||
*p = 0;
|
||||
if (mkdir(tmp, mode) != 0 && errno != EEXIST) return -1;
|
||||
*p = '/';
|
||||
}
|
||||
}
|
||||
if (mkdir(tmp, mode) != 0 && errno != EEXIST) return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Resolve the account database path: NAUT_WEBUI_DB, else an XDG/HOME default
|
||||
* under naut/. Creates the parent directory. */
|
||||
static bool resolve_auth_db_path(char *out, size_t n) {
|
||||
const char *env = getenv("NAUT_WEBUI_DB");
|
||||
if (env && *env) return (size_t)snprintf(out, n, "%s", env) < n;
|
||||
const char *xdg = getenv("XDG_DATA_HOME");
|
||||
const char *home = getenv("HOME");
|
||||
char dir[PATH_MAX];
|
||||
if (xdg && *xdg) snprintf(dir, sizeof dir, "%s/naut", xdg);
|
||||
else if (home && *home) snprintf(dir, sizeof dir, "%s/.local/share/naut", home);
|
||||
else return false;
|
||||
if (mkdir_p(dir, 0700) != 0) return false;
|
||||
return (size_t)snprintf(out, n, "%s/webui_accounts.db", dir) < n;
|
||||
}
|
||||
|
||||
/* Open the account store and, on first run (no accounts), bootstrap an admin
|
||||
* from NAUT_AUTH_USER/PASSWORD or a generated password (logged once). */
|
||||
static void init_auth(void) {
|
||||
char db_path[PATH_MAX];
|
||||
if (!resolve_auth_db_path(db_path, sizeof db_path)) {
|
||||
log_msg(0, "webui: cannot resolve account DB path; set NAUT_WEBUI_DB");
|
||||
return;
|
||||
}
|
||||
g_webui.auth = auth_store_open(db_path);
|
||||
if (!g_webui.auth) {
|
||||
log_msg(0, "webui: failed to open account database");
|
||||
return;
|
||||
}
|
||||
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);
|
||||
|
||||
if (auth_store_user_count(g_webui.auth) > 0) return; /* already set up */
|
||||
|
||||
/* No accounts yet — create the initial admin. */
|
||||
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;
|
||||
} else if (random_hex(g_webui.auth_password, sizeof g_webui.auth_password, 9)) {
|
||||
password = g_webui.auth_password;
|
||||
g_webui.generated_password = true;
|
||||
} else {
|
||||
log_msg(0, "webui: no CSPRNG; set NAUT_AUTH_PASSWORD to create the admin");
|
||||
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;
|
||||
if (!auth_store_create_user(g_webui.auth, user, password, "admin"))
|
||||
log_msg(0, "webui: failed to create the initial admin account");
|
||||
}
|
||||
|
||||
static const char *header_value(const char *headers, const char *end,
|
||||
|
|
@ -346,7 +380,12 @@ static bool cookie_token(const char *headers, const char *end,
|
|||
return false;
|
||||
}
|
||||
|
||||
static bool current_user(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. */
|
||||
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;
|
||||
|
|
@ -360,6 +399,8 @@ static bool current_user(const char *headers, const char *end) {
|
|||
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;
|
||||
}
|
||||
|
|
@ -367,7 +408,8 @@ static bool current_user(const char *headers, const char *end) {
|
|||
return ok;
|
||||
}
|
||||
|
||||
static bool create_session(char *out, size_t out_size) {
|
||||
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);
|
||||
|
|
@ -382,6 +424,8 @@ static bool create_session(char *out, size_t out_size) {
|
|||
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);
|
||||
|
|
@ -389,6 +433,17 @@ static bool create_session(char *out, size_t out_size) {
|
|||
return true;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
static void clear_session(const char *headers, const char *end) {
|
||||
char token[96];
|
||||
if (!cookie_token(headers, end, token, sizeof token)) return;
|
||||
|
|
@ -2665,6 +2720,156 @@ static void serve_cached_torrents(int fd) {
|
|||
free(copy);
|
||||
}
|
||||
|
||||
/* ============================ account management ========================== *
|
||||
* Admin-only user CRUD plus a self-service password change. The web layer owns
|
||||
* everything via auth_store; the daemon is not involved. */
|
||||
|
||||
static bool valid_username(const char *u) {
|
||||
if (!u || !*u || strlen(u) >= 64) return false;
|
||||
for (const char *p = u; *p; p++)
|
||||
if (!((*p >= 'a' && *p <= 'z') || (*p >= 'A' && *p <= 'Z') ||
|
||||
(*p >= '0' && *p <= '9') || *p == '_' || *p == '-' || *p == '.'))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/* GET /api/users → [{username, role, createdAt}] (admin only). */
|
||||
static void api_users_list(int fd) {
|
||||
json_t *users = json_array();
|
||||
if (g_webui.auth) auth_store_list_users(g_webui.auth, users);
|
||||
http_json(fd, 200, users);
|
||||
json_decref(users);
|
||||
}
|
||||
|
||||
/* POST /api/users {username, password, role} (admin only). */
|
||||
static void api_user_create(int fd, const char *body, size_t len) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *user = json_string_value(json_object_get(req, "username"));
|
||||
const char *pass = json_string_value(json_object_get(req, "password"));
|
||||
const char *role = json_string_value(json_object_get(req, "role"));
|
||||
if (!valid_username(user) || !pass || !*pass) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request",
|
||||
"username (letters/digits/._-) and password are required");
|
||||
return;
|
||||
}
|
||||
bool ok = g_webui.auth &&
|
||||
auth_store_create_user(g_webui.auth, user, pass,
|
||||
role && *role ? role : "user");
|
||||
json_decref(req);
|
||||
if (!ok) { http_text(fd, 409, "Conflict", "user already exists"); return; }
|
||||
api_users_list(fd);
|
||||
}
|
||||
|
||||
/* POST /api/users/delete {username} (admin only). Refuses to remove the last
|
||||
* admin so the instance can't lock everyone out. */
|
||||
static void api_user_delete(int fd, const char *body, size_t len,
|
||||
const char *actor) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *uname = json_string_value(json_object_get(req, "username"));
|
||||
if (!uname || !*uname) { json_decref(req); http_text(fd, 400, "Bad Request", "username required"); return; }
|
||||
char user[64];
|
||||
snprintf(user, sizeof user, "%s", uname); /* own it before decref */
|
||||
char role[16] = {0};
|
||||
/* Look up the target's role to guard the last-admin rule. */
|
||||
json_t *list = json_array();
|
||||
if (g_webui.auth) auth_store_list_users(g_webui.auth, list);
|
||||
size_t i; json_t *u;
|
||||
json_array_foreach(list, i, u)
|
||||
if (strcasecmp(json_string_or(u, "username", ""), user) == 0)
|
||||
snprintf(role, sizeof role, "%s", json_string_or(u, "role", ""));
|
||||
json_decref(list);
|
||||
if (strcmp(role, "admin") == 0 && auth_store_admin_count(g_webui.auth) <= 1) {
|
||||
json_decref(req);
|
||||
http_text(fd, 409, "Conflict", "cannot delete the last admin");
|
||||
return;
|
||||
}
|
||||
bool ok = g_webui.auth && auth_store_delete_user(g_webui.auth, user);
|
||||
json_decref(req);
|
||||
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
|
||||
drop_user_sessions(user);
|
||||
(void)actor;
|
||||
api_users_list(fd);
|
||||
}
|
||||
|
||||
/* POST /api/users/password {username, password} — admin reset. */
|
||||
static void api_user_set_password(int fd, const char *body, size_t len) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *uname = json_string_value(json_object_get(req, "username"));
|
||||
const char *pass = json_string_value(json_object_get(req, "password"));
|
||||
if (!uname || !*uname || !pass || !*pass) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request", "username and password required");
|
||||
return;
|
||||
}
|
||||
char user[64];
|
||||
snprintf(user, sizeof user, "%s", uname);
|
||||
bool ok = g_webui.auth && auth_store_set_password(g_webui.auth, user, pass);
|
||||
json_decref(req);
|
||||
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
|
||||
drop_user_sessions(user); /* force re-login with the new password */
|
||||
json_t *reply = json_pack("{s:b}", "ok", 1);
|
||||
http_json(fd, 200, reply);
|
||||
json_decref(reply);
|
||||
}
|
||||
|
||||
/* POST /api/users/role {username, role} — admin; keeps at least one admin. */
|
||||
static void api_user_set_role(int fd, const char *body, size_t len) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *user = json_string_value(json_object_get(req, "username"));
|
||||
const char *role = json_string_value(json_object_get(req, "role"));
|
||||
if (!user || !*user || (strcmp(role ? role : "", "admin") && strcmp(role ? role : "", "user"))) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request", "username and role (admin|user) required");
|
||||
return;
|
||||
}
|
||||
if (strcmp(role, "user") == 0 && auth_store_admin_count(g_webui.auth) <= 1) {
|
||||
/* Only block if the target is currently the sole admin. */
|
||||
char cur[16] = {0};
|
||||
json_t *list = json_array();
|
||||
if (g_webui.auth) auth_store_list_users(g_webui.auth, list);
|
||||
size_t i; json_t *u;
|
||||
json_array_foreach(list, i, u)
|
||||
if (strcasecmp(json_string_or(u, "username", ""), user) == 0)
|
||||
snprintf(cur, sizeof cur, "%s", json_string_or(u, "role", ""));
|
||||
json_decref(list);
|
||||
if (strcmp(cur, "admin") == 0) {
|
||||
json_decref(req);
|
||||
http_text(fd, 409, "Conflict", "cannot demote the last admin");
|
||||
return;
|
||||
}
|
||||
}
|
||||
bool ok = g_webui.auth && auth_store_set_role(g_webui.auth, user, role);
|
||||
json_decref(req);
|
||||
if (!ok) { http_text(fd, 404, "Not Found", "no such user"); return; }
|
||||
api_users_list(fd);
|
||||
}
|
||||
|
||||
/* POST /api/account/password {oldPassword, newPassword} — change own password. */
|
||||
static void api_account_password(int fd, const char *body, size_t len,
|
||||
const char *actor) {
|
||||
json_t *req = read_body_json(body, len);
|
||||
const char *oldp = json_string_value(json_object_get(req, "oldPassword"));
|
||||
const char *newp = json_string_value(json_object_get(req, "newPassword"));
|
||||
if (!oldp || !newp || !*newp) {
|
||||
json_decref(req);
|
||||
http_text(fd, 400, "Bad Request", "oldPassword and newPassword required");
|
||||
return;
|
||||
}
|
||||
char role[16] = {0};
|
||||
if (!g_webui.auth || !auth_store_verify(g_webui.auth, actor, oldp, role, sizeof role)) {
|
||||
json_decref(req);
|
||||
http_text(fd, 403, "Forbidden", "current password is incorrect");
|
||||
return;
|
||||
}
|
||||
bool ok = auth_store_set_password(g_webui.auth, actor, newp);
|
||||
json_decref(req);
|
||||
if (!ok) { http_text(fd, 500, "Internal Server Error", "could not update password"); return; }
|
||||
json_t *reply = json_pack("{s:b}", "ok", 1);
|
||||
http_json(fd, 200, reply);
|
||||
json_decref(reply);
|
||||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -2673,13 +2878,15 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
const char *qmark = strchr(path, '?');
|
||||
if (qmark) snprintf(query_str, sizeof query_str, "%s", qmark + 1);
|
||||
strip_query(path);
|
||||
char cur_user[64] = {0}, cur_role[16] = {0};
|
||||
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);
|
||||
bool authed = current_identity(headers, headers_end, cur_user,
|
||||
sizeof cur_user, cur_role, sizeof cur_role);
|
||||
json_t *json = json_pack("{s:b,s:s,s:s,s:b}",
|
||||
"authenticated", authed,
|
||||
"user", authed ? cur_user : "",
|
||||
"role", authed ? cur_role : "",
|
||||
"generatedPassword", g_webui.generated_password);
|
||||
http_json(fd, 200, json);
|
||||
json_decref(json);
|
||||
} else if (strcmp(path, "/api/login") == 0 && strcmp(method, "POST") == 0) {
|
||||
|
|
@ -2687,10 +2894,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 *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) {
|
||||
char role[16] = {0};
|
||||
bool ok = g_webui.auth && user && password &&
|
||||
auth_store_verify(g_webui.auth, user, password, role, sizeof role);
|
||||
if (!ok) {
|
||||
json_t *json = json_pack("{s:b,s:s}", "ok", 0,
|
||||
"error", "invalid credentials");
|
||||
http_json(fd, 401, json);
|
||||
|
|
@ -2699,7 +2906,7 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
return;
|
||||
}
|
||||
char token[96];
|
||||
if (!create_session(token, sizeof token)) {
|
||||
if (!create_session(user, role, token, sizeof token)) {
|
||||
json_decref(req);
|
||||
http_text(fd, 500, "Internal Server Error", "session failed");
|
||||
return;
|
||||
|
|
@ -2709,8 +2916,8 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
"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);
|
||||
json_t *json = json_pack("{s:b,s:s,s:s}", "ok", 1,
|
||||
"user", user, "role", role);
|
||||
http_json_extra(fd, 200, json, cookie);
|
||||
json_decref(json);
|
||||
json_decref(req);
|
||||
|
|
@ -2721,7 +2928,8 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
"Set-Cookie: naut_session=; Path=/; HttpOnly; "
|
||||
"SameSite=Lax; Max-Age=0\r\n");
|
||||
json_decref(json);
|
||||
} else if (!current_user(headers, headers_end)) {
|
||||
} else if (!current_identity(headers, headers_end, cur_user, sizeof cur_user,
|
||||
cur_role, sizeof cur_role)) {
|
||||
json_t *json = json_pack("{s:s}", "error",
|
||||
"authentication required");
|
||||
http_json(fd, 401, json);
|
||||
|
|
@ -2767,6 +2975,25 @@ static void handle_api(int fd, const char *method, char *path,
|
|||
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 (strcmp(path, "/api/account/password") == 0 && strcmp(method, "POST") == 0) {
|
||||
api_account_password(fd, body, body_len, cur_user);
|
||||
} else if (strncmp(path, "/api/users", 10) == 0) {
|
||||
/* All user-management endpoints are admin-only. */
|
||||
if (strcmp(cur_role, "admin") != 0) {
|
||||
http_text(fd, 403, "Forbidden", "admin privileges required");
|
||||
} else if (strcmp(path, "/api/users") == 0 && strcmp(method, "GET") == 0) {
|
||||
api_users_list(fd);
|
||||
} else if (strcmp(path, "/api/users") == 0 && strcmp(method, "POST") == 0) {
|
||||
api_user_create(fd, body, body_len);
|
||||
} else if (strcmp(path, "/api/users/delete") == 0 && strcmp(method, "POST") == 0) {
|
||||
api_user_delete(fd, body, body_len, cur_user);
|
||||
} else if (strcmp(path, "/api/users/password") == 0 && strcmp(method, "POST") == 0) {
|
||||
api_user_set_password(fd, body, body_len);
|
||||
} else if (strcmp(path, "/api/users/role") == 0 && strcmp(method, "POST") == 0) {
|
||||
api_user_set_role(fd, body, body_len);
|
||||
} else {
|
||||
http_text(fd, 404, "Not Found", "not found");
|
||||
}
|
||||
} else if (strcmp(path, "/api/rss") == 0 && strcmp(method, "GET") == 0) {
|
||||
api_rss_list(fd);
|
||||
} else if (strcmp(path, "/api/rss") == 0 && strcmp(method, "POST") == 0) {
|
||||
|
|
@ -3013,15 +3240,14 @@ static naut_err start_server(void) {
|
|||
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);
|
||||
if (!g_webui.auth) {
|
||||
log_msg(0, "webui: account store unavailable; logins will fail");
|
||||
} else if (g_webui.generated_password && g_webui.auth_password[0]) {
|
||||
/* First run: surface the generated admin credentials once. */
|
||||
snprintf(msg, sizeof msg,
|
||||
"webui: created initial admin '%s' with generated password %s",
|
||||
g_webui.auth_user, 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;
|
||||
}
|
||||
|
|
@ -3147,5 +3373,7 @@ naut_err naut_plugin_shutdown(void) {
|
|||
pthread_cond_destroy(&g_webui.conn_cond);
|
||||
pthread_mutex_destroy(&g_webui.conn_lock);
|
||||
pthread_mutex_destroy(&g_webui.auth_lock);
|
||||
auth_store_close(g_webui.auth);
|
||||
g_webui.auth = NULL;
|
||||
return NAUT_OK;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue