summary refs log tree commit diff
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.gitignore1
-rw-r--r--Makefile14
-rw-r--r--contexts.c72
-rw-r--r--events.c26
-rw-r--r--html.c91
-rw-r--r--networks.c65
-rw-r--r--scooper.c319
-rw-r--r--search.c26
-rw-r--r--server.c123
-rw-r--r--server.h129
10 files changed, 545 insertions, 321 deletions
diff --git a/.gitignore b/.gitignore
index 9d35820..ac9b3a5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
+*.o
 config.mk
 scooper
diff --git a/Makefile b/Makefile
index 94ca089..47f6aa4 100644
--- a/Makefile
+++ b/Makefile
@@ -6,10 +6,20 @@ LDLIBS = -lkcgi -lkcgihtml -lsqlite3
 
 -include config.mk
 
-scooper:
+OBJS += contexts.o
+OBJS += events.o
+OBJS += html.o
+OBJS += networks.o
+OBJS += search.o
+OBJS += server.o
+
+scooper: ${OBJS}
+	${CC} ${LDFLAGS} ${OBJS} ${LDLIBS} -o $@
+
+${OBJS}: server.h
 
 clean:
-	rm -f scooper
+	rm -f scooper ${OBJS}
 
 install: scooper scooper.1
 	install -d ${PREFIX}/bin ${MANDIR}/man1
