From 7b3ddf5cc9581d7442a420ef16a0be1be6676dcc Mon Sep 17 00:00:00 2001 From: Meutel Date: Sun, 18 Dec 2016 12:14:35 +0100 Subject: [PATCH] Quiz4: HTML + JS + JSON --- cmd.txt | 4 + quiz4.c | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ quiz4.js | 20 +++ 3 files changed, 392 insertions(+) create mode 100644 quiz4.c create mode 100644 quiz4.js diff --git a/cmd.txt b/cmd.txt index 17afba0..2c20fed 100644 --- a/cmd.txt +++ b/cmd.txt @@ -8,3 +8,7 @@ doas mkdir /var/www/tmp doas chmod 1777 /var/www/tmp cc -static -g -W -Wall -o build/example4 examples/example4.c -I/usr/local/include -L/usr/local/lib -lkcgi -lkcgihtml -lz -lsqlite3 doas install -o www -g www -m 0500 build/example4 /var/www/cgi-bin/ + +cc -static -g -W -Wall -o build/quiz4 quiz4.c -I/usr/local/include -L/usr/local/lib -lkcgi -lkcgihtml -lkcgijson -lz -lsqlite3 +doas install -o www -g www -m 0500 build/quiz4 /var/www/cgi-bin/ +doas install -o www -g www -m 0500 quiz4.js /var/www/htdocs/ diff --git a/quiz4.c b/quiz4.c new file mode 100644 index 0000000..8cea5fa --- /dev/null +++ b/quiz4.c @@ -0,0 +1,368 @@ +/* + * Copyright (c) 2016 Kristaps Dzonsons + * + * 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. + */ +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +enum page { + PAGE_INDEX, /* /index or just / */ + PAGE_JSON, /* /quiz4.json */ + PAGE__MAX +}; + +static const char *JSON_COUNT = "count"; + +static const char *const pages[PAGE__MAX] = { + "index", /* PAGE_INDEX */ + "quiz4", /* PAGE_JSON */ +}; + +typedef void (*disp)(struct kreq *); + +static void sendjson(struct kreq *req); +static void sendindex(struct kreq *req); + +static const disp disps[PAGE__MAX] = { + sendindex, /* PAGE_INDEX */ + sendjson, /* PAGE_JSON */ +}; + +/* + * 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("/tmp/example4.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); + } + } + bailed = 1; + goto again; + } + + return(NULL); +} + +static void +sendjson(struct kreq *req) +{ + struct kjsonreq r; + int rc; + sqlite3 *db; + sqlite3_stmt *stmt; + + resp_open(req, KHTTP_200); + kjson_open(&r, req); + kjson_obj_open(&r); + + 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) + kjson_putintp(&r, JSON_COUNT, + sqlite3_column_int64(stmt, 0)); + else if (SQLITE_DONE == rc) + kjson_putnullp(&r, JSON_COUNT); + // TODO err khtml_puts(&r, "No value!?"); + else + kjson_putnullp(&r, JSON_COUNT); + // TODO err khtml_puts(&r, "Error getting value."); + sqlite3_finalize(stmt); + } else + kjson_putnullp(&r, JSON_COUNT); + // TODO err 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 + kjson_putnullp(&r, JSON_COUNT); + // TODO err khtml_puts(&r, "Error opening database."); + + kjson_close(&r); +} + +static void +sendindex(struct kreq *req) +{ + struct khtmlreq r; + + 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 JSON"); + khtml_closeelem(&r, 1); + khtml_attr(&r, KELEM_SCRIPT, + KATTR_SRC, "/quiz4.js", + KATTR__MAX); + khtml_closeelem(&r, 2); + khtml_elem(&r, KELEM_BODY); + khtml_attr(&r, KELEM_DIV, + KATTR_ID, "count", + KATTR__MAX); + khtml_puts(&r, "No data."); + + khtml_close(&r); +} + +int +main(void) +{ + struct kreq r; + + /* + * Set up our main HTTP context. + */ + if (KCGI_OK != khttp_parse + (&r, NULL, 0, pages, PAGE__MAX, PAGE_INDEX)) + return(EXIT_FAILURE); + + 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_APP_JSON != r.mime && + 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. + */ + (*disps[r.page])(&r); + + /* + * Clean up our context. + */ + khttp_free(&r); + return(EXIT_SUCCESS); +} + diff --git a/quiz4.js b/quiz4.js new file mode 100644 index 0000000..166b889 --- /dev/null +++ b/quiz4.js @@ -0,0 +1,20 @@ +function loadJSON(path, success, error) +{ + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status === 200) { + success(JSON.parse(xhr.responseText)); + } else { + error(xhr); + } + } + }; + xhr.open("GET", path, true); + xhr.send(); +} + +loadJSON('/cgi-bin/quiz4/quiz4.json', + function(data) { document.getElementById("count").textContent = data.count; }, + function(xhr) { console.error(xhr); } +);