From 2733055f8c0d34f6060f32d8e94acf8991f6ec92 Mon Sep 17 00:00:00 2001 From: donbarbos Date: Tue, 25 Aug 2026 12:44:29 +0400 Subject: [PATCH] [gunicorn] Update to 26.2.0 Closes: #16296 --- stubs/gunicorn/@tests/stubtest_allowlist.txt | 5 ++++ stubs/gunicorn/METADATA.toml | 2 +- stubs/gunicorn/gunicorn/asgi/parser.pyi | 3 +++ stubs/gunicorn/gunicorn/config.pyi | 13 +++++++++ stubs/gunicorn/gunicorn/http/errors.pyi | 4 +++ stubs/gunicorn/gunicorn/http/message.pyi | 10 ++++--- stubs/gunicorn/gunicorn/http/unreader.pyi | 1 + stubs/gunicorn/gunicorn/http/wsgi.pyi | 24 ++++++++++++++--- .../gunicorn/http2/async_connection.pyi | 4 +++ stubs/gunicorn/gunicorn/http2/connection.pyi | 6 +++++ stubs/gunicorn/gunicorn/http2/negotiation.pyi | 27 +++++++++++++++++++ stubs/gunicorn/gunicorn/http2/request.pyi | 3 ++- stubs/gunicorn/gunicorn/http2/response.pyi | 17 ++++++++++++ .../gunicorn/gunicorn/workers/base_async.pyi | 9 ++++++- stubs/gunicorn/gunicorn/workers/gthread.pyi | 1 + 15 files changed, 120 insertions(+), 9 deletions(-) create mode 100644 stubs/gunicorn/gunicorn/http2/negotiation.pyi create mode 100644 stubs/gunicorn/gunicorn/http2/response.pyi diff --git a/stubs/gunicorn/@tests/stubtest_allowlist.txt b/stubs/gunicorn/@tests/stubtest_allowlist.txt index c5b7678827c3..771d1891e68d 100644 --- a/stubs/gunicorn/@tests/stubtest_allowlist.txt +++ b/stubs/gunicorn/@tests/stubtest_allowlist.txt @@ -7,3 +7,8 @@ gunicorn.__main__ # Internal API stuff: gunicorn.config.Config.forwarded_allow_networks gunicorn.config.Config.proxy_allow_networks + +# Set to None on class, but initialized to non-None value in __init__ +gunicorn.http.message.Message.scheme +gunicorn.http2.request.HTTP2Request.scheme +gunicorn.http2.request.HTTP2Request.version diff --git a/stubs/gunicorn/METADATA.toml b/stubs/gunicorn/METADATA.toml index 5464efe0bd00..60913393ea88 100644 --- a/stubs/gunicorn/METADATA.toml +++ b/stubs/gunicorn/METADATA.toml @@ -1,4 +1,4 @@ -version = "26.1.0" +version = "26.2.0" upstream-repository = "https://github.com/benoitc/gunicorn" optional-dependencies = ["types-gevent"] diff --git a/stubs/gunicorn/gunicorn/asgi/parser.pyi b/stubs/gunicorn/gunicorn/asgi/parser.pyi index 1cdb54519985..294d7681f0e0 100644 --- a/stubs/gunicorn/gunicorn/asgi/parser.pyi +++ b/stubs/gunicorn/gunicorn/asgi/parser.pyi @@ -115,6 +115,9 @@ class PythonProtocol: proxy_protocol: Literal["off", "v1", "v2", "auto"] = "off", ) -> None: ... def feed(self, data: Iterable[SupportsIndex]) -> None: ... + def remaining(self) -> bytes: ... + @property + def remaining_truncated(self) -> Literal[False]: ... @property def proxy_protocol_info(self) -> _ProxyProtocolInfo | _ProxyProtocolInfoUnknown | None: ... def reset(self) -> None: ... diff --git a/stubs/gunicorn/gunicorn/config.pyi b/stubs/gunicorn/gunicorn/config.pyi index c8ef8e0da045..390a6d591365 100644 --- a/stubs/gunicorn/gunicorn/config.pyi +++ b/stubs/gunicorn/gunicorn/config.pyi @@ -76,6 +76,7 @@ _ASGILifespanValidatorType: TypeAlias = Callable[[str | None], str] _HTTP2FrameSizeValidatorType: TypeAlias = Callable[[ConvertibleToInt], int] _HTTPProtocolsValidatorType: TypeAlias = Callable[[str | None], list[str]] _HttpParserValidatorType: TypeAlias = Callable[[str | None], str] +_HTTP2CleartextValidatorType: TypeAlias = Callable[[str | bool | None], str] _ValidatorType: TypeAlias = ( # noqa: Y047 _BoolValidatorType @@ -93,6 +94,7 @@ _ValidatorType: TypeAlias = ( # noqa: Y047 | _HTTP2FrameSizeValidatorType | _HTTPProtocolsValidatorType | _HttpParserValidatorType + | _HTTP2CleartextValidatorType ) KNOWN_SETTINGS: list[Setting] @@ -169,6 +171,8 @@ class Setting(metaclass=SettingMeta): __cmp__ = __lt__ +def validate_http2_cleartext(val: str | bool | None) -> str: ... + @overload def validate_bool(val: bool) -> bool: ... @overload @@ -1054,6 +1058,15 @@ class HTTPProtocols(Setting): default: ClassVar[str] desc: ClassVar[str] +class HTTP2Cleartext(Setting): + name: ClassVar[str] + section: ClassVar[str] + cli: ClassVar[list[str]] + meta: ClassVar[str] + validator: ClassVar[_HTTP2CleartextValidatorType] + default: ClassVar[str] + desc: ClassVar[str] + class HTTP2MaxConcurrentStreams(Setting): name: ClassVar[str] section: ClassVar[str] diff --git a/stubs/gunicorn/gunicorn/http/errors.pyi b/stubs/gunicorn/gunicorn/http/errors.pyi index a7698a0973bc..0e775b72a6f4 100644 --- a/stubs/gunicorn/gunicorn/http/errors.pyi +++ b/stubs/gunicorn/gunicorn/http/errors.pyi @@ -102,3 +102,7 @@ class ForbiddenProxyRequest(ParseException): def __init__(self, host: str) -> None: ... class InvalidSchemeHeaders(ParseException): ... + +class InvalidH2CPreface(ParseException): + data: bytes + def __init__(self, data: bytes) -> None: ... diff --git a/stubs/gunicorn/gunicorn/http/message.pyi b/stubs/gunicorn/gunicorn/http/message.pyi index 3f593c5aaeec..c7b7da7d5e12 100644 --- a/stubs/gunicorn/gunicorn/http/message.pyi +++ b/stubs/gunicorn/gunicorn/http/message.pyi @@ -1,7 +1,7 @@ import io import re from enum import IntEnum -from typing import Final +from typing import Final, Literal from gunicorn.config import Config from gunicorn.http.body import Body @@ -38,7 +38,11 @@ RFC9110_5_5_INVALID_AND_DANGEROUS: Final[re.Pattern[str]] RFC9110_6_5_1_FORBIDDEN_TRAILER: Final[frozenset[str]] RFC9110_5_3_SINGLETON_FIELDS: Final[frozenset[str]] -class Message: +class HeaderPolicy: + scheme: str | None + version: tuple[int, int] | None + +class Message(HeaderPolicy): cfg: Config unreader: Unreader peer_addr: _AddressType @@ -47,7 +51,7 @@ class Message: headers: list[tuple[str, str]] trailers: list[tuple[str, str]] body: Body | None - scheme: str + scheme: Literal["https", "http"] must_close: bool limit_request_fields: int limit_request_field_size: int diff --git a/stubs/gunicorn/gunicorn/http/unreader.pyi b/stubs/gunicorn/gunicorn/http/unreader.pyi index be3da9d5d948..bfc9992b9506 100644 --- a/stubs/gunicorn/gunicorn/http/unreader.pyi +++ b/stubs/gunicorn/gunicorn/http/unreader.pyi @@ -8,6 +8,7 @@ class Unreader: def __init__(self) -> None: ... def chunk(self) -> bytes: ... + def take_buffered(self) -> bytes: ... def read(self, size: int | None = None) -> bytes: ... def unread(self, data: ReadableBuffer) -> None: ... diff --git a/stubs/gunicorn/gunicorn/http/wsgi.pyi b/stubs/gunicorn/gunicorn/http/wsgi.pyi index 3c44489fd5c7..cbf843149c9d 100644 --- a/stubs/gunicorn/gunicorn/http/wsgi.pyi +++ b/stubs/gunicorn/gunicorn/http/wsgi.pyi @@ -2,9 +2,9 @@ import io import logging import re import socket -from _typeshed import ReadableBuffer, Unused +from _typeshed import Incomplete, ReadableBuffer, Unused from collections.abc import Callable -from typing import Any, Final, Protocol, type_check_only +from typing import Any, Final, Protocol, overload, type_check_only from typing_extensions import Self from gunicorn.config import Config @@ -44,9 +44,27 @@ class WSGIErrorsWrapper(io.RawIOBase): def base_environ(cfg: Config) -> _EnvironType: ... def default_environ(req: Request, sock: socket.socket, cfg: Config) -> _EnvironType: ... def proxy_environ(req: Request) -> _EnvironType: ... + +@overload def create( - req: Request, sock: socket.socket, client: _AddressType, server: _AddressType, cfg: Config + req: Request, + sock: socket.socket, + client: _AddressType, + server: _AddressType, + cfg: Config, + response_class: type[Response] | None = None, + response_args: tuple[Incomplete, ...] = (), ) -> tuple[Response, _EnvironType]: ... +@overload +def create( + req: Request, + sock: socket.socket, + client: _AddressType, + server: _AddressType, + cfg: Config, + response_class: type[Incomplete], + response_args: tuple[Incomplete, ...] = (), +) -> tuple[Incomplete, _EnvironType]: ... class Response: req: Request diff --git a/stubs/gunicorn/gunicorn/http2/async_connection.pyi b/stubs/gunicorn/gunicorn/http2/async_connection.pyi index 12fbc8524366..1cc1d687ea03 100644 --- a/stubs/gunicorn/gunicorn/http2/async_connection.pyi +++ b/stubs/gunicorn/gunicorn/http2/async_connection.pyi @@ -4,6 +4,7 @@ from collections.abc import Iterable from typing import ClassVar from gunicorn.config import Config +from gunicorn.http.message import Request from gunicorn.http2.connection import _H2Connection from gunicorn.http2.request import HTTP2Request from gunicorn.http2.stream import HTTP2Stream @@ -25,6 +26,9 @@ class AsyncHTTP2Connection: def __init__(self, cfg: Config, reader: StreamReader, writer: StreamWriter, client_addr: _AddressType) -> None: ... async def initiate_connection(self) -> None: ... + async def initiate_upgrade( + self, settings_header: bytes | None, http1_req: Request, body: bytes | None = b"" + ) -> HTTP2Request: ... async def receive_data(self, timeout: float | None = None) -> list[HTTP2Request]: ... async def send_informational(self, stream_id: int, status: int, headers: Iterable[tuple[str, Incomplete]]) -> None: ... async def send_response( diff --git a/stubs/gunicorn/gunicorn/http2/connection.pyi b/stubs/gunicorn/gunicorn/http2/connection.pyi index 5ef587127e7f..d4425d160b6d 100644 --- a/stubs/gunicorn/gunicorn/http2/connection.pyi +++ b/stubs/gunicorn/gunicorn/http2/connection.pyi @@ -4,6 +4,7 @@ from ssl import SSLSocket from typing import Any, ClassVar, TypeAlias from gunicorn.config import Config +from gunicorn.http.message import Request from gunicorn.http2.request import HTTP2Request from gunicorn.http2.stream import HTTP2Stream @@ -25,8 +26,13 @@ class HTTP2ServerConnection: def __init__(self, cfg: Config, sock: SSLSocket, client_addr: _AddressType) -> None: ... def initiate_connection(self) -> None: ... + def initiate_upgrade(self, settings_header: bytes | None, http1_req: Request, body: bytes | None = None) -> HTTP2Request: ... def receive_data(self, data: bytes | None = None) -> list[HTTP2Request]: ... def send_informational(self, stream_id: int, status: int, headers: Iterable[tuple[str, Incomplete]]) -> None: ... + def send_response_headers( + self, stream_id: int, status: int, headers: Iterable[tuple[str, Incomplete]], end_stream: bool = False + ) -> bool: ... + def end_stream(self, stream_id: int, trailers: Iterable[tuple[str, Incomplete]] | None = None) -> bool: ... def send_response( self, stream_id: int, status: int, headers: Iterable[tuple[str, Incomplete]], body: bytes | None = None ) -> bool: ... diff --git a/stubs/gunicorn/gunicorn/http2/negotiation.pyi b/stubs/gunicorn/gunicorn/http2/negotiation.pyi new file mode 100644 index 000000000000..f6e8bef46fdc --- /dev/null +++ b/stubs/gunicorn/gunicorn/http2/negotiation.pyi @@ -0,0 +1,27 @@ +import socket +from typing import Final, Literal, Protocol, type_check_only + +from gunicorn.config import Config + +from .._types import _AddressType + +@type_check_only +class _HasHeaders(Protocol): + headers: list[tuple[str, str]] + +H2C_PREFACE: Final = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" +H2C_PREFACE_TIMEOUT: Final = 1.0 +MATCH: Final = "match" +PARTIAL: Final = "partial" +MISMATCH: Final = "mismatch" + +def preface_match(buf: bytes) -> Literal["match", "mismatch", "partial"]: ... +def peer_trusted_for_h2c(cfg: Config, peer_addr: _AddressType) -> bool: ... +def prior_knowledge_allowed(cfg: Config, peer_addr: _AddressType) -> bool: ... +def mismatch_is_error(cfg: Config) -> bool: ... +def upgrade_allowed(cfg: Config, peer_addr: _AddressType) -> bool: ... +def read_preface_blocking(sock: socket.socket, timeout: float | None = None) -> tuple[bool, bytes]: ... + +UPGRADE_101: Final[bytes] + +def upgrade_settings(req: _HasHeaders) -> bytes | None: ... diff --git a/stubs/gunicorn/gunicorn/http2/request.pyi b/stubs/gunicorn/gunicorn/http2/request.pyi index b9a3a377d865..ae156235adce 100644 --- a/stubs/gunicorn/gunicorn/http2/request.pyi +++ b/stubs/gunicorn/gunicorn/http2/request.pyi @@ -3,6 +3,7 @@ from collections.abc import Iterator from typing import Literal from gunicorn.config import Config +from gunicorn.http.message import HeaderPolicy from gunicorn.http2.stream import HTTP2Stream from .._types import _AddressType @@ -17,7 +18,7 @@ class HTTP2Body: def __len__(self) -> int: ... def close(self) -> None: ... -class HTTP2Request: +class HTTP2Request(HeaderPolicy): stream: HTTP2Stream cfg: Config peer_addr: _AddressType diff --git a/stubs/gunicorn/gunicorn/http2/response.pyi b/stubs/gunicorn/gunicorn/http2/response.pyi new file mode 100644 index 000000000000..660fdf8b336c --- /dev/null +++ b/stubs/gunicorn/gunicorn/http2/response.pyi @@ -0,0 +1,17 @@ +import socket + +from gunicorn.config import Config +from gunicorn.http import Request +from gunicorn.http.wsgi import Response +from gunicorn.http2.connection import HTTP2ServerConnection + +class HTTP2Response(Response): + h2_conn: HTTP2ServerConnection + stream_id: int + def __init__( + self, req: Request, sock: socket.socket, cfg: Config, h2_conn: HTTP2ServerConnection, stream_id: int + ) -> None: ... + def is_chunked(self) -> bool: ... + def can_sendfile(self) -> bool: ... + def send_headers(self) -> None: ... + def close(self) -> None: ... diff --git a/stubs/gunicorn/gunicorn/workers/base_async.pyi b/stubs/gunicorn/gunicorn/workers/base_async.pyi index 8a24aaf53b67..9fc7ea06c383 100644 --- a/stubs/gunicorn/gunicorn/workers/base_async.pyi +++ b/stubs/gunicorn/gunicorn/workers/base_async.pyi @@ -15,7 +15,14 @@ class AsyncWorker(base.Worker): def timeout_ctx(self) -> None: ... def is_already_handled(self, respiter: object) -> bool: ... def handle(self, listener: socket.socket, client: socket.socket, addr: _AddressType) -> None: ... - def handle_http2(self, listener: socket.socket, client: socket.socket, addr: _AddressType) -> None: ... + def handle_http2( + self, + listener: socket.socket, + client: socket.socket, + addr: _AddressType, + preface: bytes | None = b"", + upgrade: tuple[bytes | None, Request, bytes | None] | None = None, + ) -> None: ... def handle_http2_request( self, listener_name: _AddressType, req: Request, sock: socket.socket, addr: _AddressType, h2_conn: HTTP2ServerConnection ) -> None: ... diff --git a/stubs/gunicorn/gunicorn/workers/gthread.pyi b/stubs/gunicorn/gunicorn/workers/gthread.pyi index 51fab72f4e07..65e5058f9e2e 100644 --- a/stubs/gunicorn/gunicorn/workers/gthread.pyi +++ b/stubs/gunicorn/gunicorn/workers/gthread.pyi @@ -75,6 +75,7 @@ class ThreadWorker(base.Worker): def run(self) -> None: ... def finish_request(self, conn: TConn, fs: Future[bool]) -> None: ... def handle(self, conn: TConn) -> bool: ... + def handle_h2c_upgrade(self, conn: TConn, req: Request, settings: bytes | None) -> bool: ... def handle_http2(self, conn: TConn) -> bool: ... def handle_http2_request(self, req: Request, conn: TConn, h2_conn: HTTP2ServerConnection) -> None: ... def handle_request(self, req: Request, conn: TConn) -> bool: ...