#include #include #include #include #include #include "../lib/md4c/src/entity.h" #include "../lib/md4c/src/entity.c" #include "../lib/md4c/src/md4c.h" #include "../lib/md4c/src/md4c.c" #include "../lib/md4c/src/md4c-html.h" #include "../lib/md4c/src/md4c-html.c" #include "assets.h" // FIXME: Why does compilation fail if this is a .c file? #include "queries.h" KORE_SECCOMP_FILTER("clog", KORE_SYSCALL_ALLOW(bind), KORE_SYSCALL_ALLOW(getdents64), KORE_SYSCALL_ALLOW(newfstatat), KORE_SYSCALL_ALLOW(sendmmsg), KORE_SYSCALL_ALLOW(uname) ) enum content_type { JSON, HTML }; enum query_status { QUERY_STATUS_OK, QUERY_STATUS_ERROR, QUERY_STATUS_NOT_FOUND }; struct post_query { const char *id; enum content_type type; int status; struct kore_buf *result; }; static const char *accept_json = "application/json"; static const char *database = "db"; static const char * const error_msg[] = { [HTTP_STATUS_BAD_REQUEST] = "There was an error processing the request data.", [HTTP_STATUS_NOT_FOUND] = "Resource not found.", [HTTP_STATUS_INTERNAL_ERROR] = "There was an error processing the request.", }; void post_query_init(struct post_query *pq); void post_query_cleanup(struct post_query *pq); int validate_uuid(const char *uuid); enum content_type get_content_type(struct http_request *req); int http_ok_resp( struct http_request *req, enum content_type type, enum http_status_code status, struct kore_buf *result ); int http_err_resp( struct http_request *req, enum content_type type, enum http_status_code status ); int redirect(struct http_request *req); int get_posts(struct http_request *req); int get_posts_resource(struct http_request *req); int post_posts(struct http_request *req); int sql_select_posts(struct post_query *pq); int sql_insert_posts( const char *id, const char *title, const char *body ); int sql_render_posts(struct kore_pgsql *sql, struct post_query *pq); static void process_md_output(const MD_CHAR *, MD_SIZE size, void *); static int render_md(const char *, struct kore_buf *); void post_query_init(struct post_query *pq) { pq->id = NULL; pq->type = JSON; pq->status = QUERY_STATUS_OK; pq->result = kore_buf_alloc(0); } void post_query_cleanup(struct post_query *pq) { if (pq->result != NULL) kore_buf_free(pq->result); pq->result = NULL; } int validate_uuid(const char *uuid) { int i = 0; if (strlen(uuid) != 36) return KORE_RESULT_ERROR; for (i = 0; i <= 36; i++) { if ((i == 8) || (i == 13) || (i == 18) || (i == 23)) { if (uuid[i] != '-') return KORE_RESULT_ERROR; continue; } if (i == 36) { if (uuid[i] != '\0') return KORE_RESULT_ERROR; continue; } if (!isxdigit(uuid[i])) return KORE_RESULT_ERROR; } return KORE_RESULT_OK; } enum content_type get_content_type(struct http_request *req) { int err = 0; const char *accept = NULL; err = http_request_header(req, "accept", &accept); if (err == KORE_RESULT_OK) { kore_log(LOG_DEBUG, "Accept: %s", accept); if (strncmp(accept, accept_json, sizeof(*accept_json)) == 0) return JSON; } return HTML; } int http_ok_resp( struct http_request *req, enum content_type type, enum http_status_code status, struct kore_buf *result ) { struct kore_buf *resp_buf = kore_buf_alloc(0); const char *body = kore_buf_stringify(result, NULL); if (type == JSON) { http_response_header(req, "content-type", "application/json; charset=utf-8"); kore_buf_append( resp_buf, body, strlen(body) ); } else { http_response_header(req, "content-type", "text/html; charset=utf-8"); kore_buf_append(resp_buf, asset_index_begin_html, asset_len_index_begin_html); kore_buf_append( resp_buf, body, strlen(body) ); kore_buf_append(resp_buf, asset_index_end_html, asset_len_index_end_html); } http_response( req, status, resp_buf->data, resp_buf->offset ); kore_buf_free(resp_buf); return KORE_RESULT_OK; } int http_err_resp( struct http_request *req, enum content_type type, enum http_status_code status ) { struct kore_buf *resp_buf = kore_buf_alloc(0); if (type == JSON) { http_response_header(req, "content-type", "application/json; charset=utf-8"); kore_buf_appendf( resp_buf, (const char *) asset_error_json, error_msg[status] ); } else { http_response_header(req, "content-type", "text/html; charset=utf-8"); kore_buf_append(resp_buf, asset_index_begin_html, asset_len_index_begin_html); kore_buf_appendf( resp_buf, (const char *) asset_error_html, error_msg[status] ); kore_buf_append(resp_buf, asset_index_end_html, asset_len_index_end_html); } http_response( req, status, resp_buf->data, resp_buf->offset ); kore_buf_free(resp_buf); return KORE_RESULT_OK; } int redirect(struct http_request *req) { http_response_header(req, "Location", "/"); http_response(req, HTTP_STATUS_MOVED_PERMANENTLY, NULL, 0); return KORE_RESULT_OK; } int get_posts(struct http_request *req) { struct post_query pq; post_query_init(&pq); pq.type = get_content_type(req); (void) sql_select_posts(&pq); if (pq.status != QUERY_STATUS_OK) http_err_resp(req, pq.type, HTTP_STATUS_INTERNAL_ERROR); else http_ok_resp(req, pq.type, HTTP_STATUS_OK, pq.result); post_query_cleanup(&pq); return KORE_RESULT_OK; } int get_posts_resource(struct http_request *req) { int err = 0; struct post_query pq; post_query_init(&pq); pq.id = req->path + strlen("/posts/"); pq.type = get_content_type(req); // Check for valid resource UUID kore_log(LOG_DEBUG, "Resource id /posts/%s.", pq.id); err = validate_uuid(pq.id); if (err == KORE_RESULT_ERROR) { kore_log(LOG_ERR, "Invalid post id %s.", pq.id); http_err_resp(req, pq.type, HTTP_STATUS_NOT_FOUND); goto out; } (void) sql_select_posts(&pq); if (pq.status == QUERY_STATUS_NOT_FOUND) http_err_resp(req, pq.type, HTTP_STATUS_NOT_FOUND); else if (pq.status == QUERY_STATUS_ERROR) http_err_resp(req, pq.type, HTTP_STATUS_INTERNAL_ERROR); else http_ok_resp(req, pq.type, HTTP_STATUS_OK, pq.result); out: ; post_query_cleanup(&pq); return KORE_RESULT_OK; } int post_posts(struct http_request *req) { int err = 0; int status = HTTP_STATUS_CREATED; enum content_type type = get_content_type(req); const char *id = NULL; const char *title = NULL; const char *body = NULL; struct post_query pq; struct kore_json_item *item = NULL; struct kore_json json; kore_json_init(&json, req->http_body->data, req->http_body->length); if (!kore_json_parse(&json)) { status = HTTP_STATUS_BAD_REQUEST; kore_log(LOG_ERR, "error parsing json: %s\n", kore_json_strerror()); goto out; } item = kore_json_find_string(json.root, "id"); if (item != NULL) { id = item->data.string; kore_log(LOG_INFO, "id = '%s'\n", id); } else { status = HTTP_STATUS_BAD_REQUEST; kore_log(LOG_ERR, "error parsing id: %s\n", kore_json_strerror()); goto out; } // Check for valid resource ID/UUID err = validate_uuid(id); if (err == KORE_RESULT_ERROR) { status = HTTP_STATUS_BAD_REQUEST; kore_log(LOG_ERR, "Invalid post UUID %s.", id); goto out; } item = kore_json_find_string(json.root, "title"); if (item != NULL) { title = item->data.string; kore_log(LOG_INFO, "title = '%s'\n", title); } else { status = HTTP_STATUS_BAD_REQUEST; kore_log(LOG_ERR, "error parsing title: %s\n", kore_json_strerror()); goto out; } item = kore_json_find_string(json.root, "body"); if (item != NULL) { body = item->data.string; kore_log(LOG_INFO, "body = '%s'\n", body); } else { status = HTTP_STATUS_BAD_REQUEST; kore_log(LOG_ERR, "error parsing body: %s\n", kore_json_strerror()); goto out; } err = sql_insert_posts(id, title, body); if (err == KORE_RESULT_ERROR) { status = HTTP_STATUS_INTERNAL_ERROR; goto out; } out: ; if (status == HTTP_STATUS_CREATED) { post_query_init(&pq); pq.id = id; pq.type = type; (void) sql_select_posts(&pq); if (pq.status == QUERY_STATUS_NOT_FOUND) http_err_resp(req, pq.type, HTTP_STATUS_NOT_FOUND); else if (pq.status == QUERY_STATUS_ERROR) http_err_resp(req, pq.type, HTTP_STATUS_INTERNAL_ERROR); else http_ok_resp(req, pq.type, HTTP_STATUS_OK, pq.result); post_query_cleanup(&pq); } else http_err_resp(req, type, status); kore_json_cleanup(&json); return KORE_RESULT_OK; } int sql_select_posts(struct post_query *pq) { int err = 0; struct kore_pgsql sql; kore_pgsql_init(&sql); pq->status = QUERY_STATUS_OK; // Set up synchronous database handle. err = kore_pgsql_setup(&sql, database, KORE_PGSQL_SYNC); if (err == KORE_RESULT_ERROR) { pq->status = QUERY_STATUS_ERROR; kore_pgsql_logerror(&sql); goto out; } // Query for posts, check for error. if (pq->id != NULL) { // Query a post. err = kore_pgsql_query_params( &sql, pq->type == HTML ? query_html_post : query_json_post, 0, // return string data 1, // param count KORE_PGSQL_PARAM_TEXT(pq->id) ); } else { // Query all posts. err = kore_pgsql_query( &sql, pq->type == HTML ? query_html_posts : query_json_posts ); } if (err == KORE_RESULT_ERROR) { pq->status = QUERY_STATUS_ERROR; kore_pgsql_logerror(&sql); goto out; } // XXX Always tuples from the above Postgres queries, need to check the length for results. if (pq->type == JSON && kore_pgsql_getlength(&sql, 0, 0) == 0) { pq->status = QUERY_STATUS_NOT_FOUND; goto out; } // TODO: Add test for this; When database is empty, don't return 404 for base request. if (pq->id != NULL && kore_pgsql_ntuples(&sql) == 0) { pq->status = QUERY_STATUS_NOT_FOUND; goto out; } (void) sql_render_posts(&sql, pq); out: ; kore_pgsql_cleanup(&sql); return KORE_RESULT_OK; } int sql_render_posts(struct kore_pgsql *sql, struct post_query *pq) { int err = 0; int row = 0, rows = 0; struct kore_buf *html_buf = NULL; const char *id = NULL; const char *title = NULL; const char *created_at = NULL; const char *body = NULL; if (pq->type == JSON) { kore_buf_append( pq->result, kore_pgsql_getvalue(sql, 0, 0), kore_pgsql_getlength(sql, 0, 0) ); return KORE_RESULT_OK; } // Allocate a buffer to render the markdown as HTML into. html_buf = kore_buf_alloc(0); // pg->type == HTML // Iterate over posts and render them. rows = kore_pgsql_ntuples(sql); for (row = 0; row < rows; row++) { // Reset buffer. kore_buf_cleanup(html_buf); // Fetch data. id = kore_pgsql_getvalue(sql, row, 0); title = kore_pgsql_getvalue(sql, row, 1); created_at = kore_pgsql_getvalue(sql, row, 2); body = kore_pgsql_getvalue(sql, row, 3); kore_log(LOG_DEBUG, "id: '%s'; title '%s'", id, title); // Render MD. err = render_md(body, html_buf); if (err == KORE_RESULT_ERROR) { kore_log(LOG_ERR, "Error rendering markdown for entry %s.", id); continue; } // Append rendered MD post. kore_buf_appendf( pq->result, (const char *) asset_post_html, title, created_at, kore_buf_stringify(html_buf, NULL) ); } kore_buf_free(html_buf); return KORE_RESULT_OK; } int sql_insert_posts( const char *id, const char *title, const char *body ) { int err = KORE_RESULT_OK; struct kore_pgsql sql; kore_pgsql_init(&sql); err = kore_pgsql_setup(&sql, database, KORE_PGSQL_SYNC); if (err == KORE_RESULT_ERROR) { kore_pgsql_logerror(&sql); goto out; } err = kore_pgsql_query_params( &sql, "INSERT INTO posts (id, title, body) " "VALUES ($1, $2, $3) RETURNING id;", 0, 3, KORE_PGSQL_PARAM_TEXT(id), KORE_PGSQL_PARAM_TEXT(title), KORE_PGSQL_PARAM_TEXT(body) ); if (err == KORE_RESULT_ERROR) { kore_pgsql_logerror(&sql); goto out; } out: ; kore_pgsql_cleanup(&sql); return err; } static int render_md(const char *in, struct kore_buf *out) { int err = 0; static unsigned parser_flags = 0; static unsigned renderer_flags = MD_HTML_FLAG_DEBUG; err = md_html( in, (MD_SIZE) strlen(in), process_md_output, (void*) out, parser_flags, renderer_flags ); if(err != 0) { kore_log(LOG_ERR, "Parsing Markdown failed."); return KORE_RESULT_ERROR; } return KORE_RESULT_OK; } static void process_md_output(const MD_CHAR *html, MD_SIZE size, void *buf) { kore_buf_append( (struct kore_buf *) buf, (const void *) html, (size_t) size ); }