From 72def5ae6dd45fcae136db0fe956682897c3599b Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Wed, 16 Oct 2024 19:16:45 -0400 Subject: [PATCH] js: Move /view to C. --- core/core.js | 61 +------------------------ src/httpd.js.c | 120 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 60 deletions(-) diff --git a/core/core.js b/core/core.js index eac01400..d920cea9 100644 --- a/core/core.js +++ b/core/core.js @@ -962,66 +962,7 @@ async function blobHandler(request, response, blobId, uri) { } let process; - if (uri == '/view') { - let data; - let match; - let query = form.decodeForm(request.query); - let headers = { - 'Content-Security-Policy': k_content_security_policy, - }; - if (query.filename && query.filename.match(/^[A-Za-z0-9\.-]*$/)) { - headers['Content-Disposition'] = `attachment; filename=${query.filename}`; - } - if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { - let id = await new Database(match[1]).get('path:' + match[2]); - if (id) { - if (request.headers['if-none-match'] === '"' + id + '"') { - headers['Content-Length'] = '0'; - response.writeHead(304, headers); - response.end(); - } else { - data = await ssb.blobGet(id); - if (match[3]) { - let appObject = JSON.parse(data); - data = appObject.files[match[3]]; - } - sendData( - response, - data, - undefined, - Object.assign({etag: '"' + id + '"'}, headers) - ); - } - } else { - if (request.headers['if-none-match'] === '"' + blobId + '"') { - headers['Content-Length'] = '0'; - response.writeHead(304, headers); - response.end(); - } else { - sendData( - response, - data, - undefined, - Object.assign({etag: '"' + blobId + '"'}, headers) - ); - } - } - } else { - if (request.headers['if-none-match'] === '"' + blobId + '"') { - headers['Content-Length'] = '0'; - response.writeHead(304, headers); - response.end(); - } else { - data = await ssb.blobGet(blobId); - sendData( - response, - data, - undefined, - Object.assign({etag: '"' + blobId + '"'}, headers) - ); - } - } - } else if (uri == '/save') { + if (uri == '/save') { let match; if ((match = /^\/\~(\w+)\/(\w+)$/.exec(blobId))) { let user = match[1]; diff --git a/src/httpd.js.c b/src/httpd.js.c index 6a8a1678..6d29090e 100644 --- a/src/httpd.js.c +++ b/src/httpd.js.c @@ -41,6 +41,8 @@ static JSValue _authenticate_jwt(tf_ssb_t* ssb, JSContext* context, const char* static JSValue _httpd_websocket_upgrade(JSContext* context, JSValueConst this_val, int argc, JSValueConst* argv); static const char* _make_session_jwt(JSContext* context, tf_ssb_t* ssb, const char* name); static const char* _make_set_session_cookie_header(tf_http_request_t* request, const char* session_cookie); +const char** _form_data_decode(const char* data, int length); +const char* _form_data_get(const char** form_data, const char* key); static JSClassID _httpd_class_id; static JSClassID _httpd_request_class_id; @@ -959,6 +961,123 @@ static void _httpd_endpoint_static(tf_http_request_t* request) tf_file_stat(task, path, _httpd_endpoint_static_stat, request); } +typedef struct _view_t +{ + tf_http_request_t* request; + const char** form_data; + void* data; + size_t size; + bool not_modified; +} view_t; + +static bool _is_filename_safe(const char* filename) +{ + if (!filename) + { + return NULL; + } + for (const char* p = filename; *p; p++) + { + if ((*p <= 'a' && *p >= 'z') && + (*p <= 'A' && *p >= 'Z') && + (*p <= '0' && *p >= '9') && + *p != '.' && + *p != '-' && + *p != '_') + { + return false; + } + } + return strlen(filename) < 256; +} + +static void _httpd_endpoint_view_work(tf_ssb_t* ssb, void* user_data) +{ + view_t* view = user_data; + tf_http_request_t* request = view->request; + char blob_id[256] = ""; + if (request->path[0] == '/' && request->path[1] == '~') + { + char user[256] = ""; + char path[1024] = ""; + const char* slash = strchr(request->path + 2, '/'); + if (slash) + { + snprintf(user, sizeof(user), "%.*s", (int)(slash - (request->path + 2)), request->path + 2); + snprintf(path, sizeof(path), "path:%.*s", (int)(strlen(slash + 1) - strlen("/view")), slash + 1); + const char* value = tf_ssb_db_get_property(ssb, user, path); + snprintf(blob_id, sizeof(blob_id), "%s", value); + tf_free((void*)value); + } + } + else if (request->path[0] == '/' && request->path[1] == '&') + { + snprintf(blob_id, sizeof(blob_id), "%.*s", (int)(strlen(request->path) - strlen("/view") - 1), request->path + 1); + } + + if (*blob_id) + { + const char* if_none_match = tf_http_request_get_header(request, "if-none-match"); + char match[258]; + snprintf(match, sizeof(match), "\"%s\"", blob_id); + if (if_none_match && strcmp(if_none_match, match)) + { + view->not_modified = true; + } + else + { + tf_ssb_db_blob_get(ssb, blob_id, (uint8_t**)&view->data, &view->size); + } + } +} + +static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* user_data) +{ + view_t* view = user_data; + const char* filename = _form_data_get(view->form_data, "filename"); + if (!_is_filename_safe(filename)) + { + filename = NULL; + } + char content_disposition[512] = ""; + if (filename) + { + snprintf(content_disposition, sizeof(content_disposition), "attachment; filename=%s", filename); + } + const char* headers[] = { + "Content-Security-Policy", "sandbox allow-downloads allow-top-navigation-by-user-activation", + filename ? "Content-Disposition" : NULL, filename ? content_disposition : NULL, + }; + int count = filename ? tf_countof(headers) / 2 : (tf_countof(headers) / 2 - 1); + if (view->not_modified) + { + tf_http_respond(view->request, 304, headers, count, NULL, 0); + } + else if (view->data) + { + tf_http_respond(view->request, 200, headers, count, view->data, view->size); + tf_free(view->data); + } + else + { + const char* k_payload = tf_http_status_text(404); + tf_http_respond(view->request, 404, NULL, 0, k_payload, strlen(k_payload)); + } + tf_free(view->form_data); + tf_http_request_unref(view->request); + tf_free(view); +} + +static void _httpd_endpoint_view(tf_http_request_t* request) +{ + tf_http_request_ref(request); + tf_task_t* task = request->user_data; + tf_ssb_t* ssb = tf_task_get_ssb(task); + view_t* view = tf_malloc(sizeof(view_t)); + *view = (view_t) { .request = request, .form_data = _form_data_decode(request->query, request->query ? strlen(request->query) : 0) }; + tf_ssb_run_work(ssb, _httpd_endpoint_view_work, _httpd_endpoint_view_after_work, view); +} + static void _httpd_endpoint_root_callback(const char* path, void* user_data) { tf_http_request_t* request = user_data; @@ -1690,6 +1809,7 @@ void tf_httpd_register(JSContext* context) tf_http_add_handler(http, "/.well-known/*", _httpd_endpoint_static, NULL, task); tf_http_add_handler(http, "/~*/*/", _httpd_endpoint_static, NULL, task); tf_http_add_handler(http, "/&*.sha256/", _httpd_endpoint_static, NULL, task); + tf_http_add_handler(http, "/*/view", _httpd_endpoint_view, NULL, task); tf_http_add_handler(http, "/robots.txt", _httpd_endpoint_robots_txt, NULL, NULL); tf_http_add_handler(http, "/debug", _httpd_endpoint_debug, NULL, task);