From 46d98a092fb606c87ab4b9eef32fe68eeca2b2cb Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Apr 2026 18:41:50 +0000 Subject: [PATCH] Enhance WebSocket support: add opcode handling, binary message support, and improve connection validation --- README.md | 8 ++ doc/areas/task.txt | 1 + doc/areas/websocket.txt | 13 +++ doc/pages/1_WS.txt | 20 ++++ doc/pages/session_start.txt | 5 +- doc/pages/task_repeat.txt | 17 +++ doc/pages/ws_broadcast.txt | 17 +++ doc/pages/ws_close.txt | 14 +++ doc/pages/ws_connection_count.txt | 14 +++ doc/pages/ws_connection_id.txt | 13 +++ doc/pages/ws_connections.txt | 14 +++ doc/pages/ws_is_binary.txt | 13 +++ doc/pages/ws_message.txt | 15 +++ doc/pages/ws_opcode.txt | 16 +++ doc/pages/ws_scope.txt | 15 +++ doc/pages/ws_send.txt | 17 +++ doc/pages/ws_send_to.txt | 15 +++ src/fastcgi/src/fcgicc.cc | 188 +++++++++++++++++++++++++++--- src/fastcgi/src/fcgicc.h | 7 +- src/lib/sys.h | 2 + src/lib/types.h | 3 + src/lib/uri.cpp | 154 ++++++++++++++++++++++++ src/lib/uri.h | 5 + src/linux_fastcgi.cpp | 20 +++- test/websockets.ws.uce | 86 ++++++-------- 25 files changed, 623 insertions(+), 69 deletions(-) create mode 100644 doc/areas/websocket.txt create mode 100644 doc/pages/1_WS.txt create mode 100644 doc/pages/task_repeat.txt create mode 100644 doc/pages/ws_broadcast.txt create mode 100644 doc/pages/ws_close.txt create mode 100644 doc/pages/ws_connection_count.txt create mode 100644 doc/pages/ws_connection_id.txt create mode 100644 doc/pages/ws_connections.txt create mode 100644 doc/pages/ws_is_binary.txt create mode 100644 doc/pages/ws_message.txt create mode 100644 doc/pages/ws_opcode.txt create mode 100644 doc/pages/ws_scope.txt create mode 100644 doc/pages/ws_send.txt create mode 100644 doc/pages/ws_send_to.txt diff --git a/README.md b/README.md index a3fb116..e7260d6 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,8 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate - `ws_message()` - `ws_connection_id()` - `ws_scope()` +- `ws_opcode()` +- `ws_is_binary()` - `ws_connections([scope])` - `ws_connection_count([scope])` - `ws_send(message[, scope])` @@ -29,6 +31,12 @@ The runtime keeps the socket lifecycle in-process and exposes a low-boilerplate By default, the WebSocket scope is the current page file, so `ws_send()` broadcasts to other clients connected to that same `.ws.uce` endpoint. +`ws_message()` may contain either text or binary payload data. Use `ws_opcode()` / `ws_is_binary()` to inspect the current inbound message type. + +The current `ws_send*` helpers queue text frames. Binary send helpers are not exposed yet. + +The runtime now accepts fragmented messages, validates reserved bits and UTF-8 for text payloads, and delivers both text and binary message frames into `WS()`. + ## Service Setup The repository now includes a systemd unit template and a management script for the FastCGI runtime: diff --git a/doc/areas/task.txt b/doc/areas/task.txt index 941fa48..82809d0 100644 --- a/doc/areas/task.txt +++ b/doc/areas/task.txt @@ -2,4 +2,5 @@ Task API kill task +task_repeat task_pid diff --git a/doc/areas/websocket.txt b/doc/areas/websocket.txt new file mode 100644 index 0000000..3ad8b88 --- /dev/null +++ b/doc/areas/websocket.txt @@ -0,0 +1,13 @@ +WebSocket Functions + +ws_broadcast +ws_close +ws_connection_count +ws_connection_id +ws_connections +ws_is_binary +ws_message +ws_opcode +ws_scope +ws_send +ws_send_to diff --git a/doc/pages/1_WS.txt b/doc/pages/1_WS.txt new file mode 100644 index 0000000..c9b14ee --- /dev/null +++ b/doc/pages/1_WS.txt @@ -0,0 +1,20 @@ +:sig +WS() + +:desc +Defines the WebSocket message handler for the current `.ws.uce` page. + +The same page may expose both `RENDER()` and `WS()`. `RENDER()` serves the initial HTTP response, while `WS()` is called whenever a complete WebSocket message arrives for that page. + +UCE reassembles fragmented messages before calling `WS()`. Text and binary frames are both delivered. Use `ws_opcode()` and `ws_is_binary()` to distinguish them. + +The `call` parameter passed into `WS()` contains: + +- `message` +- `connection_id` +- `scope` +- `opcode` +- `document_uri` + +:see +>websocket diff --git a/doc/pages/session_start.txt b/doc/pages/session_start.txt index 04465eb..da58921 100644 --- a/doc/pages/session_start.txt +++ b/doc/pages/session_start.txt @@ -1,8 +1,9 @@ :sig -String session_start(String session_name) +String session_start(String session_name = "uce-session") :params -return value : the session ID, defaults to "uce-session" +session_name : optional name of the session cookie, defaults to "uce-session" +return value : the current session ID :desc Starts session or connects to existing session. This function sets a cookie with the name contained in 'session_name' if it does not exist and fills that cookie with a new unique session ID. It then loads the session data for that session ID. Afterwards, the following fields are populated in the 'context' variable: diff --git a/doc/pages/task_repeat.txt b/doc/pages/task_repeat.txt new file mode 100644 index 0000000..ee5f39e --- /dev/null +++ b/doc/pages/task_repeat.txt @@ -0,0 +1,17 @@ +:sig +pid_t task_repeat(String key, f64 interval, std::function exec_func, u64 timeout = 60*10) + +:params +key : string uniquely identifying the task +interval : repeat interval in seconds +exec_func : function to execute repeatedly +timeout : optional task timeout value +return value : the process ID of the started (or still running) task + +:desc +Starts a repeating background worker process. The function `exec_func` is executed in a loop and the worker sleeps for `interval` seconds between executions. + +If a process with the same `key` is already running, `task_repeat()` does not start a second worker and instead returns the PID of the existing one. + +:see +>task diff --git a/doc/pages/ws_broadcast.txt b/doc/pages/ws_broadcast.txt new file mode 100644 index 0000000..30dc7c7 --- /dev/null +++ b/doc/pages/ws_broadcast.txt @@ -0,0 +1,17 @@ +:sig +u64 ws_broadcast(String message, String scope = "") + +:params +message : text message to send +scope : optional scope identifier, defaults to the current WebSocket page scope +return value : number of clients the message was queued for + +:desc +Queues a text WebSocket message for every client connected to the given scope and returns the number of recipients. + +If `scope` is omitted, the current page scope is used. + +This helper currently sends text frames only. + +:see +>websocket diff --git a/doc/pages/ws_close.txt b/doc/pages/ws_close.txt new file mode 100644 index 0000000..deb4b7d --- /dev/null +++ b/doc/pages/ws_close.txt @@ -0,0 +1,14 @@ +:sig +bool ws_close(String connection_id = "") + +:params +connection_id : optional target client ID, defaults to the current WebSocket client +return value : true if the target connection exists and was scheduled to close + +:desc +Queues a WebSocket close frame and closes the targeted connection. + +If `connection_id` is omitted, the current connection handled by `WS()` is closed. + +:see +>websocket diff --git a/doc/pages/ws_connection_count.txt b/doc/pages/ws_connection_count.txt new file mode 100644 index 0000000..9f0939a --- /dev/null +++ b/doc/pages/ws_connection_count.txt @@ -0,0 +1,14 @@ +:sig +u64 ws_connection_count(String scope = "") + +:params +scope : optional scope identifier, defaults to the current WebSocket page scope +return value : number of connected WebSocket clients in that scope + +:desc +Returns the number of currently connected WebSocket clients for the given scope. + +If `scope` is omitted, the current page scope is used. + +:see +>websocket diff --git a/doc/pages/ws_connection_id.txt b/doc/pages/ws_connection_id.txt new file mode 100644 index 0000000..13a7966 --- /dev/null +++ b/doc/pages/ws_connection_id.txt @@ -0,0 +1,13 @@ +:sig +String ws_connection_id() + +:params +return value : connection ID of the current WebSocket client + +:desc +Returns the runtime-generated connection ID of the client whose message is currently being handled. + +This ID can be passed to `ws_send_to()` or `ws_close()` to target a single connected client. + +:see +>websocket diff --git a/doc/pages/ws_connections.txt b/doc/pages/ws_connections.txt new file mode 100644 index 0000000..b4390e3 --- /dev/null +++ b/doc/pages/ws_connections.txt @@ -0,0 +1,14 @@ +:sig +StringList ws_connections(String scope = "") + +:params +scope : optional scope identifier, defaults to the current WebSocket page scope +return value : list of connection IDs currently connected to that scope + +:desc +Returns the currently connected WebSocket client IDs for the given scope. + +If `scope` is omitted, the current page scope is used. + +:see +>websocket diff --git a/doc/pages/ws_is_binary.txt b/doc/pages/ws_is_binary.txt new file mode 100644 index 0000000..ca9637b --- /dev/null +++ b/doc/pages/ws_is_binary.txt @@ -0,0 +1,13 @@ +:sig +bool ws_is_binary() + +:params +return value : true if the current WebSocket message is binary + +:desc +Returns whether the message currently being handled by `WS()` arrived as a binary frame. + +If this returns `false`, the current message was delivered as a text frame. + +:see +>websocket diff --git a/doc/pages/ws_message.txt b/doc/pages/ws_message.txt new file mode 100644 index 0000000..bdda571 --- /dev/null +++ b/doc/pages/ws_message.txt @@ -0,0 +1,15 @@ +:sig +String ws_message() + +:params +return value : payload of the current WebSocket message + +:desc +Returns the payload of the current WebSocket message being handled by `WS()`. + +For text frames this is the decoded text payload. For binary frames this String contains the raw message bytes. + +Use `ws_is_binary()` or `ws_opcode()` to decide how the payload should be interpreted. + +:see +>websocket diff --git a/doc/pages/ws_opcode.txt b/doc/pages/ws_opcode.txt new file mode 100644 index 0000000..363d161 --- /dev/null +++ b/doc/pages/ws_opcode.txt @@ -0,0 +1,16 @@ +:sig +u8 ws_opcode() + +:params +return value : opcode of the current WebSocket message + +:desc +Returns the opcode of the message currently being handled by `WS()`. + +Common values are: + +- `0x1` for text messages +- `0x2` for binary messages + +:see +>websocket diff --git a/doc/pages/ws_scope.txt b/doc/pages/ws_scope.txt new file mode 100644 index 0000000..4ae94cd --- /dev/null +++ b/doc/pages/ws_scope.txt @@ -0,0 +1,15 @@ +:sig +String ws_scope() + +:params +return value : scope identifier of the current WebSocket endpoint + +:desc +Returns the runtime's scope identifier for the current WebSocket endpoint. + +This is the same default scope used by `ws_send()`, `ws_broadcast()`, `ws_connections()`, and `ws_connection_count()` when no explicit scope is supplied. + +In the current runtime implementation this scope is the page's internal endpoint identifier, typically the absolute `SCRIPT_FILENAME` of the `.ws.uce` file. + +:see +>websocket diff --git a/doc/pages/ws_send.txt b/doc/pages/ws_send.txt new file mode 100644 index 0000000..5b3ade9 --- /dev/null +++ b/doc/pages/ws_send.txt @@ -0,0 +1,17 @@ +:sig +bool ws_send(String message, String scope = "") + +:params +message : text message to send +scope : optional scope identifier, defaults to the current WebSocket page scope +return value : true if the message was queued for at least one connected client + +:desc +Queues a text WebSocket message for every client connected to the given scope. + +If `scope` is omitted, the current page scope is used. + +This helper currently sends text frames only. + +:see +>websocket diff --git a/doc/pages/ws_send_to.txt b/doc/pages/ws_send_to.txt new file mode 100644 index 0000000..971bf78 --- /dev/null +++ b/doc/pages/ws_send_to.txt @@ -0,0 +1,15 @@ +:sig +bool ws_send_to(String connection_id, String message) + +:params +connection_id : ID of the target WebSocket client +message : text message to send +return value : true if the target connection exists and the message was queued + +:desc +Queues a text WebSocket message for one specific connected client. + +This helper currently sends text frames only. + +:see +>websocket diff --git a/src/fastcgi/src/fcgicc.cc b/src/fastcgi/src/fcgicc.cc index aea2b63..dbee3ba 100644 --- a/src/fastcgi/src/fcgicc.cc +++ b/src/fastcgi/src/fcgicc.cc @@ -60,6 +60,20 @@ make_http_text_response(String status_line, String body, String extra_headers = ); } +static bool +is_valid_close_code(u16 status_code) +{ + if(status_code < 1000) + return(false); + if(status_code == 1004 || status_code == 1005 || status_code == 1006 || status_code == 1015) + return(false); + if(status_code <= 1014) + return(true); + if(status_code >= 3000 && status_code <= 4999) + return(true); + return(false); +} + void FastCGIServer::shutdown() { @@ -217,6 +231,36 @@ FastCGIServer::close_http_listeners() } } +void +FastCGIServer::close_websocket_connection(Connection& connection, u16 status_code, String reason) +{ + if(!connection.close_socket) + connection.output_buffer += ws_close_frame(status_code, reason); + connection.close_socket = true; +} + +void +FastCGIServer::fail_websocket_connection(Connection& connection, u16 status_code, String reason) +{ + close_websocket_connection(connection, status_code, reason); +} + +void +FastCGIServer::dispatch_websocket_message(Connection& connection, RequestID request_id, String payload, u8 opcode) +{ + RequestList::iterator it = connection.requests.find(request_id); + if(it == connection.requests.end() || !on_websocket_message) + return; + + it->second->resources.is_websocket = true; + it->second->resources.websocket_connection_id = connection.websocket_connection_id; + it->second->resources.websocket_scope = connection.websocket_scope; + it->second->resources.websocket_opcode = opcode; + it->second->resources.websocket_is_binary = (opcode == 0x2); + it->second->resources.websocket_is_text = (opcode == 0x1); + on_websocket_message(*it->second, payload, opcode); +} + void FastCGIServer::process(int timeout_ms) { @@ -394,9 +438,39 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data) if(to_lower(request.params["HTTP_UPGRADE"]) == "websocket") { Connection* connection = client_sockets[request.resources.client_socket]; + String request_method = trim(request.params["REQUEST_METHOD"]); + String request_protocol = trim(request.params["SERVER_PROTOCOL"]); String websocket_connection = to_lower(request.params["HTTP_CONNECTION"]); + String websocket_host = trim(request.params["HTTP_HOST"]); String websocket_key = trim(request.params["HTTP_SEC_WEBSOCKET_KEY"]); String websocket_version = trim(request.params["HTTP_SEC_WEBSOCKET_VERSION"]); + if(request_method != "GET") + { + connection->output_buffer += make_http_text_response( + "HTTP/1.1 405 Method Not Allowed", + "websocket upgrades require GET" + ); + connection->close_socket = true; + return; + } + if(request_protocol != "HTTP/1.1") + { + connection->output_buffer += make_http_text_response( + "HTTP/1.1 400 Bad Request", + "websocket upgrades require HTTP/1.1" + ); + connection->close_socket = true; + return; + } + if(websocket_host == "") + { + connection->output_buffer += make_http_text_response( + "HTTP/1.1 400 Bad Request", + "missing Host header" + ); + connection->close_socket = true; + return; + } if(websocket_connection.find("upgrade") == String::npos) { connection->output_buffer += make_http_text_response( @@ -415,6 +489,24 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data) connection->close_socket = true; return; } + if(!ws_is_valid_client_key(websocket_key)) + { + connection->output_buffer += make_http_text_response( + "HTTP/1.1 400 Bad Request", + "invalid Sec-WebSocket-Key header" + ); + connection->close_socket = true; + return; + } + if(websocket_version == "") + { + connection->output_buffer += make_http_text_response( + "HTTP/1.1 400 Bad Request", + "missing Sec-WebSocket-Version header" + ); + connection->close_socket = true; + return; + } if(websocket_version != "" && websocket_version != "13") { connection->output_buffer += make_http_text_response( @@ -482,44 +574,108 @@ FastCGIServer::process_websocket_input(Connection& connection) if(!frame.mask_bit) { - connection.output_buffer += ws_close_frame(1002, "client frames must be masked"); - connection.close_socket = true; + fail_websocket_connection(connection, 1002, "client frames must be masked"); return; } - if(!frame.is_final_fragment || frame.opcode == 0x0) + bool is_control_frame = (frame.opcode & 0x08) != 0; + if(is_control_frame && !frame.is_final_fragment) { - connection.output_buffer += ws_close_frame(1003, "fragmented frames are not supported"); - connection.close_socket = true; + fail_websocket_connection(connection, 1002, "control frames must not be fragmented"); return; } switch(frame.opcode) { - case 0x1: + case 0x0: { - RequestList::iterator it = connection.requests.find(connection.client_socket); - if(it != connection.requests.end() && on_websocket_message) + if(connection.websocket_fragment_opcode == 0) { - it->second->resources.is_websocket = true; - it->second->resources.websocket_connection_id = connection.websocket_connection_id; - it->second->resources.websocket_scope = connection.websocket_scope; - on_websocket_message(*it->second, frame.payload); + fail_websocket_connection(connection, 1002, "unexpected continuation frame"); + return; } + + connection.websocket_fragment_buffer += frame.payload; + if(!frame.is_final_fragment) + break; + + String payload = connection.websocket_fragment_buffer; + u8 opcode = connection.websocket_fragment_opcode; + connection.websocket_fragment_buffer = ""; + connection.websocket_fragment_opcode = 0; + + if(opcode == 0x1 && !ws_is_valid_utf8(payload)) + { + fail_websocket_connection(connection, 1007, "invalid UTF-8 text message"); + return; + } + + dispatch_websocket_message(connection, connection.client_socket, payload, opcode); + break; + } + case 0x1: + case 0x2: + { + if(connection.websocket_fragment_opcode != 0) + { + fail_websocket_connection(connection, 1002, "new data frame while fragmented message is active"); + return; + } + + if(frame.is_final_fragment) + { + if(frame.opcode == 0x1 && !ws_is_valid_utf8(frame.payload)) + { + fail_websocket_connection(connection, 1007, "invalid UTF-8 text message"); + return; + } + + dispatch_websocket_message(connection, connection.client_socket, frame.payload, frame.opcode); + break; + } + + connection.websocket_fragment_buffer = frame.payload; + connection.websocket_fragment_opcode = frame.opcode; break; } case 0x8: - connection.output_buffer += ws_close_frame(); - connection.close_socket = true; + { + if(frame.payload.length() == 1) + { + fail_websocket_connection(connection, 1002, "invalid close frame payload"); + return; + } + + u16 status_code = 1000; + String reason = ""; + if(frame.payload.length() >= 2) + { + status_code = + ((u16)(u8)frame.payload[0] << 8) | + (u16)(u8)frame.payload[1]; + reason = frame.payload.substr(2); + if(!is_valid_close_code(status_code)) + { + fail_websocket_connection(connection, 1002, "invalid close status code"); + return; + } + if(!ws_is_valid_utf8(reason)) + { + fail_websocket_connection(connection, 1007, "invalid UTF-8 close reason"); + return; + } + } + + close_websocket_connection(connection, status_code, reason); return; + } case 0x9: connection.output_buffer += ws_encode_frame(frame.payload, 0xA); break; case 0xA: break; default: - connection.output_buffer += ws_close_frame(1003, "unsupported websocket opcode"); - connection.close_socket = true; + fail_websocket_connection(connection, 1002, "unsupported websocket opcode"); return; } } diff --git a/src/fastcgi/src/fcgicc.h b/src/fastcgi/src/fcgicc.h index f2999c6..c94a529 100644 --- a/src/fastcgi/src/fcgicc.h +++ b/src/fastcgi/src/fcgicc.h @@ -48,7 +48,7 @@ public: std::function on_request = 0; std::function on_data = 0; std::function on_complete = 0; - std::function on_websocket_message = 0; + std::function on_websocket_message = 0; int listen(unsigned tcp_port); int listen_http(unsigned tcp_port); @@ -72,6 +72,8 @@ public: bool is_websocket = false; String websocket_connection_id; String websocket_scope; + String websocket_fragment_buffer; + u8 websocket_fragment_opcode = 0; char type = 'F'; // F = FastCGI, H = HttpServer }; @@ -86,6 +88,9 @@ public: void close_http_listeners(); void read_fgci(Connection&); void process_websocket_input(Connection&); + void fail_websocket_connection(Connection& connection, u16 status_code, String reason = ""); + void close_websocket_connection(Connection& connection, u16 status_code = 1000, String reason = ""); + void dispatch_websocket_message(Connection& connection, RequestID request_id, String payload, u8 opcode); bool websocket_send_to(String connection_id, String message); u64 websocket_broadcast(String scope, String message); StringList websocket_connection_ids(String scope = ""); diff --git a/src/lib/sys.h b/src/lib/sys.h index 1e6fcb5..d3982de 100644 --- a/src/lib/sys.h +++ b/src/lib/sys.h @@ -39,6 +39,8 @@ String socket_read(u64 sockfd, u32 max_length = 1024*128, u32 timeout = 1); String ws_message(); String ws_connection_id(); String ws_scope(); +u8 ws_opcode(); +bool ws_is_binary(); StringList ws_connections(String scope = ""); u64 ws_connection_count(String scope = ""); bool ws_send(String message, String scope = ""); diff --git a/src/lib/types.h b/src/lib/types.h index 6464aec..04aeab2 100644 --- a/src/lib/types.h +++ b/src/lib/types.h @@ -173,6 +173,9 @@ struct Request { bool is_websocket = false; String websocket_connection_id = ""; String websocket_scope = ""; + u8 websocket_opcode = 0; + bool websocket_is_binary = false; + bool websocket_is_text = false; std::string params_buffer; } resources; diff --git a/src/lib/uri.cpp b/src/lib/uri.cpp index 03cc2fb..4b7fe9d 100644 --- a/src/lib/uri.cpp +++ b/src/lib/uri.cpp @@ -27,6 +27,134 @@ static String base64_encode(String raw) return(result); } +static int base64_decode_value(char c) +{ + if(c >= 'A' && c <= 'Z') + return(c - 'A'); + if(c >= 'a' && c <= 'z') + return(c - 'a' + 26); + if(c >= '0' && c <= '9') + return(c - '0' + 52); + if(c == '+') + return(62); + if(c == '/') + return(63); + return(-1); +} + +static String base64_decode(String raw, bool& ok) +{ + ok = false; + String cleaned; + for(char c : raw) + { + if(!isspace(c)) + cleaned.append(1, c); + } + + if(cleaned.length() == 0 || (cleaned.length() % 4) != 0) + return(""); + + String result; + for(u32 i = 0; i < cleaned.length(); i += 4) + { + int values[4]; + int padding = 0; + for(u32 j = 0; j < 4; j++) + { + char c = cleaned[i + j]; + if(c == '=') + { + values[j] = 0; + padding += 1; + } + else + { + values[j] = base64_decode_value(c); + if(values[j] < 0) + return(""); + } + } + + if(padding > 2) + return(""); + if(padding > 0 && i + 4 != cleaned.length()) + return(""); + if(cleaned[i + 2] == '=' && cleaned[i + 3] != '=') + return(""); + + result.append(1, (char)((values[0] << 2) | (values[1] >> 4))); + if(cleaned[i + 2] != '=') + result.append(1, (char)(((values[1] & 0x0F) << 4) | (values[2] >> 2))); + if(cleaned[i + 3] != '=') + result.append(1, (char)(((values[2] & 0x03) << 6) | values[3])); + } + + ok = true; + return(result); +} + +bool ws_is_valid_utf8(String input) +{ + u32 i = 0; + while(i < input.length()) + { + u8 c = (u8)input[i]; + u32 trailing = 0; + u32 codepoint = 0; + + if(c <= 0x7F) + { + i += 1; + continue; + } + else if((c & 0xE0) == 0xC0) + { + trailing = 1; + codepoint = c & 0x1F; + if(codepoint == 0) + return(false); + } + else if((c & 0xF0) == 0xE0) + { + trailing = 2; + codepoint = c & 0x0F; + } + else if((c & 0xF8) == 0xF0) + { + trailing = 3; + codepoint = c & 0x07; + } + else + { + return(false); + } + + if(i + trailing >= input.length()) + return(false); + + for(u32 j = 1; j <= trailing; j++) + { + u8 follow = (u8)input[i + j]; + if((follow & 0xC0) != 0x80) + return(false); + codepoint = (codepoint << 6) | (follow & 0x3F); + } + + if((trailing == 1 && codepoint < 0x80) || + (trailing == 2 && codepoint < 0x800) || + (trailing == 3 && codepoint < 0x10000)) + return(false); + if(codepoint > 0x10FFFF) + return(false); + if(codepoint >= 0xD800 && codepoint <= 0xDFFF) + return(false); + + i += trailing + 1; + } + return(true); +} + String var_dump(URI uri, String prefix, String postfix) { return( @@ -463,6 +591,13 @@ String ws_make_accept_key(String client_key) ))); } +bool ws_is_valid_client_key(String client_key) +{ + bool ok = false; + String decoded = base64_decode(trim(client_key), ok); + return(ok && decoded.length() == 16); +} + String ws_encode_frame(String payload, u8 opcode, bool is_final_fragment) { String frame; @@ -513,6 +648,9 @@ bool WSFrame::parse(const String& buffer, String& error) const unsigned char* raw = (const unsigned char*)buffer.data(); opcode = raw[0] & 0x0F; is_final_fragment = (raw[0] & 0x80) != 0; + rsv1 = (raw[0] & 0x40) != 0; + rsv2 = (raw[0] & 0x20) != 0; + rsv3 = (raw[0] & 0x10) != 0; mask_bit = (raw[1] & 0x80) != 0; payload_length = raw[1] & 0x7F; header_length = 2; @@ -528,6 +666,11 @@ bool WSFrame::parse(const String& buffer, String& error) { if(buffer.length() < 10) return(false); + if((raw[2] & 0x80) != 0) + { + error = "invalid websocket frame length"; + return(false); + } payload_length = 0; for(u32 i = 0; i < 8; i++) payload_length = (payload_length << 8) | (u64)raw[2 + i]; @@ -544,6 +687,17 @@ bool WSFrame::parse(const String& buffer, String& error) error = "invalid websocket frame length"; return(false); } + if(rsv1 || rsv2 || rsv3) + { + error = "reserved websocket bits are not supported"; + return(false); + } + bool is_control_frame = (opcode & 0x08) != 0; + if(is_control_frame && payload_length > 125) + { + error = "control frames must be 125 bytes or less"; + return(false); + } if(buffer.length() < frame_length) return(false); diff --git a/src/lib/uri.h b/src/lib/uri.h index 7f758bb..a0913e9 100644 --- a/src/lib/uri.h +++ b/src/lib/uri.h @@ -18,14 +18,19 @@ void save_session_data(String session_id, StringMap data); String session_start(String session_name = "uce-session"); void session_destroy(String session_name = "uce-session"); String ws_make_accept_key(String client_key); +bool ws_is_valid_client_key(String client_key); String ws_encode_frame(String payload, u8 opcode = 0x1, bool is_final_fragment = true); String ws_close_frame(u16 status_code = 1000, String reason = ""); +bool ws_is_valid_utf8(String input); struct WSFrame { u8 opcode = 0; bool is_final_fragment = false; bool mask_bit = false; + bool rsv1 = false; + bool rsv2 = false; + bool rsv3 = false; u64 payload_length = 0; u64 header_length = 0; u64 frame_length = 0; diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 384356d..96e593d 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -58,6 +58,20 @@ String ws_scope() return(current_ws_scope()); } +u8 ws_opcode() +{ + if(!context) + return(0); + return(context->resources.websocket_opcode); +} + +bool ws_is_binary() +{ + if(!context) + return(false); + return(context->resources.websocket_is_binary); +} + StringList ws_connections(String scope) { return(server.websocket_connection_ids(normalize_ws_scope(scope))); @@ -173,7 +187,7 @@ int handle_complete(FastCGIRequest& request) { return 0; } -int handle_websocket_message(FastCGIRequest& request, const String& message) +int handle_websocket_message(FastCGIRequest& request, const String& message, u8 opcode) { Request event_request; ByteStream ws_output; @@ -202,6 +216,9 @@ int handle_websocket_message(FastCGIRequest& request, const String& message) event_request.var["ws"]["connection_id"] = request.resources.websocket_connection_id; event_request.var["ws"]["scope"] = request.resources.websocket_scope; event_request.var["ws"]["connection_count"] = (f64)server.websocket_connection_ids(request.resources.websocket_scope).size(); + event_request.var["ws"]["opcode"] = (f64)opcode; + event_request.var["ws"]["is_binary"].set_bool(request.resources.websocket_is_binary); + event_request.var["ws"]["is_text"].set_bool(request.resources.websocket_is_text); event_request.var["ws"]["document_uri"] = first( request.params["DOCUMENT_URI"], request.params["REQUEST_URI"] @@ -210,6 +227,7 @@ int handle_websocket_message(FastCGIRequest& request, const String& message) call_param["message"] = message; call_param["connection_id"] = request.resources.websocket_connection_id; call_param["scope"] = request.resources.websocket_scope; + call_param["opcode"] = (f64)opcode; call_param["document_uri"] = event_request.var["ws"]["document_uri"].to_string(); compiler_invoke_websocket(&event_request, request.params["SCRIPT_FILENAME"], call_param); diff --git a/test/websockets.ws.uce b/test/websockets.ws.uce index 96d0a95..e07aec0 100644 --- a/test/websockets.ws.uce +++ b/test/websockets.ws.uce @@ -26,46 +26,33 @@ RENDER() u64 online = ws_connection_count(); <> - -
-

- UCE Test: - WebSocket Chat -

-
-
Page scope:
-
Connected right now:
-
Connecting...
+

+ UCE Test: + WebSocket Chat +

+ + +
Page scope: 
+Connected right now: 
+Status: Connecting...
+ +
+
+ +
-
-
+
+ +
- -
- - -
-
- -
- -
+
+ +
+ + + +

+