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:
M | Makefile | | | 5 | ++++- |
M | timekeeper.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");