/* * nanobbs, a tiny forums software. * Copyright (C) 2025-2026 Xavier Del Campo Romero * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ #define _POSIX_C_SOURCE 200809L #include "auth.h" #include "db.h" #include "defs.h" #include "endpoints.h" #include "jwt.h" #include "op.h" #include #include #include #include #include #include #include #include struct validate { bool valid; unsigned long id; enum auth_role role; sqlite3 *db; auth_fn fn; void *user; }; static void free_validate(void *const p) { int error; struct validate *const v = p; if (!v) return; else if ((error = sqlite3_close(v->db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(v); } static int check_role(const char *const s, enum auth_role *const out) { static const struct r { const char *s; enum auth_role role; } roles[] = { {"admin", AUTH_ROLE_ADMIN}, {"mod", AUTH_ROLE_MOD}, {"user", AUTH_ROLE_USER}, {"banned", AUTH_ROLE_BANNED} }; for (size_t i = 0; i < sizeof roles / sizeof *roles; i++) { const struct r *const r = &roles[i]; if (!strcmp(s, r->s)) { *out = r->role; return 0; } } fprintf(stderr, "%s: invalid role: %s\n", __func__, s); return -1; } static int end(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; struct validate *const v = args; if (!v->valid) ret = v->fn(p, r, v->user, v->db, NULL); else { const struct http_cookie *const c = &p->cookie; const char *const username = c->field; const struct auth_user u = { .username = username, .role = v->role, .id = v->id }; ret = v->fn(p, r, v->user, v->db, &u); } free(v); return ret; } static int signkey(const struct http_cookie *const c, const char *const key, struct validate *const v) { int ret = -1; cJSON *j = NULL; unsigned char dkey[crypto_auth_hmacsha256_KEYBYTES]; const size_t len = strlen(key), explen = sizeof dkey * 2; const cJSON *u; const char *user; if (len != explen) { fprintf(stderr, "%s: key size mismatch, got %zu, expected %zu\n", __func__, len, explen); goto end; } else if (sodium_hex2bin(dkey, sizeof dkey, key, len, NULL, NULL, NULL)) { fprintf(stderr, "%s: sodium_hex2bin failed\n", __func__); goto end; } else if ((ret = jwt_decode(c->value, dkey, sizeof dkey, &j))) { if (ret < 0) fprintf(stderr, "%s: jwt_check failed\n", __func__); goto end; } else if (!(u = cJSON_GetObjectItem(j, "name")) || !(user = cJSON_GetStringValue(u)) || strcmp(user, c->field)) { ret = 1; goto end; } ret = 0; end: cJSON_Delete(j); return ret; } static int row(sqlite3_stmt *const stmt, const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, error; struct validate *const v = args; sqlite3 *const db = v->db; char *const key = db_str(db, stmt, "signkey"), *name = NULL; enum auth_role role; unsigned id; if (!key) { fprintf(stderr, "%s: db_str %s failed\n", __func__, "signkey"); goto end; } else if ((ret = db_uint(db, stmt, "id", &id))) { fprintf(stderr, "%s: db_uint %s failed\n", __func__, "id"); goto end; } else if (!(name = db_str(db, stmt, "name"))) { fprintf(stderr, "%s: db_str %s failed\n", __func__, "name"); goto end; } else if (check_role(name, &role)) { fprintf(stderr, "%s: role failed\n", __func__); goto end; } else if ((error = signkey(&p->cookie, key, v))) { if (error < 0) { fprintf(stderr, "%s: signkey failed\n", __func__); goto end; } } else if (!error) { v->valid = true; v->id = id; v->role = role; } ret = 0; end: free(name); free(key); return ret; } static int check_username(const char *const name) { return strspn(name, USER_SYMS) != strlen(name); } static int setup(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; const struct http_cookie *const c = &p->cookie; const char *const username = c->field; struct validate *const v = args; struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "SELECT users.id, users.signkey, roles.name " "FROM users JOIN roles ON users.roleid = roles.id " "WHERE users.name = '%s';", username)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_validate, .end = end, .row = row, .args = v }; if (!op_run(v->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } ret = 0; end: if (ret) free_validate(v); dynstr_free(&d); return ret; } static int open_db(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { struct validate *const v = args; const struct cfg *const cfg = user; const int error = db_open(cfg->dir, &v->db); if (error != SQLITE_OK) { if (error != SQLITE_BUSY) { fprintf(stderr, "%s: db_open: %s\n", __func__, sqlite3_errstr(error)); goto failure; } } else *r = (const struct http_response) { .step.payload = setup, .args = v }; return 0; failure: free_validate(v); return -1; } int auth_validate(const struct http_payload *const p, struct http_response *const r, void *const user, const auth_fn fn) { int ret = -1; struct validate *v = NULL; const struct http_cookie *const c = &p->cookie; const char *const username = c->field; if (!username || !c->value || check_username(username)) { ret = 1; goto failure; } else if (!(v = malloc(sizeof *v))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto failure; } *r = (const struct http_response) { .step.payload = open_db, .free = free_validate, .args = v }; *v = (const struct validate) { .fn = fn, .user = user }; return 0; failure: free(v); return ret; }