Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Websocket parser refactor #72

Closed
wants to merge 2 commits into from

2 participants

@RJ
RJ commented

I refactored the websocket frame/parsing code in hybi-10, I hope it will be easier to follow, and to extend to support websocket protocol extensions later.

It should also handle fragmented messages with control messages mixed in now, although I can't find a client in the wild that will send fragmented messages, so haven't really tested that bit.

Would be good to get some feedback :)
(not expecting a merge atm, using this pull request for discussion)

Cheers,
RJ

cc @dbudworth

RJ added some commits
@RJ RJ Add support for haproxy PROXY protocol
Adds a {proxy_protocol,true} option, causing misultin acceptors to read the
first line of a new socket connection for the PROXY line, as per:
 http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt

This allows misultin to see the correct client IP, when behind stunnel.
54d2210
@RJ RJ Refactor websocket parsing
* Introduce #frame{} for each sucessfully parsed websocket frame
* Separate frame parsing into take_frame/1
* New parser should be easier to support extensions with later
* Support large fragmented messages and multiplexed non-fragmented
  messages, such as PING etc
* Can't find client that sends fragmented messages to help test...
50974ce
@RJ
RJ commented

Oh, whoops, ignore the PROXY protocol commit, just looking for feedback on the websocket one

@ostinelli
Owner

Hi RJ,

this looks really nice. FYI, this weekend I've planned to bring misultin close to a 0.9 release. I'll wait for this issue to be fully solved before I do though.

It'd be nice to find a library that could actually test the fragmented support implementation, this could also be added in the integration tests, which currently only test for a previous websocket release.

I'll take a deeper look at this during the weekend.

r.

ps: @awsong if you feel like telling your opinion, please do so! ^^_

@ostinelli
Owner

btw, out of curiosity why hybi-10 and not 17? :)

@RJ
RJ commented

Hybi-17 is just a copy paste of hybi-10, with the version bumped and a header check removed. I worked on -10 because that's what the version of chrome on my desktop uses.

I'd like to find a way to share the majority of the code between -10 and -17. Perhaps extract some out to a shared module, or make one module handle both. Any thoughts on the best way to do that?

@ostinelli
Owner

sure, refactoring to DRY is simple to do.

i'm more interested in bringing the code you submitted to master, it's the first code i see that supports segments :)

@RJ
RJ commented

ok, i'm away in germany atm, back home tomorrow (monday) night. I'll be pushing this live to irccloud this week, will let you know how it goes. provided it works as expected, i'll dry out hybi 17 and it should be safe to roll a new misultin release :)

do you know of any decent websocket client that can do multiple versions, to aid testing this stuff?

@ostinelli
Owner

i've refactored your code and set a common module for protocols 10 and 17. you can find it in the dedicated branch:
https://github.com/ostinelli/misultin/tree/ws-refactor

branch 'dev' does not exist anymore, everything else except this commit has been pushed to 'master'.

let me know what do you think.

ps: unfortunately no, i do not know a comprehensive library for testing purposes.

@RJ
RJ commented

Cool, I pushed a couple of minor tweaks, and added a max unparsed frame size:
https://github.com/RJ/misultin/commits/ws-refactor

@RJ
RJ commented

Pushed my fork of ws-refactor live to irccloud just now then rolled it back.
Looks like the hybi stuff and proxy support all worked fine, but hixie-76 is broken, seeing these errors:

=ERROR REPORT==== 22-Nov-2011::12:24:00 ===
        module: misultin_server
        line: 216
http process <0.12776.0> has died with reason: {badarg,
                                            [{erlang,byte_size,
                                              [[223,155,227,53,25,19,77,
                                                231]]},
                                             {'misultin_websocket_draft-hixie-76',
                                              handshake,3},
                                             {misultin_websocket,connect,
                                              4},
                                             {misultin_acceptor,
                                              open_connections_switch,8}]}, removing from references of open connections

Not sure which browsers still use hixie-76, but the popular flash websocket alternative uses hixie-76, and that was causing a fair amount of errors.

