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 }