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
+234 -12
View File
@@ -47,6 +47,19 @@
#include "../fastcgi_devkit/fastcgi.h"
static String
make_http_text_response(String status_line, String body, String extra_headers = "")
{
return(
status_line + "\r\n"
"Content-Type: text/plain; charset=utf-8\r\n" +
extra_headers +
"Content-Length: " + std::to_string(body.length()) + "\r\n"
"Connection: close\r\n\r\n" +
body
);
}
void
FastCGIServer::shutdown()
{
@@ -187,6 +200,23 @@ FastCGIServer::send_output_buffer(Connection& con)
return write_result;
}
void
FastCGIServer::close_http_listeners()
{
for(std::vector<int>::iterator it = server_sockets.begin(); it != server_sockets.end();)
{
int socket_handle = *it;
if(server_socket_types[socket_handle] == 'H')
{
close(socket_handle);
server_socket_types.erase(socket_handle);
it = server_sockets.erase(it);
continue;
}
++it;
}
}
void
FastCGIServer::process(int timeout_ms)
{
@@ -257,7 +287,11 @@ FastCGIServer::process(int timeout_ms)
throw std::runtime_error("read() on socket failed");
if (read_result == 0)
{
if(it->second->type == 'H' && it->second->input_buffer != "")
if(it->second->type == 'H' && it->second->is_websocket)
{
it->second->close_socket = true;
}
else if(it->second->type == 'H' && it->second->input_buffer != "")
{
process_http_request(
*client_sockets[it->second->client_socket]->requests[it->second->client_socket],
@@ -277,11 +311,18 @@ FastCGIServer::process(int timeout_ms)
it->second->input_buffer.append(buffer, read_result);
if(it->second->type == 'H')
{
process_http_request(
*client_sockets[it->second->client_socket]->requests[it->second->client_socket],
it->second->input_buffer);
if(it->second->close_socket || !it->second->output_buffer.empty())
it->second->input_buffer = "";
if(it->second->is_websocket)
{
process_websocket_input(*it->second);
}
else
{
process_http_request(
*client_sockets[it->second->client_socket]->requests[it->second->client_socket],
it->second->input_buffer);
if(it->second->close_socket || !it->second->output_buffer.empty())
it->second->input_buffer = "";
}
}
else
{
@@ -293,7 +334,8 @@ FastCGIServer::process(int timeout_ms)
if (!it->second->output_buffer.empty() &&
FD_ISSET(read_socket, &fs_write))
{
write_fgci(*it->second);
if(it->second->type == 'F')
write_fgci(*it->second);
send_output_buffer(*it->second);
}
@@ -341,16 +383,68 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
}
}
request.in = data.substr(header_end + 4);
u64 content_length = int_val(first(request.params["CONTENT_LENGTH"], "0"));
if(request.in.length() < content_length)
u64 request_size = header_end + 4 + content_length;
if(data.length() < request_size)
return;
request.in = data.substr(header_end + 4, content_length);
request.flags.input_closed = true;
data.erase(0, request_size);
if(request.params["HTTP_UPGRADE"] == "websocket")
if(to_lower(request.params["HTTP_UPGRADE"]) == "websocket")
{
printf("(i) got upgrade request %s\n", var_dump(request.params).c_str());
client_sockets[request.resources.client_socket]->close_socket = true;
Connection* connection = client_sockets[request.resources.client_socket];
String websocket_connection = to_lower(request.params["HTTP_CONNECTION"]);
String websocket_key = trim(request.params["HTTP_SEC_WEBSOCKET_KEY"]);
String websocket_version = trim(request.params["HTTP_SEC_WEBSOCKET_VERSION"]);
if(websocket_connection.find("upgrade") == String::npos)
{
connection->output_buffer += make_http_text_response(
"HTTP/1.1 400 Bad Request",
"missing Connection: Upgrade header"
);
connection->close_socket = true;
return;
}
if(websocket_key == "")
{
connection->output_buffer += make_http_text_response(
"HTTP/1.1 400 Bad Request",
"missing Sec-WebSocket-Key header"
);
connection->close_socket = true;
return;
}
if(websocket_version != "" && websocket_version != "13")
{
connection->output_buffer += make_http_text_response(
"HTTP/1.1 426 Upgrade Required",
"unsupported websocket version",
"Sec-WebSocket-Version: 13\r\n"
);
connection->close_socket = true;
return;
}
connection->is_websocket = true;
connection->websocket_connection_id = std::to_string(getpid()) + ":" + std::to_string(connection->client_socket);
connection->websocket_scope = first(
request.params["SCRIPT_FILENAME"],
request.params["DOCUMENT_URI"],
request.params["REQUEST_URI"]
);
request.resources.is_websocket = true;
request.resources.websocket_connection_id = connection->websocket_connection_id;
request.resources.websocket_scope = connection->websocket_scope;
connection->output_buffer +=
"HTTP/1.1 101 Switching Protocols\r\n"
"Upgrade: websocket\r\n"
"Connection: Upgrade\r\n"
"Sec-WebSocket-Accept: " + ws_make_accept_key(websocket_key) + "\r\n\r\n";
if(!data.empty())
process_websocket_input(*connection);
}
else
{
@@ -367,6 +461,134 @@ FastCGIServer::process_http_request(FastCGIRequest& request, String& data)
}
}
void
FastCGIServer::process_websocket_input(Connection& connection)
{
while(!connection.input_buffer.empty())
{
WSFrame frame;
String error;
if(!frame.parse(connection.input_buffer, error))
{
if(error != "")
{
connection.output_buffer += ws_close_frame(1002, error);
connection.close_socket = true;
}
return;
}
connection.input_buffer.erase(0, frame.frame_length);
if(!frame.mask_bit)
{
connection.output_buffer += ws_close_frame(1002, "client frames must be masked");
connection.close_socket = true;
return;
}
if(!frame.is_final_fragment || frame.opcode == 0x0)
{
connection.output_buffer += ws_close_frame(1003, "fragmented frames are not supported");
connection.close_socket = true;
return;
}
switch(frame.opcode)
{
case 0x1:
{
RequestList::iterator it = connection.requests.find(connection.client_socket);
if(it != connection.requests.end() && on_websocket_message)
{
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);
}
break;
}
case 0x8:
connection.output_buffer += ws_close_frame();
connection.close_socket = true;
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;
return;
}
}
}
bool
FastCGIServer::websocket_send_to(String connection_id, String message)
{
for(auto& item : client_sockets)
{
Connection* connection = item.second;
if(connection->is_websocket && connection->websocket_connection_id == connection_id)
{
connection->output_buffer += ws_encode_frame(message);
return(true);
}
}
return(false);
}
u64
FastCGIServer::websocket_broadcast(String scope, String message)
{
u64 sent = 0;
for(auto& item : client_sockets)
{
Connection* connection = item.second;
if(!connection->is_websocket)
continue;
if(scope != "" && connection->websocket_scope != scope)
continue;
connection->output_buffer += ws_encode_frame(message);
sent += 1;
}
return(sent);
}
StringList
FastCGIServer::websocket_connection_ids(String scope)
{
StringList result;
for(auto& item : client_sockets)
{
Connection* connection = item.second;
if(!connection->is_websocket)
continue;
if(scope != "" && connection->websocket_scope != scope)
continue;
result.push_back(connection->websocket_connection_id);
}
return(result);
}
bool
FastCGIServer::websocket_close(String connection_id, u16 status_code, String reason)
{
for(auto& item : client_sockets)
{
Connection* connection = item.second;
if(connection->is_websocket && connection->websocket_connection_id == connection_id)
{
connection->output_buffer += ws_close_frame(status_code, reason);
connection->close_socket = true;
return(true);
}
}
return(false);
}
void
FastCGIServer::process_forever()
{
+10
View File
@@ -48,6 +48,7 @@ public:
std::function<int(FastCGIRequest&)> on_request = 0;
std::function<int(FastCGIRequest&)> on_data = 0;
std::function<int(FastCGIRequest&)> on_complete = 0;
std::function<int(FastCGIRequest&, const String&)> on_websocket_message = 0;
int listen(unsigned tcp_port);
int listen_http(unsigned tcp_port);
@@ -68,6 +69,9 @@ public:
std::string output_buffer;
bool close_responsibility = false;
bool close_socket = false;
bool is_websocket = false;
String websocket_connection_id;
String websocket_scope;
char type = 'F'; // F = FastCGI, H = HttpServer
};
@@ -79,7 +83,13 @@ public:
std::map<int, char> server_socket_types;
std::map<int, Connection*> client_sockets;
void close_http_listeners();
void read_fgci(Connection&);
void process_websocket_input(Connection&);
bool websocket_send_to(String connection_id, String message);
u64 websocket_broadcast(String scope, String message);
StringList websocket_connection_ids(String scope = "");
bool websocket_close(String connection_id, u16 status_code = 1000, String reason = "");
static void request_write_fgci(Connection&, RequestID, FastCGIRequest&);
static void write_fgci(Connection&);
static Pairs parse_pairs_fcgi(const char*, std::string::size_type);