@RJ
RJ commented

This was caused by the code reading the PROXY line leaving the socket options set to list, but hixie-76 (and possibly other places) expected binary.

Fixed in RJ@839137d

@ostinelli
Owner

indeed. will check implications of this asap and proceed.

i don't think it's necessary to do all this: probably at this stage the inet options are the default ones, and are probably best hard-coded instead of saved and retreived. i will check for this and provide a patch based on yours. unless you feel curious and want to provide it yourself: i'll probably be unable to look into this for some days unfortunately. ^^_

thank you,

r.

@RJ
RJ commented

ok - wasn't sure whether to do this or not - i suppose misultin has no reason to change the options before this point anyway..
I'll update it to use hardcoded values

@RJ
RJ commented

Changed to restore hardcoded socket options: RJ@d0f5c61

There's also a websocket example app in examples/websocket in my branch. I made an app so that I could use rebar to pull in erlflashpol dep, to test the flash websocket fallback (which needs a flash policy server running on port 843).

@ostinelli
Owner

perfect. yes, saw the websocket apps, however i prefer to have those in the test, so i'll skip them for now ^^_

could you please however provide a pull request for all of your work? i like most of it, and in this way we can keep track of your history too.

thank you :)

r.

@RJ
RJ commented

Sure, what do you want exactly - a pull request to master with everything in my ws-refactor except the websocket app?

@ostinelli
Owner
@RJ
RJ commented

Here it is: #74

this pull request is now redundant

@RJ RJ closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Nov 17, 2011
  1. @RJ

    Add support for haproxy PROXY protocol

    RJ authored
    Adds a {proxy_protocol,true} option, causing misultin acceptors to read the
    first line of a new socket connection for the PROXY line, as per:
     http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
    
    This allows misultin to see the correct client IP, when behind stunnel.
Commits on Nov 18, 2011
  1. @RJ

    Refactor websocket parsing

    RJ authored
    * Introduce #frame{} for each sucessfully parsed websocket frame
    * Separate frame parsing into take_frame/1
    * New parser should be easier to support extensions with later
    * Support large fragmented messages and multiplexed non-fragmented
      messages, such as PING etc
    * Can't find client that sends fragmented messages to help test...
