# -*- 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_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_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)