/*
* 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;
}