mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 12:20:24 +00:00
Add support for the webkit deflate frame extension
This commit is contained in:
parent
47396211cf
commit
a63faff35e
4 changed files with 288 additions and 97 deletions
|
@ -38,6 +38,7 @@
|
|||
-export([quoted_string/2]).
|
||||
-export([authorization/2]).
|
||||
-export([range/1]).
|
||||
-export([parameterized_tokens/1]).
|
||||
|
||||
%% Decoding.
|
||||
-export([te_chunked/2]).
|
||||
|
@ -905,6 +906,49 @@ range_digits(Data, Default, Fun) ->
|
|||
Fun(Data, Default)
|
||||
end).
|
||||
|
||||
%% @doc Parse a non empty list of tokens followed with optional parameters.
|
||||
-spec parameterized_tokens(binary()) -> any().
|
||||
parameterized_tokens(Data) ->
|
||||
nonempty_list(Data,
|
||||
fun (D, Fun) ->
|
||||
token(D,
|
||||
fun (_Rest, <<>>) -> {error, badarg};
|
||||
(Rest, Token) ->
|
||||
parameterized_tokens_params(Rest,
|
||||
fun (Rest2, Params) ->
|
||||
Fun(Rest2, {Token, Params})
|
||||
end, [])
|
||||
end)
|
||||
end).
|
||||
|
||||
-spec parameterized_tokens_params(binary(), fun(), [binary() | {binary(), binary()}]) -> any().
|
||||
parameterized_tokens_params(Data, Fun, Acc) ->
|
||||
whitespace(Data,
|
||||
fun (<< $;, Rest/binary >>) ->
|
||||
parameterized_tokens_param(Rest,
|
||||
fun (Rest2, Param) ->
|
||||
parameterized_tokens_params(Rest2, Fun, [Param|Acc])
|
||||
end);
|
||||
(Rest) ->
|
||||
Fun(Rest, lists:reverse(Acc))
|
||||
end).
|
||||
|
||||
-spec parameterized_tokens_param(binary(), fun()) -> any().
|
||||
parameterized_tokens_param(Data, Fun) ->
|
||||
whitespace(Data,
|
||||
fun (Rest) ->
|
||||
token(Rest,
|
||||
fun (_Rest2, <<>>) -> {error, badarg};
|
||||
(<< $=, Rest2/binary >>, Attr) ->
|
||||
word(Rest2,
|
||||
fun (Rest3, Value) ->
|
||||
Fun(Rest3, {Attr, Value})
|
||||
end);
|
||||
(Rest2, Attr) ->
|
||||
Fun(Rest2, Attr)
|
||||
end)
|
||||
end).
|
||||
|
||||
%% Decoding.
|
||||
|
||||
%% @doc Decode a stream of chunks.
|
||||
|
@ -1290,6 +1334,17 @@ content_type_test_() ->
|
|||
],
|
||||
[{V, fun () -> R = content_type(V) end} || {V, R} <- Tests].
|
||||
|
||||
parameterized_tokens_test_() ->
|
||||
%% {ParameterizedTokens, Result}
|
||||
Tests = [
|
||||
{<<"foo">>, [{<<"foo">>, []}]},
|
||||
{<<"bar; baz=2">>, [{<<"bar">>, [{<<"baz">>, <<"2">>}]}]},
|
||||
{<<"bar; baz=2;bat">>, [{<<"bar">>, [{<<"baz">>, <<"2">>}, <<"bat">>]}]},
|
||||
{<<"bar; baz=2;bat=\"z=1,2;3\"">>, [{<<"bar">>, [{<<"baz">>, <<"2">>}, {<<"bat">>, <<"z=1,2;3">>}]}]},
|
||||
{<<"foo, bar; baz=2">>, [{<<"foo">>, []}, {<<"bar">>, [{<<"baz">>, <<"2">>}]}]}
|
||||
],
|
||||
[{V, fun () -> R = parameterized_tokens(V) end} || {V, R} <- Tests].
|
||||
|
||||
digits_test_() ->
|
||||
%% {Digits, Result}
|
||||
Tests = [
|
||||
|
|
|
@ -460,6 +460,8 @@ parse_header(Name, Req, Default)
|
|||
fun (Value) ->
|
||||
cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2)
|
||||
end);
|
||||
parse_header(Name = <<"sec-websocket-extensions">>, Req, Default) ->
|
||||
parse_header(Name, Req, Default, fun cowboy_http:parameterized_tokens/1);
|
||||
parse_header(Name, Req, Default) ->
|
||||
{Value, Req2} = header(Name, Req, Default),
|
||||
{undefined, Value, Req2}.
|
||||
|
@ -1173,6 +1175,7 @@ g(port, #http_req{port=Ret}) -> Ret;
|
|||
g(qs, #http_req{qs=Ret}) -> Ret;
|
||||
g(qs_vals, #http_req{qs_vals=Ret}) -> Ret;
|
||||
g(resp_body, #http_req{resp_body=Ret}) -> Ret;
|
||||
g(resp_compress, #http_req{resp_compress=Ret}) -> Ret;
|
||||
g(resp_headers, #http_req{resp_headers=Ret}) -> Ret;
|
||||
g(resp_state, #http_req{resp_state=Ret}) -> Ret;
|
||||
g(socket, #http_req{socket=Ret}) -> Ret;
|
||||
|
|
|
@ -37,6 +37,7 @@
|
|||
-type mask_key() :: 0..16#ffffffff.
|
||||
-type frag_state() :: undefined
|
||||
| {nofin, opcode(), binary()} | {fin, opcode(), binary()}.
|
||||
-type rsv() :: << _:3 >>.
|
||||
|
||||
-record(state, {
|
||||
env :: cowboy_middleware:env(),
|
||||
|
@ -50,7 +51,11 @@
|
|||
messages = undefined :: undefined | {atom(), atom(), atom()},
|
||||
hibernate = false :: boolean(),
|
||||
frag_state = undefined :: frag_state(),
|
||||
utf8_state = <<>> :: binary()
|
||||
utf8_state = <<>> :: binary(),
|
||||
deflate_frame = false :: boolean(),
|
||||
inflate_state :: any(),
|
||||
inflate_buffer = <<>> :: binary(),
|
||||
deflate_state :: any()
|
||||
}).
|
||||
|
||||
%% @doc Upgrade an HTTP request to the Websocket protocol.
|
||||
|
@ -88,8 +93,39 @@ websocket_upgrade(State, Req) ->
|
|||
orelse (IntVersion =:= 13),
|
||||
{Key, Req5} = cowboy_req:header(<<"sec-websocket-key">>, Req4),
|
||||
false = Key =:= undefined,
|
||||
{ok, State#state{key=Key},
|
||||
cowboy_req:set_meta(websocket_version, IntVersion, Req5)}.
|
||||
websocket_extensions(State#state{key=Key},
|
||||
cowboy_req:set_meta(websocket_version, IntVersion, Req5)).
|
||||
|
||||
-spec websocket_extensions(#state{}, Req)
|
||||
-> {ok, #state{}, Req} when Req::cowboy_req:req().
|
||||
websocket_extensions(State, Req) ->
|
||||
case cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req) of
|
||||
{ok, Extensions, Req2} when Extensions =/= undefined ->
|
||||
[Compress] = cowboy_req:get([resp_compress], Req),
|
||||
case lists:keyfind(<<"x-webkit-deflate-frame">>, 1, Extensions) of
|
||||
{<<"x-webkit-deflate-frame">>, []} when Compress =:= true ->
|
||||
Inflate = zlib:open(),
|
||||
Deflate = zlib:open(),
|
||||
% Since we are negotiating an unconstrained deflate-frame
|
||||
% then we must be willing to accept frames using the
|
||||
% maximum window size which is 2^15. The negative value
|
||||
% indicates that zlib headers are not used.
|
||||
ok = zlib:inflateInit(Inflate, -15),
|
||||
% Initialize the deflater with a window size of 2^15 bits and disable
|
||||
% the zlib headers.
|
||||
ok = zlib:deflateInit(Deflate, best_compression, deflated, -15, 8, default),
|
||||
{ok, State#state{
|
||||
deflate_frame = true,
|
||||
inflate_state = Inflate,
|
||||
inflate_buffer = <<>>,
|
||||
deflate_state = Deflate
|
||||
}, Req2};
|
||||
_ ->
|
||||
{ok, State, Req2}
|
||||
end;
|
||||
_ ->
|
||||
{ok, State, Req}
|
||||
end.
|
||||
|
||||
-spec handler_init(#state{}, Req)
|
||||
-> {ok, Req, cowboy_middleware:env()} | {error, 400, Req}
|
||||
|
@ -137,14 +173,20 @@ upgrade_error(Req, Env) ->
|
|||
-> {ok, Req, cowboy_middleware:env()}
|
||||
| {suspend, module(), atom(), [any()]}
|
||||
when Req::cowboy_req:req().
|
||||
websocket_handshake(State=#state{transport=Transport, key=Key},
|
||||
websocket_handshake(State=#state{
|
||||
transport=Transport, key=Key, deflate_frame=DeflateFrame},
|
||||
Req, HandlerState) ->
|
||||
Challenge = base64:encode(crypto:sha(
|
||||
<< Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)),
|
||||
Extensions = case DeflateFrame of
|
||||
false -> [];
|
||||
true -> [{<<"sec-websocket-extensions">>, <<"x-webkit-deflate-frame">>}]
|
||||
end,
|
||||
{ok, Req2} = cowboy_req:upgrade_reply(
|
||||
101,
|
||||
[{<<"upgrade">>, <<"websocket">>},
|
||||
{<<"sec-websocket-accept">>, Challenge}],
|
||||
{<<"sec-websocket-accept">>, Challenge}|
|
||||
Extensions],
|
||||
Req),
|
||||
%% Flush the resp_sent message before moving on.
|
||||
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
|
||||
|
@ -211,7 +253,7 @@ handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
|
|||
%% RSV bits MUST be 0 unless an extension is negotiated
|
||||
%% that defines meanings for non-zero values.
|
||||
websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>)
|
||||
when Rsv =/= 0 ->
|
||||
when Rsv =/= 0, State#state.deflate_frame =:= false ->
|
||||
websocket_close(State, Req, HandlerState, {error, badframe});
|
||||
%% Invalid opcode. Note that these opcodes may be used by extensions.
|
||||
websocket_data(State, Req, HandlerState, << _:4, Opcode:4, _/bits >>)
|
||||
|
@ -239,23 +281,23 @@ websocket_data(State, Req, HandlerState,
|
|||
when Len > 1, byte_size(Data) < 8 ->
|
||||
handler_before_loop(State, Req, HandlerState, Data);
|
||||
%% 7 bits payload length.
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1,
|
||||
Len:7, MaskKey:32, Rest/bits >>)
|
||||
when Len < 126 ->
|
||||
websocket_data(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Rest, Fin);
|
||||
Opcode, Len, MaskKey, Rest, Rsv, Fin);
|
||||
%% 16 bits payload length.
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1,
|
||||
126:7, Len:16, MaskKey:32, Rest/bits >>)
|
||||
when Len > 125, Opcode < 8 ->
|
||||
websocket_data(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Rest, Fin);
|
||||
Opcode, Len, MaskKey, Rest, Rsv, Fin);
|
||||
%% 63 bits payload length.
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
|
||||
websocket_data(State, Req, HandlerState, << Fin:1, Rsv:3/bits, Opcode:4, 1:1,
|
||||
127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>)
|
||||
when Len > 16#ffff, Opcode < 8 ->
|
||||
websocket_data(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Rest, Fin);
|
||||
Opcode, Len, MaskKey, Rest, Rsv, Fin);
|
||||
%% When payload length is over 63 bits, the most significant bit MUST be 0.
|
||||
websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _:7, _/binary >>) ->
|
||||
websocket_close(State, Req, HandlerState, {error, badframe});
|
||||
|
@ -276,120 +318,141 @@ websocket_data(State, Req, HandlerState, Data) ->
|
|||
|
||||
%% Initialize or update fragmentation state.
|
||||
-spec websocket_data(#state{}, Req, any(),
|
||||
opcode(), non_neg_integer(), mask_key(), binary(), 0 | 1)
|
||||
opcode(), non_neg_integer(), mask_key(), binary(), rsv(), 0 | 1)
|
||||
-> {ok, Req, cowboy_middleware:env()}
|
||||
| {suspend, module(), atom(), [any()]}
|
||||
when Req::cowboy_req:req().
|
||||
%% The opcode is only included in the first frame fragment.
|
||||
websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Data, 0) ->
|
||||
Opcode, Len, MaskKey, Data, Rsv, 0) ->
|
||||
websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}},
|
||||
Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
|
||||
Req, HandlerState, 0, Len, MaskKey, <<>>, Data, Rsv);
|
||||
%% Subsequent frame fragments.
|
||||
websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
|
||||
0, Len, MaskKey, Data, 0) ->
|
||||
0, Len, MaskKey, Data, Rsv, 0) ->
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
0, Len, MaskKey, <<>>, Data);
|
||||
0, Len, MaskKey, <<>>, Data, Rsv);
|
||||
%% Final frame fragment.
|
||||
websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}},
|
||||
Req, HandlerState, 0, Len, MaskKey, Data, 1) ->
|
||||
Req, HandlerState, 0, Len, MaskKey, Data, Rsv, 1) ->
|
||||
websocket_payload(State#state{frag_state={fin, Opcode, SoFar}},
|
||||
Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
|
||||
Req, HandlerState, 0, Len, MaskKey, <<>>, Data, Rsv);
|
||||
%% Unfragmented frame.
|
||||
websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, 1) ->
|
||||
websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, Rsv, 1) ->
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, <<>>, Data).
|
||||
Opcode, Len, MaskKey, <<>>, Data, Rsv).
|
||||
|
||||
-spec websocket_payload(#state{}, Req, any(),
|
||||
opcode(), non_neg_integer(), mask_key(), binary(), binary())
|
||||
opcode(), non_neg_integer(), mask_key(), binary(), binary(), rsv())
|
||||
-> {ok, Req, cowboy_middleware:env()}
|
||||
| {suspend, module(), atom(), [any()]}
|
||||
when Req::cowboy_req:req().
|
||||
%% Close control frames with a payload MUST contain a valid close code.
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
Opcode=8, Len, MaskKey, <<>>, << MaskedCode:2/binary, Rest/bits >>) ->
|
||||
Opcode=8, Len, MaskKey, <<>>, << MaskedCode:2/binary, Rest/bits >>, Rsv) ->
|
||||
Unmasked = << Code:16 >> = websocket_unmask(MaskedCode, MaskKey, <<>>),
|
||||
if Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006;
|
||||
(Code > 1011) and (Code < 3000); Code > 4999 ->
|
||||
websocket_close(State, Req, HandlerState, {error, badframe});
|
||||
true ->
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
Opcode, Len - 2, MaskKey, Unmasked, Rest)
|
||||
Opcode, Len - 2, MaskKey, Unmasked, Rest, Rsv)
|
||||
end;
|
||||
%% Text frames and close control frames MUST have a payload that is valid UTF-8.
|
||||
websocket_payload(State=#state{utf8_state=Incomplete},
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data, Rsv)
|
||||
when (byte_size(Data) < Len) andalso ((Opcode =:= 1) orelse
|
||||
((Opcode =:= 8) andalso (Unmasked =/= <<>>))) ->
|
||||
Unmasked2 = websocket_unmask(Data,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of
|
||||
false ->
|
||||
websocket_close(State, Req, HandlerState, {error, badencoding});
|
||||
websocket_close(State2, Req, HandlerState, {error, badencoding});
|
||||
Utf8State ->
|
||||
websocket_payload_loop(State#state{utf8_state=Utf8State},
|
||||
websocket_payload_loop(State2#state{utf8_state=Utf8State},
|
||||
Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
|
||||
<< Unmasked/binary, Unmasked2/binary >>)
|
||||
<< Unmasked/binary, Unmasked3/binary >>, Rsv)
|
||||
end;
|
||||
websocket_payload(State=#state{utf8_state=Incomplete},
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data, Rsv)
|
||||
when Opcode =:= 1; (Opcode =:= 8) and (Unmasked =/= <<>>) ->
|
||||
<< End:Len/binary, Rest/bits >> = Data,
|
||||
Unmasked2 = websocket_unmask(End,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, true, State),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of
|
||||
<<>> ->
|
||||
websocket_dispatch(State#state{utf8_state= <<>>},
|
||||
websocket_dispatch(State2#state{utf8_state= <<>>},
|
||||
Req, HandlerState, Rest, Opcode,
|
||||
<< Unmasked/binary, Unmasked2/binary >>);
|
||||
<< Unmasked/binary, Unmasked3/binary >>);
|
||||
_ ->
|
||||
websocket_close(State, Req, HandlerState, {error, badencoding})
|
||||
websocket_close(State2, Req, HandlerState, {error, badencoding})
|
||||
end;
|
||||
%% Fragmented text frames may cut payload in the middle of UTF-8 codepoints.
|
||||
websocket_payload(State=#state{frag_state={_, 1, _}, utf8_state=Incomplete},
|
||||
Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data)
|
||||
Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data, Rsv)
|
||||
when byte_size(Data) < Len ->
|
||||
Unmasked2 = websocket_unmask(Data,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of
|
||||
false ->
|
||||
websocket_close(State, Req, HandlerState, {error, badencoding});
|
||||
websocket_close(State2, Req, HandlerState, {error, badencoding});
|
||||
Utf8State ->
|
||||
websocket_payload_loop(State#state{utf8_state=Utf8State},
|
||||
websocket_payload_loop(State2#state{utf8_state=Utf8State},
|
||||
Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
|
||||
<< Unmasked/binary, Unmasked2/binary >>)
|
||||
<< Unmasked/binary, Unmasked3/binary >>, Rsv)
|
||||
end;
|
||||
websocket_payload(State=#state{frag_state={Fin, 1, _}, utf8_state=Incomplete},
|
||||
Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data) ->
|
||||
Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data, Rsv) ->
|
||||
<< End:Len/binary, Rest/bits >> = Data,
|
||||
Unmasked2 = websocket_unmask(End,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, true, State),
|
||||
case is_utf8(<< Incomplete/binary, Unmasked3/binary >>) of
|
||||
<<>> ->
|
||||
websocket_dispatch(State#state{utf8_state= <<>>},
|
||||
websocket_dispatch(State2#state{utf8_state= <<>>},
|
||||
Req, HandlerState, Rest, Opcode,
|
||||
<< Unmasked/binary, Unmasked2/binary >>);
|
||||
<< Unmasked/binary, Unmasked3/binary >>);
|
||||
Utf8State when is_binary(Utf8State), Fin =:= nofin ->
|
||||
websocket_dispatch(State#state{utf8_state=Utf8State},
|
||||
websocket_dispatch(State2#state{utf8_state=Utf8State},
|
||||
Req, HandlerState, Rest, Opcode,
|
||||
<< Unmasked/binary, Unmasked2/binary >>);
|
||||
<< Unmasked/binary, Unmasked3/binary >>);
|
||||
_ ->
|
||||
websocket_close(State, Req, HandlerState, {error, badencoding})
|
||||
end;
|
||||
%% Other frames have a binary payload.
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Unmasked, Data)
|
||||
Opcode, Len, MaskKey, Unmasked, Data, Rsv)
|
||||
when byte_size(Data) < Len ->
|
||||
Unmasked2 = websocket_unmask(Data,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
|
||||
websocket_payload_loop(State, Req, HandlerState,
|
||||
Opcode, Len - byte_size(Data), MaskKey, Unmasked2);
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, false, State),
|
||||
websocket_payload_loop(State2, Req, HandlerState,
|
||||
Opcode, Len - byte_size(Data), MaskKey, Unmasked3, Rsv);
|
||||
websocket_payload(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Unmasked, Data) ->
|
||||
Opcode, Len, MaskKey, Unmasked, Data, Rsv) ->
|
||||
<< End:Len/binary, Rest/bits >> = Data,
|
||||
Unmasked2 = websocket_unmask(End,
|
||||
rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
|
||||
websocket_dispatch(State, Req, HandlerState, Rest, Opcode, Unmasked2).
|
||||
{Unmasked3, State2} = websocket_inflate_frame(Unmasked2, Rsv, true, State),
|
||||
websocket_dispatch(State2, Req, HandlerState, Rest, Opcode, Unmasked3).
|
||||
|
||||
-spec websocket_inflate_frame(binary(), rsv(), boolean(), #state{}) ->
|
||||
{binary(), #state{}}.
|
||||
websocket_inflate_frame(Data, << Rsv1:1, _:2 >>, _,
|
||||
#state{deflate_frame = DeflateFrame} = State)
|
||||
when DeflateFrame =:= false orelse Rsv1 =:= 0 ->
|
||||
{Data, State};
|
||||
websocket_inflate_frame(Data, << 1:1, _:2 >>, false,
|
||||
#state{inflate_buffer = Buffer} = State) ->
|
||||
{<<>>, State#state{inflate_buffer = << Buffer/binary, Data/binary >>}};
|
||||
websocket_inflate_frame(Data, << 1:1, _:2 >>, true,
|
||||
#state{inflate_state = Inflate, inflate_buffer = Buffer} = State) ->
|
||||
Deflated = << Buffer/binary, Data/binary, 0:8, 0:8, 255:8, 255:8 >>,
|
||||
Result = zlib:inflate(Inflate, Deflated),
|
||||
{iolist_to_binary(Result), State#state{inflate_buffer = <<>>}}.
|
||||
|
||||
-spec websocket_unmask(B, mask_key(), B) -> B when B::binary().
|
||||
websocket_unmask(<<>>, _, Unmasked) ->
|
||||
|
@ -448,19 +511,19 @@ is_utf8(_) ->
|
|||
false.
|
||||
|
||||
-spec websocket_payload_loop(#state{}, Req, any(),
|
||||
opcode(), non_neg_integer(), mask_key(), binary())
|
||||
opcode(), non_neg_integer(), mask_key(), binary(), rsv())
|
||||
-> {ok, Req, cowboy_middleware:env()}
|
||||
| {suspend, module(), atom(), [any()]}
|
||||
when Req::cowboy_req:req().
|
||||
websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
|
||||
messages={OK, Closed, Error}, timeout_ref=TRef},
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked) ->
|
||||
Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Rsv) ->
|
||||
Transport:setopts(Socket, [{active, once}]),
|
||||
receive
|
||||
{OK, Socket, Data} ->
|
||||
State2 = handler_loop_timeout(State),
|
||||
websocket_payload(State2, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Unmasked, Data);
|
||||
Opcode, Len, MaskKey, Unmasked, Data, Rsv);
|
||||
{Closed, Socket} ->
|
||||
handler_terminate(State, Req, HandlerState, {error, closed});
|
||||
{Error, Socket, Reason} ->
|
||||
|
@ -469,13 +532,13 @@ websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
|
|||
websocket_close(State, Req, HandlerState, {normal, timeout});
|
||||
{timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
|
||||
websocket_payload_loop(State, Req, HandlerState,
|
||||
Opcode, Len, MaskKey, Unmasked);
|
||||
Opcode, Len, MaskKey, Unmasked, Rsv);
|
||||
Message ->
|
||||
handler_call(State, Req, HandlerState,
|
||||
<<>>, websocket_info, Message,
|
||||
fun (State2, Req2, HandlerState2, _) ->
|
||||
websocket_payload_loop(State2, Req2, HandlerState2,
|
||||
Opcode, Len, MaskKey, Unmasked)
|
||||
Opcode, Len, MaskKey, Unmasked, Rsv)
|
||||
end)
|
||||
end.
|
||||
|
||||
|
@ -534,48 +597,48 @@ handler_call(State=#state{handler=Handler, handler_opts=HandlerOpts}, Req,
|
|||
{reply, Payload, Req2, HandlerState2}
|
||||
when is_tuple(Payload) ->
|
||||
case websocket_send(Payload, State) of
|
||||
ok ->
|
||||
NextState(State, Req2, HandlerState2, RemainingData);
|
||||
shutdown ->
|
||||
handler_terminate(State, Req2, HandlerState2,
|
||||
{ok, State2} ->
|
||||
NextState(State2, Req2, HandlerState2, RemainingData);
|
||||
{shutdown, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2,
|
||||
{normal, shutdown});
|
||||
{error, _} = Error ->
|
||||
handler_terminate(State, Req2, HandlerState2, Error)
|
||||
{{error, _} = Error, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2, Error)
|
||||
end;
|
||||
{reply, Payload, Req2, HandlerState2, hibernate}
|
||||
when is_tuple(Payload) ->
|
||||
case websocket_send(Payload, State) of
|
||||
ok ->
|
||||
NextState(State#state{hibernate=true},
|
||||
{ok, State2} ->
|
||||
NextState(State2#state{hibernate=true},
|
||||
Req2, HandlerState2, RemainingData);
|
||||
shutdown ->
|
||||
handler_terminate(State, Req2, HandlerState2,
|
||||
{shutdown, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2,
|
||||
{normal, shutdown});
|
||||
{error, _} = Error ->
|
||||
handler_terminate(State, Req2, HandlerState2, Error)
|
||||
{{error, _} = Error, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2, Error)
|
||||
end;
|
||||
{reply, Payload, Req2, HandlerState2}
|
||||
when is_list(Payload) ->
|
||||
case websocket_send_many(Payload, State) of
|
||||
ok ->
|
||||
NextState(State, Req2, HandlerState2, RemainingData);
|
||||
shutdown ->
|
||||
handler_terminate(State, Req2, HandlerState2,
|
||||
{ok, State2} ->
|
||||
NextState(State2, Req2, HandlerState2, RemainingData);
|
||||
{shutdown, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2,
|
||||
{normal, shutdown});
|
||||
{error, _} = Error ->
|
||||
handler_terminate(State, Req2, HandlerState2, Error)
|
||||
{{error, _} = Error, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2, Error)
|
||||
end;
|
||||
{reply, Payload, Req2, HandlerState2, hibernate}
|
||||
when is_list(Payload) ->
|
||||
case websocket_send_many(Payload, State) of
|
||||
ok ->
|
||||
NextState(State#state{hibernate=true},
|
||||
{ok, State2} ->
|
||||
NextState(State2#state{hibernate=true},
|
||||
Req2, HandlerState2, RemainingData);
|
||||
shutdown ->
|
||||
handler_terminate(State, Req2, HandlerState2,
|
||||
{shutdown, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2,
|
||||
{normal, shutdown});
|
||||
{error, _} = Error ->
|
||||
handler_terminate(State, Req2, HandlerState2, Error)
|
||||
{{error, _} = Error, State2} ->
|
||||
handler_terminate(State2, Req2, HandlerState2, Error)
|
||||
end;
|
||||
{shutdown, Req2, HandlerState2} ->
|
||||
websocket_close(State, Req2, HandlerState2, {normal, shutdown})
|
||||
|
@ -597,22 +660,36 @@ websocket_opcode(close) -> 8;
|
|||
websocket_opcode(ping) -> 9;
|
||||
websocket_opcode(pong) -> 10.
|
||||
|
||||
-spec websocket_deflate_frame(opcode(), binary(), #state{}) -> {binary(), <<_:3>>, #state{}}.
|
||||
websocket_deflate_frame(Opcode, Payload,
|
||||
State=#state{deflate_frame = DeflateFrame})
|
||||
when DeflateFrame =:= false orelse Opcode >= 8 ->
|
||||
{Payload, <<0:3>>, State};
|
||||
websocket_deflate_frame(_, Payload, State=#state{deflate_state = Deflate}) ->
|
||||
Deflated = iolist_to_binary(zlib:deflate(Deflate, Payload, sync)),
|
||||
DeflatedBodyLength = erlang:size(Deflated) - 4,
|
||||
Deflated1 = case Deflated of
|
||||
<<Body:DeflatedBodyLength/binary, 0:8, 0:8, 255:8, 255:8>> -> Body;
|
||||
_ -> Deflated
|
||||
end,
|
||||
{Deflated1, <<1:1, 0:2>>, State}.
|
||||
|
||||
-spec websocket_send(frame(), #state{})
|
||||
-> ok | shutdown | {error, atom()}.
|
||||
websocket_send(Type, #state{socket=Socket, transport=Transport})
|
||||
-> {ok, #state{}} | {shutdown, #state{}} | {{error, atom()}, #state{}}.
|
||||
websocket_send(Type, State=#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
|
||||
ok -> {shutdown, State};
|
||||
Error -> {Error, State}
|
||||
end;
|
||||
websocket_send(Type, #state{socket=Socket, transport=Transport})
|
||||
websocket_send(Type, State=#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 >>);
|
||||
{Transport:send(Socket, << 1:1, 0:3, Opcode:4, 0:8 >>), State};
|
||||
websocket_send({close, Payload}, State) ->
|
||||
websocket_send({close, 1000, Payload}, State);
|
||||
websocket_send({Type = close, StatusCode, Payload}, #state{
|
||||
websocket_send({Type = close, StatusCode, Payload}, State=#state{
|
||||
socket=Socket, transport=Transport}) ->
|
||||
Opcode = websocket_opcode(Type),
|
||||
Len = 2 + iolist_size(Payload),
|
||||
|
@ -621,9 +698,10 @@ websocket_send({Type = close, StatusCode, Payload}, #state{
|
|||
BinLen = payload_length_to_binary(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}) ->
|
||||
{shutdown, State};
|
||||
websocket_send({Type, Payload0}, State=#state{socket=Socket, transport=Transport}) ->
|
||||
Opcode = websocket_opcode(Type),
|
||||
{Payload, Rsv, State2} = websocket_deflate_frame(Opcode, iolist_to_binary(Payload0), State),
|
||||
Len = iolist_size(Payload),
|
||||
%% Control packets must not be > 125 in length.
|
||||
true = if Type =:= ping; Type =:= pong ->
|
||||
|
@ -632,18 +710,18 @@ websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) ->
|
|||
true
|
||||
end,
|
||||
BinLen = payload_length_to_binary(Len),
|
||||
Transport:send(Socket,
|
||||
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]).
|
||||
{Transport:send(Socket,
|
||||
[<< 1:1, Rsv/bits, Opcode:4, 0:1, BinLen/bits >>, Payload]), State2}.
|
||||
|
||||
-spec websocket_send_many([frame()], #state{})
|
||||
-> ok | shutdown | {error, atom()}.
|
||||
websocket_send_many([], _) ->
|
||||
ok;
|
||||
-> {ok, #state{}} | {shutdown, #state{}} | {{error, atom()}, #state{}}.
|
||||
websocket_send_many([], State) ->
|
||||
{ok, State};
|
||||
websocket_send_many([Frame|Tail], State) ->
|
||||
case websocket_send(Frame, State) of
|
||||
ok -> websocket_send_many(Tail, State);
|
||||
shutdown -> shutdown;
|
||||
Error -> Error
|
||||
{ok, State2} -> websocket_send_many(Tail, State2);
|
||||
{shutdown, State2} -> {shutdown, State2};
|
||||
{Error, State2} -> {Error, State2}
|
||||
end.
|
||||
|
||||
-spec websocket_close(#state{}, Req, any(),
|
||||
|
|
|
@ -30,6 +30,7 @@
|
|||
-export([ws8_init_shutdown/1]).
|
||||
-export([ws8_single_bytes/1]).
|
||||
-export([ws13/1]).
|
||||
-export([ws_deflate/1]).
|
||||
-export([ws_send_close/1]).
|
||||
-export([ws_send_close_payload/1]).
|
||||
-export([ws_send_many/1]).
|
||||
|
@ -51,6 +52,7 @@ groups() ->
|
|||
ws8_init_shutdown,
|
||||
ws8_single_bytes,
|
||||
ws13,
|
||||
ws_deflate,
|
||||
ws_send_close,
|
||||
ws_send_close_payload,
|
||||
ws_send_many,
|
||||
|
@ -76,7 +78,8 @@ end_per_suite(_Config) ->
|
|||
|
||||
init_per_group(ws, Config) ->
|
||||
cowboy:start_http(ws, 100, [{port, 0}], [
|
||||
{env, [{dispatch, init_dispatch()}]}
|
||||
{env, [{dispatch, init_dispatch()}]},
|
||||
{compress, true}
|
||||
]),
|
||||
Port = ranch:get_port(ws),
|
||||
[{port, Port}|Config].
|
||||
|
@ -309,6 +312,58 @@ ws13(Config) ->
|
|||
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
|
||||
ok.
|
||||
|
||||
ws_deflate(Config) ->
|
||||
{port, Port} = lists:keyfind(port, 1, Config),
|
||||
{ok, Socket} = gen_tcp:connect("localhost", Port,
|
||||
[binary, {active, false}, {packet, raw}]),
|
||||
ok = gen_tcp:send(Socket, [
|
||||
"GET /ws_echo HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Connection: Upgrade\r\n"
|
||||
"Upgrade: websocket\r\n"
|
||||
"Sec-WebSocket-Origin: http://localhost\r\n"
|
||||
"Sec-WebSocket-Version: 8\r\n"
|
||||
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
|
||||
"Sec-WebSocket-Extensions: x-webkit-deflate-frame\r\n"
|
||||
"\r\n"]),
|
||||
{ok, Handshake} = gen_tcp:recv(Socket, 0, 6000),
|
||||
{ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest}
|
||||
= erlang:decode_packet(http, Handshake, []),
|
||||
[Headers, <<>>] = websocket_headers(
|
||||
erlang:decode_packet(httph, Rest, []), []),
|
||||
{'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers),
|
||||
{'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers),
|
||||
{"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}
|
||||
= lists:keyfind("sec-websocket-accept", 1, Headers),
|
||||
{"sec-websocket-extensions", "x-webkit-deflate-frame"}
|
||||
= lists:keyfind("sec-websocket-extensions", 1, Headers),
|
||||
|
||||
% send uncompressed text frame containing the Hello string
|
||||
ok = gen_tcp:send(Socket, << 16#81, 16#85, 16#37, 16#fa, 16#21, 16#3d,
|
||||
16#7f, 16#9f, 16#4d, 16#51, 16#58 >>),
|
||||
% receive compressed text frame containing the Hello string
|
||||
{ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 7:7, 242, 72, 205, 201, 201, 7, 0 >>}
|
||||
= gen_tcp:recv(Socket, 0, 6000),
|
||||
|
||||
% send uncompressed text frame containing the HelloHello string
|
||||
% as 2 separate fragments
|
||||
ok = gen_tcp:send(Socket, [
|
||||
<< 0:1, 0:3, 1:4, 1:1, 5:7 >>,
|
||||
<< 16#37 >>, << 16#fa >>, << 16#21 >>, << 16#3d >>, << 16#7f >>,
|
||||
<< 16#9f >>, << 16#4d >>, << 16#51 >>, << 16#58 >>]),
|
||||
ok = gen_tcp:send(Socket, [
|
||||
<< 1:1, 0:3, 0:4, 1:1, 5:7 >>,
|
||||
<< 16#37 >>, << 16#fa >>, << 16#21 >>, << 16#3d >>, << 16#7f >>,
|
||||
<< 16#9f >>, << 16#4d >>, << 16#51 >>, << 16#58 >>]),
|
||||
% receive compressed text frame containing the HelloHello string
|
||||
{ok, << 1:1, 1:1, 0:2, 1:4, 0:1, 5:7, 242, 128, 19, 0, 0 >>}
|
||||
= gen_tcp:recv(Socket, 0, 6000),
|
||||
|
||||
ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 1:1, 0:7, 0:32 >>), %% close
|
||||
{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),
|
||||
{error, closed} = gen_tcp:recv(Socket, 0, 6000),
|
||||
ok.
|
||||
|
||||
ws_send_close(Config) ->
|
||||
{port, Port} = lists:keyfind(port, 1, Config),
|
||||
{ok, Socket} = gen_tcp:connect("localhost", Port,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue