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 /main.c | |
| parent | 107a2e43d54f9a42fb85b00b83cb0d9abb194680 (diff) | |
Diffstat (limited to 'main.c')
| -rw-r--r-- | main.c | 648 |
1 files changed, 648 insertions, 0 deletions
@@ -0,0 +1,648 @@ +/* + * 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/>. + */ + +/* As of FreeBSD 13.2, sigaction(2) still conforms to IEEE Std + * 1003.1-1990 (POSIX.1), which did not define SA_RESTART. + * FreeBSD supports it as an extension, but then _POSIX_C_SOURCE must + * not be defined. */ +#ifndef __FreeBSD__ +#define _POSIX_C_SOURCE 200809L +#endif + +#include "db.h" +#include "defs.h" +#include "default.h" +#include "endpoints.h" +#include <libweb/handler.h> +#include <dynstr.h> +#include <sodium.h> +#include <sqlite3.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include <signal.h> +#include <stdint.h> +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <time.h> + +static struct handler *handler; + +static void usage(char *const argv[]) +{ + fprintf(stderr, "%s [-t tmpdir] [-p port] dir\n", *argv); +} + +static int parse_args(const int argc, char *const argv[], + const char **const dir, unsigned short *const port, + const char **const tmpdir) +{ + const char *const envtmp = getenv("TMPDIR"); + int opt; + + /* Default values. */ + *port = 0; + *tmpdir = envtmp ? envtmp : "/tmp"; + + while ((opt = getopt(argc, argv, "t:p:")) != -1) + { + switch (opt) + { + case 't': + *tmpdir = optarg; + break; + + case 'p': + { + char *endptr; + const unsigned long portul = strtoul(optarg, &endptr, 10); + + if (*endptr || portul > UINT16_MAX) + { + fprintf(stderr, "%s: invalid port %s\n", __func__, optarg); + return -1; + } + + *port = portul; + } + break; + + default: + usage(argv); + return -1; + } + } + + if (optind >= argc) + { + usage(argv); + return -1; + } + + *dir = argv[optind]; + return 0; +} + +static int ensure_dir(const char *const dir) +{ + struct stat sb; + + if (stat(dir, &sb)) + { + switch (errno) + { + case ENOENT: + if (mkdir(dir, S_IRWXU)) + { + fprintf(stderr, "%s: mkdir(2) %s: %s\n", + __func__, dir, strerror(errno)); + return -1; + } + + printf("Created empty directory at %s\n", dir); + break; + + default: + fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); + return -1; + } + } + else if (!S_ISDIR(sb.st_mode)) + { + fprintf(stderr, "%s: %s not a directory\n", __func__, dir); + return -1; + } + + return 0; +} + +static int dump_file(const char *const path, const void *const buf, + const size_t n) +{ + int ret = -1; + FILE *const f = fopen(path, "wb"); + + if (!f) + { + fprintf(stderr, "%s: fopen(3) %s: %s\n", + __func__, path, strerror(errno)); + goto end; + } + else if (!fwrite(buf, n, 1, f)) + { + fprintf(stderr, "%s: fwrite(3): %s\n", __func__, strerror(errno)); + goto end; + } + + ret = 0; + +end: + if (f && fclose(f)) + { + fprintf(stderr, "%s: fclose(3): %s\n", __func__, strerror(errno)); + ret = -1; + } + + return ret; +} + +static int dump_default_style(const char *const path) +{ + if (dump_file(path, default_style, default_style_len)) + return -1; + + printf("Dumped default stylesheet into %s\n", path); + return 0; +} + +static int dump_default_terms(const char *const path) +{ + if (dump_file(path, default_terms, default_terms_len)) + return -1; + + printf("Dumped default terms of service into %s\n", path); + return 0; +} + +static int dump_default_prv_policy(const char *const path) +{ + if (dump_file(path, default_prv_policy, default_prv_policy_len)) + return -1; + + printf("Dumped default privacy policy into %s\n", path); + return 0; +} + +static int ensure_file(const char *const dir, const char *const f, + int (*const fn)(const char *)) +{ + int ret = -1; + struct dynstr d; + struct stat sb; + + dynstr_init(&d); + + if (dynstr_append(&d, "%s/%s", dir, f)) + { + fprintf(stderr, "%s: dynstr_append user failed\n", __func__); + goto end; + } + else if (stat(d.str, &sb)) + { + if (errno == ENOENT) + { + if (fn(d.str)) + { + fprintf(stderr, "%s: user callback failed\n", __func__); + goto end; + } + } + else + { + fprintf(stderr, "%s: stat(2): %s\n", __func__, strerror(errno)); + goto end; + } + } + else if (!S_ISREG(sb.st_mode)) + { + fprintf(stderr, "%s: %s not a regular file\n", __func__, d.str); + return -1; + } + + ret = 0; + +end: + dynstr_free(&d); + return ret; +} + +static int ensure_style(const char *const dir) +{ + return ensure_file(dir, STYLE_PATH, dump_default_style); +} + +static int ensure_terms(const char *const dir) +{ + return ensure_file(dir, TERMS_PATH, dump_default_terms); +} + +static int ensure_prv_policy(const char *const dir) +{ + return ensure_file(dir, PRV_PATH, dump_default_prv_policy); +} + +static void handle_signal(const int signum) +{ + switch (signum) + { + case SIGINT: + /* Fall through. */ + case SIGTERM: + handler_notify_close(handler); + break; + + default: + break; + } +} + +static int init_signals(void) +{ + struct sigaction sa = + { + .sa_handler = handle_signal, + .sa_flags = SA_RESTART + }; + + sigemptyset(&sa.sa_mask); + + static const struct signal + { + int signal; + const char *name; + } signals[] = + { + {.signal = SIGINT, .name = "SIGINT"}, + {.signal = SIGTERM, .name = "SIGTERM"}, + {.signal = SIGPIPE, .name = "SIGPIPE"} + }; + + for (size_t i = 0; i < sizeof signals / sizeof *signals; i++) + { + const struct signal *const s = &signals[i]; + + if (sigaction(s->signal, &sa, NULL)) + { + fprintf(stderr, "%s: sigaction(2) %s: %s\n", + __func__, s->name, strerror(errno)); + return -1; + } + } + + return 0; +} + +static int add_urls(struct handler *const h, void *const user) +{ + static const struct url + { + const char *url; + enum http_op op; + handler_fn f; + } urls[] = + { + {.url = "/", .op = HTTP_OP_GET, .f = ep_index}, + {.url = "/view/*", .op = HTTP_OP_GET, .f = ep_view}, + {.url = "/index.html", .op = HTTP_OP_GET, .f = ep_index}, + {.url = "/style.css", .op = HTTP_OP_GET, .f = ep_style}, + {.url = "/login", .op = HTTP_OP_POST, .f = ep_login}, + {.url = "/logout", .op = HTTP_OP_POST, .f = ep_logout}, + {.url = "/signup", .op = HTTP_OP_GET, .f = ep_signup}, + {.url = "/signup", .op = HTTP_OP_POST, .f = ep_signup}, + {.url = "/passwd", .op = HTTP_OP_POST, .f = ep_passwd}, + {.url = "/create/*", .op = HTTP_OP_POST, .f = ep_create}, + {.url = "/ucp", .op = HTTP_OP_GET, .f = ep_ucp} + }; + + for (size_t i = 0; i < sizeof urls / sizeof *urls; i++) + { + const struct url *const u = &urls[i]; + + if (handler_add(h, u->url, u->op, u->f, user)) + { + fprintf(stderr, "%s: handler_add %s failed\n", __func__, u->url); + return -1; + } + } + + return 0; +} + +static int check_length(const enum http_op op, const char *const res, + const unsigned long long len, const struct http_cookie *const c, + struct http_response *const r, void *const user) +{ + return 1; +} + +static int statement(sqlite3 *const db, const char *const st) +{ + int ret = -1; + sqlite3_stmt *stmt = NULL; + int error = sqlite3_prepare_v2(db, st, -1, &stmt, NULL); + + if (error != SQLITE_OK) + { + fprintf(stderr, "%s: sqlite3_prepare_v2 \"%s\": %s\n", __func__, st, + sqlite3_errstr(error)); + goto end; + } + + while ((error = sqlite3_step(stmt)) == SQLITE_BUSY) + ; + + if (error != SQLITE_DONE) + { + sqlite3_reset(stmt); + fprintf(stderr, "%s: sqlite3_step: %s\n", __func__, + sqlite3_errstr(error)); + goto end; + } + + ret = 0; + +end: + + if ((error = sqlite3_finalize(stmt))) + { + fprintf(stderr, "%s: sqlite3_finalize: %s\n", __func__, + sqlite3_errstr(error)); + ret = -1; + } + + return ret; +} + +static int ensure_admin(sqlite3 *const db) +{ + int ret = -1, error; + sqlite3_stmt *stmt = NULL; + unsigned char key[crypto_auth_hmacsha256_KEYBYTES]; + char hashpwd[crypto_pwhash_STRBYTES], enckey[sizeof key * 2 + 1]; + struct dynstr d; + static const char username[] = "admin", password[] = "1234"; + 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 (!sodium_bin2hex(enckey, sizeof enckey, key, sizeof key)) + { + fprintf(stderr, "%s: sodium_bin2hex 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, roleid) VALUES" + "('%s', '%s', '%s', %jd, (SELECT id FROM roles WHERE name = '%s'));", + username, hashpwd, enckey, (intmax_t)t, 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; + } + + while ((error = sqlite3_step(stmt)) == SQLITE_BUSY) + ; + + if (error == SQLITE_DONE) + fprintf(stderr, "Created default '%s' user with password '%s'. " + "Please change its password before deploying.\n", username, + password); + else + { + sqlite3_reset(stmt); + + if (error != SQLITE_CONSTRAINT) + { + fprintf(stderr, "%s: sqlite3_step: %s\n", __func__, + sqlite3_errstr(error)); + goto end; + } + } + + ret = 0; + +end: + + if ((error = sqlite3_finalize(stmt)) != SQLITE_OK) + { + fprintf(stderr, "%s: sqlite3_finalize: %s\n", __func__, + sqlite3_errstr(error)); + ret = -1; + } + + dynstr_free(&d); + return ret; +} + +static int ensure_tokenkey(sqlite3 *const db) +{ + int ret = -1; + struct dynstr d; + unsigned char key[32]; + char enckey[sizeof key * 2 + 1]; + + dynstr_init(&d); + randombytes_buf(key, sizeof key); + + if (!sodium_bin2hex(enckey, sizeof enckey, key, sizeof key)) + { + fprintf(stderr, "%s: sodium_bin2hex failed\n", __func__); + goto end; + } + else if (dynstr_append(&d, "INSERT OR IGNORE INTO globals(key, value) " + "VALUES('tokenkey', '%s');", enckey)) + { + fprintf(stderr, "%s: dynstr_append failed\n", __func__); + goto end; + } + else if (statement(db, d.str)) + { + fprintf(stderr, "%s: statement failed\n", __func__); + goto end; + } + + ret = 0; + +end: + dynstr_free(&d); + return ret; +} + +static int ensure_tables(const char *const dir) +{ + static const char roles[] = "CREATE TABLE IF NOT EXISTS roles (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL UNIQUE" + ");", + users[] = "CREATE TABLE IF NOT EXISTS users (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL UNIQUE," + "password TEXT NOT NULL," + "signkey TEXT NOT NULL," + "creat BIGINT NOT NULL," + "thumbnail BLOB," + "roleid INTEGER NOT NULL," + "FOREIGN KEY (roleid) REFERENCES roles (id)" + ");", + categories[] = "CREATE TABLE IF NOT EXISTS categories (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL" + ");", + sections[] = "CREATE TABLE IF NOT EXISTS sections (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "name TEXT NOT NULL," + "description TEXT NOT NULL," + "catid INTEGER NOT NULL," + "FOREIGN KEY (catid) REFERENCES categories (id)" + ");", + topics[] = "CREATE TABLE IF NOT EXISTS topics (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "title TEXT NOT NULL," + "creat INTEGER NOT NULL," + "secid INTEGER NOT NULL," + "uid INTEGER NOT NULL," + "FOREIGN KEY (secid) REFERENCES sections (id)" + "FOREIGN KEY (uid) REFERENCES users (id)" + ");", + posts[] = "CREATE TABLE IF NOT EXISTS posts (" + "id INTEGER PRIMARY KEY AUTOINCREMENT," + "text TEXT NOT NULL," + "creat BIGINT NOT NULL," + "uid INTEGER NOT NULL," + "topid INTEGER NOT NULL," + "FOREIGN KEY (topid) REFERENCES topics (id)," + "FOREIGN KEY (uid) REFERENCES users (id)" + ");", + globals[] = "CREATE TABLE IF NOT EXISTS globals (" + "id INTEGER PRIMARY KEY," + "key TEXT NOT NULL UNIQUE," + "value TEXT NOT NULL" + ");", + role_insert[] = "INSERT OR IGNORE INTO roles(name) " + "VALUES('admin'),('mod'),('user'),('banned');", + version_insert[] = "INSERT OR IGNORE INTO globals(key, value) " + "VALUES('version', '1');"; + + int ret = -1, error; + sqlite3 *db = NULL; + + if (db_open(dir, &db)) + { + fprintf(stderr, "%s: db_open failed\n", __func__); + goto end; + } + else if (statement(db, "BEGIN TRANSACTION;") + || statement(db, roles) + || statement(db, role_insert) + || statement(db, users) + || statement(db, categories) + || statement(db, sections) + || statement(db, topics) + || statement(db, posts) + || statement(db, globals) + || statement(db, version_insert) + || ensure_tokenkey(db) + || ensure_admin(db) + || statement(db, "COMMIT;")) + { + if (statement(db, "ROLLBACK;")) + fprintf(stderr, "%s: rollback failed\n", __func__); + + goto end; + } + + ret = 0; + +end: + + if ((error = sqlite3_close(db)) != SQLITE_OK) + { + fprintf(stderr, "%s: sqlite3_close: %s\n", __func__, + sqlite3_errstr(error)); + ret = -1; + } + + return ret; +} + +int main(int argc, char *argv[]) +{ + int ret = EXIT_FAILURE; + const char *dir, *tmpdir; + unsigned short port; + + if (sodium_init()) + { + fprintf(stderr, "%s: sodium_init failed\n", __func__); + goto end; + } + else if (parse_args(argc, argv, &dir, &port, &tmpdir) + || ensure_dir(dir) + || ensure_terms(dir) + || ensure_prv_policy(dir) + || ensure_style(dir) + || ensure_tables(dir)) + goto end; + + const struct handler_cfg cfg = + { + .length = check_length, + .tmpdir = tmpdir, + /* Arbitrary limit. */ + .max_headers = 25, + .post = + { + /* Arbitrary limit. */ + .max_files = 1, + /* File upload only requires one pair. */ + .max_pairs = 1 + } + }; + + unsigned short outport; + struct cfg c = {.dir = dir}; + + if (!(handler = handler_alloc(&cfg)) + || add_urls(handler, &c) + || init_signals() + || handler_listen(handler, port, &outport)) + goto end; + + printf("Listening on port %hu\n", outport); + + if (handler_loop(handler)) + { + fprintf(stderr, "%s: handler_loop failed\n", __func__); + goto end; + } + + ret = EXIT_SUCCESS; + +end: + handler_free(handler); + return ret; +} |
