diff options
Diffstat (limited to 'src/cowboy_websocket.erl')
-rw-r--r-- | src/cowboy_websocket.erl | 159 |
1 files changed, 115 insertions, 44 deletions
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index 1c6d20c..8c02ac7 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -21,6 +21,11 @@ %% Internal. -export([handler_loop/4]). +-type frame() :: close | ping | pong + | {text | binary | close | ping | pong, binary()} + | {close, 1000..4999, binary()}. +-export_type([frame/0]). + -type opcode() :: 0 | 1 | 2 | 8 | 9 | 10. -type mask_key() :: 0..16#ffffffff. @@ -125,12 +130,12 @@ handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts}, closed catch Class:Reason -> upgrade_error(Req), - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( - "** Handler ~p terminating in websocket_init/3~n" + "** Cowboy handler ~p terminating in ~p/~p~n" " for the reason ~p:~p~n** Options were ~p~n" "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]) + [Handler, websocket_init, 3, Class, Reason, Opts, + cowboy_req:to_list(Req),erlang:get_stacktrace()]) end. -spec upgrade_error(cowboy_req:req()) -> closed. @@ -149,9 +154,9 @@ websocket_handshake(State=#state{socket=Socket, transport=Transport, {<< "http", Location/binary >>, Req1} = cowboy_req:url(Req), {ok, Req2} = cowboy_req:upgrade_reply( <<"101 WebSocket Protocol Handshake">>, - [{<<"Upgrade">>, <<"WebSocket">>}, - {<<"Sec-Websocket-Location">>, << "ws", Location/binary >>}, - {<<"Sec-Websocket-Origin">>, Origin}], + [{<<"upgrade">>, <<"WebSocket">>}, + {<<"sec-websocket-location">>, << "ws", Location/binary >>}, + {<<"sec-websocket-origin">>, Origin}], Req1), %% Flush the resp_sent message before moving on. receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, @@ -171,18 +176,20 @@ websocket_handshake(State=#state{socket=Socket, transport=Transport, handler_before_loop(State#state{messages=Transport:messages()}, Req4, HandlerState, <<>>); _Any -> - closed %% If an error happened reading the body, stop there. + %% If an error happened reading the body, stop there. + handler_terminate(State, Req3, HandlerState, {error, closed}) end; websocket_handshake(State=#state{transport=Transport, challenge=Challenge}, Req, HandlerState) -> {ok, Req2} = cowboy_req:upgrade_reply( 101, - [{<<"Upgrade">>, <<"websocket">>}, - {<<"Sec-Websocket-Accept">>, Challenge}], + [{<<"upgrade">>, <<"websocket">>}, + {<<"sec-websocket-accept">>, Challenge}], Req), %% Flush the resp_sent message before moving on. receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, - handler_before_loop(State#state{messages=Transport:messages()}, + State2 = handler_loop_timeout(State), + handler_before_loop(State2#state{messages=Transport:messages()}, Req2, HandlerState, <<>>). -spec handler_before_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed. @@ -190,15 +197,13 @@ handler_before_loop(State=#state{ socket=Socket, transport=Transport, hibernate=true}, Req, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), - State2 = handler_loop_timeout(State), catch erlang:hibernate(?MODULE, handler_loop, - [State2#state{hibernate=false}, Req, HandlerState, SoFar]), + [State#state{hibernate=false}, Req, HandlerState, SoFar]), closed; handler_before_loop(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), - State2 = handler_loop_timeout(State), - handler_loop(State2, Req, HandlerState, SoFar). + handler_loop(State, Req, HandlerState, SoFar). -spec handler_loop_timeout(#state{}) -> #state{}. handler_loop_timeout(State=#state{timeout=infinity}) -> @@ -216,7 +221,8 @@ handler_loop(State=#state{ Req, HandlerState, SoFar) -> receive {OK, Socket, Data} -> - websocket_data(State, Req, HandlerState, + State2 = handler_loop_timeout(State), + websocket_data(State2, Req, HandlerState, << SoFar/binary, Data/binary >>); {Closed, Socket} -> handler_terminate(State, Req, HandlerState, {error, closed}); @@ -452,38 +458,76 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState, Req2, HandlerState2, RemainingData); {reply, Payload, Req2, HandlerState2} when is_tuple(Payload) -> - ok = websocket_send(Payload, State), - NextState(State, Req2, HandlerState2, RemainingData); + case websocket_send(Payload, State) of + ok -> + State2 = handler_loop_timeout(State), + NextState(State2, Req2, HandlerState2, RemainingData); + shutdown -> + handler_terminate(State, Req2, HandlerState, + {normal, shutdown}); + {error, _} = Error -> + handler_terminate(State, Req2, HandlerState2, Error) + end; {reply, Payload, Req2, HandlerState2, hibernate} when is_tuple(Payload) -> - ok = websocket_send(Payload, State), - NextState(State#state{hibernate=true}, - Req2, HandlerState2, RemainingData); + case websocket_send(Payload, State) of + ok -> + State2 = handler_loop_timeout(State), + NextState(State2#state{hibernate=true}, + Req2, HandlerState2, RemainingData); + shutdown -> + handler_terminate(State, Req2, HandlerState, + {normal, shutdown}); + {error, _} = Error -> + handler_terminate(State, Req2, HandlerState2, Error) + end; {reply, Payload, Req2, HandlerState2} when is_list(Payload) -> - ok = websocket_send_many(Payload, State), - NextState(State, Req2, HandlerState2, RemainingData); + case websocket_send_many(Payload, State) of + ok -> + State2 = handler_loop_timeout(State), + NextState(State2, Req2, HandlerState2, RemainingData); + shutdown -> + handler_terminate(State, Req2, HandlerState, + {normal, shutdown}); + {error, _} = Error -> + handler_terminate(State, Req2, HandlerState2, Error) + end; {reply, Payload, Req2, HandlerState2, hibernate} when is_list(Payload) -> - ok = websocket_send_many(Payload, State), - NextState(State#state{hibernate=true}, - Req2, HandlerState2, RemainingData); + case websocket_send_many(Payload, State) of + ok -> + State2 = handler_loop_timeout(State), + NextState(State2#state{hibernate=true}, + Req2, HandlerState2, RemainingData); + shutdown -> + handler_terminate(State, Req2, HandlerState, + {normal, shutdown}); + {error, _} = Error -> + handler_terminate(State, Req2, HandlerState2, Error) + end; {shutdown, Req2, HandlerState2} -> websocket_close(State, Req2, HandlerState2, {normal, shutdown}) catch Class:Reason -> PLReq = cowboy_req:to_list(Req), error_logger:error_msg( - "** Handler ~p terminating in ~p/3~n" + "** Cowboy handler ~p terminating in ~p/~p~n" " for the reason ~p:~p~n** Message was ~p~n" "** Options were ~p~n** Handler state was ~p~n" "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Callback, Class, Reason, Message, Opts, + [Handler, Callback, 3, Class, Reason, Message, Opts, HandlerState, PLReq, erlang:get_stacktrace()]), websocket_close(State, Req, HandlerState, {error, handler}) end. --spec websocket_send({text | binary | ping | pong, binary()}, #state{}) - -> ok | {error, atom()}. +websocket_opcode(text) -> 1; +websocket_opcode(binary) -> 2; +websocket_opcode(close) -> 8; +websocket_opcode(ping) -> 9; +websocket_opcode(pong) -> 10. + +-spec websocket_send(frame(), #state{}) + -> ok | shutdown | {error, atom()}. %% hixie-76 text frame. websocket_send({text, Payload}, #state{ socket=Socket, transport=Transport, version=0}) -> @@ -491,24 +535,52 @@ websocket_send({text, Payload}, #state{ %% Ignore all unknown frame types for compatibility with hixie 76. websocket_send(_Any, #state{version=0}) -> ok; +websocket_send(Type, #state{socket=Socket, transport=Transport}) + when Type =:= close -> + Opcode = websocket_opcode(Type), + case Transport:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>) of + ok -> shutdown; + Error -> Error + end; +websocket_send(Type, #state{socket=Socket, transport=Transport}) + when Type =:= ping; Type =:= pong -> + Opcode = websocket_opcode(Type), + Transport:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>); +websocket_send({close, Payload}, State) -> + websocket_send({close, 1000, Payload}, State); +websocket_send({Type = close, StatusCode, Payload}, #state{ + socket=Socket, transport=Transport}) -> + Opcode = websocket_opcode(Type), + Len = 2 + iolist_size(Payload), + %% Control packets must not be > 125 in length. + true = Len =< 125, + BinLen = hybi_payload_length(Len), + Transport:send(Socket, + [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]), + shutdown; websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) -> - Opcode = case Type of - text -> 1; - binary -> 2; - ping -> 9; - pong -> 10 + Opcode = websocket_opcode(Type), + Len = iolist_size(Payload), + %% Control packets must not be > 125 in length. + true = if Type =:= ping; Type =:= pong -> + Len =< 125; + true -> + true end, - Len = hybi_payload_length(iolist_size(Payload)), - Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, Len/bits >>, - Payload]). + BinLen = hybi_payload_length(Len), + Transport:send(Socket, + [<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]). --spec websocket_send_many([{text | binary | ping | pong, binary()}], #state{}) - -> ok | {error, atom()}. +-spec websocket_send_many([frame()], #state{}) + -> ok | shutdown | {error, atom()}. websocket_send_many([], _) -> ok; websocket_send_many([Frame|Tail], State) -> - ok = websocket_send(Frame, State), - websocket_send_many(Tail, State). + case websocket_send(Frame, State) of + ok -> websocket_send_many(Tail, State); + shutdown -> shutdown; + Error -> Error + end. -spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()}) -> closed. @@ -516,7 +588,6 @@ websocket_close(State=#state{socket=Socket, transport=Transport, version=0}, Req, HandlerState, Reason) -> Transport:send(Socket, << 255, 0 >>), handler_terminate(State, Req, HandlerState, Reason); -%% @todo Send a Payload? Using Reason is usually good but we're quite careless. websocket_close(State=#state{socket=Socket, transport=Transport}, Req, HandlerState, Reason) -> Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), @@ -531,11 +602,11 @@ handler_terminate(#state{handler=Handler, opts=Opts}, catch Class:Reason -> PLReq = cowboy_req:to_list(Req), error_logger:error_msg( - "** Handler ~p terminating in websocket_terminate/3~n" + "** Cowboy handler ~p terminating in ~p/~p~n" " for the reason ~p:~p~n** Initial reason was ~p~n" "** Options were ~p~n** Handler state was ~p~n" "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, TerminateReason, Opts, + [Handler, websocket_terminate, 3, Class, Reason, TerminateReason, Opts, HandlerState, PLReq, erlang:get_stacktrace()]) end, closed. |