From 1972ce7091c5bdfb7e771e4af7feeeffd54f7c27 Mon Sep 17 00:00:00 2001 From: Cory McWilliams Date: Thu, 4 Dec 2025 12:49:35 -0500 Subject: [PATCH] core: Beginnings of websocket handling in C. --- src/httpd.app.c | 148 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 147 insertions(+), 1 deletion(-) diff --git a/src/httpd.app.c b/src/httpd.app.c index df71f27f..b8e24c87 100644 --- a/src/httpd.app.c +++ b/src/httpd.app.c @@ -3,6 +3,7 @@ #include "http.h" #include "log.h" #include "mem.h" +#include "sha1.h" #include "ssb.db.h" #include "ssb.h" #include "task.h" @@ -213,9 +214,154 @@ void tf_httpd_endpoint_app(tf_http_request_t* 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) { - tf_printf("Unimplemented.\n"); + 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)