Source code for pcapkit.protocols.application.http

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

.. module:: pcapkit.protocols.application.http

:mod:`pcapkit.protocols.application.http` contains
:class:`~pcapkit.protocols.application.http.HTTP`,
which is a base class for Hypertext Transfer
Protocol (HTTP) [*]_ family, e.g.
:class:`HTTP/1.* <pcapkit.protocols.application.httpv1.HTTP>`
and :class:`HTTP/2 <pcapkit.protocols.application.httpv2.HTTP>`,
and :func:`~pcapkit.protocols.application.http.test_start_line`,
which tells whether a payload opens with an HTTP/1.* start line.

.. [*] https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol

"""
import contextlib
import re
import struct
from typing import TYPE_CHECKING, Generic, cast

from pcapkit.corekit.protochain import ProtoChain
from pcapkit.protocols.application.application import Application
from pcapkit.protocols.data.data import Data
from pcapkit.protocols.misc.null import NoPayload
from pcapkit.protocols.protocol import _PT, _ST
from pcapkit.utilities.exceptions import ProtocolError

if TYPE_CHECKING:
    from typing import Any, Optional

    from typing_extensions import Literal, Self

__all__ = ['HTTP', 'test_start_line']

#: The HTTP/2 connection preface (:rfc:`9113#section-3.4`). A client opens every
#: HTTP/2 connection -- with prior knowledge, over TLS, or after an upgrade --
#: by sending exactly these 24 octets, and the sequence is *designed* to be
#: identifiable without parsing: it is a well-formed HTTP/1.1 request line whose
#: method ``PRI`` is reserved and permanently unregistered, so no valid HTTP/1
#: message can begin with it and a prefix compare cannot false-positive on one.
#: That makes it a positive identification rather than a heuristic, which is why
#: :meth:`HTTP._guess_version` tests it before attempting any parse.
_HTTP2_PREFACE = b'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'

#: What :rfc:`9113#section-6` allows a sender to put in the frame header of each
#: frame type it defines: the flags that type defines, and the stream identifier
#: it is sent on -- ``True`` for a non-zero stream, ``False`` for stream 0 and
#: :obj:`None` for either. :meth:`HTTP._guess_version` checks a bare frame
#: against this before its HTTP/2 trial parse; see :func:`_test_http2_frame`.
_HTTP2_FRAME_RULES = {
    0x0: (0x09, True),    # DATA: END_STREAM, PADDED (Section 6.1)
    0x1: (0x2D, True),    # HEADERS: END_STREAM, END_HEADERS, PADDED, PRIORITY (Section 6.2)
    0x2: (0x00, True),    # PRIORITY (Section 6.3)
    0x3: (0x00, True),    # RST_STREAM (Section 6.4)
    0x4: (0x01, False),   # SETTINGS: ACK (Section 6.5)
    0x5: (0x0C, True),    # PUSH_PROMISE: END_HEADERS, PADDED (Section 6.6)
    0x6: (0x01, False),   # PING: ACK (Section 6.7)
    0x7: (0x00, False),   # GOAWAY (Section 6.8)
    0x8: (0x00, None),    # WINDOW_UPDATE: either stream (Section 6.9)
    0x9: (0x04, True),    # CONTINUATION: END_HEADERS (Section 6.10)
}  # type: dict[int, tuple[int, Optional[bool]]]


def _test_http2_frame(data: 'bytes') -> 'bool':
    """Test whether ``data`` opens with a frame header a conforming sender could write.

    This is a plausibility test for :meth:`HTTP._guess_version`'s fall-through
    only. ``httpv2.HTTP`` itself parses every frame :rfc:`9113` lets a receiver
    accept, which includes frames this test turns down.

    Args:
        data: Payload, starting at the frame header.

    Returns:
        :data:`True` if the nine-octet header carries a frame type defined by
        :rfc:`9113#section-6`, sets no flag that type leaves undefined, leaves the
        reserved bit clear, and names a stream that type may be sent on.

    Notes:
        Undefined flags and the reserved bit "MUST be left unset (0x00) when
        sending" (:rfc:`9113#section-4.1`), and each stream rule is one a
        receiver "MUST treat as a connection error" when broken (e.g. ``DATA``
        on stream 0, :rfc:`9113#section-6.1`), so no frame a conforming peer
        sends fails here. Frame types :rfc:`9113` does not define
        are legal and ignored by a receiver (:rfc:`9113#section-5.5`), but on a
        payload with no other evidence they are what arbitrary binary looks
        like most of the time, so they are not guessed.

    """
    if len(data) < 9:
        return False
    rules = _HTTP2_FRAME_RULES.get(data[3])
    if rules is None:
        return False
    flags, stream = rules
    if data[4] & ~flags:
        return False
    if data[5] & 0x80:
        return False
    if stream is None:
        return True
    return any(data[5:9]) is stream


# Regular expression to match HTTP methods. Anchored at both ends: :func:`re.match`
# anchors only at the start, so an unanchored pattern prefix-matches and accepts the
# leading ``G`` of ``Get`` as a whole method token. Method tokens are case-sensitive
# per :rfc:`9110#section-9.1`, so ``Get`` is not ``GET`` and must not parse as one.
_RE_METHOD = re.compile(rb"(?P<method>[A-Z][A-Z-]*)\Z")  # RFC 9110, section 16.1.1, 9.1, 5.6.2
# Regular expression to match HTTP version string.
_RE_VERSION = re.compile(rb"HTTP/(?P<version>\d\.\d)")
# Regular expression to match HTTP status code. Anchored for the same reason as
# ``_RE_METHOD``, and it matters more here: this pattern is only a guard, and
# ``httpv1.HTTP`` takes the value from ``int(para2)`` on the raw token, so an
# unanchored prefix match would let ``200x`` and ``2000`` past the guard and then
# out of ``int()`` as a bare ``ValueError`` -- where ``_read_http_header``
# documents ``ProtocolError``.
# :rfc:`9112#section-4` gives ``status-code = 3DIGIT``, exactly three -- the
# grammar is in HTTP/1.1 because ``status-code`` is part of its ``status-line``
# production; :rfc:`9110#section-15` covers the code semantics and registry, not
# the syntax.
_RE_STATUS = re.compile(rb'\d{3}\Z')

# The HTTP/2 connection preface's header block, as ``httpv1.HTTP.read`` splits it
# off at the first CRLFCRLF: ``PRI * HTTP/2.0``. A start line with zero field
# lines, so the patterns above accept it; it is refused by name in
# ``httpv1.HTTP._read_http_header`` and :func:`test_start_line` so that the
# preface never parses as HTTP/1.
_HTTP2_PREFACE_HEADER = _HTTP2_PREFACE.split(b'\r\n\r\n', maxsplit=1)[0]


[docs] def test_start_line(data: 'bytes') -> 'bool': """Whether ``data`` opens with an HTTP/1.* start line. This is a *classification* predicate and parses nothing: it answers "is this HTTP/1?" for :meth:`HTTP._guess_version <pcapkit.protocols.application.http.HTTP._guess_version>`. Classifying by trial-parsing every version in the family would decide a payload that is not HTTP at all by which parser happened to fail less loudly. Args: data: Payload to classify. Returns: Whether the payload's first line is a ``request-line`` or a ``status-line`` (:rfc:`9112#section-2.1`). Note: The acceptance rule is deliberately the *same* one :meth:`HTTP._read_http_header <pcapkit.protocols.application.httpv1.HTTP._read_http_header>` applies -- ``_RE_METHOD`` with ``_RE_VERSION`` for a request, ``_RE_VERSION`` with ``_RE_STATUS`` for a response -- which is why those patterns are defined beside this predicate and :mod:`~pcapkit.protocols.application.httpv1` imports them from here. The two must accept the same start lines: a predicate looser than the parser classifies payloads the parser then refuses, and one tighter than the parser hands real HTTP/1 to a later arm. ``test_start_line_predicate_agrees_with_the_httpv1_parser`` pins that agreement. The checks the parser performs *before* those patterns are mirrored too, and this is not pedantry -- they are the whole reason the HTTP/2 connection preface is not claimed here. ``PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n`` is deliberately a well-formed HTTP/1.1 *request line* (:rfc:`9113#section-3.4`), so a predicate that tested only the first line would answer :data:`True` for it. Split at the header/body separator first, as :meth:`HTTP.read <pcapkit.protocols.application.httpv1.HTTP.read>` does, and the preface's header is exactly ``PRI * HTTP/2.0`` (``_HTTP2_PREFACE_HEADER``), which the parser refuses by name and so does this. Any *other* header with no CRLF in it is a start line with zero field lines, which :rfc:`9112#section-2.1` allows and both accept (:issue:`1041`). An HTTP/0.9 request line carries only two tokens and so is not recognised here either, matching the parser, which raises on fewer than three. """ header = data.split(b'\r\n\r\n', maxsplit=1)[0] if header == data: # no header/body separator -- ``read`` raises return False if header == _HTTP2_PREFACE_HEADER: # the preface -- ``_read_http_header`` raises return False startline = header.split(b'\r\n', maxsplit=1)[0] try: para1, para2, para3 = re.split(rb'\s+', startline, maxsplit=2) except ValueError: return False return bool( (re.match(_RE_METHOD, para1) and re.match(_RE_VERSION, para3)) # request-line or (re.match(_RE_VERSION, para1) and re.match(_RE_STATUS, para2)) # status-line )
[docs] class HTTP(Application[_PT, _ST], Generic[_PT, _ST]): """This class implements all protocols in HTTP family. - Hypertext Transfer Protocol (HTTP/1.1) [:rfc:`7230`] - Hypertext Transfer Protocol version 2 (HTTP/2) [:rfc:`7540`] """ if TYPE_CHECKING: #: Saved subclass protocol data (only for HTTP base class). _http: 'HTTP[_PT, _ST]' #: Octets consumed ahead of the identified version's own header -- in practice #: the 24-octet HTTP/2 connection preface, when :meth:`_guess_version` #: identified one and parsed the frame that follows it. They belong to this #: packet's header rather than to its payload, so :meth:`read` adds them to #: :attr:`length`; otherwise ``ProtocolBase.__init__``'s #: ``self._info.__update__(packet=self.packet.payload)`` would slice the #: payload from octet 9 of a buffer whose frame starts at octet 24 and report #: the tail of the preface as packet payload. _preface_length = 0 #: This class is a version dispatcher rather than a protocol with a header of #: its own, so its construction keywords cannot be enumerated: :meth:`make` #: declares only ``version`` and forwards everything else to #: :meth:`HTTPv1.make <pcapkit.protocols.application.httpv1.HTTP.make>` or #: :meth:`HTTPv2.make <pcapkit.protocols.application.httpv2.HTTP.make>` #: according to that value, so the correct set of names depends on an #: argument. :obj:`None` therefore opts out of the construction keyword check #: that :meth:`ProtocolBase.__init__ #: <pcapkit.protocols.protocol.ProtocolBase.__init__>` performs; the #: versioned classes are checked normally when constructed directly. __keywords__ = None ########################################################################## # Properties. ########################################################################## @property def name(self) -> 'Literal["Hypertext Transfer Protocol"]': """Name of current protocol.""" return 'Hypertext Transfer Protocol' @property def alias(self) -> 'Literal["HTTP/0.9", "HTTP/1.0", "HTTP/1.1", "HTTP/2"]': """Acronym of current protocol.""" return f'HTTP/{self.version}' # type: ignore[return-value] @property def length(self) -> 'int': """Header length of current protocol.""" return self._length @property def version(self) -> 'Literal["0.9", "1.0", "1.1", "2"]': """Version of current protocol.""" return self._version ########################################################################## # Methods. ##########################################################################
[docs] @classmethod def id(cls) -> 'tuple[Literal["HTTP"], Literal["HTTPv1"], Literal["HTTPv2"]]': """Index ID of the protocol.""" return ('HTTP', 'HTTPv1', 'HTTPv2')
[docs] def read(self, length: 'Optional[int]' = None, *, version: 'Optional[Literal[1, 2]]' = None, preface: 'bool' = False, # pylint: disable=unused-argument **kwargs: 'Any') -> '_PT': """Read (parse) packet data. Args: length: Length of packet data. version: Version of HTTP. preface: Construction keyword of :meth:`make`; whether a preface is parsed is decided by the octets alone. **kwargs: Arbitrary keyword arguments. Returns: Parsed packet data. When the payload opens with the HTTP/2 connection preface, its 24 octets are recorded as ``preface``. """ if length is None: length = len(self) # An explicit ``version=2`` -- what :meth:`_make_data` hands back for a # rebuild -- may still open with the connection preface, and the # identification in :meth:`_guess_version` is what skips it. if version is None or (version == 2 and self._test_preface(length)): http = self._guess_version(length, **kwargs) else: if version == 1: from pcapkit.protocols.application.httpv1 import HTTP as protocol # isort: skip # pylint: disable=line-too-long,import-outside-toplevel elif version == 2: from pcapkit.protocols.application.httpv2 import HTTP as protocol # type: ignore[assignment] # isort: skip # pylint: disable=line-too-long,import-outside-toplevel else: raise ProtocolError(f"invalid HTTP version: {version}") try: http = protocol(self._data, length, **kwargs) except ProtocolError: raise # NOTE: :exc:`struct.error` alongside :exc:`ValueError` because the # two are disjoint -- it derives straight from :exc:`Exception` -- # and a payload too short for a versioned parser's fixed header can # raise the former from inside the schema machinery. A caller cannot # catch that as a protocol error, so it is converted too. except (ValueError, struct.error) as error: raise ProtocolError(f'HTTP/{version}: invalid format') from error self._version = http.version self._length = http.length + self._preface_length self._http = http info = http.info if self._preface_length: info.__update__({ 'preface': _HTTP2_PREFACE, }) # The versioned parser may have kept octets after its message as the # next layer, e.g. the frames after an HTTP/2 frame. next_ = getattr(http, '_next', None) if next_ is not None and not isinstance(next_, NoPayload): self._next = next_ self._protos = ProtoChain(self.__class__, self.alias, basis=next_.protochain) return info
[docs] def make(self, version: 'Literal[1, 2]' = 1, *, preface: 'bool' = False, **kwargs: 'Any') -> '_ST': """Make (construct) packet data. Args: version: Version of HTTP. preface: Whether the HTTP/2 connection preface goes ahead of the frame; :meth:`~pcapkit.protocols.protocol.Protocol.pack` writes it. **kwargs: Arbitrary keyword arguments. Returns: The header schema the versioned ``make`` constructs. Raises: ProtocolError: If ``version`` is unknown, or if ``preface`` is requested for a version other than HTTP/2. """ if preface and version != 2: raise ProtocolError(f'HTTP/{version}: connection preface is HTTP/2 only') if version == 1: from pcapkit.protocols.application.httpv1 import HTTP as protocol # isort: skip # pylint: disable=line-too-long,import-outside-toplevel elif version == 2: from pcapkit.protocols.application.httpv2 import HTTP as protocol # type: ignore[assignment] # isort: skip # pylint: disable=line-too-long,import-outside-toplevel else: raise ProtocolError(f"invalid HTTP version: {version}") # NOTE: ``protocol.make`` is an ordinary instance method (the abstract # declaration at ``ProtocolBase.make`` takes ``self``, and the # versioned overrides use instance-bound helpers such as # ``self._make_index``/``self.__frame__``), so it cannot be called on the # class itself. There is no ``file`` or ``length`` to construct with here, # since building a packet from keyword arguments is the inverse of # parsing one, so a bare instance via ``protocol.__new__`` -- bypassing # ``__init__``'s parse/pack machinery entirely -- is what the versioned # ``make`` is called on. This is safe only while the versioned ``make`` # reads no state that ``__init__``/``__post_init__`` would otherwise have # established (neither ``HTTPv1.make`` nor ``HTTPv2.make`` does); a # ``make`` override that did would need a different dispatch here. return protocol.__new__(protocol).make(**kwargs) # type: ignore[return-value]
def pack(self, **kwargs: 'Any') -> 'bytes': """Pack (construct) packet data. Args: **kwargs: Arbitrary keyword arguments. Returns: Constructed packet data, with the HTTP/2 connection preface ahead of the frame if ``preface`` is set. """ data = super().pack(**kwargs) if kwargs.get('preface', False): return _HTTP2_PREFACE + data return data ########################################################################## # Utilities. ########################################################################## @classmethod def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self': """Create protocol instance from data. Args: data: Protocol data. A :obj:`dict`, as :meth:`Info.to_dict <pcapkit.corekit.infoclass.Info.to_dict>` returns it, is rebuilt into the versioned data model its keys belong to -- ``receipt`` is HTTP/1.*'s and ``sid`` HTTP/2's -- since this class's own data model carries no version. **kwargs: As :meth:`ProtocolBase.from_data <pcapkit.protocols.protocol.ProtocolBase.from_data>`. Returns: Protocol instance. """ from pcapkit.protocols.data.application.httpv1 import HTTP as Data_HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel from pcapkit.protocols.data.application.httpv2 import HTTP as Data_HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel if not isinstance(data, Data) and not issubclass(cls.__data__, (Data_HTTPv1, Data_HTTPv2)): if 'receipt' in data: data = cast('_PT', Data_HTTPv1.from_dict(data)) elif 'sid' in data: data = cast('_PT', Data_HTTPv2.from_dict(data)) return super().from_data(data, **kwargs)
[docs] @classmethod def _make_data(cls, data: '_PT') -> '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, including the ``version`` that :meth:`make` dispatches on. Raises: ProtocolError: If ``data`` is neither HTTP/1.* nor HTTP/2 data. """ # NOTE: Neither versioned data class carries a ``version`` field -- # HTTP/1.*'s lives in ``receipt`` as the start line's string, and HTTP/2 # has none -- so the version is the class of ``data`` that :meth:`read` # returned from the versioned parser. from pcapkit.protocols.application.httpv1 import HTTP as HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel from pcapkit.protocols.data.application.httpv1 import HTTP as Data_HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel from pcapkit.protocols.data.application.httpv2 import HTTP as Data_HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel if isinstance(data, Data_HTTPv1): return {'version': 1, **HTTPv1._make_data(data)} if isinstance(data, Data_HTTPv2): return {'version': 2, 'preface': bool(data.get('preface')), **HTTPv2._make_data(data)} raise ProtocolError(f"invalid HTTP data: {type(data).__name__}")
def _test_preface(self, length: 'int') -> 'bool': """Test whether the first ``length`` octets open with the HTTP/2 connection preface. Args: length: Length of packet data. Returns: :data:`True` if they do. """ preface_len = len(_HTTP2_PREFACE) return length >= preface_len and self._data[:preface_len] == _HTTP2_PREFACE
[docs] def _guess_version(self, length: 'int', **kwargs: 'Any') -> 'HTTP': """Identify the HTTP version of the payload, and parse it with that version. The payload is *identified* first and trial-parsed only as a last resort. Trying each version in turn and taking whichever parser does not object answers "did a parser accept this?" where the question is "what is this?": the HTTP/2 preface reads as a frame whose declared length is 5,265,993 (``b'PRI'`` as a 24-bit integer), and text that is not HTTP at all can be accepted the same way. Args: length: Length of packet data. Keyword Args: **kwargs: Arbitrary keyword arguments. Returns: Parsed packet data. Raises: ProtocolError: If no version in the family accepts the payload, or if an identified version's own parser refuses it. Only a ``ProtocolError`` from a trial parse falls through to the next version, and the final HTTP/2 trial also absorbs :exc:`struct.error`; any other exception from either HTTP/1 arm, or from that HTTP/2 trial, propagates unconverted. Note: This deliberately does **not** decide the following, because one payload with no flow context cannot: * **An** ``Upgrade: h2c`` **exchange** (:rfc:`7540#section-3.2`, deprecated but not removed by :rfc:`9113#section-3.1`) stays HTTP/1.1 here, and is *correctly* HTTP/1.1: on the wire the upgrade request and its ``101 Switching Protocols`` response are HTTP/1.1 messages and parse as such. The switch takes effect only *after* the ``101``, so deciding that later segments of the same connection are HTTP/2 needs per-connection state keyed on the 4-tuple, and this method is handed a single payload with no such context. Recognising the ``Upgrade: h2c`` field is possible; acting on it is not, so it is left alone rather than half-implemented. * **A mid-stream segment** -- a bare HTTP/2 frame header with no preface ahead of it, or an opaque HTTP/1 message body -- is genuinely undecidable from one payload, and the honest answer is the ``Raw`` that an escaping ``ProtocolError`` becomes under :func:`~pcapkit.corekit.decorators.beholder`. The nine-octet frame header is therefore never used to *identify* HTTP/2 ahead of HTTP/1: as identification it misfires on binary HTTP/1 bodies, which is precisely how garbage text was classified HTTP/2 to begin with. A bare frame is parsed as HTTP/2 only by the fall-through below, and only if its header is one a conforming sender could write (see ``_test_http2_frame()``): a frame type :rfc:`9113` defines, no undefined flag, and a stream that type may be sent on. Nine zero octets -- a ``DATA`` frame on stream 0, a protocol error under :rfc:`9113#section-6.1` -- are not guessed as HTTP/2. """ # NOTE: Positive identification, before any parse attempt. The preface is # a fixed 24-octet sequence that only an HTTP/2 client sends and that no # valid HTTP/1 message can begin with (see ``_HTTP2_PREFACE``), so a # prefix compare is an *answer* rather than evidence: it cannot # false-positive on HTTP/1, and it needs no parse to reach. # # ``length`` bounds the compare as well as ``self._data``, because a # caller may hand this method fewer octets than the buffer holds, and a # preface must not be claimed out of octets outside this payload. preface_len = len(_HTTP2_PREFACE) if self._test_preface(length): from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel # The preface is not a frame -- the frames begin after it # (:rfc:`9113#section-3.4` requires a ``SETTINGS`` frame immediately # following), so it is skipped rather than fed to ``httpv2.HTTP``, # which would misread it as framing. if length == preface_len: # A preface with nothing after it *is* HTTP/2, but this library's # HTTP/2 data model is one frame per packet and has no # representation for a frameless segment, so there is nothing to # return. Refused with a message that says so -- an HTTP/2 # connection opening truncated at the preface, not an # unrecognised payload -- because that distinction is the point # of identifying before parsing. raise ProtocolError('HTTP/2: connection preface with no frame') try: http = HTTPv2(self._data[preface_len:length], length - preface_len, **kwargs) except ProtocolError: raise # NOTE: Converted, unlike the HTTP/1 commit below, because this route # has no escaping-error contract to keep: a preface followed by a # malformed frame must reach the caller as something it can catch, # not as a bare stdlib error. The schema machinery raises # ``ProtocolError`` itself for a short inner field (e.g. a 16-octet # ``GOAWAY``, via ``FieldBase.length``), which the ``except # ProtocolError: raise`` above passes through, so this clause is a # backstop for any :exc:`ValueError` or :exc:`struct.error` it has # not been shown never to raise. Same normalisation as ``read``'s # explicit ``version=`` path above. except (ValueError, struct.error) as error: raise ProtocolError('HTTP/2: invalid format') from error # The preface is header, not payload -- see ``_preface_length``. self._preface_length = preface_len return http from pcapkit.protocols.application.httpv1 import HTTP as HTTPv1 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel # NOTE: The second identification: an HTTP/1 ``request-line`` or # ``status-line``, tested with the very patterns ``httpv1.HTTP`` parses # with (``_RE_METHOD``, ``_RE_VERSION`` and ``_RE_STATUS`` above, all # anchored, which ``httpv1.py`` imports from this module), so the # predicate and the parser cannot disagree about what HTTP/1 looks like. # # Identified means *committed*: a malformed HTTP/1 message is reported as # such instead of being handed to the HTTP/2 arm, which accepts any # self-consistent buffer of nine octets or more whose header passes # :func:`_test_http2_frame`. Retrying an identified HTTP/1 payload as # HTTP/2 is how HTTP/1 traffic acquires a confident HTTP/2 mislabel. # # Nothing is suppressed on this arm, deliberately: it is not a candidate # to be declined, so there is nothing to decline *to*, and # ``test_guess_version_does_not_suppress_struct_error_on_the_http1_arm`` # pins that a :exc:`struct.error` -- or pcapkit's own ``StructError``, # whose ``eof`` flag ``NoPayload`` handling reads -- reaches the caller # from this route rather than being converted or swallowed. if test_start_line(self._data[:length]): return HTTPv1(self._data, length, **kwargs) # NOTE: Neither identification matched, so this is the fall-through: a # trial parse, kept because a *mid-stream* payload carries no start line # and no preface, and a self-consistent HTTP/2 frame is still the best # answer available for one. It is the last resort, not the whole method. # # The arms of this trial suppress different sets, deliberately. Only the *last* # arm also suppresses :exc:`struct.error`, as defence in depth: a payload # too short for HTTP/2's nine-octet frame header, or whose frame-specific # fields exceed what the buffer holds, must not escape as a stdlib # exception that no caller of this method can catch. Such payloads raise # ``ProtocolError`` at the source -- ``httpv2.HTTP.unpack`` rejects a # buffer under nine octets, and :attr:`FieldBase.length # <pcapkit.corekit.fields.field.FieldBase.length>` re-raises a # negative-length template as # :exc:`~pcapkit.utilities.exceptions.ProtocolError` -- so the # suppression only guards whatever path is not yet known to do likewise. # # Suppressing it on the *first* arm is wrong: nine byte patterns over # lengths 0-24 raised only ``ProtocolError`` through ``httpv1.HTTP``, so # it buys nothing, and # :class:`~pcapkit.utilities.exceptions.StructError` *subclasses* # :exc:`struct.error`, so suppressing it on a non-final arm swallows # pcapkit's own signal and hands the payload to the arm below, which # accepts any self-consistent buffer whose header passes # :func:`_test_http2_frame`. With a fault injected at arm 1 and without # that test, a *valid* HTTP/1.1 request came back ``version='2'``, and # over UDP port 80 its ``protochain`` read ``UDP:HTTP/2``. A confident # HTTP/2 mislabel of HTTP/1 traffic is worse than letting the error # escape to ``beholder``, which turns it into ``Raw``; it would also # erase ``StructError.eof``, which ``NoPayload`` handling reads. with contextlib.suppress(ProtocolError): return HTTPv1(self._data, length, **kwargs) # NOTE: The HTTP/2 trial runs only on a header that passes # :func:`_test_http2_frame`. Since a frame's Length counts the payload # alone (:rfc:`9113#section-4.1`), any nine octets with a small enough # Length are self-consistent, so the parser's own checks accept most # zero-heavy binary as a ``DATA`` frame or a frame of unknown type. The # test rejects only headers that no conforming sender writes, and it # gates this guess alone: an explicit ``version=2`` is not affected. if not _test_http2_frame(self._data[:length]): raise ProtocolError("unknown HTTP version") from pcapkit.protocols.application.httpv2 import HTTP as HTTPv2 # isort: skip # pylint: disable=line-too-long,import-outside-toplevel with contextlib.suppress(ProtocolError, struct.error): return HTTPv2(self._data, length, **kwargs) raise ProtocolError("unknown HTTP version")