Source code for pcapkit.protocols.application.httpv2.protocol

# -*- coding: utf-8 -*-
"""HTTP/2 - Hypertext Transfer Protocol
==========================================

.. module:: pcapkit.protocols.application.httpv2.protocol

:mod:`pcapkit.protocols.application.httpv2.protocol` holds
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`, the extractor for
the Hypertext Transfer Protocol (HTTP/2) [*]_, whose frame is:

======= ========= ===================== ==========================
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

Frames in One Segment
---------------------

A whole frame after the first in a TCP segment is dissected as a further
:class:`HTTPv2` layer, the previous frame's payload (:issue:`2536`), as
:class:`~pcapkit.protocols.application.tls.protocol.TLS` chains records: a ``SETTINGS``
frame followed by a ``WINDOW_UPDATE`` reads ``TCP:HTTP/2:HTTP/2``. The
exceptions:

* A whole frame that fails to decode becomes
  :class:`~pcapkit.protocols.misc.raw.protocol.Raw` together with everything after it,
  with the parser's ``error``.
* A frame the segment ends inside, its header or its payload cut short, stays
  raw, with no error.
* A parse told to stop at HTTP/2 (``protocol='HTTP/2'`` or
  ``layer='Application'``) keeps the rest of the segment raw.
* Each frame is a layer, so :data:`~pcapkit.utilities.config.FRAME_LAYER_LIMIT`
  bounds how many are dissected.

.. 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`).

.. versionremoved:: 1.6.0

   ``pcapkit.protocols.application.httpv2.HTTP``, with no alias. The
   class is renamed :class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`,
   after its module (:issue:`2529`).

"""
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.decorators import beholder
from pcapkit.corekit.multidict import MultiDict, OrderedMultiDict
from pcapkit.protocols.application.http.protocol import _HTTP2_PREFACE
from pcapkit.protocols.application.http.protocol import HTTP as HTTPBase
from pcapkit.protocols.application.httpv2.data import ContinuationFrame as Data_ContinuationFrame
from pcapkit.protocols.application.httpv2.data import \
    ContinuationFrameFlags as Data_ContinuationFrameFlags
from pcapkit.protocols.application.httpv2.data import DataFrame as Data_DataFrame
from pcapkit.protocols.application.httpv2.data import DataFrameFlags as Data_DataFrameFlags
from pcapkit.protocols.application.httpv2.data import Flags as Data_Flags
from pcapkit.protocols.application.httpv2.data import GoawayFrame as Data_GoawayFrame
from pcapkit.protocols.application.httpv2.data import HeadersFrame as Data_HeadersFrame
from pcapkit.protocols.application.httpv2.data import HeadersFrameFlags as Data_HeadersFrameFlags
from pcapkit.protocols.application.httpv2.data import HTTPv2 as Data_HTTP
from pcapkit.protocols.application.httpv2.data import PingFrame as Data_PingFrame
from pcapkit.protocols.application.httpv2.data import PingFrameFlags as Data_PingFrameFlags
from pcapkit.protocols.application.httpv2.data import PriorityFrame as Data_PriorityFrame
from pcapkit.protocols.application.httpv2.data import PushPromiseFrame as Data_PushPromiseFrame
from pcapkit.protocols.application.httpv2.data import \
    PushPromiseFrameFlags as Data_PushPromiseFrameFlags
from pcapkit.protocols.application.httpv2.data import RSTStreamFrame as Data_RSTStreamFrame
from pcapkit.protocols.application.httpv2.data import Settings as Data_Settings
from pcapkit.protocols.application.httpv2.data import SettingsFrame as Data_SettingsFrame
from pcapkit.protocols.application.httpv2.data import SettingsFrameFlags as Data_SettingsFrameFlags
from pcapkit.protocols.application.httpv2.data import UnassignedFrame as Data_UnassignedFrame
from pcapkit.protocols.application.httpv2.data import WindowUpdateFrame as Data_WindowUpdateFrame
from pcapkit.protocols.application.httpv2.schema import \
    ContinuationFrame as Schema_ContinuationFrame
