gunicorn/gunicorn/http/parser.py
Benoit Chesneau e90b1c2c1e fix: address six WSGI/ASGI parser and protocol findings
- WSGI fast parser now applies the same per-header policy as the Python
  parser (Expect, secure_scheme_headers, forwarded_allow_ips trust gate,
  forwarder_headers / header_map). Shared helpers extracted on Message.

- ASGI keepalive no longer resets the parser when the previous request
  body was not fully framed; the connection closes instead, preventing
  request smuggling on pipelined connections.

- BodyReceiver._wait_for_data timeout flips _closed and yields
  http.disconnect rather than synthesizing more_body=False. Timeout
  honors cfg.timeout.

- ASGI chunked encoding now skips HEAD, 204, and 304 (matches
  Response.is_chunked in the WSGI path) via a small helper.

- _setup_callback_parser passes proxy_protocol to PythonProtocol; auto
  falls back to the Python parser when proxy_protocol != off (the C
  parser does not implement PROXY framing). _effective_peername swaps
  the transport peer with the PROXY-supplied client address.

- Parser.finish_body accepts a deadline and a 64KiB byte cap; gthread
  passes a deadline and abandons keepalive on incomplete drain so a
  stalled client cannot tie up a worker thread.
2026-05-03 18:19:08 +02:00

115 lines
3.7 KiB
Python

#
# This file is part of gunicorn released under the MIT license.
# See the NOTICE for more information.
import socket
import ssl
import time
from gunicorn.http.message import Request
from gunicorn.http.unreader import SocketUnreader, IterUnreader
# Cap on bytes drained from an unconsumed request body before a keepalive
# reset. Defends against a slow-but-steady client that stays under a per-read
# deadline yet streams indefinitely.
_DRAIN_MAX_BYTES = 64 * 1024
class Parser:
mesg_class = None
def __init__(self, cfg, source, source_addr):
self.cfg = cfg
if hasattr(source, "recv"):
self.unreader = SocketUnreader(source)
else:
self.unreader = IterUnreader(source)
self.mesg = None
self.source_addr = source_addr
# request counter (for keepalive connetions)
self.req_count = 0
def __iter__(self):
return self
def finish_body(self, deadline=None, max_bytes=_DRAIN_MAX_BYTES):
"""Discard any unread body of the current message.
Called before returning a keepalive connection to the poller so the
socket does not appear readable due to leftover body bytes.
``deadline`` is an absolute ``time.monotonic()`` value; when set the
socket read timeout is bounded by the remaining time before each read.
``max_bytes`` caps the total drained bytes to defend against a slow
client that keeps trickling under the deadline.
Returns ``True`` when the body was fully drained, ``False`` when the
drain was abandoned (deadline, byte cap, or socket timeout). Callers
that observe ``False`` MUST close the connection rather than serve
another request on it.
"""
if not self.mesg:
return True
sock = getattr(self.unreader, "sock", None)
# gettimeout/settimeout only matter when bounding a real socket; a
# mock or non-socket source skips the timeout plumbing.
if sock is not None and hasattr(sock, "gettimeout") and hasattr(sock, "settimeout"):
timeoutable_sock = sock
prior_timeout = sock.gettimeout()
else:
timeoutable_sock = None
prior_timeout = None
drained = 0
try:
while True:
if deadline is not None and timeoutable_sock is not None:
remaining = deadline - time.monotonic()
if remaining <= 0:
return False
timeoutable_sock.settimeout(remaining)
try:
data = self.mesg.body.read(1024)
except (socket.timeout, TimeoutError):
return False
except ssl.SSLWantReadError:
# SSL socket has no more application data available
return True
if not data:
return True
drained += len(data)
if drained >= max_bytes:
return False
finally:
if timeoutable_sock is not None:
try:
timeoutable_sock.settimeout(prior_timeout)
except OSError:
pass
def __next__(self):
# Stop if HTTP dictates a stop.
if self.mesg and self.mesg.should_close():
raise StopIteration()
# Discard any unread body of the previous message
self.finish_body()
# Parse the next request
self.req_count += 1
self.mesg = self.mesg_class(self.cfg, self.unreader, self.source_addr, self.req_count)
if not self.mesg:
raise StopIteration()
return self.mesg
next = __next__
class RequestParser(Parser):
mesg_class = Request