done
This commit is contained in:
		
							
								
								
									
										53
									
								
								lib/python3.11/site-packages/urllib3/http2/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								lib/python3.11/site-packages/urllib3/http2/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @ -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"] | ||||
							
								
								
									
										356
									
								
								lib/python3.11/site-packages/urllib3/http2/connection.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										356
									
								
								lib/python3.11/site-packages/urllib3/http2/connection.py
									
									
									
									
									
										Normal file
									
								
							| @ -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:  # type: ignore[override] | ||||
|         # 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 | ||||
							
								
								
									
										87
									
								
								lib/python3.11/site-packages/urllib3/http2/probe.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								lib/python3.11/site-packages/urllib3/http2/probe.py
									
									
									
									
									
										Normal file
									
								
							| @ -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", | ||||
| ] | ||||
		Reference in New Issue
	
	Block a user