aboutsummaryrefslogtreecommitdiffstats
path: root/src/gun_ws.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2015-03-12 19:22:19 +0100
committerLoïc Hoguin <[email protected]>2015-03-12 19:22:19 +0100
commitc409897f508eedff8ecc6f0860c9379fcc11bf23 (patch)
treed5a651df4ef5e8f9c6c6bb77366defa53c686e20 /src/gun_ws.erl
parentea2de24f18741fc89d7a1dd6a3a0a43f3ccb1fd4 (diff)
downloadgun-c409897f508eedff8ecc6f0860c9379fcc11bf23.tar.gz
gun-c409897f508eedff8ecc6f0860c9379fcc11bf23.tar.bz2
gun-c409897f508eedff8ecc6f0860c9379fcc11bf23.zip
Add initial Websocket support
All autobahntestsuite tests pass including the permessage-deflate compression tests. Some of the tests pass in a non-strict fashion. They are testing for protocol errors and expect events to happen in a particular order, which is not respected by Gun. Gun fails earlier than is expected due to concurrent processing of frames. The implementation when error occurs during handshake is probably a bit rough at this point. The documentation is also incomplete and/or wrong at this time, though this is the general state of the Gun documentation and will be resolved in a separate commit.
Diffstat (limited to 'src/gun_ws.erl')
-rw-r--r--src/gun_ws.erl125
1 files changed, 125 insertions, 0 deletions
diff --git a/src/gun_ws.erl b/src/gun_ws.erl
new file mode 100644
index 0000000..5379362
--- /dev/null
+++ b/src/gun_ws.erl
@@ -0,0 +1,125 @@
+%% Copyright (c) 2015, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(gun_ws).
+
+-export([init/5]).
+-export([handle/2]).
+-export([send/2]).
+
+-record(payload, {
+ type = undefined :: cow_ws:frame_type(),
+ rsv = undefined :: cow_ws:rsv(),
+ len = undefined :: non_neg_integer(),
+ mask_key = undefined :: cow_ws:mask_key(),
+ close_code = undefined :: undefined | cow_ws:close_code(),
+ unmasked = <<>> :: binary(),
+ unmasked_len = 0 :: non_neg_integer()
+}).
+
+-record(ws_state, {
+ owner :: pid(),
+ socket :: inet:socket() | ssl:sslsocket(),
+ transport :: module(),
+ buffer = <<>> :: binary(),
+ in = head :: head | #payload{} | close,
+ frag_state = undefined :: cow_ws:frag_state(),
+ frag_buffer = <<>> :: binary(),
+ utf8_state = 0 :: cow_ws:utf8_state(),
+ extensions = #{} :: cow_ws:extensions()
+}).
+
+%% @todo Protocols
+init(Owner, Socket, Transport, Extensions, _Protocols) ->
+ Owner ! {gun_ws_upgrade, self(), ok},
+ {upgrade, ?MODULE, #ws_state{owner=Owner, socket=Socket, transport=Transport, extensions=Extensions}}.
+
+%% Do not handle anything if we received a close frame.
+handle(_, State=#ws_state{in=close}) ->
+ State;
+%% Shortcut for common case when Data is empty after processing a frame.
+handle(<<>>, State=#ws_state{in=head}) ->
+ State;
+handle(Data, State=#ws_state{buffer=Buffer, in=head, frag_state=FragState, extensions=Extensions}) ->
+ Data2 = << Buffer/binary, Data/binary >>,
+ case cow_ws:parse_header(Data2, Extensions, FragState) of
+ {Type, FragState2, Rsv, Len, MaskKey, Rest} ->
+ handle(Rest, State#ws_state{buffer= <<>>,
+ in=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey}, frag_state=FragState2});
+ more ->
+ State#ws_state{buffer=Data2};
+ error ->
+ close({error, badframe}, State)
+ end;
+handle(Data, State=#ws_state{in=In=#payload{type=Type, rsv=Rsv, len=Len, mask_key=MaskKey, close_code=CloseCode,
+ unmasked=Unmasked, unmasked_len=UnmaskedLen}, frag_state=FragState, utf8_state=Utf8State, extensions=Extensions}) ->
+ case cow_ws:parse_payload(Data, MaskKey, Utf8State, UnmaskedLen, Type, Len, FragState, Extensions, Rsv) of
+ {ok, CloseCode2, Payload, Utf8State2, Rest} ->
+ dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, << Unmasked/binary, Payload/binary >>, CloseCode2);
+ {ok, Payload, Utf8State2, Rest} ->
+ dispatch(Rest, State#ws_state{in=head, utf8_state=Utf8State2}, Type, << Unmasked/binary, Payload/binary >>, CloseCode);
+ {more, CloseCode2, Payload, Utf8State2} ->
+ State#ws_state{in=In#payload{close_code=CloseCode2, unmasked= << Unmasked/binary, Payload/binary >>,
+ len=Len - byte_size(Data), unmasked_len=2 + byte_size(Data)}, utf8_state=Utf8State2};
+ {more, Payload, Utf8State2} ->
+ State#ws_state{in=In#payload{unmasked= << Unmasked/binary, Payload/binary >>,
+ len=Len - byte_size(Data), unmasked_len=UnmaskedLen + byte_size(Data)}, utf8_state=Utf8State2};
+ Error = {error, _Reason} ->
+ close(Error, State)
+ end.
+
+dispatch(Rest, State=#ws_state{owner=Owner, frag_state=FragState, frag_buffer=SoFar},
+ Type0, Payload0, CloseCode0) ->
+ case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of
+ {fragment, nofin, _, Payload} ->
+ handle(Rest, State#ws_state{frag_buffer= << SoFar/binary, Payload/binary >>});
+ {fragment, fin, Type, Payload} ->
+ Owner ! {gun_ws, self(), {Type, << SoFar/binary, Payload/binary >>}},
+ handle(Rest, State#ws_state{frag_state=undefined, frag_buffer= <<>>});
+ ping ->
+ State2 = send(pong, State),
+ handle(Rest, State2);
+ {ping, Payload} ->
+ State2 = send({pong, Payload}, State),
+ handle(Rest, State2);
+ pong ->
+ handle(Rest, State);
+ {pong, _} ->
+ handle(Rest, State);
+ Frame ->
+ Owner ! {gun_ws, self(), Frame},
+ case Frame of
+ close -> handle(Rest, State#ws_state{in=close});
+ {close, _, _} -> handle(Rest, State#ws_state{in=close});
+ _ -> handle(Rest, State)
+ end
+ end.
+
+close(Reason, State) ->
+ case Reason of
+ Normal when Normal =:= stop; Normal =:= timeout ->
+ send({close, 1000, <<>>}, State);
+ {error, badframe} ->
+ send({close, 1002, <<>>}, State);
+ {error, badencoding} ->
+ send({close, 1007, <<>>}, State)
+ end.
+
+send(Frame, State=#ws_state{socket=Socket, transport=Transport, extensions=Extensions}) ->
+ Transport:send(Socket, cow_ws:masked_frame(Frame, Extensions)),
+ case Frame of
+ close -> close;
+ {close, _, _} -> close;
+ _ -> State
+ end.