From 1865f030851e42fa71ab23c23878071bd099d40f Mon Sep 17 00:00:00 2001 From: Peter Harpending Date: Tue, 21 Oct 2025 12:10:28 -0700 Subject: [PATCH] Almost done... have to fix send and then of course test it there will be no bugs, right? --- src/fd_ws.erl | 103 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 97 insertions(+), 6 deletions(-) diff --git a/src/fd_ws.erl b/src/fd_ws.erl index 8094de2..9a8e3fe 100644 --- a/src/fd_ws.erl +++ b/src/fd_ws.erl @@ -391,7 +391,7 @@ recv(Sock, Received, Frames) -> Message :: ws_msg(), NewFrames :: Frames, Reason :: any(). -% @doc +% @private % try to parse the stack of frames into a single message % % ignores RSV bits @@ -400,14 +400,100 @@ recv(Sock, Received, Frames) -> maybe_pop_msg([]) -> incomplete; % case 1: control frames -maybe_pop_msg([Frame = #frame{opcode = Opcode} | Frames]) - when Opcode =:= close; Opcode =:= ping; Opcode =:= pong -> +% note that maybe_control_msg checks that the fin bit is true +% +% meaning if the client sends a malicious control frame with fin=false, that +% error will be caught in maybe_control_msg +maybe_pop_msg([Frame = #frame{opcode = ControlOpcode} | Frames]) + when (ControlOpcode =:= close) + orelse (ControlOpcode =:= ping) + orelse (ControlOpcode =:= pong) -> case maybe_control_msg(Frame) of {ok, Msg} -> {ok, Msg, Frames}; Error -> Error end; -maybe_pop_msg(_) -> - error(nyi). +% case 2: messages +% finished message in a single frame, just pull here +maybe_pop_msg([Frame = #frame{fin = true, + opcode = DataOpcode, + mask = Mask, + masking_key = Key, + payload = Payload} + | Rest]) + when DataOpcode =:= text; DataOpcode =:= binary -> + case maybe_unmask(Frame, Mask, Key, Payload) of + {ok, Unmasked} -> + Message = {DataOpcode, Unmasked}, + {ok, Message, Rest}; + Error -> + Error + end; +% end of a long message +maybe_pop_msg(Frames = [#frame{fin = true, + opcode = continuation} | _]) -> + maybe_long_data_msg(Frames); +% unfinished message, say we need more +maybe_pop_msg([#frame{fin = false, + opcode = continuation} + | _]) -> + incomplete; +% wtf... this case should be impossible +maybe_pop_msg([Frame | _]) -> + {error, {wtf_frame, Frame}}. + + + +-spec maybe_long_data_msg(Frames) -> Result + when Frames :: [frame()], + Result :: {ok, Message, NewFrames} + | {error, Reason}, + Message :: ws_msg(), + NewFrames :: Frames, + Reason :: any(). +% @private +% assumes: +% 1. top of stack is a finished frame +% 2. top opcode is continuation +% 3. the stack corresponds to a linear sequence of frames all corresponding to +% one message, until we get to the leading frame of the message, which must +% have opcode text|binary +% +% the reason we can make this assumption is because anterior in the call +% chain is recv/3, which eagerly consumes control messages +% +% meaning if we encounter a control frame in the middle here, we can assume +% there is some sort of bug +% +% TODO: I am NOT enforcing that the data message consumes the entire stack of +% frames. Given that the context here is eager consumption, this might be a +% point of enforcement. Need to think about this. +% @end + +maybe_long_data_msg(Frames) -> + mldm(Frames, Frames, <<>>). + + +% general case: decode the payload in this frame +mldm(OrigFrames, [Frame | Rest], Acc) -> + Opcode = Frame#frame.opcode, + Mask = Frame#frame.mask, + Key = Frame#frame.masking_key, + Payload = Frame#frame.payload, + case maybe_unmask(Frame, Mask, Key, Payload) of + {ok, Unmasked} -> + NewAcc = <>, + case Opcode of + continuation -> mldm(OrigFrames, Rest, NewAcc); + text -> {ok, {text, NewAcc}, Rest}; + binary -> {ok, {binary, NewAcc}, Rest}; + _ -> {error, {illegal_data_frame, Frame, OrigFrames, Acc}} + end; + Error -> + Error + end; +% out of frames +mldm(OrigFrames, [], Acc) -> + {error, {no_start_frame, Acc, OrigFrames}}. @@ -419,6 +505,9 @@ maybe_pop_msg(_) -> Reason :: any(). % @private % assume the frame is a control frame, validate it, and unmask the payload +% +% TODO: this doesn't enforce that messages from the client HAVE to be masked, +% which strictly speaking is part of the protocol. maybe_control_msg(F = #frame{fin = true, opcode = Opcode, @@ -611,11 +700,13 @@ recv_frame_await(Frame, Sock, Received) -> Reason :: closed | {timeout, RestData} | inet:posix(), RestData :: binary() | erlang:iovec(). % @doc +% FIXME: this should be sending a message, not an arbitrary payload +% % send binary data over Socket. handles frame nonsense % % types the payload as bytes % -% max payload size is 2^64 - 1 bytes +% max payload size is 2^63 - 1 bytes % @end send(Socket, Payload) ->