/* $Id$ */ /* * 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 #include #include #include "extern.h" /* * Start with five pages. * As you add more pages, you'll start by giving them an identifier key * in this enum. */ enum page { PAGE_INDEX, PAGE_BOOKS, PAGE_AUTHORS, PAGE_SERIES, PAGE__MAX }; enum key { KEY_PAGE, KEY_PERPAGE, KEY_SORT, KEY_ORDER, KEY_TERM, KEY__MAX }; static const char *const pages[PAGE__MAX] = { "index", /* PAGE_INDEX */ "books", /* PAGE_BOOKS */ "authors", /* PAGE_AUTHORS */ "series", /* PAGE_SERIES */ }; static const struct kvalid keys[KEY__MAX] = { { kvalid_int, "page" }, /* KEY_PAGE */ { kvalid_int, "perpage" }, /* KEY_PERPAGE */ { kvalid_string, "sort" }, /* KEY_SORT */ { kvalid_string, "order" }, /* KEY_ORDER */ { kvalid_stringne, "term" }, /* KEY_TERM */ }; /* * Fill out all HTTP secure headers. * Use the existing document's MIME type. */ static void http_alloc(struct kreq *r, enum khttp code) { khttp_head(r, kresps[KRESP_STATUS], "%s", khttps[code]); khttp_head(r, kresps[KRESP_CONTENT_TYPE], "%s", kmimetypes[r->mime]); khttp_head(r, "X-Content-Type-Options", "nosniff"); khttp_head(r, "X-Frame-Options", "DENY"); khttp_head(r, "X-XSS-Protection", "1; mode=block"); } /* * Fill out all headers with http_alloc() then start the HTTP document * body (no more headers after this point!) */ static void http_open(struct kreq *r, enum khttp code) { http_alloc(r, code); khttp_body(r); } static void puterror(struct kjsonreq *req, char *message) { kjson_obj_open(req); kjson_putstringp(req, "msg", message); kjson_obj_close(req); } /* * Append author object to json (NOT CLOSED). */ static void putauthor(struct kjsonreq *req, Author *a) { kjson_obj_open(req); kjson_putintp(req, "id", a->id); kjson_putstringp(req, "name", a->name); } /* * Append book object to json (NOT CLOSED). */ static void putbook(struct kjsonreq *req, Book *b) { kjson_obj_open(req); kjson_putintp(req, "id", b->id); kjson_putstringp(req, "title", b->title); if (b->s.id >= 0) { kjson_objp_open(req, "series"); kjson_putintp(req, "id", b->s.id); kjson_putstringp(req, "name", b->s.name); kjson_putdoublep(req, "idx", b->s_idx); kjson_obj_close(req); } } static void puttags(struct kjsonreq *req, BookAdv *b) { if (b->tsize > 0) { kjson_arrayp_open(req, "tags"); for (size_t i=0; itsize;i++) { kjson_putstring(req, b->tags[i]); } kjson_array_close(req); } } static void putauthors(struct kjsonreq *req, BookAdv *b) { if (b->asize > 0) { kjson_arrayp_open(req, "authors"); for (size_t j=0; jasize; j++) { putauthor(req, b->a[j]); kjson_obj_close(req); } kjson_array_close(req); } } static void putbookadv(struct kjsonreq *req, BookAdv *ba) { putbook(req, &ba->b); putauthors(req, ba); puttags(req, ba); kjson_obj_close(req); db_book_adv_free(ba); } static void putbookdata(struct kjsonreq *req, BookFull *b) { if (b->dsize > 0) { kjson_arrayp_open(req, "data"); for (size_t i=0;idsize;i++) { kjson_obj_open(req); kjson_putstringp(req, "name", b->data[i]->name); kjson_putstringp(req, "format", b->data[i]->format); kjson_putintp(req, "size", b->data[i]->size); kjson_obj_close(req); } kjson_array_close(req); } } static void putbookfull(struct kjsonreq *req, BookFull *b) { char tsstr[30]; putbook(req, &b->ba.b); kutil_epoch2str(b->timestamp, tsstr, sizeof(tsstr)); kjson_putstringp(req, "timestamp", tsstr); kjson_putintp(req, "pubdate", b->pubdate); kjson_putstringp(req, "isbn", b->isbn); kjson_putstringp(req, "lccn", b->lccn); kjson_putstringp(req, "path", b->path); kjson_putstringp(req, "uuid", b->uuid); kjson_putboolp(req, "has_cover", b->has_cover); kjson_putstringp(req, "lang", b->lang); if (NULL != b->publisher) kjson_putstringp(req, "publisher", b->publisher); } static const char* initsort(struct kreq *r) { if (NULL != r->fieldmap[KEY_SORT]) return r->fieldmap[KEY_SORT]->parsed.s; return NULL; } static const char* initorder(struct kreq *r) { if (NULL != r->fieldmap[KEY_ORDER]) return r->fieldmap[KEY_ORDER]->parsed.s; return NULL; } static int initpage(struct kreq *r) { if (NULL != r->fieldmap[KEY_PAGE]) return r->fieldmap[KEY_PAGE]->parsed.i; return 1; } static size_t inittermz(struct kreq *r) { size_t termz = 0; for (size_t j=0; jfieldsz; j++) { struct kpair field = r->fields[j]; if (KPAIR_VALID == field.state && 0 == strcmp(keys[KEY_TERM].name, field.key)) termz++; } return termz; } static void initterms(struct kreq *r, const char **terms) { int i = 0; for (size_t j=0; jfieldsz; j++) { struct kpair field = r->fields[j]; if (KPAIR_VALID == field.state && 0 == strcmp(keys[KEY_TERM].name, field.key)) terms[i++] = field.parsed.s; } } static int64_t idfrompath(struct kreq *r) { const char *errid; if (r->path[0] != '\0') return strtonum(r->path, INT64_MIN, INT64_MAX, &errid); return -1; } static int initperpage(struct kreq *r) { if (NULL != r->fieldmap[KEY_PERPAGE]) return r->fieldmap[KEY_PERPAGE]->parsed.i; return 20; } static void sendbooks(struct kreq *r) { struct kjsonreq req; BookFull *b = NULL; int res, i = 0; debug(r, "sendbooks"); int page = initpage(r); int per = initperpage(r); const char* sort = initsort(r); const char* order = initorder(r); size_t termz = inittermz(r); int64_t id = idfrompath(r); if (id >= 0) { b = db_book_load(r, id); } http_open(r, (id > 0 && NULL == b) ? KHTTP_404 : KHTTP_200); kjson_open(&req, r); if (id >= 0 && NULL == b) { puterror(&req, "Unknown book"); } else if (NULL != b) { putbookfull(&req, b); putauthors(&req, &b->ba); puttags(&req, &b->ba); putbookdata(&req, b); kjson_obj_close(&req); db_book_full_free(b); } else if (termz > 0) { const char **terms = kcalloc(termz, sizeof(char *)); initterms(r, terms); Book **books = kcalloc(per, sizeof(Book)); int c = db_books_search(r, per, books, terms, termz, 0); kjson_obj_open(&req); kjson_putintp(&req, "count", c); kjson_arrayp_open(&req, "books"); while (i < per && i < c) { putbook(&req, books[i]); kjson_obj_close(&req); i++; } kjson_array_close(&req); kjson_obj_close(&req); free(books); free(terms); } else { kjson_array_open(&req); BookAdv **books = kcalloc(per, sizeof(BookAdv)); res = db_books_load(r, books, sort, order, per, per * (page-1)); while (i < res) { putbookadv(&req, books[i]); i++; } free(books); kjson_array_close(&req); } kjson_close(&req); } static void sendauthors(struct kreq *r) { struct kjsonreq req; int res, i = 0; AuthorFull *a = NULL; debug(r, "sendauthors"); int page = initpage(r); int per = initperpage(r); const char* sort = initsort(r); const char* order = initorder(r); size_t termz = inittermz(r); int64_t id = idfrompath(r); http_open(r, KHTTP_200); kjson_open(&req, r); if (id >= 0) a = db_author_load(r, id); if (id >= 0 && NULL == a) { puterror(&req, "Unknown author"); } else if (id >= 0) { kjson_obj_open(&req); kjson_putintp(&req, "id", a->a.id); kjson_putstringp(&req, "name", a->a.name); if (a->bsize>0) { kjson_arrayp_open(&req, "books"); for (size_t j = 0; jbsize; j++) { putbookadv(&req, a->books[j]); } kjson_array_close(&req); } kjson_obj_close(&req); } else if (termz > 0) { const char **terms = kcalloc(termz, sizeof(char *)); initterms(r, terms); Author **authors = kcalloc(per, sizeof(Author)); int c = db_authors_search(r, per, authors, terms, termz, 0); kjson_obj_open(&req); kjson_putintp(&req, "count", c); kjson_arrayp_open(&req, "authors"); while (i < per && i < c) { kjson_obj_open(&req); kjson_putintp(&req, "id", authors[i]->id); kjson_putstringp(&req, "name", authors[i]->name); kjson_obj_close(&req); i++; } kjson_array_close(&req); kjson_obj_close(&req); free(authors); free(terms); } else { kjson_array_open(&req); AuthorAdv **authors = kcalloc(per, sizeof(AuthorAdv)); res = db_authors_load(r, authors, sort, order, per, per * (page-1)); while (i < res) { kjson_obj_open(&req); kjson_putintp(&req, "id", authors[i]->a.id); kjson_putstringp(&req, "name", authors[i]->a.name); kjson_putintp(&req, "count", authors[i]->count); kjson_obj_close(&req); db_author_adv_free(authors[i]); i++; } free(authors); kjson_array_close(&req); } kjson_close(&req); } static void sendseries(struct kreq *r) { struct kjsonreq req; int res, i = 0; SeriesFull *s = NULL; debug(r, "sendseries"); int page = initpage(r); int per = initperpage(r); size_t termz = inittermz(r); const char* sort = initsort(r); const char* order = initorder(r); int64_t id = idfrompath(r); http_open(r, KHTTP_200); kjson_open(&req, r); if (id >= 0) s = db_serie_load(r, id); if (id >= 0 && NULL == s) { puterror(&req, "Unknown series"); } else if (id >= 0) { kjson_obj_open(&req); kjson_putintp(&req, "id", s->s.s.id); kjson_putstringp(&req, "name", s->s.s.name); if (s->s.books>0) { kjson_arrayp_open(&req, "books"); for (int64_t j = 0; js.books; j++) { putbook(&req, s->b[j]); kjson_obj_close(&req); } kjson_array_close(&req); } if (s->s.asize > 0) { kjson_arrayp_open(&req, "authors"); for (size_t j=0; js.asize; j++) { putauthor(&req, s->s.a[j]); kjson_obj_close(&req); } kjson_array_close(&req); } kjson_obj_close(&req); } else if (termz > 0) { const char **terms = kcalloc(termz, sizeof(char *)); initterms(r, terms); Series **series = kcalloc(per, sizeof(Series)); int c = db_series_search(r, per, series, terms, termz, 0); kjson_obj_open(&req); kjson_putintp(&req, "count", c); kjson_arrayp_open(&req, "series"); while (i < per && i < c) { kjson_obj_open(&req); kjson_putintp(&req, "id", series[i]->id); kjson_putstringp(&req, "name", series[i]->name); kjson_obj_close(&req); i++; } kjson_array_close(&req); kjson_obj_close(&req); free(series); free(terms); } else { kjson_array_open(&req); SeriesAdv **series = kcalloc(per, sizeof(SeriesAdv)); res = db_series_load(r, series, sort, order, per, per * (page-1)); while (i < res) { kjson_obj_open(&req); kjson_putintp(&req, "id", series[i]->s.id); kjson_putstringp(&req, "name", series[i]->s.name); kjson_putintp(&req, "count", series[i]->books); if (series[i]->asize > 0) { kjson_arrayp_open(&req, "authors"); for (size_t j=0; jasize; j++) { kjson_obj_open(&req); kjson_putintp(&req, "id", series[i]->a[j]->id); kjson_putstringp(&req, "name", series[i]->a[j]->name); kjson_obj_close(&req); } kjson_array_close(&req); } kjson_obj_close(&req); db_series_adv_free(series[i]); i++; } free(series); kjson_array_close(&req); } kjson_close(&req); } static void sendindex(struct kreq *r) { struct kjsonreq req; debug(r, "sendindex"); http_open(r, KHTTP_200); kjson_open(&req, r); kjson_obj_open(&req); kjson_putintp(&req, "count", db_books_count(r)); kjson_obj_close(&req); kjson_close(&req); } int main(void) { struct kreq r; enum kcgi_err er; kutil_openlog(LOGFILE); /* Actually parse HTTP document. */ er = khttp_parsex(&r, ksuffixmap, kmimetypes, KMIME__MAX, keys, KEY__MAX, pages, PAGE__MAX, KMIME_APP_JSON, PAGE_INDEX, NULL, NULL, 0, NULL); if (KCGI_OK != er) { fprintf(stderr, "HTTP parse error: %d\n", er); return(EXIT_FAILURE); } #ifdef __OpenBSD__ if (-1 == pledge("stdio rpath cpath wpath flock fattr", NULL)) { fputs("pledge", stderr); khttp_free(&r); return(EXIT_FAILURE); } #endif /* * Front line of defence: make sure we're a proper method, make * sure we're a page, make sure we're a JSON file. */ if (KMETHOD_GET != r.method && KMETHOD_POST != r.method) { http_open(&r, KHTTP_405); khttp_free(&r); return(EXIT_SUCCESS); } else if (PAGE__MAX == r.page || KMIME_APP_JSON != r.mime) { http_open(&r, KHTTP_404); khttp_puts(&r, "Page not found."); khttp_free(&r); return(EXIT_SUCCESS); } debug(&r, "Opening database at %s/%s", DATADIR, DATABASE); if ( ! db_open(&r, DATADIR "/" DATABASE)) { kutil_warn(&r, NULL, "Error opening database at %s/%s", DATADIR, DATABASE); http_open(&r, KHTTP_500); json_emptydoc(&r); khttp_free(&r); return(EXIT_SUCCESS); } debug(&r, "Database ready %s/%s", DATADIR, DATABASE); switch (r.page) { case (PAGE_INDEX): sendindex(&r); break; case (PAGE_BOOKS): sendbooks(&r); break; case (PAGE_AUTHORS): sendauthors(&r); break; case (PAGE_SERIES): sendseries(&r); break; default: abort(); } db_close(&r); khttp_free(&r); return(EXIT_SUCCESS); }