/* * 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 "jwt.h" #include "op.h" #include "utils.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include struct signup { bool exists; char *cookie; sqlite3 *db; unsigned char key[32]; }; static void free_signup(void *const p) { int error; struct signup *const s = p; if (!s) return; else if ((error = sqlite3_close(s->db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(s->cookie); free(s); } static char *dump_terms(const char *const dir) { char *ret = NULL, *s = NULL, *encs = NULL; int fd = -1; struct dynstr d; struct stat sb; dynstr_init(&d); if (dynstr_append(&d, "%s/%s", dir, TERMS_PATH)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if ((fd = open(d.str, O_RDONLY)) < 0) { fprintf(stderr, "%s: open(2) %s: %s\n", __func__, d.str, strerror(errno)); goto end; } else if (fstat(fd, &sb)) { fprintf(stderr, "%s: fstat(2) %s: %s\n", __func__, d.str, strerror(errno)); goto end; } else if (!(s = malloc(sb.st_size + 1))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto end; } off_t rem = sb.st_size; char *p = s; while (rem) { const ssize_t r = read(fd, p, rem); if (r < 0) { fprintf(stderr, "%s: read(2): %s\n", __func__, strerror(errno)); goto end; } rem -= r; p += r; } s[sb.st_size] = '\0'; if (!(encs = html_encode(s))) { fprintf(stderr, "%s: html_encode failed\n", __func__); goto end; } ret = encs; end: if (fd >= 0 && close(fd)) { fprintf(stderr, "%s: close(2) %s: %s\n", __func__, d.str, strerror(errno)); ret = NULL; } if (!ret) free(encs); free(s); dynstr_free(&d); return ret; } static int get(const struct http_payload *const p, struct http_response *const r, void *const user) { int ret = -1; char *terms = NULL; const struct cfg *const cfg = user; struct dynstr d; struct html_node *root = NULL, *body, *form, *luser, *iuser, *lpass, *ipass, *ltoken, *itoken, *submit, *pterms; dynstr_init(&d); if (!(terms = dump_terms(cfg->dir))) { fprintf(stderr, "%s: dump_terms failed\n", __func__); goto end; } else if (!(root = html_node_alloc("html"))) { fprintf(stderr, "%s: html_node_alloc failed\n", __func__); goto end; } else if (form_head(root)) { fprintf(stderr, "%s: form_head failed\n", __func__); goto end; } else if (!(body = html_node_add_child(root, "body")) || !(form = html_node_add_child(body, "form")) || !(luser = html_node_add_child(form, "label")) || !(iuser = html_node_add_child(form, "input")) || !(lpass = html_node_add_child(form, "label")) || !(ipass = html_node_add_child(form, "input")) || !(ltoken = html_node_add_child(form, "label")) || !(itoken = html_node_add_child(form, "input")) || !(submit = html_node_add_child(form, "input")) || !(pterms = html_node_add_child(body, "p"))) { fprintf(stderr, "%s: html_node_add_child failed\n", __func__); goto end; } else if (html_node_add_attr(form, "action", "/signup") || html_node_add_attr(form, "form", "loginform") || html_node_add_attr(form, "method", "post") || html_node_add_attr(luser, "for", "username") || html_node_add_attr(lpass, "for", "password") || html_node_add_attr(ltoken, "for", "token") || html_node_add_attr(iuser, "type", "text") || html_node_add_attr(iuser, "id", "username") || html_node_add_attr(iuser, "name", "username") || html_node_add_attr(ipass, "type", "password") || html_node_add_attr(ipass, "id", "password") || html_node_add_attr(ipass, "name", "password") || html_node_add_attr(itoken, "type", "text") || html_node_add_attr(itoken, "id", "token") || html_node_add_attr(itoken, "name", "token") || html_node_add_attr(submit, "type", "submit") || html_node_add_attr(submit, "value", "Sign up")) { fprintf(stderr, "%s: html_node_add_attr failed\n", __func__); goto end; } else if (html_node_set_value(luser, "Username:") || html_node_set_value(lpass, "Password:") || html_node_set_value(ltoken, "Token:") || html_node_set_value_unescaped(pterms, terms)) { fprintf(stderr, "%s: html_node_set_value failed\n", __func__); goto end; } else if (form_footer(body, p->resource)) { fprintf(stderr, "%s: form_footer failed\n", __func__); goto end; } else if (dynstr_append(&d, "%s", DOCTYPE_TAG)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if (html_serialize(root, &d)) { fprintf(stderr, "%s: html_serialize failed\n", __func__); goto end; } *r = (const struct http_response) { .status = HTTP_STATUS_OK, .buf.rw = d.str, .n = d.len, .free = free }; ret = 0; end: if (ret) dynstr_free(&d); free(terms); html_node_free(root); return ret; } static int check_user(const char *const username) { return strspn(username, USER_SYMS) != strlen(username); } static int check_token(const char *const token, const struct signup *const s, const char *const expuser) { int ret = -1; cJSON *j = NULL; const cJSON *u, *e; const char *user; if ((ret = jwt_decode(token, s->key, sizeof s->key, &j))) { if (ret < 0) fprintf(stderr, "%s: jwt_decode failed\n", __func__); goto end; } else if (!(u = cJSON_GetObjectItem(j, "name")) || !(e = cJSON_GetObjectItem(j, "exp")) || !(user = cJSON_GetStringValue(u)) || !cJSON_IsNumber(e) || strcmp(user, expuser)) { ret = 1; goto end; } const time_t t = time(NULL); if (t == (time_t)-1) { fprintf(stderr, "%s: time(2): %s\n", __func__, strerror(errno)); ret = -1; goto end; } else if (t >= (intmax_t)cJSON_GetNumberValue(e)) { ret = 1; goto end; } ret = 0; end: cJSON_Delete(j); return ret; } static int report_usersyms(struct http_response *const r) { int ret = -1; char *enc = NULL; struct dynstr d; dynstr_init(&d); if (dynstr_append(&d, "Invalid username. Accepted symbols: %s", USER_SYMS)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } else if (!(enc = html_encode(d.str))) { fprintf(stderr, "%s: html_encode failed\n", __func__); goto end; } ret = form_badreq(d.str, r); end: free(enc); dynstr_free(&d); return ret; } static int end_signup(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { struct signup *const s = args; const int error = sqlite3_close(s->db); s->db = NULL; if (error != SQLITE_OK) { fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); return -1; } else if (s->exists) { if (form_badreq("Username already exists", r)) { fprintf(stderr, "%s: form_badreq failed\n", __func__); return -1; } } else { *r = (const struct http_response){.status = HTTP_STATUS_SEE_OTHER}; if (http_response_add_header(r, "Location", "/") || http_response_add_header(r, "Set-Cookie", s->cookie)) { fprintf(stderr, "%s: http_response_add_header failed\n", __func__); return -1; } } free_signup(s); return 0; } static int constraint(void *const args) { struct signup *const s = args; s->exists = true; return 0; } static int end_tr(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { struct signup *const s = args; const struct op_cfg op = { .error = free_signup, .end = end_signup, .args = s }; if (!op_run(s->db, "END TRANSACTION;", &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto failure; } return 0; failure: free_signup(s); return -1; } static int end_tokenkey(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { int ret = -1, success = 0, error; struct form *f = NULL; char *cookie = NULL; struct dynstr d; unsigned char key[crypto_auth_hmacsha256_KEYBYTES]; char hashpwd[crypto_pwhash_STRBYTES], enckey[sizeof key * 2 + 1]; const char *username, *password, *token; struct signup *const s = args; const time_t t = time(NULL); dynstr_init(&d); crypto_auth_hmacsha256_keygen(key); if (t == (time_t)-1) { fprintf(stderr, "%s: time(2): %s\n", __func__, strerror(errno)); goto end; } 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 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 (!(token = form_value(f, "token"))) { fprintf(stderr, "%s: missing token\n", __func__); ret = form_badreq("Missing token", r); goto end; } else if (check_user(username)) { if ((ret = report_usersyms(r))) fprintf(stderr, "%s: report_usersyms failed\n", __func__); goto end; } else if (strlen(password) < MINPWDLEN) { if ((ret = form_shortpwd(r))) fprintf(stderr, "%s: form_shortpwd failed\n", __func__); goto end; } else if ((ret = check_token(token, s, username))) { if (ret < 0) fprintf(stderr, "%s: check_token failed\n", __func__); else if ((ret = form_badreq("Invalid registration token", r))) fprintf(stderr, "%s: form_badreq failed\n", __func__); goto end; } else if (!sodium_bin2hex(enckey, sizeof enckey, key, sizeof key)) { fprintf(stderr, "%s: sodium_bin2hex failed\n", __func__); goto end; } else if (!(cookie = gencookie(username, enckey))) { fprintf(stderr, "%s: gencookie failed\n", __func__); goto end; } else if (crypto_pwhash_str(hashpwd, password, strlen(password), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE)) { fprintf(stderr, "%s: crypto_pwhash_str failed\n", __func__); goto end; } else if (dynstr_append(&d, "INSERT INTO " "users(name, password, signkey, creat, thumbnail, roleid) VALUES" "('%s', '%s', '%s', %jd, NULL, " "(SELECT id FROM roles WHERE name = 'user'));", username, hashpwd, enckey, (intmax_t)t)) { fprintf(stderr, "%s: dynstr_append failed\n", __func__); goto end; } const struct op_cfg op = { .constraint = constraint, .error = free_signup, .end = end_tr, .args = s }; if (!op_run(s->db, d.str, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto end; } s->cookie = cookie; ret = 0; success = 1; end: if (!success) free_signup(s); if (ret) free(cookie); dynstr_free(&d); form_free(f); return ret; } static int tokenkey(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 signup *const s = args; char *const key = db_str(s->db, stmt, "value"); if (!key) { fprintf(stderr, "%s: missing tokenkey\n", __func__); goto end; } else if (strlen(key) != sizeof s->key * 2) { fprintf(stderr, "%s: unexpected key length: %zu\n", __func__, strlen(key)); goto end; } else if (sodium_hex2bin(s->key, sizeof s->key, key, strlen(key), NULL, NULL, NULL)) { fprintf(stderr, "%s: sodium_hex2bin failed\n", __func__); goto end; } ret = 0; end: free(key); return ret; } static int signup_tr(const struct http_payload *const p, struct http_response *const r, void *const user, void *const args) { static const char query[] = "SELECT value FROM globals " "WHERE key = 'tokenkey'"; struct signup *const s = args; const struct op_cfg op = { .error = free_signup, .end = end_tokenkey, .row = tokenkey, .args = s }; if (!op_run(s->db, query, &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); return -1; } return 0; } static int post(const struct http_payload *const p, struct http_response *const r, void *const user) { int error; const struct cfg *const cfg = user; struct signup *s = NULL; sqlite3 *db = NULL; if ((error = db_open(cfg->dir, &db)) != SQLITE_OK) { if (error != SQLITE_BUSY) { fprintf(stderr, "%s: db_open: %s\n", __func__, sqlite3_errstr(error)); goto failure; } } else if (!(s = malloc(sizeof *s))) { fprintf(stderr, "%s: malloc(3): %s\n", __func__, strerror(errno)); goto failure; } else { *s = (const struct signup){.db = db}; const struct op_cfg op = { .error = free_signup, .end = signup_tr, .args = s }; if (!op_run(db, "BEGIN TRANSACTION;", &op, r)) { fprintf(stderr, "%s: op_run failed\n", __func__); goto failure; } } return 0; failure: if ((error = sqlite3_close(db)) != SQLITE_OK) fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, sqlite3_errstr(error)); free(s); return -1; } int ep_signup(const struct http_payload *const p, struct http_response *const r, void *const user) { switch (p->op) { case HTTP_OP_GET: return get(p, r, user); case HTTP_OP_POST: return post(p, r, user); default: fprintf(stderr, "%s: unreachable\n", __func__); break; } return -1; }