From f760f979b48e6d8991be10499d9dd7217c98060b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 12 Nov 2018 18:08:28 +0100 Subject: Add deflate options for Websocket compression They allow the server to configure what it is willing to accept for the negotiated configuration (takeover and window bits). --- src/cow_ws.erl | 156 +++++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 123 insertions(+), 33 deletions(-) diff --git a/src/cow_ws.erl b/src/cow_ws.erl index bac8450..1300a65 100644 --- a/src/cow_ws.erl +++ b/src/cow_ws.erl @@ -35,6 +35,22 @@ -type extensions() :: map(). -export_type([extensions/0]). +-type deflate_opts() :: #{ + %% Compression parameters. + level => zlib:zlevel(), + mem_level => zlib:zmemlevel(), + strategy => zlib:zstrategy(), + + %% Whether the compression context will carry over between frames. + server_context_takeover => takeover | no_takeover, + client_context_takeover => takeover | no_takeover, + + %% LZ77 sliding window size limits. + server_max_window_bits => 8..15, + client_max_window_bits => 8..15 +}. +-export_type([deflate_opts/0]). + -type frag_state() :: undefined | {fin | nofin, text | binary, rsv()}. -export_type([frag_state/0]). @@ -70,6 +86,9 @@ encode_key(Key) -> %% @doc Negotiate the permessage-deflate extension. +-spec negotiate_permessage_deflate( + [binary() | {binary(), binary()}], Exts, deflate_opts()) + -> ignore | {ok, iolist(), Exts} when Exts::extensions(). %% Ignore if deflate already negotiated. negotiate_permessage_deflate(_, #{deflate := _}, _) -> ignore; @@ -79,48 +98,117 @@ negotiate_permessage_deflate(Params, Extensions, Opts) -> Params2 when length(Params) =/= length(Params2) -> ignore; Params2 -> - %% @todo Might want to make these configurable defaults. - case parse_request_permessage_deflate_params(Params2, 15, takeover, 15, takeover, []) of - ignore -> - ignore; - {ClientWindowBits, ClientTakeOver, ServerWindowBits, ServerTakeOver, RespParams} -> - {Inflate, Deflate} = init_permessage_deflate(ClientWindowBits, ServerWindowBits, Opts), - {ok, [<<"permessage-deflate">>, RespParams], - Extensions#{ - deflate => Deflate, - deflate_takeover => ServerTakeOver, - inflate => Inflate, - inflate_takeover => ClientTakeOver}} - end + negotiate_permessage_deflate1(Params2, Extensions, Opts) end. -parse_request_permessage_deflate_params([], CB, CTO, SB, STO, RespParams) -> - {CB, CTO, SB, STO, RespParams}; -parse_request_permessage_deflate_params([<<"client_max_window_bits">>|Tail], CB, CTO, SB, STO, RespParams) -> - parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, - [<<"; ">>, <<"client_max_window_bits=">>, integer_to_binary(CB)|RespParams]); -parse_request_permessage_deflate_params([{<<"client_max_window_bits">>, Max}|Tail], _, CTO, SB, STO, RespParams) -> +negotiate_permessage_deflate1(Params, Extensions, Opts) -> + %% We are allowed to send back no_takeover even if the client + %% accepts takeover. Therefore we use no_takeover if any of + %% the inputs have it. + ServerTakeover = maps:get(server_context_takeover, Opts, takeover), + ClientTakeover = maps:get(client_context_takeover, Opts, takeover), + %% We can send back window bits smaller than or equal to what + %% the client sends us. + ServerMaxWindowBits = maps:get(server_max_window_bits, Opts, 15), + ClientMaxWindowBits = maps:get(client_max_window_bits, Opts, 15), + %% We may need to send back no_context_takeover depending on configuration. + RespParams0 = case ServerTakeover of + takeover -> []; + no_takeover -> [<<"; server_no_context_takeover">>] + end, + RespParams1 = case ClientTakeover of + takeover -> RespParams0; + no_takeover -> [<<"; client_no_context_takeover">>|RespParams0] + end, + Negotiated0 = #{ + server_context_takeover => ServerTakeover, + client_context_takeover => ClientTakeover, + server_max_window_bits => ServerMaxWindowBits, + client_max_window_bits => ClientMaxWindowBits + }, + case negotiate_params(Params, Negotiated0, RespParams1) of + ignore -> + ignore; + {#{server_max_window_bits := SB}, _} when SB > ServerMaxWindowBits -> + ignore; + {#{client_max_window_bits := CB}, _} when CB > ClientMaxWindowBits -> + ignore; + {Negotiated, RespParams2} -> + %% We add the configured max window bits if necessary. + RespParams = case Negotiated of + #{server_max_window_bits_set := true} -> RespParams2; + _ when ServerMaxWindowBits =:= 15 -> RespParams2; + _ -> [<<"; server_max_window_bits=">>, + integer_to_binary(ServerMaxWindowBits)|RespParams2] + end, + {Inflate, Deflate} = init_permessage_deflate( + maps:get(client_max_window_bits, Negotiated), + maps:get(server_max_window_bits, Negotiated), Opts), + {ok, [<<"permessage-deflate">>, RespParams], Extensions#{ + deflate => Deflate, + deflate_takeover => maps:get(server_context_takeover, Negotiated), + inflate => Inflate, + inflate_takeover => maps:get(client_context_takeover, Negotiated)}} + end. + +negotiate_params([], Negotiated, RespParams) -> + {Negotiated, RespParams}; +%% We must only send the client_max_window_bits parameter if the +%% request explicitly indicated the client supports it. +negotiate_params([<<"client_max_window_bits">>|Tail], Negotiated, RespParams) -> + CB = maps:get(client_max_window_bits, Negotiated), + negotiate_params(Tail, Negotiated#{client_max_window_bits_set => true}, + [<<"; client_max_window_bits=">>, integer_to_binary(CB)|RespParams]); +negotiate_params([{<<"client_max_window_bits">>, Max}|Tail], Negotiated, RespParams) -> + CB0 = maps:get(client_max_window_bits, Negotiated, undefined), case parse_max_window_bits(Max) of error -> ignore; - CB -> - parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, - [<<"; ">>, <<"client_max_window_bits=">>, Max|RespParams]) + CB when CB =< CB0 -> + negotiate_params(Tail, Negotiated#{client_max_window_bits => CB}, + [<<"; client_max_window_bits=">>, Max|RespParams]); + %% When the client sends window bits larger than the server wants + %% to use, we use what the server defined. + _ -> + negotiate_params(Tail, Negotiated, + [<<"; client_max_window_bits=">>, integer_to_binary(CB0)|RespParams]) end; -parse_request_permessage_deflate_params([<<"client_no_context_takeover">>|Tail], CB, _, SB, STO, RespParams) -> - parse_request_permessage_deflate_params(Tail, CB, no_takeover, SB, STO, [<<"; ">>, <<"client_no_context_takeover">>|RespParams]); -parse_request_permessage_deflate_params([{<<"server_max_window_bits">>, Max}|Tail], CB, CTO, _, STO, RespParams) -> +negotiate_params([{<<"server_max_window_bits">>, Max}|Tail], Negotiated, RespParams) -> + SB0 = maps:get(server_max_window_bits, Negotiated, undefined), case parse_max_window_bits(Max) of error -> ignore; - SB -> - parse_request_permessage_deflate_params(Tail, CB, CTO, SB, STO, - [<<"; ">>, <<"server_max_window_bits=">>, Max|RespParams]) + SB when SB =< SB0 -> + negotiate_params(Tail, Negotiated#{ + server_max_window_bits => SB, + server_max_window_bits_set => true}, + [<<"; server_max_window_bits=">>, Max|RespParams]); + %% When the client sends window bits larger than the server wants + %% to use, we use what the server defined. The parameter will be + %% set only when this function returns. + _ -> + negotiate_params(Tail, Negotiated, RespParams) + end; +%% We only need to send the no_context_takeover parameter back +%% here if we didn't already define it via configuration. +negotiate_params([<<"client_no_context_takeover">>|Tail], Negotiated, RespParams) -> + case maps:get(client_context_takeover, Negotiated) of + no_takeover -> + negotiate_params(Tail, Negotiated, RespParams); + takeover -> + negotiate_params(Tail, Negotiated#{client_context_takeover => no_takeover}, + [<<"; client_no_context_takeover">>|RespParams]) + end; +negotiate_params([<<"server_no_context_takeover">>|Tail], Negotiated, RespParams) -> + case maps:get(server_context_takeover, Negotiated) of + no_takeover -> + negotiate_params(Tail, Negotiated, RespParams); + takeover -> + negotiate_params(Tail, Negotiated#{server_context_takeover => no_takeover}, + [<<"; server_no_context_takeover">>|RespParams]) end; -parse_request_permessage_deflate_params([<<"server_no_context_takeover">>|Tail], CB, CTO, SB, _, RespParams) -> - parse_request_permessage_deflate_params(Tail, CB, CTO, SB, no_takeover, [<<"; ">>, <<"server_no_context_takeover">>|RespParams]); %% Ignore if unknown parameter; ignore if parameter with invalid or missing value. -parse_request_permessage_deflate_params(_, _, _, _, _, _) -> +negotiate_params(_, _, _) -> ignore. parse_max_window_bits(<<"8">>) -> 8; @@ -133,7 +221,7 @@ parse_max_window_bits(<<"14">>) -> 14; parse_max_window_bits(<<"15">>) -> 15; parse_max_window_bits(_) -> error. -% A negative WindowBits value indicates that zlib headers are not used. +%% A negative WindowBits value indicates that zlib headers are not used. init_permessage_deflate(InflateWindowBits, DeflateWindowBits, Opts) -> Inflate = zlib:open(), ok = zlib:inflateInit(Inflate, -InflateWindowBits), @@ -194,6 +282,9 @@ set_owner(Pid, Inflate, Deflate) -> %% The implementation is very basic and none of the parameters %% are currently supported. +-spec negotiate_x_webkit_deflate_frame( + [binary() | {binary(), binary()}], Exts, deflate_opts()) + -> ignore | {ok, binary(), Exts} when Exts::extensions(). negotiate_x_webkit_deflate_frame(_, #{deflate := _}, _) -> ignore; negotiate_x_webkit_deflate_frame(_Params, Extensions, Opts) -> @@ -219,7 +310,6 @@ validate_permessage_deflate(Params, Extensions, Opts) -> Params2 when length(Params) =/= length(Params2) -> error; Params2 -> - %% @todo Might want to make some of these configurable defaults if at all possible. case parse_response_permessage_deflate_params(Params2, 15, takeover, 15, takeover) of error -> error; -- cgit v1.2.3