Browse Source

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).
Loïc Hoguin 6 years ago
parent
commit
f760f979b4
1 changed files with 123 additions and 33 deletions
  1. 123 33
      src/cow_ws.erl

+ 123 - 33
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;