nhttp_hpack (nhttp_lib v1.2.1)

View Source

HPACK header compression for HTTP/2 (RFC 7541).

This module implements the HPACK header compression format used by HTTP/2. It provides stateful encoding and decoding of header fields using static and dynamic tables.

Usage

{ok, EncState0} = nhttp_hpack:new(),
{ok, DecState0} = nhttp_hpack:new(),

Headers = [{<<":method">>, <<"GET">>}, {<<":path">>, <<"/">>}],
{ok, HeaderBlock, EncState1} = nhttp_hpack:encode(Headers, EncState0),

{ok, DecodedHeaders, DecState1} = nhttp_hpack:decode(HeaderBlock, DecState0).

Field names on encode

encode/2 and encode/3 write every field name in lowercase. RFC 9113 §8.2 requires that a field name is converted to lowercase when an HTTP/2 message is constructed, and RFC 9113 §8.2.1 makes an uppercase name on the wire malformed. The conversion is silent: the return type does not change, and the caller reads no report of it.

A name that already holds no octet in 0x41-0x5A costs one scan and no allocation. The lowercase name is what the static table lookup reads, what the literal representation writes and what the dynamic table holds, so the octets in the table and the octets on the wire agree.

Field validity on decode

Every field name and every field value that arrives as a literal is read against the minimal rule of RFC 9113 §8.2.1. A block that carries such a field returns {invalid_field, Reason, NewState}, which is apart from the {error, Reason} of an HPACK failure: RFC 9113 §8.1.1 makes a malformed field a stream error, where RFC 9113 §4.3 makes a decode failure a connection error.

The block runs to its end either way. RFC 9113 §4.3 makes an endpoint decompress a field block even when it discards the frames, so NewState carries every dynamic table update that the block asks for. A decoder that skips the update of a refused field falls out of step with the peer encoder, and every later block on that connection then decodes to the wrong field.

An entry therefore holds the verdict on its field, read once at insert. A field that arrives by index carries that verdict out again and needs no second read of the octets. The verdict names the part that failed, so a field that reuses the name by index drops a verdict against the value and reads a fresh value from the wire.

Indexing policy

new/2 takes a policy that names the fields the encoder keeps out of the dynamic table. RFC 7541 §6.2 gives three literal representations, and the policy selects one for each field name:

  • A name in neither list uses literal with incremental indexing (§6.2.1). The field enters the dynamic table of both endpoints.
  • A name in no_index uses literal without indexing (§6.2.2). The field enters no table on this hop. Use it for a value that differs on every request. Such a value costs a table entry that no later request reuses.
  • A name in never_index uses literal never indexed (§6.2.3). Every intermediary keeps this form on every later hop. Use it for a secret: RFC 7541 §7.1 shows how an attacker who places values in the same block and reads the compressed size recovers an indexed secret, and §7.1.3 names this form as the mitigation.

A name in both lists resolves to never_index. The policy holds every name in lowercase. A name with a static entry keeps its static name reference under both literal forms. The default of new/0 and new/1 is the empty policy, which changes no byte of output.

The dynamic table evicts in insertion order (§4.4). One field that differs on every request and still inserts pushes every constant entry out once the table fills, and the encoder then re-emits the constant fields as literals. A no_index list must therefore name every such field.

A client that shares a connection between principals, or that compresses a value an attacker controls in the same block as a credential, puts authorization, cookie and proxy-authorization in never_index. A client with one principal on a TLS connection and no attacker-controlled value in the block can leave a constant credential indexed and put the fields that differ on every request in no_index.

Summary

Types

The outcome of a header block decode.

A field that breaks the minimal rule of RFC 9113 §8.2.1.

The field names that the encoder keeps out of the dynamic table.

The dynamic table of one HPACK endpoint.

Functions

Decode a header block. See decode/3 for the invalid_field return.

Decode a header block, aborting with {error, header_list_too_large} once the cumulative decoded list size exceeds max_list_size. The check matches the RFC 9113 §10.5.1 octet count (name + value + 32 per entry).

Encode headers without Huffman encoding.

Encode headers with options.

Check if the dynamic table is empty.

Create a new HPACK state with default max size (4096 bytes).

Create a new HPACK state with specified max size.

Create a new HPACK encoder state with a max size and an indexing policy.

Update the maximum table size (from SETTINGS_HEADER_TABLE_SIZE). Immediately evicts entries if the new size is smaller than current table size.

