Permalink
Browse files

added specs and refactored hybi-10 with RJ's code

  • Loading branch information...
1 parent d5233f0 commit 1b1fdadefad7ac818baf62ec231c01474b9f0c40 @ostinelli ostinelli committed with Nov 20, 2011
Showing with 236 additions and 95 deletions.
  1. +236 −95 src/misultin_websocket_draft-hybi-10.erl
@@ -3,8 +3,8 @@
%
% >-|-|-(°>
%
-% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>,
-% portions of code from Andy W. Song <https://github.com/awsong/erl_websocket>
+% Copyright (C) 2011, Richard Jones <rj@metabrew.com>, Roberto Ostinelli <roberto@ostinelli.net>,
+% portions of code from Andy W. Song <https://github.com/awsong/erl_websocket>
% All rights reserved.
%
% Code portions from Joe Armstrong have been originally taken under MIT license at the address:
@@ -40,10 +40,21 @@
% records
-record(state, {
- buffer,
- mask_key = <<0,0,0,0>>
+ buffer = <<>>,
+ mask_key = <<0,0,0,0>>,
+ fragments = [] %% if we are in the midst of receving a fragmented message, fragments are contained here in reverse order
}).
+-record(frame, {fin,
+ rsv1,
+ rsv2,
+ rsv3,
+ opcode,
+ maskbit,
+ length,
+ maskkey,
+ data}).
+
% macros
-define(OP_CONT, 0).
-define(OP_TEXT, 1).
@@ -52,6 +63,8 @@
-define(OP_PING, 9).
-define(OP_PONG, 10).
+-define(IS_CONTROL_OPCODE(X), ((X band 8)=:=8) ).
+
% includes
-include("../include/misultin.hrl").
@@ -96,13 +109,17 @@ handshake(_Req, Headers, {_Path, _Origin, _Host}) ->
% Function: -> websocket_close | {websocket_close, DataToSendBeforeClose::binary() | iolist()} | NewStatus
% Description: Callback to handle incomed data.
% ----------------------------------------------------------------------------------------------------------
--spec handle_data(Data::binary(), Status::undefined | term(), {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | term().
-handle_data(Data, undefined, {Socket, SocketMode, WsHandleLoopPid}) ->
+-spec handle_data(Data::binary(),
+ Status::undefined | term(),
+ {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | term().
+handle_data(Data, St, Tuple) when is_list(Data) ->
+ handle_data(list_to_binary(Data), St, Tuple);
+handle_data(Data, undefined, {Socket, SocketMode, WsHandleLoopPid}) when is_binary(Data) ->
% init status
- handle_data(Data, #state{buffer = none}, {Socket, SocketMode, WsHandleLoopPid});
-handle_data(Data, State, {Socket, SocketMode, WsHandleLoopPid}) ->
+ i_handle_data(#state{buffer = Data}, {Socket, SocketMode, WsHandleLoopPid});
+handle_data(Data, State = #state{buffer=Buffer}, {Socket, SocketMode, WsHandleLoopPid}) when is_binary(Data) ->
% read status
- i_handle_data(Data, State, {Socket, SocketMode, WsHandleLoopPid}).
+ i_handle_data(State#state{buffer = <<Buffer/binary, Data/binary>>}, {Socket, SocketMode, WsHandleLoopPid}).
% ----------------------------------------------------------------------------------------------------------
% Function: -> binary() | iolist()
@@ -122,106 +139,230 @@ send_format(Data, OpCode, _State) ->
true ->
<<1:1, 0:3, OpCode:4, 0:1, 127:7, 0:1, Len:63, BData/binary>>
end.
-
% ============================ /\ API ======================================================================
% ============================ \/ INTERNAL FUNCTIONS =======================================================
-% 0 1 2 3
-% 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
-% +-+-+-+-+-------+-+-------------+-------------------------------+
-% |F|R|R|R| opcode|M| Payload len | Extended payload length |
-% |I|S|S|S| (4) |A| (7) | (16/63) |
-% |N|V|V|V| |S| | (if payload len==126/127) |
-% | |1|2|3| |K| | |
-% +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
-% | Extended payload length continued, if payload len == 127 |
-% + - - - - - - - - - - - - - - - +-------------------------------+
-% | |Masking-key, if MASK set to 1 |
-% +-------------------------------+-------------------------------+
-% | Masking-key (continued) | Payload Data |
-% +-------------------------------- - - - - - - - - - - - - - - - +
-% : Payload Data continued ... :
-% + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
-% | Payload Data continued ... |
-% +---------------------------------------------------------------+
-
-% handle incomed data
--spec i_handle_data(Data::binary(), State::undefined | term(), {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | term().
-i_handle_data(Data, #state{buffer = Buffer} = State, {Socket, SocketMode, WsHandleLoopPid}) when is_binary(Buffer) ->
- i_handle_data(<<Buffer/binary, Data/binary>>, State#state{buffer = none}, {Socket, SocketMode, WsHandleLoopPid});
-i_handle_data(<<Fin:1, 0:3, Opcode:4, 1:1, PayloadLen:7, MaskKey:4/binary, PayloadData/binary>>, State, {Socket, SocketMode, WsHandleLoopPid}) when PayloadLen < 126 andalso PayloadLen =< size(PayloadData) ->
- handle_frame(Fin, Opcode, PayloadLen, MaskKey, PayloadData, State#state{mask_key = MaskKey}, {Socket, SocketMode, WsHandleLoopPid});
-i_handle_data(<<Fin:1, 0:3, Opcode:4, 1:1, 126:7, PayloadLen:16, MaskKey:4/binary, PayloadData/binary>>, State, {Socket, SocketMode, WsHandleLoopPid}) when PayloadLen =< size(PayloadData) ->
- handle_frame(Fin, Opcode, PayloadLen, MaskKey, PayloadData, State#state{mask_key = MaskKey}, {Socket, SocketMode, WsHandleLoopPid});
-i_handle_data(<<Fin:1, 0:3, Opcode:4, 1:1, 127:7, 0:1, PayloadLen:63, MaskKey:4/binary, PayloadData/binary>>, State, {Socket, SocketMode, WsHandleLoopPid}) when PayloadLen =< size(PayloadData) ->
- handle_frame(Fin, Opcode, PayloadLen, MaskKey, PayloadData, State#state{mask_key = MaskKey}, {Socket, SocketMode, WsHandleLoopPid});
-i_handle_data(<<_Fin:1, 0:3, _Opcode:4, 0:1, _PayloadLen:7, _Data/binary>>, _State, {_Socket, _SocketMode, _WsHandleLoopPid}) ->
- ?LOG_DEBUG("client to server message was not sent masked, close websocket",[]),
- {websocket_close, websocket_close_data()};
-i_handle_data(Data, #state{buffer = none} = State, {_Socket, _SocketMode, _WsHandleLoopPid}) ->
- State#state{buffer = Data}.
-
-% handle frames
--spec handle_frame(
- Fin::integer(),
- Opcode::integer(),
- PayloadLen::integer(),
- MaskKey::binary(),
- PayloadData::binary(),
- State::term(),
- {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | term().
-handle_frame(1, ?OP_CONT, _Len, _MaskKey, _Data, _State, {_Socket, _SocketMode, _WsHandleLoopPid}) ->
- ?LOG_WARNING("received an unsupported segment ~p, closing websocket", [{1, ?OP_CONT}]),
+% 0 1 2 3
+% 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+% +-+-+-+-+-------+-+-------------+-------------------------------+
+% |F|R|R|R| opcode|M| Payload len | Extended payload length |
+% |I|S|S|S| (4) |A| (7) | (16/63) |
+% |N|V|V|V| |S| | (if payload len==126/127) |
+% | |1|2|3| |K| | |
+% +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
+% | Extended payload length continued, if payload len == 127 |
+% + - - - - - - - - - - - - - - - +-------------------------------+
+% | |Masking-key, if MASK set to 1 |
+% +-------------------------------+-------------------------------+
+% | Masking-key (continued) | Payload Data |
+% +-------------------------------- - - - - - - - - - - - - - - - +
+% : Payload Data continued ... :
+% + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+% | Payload Data continued ... |
+% +---------------------------------------------------------------+
+
+
+% ---------------------------- \/ frame parsing ------------------------------------------------------------
+
+% parse received data and get the frames
+-spec take_frame(Data::binary()) -> {#frame{} | undefined, Rest::binary()}.
+% normal length
+take_frame(<<Fin:1,
+ Rsv1:1, %% Rsv1 = 0
+ Rsv2:1, %% Rsv2 = 0
+ Rsv3:1, %% Rsv3 = 0
+ Opcode:4,
+ MaskBit:1, %% must be 1
+ PayloadLen:7,
+ MaskKey:4/binary,
+ PayloadData:PayloadLen/binary-unit:8,
+ Rest/binary>>) when PayloadLen < 126 ->
+ %% Don't auto-unmask control frames
+ Data = case ?IS_CONTROL_OPCODE(Opcode) of
+ true -> PayloadData;
+ false -> unmask(MaskKey,PayloadData)
+ end,
+ {#frame{fin=Fin,
+ rsv1=Rsv1,
+ rsv2=Rsv2,
+ rsv3=Rsv3,
+ opcode=Opcode,
+ maskbit=MaskBit,
+ length=PayloadLen,
+ maskkey=MaskKey,
+ data = Data}, Rest};
+% extende payload (126)
+take_frame(<<Fin:1,
+ Rsv1:1, %% Rsv1 = 0
+ Rsv2:1, %% Rsv2 = 0
+ Rsv3:1, %% Rsv3 = 0
+ Opcode:4,
+ MaskBit:1, %% must be 1
+ 126:7,
+ PayloadLen:16,
+ MaskKey:4/binary,
+ PayloadData:PayloadLen/binary-unit:8,
+ Rest/binary>>) ->
+ {#frame{fin=Fin,
+ rsv1=Rsv1,
+ rsv2=Rsv2,
+ rsv3=Rsv3,
+ opcode=Opcode,
+ maskbit=MaskBit,
+ length=PayloadLen,
+ maskkey=MaskKey,
+ data=unmask(MaskKey,PayloadData)}, Rest};
+% extende payload (127)
+take_frame(<<Fin:1,
+ Rsv1:1, %% Rsv1 = 0
+ Rsv2:1, %% Rsv2 = 0
+ Rsv3:1, %% Rsv3 = 0
+ Opcode:4,
+ MaskBit:1, %% must be 1
+ 127:7, %% "If 127, the following 8 bytes interpreted as a 64-bit unsigned integer (the most significant bit MUST be 0)"
+ 0:1, %% MSB of 0
+ PayloadLen:63,
+ MaskKey:4/binary,
+ PayloadData:PayloadLen/binary-unit:8,
+ Rest/binary>>) ->
+ {#frame{fin=Fin,
+ rsv1=Rsv1,
+ rsv2=Rsv2,
+ rsv3=Rsv3,
+ opcode=Opcode,
+ maskbit=MaskBit,
+ length=PayloadLen,
+ maskkey=MaskKey,
+ data=unmask(MaskKey, PayloadData)}, Rest};
+% catch wrong format
+take_frame(Data) when is_binary(Data) ->
+ {undefined, Data}.
+
+% process incoming data
+-spec i_handle_data(#state{}, {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> #state{} | {websocket_close, term()}.
+i_handle_data(#state{buffer=ToParse} = State, {Socket, SocketMode, WsHandleLoopPid}) ->
+ case take_frame(ToParse) of
+ {undefined, Rest} ->
+ ?LOG_DEBUG("no frame to take, add to buffer: ~p", [Rest]),
+ %% no full frame to be had yet
+ State#state{buffer = Rest};
+ {Frame=#frame{}, Rest} ->
+ ?LOG_DEBUG("parsed frame ~p, remaining buffer is: ~p", [Frame,Rest]),
+ %% sanity check, in case client is broken
+ case sanity_check(Frame) of
+ ok ->
+ ?LOG_DEBUG("sanity checks successfully performed",[]),
+ case handle_frame(Frame,
+ State#state{buffer = Rest},
+ {Socket, SocketMode, WsHandleLoopPid}) of
+ %% tail-call if there is stuff in the buffer still to parse
+ NewState = #state{buffer = B} when is_binary(B), B =/= <<>> ->
+ i_handle_data(NewState, {Socket, SocketMode, WsHandleLoopPid});
+ Other ->
+ Other
+ end;
+ protocol_error ->
+ ?LOG_DEBUG("sanity checks errors encountered, closing websocket",[]),
+ {websocket_close, websocket_close_data()}
+ end
+ end.
+
+% format sanity checks
+-spec sanity_check(#frame{}) -> ok | protocol_error.
+sanity_check(Frame) ->
+ Checks = [
+ {1, Frame#frame.maskbit},
+ {0, Frame#frame.rsv1},
+ {0, Frame#frame.rsv2},
+ {0, Frame#frame.rsv3}
+ ],
+ ChecksVerified = lists:dropwhile(
+ fun ({A, A}) -> true;
+ ({_A, _}) -> false
+ end, Checks
+ ),
+ case ChecksVerified of
+ [] -> ok;
+ _ -> protocol_error
+ end.
+
+% ---------------------------- /\ frame parsing ------------------------------------------------------------
+
+% ---------------------------- \/ fragment handling --------------------------------------------------------
+
+-spec handle_frame(#frame{}, #state{}, {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> #state{} | {websocket_close, term()}.
+%% FRAGMENT - add to the list and carry on
+%% "A fragmented message consists of a single frame with the FIN bit
+%% clear and an opcode other than 0, followed by zero or more frames
+%% with the FIN bit clear and the opcode set to 0, and terminated by
+%% a single frame with the FIN bit set and an opcode of 0"
+handle_frame(#frame{fin = 0, opcode = Opcode}, %% first fragment
+ State = #state{fragments = []} = Frame,
+ _) when Opcode =/= ?OP_CONT ->
+ ?LOG_DEBUG("first fragment: ~p", [Frame]),
+ State#state{fragments = [Frame]};
+handle_frame(#frame{fin = 0, opcode = ?OP_CONT}, %% subsequent fragments
+ State = #state{fragments = Frags} = Frame,
+ _) when Frags =/= [] ->
+ ?LOG_DEBUG("next fragment: ~p", [Frame]),
+ State#state{fragments = [Frame | Frags]};
+
+%% Last frame in a fragmented message.
+%% reassemble one large frame based on all the fragments, keeping opcode from first:
+handle_frame(#frame{fin = 1, opcode = ?OP_CONT } = F,
+ State = #state{fragments = Frags},
+ {Socket, SocketMode, WsHandleLoopPid}) when Frags =/= [] ->
+ [Frame1|Frames] = lists:reverse([F|Frags]),
+ Frame = lists:foldl(
+ fun(#frame{length = L, data = D}, AccF) ->
+ %% NB: we unmask data as we parse frames, so concating here is ok:
+ AccF#frame{length = (AccF#frame.length + L), data = << (AccF#frame.data)/binary, D/binary >>}
+ end,
+ Frame1#frame{fin=1},
+ Frames
+ ),
+ ?LOG_DEBUG("final fragment, assembled: ~p",[Frame]),
+ %% now process this new combined message as if we got it all at once:
+ handle_frame(Frame, State#state{fragments = []}, {Socket, SocketMode, WsHandleLoopPid});
+
+%% end of fragments but no fragments stored - error
+handle_frame(#frame{fin = 1, opcode = ?OP_CONT}, _, _) ->
+ %% closing, should only happen if client is broken
{websocket_close, websocket_close_data()};
-handle_frame(1, Opcode, Len, MaskKey, Data, State, {Socket, SocketMode, WsHandleLoopPid}) ->
- % frame without segment
- <<Data1:Len/binary, Rest/binary>> = Data,
+
+% ---------------------------- /\ fragment handling --------------------------------------------------------
+
+% ---------------------------- \/ frame handling -----------------------------------------------------------
+
+%% CONTROL FRAMES: 1) cannot be fragmented, thus have size <= 125bytes
+%% 2) have an opcode where MSB is set
+%% 3) can appear between larger fragmented message frames
+handle_frame(#frame{fin=1, opcode=Opcode, data=Data},
+ State,
+ {Socket, SocketMode, _WsHandleLoopPid}) when ?IS_CONTROL_OPCODE(Opcode) ->
+ %% handle all known control opcodes:
case Opcode of
- ?OP_BIN ->
- handle_frame_received_msg(MaskKey, Data1, Rest, State, {Socket, SocketMode, WsHandleLoopPid});
- ?OP_TEXT ->
- handle_frame_received_msg(MaskKey, Data1, Rest, State, {Socket, SocketMode, WsHandleLoopPid});
?OP_PING ->
- % ping
- misultin_socket:send(Socket, send_format(Data1, ?OP_PONG, State), SocketMode),
- handle_frame_continue(Rest, State, {Socket, SocketMode, WsHandleLoopPid});
+ misultin_socket:send(Socket, send_format(Data, ?OP_PONG, State), SocketMode),
+ State;
?OP_CLOSE ->
?LOG_DEBUG("received a websocket close request",[]),
websocket_close;
_OpOther ->
- ?LOG_DEBUG("received segment with the unknown OpCode ~p, closing websocket", [_OpOther]),
+ ?LOG_DEBUG("received segment with the unknown control OpCode ~p, closing websocket", [_OpOther]),
{websocket_close, websocket_close_data()}
end;
-% first frame of a segment, TODO: comply to multiple segments
-handle_frame(0, _Opcode, _Len, _MaskKey, _Data, _State, {_Socket, _SocketMode, _WsHandleLoopPid}) ->
- ?LOG_WARNING("received an unsupported continuation segment with opcode ~p, closing websocket", [{0, _Opcode}]),
- {websocket_close, websocket_close_data()}.
-
-% received a message, send to websocket process
--spec handle_frame_received_msg(
- MaskKey::binary(),
- Data::binary(),
- Rest::binary(),
- State::term(),
- {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | #state{}.
-handle_frame_received_msg(MaskKey, Data, Rest, State, {Socket, SocketMode, WsHandleLoopPid}) ->
- Unmasked = binary_to_list(unmask(MaskKey, Data)),
- ?LOG_DEBUG("received message from client: ~p", [Unmasked]),
- misultin_websocket:send_to_browser(WsHandleLoopPid, Unmasked),
- handle_frame_continue(Rest, State, {Socket, SocketMode, WsHandleLoopPid}).
-
-% continue with rest of data
--spec handle_frame_continue(
- Rest::binary(),
- State::term(),
- {Socket::socket(), SocketMode::socketmode(), WsHandleLoopPid::pid()}) -> websocket_close | {websocket_close, binary()} | #state{}.
-handle_frame_continue(Rest, State, {Socket, SocketMode, WsHandleLoopPid}) ->
- case Rest of
- <<>> -> State#state{buffer = none};
- _ -> i_handle_data(Rest, State#state{buffer = none}, {Socket, SocketMode, WsHandleLoopPid})
- end.
+
+%% NORMAL FRAME (not a fragment, not a control frame)
+handle_frame(#frame{fin=1, opcode=Opcode, data=Data},
+ State,
+ {_Socket, _SocketMode, WsHandleLoopPid}) when Opcode =:= ?OP_BIN; Opcode =:= ?OP_TEXT ->
+ misultin_websocket:send_to_browser(WsHandleLoopPid, binary_to_list(Data)),
+ State.
+
+% ---------------------------- /\ frame handling -----------------------------------------------------------
% unmask
-spec unmask(Key::binary(), Data::binary()) -> binary().

0 comments on commit 1b1fdad

Please sign in to comment.