This commit is contained in:
2026-07-14 22:50:46 +04:00
parent 27143319e3
commit bd19e0682b
3116 changed files with 467189 additions and 0 deletions

View File

@@ -0,0 +1,360 @@
defmodule HPAX do
@moduledoc """
Support for the HPACK header compression algorithm.
This module provides support for the HPACK header compression algorithm used mainly in HTTP/2.
## Encoding and decoding contexts
The HPACK algorithm requires both
* an encoding context on the encoder side
* a decoding context on the decoder side
These contexts are semantically different but structurally the same. In HPACK they are
implemented as **HPACK tables**. This library uses the name "tables" everywhere internally
HPACK tables can be created through the `new/1` function.
"""
alias HPAX.{Table, Types}
@typedoc """
An HPACK table.
This can be used for encoding or decoding.
"""
@typedoc since: "0.2.0"
@opaque table() :: Table.t()
@typedoc """
An HPACK header name.
"""
@type header_name() :: binary()
@typedoc """
An HPACK header value.
"""
@type header_value() :: binary()
@valid_header_actions [:store, :store_name, :no_store, :never_store]
@doc """
Creates a new HPACK table.
Same as `new/2` with default options.
"""
@spec new(non_neg_integer()) :: table()
def new(max_table_size), do: new(max_table_size, [])
@doc """
Create a new HPACK table that can be used as encoding or decoding context.
See the "Encoding and decoding contexts" section in the module documentation.
`max_table_size` is the maximum table size (in bytes) for the newly created table.
## Options
This function accepts the following `options`:
* `:huffman_encoding` - (since 0.2.0) `:always` or `:never`. If `:always`,
then HPAX will always encode headers using Huffman encoding. If `:never`,
HPAX will not use any Huffman encoding. Defaults to `:never`.
## Examples
encoding_context = HPAX.new(4096)
"""
@doc since: "0.2.0"
@spec new(non_neg_integer(), [keyword()]) :: table()
def new(max_table_size, options)
when is_integer(max_table_size) and max_table_size >= 0 and is_list(options) do
options = Keyword.put_new(options, :huffman_encoding, :never)
Enum.each(options, fn
{:huffman_encoding, _huffman_encoding} -> :ok
{key, _value} -> raise ArgumentError, "unknown option: #{inspect(key)}"
end)
Table.new(max_table_size, Keyword.fetch!(options, :huffman_encoding))
end
@doc """
Resizes the given table to the given maximum size.
This is intended for use where the overlying protocol has signaled a change to the table's
maximum size, such as when an HTTP/2 `SETTINGS` frame is received.
If the indicated size is less than the table's current size, entries
will be evicted as needed to fit within the specified size, and the table's
maximum size will be decreased to the specified value. A flag will also be
set which will enqueue a "dynamic table size update" command to be prefixed
to the next block encoded with this table, per
[RFC9113§4.3.1](https://www.rfc-editor.org/rfc/rfc9113.html#section-4.3.1).
If the indicated size is greater than or equal to the table's current max size, no entries are evicted
and the table's maximum size changes to the specified value.
## Examples
decoding_context = HPAX.new(4096)
HPAX.resize(decoding_context, 8192)
"""
@spec resize(table(), non_neg_integer()) :: table()
defdelegate resize(table, new_max_size), to: Table
@doc """
Decodes a header block fragment (HBF) through a given table.
If decoding is successful, this function returns a `{:ok, headers, updated_table}` tuple where
`headers` is a list of decoded headers, and `updated_table` is the updated table. If there's
an error in decoding, this function returns `{:error, reason}`.
## Examples
decoding_context = HPAX.new(1000)
hbf = get_hbf_from_somewhere()
HPAX.decode(hbf, decoding_context)
#=> {:ok, [{":method", "GET"}], decoding_context}
"""
@spec decode(binary(), table()) ::
{:ok, [{header_name(), header_value()}], table()} | {:error, term()}
# Dynamic resizes must occur only at the start of a block
# https://datatracker.ietf.org/doc/html/rfc7541#section-4.2
def decode(<<0b001::3, rest::bitstring>>, %Table{} = table) do
{new_max_size, rest} = decode_integer(rest, 5)
# Dynamic resizes must be less than protocol max table size
# https://datatracker.ietf.org/doc/html/rfc7541#section-6.3
if new_max_size <= table.protocol_max_table_size do
decode(rest, Table.dynamic_resize(table, new_max_size))
else
{:error, :protocol_error}
end
end
def decode(block, %Table{} = table) when is_binary(block) do
decode_headers(block, table, _acc = [])
catch
:throw, {:hpax, error} -> {:error, error}
end
@doc """
Encodes a list of headers through the given table.
Returns a two-element tuple where the first element is a binary representing the encoded headers
and the second element is an updated table.
## Examples
headers = [{:store, ":authority", "https://example.com"}]
encoding_context = HPAX.new(1000)
HPAX.encode(headers, encoding_context)
#=> {iodata, updated_encoding_context}
"""
@spec encode([header], table()) :: {iodata(), table()}
when header: {action, header_name(), header_value()},
action: :store | :store_name | :no_store | :never_store
def encode(headers, %Table{} = table) when is_list(headers) do
{table, pending_resizes} = Table.pop_pending_resizes(table)
acc = Enum.map(pending_resizes, &[<<0b001::3, Types.encode_integer(&1, 5)::bitstring>>])
encode_headers(headers, table, acc)
end
@doc """
Encodes a list of headers through the given table, applying the same `action` to all of them.
This function is the similar to `encode/2`, but `headers` are `{name, value}` tuples instead,
and the same `action` is applied to all headers.
## Examples
headers = [{":authority", "https://example.com"}]
encoding_context = HPAX.new(1000)
HPAX.encode(:store, headers, encoding_context)
#=> {iodata, updated_encoding_context}
"""
@doc since: "0.2.0"
@spec encode(action, [header], table()) :: {iodata(), table()}
when action: :store | :store_name | :no_store | :never_store,
header: {header_name(), header_value()}
def encode(action, headers, %Table{} = table)
when is_list(headers) and action in [:store, :store_name, :no_store, :never_store] do
headers
|> Enum.map(fn {name, value} -> {action, name, value} end)
|> encode(table)
end
## Helpers
defp decode_headers(<<>>, table, acc) do
{:ok, Enum.reverse(acc), table}
end
# Indexed header field
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.1
defp decode_headers(<<0b1::1, rest::bitstring>>, table, acc) do
{index, rest} = decode_integer(rest, 7)
decode_headers(rest, table, [lookup_by_index!(table, index) | acc])
end
# Literal header field with incremental indexing
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.1
defp decode_headers(<<0b01::2, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
# The header name is a string.
<<0::6, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
# The header name is an index to be looked up in the table.
_other ->
{index, rest} = decode_integer(rest, 6)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
decode_headers(rest, Table.add(table, name, value), [{name, value} | acc])
end
# Literal header field without indexing
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.2
defp decode_headers(<<0b0000::4, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
<<0::4, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
_other ->
{index, rest} = decode_integer(rest, 4)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
decode_headers(rest, table, [{name, value} | acc])
end
# Literal header field never indexed
# http://httpwg.org/specs/rfc7541.html#rfc.section.6.2.3
defp decode_headers(<<0b0001::4, rest::bitstring>>, table, acc) do
{name, value, rest} =
case rest do
<<0::4, rest::binary>> ->
{name, rest} = decode_binary(rest)
{value, rest} = decode_binary(rest)
{name, value, rest}
_other ->
{index, rest} = decode_integer(rest, 4)
{value, rest} = decode_binary(rest)
{name, _value} = lookup_by_index!(table, index)
{name, value, rest}
end
# TODO: enforce the "never indexed" part somehow.
decode_headers(rest, table, [{name, value} | acc])
end
defp decode_headers(_other, _table, _acc) do
throw({:hpax, :protocol_error})
end
defp lookup_by_index!(table, index) do
case Table.lookup_by_index(table, index) do
{:ok, header} -> header
:error -> throw({:hpax, {:index_not_found, index}})
end
end
defp decode_integer(bitstring, prefix) do
case Types.decode_integer(bitstring, prefix) do
{:ok, int, rest} -> {int, rest}
:error -> throw({:hpax, :bad_integer_encoding})
end
end
defp decode_binary(binary) do
case Types.decode_binary(binary) do
{:ok, binary, rest} -> {binary, rest}
:error -> throw({:hpax, :bad_binary_encoding})
end
end
defp encode_headers([], table, acc) do
{acc, table}
end
defp encode_headers([{action, name, value} | rest], table, acc)
when action in @valid_header_actions and is_binary(name) and is_binary(value) do
huffman? = table.huffman_encoding == :always
{encoded, table} =
case Table.lookup_by_header(table, name, value) do
{:full, index} ->
{encode_indexed_header(index), table}
{:name, index} when action == :store ->
{encode_literal_header_with_indexing(index, value, huffman?),
Table.add(table, name, value)}
{:name, index} when action in [:store_name, :no_store] ->
{encode_literal_header_without_indexing(index, value, huffman?), table}
{:name, index} when action == :never_store ->
{encode_literal_header_never_indexed(index, value, huffman?), table}
:not_found when action in [:store, :store_name] ->
{encode_literal_header_with_indexing(name, value, huffman?),
Table.add(table, name, value)}
:not_found when action == :no_store ->
{encode_literal_header_without_indexing(name, value, huffman?), table}
:not_found when action == :never_store ->
{encode_literal_header_never_indexed(name, value, huffman?), table}
end
encode_headers(rest, table, [acc, encoded])
end
defp encode_indexed_header(index) do
<<1::1, Types.encode_integer(index, 7)::bitstring>>
end
defp encode_literal_header_with_indexing(index, value, huffman?) when is_integer(index) do
[<<1::2, Types.encode_integer(index, 6)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_with_indexing(name, value, huffman?) when is_binary(name) do
[<<1::2, 0::6>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_without_indexing(index, value, huffman?) when is_integer(index) do
[<<0::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_without_indexing(name, value, huffman?) when is_binary(name) do
[<<0::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_never_indexed(index, value, huffman?) when is_integer(index) do
[<<1::4, Types.encode_integer(index, 4)::bitstring>>, Types.encode_binary(value, huffman?)]
end
defp encode_literal_header_never_indexed(name, value, huffman?) when is_binary(name) do
[<<1::4, 0::4>>, Types.encode_binary(name, huffman?), Types.encode_binary(value, huffman?)]
end
end

View File

@@ -0,0 +1,94 @@
defmodule HPAX.Huffman do
@moduledoc false
import Bitwise, only: [>>>: 2]
# This file is downloaded from the spec directly.
# http://httpwg.org/specs/rfc7541.html#huffman.code
table_file = Path.absname("huffman_table", __DIR__)
@external_resource table_file
entries =
Enum.map(File.stream!(table_file), fn line ->
[byte_value, bits, _hex, bit_count] =
line
|> case do
<<?', _, ?', ?\s, rest::binary>> -> rest
"EOS " <> rest -> rest
_other -> line
end
|> String.replace(["|", "(", ")", "[", "]"], "")
|> String.split()
byte_value = String.to_integer(byte_value)
bits = String.to_integer(bits, 2)
bit_count = String.to_integer(bit_count)
{byte_value, bits, bit_count}
end)
{regular_entries, [eos_entry]} = Enum.split(entries, -1)
{_eos_byte_value, eos_bits, eos_bit_count} = eos_entry
## Encoding
@spec encode(binary()) :: binary()
def encode(binary) do
encode(binary, _acc = <<>>)
end
for {byte_value, bits, bit_count} <- regular_entries do
defp encode(<<unquote(byte_value), rest::binary>>, acc) do
encode(rest, <<acc::bitstring, unquote(bits)::size(unquote(bit_count))>>)
end
end
defp encode(<<>>, acc) do
overflowing_bits = rem(bit_size(acc), 8)
if overflowing_bits == 0 do
acc
else
bits_to_add = 8 - overflowing_bits
value_of_bits_to_add =
take_significant_bits(unquote(eos_bits), unquote(eos_bit_count), bits_to_add)
<<acc::bitstring, value_of_bits_to_add::size(bits_to_add)>>
end
end
## Decoding
@spec decode(binary()) :: binary()
def decode(binary)
for {byte_value, bits, bit_count} <- regular_entries do
def decode(<<unquote(bits)::size(unquote(bit_count)), rest::bitstring>>) do
<<unquote(byte_value), decode(rest)::binary>>
end
end
def decode(<<>>) do
<<>>
end
# Use binary syntax for single match context optimization.
def decode(<<padding::bitstring>>) when bit_size(padding) in 1..7 do
padding_size = bit_size(padding)
<<padding::size(padding_size)>> = padding
if take_significant_bits(unquote(eos_bits), unquote(eos_bit_count), padding_size) == padding do
<<>>
else
throw({:hpax, {:protocol_error, :invalid_huffman_encoding}})
end
end
## Helpers
@compile {:inline, take_significant_bits: 3}
defp take_significant_bits(value, bit_count, bits_to_take) do
value >>> (bit_count - bits_to_take)
end
end

View File

@@ -0,0 +1,257 @@
( 0) |11111111|11000 1ff8 [13]
( 1) |11111111|11111111|1011000 7fffd8 [23]
( 2) |11111111|11111111|11111110|0010 fffffe2 [28]
( 3) |11111111|11111111|11111110|0011 fffffe3 [28]
( 4) |11111111|11111111|11111110|0100 fffffe4 [28]
( 5) |11111111|11111111|11111110|0101 fffffe5 [28]
( 6) |11111111|11111111|11111110|0110 fffffe6 [28]
( 7) |11111111|11111111|11111110|0111 fffffe7 [28]
( 8) |11111111|11111111|11111110|1000 fffffe8 [28]
( 9) |11111111|11111111|11101010 ffffea [24]
( 10) |11111111|11111111|11111111|111100 3ffffffc [30]
( 11) |11111111|11111111|11111110|1001 fffffe9 [28]
( 12) |11111111|11111111|11111110|1010 fffffea [28]
( 13) |11111111|11111111|11111111|111101 3ffffffd [30]
( 14) |11111111|11111111|11111110|1011 fffffeb [28]
( 15) |11111111|11111111|11111110|1100 fffffec [28]
( 16) |11111111|11111111|11111110|1101 fffffed [28]
( 17) |11111111|11111111|11111110|1110 fffffee [28]
( 18) |11111111|11111111|11111110|1111 fffffef [28]
( 19) |11111111|11111111|11111111|0000 ffffff0 [28]
( 20) |11111111|11111111|11111111|0001 ffffff1 [28]
( 21) |11111111|11111111|11111111|0010 ffffff2 [28]
( 22) |11111111|11111111|11111111|111110 3ffffffe [30]
( 23) |11111111|11111111|11111111|0011 ffffff3 [28]
( 24) |11111111|11111111|11111111|0100 ffffff4 [28]
( 25) |11111111|11111111|11111111|0101 ffffff5 [28]
( 26) |11111111|11111111|11111111|0110 ffffff6 [28]
( 27) |11111111|11111111|11111111|0111 ffffff7 [28]
( 28) |11111111|11111111|11111111|1000 ffffff8 [28]
( 29) |11111111|11111111|11111111|1001 ffffff9 [28]
( 30) |11111111|11111111|11111111|1010 ffffffa [28]
( 31) |11111111|11111111|11111111|1011 ffffffb [28]
' ' ( 32) |010100 14 [ 6]
'!' ( 33) |11111110|00 3f8 [10]
'"' ( 34) |11111110|01 3f9 [10]
'#' ( 35) |11111111|1010 ffa [12]
'$' ( 36) |11111111|11001 1ff9 [13]
'%' ( 37) |010101 15 [ 6]
'&' ( 38) |11111000 f8 [ 8]
''' ( 39) |11111111|010 7fa [11]
'(' ( 40) |11111110|10 3fa [10]
')' ( 41) |11111110|11 3fb [10]
'*' ( 42) |11111001 f9 [ 8]
'+' ( 43) |11111111|011 7fb [11]
',' ( 44) |11111010 fa [ 8]
'-' ( 45) |010110 16 [ 6]
'.' ( 46) |010111 17 [ 6]
'/' ( 47) |011000 18 [ 6]
'0' ( 48) |00000 0 [ 5]
'1' ( 49) |00001 1 [ 5]
'2' ( 50) |00010 2 [ 5]
'3' ( 51) |011001 19 [ 6]
'4' ( 52) |011010 1a [ 6]
'5' ( 53) |011011 1b [ 6]
'6' ( 54) |011100 1c [ 6]
'7' ( 55) |011101 1d [ 6]
'8' ( 56) |011110 1e [ 6]
'9' ( 57) |011111 1f [ 6]
':' ( 58) |1011100 5c [ 7]
';' ( 59) |11111011 fb [ 8]
'<' ( 60) |11111111|1111100 7ffc [15]
'=' ( 61) |100000 20 [ 6]
'>' ( 62) |11111111|1011 ffb [12]
'?' ( 63) |11111111|00 3fc [10]
'@' ( 64) |11111111|11010 1ffa [13]
'A' ( 65) |100001 21 [ 6]
'B' ( 66) |1011101 5d [ 7]
'C' ( 67) |1011110 5e [ 7]
'D' ( 68) |1011111 5f [ 7]
'E' ( 69) |1100000 60 [ 7]
'F' ( 70) |1100001 61 [ 7]
'G' ( 71) |1100010 62 [ 7]
'H' ( 72) |1100011 63 [ 7]
'I' ( 73) |1100100 64 [ 7]
'J' ( 74) |1100101 65 [ 7]
'K' ( 75) |1100110 66 [ 7]
'L' ( 76) |1100111 67 [ 7]
'M' ( 77) |1101000 68 [ 7]
'N' ( 78) |1101001 69 [ 7]
'O' ( 79) |1101010 6a [ 7]
'P' ( 80) |1101011 6b [ 7]
'Q' ( 81) |1101100 6c [ 7]
'R' ( 82) |1101101 6d [ 7]
'S' ( 83) |1101110 6e [ 7]
'T' ( 84) |1101111 6f [ 7]
'U' ( 85) |1110000 70 [ 7]
'V' ( 86) |1110001 71 [ 7]
'W' ( 87) |1110010 72 [ 7]
'X' ( 88) |11111100 fc [ 8]
'Y' ( 89) |1110011 73 [ 7]
'Z' ( 90) |11111101 fd [ 8]
'[' ( 91) |11111111|11011 1ffb [13]
'\' ( 92) |11111111|11111110|000 7fff0 [19]
']' ( 93) |11111111|11100 1ffc [13]
'^' ( 94) |11111111|111100 3ffc [14]
'_' ( 95) |100010 22 [ 6]
'`' ( 96) |11111111|1111101 7ffd [15]
'a' ( 97) |00011 3 [ 5]
'b' ( 98) |100011 23 [ 6]
'c' ( 99) |00100 4 [ 5]
'd' (100) |100100 24 [ 6]
'e' (101) |00101 5 [ 5]
'f' (102) |100101 25 [ 6]
'g' (103) |100110 26 [ 6]
'h' (104) |100111 27 [ 6]
'i' (105) |00110 6 [ 5]
'j' (106) |1110100 74 [ 7]
'k' (107) |1110101 75 [ 7]
'l' (108) |101000 28 [ 6]
'm' (109) |101001 29 [ 6]
'n' (110) |101010 2a [ 6]
'o' (111) |00111 7 [ 5]
'p' (112) |101011 2b [ 6]
'q' (113) |1110110 76 [ 7]
'r' (114) |101100 2c [ 6]
's' (115) |01000 8 [ 5]
't' (116) |01001 9 [ 5]
'u' (117) |101101 2d [ 6]
'v' (118) |1110111 77 [ 7]
'w' (119) |1111000 78 [ 7]
'x' (120) |1111001 79 [ 7]
'y' (121) |1111010 7a [ 7]
'z' (122) |1111011 7b [ 7]
'{' (123) |11111111|1111110 7ffe [15]
'|' (124) |11111111|100 7fc [11]
'}' (125) |11111111|111101 3ffd [14]
'~' (126) |11111111|11101 1ffd [13]
(127) |11111111|11111111|11111111|1100 ffffffc [28]
(128) |11111111|11111110|0110 fffe6 [20]
(129) |11111111|11111111|010010 3fffd2 [22]
(130) |11111111|11111110|0111 fffe7 [20]
(131) |11111111|11111110|1000 fffe8 [20]
(132) |11111111|11111111|010011 3fffd3 [22]
(133) |11111111|11111111|010100 3fffd4 [22]
(134) |11111111|11111111|010101 3fffd5 [22]
(135) |11111111|11111111|1011001 7fffd9 [23]
(136) |11111111|11111111|010110 3fffd6 [22]
(137) |11111111|11111111|1011010 7fffda [23]
(138) |11111111|11111111|1011011 7fffdb [23]
(139) |11111111|11111111|1011100 7fffdc [23]
(140) |11111111|11111111|1011101 7fffdd [23]
(141) |11111111|11111111|1011110 7fffde [23]
(142) |11111111|11111111|11101011 ffffeb [24]
(143) |11111111|11111111|1011111 7fffdf [23]
(144) |11111111|11111111|11101100 ffffec [24]
(145) |11111111|11111111|11101101 ffffed [24]
(146) |11111111|11111111|010111 3fffd7 [22]
(147) |11111111|11111111|1100000 7fffe0 [23]
(148) |11111111|11111111|11101110 ffffee [24]
(149) |11111111|11111111|1100001 7fffe1 [23]
(150) |11111111|11111111|1100010 7fffe2 [23]
(151) |11111111|11111111|1100011 7fffe3 [23]
(152) |11111111|11111111|1100100 7fffe4 [23]
(153) |11111111|11111110|11100 1fffdc [21]
(154) |11111111|11111111|011000 3fffd8 [22]
(155) |11111111|11111111|1100101 7fffe5 [23]
(156) |11111111|11111111|011001 3fffd9 [22]
(157) |11111111|11111111|1100110 7fffe6 [23]
(158) |11111111|11111111|1100111 7fffe7 [23]
(159) |11111111|11111111|11101111 ffffef [24]
(160) |11111111|11111111|011010 3fffda [22]
(161) |11111111|11111110|11101 1fffdd [21]
(162) |11111111|11111110|1001 fffe9 [20]
(163) |11111111|11111111|011011 3fffdb [22]
(164) |11111111|11111111|011100 3fffdc [22]
(165) |11111111|11111111|1101000 7fffe8 [23]
(166) |11111111|11111111|1101001 7fffe9 [23]
(167) |11111111|11111110|11110 1fffde [21]
(168) |11111111|11111111|1101010 7fffea [23]
(169) |11111111|11111111|011101 3fffdd [22]
(170) |11111111|11111111|011110 3fffde [22]
(171) |11111111|11111111|11110000 fffff0 [24]
(172) |11111111|11111110|11111 1fffdf [21]
(173) |11111111|11111111|011111 3fffdf [22]
(174) |11111111|11111111|1101011 7fffeb [23]
(175) |11111111|11111111|1101100 7fffec [23]
(176) |11111111|11111111|00000 1fffe0 [21]
(177) |11111111|11111111|00001 1fffe1 [21]
(178) |11111111|11111111|100000 3fffe0 [22]
(179) |11111111|11111111|00010 1fffe2 [21]
(180) |11111111|11111111|1101101 7fffed [23]
(181) |11111111|11111111|100001 3fffe1 [22]
(182) |11111111|11111111|1101110 7fffee [23]
(183) |11111111|11111111|1101111 7fffef [23]
(184) |11111111|11111110|1010 fffea [20]
(185) |11111111|11111111|100010 3fffe2 [22]
(186) |11111111|11111111|100011 3fffe3 [22]
(187) |11111111|11111111|100100 3fffe4 [22]
(188) |11111111|11111111|1110000 7ffff0 [23]
(189) |11111111|11111111|100101 3fffe5 [22]
(190) |11111111|11111111|100110 3fffe6 [22]
(191) |11111111|11111111|1110001 7ffff1 [23]
(192) |11111111|11111111|11111000|00 3ffffe0 [26]
(193) |11111111|11111111|11111000|01 3ffffe1 [26]
(194) |11111111|11111110|1011 fffeb [20]
(195) |11111111|11111110|001 7fff1 [19]
(196) |11111111|11111111|100111 3fffe7 [22]
(197) |11111111|11111111|1110010 7ffff2 [23]
(198) |11111111|11111111|101000 3fffe8 [22]
(199) |11111111|11111111|11110110|0 1ffffec [25]
(200) |11111111|11111111|11111000|10 3ffffe2 [26]
(201) |11111111|11111111|11111000|11 3ffffe3 [26]
(202) |11111111|11111111|11111001|00 3ffffe4 [26]
(203) |11111111|11111111|11111011|110 7ffffde [27]
(204) |11111111|11111111|11111011|111 7ffffdf [27]
(205) |11111111|11111111|11111001|01 3ffffe5 [26]
(206) |11111111|11111111|11110001 fffff1 [24]
(207) |11111111|11111111|11110110|1 1ffffed [25]
(208) |11111111|11111110|010 7fff2 [19]
(209) |11111111|11111111|00011 1fffe3 [21]
(210) |11111111|11111111|11111001|10 3ffffe6 [26]
(211) |11111111|11111111|11111100|000 7ffffe0 [27]
(212) |11111111|11111111|11111100|001 7ffffe1 [27]
(213) |11111111|11111111|11111001|11 3ffffe7 [26]
(214) |11111111|11111111|11111100|010 7ffffe2 [27]
(215) |11111111|11111111|11110010 fffff2 [24]
(216) |11111111|11111111|00100 1fffe4 [21]
(217) |11111111|11111111|00101 1fffe5 [21]
(218) |11111111|11111111|11111010|00 3ffffe8 [26]
(219) |11111111|11111111|11111010|01 3ffffe9 [26]
(220) |11111111|11111111|11111111|1101 ffffffd [28]
(221) |11111111|11111111|11111100|011 7ffffe3 [27]
(222) |11111111|11111111|11111100|100 7ffffe4 [27]
(223) |11111111|11111111|11111100|101 7ffffe5 [27]
(224) |11111111|11111110|1100 fffec [20]
(225) |11111111|11111111|11110011 fffff3 [24]
(226) |11111111|11111110|1101 fffed [20]
(227) |11111111|11111111|00110 1fffe6 [21]
(228) |11111111|11111111|101001 3fffe9 [22]
(229) |11111111|11111111|00111 1fffe7 [21]
(230) |11111111|11111111|01000 1fffe8 [21]
(231) |11111111|11111111|1110011 7ffff3 [23]
(232) |11111111|11111111|101010 3fffea [22]
(233) |11111111|11111111|101011 3fffeb [22]
(234) |11111111|11111111|11110111|0 1ffffee [25]
(235) |11111111|11111111|11110111|1 1ffffef [25]
(236) |11111111|11111111|11110100 fffff4 [24]
(237) |11111111|11111111|11110101 fffff5 [24]
(238) |11111111|11111111|11111010|10 3ffffea [26]
(239) |11111111|11111111|1110100 7ffff4 [23]
(240) |11111111|11111111|11111010|11 3ffffeb [26]
(241) |11111111|11111111|11111100|110 7ffffe6 [27]
(242) |11111111|11111111|11111011|00 3ffffec [26]
(243) |11111111|11111111|11111011|01 3ffffed [26]
(244) |11111111|11111111|11111100|111 7ffffe7 [27]
(245) |11111111|11111111|11111101|000 7ffffe8 [27]
(246) |11111111|11111111|11111101|001 7ffffe9 [27]
(247) |11111111|11111111|11111101|010 7ffffea [27]
(248) |11111111|11111111|11111101|011 7ffffeb [27]
(249) |11111111|11111111|11111111|1110 ffffffe [28]
(250) |11111111|11111111|11111101|100 7ffffec [27]
(251) |11111111|11111111|11111101|101 7ffffed [27]
(252) |11111111|11111111|11111101|110 7ffffee [27]
(253) |11111111|11111111|11111101|111 7ffffef [27]
(254) |11111111|11111111|11111110|000 7fffff0 [27]
(255) |11111111|11111111|11111011|10 3ffffee [26]
EOS (256) |11111111|11111111|11111111|111111 3fffffff [30]

View File

@@ -0,0 +1,348 @@
defmodule HPAX.Table do
@moduledoc false
@enforce_keys [:max_table_size, :huffman_encoding]
defstruct [
:protocol_max_table_size,
:max_table_size,
:huffman_encoding,
entries: [],
size: 0,
length: 0,
pending_minimum_resize: nil
]
@type huffman_encoding() :: :always | :never
@type t() :: %__MODULE__{
protocol_max_table_size: non_neg_integer(),
max_table_size: non_neg_integer(),
huffman_encoding: huffman_encoding(),
entries: [{binary(), binary()}],
size: non_neg_integer(),
length: non_neg_integer(),
pending_minimum_resize: non_neg_integer() | nil
}
@static_table [
{":authority", nil},
{":method", "GET"},
{":method", "POST"},
{":path", "/"},
{":path", "/index.html"},
{":scheme", "http"},
{":scheme", "https"},
{":status", "200"},
{":status", "204"},
{":status", "206"},
{":status", "304"},
{":status", "400"},
{":status", "404"},
{":status", "500"},
{"accept-charset", nil},
{"accept-encoding", "gzip, deflate"},
{"accept-language", nil},
{"accept-ranges", nil},
{"accept", nil},
{"access-control-allow-origin", nil},
{"age", nil},
{"allow", nil},
{"authorization", nil},
{"cache-control", nil},
{"content-disposition", nil},
{"content-encoding", nil},
{"content-language", nil},
{"content-length", nil},
{"content-location", nil},
{"content-range", nil},
{"content-type", nil},
{"cookie", nil},
{"date", nil},
{"etag", nil},
{"expect", nil},
{"expires", nil},
{"from", nil},
{"host", nil},
{"if-match", nil},
{"if-modified-since", nil},
{"if-none-match", nil},
{"if-range", nil},
{"if-unmodified-since", nil},
{"last-modified", nil},
{"link", nil},
{"location", nil},
{"max-forwards", nil},
{"proxy-authenticate", nil},
{"proxy-authorization", nil},
{"range", nil},
{"referer", nil},
{"refresh", nil},
{"retry-after", nil},
{"server", nil},
{"set-cookie", nil},
{"strict-transport-security", nil},
{"transfer-encoding", nil},
{"user-agent", nil},
{"vary", nil},
{"via", nil},
{"www-authenticate", nil}
]
@static_table_size length(@static_table)
@dynamic_table_start @static_table_size + 1
@doc """
Creates a new HPACK table with the given maximum size.
The maximum size is not the maximum number of entries but rather the maximum size as defined in
http://httpwg.org/specs/rfc7541.html#maximum.table.size.
"""
@spec new(non_neg_integer(), huffman_encoding()) :: t()
def new(protocol_max_table_size, huffman_encoding)
when is_integer(protocol_max_table_size) and protocol_max_table_size >= 0 and
huffman_encoding in [:always, :never] do
%__MODULE__{
protocol_max_table_size: protocol_max_table_size,
max_table_size: protocol_max_table_size,
huffman_encoding: huffman_encoding
}
end
@doc """
Adds the given header to the given table.
If the new entry does not fit within the max table size then the oldest entries will be evicted.
Header names should be lowercase when added to the HPACK table
as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
> header field names MUST be converted to lowercase prior to their encoding in HTTP/2
"""
@spec add(t(), binary(), binary()) :: t()
def add(%__MODULE__{} = table, name, value) do
%{max_table_size: max_table_size, size: size} = table
entry_size = entry_size(name, value)
cond do
# An attempt to add an entry larger than the maximum size causes the table to be emptied of
# all existing entries and results in an empty table.
entry_size > max_table_size ->
%{table | entries: [], size: 0, length: 0}
size + entry_size > max_table_size ->
table
|> evict_to_size(max_table_size - entry_size)
|> add_header(name, value, entry_size)
true ->
add_header(table, name, value, entry_size)
end
end
defp add_header(%__MODULE__{} = table, name, value, entry_size) do
%{entries: entries, size: size, length: length} = table
%{table | entries: [{name, value} | entries], size: size + entry_size, length: length + 1}
end
@doc """
Looks up a header by index `index` in the given `table`.
Returns `{:ok, {name, value}}` if a header is found at the given `index`, otherwise returns
`:error`. `value` can be a binary in case both the header name and value are present in the
table, or `nil` if only the name is present (this can only happen in the static table).
"""
@spec lookup_by_index(t(), pos_integer()) :: {:ok, {binary(), binary() | nil}} | :error
def lookup_by_index(table, index)
# Static table
for {header, index} <- Enum.with_index(@static_table, 1) do
def lookup_by_index(%__MODULE__{}, unquote(index)), do: {:ok, unquote(header)}
end
def lookup_by_index(%__MODULE__{length: 0}, _index) do
:error
end
def lookup_by_index(%__MODULE__{entries: entries, length: length}, index)
when index >= @dynamic_table_start and index <= @dynamic_table_start + length - 1 do
{:ok, Enum.at(entries, index - @dynamic_table_start)}
end
def lookup_by_index(%__MODULE__{}, _index) do
:error
end
@doc """
Looks up the index of a header by its name and value.
It returns:
* `{:full, index}` if the full header (name and value) are present in the table at `index`
* `{:name, index}` if `name` is present in the table but with a different value than `value`
* `:not_found` if the header name is not in the table at all
Header names should be lowercase when looked up in the HPACK table
as per the [HTTP/2 spec](https://http2.github.io/http2-spec/#rfc.section.8.1.2):
> header field names MUST be converted to lowercase prior to their encoding in HTTP/2
"""
@spec lookup_by_header(t(), binary(), binary() | nil) ::
{:full, pos_integer()} | {:name, pos_integer()} | :not_found
def lookup_by_header(table, name, value)
def lookup_by_header(%__MODULE__{entries: entries}, name, value) do
case static_lookup_by_header(name, value) do
{:full, _index} = result ->
result
{:name, index} ->
# Check if we get full match in the dynamic tabble
case dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil) do
{:full, _index} = result -> result
_other -> {:name, index}
end
:not_found ->
dynamic_lookup_by_header(entries, name, value, @dynamic_table_start, nil)
end
end
for {{name, value}, index} when is_binary(value) <- Enum.with_index(@static_table, 1) do
defp static_lookup_by_header(unquote(name), unquote(value)) do
{:full, unquote(index)}
end
end
static_table_names =
@static_table
|> Enum.map(&elem(&1, 0))
|> Enum.with_index(1)
|> Enum.uniq_by(&elem(&1, 0))
for {name, index} <- static_table_names do
defp static_lookup_by_header(unquote(name), _value) do
{:name, unquote(index)}
end
end
defp static_lookup_by_header(_name, _value) do
:not_found
end
defp dynamic_lookup_by_header([{name, value} | _rest], name, value, index, _name_index) do
{:full, index}
end
defp dynamic_lookup_by_header([{name, _} | rest], name, value, index, _name_index) do
dynamic_lookup_by_header(rest, name, value, index + 1, index)
end
defp dynamic_lookup_by_header([_other | rest], name, value, index, name_index) do
dynamic_lookup_by_header(rest, name, value, index + 1, name_index)
end
defp dynamic_lookup_by_header([], _name, _value, _index, name_index) do
if name_index, do: {:name, name_index}, else: :not_found
end
@doc """
Changes the table's protocol negotiated maximum size, possibly evicting entries as needed to satisfy.
If the indicated size is less than the table's current max size, entries
will be evicted as needed to fit within the specified size, and the table's
maximum size will be decreased to the specified value. An will also be
set which will enqueue a 'dynamic table size update' command to be prefixed
to the next block encoded with this table, per RFC9113§4.3.1.
If the indicated size is greater than or equal to the table's current max size, no entries are evicted
and the table's maximum size changes to the specified value.
In all cases, the table's `:protocol_max_table_size` is updated accordingly
"""
@spec resize(t(), non_neg_integer()) :: t()
def resize(%__MODULE__{} = table, new_protocol_max_table_size) do
pending_minimum_resize =
case table.pending_minimum_resize do
nil -> new_protocol_max_table_size
current -> min(current, new_protocol_max_table_size)
end
%{
evict_to_size(table, new_protocol_max_table_size)
| protocol_max_table_size: new_protocol_max_table_size,
max_table_size: new_protocol_max_table_size,
pending_minimum_resize: pending_minimum_resize
}
end
def dynamic_resize(%__MODULE__{} = table, new_max_table_size) do
%{
evict_to_size(table, new_max_table_size)
| max_table_size: new_max_table_size
}
end
@doc """
Returns (and clears) any pending resize events on the table which will need to be signalled to
the decoder via dynamic table size update messages. Intended to be called at the start of any
block encode to prepend such dynamic table size update(s) as needed. The value of
`pending_minimum_resize` indicates the smallest maximum size of this table which has not yet
been signalled to the decoder, and is always included in the list returned if it is set.
Additionally, if the current max table size is larger than this value, it is also included int
the list, per https://www.rfc-editor.org/rfc/rfc7541#section-4.2
"""
def pop_pending_resizes(%__MODULE__{pending_minimum_resize: nil} = table), do: {table, []}
def pop_pending_resizes(%__MODULE__{} = table) do
pending_resizes =
if table.max_table_size > table.pending_minimum_resize,
do: [table.pending_minimum_resize, table.max_table_size],
else: [table.pending_minimum_resize]
{%{table | pending_minimum_resize: nil}, pending_resizes}
end
# Removes records as necessary to have the total size of entries within the table be less than
# or equal to the specified value. Does not change the table's max size.
defp evict_to_size(%__MODULE__{size: size} = table, new_size) when size <= new_size, do: table
defp evict_to_size(%__MODULE__{entries: entries, size: size} = table, new_size) do
{new_entries_reversed, new_size} =
evict_towards_size(Enum.reverse(entries), size, new_size)
%{
table
| entries: Enum.reverse(new_entries_reversed),
size: new_size,
length: length(new_entries_reversed)
}
end
defp evict_towards_size([{name, value} | rest], size, max_target_size) do
new_size = size - entry_size(name, value)
if new_size <= max_target_size do
{rest, new_size}
else
evict_towards_size(rest, new_size, max_target_size)
end
end
defp evict_towards_size([], 0, _max_target_size) do
{[], 0}
end
defp entry_size(name, value) do
byte_size(name) + byte_size(value) + 32
end
# Made public to be used in tests.
@doc false
def __static_table__() do
@static_table
end
end

View File

@@ -0,0 +1,89 @@
defmodule HPAX.Types do
@moduledoc false
import Bitwise, only: [<<<: 2]
alias HPAX.Huffman
# This is used as a macro and not an inlined function because we want to be able to use it in
# guards.
defmacrop power_of_two(n) do
quote do: 1 <<< unquote(n)
end
## Encoding
@spec encode_integer(non_neg_integer(), 1..8) :: bitstring()
def encode_integer(integer, prefix)
def encode_integer(integer, prefix) when integer < power_of_two(prefix) - 1 do
<<integer::size(prefix)>>
end
def encode_integer(integer, prefix) do
initial = power_of_two(prefix) - 1
remaining = integer - initial
<<initial::size(prefix), encode_remaining_integer(remaining)::binary>>
end
defp encode_remaining_integer(remaining) when remaining >= 128 do
first = rem(remaining, 128) + 128
<<first::8, encode_remaining_integer(div(remaining, 128))::binary>>
end
defp encode_remaining_integer(remaining) do
<<remaining::8>>
end
@spec encode_binary(binary(), boolean()) :: iodata()
def encode_binary(binary, huffman?) do
binary = if huffman?, do: Huffman.encode(binary), else: binary
huffman_bit = if huffman?, do: 1, else: 0
binary_size = encode_integer(byte_size(binary), 7)
[<<huffman_bit::1, binary_size::bitstring>>, binary]
end
## Decoding
@spec decode_integer(bitstring, 1..8) :: {:ok, non_neg_integer(), binary()} | :error
def decode_integer(bitstring, prefix) when is_bitstring(bitstring) and prefix in 1..8 do
with <<value::size(prefix), rest::binary>> <- bitstring do
if value < power_of_two(prefix) - 1 do
{:ok, value, rest}
else
decode_remaining_integer(rest, value, 0)
end
else
_ -> :error
end
end
defp decode_remaining_integer(<<0::1, value::7, rest::binary>>, int, m) do
{:ok, int + (value <<< m), rest}
end
defp decode_remaining_integer(<<1::1, value::7, rest::binary>>, int, m) do
decode_remaining_integer(rest, int + (value <<< m), m + 7)
end
defp decode_remaining_integer(_, _, _) do
:error
end
@spec decode_binary(binary) :: {:ok, binary(), binary()} | :error
def decode_binary(binary) when is_binary(binary) do
with <<huffman_bit::1, rest::bitstring>> <- binary,
{:ok, length, rest} <- decode_integer(rest, 7),
<<contents::binary-size(length), rest::binary>> <- rest do
contents =
case huffman_bit do
0 -> contents
1 -> Huffman.decode(contents)
end
{:ok, contents, rest}
else
_ -> :error
end
end
end