/*
* 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 "auth.h"
#include "db.h"
#include "defs.h"
#include "form.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
struct passwd
{
bool valid;
sqlite3 *db;
sqlite3_stmt *stmt;
char *username, *old, *new;
};
static void free_passwd(struct passwd *const p)
{
int error;
if (!p)
return;
sqlite3_finalize(p->stmt);
if ((error = sqlite3_close(p->db) != SQLITE_OK))
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
free(p->username);
free(p->old);
free(p->new);
free(p);
}
static int row(struct passwd *const p)
{
char *const hashpwd = db_str(p->db, p->stmt, "password");
if (!hashpwd)
{
fprintf(stderr, "%s: missing password\n", __func__);
return -1;
}
else if (!crypto_pwhash_str_verify(hashpwd, p->old, strlen(p->old)))
p->valid = true;
free(hashpwd);
return 0;
}
static int finalize_update(struct passwd *const p,
struct http_response *const r)
{
*r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER};
if (http_response_add_header(r, "Location", "/"))
{
fprintf(stderr, "%s: http_response_add_header failed\n", __func__);
return -1;
}
free_passwd(p);
return 0;
}
static int update(const struct http_payload *const pl,
struct http_response *const r, void *const user, void *const args)
{
struct passwd *const p = args;
sqlite3_stmt *const stmt = p->stmt;
const int error = sqlite3_step(stmt);
switch (error)
{
case SQLITE_BUSY:
break;
case SQLITE_DONE:
if (finalize_update(p, r))
goto failure;
break;
default:
fprintf(stderr, "%s: sqlite3_step: %s\n", __func__,
sqlite3_errstr(error));
sqlite3_reset(stmt);
goto failure;
}
return 0;
failure:
free_passwd(p);
return -1;
}
static int prepare_change(struct passwd *const p, struct http_response *const r)
{
int ret = -1, error;
struct dynstr d;
sqlite3_stmt *stmt = NULL;
char hashpwd[crypto_pwhash_STRBYTES];
dynstr_init(&d);
if (crypto_pwhash_str(hashpwd, p->new, strlen(p->new),
crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE))
{
fprintf(stderr, "%s: crypto_pwhash_str failed\n", __func__);
goto end;
}
else if (dynstr_append(&d,
"UPDATE users SET password='%s' WHERE name = '%s';", hashpwd,
p->username))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if ((error = sqlite3_prepare_v2(p->db, d.str, d.len, &stmt, NULL))
!= SQLITE_OK)
{
fprintf(stderr, "%s: sqlite3_prepare_v2 \"%s\": %s\n", __func__,
d.str, sqlite3_errstr(error));
goto end;
}
p->stmt = stmt;
*r = (const struct http_response)
{
.step.payload = update,
.args = p
};
ret = 0;
end:
dynstr_free(&d);
return ret;
}
static int finalize(struct passwd *const p, struct http_response *const r)
{
const int error = sqlite3_finalize(p->stmt);
p->stmt = NULL;
if (error != SQLITE_OK)
{
fprintf(stderr, "%s: sqlite3_finalize: %s\n", __func__,
sqlite3_errstr(error));
return -1;
}
else if (!p->valid)
{
const int ret = form_unauthorized("Invalid current password", r);
free_passwd(p);
return ret;
}
else if (prepare_change(p, r))
{
fprintf(stderr, "%s: prepare_change failed\n", __func__);
return -1;
}
return 0;
}
static int run_query(const struct http_payload *const pl,
struct http_response *const r, void *const user, void *const args)
{
struct passwd *const p = args;
sqlite3_stmt *const stmt = p->stmt;
const int error = sqlite3_step(stmt);
switch (error)
{
case SQLITE_BUSY:
break;
case SQLITE_DONE:
if (finalize(p, r))
goto failure;
break;
case SQLITE_ROW:
if (row(p))
goto failure;
break;
default:
fprintf(stderr, "%s: sqlite3_step: %s\n", __func__,
sqlite3_errstr(error));
sqlite3_reset(stmt);
goto failure;
}
return 0;
failure:
free_passwd(p);
return -1;
}
static int passwd(const struct cfg *const cfg, const char *const username,
const char *const old, const char *const new, sqlite3 *const db,
struct http_response *const r)
{
int ret = -1, error;
struct dynstr d;
sqlite3_stmt *stmt = NULL;
char *userdup = NULL, *olddup = NULL, *newdup = NULL;
struct passwd *p = NULL;
dynstr_init(&d);
if (!(userdup = strdup(username))
|| !(olddup = strdup(old))
|| !(newdup = strdup(new)))
{
fprintf(stderr, "%s: strdup(3): %s\n", __func__, strerror(errno));
goto end;
}
else if (dynstr_append(&d,
"SELECT password from users WHERE name = '%s'", username))
{
fprintf(stderr, "%s: dynstr_append failed\n", __func__);
goto end;
}
else if ((error = sqlite3_prepare_v2(db, d.str, d.len, &stmt,
NULL)) != SQLITE_OK)
{
fprintf(stderr, "%s: sqlite3_prepare_v2 \"%s\": %s\n", __func__,
d.str, sqlite3_errstr(error));
goto end;
}
else if (!(p = malloc(sizeof *p)))
{
fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno));
goto end;
}
*p = (const struct passwd)
{
.username = userdup,
.old = olddup,
.new = newdup,
.stmt = stmt,
.db = db
};
*r = (const struct http_response)
{
.step.payload = run_query,
.args = p
};
ret = 0;
end:
if (ret)
{
free(p);
free(userdup);
free(olddup);
free(newdup);
sqlite3_finalize(stmt);
}
dynstr_free(&d);
return ret;
}
static int setup(const struct http_payload *const p,
struct http_response *const r, void *const user, sqlite3 *const db,
const struct auth_user *const u)
{
int ret = -1, error;
struct form *f = NULL;
const char *old, *new, *cnew;
const struct cfg *const cfg = user;
*r = (const struct http_response){0};
if (!u)
{
ret = form_unauthorized("Authentication required", r);
goto failure;
}
else 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 failure;
}
else if (!(old = form_value(f, "old")))
{
ret = form_badreq("Missing old password", r);
goto failure;
}
else if (!(new = form_value(f, "new")))
{
ret = form_badreq("Missing new password", r);
goto failure;
}
else if (!(cnew = form_value(f, "cnew")))
{
ret = form_badreq("Missing confirmation password", r);
goto failure;
}
else if (strcmp(new, cnew))
{
ret = form_badreq("New password mismatch", r);
goto failure;
}
else if (strlen(new) < MINPWDLEN)
{
ret = form_shortpwd(r);
goto failure;
}
else if ((ret = passwd(cfg, u->username, old, new, db, r)))
{
fprintf(stderr, "%s: passwd failed\n", __func__);
goto failure;
}
form_free(f);
return 0;
failure:
if ((error = sqlite3_close(db)) != SQLITE_OK)
fprintf(stderr, "%s: sqlite3_close: %s\n", __func__,
sqlite3_errstr(error));
form_free(f);
return ret;
}
int ep_passwd(const struct http_payload *const p,
struct http_response *const r, void *const user)
{
const int n = auth_validate(p, r, user, setup);
if (n < 0)
fprintf(stderr, "%s: auth_validate failed\n", __func__);
else if (n)
{
const struct cfg *const cfg = user;
sqlite3 *db;
if (db_open(cfg->dir, &db))
{
fprintf(stderr, "%s: db_open failed\n", __func__);
return -1;
}
return setup(p, r, user, db, NULL);
}
return n;
}