Browse Source

Add proper support for table size updates

Loïc Hoguin 7 years ago
parent
commit
db8c905ec0
1 changed files with 246 additions and 17 deletions
  1. 246 17
      src/cow_hpack.erl

+ 246 - 17
src/cow_hpack.erl

@@ -1,4 +1,4 @@
-%% Copyright (c) 2015, Loïc Hoguin <essen@ninenines.eu>
+%% Copyright (c) 2015-2018, Loïc Hoguin <essen@ninenines.eu>
 %%
 %% Permission to use, copy, modify, and/or distribute this software for any
 %% purpose with or without fee is hereby granted, provided that the above
@@ -20,6 +20,7 @@
 
 -export([init/0]).
 -export([init/1]).
+-export([set_max_size/2]).
 
 -export([decode/1]).
 -export([decode/2]).
@@ -32,6 +33,7 @@
 -record(state, {
 	size = 0 :: non_neg_integer(),
 	max_size = 4096 :: non_neg_integer(),
+	configured_max_size = 4096 :: non_neg_integer(),
 	dyn_table = [] :: [{pos_integer(), {binary(), binary()}}]
 }).
 
@@ -53,7 +55,24 @@ init() ->
 
 -spec init(non_neg_integer()) -> state().
 init(MaxSize) ->
-	#state{max_size=MaxSize}.
+	#state{max_size=MaxSize, configured_max_size=MaxSize}.
+
+%% Update the configured max size.
+%%
+%% When decoding, the local endpoint also needs to send a SETTINGS
+%% frame with this value and it is then up to the remote endpoint
+%% to decide what actual limit it will use. The actual limit is
+%% signaled via dynamic table size updates in the encoded data.
+%%
+%% When encoding, the local endpoint will call this function after
+%% receiving a SETTINGS frame with this value. The encoder will
+%% then use this value as the new max after signaling via a dynamic
+%% table size update. The value given as argument may be lower
+%% than the one received in the SETTINGS.
+
+-spec set_max_size(non_neg_integer(), State) -> State when State::state().
+set_max_size(MaxSize, State) ->
+	State#state{configured_max_size=MaxSize}.
 
 %% Decoding.
 