from pcapkit.protocols.application.httpv2.schema import DataFrame as Schema_DataFrame
from pcapkit.protocols.application.httpv2.schema import FrameType as Schema_FrameType
from pcapkit.protocols.application.httpv2.schema import GoawayFrame as Schema_GoawayFrame
from pcapkit.protocols.application.httpv2.schema import HeadersFrame as Schema_HeadersFrame
from pcapkit.protocols.application.httpv2.schema import HTTPv2 as Schema_HTTP
from pcapkit.protocols.application.httpv2.schema import PingFrame as Schema_PingFrame
from pcapkit.protocols.application.httpv2.schema import PriorityFrame as Schema_PriorityFrame
from pcapkit.protocols.application.httpv2.schema import PushPromiseFrame as Schema_PushPromiseFrame
from pcapkit.protocols.application.httpv2.schema import RSTStreamFrame as Schema_RSTStreamFrame
from pcapkit.protocols.application.httpv2.schema import SettingPair as Schema_SettingPair
from pcapkit.protocols.application.httpv2.schema import SettingsFrame as Schema_SettingsFrame
from pcapkit.protocols.application.httpv2.schema import UnassignedFrame as Schema_UnassignedFrame
from pcapkit.protocols.application.httpv2.schema import \
    WindowUpdateFrame as Schema_WindowUpdateFrame
from pcapkit.protocols.schema import Schema
from pcapkit.utilities.exceptions import ProtocolError
from pcapkit.utilities.warnings import ProtocolWarning, RegistryWarning, warn

if TYPE_CHECKING:
    from typing import Any, Callable, DefaultDict, Optional, Tuple

    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')`` in the alias, which another module
    # annotating with ``FrameConstructor`` cannot resolve in its own namespace.
    FrameConstructor = Callable[[DefaultArg(Optional[Data_HTTP]),
                                 KwArg(Any)], Tuple[Schema_FrameType, Flags]]

__all__ = ['HTTPv2']


def _opens_whole_frame(data: 'bytes') -> 'bool':
    """Whether ``data`` opens with a whole HTTP/2 frame.

    Only the 9-octet frame header is read: the frame is whole when its Length,
    which counts the frame payload alone (:rfc:`9113#section-4.1`), ends inside
    ``data``. The type, flags and stream are left to :meth:`HTTPv2.read`, so a
    whole frame it refuses is kept with its error.

    Args:
        data: Octets after a frame.

    Returns:
        Whether a further frame opens ``data`` and ends inside it.

    """
    return len(data) >= 9 and 9 + int.from_bytes(data[:3], 'big') <= len(data)


