diff options
-rw-r--r-- | README.md | 1 | ||||
-rw-r--r-- | conf/clog.conf | 13 | ||||
-rw-r--r-- | src/clog.c | 596 | ||||
-rwxr-xr-x | tests.py | 16 |
4 files changed, 355 insertions, 271 deletions
@@ -36,6 +36,7 @@ seccomp_tracing no ## TODO * Have `tests.py` load and drop database. +* Test content-type in responses. Add functions: diff --git a/conf/clog.conf b/conf/clog.conf index 9653e94..1131930 100644 --- a/conf/clog.conf +++ b/conf/clog.conf @@ -23,17 +23,22 @@ domain * { filemap /static/ assets/static route / { - handler posts + handler get_posts methods get } route /posts { - handler posts - methods get post + handler get_posts + methods get + } + + route /posts { + handler post_posts + methods post } route ^/posts/[a-z0-9\-]+$ { - handler GET_post + handler get_posts_resource methods get } @@ -24,15 +24,16 @@ KORE_SECCOMP_FILTER("clog", KORE_SYSCALL_ALLOW(uname) ) -enum request_type { JSON, HTML }; +enum content_type { JSON, HTML }; -struct post_request { - struct http_request *req; - const char *resource; - enum request_type type; +enum query_status { QUERY_STATUS_OK, QUERY_STATUS_ERROR, QUERY_STATUS_NOT_FOUND }; - int resp_status; - struct kore_buf *resp_buf; +struct post_query { + const char *id; + enum content_type type; + + int status; + struct kore_buf *result; }; static const char *accept_json = "application/json"; @@ -44,66 +45,177 @@ static const char * const error_msg[] = { [HTTP_STATUS_INTERNAL_ERROR] = "There was an error processing the request.", }; -void post_request_init(struct post_request *post_req); -void post_request_cleanup(struct post_request *post_req); +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 validate_uuid(const char *input); +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_post(struct http_request *req); -int POST_post(struct http_request *req); -int posts(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 render_posts(struct http_request *req, const char *resource); -int query_posts(struct post_request *post_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_request_init(struct post_request *post_req) { - post_req->req = NULL; - post_req->resource = NULL; - post_req->type = JSON; +post_query_init(struct post_query *pq) { + pq->id = NULL; + pq->type = JSON; - post_req->resp_status = HTTP_STATUS_OK; - post_req->resp_buf = kore_buf_alloc(0); + pq->status = QUERY_STATUS_OK; + pq->result = kore_buf_alloc(0); } void -post_request_cleanup(struct post_request *post_req) { - if (post_req->resp_buf != NULL) - kore_buf_free(post_req->resp_buf); - post_req->resp_buf = NULL; +post_query_cleanup(struct post_query *pq) { + if (pq->result != NULL) + kore_buf_free(pq->result); + pq->result = NULL; } int -validate_uuid(const char *input) { +validate_uuid(const char *uuid) { int i = 0; - const char *p = NULL; - if (strlen(input) != 36) + if (strlen(uuid) != 36) return KORE_RESULT_ERROR; - for (i = 0, p = input; i <= 36; i++) { + for (i = 0; i <= 36; i++) { if ((i == 8) || (i == 13) || (i == 18) || (i == 23)) { - if (p[i] != '-') + if (uuid[i] != '-') return KORE_RESULT_ERROR; continue; } if (i == 36) { - if (p[i] != '\0') + if (uuid[i] != '\0') return KORE_RESULT_ERROR; continue; } - if (!isxdigit(p[i])) + 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", "/"); @@ -113,208 +225,209 @@ redirect(struct http_request *req) { } int -GET_post(struct http_request *req) { - const char *resource = NULL; +get_posts(struct http_request *req) { + struct post_query pq; - resource = req->path + strlen("/posts/"); - kore_log(LOG_DEBUG, "Resource /posts/%s", resource); + post_query_init(&pq); + pq.type = get_content_type(req); - (void) render_posts(req, resource); + (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 -posts(struct http_request *req) { - if (req->method == HTTP_METHOD_GET) - (void) render_posts(req, NULL); - else if (req->method == HTTP_METHOD_POST) - (void) POST_post(req); +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 -render_posts(struct http_request *req, const char *resource) { +post_posts(struct http_request *req) { int err = 0; - const char *accept = NULL; + int status = HTTP_STATUS_CREATED; - struct post_request post_req; + enum content_type type = get_content_type(req); - post_request_init(&post_req); + const char *id = NULL; + const char *title = NULL; + const char *body = NULL; - post_req.req = req; - post_req.resource = resource; + struct post_query pq; - 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) - post_req.type = JSON; - else - post_req.type = HTML; + 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; } - if (post_req.type == JSON) { - http_response_header(post_req.req, "content-type", "application/json; charset=utf-8"); - - (void) query_posts(&post_req); - // TOOD: Create a status_ok function - if (post_req.resp_status != HTTP_STATUS_OK && post_req.resp_status != HTTP_STATUS_CREATED) { - kore_buf_appendf( - post_req.resp_buf, - (const char *) asset_error_json, - error_msg[post_req.resp_status] - ); - } + 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 { - http_response_header(post_req.req, "content-type", "text/html; charset=utf-8"); + status = HTTP_STATUS_BAD_REQUEST; + kore_log(LOG_ERR, "error parsing title: %s\n", kore_json_strerror()); + goto out; + } - kore_buf_append(post_req.resp_buf, asset_index_begin_html, asset_len_index_begin_html); + 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; + } - (void) query_posts(&post_req); - if (post_req.resp_status != HTTP_STATUS_OK && post_req.resp_status != HTTP_STATUS_CREATED) { - kore_buf_appendf( - post_req.resp_buf, - (const char *) asset_error_html, - error_msg[post_req.resp_status] - ); - } + err = sql_insert_posts(id, title, body); - kore_buf_append(post_req.resp_buf, asset_index_end_html, asset_len_index_end_html); + if (err == KORE_RESULT_ERROR) { + status = HTTP_STATUS_INTERNAL_ERROR; + goto out; } - http_response( - post_req.req, - post_req.resp_status, - post_req.resp_buf->data, - post_req.resp_buf->offset - ); +out: ; - post_request_cleanup(&post_req); + 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 -query_posts(struct post_request *post_req) { +sql_select_posts(struct post_query *pq) { int err = 0; - int row = 0, rows = 0; - - const char *id = NULL; - const char *title = NULL; - const char *created_at = NULL; - const char *body = NULL; - - const char *json = NULL; - struct kore_pgsql sql; kore_pgsql_init(&sql); - // TODO use kore validation here. - if (post_req->resource) { - // Check for valid resource ID/UUID - err = validate_uuid(post_req->resource); - if (err == KORE_RESULT_ERROR) { - post_req->resp_status = HTTP_STATUS_NOT_FOUND; - kore_log(LOG_ERR, "Invalid post UUID %s.", post_req->resource); - goto out; - } - } + pq->status = QUERY_STATUS_OK; - // Initialize our kore_pgsql data structure with the database name - // we want to connect to (note that we registered this earlier with - // kore_pgsql_register()). We also say we will perform a synchronous - // query (KORE_PGSQL_SYNC). + // Set up synchronous database handle. err = kore_pgsql_setup(&sql, database, KORE_PGSQL_SYNC); if (err == KORE_RESULT_ERROR) { - post_req->resp_status = HTTP_STATUS_INTERNAL_ERROR; + pq->status = QUERY_STATUS_ERROR; kore_pgsql_logerror(&sql); goto out; } // Query for posts, check for error. - if (post_req->resource != NULL) { + if (pq->id != NULL) { // Query a post. err = kore_pgsql_query_params( &sql, - post_req->type == HTML ? query_html_post : query_json_post, + pq->type == HTML ? query_html_post : query_json_post, 0, // return string data 1, // param count - KORE_PGSQL_PARAM_TEXT(post_req->resource) + KORE_PGSQL_PARAM_TEXT(pq->id) ); } else { // Query all posts. err = kore_pgsql_query( &sql, - post_req->type == HTML ? query_html_posts : query_json_posts + pq->type == HTML ? query_html_posts : query_json_posts ); } if (err == KORE_RESULT_ERROR) { - post_req->resp_status = HTTP_STATUS_INTERNAL_ERROR; + pq->status = QUERY_STATUS_ERROR; kore_pgsql_logerror(&sql); goto out; } - if (post_req->type == JSON) { - // XXX Always tuples from the above Postgres queries, need to check the length for results. - if (kore_pgsql_getlength(&sql, 0, 0) == 0) { - post_req->resp_status = HTTP_STATUS_NOT_FOUND; - goto out; - } - - json = kore_pgsql_getvalue(&sql, 0, 0); - kore_buf_append( - post_req->resp_buf, - json, - strlen(json) - ); - } else { // post_req->type == HTML - rows = kore_pgsql_ntuples(&sql); - // TODO: Add test for this; When database is empty, don't return 404 for base request. - if (post_req->resource && rows == 0) { - post_req->resp_status = HTTP_STATUS_NOT_FOUND; - 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; + } - // Iterate over posts and render them. - for (row = 0; row < rows; row++) { - 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); - - // Allocate a buffer to render the markdown as HTML into. - struct kore_buf *html_buf = kore_buf_alloc(0); - - // Render MD. - err = render_md(body, html_buf); - if (err == KORE_RESULT_ERROR) { - kore_log(LOG_ERR, "Error rendering markdown for entry %s.", id); - kore_buf_free(html_buf); - continue; - } - - // Append rendered MD post. - kore_buf_appendf( - post_req->resp_buf, - (const char *) asset_post_html, - title, - created_at, - kore_buf_stringify(html_buf, NULL) - ); - - kore_buf_free(html_buf); - } + // 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); @@ -322,78 +435,84 @@ out: ; return KORE_RESULT_OK; } -int -POST_post(struct http_request *req) { +int sql_render_posts(struct kore_pgsql *sql, struct post_query *pq) { int err = 0; - int status = HTTP_STATUS_CREATED; + int row = 0, rows = 0; - struct kore_json_item *item = NULL; + struct kore_buf *html_buf = NULL; const char *id = NULL; const char *title = NULL; + const char *created_at = NULL; const char *body = NULL; - struct kore_json json; - struct kore_pgsql sql; + 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; + } - kore_json_init(&json, req->http_body->data, req->http_body->length); + // 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; + } - kore_pgsql_init(&sql); + // Append rendered MD post. + kore_buf_appendf( + pq->result, + (const char *) asset_post_html, + title, + created_at, + kore_buf_stringify(html_buf, NULL) + ); - if (!kore_json_parse(&json)) { - status = HTTP_STATUS_INTERNAL_ERROR; - 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_INTERNAL_ERROR; - kore_log(LOG_ERR, "error parsing id: %s\n", kore_json_strerror()); - goto out; - } + kore_buf_free(html_buf); - // 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; - } + return KORE_RESULT_OK; +} - 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_INTERNAL_ERROR; - kore_log(LOG_ERR, "error parsing title: %s\n", kore_json_strerror()); - goto out; - } +int +sql_insert_posts( + const char *id, + const char *title, + const char *body +) { + int err = KORE_RESULT_OK; - 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_INTERNAL_ERROR; - kore_log(LOG_ERR, "error parsing body: %s\n", kore_json_strerror()); - goto out; - } + struct kore_pgsql sql; + kore_pgsql_init(&sql); err = kore_pgsql_setup(&sql, database, KORE_PGSQL_SYNC); if (err == KORE_RESULT_ERROR) { - status = HTTP_STATUS_INTERNAL_ERROR; kore_pgsql_logerror(&sql); goto out; } - // Insert a post. - // err = kore_pgsql_query( err = kore_pgsql_query_params( &sql, "INSERT INTO posts (id, title, body) " @@ -404,63 +523,16 @@ POST_post(struct http_request *req) { KORE_PGSQL_PARAM_TEXT(title), KORE_PGSQL_PARAM_TEXT(body) ); - if (err == KORE_RESULT_ERROR) { - status = HTTP_STATUS_INTERNAL_ERROR; kore_pgsql_logerror(&sql); goto out; } out: ; - if (status == HTTP_STATUS_CREATED) - render_posts(req, id); - else { // ERRORs - int type = HTML; - struct kore_buf *resp_buf = kore_buf_alloc(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) - type = JSON; - else - type = HTML; - } - - 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); - } - - kore_json_cleanup(&json); kore_pgsql_cleanup(&sql); - return KORE_RESULT_OK; + return err; } static int @@ -106,7 +106,7 @@ def test_json_post_posts(): j = r.json() assert len(j) == 1 - assert "title" == j[0]["title"] + assert "title" == j[0]["title"] assert "body" == j[0]["body"] url = f"{BASE_URL}/posts/{uuid_}" @@ -115,7 +115,7 @@ def test_json_post_posts(): j = r.json() assert len(j) == 1 - assert "title" == j[0]["title"] + assert "title" == j[0]["title"] assert "body" == j[0]["body"] @@ -136,11 +136,17 @@ def test_json_get_post_not_found(): def main(): + winner = True for func in dir(sys.modules[__name__]): if func.startswith("test_"): - globals()[func]() - - print("🏆 You're winner !") + try: + globals()[func]() + except Exception: + winner = False + print(f"{func} failed.") + + if winner: + print("🏆 You're winner !") if __name__ == "__main__": |