actualizado 3-sept

This commit is contained in:
2026-09-03 19:40:44 +02:00
parent 89992823b4
commit 5d2912f789
4338 changed files with 350157 additions and 386 deletions
@@ -0,0 +1,86 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 support for Gunicorn.
This module provides HTTP/2 protocol support using the hyper-h2 library.
HTTP/2 requires TLS with ALPN negotiation.
"""
H2_MIN_VERSION = (4, 1, 0)
_h2_available = None
_h2_version = None
def is_http2_available():
"""Check if HTTP/2 support is available.
Returns:
bool: True if the h2 library is installed with minimum required version.
"""
global _h2_available, _h2_version # pylint: disable=global-statement
if _h2_available is not None:
return _h2_available
try:
import h2
version_str = getattr(h2, '__version__', '0.0.0')
version_parts = tuple(int(x) for x in version_str.split('.')[:3])
_h2_version = version_parts
_h2_available = version_parts >= H2_MIN_VERSION
except ImportError:
_h2_available = False
_h2_version = None
return _h2_available
def get_h2_version():
"""Get the installed h2 library version.
Returns:
tuple: Version tuple (major, minor, patch) or None if not installed.
"""
if _h2_version is None:
is_http2_available() # Populate _h2_version
return _h2_version
def get_http2_connection_class():
"""Get the HTTP2ServerConnection class if h2 is available.
Returns:
HTTP2ServerConnection class, or raises HTTP2NotAvailable
"""
if not is_http2_available():
from .errors import HTTP2NotAvailable
raise HTTP2NotAvailable()
from .connection import HTTP2ServerConnection
return HTTP2ServerConnection
def get_async_http2_connection_class():
"""Get the AsyncHTTP2Connection class if h2 is available.
Returns:
AsyncHTTP2Connection class, or raises HTTP2NotAvailable
"""
if not is_http2_available():
from .errors import HTTP2NotAvailable
raise HTTP2NotAvailable()
from .async_connection import AsyncHTTP2Connection
return AsyncHTTP2Connection
__all__ = [
'is_http2_available',
'get_h2_version',
'get_http2_connection_class',
'get_async_http2_connection_class',
'H2_MIN_VERSION',
]
@@ -0,0 +1,656 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
Async HTTP/2 server connection implementation for ASGI workers.
Uses the hyper-h2 library for HTTP/2 protocol handling with
asyncio for non-blocking I/O.
"""
import asyncio
import collections
from .errors import (
HTTP2Error, HTTP2ProtocolError, HTTP2ConnectionError,
HTTP2NotAvailable, HTTP2ErrorCode,
)
from .stream import HTTP2Stream
from .request import HTTP2Request
# Import h2 lazily to allow graceful fallback
_h2 = None
_h2_config = None
_h2_events = None
_h2_exceptions = None
_h2_settings = None
def _import_h2():
"""Lazily import h2 library components."""
global _h2, _h2_config, _h2_events, _h2_exceptions, _h2_settings # pylint: disable=global-statement
if _h2 is not None:
return
try:
import h2.connection as _h2
import h2.config as _h2_config
import h2.events as _h2_events
import h2.exceptions as _h2_exceptions
import h2.settings as _h2_settings
except ImportError:
raise HTTP2NotAvailable()
class AsyncHTTP2Connection:
"""Async HTTP/2 server-side connection handler for ASGI.
Manages the HTTP/2 connection state and multiplexed streams
using asyncio for non-blocking I/O operations.
"""
# Default buffer size for socket reads
READ_BUFFER_SIZE = 65536
def __init__(self, cfg, reader, writer, client_addr):
"""Initialize an async HTTP/2 server connection.
Args:
cfg: Gunicorn configuration object
reader: asyncio StreamReader
writer: asyncio StreamWriter
client_addr: Client address tuple (host, port)
Raises:
HTTP2NotAvailable: If h2 library is not installed
"""
_import_h2()
self.cfg = cfg
self.reader = reader
self.writer = writer
self.client_addr = client_addr
# Active streams indexed by stream ID
self.streams = {}
# Events pulled off the wire while blocked on a flow-control window.
# They have left the h2 state machine already, so they are held here
# for the main receive loop rather than discarded.
self._deferred_events = collections.deque()
# Queue of completed requests for the worker
self._request_queue = asyncio.Queue()
# Connection settings from config
self.initial_window_size = cfg.http2_initial_window_size
self.max_concurrent_streams = cfg.http2_max_concurrent_streams
self.max_frame_size = cfg.http2_max_frame_size
self.max_header_list_size = cfg.http2_max_header_list_size
# Initialize h2 connection
config = _h2_config.H2Configuration(
client_side=False,
header_encoding='utf-8',
)
self.h2_conn = _h2.H2Connection(config=config)
# Connection state
self._closed = False
self._initialized = False
self._receive_task = None
async def initiate_connection(self):
"""Send initial HTTP/2 settings to client.
Should be called after the SSL handshake completes and
before processing any data.
"""
if self._initialized:
return
# Update local settings before initiating
self.h2_conn.update_settings({
_h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,
_h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,
_h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,
_h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,
})
self.h2_conn.initiate_connection()
await self._send_pending_data()
self._initialized = True
async def initiate_upgrade(self, settings_header, http1_req, body=b""):
"""Switch a connection to HTTP/2 after an Upgrade: h2c request.
The async twin of HTTP2Connection.initiate_upgrade. The upgraded
request becomes stream 1 (RFC 7540 section 3.2): h2 opens it in the
state machine, and the matching gunicorn stream is built here from the
HTTP/1 request that carried the upgrade, so the worker sees an
ordinary HTTP/2 request.
The body is passed in rather than read off the request: the callback
parser hands body chunks to the protocol, not to the request object.
Returns the HTTP2Request for stream 1.
"""
self.h2_conn.update_settings({
_h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,
_h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,
_h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,
_h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,
})
self.h2_conn.initiate_upgrade_connection(settings_header=settings_header)
await self._send_pending_data()
self._initialized = True
stream = HTTP2Stream(stream_id=1, connection=self)
authority = ""
for name, value in http1_req.headers:
if name == "HOST":
authority = value
break
pseudo = [
(':method', http1_req.method),
(':path', http1_req.uri),
(':scheme', http1_req.scheme),
]
if authority:
pseudo.append((':authority', authority))
regular = [(name.lower(), value) for name, value in http1_req.headers
if name not in ("CONNECTION", "UPGRADE", "HTTP2-SETTINGS")]
stream.receive_headers(pseudo + regular, end_stream=not body)
if body:
stream.receive_data(body, end_stream=True)
self.streams[1] = stream
return HTTP2Request(stream, self.cfg, self.client_addr)
async def receive_data(self, timeout=None):
"""Receive data and return completed requests.
Args:
timeout: Optional timeout in seconds for read operation
Returns:
list: List of HTTP2Request objects for completed requests
Raises:
HTTP2ConnectionError: On protocol or connection errors
asyncio.TimeoutError: If timeout expires
"""
try:
if timeout is not None:
data = await asyncio.wait_for(
self.reader.read(self.READ_BUFFER_SIZE),
timeout=timeout
)
else:
data = await self.reader.read(self.READ_BUFFER_SIZE)
except (OSError, IOError) as e:
raise HTTP2ConnectionError(f"Socket read error: {e}")
if not data:
# Connection closed by peer
self._closed = True
return []
# Feed data to h2
# Note: Specific exceptions must come before ProtocolError (their parent class)
try:
events = self.h2_conn.receive_data(data)
except _h2_exceptions.FlowControlError as e:
# Send GOAWAY with FLOW_CONTROL_ERROR
await self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.FrameTooLargeError as e:
# Send GOAWAY with FRAME_SIZE_ERROR
await self.close(error_code=HTTP2ErrorCode.FRAME_SIZE_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.InvalidSettingsValueError as e:
# Use error_code from h2 exception (RFC 7540 Section 6.5.2):
# INITIAL_WINDOW_SIZE > 2^31-1 gives FLOW_CONTROL_ERROR
# Other invalid settings give PROTOCOL_ERROR
error_code = getattr(e, 'error_code', None)
if error_code is not None:
await self.close(error_code=error_code)
else:
await self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.TooManyStreamsError as e:
# Send GOAWAY with REFUSED_STREAM
await self.close(error_code=HTTP2ErrorCode.REFUSED_STREAM)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.ProtocolError as e:
# Send GOAWAY with PROTOCOL_ERROR before raising
await self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)
raise HTTP2ProtocolError(str(e))
# Process events, oldest first: anything set aside during a
# flow-control wait arrived before this batch.
completed_requests = []
if self._deferred_events:
events = list(self._deferred_events) + list(events)
self._deferred_events.clear()
for event in events:
request = self._handle_event(event)
if request is not None:
completed_requests.append(request)
# Send any pending data (WINDOW_UPDATE, etc.)
await self._send_pending_data()
return completed_requests
def _handle_event(self, event):
"""Handle a single h2 event.
Args:
event: h2 event object
Returns:
HTTP2Request if a request is complete, None otherwise
"""
if isinstance(event, _h2_events.RequestReceived):
return self._handle_request_received(event)
elif isinstance(event, _h2_events.DataReceived):
return self._handle_data_received(event)
elif isinstance(event, _h2_events.StreamEnded):
return self._handle_stream_ended(event)
elif isinstance(event, _h2_events.StreamReset):
self._handle_stream_reset(event)
elif isinstance(event, _h2_events.WindowUpdated):
pass # Flow control update, handled by h2
elif isinstance(event, _h2_events.PriorityUpdated):
self._handle_priority_updated(event)
elif isinstance(event, _h2_events.SettingsAcknowledged):
pass # Settings ACK received
elif isinstance(event, _h2_events.ConnectionTerminated):
self._handle_connection_terminated(event)
elif isinstance(event, _h2_events.TrailersReceived):
return self._handle_trailers_received(event)
return None
def _handle_request_received(self, event):
"""Handle RequestReceived event (HEADERS frame)."""
stream_id = event.stream_id
headers = event.headers
# Create new stream
stream = HTTP2Stream(stream_id, self)
self.streams[stream_id] = stream
# Process headers
stream.receive_headers(headers, end_stream=False)
def _handle_data_received(self, event):
"""Handle DataReceived event."""
stream_id = event.stream_id
data = event.data
stream = self.streams.get(stream_id)
if stream is None:
return None
stream.receive_data(data, end_stream=False)
# Increment flow control windows (only if data received)
if len(data) > 0:
try:
# Update stream-level window
self.h2_conn.increment_flow_control_window(len(data), stream_id=stream_id)
# Update connection-level window
self.h2_conn.increment_flow_control_window(len(data), stream_id=None)
except (ValueError, _h2_exceptions.FlowControlError):
# Window overflow - prepare GOAWAY with FLOW_CONTROL_ERROR
# (will be sent by receive_data's _send_pending_data call)
self._closed = True
try:
self.h2_conn.close_connection(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)
except Exception:
pass
return None
def _handle_stream_ended(self, event):
"""Handle StreamEnded event."""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is None:
return None
# Mark stream as request complete and body complete so the
# receive() closure's _body_complete guard fires, preventing
# the fast path from re-reading already-consumed data from BytesIO.
stream.request_complete = True
stream._body_complete = True
if stream._body_event:
stream._body_event.set()
return HTTP2Request(stream, self.cfg, self.client_addr)
def _handle_stream_reset(self, event):
"""Handle StreamReset event."""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is not None:
stream.reset(event.error_code)
def _handle_connection_terminated(self, event):
"""Handle ConnectionTerminated event."""
self._closed = True
def _handle_trailers_received(self, event):
"""Handle TrailersReceived event."""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is None:
return None
stream.receive_trailers(event.headers)
return HTTP2Request(stream, self.cfg, self.client_addr)
def _handle_priority_updated(self, event):
"""Handle PriorityUpdated event (PRIORITY frame).
Args:
event: PriorityUpdated event with priority info
"""
stream = self.streams.get(event.stream_id)
if stream is not None:
stream.update_priority(
weight=event.weight,
depends_on=event.depends_on,
exclusive=event.exclusive
)
async def send_informational(self, stream_id, status, headers):
"""Send an informational response (1xx) on a stream.
This is used for 103 Early Hints and other 1xx responses.
Informational responses are sent before the final response
and do not end the stream.
Args:
stream_id: The stream ID
status: HTTP status code (100-199)
headers: List of (name, value) header tuples
Raises:
HTTP2Error: If status is not in 1xx range
"""
if status < 100 or status >= 200:
raise HTTP2Error(f"Invalid informational status: {status}")
stream = self.streams.get(stream_id)
if stream is None:
raise HTTP2Error(f"Stream {stream_id} not found")
# Build headers with :status pseudo-header
response_headers = [(':status', str(status))]
for name, value in headers:
# HTTP/2 headers must be lowercase
response_headers.append((name.lower(), str(value)))
# Send headers with end_stream=False (informational, more to follow)
self.h2_conn.send_headers(stream_id, response_headers, end_stream=False)
await self._send_pending_data()
async def send_response(self, stream_id, status, headers, body=None):
"""Send a response on a stream.
Args:
stream_id: The stream ID to respond on
status: HTTP status code (int)
headers: List of (name, value) header tuples
body: Optional response body bytes
Returns:
bool: True if response sent, False if stream was already closed
"""
stream = self.streams.get(stream_id)
if stream is None:
# Stream was already cleaned up (reset/closed) - return gracefully
return False
# Build response headers with :status pseudo-header
response_headers = [(':status', str(status))]
for name, value in headers:
response_headers.append((name.lower(), str(value)))
end_stream = body is None or len(body) == 0
try:
# Send headers
self.h2_conn.send_headers(stream_id, response_headers, end_stream=end_stream)
stream.send_headers(response_headers, end_stream=end_stream)
await self._send_pending_data()
# Send body if present
if body and len(body) > 0:
await self.send_data(stream_id, body, end_stream=True)
return True
except _h2_exceptions.StreamClosedError:
# Stream was reset by client - clean up gracefully
stream.close()
self.cleanup_stream(stream_id)
return False
async def _wait_for_flow_control_window(self, stream_id):
"""Wait for flow control window to become positive.
Returns:
int: Available window size, or -1 if waiting failed
"""
max_wait_attempts = 50 # ~5 seconds at 100ms per attempt
for _ in range(max_wait_attempts):
available = self.h2_conn.local_flow_control_window(stream_id)
if available > 0:
return available
# Read more data from connection (may receive WINDOW_UPDATE)
try:
incoming = await asyncio.wait_for(
self.reader.read(self.READ_BUFFER_SIZE),
timeout=0.1
)
if incoming:
events = self.h2_conn.receive_data(incoming)
# Process events but don't create new requests
for event in events:
if isinstance(event, _h2_events.StreamReset):
if event.stream_id == stream_id:
return -1
elif isinstance(event, _h2_events.ConnectionTerminated):
self._closed = True
return -1
else:
# Anything else arriving alongside the
# WINDOW_UPDATE belongs to the main loop. It has
# already left the h2 state machine, so dropping
# it here loses a request or its body for good.
self._deferred_events.append(event)
await self._send_pending_data()
else:
# Connection closed
self._closed = True
return -1
except asyncio.TimeoutError:
continue
except _h2_exceptions.ProtocolError:
return -1
return self.h2_conn.local_flow_control_window(stream_id)
async def send_data(self, stream_id, data, end_stream=False):
"""Send data on a stream.
Args:
stream_id: The stream ID
data: Body data bytes
end_stream: Whether this ends the stream
Returns:
bool: True if data sent, False if stream was already closed
"""
stream = self.streams.get(stream_id)
if stream is None:
return False
data_to_send = data
try:
while data_to_send:
available = self.h2_conn.local_flow_control_window(stream_id)
chunk_size = min(available, self.max_frame_size, len(data_to_send))
if chunk_size <= 0:
# Wait for WINDOW_UPDATE per RFC 7540 Section 6.9.2
await self._send_pending_data()
available = await self._wait_for_flow_control_window(stream_id)
if available <= 0:
return False
chunk_size = min(available, self.max_frame_size, len(data_to_send))
chunk = data_to_send[:chunk_size]
data_to_send = data_to_send[chunk_size:]
is_final = end_stream and len(data_to_send) == 0
self.h2_conn.send_data(stream_id, chunk, end_stream=is_final)
await self._send_pending_data()
stream.send_data(data, end_stream=end_stream)
return True
except (_h2_exceptions.StreamClosedError, _h2_exceptions.FlowControlError):
stream.close()
self.cleanup_stream(stream_id)
return False
async def send_trailers(self, stream_id, trailers):
"""Send trailing headers on a stream.
Trailers are headers sent after the response body, commonly used
for gRPC status codes, checksums, and timing information.
Args:
stream_id: The stream ID
trailers: List of (name, value) trailer tuples
Raises:
HTTP2Error: If stream not found, headers not sent, or pseudo-headers used
Returns:
bool: True if trailers sent, False if stream was already closed
"""
stream = self.streams.get(stream_id)
if stream is None:
# Stream was already cleaned up (reset/closed) - return gracefully
return False
if not stream.response_headers_sent:
# Can't send trailers without headers - return False
return False
# Validate and normalize trailer headers
trailer_headers = []
for name, value in trailers:
lname = name.lower()
if lname.startswith(':'):
raise HTTP2Error(f"Pseudo-header '{name}' not allowed in trailers")
trailer_headers.append((lname, str(value)))
try:
# Send trailers with end_stream=True
self.h2_conn.send_headers(stream_id, trailer_headers, end_stream=True)
stream.send_trailers(trailer_headers)
await self._send_pending_data()
return True
except _h2_exceptions.StreamClosedError:
# Stream was reset by client - clean up gracefully
stream.close()
self.cleanup_stream(stream_id)
return False
async def send_error(self, stream_id, status_code, message=None):
"""Send an error response on a stream."""
body = message.encode() if message else b''
headers = [('content-length', str(len(body)))]
if body:
headers.append(('content-type', 'text/plain; charset=utf-8'))
await self.send_response(stream_id, status_code, headers, body)
async def reset_stream(self, stream_id, error_code=0x8):
"""Reset a stream with RST_STREAM."""
stream = self.streams.get(stream_id)
if stream is not None:
stream.reset(error_code)
self.h2_conn.reset_stream(stream_id, error_code=error_code)
await self._send_pending_data()
async def close(self, error_code=0x0, last_stream_id=None):
"""Close the connection gracefully with GOAWAY."""
if self._closed:
return
self._closed = True
if last_stream_id is None:
last_stream_id = max(self.streams.keys()) if self.streams else 0
try:
self.h2_conn.close_connection(error_code=error_code)
await self._send_pending_data()
except Exception:
pass
try:
self.writer.close()
await self.writer.wait_closed()
except Exception:
pass
async def _send_pending_data(self):
"""Send any pending data from h2 to the socket."""
data = self.h2_conn.data_to_send()
if data:
try:
self.writer.write(data)
await self.writer.drain()
except (OSError, IOError) as e:
self._closed = True
raise HTTP2ConnectionError(f"Socket write error: {e}")
@property
def is_closed(self):
"""Check if connection is closed."""
return self._closed
def cleanup_stream(self, stream_id):
"""Remove a stream after processing is complete."""
self.streams.pop(stream_id, None)
def __repr__(self):
return (
f"<AsyncHTTP2Connection "
f"streams={len(self.streams)} "
f"closed={self._closed}>"
)
__all__ = ['AsyncHTTP2Connection']
@@ -0,0 +1,765 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 server connection implementation.
Uses the hyper-h2 library for HTTP/2 protocol handling.
"""
import collections
import selectors
from io import BytesIO
from .errors import (
HTTP2Error, HTTP2ProtocolError, HTTP2ConnectionError,
HTTP2NotAvailable, HTTP2ErrorCode,
)
from .stream import HTTP2Stream
from .request import HTTP2Request
# Import h2 lazily to allow graceful fallback
_h2 = None
_h2_config = None
_h2_events = None
_h2_exceptions = None
_h2_settings = None
def _import_h2():
"""Lazily import h2 library components."""
global _h2, _h2_config, _h2_events, _h2_exceptions, _h2_settings # pylint: disable=global-statement
if _h2 is not None:
return
try:
import h2.connection as _h2
import h2.config as _h2_config
import h2.events as _h2_events
import h2.exceptions as _h2_exceptions
import h2.settings as _h2_settings
except ImportError:
raise HTTP2NotAvailable()
class HTTP2ServerConnection:
"""HTTP/2 server-side connection handler.
Manages the HTTP/2 connection state and multiplexed streams.
This class wraps the h2 library and provides a higher-level
interface for gunicorn workers.
"""
# Default buffer size for socket reads
READ_BUFFER_SIZE = 65536
def __init__(self, cfg, sock, client_addr):
"""Initialize an HTTP/2 server connection.
Args:
cfg: Gunicorn configuration object
sock: SSL socket with completed handshake
client_addr: Client address tuple (host, port)
Raises:
HTTP2NotAvailable: If h2 library is not installed
"""
_import_h2()
self.cfg = cfg
self.sock = sock
self.client_addr = client_addr
# Active streams indexed by stream ID
self.streams = {}
# Events pulled off the wire while blocked on a flow-control window.
# They have left the h2 state machine already, so they are held here
# for the main receive loop rather than discarded.
self._deferred_events = collections.deque()
# Completed requests ready for processing
self._pending_requests = []
# Connection settings from config
self.initial_window_size = cfg.http2_initial_window_size
self.max_concurrent_streams = cfg.http2_max_concurrent_streams
self.max_frame_size = cfg.http2_max_frame_size
self.max_header_list_size = cfg.http2_max_header_list_size
# Initialize h2 connection
config = _h2_config.H2Configuration(
client_side=False,
header_encoding='utf-8',
)
self.h2_conn = _h2.H2Connection(config=config)
# Read buffer for partial frames
self._read_buffer = BytesIO()
# Connection state
self._closed = False
self._initialized = False
def initiate_connection(self):
"""Send initial HTTP/2 settings to client.
Should be called after the SSL handshake completes and
before processing any data.
"""
if self._initialized:
return
# Update local settings before initiating
self.h2_conn.update_settings({
_h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,
_h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,
_h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,
_h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,
})
self.h2_conn.initiate_connection()
self._send_pending_data()
self._initialized = True
def initiate_upgrade(self, settings_header, http1_req, body=None):
"""Switch a connection to HTTP/2 after an Upgrade: h2c request.
The upgraded request becomes stream 1 (RFC 7540 section 3.2). h2
opens it in the state machine; the matching gunicorn stream is built
here from the HTTP/1 request that carried the upgrade, so the worker
sees an ordinary HTTP/2 request.
``body`` is the request payload. A caller that has to drain it before
collecting the bytes pipelined behind the request passes it here;
leaving it None reads it off the request.
Returns the HTTP2Request for stream 1.
"""
self.h2_conn.update_settings({
_h2_settings.SettingCodes.MAX_CONCURRENT_STREAMS: self.max_concurrent_streams,
_h2_settings.SettingCodes.INITIAL_WINDOW_SIZE: self.initial_window_size,
_h2_settings.SettingCodes.MAX_FRAME_SIZE: self.max_frame_size,
_h2_settings.SettingCodes.MAX_HEADER_LIST_SIZE: self.max_header_list_size,
})
self.h2_conn.initiate_upgrade_connection(settings_header=settings_header)
self._send_pending_data()
self._initialized = True
stream = HTTP2Stream(stream_id=1, connection=self)
authority = ""
for name, value in http1_req.headers:
if name == "HOST":
authority = value
break
pseudo = [
(':method', http1_req.method),
(':path', http1_req.uri),
(':scheme', http1_req.scheme),
]
if authority:
pseudo.append((':authority', authority))
regular = [(name.lower(), value) for name, value in http1_req.headers
if name not in ("CONNECTION", "UPGRADE", "HTTP2-SETTINGS")]
if body is None:
body = b""
if http1_req.body is not None:
body = http1_req.body.read() or b""
stream.receive_headers(pseudo + regular, end_stream=not body)
if body:
stream.receive_data(body, end_stream=True)
self.streams[1] = stream
return HTTP2Request(stream, self.cfg, self.client_addr)
def receive_data(self, data=None):
"""Process received data and return completed requests.
Args:
data: Optional bytes to process. If None, reads from socket.
Returns:
list: List of HTTP2Request objects for completed requests
Raises:
HTTP2ConnectionError: On protocol or connection errors
"""
if data is None:
try:
data = self.sock.recv(self.READ_BUFFER_SIZE)
except (OSError, IOError) as e:
raise HTTP2ConnectionError(f"Socket read error: {e}")
if not data:
# Connection closed by peer
self._closed = True
return []
# Feed data to h2
# Note: Specific exceptions must come before ProtocolError (their parent class)
try:
events = self.h2_conn.receive_data(data)
except _h2_exceptions.FlowControlError as e:
# Send GOAWAY with FLOW_CONTROL_ERROR
self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.FrameTooLargeError as e:
# Send GOAWAY with FRAME_SIZE_ERROR
self.close(error_code=HTTP2ErrorCode.FRAME_SIZE_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.InvalidSettingsValueError as e:
# Use error_code from h2 exception (RFC 7540 Section 6.5.2):
# INITIAL_WINDOW_SIZE > 2^31-1 gives FLOW_CONTROL_ERROR
# Other invalid settings give PROTOCOL_ERROR
error_code = getattr(e, 'error_code', None)
if error_code is not None:
self.close(error_code=error_code)
else:
self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.TooManyStreamsError as e:
# Send GOAWAY with REFUSED_STREAM
self.close(error_code=HTTP2ErrorCode.REFUSED_STREAM)
raise HTTP2ProtocolError(str(e))
except _h2_exceptions.ProtocolError as e:
# Send GOAWAY with PROTOCOL_ERROR before raising
self.close(error_code=HTTP2ErrorCode.PROTOCOL_ERROR)
raise HTTP2ProtocolError(str(e))
# Process events, oldest first: anything set aside during a
# flow-control wait arrived before this batch.
completed_requests = []
if self._deferred_events:
events = list(self._deferred_events) + list(events)
self._deferred_events.clear()
for event in events:
request = self._handle_event(event)
if request is not None:
completed_requests.append(request)
# Send any pending data (WINDOW_UPDATE, etc.)
self._send_pending_data()
return completed_requests
def _handle_event(self, event):
"""Handle a single h2 event.
Args:
event: h2 event object
Returns:
HTTP2Request if a request is complete, None otherwise
"""
if isinstance(event, _h2_events.RequestReceived):
return self._handle_request_received(event)
elif isinstance(event, _h2_events.DataReceived):
return self._handle_data_received(event)
elif isinstance(event, _h2_events.StreamEnded):
return self._handle_stream_ended(event)
elif isinstance(event, _h2_events.StreamReset):
self._handle_stream_reset(event)
elif isinstance(event, _h2_events.WindowUpdated):
pass # Flow control update, handled by h2
elif isinstance(event, _h2_events.PriorityUpdated):
self._handle_priority_updated(event)
elif isinstance(event, _h2_events.SettingsAcknowledged):
pass # Settings ACK received
elif isinstance(event, _h2_events.ConnectionTerminated):
self._handle_connection_terminated(event)
elif isinstance(event, _h2_events.TrailersReceived):
return self._handle_trailers_received(event)
return None
def _handle_request_received(self, event):
"""Handle RequestReceived event (HEADERS frame).
Args:
event: RequestReceived event with headers
"""
stream_id = event.stream_id
headers = event.headers
# Create new stream
stream = HTTP2Stream(stream_id, self)
self.streams[stream_id] = stream
# Process headers
# The StreamEnded event will come separately for GET/HEAD with no body
stream.receive_headers(headers, end_stream=False)
def _handle_data_received(self, event):
"""Handle DataReceived event.
Args:
event: DataReceived event with body data
Returns:
None (request completion handled by StreamEnded)
"""
stream_id = event.stream_id
data = event.data
stream = self.streams.get(stream_id)
if stream is None:
# Stream was reset or doesn't exist
return None
stream.receive_data(data, end_stream=False)
# Increment flow control windows (only if data received)
if len(data) > 0:
try:
# Update stream-level window
self.h2_conn.increment_flow_control_window(len(data), stream_id=stream_id)
# Update connection-level window
self.h2_conn.increment_flow_control_window(len(data), stream_id=None)
# Send WINDOW_UPDATE frames immediately
self._send_pending_data()
except (ValueError, _h2_exceptions.FlowControlError):
# Window overflow - send FLOW_CONTROL_ERROR and close
self.close(error_code=HTTP2ErrorCode.FLOW_CONTROL_ERROR)
return None
def _handle_stream_ended(self, event):
"""Handle StreamEnded event.
Args:
event: StreamEnded event
Returns:
HTTP2Request for the completed request
"""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is None:
return None
# Mark stream as request complete and body complete so the
# receive() closure's _body_complete guard fires, preventing
# the fast path from re-reading already-consumed data from BytesIO.
stream.request_complete = True
stream._body_complete = True
if stream._body_event:
stream._body_event.set()
# Create request object
return HTTP2Request(stream, self.cfg, self.client_addr)
def _handle_stream_reset(self, event):
"""Handle StreamReset event (RST_STREAM frame).
Args:
event: StreamReset event
"""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is not None:
stream.reset(event.error_code)
# Keep stream in dict for potential cleanup
def _handle_connection_terminated(self, event):
"""Handle ConnectionTerminated event (GOAWAY frame).
Args:
event: ConnectionTerminated event
"""
self._closed = True
# Could log event.error_code and event.additional_data
def _handle_trailers_received(self, event):
"""Handle TrailersReceived event.
Args:
event: TrailersReceived event with trailer headers
Returns:
HTTP2Request if this completes the request
"""
stream_id = event.stream_id
stream = self.streams.get(stream_id)
if stream is None:
return None
stream.receive_trailers(event.headers)
# Trailers always end the request
return HTTP2Request(stream, self.cfg, self.client_addr)
def _handle_priority_updated(self, event):
"""Handle PriorityUpdated event (PRIORITY frame).
Args:
event: PriorityUpdated event with priority info
"""
stream = self.streams.get(event.stream_id)
if stream is not None:
stream.update_priority(
weight=event.weight,
depends_on=event.depends_on,
exclusive=event.exclusive
)
def send_informational(self, stream_id, status, headers):
"""Send an informational response (1xx) on a stream.
This is used for 103 Early Hints and other 1xx responses.
Informational responses are sent before the final response
and do not end the stream.
Args:
stream_id: The stream ID
status: HTTP status code (100-199)
headers: List of (name, value) header tuples
Raises:
HTTP2Error: If status is not in 1xx range
"""
if status < 100 or status >= 200:
raise HTTP2Error(f"Invalid informational status: {status}")
stream = self.streams.get(stream_id)
if stream is None:
raise HTTP2Error(f"Stream {stream_id} not found")
# Build headers with :status pseudo-header
response_headers = [(':status', str(status))]
for name, value in headers:
# HTTP/2 headers must be lowercase
response_headers.append((name.lower(), str(value)))
# Send headers with end_stream=False (informational, more to follow)
self.h2_conn.send_headers(stream_id, response_headers, end_stream=False)
self._send_pending_data()
def send_response_headers(self, stream_id, status, headers,
end_stream=False):
"""Send response headers on a stream without ending it.
Returns False if the stream is already gone. Split out of
send_response() so a response can be streamed: headers first, then
any number of data frames, then end_stream().
"""
stream = self.streams.get(stream_id)
if stream is None:
# Stream was already cleaned up (reset/closed)
return False
# Build response headers with :status pseudo-header
response_headers = [(':status', str(status))]
for name, value in headers:
# HTTP/2 headers must be lowercase
response_headers.append((name.lower(), str(value)))
self.h2_conn.send_headers(stream_id, response_headers,
end_stream=end_stream)
stream.send_headers(response_headers, end_stream=end_stream)
self._send_pending_data()
return True
def end_stream(self, stream_id, trailers=None):
"""Close the sending half of a stream, with trailers if given."""
if self.streams.get(stream_id) is None:
return False
if trailers:
self.send_trailers(stream_id, trailers)
return True
# Not send_data(): it chunks against the flow-control window and an
# empty payload skips that loop entirely, so END_STREAM would never
# reach the peer and the client would wait for a response that is
# already finished.
# Not send_data(): it chunks against the flow-control window and an
# empty payload skips that loop entirely, so END_STREAM would never
# reach the peer and the client would wait for a response that is
# already finished.
try:
self.h2_conn.send_data(stream_id, b"", end_stream=True)
self.streams[stream_id].send_data(b"", end_stream=True)
self._send_pending_data()
except _h2_exceptions.StreamClosedError:
self.cleanup_stream(stream_id)
return False
return True
def send_response(self, stream_id, status, headers, body=None):
"""Send a response on a stream.
Args:
stream_id: The stream ID to respond on
status: HTTP status code (int)
headers: List of (name, value) header tuples
body: Optional response body bytes
Raises:
HTTP2Error: If stream not found or in invalid state
Returns:
bool: True if response sent, False if stream was already closed
"""
end_stream = body is None or len(body) == 0
try:
if not self.send_response_headers(stream_id, status, headers,
end_stream=end_stream):
return False
# Send body if present
if body and len(body) > 0:
self.send_data(stream_id, body, end_stream=True)
return True
except _h2_exceptions.StreamClosedError:
# Stream was reset by client - clean up gracefully
stream = self.streams.get(stream_id)
if stream is not None:
stream.close()
self.cleanup_stream(stream_id)
return False
def _wait_for_flow_control_window(self, stream_id):
"""Wait for flow control window to become positive.
Returns:
int: Available window size, or -1 if waiting failed
"""
max_wait_attempts = 50 # ~5 seconds at 100ms per attempt
try:
sel = selectors.DefaultSelector()
sel.register(self.sock, selectors.EVENT_READ)
except (TypeError, ValueError):
# Socket doesn't support selectors (e.g., mock socket)
return -1
result = -1
try:
for _ in range(max_wait_attempts):
available = self.h2_conn.local_flow_control_window(stream_id)
if available > 0:
result = available
break
ready = sel.select(timeout=0.1)
if ready:
try:
incoming = self.sock.recv(self.READ_BUFFER_SIZE)
except (OSError, IOError, _h2_exceptions.ProtocolError):
break
if not incoming:
self._closed = True
break
try:
events = self.h2_conn.receive_data(incoming)
except _h2_exceptions.ProtocolError:
break
for event in events:
if isinstance(event, _h2_events.StreamReset):
if event.stream_id == stream_id:
result = -1
break
elif isinstance(event, _h2_events.ConnectionTerminated):
self._closed = True
result = -1
break
else:
# Anything else arriving alongside the
# WINDOW_UPDATE belongs to the main loop. It has
# already left the h2 state machine, so dropping
# it here loses a request or its body for good.
self._deferred_events.append(event)
else:
self._send_pending_data()
continue
break # Break outer loop if inner loop broke
else:
# Loop completed without break - check final window
result = self.h2_conn.local_flow_control_window(stream_id)
finally:
sel.close()
return result
def send_data(self, stream_id, data, end_stream=False):
"""Send data on a stream.
Args:
stream_id: The stream ID
data: Body data bytes
end_stream: Whether this ends the stream
Returns:
bool: True if data sent, False if stream was already closed
"""
stream = self.streams.get(stream_id)
if stream is None:
return False
data_to_send = data
try:
while data_to_send:
available = self.h2_conn.local_flow_control_window(stream_id)
chunk_size = min(available, self.max_frame_size, len(data_to_send))
if chunk_size <= 0:
# Wait for WINDOW_UPDATE per RFC 7540 Section 6.9.2
self._send_pending_data()
available = self._wait_for_flow_control_window(stream_id)
if available <= 0:
return False
chunk_size = min(available, self.max_frame_size, len(data_to_send))
chunk = data_to_send[:chunk_size]
data_to_send = data_to_send[chunk_size:]
is_final = end_stream and len(data_to_send) == 0
self.h2_conn.send_data(stream_id, chunk, end_stream=is_final)
self._send_pending_data()
stream.send_data(data, end_stream=end_stream)
return True
except (_h2_exceptions.StreamClosedError, _h2_exceptions.FlowControlError):
# Stream was reset by client or flow control error - clean up gracefully
stream.close()
self.cleanup_stream(stream_id)
return False
def send_trailers(self, stream_id, trailers):
"""Send trailing headers on a stream.
Trailers are headers sent after the response body, commonly used
for gRPC status codes, checksums, and timing information.
Args:
stream_id: The stream ID
trailers: List of (name, value) trailer tuples
Raises:
HTTP2Error: If stream not found, headers not sent, or pseudo-headers used
Returns:
bool: True if trailers sent, False if stream was already closed
"""
stream = self.streams.get(stream_id)
if stream is None:
# Stream was already cleaned up (reset/closed) - return gracefully
return False
if not stream.response_headers_sent:
# Can't send trailers without headers - return False
return False
# Validate and normalize trailer headers
trailer_headers = []
for name, value in trailers:
lname = name.lower()
if lname.startswith(':'):
raise HTTP2Error(f"Pseudo-header '{name}' not allowed in trailers")
trailer_headers.append((lname, str(value)))
try:
# Send trailers with end_stream=True
self.h2_conn.send_headers(stream_id, trailer_headers, end_stream=True)
stream.send_trailers(trailer_headers)
self._send_pending_data()
return True
except _h2_exceptions.StreamClosedError:
# Stream was reset by client - clean up gracefully
stream.close()
self.cleanup_stream(stream_id)
return False
def send_error(self, stream_id, status_code, message=None):
"""Send an error response on a stream.
Args:
stream_id: The stream ID
status_code: HTTP status code
message: Optional error message body
"""
body = message.encode() if message else b''
headers = [('content-length', str(len(body)))]
if body:
headers.append(('content-type', 'text/plain; charset=utf-8'))
self.send_response(stream_id, status_code, headers, body)
def reset_stream(self, stream_id, error_code=0x8):
"""Reset a stream with RST_STREAM.
Args:
stream_id: The stream ID to reset
error_code: HTTP/2 error code (default: CANCEL)
"""
stream = self.streams.get(stream_id)
if stream is not None:
stream.reset(error_code)
self.h2_conn.reset_stream(stream_id, error_code=error_code)
self._send_pending_data()
def close(self, error_code=0x0, last_stream_id=None):
"""Close the connection gracefully with GOAWAY.
Args:
error_code: HTTP/2 error code (default: NO_ERROR)
last_stream_id: Last processed stream ID (default: highest)
"""
if self._closed:
return
self._closed = True
if last_stream_id is None:
# Use highest stream ID we've seen
last_stream_id = max(self.streams.keys()) if self.streams else 0
try:
self.h2_conn.close_connection(error_code=error_code)
self._send_pending_data()
except Exception:
pass # Best effort
def _send_pending_data(self):
"""Send any pending data from h2 to the socket."""
data = self.h2_conn.data_to_send()
if data:
try:
self.sock.sendall(data)
except (OSError, IOError) as e:
self._closed = True
raise HTTP2ConnectionError(f"Socket write error: {e}")
@property
def is_closed(self):
"""Check if connection is closed."""
return self._closed
def cleanup_stream(self, stream_id):
"""Remove a stream after processing is complete.
Args:
stream_id: The stream ID to clean up
"""
self.streams.pop(stream_id, None)
def __repr__(self):
return (
f"<HTTP2ServerConnection "
f"streams={len(self.streams)} "
f"closed={self._closed}>"
)
__all__ = ['HTTP2ServerConnection']
+169
View File
@@ -0,0 +1,169 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 specific exceptions.
These exceptions map to HTTP/2 error codes defined in RFC 7540.
"""
class HTTP2ErrorCode:
"""HTTP/2 Error Codes (RFC 7540 Section 7)."""
NO_ERROR = 0x0
PROTOCOL_ERROR = 0x1
INTERNAL_ERROR = 0x2
FLOW_CONTROL_ERROR = 0x3
SETTINGS_TIMEOUT = 0x4
STREAM_CLOSED = 0x5
FRAME_SIZE_ERROR = 0x6
REFUSED_STREAM = 0x7
CANCEL = 0x8
COMPRESSION_ERROR = 0x9
CONNECT_ERROR = 0xa
ENHANCE_YOUR_CALM = 0xb
INADEQUATE_SECURITY = 0xc
HTTP_1_1_REQUIRED = 0xd
class HTTP2Error(Exception):
"""Base exception for HTTP/2 errors."""
error_code = 0x0 # NO_ERROR
def __init__(self, message=None, error_code=None):
self.message = message or self.__class__.__doc__
if error_code is not None:
self.error_code = error_code
super().__init__(self.message)
class HTTP2ProtocolError(HTTP2Error):
"""Protocol error detected."""
error_code = 0x1 # PROTOCOL_ERROR
class HTTP2InternalError(HTTP2Error):
"""Internal error occurred."""
error_code = 0x2 # INTERNAL_ERROR
class HTTP2FlowControlError(HTTP2Error):
"""Flow control limits exceeded."""
error_code = 0x3 # FLOW_CONTROL_ERROR
class HTTP2SettingsTimeout(HTTP2Error):
"""Settings acknowledgment timeout."""
error_code = 0x4 # SETTINGS_TIMEOUT
class HTTP2StreamClosed(HTTP2Error):
"""Stream was closed."""
error_code = 0x5 # STREAM_CLOSED
class HTTP2FrameSizeError(HTTP2Error):
"""Frame size is incorrect."""
error_code = 0x6 # FRAME_SIZE_ERROR
class HTTP2RefusedStream(HTTP2Error):
"""Stream was refused."""
error_code = 0x7 # REFUSED_STREAM
class HTTP2Cancel(HTTP2Error):
"""Stream was cancelled."""
error_code = 0x8 # CANCEL
class HTTP2CompressionError(HTTP2Error):
"""Compression state error."""
error_code = 0x9 # COMPRESSION_ERROR
class HTTP2ConnectError(HTTP2Error):
"""Connection error during CONNECT."""
error_code = 0xa # CONNECT_ERROR
class HTTP2EnhanceYourCalm(HTTP2Error):
"""Peer is generating excessive load."""
error_code = 0xb # ENHANCE_YOUR_CALM
class HTTP2InadequateSecurity(HTTP2Error):
"""Transport security is inadequate."""
error_code = 0xc # INADEQUATE_SECURITY
class HTTP2RequiresHTTP11(HTTP2Error):
"""HTTP/1.1 is required for this request."""
error_code = 0xd # HTTP_1_1_REQUIRED
class HTTP2StreamError(HTTP2Error):
"""Error specific to a single stream."""
def __init__(self, stream_id, message=None, error_code=None):
self.stream_id = stream_id
super().__init__(message, error_code)
def __str__(self):
return f"Stream {self.stream_id}: {self.message}"
class HTTP2ConnectionError(HTTP2Error):
"""Error affecting the entire connection."""
class HTTP2ConfigurationError(HTTP2Error):
"""Invalid HTTP/2 configuration."""
class HTTP2NotAvailable(HTTP2Error):
"""HTTP/2 support is not available (h2 library not installed)."""
def __init__(self, message=None):
message = message or "HTTP/2 requires the h2 library: pip install gunicorn[http2]"
super().__init__(message)
__all__ = [
'HTTP2ErrorCode',
'HTTP2Error',
'HTTP2ProtocolError',
'HTTP2InternalError',
'HTTP2FlowControlError',
'HTTP2SettingsTimeout',
'HTTP2StreamClosed',
'HTTP2FrameSizeError',
'HTTP2RefusedStream',
'HTTP2Cancel',
'HTTP2CompressionError',
'HTTP2ConnectError',
'HTTP2EnhanceYourCalm',
'HTTP2InadequateSecurity',
'HTTP2RequiresHTTP11',
'HTTP2StreamError',
'HTTP2ConnectionError',
'HTTP2ConfigurationError',
'HTTP2NotAvailable',
]
@@ -0,0 +1,170 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""Cleartext HTTP/2 (h2c) negotiation, shared by every worker.
The I/O differs per worker: gthread and gevent read from a socket, the ASGI
worker is handed bytes by asyncio. The decisions do not. Keeping the pure,
I/O-free part here stops the blocking and push-based paths from drifting apart.
"""
import time
from gunicorn.http.message import _ip_in_allow_list
#: HTTP/2 connection preface sent by clients using prior knowledge,
#: RFC 9113 section 3.4.
H2C_PREFACE = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n"
#: How long to wait for the whole preface once the first bytes arrive. This
#: is a budget for the entire preface, not per read.
H2C_PREFACE_TIMEOUT = 1.0
MATCH = "match"
PARTIAL = "partial"
MISMATCH = "mismatch"
def preface_match(buf):
"""Compare buffered bytes against the connection preface.
Returns ``MATCH`` when the whole preface is present, ``PARTIAL`` when the
bytes so far are a prefix of it and more could still arrive, and
``MISMATCH`` as soon as a byte diverges. Never blocks and never reads.
"""
if len(buf) >= len(H2C_PREFACE):
return MATCH if buf.startswith(H2C_PREFACE) else MISMATCH
return PARTIAL if H2C_PREFACE.startswith(buf) else MISMATCH
def peer_trusted_for_h2c(cfg, peer_addr):
"""Whether this peer may negotiate cleartext HTTP/2.
Reuses the ``forwarded_allow_ips`` trust list: h2c is only ever expected
from the TLS-terminating proxy in front of gunicorn, which is the same
peer already trusted to set forwarded headers. Unix socket peers are
trusted, matching that policy.
"""
if not isinstance(peer_addr, tuple):
return True
return _ip_in_allow_list(
peer_addr[0], cfg.forwarded_allow_ips, cfg.forwarded_allow_networks()
)
def _h2c_available(cfg):
"""Whether cleartext HTTP/2 could apply to this server at all."""
return (
"h2" in cfg.http_protocols
and getattr(cfg, "protocol", "http") == "http"
and not cfg.is_ssl
)
def prior_knowledge_allowed(cfg, peer_addr):
"""Whether to sniff for the connection preface from this peer.
Deliberately separate from :func:`upgrade_allowed`: enabling one mechanism
must not quietly enable the other.
"""
if cfg.http2_cleartext not in ("prior-knowledge", "both"):
return False
return _h2c_available(cfg) and peer_trusted_for_h2c(cfg, peer_addr)
def mismatch_is_error(cfg):
"""Whether a trusted peer failing to send the preface is a 400.
Only when prior knowledge is the sole mechanism: such a peer is expected
to speak HTTP/2 and a silent downgrade would hide a misconfiguration.
When upgrade is also enabled, an HTTP/1 request is not a mistake, it is
how an upgrade begins, so it has to be allowed through.
"""
return cfg.http2_cleartext == "prior-knowledge"
def upgrade_allowed(cfg, peer_addr):
"""Whether to honour an ``Upgrade: h2c`` request from this peer."""
if cfg.http2_cleartext not in ("upgrade", "both"):
return False
return _h2c_available(cfg) and peer_trusted_for_h2c(cfg, peer_addr)
def read_preface_blocking(sock, timeout=None):
"""Read up to the length of the preface from a blocking socket.
Returns ``(matched, consumed_bytes)``. The caller owns the consumed bytes
and must hand them to whichever protocol wins, since they have already
left the socket.
The timeout is an absolute budget for the whole preface, checked before
every read. ``socket.settimeout()`` alone would bound each call instead,
which lets a client trickle one byte per interval and hold the connection
(and, on gthread, a pool slot) for as many intervals as the preface has
bytes.
"""
if timeout is None:
# read at call time so the module attribute stays adjustable
timeout = H2C_PREFACE_TIMEOUT
buf = b""
deadline = time.monotonic() + timeout
original = sock.gettimeout()
try:
while True:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False, buf
sock.settimeout(remaining)
try:
chunk = sock.recv(len(H2C_PREFACE) - len(buf))
except (TimeoutError, OSError):
return False, buf
if not chunk:
return False, buf
buf += chunk
state = preface_match(buf)
if state is MATCH:
return True, buf
if state is MISMATCH:
return False, buf
finally:
sock.settimeout(original)
#: Sent before switching an HTTP/1.1 connection over to HTTP/2.
UPGRADE_101 = (
b"HTTP/1.1 101 Switching Protocols\r\n"
b"Connection: Upgrade\r\n"
b"Upgrade: h2c\r\n"
b"\r\n"
)
def upgrade_settings(req):
"""Return the HTTP2-Settings payload if this request asks for h2c.
RFC 7540 section 3.2: the request must name ``h2c`` in Upgrade and carry
exactly one HTTP2-Settings header, itself named in Connection. Returns
None when the request is not a well-formed upgrade attempt, so the caller
simply carries on with HTTP/1.
"""
upgrade = None
settings = []
connection = ""
for name, value in req.headers:
if name == "UPGRADE":
upgrade = value.strip().lower()
elif name == "HTTP2-SETTINGS":
settings.append(value.strip())
elif name == "CONNECTION":
connection = value.lower()
if upgrade != "h2c":
return None
# Exactly one, per RFC 7540 3.2.1: a second one is ambiguous.
if len(settings) != 1:
return None
if "http2-settings" not in connection or "upgrade" not in connection:
return None
return settings[0].encode("latin-1")
+267
View File
@@ -0,0 +1,267 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 request wrapper.
Provides a Request-compatible interface for HTTP/2 streams.
"""
from io import BytesIO
from gunicorn.http.message import (
HeaderPolicy,
RFC9110_5_5_INVALID_AND_DANGEROUS,
)
from gunicorn.http.errors import InvalidHeader
from gunicorn.util import split_request_uri
class HTTP2Body:
"""Body wrapper for HTTP/2 request data.
Provides a file-like interface to the request body,
compatible with gunicorn's Body class expectations.
"""
def __init__(self, data):
"""Initialize with body data.
Args:
data: bytes containing the request body
"""
self._data = BytesIO(data)
self._len = len(data)
def read(self, size=None):
"""Read data from the body.
Args:
size: Number of bytes to read, or None for all remaining
Returns:
bytes: The requested data
"""
if size is None:
return self._data.read()
return self._data.read(size)
def readline(self, size=None):
"""Read a line from the body.
Args:
size: Maximum bytes to read
Returns:
bytes: A line of data
"""
if size is None:
return self._data.readline()
return self._data.readline(size)
def readlines(self, hint=None):
"""Read all lines from the body.
Args:
hint: Approximate byte count hint
Returns:
list: List of lines
"""
return self._data.readlines(hint)
def __iter__(self):
"""Iterate over lines in the body."""
return iter(self._data)
def __len__(self):
"""Return the content length."""
return self._len
def close(self):
"""Close the body stream."""
self._data.close()
class HTTP2Request(HeaderPolicy):
"""HTTP/2 request wrapper compatible with gunicorn Request interface.
Wraps an HTTP2Stream to provide the same interface as the HTTP/1.x
Request class, allowing workers to handle HTTP/2 requests using
existing code paths.
"""
#: HTTP/2 carries no 100-continue handshake gunicorn can answer: the
#: response would be written as HTTP/1 bytes onto an HTTP/2 connection.
_policy_expect_continue = False
def __init__(self, stream, cfg, peer_addr):
"""Initialize from an HTTP/2 stream.
Args:
stream: HTTP2Stream instance with received headers/body
cfg: Gunicorn configuration object
peer_addr: Client address tuple (host, port)
"""
self.stream = stream
self.cfg = cfg
self.peer_addr = peer_addr
self.remote_addr = peer_addr
# HTTP/2 version tuple
self.version = (2, 0)
# Parse pseudo-headers
pseudo = stream.get_pseudo_headers()
self.method = pseudo.get(':method', 'GET')
# Derive the scheme from the transport, as HTTP/1 does. A client
# supplied :scheme is honoured only from a peer allowed to speak for
# the connection; otherwise it is ignored rather than rejected, which
# mirrors how an untrusted X-Forwarded-Proto is treated on HTTP/1.
self.scheme = "https" if cfg.is_ssl else "http"
claimed_scheme = pseudo.get(':scheme')
if claimed_scheme and self._peer_is_trusted_proxy():
self.scheme = claimed_scheme
authority = pseudo.get(':authority', '')
path = pseudo.get(':path', '/')
# Parse the path into components
self.uri = path
try:
parts = split_request_uri(path)
self.path = parts.path or ""
self.query = parts.query or ""
self.fragment = parts.fragment or ""
except ValueError:
self.path = path
self.query = ""
self.fragment = ""
# Store authority for Host header equivalent
self._authority = authority
# Convert HTTP/2 headers to HTTP/1.1 style and put them through the
# same policy as HTTP/1, so a rule cannot hold on one protocol and be
# skipped on the other.
self.headers = []
scheme_state = [False]
seen = set()
secure_scheme_headers, forwarder_headers = \
self._peer_trusted_for_forwarded()
for name, value in stream.get_regular_headers():
# Convert to uppercase for WSGI compatibility
name = name.upper()
if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):
raise InvalidHeader(name, req=self)
kept = self._apply_header_policy(
name, value, scheme_state, seen,
secure_scheme_headers, forwarder_headers,
)
if kept is None:
continue
self.headers.append(kept)
# Set Host header from :authority (RFC 9113 section 8.3.1)
# :authority MUST take precedence over Host header. Runs after the
# policy so a duplicate Host is still rejected rather than replaced.
if authority:
self.headers = [(n, v) for n, v in self.headers if n != 'HOST']
self.headers.append(('HOST', authority))
# Trailers (if any)
self.trailers = []
if stream.trailers:
self.trailers = [
(name.upper(), value)
for name, value in stream.trailers
]
# Body - HTTP/2 streams have complete body data
body_data = stream.get_request_body()
self.body = HTTP2Body(body_data)
# Connection state
self.must_close = False
# Never set on HTTP/2: gunicorn answers it with HTTP/1 bytes written
# straight to the socket, which would corrupt the connection.
self._expected_100_continue = False
# Request numbering (for logging)
self.req_number = stream.stream_id
# HTTP/2 does not use proxy protocol through the data stream
self.proxy_protocol_info = None
# Stream priority (RFC 7540 Section 5.3)
self.priority_weight = stream.priority_weight
self.priority_depends_on = stream.priority_depends_on
def force_close(self):
"""Force the connection to close after this request."""
self.must_close = True
def should_close(self):
"""Check if connection should close after this request.
HTTP/2 connections are persistent by design, but we may still
need to close if explicitly requested.
Returns:
bool: True if connection should close
"""
if self.must_close:
return True
# HTTP/2 connections are persistent, don't close by default
return False
def get_header(self, name):
"""Get a header value by name.
Args:
name: Header name (case-insensitive)
Returns:
str: Header value, or None if not found
"""
name = name.upper()
for h_name, h_value in self.headers:
if h_name == name:
return h_value
return None
@property
def content_length(self):
"""Get the Content-Length header value.
Returns:
int: Content length, or None if not set
"""
cl = self.get_header('CONTENT-LENGTH')
if cl is not None:
try:
return int(cl)
except ValueError:
pass
return None
@property
def content_type(self):
"""Get the Content-Type header value.
Returns:
str: Content type, or None if not set
"""
return self.get_header('CONTENT-TYPE')
def __repr__(self):
return (
f"<HTTP2Request "
f"method={self.method} "
f"path={self.path} "
f"stream_id={self.stream.stream_id}>"
)
__all__ = ['HTTP2Request', 'HTTP2Body']
@@ -0,0 +1,59 @@
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""WSGI response writer for HTTP/2 streams."""
from gunicorn.http.wsgi import Response
class HTTP2Response(Response):
"""A WSGI Response that frames its output as HTTP/2 instead of HTTP/1.
Only the wire framing is overridden. Everything the WSGI protocol needs
(``start_response``, header processing, the no-body rules for HEAD, 1xx,
204 and 304, the Content-Length accounting in ``write()``) is inherited,
so HTTP/2 responses obey the same rules as HTTP/1 ones rather than a
parallel set that has to be kept in step by hand.
"""
def __init__(self, req, sock, cfg, h2_conn, stream_id):
# sock is unused: every write goes through the HTTP/2 connection.
# The signature matches Response so wsgi.create() can build either.
super().__init__(req, sock, cfg)
self.h2_conn = h2_conn
self.stream_id = stream_id
self._stream_ended = False
def is_chunked(self):
# HTTP/2 has its own framing; chunked transfer coding is forbidden
# (RFC 9113 section 8.1).
return False
def can_sendfile(self):
# sendfile() writes raw bytes to a socket, which would bypass HTTP/2
# framing entirely. Base Response guards this with cfg.is_ssl, which
# happens to cover HTTP/2 over TLS but not over cleartext.
return False
def send_headers(self):
if self.headers_sent:
return
self.h2_conn.send_response_headers(
self.stream_id, self.status_code, self.headers, end_stream=False
)
self.headers_sent = True
def _emit_body(self, data):
if not data:
return
self.h2_conn.send_data(self.stream_id, data, end_stream=False)
def close(self):
if not self.headers_sent:
self.send_headers()
if self._stream_ended:
return
self._stream_ended = True
trailers = getattr(self, "trailers", None)
self.h2_conn.end_stream(self.stream_id, trailers=trailers)
+364
View File
@@ -0,0 +1,364 @@
# -*- coding: utf-8 -
#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
"""
HTTP/2 stream state management.
Each HTTP/2 stream represents a single request/response exchange.
"""
from enum import Enum, auto
from io import BytesIO
from .errors import HTTP2StreamError
class StreamState(Enum):
"""HTTP/2 stream states as defined in RFC 7540 Section 5.1."""
IDLE = auto()
RESERVED_LOCAL = auto()
RESERVED_REMOTE = auto()
OPEN = auto()
HALF_CLOSED_LOCAL = auto()
HALF_CLOSED_REMOTE = auto()
CLOSED = auto()
class HTTP2Stream:
"""Represents a single HTTP/2 stream.
Manages stream state, headers, and body data for a single
request/response exchange within an HTTP/2 connection.
"""
def __init__(self, stream_id, connection):
"""Initialize an HTTP/2 stream.
Args:
stream_id: The unique stream identifier (odd for client-initiated)
connection: The parent HTTP2ServerConnection
"""
self.stream_id = stream_id
self.connection = connection
# Stream state
self.state = StreamState.IDLE
# Request data
self.request_headers = []
self.request_body = BytesIO()
self.request_complete = False
# Response data
self.response_started = False
self.response_headers_sent = False
self.response_complete = False
# Flow control
self.window_size = connection.initial_window_size
# Request trailers
self.trailers = None
# Response trailers
self.response_trailers = None
# Stream priority (RFC 7540 Section 5.3)
self.priority_weight = 16
self.priority_depends_on = 0
self.priority_exclusive = False
# Streaming body support (avoids buffering entire uploads)
self._body_chunks = []
self._body_event = None # Lazy-init asyncio.Event
self._body_complete = False
@property
def is_client_stream(self):
"""Check if this is a client-initiated stream (odd stream ID)."""
return self.stream_id % 2 == 1
@property
def is_server_stream(self):
"""Check if this is a server-initiated stream (even stream ID)."""
return self.stream_id % 2 == 0
@property
def can_receive(self):
"""Check if this stream can receive data."""
return self.state in (
StreamState.OPEN,
StreamState.HALF_CLOSED_LOCAL,
)
@property
def can_send(self):
"""Check if this stream can send data."""
return self.state in (
StreamState.OPEN,
StreamState.HALF_CLOSED_REMOTE,
)
def receive_headers(self, headers, end_stream=False):
"""Process received HEADERS frame.
Args:
headers: List of (name, value) tuples
end_stream: True if END_STREAM flag is set
Raises:
HTTP2StreamError: If headers received in invalid state
"""
if self.state == StreamState.IDLE:
self.state = StreamState.OPEN
elif self.state not in (StreamState.OPEN, StreamState.HALF_CLOSED_LOCAL):
raise HTTP2StreamError(
self.stream_id,
f"Cannot receive headers in state {self.state.name}"
)
self.request_headers.extend(headers)
if end_stream:
self._half_close_remote()
self.request_complete = True
def receive_data(self, data, end_stream=False):
"""Process received DATA frame with streaming support.
Args:
data: Bytes received
end_stream: True if END_STREAM flag is set
Raises:
HTTP2StreamError: If data received in invalid state
"""
if not self.can_receive:
raise HTTP2StreamError(
self.stream_id,
f"Cannot receive data in state {self.state.name}"
)
# Add to chunks queue for streaming reads
if data:
self._body_chunks.append(data)
if self._body_event:
self._body_event.set()
# Also write to legacy BytesIO for compatibility
self.request_body.write(data)
if end_stream:
self._half_close_remote()
self.request_complete = True
self._body_complete = True
if self._body_event:
self._body_event.set()
def receive_trailers(self, trailers):
"""Process received trailing headers.
Args:
trailers: List of (name, value) tuples
"""
if not self.can_receive:
raise HTTP2StreamError(
self.stream_id,
f"Cannot receive trailers in state {self.state.name}"
)
self.trailers = trailers
self._half_close_remote()
self.request_complete = True
def send_headers(self, headers, end_stream=False):
"""Mark headers as sent.
Args:
headers: List of (name, value) tuples to send
end_stream: True if this completes the response
Raises:
HTTP2StreamError: If headers cannot be sent in current state
"""
if not self.can_send:
raise HTTP2StreamError(
self.stream_id,
f"Cannot send headers in state {self.state.name}"
)
self.response_started = True
self.response_headers_sent = True
if end_stream:
self._half_close_local()
self.response_complete = True
def send_data(self, data, end_stream=False):
"""Mark data as sent.
Args:
data: Bytes to send
end_stream: True if this completes the response
Raises:
HTTP2StreamError: If data cannot be sent in current state
"""
if not self.can_send:
raise HTTP2StreamError(
self.stream_id,
f"Cannot send data in state {self.state.name}"
)
if end_stream:
self._half_close_local()
self.response_complete = True
def send_trailers(self, trailers):
"""Mark trailers as sent and close the stream.
Args:
trailers: List of (name, value) trailer tuples
Raises:
HTTP2StreamError: If trailers cannot be sent in current state
"""
if not self.can_send:
raise HTTP2StreamError(
self.stream_id,
f"Cannot send trailers in state {self.state.name}"
)
self.response_trailers = trailers
self._half_close_local()
self.response_complete = True
def reset(self, error_code=0x8):
"""Reset this stream with RST_STREAM.
Args:
error_code: HTTP/2 error code (default: CANCEL)
"""
self.state = StreamState.CLOSED
self.response_complete = True
self.request_complete = True
def close(self):
"""Close this stream normally."""
self.state = StreamState.CLOSED
self.response_complete = True
self.request_complete = True
def update_priority(self, weight=None, depends_on=None, exclusive=None):
"""Update stream priority from PRIORITY frame.
Args:
weight: Priority weight (1-256), higher = more resources
depends_on: Stream ID this stream depends on
exclusive: Whether this is an exclusive dependency
"""
if weight is not None:
self.priority_weight = max(1, min(256, weight))
if depends_on is not None:
self.priority_depends_on = depends_on
if exclusive is not None:
self.priority_exclusive = exclusive
def _half_close_local(self):
"""Transition to half-closed (local) state."""
if self.state == StreamState.OPEN:
self.state = StreamState.HALF_CLOSED_LOCAL
elif self.state == StreamState.HALF_CLOSED_REMOTE:
self.state = StreamState.CLOSED
else:
raise HTTP2StreamError(
self.stream_id,
f"Cannot half-close local in state {self.state.name}"
)
def _half_close_remote(self):
"""Transition to half-closed (remote) state."""
if self.state == StreamState.OPEN:
self.state = StreamState.HALF_CLOSED_REMOTE
elif self.state == StreamState.HALF_CLOSED_LOCAL:
self.state = StreamState.CLOSED
else:
raise HTTP2StreamError(
self.stream_id,
f"Cannot half-close remote in state {self.state.name}"
)
def get_request_body(self):
"""Get the complete request body.
Returns:
bytes: The request body data
"""
return self.request_body.getvalue()
async def read_body_chunk(self):
"""Read next body chunk asynchronously for streaming.
Returns:
bytes: Next chunk of body data, or None if body is complete.
"""
import asyncio
# Initialize event lazily (avoids event loop issues at construction)
if self._body_event is None:
self._body_event = asyncio.Event()
# If data already arrived before event existed, set it now
# This prevents race where DATA frames arrive before first read
if self._body_chunks or self._body_complete:
self._body_event.set()
while True:
# Return chunk if available
if self._body_chunks:
return self._body_chunks.pop(0)
# No more data expected
if self._body_complete:
return None
# Wait for more data
self._body_event.clear()
await self._body_event.wait()
def get_pseudo_headers(self):
"""Extract HTTP/2 pseudo-headers from request headers.
Returns:
dict: Mapping of pseudo-header names to values
(e.g., {':method': 'GET', ':path': '/'})
"""
pseudo = {}
for name, value in self.request_headers:
if name.startswith(':'):
pseudo[name] = value
return pseudo
def get_regular_headers(self):
"""Get regular (non-pseudo) headers from request.
Returns:
list: List of (name, value) tuples for regular headers
"""
return [
(name, value)
for name, value in self.request_headers
if not name.startswith(':')
]
def __repr__(self):
return (
f"<HTTP2Stream id={self.stream_id} "
f"state={self.state.name} "
f"req_complete={self.request_complete} "
f"resp_complete={self.response_complete}>"
)
__all__ = ['HTTP2Stream', 'StreamState']