Add WebSocket support and enhance chat functionality

- Implement WebSocket handling in the FastCGI server, allowing for real-time communication.
- Introduce functions for managing WebSocket connections, broadcasting messages, and sending to specific connections.
- Create a chat interface in the `websockets.ws.uce` file, including message handling and user notifications.
- Refactor existing code to accommodate WebSocket integration, ensuring compatibility with HTTP requests.
- Update documentation to reflect new features and usage instructions for WebSocket functionality.
This commit is contained in:
udo
2026-04-18 15:10:15 +00:00
parent cd8f07aaa7
commit 86dc93864e
13 changed files with 794 additions and 110 deletions
+29 -1
View File
@@ -202,6 +202,7 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name)
//setup_unit_paths(context, su, file_name);
su->on_render = 0;
su->on_websocket = 0;
su->on_setup = 0;
su->compiler_messages = "";
@@ -223,6 +224,9 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name)
if ((error = dlerror()) != NULL)
printf("Error - %s in %s\n", error, su->file_name.c_str());
su->on_render = (call_handler)dlsym(su->so_handle, "render");
dlerror();
su->on_websocket = (call_handler)dlsym(su->so_handle, "websocket");
dlerror();
su->api_declarations = split(file_get_contents(su->api_file_name), "\n");
//else
// printf("(i) loaded unit %s\n", su->file_name.c_str());
@@ -403,6 +407,31 @@ void compiler_invoke(Request* context, String file_name, DTree& call_param)
}
}
void compiler_invoke_websocket(Request* context, String file_name, DTree& call_param)
{
auto su = compiler_load_shared_unit(context, file_name, "", false);
if(!su)
return;
if(!su->on_setup)
{
printf("internal error: set_current_request() not defined in %s\n", file_name.c_str());
return;
}
if(!su->on_websocket)
{
printf("no WS() entry point in %s\n", file_name.c_str());
return;
}
String prev_wd = get_cwd();
set_cwd(su->src_path);
su->on_setup(context);
su->on_websocket(call_param);
set_cwd(prev_wd);
}
void render_file(String file_name)
{
//printf("(i) render_file(%s)\n", file_name.c_str());
@@ -461,4 +490,3 @@ DTree* call_file(String file_name, String function_name, DTree* call_param)
}
return(result);
}
+2
View File
@@ -1,4 +1,5 @@
#define RENDER() extern "C" void render(DTree& call)
#define WS() extern "C" void websocket(DTree& call)
#define EXPORT extern "C"
String process_html_literal(Request* context, SharedUnit* su, String content);
@@ -8,6 +9,7 @@ void load_shared_unit(Request* context, SharedUnit* su, String file_name);
void compile_shared_unit(Request* context, SharedUnit* su, String file_name);
SharedUnit* get_shared_unit(Request* context, String file_name, bool opt_so_optional = false);
void compiler_invoke(Request* context, String file_name, DTree& call_param);
void compiler_invoke_websocket(Request* context, String file_name, DTree& call_param);
SharedUnit* compiler_load_shared_unit(Request* context, String file_name, String current_path = "", bool opt_so_optional = false);
SharedUnit* load_file(String file_name);
+3 -1
View File
@@ -431,7 +431,7 @@ std::map<pid_t, Worker> workers;
#include <sys/resource.h>
#include <sys/prctl.h>
void spawn_subprocess(std::function<void()> exec_after_spawn)
pid_t spawn_subprocess(std::function<void()> exec_after_spawn)
{
parent_pid = getpid();
pid_t p;
@@ -442,6 +442,7 @@ void spawn_subprocess(std::function<void()> exec_after_spawn)
//printf("(C) child procress started, PID:%i\n", my_pid);
prctl(PR_SET_PDEATHSIG, SIGHUP);
exec_after_spawn();
return(0);
}
else
{
@@ -449,6 +450,7 @@ void spawn_subprocess(std::function<void()> exec_after_spawn)
w.pid = p;
workers[w.pid] = w;
printf("(P) child procress spawned: PID %i\n", p);
return(p);
}
}
+10
View File
@@ -36,6 +36,16 @@ void socket_close(u64 sockfd);
bool socket_write(u64 sockfd, String data);
String socket_read(u64 sockfd, u32 max_length = 1024*128, u32 timeout = 1);
String ws_message();
String ws_connection_id();
String ws_scope();
StringList ws_connections(String scope = "");
u64 ws_connection_count(String scope = "");
bool ws_send(String message, String scope = "");
u64 ws_broadcast(String message, String scope = "");
bool ws_send_to(String connection_id, String message);
bool ws_close(String connection_id = "");
String memcache_escape_key(String key);
StringList memcache_escape_keys(StringList keys);
u64 memcache_connect(String host = "127.0.0.1", short port = 11211);
+4 -1
View File
@@ -47,7 +47,10 @@ void Request::ob_start()
Request::~Request()
{
for(auto* stream : ob_stack)
delete stream;
ob_stack.clear();
ob = 0;
for(auto& sockfd : resources.sockets)
close(sockfd);
}
+4
View File
@@ -78,6 +78,7 @@ struct SharedUnit {
request_handler on_setup;
call_handler on_render;
call_handler on_websocket;
String compiler_messages;
time_t last_compiled;
@@ -169,6 +170,9 @@ struct Request {
std::vector<void*> mysql_connections;
u64 client_socket = 0;
u64 server_socket = 0;
bool is_websocket = false;
String websocket_connection_id = "";
String websocket_scope = "";
std::string params_buffer;
} resources;
+130
View File
@@ -1,5 +1,32 @@
#include "uri.h"
static String base64_encode(String raw)
{
static const char* chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
String result;
int val = 0;
int bits = -6;
for(unsigned char c : raw)
{
val = (val << 8) + c;
bits += 8;
while(bits >= 0)
{
result.append(1, chars[(val >> bits) & 0x3F]);
bits -= 6;
}
}
if(bits > -6)
result.append(1, chars[((val << 8) >> (bits + 8)) & 0x3F]);
while(result.length() % 4 != 0)
result.append(1, '=');
return(result);
}
String var_dump(URI uri, String prefix, String postfix)
{
return(
@@ -427,3 +454,106 @@ void session_destroy(String session_name)
context->session_id = "";
}
}
String ws_make_accept_key(String client_key)
{
return(base64_encode(gen_sha1(
client_key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11",
true
)));
}
String ws_encode_frame(String payload, u8 opcode, bool is_final_fragment)
{
String frame;
u8 first_byte = opcode & 0x0F;
if(is_final_fragment)
first_byte |= 0x80;
frame.append(1, first_byte);
u64 payload_length = payload.length();
if(payload_length <= 125)
{
frame.append(1, (char)payload_length);
}
else if(payload_length <= 0xFFFF)
{
frame.append(1, 126);
frame.append(1, (char)((payload_length >> 8) & 0xFF));
frame.append(1, (char)(payload_length & 0xFF));
}
else
{
frame.append(1, 127);
for(int shift = 56; shift >= 0; shift -= 8)
frame.append(1, (char)((payload_length >> shift) & 0xFF));
}
frame += payload;
return(frame);
}
String ws_close_frame(u16 status_code, String reason)
{
String payload;
payload.append(1, (char)((status_code >> 8) & 0xFF));
payload.append(1, (char)(status_code & 0xFF));
payload += reason;
return(ws_encode_frame(payload, 0x8));
}
bool WSFrame::parse(const String& buffer, String& error)
{
error = "";
payload = "";
if(buffer.length() < 2)
return(false);
const unsigned char* raw = (const unsigned char*)buffer.data();
opcode = raw[0] & 0x0F;
is_final_fragment = (raw[0] & 0x80) != 0;
mask_bit = (raw[1] & 0x80) != 0;
payload_length = raw[1] & 0x7F;
header_length = 2;
if(payload_length == 126)
{
if(buffer.length() < 4)
return(false);
payload_length = ((u64)raw[2] << 8) | (u64)raw[3];
header_length = 4;
}
else if(payload_length == 127)
{
if(buffer.length() < 10)
return(false);
payload_length = 0;
for(u32 i = 0; i < 8; i++)
payload_length = (payload_length << 8) | (u64)raw[2 + i];
header_length = 10;
}
u64 mask_offset = header_length;
if(mask_bit)
header_length += 4;
frame_length = header_length + payload_length;
if(frame_length < header_length)
{
error = "invalid websocket frame length";
return(false);
}
if(buffer.length() < frame_length)
return(false);
payload.assign(buffer.data() + header_length, payload_length);
if(mask_bit)
{
const unsigned char* mask = raw + mask_offset;
for(u64 i = 0; i < payload.length(); i++)
payload[i] = payload[i] ^ mask[i % 4];
}
return(true);
}
+11 -60
View File
@@ -17,69 +17,20 @@ StringMap load_session_data(String session_id);
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);
String ws_encode_frame(String payload, u8 opcode = 0x1, bool is_final_fragment = true);
String ws_close_frame(u16 status_code = 1000, String reason = "");
struct WSFrame {
u8 opcode;
u8 is_final_fragment;
u8 mask_bit;
u64 payload_length;
u64 offset;
u8* payload;
u8 opcode = 0;
bool is_final_fragment = false;
bool mask_bit = false;
u64 payload_length = 0;
u64 header_length = 0;
u64 frame_length = 0;
String payload;
WSFrame(String& bufstr)
{
const char* buffer = bufstr.c_str();
u64 buffer_size = bufstr.size();
opcode = buffer[0] & 0x0F;
is_final_fragment = buffer[0] & 0x80;
mask_bit = buffer[1] & 0x80;
payload_length = buffer[1] & 0x7F;
offset = 2;
if (payload_length == 126) {
payload_length = ((u64) buffer[2] << 8) | buffer[3];
offset = 4;
} else if (payload_length == 127) {
payload_length = ((u64) buffer[2] << 56) |
((u64) buffer[3] << 48) |
((u64) buffer[4] << 40) |
((u64) buffer[5] << 32) |
((u64) buffer[6] << 24) |
((u64) buffer[7] << 16) |
((u64) buffer[8] << 8) |
buffer[9];
offset = 10;
}
uint8_t* maskingKey = nullptr;
if (mask_bit) {
maskingKey = reinterpret_cast<uint8_t*>(const_cast<char*>(buffer + offset));
offset += 4;
}
// Parse the frame payload
payload = reinterpret_cast<uint8_t*>(const_cast<char*>(buffer + offset));
if (mask_bit) {
for (int i = 0; i < payload_length; i++) {
payload[i] = payload[i] ^ maskingKey[i % 4];
}
}
// Print the parsed frame
std::cout << "Parsed WebSocket frame:" << std::endl;
std::cout << " Opcode: " << (int) opcode << std::endl;
std::cout << " Is final fragment: " << (is_final_fragment ? "true" : "false") << std::endl;
std::cout << " Payload length: " << payload_length << std::endl;
if (mask_bit) {
std::cout << " Masking key: ";
for (int i = 0; i < 4; i++) {
std::cout << std::hex << (int) maskingKey[i] << " ";
}
std::cout << std::endl;
}
std::cout << " Payload: " << std::endl << std::string((const char*) payload, payload_length) << std::endl;
}
bool parse(const String& buffer, String& error);
};