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