#include "httpd.js.h" #include "http.h" #include "mem.h" #include "ssb.db.h" #include "ssb.h" #include "task.h" #include "util.js.h" typedef struct _view_t { tf_http_request_t* request; const char** form_data; void* data; size_t size; char etag[256]; char notify_want_blob_id[k_blob_id_len]; 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[k_blob_id_len] = ""; tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/view"); if (user_app) { size_t app_path_length = strlen("path:") + strlen(user_app->app) + 1; char* app_path = tf_malloc(app_path_length); snprintf(app_path, app_path_length, "path:%s", user_app->app); const char* value = tf_ssb_db_get_property(ssb, user_app->user, app_path); tf_string_set(blob_id, sizeof(blob_id), value); tf_free(app_path); 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); } tf_free(user_app); if (*blob_id) { snprintf(view->etag, sizeof(view->etag), "\"%s\"", 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) == 0) { view->not_modified = true; } else { if (!tf_ssb_db_blob_get(ssb, blob_id, (uint8_t**)&view->data, &view->size)) { sqlite3* db = tf_ssb_acquire_db_writer(ssb); tf_ssb_db_add_blob_wants(db, blob_id); tf_ssb_release_db_writer(ssb, db); tf_string_set(view->notify_want_blob_id, sizeof(view->notify_want_blob_id), blob_id); } } } } static void _httpd_endpoint_view_after_work(tf_ssb_t* ssb, int status, void* user_data) { view_t* view = user_data; const char* filename = tf_httpd_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", "Content-Type", view->data ? tf_httpd_magic_bytes_to_content_type(view->data, view->size) : "text/plain", "etag", view->etag, 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)); } if (*view->notify_want_blob_id) { tf_ssb_notify_blob_want_added(ssb, view->notify_want_blob_id); } tf_free(view->form_data); tf_http_request_unref(view->request); tf_free(view); } void tf_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 = tf_httpd_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); }