%% Copyright (c) 2015-2018, Loïc Hoguin %% %% 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([check_options/1]). -export([name/0]). -export([init/8]). -export([handle/2]). -export([close/2]). -export([send/2]). -export([down/1]). -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(), utf8_state = 0 :: cow_ws:utf8_state(), extensions = #{} :: cow_ws:extensions(), handler :: module(), handler_state :: any() }). check_options(Opts) -> do_check_options(maps:to_list(Opts)). do_check_options([]) -> ok; do_check_options([{compress, B}|Opts]) when B =:= true; B =:= false -> do_check_options(Opts); do_check_options([{default_protocol, M}|Opts]) when is_atom(M) -> do_check_options(Opts); do_check_options([Opt={protocols, L}|Opts]) when is_list(L) -> case lists:usort(lists:flatten([[is_binary(B), is_atom(M)] || {B, M} <- L])) of [true] -> do_check_options(Opts); _ -> {error, {options, {ws, Opt}}} end; do_check_options([{user_opts, _}|Opts]) -> do_check_options(Opts); do_check_options([Opt|_]) -> {error, {options, {ws, Opt}}}. name() -> ws. init(Owner, Socket, Transport, StreamRef, Headers, Extensions, Handler, Opts) -> Owner ! {gun_upgrade, self(), StreamRef, [<<"websocket">>], Headers}, HandlerState = Handler:init(Owner, StreamRef, Headers, Opts), {switch_protocol, ?MODULE, #ws_state{owner=Owner, socket=Socket, transport=Transport, extensions=Extensions, handler=Handler, handler_state=HandlerState}}. %% 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, State0=#ws_state{frag_state=FragState, handler=Handler, handler_state=HandlerState0}, Type0, Payload0, CloseCode0) -> case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of ping -> State = send(pong, State0), handle(Rest, State); {ping, Payload} -> State = send({pong, Payload}, State0), handle(Rest, State); pong -> handle(Rest, State0); {pong, _} -> handle(Rest, State0); Frame -> HandlerState = Handler:handle(Frame, HandlerState0), State = State0#ws_state{handler_state=HandlerState}, case Frame of close -> handle(Rest, State#ws_state{in=close}); {close, _, _} -> handle(Rest, State#ws_state{in=close}); {fragment, fin, _, _} -> handle(Rest, State#ws_state{frag_state=undefined}); _ -> handle(Rest, State) end end. close(Reason, State) -> case Reason of %% @todo We need to send a close frame from gun:ws_loop on close. % Normal when Normal =:= stop; Normal =:= timeout -> % send({close, 1000, <<>>}, State); owner_gone -> send({close, 1001, <<>>}, 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. %% Websocket has no concept of streams. down(_) -> {[], []}.