fix: re-add WebSocket continuation-frame support for fragmented messages

Prosody 13.0.6 removed continuation-frame (fragmented-message) support
from mod_websocket — validate_frame hard-rejects any frame with FIN=false
(close code 1003). VNCtalk clients fragment large WebSocket messages
(e.g. vCard sets with avatars >~64 KB), so the connection is silently
reset. The rejection fires on the partial-frame validation path before
handle_frame is reached, and websocket_close() does not log, so nothing
appears in prosody debug logs.

This patch removes the blanket FIN=false rejection from validate_frame
and restores the dataBuffer fragment-accumulation logic from 0.11.6 in
handle_frame: text frames (0x1) with FIN=false start a buffer,
continuation frames (0x0) append, and concatenated data is returned
only when FIN=true arrives.

Part-of: <http://gitlab.vnc.biz/uxf/vnctalk-prosody/-/merge_requests/3>
This commit is contained in:
2026-07-15 17:58:02 +02:00
parent bc5b550d9c
commit a2f46c99bc
2 changed files with 55 additions and 0 deletions
+1
View File
@@ -21,6 +21,7 @@ To change a patch: extract the pristine tarball, apply all patches, edit, and re
| `muc.lib.patch` | `plugins/muc/muc.lib.lua` | (a) `broadcast()` also routes to offline *remote* affiliated members; (b) suppress the unavailable self-presence in `publicise_occupant_status`; (c) let owner/admin/member post to a room without being a present occupant (bypasses the `muc-occupant-groupchat` event and its not-in-room rejection); (d) fire `muc-config-sub-mitted` on config changes — **consumed by an external service**. |
| `hidden.lib.patch` | `plugins/muc/hidden.lib.lua` | Hide the "publicly searchable" room config option for everyone when public rooms are restricted (upstream exempts admins). |
| `mod_muc_unique.patch` | `plugins/mod_muc_unique.lua` | Rework muc#unique handler; answer `item-not-found` for bare-JID requests. |
| `mod_websocket.patch` | `plugins/mod_websocket.lua` | Re-add WebSocket continuation-frame (fragmented-message) support that was removed upstream in the 0.12/13.0 rewrite. VNCtalk clients fragment large WebSocket messages (e.g. vCard sets with avatars >~64 KB); 13.0.6's `validate_frame` hard-rejected any frame with `FIN=false` (close code 1003) **silently** — no log was emitted because the rejection fired on the partial-frame path before `handle_frame` was reached, and `websocket_close()` itself does not log. This patch removes the blanket `not frame.FIN` rejection from `validate_frame` and restores the `dataBuffer` fragment-accumulation logic from 0.11.6 inside `handle_frame`: text frames (opcode 0x1) with `FIN=false` start a buffer, continuation frames (opcode 0x0) append to it, and the concatenated data is returned only when a frame with `FIN=true` arrives. |
## Patches removed at the 0.12.6 upgrade (M1)
+54
View File
@@ -0,0 +1,54 @@
--- a/plugins/mod_websocket.lua 2026-07-15 09:48:22.424559760 +0200
+++ b/plugins/mod_websocket.lua 2026-07-15 09:49:05.585990882 +0200
@@ -199,9 +199,6 @@
end
-- Other (XMPP-specific) validity checks
- if not frame.FIN then
- return false, 1003, "Continuation frames are not supported, RFC 7395 3.3.3";
- end
if opcode == 0x01 and frame.data and frame.data:byte(1, 1) ~= 60 then
return false, 1007, "Invalid payload start character, RFC 7395 3.3.3";
end
@@ -249,6 +246,7 @@
end
end
+ local dataBuffer;
local function handle_frame(frame)
module:log("debug", "Websocket received frame: opcode=%0x, %i bytes", frame.opcode, #frame.data);
@@ -266,12 +264,31 @@
return "";
elseif opcode == 0xA then -- Pong frame, MAY be sent unsolicited, eg as keepalive
return "";
- elseif opcode ~= 0x1 then -- Not text frame (which is all we support)
+ end
+
+ -- Text (0x1) and continuation (0x0) frames: accumulate fragments
+ if opcode == 0x0 and not dataBuffer then
+ return false, 1002, "Unexpected continuation frame";
+ end
+ if opcode == 0x1 and dataBuffer then
+ return false, 1002, "Continuation frame expected";
+ end
+
+ if opcode == 0x0 then -- Continuation frame
+ dataBuffer[#dataBuffer+1] = frame.data;
+ elseif opcode == 0x1 then -- Text frame
+ dataBuffer = { frame.data };
+ else
log("warn", "Received frame with unsupported opcode %i", opcode);
return "";
end
- return frame.data;
+ if frame.FIN then
+ local data = t_concat(dataBuffer, "");
+ dataBuffer = nil;
+ return data;
+ end
+ return "";
end
conn:setlistener(c2s_listener);