# -*- coding: utf-8 -*-
"""HTTP/2 - Hypertext Transfer Protocol
==========================================
.. module:: pcapkit.protocols.application.httpv2
:mod:`pcapkit.protocols.application.httpv2` contains
:class:`~pcapkit.protocols.application.httpv2.HTTP`
only, which implements extractor for Hypertext Transfer
Protocol (HTTP/2) [*]_, whose structure is described as
below:
======= ========= ===================== ==========================
Octets Bits Name Description
======= ========= ===================== ==========================
0 0 ``http.length`` Payload Length
3 24 ``http.type`` Type
4 32 ``http.flags`` Flags
5 40 Reserved
5 41 ``http.sid`` Stream Identifier
9 72 ``http.payload`` Frame Payload
======= ========= ===================== ==========================
.. [*] https://en.wikipedia.org/wiki/HTTP/2
.. versionchanged:: 1.5.0
A frame's Length counts its payload alone, not the 9-octet frame header,
as :rfc:`9113#section-4.1` defines it, and the payload is read to that
Length rather than to the end of the buffer (:issue:`1121`,
:issue:`1243`). A frame rebuilds byte for byte: ``END_STREAM``, undefined
flag bits, the reserved bits, a zero Pad Length, duplicate SETTINGS
entries and the payload of an unpadded DATA, HEADERS or PUSH_PROMISE frame
are kept (:issue:`652`, :issue:`668`, :issue:`1244`, :issue:`1245`,
:issue:`1246`, :issue:`1247`).
"""
import collections
from typing import TYPE_CHECKING, cast
from pcapkit.const.http.error_code import ErrorCode as Enum_ErrorCode
from pcapkit.const.http.frame import Frame as Enum_Frame
from pcapkit.const.http.setting import Setting as Enum_Setting
from pcapkit.corekit.multidict import OrderedMultiDict
from pcapkit.protocols.application.http import HTTP as HTTPBase
from pcapkit.protocols.data.application.httpv2 import HTTP as Data_HTTP
from pcapkit.protocols.data.application.httpv2 import ContinuationFrame as Data_ContinuationFrame
from pcapkit.protocols.data.application.httpv2 import \
ContinuationFrameFlags as Data_ContinuationFrameFlags
from pcapkit.protocols.data.application.httpv2 import DataFrame as Data_DataFrame
from pcapkit.protocols.data.application.httpv2 import DataFrameFlags as Data_DataFrameFlags
from pcapkit.protocols.data.application.httpv2 import Flags as Data_Flags
from pcapkit.protocols.data.application.httpv2 import GoawayFrame as Data_GoawayFrame
from pcapkit.protocols.data.application.httpv2 import HeadersFrame as Data_HeadersFrame
from pcapkit.protocols.data.application.httpv2 import HeadersFrameFlags as Data_HeadersFrameFlags
from pcapkit.protocols.data.application.httpv2 import PingFrame as Data_PingFrame
from pcapkit.protocols.data.application.httpv2 import PingFrameFlags as Data_PingFrameFlags
from pcapkit.protocols.data.application.httpv2 import PriorityFrame as Data_PriorityFrame
from pcapkit.protocols.data.application.httpv2 import PushPromiseFrame as Data_PushPromiseFrame
from pcapkit.protocols.data.application.httpv2 import \
PushPromiseFrameFlags as Data_PushPromiseFrameFlags
from pcapkit.protocols.data.application.httpv2 import RSTStreamFrame as Data_RSTStreamFrame
from pcapkit.protocols.data.application.httpv2 import Settings as Data_Settings
from pcapkit.protocols.data.application.httpv2 import SettingsFrame as Data_SettingsFrame
from pcapkit.protocols.data.application.httpv2 import SettingsFrameFlags as Data_SettingsFrameFlags
from pcapkit.protocols.data.application.httpv2 import UnassignedFrame as Data_UnassignedFrame
from pcapkit.protocols.data.application.httpv2 import WindowUpdateFrame as Data_WindowUpdateFrame
from pcapkit.protocols.schema.application.httpv2 import HTTP as Schema_HTTP
from pcapkit.protocols.schema.application.httpv2 import \
ContinuationFrame as Schema_ContinuationFrame
from pcapkit.protocols.schema.application.httpv2 import DataFrame as Schema_DataFrame
from pcapkit.protocols.schema.application.httpv2 import FrameType as Schema_FrameType
from pcapkit.protocols.schema.application.httpv2 import GoawayFrame as Schema_GoawayFrame
from pcapkit.protocols.schema.application.httpv2 import HeadersFrame as Schema_HeadersFrame
from pcapkit.protocols.schema.application.httpv2 import PingFrame as Schema_PingFrame
from pcapkit.protocols.schema.application.httpv2 import PriorityFrame as Schema_PriorityFrame
from pcapkit.protocols.schema.application.httpv2 import PushPromiseFrame as Schema_PushPromiseFrame
from pcapkit.protocols.schema.application.httpv2 import RSTStreamFrame as Schema_RSTStreamFrame
from pcapkit.protocols.schema.application.httpv2 import SettingPair as Schema_SettingPair
from pcapkit.protocols.schema.application.httpv2 import SettingsFrame as Schema_SettingsFrame
from pcapkit.protocols.schema.application.httpv2 import UnassignedFrame as Schema_UnassignedFrame
from pcapkit.protocols.schema.application.httpv2 import \
WindowUpdateFrame as Schema_WindowUpdateFrame
from pcapkit.protocols.schema.schema import Schema
from pcapkit.utilities.exceptions import ProtocolError
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn
if TYPE_CHECKING:
from enum import IntEnum as StdlibEnum
from typing import Any, Callable, DefaultDict, Optional, Tuple, Type
from aenum import IntEnum as AenumEnum
from mypy_extensions import DefaultArg, KwArg, NamedArg
from typing_extensions import Literal
from pcapkit.protocols.protocol import ProtocolBase
Flags = Schema_FrameType.Flags
FrameParser = Callable[[Schema_FrameType, NamedArg(Schema_HTTP, 'header')], Data_HTTP]
# NB: ``Flags`` is bound just above, so it goes in unquoted. Quoted, it would
# leave a bare ``ForwardRef('Flags')`` inside the alias, which every *other*
# module that spells ``FrameConstructor`` in an annotation would have to
# resolve in its own namespace -- where the name does not exist.
FrameConstructor = Callable[[DefaultArg(Optional[Data_HTTP]),
KwArg(Any)], Tuple[Schema_FrameType, Flags]]
__all__ = ['HTTP']
[docs]
class HTTP(HTTPBase[Data_HTTP, Schema_HTTP],
schema=Schema_HTTP, data=Data_HTTP):
"""This class implements Hypertext Transfer Protocol (HTTP/2).
This class supports parsing of the following HTTP/2 frames,
which are directly mapped to the :class:`pcapkit.const.http.frame.Frame`
enumeration:
.. list-table::
:header-rows: 1
* - Frame Code
- Frame Parser
- Frame Constructor
* - :attr:`~pcapkit.const.http.frame.Frame.DATA`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_data`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_data`
* - :attr:`~pcapkit.const.http.frame.Frame.HEADERS`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_headers`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_headers`
* - :attr:`~pcapkit.const.http.frame.Frame.PRIORITY`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_priority`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_priority`
* - :attr:`~pcapkit.const.http.frame.Frame.RST_STREAM`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_rst_stream`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_rst_stream`
* - :attr:`~pcapkit.const.http.frame.Frame.SETTINGS`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_settings`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_settings`
* - :attr:`~pcapkit.const.http.frame.Frame.PUSH_PROMISE`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_push_promise`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_push_promise`
* - :attr:`~pcapkit.const.http.frame.Frame.PING`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_ping`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_ping`
* - :attr:`~pcapkit.const.http.frame.Frame.GOAWAY`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_goaway`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_goaway`
* - :attr:`~pcapkit.const.http.frame.Frame.WINDOW_UPDATE`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_window_update`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_window_update`
* - :attr:`~pcapkit.const.http.frame.Frame.CONTINUATION`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._read_http_continuation`
- :meth:`~pcapkit.protocols.application.httpv2.HTTP._make_http_continuation`
"""
##########################################################################
# Defaults.
##########################################################################
#: Frame code to method mapping. A :obj:`str` value names the
#: ``_read_http_${name}`` and ``_make_http_${name}`` methods, which fall back
#: to ``_read_http_none`` and ``_make_http_none`` when not defined, as an
#: unregistered code does; any other value is a ``(parser, constructor)``
#: pair of callables.
__frame__ = collections.defaultdict(
lambda: 'none',
{
Enum_Frame.DATA: 'data', # DATA
Enum_Frame.HEADERS: 'headers', # HEADERS
Enum_Frame.PRIORITY: 'priority', # PRIORITY
Enum_Frame.RST_STREAM: 'rst_stream', # RST_STREAM
Enum_Frame.SETTINGS: 'settings', # SETTINGS
Enum_Frame.PUSH_PROMISE: 'push_promise', # PUSH_PROMISE
Enum_Frame.PING: 'ping', # PING
Enum_Frame.GOAWAY: 'goaway', # GOAWAY
Enum_Frame.WINDOW_UPDATE: 'window_update', # WINDOW_UPDATE
Enum_Frame.CONTINUATION: 'continuation', # CONTINUATION
},
) # type: DefaultDict[Enum_Frame | int, str | tuple[FrameParser, FrameConstructor]]
##########################################################################
# Properties.
##########################################################################
@property
def alias(self) -> 'Literal["HTTP/2"]':
"""Acronym of current protocol."""
return 'HTTP/2'
@property
def length(self) -> 'Literal[9]':
"""Header length of current protocol."""
return 9
@property
def version(self) -> 'Literal["2"]':
"""Version of current protocol."""
return '2'
##########################################################################
# Methods.
##########################################################################
def unpack(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_HTTP':
"""Unpack (parse) packet data.
Args:
length: Length of packet data.
**kwargs: Arbitrary keyword arguments.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
Notes:
This guards ahead of :meth:`Schema.unpack
<pcapkit.protocols.schema.schema.Schema.unpack>` rather than
leaving the check to :meth:`read`, because a buffer shorter than the
fixed 9-octet header cannot be parsed meaningfully.
``pkt['__length__']`` there is decremented by each field's
*nominal* width regardless of how many octets the buffer had, so it
goes negative and the frame payload's length-derived fields (e.g. an
unpadded ``DATA`` frame's ``data: BytesField(length=lambda pkt:
pkt['__length__'])``) resolve to a negative width. Rejecting here
keeps such a frame from reaching that arithmetic. An inner field can
still drive it negative on a buffer that clears nine (e.g. a
``GOAWAY`` frame at 9-16 octets, whose fixed ``stream`` and
``error`` fields alone consume eight); the schema layer raises
:exc:`ProtocolError` for that itself.
``length`` is resolved against :func:`len` only for *this*
method's own check, and the *original* argument -- ``None``
included -- is what is forwarded to :meth:`Protocol.unpack
<pcapkit.protocols.protocol.ProtocolBase.unpack>`. Collapsing
``None`` to a concrete ``0`` would change what
:meth:`Schema.unpack`'s own ``prepare`` decorator does with an
exhausted stream: it raises
:exc:`~pcapkit.utilities.exceptions.StreamEOFError` (an
:exc:`EOFError`, the documented "no more packets" signal) only when
the *caller* left ``length`` unspecified, and a resolved ``0``
would report an exhausted stream as a malformed packet. A
*non-zero* short buffer is malformed either way, so it is rejected
here whether or not ``length`` was given.
"""
if length is None:
# ``0 < ... < 9`` rather than ``... < 9``: an exhausted stream
# (``len(self) == 0``) is left alone, so ``length`` reaches
# :meth:`Protocol.unpack <pcapkit.protocols.protocol.ProtocolBase.unpack>`
# still ``None`` and its ``prepare`` decorator raises
# :exc:`StreamEOFError` as documented above; only a *non-empty*
# short stream is rejected as malformed here.
if 0 < len(self) < 9:
raise ProtocolError(f'HTTP/2: invalid format, packet ({len(self)} octet(s)) '
f'shorter than the 9-octet frame header')
elif length < 9:
# An *explicit* length, zero included, is a caller assertion about
# how much data this frame has -- not the "figure it out"
# ``None`` above -- so every value under nine is rejected the same
# way, with no exhaustion signal to preserve.
raise ProtocolError(f'HTTP/2: invalid format, packet ({length} octet(s)) '
f'shorter than the 9-octet frame header')
return super().unpack(length, **kwargs)
[docs]
def read(self, length: 'Optional[int]' = None, **kwargs: 'Any') -> 'Data_HTTP':
"""Read Hypertext Transfer Protocol (HTTP/2).
Structure of HTTP/2 packet [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+=+=============================================================+
| Frame Payload (0...) ...
+---------------------------------------------------------------+
Args:
length: Length of packet data.
**kwargs: Arbitrary keyword arguments.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if length is None:
length = len(self)
schema = self.__header__
# NOTE: ``schema.length`` is the *declared* length off the wire -- the
# 24-bit field a hostile or truncated capture controls outright -- so
# checking only it would let a frame whose buffer holds far fewer octets
# than it claims report a length nothing backs (e.g. a nine-octet buffer
# declaring 16777215). ``length`` is the number of octets actually
# available for this frame (``Protocol.__len__`` returns
# ``len(self._data)``, and an explicit ``length`` means the same), so it
# has to clear the 9-octet header *and* back the declared value. The
# declared value counts the frame payload only, the header excluded
# (:rfc:`9113#section-4.1`), so ``length`` must hold both.
if length < 9 or schema.length + 9 > length:
raise ProtocolError(f'HTTP/2: [Type {schema.type}] invalid format')
if schema.type in (Enum_Frame.SETTINGS, Enum_Frame.PING) and schema.stream['sid'] != 0:
raise ProtocolError(f'HTTP/2: [Type {schema.type}] invalid format')
name = self._lookup_registry(self.__frame__, schema.type)
if isinstance(name, str):
meth_name = f'_read_http_{name}'
meth = cast('FrameParser',
getattr(self, meth_name, self._read_http_none))
else:
meth = name[0]
http = meth(schema.frame, header=schema)
# The frame ends at its declared Length; whatever follows it in the
# buffer (e.g. the next frame) is the undissected remainder.
return self._decode_next_layer(http, -1, length - 9 - schema.length)
[docs]
def make(self, # type: ignore[override]
type: 'Enum_Frame | StdlibEnum | AenumEnum | str | int' = Enum_Frame.DATA,
type_default: 'Optional[int]' = None,
type_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
type_reversed: 'bool' = False,
flags: 'Flags' = 0, # type: ignore[assignment]
reserved: 'int' = 0,
sid: 'int' = 0,
frame: 'bytes | Data_HTTP | Schema_FrameType | dict[str, Any]' = b'',
payload: 'bytes | ProtocolBase | Schema' = b'',
**kwargs: 'Any') -> 'Schema_HTTP':
"""Make (construct) packet data.
Args:
type: Type of HTTP/2 frame.
type_default: Default frame type.
type_namespace: Namespace of frame type.
type_reversed: Whether to reverse the namespace.
flags: Flags of HTTP/2 frame, combined with those the frame
constructor derives from ``frame``.
reserved: Reserved bit of the frame header.
sid: Stream ID of HTTP/2 frame.
frame: Frame data of HTTP/2 frame.
payload: Octets after the frame, e.g. the next frame.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed packet data.
.. versionchanged:: 1.5.0
``reserved`` is a new parameter before ``sid``, so a 1.4.x
positional argument meant for ``sid`` or ``frame`` lands one
parameter early (:issue:`1246`).
"""
type_val = cast('Enum_Frame',
self._make_index(type, type_default, namespace=type_namespace,
reversed=type_reversed, pack=False))
if isinstance(frame, bytes):
frame_val = frame # type: bytes | Schema_FrameType
elif isinstance(frame, (dict, Data_HTTP)):
name = self._lookup_registry(self.__frame__, type_val)
if isinstance(name, str):
meth_name = f'_make_http_{name}'
meth = cast('FrameConstructor',
getattr(self, meth_name, self._make_http_none))
else:
meth = name[1]
if isinstance(frame, dict):
frame_val, frame_flags = meth(**frame)
else:
frame_val, frame_flags = meth(frame)
flags = cast('Flags', int(flags) | int(frame_flags))
elif isinstance(frame, Schema):
frame_val = frame
else:
raise ProtocolError(f'HTTP/2: [Type {type_val}] invalid format')
flags_val = {} # type: dict[str, int]
for bit in range(8):
flags_val[f'bit_{bit}'] = (int(flags) & (1 << bit)) >> bit
length = self._make_http_length(frame_val, flags)
return Schema_HTTP(
length=length,
type=type_val,
flags=flags_val, # type: ignore[arg-type]
stream={
'reserved': reserved,
'sid': sid,
},
frame=frame_val,
payload=payload,
)
def pack(self, **kwargs: 'Any') -> 'bytes':
"""Pack (construct) packet data.
Args:
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed packet data.
Notes:
:meth:`make` takes the frame payload as raw octets or as a built
frame schema as well, and neither carries what the frame readers
need, e.g. ``__flags__``. So while the instance is being
constructed, the header :meth:`make` returns is dropped once packed,
and :meth:`unpack` parses the packed octets back with the frame
type's own schema. Any other call leaves the instance's header as it
was (:issue:`1769`).
"""
data = super().pack(**kwargs)
# NOTE: Constructing, by the test ``ProtocolBase._packing_target`` makes.
if '_info' not in vars(self):
self.__header__ = None # type: ignore[assignment]
return data
@staticmethod
def _make_http_length(frame: 'bytes | Schema_FrameType', flags: 'Flags') -> 'int':
"""Calculate HTTP/2 frame payload length for construction.
Several frame payload schemas use conditional fields keyed by the
containing HTTP/2 header flags, so packing them without that context can
produce incorrect lengths or fail outright.
"""
if isinstance(frame, bytes):
return len(frame)
flags_int = int(flags)
if isinstance(frame, Schema_UnassignedFrame):
return len(frame.data)
if isinstance(frame, Schema_DataFrame):
padded = flags_int & int(Schema_DataFrame.Flags.PADDED)
return len(frame.data) + ((frame.pad_len or 0) + 1 if padded else 0)
if isinstance(frame, Schema_HeadersFrame):
padded = flags_int & int(Schema_HeadersFrame.Flags.PADDED)
priority_len = 5 if flags_int & int(Schema_HeadersFrame.Flags.PRIORITY) else 0
return len(frame.fragment) + priority_len + ((frame.pad_len or 0) + 1 if padded else 0)
if isinstance(frame, Schema_PriorityFrame):
return 5
if isinstance(frame, Schema_RSTStreamFrame):
return 4
if isinstance(frame, Schema_SettingsFrame):
settings = cast('object', frame.settings)
if isinstance(settings, bytes):
return len(settings)
return len(frame.settings) * 6
if isinstance(frame, Schema_PushPromiseFrame):
padded = flags_int & int(Schema_PushPromiseFrame.Flags.PADDED)
return len(frame.fragment) + 4 + ((frame.pad_len or 0) + 1 if padded else 0)
if isinstance(frame, Schema_PingFrame):
return 8
if isinstance(frame, Schema_GoawayFrame):
return len(frame.debug) + 8
if isinstance(frame, Schema_WindowUpdateFrame):
return 4
if isinstance(frame, Schema_ContinuationFrame):
return len(frame.fragment)
return len(frame.pack())
[docs]
@classmethod
def id(cls) -> 'tuple[Literal["HTTP"], Literal["HTTPv2"]]': # type: ignore[override]
"""Index ID of the protocol.
Returns:
Index ID of the protocol.
"""
return (cls.__name__, 'HTTPv2') # type: ignore[return-value]
[docs]
@classmethod
def register_frame(cls, code: 'Enum_Frame', meth: 'str | tuple[FrameParser, FrameConstructor]') -> 'None':
"""Register a frame parser.
Args:
code: HTTP frame type code.
meth: Method name or callable to parse and/or construct the frame.
Warns:
RegistryWarning: If a different entry is already registered for
``code``; it is overwritten. Handing back the entry already
stored, or one equal to it, is a silent no-op. The entry
``code`` shipped with is accepted like any other, so an
override can be undone (:issue:`1362`, :issue:`1363`).
"""
incumbent = cls.__frame__.get(code)
if incumbent is not None and incumbent == meth:
return
if incumbent is not None:
warn(f'HTTP/2: [Type {code}] frame already registered', RegistryWarning)
cls.__frame__[code] = meth
##########################################################################
# Data models.
##########################################################################
def __length_hint__(self) -> 'Literal[9]':
"""Total length of corresponding protocol."""
return 9
##########################################################################
# Utilities.
##########################################################################
[docs]
@classmethod
def _make_data(cls, data: 'Data_HTTP') -> 'dict[str, Any]': # type: ignore[override]
"""Create key-value pairs from ``data`` for protocol construction.
Args:
data: protocol data
Returns:
Key-value pairs for protocol construction.
"""
# NOTE: No ``length``: :meth:`make` computes the frame's Length from the
# frame it builds, and a ``length`` keyword would instead be taken by
# ``ProtocolBase.__init__`` as the number of octets to parse back --
# the payload length alone, nine octets short of the frame.
return {
'type': data.type,
'flags': data.flags.__value__ if data.flags is not None else 0,
'reserved': data.reserved,
'sid': data.sid,
'frame': data,
'payload': cls._make_payload(data),
}
@staticmethod
def _read_http_flags(schema: 'Schema_FrameType') -> 'Data_Flags':
"""Read the flags of an HTTP/2 frame type that defines none.
Args:
schema: Parsed frame schema.
Returns:
Flags with the raw flags octet as ``__value__``.
"""
flags = Data_Flags()
flags.__update__({
'__value__': schema.__flags__,
})
return flags
[docs]
def _read_http_none(self, schema: 'Schema_UnassignedFrame', *,
header: 'Schema_HTTP') -> 'Data_UnassignedFrame':
"""Read HTTP packet with unassigned type.
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
"""
if any(header.flags.values()):
#raise ProtocolError(f'HTTP/2: [Type {frame}] invalid format')
warn(f'HTTP/2: [Type {header.type}] invalid format', ProtocolWarning)
data = Data_UnassignedFrame(
length=header.length,
type=header.type,
flags=self._read_http_flags(schema),
reserved=header.stream['reserved'],
sid=header.stream['sid'],
data=schema.data,
)
return data
[docs]
def _read_http_data(self, schema: 'Schema_DataFrame', *,
header: 'Schema_HTTP') -> 'Data_DataFrame':
"""Read HTTP/2 ``DATA`` frames.
Structure of HTTP/2 ``DATA`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------+-----------------------------------------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
| Data (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
"""
flag = Data_DataFrameFlags(
END_STREAM=bool(header.flags['bit_0']), # bit 0
PADDED=bool(header.flags['bit_3']), # bit 3
)
flag.__update__({
'__value__': schema.__flags__,
})
data = Data_DataFrame(
length=header.length,
type=header.type,
flags=flag,
pad_len=schema.pad_len if flag.PADDED else 0,
reserved=header.stream['reserved'],
sid=header.stream['sid'],
data=schema.data,
padding=schema.padding if flag.PADDED else b'',
)
return data
[docs]
def _read_http_priority(self, schema: 'Schema_PriorityFrame', *,
header: 'Schema_HTTP') -> 'Data_PriorityFrame': # pylint: disable=unused-argument
"""Read HTTP/2 ``PRIORITY`` frames.
Structure of HTTP/2 ``PRIORITY`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+-+-------------------------------------------------------------+
|E| Stream Dependency (31) |
+-+-------------+-----------------------------------------------+
| Weight (8) |
+-+-------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length != 5:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
data = Data_PriorityFrame(
length=header.length,
type=header.type,
flags=self._read_http_flags(schema),
reserved=header.stream['reserved'],
sid=header.stream['sid'],
excl_dependency=bool(schema.stream['exclusive']),
stream_dependency=schema.stream['sid'],
weight=schema.weight + 1,
)
return data
[docs]
def _read_http_rst_stream(self, schema: 'Schema_RSTStreamFrame', *,
header: 'Schema_HTTP') -> 'Data_RSTStreamFrame': # pylint: disable=unused-argument
"""Read HTTP/2 ``RST_STREAM`` frames.
Structure of HTTP/2 ``RST_STREAM`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length != 4:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
data = Data_RSTStreamFrame(
length=header.length,
type=header.type,
flags=self._read_http_flags(schema),
reserved=header.stream['reserved'],
sid=header.stream['sid'],
error=schema.error,
)
return data
[docs]
def _read_http_settings(self, schema: 'Schema_SettingsFrame', *,
header: 'Schema_HTTP') -> 'Data_SettingsFrame':
"""Read HTTP/2 ``SETTINGS`` frames.
Structure of HTTP/2 ``SETTINGS`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------------------------------------------------------+
| Identifier (16) |
+-------------------------------+-------------------------------+
| Value (32) |
+---------------------------------------------------------------+
| ...... |
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length % 6 != 0 or header.stream['sid'] != 0:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
flag = Data_SettingsFrameFlags(
ACK=bool(header.flags['bit_0']), # bit 0
)
flag.__update__({
'__value__': schema.__flags__,
})
if flag.ACK and header.length != 0:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
# NOTE: every pair rather than a mapping: an identifier may repeat, and
# the values apply in order (:rfc:`9113#section-6.5.3`). ``Settings``
# keeps every entry, and answers an index with the last value.
sets = Data_Settings([(setting.id, setting.value) for setting in schema.settings])
data = Data_SettingsFrame(
length=header.length,
type=header.type,
flags=flag,
reserved=header.stream['reserved'],
sid=header.stream['sid'],
settings=sets,
)
return data
[docs]
def _read_http_push_promise(self, schema: 'Schema_PushPromiseFrame', *,
header: 'Schema_HTTP') -> 'Data_PushPromiseFrame':
"""Read HTTP/2 ``PUSH_PROMISE`` frames.
Structure of HTTP/2 ``PUSH_PROMISE`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------+-----------------------------------------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length < 4:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
flag = Data_PushPromiseFrameFlags(
END_HEADERS=bool(header.flags['bit_2']), # bit 2
PADDED=bool(header.flags['bit_3']), # bit 3
)
flag.__update__({
'__value__': schema.__flags__,
})
data = Data_PushPromiseFrame(
length=header.length,
type=header.type,
flags=flag,
reserved=header.stream['reserved'],
sid=header.stream['sid'],
pad_len=schema.pad_len if flag.PADDED else 0,
promised_reserved=schema.stream['reserved'],
promised_sid=schema.stream['sid'],
fragment=schema.fragment,
padding=schema.padding if flag.PADDED else b'',
)
return data
[docs]
def _read_http_ping(self, schema: 'Schema_PingFrame', *,
header: 'Schema_HTTP') -> 'Data_PingFrame':
"""Read HTTP/2 ``PING`` frames.
Structure of HTTP/2 ``PING`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------------------------------------------------------+
| |
| Opaque Data (64) |
| |
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length != 8:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
flag = Data_PingFrameFlags(
ACK=bool(header.flags['bit_0']), # bit 0
)
flag.__update__({
'__value__': schema.__flags__,
})
data = Data_PingFrame(
length=header.length,
type=header.type,
flags=flag,
reserved=header.stream['reserved'],
sid=header.stream['sid'],
data=schema.data,
)
return data
[docs]
def _read_http_goaway(self, schema: 'Schema_GoawayFrame', *,
header: 'Schema_HTTP') -> 'Data_GoawayFrame': # pylint: disable=unused-argument
"""Read HTTP/2 ``GOAWAY`` frames.
Structure of HTTP/2 ``GOAWAY`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+-+-------------+---------------+-------------------------------+
|R| Last-Stream-ID (31) |
+-+-------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
| Additional Debug Data (*) |
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
"""
data = Data_GoawayFrame(
length=header.length,
type=header.type,
flags=self._read_http_flags(schema),
reserved=header.stream['reserved'],
sid=header.stream['sid'],
last_reserved=schema.stream['reserved'],
last_sid=schema.stream['sid'],
error=schema.error,
debug_data=schema.debug,
)
return data
[docs]
def _read_http_window_update(self, schema: 'Schema_WindowUpdateFrame', *,
header: 'Schema_HTTP') -> 'Data_WindowUpdateFrame': # pylint: disable=unused-argument
"""Read HTTP/2 ``WINDOW_UPDATE`` frames.
Structure of HTTP/2 ``WINDOW_UPDATE`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+-+-------------+---------------+-------------------------------+
|R| Window Size Increment (31) |
+-+-------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
Raises:
ProtocolError: If the packet is malformed.
"""
if header.length != 4:
raise ProtocolError(f'HTTP/2: [Type {header.type}] invalid format')
data = Data_WindowUpdateFrame(
length=header.length,
type=header.type,
flags=self._read_http_flags(schema),
reserved=header.stream['reserved'],
sid=header.stream['sid'],
increment_reserved=schema.size['reserved'],
increment=schema.size['incr'],
)
return data
[docs]
def _read_http_continuation(self, schema: 'Schema_ContinuationFrame', *,
header: 'Schema_HTTP') -> 'Data_ContinuationFrame':
"""Read HTTP/2 ``CONTINUATION`` frames.
Structure of HTTP/2 ``CONTINUATION`` frame [:rfc:`7540`]:
.. code-block:: text
+-----------------------------------------------+
| Length (24) |
+---------------+---------------+---------------+
| Type (8) | Flags (8) |
+-+-------------+---------------+-------------------------------+
|R| Stream Identifier (31) |
+---------------------------------------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
Args:
schema: Parsed frame schema.
header: Parsed HTTP/2 header schema.
Returns:
Parsed packet data.
"""
flag = Data_ContinuationFrameFlags(
END_HEADERS=bool(header.flags['bit_2']), # bit 2
)
flag.__update__({
'__value__': schema.__flags__,
})
data = Data_ContinuationFrame(
length=header.length,
type=header.type,
flags=flag,
reserved=header.stream['reserved'],
sid=header.stream['sid'],
fragment=schema.fragment,
)
return data
[docs]
def _make_http_none(self, frame: 'Optional[Data_UnassignedFrame]' = None, *,
data: 'bytes' = b'',
**kwargs: 'Any') -> 'tuple[Schema_UnassignedFrame, Flags]':
"""Make HTTP/2 unassigned frame type.
Args:
frame: Frame data model.
data: Unspecified frame data.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
data = frame.data
return Schema_UnassignedFrame(
data=data,
), cast('Flags', 0)
[docs]
def _make_http_data(self, frame: 'Optional[Data_DataFrame]' = None, *,
end_stream: 'bool' = False,
padded: 'bool' = False,
pad_len: 'int' = 0,
data: 'bytes' = b'',
padding: 'bytes' = b'',
**kwargs: 'Any') -> 'tuple[Schema_DataFrame, Flags]':
"""Make HTTP/2 ``DATA`` frame.
Args:
frame: Frame data model.
end_stream: End of stream flag.
padded: Padded flag, implied by a non-zero ``pad_len``.
pad_len: Padding length.
data: Frame data.
padding: Padding octets, truncated or zero-filled to ``pad_len``
octets.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
end_stream = frame.flags.END_STREAM
padded = frame.flags.PADDED
pad_len = frame.pad_len
data = frame.data
padding = getattr(frame, 'padding', b'')
flags = Schema_DataFrame.Flags(0)
if end_stream:
flags |= Schema_DataFrame.Flags.END_STREAM
if padded or pad_len:
flags |= Schema_DataFrame.Flags.PADDED
return Schema_DataFrame(
pad_len=pad_len,
data=data,
padding=padding,
), flags
[docs]
def _make_http_priority(self, frame: 'Optional[Data_PriorityFrame]' = None, *,
sid_dep: 'int' = 0,
excl_dep: 'bool' = False,
weight: 'int' = 0,
**kwargs: 'Any') -> 'tuple[Schema_PriorityFrame, Flags]':
"""Make HTTP/2 ``PRIORITY`` frame.
Args:
frame: Frame data model.
excl_dep: Exclusive dependency flag.
sid_dep: Dependency stream identifier.
weight: Priority weight value.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
excl_dep = frame.excl_dependency
sid_dep = frame.stream_dependency
weight = frame.weight
return Schema_PriorityFrame(
stream={
'exclusive': excl_dep,
'sid': sid_dep,
},
weight=weight - 1 if weight else 0,
), cast('Flags', 0)
[docs]
def _make_http_rst_stream(self, frame: 'Optional[Data_RSTStreamFrame]' = None, *,
error: 'Enum_ErrorCode | str | int | StdlibEnum | AenumEnum' = Enum_ErrorCode.HTTP_1_1_REQUIRED,
error_default: 'Optional[int]' = None,
error_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
error_reversed: 'bool' = False,
**kwargs: 'Any') -> 'tuple[Schema_RSTStreamFrame, Flags]':
"""Make HTTP/2 ``RST_STREAM`` frame.
Args:
frame: Frame data model.
error: Error code.
error_default: Default value of error code.
error_namespace: Namespace of error code.
error_reversed: Reversed namespace of error code.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
error_val = frame.error
else:
error_val = self._make_index(error, error_default, namespace=error_namespace, # type: ignore[assignment]
reversed=error_reversed, pack=False)
return Schema_RSTStreamFrame(
error=error_val,
), cast('Flags', 0)
[docs]
def _make_http_settings(self, frame: 'Optional[Data_SettingsFrame]' = None, *,
ack: 'bool' = False,
settings: 'Optional[OrderedMultiDict[Enum_Setting, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting, int]]]' = None, # pylint: disable=line-too-long
**kwargs: 'Any') -> 'tuple[Schema_SettingsFrame, Flags]':
"""Make HTTP/2 ``SETTINGS`` frame.
Args:
frame: Frame data model.
ack: Acknowledge flag.
settings: Settings, as raw octets, an
:class:`~pcapkit.corekit.multidict.OrderedMultiDict` or a list of
``(identifier, value)`` pairs.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
Raises:
ProtocolError: If ``settings`` is not :obj:`bytes`, a :obj:`dict`
or a :obj:`list`, :data:`None` (the default) included.
"""
if frame is not None:
ack = frame.flags.ACK
settings = frame.settings
flags = Schema_SettingsFrame.Flags(0)
if ack:
flags |= Schema_SettingsFrame.Flags.ACK
if isinstance(settings, bytes):
settings_val = settings # type: bytes | list[Schema_SettingPair]
elif isinstance(settings, dict):
settings_val = []
for key, val in settings.items(multi=True):
settings_val.append(Schema_SettingPair(
id=key,
value=val,
))
elif isinstance(settings, list):
settings_val = []
for item in settings:
if isinstance(item, Schema_SettingPair):
temp = item
else:
id, value = item
temp = Schema_SettingPair(
id=id,
value=value,
)
settings_val.append(temp)
else:
raise ProtocolError(f'HTTP/2 : [Type {Enum_Frame.SETTINGS}] invalid settings')
return Schema_SettingsFrame(
settings=settings_val,
), flags
[docs]
def _make_http_push_promise(self, frame: 'Optional[Data_PushPromiseFrame]' = None, *,
end_headers: 'bool' = False,
padded: 'bool' = False,
pad_len: 'int' = 0,
promised_reserved: 'int' = 0,
promised_sid: 'int' = 0,
fragment: 'bytes' = b'',
padding: 'bytes' = b'',
**kwargs: 'Any') -> 'tuple[Schema_PushPromiseFrame, Flags]':
"""Make HTTP/2 ``PUSH_PROMISE`` frame.
Args:
frame: Frame data model.
end_headers: End of headers flag.
padded: Padded flag, implied by a non-zero ``pad_len``.
pad_len: Padding length.
promised_reserved: Reserved bit of the promised stream identifier.
promised_sid: Promised stream identifier.
fragment: Header block fragment.
padding: Padding octets, truncated or zero-filled to ``pad_len``
octets.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
end_headers = frame.flags.END_HEADERS
padded = frame.flags.PADDED
pad_len = frame.pad_len
promised_reserved = frame.promised_reserved
promised_sid = frame.promised_sid
fragment = frame.fragment
padding = getattr(frame, 'padding', b'')
flags = Schema_PushPromiseFrame.Flags(0)
if end_headers:
flags |= Schema_PushPromiseFrame.Flags.END_HEADERS
if padded or pad_len:
flags |= Schema_PushPromiseFrame.Flags.PADDED
return Schema_PushPromiseFrame(
pad_len=pad_len,
stream={
'reserved': promised_reserved,
'sid': promised_sid,
},
fragment=fragment,
padding=padding,
), flags
[docs]
def _make_http_ping(self, frame: 'Optional[Data_PingFrame]' = None, *,
ack: 'bool' = False,
opaque_data: 'bytes' = bytes(8),
**kwargs: 'Any') -> 'tuple[Schema_PingFrame, Flags]':
"""Make HTTP/2 ``PING`` frame.
Args:
frame: Frame data model.
ack: Acknowledge flag.
opaque_data: Opaque data, exactly 8 octets.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
Raises:
ProtocolError: If ``opaque_data`` is not 8 octets long.
"""
if frame is not None:
ack = frame.flags.ACK
opaque_data = frame.data
if len(opaque_data) != 8:
raise ProtocolError(f'HTTP/2: [Type {Enum_Frame.PING}] opaque data must be 8 octets, '
f'not {len(opaque_data)}')
flags = Schema_PingFrame.Flags(0)
if ack:
flags |= Schema_PingFrame.Flags.ACK
return Schema_PingFrame(
data=opaque_data,
), flags
[docs]
def _make_http_goaway(self, frame: 'Optional[Data_GoawayFrame]' = None, *,
last_reserved: 'int' = 0,
last_sid: 'int' = 0,
error: 'Enum_ErrorCode | str | int | StdlibEnum | AenumEnum' = Enum_ErrorCode.HTTP_1_1_REQUIRED,
error_default: 'Optional[int]' = None,
error_namespace: 'Optional[dict[str, int] | dict[int, str] | Type[StdlibEnum] | Type[AenumEnum]]' = None, # pylint: disable=line-too-long
error_reversed: 'bool' = False,
debug_data: 'bytes' = b'',
**kwargs: 'Any') -> 'tuple[Schema_GoawayFrame, Flags]':
"""Make HTTP/2 ``GOAWAY`` frame.
Args:
frame: Frame data model.
last_reserved: Reserved bit of the last stream identifier.
last_sid: Last stream identifier.
error: Error code.
error_default: Default value of error code.
error_namespace: Namespace of error code.
error_reversed: Reversed namespace of error code.
debug_data: Additional debug data.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
last_reserved = frame.last_reserved
last_sid = frame.last_sid
error_val = frame.error
debug = frame.debug_data
else:
error_val = self._make_index(error, error_default, namespace=error_namespace, # type: ignore[assignment]
reversed=error_reversed, pack=False)
debug = debug_data
return Schema_GoawayFrame(
stream={
'reserved': last_reserved,
'sid': last_sid,
},
error=error_val,
debug=debug,
), cast('Flags', 0)
[docs]
def _make_http_window_update(self, frame: 'Optional[Data_WindowUpdateFrame]' = None, *,
incr_reserved: 'int' = 0,
incr: 'int' = 0,
**kwargs: 'Any') -> 'tuple[Schema_WindowUpdateFrame, Flags]':
"""Make HTTP/2 ``WINDOW_UPDATE`` frame.
Args:
frame: Frame data model.
incr_reserved: Reserved bit of the window size increment.
incr: Window size increment.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
incr_reserved = frame.increment_reserved
incr = frame.increment
return Schema_WindowUpdateFrame(
size={
'reserved': incr_reserved,
'incr': incr,
}
), cast('Flags', 0)
[docs]
def _make_http_continuation(self, frame: 'Optional[Data_ContinuationFrame]' = None, *,
end_headers: 'bool' = False,
fragment: 'bytes' = b'',
**kwargs: 'Any') -> 'tuple[Schema_ContinuationFrame, Flags]':
"""Make HTTP/2 ``CONTINUATION`` frame.
Args:
frame: Frame data model.
end_headers: End of headers flag.
fragment: Header block fragment.
**kwargs: Arbitrary keyword arguments.
Returns:
Constructed frame schema and updated flags.
"""
if frame is not None:
end_headers = frame.flags.END_HEADERS
fragment = frame.fragment
flags = Schema_ContinuationFrame.Flags(0)
if end_headers:
flags |= Schema_ContinuationFrame.Flags.END_HEADERS
return Schema_ContinuationFrame(
fragment=fragment,
), flags