Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/hpack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
HTTP/2 header encoding for Python.
"""

from __future__ import annotations

from .exceptions import HPACKDecodingError, HPACKError, InvalidTableIndex, InvalidTableIndexError, InvalidTableSizeError, OversizedHeaderListError
Expand Down
4 changes: 2 additions & 2 deletions src/hpack/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Exceptions used in hpack.
"""

from __future__ import annotations


Expand All @@ -10,21 +11,20 @@ class HPACKError(Exception):
"""



class HPACKDecodingError(HPACKError):
"""
An error has been encountered while performing HPACK decoding.
"""



class InvalidTableIndexError(HPACKDecodingError):
"""
An invalid table index was received.

.. versionadded:: 4.1.0
"""


class InvalidTableIndex(InvalidTableIndexError): # noqa: N818
"""
An invalid table index was received.
Expand Down
58 changes: 37 additions & 21 deletions src/hpack/hpack.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Implements the HPACK header compression algorithm as detailed by RFC 7541.
"""

from __future__ import annotations

import logging
Expand All @@ -9,7 +10,7 @@
from .exceptions import HPACKDecodingError, InvalidTableSizeError, OversizedHeaderListError
from .huffman import HuffmanEncoder
from .huffman_constants import REQUEST_CODES, REQUEST_CODES_LENGTH
from .huffman_table import decode_huffman
from .huffman_table import HuffmanDecoder
from .struct import HeaderTuple, HeaderWeaklyTyped, NeverIndexedHeaderTuple
from .table import HeaderTable, table_entry_size

Expand All @@ -25,11 +26,11 @@
# Precompute 2^i for 1-8 for use in prefix calcs.
# Zero index is not used but there to save a subtraction
# as prefix numbers are not zero indexed.
_PREFIX_BIT_MAX_NUMBERS = [(2 ** i) - 1 for i in range(9)]
_PREFIX_BIT_MAX_NUMBERS = [(2**i) - 1 for i in range(9)]

# We default the maximum header list we're willing to accept to 64kB. That's a
# lot of headers, but if applications want to raise it they can do.
DEFAULT_MAX_HEADER_LIST_SIZE = 2 ** 16
DEFAULT_MAX_HEADER_LIST_SIZE = 2**16


def _unicode_if_needed(header: HeaderWeaklyTyped, raw: bool) -> HeaderTuple:
Expand Down Expand Up @@ -90,7 +91,7 @@ def decode_integer(data: bytes, prefix_bits: int) -> tuple[int, int]:
max_number = _PREFIX_BIT_MAX_NUMBERS[prefix_bits]
index = 1
shift = 0
mask = (0xFF >> (8 - prefix_bits))
mask = 0xFF >> (8 - prefix_bits)

try:
number = data[0] & mask
Expand All @@ -115,8 +116,7 @@ def decode_integer(data: bytes, prefix_bits: int) -> tuple[int, int]:
return number, index


def _dict_to_iterable(header_dict: dict[bytes | str, bytes | str]) \
-> Iterable[tuple[bytes | str, bytes | str]]:
def _dict_to_iterable(header_dict: dict[bytes | str, bytes | str]) -> Iterable[tuple[bytes | str, bytes | str]]:
"""
Converts a dictionary to an iterable of key-value tuples. This is a
HPACK-specific function because it pulls "special-headers" out first and
Expand Down Expand Up @@ -152,10 +152,13 @@ class Encoder:
HTTP/2 header blocks.
"""

__slots__ = ("header_table", "huffman_coder", "table_size_changes")

def __init__(self) -> None:
self.header_table = HeaderTable()
self.huffman_coder = HuffmanEncoder(
REQUEST_CODES, REQUEST_CODES_LENGTH,
REQUEST_CODES,
REQUEST_CODES_LENGTH,
)
self.table_size_changes: list[int] = []

Expand All @@ -172,13 +175,12 @@ def header_table_size(self, value: int) -> None:
if self.header_table.resized:
self.table_size_changes.append(value)

def encode(self,
headers: Iterable[\
HeaderTuple | \
tuple[bytes | str, bytes | str] | \
tuple[bytes | str, bytes | str, bool | None]] | \
dict[bytes | str, bytes | str],
huffman: bool = True) -> bytes:
def encode(
self,
headers: Iterable[HeaderTuple | tuple[bytes | str, bytes | str] | tuple[bytes | str, bytes | str, bool | None]]
| dict[bytes | str, bytes | str],
huffman: bool = True,
) -> bytes:
"""
Takes a set of headers and encodes them into a HPACK-encoded header
block.
Expand Down Expand Up @@ -323,7 +325,10 @@ def add(self, to_add: tuple[bytes, bytes], sensitive: bool, huffman: bool = Fals
# indexing since they just take space in the table and
# pushed out other valuable headers.
encoded = self._encode_indexed_literal(
index, value, indexbit, huffman,
index,
value,
indexbit,
huffman,
)
if not sensitive:
self.header_table.add(name, value)
Expand Down Expand Up @@ -391,7 +396,8 @@ def _encode_table_size_change(self) -> bytes:
b = encode_integer(size_bytes, 5)
b[0] |= 0x20
block += bytes(b)
self.table_size_changes = []
# use clear instead of adding a new reference
self.table_size_changes.clear()
return block


Expand All @@ -417,6 +423,12 @@ class Decoder:
:type max_header_list_size: ``int``
"""

__slots__ = (
"header_table",
"max_header_list_size",
"max_allowed_table_size",
"decoder"
)
def __init__(self, max_header_list_size: int = DEFAULT_MAX_HEADER_LIST_SIZE) -> None:
self.header_table = HeaderTable()

Expand Down Expand Up @@ -445,6 +457,10 @@ def __init__(self, max_header_list_size: int = DEFAULT_MAX_HEADER_LIST_SIZE) ->
#: to confirm that it fits in this size.
self.max_allowed_table_size = self.header_table.maxsize

#: Threadsafe attribute allowing for multiple threads
#: to share one large huffman-table without extereme lagging.
self.decoder = HuffmanDecoder()

@property
def header_table_size(self) -> int:
"""
Expand Down Expand Up @@ -608,26 +624,26 @@ def _decode_literal(self, data: bytes, should_index: bool) -> tuple[HeaderTuple,
data = data[1:]

length, consumed = decode_integer(data, 7)
name = data[consumed:consumed + length]
name = data[consumed : consumed + length]
if len(name) != length:
msg = "Truncated header block"
raise HPACKDecodingError(msg)

if data[0] & 0x80:
name = decode_huffman(name)
name = self.decoder.decode(name)
total_consumed = consumed + length + 1 # Since we moved forward 1.

data = data[consumed + length:]
data = data[consumed + length :]

# The header value is definitely length-based.
length, consumed = decode_integer(data, 7)
value = data[consumed:consumed + length]
value = data[consumed : consumed + length]
if len(value) != length:
msg = "Truncated header block"
raise HPACKDecodingError(msg)

if data[0] & 0x80:
value = decode_huffman(value)
value = self.decoder.decode(value)

# Updated the total consumed length.
total_consumed += length + consumed
Expand Down
10 changes: 6 additions & 4 deletions src/hpack/huffman.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
An implementation of a bitwise prefix tree specially built for decoding
Huffman-coded content where we already know the Huffman table.
"""

from __future__ import annotations
from array import array


class HuffmanEncoder:
Expand All @@ -11,7 +13,9 @@ class HuffmanEncoder:
HPACK specification.
"""

def __init__(self, huffman_code_list: list[int], huffman_code_list_lengths: list[int]) -> None:
__slots__ = ("huffman_code_list", "huffman_code_list_lengths")

def __init__(self, huffman_code_list: array, huffman_code_list_lengths: array) -> None:
self.huffman_code_list = huffman_code_list
self.huffman_code_list_lengths = huffman_code_list_lengths

Expand All @@ -32,9 +36,7 @@ def encode(self, bytes_to_encode: bytes | None) -> bytes:
# handle this cleanly, just use a single giant integer.
for byte in bytes_to_encode:
bin_int_len = self.huffman_code_list_lengths[byte]
bin_int = self.huffman_code_list[byte] & (
2 ** (bin_int_len + 1) - 1
)
bin_int = self.huffman_code_list[byte] & (2 ** (bin_int_len + 1) - 1)
final_num <<= bin_int_len
final_num |= bin_int
final_int_len += bin_int_len
Expand Down
Loading