aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2018-04-23 16:02:23 +0200
committerLoïc Hoguin <[email protected]>2018-04-23 16:02:23 +0200
commitdb8c905ec08ca2afd07e50217868a1d84f555665 (patch)
tree354028282119690b98e5ea4c12328656476ec1df
parentab37df9d5273ea8541dbc08f2bd0d00e1b83479a (diff)
downloadcowlib-db8c905ec08ca2afd07e50217868a1d84f555665.tar.gz
cowlib-db8c905ec08ca2afd07e50217868a1d84f555665.tar.bz2
cowlib-db8c905ec08ca2afd07e50217868a1d84f555665.zip
Add proper support for table size updates
-rw-r--r--src/cow_hpack.erl263
1 files changed, 246 insertions, 17 deletions
diff --git a/src/cow_hpack.erl b/src/cow_hpack.erl
index 694acfb..9583926 100644
--- a/src/cow_hpack.erl
+++ b/src/cow_hpack.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2015, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2015-2018, Loïc Hoguin <[email protected]>
%%
%% 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">>},