123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- # High level events that make up HTTP/1.1 conversations. Loosely inspired by
- # the corresponding events in hyper-h2:
- #
- # http://python-hyper.org/h2/en/stable/api.html#events
- #
- # Don't subclass these. Stuff will break.
- import re
- from abc import ABC
- from dataclasses import dataclass, field
- from typing import Any, cast, Dict, List, Tuple, Union
- from ._abnf import method, request_target
- from ._headers import Headers, normalize_and_validate
- from ._util import bytesify, LocalProtocolError, validate
- # Everything in __all__ gets re-exported as part of the h11 public API.
- __all__ = [
- "Event",
- "Request",
- "InformationalResponse",
- "Response",
- "Data",
- "EndOfMessage",
- "ConnectionClosed",
- ]
- method_re = re.compile(method.encode("ascii"))
- request_target_re = re.compile(request_target.encode("ascii"))
- class Event(ABC):
- """
- Base class for h11 events.
- """
- __slots__ = ()
- @dataclass(init=False, frozen=True)
- class Request(Event):
- """The beginning of an HTTP request.
- Fields:
- .. attribute:: method
- An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte
- string. :term:`Bytes-like objects <bytes-like object>` and native
- strings containing only ascii characters will be automatically
- converted to byte strings.
- .. attribute:: target
- The target of an HTTP request, e.g. ``b"/index.html"``, or one of the
- more exotic formats described in `RFC 7320, section 5.3
- <https://tools.ietf.org/html/rfc7230#section-5.3>`_. Always a byte
- string. :term:`Bytes-like objects <bytes-like object>` and native
- strings containing only ascii characters will be automatically
- converted to byte strings.
- .. attribute:: headers
- Request headers, represented as a list of (name, value) pairs. See
- :ref:`the header normalization rules <headers-format>` for details.
- .. attribute:: http_version
- The HTTP protocol version, represented as a byte string like
- ``b"1.1"``. See :ref:`the HTTP version normalization rules
- <http_version-format>` for details.
- """
- __slots__ = ("method", "headers", "target", "http_version")
- method: bytes
- headers: Headers
- target: bytes
- http_version: bytes
- def __init__(
- self,
- *,
- method: Union[bytes, str],
- headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],
- target: Union[bytes, str],
- http_version: Union[bytes, str] = b"1.1",
- _parsed: bool = False,
- ) -> None:
- super().__init__()
- if isinstance(headers, Headers):
- object.__setattr__(self, "headers", headers)
- else:
- object.__setattr__(
- self, "headers", normalize_and_validate(headers, _parsed=_parsed)
- )
- if not _parsed:
- object.__setattr__(self, "method", bytesify(method))
- object.__setattr__(self, "target", bytesify(target))
- object.__setattr__(self, "http_version", bytesify(http_version))
- else:
- object.__setattr__(self, "method", method)
- object.__setattr__(self, "target", target)
- object.__setattr__(self, "http_version", http_version)
- # "A server MUST respond with a 400 (Bad Request) status code to any
- # HTTP/1.1 request message that lacks a Host header field and to any
- # request message that contains more than one Host header field or a
- # Host header field with an invalid field-value."
- # -- https://tools.ietf.org/html/rfc7230#section-5.4
- host_count = 0
- for name, value in self.headers:
- if name == b"host":
- host_count += 1
- if self.http_version == b"1.1" and host_count == 0:
- raise LocalProtocolError("Missing mandatory Host: header")
- if host_count > 1:
- raise LocalProtocolError("Found multiple Host: headers")
- validate(method_re, self.method, "Illegal method characters")
- validate(request_target_re, self.target, "Illegal target characters")
- # This is an unhashable type.
- __hash__ = None # type: ignore
- @dataclass(init=False, frozen=True)
- class _ResponseBase(Event):
- __slots__ = ("headers", "http_version", "reason", "status_code")
- headers: Headers
- http_version: bytes
- reason: bytes
- status_code: int
- def __init__(
- self,
- *,
- headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],
- status_code: int,
- http_version: Union[bytes, str] = b"1.1",
- reason: Union[bytes, str] = b"",
- _parsed: bool = False,
- ) -> None:
- super().__init__()
- if isinstance(headers, Headers):
- object.__setattr__(self, "headers", headers)
- else:
- object.__setattr__(
- self, "headers", normalize_and_validate(headers, _parsed=_parsed)
- )
- if not _parsed:
- object.__setattr__(self, "reason", bytesify(reason))
- object.__setattr__(self, "http_version", bytesify(http_version))
- if not isinstance(status_code, int):
- raise LocalProtocolError("status code must be integer")
- # Because IntEnum objects are instances of int, but aren't
- # duck-compatible (sigh), see gh-72.
- object.__setattr__(self, "status_code", int(status_code))
- else:
- object.__setattr__(self, "reason", reason)
- object.__setattr__(self, "http_version", http_version)
- object.__setattr__(self, "status_code", status_code)
- self.__post_init__()
- def __post_init__(self) -> None:
- pass
- # This is an unhashable type.
- __hash__ = None # type: ignore
- @dataclass(init=False, frozen=True)
- class InformationalResponse(_ResponseBase):
- """An HTTP informational response.
- Fields:
- .. attribute:: status_code
- The status code of this response, as an integer. For an
- :class:`InformationalResponse`, this is always in the range [100,
- 200).
- .. attribute:: headers
- Request headers, represented as a list of (name, value) pairs. See
- :ref:`the header normalization rules <headers-format>` for
- details.
- .. attribute:: http_version
- The HTTP protocol version, represented as a byte string like
- ``b"1.1"``. See :ref:`the HTTP version normalization rules
- <http_version-format>` for details.
- .. attribute:: reason
- The reason phrase of this response, as a byte string. For example:
- ``b"OK"``, or ``b"Not Found"``.
- """
- def __post_init__(self) -> None:
- if not (100 <= self.status_code < 200):
- raise LocalProtocolError(
- "InformationalResponse status_code should be in range "
- "[100, 200), not {}".format(self.status_code)
- )
- # This is an unhashable type.
- __hash__ = None # type: ignore
- @dataclass(init=False, frozen=True)
- class Response(_ResponseBase):
- """The beginning of an HTTP response.
- Fields:
- .. attribute:: status_code
- The status code of this response, as an integer. For an
- :class:`Response`, this is always in the range [200,
- 1000).
- .. attribute:: headers
- Request headers, represented as a list of (name, value) pairs. See
- :ref:`the header normalization rules <headers-format>` for details.
- .. attribute:: http_version
- The HTTP protocol version, represented as a byte string like
- ``b"1.1"``. See :ref:`the HTTP version normalization rules
- <http_version-format>` for details.
- .. attribute:: reason
- The reason phrase of this response, as a byte string. For example:
- ``b"OK"``, or ``b"Not Found"``.
- """
- def __post_init__(self) -> None:
- if not (200 <= self.status_code < 1000):
- raise LocalProtocolError(
- "Response status_code should be in range [200, 1000), not {}".format(
- self.status_code
- )
- )
- # This is an unhashable type.
- __hash__ = None # type: ignore
- @dataclass(init=False, frozen=True)
- class Data(Event):
- """Part of an HTTP message body.
- Fields:
- .. attribute:: data
- A :term:`bytes-like object` containing part of a message body. Or, if
- using the ``combine=False`` argument to :meth:`Connection.send`, then
- any object that your socket writing code knows what to do with, and for
- which calling :func:`len` returns the number of bytes that will be
- written -- see :ref:`sendfile` for details.
- .. attribute:: chunk_start
- A marker that indicates whether this data object is from the start of a
- chunked transfer encoding chunk. This field is ignored when when a Data
- event is provided to :meth:`Connection.send`: it is only valid on
- events emitted from :meth:`Connection.next_event`. You probably
- shouldn't use this attribute at all; see
- :ref:`chunk-delimiters-are-bad` for details.
- .. attribute:: chunk_end
- A marker that indicates whether this data object is the last for a
- given chunked transfer encoding chunk. This field is ignored when when
- a Data event is provided to :meth:`Connection.send`: it is only valid
- on events emitted from :meth:`Connection.next_event`. You probably
- shouldn't use this attribute at all; see
- :ref:`chunk-delimiters-are-bad` for details.
- """
- __slots__ = ("data", "chunk_start", "chunk_end")
- data: bytes
- chunk_start: bool
- chunk_end: bool
- def __init__(
- self, data: bytes, chunk_start: bool = False, chunk_end: bool = False
- ) -> None:
- object.__setattr__(self, "data", data)
- object.__setattr__(self, "chunk_start", chunk_start)
- object.__setattr__(self, "chunk_end", chunk_end)
- # This is an unhashable type.
- __hash__ = None # type: ignore
- # XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that
- # are forbidden to be sent in a trailer, since processing them as if they were
- # present in the header section might bypass external security filters."
- # https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part
- # Unfortunately, the list of forbidden fields is long and vague :-/
- @dataclass(init=False, frozen=True)
- class EndOfMessage(Event):
- """The end of an HTTP message.
- Fields:
- .. attribute:: headers
- Default value: ``[]``
- Any trailing headers attached to this message, represented as a list of
- (name, value) pairs. See :ref:`the header normalization rules
- <headers-format>` for details.
- Must be empty unless ``Transfer-Encoding: chunked`` is in use.
- """
- __slots__ = ("headers",)
- headers: Headers
- def __init__(
- self,
- *,
- headers: Union[
- Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None
- ] = None,
- _parsed: bool = False,
- ) -> None:
- super().__init__()
- if headers is None:
- headers = Headers([])
- elif not isinstance(headers, Headers):
- headers = normalize_and_validate(headers, _parsed=_parsed)
- object.__setattr__(self, "headers", headers)
- # This is an unhashable type.
- __hash__ = None # type: ignore
- @dataclass(frozen=True)
- class ConnectionClosed(Event):
- """This event indicates that the sender has closed their outgoing
- connection.
- Note that this does not necessarily mean that they can't *receive* further
- data, because TCP connections are composed to two one-way channels which
- can be closed independently. See :ref:`closing` for details.
- No fields.
- """
- pass
|