/* * Copyright (c) 2016 Kristaps Dzonsons <kristaps@bsd.lv> * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #if defined(__APPLE__) #include <sandbox.h> #endif #include <stdint.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <unistd.h> #include <kcgi.h> #include <kcgihtml.h> #include <sqlite3.h> enum page { PAGE_INDEX, /* /index or just / */ PAGE__MAX }; static const char *const pages[PAGE__MAX] = { "index", /* PAGE_INDEX */ }; /* * Generic routine to prime the HTTP document. */ static void resp_open(struct kreq *req, enum khttp http) { khttp_head(req, kresps[KRESP_STATUS], "%s", khttps[http]); khttp_head(req, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[req->mime]); khttp_body(req); } /* * When the database is busy or locked, sleep for a random amount of * time before continuing. * We could put all sorts of heuristics in here to back off, but we do * something really simple. */ static void db_sleep(size_t attempt) { if (attempt < 10) usleep(arc4random_uniform(100000)); else usleep(arc4random_uniform(500000)); } /* * Keep trying to open the database forever. */ static sqlite3 * db_open(void) { size_t attempt; sqlite3 *db; int rc; attempt = 0; again: rc = sqlite3_open(DATADIR "/example7.db", &db); if (SQLITE_BUSY == rc) { db_sleep(attempt++); goto again; } else if (SQLITE_LOCKED == rc) { fprintf(stderr, "sqlite3_open: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_PROTOCOL == rc) { fprintf(stderr, "sqlite3_open: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_OK == rc) { sqlite3_busy_timeout(db, 500); return(db); } fprintf(stderr, "sqlite3_open: %s\n", sqlite3_errmsg(db)); return(NULL); } /* * Close the database. */ static void db_close(sqlite3 *db) { if (SQLITE_OK == sqlite3_close(db)) return; fprintf(stderr, "sqlite3_close: %s\n", sqlite3_errmsg(db)); } /* * Step through the result set of a database query. */ static int db_step(sqlite3 *db, sqlite3_stmt *stmt) { int rc; size_t attempt = 0; again: rc = sqlite3_step(stmt); if (SQLITE_BUSY == rc) { db_sleep(attempt++); goto again; } else if (SQLITE_LOCKED == rc) { fprintf(stderr, "sqlite3_step: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_PROTOCOL == rc) { fprintf(stderr, "sqlite3_step: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } if (SQLITE_DONE == rc || SQLITE_ROW == rc) return(rc); fprintf(stderr, "sqlite3_step: %s\n", sqlite3_errmsg(db)); return(rc); } /* * Execute a database query. */ static int db_exec(sqlite3 *db, const char *sql) { size_t attempt = 0; int rc; again: rc = sqlite3_exec(db, sql, NULL, NULL, NULL); if (SQLITE_BUSY == rc) { db_sleep(attempt++); goto again; } else if (SQLITE_LOCKED == rc) { fprintf(stderr, "sqlite3_exec: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_PROTOCOL == rc) { fprintf(stderr, "sqlite3_exec: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_OK == rc) return(1); fprintf(stderr, "sqlite3_exec: %s (%s)\n", sqlite3_errmsg(db), sql); return(0); } /* * Prepare a database statement. * If we fail, try to populate the database: it might not have been * initialised yet. */ static sqlite3_stmt * db_stmt(sqlite3 *db, const char *sql, int recurse) { sqlite3_stmt *stmt; const char *esql; size_t attempt = 0; int rc, bailed; bailed = 0; again: rc = sqlite3_prepare_v2(db, sql, -1, &stmt, NULL); if (SQLITE_BUSY == rc) { db_sleep(attempt++); goto again; } else if (SQLITE_LOCKED == rc) { fprintf(stderr, "sqlite3_prepare_v2: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_PROTOCOL == rc) { fprintf(stderr, "sqlite3_prepare_v2: %s\n", sqlite3_errmsg(db)); db_sleep(attempt++); goto again; } else if (SQLITE_OK == rc) return(stmt); fprintf(stderr, "sqlite3_prepare_v2: %s (%s) (%d)\n", sqlite3_errmsg(db), sql, sqlite3_errcode(db)); sqlite3_finalize(stmt); /* * Only attempt to do this if (1) we have a certain error, (2) * we haven't already bailed out, and (3) we haven't recursed * into the db_stmt() function. */ if (SQLITE_ERROR == rc && 0 == bailed && recurse) { esql = "CREATE TABLE record " "(count INTEGER NOT NULL DEFAULT(0))"; if (db_exec(db, esql)) { esql = "INSERT INTO record DEFAULT VALUES"; if (NULL != (stmt = db_stmt(db, esql, 0))) { db_step(db, stmt); sqlite3_finalize(stmt); } } esql = "PRAGMA journal_mode=WAL"; (void)db_exec(db, esql); bailed = 1; goto again; } return(NULL); } static void sendindex(struct kreq *req) { struct khtmlreq r; int rc; sqlite3 *db; sqlite3_stmt *stmt; resp_open(req, KHTTP_200); khtml_open(&r, req); khtml_elem(&r, KELEM_DOCTYPE); khtml_elem(&r, KELEM_HTML); khtml_elem(&r, KELEM_HEAD); khtml_elem(&r, KELEM_TITLE); khtml_puts(&r, "Test"); khtml_closeelem(&r, 2); khtml_elem(&r, KELEM_BODY); if (NULL != (db = db_open())) { stmt = db_stmt(db, "SELECT count FROM record", 1); if (NULL != stmt) { rc = db_step(db, stmt); if (SQLITE_ROW == rc) khtml_puts(&r, (char *) sqlite3_column_text(stmt, 0)); else if (SQLITE_DONE == rc) khtml_puts(&r, "No value!?"); else khtml_puts(&r, "Error getting value."); sqlite3_finalize(stmt); } else khtml_puts(&r, "Error getting statement."); stmt = db_stmt(db, "UPDATE record SET count=count+1", 0); (void)db_step(db, stmt); sqlite3_finalize(stmt); db_close(db); } else khtml_puts(&r, "Error opening database."); khtml_close(&r); } int main(void) { struct kreq r; #if defined(__APPLE__) char *ep; int rc; #endif /* * Set up our main HTTP context. */ if (KCGI_OK != khttp_parse (&r, NULL, 0, pages, PAGE__MAX, PAGE_INDEX)) return(EXIT_FAILURE); /* * Set up a sandbox allowing for file-system access. * We need this for the database, which is WAL and will thus use * shared memory primitives. */ #if defined(__APPLE__) rc = sandbox_init (kSBXProfileNoNetwork, SANDBOX_NAMED, &ep); if (-1 == rc) { fprintf(stderr, "sandbox_init: %s\n", ep); sandbox_free_error(ep); khttp_free(&r); return(EXIT_FAILURE); } #elif defined(__OpenBSD__) if (-1 == pledge("stdio rpath cpath wpath flock", NULL)) { perror("pledge"); khttp_free(&r); return(EXIT_FAILURE); } #endif if (KMETHOD_OPTIONS == r.method) { /* * Indicate that we accept GET and POST methods. * This isn't really needed. */ khttp_head(&r, kresps[KRESP_STATUS], "%s", khttps[KHTTP_200]); khttp_head(&r, kresps[KRESP_ALLOW], "OPTIONS GET POST"); khttp_body(&r); } else if (KMETHOD_GET != r.method) { /* * Don't accept non-GET and non-POST methods. */ resp_open(&r, KHTTP_405); } else if (PAGE__MAX == r.page || KMIME_TEXT_HTML != r.mime) { /* * We've been asked for an unknown page or something * with an unknown extension. */ resp_open(&r, KHTTP_404); khttp_puts(&r, "Page not found."); } else /* * Route to page handler. */ sendindex(&r); /* * Clean up our context. */ khttp_free(&r); return(EXIT_SUCCESS); }