diff options
author | June McEnroe <june@causal.agency> | 2020-07-09 18:57:29 -0400 |
---|---|---|
committer | June McEnroe <june@causal.agency> | 2020-07-09 18:57:29 -0400 |
commit | 7b521cf75376ccfd38c2ff077c854b408fe616ff (patch) | |
tree | 4e510cf548fc02e9a7cbbf6bc209259d5ec06428 | |
parent | Implement basic contexts listing (diff) | |
download | scooper-7b521cf75376ccfd38c2ff077c854b408fe616ff.tar.gz scooper-7b521cf75376ccfd38c2ff077c854b408fe616ff.zip |
Split code and add breadcrumb nav
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 14 | ||||
-rw-r--r-- | contexts.c | 72 | ||||
-rw-r--r-- | events.c | 26 | ||||
-rw-r--r-- | html.c | 91 | ||||
-rw-r--r-- | networks.c | 65 | ||||
-rw-r--r-- | scooper.c | 319 | ||||
-rw-r--r-- | search.c | 26 | ||||
-rw-r--r-- | server.c | 123 | ||||
-rw-r--r-- | server.h | 129 |
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 +); |