This page is out of date. Refresh to see the latest.
View
11 README.txt
@@ -68,6 +68,17 @@ DOCUMENTATION
API Documentation is available online on the Misultin's wiki: https://github.com/ostinelli/misultin/wiki
+SSL NOTES
+==========================================================================================================
+If you are running misultin behind an SSL terminator such as stunnel or stud, then set
+ {ws_force_ssl, true}
+so that websocket handshakes work.
+
+If you are using stunnel to terminate, you can also set
+ {proxy_protocol, true}
+to make misultin expect a PROXY.. line as per http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
+Newer versions of stunnel support this with the "protocol = proxy" config option.
+
CHANGELOG
==========================================================================================================
View
49 examples/misultin_proxy_protocol.erl
@@ -0,0 +1,49 @@
+% ==========================================================================================================
+% MISULTIN - Example: Proxy protocol
+%
+% >-|-|-(°>
+%
+% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>
+% All rights reserved.
+%
+% BSD License
+%
+% Redistribution and use in source and binary forms, with or without modification, are permitted provided
+% that the following conditions are met:
+%
+% * Redistributions of source code must retain the above copyright notice, this list of conditions and the
+% following disclaimer.
+% * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and
+% the following disclaimer in the documentation and/or other materials provided with the distribution.
+% * Neither the name of the authors nor the names of its contributors may be used to endorse or promote
+% products derived from this software without specific prior written permission.
+%
+% THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
+% WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+% PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
+% ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+% TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+% HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+% NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+% POSSIBILITY OF SUCH DAMAGE.
+% ==========================================================================================================
+%
+% This test assumes you are accessing misultin via something that supports the
+% haproxy proxy protocol, such as recent versions of stunnel.
+% see: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
+-module(misultin_proxy_protocol).
+-export([start/1, stop/0]).
+
+% start misultin http server
+start(Port) ->
+ misultin:start_link([{proxy_protocol, true}, {port, Port}, {loop, fun(Req) -> handle_http(Req) end}]).
+
+% stop misultin
+stop() ->
+ misultin:stop().
+
+% callback on request received
+handle_http(Req) ->
+ {A,B,C,D} = Req:get(peer_addr),
+ Msg = io_lib:format("Hello ~B.~B.~B.~B", [A,B,C,D]),
+ Req:ok(Msg).
View
4 include/misultin.hrl
@@ -151,7 +151,9 @@
ws_autoexit = true :: boolean(), % shoud the ws process be automatically killed?
ws_versions = undefined :: [websocket_version()], % list of supported ws versions
access_log = undefined :: undefined | function(), % access log function
- ws_force_ssl = false :: boolean() % if we are deployed behind stunnel, or other ssl proxy
+ ws_force_ssl = false :: boolean(), % if we are deployed behind stunnel, or other ssl proxy
+ proxy_protocol = false :: boolean() % upstream proxy is sending us http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
+
}).
% Request
View
5 src/misultin.erl
@@ -106,6 +106,7 @@ init([Options]) ->
{max_connections, 4096, fun is_non_neg_integer/1, invalid_max_connections_option},
{ssl, false, fun check_ssl_options/1, invalid_ssl_options},
{ws_force_ssl, false, fun is_boolean/1, invalid_ssl_external},
+ {proxy_protocol, false, fun is_boolean/1, invalid_proxy_protocol},
{recbuf, default, fun check_recbuf/1, recbuf_not_integer},
% misultin
{post_max_size, 4*1024*1024, fun is_non_neg_integer/1, invalid_post_max_size_option}, % defaults to 4 MB
@@ -132,6 +133,7 @@ init([Options]) ->
MaxConnections = proplists:get_value(max_connections, OptionsVerified),
SslOptions0 = proplists:get_value(ssl, OptionsVerified),
WsForceSsl = proplists:get_value(ws_force_ssl, OptionsVerified),
+ ProxyProtocol = proplists:get_value(proxy_protocol, OptionsVerified),
% misultin options
PostMaxSize = proplists:get_value(post_max_size, OptionsVerified),
GetUrlMaxSize = proplists:get_value(get_url_max_size, OptionsVerified),
@@ -197,7 +199,8 @@ init([Options]) ->
ws_autoexit = WsAutoExit,
ws_versions = WsVersions,
access_log = AccessLogFun,
- ws_force_ssl = WsForceSsl
+ ws_force_ssl = WsForceSsl,
+ proxy_protocol = ProxyProtocol
},
% define misultin_server supervisor specs
ServerSpec = {server, {misultin_server, start_link, [{MaxConnections}]}, permanent, 60000, worker, [misultin_server]},
View
43 src/misultin_acceptor.erl
@@ -186,7 +186,10 @@ open_connections_switch(ServerRef, SessionsRef, TableDateRef, Sock, ListenPort,
case misultin_server:http_pid_ref_add(ServerRef, self()) of
ok ->
% get peer address and port
- {PeerAddr, PeerPort} = misultin_socket:peername(Sock, SocketMode),
+ case CustomOpts#custom_opts.proxy_protocol of
+ false -> {PeerAddr, PeerPort} = misultin_socket:peername(Sock, SocketMode);
+ true -> {PeerAddr, PeerPort} = peername_from_proxy_line(Sock, SocketMode)
+ end,
?LOG_DEBUG("remote peer is ~p", [{PeerAddr, PeerPort}]),
% get peer certificate, if any
PeerCert = misultin_socket:peercert(Sock, SocketMode),
@@ -205,3 +208,41 @@ open_connections_switch(ServerRef, SessionsRef, TableDateRef, Sock, ListenPort,
end.
% ============================ /\ INTERNAL FUNCTIONS =======================================================
+
+%% receive the first line, and extract peer address details as per http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt
+peername_from_proxy_line(Sock, SocketMode) ->
+ misultin_socket:setopts(Sock, [{active,once}, {packet,line}, list], SocketMode),
+ receive
+ {TcpOrSsl, Sock, "PROXY " ++ ProxyLine} when TcpOrSsl =:= tcp; TcpOrSsl =:= ssl ->
+ case string:tokens(ProxyLine, "\r\n ") of
+ [_Proto, SrcAddrStr, _DestAddr, SrcPortStr, _DestPort] ->
+ {SrcPort, _} = string:to_integer(SrcPortStr),
+ {ok, SrcAddr} = inet_parse:address(SrcAddrStr),
+ ?LOG_DEBUG("Got peer address from proxy line: ~p",[{SrcAddr, SrcPort} ]),
+ {SrcAddr, SrcPort}
+ end;
+
+ {_, Sock, FirstLine} ->
+ ?LOG_DEBUG("FIRST LINE NOT 'PROXY', but 'PROXY ...' expected due to config option; line was: '~s'", [FirstLine]),
+ misultin_socket:send(Sock, [
+ "HTTP 502 Server Error\r\n",
+ "Connection: close\r\n\r\n",
+ "<h1>PROXY line expected</h1>",
+ "Misultin configured to expect PROXY line first, as per ",
+ "<a href=\"http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt\">the haproxy proxy protocol spec</a>, ",
+ "but first line received was:<br/><pre>\n",
+ FirstLine,
+ "\n</pre>"],
+ SocketMode),
+ misultin_socket:close(Sock, SocketMode),
+ exit(normal)
+
+ after 1000 ->
+ ?LOG_DEBUG("Timeout receiving PROXY line from upstream proxy, closing", []),
+ misultin_socket:send(Sock, ["HTTP 502 Server Error\r\n",
+ "Connection: close\r\n\r\n",
+ "<h1>502 timeout on receiving proxy line from upstream</h1>"],
+ SocketMode),
+ misultin_socket:close(Sock, SocketMode),
+ exit(normal)
+ end.
View
253 src/misultin_websocket_draft-hybi-10.erl
@@ -4,7 +4,7 @@
% >-|-|-(°>
%
% Copyright (C) 2011, Roberto Ostinelli <roberto@ostinelli.net>,
-% portions of code from Andy W. Song <https://github.com/awsong/erl_websocket>
+% 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()
@@ -147,81 +164,171 @@ send_format(Data, OpCode, _State) ->
% | 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}.
+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};
+
+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};
+
+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};
-% 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}]),
- {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,
+take_frame(Data) when is_binary(Data) ->
+ {undefined, Data}.
+
+orthrow(A,A,_T) -> ok;
+orthrow(_A,_,T) -> throw(T).
+
+%% Process incoming data
+i_handle_data(#state{buffer=ToParse} = State, {Socket, SocketMode, WsHandleLoopPid}) ->
+ case take_frame(ToParse) of
+ {undefined, Rest} ->
+ ?LOG_DEBUG("No frame to take, buffer=~p",[Rest]),
+ %% no full frame to be had yet
+ State#state{buffer = Rest};
+ {Frame=#frame{}, Rest} ->
+ ?LOG_DEBUG("Took frame ~p, buffer=~p",[Frame,Rest]),
+ %% Sanity check, in case client is broken:
+ orthrow(1, Frame#frame.maskbit, {protocol_error, mask_bit_clear}),
+ orthrow(0, Frame#frame.rsv1, {protocol_error, rsv1_set}),
+ orthrow(0, Frame#frame.rsv2, {protocol_error, rsv2_set}),
+ orthrow(0, Frame#frame.rsv3, {protocol_error, rsv3_set}),
+ 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
+ end.
+
+%% 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}, _, _) ->
+ %% Throwing here, should only happen if client is broken
+ throw({protocol_error, end_of_fragments_but_no_prior_fragments});
+ %% {websocket_close, websocket_close_data()};
+%% END OF FRAGMENT 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.
% unmask
-spec unmask(Key::binary(), Data::binary()) -> binary().
Something went wrong with that request. Please try again.