/* Copyright (C) 2020 C. McEnroe * * 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 . */ #include #include #include #include #include #include #include #include #include #include #include #include #define SQL(...) #__VA_ARGS__ enum { DatabaseVersion = 4 }; static sqlite3 *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 htmlHead(struct khtmlreq *html, const char *title) { enum kcgi_err error = 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); if (error) return error; if (stylesheet) { error = khtml_attr( html, KELEM_LINK, KATTR_REL, "stylesheet", KATTR_HREF, stylesheet, KATTR__MAX ); if (error) return error; } return 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) goto fail; int result; while (SQLITE_ROW == (result = sqlite3_step(stmt.networks))) { const char *network = 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) goto fail; } if (result != SQLITE_DONE) errx(EX_SOFTWARE, "%s", sqlite3_errmsg(db)); error = khtml_close(&html); fail: sqlite3_reset(stmt.networks); return error; } static enum kcgi_err contexts(struct kreq *req) { return KCGI_OK; } 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; bool public = 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); 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); } }