mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-16 05:00:24 +00:00
Add deflate options for Websocket compression
They allow the server to configure what it is willing to accept for both the negotiated configuration (takeover and window bits) and the other zlib options (level, mem_level and strategy). This can be used to reduce the memory and/or CPU footprint of the compressed data, which comes with a cost in compression ratio.
This commit is contained in:
parent
fe1ee080de
commit
8164b50453
4 changed files with 220 additions and 50 deletions
|
@ -66,6 +66,7 @@
|
|||
|
||||
-type opts() :: #{
|
||||
compress => boolean(),
|
||||
deflate_opts => cow_ws:deflate_opts(),
|
||||
idle_timeout => timeout(),
|
||||
max_frame_size => non_neg_integer() | infinity,
|
||||
req_filter => fun((cowboy_req:req()) -> map())
|
||||
|
@ -77,13 +78,11 @@
|
|||
ref :: ranch:ref(),
|
||||
socket = undefined :: inet:socket() | {pid(), cowboy_stream:streamid()} | undefined,
|
||||
transport = undefined :: module() | undefined,
|
||||
opts = #{} :: opts(),
|
||||
active = true :: boolean(),
|
||||
handler :: module(),
|
||||
key = undefined :: undefined | binary(),
|
||||
timeout = infinity :: timeout(),
|
||||
timeout_ref = undefined :: undefined | reference(),
|
||||
compress = false :: boolean(),
|
||||
max_frame_size :: non_neg_integer() | infinity,
|
||||
messages = undefined :: undefined | {atom(), atom(), atom()},
|
||||
hibernate = false :: boolean(),
|
||||
frag_state = undefined :: cow_ws:frag_state(),
|
||||
|
@ -125,15 +124,11 @@ upgrade(Req, Env, Handler, HandlerState) ->
|
|||
when Req::cowboy_req:req(), Env::cowboy_middleware:env().
|
||||
%% @todo Immediately crash if a response has already been sent.
|
||||
upgrade(Req0=#{version := Version}, Env, Handler, HandlerState, Opts) ->
|
||||
Timeout = maps:get(idle_timeout, Opts, 60000),
|
||||
MaxFrameSize = maps:get(max_frame_size, Opts, infinity),
|
||||
Compress = maps:get(compress, Opts, false),
|
||||
FilteredReq = case maps:get(req_filter, Opts, undefined) of
|
||||
undefined -> maps:with([method, version, scheme, host, port, path, qs, peer], Req0);
|
||||
FilterFun -> FilterFun(Req0)
|
||||
end,
|
||||
State0 = #state{handler=Handler, timeout=Timeout, compress=Compress,
|
||||
max_frame_size=MaxFrameSize, req=FilteredReq},
|
||||
State0 = #state{opts=Opts, handler=Handler, req=FilteredReq},
|
||||
try websocket_upgrade(State0, Req0) of
|
||||
{ok, State, Req} ->
|
||||
websocket_handshake(State, Req, HandlerState, Env);
|
||||
|
@ -174,13 +169,14 @@ websocket_version(State, Req) ->
|
|||
end,
|
||||
websocket_extensions(State, Req#{websocket_version => WsVersion}).
|
||||
|
||||
websocket_extensions(State=#state{compress=Compress}, Req) ->
|
||||
websocket_extensions(State=#state{opts=Opts}, Req) ->
|
||||
%% @todo We want different options for this. For example
|
||||
%% * compress everything auto
|
||||
%% * compress only text auto
|
||||
%% * compress only binary auto
|
||||
%% * compress nothing auto (but still enabled it)
|
||||
%% * disable compression
|
||||
Compress = maps:get(compress, Opts, false),
|
||||
case {Compress, cowboy_req:parse_header(<<"sec-websocket-extensions">>, Req)} of
|
||||
{true, Extensions} when Extensions =/= undefined ->
|
||||
websocket_extensions(State, Req, Extensions, []);
|
||||
|
@ -193,15 +189,15 @@ websocket_extensions(State, Req, [], []) ->
|
|||
websocket_extensions(State, Req, [], [<<", ">>|RespHeader]) ->
|
||||
{ok, State, cowboy_req:set_resp_header(<<"sec-websocket-extensions">>, lists:reverse(RespHeader), Req)};
|
||||
%% For HTTP/2 we ARE on the controlling process and do NOT want to update the owner.
|
||||
websocket_extensions(State=#state{extensions=Extensions}, Req=#{pid := Pid, version := Version},
|
||||
websocket_extensions(State=#state{opts=Opts, extensions=Extensions},
|
||||
Req=#{pid := Pid, version := Version},
|
||||
[{<<"permessage-deflate">>, Params}|Tail], RespHeader) ->
|
||||
%% @todo Make deflate options configurable.
|
||||
Opts0 = #{level => best_compression, mem_level => 8, strategy => default},
|
||||
Opts = case Version of
|
||||
'HTTP/1.1' -> Opts0#{owner => Pid};
|
||||
_ -> Opts0
|
||||
DeflateOpts0 = maps:get(deflate_opts, Opts, #{}),
|
||||
DeflateOpts = case Version of
|
||||
'HTTP/1.1' -> DeflateOpts0#{owner => Pid};
|
||||
_ -> DeflateOpts0
|
||||
end,
|
||||
try cow_ws:negotiate_permessage_deflate(Params, Extensions, Opts) of
|
||||
try cow_ws:negotiate_permessage_deflate(Params, Extensions, DeflateOpts) of
|
||||
{ok, RespExt, Extensions2} ->
|
||||
websocket_extensions(State#state{extensions=Extensions2},
|
||||
Req, Tail, [<<", ">>, RespExt|RespHeader]);
|
||||
|
@ -210,15 +206,15 @@ websocket_extensions(State=#state{extensions=Extensions}, Req=#{pid := Pid, vers
|
|||
catch exit:{error, incompatible_zlib_version, _} ->
|
||||
websocket_extensions(State, Req, Tail, RespHeader)
|
||||
end;
|
||||
websocket_extensions(State=#state{extensions=Extensions}, Req=#{pid := Pid, version := Version},
|
||||
websocket_extensions(State=#state{opts=Opts, extensions=Extensions},
|
||||
Req=#{pid := Pid, version := Version},
|
||||
[{<<"x-webkit-deflate-frame">>, Params}|Tail], RespHeader) ->
|
||||
%% @todo Make deflate options configurable.
|
||||
Opts0 = #{level => best_compression, mem_level => 8, strategy => default},
|
||||
Opts = case Version of
|
||||
'HTTP/1.1' -> Opts0#{owner => Pid};
|
||||
_ -> Opts0
|
||||
DeflateOpts0 = maps:get(deflate_opts, Opts, #{}),
|
||||
DeflateOpts = case Version of
|
||||
'HTTP/1.1' -> DeflateOpts0#{owner => Pid};
|
||||
_ -> DeflateOpts0
|
||||
end,
|
||||
try cow_ws:negotiate_x_webkit_deflate_frame(Params, Extensions, Opts) of
|
||||
try cow_ws:negotiate_x_webkit_deflate_frame(Params, Extensions, DeflateOpts) of
|
||||
{ok, RespExt, Extensions2} ->
|
||||
websocket_extensions(State#state{extensions=Extensions2},
|
||||
Req, Tail, [<<", ">>, RespExt|RespHeader]);
|
||||
|
@ -317,13 +313,18 @@ before_loop(State=#state{socket=Socket, transport=Transport},
|
|||
loop(State, HandlerState, ParseState).
|
||||
|
||||
-spec loop_timeout(#state{}) -> #state{}.
|
||||
loop_timeout(State=#state{timeout=infinity}) ->
|
||||
State#state{timeout_ref=undefined};
|
||||
loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
|
||||
_ = case PrevRef of undefined -> ignore; PrevRef ->
|
||||
erlang:cancel_timer(PrevRef) end,
|
||||
TRef = erlang:start_timer(Timeout, self(), ?MODULE),
|
||||
State#state{timeout_ref=TRef}.
|
||||
loop_timeout(State=#state{opts=Opts, timeout_ref=PrevRef}) ->
|
||||
_ = case PrevRef of
|
||||
undefined -> ignore;
|
||||
PrevRef -> erlang:cancel_timer(PrevRef)
|
||||
end,
|
||||
case maps:get(idle_timeout, Opts, 60000) of
|
||||
infinity ->
|
||||
State#state{timeout_ref=undefined};
|
||||
Timeout ->
|
||||
TRef = erlang:start_timer(Timeout, self(), ?MODULE),
|
||||
State#state{timeout_ref=TRef}
|
||||
end.
|
||||
|
||||
-spec loop(#state{}, any(), parse_state()) -> no_return().
|
||||
loop(State=#state{parent=Parent, socket=Socket, messages=Messages,
|
||||
|
@ -377,9 +378,9 @@ parse(State, HandlerState, PS=#ps_payload{buffer=Buffer}, Data) ->
|
|||
parse_payload(State, HandlerState, PS#ps_payload{buffer= <<>>},
|
||||
<<Buffer/binary, Data/binary>>).
|
||||
|
||||
parse_header(State=#state{max_frame_size=MaxFrameSize,
|
||||
frag_state=FragState, extensions=Extensions},
|
||||
parse_header(State=#state{opts=Opts, frag_state=FragState, extensions=Extensions},
|
||||
HandlerState, ParseState=#ps_header{buffer=Data}) ->
|
||||
MaxFrameSize = maps:get(max_frame_size, Opts, infinity),
|
||||
case cow_ws:parse_header(Data, Extensions, FragState) of
|
||||
%% All frames sent from the client to the server are masked.
|
||||
{_, _, _, _, undefined, _} ->
|
||||
|
@ -423,10 +424,11 @@ parse_payload(State=#state{frag_state=FragState, utf8_state=Incomplete, extensio
|
|||
websocket_close(State, HandlerState, Error)
|
||||
end.
|
||||
|
||||
dispatch_frame(State=#state{max_frame_size=MaxFrameSize, frag_state=FragState,
|
||||
dispatch_frame(State=#state{opts=Opts, frag_state=FragState,
|
||||
frag_buffer=SoFar, extensions=Extensions}, HandlerState,
|
||||
#ps_payload{type=Type0, unmasked=Payload0, close_code=CloseCode0},
|
||||
RemainingData) ->
|
||||
MaxFrameSize = maps:get(max_frame_size, Opts, infinity),
|
||||
case cow_ws:make_frame(Type0, Payload0, CloseCode0, FragState) of
|
||||
%% @todo Allow receiving fragments.
|
||||
{fragment, _, _, Payload} when byte_size(Payload) + byte_size(SoFar) > MaxFrameSize ->
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue