#include #include #include #include #include #include #include "sqlite3.h" #include "chttp.h" #include "WL.h" #include "sqlite3utils.h" sqlite3 *db; int load_file(char *file, char **data, long *size) { FILE *f = fopen(file, "rb"); if (f == NULL) return -1; fseek(f, 0, SEEK_END); *size = ftell(f); fseek(f, 0, SEEK_SET); *data = malloc(*size + 1); fread(*data, 1, *size, f); (*data)[*size] = '\0'; fclose(f); return 0; } static int query_routine(WL_State *state) { long long num_args; if (!WL_popint(state, &num_args)) { assert(0); // TODO } if (num_args == 0) { assert(0); // TODO } WL_String format; if (!WL_peekstr(state, -num_args, &format)) { assert(0); // TODO } sqlite3_stmt *res; int ret = sqlite3_prepare_v2(db, format.ptr, format.len, &res, 0); if (ret != SQLITE_OK) { fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); assert(0); // TODO } for (int i = 0; i < num_args-1; i++) { if (ret != SQLITE_OK) { fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); assert(0); // TODO } if (WL_peeknone(state, -num_args + i + 1)) { ret = sqlite3_bind_null(res, i+1); continue; } long long ival; if (WL_peekint(state, -num_args + i + 1, &ival)) { ret = sqlite3_bind_int64(res, i+1, ival); continue; } float fval; if (WL_peekfloat(state, -num_args + i + 1, &fval)) { ret = sqlite3_bind_double(res, i+1, fval); continue; } WL_String str; if (WL_peekstr(state, -num_args + i + 1, &str)) { ret = sqlite3_bind_text(res, i+1, str.ptr, str.len, NULL); continue; } break; } ret = sqlite3_step(res); if (ret == SQLITE_DONE) { WL_pushnone(state); } else if (ret != SQLITE_ROW) { WL_pushnone(state); } else { WL_pusharray(state, 0); do { int num_cols = sqlite3_column_count(res); if (num_cols < 0) { assert(0); // TODO } WL_pushmap(state, 0); for (int i = 0; i < num_cols; i++) { ret = sqlite3_column_type(res, i); switch (ret) { case SQLITE_INTEGER: { int64_t x = sqlite3_column_int64(res, i); WL_pushint(state, x); } break; case SQLITE_FLOAT: { double x = sqlite3_column_double(res, i); WL_pushfloat(state, x); } break; case SQLITE_TEXT: { const void *x = sqlite3_column_text(res, i); int n = sqlite3_column_bytes(res, i); WL_pushstr(state, (WL_String) { (char*) x, n }); } break; case SQLITE_BLOB: { const void *x = sqlite3_column_blob(res, i); int n = sqlite3_column_bytes(res, i); WL_pushstr(state, (WL_String) { (char*) x, n }); } break; case SQLITE_NULL: { WL_pushnone(state); } break; } const char *name = sqlite3_column_name(res, i); WL_pushstr(state, (WL_String) { (char*) name, strlen(name) }); WL_insert(state); } WL_append(state); } while (sqlite3_step(res) == SQLITE_ROW); } WL_pushint(state, 1); sqlite3_finalize(res); return 0; } int evaluate_template(HTTP_ResponseBuilder builder, WL_Program program, WL_Arena arena, char *err, int errmax, int user_id) { //WL_dump_program(program); WL_State *state = WL_State_init(&arena, program, err, errmax); if (state == NULL) return -1; WL_State_trace(state, 0); for (;;) { WL_Result result = WL_eval(state); switch (result.type) { case WL_DONE: WL_State_free(state); return 0; case WL_ERROR: WL_State_free(state); return -1; case WL_VAR: if (WL_streq(result.str, "login_user_id", -1)) { if (user_id < 0) WL_pushnone(state); else WL_pushint(state, user_id); } break; case WL_CALL: if (WL_streq(result.str, "query", -1)) { query_routine(state); break; } break; case WL_OUTPUT: http_response_builder_body(builder, (HTTP_String) { result.str.ptr, result.str.len }); break; } } return 0; } void evaluate_template_2(HTTP_ResponseBuilder builder, WL_Arena arena, char *file, int user_id) { http_response_builder_status(builder, 200); http_response_builder_header(builder, HTTP_STR("Content-Type: text/html")); WL_Compiler *compiler = WL_Compiler_init(&arena); if (compiler == NULL) { assert(0); // TODO } char *loaded_files[128]; int num_loaded_files = 0; WL_CompileResult result; WL_String path = { file, strlen(file) }; for (int i = 0;; i++) { char buf[1<<10]; if (path.len >= (int) sizeof(buf)) { assert(0); // TODO } memcpy(buf, path.ptr, path.len); buf[path.len] = '\0'; FILE *f = fopen(buf, "rb"); if (f == NULL) { http_response_builder_undo(builder); http_response_builder_status(builder, 500); http_response_builder_body(builder, HTTP_STR("Couldn't find file '")); http_response_builder_body(builder, (HTTP_String) { path.ptr, path.len }); http_response_builder_body(builder, HTTP_STR("'")); http_response_builder_done(builder); return; } fseek(f, 0, SEEK_END); long file_size = ftell(f); fseek(f, 0, SEEK_SET); char *file_data = malloc(file_size); fread(file_data, 1, file_size, f); fclose(f); result = WL_compile(compiler, path, (WL_String) { file_data, file_size }); loaded_files[num_loaded_files++] = file_data; if (result.type == WL_COMPILE_RESULT_ERROR) { printf("Compilation of '%.*s' failed\n", path.len, path.ptr); break; } if (result.type == WL_COMPILE_RESULT_DONE) break; assert(result.type == WL_COMPILE_RESULT_FILE); path = result.path; } for (int i = 0; i < num_loaded_files; i++) free(loaded_files[i]); WL_Compiler_free(compiler); if (result.type == WL_COMPILE_RESULT_ERROR) { http_response_builder_undo(builder); http_response_builder_status(builder, 500); http_response_builder_body(builder, HTTP_STR("Couldn't compile the template")); http_response_builder_done(builder); return; } WL_Program program = result.program; char err[1<<9]; if (evaluate_template(builder, program, arena, err, (int) sizeof(err), user_id) < 0) { http_response_builder_undo(builder); http_response_builder_status(builder, 500); http_response_builder_body(builder, (HTTP_String) { err, (int) strlen(err) }); http_response_builder_done(builder); return; } http_response_builder_done(builder); } int create_user(HTTP_String name, HTTP_String email, HTTP_String pass) { sqlite3_stmt *stmt; int ret = sqlite3utils_prepare(db, &stmt, "INSERT INTO Users(username, email, password) VALUES (?, ?, ?)", name, email, pass); if (ret != SQLITE_OK) return -500; ret = sqlite3_step(stmt); if (ret != SQLITE_DONE) { fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); // TODO: What if the user exists? sqlite3_finalize(stmt); return -500; } int64_t tmp = sqlite3_last_insert_rowid(db); if (tmp < 0 || tmp > INT_MAX) { sqlite3_finalize(stmt); return -500; } int user_id = (int) tmp; ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) { fprintf(stderr, "Failed to prepare statement: %s\n", sqlite3_errmsg(db)); sqlite3_finalize(stmt); return -500; } return user_id; } int user_exists(HTTP_String name, HTTP_String pass) { sqlite3_stmt *stmt; int ret = sqlite3utils_prepare(db, &stmt, "SELECT id FROM Users WHERE username=? AND password=?", name, pass); if (ret != SQLITE_OK) return -500; ret = sqlite3_step(stmt); if (ret == SQLITE_DONE) { sqlite3_finalize(stmt); return -404; } if (ret != SQLITE_ROW) { sqlite3_finalize(stmt); return -500; } int user_id = sqlite3_column_int(stmt, 0); if (user_id < 0) { sqlite3_finalize(stmt); return -500; } ret = sqlite3_finalize(stmt); if (ret != SQLITE_OK) return -500; return user_id; } HTTP_String http_getcookie(HTTP_Request *req, HTTP_String name) { // TODO: best-effort implementation for (int i = 0; i < req->num_headers; i++) { if (!http_streqcase(req->headers[i].name, HTTP_STR("Cookie"))) continue; char *src = req->headers[i].value.ptr; int len = req->headers[i].value.len; int cur = 0; // Cookie: name1=value1; name2=value2; name3=value3 for (;;) { while (cur < len && src[cur] == ' ') cur++; int off = cur; while (cur < len && src[cur] != '=') cur++; HTTP_String cookie_name = { src + off, cur - off }; if (cur == len) break; cur++; off = cur; while (cur < len && src[cur] != ';') cur++; HTTP_String cookie_value = { src + off, cur - off }; if (http_streq(name, cookie_name)) return cookie_value; if (cur == len) break; cur++; } } return HTTP_STR(""); } HTTP_String http_getparam(HTTP_String body, HTTP_String str) { // This is just a best-effort implementation char *src = body.ptr; int len = body.len; int cur = 0; if (cur < len && src[cur] == '?') cur++; while (cur < len) { HTTP_String name; { int off = cur; while (cur < len && src[cur] != '=' && src[cur] != '&') cur++; name = (HTTP_String) { src + off, cur - off }; } HTTP_String body = HTTP_STR(""); if (cur < len) { cur++; if (src[cur-1] == '=') { int off = cur; while (cur < len && src[cur] != '&') cur++; body = (HTTP_String) { src + off, cur - off }; if (cur < len) cur++; } } if (http_streq(str, name)) return body; } return HTTP_STR(""); } #define SESSION_LIMIT 1024 #define USERNAME_LIMIT 64 typedef struct { int sess_id; int user_id; } Session; typedef struct { Session sessions[SESSION_LIMIT]; int count; int next_sess_id; // TODO: this should be choosen randomly and on 64 bits } SessionSet; void session_set_init(SessionSet *set) { set->count = 0; set->next_sess_id = 1; } int session_set_add(SessionSet *set, int user_id) { if (set->count == SESSION_LIMIT) return -1; int sess_id = set->next_sess_id++; set->sessions[set->count++] = (Session) { .sess_id=sess_id, .user_id=user_id, }; return sess_id; } void session_set_remove(SessionSet *set, int sess_id) { int i = 0; while (i < set->count && set->sessions[i].sess_id != sess_id) i++; if (i == set->count) return; set->sessions[i] = set->sessions[--set->count]; } int session_set_find(SessionSet *set, int sess_id) { int i = 0; while (i < set->count && set->sessions[i].sess_id != sess_id) i++; if (i == set->count) return -1; return set->sessions[i].user_id; } bool valid_name(HTTP_String str) { (void) str; // TODO return true; } bool valid_email(HTTP_String str) { (void) str; // TODO return true; } bool valid_pass(HTTP_String str) { (void) str; // TODO return true; } bool valid_post_title(HTTP_String str) { (void) str; // TODO return true; } bool valid_post_content(HTTP_String str) { (void) str; // TODO return true; } int main(void) { http_global_init(); printf("%s\n", sqlite3_libversion()); int ret = sqlite3_open(":memory:", &db); if (ret != SQLITE_OK) { fprintf(stderr, "Cannot open database: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } char *schema_data; long schema_size; ret = load_file("schema.sql", &schema_data, &schema_size); if (ret < 0) { printf("Couldn't load schema\n"); return -1; } ret = sqlite3_exec(db, schema_data, NULL, NULL, NULL); if (ret != SQLITE_OK) { fprintf(stderr, "Cannot run schema: %s\n", sqlite3_errmsg(db)); sqlite3_close(db); return -1; } free(schema_data); HTTP_String addr = HTTP_STR("127.0.0.1"); uint16_t port = 8080; HTTP_Server *server = http_server_init(addr, port); if (server == NULL) return -1; http_server_set_trace(server, false); int pool_cap = 1<<20; char *pool = malloc(pool_cap); WL_Arena arena = { pool, pool_cap, 0 }; SessionSet sessions; session_set_init(&sessions); for (;;) { HTTP_Request *req; HTTP_ResponseBuilder builder; int ret = http_server_wait(server, &req, &builder); if (ret < 0) return -1; //printf("%.*s\n", req->raw.len, req->raw.ptr); // TODO // If logged in, these are set to non-negative values int sess_id = -1; int user_id = -1; { HTTP_String str = http_getcookie(req, HTTP_STR("sess_id")); if (str.len > 0) { char tmp[1<<9]; // TODO memcpy(tmp, str.ptr, str.len); tmp[str.len] = '\0'; sess_id = atoi(tmp); if (sess_id == 0) sess_id = -1; } user_id = session_set_find(&sessions, sess_id); } HTTP_String path = req->url.path; if (http_streq(path, HTTP_STR("/api/login"))) { if (req->method != HTTP_METHOD_POST) { http_response_builder_status(builder, 405); http_response_builder_done(builder); continue; } if (user_id != -1) { // Already logged in http_response_builder_status(builder, 303); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); continue; } HTTP_String name = http_getparam(req->body, HTTP_STR("username")); HTTP_String pass = http_getparam(req->body, HTTP_STR("password")); name = http_trim(name); pass = http_trim(pass); if (!valid_name(name) || !valid_pass(pass)) { http_response_builder_status(builder, 400); http_response_builder_done(builder); continue; } int ret = user_exists(name, pass); if (ret < 0) { http_response_builder_status(builder, -ret); http_response_builder_done(builder); continue; } int user_id = ret; int sess_id = session_set_add(&sessions, user_id); if (sess_id < 0) { http_response_builder_status(builder, 500); http_response_builder_done(builder); continue; } char cookie[1<<9]; int cookie_len = snprintf(cookie, sizeof(cookie), "Set-Cookie: sess_id=%d; Path=/; HttpOnly", sess_id); if (cookie_len < 0 || cookie_len >= (int) sizeof(cookie)) { http_response_builder_status(builder, 500); http_response_builder_done(builder); continue; } http_response_builder_status(builder, 303); // TODO: Whats the correct code here? http_response_builder_header(builder, (HTTP_String) { cookie, cookie_len }); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); } else if (http_streq(path, HTTP_STR("/api/signup"))) { if (req->method != HTTP_METHOD_POST) { http_response_builder_status(builder, 405); http_response_builder_done(builder); continue; } if (user_id != -1) { // Already logged in http_response_builder_status(builder, 303); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); continue; } HTTP_String name = http_getparam(req->body, HTTP_STR("username")); HTTP_String email = http_getparam(req->body, HTTP_STR("email")); HTTP_String pass1 = http_getparam(req->body, HTTP_STR("password1")); HTTP_String pass2 = http_getparam(req->body, HTTP_STR("password2")); if (!valid_name(name) || !valid_email(email) || !valid_pass(pass1)) { http_response_builder_status(builder, 400); http_response_builder_done(builder); continue; } if (!http_streq(pass1, pass2)) { http_response_builder_status(builder, 400); http_response_builder_done(builder); continue; } int ret = create_user(name, email, pass1); if (ret < 0) { http_response_builder_status(builder, -ret); http_response_builder_done(builder); continue; } user_id = ret; int sess_id = session_set_add(&sessions, user_id); if (sess_id < 0) { http_response_builder_status(builder, 500); http_response_builder_done(builder); continue; } char cookie[1<<9]; int cookie_len = snprintf(cookie, sizeof(cookie), "Set-Cookie: sess_id=%d; Path=/; HttpOnly", sess_id); if (cookie_len < 0 || cookie_len >= (int) sizeof(cookie)) { http_response_builder_status(builder, 500); http_response_builder_done(builder); continue; } http_response_builder_status(builder, 303); // TODO: Whats the correct code here? http_response_builder_header(builder, (HTTP_String) { cookie, cookie_len }); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); } else if (http_streq(path, HTTP_STR("/api/logout"))) { if (sess_id != -1) session_set_remove(&sessions, sess_id); http_response_builder_status(builder, 303); // TODO: Whats the correct code here? http_response_builder_header(builder, HTTP_STR("Set-Cookie: sess_id=; Path=/; HttpOnly")); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); } else if (http_streq(path, HTTP_STR("/api/post"))) { if (req->method != HTTP_METHOD_POST) { assert(0); // TODO } if (user_id == -1) { assert(0); // TODO } HTTP_String title = http_getparam(req->body, HTTP_STR("title")); HTTP_String content = http_getparam(req->body, HTTP_STR("content")); title = http_trim(title); content = http_trim(content); if (!valid_post_title(title) || !valid_post_content(content)) { assert(0); // TODO } // TODO } else if (http_streq(path, HTTP_STR("/index"))) { evaluate_template_2(builder, arena, "pages/index.wl", user_id); } else if (http_streq(path, HTTP_STR("/write"))) { if (user_id == -1) { // Not logged in http_response_builder_status(builder, 303); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); continue; } evaluate_template_2(builder, arena, "pages/write.wl", user_id); } else if (http_streq(path, HTTP_STR("/login"))) { if (user_id != -1) { // Already logged in http_response_builder_status(builder, 303); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); continue; } evaluate_template_2(builder, arena, "pages/login.wl", user_id); } else if (http_streq(path, HTTP_STR("/signup"))) { if (user_id != -1) { // Already logged in http_response_builder_status(builder, 303); http_response_builder_header(builder, HTTP_STR("Location: /index")); http_response_builder_done(builder); continue; } evaluate_template_2(builder, arena, "pages/signup.wl", user_id); } else if (http_streq(path, HTTP_STR("/thread"))) { evaluate_template_2(builder, arena, "pages/thread.wl", user_id); } else { evaluate_template_2(builder, arena, "pages/notfound.wl", user_id); } } sqlite3_close(db); http_server_free(server); http_global_free(); return 0; }