diff --git a/contexts.c b/contexts.c
new file mode 100644
index 0000000..7adfa34
--- /dev/null
+++ b/contexts.c
@@ -0,0 +1,72 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "server.h"
+
+const char *ContextsQuery = SQL(
+	SELECT name
+	FROM contexts
+	WHERE network = :network AND coalesce(query = :query, true)
+	ORDER BY query, name;
+);
+
+enum kcgi_err pageContexts(struct kreq *req) {
+	if (!req->fieldmap[Network]) return httpFail(req, KHTTP_404);
+	enum kcgi_err error = httpHead(req, KHTTP_200, KMIME_TEXT_HTML);
+	if (req->method == KMETHOD_HEAD) return error;
+
+	const char *network = req->fieldmap[Network]->parsed.s;
+
+	struct khtmlreq html;
+	error = error
+		|| khttp_body(req)
+		|| khtml_open(&html, req, KHTML_PRETTY)
+		|| htmlHead(&html, network)
+		|| htmlNav(&html, network, NULL)
+		|| khtml_elem(&html, KELEM_UL);
+	if (error) return error;
+
+	dbBindText(stmt.contexts, ":network", network);
+	if (pagePublic) dbBindInt(stmt.contexts, ":query", false);
+
+	int result;
+	while (SQLITE_ROW == (result = sqlite3_step(stmt.contexts))) {
+		const char *context = (const char *)sqlite3_column_text(stmt.contexts, 0);
+		char *href = khttp_urlpart(
+			NULL, NULL, Pages[Events],
+			Keys[Network].name, network,
+			Keys[Context].name, context,
+			NULL
+		);
+		if (!href) err(EX_OSERR, "khttp_urlpart");
+
+		error = khtml_elem(&html, KELEM_LI)
+			|| khtml_attr(&html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
+			|| khtml_puts(&html, context)
+			|| khtml_closeelem(&html, 2);
+		free(href);
+		if (error) break;
+	}
+	if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
+	sqlite3_reset(stmt.contexts);
+
+	return error || khtml_close(&html);
+}
diff --git a/events.c b/events.c
new file mode 100644
index 0000000..c891bff
--- /dev/null
+++ b/events.c
@@ -0,0 +1,26 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "server.h"
+
+enum kcgi_err pageEvents(struct kreq *req) {
+	return httpFail(req, KHTTP_501);
+}
diff --git a/html.c b/html.c
new file mode 100644
index 0000000..ad80b6f
--- /dev/null
+++ b/html.c
@@ -0,0 +1,91 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "server.h"
+
+const char *htmlStylesheet;
+
+static enum kcgi_err stylesheet(struct khtmlreq *html) {
+	if (!htmlStylesheet) return KCGI_OK;
+	return khtml_attr(
+		html, KELEM_LINK,
+		KATTR_REL, "stylesheet",
+		KATTR_HREF, htmlStylesheet,
+		KATTR__MAX
+	);
+}
+
+enum kcgi_err htmlHead(struct khtmlreq *html, const char *title) {
+	return khtml_elem(html, KELEM_DOCTYPE)
+		|| khtml_attr(html, KELEM_META, KATTR_CHARSET, "utf-8", KATTR__MAX)
+		|| khtml_elem(html, KELEM_TITLE)
+		|| khtml_puts(html, title)
+		|| khtml_closeelem(html, 1)
+		|| stylesheet(html)
+		|| khtml_elem(html, KELEM_H1)
+		|| khtml_puts(html, title)
+		|| khtml_closeelem(html, 1);
+}
+
+enum kcgi_err
+htmlNav(struct khtmlreq *html, const char *network, const char *context) {
+	enum kcgi_err error = 0
+		|| khtml_elem(html, KELEM_NAV)
+		|| khtml_elem(html, KELEM_OL)
+		|| khtml_elem(html, KELEM_LI)
+		|| khtml_attr(html, KELEM_A, KATTR_HREF, Pages[Networks], KATTR__MAX)
+		|| khtml_puts(html, "Networks")
+		|| khtml_closeelem(html, 2);
+	if (error) return error;
+
+	if (network) {
+		char *href = khttp_urlpart(
+			NULL, NULL, Pages[Contexts],
+			Keys[Network].name, network,
+			NULL
+		);
+		if (!href) err(EX_OSERR, "khttp_urlpart");
+		error = 0
+			|| khtml_elem(html, KELEM_LI)
+			|| khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
+			|| khtml_puts(html, network)
+			|| khtml_closeelem(html, 2);
+		if (error) return error;
+	}
+
+	if (network && context) {
+		char *href = khttp_urlpart(
+			NULL, NULL, Pages[Events],
+			Keys[Network].name, network,
+			Keys[Context].name, context,
+			NULL
+		);
+		if (!href) err(EX_OSERR, "khttp_urlpart");
+		error = 0
+			|| khtml_elem(html, KELEM_LI)
+			|| khtml_attr(html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
+			|| khtml_puts(html, context)
+			|| khtml_closeelem(html, 2);
+		if (error) return error;
+	}
+
+	return khtml_closeelem(html, 2);
+}
diff --git a/networks.c b/networks.c
new file mode 100644
index 0000000..15ca95d
--- /dev/null
+++ b/networks.c
@@ -0,0 +1,65 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "server.h"
+
+const char *NetworksQuery = SQL(
+	SELECT DISTINCT network
+	FROM contexts
+	ORDER BY network;
+);
+
+enum kcgi_err pageNetworks(struct kreq *req) {
+	enum kcgi_err error = httpHead(req, KHTTP_200, KMIME_TEXT_HTML);
+	if (req->method == KMETHOD_HEAD) return error;
+
+	struct khtmlreq html;
+	error = error
+		|| khttp_body(req)
+		|| khtml_open(&html, req, KHTML_PRETTY)
+		|| htmlHead(&html, "Networks")
+		|| htmlNav(&html, NULL, NULL)
+		|| khtml_elem(&html, KELEM_UL);
+	if (error) return error;
+
+	int result;
+	while (SQLITE_ROW == (result = sqlite3_step(stmt.networks))) {
+		const char *network = (const char *)sqlite3_column_text(stmt.networks, 0);
+		char *href = khttp_urlpart(
+			NULL, NULL, Pages[Contexts],
+			Keys[Network].name, network,
+			NULL
+		);
+		if (!href) err(EX_OSERR, "khttp_urlpart");
+
+		error = 0
+			|| khtml_elem(&html, KELEM_LI)
+			|| khtml_attr(&html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
+			|| khtml_puts(&html, network)
+			|| khtml_closeelem(&html, 2);
+		free(href);
+		if (error) break;
+	}
+	if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
+	sqlite3_reset(stmt.networks);
+
+	return error || khtml_close(&html);
+}
diff --git a/scooper.c b/scooper.c
deleted file mode 100644
index 2c7b58a..0000000
--- a/scooper.c
+++ /dev/null
@@ -1,319 +0,0 @@
-/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
- *
- * 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 <http://www.gnu.org/licenses/>.
- */
-
-#include <err.h>
-#include <stdarg.h>
-#include <stdbool.h>
-#include <stdint.h>
-#include <stdio.h>
-#include <stdlib.h>
-#include <sys/types.h>
-#include <sysexits.h>
-#include <unistd.h>
-
-#include <kcgi.h>
-#include <kcgihtml.h>
-#include <sqlite3.h>
-
-#if KCGI_VMAJOR == 0 && KCGI_VMINOR < 12
-#define khttp_urlpart(...) kutil_urlpart(req, __VA_ARGS__)
-#endif
-
-#define SQL(...) #__VA_ARGS__
-
-enum { DatabaseVersion = 4 };
-
-static sqlite3 *db;
-
-static int dbParam(sqlite3_stmt *stmt, const char *param) {
-	int index = sqlite3_bind_parameter_index(stmt, param);
-	if (index) return index;
-	errx(EX_SOFTWARE, "no such parameter %s: %s", param, sqlite3_sql(stmt));
-}
-
-static void
-dbBindInt(sqlite3_stmt *stmt, const char *param, sqlite3_int64 value) {
-	if (!sqlite3_bind_int64(stmt, dbParam(stmt, param), value)) return;
-	errx(EX_SOFTWARE, "sqlite3_bind_int64: %s", sqlite3_errmsg(db));
-}
-
-static void
-dbBindText(sqlite3_stmt *stmt, const char *param, const char *value) {
-	if (!sqlite3_bind_text(stmt, dbParam(stmt, param), value, -1, NULL)) return;
-	errx(EX_SOFTWARE, "sqlite3_bind_text: %s", sqlite3_errmsg(db));
-}
-
-static struct {
-	sqlite3_stmt *networks;
-	sqlite3_stmt *contexts;
-	sqlite3_stmt *events;
-	sqlite3_stmt *search;
-} stmt;
-
-static void dbClose(void) {
-	if (stmt.networks) sqlite3_finalize(stmt.networks);
-	if (stmt.contexts) sqlite3_finalize(stmt.contexts);
-	if (stmt.events) sqlite3_finalize(stmt.events);
-	if (stmt.search) sqlite3_finalize(stmt.search);
-	sqlite3_close(db);
-}
-
-#define ENUM_PAGES \
-	X(Networks, "networks") \
-	X(Contexts, "contexts") \
-	X(Events, "events") \
-	X(Search, "search")
-
-enum {
-#define X(page, path) page,
-	ENUM_PAGES
-#undef X
-	PagesLen,
-};
-
-static const char *Pages[PagesLen] = {
-#define X(page, path) [page] = path,
-	ENUM_PAGES
-#undef X
-};
-
-#define ENUM_KEYS \
-	X(Network, "network", kvalid_stringne) \
-	X(Context, "context", kvalid_stringne) \
-	X(After, "after", kvalid_stringne) \
-	X(Query, "query", kvalid_stringne)
-
-enum {
-#define X(key, name, valid) key,
-	ENUM_KEYS
-#undef X
-	KeysLen,
-};
-
-static const struct kvalid Keys[KeysLen] = {
-#define X(key, name, valid) [key] = { valid, name },
-	ENUM_KEYS
-#undef X
-};
-
-static enum kcgi_err head(struct kreq *req, enum khttp http, enum kmime mime) {
-	return khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[http])
-		|| khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
-}
-
-static enum kcgi_err fail(struct kreq *req, enum khttp http) {
-	return head(req, http, KMIME_TEXT_PLAIN)
-		|| khttp_body(req)
-		|| khttp_puts(req, khttps[http])
-		|| khttp_putc(req, '\n');
-}
-
-static const char *stylesheet;
-static enum kcgi_err htmlStylesheet(struct khtmlreq *html) {
-	if (!stylesheet) return KCGI_OK;
-	return khtml_attr(
-		html, KELEM_LINK,
-		KATTR_REL, "stylesheet",
-		KATTR_HREF, stylesheet,
-		KATTR__MAX
-	);
-}
-
-static enum kcgi_err htmlHead(struct khtmlreq *html, const char *title) {
-	return khtml_elem(html, KELEM_DOCTYPE)
-		|| khtml_attr(html, KELEM_META, KATTR_CHARSET, "utf-8", KATTR__MAX)
-		|| khtml_elem(html, KELEM_TITLE)
-		|| khtml_puts(html, title)
-		|| khtml_closeelem(html, 1)
-		|| htmlStylesheet(html)
-		|| khtml_elem(html, KELEM_H1)
-		|| khtml_puts(html, title)
-		|| khtml_closeelem(html, 1);
-}
-
-static const char *NetworksQuery = SQL(
-	SELECT DISTINCT network
-	FROM contexts
-	ORDER BY network;
-);
-
-static enum kcgi_err networks(struct kreq *req) {
-	struct khtmlreq html;
-	enum kcgi_err error = head(req, KHTTP_200, KMIME_TEXT_HTML)
-		|| khttp_body(req)
-		|| khtml_open(&html, req, KHTML_PRETTY)
-		|| htmlHead(&html, "Networks")
-		|| khtml_elem(&html, KELEM_UL);
-	if (error) return error;
-
-	int result;
-	while (SQLITE_ROW == (result = sqlite3_step(stmt.networks))) {
-		const char *network = (const char *)sqlite3_column_text(stmt.networks, 0);
-		char *href = khttp_urlpart(
-			NULL, NULL, Pages[Contexts], Keys[Network].name, network, NULL
-		);
-		if (!href) err(EX_OSERR, "khttp_urlpart");
-
-		error = khtml_elem(&html, KELEM_LI)
-			|| khtml_attr(&html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
-			|| khtml_puts(&html, network)
-			|| khtml_closeelem(&html, 2);
-		free(href);
-		if (error) break;
-	}
-	if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
-
-	sqlite3_reset(stmt.networks);
-	return error || khtml_close(&html);
-}
-
-static const char *ContextsQuery = SQL(
-	SELECT name
-	FROM contexts
-	WHERE network = :network AND coalesce(query = :query, true)
-	ORDER BY query, name;
-);
-
-bool public = false;
-
-static enum kcgi_err contexts(struct kreq *req) {
-	if (!req->fieldmap[Network]) return fail(req, KHTTP_404);
-	const char *network = req->fieldmap[Network]->parsed.s;
-
-	struct khtmlreq html;
-	enum kcgi_err error = head(req, KHTTP_200, KMIME_TEXT_HTML)
-		|| khttp_body(req)
-		|| khtml_open(&html, req, KHTML_PRETTY)
-		|| htmlHead(&html, network)
-		|| khtml_elem(&html, KELEM_UL);
-	if (error) return error;
-
-	dbBindText(stmt.contexts, ":network", network);
-	if (public) dbBindInt(stmt.contexts, ":query", false);
-
-	int result;
-	while (SQLITE_ROW == (result = sqlite3_step(stmt.contexts))) {
-		const char *context = (const char *)sqlite3_column_text(stmt.contexts, 0);
-		char *href = khttp_urlpart(
-			NULL, NULL, Pages[Events],
-			Keys[Network].name, network,
-			Keys[Context].name, context,
-			NULL
-		);
-		if (!href) err(EX_OSERR, "khttp_urlpart");
-
-		error = khtml_elem(&html, KELEM_LI)
-			|| khtml_attr(&html, KELEM_A, KATTR_HREF, href, KATTR__MAX)
-			|| khtml_puts(&html, context)
-			|| khtml_closeelem(&html, 2);
-		free(href);
-		if (error) break;
-	}
-	if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
-
-	sqlite3_reset(stmt.contexts);
-	return error || khtml_close(&html);
-}
-
-static enum kcgi_err events(struct kreq *req) {
-	return KCGI_OK;
-}
-
-static enum kcgi_err search(struct kreq *req) {
-	return KCGI_OK;
-}
-
-static enum kcgi_err request(struct kreq *req) {
-	if (req->method != KMETHOD_HEAD && req->method != KMETHOD_GET) {
-		return fail(req, KHTTP_405);
-	}
-	switch (req->page) {
-		case Networks: return networks(req);
-		case Contexts: return contexts(req);
-		case Events:   return events(req);
-		case Search:   return search(req);
-		default:       return fail(req, KHTTP_404);
-	}
-}
-
-static void prepare(sqlite3_stmt **stmt, const char *query) {
-	int error = sqlite3_prepare_v3(
-		db, query, -1, SQLITE_PREPARE_PERSISTENT, stmt, NULL
-	);
-	if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), query);
-}
-
-int main(int argc, char *argv[]) {
-	bool fastCGI = false;
-
-	for (int opt; 0 < (opt = getopt(argc, argv, "fps:"));) {
-		switch (opt) {
-			break; case 'f': fastCGI = true;
-			break; case 'p': public = true;
-			break; case 's': stylesheet = optarg;
-			break; default:  return EX_USAGE;
-		}
-	}
-	if (optind == argc) errx(EX_USAGE, "database path required");
-
-	int error = sqlite3_open_v2(argv[optind], &db, SQLITE_OPEN_READONLY, NULL);
-	if (error) errx(EX_NOINPUT, "%s: %s", argv[optind], sqlite3_errmsg(db));
-	atexit(dbClose);
-
-	sqlite3_stmt *check;
-	error = sqlite3_prepare_v2(
-		db, SQL(PRAGMA user_version;), -1, &check, NULL
-	);
-	if (error) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
-
-	error = sqlite3_step(check);
-	if (error != SQLITE_ROW) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
-
-	int version = sqlite3_column_int(check, 0);
-	if (version != DatabaseVersion) {
-		errx(EX_DATAERR, "unsupported database version %d", version);
-	}
-	sqlite3_finalize(check);
-
-	prepare(&stmt.networks, NetworksQuery);
-	prepare(&stmt.contexts, ContextsQuery);
-	
-	if (fastCGI) {
-		struct kfcgi *fcgi;
-		enum kcgi_err error = khttp_fcgi_init(
-			&fcgi, Keys, KeysLen, Pages, PagesLen, Networks
-		);
-		if (error) errx(EX_CONFIG, "khttp_fcgi_init: %s", kcgi_strerror(error));
-		for (
-			struct kreq req;
-			KCGI_OK == (error = khttp_fcgi_parse(fcgi, &req));
-			khttp_free(&req)
-		) {
-			error = request(&req);
-			if (error && error != KCGI_HUP) break;
-		}
-		errx(EX_PROTOCOL, "khttp_fcgi_parse: %s", kcgi_strerror(error));
-	} else {
-		struct kreq req;
-		enum kcgi_err error = khttp_parse(
-			&req, Keys, KeysLen, Pages, PagesLen, Networks
-		);
-		if (error) errx(EX_PROTOCOL, "khttp_parse: %s", kcgi_strerror(error));
-		error = request(&req);
-		if (error) errx(EX_PROTOCOL, "%s", kcgi_strerror(error));
-		khttp_free(&req);
-	}
-}
diff --git a/search.c b/search.c
new file mode 100644
index 0000000..c9a9044
--- /dev/null
+++ b/search.c
@@ -0,0 +1,26 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+
+#include "server.h"
+
+enum kcgi_err pageSearch(struct kreq *req) {
+	return httpFail(req, KHTTP_501);
+}
diff --git a/server.c b/server.c
new file mode 100644
index 0000000..de18e7d
--- /dev/null
+++ b/server.c
@@ -0,0 +1,123 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sysexits.h>
+#include <unistd.h>
+
+#include "server.h"
+
+sqlite3 *db;
+struct Statements stmt;
+
+static void prepare(sqlite3_stmt **stmt, const char *query) {
+	int error = sqlite3_prepare_v3(
+		db, query, -1, SQLITE_PREPARE_PERSISTENT, stmt, NULL
+	);
+	if (error) errx(EX_SOFTWARE, "%s: %s", sqlite3_errmsg(db), query);
+}
+
+const char *Pages[PagesLen] = {
+#define X(page, path) [page] = path,
+	ENUM_PAGES
+#undef X
+};
+
+const struct kvalid Keys[KeysLen] = {
+#define X(key, name, valid) [key] = { valid, name },
+	ENUM_KEYS
+#undef X
+};
+
+bool pagePublic;
+
+static enum kcgi_err request(struct kreq *req) {
+	if (req->method != KMETHOD_HEAD && req->method != KMETHOD_GET) {
+		return httpFail(req, KHTTP_405);
+	}
+	switch (req->page) {
+		case Networks: return pageNetworks(req);
+		case Contexts: return pageContexts(req);
+		case Events:   return pageEvents(req);
+		case Search:   return pageSearch(req);
+		default:       return httpFail(req, KHTTP_404);
+	}
+}
+
+int main(int argc, char *argv[]) {
+	bool fastCGI = false;
+
+	for (int opt; 0 < (opt = getopt(argc, argv, "fps:"));) {
+		switch (opt) {
+			break; case 'f': fastCGI = true;
+			break; case 'p': pagePublic = true;
+			break; case 's': htmlStylesheet = optarg;
+			break; default:  return EX_USAGE;
+		}
+	}
+	if (optind == argc) errx(EX_USAGE, "database path required");
+
+	int error = sqlite3_open_v2(argv[optind], &db, SQLITE_OPEN_READONLY, NULL);
+	if (error) errx(EX_NOINPUT, "%s: %s", argv[optind], sqlite3_errmsg(db));
+	atexit(dbClose);
+
+	sqlite3_stmt *check;
+	error = sqlite3_prepare_v2(
+		db, SQL(PRAGMA user_version;), -1, &check, NULL
+	);
+	if (error) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
+
+	error = sqlite3_step(check);
+	if (error != SQLITE_ROW) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db));
+
+	int version = sqlite3_column_int(check, 0);
+	if (version != DatabaseVersion) {
+		errx(EX_DATAERR, "unsupported database version %d", version);
+	}
+	sqlite3_finalize(check);
+
+	prepare(&stmt.networks, NetworksQuery);
+	prepare(&stmt.contexts, ContextsQuery);
+	
+	if (fastCGI) {
+		struct kfcgi *fcgi;
+		enum kcgi_err error = khttp_fcgi_init(
+			&fcgi, Keys, KeysLen, Pages, PagesLen, Networks
+		);
+		if (error) errx(EX_CONFIG, "khttp_fcgi_init: %s", kcgi_strerror(error));
+		for (
+			struct kreq req;
+			KCGI_OK == (error = khttp_fcgi_parse(fcgi, &req));
+			khttp_free(&req)
+		) {
+			error = request(&req);
+			if (error && error != KCGI_HUP) break;
+		}
+		errx(EX_PROTOCOL, "khttp_fcgi_parse: %s", kcgi_strerror(error));
+	} else {
+		struct kreq req;
+		enum kcgi_err error = khttp_parse(
+			&req, Keys, KeysLen, Pages, PagesLen, Networks
+		);
+		if (error) errx(EX_PROTOCOL, "khttp_parse: %s", kcgi_strerror(error));
+		error = request(&req);
+		if (error) errx(EX_PROTOCOL, "%s", kcgi_strerror(error));
+		khttp_free(&req);
+	}
+}
diff --git a/server.h b/server.h
new file mode 100644
index 0000000..80eb5bb
--- /dev/null
+++ b/server.h
@@ -0,0 +1,129 @@
+/* Copyright (C) 2020  C. McEnroe <june@causal.agency>
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ */
+
+#include <err.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <sys/types.h>
+#include <sysexits.h>
+
+#include <kcgi.h>
+#include <kcgihtml.h>
+#include <sqlite3.h>
+
+#if KCGI_VMAJOR == 0 && KCGI_VMINOR < 12
+#define khttp_urlpart(...) kutil_urlpart(NULL, __VA_ARGS__)
+#endif
+
+#define SQL(...) #__VA_ARGS__
+
+enum { DatabaseVersion = 4 };
+
+extern sqlite3 *db;
+
+extern const char *NetworksQuery;
+extern const char *ContextsQuery;
+extern const char *EventsQuery;
+extern const char *SearchQuery;
+
+extern struct Statements {
+	sqlite3_stmt *networks;
+	sqlite3_stmt *contexts;
+	sqlite3_stmt *events;
+	sqlite3_stmt *search;
+} stmt;
+
+static inline void dbClose(void) {
+	if (stmt.networks) sqlite3_finalize(stmt.networks);
+	if (stmt.contexts) sqlite3_finalize(stmt.contexts);
+	if (stmt.events) sqlite3_finalize(stmt.events);
+	if (stmt.search) sqlite3_finalize(stmt.search);
+	sqlite3_close(db);
+}
+
+static inline int dbParam(sqlite3_stmt *stmt, const char *param) {
+	int index = sqlite3_bind_parameter_index(stmt, param);
+	if (index) return index;
+	errx(EX_SOFTWARE, "no such parameter %s: %s", param, sqlite3_sql(stmt));
+}
+
+static inline void
+dbBindInt(sqlite3_stmt *stmt, const char *param, sqlite3_int64 value) {
+	if (!sqlite3_bind_int64(stmt, dbParam(stmt, param), value)) return;
+	errx(EX_SOFTWARE, "sqlite3_bind_int64: %s", sqlite3_errmsg(db));
+}
+
+static inline void
+dbBindText(sqlite3_stmt *stmt, const char *param, const char *value) {
+	if (!sqlite3_bind_text(stmt, dbParam(stmt, param), value, -1, NULL)) return;
+	errx(EX_SOFTWARE, "sqlite3_bind_text: %s", sqlite3_errmsg(db));
+}
+
+#define ENUM_PAGES \
+	X(Networks, "networks") \
+	X(Contexts, "contexts") \
+	X(Events, "events") \
+	X(Search, "search")
+
+enum {
+#define X(page, path) page,
+	ENUM_PAGES
+#undef X
+	PagesLen,
+};
+
+extern const char *Pages[PagesLen];
+
+#define ENUM_KEYS \
+	X(Network, "network", kvalid_stringne) \
+	X(Context, "context", kvalid_stringne) \
+	X(After, "after", kvalid_stringne) \
+	X(Query, "query", kvalid_stringne)
+
+enum {
+#define X(key, name, valid) key,
+	ENUM_KEYS
+#undef X
+	KeysLen,
+};
+
+extern const struct kvalid Keys[KeysLen];
+
+extern bool pagePublic;
+
+enum kcgi_err pageNetworks(struct kreq *req);
+enum kcgi_err pageContexts(struct kreq *req);
+enum kcgi_err pageEvents(struct kreq *req);
+enum kcgi_err pageSearch(struct kreq *req);
+
+static inline enum kcgi_err
+httpHead(struct kreq *req, enum khttp http, enum kmime mime) {
+	return khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[http])
+		|| khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[mime]);
+}
+
+static inline enum kcgi_err httpFail(struct kreq *req, enum khttp http) {
+	return httpHead(req, http, KMIME_TEXT_PLAIN)
+		|| khttp_body(req)
+		|| khttp_printf(req, "%s\n", khttps[http]);
+}
+
+extern const char *htmlStylesheet;
+enum kcgi_err htmlHead(struct khtmlreq *html, const char *title);
+enum kcgi_err htmlNav(
+	struct khtmlreq *html, const char *network, const char *context
+);