codekingpro/portable-devtools
114k
1# High level events that make up HTTP/1.1 conversations. Loosely inspired by2# the corresponding events in hyper-h2:3#4# http://python-hyper.org/h2/en/stable/api.html#events5#6# Don't subclass these. Stuff will break.7 8import re9from abc import ABC10from dataclasses import dataclass, field11from typing import Any, cast, Dict, List, Tuple, Union12 13from ._abnf import method, request_target14from ._headers import Headers, normalize_and_validate15from ._util import bytesify, LocalProtocolError, validate16 17# Everything in __all__ gets re-exported as part of the h11 public API.18__all__ = [19 "Event",20 "Request",21 "InformationalResponse",22 "Response",23 "Data",24 "EndOfMessage",25 "ConnectionClosed",26]27 28method_re = re.compile(method.encode("ascii"))29request_target_re = re.compile(request_target.encode("ascii"))30 31 32class Event(ABC):33 """34 Base class for h11 events.35 """36 37 __slots__ = ()38 39 40@dataclass(init=False, frozen=True)41class Request(Event):42 """The beginning of an HTTP request.43 44 Fields:45 46 .. attribute:: method47 48 An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte49 string. :term:`Bytes-like objects <bytes-like object>` and native50 strings containing only ascii characters will be automatically51 converted to byte strings.52 53 .. attribute:: target54 55 The target of an HTTP request, e.g. ``b"/index.html"``, or one of the56 more exotic formats described in `RFC 7320, section 5.357 <https://tools.ietf.org/html/rfc7230#section-5.3>`_. Always a byte58 string. :term:`Bytes-like objects <bytes-like object>` and native59 strings containing only ascii characters will be automatically60 converted to byte strings.61 62 .. attribute:: headers63 64 Request headers, represented as a list of (name, value) pairs. See65 :ref:`the header normalization rules <headers-format>` for details.66 67 .. attribute:: http_version68 69 The HTTP protocol version, represented as a byte string like70 ``b"1.1"``. See :ref:`the HTTP version normalization rules71 <http_version-format>` for details.72 73 """74 75 __slots__ = ("method", "headers", "target", "http_version")76 77 method: bytes78 headers: Headers79 target: bytes80 http_version: bytes81 82 def __init__(83 self,84 *,85 method: Union[bytes, str],86 headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],87 target: Union[bytes, str],88 http_version: Union[bytes, str] = b"1.1",89 _parsed: bool = False,90 ) -> None:91 super().__init__()92 if isinstance(headers, Headers):93 object.__setattr__(self, "headers", headers)94 else:95 object.__setattr__(96 self, "headers", normalize_and_validate(headers, _parsed=_parsed)97 )98 if not _parsed:99 object.__setattr__(self, "method", bytesify(method))100 object.__setattr__(self, "target", bytesify(target))101 object.__setattr__(self, "http_version", bytesify(http_version))102 else:103 object.__setattr__(self, "method", method)104 object.__setattr__(self, "target", target)105 object.__setattr__(self, "http_version", http_version)106 107 # "A server MUST respond with a 400 (Bad Request) status code to any108 # HTTP/1.1 request message that lacks a Host header field and to any109 # request message that contains more than one Host header field or a110 # Host header field with an invalid field-value."111 # -- https://tools.ietf.org/html/rfc7230#section-5.4112 host_count = 0113 for name, value in self.headers:114 if name == b"host":115 host_count += 1116 if self.http_version == b"1.1" and host_count == 0:117 raise LocalProtocolError("Missing mandatory Host: header")118 if host_count > 1:119 raise LocalProtocolError("Found multiple Host: headers")120 121 validate(method_re, self.method, "Illegal method characters")122 validate(request_target_re, self.target, "Illegal target characters")123 124 # This is an unhashable type.125 __hash__ = None # type: ignore126 127 128@dataclass(init=False, frozen=True)129class _ResponseBase(Event):130 __slots__ = ("headers", "http_version", "reason", "status_code")131 132 headers: Headers133 http_version: bytes134 reason: bytes135 status_code: int136 137 def __init__(138 self,139 *,140 headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]],141 status_code: int,142 http_version: Union[bytes, str] = b"1.1",143 reason: Union[bytes, str] = b"",144 _parsed: bool = False,145 ) -> None:146 super().__init__()147 if isinstance(headers, Headers):148 object.__setattr__(self, "headers", headers)149 else:150 object.__setattr__(151 self, "headers", normalize_and_validate(headers, _parsed=_parsed)152 )153 if not _parsed:154 object.__setattr__(self, "reason", bytesify(reason))155 object.__setattr__(self, "http_version", bytesify(http_version))156 if not isinstance(status_code, int):157 raise LocalProtocolError("status code must be integer")158 # Because IntEnum objects are instances of int, but aren't159 # duck-compatible (sigh), see gh-72.160 object.__setattr__(self, "status_code", int(status_code))161 else:162 object.__setattr__(self, "reason", reason)163 object.__setattr__(self, "http_version", http_version)164 object.__setattr__(self, "status_code", status_code)165 166 self.__post_init__()167 168 def __post_init__(self) -> None:169 pass170 171 # This is an unhashable type.172 __hash__ = None # type: ignore173 174 175@dataclass(init=False, frozen=True)176class InformationalResponse(_ResponseBase):177 """An HTTP informational response.178 179 Fields:180 181 .. attribute:: status_code182 183 The status code of this response, as an integer. For an184 :class:`InformationalResponse`, this is always in the range [100,185 200).186 187 .. attribute:: headers188 189 Request headers, represented as a list of (name, value) pairs. See190 :ref:`the header normalization rules <headers-format>` for191 details.192 193 .. attribute:: http_version194 195 The HTTP protocol version, represented as a byte string like196 ``b"1.1"``. See :ref:`the HTTP version normalization rules197 <http_version-format>` for details.198 199 .. attribute:: reason200 201 The reason phrase of this response, as a byte string. For example:202 ``b"OK"``, or ``b"Not Found"``.203 204 """205 206 def __post_init__(self) -> None:207 if not (100 <= self.status_code < 200):208 raise LocalProtocolError(209 "InformationalResponse status_code should be in range "210 "[100, 200), not {}".format(self.status_code)211 )212 213 # This is an unhashable type.214 __hash__ = None # type: ignore215 216 217@dataclass(init=False, frozen=True)218class Response(_ResponseBase):219 """The beginning of an HTTP response.220 221 Fields:222 223 .. attribute:: status_code224 225 The status code of this response, as an integer. For an226 :class:`Response`, this is always in the range [200,227 1000).228 229 .. attribute:: headers230 231 Request headers, represented as a list of (name, value) pairs. See232 :ref:`the header normalization rules <headers-format>` for details.233 234 .. attribute:: http_version235 236 The HTTP protocol version, represented as a byte string like237 ``b"1.1"``. See :ref:`the HTTP version normalization rules238 <http_version-format>` for details.239 240 .. attribute:: reason241 242 The reason phrase of this response, as a byte string. For example:243 ``b"OK"``, or ``b"Not Found"``.244 245 """246 247 def __post_init__(self) -> None:248 if not (200 <= self.status_code < 1000):249 raise LocalProtocolError(250 "Response status_code should be in range [200, 1000), not {}".format(251 self.status_code252 )253 )254 255 # This is an unhashable type.256 __hash__ = None # type: ignore257 258 259@dataclass(init=False, frozen=True)260class Data(Event):261 """Part of an HTTP message body.262 263 Fields:264 265 .. attribute:: data266 267 A :term:`bytes-like object` containing part of a message body. Or, if268 using the ``combine=False`` argument to :meth:`Connection.send`, then269 any object that your socket writing code knows what to do with, and for270 which calling :func:`len` returns the number of bytes that will be271 written -- see :ref:`sendfile` for details.272 273 .. attribute:: chunk_start274 275 A marker that indicates whether this data object is from the start of a276 chunked transfer encoding chunk. This field is ignored when when a Data277 event is provided to :meth:`Connection.send`: it is only valid on278 events emitted from :meth:`Connection.next_event`. You probably279 shouldn't use this attribute at all; see280 :ref:`chunk-delimiters-are-bad` for details.281 282 .. attribute:: chunk_end283 284 A marker that indicates whether this data object is the last for a285 given chunked transfer encoding chunk. This field is ignored when when286 a Data event is provided to :meth:`Connection.send`: it is only valid287 on events emitted from :meth:`Connection.next_event`. You probably288 shouldn't use this attribute at all; see289 :ref:`chunk-delimiters-are-bad` for details.290 291 """292 293 __slots__ = ("data", "chunk_start", "chunk_end")294 295 data: bytes296 chunk_start: bool297 chunk_end: bool298 299 def __init__(300 self, data: bytes, chunk_start: bool = False, chunk_end: bool = False301 ) -> None:302 object.__setattr__(self, "data", data)303 object.__setattr__(self, "chunk_start", chunk_start)304 object.__setattr__(self, "chunk_end", chunk_end)305 306 # This is an unhashable type.307 __hash__ = None # type: ignore308 309 310# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that311# are forbidden to be sent in a trailer, since processing them as if they were312# present in the header section might bypass external security filters."313# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part314# Unfortunately, the list of forbidden fields is long and vague :-/315@dataclass(init=False, frozen=True)316class EndOfMessage(Event):317 """The end of an HTTP message.318 319 Fields:320 321 .. attribute:: headers322 323 Default value: ``[]``324 325 Any trailing headers attached to this message, represented as a list of326 (name, value) pairs. See :ref:`the header normalization rules327 <headers-format>` for details.328 329 Must be empty unless ``Transfer-Encoding: chunked`` is in use.330 331 """332 333 __slots__ = ("headers",)334 335 headers: Headers336 337 def __init__(338 self,339 *,340 headers: Union[341 Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None342 ] = None,343 _parsed: bool = False,344 ) -> None:345 super().__init__()346 if headers is None:347 headers = Headers([])348 elif not isinstance(headers, Headers):349 headers = normalize_and_validate(headers, _parsed=_parsed)350 351 object.__setattr__(self, "headers", headers)352 353 # This is an unhashable type.354 __hash__ = None # type: ignore355 356 357@dataclass(frozen=True)358class ConnectionClosed(Event):359 """This event indicates that the sender has closed their outgoing360 connection.361 362 Note that this does not necessarily mean that they can't *receive* further363 data, because TCP connections are composed to two one-way channels which364 can be closed independently. See :ref:`closing` for details.365 366 No fields.367 """368 369 pass370 