|
@@ -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">>},
|