Spaces:
Running
Running
# 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__ = () | |
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 | |
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 | |
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 | |
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 | |
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 :-/ | |
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 | |
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 | |