From db8c905ec08ca2afd07e50217868a1d84f555665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 23 Apr 2018 16:02:23 +0200 Subject: Add proper support for table size updates --- src/cow_hpack.erl | 263 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file 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 +%% Copyright (c) 2015-2018, Loïc Hoguin %% %% 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">>}, -- cgit v1.2.3