diff --git a/src/httpd.app.c b/src/httpd.app.c new file mode 100644 index 000000000..61358249b --- /dev/null +++ b/src/httpd.app.c @@ -0,0 +1,212 @@ +#include "httpd.js.h" + +#include "http.h" +#include "mem.h" +#include "ssb.db.h" +#include "ssb.h" +#include "task.h" +#include "util.js.h" + +#include "picohttpparser.h" + +#include + +#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32) +#include +#endif + +typedef struct _app_blob_t +{ + tf_http_request_t* request; + bool found; + bool not_modified; + bool use_handler; + bool use_static; + void* data; + size_t size; + char app_blob_id[k_blob_id_len]; + const char* file; + tf_httpd_user_app_t* user_app; + char etag[256]; +} app_blob_t; + +static void _httpd_endpoint_app_blob_work(tf_ssb_t* ssb, void* user_data) +{ + app_blob_t* data = user_data; + tf_http_request_t* request = data->request; + if (request->path[0] == '/' && request->path[1] == '~') + { + const char* last_slash = strchr(request->path + 1, '/'); + if (last_slash) + { + last_slash = strchr(last_slash + 1, '/'); + } + data->user_app = last_slash ? tf_httpd_parse_user_app_from_path(request->path, last_slash) : NULL; + if (data->user_app) + { + size_t path_length = strlen("path:") + strlen(data->user_app->app) + 1; + char* app_path = tf_malloc(path_length); + snprintf(app_path, path_length, "path:%s", data->user_app->app); + const char* value = tf_ssb_db_get_property(ssb, data->user_app->user, app_path); + tf_string_set(data->app_blob_id, sizeof(data->app_blob_id), value); + tf_free(app_path); + tf_free((void*)value); + data->file = last_slash + 1; + } + } + else if (request->path[0] == '/' && request->path[1] == '&') + { + const char* end = strstr(request->path, ".sha256/"); + if (end) + { + snprintf(data->app_blob_id, sizeof(data->app_blob_id), "%.*s", (int)(end + strlen(".sha256") - request->path - 1), request->path + 1); + data->file = end + strlen(".sha256/"); + } + } + + char* app_blob = NULL; + size_t app_blob_size = 0; + if (*data->app_blob_id && tf_ssb_db_blob_get(ssb, data->app_blob_id, (uint8_t**)&app_blob, &app_blob_size)) + { + JSMallocFunctions funcs = { 0 }; + tf_get_js_malloc_functions(&funcs); + JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL); + JSContext* context = JS_NewContext(runtime); + + JSValue app_object = JS_ParseJSON(context, app_blob, app_blob_size, NULL); + JSValue files = JS_GetPropertyStr(context, app_object, "files"); + JSValue blob_id = JS_GetPropertyStr(context, files, data->file); + if (JS_IsUndefined(blob_id)) + { + blob_id = JS_GetPropertyStr(context, files, "handler.js"); + if (!JS_IsUndefined(blob_id)) + { + data->use_handler = true; + } + } + else + { + const char* blob_id_str = JS_ToCString(context, blob_id); + if (blob_id_str) + { + snprintf(data->etag, sizeof(data->etag), "\"%s\"", blob_id_str); + const char* match = tf_http_request_get_header(data->request, "if-none-match"); + if (match && strcmp(match, data->etag) == 0) + { + data->not_modified = true; + } + else + { + data->found = tf_ssb_db_blob_get(ssb, blob_id_str, (uint8_t**)&data->data, &data->size); + } + } + JS_FreeCString(context, blob_id_str); + } + JS_FreeValue(context, blob_id); + JS_FreeValue(context, files); + JS_FreeValue(context, app_object); + + JS_FreeContext(context); + JS_FreeRuntime(runtime); + tf_free(app_blob); + } +} + +static void _httpd_call_app_handler(tf_ssb_t* ssb, tf_http_request_t* request, const char* app_blob_id, const char* path, const char* package_owner, const char* app) +{ + JSContext* context = tf_ssb_get_context(ssb); + JSValue global = JS_GetGlobalObject(context); + JSValue exports = JS_GetPropertyStr(context, global, "exports"); + JSValue call_app_handler = JS_GetPropertyStr(context, exports, "callAppHandler"); + + JSValue response = tf_httpd_make_response_object(context, request); + tf_http_request_ref(request); + JSValue handler_blob_id = JS_NewString(context, app_blob_id); + JSValue path_value = JS_NewString(context, path); + JSValue package_owner_value = JS_NewString(context, package_owner); + JSValue app_value = JS_NewString(context, app); + JSValue query_value = request->query ? JS_NewString(context, request->query) : JS_UNDEFINED; + + JSValue headers = JS_NewObject(context); + for (int i = 0; i < request->headers_count; i++) + { + char name[256] = ""; + snprintf(name, sizeof(name), "%.*s", (int)request->headers[i].name_len, request->headers[i].name); + JS_SetPropertyStr(context, headers, name, JS_NewStringLen(context, request->headers[i].value, request->headers[i].value_len)); + } + + JSValue args[] = { + response, + handler_blob_id, + path_value, + query_value, + headers, + package_owner_value, + app_value, + }; + + JSValue result = JS_Call(context, call_app_handler, JS_NULL, tf_countof(args), args); + tf_util_report_error(context, result); + JS_FreeValue(context, result); + + JS_FreeValue(context, headers); + JS_FreeValue(context, query_value); + JS_FreeValue(context, app_value); + JS_FreeValue(context, package_owner_value); + JS_FreeValue(context, handler_blob_id); + JS_FreeValue(context, path_value); + JS_FreeValue(context, response); + JS_FreeValue(context, call_app_handler); + JS_FreeValue(context, exports); + JS_FreeValue(context, global); +} + +static void _httpd_endpoint_app_blob_after_work(tf_ssb_t* ssb, int status, void* user_data) +{ + app_blob_t* data = user_data; + if (data->not_modified) + { + tf_http_respond(data->request, 304, NULL, 0, NULL, 0); + } + else if (data->use_static) + { + tf_httpd_endpoint_static(data->request); + } + else if (data->use_handler) + { + _httpd_call_app_handler(ssb, data->request, data->app_blob_id, data->file, data->user_app->user, data->user_app->app); + } + else if (data->found) + { + const char* mime_type = tf_httpd_ext_to_content_type(strrchr(data->request->path, '.'), false); + if (!mime_type) + { + mime_type = tf_httpd_magic_bytes_to_content_type(data->data, data->size); + } + const char* headers[] = { + "Access-Control-Allow-Origin", + "*", + "Content-Security-Policy", + "sandbox allow-downloads allow-top-navigation-by-user-activation", + "Content-Type", + mime_type ? mime_type : "application/binary", + "etag", + data->etag, + }; + tf_http_respond(data->request, 200, headers, tf_countof(headers) / 2, data->data, data->size); + } + tf_free(data->user_app); + tf_free(data->data); + tf_http_request_unref(data->request); + tf_free(data); +} + +void tf_httpd_endpoint_app(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); + app_blob_t* data = tf_malloc(sizeof(app_blob_t)); + *data = (app_blob_t) { .request = request }; + tf_ssb_run_work(ssb, _httpd_endpoint_app_blob_work, _httpd_endpoint_app_blob_after_work, data); +} diff --git a/src/httpd.delete.c b/src/httpd.delete.c new file mode 100644 index 000000000..c2a4e3a6e --- /dev/null +++ b/src/httpd.delete.c @@ -0,0 +1,91 @@ +#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 _delete_t +{ + tf_http_request_t* request; + const char* session; + int response; +} delete_t; + +static void _httpd_endpoint_delete_work(tf_ssb_t* ssb, void* user_data) +{ + delete_t* delete = user_data; + tf_http_request_t* request = delete->request; + + JSMallocFunctions funcs = { 0 }; + tf_get_js_malloc_functions(&funcs); + JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL); + JSContext* context = JS_NewContext(runtime); + + JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, delete->session); + JSValue user = JS_GetPropertyStr(context, jwt, "name"); + const char* user_string = JS_ToCString(context, user); + if (user_string && tf_httpd_is_name_valid(user_string)) + { + tf_httpd_user_app_t* user_app = tf_httpd_parse_user_app_from_path(request->path, "/delete"); + if (user_app) + { + if (strcmp(user_string, user_app->user) == 0 || (strcmp(user_app->user, "core") == 0 && tf_ssb_db_user_has_permission(ssb, NULL, user_string, "administration"))) + { + size_t path_length = strlen("path:") + strlen(user_app->app) + 1; + char* app_path = tf_malloc(path_length); + snprintf(app_path, path_length, "path:%s", user_app->app); + + bool changed = false; + changed = tf_ssb_db_remove_value_from_array_property(ssb, user_string, "apps", user_app->app) || changed; + changed = tf_ssb_db_remove_property(ssb, user_string, app_path) || changed; + delete->response = changed ? 200 : 404; + tf_free(app_path); + } + else + { + delete->response = 401; + } + } + else + { + delete->response = 404; + } + tf_free(user_app); + } + else + { + delete->response = 401; + } + + JS_FreeCString(context, user_string); + JS_FreeValue(context, user); + JS_FreeValue(context, jwt); + JS_FreeContext(context); + JS_FreeRuntime(runtime); +} + +static void _httpd_endpoint_delete_after_work(tf_ssb_t* ssb, int status, void* user_data) +{ + delete_t* delete = user_data; + const char* k_payload = tf_http_status_text(delete->response ? delete->response : 404); + tf_http_respond(delete->request, delete->response ? delete->response : 404, NULL, 0, k_payload, strlen(k_payload)); + tf_http_request_unref(delete->request); + tf_free((void*)delete->session); + tf_free(delete); +} + +void tf_httpd_endpoint_delete(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); + delete_t* delete = tf_malloc(sizeof(delete_t)); + *delete = (delete_t) { + .request = request, + .session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session"), + }; + tf_ssb_run_work(ssb, _httpd_endpoint_delete_work, _httpd_endpoint_delete_after_work, delete); +} diff --git a/src/httpd.index.c b/src/httpd.index.c new file mode 100644 index 000000000..b776ac942 --- /dev/null +++ b/src/httpd.index.c @@ -0,0 +1,233 @@ +#include "httpd.js.h" + +#include "file.js.h" +#include "http.h" +#include "mem.h" +#include "ssb.db.h" +#include "ssb.h" +#include "task.h" +#include "util.js.h" + +#include + +#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32) +#include +#endif + +typedef struct _index_t +{ + tf_http_request_t* request; + bool found; + bool not_modified; + bool use_handler; + bool use_static; + void* data; + size_t size; + char app_blob_id[k_blob_id_len]; + const char* file; + tf_httpd_user_app_t* user_app; + char etag[256]; +} index_t; + +static bool _has_property(JSContext* context, JSValue object, const char* name) +{ + JSAtom atom = JS_NewAtom(context, name); + bool result = JS_HasProperty(context, object, atom) > 0; + JS_FreeAtom(context, atom); + return result; +} + +static void _httpd_endpoint_app_index_work(tf_ssb_t* ssb, void* user_data) +{ + index_t* data = user_data; + data->use_static = true; + tf_httpd_user_app_t* user_app = data->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* app_blob_id = tf_ssb_db_get_property(ssb, user_app->user, app_path); + tf_free(app_path); + + uint8_t* app_blob = NULL; + size_t app_blob_size = 0; + + if (tf_ssb_db_blob_get(ssb, app_blob_id, &app_blob, &app_blob_size)) + { + JSMallocFunctions funcs = { 0 }; + tf_get_js_malloc_functions(&funcs); + JSRuntime* runtime = JS_NewRuntime2(&funcs, NULL); + JSContext* context = JS_NewContext(runtime); + + JSValue app = JS_ParseJSON(context, (const char*)app_blob, app_blob_size, NULL); + JSValue files = JS_GetPropertyStr(context, app, "files"); + + if (!_has_property(context, files, "app.js")) + { + JSValue index = JS_GetPropertyStr(context, files, "index.html"); + if (JS_IsString(index)) + { + const char* index_string = JS_ToCString(context, index); + tf_ssb_db_blob_get(ssb, index_string, (uint8_t**)&data->data, &data->size); + JS_FreeCString(context, index_string); + } + JS_FreeValue(context, index); + } + + JS_FreeValue(context, files); + JS_FreeValue(context, app); + + JS_FreeContext(context); + JS_FreeRuntime(runtime); + + tf_free(app_blob); + } + tf_free((void*)app_blob_id); +} + +static char* _replace(const char* original, size_t size, const char* find, const char* replace, size_t* out_size) +{ + char* pos = strstr(original, find); + if (!pos) + { + return tf_strdup(original); + } + + size_t replace_length = strlen(replace); + size_t find_length = strlen(find); + size_t new_size = size + replace_length - find_length; + char* buffer = tf_malloc(new_size); + memcpy(buffer, original, pos - original); + memcpy(buffer + (pos - original), replace, replace_length); + memcpy(buffer + (pos - original) + replace_length, pos + find_length, size - (pos - original) - find_length); + *out_size = new_size; + return buffer; +} + +static char* _append_raw(char* document, size_t* current_size, const char* data, size_t size) +{ + document = tf_resize_vec(document, *current_size + size); + memcpy(document + *current_size, data, size); + document[*current_size + size] = '\0'; + *current_size += size; + return document; +} + +static char* _append_encoded(char* document, const char* data, size_t size, size_t* out_size) +{ + size_t current_size = strlen(document); + int accum = 0; + for (int i = 0; (size_t)i < size; i++) + { + switch (data[i]) + { + case '"': + if (i > accum) + { + document = _append_raw(document, ¤t_size, data + accum, i - accum); + } + document = _append_raw(document, ¤t_size, """, strlen(""")); + accum = i + 1; + break; + case '\'': + if (i > accum) + { + document = _append_raw(document, ¤t_size, data + accum, i - accum); + } + document = _append_raw(document, ¤t_size, "'", strlen("'")); + accum = i + 1; + break; + case '<': + if (i > accum) + { + document = _append_raw(document, ¤t_size, data + accum, i - accum); + } + document = _append_raw(document, ¤t_size, "<", strlen("<")); + accum = i + 1; + break; + case '>': + if (i > accum) + { + document = _append_raw(document, ¤t_size, data + accum, i - accum); + } + document = _append_raw(document, ¤t_size, ">", strlen(">")); + accum = i + 1; + break; + case '&': + if (i > accum) + { + document = _append_raw(document, ¤t_size, data + accum, i - accum); + } + document = _append_raw(document, ¤t_size, "&", strlen("&")); + accum = i + 1; + break; + default: + break; + } + } + *out_size = current_size; + return document; +} + +static void _httpd_endpoint_app_index_file_read(tf_task_t* task, const char* path, int result, const void* data, void* user_data) +{ + index_t* state = user_data; + if (result > 0) + { + char* replacement = tf_strdup("