/* * 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 "endpoints.h" #include "db.h" #include "defs.h" #include "form.h" #include "op.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include struct login { bool valid; char *username, *password, *key; sqlite3 *db; }; static void free_login(void *const p) { int error; struct login *const l = p; if (!l) return; else if ((error = sqlite3_close(l->db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(l->username); free(l->password); free(l->key); free(l); } 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; struct login *const l = args; sqlite3 *const db = l->db; const size_t len = strlen(l->password); char *const hashpwd = db_str(db, stmt, "password"), *key = db_str(db, stmt, "signkey"); if (!hashpwd) { fprintf(stderr, "%s: missing password\n", __func__); goto end; } else if (!key) { fprintf(stderr, "%s: missing signkey\n", __func__); goto end; } else if (!crypto_pwhash_str_verify(hashpwd, l->password, len)) { l->valid = true; l->key = key; key = NULL; } ret = 0; end: free(key); free(hashpwd); return ret; } static int authorize(const struct login *const l, struct http_response *const r) { int ret = -1; char *const cookie = gencookie(l->username, l->key); if (!cookie) { fprintf(stderr, "%s: gencookie failed\n", __func__); goto end; } *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", "/") || http_response_add_header(r, "Set-Cookie", cookie)) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); goto end; } ret = 0; end: free(cookie); return ret; } static int end(const struct http_payload *const pl, struct http_response *const r, void *const user, void *const args) { struct login *const l = args; const int error = sqlite3_close(l->db); l->db = NULL; if (error != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); return -1; } else if (l->valid) { if (authorize(l, r)) { fprintf(stderr, "%s: authorize failed\n", __func__); return -1; } } else if (form_unauthorized("Invalid username or password", r)) { fprintf(stderr, "%s: form_unauthorized failed\n", __func__); return -1; } free_login(l); return 0; } static int query_creds(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1; struct login *const l = args; struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "SELECT signkey, password FROM users WHERE name = '%s'", l->username)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .error = free_login, .end = end, .row = row, .args = l }; if (!op_run(l->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } ret = 0; end: if (ret) free_login(l); 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) { const struct cfg *const cfg = user; struct login *const l = args; const int error = db_open(cfg->dir, &l->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 = query_creds, .args = l }; return 0; failure: free(l); return -1; } static int check_user(const char *const username) { return strspn(username, USER_SYMS) != strlen(username); } int ep_login(const struct http_payload *const p, struct http_response *const r, void *const user) { int error, ret = -1; char *userdup = NULL, *passdup = NULL; struct form *f = NULL; struct login *l = NULL; const char *username, *password; if ((error = form_alloc(p->u.post.data, &f))) { if ((ret = error) < 0) fprintf(stderr, "%s: form_alloc failed\n", __func__); else ret = form_badreq("Invalid request", r); goto end; } else if (!(username = form_value(f, "username"))) { fprintf(stderr, "%s: missing username\n", __func__); ret = form_badreq("Missing username", r); goto end; } else if (!(password = form_value(f, "password"))) { fprintf(stderr, "%s: missing password\n", __func__); ret = form_badreq("Missing password", r); goto end; } else if (check_user(username)) { fprintf(stderr, "%s: check_user failed\n", __func__); ret = form_unauthorized("Invalid username", r); goto end; } else if (!(userdup = strdup(username)) || !(passdup = strdup(password))) { fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno)); goto end; } else if (!(l = malloc(sizeof *l))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } *l = (const struct login) { .username = userdup, .password = passdup, }; *r = (const struct http_response) { .step.payload = open_db, .args = l }; ret = 0; end: if (ret) { free(userdup); free(passdup); free(l); } form_free(f); return 0; }