summaryrefslogtreecommitdiff
path: root/venv/lib/python3.11/site-packages/h11
diff options
context:
space:
mode:
authorcyfraeviolae <cyfraeviolae>2024-04-03 03:10:44 -0400
committercyfraeviolae <cyfraeviolae>2024-04-03 03:10:44 -0400
commit6d7ba58f880be618ade07f8ea080fe8c4bf8a896 (patch)
treeb1c931051ffcebd2bd9d61d98d6233ffa289bbce /venv/lib/python3.11/site-packages/h11
parent4f884c9abc32990b4061a1bb6997b4b37e58ea0b (diff)
venv
Diffstat (limited to 'venv/lib/python3.11/site-packages/h11')
-rw-r--r--venv/lib/python3.11/site-packages/h11/__init__.py62
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/__init__.cpython-311.pycbin0 -> 1320 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_abnf.cpython-311.pycbin0 -> 1825 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_connection.cpython-311.pycbin0 -> 24311 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_events.cpython-311.pycbin0 -> 15199 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_headers.cpython-311.pycbin0 -> 9116 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_readers.cpython-311.pycbin0 -> 10793 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_receivebuffer.cpython-311.pycbin0 -> 5152 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_state.cpython-311.pycbin0 -> 9788 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_util.cpython-311.pycbin0 -> 5373 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_version.cpython-311.pycbin0 -> 210 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/__pycache__/_writers.cpython-311.pycbin0 -> 7310 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/_abnf.py132
-rw-r--r--venv/lib/python3.11/site-packages/h11/_connection.py633
-rw-r--r--venv/lib/python3.11/site-packages/h11/_events.py369
-rw-r--r--venv/lib/python3.11/site-packages/h11/_headers.py278
-rw-r--r--venv/lib/python3.11/site-packages/h11/_readers.py247
-rw-r--r--venv/lib/python3.11/site-packages/h11/_receivebuffer.py153
-rw-r--r--venv/lib/python3.11/site-packages/h11/_state.py367
-rw-r--r--venv/lib/python3.11/site-packages/h11/_util.py135
-rw-r--r--venv/lib/python3.11/site-packages/h11/_version.py16
-rw-r--r--venv/lib/python3.11/site-packages/h11/_writers.py145
-rw-r--r--venv/lib/python3.11/site-packages/h11/py.typed1
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__init__.py0
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/__init__.cpython-311.pycbin0 -> 193 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/helpers.cpython-311.pycbin0 -> 5113 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-311.pycbin0 -> 8277 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_connection.cpython-311.pycbin0 -> 68029 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_events.cpython-311.pycbin0 -> 6829 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_headers.cpython-311.pycbin0 -> 9606 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_helpers.cpython-311.pycbin0 -> 1529 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_io.cpython-311.pycbin0 -> 25542 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_receivebuffer.cpython-311.pycbin0 -> 4608 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_state.cpython-311.pycbin0 -> 15210 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_util.cpython-311.pycbin0 -> 7816 bytes
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/data/test-file1
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/helpers.py101
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_against_stdlib_http.py115
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_connection.py1122
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_events.py150
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_headers.py157
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_helpers.py32
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_io.py572
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_receivebuffer.py135
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_state.py271
-rw-r--r--venv/lib/python3.11/site-packages/h11/tests/test_util.py112
46 files changed, 5306 insertions, 0 deletions
diff --git a/venv/lib/python3.11/site-packages/h11/__init__.py b/venv/lib/python3.11/site-packages/h11/__init__.py
new file mode 100644
index 0000000..989e92c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__init__.py
@@ -0,0 +1,62 @@
+# A highish-level implementation of the HTTP/1.1 wire protocol (RFC 7230),
+# containing no networking code at all, loosely modelled on hyper-h2's generic
+# implementation of HTTP/2 (and in particular the h2.connection.H2Connection
+# class). There's still a bunch of subtle details you need to get right if you
+# want to make this actually useful, because it doesn't implement all the
+# semantics to check that what you're asking to write to the wire is sensible,
+# but at least it gets you out of dealing with the wire itself.
+
+from h11._connection import Connection, NEED_DATA, PAUSED
+from h11._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from h11._state import (
+ CLIENT,
+ CLOSED,
+ DONE,
+ ERROR,
+ IDLE,
+ MIGHT_SWITCH_PROTOCOL,
+ MUST_CLOSE,
+ SEND_BODY,
+ SEND_RESPONSE,
+ SERVER,
+ SWITCHED_PROTOCOL,
+)
+from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError
+from h11._version import __version__
+
+PRODUCT_ID = "python-h11/" + __version__
+
+
+__all__ = (
+ "Connection",
+ "NEED_DATA",
+ "PAUSED",
+ "ConnectionClosed",
+ "Data",
+ "EndOfMessage",
+ "Event",
+ "InformationalResponse",
+ "Request",
+ "Response",
+ "CLIENT",
+ "CLOSED",
+ "DONE",
+ "ERROR",
+ "IDLE",
+ "MUST_CLOSE",
+ "SEND_BODY",
+ "SEND_RESPONSE",
+ "SERVER",
+ "SWITCHED_PROTOCOL",
+ "ProtocolError",
+ "LocalProtocolError",
+ "RemoteProtocolError",
+)
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..76942e2
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/__init__.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_abnf.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_abnf.cpython-311.pyc
new file mode 100644
index 0000000..53e94a7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_abnf.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_connection.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_connection.cpython-311.pyc
new file mode 100644
index 0000000..7cca1ce
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_connection.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_events.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_events.cpython-311.pyc
new file mode 100644
index 0000000..9ae2694
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_events.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_headers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_headers.cpython-311.pyc
new file mode 100644
index 0000000..67a3984
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_headers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_readers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_readers.cpython-311.pyc
new file mode 100644
index 0000000..387c362
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_readers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_receivebuffer.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_receivebuffer.cpython-311.pyc
new file mode 100644
index 0000000..83b85cb
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_receivebuffer.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_state.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_state.cpython-311.pyc
new file mode 100644
index 0000000..eaa2960
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_state.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_util.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_util.cpython-311.pyc
new file mode 100644
index 0000000..73e2a9c
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_util.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_version.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_version.cpython-311.pyc
new file mode 100644
index 0000000..1ae2072
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_version.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/__pycache__/_writers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/__pycache__/_writers.cpython-311.pyc
new file mode 100644
index 0000000..ea23e1a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/__pycache__/_writers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/_abnf.py b/venv/lib/python3.11/site-packages/h11/_abnf.py
new file mode 100644
index 0000000..933587f
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_abnf.py
@@ -0,0 +1,132 @@
+# We use native strings for all the re patterns, to take advantage of string
+# formatting, and then convert to bytestrings when compiling the final re
+# objects.
+
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#whitespace
+# OWS = *( SP / HTAB )
+# ; optional whitespace
+OWS = r"[ \t]*"
+
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.token.separators
+# token = 1*tchar
+#
+# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*"
+# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~"
+# / DIGIT / ALPHA
+# ; any VCHAR, except delimiters
+token = r"[-!#$%&'*+.^_`|~0-9a-zA-Z]+"
+
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#header.fields
+# field-name = token
+field_name = token
+
+# The standard says:
+#
+# field-value = *( field-content / obs-fold )
+# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ]
+# field-vchar = VCHAR / obs-text
+# obs-fold = CRLF 1*( SP / HTAB )
+# ; obsolete line folding
+# ; see Section 3.2.4
+#
+# https://tools.ietf.org/html/rfc5234#appendix-B.1
+#
+# VCHAR = %x21-7E
+# ; visible (printing) characters
+#
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.quoted-string
+# obs-text = %x80-FF
+#
+# However, the standard definition of field-content is WRONG! It disallows
+# fields containing a single visible character surrounded by whitespace,
+# e.g. "foo a bar".
+#
+# See: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189
+#
+# So our definition of field_content attempts to fix it up...
+#
+# Also, we allow lots of control characters, because apparently people assume
+# that they're legal in practice (e.g., google analytics makes cookies with
+# \x01 in them!):
+# https://github.com/python-hyper/h11/issues/57
+# We still don't allow NUL or whitespace, because those are often treated as
+# meta-characters and letting them through can lead to nasty issues like SSRF.
+vchar = r"[\x21-\x7e]"
+vchar_or_obs_text = r"[^\x00\s]"
+field_vchar = vchar_or_obs_text
+field_content = r"{field_vchar}+(?:[ \t]+{field_vchar}+)*".format(**globals())
+
+# We handle obs-fold at a different level, and our fixed-up field_content
+# already grows to swallow the whole value, so ? instead of *
+field_value = r"({field_content})?".format(**globals())
+
+# header-field = field-name ":" OWS field-value OWS
+header_field = (
+ r"(?P<field_name>{field_name})"
+ r":"
+ r"{OWS}"
+ r"(?P<field_value>{field_value})"
+ r"{OWS}".format(**globals())
+)
+
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#request.line
+#
+# request-line = method SP request-target SP HTTP-version CRLF
+# method = token
+# HTTP-version = HTTP-name "/" DIGIT "." DIGIT
+# HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive
+#
+# request-target is complicated (see RFC 7230 sec 5.3) -- could be path, full
+# URL, host+port (for connect), or even "*", but in any case we are guaranteed
+# that it contists of the visible printing characters.
+method = token
+request_target = r"{vchar}+".format(**globals())
+http_version = r"HTTP/(?P<http_version>[0-9]\.[0-9])"
+request_line = (
+ r"(?P<method>{method})"
+ r" "
+ r"(?P<target>{request_target})"
+ r" "
+ r"{http_version}".format(**globals())
+)
+
+# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#status.line
+#
+# status-line = HTTP-version SP status-code SP reason-phrase CRLF
+# status-code = 3DIGIT
+# reason-phrase = *( HTAB / SP / VCHAR / obs-text )
+status_code = r"[0-9]{3}"
+reason_phrase = r"([ \t]|{vchar_or_obs_text})*".format(**globals())
+status_line = (
+ r"{http_version}"
+ r" "
+ r"(?P<status_code>{status_code})"
+ # However, there are apparently a few too many servers out there that just
+ # leave out the reason phrase:
+ # https://github.com/scrapy/scrapy/issues/345#issuecomment-281756036
+ # https://github.com/seanmonstar/httparse/issues/29
+ # so make it optional. ?: is a non-capturing group.
+ r"(?: (?P<reason>{reason_phrase}))?".format(**globals())
+)
+
+HEXDIG = r"[0-9A-Fa-f]"
+# Actually
+#
+# chunk-size = 1*HEXDIG
+#
+# but we impose an upper-limit to avoid ridiculosity. len(str(2**64)) == 20
+chunk_size = r"({HEXDIG}){{1,20}}".format(**globals())
+# Actually
+#
+# chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] )
+#
+# but we aren't parsing the things so we don't really care.
+chunk_ext = r";.*"
+chunk_header = (
+ r"(?P<chunk_size>{chunk_size})"
+ r"(?P<chunk_ext>{chunk_ext})?"
+ r"{OWS}\r\n".format(
+ **globals()
+ ) # Even though the specification does not allow for extra whitespaces,
+ # we are lenient with trailing whitespaces because some servers on the wild use it.
+)
diff --git a/venv/lib/python3.11/site-packages/h11/_connection.py b/venv/lib/python3.11/site-packages/h11/_connection.py
new file mode 100644
index 0000000..d175270
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_connection.py
@@ -0,0 +1,633 @@
+# This contains the main Connection class. Everything in h11 revolves around
+# this.
+from typing import Any, Callable, cast, Dict, List, Optional, Tuple, Type, Union
+
+from ._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from ._headers import get_comma_header, has_expect_100_continue, set_comma_header
+from ._readers import READERS, ReadersType
+from ._receivebuffer import ReceiveBuffer
+from ._state import (
+ _SWITCH_CONNECT,
+ _SWITCH_UPGRADE,
+ CLIENT,
+ ConnectionState,
+ DONE,
+ ERROR,
+ MIGHT_SWITCH_PROTOCOL,
+ SEND_BODY,
+ SERVER,
+ SWITCHED_PROTOCOL,
+)
+from ._util import ( # Import the internal things we need
+ LocalProtocolError,
+ RemoteProtocolError,
+ Sentinel,
+)
+from ._writers import WRITERS, WritersType
+
+# Everything in __all__ gets re-exported as part of the h11 public API.
+__all__ = ["Connection", "NEED_DATA", "PAUSED"]
+
+
+class NEED_DATA(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class PAUSED(Sentinel, metaclass=Sentinel):
+ pass
+
+
+# If we ever have this much buffered without it making a complete parseable
+# event, we error out. The only time we really buffer is when reading the
+# request/response line + headers together, so this is effectively the limit on
+# the size of that.
+#
+# Some precedents for defaults:
+# - node.js: 80 * 1024
+# - tomcat: 8 * 1024
+# - IIS: 16 * 1024
+# - Apache: <8 KiB per line>
+DEFAULT_MAX_INCOMPLETE_EVENT_SIZE = 16 * 1024
+
+# RFC 7230's rules for connection lifecycles:
+# - If either side says they want to close the connection, then the connection
+# must close.
+# - HTTP/1.1 defaults to keep-alive unless someone says Connection: close
+# - HTTP/1.0 defaults to close unless both sides say Connection: keep-alive
+# (and even this is a mess -- e.g. if you're implementing a proxy then
+# sending Connection: keep-alive is forbidden).
+#
+# We simplify life by simply not supporting keep-alive with HTTP/1.0 peers. So
+# our rule is:
+# - If someone says Connection: close, we will close
+# - If someone uses HTTP/1.0, we will close.
+def _keep_alive(event: Union[Request, Response]) -> bool:
+ connection = get_comma_header(event.headers, b"connection")
+ if b"close" in connection:
+ return False
+ if getattr(event, "http_version", b"1.1") < b"1.1":
+ return False
+ return True
+
+
+def _body_framing(
+ request_method: bytes, event: Union[Request, Response]
+) -> Tuple[str, Union[Tuple[()], Tuple[int]]]:
+ # Called when we enter SEND_BODY to figure out framing information for
+ # this body.
+ #
+ # These are the only two events that can trigger a SEND_BODY state:
+ assert type(event) in (Request, Response)
+ # Returns one of:
+ #
+ # ("content-length", count)
+ # ("chunked", ())
+ # ("http/1.0", ())
+ #
+ # which are (lookup key, *args) for constructing body reader/writer
+ # objects.
+ #
+ # Reference: https://tools.ietf.org/html/rfc7230#section-3.3.3
+ #
+ # Step 1: some responses always have an empty body, regardless of what the
+ # headers say.
+ if type(event) is Response:
+ if (
+ event.status_code in (204, 304)
+ or request_method == b"HEAD"
+ or (request_method == b"CONNECT" and 200 <= event.status_code < 300)
+ ):
+ return ("content-length", (0,))
+ # Section 3.3.3 also lists another case -- responses with status_code
+ # < 200. For us these are InformationalResponses, not Responses, so
+ # they can't get into this function in the first place.
+ assert event.status_code >= 200
+
+ # Step 2: check for Transfer-Encoding (T-E beats C-L):
+ transfer_encodings = get_comma_header(event.headers, b"transfer-encoding")
+ if transfer_encodings:
+ assert transfer_encodings == [b"chunked"]
+ return ("chunked", ())
+
+ # Step 3: check for Content-Length
+ content_lengths = get_comma_header(event.headers, b"content-length")
+ if content_lengths:
+ return ("content-length", (int(content_lengths[0]),))
+
+ # Step 4: no applicable headers; fallback/default depends on type
+ if type(event) is Request:
+ return ("content-length", (0,))
+ else:
+ return ("http/1.0", ())
+
+
+################################################################
+#
+# The main Connection class
+#
+################################################################
+
+
+class Connection:
+ """An object encapsulating the state of an HTTP connection.
+
+ Args:
+ our_role: If you're implementing a client, pass :data:`h11.CLIENT`. If
+ you're implementing a server, pass :data:`h11.SERVER`.
+
+ max_incomplete_event_size (int):
+ The maximum number of bytes we're willing to buffer of an
+ incomplete event. In practice this mostly sets a limit on the
+ maximum size of the request/response line + headers. If this is
+ exceeded, then :meth:`next_event` will raise
+ :exc:`RemoteProtocolError`.
+
+ """
+
+ def __init__(
+ self,
+ our_role: Type[Sentinel],
+ max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE,
+ ) -> None:
+ self._max_incomplete_event_size = max_incomplete_event_size
+ # State and role tracking
+ if our_role not in (CLIENT, SERVER):
+ raise ValueError("expected CLIENT or SERVER, not {!r}".format(our_role))
+ self.our_role = our_role
+ self.their_role: Type[Sentinel]
+ if our_role is CLIENT:
+ self.their_role = SERVER
+ else:
+ self.their_role = CLIENT
+ self._cstate = ConnectionState()
+
+ # Callables for converting data->events or vice-versa given the
+ # current state
+ self._writer = self._get_io_object(self.our_role, None, WRITERS)
+ self._reader = self._get_io_object(self.their_role, None, READERS)
+
+ # Holds any unprocessed received data
+ self._receive_buffer = ReceiveBuffer()
+ # If this is true, then it indicates that the incoming connection was
+ # closed *after* the end of whatever's in self._receive_buffer:
+ self._receive_buffer_closed = False
+
+ # Extra bits of state that don't fit into the state machine.
+ #
+ # These two are only used to interpret framing headers for figuring
+ # out how to read/write response bodies. their_http_version is also
+ # made available as a convenient public API.
+ self.their_http_version: Optional[bytes] = None
+ self._request_method: Optional[bytes] = None
+ # This is pure flow-control and doesn't at all affect the set of legal
+ # transitions, so no need to bother ConnectionState with it:
+ self.client_is_waiting_for_100_continue = False
+
+ @property
+ def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]:
+ """A dictionary like::
+
+ {CLIENT: <client state>, SERVER: <server state>}
+
+ See :ref:`state-machine` for details.
+
+ """
+ return dict(self._cstate.states)
+
+ @property
+ def our_state(self) -> Type[Sentinel]:
+ """The current state of whichever role we are playing. See
+ :ref:`state-machine` for details.
+ """
+ return self._cstate.states[self.our_role]
+
+ @property
+ def their_state(self) -> Type[Sentinel]:
+ """The current state of whichever role we are NOT playing. See
+ :ref:`state-machine` for details.
+ """
+ return self._cstate.states[self.their_role]
+
+ @property
+ def they_are_waiting_for_100_continue(self) -> bool:
+ return self.their_role is CLIENT and self.client_is_waiting_for_100_continue
+
+ def start_next_cycle(self) -> None:
+ """Attempt to reset our connection state for a new request/response
+ cycle.
+
+ If both client and server are in :data:`DONE` state, then resets them
+ both to :data:`IDLE` state in preparation for a new request/response
+ cycle on this same connection. Otherwise, raises a
+ :exc:`LocalProtocolError`.
+
+ See :ref:`keepalive-and-pipelining`.
+
+ """
+ old_states = dict(self._cstate.states)
+ self._cstate.start_next_cycle()
+ self._request_method = None
+ # self.their_http_version gets left alone, since it presumably lasts
+ # beyond a single request/response cycle
+ assert not self.client_is_waiting_for_100_continue
+ self._respond_to_state_changes(old_states)
+
+ def _process_error(self, role: Type[Sentinel]) -> None:
+ old_states = dict(self._cstate.states)
+ self._cstate.process_error(role)
+ self._respond_to_state_changes(old_states)
+
+ def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]:
+ if type(event) is InformationalResponse and event.status_code == 101:
+ return _SWITCH_UPGRADE
+ if type(event) is Response:
+ if (
+ _SWITCH_CONNECT in self._cstate.pending_switch_proposals
+ and 200 <= event.status_code < 300
+ ):
+ return _SWITCH_CONNECT
+ return None
+
+ # All events go through here
+ def _process_event(self, role: Type[Sentinel], event: Event) -> None:
+ # First, pass the event through the state machine to make sure it
+ # succeeds.
+ old_states = dict(self._cstate.states)
+ if role is CLIENT and type(event) is Request:
+ if event.method == b"CONNECT":
+ self._cstate.process_client_switch_proposal(_SWITCH_CONNECT)
+ if get_comma_header(event.headers, b"upgrade"):
+ self._cstate.process_client_switch_proposal(_SWITCH_UPGRADE)
+ server_switch_event = None
+ if role is SERVER:
+ server_switch_event = self._server_switch_event(event)
+ self._cstate.process_event(role, type(event), server_switch_event)
+
+ # Then perform the updates triggered by it.
+
+ if type(event) is Request:
+ self._request_method = event.method
+
+ if role is self.their_role and type(event) in (
+ Request,
+ Response,
+ InformationalResponse,
+ ):
+ event = cast(Union[Request, Response, InformationalResponse], event)
+ self.their_http_version = event.http_version
+
+ # Keep alive handling
+ #
+ # RFC 7230 doesn't really say what one should do if Connection: close
+ # shows up on a 1xx InformationalResponse. I think the idea is that
+ # this is not supposed to happen. In any case, if it does happen, we
+ # ignore it.
+ if type(event) in (Request, Response) and not _keep_alive(
+ cast(Union[Request, Response], event)
+ ):
+ self._cstate.process_keep_alive_disabled()
+
+ # 100-continue
+ if type(event) is Request and has_expect_100_continue(event):
+ self.client_is_waiting_for_100_continue = True
+ if type(event) in (InformationalResponse, Response):
+ self.client_is_waiting_for_100_continue = False
+ if role is CLIENT and type(event) in (Data, EndOfMessage):
+ self.client_is_waiting_for_100_continue = False
+
+ self._respond_to_state_changes(old_states, event)
+
+ def _get_io_object(
+ self,
+ role: Type[Sentinel],
+ event: Optional[Event],
+ io_dict: Union[ReadersType, WritersType],
+ ) -> Optional[Callable[..., Any]]:
+ # event may be None; it's only used when entering SEND_BODY
+ state = self._cstate.states[role]
+ if state is SEND_BODY:
+ # Special case: the io_dict has a dict of reader/writer factories
+ # that depend on the request/response framing.
+ framing_type, args = _body_framing(
+ cast(bytes, self._request_method), cast(Union[Request, Response], event)
+ )
+ return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index]
+ else:
+ # General case: the io_dict just has the appropriate reader/writer
+ # for this state
+ return io_dict.get((role, state)) # type: ignore[return-value]
+
+ # This must be called after any action that might have caused
+ # self._cstate.states to change.
+ def _respond_to_state_changes(
+ self,
+ old_states: Dict[Type[Sentinel], Type[Sentinel]],
+ event: Optional[Event] = None,
+ ) -> None:
+ # Update reader/writer
+ if self.our_state != old_states[self.our_role]:
+ self._writer = self._get_io_object(self.our_role, event, WRITERS)
+ if self.their_state != old_states[self.their_role]:
+ self._reader = self._get_io_object(self.their_role, event, READERS)
+
+ @property
+ def trailing_data(self) -> Tuple[bytes, bool]:
+ """Data that has been received, but not yet processed, represented as
+ a tuple with two elements, where the first is a byte-string containing
+ the unprocessed data itself, and the second is a bool that is True if
+ the receive connection was closed.
+
+ See :ref:`switching-protocols` for discussion of why you'd want this.
+ """
+ return (bytes(self._receive_buffer), self._receive_buffer_closed)
+
+ def receive_data(self, data: bytes) -> None:
+ """Add data to our internal receive buffer.
+
+ This does not actually do any processing on the data, just stores
+ it. To trigger processing, you have to call :meth:`next_event`.
+
+ Args:
+ data (:term:`bytes-like object`):
+ The new data that was just received.
+
+ Special case: If *data* is an empty byte-string like ``b""``,
+ then this indicates that the remote side has closed the
+ connection (end of file). Normally this is convenient, because
+ standard Python APIs like :meth:`file.read` or
+ :meth:`socket.recv` use ``b""`` to indicate end-of-file, while
+ other failures to read are indicated using other mechanisms
+ like raising :exc:`TimeoutError`. When using such an API you
+ can just blindly pass through whatever you get from ``read``
+ to :meth:`receive_data`, and everything will work.
+
+ But, if you have an API where reading an empty string is a
+ valid non-EOF condition, then you need to be aware of this and
+ make sure to check for such strings and avoid passing them to
+ :meth:`receive_data`.
+
+ Returns:
+ Nothing, but after calling this you should call :meth:`next_event`
+ to parse the newly received data.
+
+ Raises:
+ RuntimeError:
+ Raised if you pass an empty *data*, indicating EOF, and then
+ pass a non-empty *data*, indicating more data that somehow
+ arrived after the EOF.
+
+ (Calling ``receive_data(b"")`` multiple times is fine,
+ and equivalent to calling it once.)
+
+ """
+ if data:
+ if self._receive_buffer_closed:
+ raise RuntimeError("received close, then received more data?")
+ self._receive_buffer += data
+ else:
+ self._receive_buffer_closed = True
+
+ def _extract_next_receive_event(
+ self,
+ ) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
+ state = self.their_state
+ # We don't pause immediately when they enter DONE, because even in
+ # DONE state we can still process a ConnectionClosed() event. But
+ # if we have data in our buffer, then we definitely aren't getting
+ # a ConnectionClosed() immediately and we need to pause.
+ if state is DONE and self._receive_buffer:
+ return PAUSED
+ if state is MIGHT_SWITCH_PROTOCOL or state is SWITCHED_PROTOCOL:
+ return PAUSED
+ assert self._reader is not None
+ event = self._reader(self._receive_buffer)
+ if event is None:
+ if not self._receive_buffer and self._receive_buffer_closed:
+ # In some unusual cases (basically just HTTP/1.0 bodies), EOF
+ # triggers an actual protocol event; in that case, we want to
+ # return that event, and then the state will change and we'll
+ # get called again to generate the actual ConnectionClosed().
+ if hasattr(self._reader, "read_eof"):
+ event = self._reader.read_eof() # type: ignore[attr-defined]
+ else:
+ event = ConnectionClosed()
+ if event is None:
+ event = NEED_DATA
+ return event # type: ignore[no-any-return]
+
+ def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]:
+ """Parse the next event out of our receive buffer, update our internal
+ state, and return it.
+
+ This is a mutating operation -- think of it like calling :func:`next`
+ on an iterator.
+
+ Returns:
+ : One of three things:
+
+ 1) An event object -- see :ref:`events`.
+
+ 2) The special constant :data:`NEED_DATA`, which indicates that
+ you need to read more data from your socket and pass it to
+ :meth:`receive_data` before this method will be able to return
+ any more events.
+
+ 3) The special constant :data:`PAUSED`, which indicates that we
+ are not in a state where we can process incoming data (usually
+ because the peer has finished their part of the current
+ request/response cycle, and you have not yet called
+ :meth:`start_next_cycle`). See :ref:`flow-control` for details.
+
+ Raises:
+ RemoteProtocolError:
+ The peer has misbehaved. You should close the connection
+ (possibly after sending some kind of 4xx response).
+
+ Once this method returns :class:`ConnectionClosed` once, then all
+ subsequent calls will also return :class:`ConnectionClosed`.
+
+ If this method raises any exception besides :exc:`RemoteProtocolError`
+ then that's a bug -- if it happens please file a bug report!
+
+ If this method raises any exception then it also sets
+ :attr:`Connection.their_state` to :data:`ERROR` -- see
+ :ref:`error-handling` for discussion.
+
+ """
+
+ if self.their_state is ERROR:
+ raise RemoteProtocolError("Can't receive data when peer state is ERROR")
+ try:
+ event = self._extract_next_receive_event()
+ if event not in [NEED_DATA, PAUSED]:
+ self._process_event(self.their_role, cast(Event, event))
+ if event is NEED_DATA:
+ if len(self._receive_buffer) > self._max_incomplete_event_size:
+ # 431 is "Request header fields too large" which is pretty
+ # much the only situation where we can get here
+ raise RemoteProtocolError(
+ "Receive buffer too long", error_status_hint=431
+ )
+ if self._receive_buffer_closed:
+ # We're still trying to complete some event, but that's
+ # never going to happen because no more data is coming
+ raise RemoteProtocolError("peer unexpectedly closed connection")
+ return event
+ except BaseException as exc:
+ self._process_error(self.their_role)
+ if isinstance(exc, LocalProtocolError):
+ exc._reraise_as_remote_protocol_error()
+ else:
+ raise
+
+ def send(self, event: Event) -> Optional[bytes]:
+ """Convert a high-level event into bytes that can be sent to the peer,
+ while updating our internal state machine.
+
+ Args:
+ event: The :ref:`event <events>` to send.
+
+ Returns:
+ If ``type(event) is ConnectionClosed``, then returns
+ ``None``. Otherwise, returns a :term:`bytes-like object`.
+
+ Raises:
+ LocalProtocolError:
+ Sending this event at this time would violate our
+ understanding of the HTTP/1.1 protocol.
+
+ If this method raises any exception then it also sets
+ :attr:`Connection.our_state` to :data:`ERROR` -- see
+ :ref:`error-handling` for discussion.
+
+ """
+ data_list = self.send_with_data_passthrough(event)
+ if data_list is None:
+ return None
+ else:
+ return b"".join(data_list)
+
+ def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]:
+ """Identical to :meth:`send`, except that in situations where
+ :meth:`send` returns a single :term:`bytes-like object`, this instead
+ returns a list of them -- and when sending a :class:`Data` event, this
+ list is guaranteed to contain the exact object you passed in as
+ :attr:`Data.data`. See :ref:`sendfile` for discussion.
+
+ """
+ if self.our_state is ERROR:
+ raise LocalProtocolError("Can't send data when our state is ERROR")
+ try:
+ if type(event) is Response:
+ event = self._clean_up_response_headers_for_sending(event)
+ # We want to call _process_event before calling the writer,
+ # because if someone tries to do something invalid then this will
+ # give a sensible error message, while our writers all just assume
+ # they will only receive valid events. But, _process_event might
+ # change self._writer. So we have to do a little dance:
+ writer = self._writer
+ self._process_event(self.our_role, event)
+ if type(event) is ConnectionClosed:
+ return None
+ else:
+ # In any situation where writer is None, process_event should
+ # have raised ProtocolError
+ assert writer is not None
+ data_list: List[bytes] = []
+ writer(event, data_list.append)
+ return data_list
+ except:
+ self._process_error(self.our_role)
+ raise
+
+ def send_failed(self) -> None:
+ """Notify the state machine that we failed to send the data it gave
+ us.
+
+ This causes :attr:`Connection.our_state` to immediately become
+ :data:`ERROR` -- see :ref:`error-handling` for discussion.
+
+ """
+ self._process_error(self.our_role)
+
+ # When sending a Response, we take responsibility for a few things:
+ #
+ # - Sometimes you MUST set Connection: close. We take care of those
+ # times. (You can also set it yourself if you want, and if you do then
+ # we'll respect that and close the connection at the right time. But you
+ # don't have to worry about that unless you want to.)
+ #
+ # - The user has to set Content-Length if they want it. Otherwise, for
+ # responses that have bodies (e.g. not HEAD), then we will automatically
+ # select the right mechanism for streaming a body of unknown length,
+ # which depends on depending on the peer's HTTP version.
+ #
+ # This function's *only* responsibility is making sure headers are set up
+ # right -- everything downstream just looks at the headers. There are no
+ # side channels.
+ def _clean_up_response_headers_for_sending(self, response: Response) -> Response:
+ assert type(response) is Response
+
+ headers = response.headers
+ need_close = False
+
+ # HEAD requests need some special handling: they always act like they
+ # have Content-Length: 0, and that's how _body_framing treats
+ # them. But their headers are supposed to match what we would send if
+ # the request was a GET. (Technically there is one deviation allowed:
+ # we're allowed to leave out the framing headers -- see
+ # https://tools.ietf.org/html/rfc7231#section-4.3.2 . But it's just as
+ # easy to get them right.)
+ method_for_choosing_headers = cast(bytes, self._request_method)
+ if method_for_choosing_headers == b"HEAD":
+ method_for_choosing_headers = b"GET"
+ framing_type, _ = _body_framing(method_for_choosing_headers, response)
+ if framing_type in ("chunked", "http/1.0"):
+ # This response has a body of unknown length.
+ # If our peer is HTTP/1.1, we use Transfer-Encoding: chunked
+ # If our peer is HTTP/1.0, we use no framing headers, and close the
+ # connection afterwards.
+ #
+ # Make sure to clear Content-Length (in principle user could have
+ # set both and then we ignored Content-Length b/c
+ # Transfer-Encoding overwrote it -- this would be naughty of them,
+ # but the HTTP spec says that if our peer does this then we have
+ # to fix it instead of erroring out, so we'll accord the user the
+ # same respect).
+ headers = set_comma_header(headers, b"content-length", [])
+ if self.their_http_version is None or self.their_http_version < b"1.1":
+ # Either we never got a valid request and are sending back an
+ # error (their_http_version is None), so we assume the worst;
+ # or else we did get a valid HTTP/1.0 request, so we know that
+ # they don't understand chunked encoding.
+ headers = set_comma_header(headers, b"transfer-encoding", [])
+ # This is actually redundant ATM, since currently we
+ # unconditionally disable keep-alive when talking to HTTP/1.0
+ # peers. But let's be defensive just in case we add
+ # Connection: keep-alive support later:
+ if self._request_method != b"HEAD":
+ need_close = True
+ else:
+ headers = set_comma_header(headers, b"transfer-encoding", [b"chunked"])
+
+ if not self._cstate.keep_alive or need_close:
+ # Make sure Connection: close is set
+ connection = set(get_comma_header(headers, b"connection"))
+ connection.discard(b"keep-alive")
+ connection.add(b"close")
+ headers = set_comma_header(headers, b"connection", sorted(connection))
+
+ return Response(
+ headers=headers,
+ status_code=response.status_code,
+ http_version=response.http_version,
+ reason=response.reason,
+ )
diff --git a/venv/lib/python3.11/site-packages/h11/_events.py b/venv/lib/python3.11/site-packages/h11/_events.py
new file mode 100644
index 0000000..075bf8a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_events.py
@@ -0,0 +1,369 @@
+# 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
diff --git a/venv/lib/python3.11/site-packages/h11/_headers.py b/venv/lib/python3.11/site-packages/h11/_headers.py
new file mode 100644
index 0000000..b97d020
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_headers.py
@@ -0,0 +1,278 @@
+import re
+from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union
+
+from ._abnf import field_name, field_value
+from ._util import bytesify, LocalProtocolError, validate
+
+if TYPE_CHECKING:
+ from ._events import Request
+
+try:
+ from typing import Literal
+except ImportError:
+ from typing_extensions import Literal # type: ignore
+
+
+# Facts
+# -----
+#
+# Headers are:
+# keys: case-insensitive ascii
+# values: mixture of ascii and raw bytes
+#
+# "Historically, HTTP has allowed field content with text in the ISO-8859-1
+# charset [ISO-8859-1], supporting other charsets only through use of
+# [RFC2047] encoding. In practice, most HTTP header field values use only a
+# subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD
+# limit their field values to US-ASCII octets. A recipient SHOULD treat other
+# octets in field content (obs-text) as opaque data."
+# And it deprecates all non-ascii values
+#
+# Leading/trailing whitespace in header names is forbidden
+#
+# Values get leading/trailing whitespace stripped
+#
+# Content-Disposition actually needs to contain unicode semantically; to
+# accomplish this it has a terrifically weird way of encoding the filename
+# itself as ascii (and even this still has lots of cross-browser
+# incompatibilities)
+#
+# Order is important:
+# "a proxy MUST NOT change the order of these field values when forwarding a
+# message"
+# (and there are several headers where the order indicates a preference)
+#
+# Multiple occurences of the same header:
+# "A sender MUST NOT generate multiple header fields with the same field name
+# in a message unless either the entire field value for that header field is
+# defined as a comma-separated list [or the header is Set-Cookie which gets a
+# special exception]" - RFC 7230. (cookies are in RFC 6265)
+#
+# So every header aside from Set-Cookie can be merged by b", ".join if it
+# occurs repeatedly. But, of course, they can't necessarily be split by
+# .split(b","), because quoting.
+#
+# Given all this mess (case insensitive, duplicates allowed, order is
+# important, ...), there doesn't appear to be any standard way to handle
+# headers in Python -- they're almost like dicts, but... actually just
+# aren't. For now we punt and just use a super simple representation: headers
+# are a list of pairs
+#
+# [(name1, value1), (name2, value2), ...]
+#
+# where all entries are bytestrings, names are lowercase and have no
+# leading/trailing whitespace, and values are bytestrings with no
+# leading/trailing whitespace. Searching and updating are done via naive O(n)
+# methods.
+#
+# Maybe a dict-of-lists would be better?
+
+_content_length_re = re.compile(rb"[0-9]+")
+_field_name_re = re.compile(field_name.encode("ascii"))
+_field_value_re = re.compile(field_value.encode("ascii"))
+
+
+class Headers(Sequence[Tuple[bytes, bytes]]):
+ """
+ A list-like interface that allows iterating over headers as byte-pairs
+ of (lowercased-name, value).
+
+ Internally we actually store the representation as three-tuples,
+ including both the raw original casing, in order to preserve casing
+ over-the-wire, and the lowercased name, for case-insensitive comparisions.
+
+ r = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.org"), ("Connection", "keep-alive")],
+ http_version="1.1",
+ )
+ assert r.headers == [
+ (b"host", b"example.org"),
+ (b"connection", b"keep-alive")
+ ]
+ assert r.headers.raw_items() == [
+ (b"Host", b"example.org"),
+ (b"Connection", b"keep-alive")
+ ]
+ """
+
+ __slots__ = "_full_items"
+
+ def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None:
+ self._full_items = full_items
+
+ def __bool__(self) -> bool:
+ return bool(self._full_items)
+
+ def __eq__(self, other: object) -> bool:
+ return list(self) == list(other) # type: ignore
+
+ def __len__(self) -> int:
+ return len(self._full_items)
+
+ def __repr__(self) -> str:
+ return "<Headers(%s)>" % repr(list(self))
+
+ def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override]
+ _, name, value = self._full_items[idx]
+ return (name, value)
+
+ def raw_items(self) -> List[Tuple[bytes, bytes]]:
+ return [(raw_name, value) for raw_name, _, value in self._full_items]
+
+
+HeaderTypes = Union[
+ List[Tuple[bytes, bytes]],
+ List[Tuple[bytes, str]],
+ List[Tuple[str, bytes]],
+ List[Tuple[str, str]],
+]
+
+
+@overload
+def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers:
+ ...
+
+
+@overload
+def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers:
+ ...
+
+
+@overload
+def normalize_and_validate(
+ headers: Union[Headers, HeaderTypes], _parsed: bool = False
+) -> Headers:
+ ...
+
+
+def normalize_and_validate(
+ headers: Union[Headers, HeaderTypes], _parsed: bool = False
+) -> Headers:
+ new_headers = []
+ seen_content_length = None
+ saw_transfer_encoding = False
+ for name, value in headers:
+ # For headers coming out of the parser, we can safely skip some steps,
+ # because it always returns bytes and has already run these regexes
+ # over the data:
+ if not _parsed:
+ name = bytesify(name)
+ value = bytesify(value)
+ validate(_field_name_re, name, "Illegal header name {!r}", name)
+ validate(_field_value_re, value, "Illegal header value {!r}", value)
+ assert isinstance(name, bytes)
+ assert isinstance(value, bytes)
+
+ raw_name = name
+ name = name.lower()
+ if name == b"content-length":
+ lengths = {length.strip() for length in value.split(b",")}
+ if len(lengths) != 1:
+ raise LocalProtocolError("conflicting Content-Length headers")
+ value = lengths.pop()
+ validate(_content_length_re, value, "bad Content-Length")
+ if seen_content_length is None:
+ seen_content_length = value
+ new_headers.append((raw_name, name, value))
+ elif seen_content_length != value:
+ raise LocalProtocolError("conflicting Content-Length headers")
+ elif name == b"transfer-encoding":
+ # "A server that receives a request message with a transfer coding
+ # it does not understand SHOULD respond with 501 (Not
+ # Implemented)."
+ # https://tools.ietf.org/html/rfc7230#section-3.3.1
+ if saw_transfer_encoding:
+ raise LocalProtocolError(
+ "multiple Transfer-Encoding headers", error_status_hint=501
+ )
+ # "All transfer-coding names are case-insensitive"
+ # -- https://tools.ietf.org/html/rfc7230#section-4
+ value = value.lower()
+ if value != b"chunked":
+ raise LocalProtocolError(
+ "Only Transfer-Encoding: chunked is supported",
+ error_status_hint=501,
+ )
+ saw_transfer_encoding = True
+ new_headers.append((raw_name, name, value))
+ else:
+ new_headers.append((raw_name, name, value))
+ return Headers(new_headers)
+
+
+def get_comma_header(headers: Headers, name: bytes) -> List[bytes]:
+ # Should only be used for headers whose value is a list of
+ # comma-separated, case-insensitive values.
+ #
+ # The header name `name` is expected to be lower-case bytes.
+ #
+ # Connection: meets these criteria (including cast insensitivity).
+ #
+ # Content-Length: technically is just a single value (1*DIGIT), but the
+ # standard makes reference to implementations that do multiple values, and
+ # using this doesn't hurt. Ditto, case insensitivity doesn't things either
+ # way.
+ #
+ # Transfer-Encoding: is more complex (allows for quoted strings), so
+ # splitting on , is actually wrong. For example, this is legal:
+ #
+ # Transfer-Encoding: foo; options="1,2", chunked
+ #
+ # and should be parsed as
+ #
+ # foo; options="1,2"
+ # chunked
+ #
+ # but this naive function will parse it as
+ #
+ # foo; options="1
+ # 2"
+ # chunked
+ #
+ # However, this is okay because the only thing we are going to do with
+ # any Transfer-Encoding is reject ones that aren't just "chunked", so
+ # both of these will be treated the same anyway.
+ #
+ # Expect: the only legal value is the literal string
+ # "100-continue". Splitting on commas is harmless. Case insensitive.
+ #
+ out: List[bytes] = []
+ for _, found_name, found_raw_value in headers._full_items:
+ if found_name == name:
+ found_raw_value = found_raw_value.lower()
+ for found_split_value in found_raw_value.split(b","):
+ found_split_value = found_split_value.strip()
+ if found_split_value:
+ out.append(found_split_value)
+ return out
+
+
+def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers:
+ # The header name `name` is expected to be lower-case bytes.
+ #
+ # Note that when we store the header we use title casing for the header
+ # names, in order to match the conventional HTTP header style.
+ #
+ # Simply calling `.title()` is a blunt approach, but it's correct
+ # here given the cases where we're using `set_comma_header`...
+ #
+ # Connection, Content-Length, Transfer-Encoding.
+ new_headers: List[Tuple[bytes, bytes]] = []
+ for found_raw_name, found_name, found_raw_value in headers._full_items:
+ if found_name != name:
+ new_headers.append((found_raw_name, found_raw_value))
+ for new_value in new_values:
+ new_headers.append((name.title(), new_value))
+ return normalize_and_validate(new_headers)
+
+
+def has_expect_100_continue(request: "Request") -> bool:
+ # https://tools.ietf.org/html/rfc7231#section-5.1.1
+ # "A server that receives a 100-continue expectation in an HTTP/1.0 request
+ # MUST ignore that expectation."
+ if request.http_version < b"1.1":
+ return False
+ expect = get_comma_header(request.headers, b"expect")
+ return b"100-continue" in expect
diff --git a/venv/lib/python3.11/site-packages/h11/_readers.py b/venv/lib/python3.11/site-packages/h11/_readers.py
new file mode 100644
index 0000000..08a9574
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_readers.py
@@ -0,0 +1,247 @@
+# Code to read HTTP data
+#
+# Strategy: each reader is a callable which takes a ReceiveBuffer object, and
+# either:
+# 1) consumes some of it and returns an Event
+# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate()
+# and it might raise a LocalProtocolError, so simpler just to always use
+# this)
+# 3) returns None, meaning "I need more data"
+#
+# If they have a .read_eof attribute, then this will be called if an EOF is
+# received -- but this is optional. Either way, the actual ConnectionClosed
+# event will be generated afterwards.
+#
+# READERS is a dict describing how to pick a reader. It maps states to either:
+# - a reader
+# - or, for body readers, a dict of per-framing reader factories
+
+import re
+from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union
+
+from ._abnf import chunk_header, header_field, request_line, status_line
+from ._events import Data, EndOfMessage, InformationalResponse, Request, Response
+from ._receivebuffer import ReceiveBuffer
+from ._state import (
+ CLIENT,
+ CLOSED,
+ DONE,
+ IDLE,
+ MUST_CLOSE,
+ SEND_BODY,
+ SEND_RESPONSE,
+ SERVER,
+)
+from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate
+
+__all__ = ["READERS"]
+
+header_field_re = re.compile(header_field.encode("ascii"))
+obs_fold_re = re.compile(rb"[ \t]+")
+
+
+def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]:
+ it = iter(lines)
+ last: Optional[bytes] = None
+ for line in it:
+ match = obs_fold_re.match(line)
+ if match:
+ if last is None:
+ raise LocalProtocolError("continuation line at start of headers")
+ if not isinstance(last, bytearray):
+ # Cast to a mutable type, avoiding copy on append to ensure O(n) time
+ last = bytearray(last)
+ last += b" "
+ last += line[match.end() :]
+ else:
+ if last is not None:
+ yield last
+ last = line
+ if last is not None:
+ yield last
+
+
+def _decode_header_lines(
+ lines: Iterable[bytes],
+) -> Iterable[Tuple[bytes, bytes]]:
+ for line in _obsolete_line_fold(lines):
+ matches = validate(header_field_re, line, "illegal header line: {!r}", line)
+ yield (matches["field_name"], matches["field_value"])
+
+
+request_line_re = re.compile(request_line.encode("ascii"))
+
+
+def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]:
+ lines = buf.maybe_extract_lines()
+ if lines is None:
+ if buf.is_next_line_obviously_invalid_request_line():
+ raise LocalProtocolError("illegal request line")
+ return None
+ if not lines:
+ raise LocalProtocolError("no request line received")
+ matches = validate(
+ request_line_re, lines[0], "illegal request line: {!r}", lines[0]
+ )
+ return Request(
+ headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches
+ )
+
+
+status_line_re = re.compile(status_line.encode("ascii"))
+
+
+def maybe_read_from_SEND_RESPONSE_server(
+ buf: ReceiveBuffer,
+) -> Union[InformationalResponse, Response, None]:
+ lines = buf.maybe_extract_lines()
+ if lines is None:
+ if buf.is_next_line_obviously_invalid_request_line():
+ raise LocalProtocolError("illegal request line")
+ return None
+ if not lines:
+ raise LocalProtocolError("no response line received")
+ matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0])
+ http_version = (
+ b"1.1" if matches["http_version"] is None else matches["http_version"]
+ )
+ reason = b"" if matches["reason"] is None else matches["reason"]
+ status_code = int(matches["status_code"])
+ class_: Union[Type[InformationalResponse], Type[Response]] = (
+ InformationalResponse if status_code < 200 else Response
+ )
+ return class_(
+ headers=list(_decode_header_lines(lines[1:])),
+ _parsed=True,
+ status_code=status_code,
+ reason=reason,
+ http_version=http_version,
+ )
+
+
+class ContentLengthReader:
+ def __init__(self, length: int) -> None:
+ self._length = length
+ self._remaining = length
+
+ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
+ if self._remaining == 0:
+ return EndOfMessage()
+ data = buf.maybe_extract_at_most(self._remaining)
+ if data is None:
+ return None
+ self._remaining -= len(data)
+ return Data(data=data)
+
+ def read_eof(self) -> NoReturn:
+ raise RemoteProtocolError(
+ "peer closed connection without sending complete message body "
+ "(received {} bytes, expected {})".format(
+ self._length - self._remaining, self._length
+ )
+ )
+
+
+chunk_header_re = re.compile(chunk_header.encode("ascii"))
+
+
+class ChunkedReader:
+ def __init__(self) -> None:
+ self._bytes_in_chunk = 0
+ # After reading a chunk, we have to throw away the trailing \r\n; if
+ # this is >0 then we discard that many bytes before resuming regular
+ # de-chunkification.
+ self._bytes_to_discard = 0
+ self._reading_trailer = False
+
+ def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]:
+ if self._reading_trailer:
+ lines = buf.maybe_extract_lines()
+ if lines is None:
+ return None
+ return EndOfMessage(headers=list(_decode_header_lines(lines)))
+ if self._bytes_to_discard > 0:
+ data = buf.maybe_extract_at_most(self._bytes_to_discard)
+ if data is None:
+ return None
+ self._bytes_to_discard -= len(data)
+ if self._bytes_to_discard > 0:
+ return None
+ # else, fall through and read some more
+ assert self._bytes_to_discard == 0
+ if self._bytes_in_chunk == 0:
+ # We need to refill our chunk count
+ chunk_header = buf.maybe_extract_next_line()
+ if chunk_header is None:
+ return None
+ matches = validate(
+ chunk_header_re,
+ chunk_header,
+ "illegal chunk header: {!r}",
+ chunk_header,
+ )
+ # XX FIXME: we discard chunk extensions. Does anyone care?
+ self._bytes_in_chunk = int(matches["chunk_size"], base=16)
+ if self._bytes_in_chunk == 0:
+ self._reading_trailer = True
+ return self(buf)
+ chunk_start = True
+ else:
+ chunk_start = False
+ assert self._bytes_in_chunk > 0
+ data = buf.maybe_extract_at_most(self._bytes_in_chunk)
+ if data is None:
+ return None
+ self._bytes_in_chunk -= len(data)
+ if self._bytes_in_chunk == 0:
+ self._bytes_to_discard = 2
+ chunk_end = True
+ else:
+ chunk_end = False
+ return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end)
+
+ def read_eof(self) -> NoReturn:
+ raise RemoteProtocolError(
+ "peer closed connection without sending complete message body "
+ "(incomplete chunked read)"
+ )
+
+
+class Http10Reader:
+ def __call__(self, buf: ReceiveBuffer) -> Optional[Data]:
+ data = buf.maybe_extract_at_most(999999999)
+ if data is None:
+ return None
+ return Data(data=data)
+
+ def read_eof(self) -> EndOfMessage:
+ return EndOfMessage()
+
+
+def expect_nothing(buf: ReceiveBuffer) -> None:
+ if buf:
+ raise LocalProtocolError("Got data when expecting EOF")
+ return None
+
+
+ReadersType = Dict[
+ Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]],
+ Union[Callable[..., Any], Dict[str, Callable[..., Any]]],
+]
+
+READERS: ReadersType = {
+ (CLIENT, IDLE): maybe_read_from_IDLE_client,
+ (SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server,
+ (SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server,
+ (CLIENT, DONE): expect_nothing,
+ (CLIENT, MUST_CLOSE): expect_nothing,
+ (CLIENT, CLOSED): expect_nothing,
+ (SERVER, DONE): expect_nothing,
+ (SERVER, MUST_CLOSE): expect_nothing,
+ (SERVER, CLOSED): expect_nothing,
+ SEND_BODY: {
+ "chunked": ChunkedReader,
+ "content-length": ContentLengthReader,
+ "http/1.0": Http10Reader,
+ },
+}
diff --git a/venv/lib/python3.11/site-packages/h11/_receivebuffer.py b/venv/lib/python3.11/site-packages/h11/_receivebuffer.py
new file mode 100644
index 0000000..e5c4e08
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_receivebuffer.py
@@ -0,0 +1,153 @@
+import re
+import sys
+from typing import List, Optional, Union
+
+__all__ = ["ReceiveBuffer"]
+
+
+# Operations we want to support:
+# - find next \r\n or \r\n\r\n (\n or \n\n are also acceptable),
+# or wait until there is one
+# - read at-most-N bytes
+# Goals:
+# - on average, do this fast
+# - worst case, do this in O(n) where n is the number of bytes processed
+# Plan:
+# - store bytearray, offset, how far we've searched for a separator token
+# - use the how-far-we've-searched data to avoid rescanning
+# - while doing a stream of uninterrupted processing, advance offset instead
+# of constantly copying
+# WARNING:
+# - I haven't benchmarked or profiled any of this yet.
+#
+# Note that starting in Python 3.4, deleting the initial n bytes from a
+# bytearray is amortized O(n), thanks to some excellent work by Antoine
+# Martin:
+#
+# https://bugs.python.org/issue19087
+#
+# This means that if we only supported 3.4+, we could get rid of the code here
+# involving self._start and self.compress, because it's doing exactly the same
+# thing that bytearray now does internally.
+#
+# BUT unfortunately, we still support 2.7, and reading short segments out of a
+# long buffer MUST be O(bytes read) to avoid DoS issues, so we can't actually
+# delete this code. Yet:
+#
+# https://pythonclock.org/
+#
+# (Two things to double-check first though: make sure PyPy also has the
+# optimization, and benchmark to make sure it's a win, since we do have a
+# slightly clever thing where we delay calling compress() until we've
+# processed a whole event, which could in theory be slightly more efficient
+# than the internal bytearray support.)
+blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE)
+
+
+class ReceiveBuffer:
+ def __init__(self) -> None:
+ self._data = bytearray()
+ self._next_line_search = 0
+ self._multiple_lines_search = 0
+
+ def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer":
+ self._data += byteslike
+ return self
+
+ def __bool__(self) -> bool:
+ return bool(len(self))
+
+ def __len__(self) -> int:
+ return len(self._data)
+
+ # for @property unprocessed_data
+ def __bytes__(self) -> bytes:
+ return bytes(self._data)
+
+ def _extract(self, count: int) -> bytearray:
+ # extracting an initial slice of the data buffer and return it
+ out = self._data[:count]
+ del self._data[:count]
+
+ self._next_line_search = 0
+ self._multiple_lines_search = 0
+
+ return out
+
+ def maybe_extract_at_most(self, count: int) -> Optional[bytearray]:
+ """
+ Extract a fixed number of bytes from the buffer.
+ """
+ out = self._data[:count]
+ if not out:
+ return None
+
+ return self._extract(count)
+
+ def maybe_extract_next_line(self) -> Optional[bytearray]:
+ """
+ Extract the first line, if it is completed in the buffer.
+ """
+ # Only search in buffer space that we've not already looked at.
+ search_start_index = max(0, self._next_line_search - 1)
+ partial_idx = self._data.find(b"\r\n", search_start_index)
+
+ if partial_idx == -1:
+ self._next_line_search = len(self._data)
+ return None
+
+ # + 2 is to compensate len(b"\r\n")
+ idx = partial_idx + 2
+
+ return self._extract(idx)
+
+ def maybe_extract_lines(self) -> Optional[List[bytearray]]:
+ """
+ Extract everything up to the first blank line, and return a list of lines.
+ """
+ # Handle the case where we have an immediate empty line.
+ if self._data[:1] == b"\n":
+ self._extract(1)
+ return []
+
+ if self._data[:2] == b"\r\n":
+ self._extract(2)
+ return []
+
+ # Only search in buffer space that we've not already looked at.
+ match = blank_line_regex.search(self._data, self._multiple_lines_search)
+ if match is None:
+ self._multiple_lines_search = max(0, len(self._data) - 2)
+ return None
+
+ # Truncate the buffer and return it.
+ idx = match.span(0)[-1]
+ out = self._extract(idx)
+ lines = out.split(b"\n")
+
+ for line in lines:
+ if line.endswith(b"\r"):
+ del line[-1]
+
+ assert lines[-2] == lines[-1] == b""
+
+ del lines[-2:]
+
+ return lines
+
+ # In theory we should wait until `\r\n` before starting to validate
+ # incoming data. However it's interesting to detect (very) invalid data
+ # early given they might not even contain `\r\n` at all (hence only
+ # timeout will get rid of them).
+ # This is not a 100% effective detection but more of a cheap sanity check
+ # allowing for early abort in some useful cases.
+ # This is especially interesting when peer is messing up with HTTPS and
+ # sent us a TLS stream where we were expecting plain HTTP given all
+ # versions of TLS so far start handshake with a 0x16 message type code.
+ def is_next_line_obviously_invalid_request_line(self) -> bool:
+ try:
+ # HTTP header line must not contain non-printable characters
+ # and should not start with a space
+ return self._data[0] < 0x21
+ except IndexError:
+ return False
diff --git a/venv/lib/python3.11/site-packages/h11/_state.py b/venv/lib/python3.11/site-packages/h11/_state.py
new file mode 100644
index 0000000..3593430
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_state.py
@@ -0,0 +1,367 @@
+################################################################
+# The core state machine
+################################################################
+#
+# Rule 1: everything that affects the state machine and state transitions must
+# live here in this file. As much as possible goes into the table-based
+# representation, but for the bits that don't quite fit, the actual code and
+# state must nonetheless live here.
+#
+# Rule 2: this file does not know about what role we're playing; it only knows
+# about HTTP request/response cycles in the abstract. This ensures that we
+# don't cheat and apply different rules to local and remote parties.
+#
+#
+# Theory of operation
+# ===================
+#
+# Possibly the simplest way to think about this is that we actually have 5
+# different state machines here. Yes, 5. These are:
+#
+# 1) The client state, with its complicated automaton (see the docs)
+# 2) The server state, with its complicated automaton (see the docs)
+# 3) The keep-alive state, with possible states {True, False}
+# 4) The SWITCH_CONNECT state, with possible states {False, True}
+# 5) The SWITCH_UPGRADE state, with possible states {False, True}
+#
+# For (3)-(5), the first state listed is the initial state.
+#
+# (1)-(3) are stored explicitly in member variables. The last
+# two are stored implicitly in the pending_switch_proposals set as:
+# (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals)
+# (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals)
+#
+# And each of these machines has two different kinds of transitions:
+#
+# a) Event-triggered
+# b) State-triggered
+#
+# Event triggered is the obvious thing that you'd think it is: some event
+# happens, and if it's the right event at the right time then a transition
+# happens. But there are somewhat complicated rules for which machines can
+# "see" which events. (As a rule of thumb, if a machine "sees" an event, this
+# means two things: the event can affect the machine, and if the machine is
+# not in a state where it expects that event then it's an error.) These rules
+# are:
+#
+# 1) The client machine sees all h11.events objects emitted by the client.
+#
+# 2) The server machine sees all h11.events objects emitted by the server.
+#
+# It also sees the client's Request event.
+#
+# And sometimes, server events are annotated with a _SWITCH_* event. For
+# example, we can have a (Response, _SWITCH_CONNECT) event, which is
+# different from a regular Response event.
+#
+# 3) The keep-alive machine sees the process_keep_alive_disabled() event
+# (which is derived from Request/Response events), and this event
+# transitions it from True -> False, or from False -> False. There's no way
+# to transition back.
+#
+# 4&5) The _SWITCH_* machines transition from False->True when we get a
+# Request that proposes the relevant type of switch (via
+# process_client_switch_proposals), and they go from True->False when we
+# get a Response that has no _SWITCH_* annotation.
+#
+# So that's event-triggered transitions.
+#
+# State-triggered transitions are less standard. What they do here is couple
+# the machines together. The way this works is, when certain *joint*
+# configurations of states are achieved, then we automatically transition to a
+# new *joint* state. So, for example, if we're ever in a joint state with
+#
+# client: DONE
+# keep-alive: False
+#
+# then the client state immediately transitions to:
+#
+# client: MUST_CLOSE
+#
+# This is fundamentally different from an event-based transition, because it
+# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state
+# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive
+# transitioned True -> False. Either way, once this precondition is satisfied,
+# this transition is immediately triggered.
+#
+# What if two conflicting state-based transitions get enabled at the same
+# time? In practice there's only one case where this arises (client DONE ->
+# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by
+# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition.
+#
+# Implementation
+# --------------
+#
+# The event-triggered transitions for the server and client machines are all
+# stored explicitly in a table. Ditto for the state-triggered transitions that
+# involve just the server and client state.
+#
+# The transitions for the other machines, and the state-triggered transitions
+# that involve the other machines, are written out as explicit Python code.
+#
+# It'd be nice if there were some cleaner way to do all this. This isn't
+# *too* terrible, but I feel like it could probably be better.
+#
+# WARNING
+# -------
+#
+# The script that generates the state machine diagrams for the docs knows how
+# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS
+# tables. But it can't automatically read the transitions that are written
+# directly in Python code. So if you touch those, you need to also update the
+# script to keep it in sync!
+from typing import cast, Dict, Optional, Set, Tuple, Type, Union
+
+from ._events import *
+from ._util import LocalProtocolError, Sentinel
+
+# Everything in __all__ gets re-exported as part of the h11 public API.
+__all__ = [
+ "CLIENT",
+ "SERVER",
+ "IDLE",
+ "SEND_RESPONSE",
+ "SEND_BODY",
+ "DONE",
+ "MUST_CLOSE",
+ "CLOSED",
+ "MIGHT_SWITCH_PROTOCOL",
+ "SWITCHED_PROTOCOL",
+ "ERROR",
+]
+
+
+class CLIENT(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class SERVER(Sentinel, metaclass=Sentinel):
+ pass
+
+
+# States
+class IDLE(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class SEND_RESPONSE(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class SEND_BODY(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class DONE(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class MUST_CLOSE(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class CLOSED(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class ERROR(Sentinel, metaclass=Sentinel):
+ pass
+
+
+# Switch types
+class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel):
+ pass
+
+
+class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel):
+ pass
+
+
+EventTransitionType = Dict[
+ Type[Sentinel],
+ Dict[
+ Type[Sentinel],
+ Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]],
+ ],
+]
+
+EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = {
+ CLIENT: {
+ IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED},
+ SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
+ DONE: {ConnectionClosed: CLOSED},
+ MUST_CLOSE: {ConnectionClosed: CLOSED},
+ CLOSED: {ConnectionClosed: CLOSED},
+ MIGHT_SWITCH_PROTOCOL: {},
+ SWITCHED_PROTOCOL: {},
+ ERROR: {},
+ },
+ SERVER: {
+ IDLE: {
+ ConnectionClosed: CLOSED,
+ Response: SEND_BODY,
+ # Special case: server sees client Request events, in this form
+ (Request, CLIENT): SEND_RESPONSE,
+ },
+ SEND_RESPONSE: {
+ InformationalResponse: SEND_RESPONSE,
+ Response: SEND_BODY,
+ (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL,
+ (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL,
+ },
+ SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE},
+ DONE: {ConnectionClosed: CLOSED},
+ MUST_CLOSE: {ConnectionClosed: CLOSED},
+ CLOSED: {ConnectionClosed: CLOSED},
+ SWITCHED_PROTOCOL: {},
+ ERROR: {},
+ },
+}
+
+StateTransitionType = Dict[
+ Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]]
+]
+
+# NB: there are also some special-case state-triggered transitions hard-coded
+# into _fire_state_triggered_transitions below.
+STATE_TRIGGERED_TRANSITIONS: StateTransitionType = {
+ # (Client state, Server state) -> new states
+ # Protocol negotiation
+ (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL},
+ # Socket shutdown
+ (CLOSED, DONE): {SERVER: MUST_CLOSE},
+ (CLOSED, IDLE): {SERVER: MUST_CLOSE},
+ (ERROR, DONE): {SERVER: MUST_CLOSE},
+ (DONE, CLOSED): {CLIENT: MUST_CLOSE},
+ (IDLE, CLOSED): {CLIENT: MUST_CLOSE},
+ (DONE, ERROR): {CLIENT: MUST_CLOSE},
+}
+
+
+class ConnectionState:
+ def __init__(self) -> None:
+ # Extra bits of state that don't quite fit into the state model.
+
+ # If this is False then it enables the automatic DONE -> MUST_CLOSE
+ # transition. Don't set this directly; call .keep_alive_disabled()
+ self.keep_alive = True
+
+ # This is a subset of {UPGRADE, CONNECT}, containing the proposals
+ # made by the client for switching protocols.
+ self.pending_switch_proposals: Set[Type[Sentinel]] = set()
+
+ self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE}
+
+ def process_error(self, role: Type[Sentinel]) -> None:
+ self.states[role] = ERROR
+ self._fire_state_triggered_transitions()
+
+ def process_keep_alive_disabled(self) -> None:
+ self.keep_alive = False
+ self._fire_state_triggered_transitions()
+
+ def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None:
+ self.pending_switch_proposals.add(switch_event)
+ self._fire_state_triggered_transitions()
+
+ def process_event(
+ self,
+ role: Type[Sentinel],
+ event_type: Type[Event],
+ server_switch_event: Optional[Type[Sentinel]] = None,
+ ) -> None:
+ _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type
+ if server_switch_event is not None:
+ assert role is SERVER
+ if server_switch_event not in self.pending_switch_proposals:
+ raise LocalProtocolError(
+ "Received server {} event without a pending proposal".format(
+ server_switch_event
+ )
+ )
+ _event_type = (event_type, server_switch_event)
+ if server_switch_event is None and _event_type is Response:
+ self.pending_switch_proposals = set()
+ self._fire_event_triggered_transitions(role, _event_type)
+ # Special case: the server state does get to see Request
+ # events.
+ if _event_type is Request:
+ assert role is CLIENT
+ self._fire_event_triggered_transitions(SERVER, (Request, CLIENT))
+ self._fire_state_triggered_transitions()
+
+ def _fire_event_triggered_transitions(
+ self,
+ role: Type[Sentinel],
+ event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]],
+ ) -> None:
+ state = self.states[role]
+ try:
+ new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type]
+ except KeyError:
+ event_type = cast(Type[Event], event_type)
+ raise LocalProtocolError(
+ "can't handle event type {} when role={} and state={}".format(
+ event_type.__name__, role, self.states[role]
+ )
+ ) from None
+ self.states[role] = new_state
+
+ def _fire_state_triggered_transitions(self) -> None:
+ # We apply these rules repeatedly until converging on a fixed point
+ while True:
+ start_states = dict(self.states)
+
+ # It could happen that both these special-case transitions are
+ # enabled at the same time:
+ #
+ # DONE -> MIGHT_SWITCH_PROTOCOL
+ # DONE -> MUST_CLOSE
+ #
+ # For example, this will always be true of a HTTP/1.0 client
+ # requesting CONNECT. If this happens, the protocol switch takes
+ # priority. From there the client will either go to
+ # SWITCHED_PROTOCOL, in which case it's none of our business when
+ # they close the connection, or else the server will deny the
+ # request, in which case the client will go back to DONE and then
+ # from there to MUST_CLOSE.
+ if self.pending_switch_proposals:
+ if self.states[CLIENT] is DONE:
+ self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL
+
+ if not self.pending_switch_proposals:
+ if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL:
+ self.states[CLIENT] = DONE
+
+ if not self.keep_alive:
+ for role in (CLIENT, SERVER):
+ if self.states[role] is DONE:
+ self.states[role] = MUST_CLOSE
+
+ # Tabular state-triggered transitions
+ joint_state = (self.states[CLIENT], self.states[SERVER])
+ changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {})
+ self.states.update(changes)
+
+ if self.states == start_states:
+ # Fixed point reached
+ return
+
+ def start_next_cycle(self) -> None:
+ if self.states != {CLIENT: DONE, SERVER: DONE}:
+ raise LocalProtocolError(
+ "not in a reusable state. self.states={}".format(self.states)
+ )
+ # Can't reach DONE/DONE with any of these active, but still, let's be
+ # sure.
+ assert self.keep_alive
+ assert not self.pending_switch_proposals
+ self.states = {CLIENT: IDLE, SERVER: IDLE}
diff --git a/venv/lib/python3.11/site-packages/h11/_util.py b/venv/lib/python3.11/site-packages/h11/_util.py
new file mode 100644
index 0000000..6718445
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_util.py
@@ -0,0 +1,135 @@
+from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union
+
+__all__ = [
+ "ProtocolError",
+ "LocalProtocolError",
+ "RemoteProtocolError",
+ "validate",
+ "bytesify",
+]
+
+
+class ProtocolError(Exception):
+ """Exception indicating a violation of the HTTP/1.1 protocol.
+
+ This as an abstract base class, with two concrete base classes:
+ :exc:`LocalProtocolError`, which indicates that you tried to do something
+ that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which
+ indicates that the remote peer tried to do something that HTTP/1.1 says is
+ illegal. See :ref:`error-handling` for details.
+
+ In addition to the normal :exc:`Exception` features, it has one attribute:
+
+ .. attribute:: error_status_hint
+
+ This gives a suggestion as to what status code a server might use if
+ this error occurred as part of a request.
+
+ For a :exc:`RemoteProtocolError`, this is useful as a suggestion for
+ how you might want to respond to a misbehaving peer, if you're
+ implementing a server.
+
+ For a :exc:`LocalProtocolError`, this can be taken as a suggestion for
+ how your peer might have responded to *you* if h11 had allowed you to
+ continue.
+
+ The default is 400 Bad Request, a generic catch-all for protocol
+ violations.
+
+ """
+
+ def __init__(self, msg: str, error_status_hint: int = 400) -> None:
+ if type(self) is ProtocolError:
+ raise TypeError("tried to directly instantiate ProtocolError")
+ Exception.__init__(self, msg)
+ self.error_status_hint = error_status_hint
+
+
+# Strategy: there are a number of public APIs where a LocalProtocolError can
+# be raised (send(), all the different event constructors, ...), and only one
+# public API where RemoteProtocolError can be raised
+# (receive_data()). Therefore we always raise LocalProtocolError internally,
+# and then receive_data will translate this into a RemoteProtocolError.
+#
+# Internally:
+# LocalProtocolError is the generic "ProtocolError".
+# Externally:
+# LocalProtocolError is for local errors and RemoteProtocolError is for
+# remote errors.
+class LocalProtocolError(ProtocolError):
+ def _reraise_as_remote_protocol_error(self) -> NoReturn:
+ # After catching a LocalProtocolError, use this method to re-raise it
+ # as a RemoteProtocolError. This method must be called from inside an
+ # except: block.
+ #
+ # An easy way to get an equivalent RemoteProtocolError is just to
+ # modify 'self' in place.
+ self.__class__ = RemoteProtocolError # type: ignore
+ # But the re-raising is somewhat non-trivial -- you might think that
+ # now that we've modified the in-flight exception object, that just
+ # doing 'raise' to re-raise it would be enough. But it turns out that
+ # this doesn't work, because Python tracks the exception type
+ # (exc_info[0]) separately from the exception object (exc_info[1]),
+ # and we only modified the latter. So we really do need to re-raise
+ # the new type explicitly.
+ # On py3, the traceback is part of the exception object, so our
+ # in-place modification preserved it and we can just re-raise:
+ raise self
+
+
+class RemoteProtocolError(ProtocolError):
+ pass
+
+
+def validate(
+ regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any
+) -> Dict[str, bytes]:
+ match = regex.fullmatch(data)
+ if not match:
+ if format_args:
+ msg = msg.format(*format_args)
+ raise LocalProtocolError(msg)
+ return match.groupdict()
+
+
+# Sentinel values
+#
+# - Inherit identity-based comparison and hashing from object
+# - Have a nice repr
+# - Have a *bonus property*: type(sentinel) is sentinel
+#
+# The bonus property is useful if you want to take the return value from
+# next_event() and do some sort of dispatch based on type(event).
+
+_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel")
+
+
+class Sentinel(type):
+ def __new__(
+ cls: Type[_T_Sentinel],
+ name: str,
+ bases: Tuple[type, ...],
+ namespace: Dict[str, Any],
+ **kwds: Any
+ ) -> _T_Sentinel:
+ assert bases == (Sentinel,)
+ v = super().__new__(cls, name, bases, namespace, **kwds)
+ v.__class__ = v # type: ignore
+ return v
+
+ def __repr__(self) -> str:
+ return self.__name__
+
+
+# Used for methods, request targets, HTTP versions, header names, and header
+# values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always
+# returns bytes.
+def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes:
+ # Fast-path:
+ if type(s) is bytes:
+ return s
+ if isinstance(s, str):
+ s = s.encode("ascii")
+ if isinstance(s, int):
+ raise TypeError("expected bytes-like object, not int")
+ return bytes(s)
diff --git a/venv/lib/python3.11/site-packages/h11/_version.py b/venv/lib/python3.11/site-packages/h11/_version.py
new file mode 100644
index 0000000..4c89113
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_version.py
@@ -0,0 +1,16 @@
+# This file must be kept very simple, because it is consumed from several
+# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc.
+
+# We use a simple scheme:
+# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev
+# where the +dev versions are never released into the wild, they're just what
+# we stick into the VCS in between releases.
+#
+# This is compatible with PEP 440:
+# http://legacy.python.org/dev/peps/pep-0440/
+# via the use of the "local suffix" "+dev", which is disallowed on index
+# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we
+# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before*
+# 1.0.0.)
+
+__version__ = "0.14.0"
diff --git a/venv/lib/python3.11/site-packages/h11/_writers.py b/venv/lib/python3.11/site-packages/h11/_writers.py
new file mode 100644
index 0000000..939cdb9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/_writers.py
@@ -0,0 +1,145 @@
+# Code to read HTTP data
+#
+# Strategy: each writer takes an event + a write-some-bytes function, which is
+# calls.
+#
+# WRITERS is a dict describing how to pick a reader. It maps states to either:
+# - a writer
+# - or, for body writers, a dict of framin-dependent writer factories
+
+from typing import Any, Callable, Dict, List, Tuple, Type, Union
+
+from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response
+from ._headers import Headers
+from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER
+from ._util import LocalProtocolError, Sentinel
+
+__all__ = ["WRITERS"]
+
+Writer = Callable[[bytes], Any]
+
+
+def write_headers(headers: Headers, write: Writer) -> None:
+ # "Since the Host field-value is critical information for handling a
+ # request, a user agent SHOULD generate Host as the first header field
+ # following the request-line." - RFC 7230
+ raw_items = headers._full_items
+ for raw_name, name, value in raw_items:
+ if name == b"host":
+ write(b"%s: %s\r\n" % (raw_name, value))
+ for raw_name, name, value in raw_items:
+ if name != b"host":
+ write(b"%s: %s\r\n" % (raw_name, value))
+ write(b"\r\n")
+
+
+def write_request(request: Request, write: Writer) -> None:
+ if request.http_version != b"1.1":
+ raise LocalProtocolError("I only send HTTP/1.1")
+ write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target))
+ write_headers(request.headers, write)
+
+
+# Shared between InformationalResponse and Response
+def write_any_response(
+ response: Union[InformationalResponse, Response], write: Writer
+) -> None:
+ if response.http_version != b"1.1":
+ raise LocalProtocolError("I only send HTTP/1.1")
+ status_bytes = str(response.status_code).encode("ascii")
+ # We don't bother sending ascii status messages like "OK"; they're
+ # optional and ignored by the protocol. (But the space after the numeric
+ # status code is mandatory.)
+ #
+ # XX FIXME: could at least make an effort to pull out the status message
+ # from stdlib's http.HTTPStatus table. Or maybe just steal their enums
+ # (either by import or copy/paste). We already accept them as status codes
+ # since they're of type IntEnum < int.
+ write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason))
+ write_headers(response.headers, write)
+
+
+class BodyWriter:
+ def __call__(self, event: Event, write: Writer) -> None:
+ if type(event) is Data:
+ self.send_data(event.data, write)
+ elif type(event) is EndOfMessage:
+ self.send_eom(event.headers, write)
+ else: # pragma: no cover
+ assert False
+
+ def send_data(self, data: bytes, write: Writer) -> None:
+ pass
+
+ def send_eom(self, headers: Headers, write: Writer) -> None:
+ pass
+
+
+#
+# These are all careful not to do anything to 'data' except call len(data) and
+# write(data). This allows us to transparently pass-through funny objects,
+# like placeholder objects referring to files on disk that will be sent via
+# sendfile(2).
+#
+class ContentLengthWriter(BodyWriter):
+ def __init__(self, length: int) -> None:
+ self._length = length
+
+ def send_data(self, data: bytes, write: Writer) -> None:
+ self._length -= len(data)
+ if self._length < 0:
+ raise LocalProtocolError("Too much data for declared Content-Length")
+ write(data)
+
+ def send_eom(self, headers: Headers, write: Writer) -> None:
+ if self._length != 0:
+ raise LocalProtocolError("Too little data for declared Content-Length")
+ if headers:
+ raise LocalProtocolError("Content-Length and trailers don't mix")
+
+
+class ChunkedWriter(BodyWriter):
+ def send_data(self, data: bytes, write: Writer) -> None:
+ # if we encoded 0-length data in the naive way, it would look like an
+ # end-of-message.
+ if not data:
+ return
+ write(b"%x\r\n" % len(data))
+ write(data)
+ write(b"\r\n")
+
+ def send_eom(self, headers: Headers, write: Writer) -> None:
+ write(b"0\r\n")
+ write_headers(headers, write)
+
+
+class Http10Writer(BodyWriter):
+ def send_data(self, data: bytes, write: Writer) -> None:
+ write(data)
+
+ def send_eom(self, headers: Headers, write: Writer) -> None:
+ if headers:
+ raise LocalProtocolError("can't send trailers to HTTP/1.0 client")
+ # no need to close the socket ourselves, that will be taken care of by
+ # Connection: close machinery
+
+
+WritersType = Dict[
+ Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]],
+ Union[
+ Dict[str, Type[BodyWriter]],
+ Callable[[Union[InformationalResponse, Response], Writer], None],
+ Callable[[Request, Writer], None],
+ ],
+]
+
+WRITERS: WritersType = {
+ (CLIENT, IDLE): write_request,
+ (SERVER, IDLE): write_any_response,
+ (SERVER, SEND_RESPONSE): write_any_response,
+ SEND_BODY: {
+ "chunked": ChunkedWriter,
+ "content-length": ContentLengthWriter,
+ "http/1.0": Http10Writer,
+ },
+}
diff --git a/venv/lib/python3.11/site-packages/h11/py.typed b/venv/lib/python3.11/site-packages/h11/py.typed
new file mode 100644
index 0000000..f5642f7
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/py.typed
@@ -0,0 +1 @@
+Marker
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__init__.py b/venv/lib/python3.11/site-packages/h11/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__init__.py
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000..b45464e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/__init__.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/helpers.cpython-311.pyc
new file mode 100644
index 0000000..d8e415d
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/helpers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-311.pyc
new file mode 100644
index 0000000..564b601
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_against_stdlib_http.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_connection.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_connection.cpython-311.pyc
new file mode 100644
index 0000000..c9f269e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_connection.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_events.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_events.cpython-311.pyc
new file mode 100644
index 0000000..90bb491
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_events.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_headers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_headers.cpython-311.pyc
new file mode 100644
index 0000000..5c15287
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_headers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_helpers.cpython-311.pyc
new file mode 100644
index 0000000..dcefc5a
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_helpers.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_io.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_io.cpython-311.pyc
new file mode 100644
index 0000000..9abf567
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_io.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_receivebuffer.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_receivebuffer.cpython-311.pyc
new file mode 100644
index 0000000..8f1f6db
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_receivebuffer.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_state.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_state.cpython-311.pyc
new file mode 100644
index 0000000..1115678
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_state.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_util.cpython-311.pyc b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_util.cpython-311.pyc
new file mode 100644
index 0000000..b3bfd20
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/__pycache__/test_util.cpython-311.pyc
Binary files differ
diff --git a/venv/lib/python3.11/site-packages/h11/tests/data/test-file b/venv/lib/python3.11/site-packages/h11/tests/data/test-file
new file mode 100644
index 0000000..d0be0a6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/data/test-file
@@ -0,0 +1 @@
+92b12bc045050b55b848d37167a1a63947c364579889ce1d39788e45e9fac9e5
diff --git a/venv/lib/python3.11/site-packages/h11/tests/helpers.py b/venv/lib/python3.11/site-packages/h11/tests/helpers.py
new file mode 100644
index 0000000..571be44
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/helpers.py
@@ -0,0 +1,101 @@
+from typing import cast, List, Type, Union, ValuesView
+
+from .._connection import Connection, NEED_DATA, PAUSED
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .._state import CLIENT, CLOSED, DONE, MUST_CLOSE, SERVER
+from .._util import Sentinel
+
+try:
+ from typing import Literal
+except ImportError:
+ from typing_extensions import Literal # type: ignore
+
+
+def get_all_events(conn: Connection) -> List[Event]:
+ got_events = []
+ while True:
+ event = conn.next_event()
+ if event in (NEED_DATA, PAUSED):
+ break
+ event = cast(Event, event)
+ got_events.append(event)
+ if type(event) is ConnectionClosed:
+ break
+ return got_events
+
+
+def receive_and_get(conn: Connection, data: bytes) -> List[Event]:
+ conn.receive_data(data)
+ return get_all_events(conn)
+
+
+# Merges adjacent Data events, converts payloads to bytestrings, and removes
+# chunk boundaries.
+def normalize_data_events(in_events: List[Event]) -> List[Event]:
+ out_events: List[Event] = []
+ for event in in_events:
+ if type(event) is Data:
+ event = Data(data=bytes(event.data), chunk_start=False, chunk_end=False)
+ if out_events and type(out_events[-1]) is type(event) is Data:
+ out_events[-1] = Data(
+ data=out_events[-1].data + event.data,
+ chunk_start=out_events[-1].chunk_start,
+ chunk_end=out_events[-1].chunk_end,
+ )
+ else:
+ out_events.append(event)
+ return out_events
+
+
+# Given that we want to write tests that push some events through a Connection
+# and check that its state updates appropriately... we might as make a habit
+# of pushing them through two Connections with a fake network link in
+# between.
+class ConnectionPair:
+ def __init__(self) -> None:
+ self.conn = {CLIENT: Connection(CLIENT), SERVER: Connection(SERVER)}
+ self.other = {CLIENT: SERVER, SERVER: CLIENT}
+
+ @property
+ def conns(self) -> ValuesView[Connection]:
+ return self.conn.values()
+
+ # expect="match" if expect=send_events; expect=[...] to say what expected
+ def send(
+ self,
+ role: Type[Sentinel],
+ send_events: Union[List[Event], Event],
+ expect: Union[List[Event], Event, Literal["match"]] = "match",
+ ) -> bytes:
+ if not isinstance(send_events, list):
+ send_events = [send_events]
+ data = b""
+ closed = False
+ for send_event in send_events:
+ new_data = self.conn[role].send(send_event)
+ if new_data is None:
+ closed = True
+ else:
+ data += new_data
+ # send uses b"" to mean b"", and None to mean closed
+ # receive uses b"" to mean closed, and None to mean "try again"
+ # so we have to translate between the two conventions
+ if data:
+ self.conn[self.other[role]].receive_data(data)
+ if closed:
+ self.conn[self.other[role]].receive_data(b"")
+ got_events = get_all_events(self.conn[self.other[role]])
+ if expect == "match":
+ expect = send_events
+ if not isinstance(expect, list):
+ expect = [expect]
+ assert got_events == expect
+ return data
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_against_stdlib_http.py b/venv/lib/python3.11/site-packages/h11/tests/test_against_stdlib_http.py
new file mode 100644
index 0000000..d2ee131
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_against_stdlib_http.py
@@ -0,0 +1,115 @@
+import json
+import os.path
+import socket
+import socketserver
+import threading
+from contextlib import closing, contextmanager
+from http.server import SimpleHTTPRequestHandler
+from typing import Callable, Generator
+from urllib.request import urlopen
+
+import h11
+
+
+@contextmanager
+def socket_server(
+ handler: Callable[..., socketserver.BaseRequestHandler]
+) -> Generator[socketserver.TCPServer, None, None]:
+ httpd = socketserver.TCPServer(("127.0.0.1", 0), handler)
+ thread = threading.Thread(
+ target=httpd.serve_forever, kwargs={"poll_interval": 0.01}
+ )
+ thread.daemon = True
+ try:
+ thread.start()
+ yield httpd
+ finally:
+ httpd.shutdown()
+
+
+test_file_path = os.path.join(os.path.dirname(__file__), "data/test-file")
+with open(test_file_path, "rb") as f:
+ test_file_data = f.read()
+
+
+class SingleMindedRequestHandler(SimpleHTTPRequestHandler):
+ def translate_path(self, path: str) -> str:
+ return test_file_path
+
+
+def test_h11_as_client() -> None:
+ with socket_server(SingleMindedRequestHandler) as httpd:
+ with closing(socket.create_connection(httpd.server_address)) as s:
+ c = h11.Connection(h11.CLIENT)
+
+ s.sendall(
+ c.send( # type: ignore[arg-type]
+ h11.Request(
+ method="GET", target="/foo", headers=[("Host", "localhost")]
+ )
+ )
+ )
+ s.sendall(c.send(h11.EndOfMessage())) # type: ignore[arg-type]
+
+ data = bytearray()
+ while True:
+ event = c.next_event()
+ print(event)
+ if event is h11.NEED_DATA:
+ # Use a small read buffer to make things more challenging
+ # and exercise more paths :-)
+ c.receive_data(s.recv(10))
+ continue
+ if type(event) is h11.Response:
+ assert event.status_code == 200
+ if type(event) is h11.Data:
+ data += event.data
+ if type(event) is h11.EndOfMessage:
+ break
+ assert bytes(data) == test_file_data
+
+
+class H11RequestHandler(socketserver.BaseRequestHandler):
+ def handle(self) -> None:
+ with closing(self.request) as s:
+ c = h11.Connection(h11.SERVER)
+ request = None
+ while True:
+ event = c.next_event()
+ if event is h11.NEED_DATA:
+ # Use a small read buffer to make things more challenging
+ # and exercise more paths :-)
+ c.receive_data(s.recv(10))
+ continue
+ if type(event) is h11.Request:
+ request = event
+ if type(event) is h11.EndOfMessage:
+ break
+ assert request is not None
+ info = json.dumps(
+ {
+ "method": request.method.decode("ascii"),
+ "target": request.target.decode("ascii"),
+ "headers": {
+ name.decode("ascii"): value.decode("ascii")
+ for (name, value) in request.headers
+ },
+ }
+ )
+ s.sendall(c.send(h11.Response(status_code=200, headers=[]))) # type: ignore[arg-type]
+ s.sendall(c.send(h11.Data(data=info.encode("ascii"))))
+ s.sendall(c.send(h11.EndOfMessage()))
+
+
+def test_h11_as_server() -> None:
+ with socket_server(H11RequestHandler) as httpd:
+ host, port = httpd.server_address
+ url = "http://{}:{}/some-path".format(host, port)
+ with closing(urlopen(url)) as f:
+ assert f.getcode() == 200
+ data = f.read()
+ info = json.loads(data.decode("ascii"))
+ print(info)
+ assert info["method"] == "GET"
+ assert info["target"] == "/some-path"
+ assert "urllib" in info["headers"]["user-agent"]
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_connection.py b/venv/lib/python3.11/site-packages/h11/tests/test_connection.py
new file mode 100644
index 0000000..73a27b9
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_connection.py
@@ -0,0 +1,1122 @@
+from typing import Any, cast, Dict, List, Optional, Tuple, Type
+
+import pytest
+
+from .._connection import _body_framing, _keep_alive, Connection, NEED_DATA, PAUSED
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .._state import (
+ CLIENT,
+ CLOSED,
+ DONE,
+ ERROR,
+ IDLE,
+ MIGHT_SWITCH_PROTOCOL,
+ MUST_CLOSE,
+ SEND_BODY,
+ SEND_RESPONSE,
+ SERVER,
+ SWITCHED_PROTOCOL,
+)
+from .._util import LocalProtocolError, RemoteProtocolError, Sentinel
+from .helpers import ConnectionPair, get_all_events, receive_and_get
+
+
+def test__keep_alive() -> None:
+ assert _keep_alive(
+ Request(method="GET", target="/", headers=[("Host", "Example.com")])
+ )
+ assert not _keep_alive(
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "Example.com"), ("Connection", "close")],
+ )
+ )
+ assert not _keep_alive(
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "Example.com"), ("Connection", "a, b, cLOse, foo")],
+ )
+ )
+ assert not _keep_alive(
+ Request(method="GET", target="/", headers=[], http_version="1.0") # type: ignore[arg-type]
+ )
+
+ assert _keep_alive(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ assert not _keep_alive(Response(status_code=200, headers=[("Connection", "close")]))
+ assert not _keep_alive(
+ Response(status_code=200, headers=[("Connection", "a, b, cLOse, foo")])
+ )
+ assert not _keep_alive(Response(status_code=200, headers=[], http_version="1.0")) # type: ignore[arg-type]
+
+
+def test__body_framing() -> None:
+ def headers(cl: Optional[int], te: bool) -> List[Tuple[str, str]]:
+ headers = []
+ if cl is not None:
+ headers.append(("Content-Length", str(cl)))
+ if te:
+ headers.append(("Transfer-Encoding", "chunked"))
+ return headers
+
+ def resp(
+ status_code: int = 200, cl: Optional[int] = None, te: bool = False
+ ) -> Response:
+ return Response(status_code=status_code, headers=headers(cl, te))
+
+ def req(cl: Optional[int] = None, te: bool = False) -> Request:
+ h = headers(cl, te)
+ h += [("Host", "example.com")]
+ return Request(method="GET", target="/", headers=h)
+
+ # Special cases where the headers are ignored:
+ for kwargs in [{}, {"cl": 100}, {"te": True}, {"cl": 100, "te": True}]:
+ kwargs = cast(Dict[str, Any], kwargs)
+ for meth, r in [
+ (b"HEAD", resp(**kwargs)),
+ (b"GET", resp(status_code=204, **kwargs)),
+ (b"GET", resp(status_code=304, **kwargs)),
+ ]:
+ assert _body_framing(meth, r) == ("content-length", (0,))
+
+ # Transfer-encoding
+ for kwargs in [{"te": True}, {"cl": 100, "te": True}]:
+ kwargs = cast(Dict[str, Any], kwargs)
+ for meth, r in [(None, req(**kwargs)), (b"GET", resp(**kwargs))]: # type: ignore
+ assert _body_framing(meth, r) == ("chunked", ())
+
+ # Content-Length
+ for meth, r in [(None, req(cl=100)), (b"GET", resp(cl=100))]: # type: ignore
+ assert _body_framing(meth, r) == ("content-length", (100,))
+
+ # No headers
+ assert _body_framing(None, req()) == ("content-length", (0,)) # type: ignore
+ assert _body_framing(b"GET", resp()) == ("http/1.0", ())
+
+
+def test_Connection_basics_and_content_length() -> None:
+ with pytest.raises(ValueError):
+ Connection("CLIENT") # type: ignore
+
+ p = ConnectionPair()
+ assert p.conn[CLIENT].our_role is CLIENT
+ assert p.conn[CLIENT].their_role is SERVER
+ assert p.conn[SERVER].our_role is SERVER
+ assert p.conn[SERVER].their_role is CLIENT
+
+ data = p.send(
+ CLIENT,
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Content-Length", "10")],
+ ),
+ )
+ assert data == (
+ b"GET / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 10\r\n\r\n"
+ )
+
+ for conn in p.conns:
+ assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+ assert p.conn[CLIENT].our_state is SEND_BODY
+ assert p.conn[CLIENT].their_state is SEND_RESPONSE
+ assert p.conn[SERVER].our_state is SEND_RESPONSE
+ assert p.conn[SERVER].their_state is SEND_BODY
+
+ assert p.conn[CLIENT].their_http_version is None
+ assert p.conn[SERVER].their_http_version == b"1.1"
+
+ data = p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type]
+ assert data == b"HTTP/1.1 100 \r\n\r\n"
+
+ data = p.send(SERVER, Response(status_code=200, headers=[("Content-Length", "11")]))
+ assert data == b"HTTP/1.1 200 \r\nContent-Length: 11\r\n\r\n"
+
+ for conn in p.conns:
+ assert conn.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY}
+
+ assert p.conn[CLIENT].their_http_version == b"1.1"
+ assert p.conn[SERVER].their_http_version == b"1.1"
+
+ data = p.send(CLIENT, Data(data=b"12345"))
+ assert data == b"12345"
+ data = p.send(
+ CLIENT, Data(data=b"67890"), expect=[Data(data=b"67890"), EndOfMessage()]
+ )
+ assert data == b"67890"
+ data = p.send(CLIENT, EndOfMessage(), expect=[])
+ assert data == b""
+
+ for conn in p.conns:
+ assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY}
+
+ data = p.send(SERVER, Data(data=b"1234567890"))
+ assert data == b"1234567890"
+ data = p.send(SERVER, Data(data=b"1"), expect=[Data(data=b"1"), EndOfMessage()])
+ assert data == b"1"
+ data = p.send(SERVER, EndOfMessage(), expect=[])
+ assert data == b""
+
+ for conn in p.conns:
+ assert conn.states == {CLIENT: DONE, SERVER: DONE}
+
+
+def test_chunked() -> None:
+ p = ConnectionPair()
+
+ p.send(
+ CLIENT,
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")],
+ ),
+ )
+ data = p.send(CLIENT, Data(data=b"1234567890", chunk_start=True, chunk_end=True))
+ assert data == b"a\r\n1234567890\r\n"
+ data = p.send(CLIENT, Data(data=b"abcde", chunk_start=True, chunk_end=True))
+ assert data == b"5\r\nabcde\r\n"
+ data = p.send(CLIENT, Data(data=b""), expect=[])
+ assert data == b""
+ data = p.send(CLIENT, EndOfMessage(headers=[("hello", "there")]))
+ assert data == b"0\r\nhello: there\r\n\r\n"
+
+ p.send(
+ SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")])
+ )
+ p.send(SERVER, Data(data=b"54321", chunk_start=True, chunk_end=True))
+ p.send(SERVER, Data(data=b"12345", chunk_start=True, chunk_end=True))
+ p.send(SERVER, EndOfMessage())
+
+ for conn in p.conns:
+ assert conn.states == {CLIENT: DONE, SERVER: DONE}
+
+
+def test_chunk_boundaries() -> None:
+ conn = Connection(our_role=SERVER)
+
+ request = (
+ b"POST / HTTP/1.1\r\n"
+ b"Host: example.com\r\n"
+ b"Transfer-Encoding: chunked\r\n"
+ b"\r\n"
+ )
+ conn.receive_data(request)
+ assert conn.next_event() == Request(
+ method="POST",
+ target="/",
+ headers=[("Host", "example.com"), ("Transfer-Encoding", "chunked")],
+ )
+ assert conn.next_event() is NEED_DATA
+
+ conn.receive_data(b"5\r\nhello\r\n")
+ assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True)
+
+ conn.receive_data(b"5\r\nhel")
+ assert conn.next_event() == Data(data=b"hel", chunk_start=True, chunk_end=False)
+
+ conn.receive_data(b"l")
+ assert conn.next_event() == Data(data=b"l", chunk_start=False, chunk_end=False)
+
+ conn.receive_data(b"o\r\n")
+ assert conn.next_event() == Data(data=b"o", chunk_start=False, chunk_end=True)
+
+ conn.receive_data(b"5\r\nhello")
+ assert conn.next_event() == Data(data=b"hello", chunk_start=True, chunk_end=True)
+
+ conn.receive_data(b"\r\n")
+ assert conn.next_event() == NEED_DATA
+
+ conn.receive_data(b"0\r\n\r\n")
+ assert conn.next_event() == EndOfMessage()
+
+
+def test_client_talking_to_http10_server() -> None:
+ c = Connection(CLIENT)
+ c.send(Request(method="GET", target="/", headers=[("Host", "example.com")]))
+ c.send(EndOfMessage())
+ assert c.our_state is DONE
+ # No content-length, so Http10 framing for body
+ assert receive_and_get(c, b"HTTP/1.0 200 OK\r\n\r\n") == [
+ Response(status_code=200, headers=[], http_version="1.0", reason=b"OK") # type: ignore[arg-type]
+ ]
+ assert c.our_state is MUST_CLOSE
+ assert receive_and_get(c, b"12345") == [Data(data=b"12345")]
+ assert receive_and_get(c, b"67890") == [Data(data=b"67890")]
+ assert receive_and_get(c, b"") == [EndOfMessage(), ConnectionClosed()]
+ assert c.their_state is CLOSED
+
+
+def test_server_talking_to_http10_client() -> None:
+ c = Connection(SERVER)
+ # No content-length, so no body
+ # NB: no host header
+ assert receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n") == [
+ Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type]
+ EndOfMessage(),
+ ]
+ assert c.their_state is MUST_CLOSE
+
+ # We automatically Connection: close back at them
+ assert (
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n"
+ )
+
+ assert c.send(Data(data=b"12345")) == b"12345"
+ assert c.send(EndOfMessage()) == b""
+ assert c.our_state is MUST_CLOSE
+
+ # Check that it works if they do send Content-Length
+ c = Connection(SERVER)
+ # NB: no host header
+ assert receive_and_get(c, b"POST / HTTP/1.0\r\nContent-Length: 10\r\n\r\n1") == [
+ Request(
+ method="POST",
+ target="/",
+ headers=[("Content-Length", "10")],
+ http_version="1.0",
+ ),
+ Data(data=b"1"),
+ ]
+ assert receive_and_get(c, b"234567890") == [Data(data=b"234567890"), EndOfMessage()]
+ assert c.their_state is MUST_CLOSE
+ assert receive_and_get(c, b"") == [ConnectionClosed()]
+
+
+def test_automatic_transfer_encoding_in_response() -> None:
+ # Check that in responses, the user can specify either Transfer-Encoding:
+ # chunked or no framing at all, and in both cases we automatically select
+ # the right option depending on whether the peer speaks HTTP/1.0 or
+ # HTTP/1.1
+ for user_headers in [
+ [("Transfer-Encoding", "chunked")],
+ [],
+ # In fact, this even works if Content-Length is set,
+ # because if both are set then Transfer-Encoding wins
+ [("Transfer-Encoding", "chunked"), ("Content-Length", "100")],
+ ]:
+ user_headers = cast(List[Tuple[str, str]], user_headers)
+ p = ConnectionPair()
+ p.send(
+ CLIENT,
+ [
+ Request(method="GET", target="/", headers=[("Host", "example.com")]),
+ EndOfMessage(),
+ ],
+ )
+ # When speaking to HTTP/1.1 client, all of the above cases get
+ # normalized to Transfer-Encoding: chunked
+ p.send(
+ SERVER,
+ Response(status_code=200, headers=user_headers),
+ expect=Response(
+ status_code=200, headers=[("Transfer-Encoding", "chunked")]
+ ),
+ )
+
+ # When speaking to HTTP/1.0 client, all of the above cases get
+ # normalized to no-framing-headers
+ c = Connection(SERVER)
+ receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n")
+ assert (
+ c.send(Response(status_code=200, headers=user_headers))
+ == b"HTTP/1.1 200 \r\nConnection: close\r\n\r\n"
+ )
+ assert c.send(Data(data=b"12345")) == b"12345"
+
+
+def test_automagic_connection_close_handling() -> None:
+ p = ConnectionPair()
+ # If the user explicitly sets Connection: close, then we notice and
+ # respect it
+ p.send(
+ CLIENT,
+ [
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Connection", "close")],
+ ),
+ EndOfMessage(),
+ ],
+ )
+ for conn in p.conns:
+ assert conn.states[CLIENT] is MUST_CLOSE
+ # And if the client sets it, the server automatically echoes it back
+ p.send(
+ SERVER,
+ # no header here...
+ [Response(status_code=204, headers=[]), EndOfMessage()], # type: ignore[arg-type]
+ # ...but oh look, it arrived anyway
+ expect=[
+ Response(status_code=204, headers=[("connection", "close")]),
+ EndOfMessage(),
+ ],
+ )
+ for conn in p.conns:
+ assert conn.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE}
+
+
+def test_100_continue() -> None:
+ def setup() -> ConnectionPair:
+ p = ConnectionPair()
+ p.send(
+ CLIENT,
+ Request(
+ method="GET",
+ target="/",
+ headers=[
+ ("Host", "example.com"),
+ ("Content-Length", "100"),
+ ("Expect", "100-continue"),
+ ],
+ ),
+ )
+ for conn in p.conns:
+ assert conn.client_is_waiting_for_100_continue
+ assert not p.conn[CLIENT].they_are_waiting_for_100_continue
+ assert p.conn[SERVER].they_are_waiting_for_100_continue
+ return p
+
+ # Disabled by 100 Continue
+ p = setup()
+ p.send(SERVER, InformationalResponse(status_code=100, headers=[])) # type: ignore[arg-type]
+ for conn in p.conns:
+ assert not conn.client_is_waiting_for_100_continue
+ assert not conn.they_are_waiting_for_100_continue
+
+ # Disabled by a real response
+ p = setup()
+ p.send(
+ SERVER, Response(status_code=200, headers=[("Transfer-Encoding", "chunked")])
+ )
+ for conn in p.conns:
+ assert not conn.client_is_waiting_for_100_continue
+ assert not conn.they_are_waiting_for_100_continue
+
+ # Disabled by the client going ahead and sending stuff anyway
+ p = setup()
+ p.send(CLIENT, Data(data=b"12345"))
+ for conn in p.conns:
+ assert not conn.client_is_waiting_for_100_continue
+ assert not conn.they_are_waiting_for_100_continue
+
+
+def test_max_incomplete_event_size_countermeasure() -> None:
+ # Infinitely long headers are definitely not okay
+ c = Connection(SERVER)
+ c.receive_data(b"GET / HTTP/1.0\r\nEndless: ")
+ assert c.next_event() is NEED_DATA
+ with pytest.raises(RemoteProtocolError):
+ while True:
+ c.receive_data(b"a" * 1024)
+ c.next_event()
+
+ # Checking that the same header is accepted / rejected depending on the
+ # max_incomplete_event_size setting:
+ c = Connection(SERVER, max_incomplete_event_size=5000)
+ c.receive_data(b"GET / HTTP/1.0\r\nBig: ")
+ c.receive_data(b"a" * 4000)
+ c.receive_data(b"\r\n\r\n")
+ assert get_all_events(c) == [
+ Request(
+ method="GET", target="/", http_version="1.0", headers=[("big", "a" * 4000)]
+ ),
+ EndOfMessage(),
+ ]
+
+ c = Connection(SERVER, max_incomplete_event_size=4000)
+ c.receive_data(b"GET / HTTP/1.0\r\nBig: ")
+ c.receive_data(b"a" * 4000)
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+ # Temporarily exceeding the size limit is fine, as long as its done with
+ # complete events:
+ c = Connection(SERVER, max_incomplete_event_size=5000)
+ c.receive_data(b"GET / HTTP/1.0\r\nContent-Length: 10000")
+ c.receive_data(b"\r\n\r\n" + b"a" * 10000)
+ assert get_all_events(c) == [
+ Request(
+ method="GET",
+ target="/",
+ http_version="1.0",
+ headers=[("Content-Length", "10000")],
+ ),
+ Data(data=b"a" * 10000),
+ EndOfMessage(),
+ ]
+
+ c = Connection(SERVER, max_incomplete_event_size=100)
+ # Two pipelined requests to create a way-too-big receive buffer... but
+ # it's fine because we're not checking
+ c.receive_data(
+ b"GET /1 HTTP/1.1\r\nHost: a\r\n\r\n"
+ b"GET /2 HTTP/1.1\r\nHost: b\r\n\r\n" + b"X" * 1000
+ )
+ assert get_all_events(c) == [
+ Request(method="GET", target="/1", headers=[("host", "a")]),
+ EndOfMessage(),
+ ]
+ # Even more data comes in, still no problem
+ c.receive_data(b"X" * 1000)
+ # We can respond and reuse to get the second pipelined request
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ c.start_next_cycle()
+ assert get_all_events(c) == [
+ Request(method="GET", target="/2", headers=[("host", "b")]),
+ EndOfMessage(),
+ ]
+ # But once we unpause and try to read the next message, and find that it's
+ # incomplete and the buffer is *still* way too large, then *that's* a
+ # problem:
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ c.start_next_cycle()
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+def test_reuse_simple() -> None:
+ p = ConnectionPair()
+ p.send(
+ CLIENT,
+ [Request(method="GET", target="/", headers=[("Host", "a")]), EndOfMessage()],
+ )
+ p.send(
+ SERVER,
+ [
+ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]),
+ EndOfMessage(),
+ ],
+ )
+ for conn in p.conns:
+ assert conn.states == {CLIENT: DONE, SERVER: DONE}
+ conn.start_next_cycle()
+
+ p.send(
+ CLIENT,
+ [
+ Request(method="DELETE", target="/foo", headers=[("Host", "a")]),
+ EndOfMessage(),
+ ],
+ )
+ p.send(
+ SERVER,
+ [
+ Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]),
+ EndOfMessage(),
+ ],
+ )
+
+
+def test_pipelining() -> None:
+ # Client doesn't support pipelining, so we have to do this by hand
+ c = Connection(SERVER)
+ assert c.next_event() is NEED_DATA
+ # 3 requests all bunched up
+ c.receive_data(
+ b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n"
+ b"12345"
+ b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n"
+ b"67890"
+ b"GET /3 HTTP/1.1\r\nHost: a.com\r\n\r\n"
+ )
+ assert get_all_events(c) == [
+ Request(
+ method="GET",
+ target="/1",
+ headers=[("Host", "a.com"), ("Content-Length", "5")],
+ ),
+ Data(data=b"12345"),
+ EndOfMessage(),
+ ]
+ assert c.their_state is DONE
+ assert c.our_state is SEND_RESPONSE
+
+ assert c.next_event() is PAUSED
+
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ assert c.their_state is DONE
+ assert c.our_state is DONE
+
+ c.start_next_cycle()
+
+ assert get_all_events(c) == [
+ Request(
+ method="GET",
+ target="/2",
+ headers=[("Host", "a.com"), ("Content-Length", "5")],
+ ),
+ Data(data=b"67890"),
+ EndOfMessage(),
+ ]
+ assert c.next_event() is PAUSED
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ c.start_next_cycle()
+
+ assert get_all_events(c) == [
+ Request(method="GET", target="/3", headers=[("Host", "a.com")]),
+ EndOfMessage(),
+ ]
+ # Doesn't pause this time, no trailing data
+ assert c.next_event() is NEED_DATA
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+
+ # Arrival of more data triggers pause
+ assert c.next_event() is NEED_DATA
+ c.receive_data(b"SADF")
+ assert c.next_event() is PAUSED
+ assert c.trailing_data == (b"SADF", False)
+ # If EOF arrives while paused, we don't see that either:
+ c.receive_data(b"")
+ assert c.trailing_data == (b"SADF", True)
+ assert c.next_event() is PAUSED
+ c.receive_data(b"")
+ assert c.next_event() is PAUSED
+ # Can't call receive_data with non-empty buf after closing it
+ with pytest.raises(RuntimeError):
+ c.receive_data(b"FDSA")
+
+
+def test_protocol_switch() -> None:
+ for (req, deny, accept) in [
+ (
+ Request(
+ method="CONNECT",
+ target="example.com:443",
+ headers=[("Host", "foo"), ("Content-Length", "1")],
+ ),
+ Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]),
+ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]),
+ ),
+ (
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")],
+ ),
+ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]),
+ InformationalResponse(status_code=101, headers=[("Upgrade", "a")]),
+ ),
+ (
+ Request(
+ method="CONNECT",
+ target="example.com:443",
+ headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")],
+ ),
+ Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]),
+ # Accept CONNECT, not upgrade
+ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]),
+ ),
+ (
+ Request(
+ method="CONNECT",
+ target="example.com:443",
+ headers=[("Host", "foo"), ("Content-Length", "1"), ("Upgrade", "a, b")],
+ ),
+ Response(status_code=404, headers=[(b"transfer-encoding", b"chunked")]),
+ # Accept Upgrade, not CONNECT
+ InformationalResponse(status_code=101, headers=[("Upgrade", "b")]),
+ ),
+ ]:
+
+ def setup() -> ConnectionPair:
+ p = ConnectionPair()
+ p.send(CLIENT, req)
+ # No switch-related state change stuff yet; the client has to
+ # finish the request before that kicks in
+ for conn in p.conns:
+ assert conn.states[CLIENT] is SEND_BODY
+ p.send(CLIENT, [Data(data=b"1"), EndOfMessage()])
+ for conn in p.conns:
+ assert conn.states[CLIENT] is MIGHT_SWITCH_PROTOCOL
+ assert p.conn[SERVER].next_event() is PAUSED
+ return p
+
+ # Test deny case
+ p = setup()
+ p.send(SERVER, deny)
+ for conn in p.conns:
+ assert conn.states == {CLIENT: DONE, SERVER: SEND_BODY}
+ p.send(SERVER, EndOfMessage())
+ # Check that re-use is still allowed after a denial
+ for conn in p.conns:
+ conn.start_next_cycle()
+
+ # Test accept case
+ p = setup()
+ p.send(SERVER, accept)
+ for conn in p.conns:
+ assert conn.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
+ conn.receive_data(b"123")
+ assert conn.next_event() is PAUSED
+ conn.receive_data(b"456")
+ assert conn.next_event() is PAUSED
+ assert conn.trailing_data == (b"123456", False)
+
+ # Pausing in might-switch, then recovery
+ # (weird artificial case where the trailing data actually is valid
+ # HTTP for some reason, because this makes it easier to test the state
+ # logic)
+ p = setup()
+ sc = p.conn[SERVER]
+ sc.receive_data(b"GET / HTTP/1.0\r\n\r\n")
+ assert sc.next_event() is PAUSED
+ assert sc.trailing_data == (b"GET / HTTP/1.0\r\n\r\n", False)
+ sc.send(deny)
+ assert sc.next_event() is PAUSED
+ sc.send(EndOfMessage())
+ sc.start_next_cycle()
+ assert get_all_events(sc) == [
+ Request(method="GET", target="/", headers=[], http_version="1.0"), # type: ignore[arg-type]
+ EndOfMessage(),
+ ]
+
+ # When we're DONE, have no trailing data, and the connection gets
+ # closed, we report ConnectionClosed(). When we're in might-switch or
+ # switched, we don't.
+ p = setup()
+ sc = p.conn[SERVER]
+ sc.receive_data(b"")
+ assert sc.next_event() is PAUSED
+ assert sc.trailing_data == (b"", True)
+ p.send(SERVER, accept)
+ assert sc.next_event() is PAUSED
+
+ p = setup()
+ sc = p.conn[SERVER]
+ sc.receive_data(b"")
+ assert sc.next_event() is PAUSED
+ sc.send(deny)
+ assert sc.next_event() == ConnectionClosed()
+
+ # You can't send after switching protocols, or while waiting for a
+ # protocol switch
+ p = setup()
+ with pytest.raises(LocalProtocolError):
+ p.conn[CLIENT].send(
+ Request(method="GET", target="/", headers=[("Host", "a")])
+ )
+ p = setup()
+ p.send(SERVER, accept)
+ with pytest.raises(LocalProtocolError):
+ p.conn[SERVER].send(Data(data=b"123"))
+
+
+def test_close_simple() -> None:
+ # Just immediately closing a new connection without anything having
+ # happened yet.
+ for (who_shot_first, who_shot_second) in [(CLIENT, SERVER), (SERVER, CLIENT)]:
+
+ def setup() -> ConnectionPair:
+ p = ConnectionPair()
+ p.send(who_shot_first, ConnectionClosed())
+ for conn in p.conns:
+ assert conn.states == {
+ who_shot_first: CLOSED,
+ who_shot_second: MUST_CLOSE,
+ }
+ return p
+
+ # You can keep putting b"" into a closed connection, and you keep
+ # getting ConnectionClosed() out:
+ p = setup()
+ assert p.conn[who_shot_second].next_event() == ConnectionClosed()
+ assert p.conn[who_shot_second].next_event() == ConnectionClosed()
+ p.conn[who_shot_second].receive_data(b"")
+ assert p.conn[who_shot_second].next_event() == ConnectionClosed()
+ # Second party can close...
+ p = setup()
+ p.send(who_shot_second, ConnectionClosed())
+ for conn in p.conns:
+ assert conn.our_state is CLOSED
+ assert conn.their_state is CLOSED
+ # But trying to receive new data on a closed connection is a
+ # RuntimeError (not ProtocolError, because the problem here isn't
+ # violation of HTTP, it's violation of physics)
+ p = setup()
+ with pytest.raises(RuntimeError):
+ p.conn[who_shot_second].receive_data(b"123")
+ # And receiving new data on a MUST_CLOSE connection is a ProtocolError
+ p = setup()
+ p.conn[who_shot_first].receive_data(b"GET")
+ with pytest.raises(RemoteProtocolError):
+ p.conn[who_shot_first].next_event()
+
+
+def test_close_different_states() -> None:
+ req = [
+ Request(method="GET", target="/foo", headers=[("Host", "a")]),
+ EndOfMessage(),
+ ]
+ resp = [
+ Response(status_code=200, headers=[(b"transfer-encoding", b"chunked")]),
+ EndOfMessage(),
+ ]
+
+ # Client before request
+ p = ConnectionPair()
+ p.send(CLIENT, ConnectionClosed())
+ for conn in p.conns:
+ assert conn.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE}
+
+ # Client after request
+ p = ConnectionPair()
+ p.send(CLIENT, req)
+ p.send(CLIENT, ConnectionClosed())
+ for conn in p.conns:
+ assert conn.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE}
+
+ # Server after request -> not allowed
+ p = ConnectionPair()
+ p.send(CLIENT, req)
+ with pytest.raises(LocalProtocolError):
+ p.conn[SERVER].send(ConnectionClosed())
+ p.conn[CLIENT].receive_data(b"")
+ with pytest.raises(RemoteProtocolError):
+ p.conn[CLIENT].next_event()
+
+ # Server after response
+ p = ConnectionPair()
+ p.send(CLIENT, req)
+ p.send(SERVER, resp)
+ p.send(SERVER, ConnectionClosed())
+ for conn in p.conns:
+ assert conn.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED}
+
+ # Both after closing (ConnectionClosed() is idempotent)
+ p = ConnectionPair()
+ p.send(CLIENT, req)
+ p.send(SERVER, resp)
+ p.send(CLIENT, ConnectionClosed())
+ p.send(SERVER, ConnectionClosed())
+ p.send(CLIENT, ConnectionClosed())
+ p.send(SERVER, ConnectionClosed())
+
+ # In the middle of sending -> not allowed
+ p = ConnectionPair()
+ p.send(
+ CLIENT,
+ Request(
+ method="GET", target="/", headers=[("Host", "a"), ("Content-Length", "10")]
+ ),
+ )
+ with pytest.raises(LocalProtocolError):
+ p.conn[CLIENT].send(ConnectionClosed())
+ p.conn[SERVER].receive_data(b"")
+ with pytest.raises(RemoteProtocolError):
+ p.conn[SERVER].next_event()
+
+
+# Receive several requests and then client shuts down their side of the
+# connection; we can respond to each
+def test_pipelined_close() -> None:
+ c = Connection(SERVER)
+ # 2 requests then a close
+ c.receive_data(
+ b"GET /1 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n"
+ b"12345"
+ b"GET /2 HTTP/1.1\r\nHost: a.com\r\nContent-Length: 5\r\n\r\n"
+ b"67890"
+ )
+ c.receive_data(b"")
+ assert get_all_events(c) == [
+ Request(
+ method="GET",
+ target="/1",
+ headers=[("host", "a.com"), ("content-length", "5")],
+ ),
+ Data(data=b"12345"),
+ EndOfMessage(),
+ ]
+ assert c.states[CLIENT] is DONE
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ assert c.states[SERVER] is DONE
+ c.start_next_cycle()
+ assert get_all_events(c) == [
+ Request(
+ method="GET",
+ target="/2",
+ headers=[("host", "a.com"), ("content-length", "5")],
+ ),
+ Data(data=b"67890"),
+ EndOfMessage(),
+ ConnectionClosed(),
+ ]
+ assert c.states == {CLIENT: CLOSED, SERVER: SEND_RESPONSE}
+ c.send(Response(status_code=200, headers=[])) # type: ignore[arg-type]
+ c.send(EndOfMessage())
+ assert c.states == {CLIENT: CLOSED, SERVER: MUST_CLOSE}
+ c.send(ConnectionClosed())
+ assert c.states == {CLIENT: CLOSED, SERVER: CLOSED}
+
+
+def test_sendfile() -> None:
+ class SendfilePlaceholder:
+ def __len__(self) -> int:
+ return 10
+
+ placeholder = SendfilePlaceholder()
+
+ def setup(
+ header: Tuple[str, str], http_version: str
+ ) -> Tuple[Connection, Optional[List[bytes]]]:
+ c = Connection(SERVER)
+ receive_and_get(
+ c, "GET / HTTP/{}\r\nHost: a\r\n\r\n".format(http_version).encode("ascii")
+ )
+ headers = []
+ if header:
+ headers.append(header)
+ c.send(Response(status_code=200, headers=headers))
+ return c, c.send_with_data_passthrough(Data(data=placeholder)) # type: ignore
+
+ c, data = setup(("Content-Length", "10"), "1.1")
+ assert data == [placeholder] # type: ignore
+ # Raises an error if the connection object doesn't think we've sent
+ # exactly 10 bytes
+ c.send(EndOfMessage())
+
+ _, data = setup(("Transfer-Encoding", "chunked"), "1.1")
+ assert placeholder in data # type: ignore
+ data[data.index(placeholder)] = b"x" * 10 # type: ignore
+ assert b"".join(data) == b"a\r\nxxxxxxxxxx\r\n" # type: ignore
+
+ c, data = setup(None, "1.0") # type: ignore
+ assert data == [placeholder] # type: ignore
+ assert c.our_state is SEND_BODY
+
+
+def test_errors() -> None:
+ # After a receive error, you can't receive
+ for role in [CLIENT, SERVER]:
+ c = Connection(our_role=role)
+ c.receive_data(b"gibberish\r\n\r\n")
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+ # Now any attempt to receive continues to raise
+ assert c.their_state is ERROR
+ assert c.our_state is not ERROR
+ print(c._cstate.states)
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+ # But we can still yell at the client for sending us gibberish
+ if role is SERVER:
+ assert (
+ c.send(Response(status_code=400, headers=[])) # type: ignore[arg-type]
+ == b"HTTP/1.1 400 \r\nConnection: close\r\n\r\n"
+ )
+
+ # After an error sending, you can no longer send
+ # (This is especially important for things like content-length errors,
+ # where there's complex internal state being modified)
+ def conn(role: Type[Sentinel]) -> Connection:
+ c = Connection(our_role=role)
+ if role is SERVER:
+ # Put it into the state where it *could* send a response...
+ receive_and_get(c, b"GET / HTTP/1.0\r\n\r\n")
+ assert c.our_state is SEND_RESPONSE
+ return c
+
+ for role in [CLIENT, SERVER]:
+ if role is CLIENT:
+ # This HTTP/1.0 request won't be detected as bad until after we go
+ # through the state machine and hit the writing code
+ good = Request(method="GET", target="/", headers=[("Host", "example.com")])
+ bad = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com")],
+ http_version="1.0",
+ )
+ elif role is SERVER:
+ good = Response(status_code=200, headers=[]) # type: ignore[arg-type,assignment]
+ bad = Response(status_code=200, headers=[], http_version="1.0") # type: ignore[arg-type,assignment]
+ # Make sure 'good' actually is good
+ c = conn(role)
+ c.send(good)
+ assert c.our_state is not ERROR
+ # Do that again, but this time sending 'bad' first
+ c = conn(role)
+ with pytest.raises(LocalProtocolError):
+ c.send(bad)
+ assert c.our_state is ERROR
+ assert c.their_state is not ERROR
+ # Now 'good' is not so good
+ with pytest.raises(LocalProtocolError):
+ c.send(good)
+
+ # And check send_failed() too
+ c = conn(role)
+ c.send_failed()
+ assert c.our_state is ERROR
+ assert c.their_state is not ERROR
+ # This is idempotent
+ c.send_failed()
+ assert c.our_state is ERROR
+ assert c.their_state is not ERROR
+
+
+def test_idle_receive_nothing() -> None:
+ # At one point this incorrectly raised an error
+ for role in [CLIENT, SERVER]:
+ c = Connection(role)
+ assert c.next_event() is NEED_DATA
+
+
+def test_connection_drop() -> None:
+ c = Connection(SERVER)
+ c.receive_data(b"GET /")
+ assert c.next_event() is NEED_DATA
+ c.receive_data(b"")
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+def test_408_request_timeout() -> None:
+ # Should be able to send this spontaneously as a server without seeing
+ # anything from client
+ p = ConnectionPair()
+ p.send(SERVER, Response(status_code=408, headers=[(b"connection", b"close")]))
+
+
+# This used to raise IndexError
+def test_empty_request() -> None:
+ c = Connection(SERVER)
+ c.receive_data(b"\r\n")
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+# This used to raise IndexError
+def test_empty_response() -> None:
+ c = Connection(CLIENT)
+ c.send(Request(method="GET", target="/", headers=[("Host", "a")]))
+ c.receive_data(b"\r\n")
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ b"\x00",
+ b"\x20",
+ b"\x16\x03\x01\x00\xa5", # Typical start of a TLS Client Hello
+ ],
+)
+def test_early_detection_of_invalid_request(data: bytes) -> None:
+ c = Connection(SERVER)
+ # Early detection should occur before even receiving a `\r\n`
+ c.receive_data(data)
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ b"\x00",
+ b"\x20",
+ b"\x16\x03\x03\x00\x31", # Typical start of a TLS Server Hello
+ ],
+)
+def test_early_detection_of_invalid_response(data: bytes) -> None:
+ c = Connection(CLIENT)
+ # Early detection should occur before even receiving a `\r\n`
+ c.receive_data(data)
+ with pytest.raises(RemoteProtocolError):
+ c.next_event()
+
+
+# This used to give different headers for HEAD and GET.
+# The correct way to handle HEAD is to put whatever headers we *would* have
+# put if it were a GET -- even though we know that for HEAD, those headers
+# will be ignored.
+def test_HEAD_framing_headers() -> None:
+ def setup(method: bytes, http_version: bytes) -> Connection:
+ c = Connection(SERVER)
+ c.receive_data(
+ method + b" / HTTP/" + http_version + b"\r\n" + b"Host: example.com\r\n\r\n"
+ )
+ assert type(c.next_event()) is Request
+ assert type(c.next_event()) is EndOfMessage
+ return c
+
+ for method in [b"GET", b"HEAD"]:
+ # No Content-Length, HTTP/1.1 peer, should use chunked
+ c = setup(method, b"1.1")
+ assert (
+ c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type]
+ b"Transfer-Encoding: chunked\r\n\r\n"
+ )
+
+ # No Content-Length, HTTP/1.0 peer, frame with connection: close
+ c = setup(method, b"1.0")
+ assert (
+ c.send(Response(status_code=200, headers=[])) == b"HTTP/1.1 200 \r\n" # type: ignore[arg-type]
+ b"Connection: close\r\n\r\n"
+ )
+
+ # Content-Length + Transfer-Encoding, TE wins
+ c = setup(method, b"1.1")
+ assert (
+ c.send(
+ Response(
+ status_code=200,
+ headers=[
+ ("Content-Length", "100"),
+ ("Transfer-Encoding", "chunked"),
+ ],
+ )
+ )
+ == b"HTTP/1.1 200 \r\n"
+ b"Transfer-Encoding: chunked\r\n\r\n"
+ )
+
+
+def test_special_exceptions_for_lost_connection_in_message_body() -> None:
+ c = Connection(SERVER)
+ c.receive_data(
+ b"POST / HTTP/1.1\r\n" b"Host: example.com\r\n" b"Content-Length: 100\r\n\r\n"
+ )
+ assert type(c.next_event()) is Request
+ assert c.next_event() is NEED_DATA
+ c.receive_data(b"12345")
+ assert c.next_event() == Data(data=b"12345")
+ c.receive_data(b"")
+ with pytest.raises(RemoteProtocolError) as excinfo:
+ c.next_event()
+ assert "received 5 bytes" in str(excinfo.value)
+ assert "expected 100" in str(excinfo.value)
+
+ c = Connection(SERVER)
+ c.receive_data(
+ b"POST / HTTP/1.1\r\n"
+ b"Host: example.com\r\n"
+ b"Transfer-Encoding: chunked\r\n\r\n"
+ )
+ assert type(c.next_event()) is Request
+ assert c.next_event() is NEED_DATA
+ c.receive_data(b"8\r\n012345")
+ assert c.next_event().data == b"012345" # type: ignore
+ c.receive_data(b"")
+ with pytest.raises(RemoteProtocolError) as excinfo:
+ c.next_event()
+ assert "incomplete chunked read" in str(excinfo.value)
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_events.py b/venv/lib/python3.11/site-packages/h11/tests/test_events.py
new file mode 100644
index 0000000..bc6c313
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_events.py
@@ -0,0 +1,150 @@
+from http import HTTPStatus
+
+import pytest
+
+from .. import _events
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .._util import LocalProtocolError
+
+
+def test_events() -> None:
+ with pytest.raises(LocalProtocolError):
+ # Missing Host:
+ req = Request(
+ method="GET", target="/", headers=[("a", "b")], http_version="1.1"
+ )
+ # But this is okay (HTTP/1.0)
+ req = Request(method="GET", target="/", headers=[("a", "b")], http_version="1.0")
+ # fields are normalized
+ assert req.method == b"GET"
+ assert req.target == b"/"
+ assert req.headers == [(b"a", b"b")]
+ assert req.http_version == b"1.0"
+
+ # This is also okay -- has a Host (with weird capitalization, which is ok)
+ req = Request(
+ method="GET",
+ target="/",
+ headers=[("a", "b"), ("hOSt", "example.com")],
+ http_version="1.1",
+ )
+ # we normalize header capitalization
+ assert req.headers == [(b"a", b"b"), (b"host", b"example.com")]
+
+ # Multiple host is bad too
+ with pytest.raises(LocalProtocolError):
+ req = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "a"), ("Host", "a")],
+ http_version="1.1",
+ )
+ # Even for HTTP/1.0
+ with pytest.raises(LocalProtocolError):
+ req = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "a"), ("Host", "a")],
+ http_version="1.0",
+ )
+
+ # Header values are validated
+ for bad_char in "\x00\r\n\f\v":
+ with pytest.raises(LocalProtocolError):
+ req = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "a"), ("Foo", "asd" + bad_char)],
+ http_version="1.0",
+ )
+
+ # But for compatibility we allow non-whitespace control characters, even
+ # though they're forbidden by the spec.
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "a"), ("Foo", "asd\x01\x02\x7f")],
+ http_version="1.0",
+ )
+
+ # Request target is validated
+ for bad_byte in b"\x00\x20\x7f\xee":
+ target = bytearray(b"/")
+ target.append(bad_byte)
+ with pytest.raises(LocalProtocolError):
+ Request(
+ method="GET", target=target, headers=[("Host", "a")], http_version="1.1"
+ )
+
+ # Request method is validated
+ with pytest.raises(LocalProtocolError):
+ Request(
+ method="GET / HTTP/1.1",
+ target=target,
+ headers=[("Host", "a")],
+ http_version="1.1",
+ )
+
+ ir = InformationalResponse(status_code=100, headers=[("Host", "a")])
+ assert ir.status_code == 100
+ assert ir.headers == [(b"host", b"a")]
+ assert ir.http_version == b"1.1"
+
+ with pytest.raises(LocalProtocolError):
+ InformationalResponse(status_code=200, headers=[("Host", "a")])
+
+ resp = Response(status_code=204, headers=[], http_version="1.0") # type: ignore[arg-type]
+ assert resp.status_code == 204
+ assert resp.headers == []
+ assert resp.http_version == b"1.0"
+
+ with pytest.raises(LocalProtocolError):
+ resp = Response(status_code=100, headers=[], http_version="1.0") # type: ignore[arg-type]
+
+ with pytest.raises(LocalProtocolError):
+ Response(status_code="100", headers=[], http_version="1.0") # type: ignore[arg-type]
+
+ with pytest.raises(LocalProtocolError):
+ InformationalResponse(status_code=b"100", headers=[], http_version="1.0") # type: ignore[arg-type]
+
+ d = Data(data=b"asdf")
+ assert d.data == b"asdf"
+
+ eom = EndOfMessage()
+ assert eom.headers == []
+
+ cc = ConnectionClosed()
+ assert repr(cc) == "ConnectionClosed()"
+
+
+def test_intenum_status_code() -> None:
+ # https://github.com/python-hyper/h11/issues/72
+
+ r = Response(status_code=HTTPStatus.OK, headers=[], http_version="1.0") # type: ignore[arg-type]
+ assert r.status_code == HTTPStatus.OK
+ assert type(r.status_code) is not type(HTTPStatus.OK)
+ assert type(r.status_code) is int
+
+
+def test_header_casing() -> None:
+ r = Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.org"), ("Connection", "keep-alive")],
+ http_version="1.1",
+ )
+ assert len(r.headers) == 2
+ assert r.headers[0] == (b"host", b"example.org")
+ assert r.headers == [(b"host", b"example.org"), (b"connection", b"keep-alive")]
+ assert r.headers.raw_items() == [
+ (b"Host", b"example.org"),
+ (b"Connection", b"keep-alive"),
+ ]
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_headers.py b/venv/lib/python3.11/site-packages/h11/tests/test_headers.py
new file mode 100644
index 0000000..ba53d08
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_headers.py
@@ -0,0 +1,157 @@
+import pytest
+
+from .._events import Request
+from .._headers import (
+ get_comma_header,
+ has_expect_100_continue,
+ Headers,
+ normalize_and_validate,
+ set_comma_header,
+)
+from .._util import LocalProtocolError
+
+
+def test_normalize_and_validate() -> None:
+ assert normalize_and_validate([("foo", "bar")]) == [(b"foo", b"bar")]
+ assert normalize_and_validate([(b"foo", b"bar")]) == [(b"foo", b"bar")]
+
+ # no leading/trailing whitespace in names
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([(b"foo ", "bar")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([(b" foo", "bar")])
+
+ # no weird characters in names
+ with pytest.raises(LocalProtocolError) as excinfo:
+ normalize_and_validate([(b"foo bar", b"baz")])
+ assert "foo bar" in str(excinfo.value)
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([(b"foo\x00bar", b"baz")])
+ # Not even 8-bit characters:
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([(b"foo\xffbar", b"baz")])
+ # And not even the control characters we allow in values:
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([(b"foo\x01bar", b"baz")])
+
+ # no return or NUL characters in values
+ with pytest.raises(LocalProtocolError) as excinfo:
+ normalize_and_validate([("foo", "bar\rbaz")])
+ assert "bar\\rbaz" in str(excinfo.value)
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", "bar\nbaz")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", "bar\x00baz")])
+ # no leading/trailing whitespace
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", "barbaz ")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", " barbaz")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", "barbaz\t")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("foo", "\tbarbaz")])
+
+ # content-length
+ assert normalize_and_validate([("Content-Length", "1")]) == [
+ (b"content-length", b"1")
+ ]
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("Content-Length", "asdf")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("Content-Length", "1x")])
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("Content-Length", "1"), ("Content-Length", "2")])
+ assert normalize_and_validate(
+ [("Content-Length", "0"), ("Content-Length", "0")]
+ ) == [(b"content-length", b"0")]
+ assert normalize_and_validate([("Content-Length", "0 , 0")]) == [
+ (b"content-length", b"0")
+ ]
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate(
+ [("Content-Length", "1"), ("Content-Length", "1"), ("Content-Length", "2")]
+ )
+ with pytest.raises(LocalProtocolError):
+ normalize_and_validate([("Content-Length", "1 , 1,2")])
+
+ # transfer-encoding
+ assert normalize_and_validate([("Transfer-Encoding", "chunked")]) == [
+ (b"transfer-encoding", b"chunked")
+ ]
+ assert normalize_and_validate([("Transfer-Encoding", "cHuNkEd")]) == [
+ (b"transfer-encoding", b"chunked")
+ ]
+ with pytest.raises(LocalProtocolError) as excinfo:
+ normalize_and_validate([("Transfer-Encoding", "gzip")])
+ assert excinfo.value.error_status_hint == 501 # Not Implemented
+ with pytest.raises(LocalProtocolError) as excinfo:
+ normalize_and_validate(
+ [("Transfer-Encoding", "chunked"), ("Transfer-Encoding", "gzip")]
+ )
+ assert excinfo.value.error_status_hint == 501 # Not Implemented
+
+
+def test_get_set_comma_header() -> None:
+ headers = normalize_and_validate(
+ [
+ ("Connection", "close"),
+ ("whatever", "something"),
+ ("connectiON", "fOo,, , BAR"),
+ ]
+ )
+
+ assert get_comma_header(headers, b"connection") == [b"close", b"foo", b"bar"]
+
+ headers = set_comma_header(headers, b"newthing", ["a", "b"]) # type: ignore
+
+ with pytest.raises(LocalProtocolError):
+ set_comma_header(headers, b"newthing", [" a", "b"]) # type: ignore
+
+ assert headers == [
+ (b"connection", b"close"),
+ (b"whatever", b"something"),
+ (b"connection", b"fOo,, , BAR"),
+ (b"newthing", b"a"),
+ (b"newthing", b"b"),
+ ]
+
+ headers = set_comma_header(headers, b"whatever", ["different thing"]) # type: ignore
+
+ assert headers == [
+ (b"connection", b"close"),
+ (b"connection", b"fOo,, , BAR"),
+ (b"newthing", b"a"),
+ (b"newthing", b"b"),
+ (b"whatever", b"different thing"),
+ ]
+
+
+def test_has_100_continue() -> None:
+ assert has_expect_100_continue(
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Expect", "100-continue")],
+ )
+ )
+ assert not has_expect_100_continue(
+ Request(method="GET", target="/", headers=[("Host", "example.com")])
+ )
+ # Case insensitive
+ assert has_expect_100_continue(
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Expect", "100-Continue")],
+ )
+ )
+ # Doesn't work in HTTP/1.0
+ assert not has_expect_100_continue(
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "example.com"), ("Expect", "100-continue")],
+ http_version="1.0",
+ )
+ )
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_helpers.py b/venv/lib/python3.11/site-packages/h11/tests/test_helpers.py
new file mode 100644
index 0000000..c329c76
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_helpers.py
@@ -0,0 +1,32 @@
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .helpers import normalize_data_events
+
+
+def test_normalize_data_events() -> None:
+ assert normalize_data_events(
+ [
+ Data(data=bytearray(b"1")),
+ Data(data=b"2"),
+ Response(status_code=200, headers=[]), # type: ignore[arg-type]
+ Data(data=b"3"),
+ Data(data=b"4"),
+ EndOfMessage(),
+ Data(data=b"5"),
+ Data(data=b"6"),
+ Data(data=b"7"),
+ ]
+ ) == [
+ Data(data=b"12"),
+ Response(status_code=200, headers=[]), # type: ignore[arg-type]
+ Data(data=b"34"),
+ EndOfMessage(),
+ Data(data=b"567"),
+ ]
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_io.py b/venv/lib/python3.11/site-packages/h11/tests/test_io.py
new file mode 100644
index 0000000..2b47c0e
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_io.py
@@ -0,0 +1,572 @@
+from typing import Any, Callable, Generator, List
+
+import pytest
+
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .._headers import Headers, normalize_and_validate
+from .._readers import (
+ _obsolete_line_fold,
+ ChunkedReader,
+ ContentLengthReader,
+ Http10Reader,
+ READERS,
+)
+from .._receivebuffer import ReceiveBuffer
+from .._state import (
+ CLIENT,
+ CLOSED,
+ DONE,
+ IDLE,
+ MIGHT_SWITCH_PROTOCOL,
+ MUST_CLOSE,
+ SEND_BODY,
+ SEND_RESPONSE,
+ SERVER,
+ SWITCHED_PROTOCOL,
+)
+from .._util import LocalProtocolError
+from .._writers import (
+ ChunkedWriter,
+ ContentLengthWriter,
+ Http10Writer,
+ write_any_response,
+ write_headers,
+ write_request,
+ WRITERS,
+)
+from .helpers import normalize_data_events
+
+SIMPLE_CASES = [
+ (
+ (CLIENT, IDLE),
+ Request(
+ method="GET",
+ target="/a",
+ headers=[("Host", "foo"), ("Connection", "close")],
+ ),
+ b"GET /a HTTP/1.1\r\nHost: foo\r\nConnection: close\r\n\r\n",
+ ),
+ (
+ (SERVER, SEND_RESPONSE),
+ Response(status_code=200, headers=[("Connection", "close")], reason=b"OK"),
+ b"HTTP/1.1 200 OK\r\nConnection: close\r\n\r\n",
+ ),
+ (
+ (SERVER, SEND_RESPONSE),
+ Response(status_code=200, headers=[], reason=b"OK"), # type: ignore[arg-type]
+ b"HTTP/1.1 200 OK\r\n\r\n",
+ ),
+ (
+ (SERVER, SEND_RESPONSE),
+ InformationalResponse(
+ status_code=101, headers=[("Upgrade", "websocket")], reason=b"Upgrade"
+ ),
+ b"HTTP/1.1 101 Upgrade\r\nUpgrade: websocket\r\n\r\n",
+ ),
+ (
+ (SERVER, SEND_RESPONSE),
+ InformationalResponse(status_code=101, headers=[], reason=b"Upgrade"), # type: ignore[arg-type]
+ b"HTTP/1.1 101 Upgrade\r\n\r\n",
+ ),
+]
+
+
+def dowrite(writer: Callable[..., None], obj: Any) -> bytes:
+ got_list: List[bytes] = []
+ writer(obj, got_list.append)
+ return b"".join(got_list)
+
+
+def tw(writer: Any, obj: Any, expected: Any) -> None:
+ got = dowrite(writer, obj)
+ assert got == expected
+
+
+def makebuf(data: bytes) -> ReceiveBuffer:
+ buf = ReceiveBuffer()
+ buf += data
+ return buf
+
+
+def tr(reader: Any, data: bytes, expected: Any) -> None:
+ def check(got: Any) -> None:
+ assert got == expected
+ # Headers should always be returned as bytes, not e.g. bytearray
+ # https://github.com/python-hyper/wsproto/pull/54#issuecomment-377709478
+ for name, value in getattr(got, "headers", []):
+ assert type(name) is bytes
+ assert type(value) is bytes
+
+ # Simple: consume whole thing
+ buf = makebuf(data)
+ check(reader(buf))
+ assert not buf
+
+ # Incrementally growing buffer
+ buf = ReceiveBuffer()
+ for i in range(len(data)):
+ assert reader(buf) is None
+ buf += data[i : i + 1]
+ check(reader(buf))
+
+ # Trailing data
+ buf = makebuf(data)
+ buf += b"trailing"
+ check(reader(buf))
+ assert bytes(buf) == b"trailing"
+
+
+def test_writers_simple() -> None:
+ for ((role, state), event, binary) in SIMPLE_CASES:
+ tw(WRITERS[role, state], event, binary)
+
+
+def test_readers_simple() -> None:
+ for ((role, state), event, binary) in SIMPLE_CASES:
+ tr(READERS[role, state], binary, event)
+
+
+def test_writers_unusual() -> None:
+ # Simple test of the write_headers utility routine
+ tw(
+ write_headers,
+ normalize_and_validate([("foo", "bar"), ("baz", "quux")]),
+ b"foo: bar\r\nbaz: quux\r\n\r\n",
+ )
+ tw(write_headers, Headers([]), b"\r\n")
+
+ # We understand HTTP/1.0, but we don't speak it
+ with pytest.raises(LocalProtocolError):
+ tw(
+ write_request,
+ Request(
+ method="GET",
+ target="/",
+ headers=[("Host", "foo"), ("Connection", "close")],
+ http_version="1.0",
+ ),
+ None,
+ )
+ with pytest.raises(LocalProtocolError):
+ tw(
+ write_any_response,
+ Response(
+ status_code=200, headers=[("Connection", "close")], http_version="1.0"
+ ),
+ None,
+ )
+
+
+def test_readers_unusual() -> None:
+ # Reading HTTP/1.0
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.0\r\nSome: header\r\n\r\n",
+ Request(
+ method="HEAD",
+ target="/foo",
+ headers=[("Some", "header")],
+ http_version="1.0",
+ ),
+ )
+
+ # check no-headers, since it's only legal with HTTP/1.0
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.0\r\n\r\n",
+ Request(method="HEAD", target="/foo", headers=[], http_version="1.0"), # type: ignore[arg-type]
+ )
+
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.0 200 OK\r\nSome: header\r\n\r\n",
+ Response(
+ status_code=200,
+ headers=[("Some", "header")],
+ http_version="1.0",
+ reason=b"OK",
+ ),
+ )
+
+ # single-character header values (actually disallowed by the ABNF in RFC
+ # 7230 -- this is a bug in the standard that we originally copied...)
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.0 200 OK\r\n" b"Foo: a a a a a \r\n\r\n",
+ Response(
+ status_code=200,
+ headers=[("Foo", "a a a a a")],
+ http_version="1.0",
+ reason=b"OK",
+ ),
+ )
+
+ # Empty headers -- also legal
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.0 200 OK\r\n" b"Foo:\r\n\r\n",
+ Response(
+ status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
+ ),
+ )
+
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.0 200 OK\r\n" b"Foo: \t \t \r\n\r\n",
+ Response(
+ status_code=200, headers=[("Foo", "")], http_version="1.0", reason=b"OK"
+ ),
+ )
+
+ # Tolerate broken servers that leave off the response code
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.0 200\r\n" b"Foo: bar\r\n\r\n",
+ Response(
+ status_code=200, headers=[("Foo", "bar")], http_version="1.0", reason=b""
+ ),
+ )
+
+ # Tolerate headers line endings (\r\n and \n)
+ # \n\r\b between headers and body
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.1 200 OK\r\nSomeHeader: val\n\r\n",
+ Response(
+ status_code=200,
+ headers=[("SomeHeader", "val")],
+ http_version="1.1",
+ reason="OK",
+ ),
+ )
+
+ # delimited only with \n
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.1 200 OK\nSomeHeader1: val1\nSomeHeader2: val2\n\n",
+ Response(
+ status_code=200,
+ headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
+ http_version="1.1",
+ reason="OK",
+ ),
+ )
+
+ # mixed \r\n and \n
+ tr(
+ READERS[SERVER, SEND_RESPONSE],
+ b"HTTP/1.1 200 OK\r\nSomeHeader1: val1\nSomeHeader2: val2\n\r\n",
+ Response(
+ status_code=200,
+ headers=[("SomeHeader1", "val1"), ("SomeHeader2", "val2")],
+ http_version="1.1",
+ reason="OK",
+ ),
+ )
+
+ # obsolete line folding
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n"
+ b"Host: example.com\r\n"
+ b"Some: multi-line\r\n"
+ b" header\r\n"
+ b"\tnonsense\r\n"
+ b" \t \t\tI guess\r\n"
+ b"Connection: close\r\n"
+ b"More-nonsense: in the\r\n"
+ b" last header \r\n\r\n",
+ Request(
+ method="HEAD",
+ target="/foo",
+ headers=[
+ ("Host", "example.com"),
+ ("Some", "multi-line header nonsense I guess"),
+ ("Connection", "close"),
+ ("More-nonsense", "in the last header"),
+ ],
+ ),
+ )
+
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n" b" folded: line\r\n\r\n",
+ None,
+ )
+
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n" b"foo : line\r\n\r\n",
+ None,
+ )
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
+ None,
+ )
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n" b"foo\t: line\r\n\r\n",
+ None,
+ )
+ with pytest.raises(LocalProtocolError):
+ tr(READERS[CLIENT, IDLE], b"HEAD /foo HTTP/1.1\r\n" b": line\r\n\r\n", None)
+
+
+def test__obsolete_line_fold_bytes() -> None:
+ # _obsolete_line_fold has a defensive cast to bytearray, which is
+ # necessary to protect against O(n^2) behavior in case anyone ever passes
+ # in regular bytestrings... but right now we never pass in regular
+ # bytestrings. so this test just exists to get some coverage on that
+ # defensive cast.
+ assert list(_obsolete_line_fold([b"aaa", b"bbb", b" ccc", b"ddd"])) == [
+ b"aaa",
+ bytearray(b"bbb ccc"),
+ b"ddd",
+ ]
+
+
+def _run_reader_iter(
+ reader: Any, buf: bytes, do_eof: bool
+) -> Generator[Any, None, None]:
+ while True:
+ event = reader(buf)
+ if event is None:
+ break
+ yield event
+ # body readers have undefined behavior after returning EndOfMessage,
+ # because this changes the state so they don't get called again
+ if type(event) is EndOfMessage:
+ break
+ if do_eof:
+ assert not buf
+ yield reader.read_eof()
+
+
+def _run_reader(*args: Any) -> List[Event]:
+ events = list(_run_reader_iter(*args))
+ return normalize_data_events(events)
+
+
+def t_body_reader(thunk: Any, data: bytes, expected: Any, do_eof: bool = False) -> None:
+ # Simple: consume whole thing
+ print("Test 1")
+ buf = makebuf(data)
+ assert _run_reader(thunk(), buf, do_eof) == expected
+
+ # Incrementally growing buffer
+ print("Test 2")
+ reader = thunk()
+ buf = ReceiveBuffer()
+ events = []
+ for i in range(len(data)):
+ events += _run_reader(reader, buf, False)
+ buf += data[i : i + 1]
+ events += _run_reader(reader, buf, do_eof)
+ assert normalize_data_events(events) == expected
+
+ is_complete = any(type(event) is EndOfMessage for event in expected)
+ if is_complete and not do_eof:
+ buf = makebuf(data + b"trailing")
+ assert _run_reader(thunk(), buf, False) == expected
+
+
+def test_ContentLengthReader() -> None:
+ t_body_reader(lambda: ContentLengthReader(0), b"", [EndOfMessage()])
+
+ t_body_reader(
+ lambda: ContentLengthReader(10),
+ b"0123456789",
+ [Data(data=b"0123456789"), EndOfMessage()],
+ )
+
+
+def test_Http10Reader() -> None:
+ t_body_reader(Http10Reader, b"", [EndOfMessage()], do_eof=True)
+ t_body_reader(Http10Reader, b"asdf", [Data(data=b"asdf")], do_eof=False)
+ t_body_reader(
+ Http10Reader, b"asdf", [Data(data=b"asdf"), EndOfMessage()], do_eof=True
+ )
+
+
+def test_ChunkedReader() -> None:
+ t_body_reader(ChunkedReader, b"0\r\n\r\n", [EndOfMessage()])
+
+ t_body_reader(
+ ChunkedReader,
+ b"0\r\nSome: header\r\n\r\n",
+ [EndOfMessage(headers=[("Some", "header")])],
+ )
+
+ t_body_reader(
+ ChunkedReader,
+ b"5\r\n01234\r\n"
+ + b"10\r\n0123456789abcdef\r\n"
+ + b"0\r\n"
+ + b"Some: header\r\n\r\n",
+ [
+ Data(data=b"012340123456789abcdef"),
+ EndOfMessage(headers=[("Some", "header")]),
+ ],
+ )
+
+ t_body_reader(
+ ChunkedReader,
+ b"5\r\n01234\r\n" + b"10\r\n0123456789abcdef\r\n" + b"0\r\n\r\n",
+ [Data(data=b"012340123456789abcdef"), EndOfMessage()],
+ )
+
+ # handles upper and lowercase hex
+ t_body_reader(
+ ChunkedReader,
+ b"aA\r\n" + b"x" * 0xAA + b"\r\n" + b"0\r\n\r\n",
+ [Data(data=b"x" * 0xAA), EndOfMessage()],
+ )
+
+ # refuses arbitrarily long chunk integers
+ with pytest.raises(LocalProtocolError):
+ # Technically this is legal HTTP/1.1, but we refuse to process chunk
+ # sizes that don't fit into 20 characters of hex
+ t_body_reader(ChunkedReader, b"9" * 100 + b"\r\nxxx", [Data(data=b"xxx")])
+
+ # refuses garbage in the chunk count
+ with pytest.raises(LocalProtocolError):
+ t_body_reader(ChunkedReader, b"10\x00\r\nxxx", None)
+
+ # handles (and discards) "chunk extensions" omg wtf
+ t_body_reader(
+ ChunkedReader,
+ b"5; hello=there\r\n"
+ + b"xxxxx"
+ + b"\r\n"
+ + b'0; random="junk"; some=more; canbe=lonnnnngg\r\n\r\n',
+ [Data(data=b"xxxxx"), EndOfMessage()],
+ )
+
+ t_body_reader(
+ ChunkedReader,
+ b"5 \r\n01234\r\n" + b"0\r\n\r\n",
+ [Data(data=b"01234"), EndOfMessage()],
+ )
+
+
+def test_ContentLengthWriter() -> None:
+ w = ContentLengthWriter(5)
+ assert dowrite(w, Data(data=b"123")) == b"123"
+ assert dowrite(w, Data(data=b"45")) == b"45"
+ assert dowrite(w, EndOfMessage()) == b""
+
+ w = ContentLengthWriter(5)
+ with pytest.raises(LocalProtocolError):
+ dowrite(w, Data(data=b"123456"))
+
+ w = ContentLengthWriter(5)
+ dowrite(w, Data(data=b"123"))
+ with pytest.raises(LocalProtocolError):
+ dowrite(w, Data(data=b"456"))
+
+ w = ContentLengthWriter(5)
+ dowrite(w, Data(data=b"123"))
+ with pytest.raises(LocalProtocolError):
+ dowrite(w, EndOfMessage())
+
+ w = ContentLengthWriter(5)
+ dowrite(w, Data(data=b"123")) == b"123"
+ dowrite(w, Data(data=b"45")) == b"45"
+ with pytest.raises(LocalProtocolError):
+ dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
+
+
+def test_ChunkedWriter() -> None:
+ w = ChunkedWriter()
+ assert dowrite(w, Data(data=b"aaa")) == b"3\r\naaa\r\n"
+ assert dowrite(w, Data(data=b"a" * 20)) == b"14\r\n" + b"a" * 20 + b"\r\n"
+
+ assert dowrite(w, Data(data=b"")) == b""
+
+ assert dowrite(w, EndOfMessage()) == b"0\r\n\r\n"
+
+ assert (
+ dowrite(w, EndOfMessage(headers=[("Etag", "asdf"), ("a", "b")]))
+ == b"0\r\nEtag: asdf\r\na: b\r\n\r\n"
+ )
+
+
+def test_Http10Writer() -> None:
+ w = Http10Writer()
+ assert dowrite(w, Data(data=b"1234")) == b"1234"
+ assert dowrite(w, EndOfMessage()) == b""
+
+ with pytest.raises(LocalProtocolError):
+ dowrite(w, EndOfMessage(headers=[("Etag", "asdf")]))
+
+
+def test_reject_garbage_after_request_line() -> None:
+ with pytest.raises(LocalProtocolError):
+ tr(READERS[SERVER, SEND_RESPONSE], b"HTTP/1.0 200 OK\x00xxxx\r\n\r\n", None)
+
+
+def test_reject_garbage_after_response_line() -> None:
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1 xxxxxx\r\n" b"Host: a\r\n\r\n",
+ None,
+ )
+
+
+def test_reject_garbage_in_header_line() -> None:
+ with pytest.raises(LocalProtocolError):
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n" b"Host: foo\x00bar\r\n\r\n",
+ None,
+ )
+
+
+def test_reject_non_vchar_in_path() -> None:
+ for bad_char in b"\x00\x20\x7f\xee":
+ message = bytearray(b"HEAD /")
+ message.append(bad_char)
+ message.extend(b" HTTP/1.1\r\nHost: foobar\r\n\r\n")
+ with pytest.raises(LocalProtocolError):
+ tr(READERS[CLIENT, IDLE], message, None)
+
+
+# https://github.com/python-hyper/h11/issues/57
+def test_allow_some_garbage_in_cookies() -> None:
+ tr(
+ READERS[CLIENT, IDLE],
+ b"HEAD /foo HTTP/1.1\r\n"
+ b"Host: foo\r\n"
+ b"Set-Cookie: ___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900\r\n"
+ b"\r\n",
+ Request(
+ method="HEAD",
+ target="/foo",
+ headers=[
+ ("Host", "foo"),
+ ("Set-Cookie", "___utmvafIumyLc=kUd\x01UpAt; path=/; Max-Age=900"),
+ ],
+ ),
+ )
+
+
+def test_host_comes_first() -> None:
+ tw(
+ write_headers,
+ normalize_and_validate([("foo", "bar"), ("Host", "example.com")]),
+ b"Host: example.com\r\nfoo: bar\r\n\r\n",
+ )
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_receivebuffer.py b/venv/lib/python3.11/site-packages/h11/tests/test_receivebuffer.py
new file mode 100644
index 0000000..21a3870
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_receivebuffer.py
@@ -0,0 +1,135 @@
+import re
+from typing import Tuple
+
+import pytest
+
+from .._receivebuffer import ReceiveBuffer
+
+
+def test_receivebuffer() -> None:
+ b = ReceiveBuffer()
+ assert not b
+ assert len(b) == 0
+ assert bytes(b) == b""
+
+ b += b"123"
+ assert b
+ assert len(b) == 3
+ assert bytes(b) == b"123"
+
+ assert bytes(b) == b"123"
+
+ assert b.maybe_extract_at_most(2) == b"12"
+ assert b
+ assert len(b) == 1
+ assert bytes(b) == b"3"
+
+ assert bytes(b) == b"3"
+
+ assert b.maybe_extract_at_most(10) == b"3"
+ assert bytes(b) == b""
+
+ assert b.maybe_extract_at_most(10) is None
+ assert not b
+
+ ################################################################
+ # maybe_extract_until_next
+ ################################################################
+
+ b += b"123\n456\r\n789\r\n"
+
+ assert b.maybe_extract_next_line() == b"123\n456\r\n"
+ assert bytes(b) == b"789\r\n"
+
+ assert b.maybe_extract_next_line() == b"789\r\n"
+ assert bytes(b) == b""
+
+ b += b"12\r"
+ assert b.maybe_extract_next_line() is None
+ assert bytes(b) == b"12\r"
+
+ b += b"345\n\r"
+ assert b.maybe_extract_next_line() is None
+ assert bytes(b) == b"12\r345\n\r"
+
+ # here we stopped at the middle of b"\r\n" delimiter
+
+ b += b"\n6789aaa123\r\n"
+ assert b.maybe_extract_next_line() == b"12\r345\n\r\n"
+ assert b.maybe_extract_next_line() == b"6789aaa123\r\n"
+ assert b.maybe_extract_next_line() is None
+ assert bytes(b) == b""
+
+ ################################################################
+ # maybe_extract_lines
+ ################################################################
+
+ b += b"123\r\na: b\r\nfoo:bar\r\n\r\ntrailing"
+ lines = b.maybe_extract_lines()
+ assert lines == [b"123", b"a: b", b"foo:bar"]
+ assert bytes(b) == b"trailing"
+
+ assert b.maybe_extract_lines() is None
+
+ b += b"\r\n\r"
+ assert b.maybe_extract_lines() is None
+
+ assert b.maybe_extract_at_most(100) == b"trailing\r\n\r"
+ assert not b
+
+ # Empty body case (as happens at the end of chunked encoding if there are
+ # no trailing headers, e.g.)
+ b += b"\r\ntrailing"
+ assert b.maybe_extract_lines() == []
+ assert bytes(b) == b"trailing"
+
+
+@pytest.mark.parametrize(
+ "data",
+ [
+ pytest.param(
+ (
+ b"HTTP/1.1 200 OK\r\n",
+ b"Content-type: text/plain\r\n",
+ b"Connection: close\r\n",
+ b"\r\n",
+ b"Some body",
+ ),
+ id="with_crlf_delimiter",
+ ),
+ pytest.param(
+ (
+ b"HTTP/1.1 200 OK\n",
+ b"Content-type: text/plain\n",
+ b"Connection: close\n",
+ b"\n",
+ b"Some body",
+ ),
+ id="with_lf_only_delimiter",
+ ),
+ pytest.param(
+ (
+ b"HTTP/1.1 200 OK\n",
+ b"Content-type: text/plain\r\n",
+ b"Connection: close\n",
+ b"\n",
+ b"Some body",
+ ),
+ id="with_mixed_crlf_and_lf",
+ ),
+ ],
+)
+def test_receivebuffer_for_invalid_delimiter(data: Tuple[bytes]) -> None:
+ b = ReceiveBuffer()
+
+ for line in data:
+ b += line
+
+ lines = b.maybe_extract_lines()
+
+ assert lines == [
+ b"HTTP/1.1 200 OK",
+ b"Content-type: text/plain",
+ b"Connection: close",
+ ]
+ assert bytes(b) == b"Some body"
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_state.py b/venv/lib/python3.11/site-packages/h11/tests/test_state.py
new file mode 100644
index 0000000..bc974e6
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_state.py
@@ -0,0 +1,271 @@
+import pytest
+
+from .._events import (
+ ConnectionClosed,
+ Data,
+ EndOfMessage,
+ Event,
+ InformationalResponse,
+ Request,
+ Response,
+)
+from .._state import (
+ _SWITCH_CONNECT,
+ _SWITCH_UPGRADE,
+ CLIENT,
+ CLOSED,
+ ConnectionState,
+ DONE,
+ IDLE,
+ MIGHT_SWITCH_PROTOCOL,
+ MUST_CLOSE,
+ SEND_BODY,
+ SEND_RESPONSE,
+ SERVER,
+ SWITCHED_PROTOCOL,
+)
+from .._util import LocalProtocolError
+
+
+def test_ConnectionState() -> None:
+ cs = ConnectionState()
+
+ # Basic event-triggered transitions
+
+ assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
+
+ cs.process_event(CLIENT, Request)
+ # The SERVER-Request special case:
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ # Illegal transitions raise an error and nothing happens
+ with pytest.raises(LocalProtocolError):
+ cs.process_event(CLIENT, Request)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ cs.process_event(SERVER, InformationalResponse)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ cs.process_event(SERVER, Response)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_BODY}
+
+ cs.process_event(CLIENT, EndOfMessage)
+ cs.process_event(SERVER, EndOfMessage)
+ assert cs.states == {CLIENT: DONE, SERVER: DONE}
+
+ # State-triggered transition
+
+ cs.process_event(SERVER, ConnectionClosed)
+ assert cs.states == {CLIENT: MUST_CLOSE, SERVER: CLOSED}
+
+
+def test_ConnectionState_keep_alive() -> None:
+ # keep_alive = False
+ cs = ConnectionState()
+ cs.process_event(CLIENT, Request)
+ cs.process_keep_alive_disabled()
+ cs.process_event(CLIENT, EndOfMessage)
+ assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_RESPONSE}
+
+ cs.process_event(SERVER, Response)
+ cs.process_event(SERVER, EndOfMessage)
+ assert cs.states == {CLIENT: MUST_CLOSE, SERVER: MUST_CLOSE}
+
+
+def test_ConnectionState_keep_alive_in_DONE() -> None:
+ # Check that if keep_alive is disabled when the CLIENT is already in DONE,
+ # then this is sufficient to immediately trigger the DONE -> MUST_CLOSE
+ # transition
+ cs = ConnectionState()
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+ assert cs.states[CLIENT] is DONE
+ cs.process_keep_alive_disabled()
+ assert cs.states[CLIENT] is MUST_CLOSE
+
+
+def test_ConnectionState_switch_denied() -> None:
+ for switch_type in (_SWITCH_CONNECT, _SWITCH_UPGRADE):
+ for deny_early in (True, False):
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(switch_type)
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, Data)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ assert switch_type in cs.pending_switch_proposals
+
+ if deny_early:
+ # before client reaches DONE
+ cs.process_event(SERVER, Response)
+ assert not cs.pending_switch_proposals
+
+ cs.process_event(CLIENT, EndOfMessage)
+
+ if deny_early:
+ assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
+ else:
+ assert cs.states == {
+ CLIENT: MIGHT_SWITCH_PROTOCOL,
+ SERVER: SEND_RESPONSE,
+ }
+
+ cs.process_event(SERVER, InformationalResponse)
+ assert cs.states == {
+ CLIENT: MIGHT_SWITCH_PROTOCOL,
+ SERVER: SEND_RESPONSE,
+ }
+
+ cs.process_event(SERVER, Response)
+ assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
+ assert not cs.pending_switch_proposals
+
+
+_response_type_for_switch = {
+ _SWITCH_UPGRADE: InformationalResponse,
+ _SWITCH_CONNECT: Response,
+ None: Response,
+}
+
+
+def test_ConnectionState_protocol_switch_accepted() -> None:
+ for switch_event in [_SWITCH_UPGRADE, _SWITCH_CONNECT]:
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(switch_event)
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, Data)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ cs.process_event(CLIENT, EndOfMessage)
+ assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
+
+ cs.process_event(SERVER, InformationalResponse)
+ assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
+
+ cs.process_event(SERVER, _response_type_for_switch[switch_event], switch_event)
+ assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
+
+
+def test_ConnectionState_double_protocol_switch() -> None:
+ # CONNECT + Upgrade is legal! Very silly, but legal. So we support
+ # it. Because sometimes doing the silly thing is easier than not.
+ for server_switch in [None, _SWITCH_UPGRADE, _SWITCH_CONNECT]:
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(_SWITCH_UPGRADE)
+ cs.process_client_switch_proposal(_SWITCH_CONNECT)
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+ assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
+ cs.process_event(
+ SERVER, _response_type_for_switch[server_switch], server_switch
+ )
+ if server_switch is None:
+ assert cs.states == {CLIENT: DONE, SERVER: SEND_BODY}
+ else:
+ assert cs.states == {CLIENT: SWITCHED_PROTOCOL, SERVER: SWITCHED_PROTOCOL}
+
+
+def test_ConnectionState_inconsistent_protocol_switch() -> None:
+ for client_switches, server_switch in [
+ ([], _SWITCH_CONNECT),
+ ([], _SWITCH_UPGRADE),
+ ([_SWITCH_UPGRADE], _SWITCH_CONNECT),
+ ([_SWITCH_CONNECT], _SWITCH_UPGRADE),
+ ]:
+ cs = ConnectionState()
+ for client_switch in client_switches: # type: ignore[attr-defined]
+ cs.process_client_switch_proposal(client_switch)
+ cs.process_event(CLIENT, Request)
+ with pytest.raises(LocalProtocolError):
+ cs.process_event(SERVER, Response, server_switch)
+
+
+def test_ConnectionState_keepalive_protocol_switch_interaction() -> None:
+ # keep_alive=False + pending_switch_proposals
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(_SWITCH_UPGRADE)
+ cs.process_event(CLIENT, Request)
+ cs.process_keep_alive_disabled()
+ cs.process_event(CLIENT, Data)
+ assert cs.states == {CLIENT: SEND_BODY, SERVER: SEND_RESPONSE}
+
+ # the protocol switch "wins"
+ cs.process_event(CLIENT, EndOfMessage)
+ assert cs.states == {CLIENT: MIGHT_SWITCH_PROTOCOL, SERVER: SEND_RESPONSE}
+
+ # but when the server denies the request, keep_alive comes back into play
+ cs.process_event(SERVER, Response)
+ assert cs.states == {CLIENT: MUST_CLOSE, SERVER: SEND_BODY}
+
+
+def test_ConnectionState_reuse() -> None:
+ cs = ConnectionState()
+
+ with pytest.raises(LocalProtocolError):
+ cs.start_next_cycle()
+
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+
+ with pytest.raises(LocalProtocolError):
+ cs.start_next_cycle()
+
+ cs.process_event(SERVER, Response)
+ cs.process_event(SERVER, EndOfMessage)
+
+ cs.start_next_cycle()
+ assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
+
+ # No keepalive
+
+ cs.process_event(CLIENT, Request)
+ cs.process_keep_alive_disabled()
+ cs.process_event(CLIENT, EndOfMessage)
+ cs.process_event(SERVER, Response)
+ cs.process_event(SERVER, EndOfMessage)
+
+ with pytest.raises(LocalProtocolError):
+ cs.start_next_cycle()
+
+ # One side closed
+
+ cs = ConnectionState()
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+ cs.process_event(CLIENT, ConnectionClosed)
+ cs.process_event(SERVER, Response)
+ cs.process_event(SERVER, EndOfMessage)
+
+ with pytest.raises(LocalProtocolError):
+ cs.start_next_cycle()
+
+ # Succesful protocol switch
+
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(_SWITCH_UPGRADE)
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+ cs.process_event(SERVER, InformationalResponse, _SWITCH_UPGRADE)
+
+ with pytest.raises(LocalProtocolError):
+ cs.start_next_cycle()
+
+ # Failed protocol switch
+
+ cs = ConnectionState()
+ cs.process_client_switch_proposal(_SWITCH_UPGRADE)
+ cs.process_event(CLIENT, Request)
+ cs.process_event(CLIENT, EndOfMessage)
+ cs.process_event(SERVER, Response)
+ cs.process_event(SERVER, EndOfMessage)
+
+ cs.start_next_cycle()
+ assert cs.states == {CLIENT: IDLE, SERVER: IDLE}
+
+
+def test_server_request_is_illegal() -> None:
+ # There used to be a bug in how we handled the Request special case that
+ # made this allowed...
+ cs = ConnectionState()
+ with pytest.raises(LocalProtocolError):
+ cs.process_event(SERVER, Request)
diff --git a/venv/lib/python3.11/site-packages/h11/tests/test_util.py b/venv/lib/python3.11/site-packages/h11/tests/test_util.py
new file mode 100644
index 0000000..79bc095
--- /dev/null
+++ b/venv/lib/python3.11/site-packages/h11/tests/test_util.py
@@ -0,0 +1,112 @@
+import re
+import sys
+import traceback
+from typing import NoReturn
+
+import pytest
+
+from .._util import (
+ bytesify,
+ LocalProtocolError,
+ ProtocolError,
+ RemoteProtocolError,
+ Sentinel,
+ validate,
+)
+
+
+def test_ProtocolError() -> None:
+ with pytest.raises(TypeError):
+ ProtocolError("abstract base class")
+
+
+def test_LocalProtocolError() -> None:
+ try:
+ raise LocalProtocolError("foo")
+ except LocalProtocolError as e:
+ assert str(e) == "foo"
+ assert e.error_status_hint == 400
+
+ try:
+ raise LocalProtocolError("foo", error_status_hint=418)
+ except LocalProtocolError as e:
+ assert str(e) == "foo"
+ assert e.error_status_hint == 418
+
+ def thunk() -> NoReturn:
+ raise LocalProtocolError("a", error_status_hint=420)
+
+ try:
+ try:
+ thunk()
+ except LocalProtocolError as exc1:
+ orig_traceback = "".join(traceback.format_tb(sys.exc_info()[2]))
+ exc1._reraise_as_remote_protocol_error()
+ except RemoteProtocolError as exc2:
+ assert type(exc2) is RemoteProtocolError
+ assert exc2.args == ("a",)
+ assert exc2.error_status_hint == 420
+ new_traceback = "".join(traceback.format_tb(sys.exc_info()[2]))
+ assert new_traceback.endswith(orig_traceback)
+
+
+def test_validate() -> None:
+ my_re = re.compile(rb"(?P<group1>[0-9]+)\.(?P<group2>[0-9]+)")
+ with pytest.raises(LocalProtocolError):
+ validate(my_re, b"0.")
+
+ groups = validate(my_re, b"0.1")
+ assert groups == {"group1": b"0", "group2": b"1"}
+
+ # successful partial matches are an error - must match whole string
+ with pytest.raises(LocalProtocolError):
+ validate(my_re, b"0.1xx")
+ with pytest.raises(LocalProtocolError):
+ validate(my_re, b"0.1\n")
+
+
+def test_validate_formatting() -> None:
+ my_re = re.compile(rb"foo")
+
+ with pytest.raises(LocalProtocolError) as excinfo:
+ validate(my_re, b"", "oops")
+ assert "oops" in str(excinfo.value)
+
+ with pytest.raises(LocalProtocolError) as excinfo:
+ validate(my_re, b"", "oops {}")
+ assert "oops {}" in str(excinfo.value)
+
+ with pytest.raises(LocalProtocolError) as excinfo:
+ validate(my_re, b"", "oops {} xx", 10)
+ assert "oops 10 xx" in str(excinfo.value)
+
+
+def test_make_sentinel() -> None:
+ class S(Sentinel, metaclass=Sentinel):
+ pass
+
+ assert repr(S) == "S"
+ assert S == S
+ assert type(S).__name__ == "S"
+ assert S in {S}
+ assert type(S) is S
+
+ class S2(Sentinel, metaclass=Sentinel):
+ pass
+
+ assert repr(S2) == "S2"
+ assert S != S2
+ assert S not in {S2}
+ assert type(S) is not type(S2)
+
+
+def test_bytesify() -> None:
+ assert bytesify(b"123") == b"123"
+ assert bytesify(bytearray(b"123")) == b"123"
+ assert bytesify("123") == b"123"
+
+ with pytest.raises(UnicodeEncodeError):
+ bytesify("\u1234")
+
+ with pytest.raises(TypeError):
+ bytesify(10)