diff options
| author | Xavier Del Campo Romero <xavi92@disroot.org> | 2025-09-22 17:32:44 +0200 |
|---|---|---|
| committer | Xavier Del Campo Romero <xavi92@disroot.org> | 2026-02-13 09:57:39 +0100 |
| commit | 78bf2fe4a5bf37514f6dfd203ef969da0bf40c2e (patch) | |
| tree | 33f9440b8ee0fa7a3b3ad033616d722d2101bb4d /ep_passwd.c | |
| parent | 107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff) | |
Diffstat (limited to 'ep_passwd.c')
| -rw-r--r-- | ep_passwd.c | 400 |
1 files changed, 400 insertions, 0 deletions
diff --git a/ep_passwd.c b/ep_passwd.c new file mode 100644 index 0000000..c3dab9a --- /dev/null +++ b/ep_passwd.c @@ -0,0 +1,400 @@ +/* + * 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 <https://www.gnu.org/licenses/>. + */ + +#define _POSIX_C_SOURCE 200809L + +#include "endpoints.h" +#include "auth.h" +#include "db.h" +#include "defs.h" +#include "form.h" +#include <dynstr.h> +#include <libweb/form.h> +#include <libweb/http.h> +#include <libweb/html.h> +#include <sodium.h> +#include <sqlite3.h> +#include <errno.h> +#include <stdbool.h> +#include <stddef.h> +#include <stdlib.h> +#include <string.h> + +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; +} |
