aboutsummaryrefslogtreecommitdiffstats
path: root/src/cow_ws.erl
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-11-12 18:08:28 +0100
committerLoïc Hoguin <[email protected]>2018-11-12 18:08:28 +0100
commitf760f979b48e6d8991be10499d9dd7217c98060b (patch)
tree62c97e2e7df832bb828181c03081037cd9b25335 /src/cow_ws.erl
parent664ada1c19c277bcab250c1d38d579672c02a628 (diff)
downloadcowlib-f760f979b48e6d8991be10499d9dd7217c98060b.tar.gz
cowlib-f760f979b48e6d8991be10499d9dd7217c98060b.tar.bz2
cowlib-f760f979b48e6d8991be10499d9dd7217c98060b.zip
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).
Diffstat (limited to 'src/cow_ws.erl')
-rw-r--r--src/cow_ws.erl156
1 files 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;