about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/urllib3/http2
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/urllib3/http2')
-rw-r--r--.venv/lib/python3.12/site-packages/urllib3/http2/__init__.py53
-rw-r--r--.venv/lib/python3.12/site-packages/urllib3/http2/connection.py356
-rw-r--r--.venv/lib/python3.12/site-packages/urllib3/http2/probe.py87
3 files changed, 496 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/urllib3/http2/__init__.py b/.venv/lib/python3.12/site-packages/urllib3/http2/__init__.py
new file mode 100644
index 00000000..133e1d8f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/urllib3/http2/__init__.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+from importlib.metadata import version
+
+__all__ = [
+    "inject_into_urllib3",
+    "extract_from_urllib3",
+]
+
+import typing
+
+orig_HTTPSConnection: typing.Any = None
+
+
+def inject_into_urllib3() -> None:
+    # First check if h2 version is valid
+    h2_version = version("h2")
+    if not h2_version.startswith("4."):
+        raise ImportError(
+            "urllib3 v2 supports h2 version 4.x.x, currently "
+            f"the 'h2' module is compiled with {h2_version!r}. "
+            "See: https://github.com/urllib3/urllib3/issues/3290"
+        )
+
+    # Import here to avoid circular dependencies.
+    from .. import connection as urllib3_connection
+    from .. import util as urllib3_util
+    from ..connectionpool import HTTPSConnectionPool
+    from ..util import ssl_ as urllib3_util_ssl
+    from .connection import HTTP2Connection
+
+    global orig_HTTPSConnection
+    orig_HTTPSConnection = urllib3_connection.HTTPSConnection
+
+    HTTPSConnectionPool.ConnectionCls = HTTP2Connection
+    urllib3_connection.HTTPSConnection = HTTP2Connection  # type: ignore[misc]
+
+    # TODO: Offer 'http/1.1' as well, but for testing purposes this is handy.
+    urllib3_util.ALPN_PROTOCOLS = ["h2"]
+    urllib3_util_ssl.ALPN_PROTOCOLS = ["h2"]
+
+
+def extract_from_urllib3() -> None:
+    from .. import connection as urllib3_connection
+    from .. import util as urllib3_util
+    from ..connectionpool import HTTPSConnectionPool
+    from ..util import ssl_ as urllib3_util_ssl
+
+    HTTPSConnectionPool.ConnectionCls = orig_HTTPSConnection
+    urllib3_connection.HTTPSConnection = orig_HTTPSConnection  # type: ignore[misc]
+
+    urllib3_util.ALPN_PROTOCOLS = ["http/1.1"]
+    urllib3_util_ssl.ALPN_PROTOCOLS = ["http/1.1"]
diff --git a/.venv/lib/python3.12/site-packages/urllib3/http2/connection.py b/.venv/lib/python3.12/site-packages/urllib3/http2/connection.py
new file mode 100644
index 00000000..f4861452
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/urllib3/http2/connection.py
@@ -0,0 +1,356 @@
+from __future__ import annotations
+
+import logging
+import re
+import threading
+import types
+import typing
+
+import h2.config  # type: ignore[import-untyped]
+import h2.connection  # type: ignore[import-untyped]
+import h2.events  # type: ignore[import-untyped]
+
+from .._base_connection import _TYPE_BODY
+from .._collections import HTTPHeaderDict
+from ..connection import HTTPSConnection, _get_default_user_agent
+from ..exceptions import ConnectionError
+from ..response import BaseHTTPResponse
+
+orig_HTTPSConnection = HTTPSConnection
+
+T = typing.TypeVar("T")
+
+log = logging.getLogger(__name__)
+
+RE_IS_LEGAL_HEADER_NAME = re.compile(rb"^[!#$%&'*+\-.^_`|~0-9a-z]+$")
+RE_IS_ILLEGAL_HEADER_VALUE = re.compile(rb"[\0\x00\x0a\x0d\r\n]|^[ \r\n\t]|[ \r\n\t]$")
+
+
+def _is_legal_header_name(name: bytes) -> bool:
+    """
+    "An implementation that validates fields according to the definitions in Sections
+    5.1 and 5.5 of [HTTP] only needs an additional check that field names do not
+    include uppercase characters." (https://httpwg.org/specs/rfc9113.html#n-field-validity)
+
+    `http.client._is_legal_header_name` does not validate the field name according to the
+    HTTP 1.1 spec, so we do that here, in addition to checking for uppercase characters.
+
+    This does not allow for the `:` character in the header name, so should not
+    be used to validate pseudo-headers.
+    """
+    return bool(RE_IS_LEGAL_HEADER_NAME.match(name))
+
+
+def _is_illegal_header_value(value: bytes) -> bool:
+    """
+    "A field value MUST NOT contain the zero value (ASCII NUL, 0x00), line feed
+    (ASCII LF, 0x0a), or carriage return (ASCII CR, 0x0d) at any position. A field
+    value MUST NOT start or end with an ASCII whitespace character (ASCII SP or HTAB,
+    0x20 or 0x09)." (https://httpwg.org/specs/rfc9113.html#n-field-validity)
+    """
+    return bool(RE_IS_ILLEGAL_HEADER_VALUE.search(value))
+
+
+class _LockedObject(typing.Generic[T]):
+    """
+    A wrapper class that hides a specific object behind a lock.
+    The goal here is to provide a simple way to protect access to an object
+    that cannot safely be simultaneously accessed from multiple threads. The
+    intended use of this class is simple: take hold of it with a context
+    manager, which returns the protected object.
+    """
+
+    __slots__ = (
+        "lock",
+        "_obj",
+    )
+
+    def __init__(self, obj: T):
+        self.lock = threading.RLock()
+        self._obj = obj
+
+    def __enter__(self) -> T:
+        self.lock.acquire()
+        return self._obj
+
+    def __exit__(
+        self,
+        exc_type: type[BaseException] | None,
+        exc_val: BaseException | None,
+        exc_tb: types.TracebackType | None,
+    ) -> None:
+        self.lock.release()
+
+
+class HTTP2Connection(HTTPSConnection):
+    def __init__(
+        self, host: str, port: int | None = None, **kwargs: typing.Any
+    ) -> None:
+        self._h2_conn = self._new_h2_conn()
+        self._h2_stream: int | None = None
+        self._headers: list[tuple[bytes, bytes]] = []
+
+        if "proxy" in kwargs or "proxy_config" in kwargs:  # Defensive:
+            raise NotImplementedError("Proxies aren't supported with HTTP/2")
+
+        super().__init__(host, port, **kwargs)
+
+        if self._tunnel_host is not None:
+            raise NotImplementedError("Tunneling isn't supported with HTTP/2")
+
+    def _new_h2_conn(self) -> _LockedObject[h2.connection.H2Connection]:
+        config = h2.config.H2Configuration(client_side=True)
+        return _LockedObject(h2.connection.H2Connection(config=config))
+
+    def connect(self) -> None:
+        super().connect()
+        with self._h2_conn as conn:
+            conn.initiate_connection()
+            if data_to_send := conn.data_to_send():
+                self.sock.sendall(data_to_send)
+
+    def putrequest(  # type: ignore[override]
+        self,
+        method: str,
+        url: str,
+        **kwargs: typing.Any,
+    ) -> None:
+        """putrequest
+        This deviates from the HTTPConnection method signature since we never need to override
+        sending accept-encoding headers or the host header.
+        """
+        if "skip_host" in kwargs:
+            raise NotImplementedError("`skip_host` isn't supported")
+        if "skip_accept_encoding" in kwargs:
+            raise NotImplementedError("`skip_accept_encoding` isn't supported")
+
+        self._request_url = url or "/"
+        self._validate_path(url)  # type: ignore[attr-defined]
+
+        if ":" in self.host:
+            authority = f"[{self.host}]:{self.port or 443}"
+        else:
+            authority = f"{self.host}:{self.port or 443}"
+
+        self._headers.append((b":scheme", b"https"))
+        self._headers.append((b":method", method.encode()))
+        self._headers.append((b":authority", authority.encode()))
+        self._headers.append((b":path", url.encode()))
+
+        with self._h2_conn as conn:
+            self._h2_stream = conn.get_next_available_stream_id()
+
+    def putheader(self, header: str | bytes, *values: str | bytes) -> None:
+        # TODO SKIPPABLE_HEADERS from urllib3 are ignored.
+        header = header.encode() if isinstance(header, str) else header
+        header = header.lower()  # A lot of upstream code uses capitalized headers.
+        if not _is_legal_header_name(header):
+            raise ValueError(f"Illegal header name {str(header)}")
+
+        for value in values:
+            value = value.encode() if isinstance(value, str) else value
+            if _is_illegal_header_value(value):
+                raise ValueError(f"Illegal header value {str(value)}")
+            self._headers.append((header, value))
+
+    def endheaders(self, message_body: typing.Any = None) -> None:  # type: ignore[override]
+        if self._h2_stream is None:
+            raise ConnectionError("Must call `putrequest` first.")
+
+        with self._h2_conn as conn:
+            conn.send_headers(
+                stream_id=self._h2_stream,
+                headers=self._headers,
+                end_stream=(message_body is None),
+            )
+            if data_to_send := conn.data_to_send():
+                self.sock.sendall(data_to_send)
+        self._headers = []  # Reset headers for the next request.
+
+    def send(self, data: typing.Any) -> None:
+        """Send data to the server.
+        `data` can be: `str`, `bytes`, an iterable, or file-like objects
+        that support a .read() method.
+        """
+        if self._h2_stream is None:
+            raise ConnectionError("Must call `putrequest` first.")
+
+        with self._h2_conn as conn:
+            if data_to_send := conn.data_to_send():
+                self.sock.sendall(data_to_send)
+
+            if hasattr(data, "read"):  # file-like objects
+                while True:
+                    chunk = data.read(self.blocksize)
+                    if not chunk:
+                        break
+                    if isinstance(chunk, str):
+                        chunk = chunk.encode()  # pragma: no cover
+                    conn.send_data(self._h2_stream, chunk, end_stream=False)
+                    if data_to_send := conn.data_to_send():
+                        self.sock.sendall(data_to_send)
+                conn.end_stream(self._h2_stream)
+                return
+
+            if isinstance(data, str):  # str -> bytes
+                data = data.encode()
+
+            try:
+                if isinstance(data, bytes):
+                    conn.send_data(self._h2_stream, data, end_stream=True)
+                    if data_to_send := conn.data_to_send():
+                        self.sock.sendall(data_to_send)
+                else:
+                    for chunk in data:
+                        conn.send_data(self._h2_stream, chunk, end_stream=False)
+                        if data_to_send := conn.data_to_send():
+                            self.sock.sendall(data_to_send)
+                    conn.end_stream(self._h2_stream)
+            except TypeError:
+                raise TypeError(
+                    "`data` should be str, bytes, iterable, or file. got %r"
+                    % type(data)
+                )
+
+    def set_tunnel(
+        self,
+        host: str,
+        port: int | None = None,
+        headers: typing.Mapping[str, str] | None = None,
+        scheme: str = "http",
+    ) -> None:
+        raise NotImplementedError(
+            "HTTP/2 does not support setting up a tunnel through a proxy"
+        )
+
+    def getresponse(  # type: ignore[override]
+        self,
+    ) -> HTTP2Response:
+        status = None
+        data = bytearray()
+        with self._h2_conn as conn:
+            end_stream = False
+            while not end_stream:
+                # TODO: Arbitrary read value.
+                if received_data := self.sock.recv(65535):
+                    events = conn.receive_data(received_data)
+                    for event in events:
+                        if isinstance(event, h2.events.ResponseReceived):
+                            headers = HTTPHeaderDict()
+                            for header, value in event.headers:
+                                if header == b":status":
+                                    status = int(value.decode())
+                                else:
+                                    headers.add(
+                                        header.decode("ascii"), value.decode("ascii")
+                                    )
+
+                        elif isinstance(event, h2.events.DataReceived):
+                            data += event.data
+                            conn.acknowledge_received_data(
+                                event.flow_controlled_length, event.stream_id
+                            )
+
+                        elif isinstance(event, h2.events.StreamEnded):
+                            end_stream = True
+
+                if data_to_send := conn.data_to_send():
+                    self.sock.sendall(data_to_send)
+
+        assert status is not None
+        return HTTP2Response(
+            status=status,
+            headers=headers,
+            request_url=self._request_url,
+            data=bytes(data),
+        )
+
+    def request(  # type: ignore[override]
+        self,
+        method: str,
+        url: str,
+        body: _TYPE_BODY | None = None,
+        headers: typing.Mapping[str, str] | None = None,
+        *,
+        preload_content: bool = True,
+        decode_content: bool = True,
+        enforce_content_length: bool = True,
+        **kwargs: typing.Any,
+    ) -> None:
+        """Send an HTTP/2 request"""
+        if "chunked" in kwargs:
+            # TODO this is often present from upstream.
+            # raise NotImplementedError("`chunked` isn't supported with HTTP/2")
+            pass
+
+        if self.sock is not None:
+            self.sock.settimeout(self.timeout)
+
+        self.putrequest(method, url)
+
+        headers = headers or {}
+        for k, v in headers.items():
+            if k.lower() == "transfer-encoding" and v == "chunked":
+                continue
+            else:
+                self.putheader(k, v)
+
+        if b"user-agent" not in dict(self._headers):
+            self.putheader(b"user-agent", _get_default_user_agent())
+
+        if body:
+            self.endheaders(message_body=body)
+            self.send(body)
+        else:
+            self.endheaders()
+
+    def close(self) -> None:
+        with self._h2_conn as conn:
+            try:
+                conn.close_connection()
+                if data := conn.data_to_send():
+                    self.sock.sendall(data)
+            except Exception:
+                pass
+
+        # Reset all our HTTP/2 connection state.
+        self._h2_conn = self._new_h2_conn()
+        self._h2_stream = None
+        self._headers = []
+
+        super().close()
+
+
+class HTTP2Response(BaseHTTPResponse):
+    # TODO: This is a woefully incomplete response object, but works for non-streaming.
+    def __init__(
+        self,
+        status: int,
+        headers: HTTPHeaderDict,
+        request_url: str,
+        data: bytes,
+        decode_content: bool = False,  # TODO: support decoding
+    ) -> None:
+        super().__init__(
+            status=status,
+            headers=headers,
+            # Following CPython, we map HTTP versions to major * 10 + minor integers
+            version=20,
+            version_string="HTTP/2",
+            # No reason phrase in HTTP/2
+            reason=None,
+            decode_content=decode_content,
+            request_url=request_url,
+        )
+        self._data = data
+        self.length_remaining = 0
+
+    @property
+    def data(self) -> bytes:
+        return self._data
+
+    def get_redirect_location(self) -> None:
+        return None
+
+    def close(self) -> None:
+        pass
diff --git a/.venv/lib/python3.12/site-packages/urllib3/http2/probe.py b/.venv/lib/python3.12/site-packages/urllib3/http2/probe.py
new file mode 100644
index 00000000..9ea90076
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/urllib3/http2/probe.py
@@ -0,0 +1,87 @@
+from __future__ import annotations
+
+import threading
+
+
+class _HTTP2ProbeCache:
+    __slots__ = (
+        "_lock",
+        "_cache_locks",
+        "_cache_values",
+    )
+
+    def __init__(self) -> None:
+        self._lock = threading.Lock()
+        self._cache_locks: dict[tuple[str, int], threading.RLock] = {}
+        self._cache_values: dict[tuple[str, int], bool | None] = {}
+
+    def acquire_and_get(self, host: str, port: int) -> bool | None:
+        # By the end of this block we know that
+        # _cache_[values,locks] is available.
+        value = None
+        with self._lock:
+            key = (host, port)
+            try:
+                value = self._cache_values[key]
+                # If it's a known value we return right away.
+                if value is not None:
+                    return value
+            except KeyError:
+                self._cache_locks[key] = threading.RLock()
+                self._cache_values[key] = None
+
+        # If the value is unknown, we acquire the lock to signal
+        # to the requesting thread that the probe is in progress
+        # or that the current thread needs to return their findings.
+        key_lock = self._cache_locks[key]
+        key_lock.acquire()
+        try:
+            # If the by the time we get the lock the value has been
+            # updated we want to return the updated value.
+            value = self._cache_values[key]
+
+        # In case an exception like KeyboardInterrupt is raised here.
+        except BaseException as e:  # Defensive:
+            assert not isinstance(e, KeyError)  # KeyError shouldn't be possible.
+            key_lock.release()
+            raise
+
+        return value
+
+    def set_and_release(
+        self, host: str, port: int, supports_http2: bool | None
+    ) -> None:
+        key = (host, port)
+        key_lock = self._cache_locks[key]
+        with key_lock:  # Uses an RLock, so can be locked again from same thread.
+            if supports_http2 is None and self._cache_values[key] is not None:
+                raise ValueError(
+                    "Cannot reset HTTP/2 support for origin after value has been set."
+                )  # Defensive: not expected in normal usage
+
+        self._cache_values[key] = supports_http2
+        key_lock.release()
+
+    def _values(self) -> dict[tuple[str, int], bool | None]:
+        """This function is for testing purposes only. Gets the current state of the probe cache"""
+        with self._lock:
+            return {k: v for k, v in self._cache_values.items()}
+
+    def _reset(self) -> None:
+        """This function is for testing purposes only. Reset the cache values"""
+        with self._lock:
+            self._cache_locks = {}
+            self._cache_values = {}
+
+
+_HTTP2_PROBE_CACHE = _HTTP2ProbeCache()
+
+set_and_release = _HTTP2_PROBE_CACHE.set_and_release
+acquire_and_get = _HTTP2_PROBE_CACHE.acquire_and_get
+_values = _HTTP2_PROBE_CACHE._values
+_reset = _HTTP2_PROBE_CACHE._reset
+
+__all__ = [
+    "set_and_release",
+    "acquire_and_get",
+]