timekeeper

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

commit 0a694035152791492779793391e4fd709a0c8757
Author: Jacob R. Edwards <jacob@jacobedwards.org>
Date:   Mon, 26 Feb 2024 20:44:02 -0800

Add the starts of a users system

No authentication yet, just allows setting a cookie to tell the
server your username.

Diffstat:
AMakefile | 29+++++++++++++++++++++++++++++
AREADME | 2++
Atimekeeper.c | 275+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 306 insertions(+), 0 deletions(-)

diff --git a/Makefile b/Makefile @@ -0,0 +1,29 @@ +name = timekeeper +cc = ${CC} +cflags = ${CFLAGS} -O0 -Wall -Wextra +lddflags = ${LDDFLAGS} -L/usr/local/lib -lkcgi -lkcgihtml -lz +prefix = /var/www/htdocs/${name}.primus.lan + +#hdr = +#src = +#obj = ${src:.c=.o} + +all: ${name} + +.c.o: + ${cc} ${cflags} -c -o $@ $< + +${name}: ${name}.o ${obj} + ${cc} -o $@ $@.o ${obj} ${lddflags} + +clean: + rm -f ${name} ${name}.o ${obj} + +install: ${name} + cp -af ${name} ${prefix} + +uninstall: + rm -f ${prefix}/${name} + +.PHONY: clean install uninstall +.SUFFIX: .c .o diff --git a/README b/README @@ -0,0 +1,2 @@ +Timekeeper is a CGI program to keep track of hours spent on the +job. diff --git a/timekeeper.c b/timekeeper.c @@ -0,0 +1,275 @@ +#define const + +#include <sys/types.h> +#include <stdarg.h> +#include <stdint.h> +#include <string.h> +#include <stdlib.h> +#include <limits.h> +#include <ctype.h> + +#include <kcgi.h> +#include <kcgihtml.h> + +#define Len(X) (sizeof(X) / sizeof(X[0])) + +enum Field { + KeyUsername, + KeyPassword, + KeySession, + KeyCreate, + KeyMax +}; + +enum Page { + PageIndex, + PageLogin, + PageLogout, + Page404, + PageMax +}; + +struct pagedata { + struct kreq req; + struct khtmlreq html; + 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 + +static char *pages[] = { + [PageIndex] = "index", + [PageLogin] = "login", + [PageLogout] = "logout", + [Page404] = "404" +}; + +enum kcgi_err (*pagefunctions[])(struct pagedata *) = { + [PageIndex] = pageindex, + [PageLogin] = pagelogin, + [PageLogout] = pagelogout, + [Page404] = page404 +}; + +int +kvalid_name(struct kpair *p) +{ + char *s; + + if (!kvalid_stringne(p) || p->valsz < NameMin || p->valsz > NameMax) + return 0; + + for (s = p->val; *s; ++s) + if (!isalnum(*s) && *s != '-') + return 0; + return 1; +} + +int +kvalid_pass(struct kpair *p) +{ + if (!kvalid_stringne(p) || p->valsz < PassMin || p->valsz > PassMax) + return 0; + return 1; +} + +enum kcgi_err +starthtmldoc(struct pagedata *pd, enum khttp code) +{ + enum kcgi_err status; + + if ((status = khttp_head(&pd->req, kresps[KRESP_STATUS], "%s", khttps[code])) != KCGI_OK || + (status = khttp_head(&pd->req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML])) != KCGI_OK || + (status = khttp_body(&pd->req)) != KCGI_OK || + (status = khtml_open(&pd->html, &pd->req, KHTML_PRETTY)) != KCGI_OK) + return status; + kcgi_writer_disable(&pd->req); + + 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) +{ + enum kcgi_err status; + + if ((status = khttp_head(&pd->req, kresps[KRESP_STATUS], "%s", khttps[KHTTP_404])) != KCGI_OK || + (status = khttp_head(&pd->req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[KMIME_TEXT_HTML])) != KCGI_OK || + (status = khttp_body(&pd->req))) + return status; + return khttp_puts(&pd->req, "<!doctype HTML><html><head><title>404 Not found</title></head><h1>404 Not found</h1></html>"); +} + +enum kcgi_err +pageindex(struct pagedata *pd) +{ + enum kcgi_err status; + char *username; + + username = getuser(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_closeelem(&pd->html, 1); + + if ((status = khtml_attr(&pd->html, KELEM_A, + KATTR_HREF, username ? "/logout" : "/login", + KATTR__MAX)) != KCGI_OK || + (status = khtml_puts(&pd->html, username ? "Logout" : "Login")) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) + return status; + + return khtml_close(&pd->html); +} + +enum kcgi_err +redirect(struct pagedata *pd, char *to, char *msg) +{ + enum kcgi_err status; + + if ((status = khttp_head(&pd->req, kresps[KRESP_LOCATION], "%s", to)) != KCGI_OK) + return status; + if ((status = starthtmldoc(pd, KHTTP_303)) != KCGI_OK) + return status; + if ((status = khtml_elem(&pd->html, KELEM_H1)) != KCGI_OK || + (status = khtml_puts(&pd->html, msg)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK || + (status = khtml_elem(&pd->html, KELEM_P)) != KCGI_OK || + (status = khtml_puts(&pd->html, "You are being redirected to ")) != KCGI_OK || + (status = khtml_attr(&pd->html, KELEM_A, + KATTR_HREF, to, + KATTR__MAX)) != KCGI_OK || + (status = khtml_puts(&pd->html, to)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK || + (status = khtml_putc(&pd->html, '.')) != KCGI_OK) + return status; + return khtml_close(&pd->html); +} + +enum kcgi_err +htmlinput(struct pagedata *pd, enum Field field, char *type, int64_t minl, int64_t maxl) +{ + enum kcgi_err status; + + + /* 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 || + (status = khtml_attrx(&pd->html, KELEM_INPUT, + KATTR_NAME, KATTRX_STRING, pd->keys[field].name, + KATTR_TYPE, KATTRX_STRING, type, + KATTR_MINLENGTH, KATTRX_INT, minl, + KATTR_MAXLENGTH, KATTRX_INT, maxl, + KATTR__MAX)) != 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; + 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 (username) + 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 || + (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_attr(&pd->html, KELEM_INPUT, + KATTR_TYPE, "submit", + KATTR_VALUE, "Submit", + KATTR__MAX)) != KCGI_OK || + (status = khtml_closeelem(&pd->html, 1)) != KCGI_OK) + return status; + + return khtml_close(&pd->html); +} + +enum kcgi_err +pagelogout(struct pagedata *pd) +{ + enum kcgi_err status; + + if (getuser(pd)) { + if ((status = khttp_head(&pd->req, kresps[KRESP_SET_COOKIE], + "%s=; Path=/", pd->keys[KeyUsername].name)) != KCGI_OK) + return status; + } + + return redirect(pd, pages[PageIndex], "You are being logged out"); +} + +int +main(void) +{ + enum kcgi_err status; + struct kfcgi *fcgi; + struct kvalid keys[] = { + [KeyUsername] = { kvalid_name, "username" }, + [KeyPassword] = { kvalid_pass, "password" }, + [KeySession] = { kvalid_stringne, "session" }, + [KeyCreate] = { kvalid_stringne, "create" }, + }; + struct pagedata pd = { + .keys = keys + }; + + 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 (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); + } + + khttp_fcgi_free(fcgi); + if (status != KCGI_EXIT) + kutil_err(NULL, NULL, "Unable to parse request"); + return 0; +}