zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

table.ex (8198B)


      1 defmodule HPAX.Table do
      2   @moduledoc false
      3 
      4   defstruct [
      5     :max_table_size,
      6     entries: [],
      7     size: 0,
      8     length: 0
      9   ]
     10 
     11   @type t() :: %__MODULE__{
     12           max_table_size: non_neg_integer(),
     13           entries: [{binary(), binary()}],
     14           size: non_neg_integer(),
     15           length: non_neg_integer()
     16         }
     17 
     18   @static_table [
     19     {":authority", nil},
     20     {":method", "GET"},
     21     {":method", "POST"},
     22     {":path", "/"},
     23     {":path", "/index.html"},
     24     {":scheme", "http"},
     25     {":scheme", "https"},
     26     {":status", "200"},
     27     {":status", "204"},
     28     {":status", "206"},
     29     {":status", "304"},
     30     {":status", "400"},
     31     {":status", "404"},
     32     {":status", "500"},
     33     {"accept-charset", nil},
     34     {"accept-encoding", "gzip, deflate"},
     35     {"accept-language", nil},
     36     {"accept-ranges", nil},
     37     {"accept", nil},
     38     {"access-control-allow-origin", nil},
     39     {"age", nil},
     40     {"allow", nil},
     41     {"authorization", nil},
     42     {"cache-control", nil},
     43     {"content-disposition", nil},
     44     {"content-encoding", nil},
     45     {"content-language", nil},
     46     {"content-length", nil},
     47     {"content-location", nil},
     48     {"content-range", nil},
     49     {"content-type", nil},
     50     {"cookie", nil},
     51     {"date", nil},
     52     {"etag", nil},
     53     {"expect", nil},
     54     {"expires", nil},
     55     {"from", nil},
     56     {"host", nil},
     57     {"if-match", nil},
     58     {"if-modified-since", nil},
     59     {"if-none-match", nil},
     60     {"if-range", nil},
     61     {"if-unmodified-since", nil},
     62     {"last-modified", nil},
     63     {"link", nil},
     64     {"location", nil},
     65     {"max-forwards", nil},
     66     {"proxy-authenticate", nil},
     67     {"proxy-authorization", nil},
     68     {"range", nil},
     69     {"referer", nil},
     70     {"refresh", nil},
     71     {"retry-after", nil},
     72     {"server", nil},
     73     {"set-cookie", nil},
     74     {"strict-transport-security", nil},
     75     {"transfer-encoding", nil},
     76     {"user-agent", nil},
     77     {"vary", nil},
     78     {"via", nil},
     79     {"www-authenticate", nil}
     80   ]
     81 
     82   @static_table_size length(@static_table)
     83   @dynamic_table_start @static_table_size + 1
     84 
     85   @doc """
     86   Creates a new HPACK table with the given maximum size.
     87 
     88   The maximum size is not the maximum number of entries but rather the maximum size as defined in
     89   http://httpwg.org/specs/rfc7541.html#maximum.table.size.
     90   """
     91   @spec new(non_neg_integer()) :: t()
     92   def new(max_table_size) do
     93     %__MODULE__{max_table_size: max_table_size}
     94   end
     95 
     96   @doc """
     97   Adds the given header to the given table.
     98 
     99   If the new entry does not fit within the max table size then the oldest entries will be evicted.
    100 
    101   Header names should be lowercase when added to the HPACK table
    102   as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
    103 
    104   > header field names MUST be converted to lowercase prior to their encoding in HTTP/2
    105 
    106   """
    107   @spec add(t(), binary(), binary()) :: t()
    108   def add(%__MODULE__{} = table, name, value) do
    109     %{max_table_size: max_table_size, size: size} = table
    110     entry_size = entry_size(name, value)
    111 
    112     cond do
    113       # An attempt to add an entry larger than the maximum size causes the table to be emptied of
    114       # all existing entries and results in an empty table.
    115       entry_size > max_table_size ->
    116         %{table | entries: [], size: 0, length: 0}
    117 
    118       size + entry_size > max_table_size ->
    119         table
    120         |> resize(max_table_size - entry_size)
    121         |> add_header(name, value, entry_size)
    122 
    123       true ->
    124         add_header(table, name, value, entry_size)
    125     end
    126   end
    127 
    128   defp add_header(%__MODULE__{} = table, name, value, entry_size) do
    129     %{entries: entries, size: size, length: length} = table
    130     %{table | entries: [{name, value} | entries], size: size + entry_size, length: length + 1}
    131   end
    132 
    133   @doc """
    134   Looks up a header by index `index` in the given `table`.
    135 
    136   Returns `{:ok, {name, value}}` if a header is found at the given `index`, otherwise returns
    137   `:error`. `value` can be a binary in case both the header name and value are present in the
    138   table, or `nil` if only the name is present (this can only happen in the static table).
    139   """
    140   @spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary() | nil}} | :error
    141   def lookup_by_index(table, index)
    142 
    143   # Static table
    144   for {header, index} <- Enum.with_index(@static_table, 1) do
    145     def lookup_by_index(%__MODULE__{}, unquote(index)), do: {:ok, unquote(header)}
    146   end
    147 
    148   def lookup_by_index(%__MODULE__{length: 0}, _index) do
    149     :error
    150   end
    151 
    152   def lookup_by_index(%__MODULE__{entries: entries, length: length}, index)
    153       when index in @dynamic_table_start..(@dynamic_table_start + length - 1) do
    154     {:ok, Enum.at(entries, index - @dynamic_table_start)}
    155   end
    156 
    157   def lookup_by_index(%__MODULE__{}, _index) do
    158     :error
    159   end
    160 
    161   @doc """
    162   Looks up the index of a header by its name and value.
    163 
    164   It returns:
    165 
    166     * `{:full, index}` if the full header (name and value) are present in the table at `index`
    167 
    168     * `{:name, index}` if `name` is present in the table but with a different value than `value`
    169 
    170     * `:not_found` if the header name is not in the table at all
    171 
    172   Header names should be lowercase when looked up in the HPACK table
    173   as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
    174 
    175   > header field names MUST be converted to lowercase prior to their encoding in HTTP/2
    176 
    177   """
    178   @spec lookup_by_header(t(), binary(), binary() | nil) ::
    179           {:full, pos_integer()} | {:name, pos_integer()} | :not_found
    180   def lookup_by_header(table, name, value)
    181 
    182   def lookup_by_header(%__MODULE__{entries: entries}, name, value) do
    183     case static_lookup_by_header(name, value) do
    184       {:full, _index} = result ->
    185         result
    186 
    187       {:name, index} ->
    188         # Check if we get full match in the dynamic tabble
    189         case dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil) do
    190           {:full, _index} = result -> result
    191           _other -> {:name, index}
    192         end
    193 
    194       :not_found ->
    195         dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil)
    196     end
    197   end
    198 
    199   for {{name, value}, index} when is_binary(value) <- Enum.with_index(@static_table, 1) do
    200     defp static_lookup_by_header(unquote(name), unquote(value)) do
    201       {:full, unquote(index)}
    202     end
    203   end
    204 
    205   static_table_names =
    206     @static_table
    207     |> Enum.map(&elem(&1, 0))
    208     |> Enum.with_index(1)
    209     |> Enum.uniq_by(&elem(&1, 0))
    210 
    211   for {name, index} <- static_table_names do
    212     defp static_lookup_by_header(unquote(name), _value) do
    213       {:name, unquote(index)}
    214     end
    215   end
    216 
    217   defp static_lookup_by_header(_name, _value) do
    218     :not_found
    219   end
    220 
    221   defp dynamic_lookup_by_header([{name, value} | _rest], name, value, index, _name_index) do
    222     {:full, index}
    223   end
    224 
    225   defp dynamic_lookup_by_header([{name, _} | rest], name, value, index, _name_index) do
    226     dynamic_lookup_by_header(rest, name, value, index + 1, index)
    227   end
    228 
    229   defp dynamic_lookup_by_header([_other | rest], name, value, index, name_index) do
    230     dynamic_lookup_by_header(rest, name, value, index + 1, name_index)
    231   end
    232 
    233   defp dynamic_lookup_by_header([], _name, _value, _index, name_index) do
    234     if name_index, do: {:name, name_index}, else: :not_found
    235   end
    236 
    237   @doc """
    238   Resizes the table.
    239 
    240   If the existing entries do not fit in the new table size the oldest entries are evicted.
    241   """
    242   @spec resize(t(), non_neg_integer()) :: t()
    243   def resize(%__MODULE__{entries: entries, size: size} = table, new_size) do
    244     {new_entries_reversed, new_size} = evict_towards_size(Enum.reverse(entries), size, new_size)
    245 
    246     %{
    247       table
    248       | entries: Enum.reverse(new_entries_reversed),
    249         size: new_size,
    250         length: length(new_entries_reversed)
    251     }
    252   end
    253 
    254   defp evict_towards_size([{name, value} | rest], size, max_target_size) do
    255     new_size = size - entry_size(name, value)
    256 
    257     if new_size <= max_target_size do
    258       {rest, new_size}
    259     else
    260       evict_towards_size(rest, new_size, max_target_size)
    261     end
    262   end
    263 
    264   defp evict_towards_size([], 0, _max_target_size) do
    265     {[], 0}
    266   end
    267 
    268   defp entry_size(name, value) do
    269     byte_size(name) + byte_size(value) + 32
    270   end
    271 
    272   # Made public to be used in tests.
    273   @doc false
    274   def __static_table__() do
    275     @static_table
    276   end
    277 end