# -*- coding: utf-8 -*-
"""HTTP - Hypertext Transfer Protocol
========================================
.. module:: pcapkit.protocols.application.http.protocol
:mod:`pcapkit.protocols.application.http.protocol` holds
:class:`~pcapkit.protocols.application.http.protocol.HTTP`, the base class of the
Hypertext Transfer Protocol (HTTP) [*]_ family --
:class:`HTTP/1.* <pcapkit.protocols.application.httpv1.protocol.HTTPv1>` and
:class:`HTTP/2 <pcapkit.protocols.application.httpv2.protocol.HTTPv2>` -- and
:func:`~pcapkit.protocols.application.http.protocol.test_start_line`, which tells
whether a payload opens with an HTTP/1.* start line.
.. [*] https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
.. versionchanged:: 1.6.0
A payload a transport port binds to
:class:`~pcapkit.protocols.application.http.protocol.HTTP`, and constructing or
rebuilding that class, give
:class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2` rather than an instance
of it, so the data sits under ``httpv1`` or ``httpv2`` rather than ``http``
(:issue:`2524`). Building it from fields takes ``version`` and is refused
without it, where it built HTTP/1.*, and its
:meth:`~pcapkit.protocols.application.http.protocol.HTTP.from_data` refuses a
:obj:`dict`, where it read the ``receipt`` or ``sid`` key (:issue:`2628`).
"""
import contextlib
import re
import struct
from typing import TYPE_CHECKING, Generic, cast
from pcapkit.corekit.module import ModuleDescriptor
from pcapkit.corekit.protochain import ProtoChain
from pcapkit.protocols.application.application import Application
from pcapkit.protocols.misc.no_payload.protocol import NoPayload
from pcapkit.protocols.protocol import _PT, _ST
from pcapkit.utilities.exceptions import ProtocolError
if TYPE_CHECKING:
from typing import IO, Any, Optional, Type
from typing_extensions import Literal, Self
from pcapkit.const.reg.apptype.tcp import TCP as AppType_TCP
from pcapkit.protocols.application.httpv1.data import HTTPv1 as Data_HTTPv1
from pcapkit.protocols.application.httpv1.protocol import HTTPv1
from pcapkit.protocols.application.httpv2.data import HTTPv2 as Data_HTTPv2
from pcapkit.protocols.application.httpv2.protocol import HTTPv2
from pcapkit.protocols.protocol import ProtocolBase
__all__ = ['HTTP', 'test_start_line']
#: The TCP port registry :meth:`HTTP.__index__` reads its port from, resolved
#: there rather than imported, as ``import pcapkit`` leaves it unloaded
#: (:issue:`1538`), so its first import holds
#: :data:`~pcapkit.utilities.importlib.import_lock` (:issue:`2771`).
_TCP: 'ModuleDescriptor[AppType_TCP]' = ModuleDescriptor('pcapkit.const.reg.apptype.tcp', 'TCP')
#: The HTTP/2 connection preface (:rfc:`9113#section-3.4`), which opens every
#: HTTP/2 connection. Its method ``PRI`` is registered as one no actual client
#: uses (:rfc:`7540#section-11.6`), so a prefix match identifies HTTP/2, and
#: :meth:`HTTP._guess_version` tests it before 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.
A plausibility test for :meth:`HTTP._guess_version`'s fall-through only:
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2` itself parses
every frame :rfc:`9113` lets a receiver accept, including frames this refuses.
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:
No frame a conforming peer sends fails: undefined flags and the reserved
bit "MUST be left unset (0x00) when sending" (:rfc:`9113#section-4.1`),
and breaking a stream rule is a connection error (e.g. ``DATA`` on
stream 0, :rfc:`9113#section-6.1`). Frame types :rfc:`9113` does not
define are legal (:rfc:`9113#section-5.5`) but not guessed: on a payload
with no other evidence they are mostly arbitrary binary.
"""
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
# HTTP method token, case-sensitive (RFC 9110, section 9.1). Anchored at the end
# too, as ``re.match`` anchors only at the start and would take ``G`` of ``Get``.
_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)")
# HTTP status code, ``3DIGIT`` (RFC 9112, section 4). Anchored as ``_RE_METHOD``
# is: ``HTTPv1`` takes ``int()`` of the raw token, so a prefix match would let
# ``200x`` through to a bare ``ValueError`` rather than a ``ProtocolError``.
_RE_STATUS = re.compile(rb'\d{3}\Z')
# The HTTP/2 preface's header block, ``PRI * HTTP/2.0``, as ``HTTPv1.read`` splits
# it off at the first CRLFCRLF. The patterns above accept it, so
# ``HTTPv1._read_http_header`` and ``test_start_line`` refuse it by name.
_HTTP2_PREFACE_HEADER = _HTTP2_PREFACE.split(b'\r\n\r\n', maxsplit=1)[0]
#: The member :class:`HTTP` hands an HTTP/1.* message to; it, the HTTP/2 member and
#: their data models are resolved where used rather than imported, so a member's first
#: import holds :data:`~pcapkit.utilities.importlib.import_lock` (:issue:`2771`).
_HTTPV1: 'ModuleDescriptor[HTTPv1]' = \
ModuleDescriptor('pcapkit.protocols.application.httpv1.protocol', 'HTTPv1')
_HTTPV2: 'ModuleDescriptor[HTTPv2]' = \
ModuleDescriptor('pcapkit.protocols.application.httpv2.protocol', 'HTTPv2')
_DATA_HTTPV1: 'ModuleDescriptor[Data_HTTPv1]' = \
ModuleDescriptor('pcapkit.protocols.application.httpv1.data', 'HTTPv1')
_DATA_HTTPV2: 'ModuleDescriptor[Data_HTTPv2]' = \
ModuleDescriptor('pcapkit.protocols.application.httpv2.data', 'HTTPv2')
[docs]
def test_start_line(data: 'bytes') -> 'bool':
"""Whether ``data`` opens with an HTTP/1.* start line.
A classification predicate for :meth:`HTTP._guess_version
<pcapkit.protocols.application.http.protocol.HTTP._guess_version>`; it parses
nothing. Trial-parsing every version instead would classify a non-HTTP payload
by which parser failed 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:
It accepts exactly what :meth:`HTTPv1._read_http_header
<pcapkit.protocols.application.httpv1.protocol.HTTPv1._read_http_header>`
accepts -- ``_RE_METHOD`` with ``_RE_VERSION`` for a request,
``_RE_VERSION`` with ``_RE_STATUS`` for a response -- which is why
:mod:`~pcapkit.protocols.application.httpv1.protocol` imports those patterns
from here. A looser predicate classifies payloads the parser refuses; a
tighter one hands real HTTP/1 to a later arm.
``test_start_line_predicate_agrees_with_the_httpv1_parser`` pins the
agreement.
It mirrors the parser's earlier checks too, which keep out the HTTP/2
connection preface, itself a well-formed HTTP/1.1 request line
(:rfc:`9113#section-3.4`). Split at the header/body separator, as
:meth:`HTTPv1.read
<pcapkit.protocols.application.httpv1.protocol.HTTPv1.read>` does, the
preface's header is exactly ``PRI * HTTP/2.0``
(``_HTTP2_PREFACE_HEADER``), which both refuse by name. Any other header
with no CRLF 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 has only two tokens, so both refuse it.
"""
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`]
It dispatches on both paths: constructing it, from octets or from fields,
and :meth:`from_data` give
:class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`, never an instance of
this class (:issue:`2524`). ``version`` names the member; without it, octets
give the one :meth:`_dispatch_version` identifies, and fields are refused,
since no field picks a member (:issue:`2574`). :meth:`from_data` gives the
member whose data model it is handed, and refuses a :obj:`dict`. See
:meth:`__new__`.
"""
if TYPE_CHECKING:
#: Saved subclass protocol data (only for HTTP base class).
_http: 'HTTP[_PT, _ST]'
#: Octets consumed ahead of the identified version's header -- the HTTP/2
#: connection preface, when :meth:`_guess_version` found one. They are header,
#: so :meth:`read` adds them to :attr:`length`; otherwise
#: :meth:`Protocol.__init__ <pcapkit.protocols.protocol.Protocol.__init__>`
#: would report the tail of the preface as the packet payload.
_preface_length = 0
#: :obj:`None`, opting out of the construction keyword check of
#: :meth:`Protocol.__init__ <pcapkit.protocols.protocol.Protocol.__init__>`:
#: :meth:`make` declares only ``version`` and forwards the rest to
#: :meth:`HTTPv1.make <pcapkit.protocols.application.httpv1.protocol.HTTPv1.make>` or
#: :meth:`HTTPv2.make <pcapkit.protocols.application.httpv2.protocol.HTTPv2.make>`,
#: so the valid names depend on an argument. The versioned classes are
#: checked 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/1"], Literal["HTTP/2"]]':
"""Index ID of the protocol.
The family base lists its members' names alone, as
:meth:`IP.id() <pcapkit.protocols.internet.ip.protocol.IP.id>` does: ``HTTP/1``
for :rfc:`9112`'s HTTP/1.1 and the versions before it, and :rfc:`9113`'s
``HTTP/2``. A dispatched layer is the member :meth:`_dispatch_version`
picks, so each name selects its own version, and the ``HTTP`` key
selects either through this tuple.
Returns:
Index ID of the protocol; the versioned classes narrow it to their
own name.
.. versionchanged:: 1.6.0
``('HTTP/1', 'HTTP/2')``, where it was ``('HTTP', 'HTTPv1', 'HTTPv2')``
(:issue:`2524`).
"""
return ('HTTP/1', 'HTTP/2')
[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:
protocol = _HTTPV1.klass # type: Type[HTTP]
elif version == 2:
protocol = _HTTPV2.klass
else:
raise ProtocolError(f'HTTP: invalid version: {version!r}')
try:
http = protocol(self._data, length, **kwargs)
except ProtocolError:
raise
# NOTE: ``struct.error`` is no ``ValueError``, and a payload too
# short for a versioned parser's fixed header can raise it.
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: 'Optional[Literal[1, 2]]' = None, *,
preface: 'bool' = False,
**kwargs: 'Any') -> '_ST':
"""Make (construct) packet data.
Args:
version: Version of HTTP, ``1`` or ``2``. There is no default: no
field picks a member (:issue:`2574`).
preface: Whether the HTTP/2 connection preface goes ahead of the
frame; :meth:`pack` writes it.
**kwargs: Arbitrary keyword arguments.
Returns:
The header schema the versioned ``make`` constructs.
Raises:
ProtocolError: If ``version`` is missing or names neither member, or
if ``preface`` is requested for a version other than HTTP/2.
"""
if version is None:
raise ProtocolError('HTTP: no version to build with; pass version=1 or version=2')
protocol = self._member_of_version(version)
if preface and version != 2:
raise ProtocolError(f'HTTP/{version}: connection preface is HTTP/2 only')
# NOTE: With no file to build from, versioned ``make`` runs on a bare ``__new__``
# instance, which is safe while neither version's ``make`` reads ``__init__`` state.
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
[docs]
@classmethod
def from_data(cls, data: '_PT | dict[str, Any]', **kwargs: 'Any') -> 'Self':
"""Create protocol instance from data.
Args:
data: Protocol data.
**kwargs: As :meth:`Protocol.from_data
<pcapkit.protocols.protocol.Protocol.from_data>`.
Returns:
Protocol instance; on this class, an instance of the member whose data
model ``data`` is (:issue:`2524`).
Raises:
ProtocolError: If ``data`` is neither member's data model, on this
class. A :obj:`dict` is refused: it carries no version, and no
key picks a member (:issue:`2574`).
"""
if cls is not HTTP:
return super().from_data(data, **kwargs)
HTTPv1 = _HTTPV1.klass # pylint: disable=invalid-name
HTTPv2 = _HTTPV2.klass # pylint: disable=invalid-name
Data_HTTPv1, Data_HTTPv2 = _DATA_HTTPV1.klass, _DATA_HTTPV2.klass # pylint: disable=invalid-name
if isinstance(data, Data_HTTPv1):
return cast('Self', HTTPv1.from_data(data, **kwargs))
if isinstance(data, Data_HTTPv2):
return cast('Self', HTTPv2.from_data(data, **kwargs))
raise ProtocolError('HTTP: invalid data: must be an HTTPv1 or HTTPv2 data model')
##########################################################################
# Data models.
##########################################################################
[docs]
@classmethod
def __index__(cls) -> 'int': # type: ignore[override]
"""Numeral registry index of the protocol.
Returns:
Numeral registry index of the protocol in `IANA`_ service names and
port numbers: TCP port 80, i.e.
:attr:`TCP.http <pcapkit.const.reg.apptype.tcp.TCP.http>` as an
:class:`int` (:ref:`registry-protocol-index`). The versioned classes
inherit it.
.. _IANA: https://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xhtml
.. versionchanged:: 1.6.0
Returns TCP port 80, where it raised
:exc:`~pcapkit.utilities.exceptions.IntError` (:issue:`2627`).
"""
return int(_TCP.klass.http) # type: ignore[call-overload]
[docs]
def __init__(self, file: 'Optional[IO[bytes] | bytes]' = None,
length: 'Optional[int]' = None, *,
version: 'Optional[Literal[1, 2]]' = None, **kwargs: 'Any') -> 'None':
"""Initialisation.
Args:
file: Source packet stream.
length: Length of packet data.
version: Version of HTTP, ``1`` or ``2``: the member :meth:`__new__`
picks. Building from fields, it must be this class's own. Parsing,
it is handed on unread.
**kwargs: As :meth:`Protocol.__init__
<pcapkit.protocols.protocol.Protocol.__init__>`.
Raises:
ProtocolError: If, building from fields, ``version`` names neither
member, or is not this class's own version: that of the member
it is or derives from, which a subclass outside the members
does not have; or if ``preface`` is asked of HTTP/1.
"""
if file is not None:
if version is not None:
kwargs['version'] = version
else:
HTTPv1, HTTPv2 = _HTTPV1.klass, _HTTPV2.klass # pylint: disable=invalid-name
if version is not None and not isinstance(self, self._member_of_version(version)):
if not isinstance(self, (HTTPv1, HTTPv2)):
raise ProtocolError(f'{type(self).__name__}: no version of its own to match '
f'version={version!r}')
raise ProtocolError(f'{type(self).__name__}: version={version!r} '
'names another member')
# NOTE: ``pack`` writes the preface for any member, so HTTP/1 refuses it here.
if kwargs.get('preface') and isinstance(self, HTTPv1):
raise ProtocolError('HTTP/1: connection preface is HTTP/2 only')
super().__init__(file, length, **kwargs) # type: ignore[arg-type]
[docs]
def __new__(cls, file: 'Optional[IO[bytes] | bytes]' = None, # type: ignore[misc]
length: 'Optional[int]' = None, **kwargs: 'Any') -> 'HTTP':
"""Create the member a construction of this class describes.
The ``version`` keyword names the member, on either path. Without it,
parsing octets gives the member :meth:`_dispatch_version` identifies,
since HTTP octets carry their own version, and building from fields is
refused, since no field picks a member (:issue:`2574`). That member's
``__init__`` then runs with the same arguments. A subclass of this class
is created as itself.
Args:
file: Source packet stream, read here without being consumed.
length: Length of packet data.
**kwargs: Arbitrary keyword arguments.
Returns:
An uninitialised instance of the member.
Raises:
ProtocolError: If ``version`` names neither member; or, without it,
if building from fields, or if the octets are neither version's,
including an HTTP/2 connection preface with no frame after it.
"""
if cls is not HTTP:
return super().__new__(cls, file, length, **kwargs) # type: ignore[call-arg]
if kwargs.get('version') is not None:
member = cls._member_of_version(kwargs['version'])
elif file is None:
raise ProtocolError('HTTP: no version to build with; pass version=1 or version=2')
else:
if isinstance(file, bytes):
data = file
else:
offset = file.tell()
data = file.read(length) # type: ignore[arg-type]
file.seek(offset)
from pcapkit.protocols.misc.raw.protocol import Raw # isort: skip # pylint: disable=import-outside-toplevel
data = data[:length]
member = cls._dispatch_version(data, len(data))
if member is Raw:
if data == _HTTP2_PREFACE:
raise ProtocolError('HTTP/2: connection preface with no frame')
raise ProtocolError('HTTP: unknown version')
return member.__new__(member, file, length, **kwargs)
##########################################################################
# Utilities.
##########################################################################
[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 model has a ``version`` field, so the
# version is the class of ``data``.
HTTPv1 = _HTTPV1.klass # pylint: disable=invalid-name
HTTPv2 = _HTTPV2.klass # pylint: disable=invalid-name
Data_HTTPv1, Data_HTTPv2 = _DATA_HTTPV1.klass, _DATA_HTTPV2.klass # pylint: disable=invalid-name
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'HTTP: invalid data: must be HTTP/1.* or HTTP/2 data, '
f'not {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.
A parser accepting a payload does not make it that version: the HTTP/2
preface reads as a frame of declared length 5,263,945 (``b'PRI'`` as a
24-bit integer), and non-HTTP text can be accepted the same way.
Neither HTTP/1 arm absorbs :exc:`struct.error`, so it and its subclass
``StructError``, whose ``eof`` ``NoPayload`` handling reads, reach the
caller. Absorbed on the trial arm, they would hand valid HTTP/1 to the
HTTP/2 arm, mislabelling it; escaping, they make it ``Raw`` under
``beholder`` instead. The final HTTP/2 trial absorbs it only as a backstop:
``HTTPv2.unpack`` and ``FieldBase.length`` already raise
:exc:`~pcapkit.utilities.exceptions.ProtocolError` for a short buffer.
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
:exc:`~pcapkit.utilities.exceptions.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:
One payload with no flow context cannot decide these, so this does
not:
* **An** ``Upgrade: h2c`` **exchange** (:rfc:`7540#section-3.2`,
deprecated by :rfc:`9113#section-3.1`) parses as the HTTP/1.1 it is
on the wire. HTTP/2 starts only after the ``101 Switching
Protocols`` response, and telling that needs per-connection state
keyed on the 4-tuple.
* **A mid-stream segment** -- a bare HTTP/2 frame header with no
preface, or an opaque HTTP/1 body -- is undecidable, and becomes
the :class:`~pcapkit.protocols.misc.raw.protocol.Raw` an escaping
:exc:`~pcapkit.utilities.exceptions.ProtocolError` turns into under
:func:`~pcapkit.corekit.decorators.beholder`. A frame header never
identifies HTTP/2 ahead of HTTP/1, as it misfires on binary HTTP/1
bodies and garbage text. A bare frame is parsed as HTTP/2 only by
the fall-through, and only if ``_test_http2_frame()`` accepts its
header, so nine zero octets -- ``DATA`` on stream 0, a protocol
error under :rfc:`9113#section-6.1` -- are not guessed as HTTP/2.
"""
# NOTE: Identified before any parse, as no HTTP/1 message begins with the preface;
# ``length`` bounds the compare, so octets past the payload cannot make a preface.
preface_len = len(_HTTP2_PREFACE)
if self._test_preface(length):
HTTPv2 = _HTTPV2.klass # pylint: disable=invalid-name
# The preface is not a frame, so it is skipped rather than parsed;
# the frames follow it (RFC 9113, section 3.4).
if length == preface_len:
# HTTP/2, but the data model holds one frame per packet, so a
# frameless segment is refused, as such rather than as unknown.
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 on the HTTP/1 arm, so a bad frame is catchable; only a
# backstop, as the schema raises ``ProtocolError`` for a short field itself.
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
HTTPv1 = _HTTPV1.klass # pylint: disable=invalid-name
# NOTE: A start line, matched with ``HTTPv1``'s own patterns, identifies HTTP/1; a
# malformed one is then reported, not retried as HTTP/2, which would mislabel it.
if test_start_line(self._data[:length]):
return HTTPv1(self._data, length, **kwargs)
# NOTE: Trial parse for a mid-stream payload; ``struct.error`` escapes it, since
# absorbing its subclass ``StructError`` would hand valid HTTP/1 to the HTTP/2 arm.
with contextlib.suppress(ProtocolError):
return HTTPv1(self._data, length, **kwargs)
# NOTE: Nine octets with a small Length parse as a frame (RFC 9113, section 4.1), so
# this guess, unlike an explicit ``version=2``, first needs ``_test_http2_frame``.
if not _test_http2_frame(self._data[:length]):
raise ProtocolError('HTTP: unknown version')
with contextlib.suppress(ProtocolError, struct.error):
return _HTTPV2.klass(self._data, length, **kwargs)
raise ProtocolError('HTTP: unknown version')
[docs]
@staticmethod
def _member_of_version(version: 'Any') -> 'Type[HTTP]':
"""The member a ``version`` of ``1`` or ``2`` names.
Args:
version: Version of HTTP.
Returns:
:class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`.
Raises:
ProtocolError: If ``version`` is neither, or is no :obj:`int`: a
:obj:`bool` or a :obj:`float` is refused even where it equals one.
"""
HTTPv1, HTTPv2 = _HTTPV1.klass, _HTTPV2.klass # pylint: disable=invalid-name
if isinstance(version, int) and not isinstance(version, bool):
if version == 1:
return HTTPv1
if version == 2:
return HTTPv2
raise ProtocolError(f'HTTP: invalid version: {version!r}')
[docs]
@classmethod
def _dispatch_version(cls, data: 'bytes', length: 'int') -> 'Type[ProtocolBase]':
"""Pick the class that parses the HTTP message at the start of ``data``.
The payload is identified before any class is built, as
:meth:`_guess_version` identifies it: the connection preface, or else a
frame header a conforming sender could write, is HTTP/2, and an HTTP/1
start line is HTTP/1. A transport layer whose port binds this class
parses its payload with the class this returns, so a dispatched layer
is :class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`, never this base
(:issue:`2524`).
It is the counterpart of :meth:`IGMP._dispatch
<pcapkit.protocols.internet.igmp.protocol.IGMP._dispatch>`, named apart
because ``_dispatch`` already names :meth:`AppType._dispatch
<pcapkit.const.reg.apptype.apptype.AppType._dispatch>`, the port
registries' lookup hook.
Args:
data: Payload of the transport layer, starting at the HTTP message.
length: The HTTP message's length, in octets.
Returns:
:class:`~pcapkit.protocols.application.httpv1.protocol.HTTPv1` or
:class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2` for the version
the payload identifies as; :class:`~pcapkit.protocols.misc.raw.protocol.Raw`
for one it does not, which then keeps its octets undissected.
Note:
A connection preface with no frame after it, which a client may send
in a segment of its own, is
:class:`~pcapkit.protocols.misc.raw.protocol.Raw` here: the HTTP/2
data model holds one frame per packet, with its frame header fields
required, and :class:`~pcapkit.protocols.application.httpv2.protocol.HTTPv2`
refuses a frameless segment.
"""
HTTPv1, HTTPv2 = _HTTPV1.klass, _HTTPV2.klass # pylint: disable=invalid-name
from pcapkit.protocols.misc.raw.protocol import Raw # isort: skip # pylint: disable=import-outside-toplevel
data = data[:length]
if data.startswith(_HTTP2_PREFACE):
return HTTPv2 if len(data) > len(_HTTP2_PREFACE) else Raw
if test_start_line(data):
return HTTPv1
if _test_http2_frame(data):
return HTTPv2
return Raw