|
1 |
| -defmodule Hpax do |
| 1 | +defmodule HPax do |
2 | 2 | @moduledoc """
|
3 |
| - Documentation for `Hpax`. |
| 3 | + Support for the HPACK header compression algorithm. |
| 4 | +
|
| 5 | + This module provides support for the HPACK header compression algorithm used mainly in HTTP/2. |
| 6 | + The HPACK algorithm requires an encoding context on the encoder side and a decoding context on |
| 7 | + the decoder side. These contexts are semantically different but structurally the same and they |
| 8 | + can both be created through `new/1`. |
| 9 | + """ |
| 10 | + |
| 11 | + alias HPax.{Table, Types} |
| 12 | + |
| 13 | + @type header_name() :: binary() |
| 14 | + @type header_value() :: binary() |
| 15 | + |
| 16 | + @valid_header_actions [:store, :store_name, :no_store, :never_store] |
| 17 | + |
| 18 | + @doc """ |
| 19 | + Create a new context. |
| 20 | +
|
| 21 | + `max_table_size` is the maximum table size (in bytes) for the newly created context. |
4 | 22 | """
|
| 23 | + @spec new(non_neg_integer()) :: Table.t() |
| 24 | + def new(max_table_size) when is_integer(max_table_size) and max_table_size >= 0 do |
| 25 | + Table.new(max_table_size) |
| 26 | + end |
5 | 27 |
|
6 | 28 | @doc """
|
7 |
| - Hello world. |
| 29 | + Resizes the given table to the given size. |
| 30 | + """ |
| 31 | + @spec resize(Table.t(), non_neg_integer()) :: Table.t() |
| 32 | + defdelegate resize(table, new_size), to: Table |
| 33 | + |
| 34 | + ## Decoding |
| 35 | + |
| 36 | + @doc """ |
| 37 | + Decodes a header block fragment (HBF) through a given context. |
| 38 | +
|
| 39 | + If decoding is successful, this function returns a `{:ok, headers, updated_context}` tuple where |
| 40 | + `headers` is a list of decoded headers, and `updated_context` is the updated context. If there's |
| 41 | + an error in decoding, this function returns `{:error, reason}`. |
8 | 42 |
|
9 | 43 | ## Examples
|
10 | 44 |
|
11 |
| - iex> Hpax.hello() |
12 |
| - :world |
| 45 | + context = HPax.new(1000) |
| 46 | + hbf = get_hbf_from_somewhere() |
| 47 | + HPax.decode(hbf, context) |
| 48 | + #=> {:ok, [{":method", "GET"}], updated_context} |
13 | 49 |
|
14 | 50 | """
|
15 |
| - def hello do |
16 |
| - :world |
| 51 | + @spec decode(binary(), Table.t()) :: {:ok, [{binary(), binary()}], Table.t()} | {:error, term()} |
| 52 | + def decode(block, %Table{} = table) when is_binary(block) do |
| 53 | + decode_headers(block, table, _acc = []) |
| 54 | + catch |
| 55 | + :throw, {:mint, error} -> {:error, error} |
| 56 | + end |
| 57 | + |
| 58 | + defp decode_headers(<<>>, table, acc) do |
| 59 | + {:ok, Enum.reverse(acc), table} |
| 60 | + end |
| 61 | + |
| 62 | + # Indexed header field |
| 63 | + # http://httpwg.org/specs/rfc7541.html#rfc.section.6.1 |
| 64 | + defp decode_headers(<<0b1::1, rest::bitstring>>, table, acc) do |
| 65 | + {index, rest} = decode_integer(rest, 7) |
| 66 | + decode_headers(rest, table, [lookup_by_index!(table, index) | acc]) |
| 67 | + end |
| 68 | + |
| 69 | + # Literal header field with incremental indexing |
| 70 | + # http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1 |
| 71 | + defp decode_headers(<<0b01::2, rest::bitstring>>, table, acc) do |
| 72 | + {name, value, rest} = |
| 73 | + case rest do |
| 74 | + # The header name is a string. |
| 75 | + <<0::6, rest::binary>> -> |
| 76 | + {name, rest} = decode_binary(rest) |
| 77 | + {value, rest} = decode_binary(rest) |
| 78 | + {name, value, rest} |
| 79 | + |
| 80 | + # The header name is an index to be looked up in the table. |
| 81 | + _other -> |
| 82 | + {index, rest} = decode_integer(rest, 6) |
| 83 | + {value, rest} = decode_binary(rest) |
| 84 | + {name, _value} = lookup_by_index!(table, index) |
| 85 | + {name, value, rest} |
| 86 | + end |
| 87 | + |
| 88 | + decode_headers(rest, Table.add(table, name, value), [{name, value} | acc]) |
| 89 | + end |
| 90 | + |
| 91 | + # Literal header field without indexing |
| 92 | + # http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2 |
| 93 | + defp decode_headers(<<0b0000::4, rest::bitstring>>, table, acc) do |
| 94 | + {name, value, rest} = |
| 95 | + case rest do |
| 96 | + <<0::4, rest::binary>> -> |
| 97 | + {name, rest} = decode_binary(rest) |
| 98 | + {value, rest} = decode_binary(rest) |
| 99 | + {name, value, rest} |
| 100 | + |
| 101 | + _other -> |
| 102 | + {index, rest} = decode_integer(rest, 4) |
| 103 | + {value, rest} = decode_binary(rest) |
| 104 | + {name, _value} = lookup_by_index!(table, index) |
| 105 | + {name, value, rest} |
| 106 | + end |
| 107 | + |
| 108 | + decode_headers(rest, table, [{name, value} | acc]) |
| 109 | + end |
| 110 | + |
| 111 | + # Literal header field never indexed |
| 112 | + # http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3 |
| 113 | + defp decode_headers(<<0b0001::4, rest::bitstring>>, table, acc) do |
| 114 | + {name, value, rest} = |
| 115 | + case rest do |
| 116 | + <<0::4, rest::binary>> -> |
| 117 | + {name, rest} = decode_binary(rest) |
| 118 | + {value, rest} = decode_binary(rest) |
| 119 | + {name, value, rest} |
| 120 | + |
| 121 | + _other -> |
| 122 | + {index, rest} = decode_integer(rest, 4) |
| 123 | + {value, rest} = decode_binary(rest) |
| 124 | + {name, _value} = lookup_by_index!(table, index) |
| 125 | + {name, value, rest} |
| 126 | + end |
| 127 | + |
| 128 | + # TODO: enforce the "never indexed" part somehow. |
| 129 | + decode_headers(rest, table, [{name, value} | acc]) |
| 130 | + end |
| 131 | + |
| 132 | + # Dynamic table size update |
| 133 | + defp decode_headers(<<0b001::3, rest::bitstring>>, table, acc) do |
| 134 | + {new_size, rest} = decode_integer(rest, 5) |
| 135 | + decode_headers(rest, Table.resize(table, new_size), acc) |
| 136 | + end |
| 137 | + |
| 138 | + defp decode_headers(_other, _table, _acc) do |
| 139 | + throw({:mint, :protocol_error}) |
| 140 | + end |
| 141 | + |
| 142 | + defp lookup_by_index!(table, index) do |
| 143 | + case Table.lookup_by_index(table, index) do |
| 144 | + {:ok, header} -> header |
| 145 | + :error -> throw({:mint, {:index_not_found, index}}) |
| 146 | + end |
| 147 | + end |
| 148 | + |
| 149 | + defp decode_integer(bitstring, prefix) do |
| 150 | + case Types.decode_integer(bitstring, prefix) do |
| 151 | + {:ok, int, rest} -> {int, rest} |
| 152 | + :error -> throw({:mint, :bad_integer_encoding}) |
| 153 | + end |
| 154 | + end |
| 155 | + |
| 156 | + defp decode_binary(binary) do |
| 157 | + case Types.decode_binary(binary) do |
| 158 | + {:ok, binary, rest} -> {binary, rest} |
| 159 | + :error -> throw({:mint, :bad_binary_encoding}) |
| 160 | + end |
| 161 | + end |
| 162 | + |
| 163 | + ## Encoding |
| 164 | + |
| 165 | + @doc """ |
| 166 | + Encodes a list of headers through the given context. |
| 167 | +
|
| 168 | + Returns a two-element tuple where the first element is a binary representing the encoded headers |
| 169 | + and the second element is an updated context. |
| 170 | +
|
| 171 | + ## Examples |
| 172 | +
|
| 173 | + headers = [{:store, ":authority", "https://example.com"}] |
| 174 | + context = HPax.new(1000) |
| 175 | + HPax.encode(headers, context) |
| 176 | + #=> {<<...>>, updated_context} |
| 177 | +
|
| 178 | + """ |
| 179 | + @spec encode([header], Table.t()) :: {iodata(), Table.t()} |
| 180 | + when header: {action, header_name(), header_value()}, |
| 181 | + action: :store | :store_name | :no_store | :never_store |
| 182 | + def encode(headers, %Table{} = table) when is_list(headers) do |
| 183 | + encode_headers(headers, table, _acc = []) |
| 184 | + end |
| 185 | + |
| 186 | + defp encode_headers([], table, acc) do |
| 187 | + {acc, table} |
| 188 | + end |
| 189 | + |
| 190 | + defp encode_headers([{action, name, value} | rest], table, acc) |
| 191 | + when action in @valid_header_actions and is_binary(name) and is_binary(value) do |
| 192 | + {encoded, table} = |
| 193 | + case Table.lookup_by_header(table, name, value) do |
| 194 | + {:full, index} -> |
| 195 | + {encode_indexed_header(index), table} |
| 196 | + |
| 197 | + {:name, index} when action == :store -> |
| 198 | + {encode_literal_header_with_indexing(index, value), Table.add(table, name, value)} |
| 199 | + |
| 200 | + {:name, index} when action in [:store_name, :no_store] -> |
| 201 | + {encode_literal_header_without_indexing(index, value), table} |
| 202 | + |
| 203 | + {:name, index} when action == :never_store -> |
| 204 | + {encode_literal_header_never_indexed(index, value), table} |
| 205 | + |
| 206 | + :not_found when action in [:store, :store_name] -> |
| 207 | + {encode_literal_header_with_indexing(name, value), Table.add(table, name, value)} |
| 208 | + |
| 209 | + :not_found when action == :no_store -> |
| 210 | + {encode_literal_header_without_indexing(name, value), table} |
| 211 | + |
| 212 | + :not_found when action == :never_store -> |
| 213 | + {encode_literal_header_never_indexed(name, value), table} |
| 214 | + end |
| 215 | + |
| 216 | + encode_headers(rest, table, [acc, encoded]) |
| 217 | + end |
| 218 | + |
| 219 | + defp encode_indexed_header(index) do |
| 220 | + <<1::1, Types.encode_integer(index, 7)::bitstring>> |
| 221 | + end |
| 222 | + |
| 223 | + defp encode_literal_header_with_indexing(index, value) when is_integer(index) do |
| 224 | + [<<1::2, Types.encode_integer(index, 6)::bitstring>>, Types.encode_binary(value, false)] |
| 225 | + end |
| 226 | + |
| 227 | + defp encode_literal_header_with_indexing(name, value) when is_binary(name) do |
| 228 | + [<<1::2, 0::6>>, Types.encode_binary(name, false), Types.encode_binary(value, false)] |
| 229 | + end |
| 230 | + |
| 231 | + defp encode_literal_header_without_indexing(index, value) when is_integer(index) do |
| 232 | + [<<0::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, false)] |
| 233 | + end |
| 234 | + |
| 235 | + defp encode_literal_header_without_indexing(name, value) when is_binary(name) do |
| 236 | + [<<0::4, 0::4>>, Types.encode_binary(name, false), Types.encode_binary(value, false)] |
| 237 | + end |
| 238 | + |
| 239 | + defp encode_literal_header_never_indexed(index, value) when is_integer(index) do |
| 240 | + [<<1::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, false)] |
| 241 | + end |
| 242 | + |
| 243 | + defp encode_literal_header_never_indexed(name, value) when is_binary(name) do |
| 244 | + [<<1::4, 0::4>>, Types.encode_binary(name, false), Types.encode_binary(value, false)] |
17 | 245 | end
|
18 | 246 | end
|
0 commit comments