diff options
Diffstat (limited to 'ep_signup.c')
| -rw-r--r-- | ep_signup.c | 651 |
1 files changed, 651 insertions, 0 deletions
diff --git a/ep_signup.c b/ep_signup.c new file mode 100644 index 0000000..fddf96e --- /dev/null +++ b/ep_signup.c @@ -0,0 +1,651 @@ +/* + * 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 "db.h" +#include "defs.h" +#include "form.h" +#include "jwt.h" +#include "op.h" +#include "utils.h" +#include <cjson/cJSON.h> +#include <dynstr.h> +#include <libweb/form.h> +#include <libweb/html.h> +#include <libweb/http.h> +#include <sodium.h> +#include <sqlite3.h> +#include <fcntl.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <stdbool.h> +#include <stdint.h> +#include <stdlib.h> +#include <string.h> +#include <time.h> + +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; +} |
