timekeeper

[Abandoned unfinished] CGI web application in C for time tracking. (My first, just a learning project)
Log | Files | Refs | README

commit fefac6e2b98cefe2abfb2cb7942abccb7b95a7ce
parent eafda031d0480450aef6d1b9a8a3ba44d25e7741
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Thu, 29 Feb 2024 07:19:21 -0800

Add backend

Store authentication information in sqlite3 database (using sqlbox
interface) and verify it before logging users in.

Diffstat:
MMakefile | 5++++-
Mtimekeeper.c | 346+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
2 files changed, 292 insertions(+), 59 deletions(-)

diff --git a/Makefile b/Makefile @@ -1,7 +1,7 @@ name = timekeeper cc = ${CC} cflags = ${CFLAGS} -O0 -Wall -Wextra -I/usr/local/include -lddflags = ${LDDFLAGS} -L/usr/local/lib -lkcgi -lkcgihtml -lz +lddflags = ${LDDFLAGS} -static -L/usr/local/lib -lkcgi -lkcgihtml -lz -lsqlbox -lsqlite3 -lm -lpthread prefix = /var/www/htdocs/${name}.primus.lan #hdr = @@ -20,7 +20,10 @@ clean: rm -f ${name} ${name}.o ${obj} install: ${name} + # Otherwise 'Text file is busy' + pkill kfcgi || true cp -af ${name} ${prefix} + kfcgi -n 1 -N 1 -u www -U www -- /htdocs/timekeeper.primus.lan/timekeeper uninstall: rm -f ${prefix}/${name} diff --git a/timekeeper.c b/timekeeper.c @@ -1,23 +1,36 @@ +#include <assert.h> + #define const #include <sys/types.h> +#include <ctype.h> +#include <err.h> +#include <limits.h> +#include <pwd.h> #include <stdarg.h> #include <stdint.h> -#include <string.h> #include <stdlib.h> -#include <limits.h> -#include <ctype.h> +#include <string.h> +#include <unistd.h> #include <kcgi.h> #include <kcgihtml.h> +#include <sqlbox.h> + +#define Promises "stdio rpath wpath cpath proc recvfd unix sendfd" #define Len(X) (sizeof(X) / sizeof(X[0])) +#define NameMin 3 +#define NameMax 64 +#define PassMin 8 +#define PassMax 72 + enum Field { KeyUsername, KeyPassword, - KeySession, - KeyCreate, + KeyHash, /* Authentication hash */ + KeyCreate, /* Whether to create a new user in login page */ KeyMax }; @@ -29,21 +42,26 @@ enum Page { PageMax }; +enum StmtID { + StmtInit, + StmtAddUser, + StmtGetUserByHash, + StmtGetUserByName, + StmtMax +}; + struct pagedata { struct kreq req; struct khtmlreq html; + struct sqlbox *db; + size_t dbid; struct kvalid *keys; }; -enum kcgi_err page404(struct pagedata *pd); -enum kcgi_err pageindex(struct pagedata *pd); -enum kcgi_err pagelogin(struct pagedata *pd); -enum kcgi_err pagelogout(struct pagedata *pd); - -#define NameMin 3 -#define NameMax 64 -#define PassMin 8 -#define PassMax 72 +struct user { + char *name; + char *hash; +}; static char *pages[] = { [PageIndex] = "index", @@ -52,13 +70,6 @@ static char *pages[] = { [Page404] = "404" }; -enum kcgi_err (*pagefunctions[])(struct pagedata *) = { - [PageIndex] = pageindex, - [PageLogin] = pagelogin, - [PageLogout] = pagelogout, - [Page404] = page404 -}; - int kvalid_name(struct kpair *p) { @@ -81,6 +92,150 @@ kvalid_pass(struct kpair *p) return 1; } +enum LoginStatus { + LoginUntried = -1, + LoginValid, + LoginInvalid, + LoginError +}; + +int +initdb(struct pagedata *pd) +{ + size_t stmtid; + struct sqlbox_parmset *res; + + if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtInit, 0, NULL, 0))) + return 1; + if (!(res = sqlbox_step(pd->db, stmtid))) + err(1, "step"); + if (!sqlbox_finalise(pd->db, stmtid)) + err(1, "finalize"); + + return 0; +} + +int +adduser(struct pagedata *pd, char *name, char *key) +{ + size_t stmtid; + struct sqlbox_parmset *res; + char *salt, *hash; + + salt = bcrypt_gensalt(8); + hash = bcrypt(key, salt); + if (!hash) + return 1; + + struct sqlbox_parm p[] = { + { .sparm = hash, .type = SQLBOX_PARM_STRING }, + { .sparm = name, .type = SQLBOX_PARM_STRING } + }; + + if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, StmtAddUser, Len(p), p, 0))) + return 1; + if (!(res = sqlbox_step(pd->db, stmtid))) + return 1; + if (!sqlbox_finalise(pd->db, stmtid)) + return 1; + return 0; +} + +void +freeuser(struct user *user) +{ + if (!user) + return; + free(user->hash); + free(user->name); +} + +struct user * +getuser(struct pagedata *pd, char *field, char *value) +{ + struct user *user; + size_t stmtid; + enum StmtID gets; + struct sqlbox_parmset *res; + struct sqlbox_parm p[] = { + { .sparm = value, .type = SQLBOX_PARM_STRING } + }; + + if (strcmp(field, "name") == 0) + gets = StmtGetUserByName; + else if (strcmp(field, "hash") == 0) + gets = StmtGetUserByHash; + else + err(1, "Expected valid field"); + + if (!(stmtid = sqlbox_prepare_bind(pd->db, pd->dbid, gets, Len(p), p, 0))) + err(1, "sdqlb"); + + if (!(res = sqlbox_step(pd->db, stmtid))) + err(1, "step"); + + if (!res->psz && res->code == SQLBOX_CODE_OK) { + kutil_info(NULL, NULL, "%s: %s not found", value, field); + sqlbox_finalise(pd->db, stmtid); + return NULL; + } + + user = calloc(1, sizeof(*user)); + if (!user) + err(1, "IDC"); + + if (res->psz != 2) + errx(1, "%zu: Invalid number of fields in user-data", res->psz); + user->hash = strdup(res->ps[0].sparm); + if (!user->hash) + err(1, "IDC"); + user->name = strdup(res->ps[1].sparm); + if (!user->name) + err(1, "IDC"); + + if (!sqlbox_finalise(pd->db, stmtid)) + err(1, "finalize"); + + return user; +} + +enum LoginStatus +loginuser(struct user **userp, struct pagedata *pd, char *name, char *key) +{ + struct user *user; + char *testhash; + + user = getuser(pd, "name", name); + if (!user) + return LoginInvalid; + + testhash = bcrypt(key, user->hash); + if (!testhash) { + freeuser(user); + return LoginError; + } + + if (strcmp(user->hash, testhash) != 0) { + freeuser(user); + return LoginInvalid; + } + + if (userp) + *userp = user; + else + freeuser(user); + return LoginValid; +} + +/* getlogin is taken */ +struct user * +sitegetlogin(struct pagedata *pd) +{ + if (!pd->req.cookiemap[KeyHash]) + return NULL; + return getuser(pd, "hash", pd->req.cookiemap[KeyHash]->parsed.s); +} + enum kcgi_err starthtmldoc(struct pagedata *pd, enum khttp code) { @@ -96,14 +251,6 @@ starthtmldoc(struct pagedata *pd, enum khttp code) return KCGI_OK; } -char * -getuser(struct pagedata *pd) -{ - if (pd->req.cookiemap[KeyUsername] && pd->req.cookiemap[KeyUsername]->parsed.s[0]) - return pd->req.cookiemap[KeyUsername]->parsed.s; - return NULL; -} - enum kcgi_err page404(struct pagedata *pd) { @@ -120,21 +267,21 @@ enum kcgi_err pageindex(struct pagedata *pd) { enum kcgi_err status; - char *username; + struct user *user; - username = getuser(pd); + user = sitegetlogin(pd); if ((status = starthtmldoc(pd, KHTTP_200)) != KCGI_OK) return status; khtml_elem(&pd->html, KELEM_H1); - khtml_printf(&pd->html, "Welcome %s!", username ? username : "friend"); + khtml_printf(&pd->html, "Welcome %s!", user ? user->name : "friend"); khtml_closeelem(&pd->html, 1); if ((status = khtml_attr(&pd->html, KELEM_A, - KATTR_HREF, username ? "/logout" : "/login", + KATTR_HREF, user ? "/logout" : "/login", KATTR__MAX)) != KCGI_OK || - (status = khtml_puts(&pd->html, username ? "Logout" : "Login")) != KCGI_OK || + (status = khtml_puts(&pd->html, user ? "Logout" : "Login")) != KCGI_OK || (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) return status; @@ -170,8 +317,8 @@ htmlinput(struct pagedata *pd, enum Field field, char *type, int64_t minl, int64 { enum kcgi_err status; + assert(pd->keys[field].name[0] != 0); - /* Assumes field name is doesn't start with nul */ if ((status = khtml_elem(&pd->html, KELEM_LABEL)) != KCGI_OK || (status = khtml_printf(&pd->html, "%c%s: ", toupper(pd->keys[field].name[0]), pd->keys[field].name + 1)) != KCGI_OK || @@ -188,33 +335,81 @@ htmlinput(struct pagedata *pd, enum Field field, char *type, int64_t minl, int64 } enum kcgi_err -pagelogin(struct pagedata *pd) +htmlwithin(struct pagedata *pd, enum kelem e, char *text) { enum kcgi_err status; - char *username; - username = NULL; - if (pd->req.fieldmap[KeyUsername]) { - if ((status = khttp_head(&pd->req, kresps[KRESP_SET_COOKIE], "%s=%s; Path=/", - pd->keys[KeyUsername].name, pd->req.fieldmap[KeyUsername]->parsed.s)) != KCGI_OK) - return status; - username = pd->req.fieldmap[KeyUsername]->parsed.s; - } else if (pd->req.cookiemap[KeyUsername]) { - username = pd->req.cookiemap[KeyUsername]->parsed.s; + if ((status = khtml_elem(&pd->html, e)) != KCGI_OK || + (status = khtml_puts(&pd->html, text)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) + return status; + return KCGI_OK; +} + +enum kcgi_err +pagelogin(struct pagedata *pd) +{ + enum kcgi_err status; + struct user *user; + char *fuser, *fpass; + enum LoginStatus ls; + char *msg; + + user = NULL; + ls = LoginUntried; + if (pd->req.fieldmap[KeyUsername] && pd->req.fieldmap[KeyPassword]) { + fuser = pd->req.fieldmap[KeyUsername]->parsed.s; + fpass = pd->req.fieldmap[KeyPassword]->parsed.s; + + if (pd->req.fieldmap[KeyCreate]) { + /* Ignoring return value */ + adduser(pd, fuser, fpass); + } + ls = loginuser(&user, pd, fuser, fpass); + if (ls == LoginValid) { + status = khttp_head(&pd->req, kresps[KRESP_SET_COOKIE], "%s=%s; Path=/", + pd->keys[KeyHash].name, user->hash); + if (status != KCGI_OK) + return status; + } + } else { + user = sitegetlogin(pd); } - if (username) + if (user) return redirect(pd, "index", "Logged in"); - if ((status = starthtmldoc(pd, KHTTP_200)) != KCGI_OK || - (status = khtml_elem(&pd->html, KELEM_H1)) != KCGI_OK || - (status = khtml_puts(&pd->html, "Login")) != KCGI_OK || - (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK || - (status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK || + if ((status = starthtmldoc(pd, KHTTP_200)) != KCGI_OK) + return status; + + if ((status = htmlwithin(pd, KELEM_H1, "Login")) != KCGI_OK) + return status; + + if (ls > 0) { + if (ls == LoginInvalid) + msg = "Error: Invalid credentials"; + else if (ls == LoginError) + msg = "Error: System error"; + else + msg = "Error: Undefined error"; + + if ((status = htmlwithin(pd, KELEM_P, msg)) != KCGI_OK) + return status; + } + + if ((status = khtml_elem(&pd->html, KELEM_FORM)) != KCGI_OK || (status = htmlinput(pd, KeyUsername, "text", NameMin, NameMax)) != KCGI_OK || (status = khtml_putc(&pd->html, ' ')) != KCGI_OK || (status = htmlinput(pd, KeyPassword, "password", PassMin, PassMax)) != KCGI_OK || (status = khtml_putc(&pd->html, ' ')) != KCGI_OK || + (status = khtml_elem(&pd->html, KELEM_LABEL)) != KCGI_OK || + (status = khtml_puts(&pd->html, "Create user ")) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_INPUT, + KATTR_TYPE, "checkbox", + KATTR_NAME, "create", + KATTR__MAX)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK || + (status = khtml_putc(&pd->html, ' ')) != KCGI_OK || (status = khtml_attr(&pd->html, KELEM_INPUT, KATTR_TYPE, "submit", KATTR_VALUE, "Submit", @@ -229,10 +424,12 @@ enum kcgi_err pagelogout(struct pagedata *pd) { enum kcgi_err status; + struct user *user; - if (getuser(pd)) { + if ((user = sitegetlogin(pd))) { + freeuser(user); if ((status = khttp_head(&pd->req, kresps[KRESP_SET_COOKIE], - "%s=; Path=/", pd->keys[KeyUsername].name)) != KCGI_OK) + "%s=; Path=/", pd->keys[KeyHash].name)) != KCGI_OK) return status; } @@ -247,27 +444,60 @@ main(void) struct kvalid keys[] = { [KeyUsername] = { kvalid_name, "username" }, [KeyPassword] = { kvalid_pass, "password" }, - [KeySession] = { kvalid_stringne, "session" }, + [KeyHash] = { kvalid_stringne, "hash" }, [KeyCreate] = { kvalid_stringne, "create" }, }; struct pagedata pd = { .keys = keys }; + struct sqlbox_cfg cfg; + struct sqlbox_src srcs[] = { + { .fname = "/tmp/timekeeper.db", .mode = SQLBOX_SRC_RWC } + }; + struct sqlbox_pstmt pstmts[] = { + [StmtInit] = { .stmt = "CREATE TABLE IF NOT EXISTS users (hash TEXT PRIMARY KEY, name TEXT UNIQUE)" }, + [StmtAddUser] = { .stmt = "INSERT INTO users (hash, name) VALUES (?, ?)" }, + [StmtGetUserByName] = { .stmt = "SELECT * FROM users WHERE name IS ?" }, + [StmtGetUserByHash] = { .stmt = "SELECT * FROM users WHERE hash IS ?" } + }; + enum kcgi_err (*pagefunctions[])(struct pagedata *) = { + [PageIndex] = pageindex, + [PageLogin] = pagelogin, + [PageLogout] = pagelogout, + [Page404] = page404 + }; + + memset(&cfg, 0, sizeof(cfg)); + cfg.msg.func_short = warnx; + cfg.srcs.srcsz = 1; + cfg.srcs.srcs = srcs; + cfg.stmts.stmtsz = StmtMax; + cfg.stmts.stmts = pstmts; + + if ((pd.db = sqlbox_alloc(&cfg)) == NULL) + kutil_err(NULL, NULL, "Unable to alloc database"); + if (!(pd.dbid = sqlbox_open(pd.db, 0))) + kutil_err(NULL, NULL, "Unable to open database"); if (khttp_fcgi_init(&fcgi, pd.keys, KeyMax, pages, PageMax, 0) != KCGI_OK) kutil_err(NULL, NULL, "Unable to initialize fcgi"); - while ((status = khttp_fcgi_parse(fcgi, &pd.req)) == KCGI_OK) { + if (pledge(Promises, Promises)) + kutil_err(NULL, NULL, "pledge"); + + if (initdb(&pd)) + err(1, "Unable to setup database"); + + status = KCGI_OK; + while (status == KCGI_OK && (status = khttp_fcgi_parse(fcgi, &pd.req)) == KCGI_OK) { if (pd.req.page == PageMax) status = pagefunctions[Page404](&pd); else status = pagefunctions[pd.req.page](&pd); khttp_free(&pd.req); - - if (status != KCGI_OK) - kutil_err(&pd.req, NULL, "kcgi error code %d", status); } + sqlbox_free(pd.db); khttp_fcgi_free(fcgi); if (status != KCGI_EXIT) kutil_err(NULL, NULL, "Unable to parse request");