aboutsummaryrefslogtreecommitdiff
path: root/ep_passwd.c
diff options
context:
space:
mode:
authorXavier Del Campo Romero <xavi92@disroot.org>2025-09-22 17:32:44 +0200
committerXavier Del Campo Romero <xavi92@disroot.org>2026-02-13 09:57:39 +0100
commit78bf2fe4a5bf37514f6dfd203ef969da0bf40c2e (patch)
tree33f9440b8ee0fa7a3b3ad033616d722d2101bb4d /ep_passwd.c
parent107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff)
Setup project skeletonHEADmaster
Diffstat (limited to 'ep_passwd.c')
-rw-r--r--ep_passwd.c400
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;
+}