1 /*  $Id: kwebapp.main.c,v 1.3 2019/07/08 11:11:58 kristaps Exp $ */
    2 #include <sys/queue.h>
    3 
    4 #include <inttypes.h>
    5 #include <stdarg.h>
    6 #include <stdint.h>
    7 #include <stdio.h>
    8 #include <stdlib.h>
    9 #include <string.h>
   10 #include <time.h>
   11 #include <unistd.h>
   12 
   13 #include <kcgi.h>
   14 #include <ksql.h>
   15 #include <kcgijson.h>
   16 
   17 #include "kwebapp.db.h"
   18 
   19 enum  page {
   20   PAGE_HOME,
   21   PAGE_LOGIN,
   22   PAGE_LOGOUT,
   23   PAGE__MAX
   24 };
   25 
   26 static const char *const pages[PAGE__MAX] = {
   27   "home", /* PAGE_HOME */
   28   "login", /* PAGE_LOGIN */
   29   "logout", /* PAGE_LOGOUT */
   30 };
   31 
   32 /*
   33  * Fill out all HTTP secure headers.
   34  * Use the existing document's MIME type.
   35  */
   36 static void
   37 http_alloc(struct kreq *r, enum khttp code)
   38 {
   39 
   40   khttp_head(r, kresps[KRESP_STATUS], 
   41     "%s", khttps[code]);
   42   khttp_head(r, kresps[KRESP_CONTENT_TYPE], 
   43     "%s", kmimetypes[r->mime]);
   44   khttp_head(r, "X-Content-Type-Options", "nosniff");
   45   khttp_head(r, "X-Frame-Options", "DENY");
   46   khttp_head(r, "X-XSS-Protection", "1; mode=block");
   47 }
   48 
   49 /*
   50  * Fill out all headers with http_alloc() then start the HTTP document
   51  * body (no more headers after this point!)
   52  */
   53 static void
   54 http_open(struct kreq *r, enum khttp code)
   55 {
   56 
   57   http_alloc(r, code);
   58   khttp_body(r);
   59 }
   60 
   61 /*
   62  * Emit an empty JSON document.
   63  */
   64 static void
   65 json_emptydoc(struct kreq *r)
   66 {
   67   struct kjsonreq   req;
   68 
   69   kjson_open(&req, r);
   70   kjson_obj_open(&req);
   71   kjson_obj_close(&req);
   72   kjson_close(&req);
   73 }
   74 
   75 /*
   76  * Login a given user by their identifier.
   77  * Set the session cookie to expire in one year.
   78  * Return 400 on bad credentials or missing fields.
   79  * Return 200 on success.
   80  */
   81 static void
   82 sendlogin(struct kreq *r)
   83 {
   84   int64_t     sid, token;
   85   struct kpair  *kpi, *kpp;
   86   char     buf[64];
   87   struct user  *pp;
   88   time_t     t = time(NULL);
   89 
   90   if ((kpi = r->fieldmap[VALID_USER_EMAIL]) == NULL ||
   91       (kpp = r->fieldmap[VALID_USER_HASH]) == NULL) {
   92     http_open(r, KHTTP_400);
   93     json_emptydoc(r);
   94     return;
   95   } 
   96 
   97   pp = db_user_get_creds(r->arg, 
   98     kpi->parsed.s, kpp->parsed.s);
   99 
  100   if (pp == NULL) {
  101     http_open(r, KHTTP_400);
  102     json_emptydoc(r);
  103     return;
  104   }
  105 
  106   token = arc4random();
  107   sid = db_sess_insert(r->arg, pp->id, token);
  108   kutil_epoch2str(t + 60 * 60 * 24 * 365, buf, sizeof(buf));
  109 
  110   http_alloc(r, KHTTP_200);
  111   khttp_head(r, kresps[KRESP_SET_COOKIE],
  112     "%s=%" PRId64 "; secure; "
  113     "HttpOnly; path=/; expires=%s",
  114     valid_keys[VALID_SESS_TOKEN].name, token, buf);
  115   khttp_head(r, kresps[KRESP_SET_COOKIE],
  116     "%s=%" PRId64 "; secure; "
  117     "HttpOnly; path=/; expires=%s", 
  118     valid_keys[VALID_SESS_ID].name, sid, buf);
  119   khttp_body(r);
  120   json_emptydoc(r);
  121   db_user_free(pp);
  122 }
  123 
  124 /*
  125  * Homepage for users.
  126  * Returns 403 if not logged in or not in experiment state.
  127  * Returns 200 otherwise with empty document.
  128  */
  129 static void
  130 sendhome(struct kreq *r, const struct sess *u)
  131 {
  132   struct kjsonreq   req;
  133 
  134   http_open(r, KHTTP_200);
  135   kjson_open(&req, r);
  136   kjson_obj_open(&req);
  137   json_sess_obj(&req, u);
  138   kjson_obj_close(&req);
  139   kjson_close(&req);
  140 }
  141 
  142 static void
  143 sendlogout(struct kreq *r, const struct sess *us)
  144 {
  145   char     buf[32];
  146 
  147   kutil_epoch2str(0, buf, sizeof(buf));
  148   http_alloc(r, KHTTP_200);
  149   khttp_head(r, kresps[KRESP_SET_COOKIE],
  150     "%s=; path=/; secure; HttpOnly; expires=%s", 
  151     valid_keys[VALID_SESS_TOKEN].name, buf);
  152   khttp_head(r, kresps[KRESP_SET_COOKIE],
  153     "%s=; path=/; secure; HttpOnly; expires=%s", 
  154     valid_keys[VALID_SESS_ID].name, buf);
  155   khttp_body(r);
  156   json_emptydoc(r);
  157   db_sess_delete_id(r->arg, us->id);
  158 }
  159 
  160 int
  161 main(void)
  162 {
  163   struct kreq   r;
  164   enum kcgi_err   er;
  165   struct sess  *us = NULL;
  166 
  167   /* Log into a separate logfile (not system log). */
  168 
  169   kutil_openlog(LOGFILE);
  170 
  171   /* Actually parse HTTP document. */
  172 
  173   er = khttp_parse(&r, valid_keys, VALID__MAX, 
  174     pages, PAGE__MAX, PAGE_HOME);
  175 
  176   if (er != KCGI_OK)
  177     return EXIT_FAILURE;
  178 
  179   /* Necessary pledge for SQLite. */
  180 
  181   if (pledge("stdio rpath cpath wpath flock fattr", NULL) == -1) {
  182     khttp_free(&r);
  183     return EXIT_FAILURE;
  184   }
  185 
  186   /*
  187    * Front line of defence: make sure we're a proper method, make
  188    * sure we're a page, make sure we're a JSON file.
  189    */
  190 
  191   if (r.method != KMETHOD_GET && 
  192       r.method != KMETHOD_POST) {
  193     http_open(&r, KHTTP_405);
  194     khttp_free(&r);
  195     return EXIT_SUCCESS;
  196   } else if (r.page == PAGE__MAX|| 
  197              r.mime != KMIME_APP_JSON) {
  198     http_open(&r, KHTTP_404);
  199     khttp_puts(&r, "Page not found.");
  200     khttp_free(&r);
  201     return EXIT_SUCCESS;
  202   }
  203 
  204   r.arg = db_open(DATADIR "/" DATABASE);
  205   if (r.arg == NULL) {
  206     http_open(&r, KHTTP_500);
  207     json_emptydoc(&r);
  208     khttp_free(&r);
  209     return EXIT_SUCCESS;
  210   }
  211 
  212   if (r.page == PAGE_HOME) {
  213     if (r.cookiemap[VALID_SESS_ID] != NULL &&
  214         r.cookiemap[VALID_SESS_TOKEN] != NULL)
  215       us = db_sess_get_creds(r.arg, 
  216         r.cookiemap[VALID_SESS_TOKEN]->parsed.i,
  217         r.cookiemap[VALID_SESS_ID]->parsed.i);
  218     if (us == NULL) {
  219       http_open(&r, KHTTP_403);
  220       json_emptydoc(&r);
  221       db_close(r.arg);
  222       khttp_free(&r);
  223       return EXIT_SUCCESS;
  224     }
  225   }
  226 
  227   switch (r.page) {
  228   case PAGE_HOME:
  229     sendhome(&r, us);
  230     break;
  231   case PAGE_LOGIN:
  232     sendlogin(&r);
  233     break;
  234   case PAGE_LOGOUT:
  235     sendlogout(&r, us);
  236     break;
  237   default:
  238     abort();
  239   }
  240 
  241   db_sess_free(us);
  242   db_close(r.arg);
  243   khttp_free(&r);
  244   return EXIT_SUCCESS;
  245 }