aboutsummaryrefslogtreecommitdiffstats
path: root/src/cowboy_websocket.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2013-01-12 22:31:23 +0100
committerLoïc Hoguin <[email protected]>2013-01-12 22:31:23 +0100
commitcccc0bc475d903e553c66c844f5bc0b0934e2b59 (patch)
treed0098e11535adcf3d0530411d78230b41aa621e9 /src/cowboy_websocket.erl
parent71b68d53d91b0a8fd104ef9fe661ef0f11ee2412 (diff)
downloadcowboy-cccc0bc475d903e553c66c844f5bc0b0934e2b59.tar.gz
cowboy-cccc0bc475d903e553c66c844f5bc0b0934e2b59.tar.bz2
cowboy-cccc0bc475d903e553c66c844f5bc0b0934e2b59.zip
Unmask websocket frames on the fly and optimize the code
No change in functionality, but this will allow us to validate that text frames are utf8 without having to receive the frame entirely.
Diffstat (limited to 'src/cowboy_websocket.erl')
-rw-r--r--src/cowboy_websocket.erl306
1 files changed, 147 insertions, 159 deletions
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index ece953c..50d1c88 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -31,14 +31,8 @@
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
-type mask_key() :: 0..16#ffffffff.
-
-%% The websocket_data/4 function may be called multiple times for a message.
-%% The websocket_dispatch/4 function is only called once for each message.
--type frag_state() ::
- undefined | %% no fragmentation has been seen.
- {nofin, opcode()} | %% first fragment has been seen.
- {nofin, opcode(), binary()} | %% first fragment has been unmasked.
- {fin, opcode(), binary()}. %% last fragment has been seen.
+-type frag_state() :: undefined
+ | {nofin, opcode(), binary()} | {fin, opcode(), binary()}.
-record(state, {
env :: cowboy_middleware:env(),
@@ -181,9 +175,8 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-handler_loop(State=#state{
- socket=Socket, messages={OK, Closed, Error}, timeout_ref=TRef},
- Req, HandlerState, SoFar) ->
+handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
+ timeout_ref=TRef}, Req, HandlerState, SoFar) ->
receive
{OK, Socket, Data} ->
State2 = handler_loop_timeout(State),
@@ -202,187 +195,182 @@ handler_loop(State=#state{
SoFar, websocket_info, Message, fun handler_before_loop/4)
end.
+%% All frames passing through this function are considered valid.
-spec websocket_data(#state{}, Req, any(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-%% No more data.
-websocket_data(State, Req, HandlerState, <<>>) ->
- handler_before_loop(State, Req, HandlerState, <<>>);
-%% Incomplete.
-websocket_data(State, Req, HandlerState, Data) when byte_size(Data) =:= 1 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7 bit payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, 1:1, PayloadLen:7, Rest/bits >>
- = Data) when PayloadLen < 126 ->
+%% RSV bits MUST be 0 unless an extension is negotiated
+%% that defines meanings for non-zero values.
+websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>)
+ when Rsv =/= 0 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Control frames MUST NOT be fragmented.
+websocket_data(State, Req, HandlerState, << 0:1, _:3, Opcode:4, _/bits >>)
+ when Opcode >= 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% A fragmented message MUST start a non-zero opcode.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ << 0:1, _:3, 0:4, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Non-control opcode when expecting control message or next fragment.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ << _:4, Opcode:4, _/bits >>)
+ when Opcode =/= 0, Opcode < 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% 7 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ Len:7, MaskKey:32, Rest/bits >>)
+ when Len < 126 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, 1:1, 126:7, PayloadLen:16, Rest/bits >>
- = Data) when PayloadLen > 125 ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 16 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 126:7, Len:16, MaskKey:32, Rest/bits >>)
+ when Len > 125, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, 1:1, 126:7, Rest/bits >>
- = Data) when byte_size(Rest) < 2 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7+64 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, 1:1, 127:7, 0:1, PayloadLen:63,
- Rest/bits >> = Data) when PayloadLen > 16#FFFF ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 63 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>)
+ when Len > 16#ffff, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, PayloadLen, Rest, Data);
-%% 7+64 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, 1:1, 127:7, Rest/bits >>
- = Data) when byte_size(Rest) < 8 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% Invalid payload length or mask bit was not set.
-websocket_data(State, Req, HandlerState, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
-
--spec websocket_data(#state{}, Req, any(), non_neg_integer(),
- non_neg_integer(), non_neg_integer(),
- non_neg_integer(), binary(), binary())
- -> {ok, Req, cowboy_middleware:env()}
- when Req::cowboy_req:req().
-%% A fragmented message MUST start a non-zero opcode.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, _Opcode=0, _PayloadLen, _Rest, _Buffer) ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% When payload length is over 63 bits, the most significant bit MUST be 0.
+websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% A control message MUST NOT be fragmented.
-websocket_data(State, Req, HandlerState, _Fin=0, _Rsv=0, Opcode,
- _PayloadLen, _Rest, _Buffer) when Opcode >= 8 ->
+%% All frames sent from the client to the server are masked.
+websocket_data(State, Req, HandlerState, << _:8, 0:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% The opcode is only included in the first message fragment.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, PayloadLen);
-%% non-control opcode when expecting control message or next fragment.
-websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, _Fin,
- _Rsv=0, Opcode, _Ln, _Rest, _Data) when Opcode > 0, Opcode < 8 ->
+%% For the next two clauses, it can be one of the following:
+%%
+%% * The minimal number of bytes MUST be used to encode the length
+%% * All control frames MUST have a payload length of 125 bytes or less
+websocket_data(State, Req, HandlerState, << _:9, 126:7, _:48, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% If the first message fragment was incomplete, retry unmasking.
-websocket_data(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, PayloadLen);
-%% if the opcode is zero and the fin flag is zero, unmask and await next.
-websocket_data(State=#state{frag_state={nofin, _Opcode, _Payloads}}, Req,
- HandlerState, _Fin=0, _Rsv=0, _Opcode2=0, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, 0, PayloadLen);
-%% when the last fragment is seen. Update the fragmentation status.
-websocket_data(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, _Fin=1, _Rsv=0, _Opcode=0, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State#state{frag_state={fin, Opcode, Payloads}},
- Req, HandlerState, Data, Rest, 0, PayloadLen);
-%% control messages MUST NOT use 7+16 bits or 7+64 bits payload length prefixes
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, Opcode, PayloadLen,
- _Rest, _Data) when Opcode >= 8, PayloadLen > 125 ->
- websocket_close(State, Req, HandlerState, {error, badframe});
-%% unfragmented message. unmask and dispatch the message.
-websocket_data(State, Req, HandlerState, _Fin=1, _Rsv=0,
- Opcode, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, Opcode, PayloadLen);
-%% Something was wrong with the frame. Close the connection.
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode,
- _PayloadLen, _Rest, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
+websocket_data(State, Req, HandlerState, << _:9, 127:7, _:96, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Need more data.
+websocket_data(State, Req, HandlerState, Data) ->
+ handler_before_loop(State, Req, HandlerState, Data).
-%% Routing depending on whether unmasking is needed.
--spec websocket_before_unmask(#state{}, Req, any(), binary(),
- binary(), opcode(), non_neg_integer() | undefined)
+%% Initialize or update fragmentation state.
+-spec websocket_data(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), 0 | 1)
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-websocket_before_unmask(State, Req, HandlerState, Data,
- Rest, Opcode, PayloadLen) ->
- case PayloadLen of
- N when N + 4 > byte_size(Rest); N =:= undefined ->
- %% @todo We probably should allow limiting frame length.
- handler_before_loop(State, Req, HandlerState, Data);
- _N ->
- << MaskKey:32, Payload:PayloadLen/binary, Rest2/bits >> = Rest,
- websocket_unmask(State, Req, HandlerState, Rest2,
- Opcode, Payload, MaskKey)
- end.
+%% The opcode is only included in the first frame fragment.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ Opcode, Len, MaskKey, Data, 0) ->
+ websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Subsequent frame fragments.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ 0, Len, MaskKey, Data, 0) ->
+ websocket_payload(State, Req, HandlerState,
+ 0, Len, MaskKey, <<>>, Data);
+%% Final frame fragment.
+websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, Data, 1) ->
+ websocket_payload(State#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Unfragmented frame.
+websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, 1) ->
+ websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, <<>>, Data).
-%% Unmasking.
--spec websocket_unmask(#state{}, Req, any(), binary(),
- opcode(), binary(), mask_key())
+-spec websocket_payload(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey) ->
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey, <<>>).
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data)
+ when byte_size(Data) < Len ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len - byte_size(Data), MaskKey, Unmasked2);
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_dispatch(State, Req, HandlerState, Rest, Opcode, Unmasked2).
--spec websocket_unmask(#state{}, Req, any(), binary(),
- opcode(), binary(), mask_key(), binary())
- -> {ok, Req, cowboy_middleware:env()}
- | {suspend, module(), atom(), [any()]}
- when Req::cowboy_req:req().
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
+-spec websocket_unmask(B, mask_key(), B) -> B when B::binary().
+websocket_unmask(<<>>, _, Unmasked) ->
+ Unmasked;
+websocket_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Rest, MaskKey, << Acc/binary, T:32 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:24 >>, MaskKey, Acc) ->
+ websocket_unmask(Rest, MaskKey, << Acc/binary, T:32 >>);
+websocket_unmask(<< O:24 >>, MaskKey, Acc) ->
<< MaskKey2:24, _:8 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:24 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:16 >>, MaskKey, Acc) ->
+ << Acc/binary, T:24 >>;
+websocket_unmask(<< O:16 >>, MaskKey, Acc) ->
<< MaskKey2:16, _:16 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:16 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:8 >>, MaskKey, Acc) ->
+ << Acc/binary, T:16 >>;
+websocket_unmask(<< O:8 >>, MaskKey, Acc) ->
<< MaskKey2:8, _:24 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:8 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, <<>>, _MaskKey, Acc) ->
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, Acc).
+ << Acc/binary, T:8 >>.
+
+%% Because we unmask on the fly we need to continue from the right mask byte.
+-spec rotate_mask_key(mask_key(), non_neg_integer()) -> mask_key().
+rotate_mask_key(MaskKey, UnmaskedLen) ->
+ Left = UnmaskedLen rem 4,
+ Right = 4 - Left,
+ (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)).
+
+-spec websocket_payload_loop(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
+ messages={OK, Closed, Error}, timeout_ref=TRef},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked) ->
+ Transport:setopts(Socket, [{active, once}]),
+ receive
+ {OK, Socket, Data} ->
+ State2 = handler_loop_timeout(State),
+ websocket_payload(State2, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data);
+ {Closed, Socket} ->
+ handler_terminate(State, Req, HandlerState, {error, closed});
+ {Error, Socket, Reason} ->
+ handler_terminate(State, Req, HandlerState, {error, Reason});
+ {timeout, TRef, ?MODULE} ->
+ websocket_close(State, Req, HandlerState, {normal, timeout});
+ {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked);
+ Message ->
+ handler_call(State, Req, HandlerState,
+ <<>>, websocket_info, Message,
+ fun (State2, Req2, HandlerState2, _) ->
+ websocket_payload_loop(State2, Req2, HandlerState2,
+ Opcode, Len, MaskKey, Unmasked)
+ end)
+ end.
-%% Dispatching.
-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary())
-> {ok, Req, cowboy_middleware:env()}
| {suspend, module(), atom(), [any()]}
when Req::cowboy_req:req().
-%% First frame of a fragmented message unmasked. Expect intermediate or last.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- RemainingData, 0, Payload) ->
- websocket_data(State#state{frag_state={nofin, Opcode, Payload}},
- Req, HandlerState, RemainingData);
-%% Intermediate frame of a fragmented message unmasked. Add payload to buffer.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+%% Continuation frame.
+websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_data(State#state{frag_state={nofin, Opcode,
- <<Payloads/binary, Payload/binary>>}}, Req, HandlerState,
- RemainingData);
-%% Last frame of a fragmented message unmasked. Dispatch to handler.
-websocket_dispatch(State=#state{frag_state={fin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+ << SoFar/binary, Payload/binary >>}}, Req, HandlerState, RemainingData);
+%% Last continuation frame.
+websocket_dispatch(State=#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_dispatch(State#state{frag_state=undefined}, Req, HandlerState,
- RemainingData, Opcode, <<Payloads/binary, Payload/binary>>);
+ RemainingData, Opcode, << SoFar/binary, Payload/binary >>);
%% Text frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,