[docs] class HTTPv2(HTTPBase[Data_HTTP, Schema_HTTP], schema=Schema_HTTP, data=Data_HTTP): """This class implements Hypertext Transfer Protocol (HTTP/2). The HTTP/2 frames it parses, by :class:`pcapkit.const.http.frame.Frame` type: .. list-table:: :header-rows: 1 * - Frame Code - Frame Parser - Frame Constructor * - :attr:`~pcapkit.const.http.frame.Frame.DATA` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_data` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_data` * - :attr:`~pcapkit.const.http.frame.Frame.HEADERS` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_headers` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_headers` * - :attr:`~pcapkit.const.http.frame.Frame.PRIORITY` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_priority` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_priority` * - :attr:`~pcapkit.const.http.frame.Frame.RST_STREAM` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_rst_stream` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_rst_stream` * - :attr:`~pcapkit.const.http.frame.Frame.SETTINGS` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_settings` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_settings` * - :attr:`~pcapkit.const.http.frame.Frame.PUSH_PROMISE` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_push_promise` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_push_promise` * - :attr:`~pcapkit.const.http.frame.Frame.PING` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_ping` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_ping` * - :attr:`~pcapkit.const.http.frame.Frame.GOAWAY` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_goaway` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_goaway` * - :attr:`~pcapkit.const.http.frame.Frame.WINDOW_UPDATE` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_window_update` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_window_update` * - :attr:`~pcapkit.const.http.frame.Frame.CONTINUATION` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._read_http_continuation` - :meth:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2._make_http_continuation` .. versionchanged:: 1.6.0 * A payload opening with the connection preface parses, with the preface kept as ``preface`` and counted in :attr:`length`, and :meth:`make` takes ``preface``, since a dispatched HTTP/2 payload reaches this class rather than :class:`~pcapkit.protocols.application.http.protocol.HTTP` (:issue:`2524`). * A whole frame after the first in a segment is a further layer of this class, where it was kept as :class:`~pcapkit.protocols.misc.raw.protocol.Raw` (:issue:`2536`). * A ``SETTINGS`` frame the capture cut inside its settings parses, with the setting the cut lands inside read as zeros past it, as a cut ``DATA`` frame's data is, and no setting past the data, where it was :class:`~pcapkit.protocols.misc.raw.protocol.Raw` (:issue:`2659`, :issue:`2686`, :issue:`2756`). * Each enumeration field of :meth:`make` and of the frame makers takes a member or an :class:`int` code, resolved with the enumeration's ``get``; a name, a :obj:`bool` for a field wider than one bit, or a code the field cannot carry raises :exc:`~pcapkit.utilities.exceptions.ProtocolError`, rather than resolving against an enumeration inferred from the caller's frame or writing a different value (:issue:`2594`). .. versionremoved:: 1.6.0 The ``type_default``, ``type_namespace`` and ``type_reversed`` arguments of :meth:`make`, and the ``error_default``, ``error_namespace`` and ``error_reversed`` arguments of the ``RST_STREAM`` and ``GOAWAY`` frame makers; resolve a name with :meth:`~pcapkit.protocols.protocol.Protocol.resolve_index` first (:issue:`2594`). """ ########################################################################## # 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) -> 'int': """Header length of current protocol -- the 9-octet frame header, after the 24-octet connection preface when one opens the payload.""" return 9 + self._preface_length @property def version(self) -> 'Literal["2"]': """Version of current protocol.""" return '2' ########################################################################## # Methods. ##########################################################################
[docs] 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.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:`~pcapkit.utilities.exceptions.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.Protocol.unpack>`. Collapsing ``None`` to a concrete ``0`` would change what :meth:`Schema.unpack <pcapkit.protocols.schema.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. A payload opening with the HTTP/2 connection preface (:rfc:`9113#section-3.4`) has the frame parsed from the octets after it, as :meth:`HTTP._guess_version <pcapkit.protocols.application.http.protocol.HTTP._guess_version>` parses it, since a dispatched payload reaches this class rather than that one (:issue:`2524`); the preface counts towards :attr:`length`. A preface with no frame after it is refused, since the data model holds one frame per packet and has no form for a frameless segment. """ preface_len = len(_HTTP2_PREFACE) if self.__header__ is None and self._data[:preface_len] == _HTTP2_PREFACE \ and (length is None or length >= preface_len): remaining = (len(self) if length is None else length) - preface_len if remaining == 0: raise ProtocolError('HTTP/2: connection preface with no frame') self._preface_length = preface_len self._file.seek(preface_len) length = remaining if length is None: # ``0 <``: an exhausted stream is left for ``Protocol.unpack``'s # ``prepare`` decorator to raise ``StreamEOFError``, as documented # above; only a non-empty short stream is malformed. 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, asserts how much data the # frame has, so every value under nine is malformed. 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) - self._preface_length schema = self.__header__ # NOTE: ``schema.length`` counts the payload alone (RFC 9113, section 4.1), so # ``length`` must cover the 9-octet header and the declared payload as well. 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) if self._preface_length: http.__update__({ 'preface': _HTTP2_PREFACE, }) # The frame ends at its declared Length; what follows it is a further # whole frame, dissected as one, or else the undissected remainder (#2536). return self._decode_next_layer(http, -1, length - 9 - schema.length)
[docs] def make(self, # type: ignore[override] type: 'Enum_Frame | int' = Enum_Frame.DATA, flags: 'Flags' = 0, # type: ignore[assignment] reserved: 'int' = 0, sid: 'int' = 0, frame: 'Optional[bytes | Data_HTTP | Schema_FrameType | dict[str, Any]]' = None, payload: 'bytes | ProtocolBase | Schema' = b'', preface: 'bool' = False, # pylint: disable=unused-argument **kwargs: 'Any') -> 'Schema_HTTP': """Make (construct) packet data. Args: type: Type of HTTP/2 frame. 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. Omitted (:data:`None`), the frame type's own default payload is built, as for ``{}``; a :obj:`bytes` value, ``b''`` included, is packed as raw octets. payload: Octets after the frame, e.g. the next frame. preface: Whether the HTTP/2 connection preface goes ahead of the frame; :meth:`pack` writes it. **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data. Raises: ProtocolError: If ``type`` is a name, a :obj:`bool`, or a member or code the 8-bit Type field cannot carry; or if ``frame`` is none of the forms this maker reads -- raw :obj:`bytes`, a :obj:`dict` of frame arguments, an :class:`~pcapkit.protocols.application.httpv2.data.HTTPv2` data model or a :class:`~pcapkit.protocols.schema.Schema`. .. 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`). * ``frame`` defaults to :data:`None` and dispatches to the frame type's constructor, since the former ``b''`` default is shorter than the payload a frame type of fixed or minimum size needs, so such a frame could not be built at all (:issue:`2274`). """ type_val = self._make_index(type, Enum_Frame, bits=8, name='type') if frame is None: # An omitted frame dispatches to the type's own constructor, as # ``{}`` does, since a fixed-size payload cannot be empty octets # (#2274). An explicit ``b''`` stays raw. frame = {} 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 frame: must be bytes, a dict, ' f'an HTTP data model or a Schema, not {frame!r}') 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
[docs] @classmethod def id(cls) -> 'tuple[Literal["HTTP/2"]]': # type: ignore[override] """Index ID of the protocol. :rfc:`9113` names the protocol ``HTTP/2``. Returns: Index ID of the protocol. .. versionchanged:: 1.6.0 ``('HTTP/2',)``, narrowed to this class, where it was ``('HTTP', 'HTTPv2')`` (:issue:`2524`). """ return ('HTTP/2',)
[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: Name of the ``_read_http_${name}`` and ``_make_http_${name}`` methods, or a ``(parser, constructor)`` pair of callables; see :attr:`__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]': """Return an estimated length for the object.""" 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``: ``make`` computes the Length from the frame it builds, and # ``ProtocolBase.__init__`` would take a ``length`` as the octets to parse back. 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), 'preface': bool(data.get('preface')), }
@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] 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()): 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_headers(self, schema: 'Schema_HeadersFrame', *, header: 'Schema_HTTP') -> 'Data_HeadersFrame': """Read HTTP/2 ``HEADERS`` frames. Structure of HTTP/2 ``HEADERS`` frame [:rfc:`7540`]: .. code-block:: text +-----------------------------------------------+ | Length (24) | +---------------+---------------+---------------+ | Type (8) | Flags (8) | +-+-------------+---------------+-------------------------------+ |R| Stream Identifier (31) | +---------------+-----------------------------------------------+ |Pad Length? (8)| +-+-------------+-----------------------------------------------+ |E| Stream Dependency? (31) | +-+-------------+-----------------------------------------------+ | Weight? (8) | +-+-------------+-----------------------------------------------+ | Header Block Fragment (*) ... +---------------------------------------------------------------+ | Padding (*) ... +---------------------------------------------------------------+ Args: schema: Parsed frame schema. header: Parsed HTTP/2 header schema. Returns: Parsed packet data. """ flag = Data_HeadersFrameFlags( END_STREAM=bool(header.flags['bit_0']), # bit 0 END_HEADERS=bool(header.flags['bit_2']), # bit 2 PADDED=bool(header.flags['bit_3']), # bit 3 PRIORITY=bool(header.flags['bit_5']), # bit 5 ) flag.__update__({ '__value__': schema.__flags__, }) data = Data_HeadersFrame( 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'], excl_dependency=bool(schema.stream_dep['exclusive']) if flag.PRIORITY else False, stream_dependency=schema.stream_dep['sid'] if flag.PRIORITY else 0, weight=(schema.weight + 1) if flag.PRIORITY else 0, fragment=schema.fragment, 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, not a mapping: an identifier may repeat and applies in order # (:rfc:`9113#section-6.5.3`); ``Settings`` 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_headers(self, frame: 'Optional[Data_HeadersFrame]' = None, *, end_stream: 'bool' = False, end_headers: 'bool' = False, padded: 'bool' = False, pad_len: 'int' = 0, excl_dep: 'bool' = False, sid_dep: 'Optional[int]' = None, weight: 'int' = 0, fragment: 'bytes' = b'', padding: 'bytes' = b'', **kwargs: 'Any') -> 'tuple[Schema_HeadersFrame, Flags]': """Make HTTP/2 ``HEADERS`` frame. Args: frame: Frame data model. end_stream: End of stream flag. end_headers: End of headers flag. padded: Padded flag, implied by a non-zero ``pad_len``. pad_len: Padding length. excl_dep: Exclusive dependency flag. sid_dep: Dependency stream identifier. Giving one sets the ``PRIORITY`` flag. weight: Priority weight value. 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: priority = frame.flags.PRIORITY end_headers = frame.flags.END_HEADERS end_stream = frame.flags.END_STREAM padded = frame.flags.PADDED pad_len = frame.pad_len excl_dep = frame.excl_dependency sid_dep = frame.stream_dependency weight = frame.weight fragment = frame.fragment padding = getattr(frame, 'padding', b'') else: priority = sid_dep is not None sid_dep = sid_dep or 0 flags = Schema_HeadersFrame.Flags(0) if end_stream: flags |= Schema_HeadersFrame.Flags.END_STREAM if end_headers: flags |= Schema_HeadersFrame.Flags.END_HEADERS if padded or pad_len: flags |= Schema_HeadersFrame.Flags.PADDED if priority: flags |= Schema_HeadersFrame.Flags.PRIORITY return Schema_HeadersFrame( pad_len=pad_len, stream_dep={ 'exclusive': excl_dep, 'sid': sid_dep, }, weight=weight - 1 if weight else 0, fragment=fragment, 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 | int' = Enum_ErrorCode.NO_ERROR, **kwargs: 'Any') -> 'tuple[Schema_RSTStreamFrame, Flags]': """Make HTTP/2 ``RST_STREAM`` frame. Args: frame: Frame data model. error: Error code, ``NO_ERROR`` unless given. **kwargs: Arbitrary keyword arguments. Returns: Constructed frame schema and updated flags. Raises: ProtocolError: If ``error`` is a name, a :obj:`bool`, or a member or code the 32-bit Error Code field cannot carry. .. versionchanged:: 1.5.0 ``error`` defaults to ``NO_ERROR``, the code :rfc:`9113#section-7` gives a condition that is not the result of an error, rather than ``HTTP_1_1_REQUIRED``, which asks the peer to use HTTP/1.1 instead (:issue:`2311`). """ if frame is not None: error_val = frame.error else: error_val = self._make_index(error, Enum_ErrorCode, bits=32, name='error code') 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, int] | dict[Enum_Setting | int, int] | bytes | list[Schema_SettingPair | tuple[Enum_Setting | int, 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`, a plain :obj:`dict` of identifier to value or a list of ``(identifier, value)`` pairs. :data:`None`, the default, carries no parameters at all. **kwargs: Arbitrary keyword arguments. Returns: Constructed frame schema and updated flags. Raises: ProtocolError: If ``settings`` is not :obj:`bytes`, a :obj:`dict`, a :obj:`list` or :data:`None`, or if an identifier other than a :class:`~pcapkit.protocols.application.httpv2.schema.SettingPair`'s is a name, a :obj:`bool`, or a member or code the 16-bit Identifier field cannot carry. .. versionchanged:: 1.5.0 ``settings`` of :data:`None`, the default, builds a ``SETTINGS`` frame with no parameters, which :rfc:`9113#section-6.5` permits, rather than being rejected (:issue:`2274`). .. versionchanged:: 1.6.0 A ``frame`` the capture cut inside its settings, as its ``__short_read__`` records, packs the settings its parse read from the octets its ``packet`` holds with zeros after them to the Length it declares, since that parse reads no setting past the data, so its rebuild declares that Length too; with its settings edited, the Length is computed as for a whole frame (:issue:`2756`). """ 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 settings is None: # A SETTINGS frame carrying no parameters is valid (RFC 9113 # §6.5), so the default builds one rather than being rejected # (#2274). settings_val = [] # type: bytes | list[Schema_SettingPair] elif isinstance(settings, bytes): settings_val = settings elif isinstance(settings, dict): settings_val = [] # A plain ``dict`` has no ``multi`` keyword (#2223). items = settings.items(multi=True) if isinstance(settings, MultiDict) else settings.items() for key, val in items: settings_val.append(Schema_SettingPair( id=self._make_index(key, Enum_Setting, bits=16, name='setting identifier'), 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=self._make_index(id, Enum_Setting, bits=16, name='setting identifier'), value=value, ) settings_val.append(temp) else: raise ProtocolError(f'HTTP/2: [Type {Enum_Frame.SETTINGS}] invalid settings: ' f'{settings!r}') # NOTE: An unedited cut frame packs the octets its ``packet`` captured, zero to the pair, and # keeps its Length in the zeros past them (#2756); a dict rebuilds as the base data model. record = frame.get('__short_read__') if isinstance(frame, Data_HTTP) else None if frame is not None and record is not None and isinstance(settings_val, list): packed = b''.join(pair.pack() for pair in settings_val) # A cut in the frame header leaves no setting captured. read = cast('bytes', frame.get('packet', b''))[:record[1]] if record[0] == 'frame' else b'' if packed == read + bytes(-len(read) % 6) and len(packed) < frame.length: settings_val = packed + bytes(frame.length - len(packed)) 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 | int' = Enum_ErrorCode.NO_ERROR, 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, ``NO_ERROR`` unless given. debug_data: Additional debug data. **kwargs: Arbitrary keyword arguments. Returns: Constructed frame schema and updated flags. Raises: ProtocolError: If ``error`` is a name, a :obj:`bool`, or a member or code the 32-bit Error Code field cannot carry. .. versionchanged:: 1.5.0 ``error`` defaults to ``NO_ERROR``, the code :rfc:`9113#section-7` gives a condition that is not the result of an error, rather than ``HTTP_1_1_REQUIRED``, which asks the peer to use HTTP/1.1 instead (:issue:`2311`). """ 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, Enum_ErrorCode, bits=32, name='error code') 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
@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 def _import_next_layer(self, proto: 'int', length: 'Optional[int]' = None, *, # type: ignore[override] packet: 'Optional[dict[str, Any]]' = None) -> 'ProtocolBase': """Import next layer extractor: a further whole frame, or the rest undissected. A TCP segment may carry several frames (:issue:`2536`). So the ``-1`` remainder is dissected as this class when :func:`_opens_whole_frame` finds a whole frame opening it, and is otherwise kept as :class:`~pcapkit.protocols.misc.raw.protocol.Raw`, as :meth:`Application._import_next_layer <pcapkit.protocols.application.application.Application._import_next_layer>` keeps it. A frame the segment ends inside is the reassembly limit, and stays raw. Arguments: proto: next layer protocol index; only the ``-1`` sentinel (the rest of the segment) is accepted length: valid (*non-padding*) length packet: packet info (passed from :meth:`self.unpack <pcapkit.protocols.protocol.Protocol.unpack>`) Returns: Instance of this class for a further frame, or as :meth:`Application._import_next_layer <pcapkit.protocols.application.application.Application._import_next_layer>` returns otherwise. """ if proto == -1 and _opens_whole_frame(self._get_payload()[:length]) and not self._sigterm: return self._import_next_frame(proto, length, packet=packet) # type: ignore[misc,call-arg] return super()._import_next_layer(proto, length, packet=packet) @beholder # type: ignore[arg-type] def _import_next_frame(self, proto: 'int', length: 'Optional[int]' = None, *, # type: ignore[misc] packet: 'Optional[dict[str, Any]]' = None) -> 'ProtocolBase': """Import the further frame :meth:`_import_next_layer` found. Arguments: proto: next layer protocol index, the ``-1`` sentinel length: valid (*non-padding*) length packet: packet info (passed from :meth:`self.unpack <pcapkit.protocols.protocol.Protocol.unpack>`) Returns: Instance of this class, or of :class:`~pcapkit.protocols.misc.raw.protocol.Raw` carrying the parser's ``error`` when :func:`~pcapkit.corekit.decorators.beholder` replaces a frame that fails to parse. """ file_ = self._get_payload() return self._parse_next_layer(type(self), file_, len(file_) if length is None else length, alias=proto, packet=packet, layer=self._exlayer, protocol=self._exproto, __context__=self._exctx)