Get the number of live entries in the dynamic table.

Get the current dynamic table size in bytes.

Types

decode_error()

-type decode_error() ::
          dynamic_table_size_exceeded | invalid_table_index | integer_overflow | invalid_huffman |
          incomplete_header_block | header_list_too_large |
          field_error().

decode_opts()

-type decode_opts() :: #{max_list_size => pos_integer() | infinity}.

decode_result()

-type decode_result() ::
          {ok, headers(), state()} | {invalid_field, field_error(), state()} | {error, decode_error()}.

The outcome of a header block decode.

{invalid_field, Reason, NewState} reports a field that breaks RFC 9113 §8.2.1. The block decompressed, and NewState carries every dynamic table update that the block asks for, because RFC 9113 §4.3 makes an endpoint decompress a field block even when it discards the frames. The caller keeps NewState and treats the message as malformed, a stream error of type PROTOCOL_ERROR (RFC 9113 §8.1.1).

{error, Reason} reports a decode failure. The block did not decompress, the state is unusable, and RFC 9113 §4.3 makes this a connection error of type COMPRESSION_ERROR.

encode_opts()

-type encode_opts() :: #{huffman => boolean()}.

field_error()

-type field_error() :: uppercase_header_name | invalid_header_name | invalid_header_value.

A field that breaks the minimal rule of RFC 9113 §8.2.1.

uppercase_header_name names the case that the RFC lists apart from the other invalid name characters.

headers()

-type headers() :: [{Name :: binary(), Value :: binary()}].

index_policy()

-type index_policy() :: #{never_index => [binary()], no_index => [binary()]}.

The field names that the encoder keeps out of the dynamic table.

no_index selects literal without indexing (RFC 7541 §6.2.2) and never_index selects literal never indexed (RFC 7541 §6.2.3). A name in neither list uses literal with incremental indexing (RFC 7541 §6.2.1). See the module documentation for the choice between the two lists.

state()

-opaque state()

The dynamic table of one HPACK endpoint.

full_index and name_index hold keys for live entries only. An eviction removes the keys that bind to the evicted sequence, so the two indexes stay bounded by the table size.

policy holds the lowercased names of index_policy/0 with the literal form each one selects. The encoder reads it on the two literal arms only.

Functions

decode(Data, State)

-spec decode(Data :: binary(), State :: state()) -> decode_result().

Decode a header block. See decode/3 for the invalid_field return.

decode(Data, State, Opts)

-spec decode(Data :: binary(), State :: state(), Opts :: decode_opts()) -> decode_result().

Decode a header block, aborting with {error, header_list_too_large} once the cumulative decoded list size exceeds max_list_size. The check matches the RFC 9113 §10.5.1 octet count (name + value + 32 per entry).

encode(Headers, State)

-spec encode(Headers :: headers(), State :: state()) -> {ok, iodata(), state()}.

Encode headers without Huffman encoding.

encode(Headers, State, Opts)

-spec encode(Headers :: headers(), State :: state(), Opts :: encode_opts()) -> {ok, iodata(), state()}.

Encode headers with options.

is_empty(State)

-spec is_empty(State :: state()) -> boolean().

Check if the dynamic table is empty.

new()

-spec new() -> {ok, state()}.

Create a new HPACK state with default max size (4096 bytes).

new(MaxSize)

-spec new(MaxSize :: non_neg_integer()) -> {ok, state()}.

Create a new HPACK state with specified max size.

new(MaxSize, Policy)

-spec new(MaxSize :: non_neg_integer(), Policy :: index_policy()) -> {ok, state()}.

Create a new HPACK encoder state with a max size and an indexing policy.

The policy names the fields that never enter the dynamic table. See the module documentation for the two literal forms and the sets that fit a deployment. A name in both lists resolves to never_index, and every name is lowercased.

set_max_table_size(MaxSize, State)

-spec set_max_table_size(MaxSize :: non_neg_integer(), State :: state()) -> {ok, state()}.

Update the maximum table size (from SETTINGS_HEADER_TABLE_SIZE). Immediately evicts entries if the new size is smaller than current table size.

table_entries(State)

-spec table_entries(State :: state()) -> non_neg_integer().

Get the number of live entries in the dynamic table.

table_size(State)

-spec table_size(State :: state()) -> non_neg_integer().

Get the current dynamic table size in bytes.