Enhance WebSocket support: add opcode handling, binary message support, and improve connection validation

This commit is contained in:
udo
2026-04-18 18:41:50 +00:00
parent 86dc93864e
commit 46d98a092f
25 changed files with 623 additions and 69 deletions
+172 -16
View File
@@ -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;
}
}
+6 -1
View File
@@ -48,7 +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;
std::function<int(FastCGIRequest&, const String&, u8)> 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 = "");