Просмотр исходного кода

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 лет назад
Родитель
Сommit
f760f979b4
1 измененных файлов с 123 добавлено и 33 удалено
  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;