@@ -66,6 +85,14 @@ decode(Data, State) ->
 	decode(Data, State, #{}).
 
 -spec decode(binary(), State, opts()) -> {cow_http:headers(), State} when State::state().
+%% Dynamic table size update is only allowed at the beginning of a HEADERS block.
+decode(<< 0:2, 1:1, Rest/bits >>, State=#state{configured_max_size=ConfigMaxSize}, Opts) ->
+	{MaxSize, Rest2} = dec_int5(Rest),
+	if
+		MaxSize =< ConfigMaxSize ->
+			State2 = table_update_size(MaxSize, State),
+			decode(Rest2, State2, Opts)
+	end;
 decode(Data, State, Opts) ->
 	decode(Data, State, Opts, []).
 
@@ -93,10 +120,7 @@ decode(<< 0:3, 1:1, 0:4, Rest/bits >>, State, Opts, Acc) ->
 %% Literal header field never indexed: indexed name.
 %% @todo Keep track of "never indexed" headers.
 decode(<< 0:3, 1:1, Rest/bits >>, State, Opts, Acc) ->
-	dec_lit_no_index_indexed_name(Rest, State, Opts, Acc);
-%% Dynamic table size update.
-decode(<< 0:2, 1:1, Rest/bits >>, State, Opts, Acc) ->
-	dec_table_size_update(Rest, State, Opts, Acc).
+	dec_lit_no_index_indexed_name(Rest, State, Opts, Acc).
 
 %% Indexed header field representation.
 
@@ -138,13 +162,6 @@ dec_lit_no_index(Rest, State, Opts, Acc, Name) ->
 
 %% @todo Literal header field never indexed.
 
-%% Dynamic table size update.
-
-dec_table_size_update(Rest, State, Opts, Acc) ->
-	{MaxSize, Rest2} = dec_int5(Rest),
-	State2 = table_update_size(MaxSize, State),
-	decode(Rest2, State2, Opts, Acc).
-
 %% Decode an integer.
 
 %% The HPACK format has 4 different integer prefixes length (from 4 to 7)
@@ -544,6 +561,157 @@ resp_decode_test() ->
 		{52,{<<"content-encoding">>, <<"gzip">>}},
 		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:22 GMT">>}}]} = State3,
 	ok.
+
+table_update_decode_test() ->
+	%% Use a max_size of 256 to trigger header evictions
+	%% when the code is not updating the max size.
+	State0 = init(256),
+	%% First response (raw then huffman).
+	{Headers1, State1} = decode(<< 16#4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d:560 >>, State0),
+	{Headers1, State1} = decode(<< 16#488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3:432 >>, State0),
+	Headers1 = [
+		{<<":status">>, <<"302">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State1,
+	%% Set a new configured max_size to avoid header evictions.
+	State2 = set_max_size(512, State1),
+	%% Second response with the table size update (raw then huffman).
+	MaxSize = enc_big_int(512 - 31, []),
+	{Headers2, State3} = decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4803333037c1c0bf:64>>]),
+		State2),
+	{Headers2, State3} = decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4883640effc1c0bf:64>>]),
+		State2),
+	Headers2 = [
+		{<<":status">>, <<"307">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=264, dyn_table=[
+		{42,{<<":status">>, <<"307">>}},
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State3,
+	ok.
+
+table_update_decode_smaller_test() ->
+	%% Use a max_size of 256 to trigger header evictions
+	%% when the code is not updating the max size.
+	State0 = init(256),
+	%% First response (raw then huffman).
+	{Headers1, State1} = decode(<< 16#4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d:560 >>, State0),
+	{Headers1, State1} = decode(<< 16#488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3:432 >>, State0),
+	Headers1 = [
+		{<<":status">>, <<"302">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State1,
+	%% Set a new configured max_size to avoid header evictions.
+	State2 = set_max_size(512, State1),
+	%% Second response with the table size update smaller than the limit (raw then huffman).
+	MaxSize = enc_big_int(400 - 31, []),
+	{Headers2, State3} = decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4803333037c1c0bf:64>>]),
+		State2),
+	{Headers2, State3} = decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4883640effc1c0bf:64>>]),
+		State2),
+	Headers2 = [
+		{<<":status">>, <<"307">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=264, dyn_table=[
+		{42,{<<":status">>, <<"307">>}},
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State3,
+	ok.
+
+table_update_decode_too_large_test() ->
+	%% Use a max_size of 256 to trigger header evictions
+	%% when the code is not updating the max size.
+	State0 = init(256),
+	%% First response (raw then huffman).
+	{Headers1, State1} = decode(<< 16#4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d:560 >>, State0),
+	{Headers1, State1} = decode(<< 16#488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3:432 >>, State0),
+	Headers1 = [
+		{<<":status">>, <<"302">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State1,
+	%% Set a new configured max_size to avoid header evictions.
+	State2 = set_max_size(512, State1),
+	%% Second response with the table size update (raw then huffman).
+	MaxSize = enc_big_int(1024 - 31, []),
+	{'EXIT', _} = (catch decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4803333037c1c0bf:64>>]),
+		State2)),
+	{'EXIT', _} = (catch decode(
+		iolist_to_binary([<< 2#00111111>>, MaxSize, <<16#4883640effc1c0bf:64>>]),
+		State2)),
+	ok.
+
+table_update_decode_zero_test() ->
+	State0 = init(256),
+	%% First response (raw then huffman).
+	{Headers1, State1} = decode(<< 16#4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d:560 >>, State0),
+	{Headers1, State1} = decode(<< 16#488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3:432 >>, State0),
+	Headers1 = [
+		{<<":status">>, <<"302">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State1,
+	%% Set a new configured max_size to avoid header evictions.
+	State2 = set_max_size(512, State1),
+	%% Second response with the table size update (raw then huffman).
+	%% We set the table size to 0 to evict all values before setting
+	%% it to 512 so we only get the second request indexed.
+	MaxSize = enc_big_int(512 - 31, []),
+	{Headers1, State3} = decode(iolist_to_binary([
+		<<2#00100000, 2#00111111>>, MaxSize,
+		<<16#4803333032580770726976617465611d4d6f6e2c203231204f637420323031332032303a31333a323120474d546e1768747470733a2f2f7777772e6578616d706c652e636f6d:560>>]),
+		State2),
+	{Headers1, State3} = decode(iolist_to_binary([
+		<<2#00100000, 2#00111111>>, MaxSize,
+		<<16#488264025885aec3771a4b6196d07abe941054d444a8200595040b8166e082a62d1bff6e919d29ad171863c78f0b97c8e9ae82ae43d3:432>>]),
+		State2),
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = State3,
+	ok.
 -endif.
 
 %% Encoding.
@@ -553,12 +721,18 @@ encode(Headers) ->
 	encode(Headers, init(), #{}, []).
 
 -spec encode(cow_http:headers(), State) -> {iodata(), State} when State::state().
-encode(Headers, State) ->
-	encode(Headers, State, #{}, []).
+encode(Headers, State=#state{max_size=MaxSize, configured_max_size=MaxSize}) ->
+	encode(Headers, State, #{}, []);
+encode(Headers, State0=#state{configured_max_size=MaxSize}) ->
+	{Data, State} = encode(Headers, State0#state{max_size=MaxSize}, #{}, []),
+	{[enc_int5(MaxSize, 2#001), Data], State}.
 
 -spec encode(cow_http:headers(), State, opts()) -> {iodata(), State} when State::state().
-encode(Headers, State, Opts) ->
-	encode(Headers, State, Opts, []).
+encode(Headers, State=#state{max_size=MaxSize, configured_max_size=MaxSize}, Opts) ->
+	encode(Headers, State, Opts, []);
+encode(Headers, State0=#state{configured_max_size=MaxSize}, Opts) ->
+	{Data, State} = encode(Headers, State0#state{max_size=MaxSize}, Opts, []),
+	{[enc_int5(MaxSize, 2#001), Data], State}.
 
 %% @todo Handle cases where no/never indexing is expected.
 encode([], State, _, Acc) ->
@@ -582,6 +756,11 @@ encode([_Header0 = {Name, Value0}|Tail], State, Opts, Acc) ->
 
 %% Encode an integer.
 
+enc_int5(Int, Prefix) when Int < 31 ->
+	<< Prefix:3, Int:5 >>;
+enc_int5(Int, Prefix) ->
+	[<< Prefix:3, 2#11111:5 >>|enc_big_int(Int - 31, [])].
+
 enc_int6(Int, Prefix) when Int < 63 ->
 	<< Prefix:2, Int:6 >>;
 enc_int6(Int, Prefix) ->
@@ -977,6 +1156,56 @@ resp_encode_test() ->
 		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:22 GMT">>}}]} = State3,
 	ok.
 
+%% This test assumes that table updates work correctly when decoding.
+table_update_encode_test() ->
+	%% Use a max_size of 256 to trigger header evictions
+	%% when the code is not updating the max size.
+	DecState0 = EncState0 = init(256),
+	%% First response.
+	Headers1 = [
+		{<<":status">>, <<"302">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	{Encoded1, EncState1} = encode(Headers1, EncState0),
+	{Headers1, DecState1} = decode(iolist_to_binary(Encoded1), DecState0),
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = DecState1,
+	#state{size=222, dyn_table=[
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = EncState1,
+	%% Set a new configured max_size to avoid header evictions.
+	DecState2 = set_max_size(512, DecState1),
+	EncState2 = set_max_size(512, EncState1),
+	%% Second response.
+	Headers2 = [
+		{<<":status">>, <<"307">>},
+		{<<"cache-control">>, <<"private">>},
+		{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>},
+		{<<"location">>, <<"https://www.example.com">>}
+	],
+	{Encoded2, EncState3} = encode(Headers2, EncState2),
+	{Headers2, DecState3} = decode(iolist_to_binary(Encoded2), DecState2),
+	#state{size=264, max_size=512, dyn_table=[
+		{42,{<<":status">>, <<"307">>}},
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = DecState3,
+	#state{size=264, max_size=512, dyn_table=[
+		{42,{<<":status">>, <<"307">>}},
+		{63,{<<"location">>, <<"https://www.example.com">>}},
+		{65,{<<"date">>, <<"Mon, 21 Oct 2013 20:13:21 GMT">>}},
+		{52,{<<"cache-control">>, <<"private">>}},
+		{42,{<<":status">>, <<"302">>}}]} = EncState3,
+	ok.
+
 encode_iolist_test() ->
 	Headers = [
 		{<<":method">>, <<"GET">>},