All checks were successful
Build Tilde Friends / Build-All (push) Successful in 9m45s
428 lines
13 KiB
C
428 lines
13 KiB
C
#include "httpd.js.h"
|
|
|
|
#include "http.h"
|
|
#include "log.h"
|
|
#include "mem.h"
|
|
#include "sha1.h"
|
|
#include "ssb.db.h"
|
|
#include "ssb.h"
|
|
#include "task.h"
|
|
#include "util.js.h"
|
|
|
|
#include "picohttpparser.h"
|
|
#include "uv.h"
|
|
|
|
#include <stdlib.h>
|
|
|
|
#if !defined(__APPLE__) && !defined(__OpenBSD__) && !defined(_WIN32)
|
|
#include <alloca.h>
|
|
#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);
|
|
}
|
|
|
|
typedef struct _app_t
|
|
{
|
|
tf_http_request_t* request;
|
|
const char* settings;
|
|
JSValue credentials;
|
|
} app_t;
|
|
|
|
static void _httpd_auth_query_work(tf_ssb_t* ssb, void* user_data)
|
|
{
|
|
app_t* work = user_data;
|
|
work->settings = tf_ssb_db_get_property(ssb, "core", "settings");
|
|
}
|
|
|
|
static void _httpd_app_on_message(tf_http_request_t* request, int op_code, const void* data, size_t size)
|
|
{
|
|
tf_printf("REQUEST MESSAGE %.*s\n", (int)size, (const char*)data);
|
|
}
|
|
|
|
static void _httpd_app_on_close(tf_http_request_t* request)
|
|
{
|
|
tf_printf("REQUEST CLOSE\n");
|
|
tf_http_request_unref(request);
|
|
}
|
|
|
|
static void _httpd_auth_query_after_work(tf_ssb_t* ssb, int status, void* user_data)
|
|
{
|
|
app_t* work = user_data;
|
|
JSContext* context = tf_ssb_get_context(ssb);
|
|
JSValue session = JS_GetPropertyStr(context, work->credentials, "session");
|
|
JSValue name = JS_GetPropertyStr(context, session, "name");
|
|
JS_FreeValue(context, session);
|
|
const char* name_string = JS_ToCString(context, name);
|
|
JSValue settings_value = work->settings ? JS_ParseJSON(context, work->settings, strlen(work->settings), NULL) : JS_UNDEFINED;
|
|
tf_free((void*)work->settings);
|
|
work->settings = NULL;
|
|
|
|
JSValue out_permissions = JS_NewObject(context);
|
|
JS_SetPropertyStr(context, work->credentials, "permissions", out_permissions);
|
|
JSValue permissions = !JS_IsUndefined(settings_value) ? JS_GetPropertyStr(context, settings_value, "permissions") : JS_UNDEFINED;
|
|
JSValue user_permissions = !JS_IsUndefined(permissions) ? JS_GetPropertyStr(context, permissions, name_string) : JS_UNDEFINED;
|
|
int length = !JS_IsUndefined(user_permissions) ? tf_util_get_length(context, user_permissions) : 0;
|
|
for (int i = 0; i < length; i++)
|
|
{
|
|
JSValue permission = JS_GetPropertyUint32(context, user_permissions, i);
|
|
const char* permission_string = JS_ToCString(context, permission);
|
|
JS_SetPropertyStr(context, out_permissions, permission_string, JS_TRUE);
|
|
JS_FreeCString(context, permission_string);
|
|
JS_FreeValue(context, permission);
|
|
}
|
|
JS_FreeValue(context, user_permissions);
|
|
JS_FreeValue(context, permissions);
|
|
JS_FreeValue(context, settings_value);
|
|
JS_FreeValue(context, name);
|
|
|
|
tf_http_request_t* request = work->request;
|
|
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
|
|
|
|
static const char* k_magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
size_t key_length = strlen(header_sec_websocket_key);
|
|
size_t size = key_length + 36;
|
|
uint8_t* key_magic = alloca(size);
|
|
memcpy(key_magic, header_sec_websocket_key, key_length);
|
|
memcpy(key_magic + key_length, k_magic, 36);
|
|
|
|
uint8_t digest[20];
|
|
SHA1_CTX sha1 = { 0 };
|
|
SHA1Init(&sha1);
|
|
SHA1Update(&sha1, key_magic, size);
|
|
SHA1Final(digest, &sha1);
|
|
|
|
char key[41] = { 0 };
|
|
tf_base64_encode(digest, sizeof(digest), key, sizeof(key));
|
|
|
|
const char* headers[64] = { 0 };
|
|
int headers_count = 0;
|
|
|
|
headers[headers_count * 2 + 0] = "Upgrade";
|
|
headers[headers_count * 2 + 1] = "websocket";
|
|
headers_count++;
|
|
|
|
headers[headers_count * 2 + 0] = "Connection";
|
|
headers[headers_count * 2 + 1] = "Upgrade";
|
|
headers_count++;
|
|
|
|
headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
|
|
headers[headers_count * 2 + 1] = key;
|
|
headers_count++;
|
|
|
|
const char* session_token = tf_httpd_make_session_jwt(tf_ssb_get_context(ssb), ssb, name_string);
|
|
const char* cookie = tf_httpd_make_set_session_cookie_header(request, session_token);
|
|
tf_free((void*)session_token);
|
|
headers[headers_count * 2 + 0] = "Set-Cookie";
|
|
headers[headers_count * 2 + 1] = cookie ? cookie : "";
|
|
headers_count++;
|
|
|
|
bool send_version = !tf_http_request_get_header(request, "sec-websocket-version") || strcmp(tf_http_request_get_header(request, "sec-websocket-version"), "13") != 0;
|
|
if (send_version)
|
|
{
|
|
headers[headers_count * 2 + 0] = "Sec-WebSocket-Accept";
|
|
headers[headers_count * 2 + 1] = key;
|
|
headers_count++;
|
|
}
|
|
|
|
tf_http_request_websocket_upgrade(request);
|
|
tf_http_respond(request, 101, headers, headers_count, NULL, 0);
|
|
|
|
/* What now? */
|
|
tf_free((void*)cookie);
|
|
JS_FreeCString(context, name_string);
|
|
JS_FreeValue(context, work->credentials);
|
|
tf_free(work);
|
|
|
|
// tf_http_request_unref(request);
|
|
request->on_message = _httpd_app_on_message;
|
|
request->on_close = _httpd_app_on_close;
|
|
}
|
|
|
|
static void _tf_httpd_endpoint_app_socket_c(tf_http_request_t* request)
|
|
{
|
|
const char* header_connection = tf_http_request_get_header(request, "connection");
|
|
const char* header_upgrade = tf_http_request_get_header(request, "upgrade");
|
|
const char* header_sec_websocket_key = tf_http_request_get_header(request, "sec-websocket-key");
|
|
if (!header_connection || !header_upgrade || !header_sec_websocket_key || !strstr(header_connection, "Upgrade") || strcasecmp(header_upgrade, "websocket"))
|
|
{
|
|
tf_http_respond(request, 500, NULL, 0, NULL, 0);
|
|
return;
|
|
}
|
|
|
|
tf_task_t* task = request->user_data;
|
|
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
|
JSContext* context = tf_task_get_context(task);
|
|
const char* session = tf_http_get_cookie(tf_http_request_get_header(request, "cookie"), "session");
|
|
|
|
JSValue jwt = tf_httpd_authenticate_jwt(ssb, context, session);
|
|
tf_free((void*)session);
|
|
|
|
if (!JS_IsUndefined(jwt))
|
|
{
|
|
JSValue credentials = JS_NewObject(context);
|
|
JS_SetPropertyStr(context, credentials, "session", jwt);
|
|
tf_http_request_ref(request);
|
|
app_t* work = tf_malloc(sizeof(app_t));
|
|
*work = (app_t) {
|
|
.request = request,
|
|
.credentials = credentials,
|
|
};
|
|
tf_ssb_run_work(ssb, _httpd_auth_query_work, _httpd_auth_query_after_work, work);
|
|
}
|
|
}
|
|
|
|
static void _tf_httpd_endpoint_app_socket_js(tf_http_request_t* request)
|
|
{
|
|
tf_task_t* task = request->user_data;
|
|
tf_ssb_t* ssb = tf_task_get_ssb(task);
|
|
|
|
JSContext* context = tf_ssb_get_context(ssb);
|
|
JSValue global = JS_GetGlobalObject(context);
|
|
JSValue exports = JS_GetPropertyStr(context, global, "exports");
|
|
JSValue app_socket = JS_GetPropertyStr(context, exports, "app_socket");
|
|
|
|
JSValue request_object = JS_NewObject(context);
|
|
JSValue headers = JS_NewObject(context);
|
|
for (int i = 0; i < request->headers_count; i++)
|
|
{
|
|
JS_SetPropertyStr(context, headers, request->headers[i].name, JS_NewString(context, request->headers[i].value));
|
|
}
|
|
JS_SetPropertyStr(context, request_object, "headers", headers);
|
|
|
|
JSValue response = tf_httpd_make_response_object(context, request);
|
|
tf_http_request_ref(request);
|
|
|
|
JSValue args[] = {
|
|
request_object,
|
|
response,
|
|
};
|
|
|
|
JSValue result = JS_Call(context, app_socket, JS_NULL, tf_countof(args), args);
|
|
tf_util_report_error(context, result);
|
|
JS_FreeValue(context, result);
|
|
|
|
for (int i = 0; i < tf_countof(args); i++)
|
|
{
|
|
JS_FreeValue(context, args[i]);
|
|
}
|
|
|
|
JS_FreeValue(context, app_socket);
|
|
JS_FreeValue(context, exports);
|
|
JS_FreeValue(context, global);
|
|
}
|
|
|
|
void tf_httpd_endpoint_app_socket(tf_http_request_t* request)
|
|
{
|
|
static bool checked_env;
|
|
static bool use_c;
|
|
if (!checked_env)
|
|
{
|
|
char buffer[8] = { 0 };
|
|
size_t buffer_size = sizeof(buffer);
|
|
use_c = uv_os_getenv("TF_APP_C", buffer, &buffer_size) == 0 && strcmp(buffer, "1") == 0;
|
|
checked_env = true;
|
|
}
|
|
|
|
if (use_c)
|
|
{
|
|
_tf_httpd_endpoint_app_socket_c(request);
|
|
}
|
|
else
|
|
{
|
|
_tf_httpd_endpoint_app_socket_js(request);
|
|
}
|
|
}
|