mirror of
https://github.com/frappe/gunicorn.git
synced 2026-07-07 13:11:30 +08:00
Add optional fast HTTP parser for ASGI workers
- Add http_parser config setting (auto/fast/python) - Add gunicorn_h1c as optional dependency [fast] - Add unified HttpParser class with fallback to pure Python - Parser tries gunicorn_h1c in 'auto' mode, falls back gracefully - 'fast' mode requires gunicorn_h1c, 'python' forces pure Python Install with: pip install gunicorn[fast]
This commit is contained in:
parent
2cc38503b7
commit
b833a9b6df
639
gunicorn/asgi/parser.py
Normal file
639
gunicorn/asgi/parser.py
Normal file
@ -0,0 +1,639 @@
|
|||||||
|
#
|
||||||
|
# This file is part of gunicorn released under the MIT license.
|
||||||
|
# See the NOTICE for more information.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Unified HTTP parser interface for ASGI workers.
|
||||||
|
|
||||||
|
Provides a common interface for both fast C parser (gunicorn_h1c)
|
||||||
|
and the pure Python parser, with incremental (push-based) parsing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import ipaddress
|
||||||
|
import socket
|
||||||
|
import struct
|
||||||
|
|
||||||
|
from gunicorn.http.errors import (
|
||||||
|
InvalidHeader, InvalidHeaderName, NoMoreData,
|
||||||
|
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
|
||||||
|
LimitRequestLine, LimitRequestHeaders,
|
||||||
|
UnsupportedTransferCoding, ObsoleteFolding,
|
||||||
|
InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest,
|
||||||
|
InvalidSchemeHeaders, ExpectationFailed,
|
||||||
|
)
|
||||||
|
from gunicorn.http.message import PP_V2_SIGNATURE, PPCommand, PPFamily, PPProtocol
|
||||||
|
from gunicorn.util import bytes_to_str, split_request_uri
|
||||||
|
|
||||||
|
MAX_REQUEST_LINE = 8190
|
||||||
|
MAX_HEADERS = 32768
|
||||||
|
DEFAULT_MAX_HEADERFIELD_SIZE = 8190
|
||||||
|
|
||||||
|
# Reuse regex patterns
|
||||||
|
RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~"
|
||||||
|
TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS)))
|
||||||
|
METHOD_BADCHAR_RE = re.compile("[a-z#]")
|
||||||
|
VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
|
||||||
|
RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]")
|
||||||
|
|
||||||
|
|
||||||
|
def _ip_in_allow_list(ip_str, allow_list, networks):
|
||||||
|
"""Check if IP address is in the allow list."""
|
||||||
|
if '*' in allow_list:
|
||||||
|
return True
|
||||||
|
try:
|
||||||
|
ip = ipaddress.ip_address(ip_str)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
for network in networks:
|
||||||
|
if ip in network:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class ParseResult:
|
||||||
|
"""Result of header parsing."""
|
||||||
|
|
||||||
|
__slots__ = (
|
||||||
|
'method', 'uri', 'path', 'query', 'fragment', 'version',
|
||||||
|
'headers', 'scheme', 'content_length', 'chunked',
|
||||||
|
'keep_alive', 'consumed', 'proxy_protocol_info',
|
||||||
|
'must_close', 'expect_100_continue',
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.method = None
|
||||||
|
self.uri = None
|
||||||
|
self.path = None
|
||||||
|
self.query = None
|
||||||
|
self.fragment = None
|
||||||
|
self.version = None
|
||||||
|
self.headers = []
|
||||||
|
self.scheme = "http"
|
||||||
|
self.content_length = 0
|
||||||
|
self.chunked = False
|
||||||
|
self.keep_alive = True
|
||||||
|
self.consumed = 0
|
||||||
|
self.proxy_protocol_info = None
|
||||||
|
self.must_close = False
|
||||||
|
self.expect_100_continue = False
|
||||||
|
|
||||||
|
|
||||||
|
class HttpParser:
|
||||||
|
"""Unified incremental HTTP parser.
|
||||||
|
|
||||||
|
Works with both gunicorn_h1c (fast C extension) and pure Python parsing.
|
||||||
|
Designed for push-based parsing where data arrives via data_received().
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Class-level cache for fast parser availability (import check is expensive)
|
||||||
|
_fast_available = None
|
||||||
|
_h1c_module = None
|
||||||
|
|
||||||
|
def __init__(self, cfg, peer_addr, is_ssl=False, req_number=1, is_trusted_proxy=False):
|
||||||
|
"""Initialize the parser.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cfg: gunicorn config object
|
||||||
|
peer_addr: client address tuple (host, port)
|
||||||
|
is_ssl: whether this is an SSL connection
|
||||||
|
req_number: request number on this connection (for proxy protocol)
|
||||||
|
is_trusted_proxy: whether peer is in forwarded_allow_ips (pre-computed)
|
||||||
|
"""
|
||||||
|
self.cfg = cfg
|
||||||
|
self.peer_addr = peer_addr
|
||||||
|
self.is_ssl = is_ssl
|
||||||
|
self.req_number = req_number
|
||||||
|
self._is_trusted_proxy = is_trusted_proxy
|
||||||
|
self._result = None
|
||||||
|
|
||||||
|
# Limits
|
||||||
|
self.limit_request_line = cfg.limit_request_line
|
||||||
|
if self.limit_request_line < 0 or self.limit_request_line >= MAX_REQUEST_LINE:
|
||||||
|
self.limit_request_line = MAX_REQUEST_LINE
|
||||||
|
|
||||||
|
self.limit_request_fields = cfg.limit_request_fields
|
||||||
|
if self.limit_request_fields <= 0 or self.limit_request_fields > MAX_HEADERS:
|
||||||
|
self.limit_request_fields = MAX_HEADERS
|
||||||
|
|
||||||
|
self.limit_request_field_size = cfg.limit_request_field_size
|
||||||
|
if self.limit_request_field_size < 0:
|
||||||
|
self.limit_request_field_size = DEFAULT_MAX_HEADERFIELD_SIZE
|
||||||
|
|
||||||
|
max_header_field_size = self.limit_request_field_size or DEFAULT_MAX_HEADERFIELD_SIZE
|
||||||
|
self.max_buffer_headers = self.limit_request_fields * (max_header_field_size + 2) + 4
|
||||||
|
|
||||||
|
# Use cached fast parser check (import is expensive, do once per process)
|
||||||
|
self._use_fast = self._check_fast_available()
|
||||||
|
|
||||||
|
def _check_fast_available(self):
|
||||||
|
"""Check if fast C parser is available (cached at class level)."""
|
||||||
|
parser_setting = getattr(self.cfg, 'http_parser', 'auto')
|
||||||
|
if parser_setting == 'python':
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Use class-level cache to avoid repeated import checks
|
||||||
|
if HttpParser._fast_available is None:
|
||||||
|
try:
|
||||||
|
import gunicorn_h1c
|
||||||
|
HttpParser._fast_available = True
|
||||||
|
HttpParser._h1c_module = gunicorn_h1c
|
||||||
|
except ImportError:
|
||||||
|
HttpParser._fast_available = False
|
||||||
|
|
||||||
|
if not HttpParser._fast_available and parser_setting == 'fast':
|
||||||
|
raise RuntimeError("gunicorn_h1c not installed but http_parser='fast'")
|
||||||
|
|
||||||
|
return HttpParser._fast_available
|
||||||
|
|
||||||
|
def feed(self, buffer):
|
||||||
|
"""Parse buffer incrementally.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
buffer: bytearray containing received data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
ParseResult if headers are complete, None if more data needed
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Various HTTP parsing errors for malformed requests
|
||||||
|
"""
|
||||||
|
if self._use_fast:
|
||||||
|
return self._feed_fast(buffer)
|
||||||
|
else:
|
||||||
|
return self._feed_python(buffer)
|
||||||
|
|
||||||
|
def _feed_fast(self, buffer):
|
||||||
|
"""Parse using fast C parser."""
|
||||||
|
try:
|
||||||
|
result = HttpParser._h1c_module.parse_request(bytes(buffer))
|
||||||
|
|
||||||
|
# gunicorn_h1c returns bytes, convert to strings (latin-1)
|
||||||
|
pr = ParseResult()
|
||||||
|
pr.method = bytes_to_str(result['method'])
|
||||||
|
# gunicorn_h1c returns 'path' which is the full URI (path?query)
|
||||||
|
pr.uri = bytes_to_str(result['path'])
|
||||||
|
# Parse path/query from URI
|
||||||
|
try:
|
||||||
|
parts = split_request_uri(pr.uri)
|
||||||
|
pr.path = parts.path or ""
|
||||||
|
pr.query = parts.query or ""
|
||||||
|
pr.fragment = parts.fragment or ""
|
||||||
|
except ValueError:
|
||||||
|
pr.path = pr.uri
|
||||||
|
pr.query = ""
|
||||||
|
pr.fragment = ""
|
||||||
|
pr.version = (1, result['minor_version'])
|
||||||
|
|
||||||
|
# Headers - convert to uppercase strings
|
||||||
|
pr.headers = [(bytes_to_str(n).upper(), bytes_to_str(v)) for n, v in result['headers']]
|
||||||
|
|
||||||
|
pr.consumed = result['consumed']
|
||||||
|
pr.keep_alive = result['minor_version'] >= 1
|
||||||
|
pr.scheme = "https" if self.is_ssl else "http"
|
||||||
|
|
||||||
|
# Parse body info from headers
|
||||||
|
self._parse_body_info(pr)
|
||||||
|
|
||||||
|
self._result = pr
|
||||||
|
return pr
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if "incomplete" in str(e).lower():
|
||||||
|
return None
|
||||||
|
raise
|
||||||
|
|
||||||
|
def _feed_python(self, buffer):
|
||||||
|
"""Parse using pure Python parser."""
|
||||||
|
# Handle proxy protocol on first request
|
||||||
|
mode = self.cfg.proxy_protocol
|
||||||
|
proxy_info = None
|
||||||
|
buf_offset = 0
|
||||||
|
|
||||||
|
if mode != "off" and self.req_number == 1:
|
||||||
|
# Check for proxy protocol
|
||||||
|
if len(buffer) < 12:
|
||||||
|
return None # Need more data
|
||||||
|
|
||||||
|
if mode in ("v2", "auto") and buffer[:12] == PP_V2_SIGNATURE:
|
||||||
|
self._proxy_protocol_access_check()
|
||||||
|
consumed, proxy_info = self._parse_proxy_v2(buffer)
|
||||||
|
if consumed is None:
|
||||||
|
return None # Need more data
|
||||||
|
buf_offset = consumed
|
||||||
|
|
||||||
|
elif mode in ("v1", "auto") and buffer[:6] == b"PROXY ":
|
||||||
|
self._proxy_protocol_access_check()
|
||||||
|
consumed, proxy_info = self._parse_proxy_v1(buffer)
|
||||||
|
if consumed is None:
|
||||||
|
return None # Need more data
|
||||||
|
buf_offset = consumed
|
||||||
|
|
||||||
|
# Find request line
|
||||||
|
idx = buffer.find(b"\r\n", buf_offset)
|
||||||
|
if idx < 0:
|
||||||
|
if len(buffer) - buf_offset > self.limit_request_line:
|
||||||
|
raise LimitRequestLine(len(buffer) - buf_offset, self.limit_request_line)
|
||||||
|
return None # Need more data
|
||||||
|
|
||||||
|
line_len = idx - buf_offset
|
||||||
|
if line_len > self.limit_request_line:
|
||||||
|
raise LimitRequestLine(line_len, self.limit_request_line)
|
||||||
|
|
||||||
|
request_line = bytes(buffer[buf_offset:idx])
|
||||||
|
headers_start = idx + 2
|
||||||
|
|
||||||
|
# Find end of headers
|
||||||
|
headers_end = buffer.find(b"\r\n\r\n", headers_start)
|
||||||
|
if headers_end < 0:
|
||||||
|
# Check for empty headers case
|
||||||
|
if buffer[headers_start:headers_start + 2] == b"\r\n":
|
||||||
|
headers_end = headers_start
|
||||||
|
else:
|
||||||
|
if len(buffer) - headers_start > self.max_buffer_headers:
|
||||||
|
raise LimitRequestHeaders("max buffer headers")
|
||||||
|
return None # Need more data
|
||||||
|
|
||||||
|
# Parse request line
|
||||||
|
pr = ParseResult()
|
||||||
|
pr.proxy_protocol_info = proxy_info
|
||||||
|
self._parse_request_line(request_line, pr)
|
||||||
|
|
||||||
|
# Parse headers (if any)
|
||||||
|
if buffer[headers_start:headers_start + 2] == b"\r\n":
|
||||||
|
# Empty headers
|
||||||
|
pr.consumed = headers_start + 2
|
||||||
|
else:
|
||||||
|
headers_data = bytes(buffer[headers_start:headers_end])
|
||||||
|
pr.headers = self._parse_headers(headers_data)
|
||||||
|
pr.consumed = headers_end + 4
|
||||||
|
|
||||||
|
# Set scheme
|
||||||
|
pr.scheme = "https" if self.is_ssl else "http"
|
||||||
|
|
||||||
|
# Check for scheme headers from trusted proxy
|
||||||
|
self._apply_scheme_headers(pr)
|
||||||
|
|
||||||
|
# Parse body info
|
||||||
|
self._parse_body_info(pr)
|
||||||
|
|
||||||
|
# Determine keep-alive
|
||||||
|
pr.keep_alive = self._should_keep_alive(pr)
|
||||||
|
|
||||||
|
self._result = pr
|
||||||
|
return pr
|
||||||
|
|
||||||
|
def _proxy_protocol_access_check(self):
|
||||||
|
"""Check if proxy protocol is allowed from this peer."""
|
||||||
|
if isinstance(self.peer_addr, tuple):
|
||||||
|
if not _ip_in_allow_list(
|
||||||
|
self.peer_addr[0],
|
||||||
|
self.cfg.proxy_allow_ips,
|
||||||
|
self.cfg.proxy_allow_networks()
|
||||||
|
):
|
||||||
|
raise ForbiddenProxyRequest(self.peer_addr[0])
|
||||||
|
|
||||||
|
def _parse_proxy_v1(self, buffer):
|
||||||
|
"""Parse PROXY protocol v1 (text format).
|
||||||
|
|
||||||
|
Returns (consumed, info) or (None, None) if incomplete.
|
||||||
|
"""
|
||||||
|
idx = buffer.find(b"\r\n")
|
||||||
|
if idx < 0:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
line = bytes_to_str(bytes(buffer[:idx]))
|
||||||
|
bits = line.split(" ")
|
||||||
|
|
||||||
|
if len(bits) != 6:
|
||||||
|
raise InvalidProxyLine(line)
|
||||||
|
|
||||||
|
proto = bits[1]
|
||||||
|
s_addr = bits[2]
|
||||||
|
d_addr = bits[3]
|
||||||
|
|
||||||
|
if proto not in ["TCP4", "TCP6"]:
|
||||||
|
raise InvalidProxyLine("protocol '%s' not supported" % proto)
|
||||||
|
|
||||||
|
if proto == "TCP4":
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET, s_addr)
|
||||||
|
socket.inet_pton(socket.AF_INET, d_addr)
|
||||||
|
except OSError:
|
||||||
|
raise InvalidProxyLine(line)
|
||||||
|
elif proto == "TCP6":
|
||||||
|
try:
|
||||||
|
socket.inet_pton(socket.AF_INET6, s_addr)
|
||||||
|
socket.inet_pton(socket.AF_INET6, d_addr)
|
||||||
|
except OSError:
|
||||||
|
raise InvalidProxyLine(line)
|
||||||
|
|
||||||
|
try:
|
||||||
|
s_port = int(bits[4])
|
||||||
|
d_port = int(bits[5])
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidProxyLine("invalid port %s" % line)
|
||||||
|
|
||||||
|
if not ((0 <= s_port <= 65535) and (0 <= d_port <= 65535)):
|
||||||
|
raise InvalidProxyLine("invalid port %s" % line)
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"proxy_protocol": proto,
|
||||||
|
"client_addr": s_addr,
|
||||||
|
"client_port": s_port,
|
||||||
|
"proxy_addr": d_addr,
|
||||||
|
"proxy_port": d_port
|
||||||
|
}
|
||||||
|
|
||||||
|
return idx + 2, info
|
||||||
|
|
||||||
|
def _parse_proxy_v2(self, buffer):
|
||||||
|
"""Parse PROXY protocol v2 (binary format).
|
||||||
|
|
||||||
|
Returns (consumed, info) or (None, None) if incomplete.
|
||||||
|
"""
|
||||||
|
if len(buffer) < 16:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
ver_cmd = buffer[12]
|
||||||
|
fam_proto = buffer[13]
|
||||||
|
length = struct.unpack(">H", bytes(buffer[14:16]))[0]
|
||||||
|
|
||||||
|
version = (ver_cmd & 0xF0) >> 4
|
||||||
|
if version != 2:
|
||||||
|
raise InvalidProxyHeader("unsupported version %d" % version)
|
||||||
|
|
||||||
|
command = ver_cmd & 0x0F
|
||||||
|
if command not in (PPCommand.LOCAL, PPCommand.PROXY):
|
||||||
|
raise InvalidProxyHeader("unsupported command %d" % command)
|
||||||
|
|
||||||
|
total_size = 16 + length
|
||||||
|
if len(buffer) < total_size:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
if command == PPCommand.LOCAL:
|
||||||
|
info = {
|
||||||
|
"proxy_protocol": "LOCAL",
|
||||||
|
"client_addr": None,
|
||||||
|
"client_port": None,
|
||||||
|
"proxy_addr": None,
|
||||||
|
"proxy_port": None
|
||||||
|
}
|
||||||
|
return total_size, info
|
||||||
|
|
||||||
|
family = (fam_proto & 0xF0) >> 4
|
||||||
|
protocol = fam_proto & 0x0F
|
||||||
|
|
||||||
|
if protocol != PPProtocol.STREAM:
|
||||||
|
raise InvalidProxyHeader("only TCP protocol is supported")
|
||||||
|
|
||||||
|
addr_data = bytes(buffer[16:16 + length])
|
||||||
|
|
||||||
|
if family == PPFamily.INET:
|
||||||
|
if length < 12:
|
||||||
|
raise InvalidProxyHeader("insufficient address data for IPv4")
|
||||||
|
s_addr = socket.inet_ntop(socket.AF_INET, addr_data[0:4])
|
||||||
|
d_addr = socket.inet_ntop(socket.AF_INET, addr_data[4:8])
|
||||||
|
s_port = struct.unpack(">H", addr_data[8:10])[0]
|
||||||
|
d_port = struct.unpack(">H", addr_data[10:12])[0]
|
||||||
|
proto = "TCP4"
|
||||||
|
|
||||||
|
elif family == PPFamily.INET6:
|
||||||
|
if length < 36:
|
||||||
|
raise InvalidProxyHeader("insufficient address data for IPv6")
|
||||||
|
s_addr = socket.inet_ntop(socket.AF_INET6, addr_data[0:16])
|
||||||
|
d_addr = socket.inet_ntop(socket.AF_INET6, addr_data[16:32])
|
||||||
|
s_port = struct.unpack(">H", addr_data[32:34])[0]
|
||||||
|
d_port = struct.unpack(">H", addr_data[34:36])[0]
|
||||||
|
proto = "TCP6"
|
||||||
|
|
||||||
|
elif family == PPFamily.UNSPEC:
|
||||||
|
info = {
|
||||||
|
"proxy_protocol": "UNSPEC",
|
||||||
|
"client_addr": None,
|
||||||
|
"client_port": None,
|
||||||
|
"proxy_addr": None,
|
||||||
|
"proxy_port": None
|
||||||
|
}
|
||||||
|
return total_size, info
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidProxyHeader("unsupported address family %d" % family)
|
||||||
|
|
||||||
|
info = {
|
||||||
|
"proxy_protocol": proto,
|
||||||
|
"client_addr": s_addr,
|
||||||
|
"client_port": s_port,
|
||||||
|
"proxy_addr": d_addr,
|
||||||
|
"proxy_port": d_port
|
||||||
|
}
|
||||||
|
|
||||||
|
return total_size, info
|
||||||
|
|
||||||
|
def _parse_request_line(self, line_bytes, result):
|
||||||
|
"""Parse the HTTP request line."""
|
||||||
|
bits = [bytes_to_str(bit) for bit in line_bytes.split(b" ", 2)]
|
||||||
|
if len(bits) != 3:
|
||||||
|
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||||
|
|
||||||
|
# Method
|
||||||
|
result.method = bits[0]
|
||||||
|
|
||||||
|
if not self.cfg.permit_unconventional_http_method:
|
||||||
|
if METHOD_BADCHAR_RE.search(result.method):
|
||||||
|
raise InvalidRequestMethod(result.method)
|
||||||
|
if not 3 <= len(bits[0]) <= 20:
|
||||||
|
raise InvalidRequestMethod(result.method)
|
||||||
|
|
||||||
|
if not TOKEN_RE.fullmatch(result.method):
|
||||||
|
raise InvalidRequestMethod(result.method)
|
||||||
|
|
||||||
|
if self.cfg.casefold_http_method:
|
||||||
|
result.method = result.method.upper()
|
||||||
|
|
||||||
|
# URI
|
||||||
|
result.uri = bits[1]
|
||||||
|
if len(result.uri) == 0:
|
||||||
|
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||||
|
|
||||||
|
try:
|
||||||
|
parts = split_request_uri(result.uri)
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||||
|
|
||||||
|
result.path = parts.path or ""
|
||||||
|
result.query = parts.query or ""
|
||||||
|
result.fragment = parts.fragment or ""
|
||||||
|
|
||||||
|
# Version
|
||||||
|
match = VERSION_RE.fullmatch(bits[2])
|
||||||
|
if match is None:
|
||||||
|
raise InvalidHTTPVersion(bits[2])
|
||||||
|
|
||||||
|
result.version = (int(match.group(1)), int(match.group(2)))
|
||||||
|
if not (1, 0) <= result.version < (2, 0):
|
||||||
|
if not self.cfg.permit_unconventional_http_version:
|
||||||
|
raise InvalidHTTPVersion(result.version)
|
||||||
|
|
||||||
|
def _parse_headers(self, data):
|
||||||
|
"""Parse HTTP headers from raw data."""
|
||||||
|
headers = []
|
||||||
|
lines = [bytes_to_str(line) for line in data.split(b"\r\n")]
|
||||||
|
num_lines = len(lines)
|
||||||
|
i = 0
|
||||||
|
|
||||||
|
while i < num_lines:
|
||||||
|
if len(headers) >= self.limit_request_fields:
|
||||||
|
raise LimitRequestHeaders("limit request headers fields")
|
||||||
|
|
||||||
|
curr = lines[i]
|
||||||
|
i += 1
|
||||||
|
header_length = len(curr) + len("\r\n")
|
||||||
|
|
||||||
|
if curr.find(":") <= 0:
|
||||||
|
raise InvalidHeader(curr)
|
||||||
|
|
||||||
|
name, value = curr.split(":", 1)
|
||||||
|
if self.cfg.strip_header_spaces:
|
||||||
|
name = name.rstrip(" \t")
|
||||||
|
|
||||||
|
if not TOKEN_RE.fullmatch(name):
|
||||||
|
raise InvalidHeaderName(name)
|
||||||
|
|
||||||
|
name = name.upper()
|
||||||
|
value = [value.strip(" \t")]
|
||||||
|
|
||||||
|
# Handle obsolete folding
|
||||||
|
while i < num_lines and lines[i].startswith((" ", "\t")):
|
||||||
|
if not self.cfg.permit_obsolete_folding:
|
||||||
|
raise ObsoleteFolding(name)
|
||||||
|
curr = lines[i]
|
||||||
|
i += 1
|
||||||
|
header_length += len(curr) + len("\r\n")
|
||||||
|
if header_length > self.limit_request_field_size > 0:
|
||||||
|
raise LimitRequestHeaders("limit request headers fields size")
|
||||||
|
value.append(curr.strip("\t "))
|
||||||
|
|
||||||
|
value = " ".join(value)
|
||||||
|
|
||||||
|
if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value):
|
||||||
|
raise InvalidHeader(name)
|
||||||
|
|
||||||
|
if header_length > self.limit_request_field_size > 0:
|
||||||
|
raise LimitRequestHeaders("limit request headers fields size")
|
||||||
|
|
||||||
|
# Handle underscore in header names
|
||||||
|
if "_" in name:
|
||||||
|
forwarder_headers = self.cfg.forwarder_headers
|
||||||
|
if name in forwarder_headers or "*" in forwarder_headers:
|
||||||
|
pass
|
||||||
|
elif self.cfg.header_map == "dangerous":
|
||||||
|
pass
|
||||||
|
elif self.cfg.header_map == "drop":
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise InvalidHeaderName(name)
|
||||||
|
|
||||||
|
headers.append((name, value))
|
||||||
|
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _apply_scheme_headers(self, result):
|
||||||
|
"""Apply scheme headers from trusted proxy."""
|
||||||
|
if not isinstance(self.peer_addr, tuple):
|
||||||
|
return
|
||||||
|
|
||||||
|
# Use pre-computed trusted proxy check (avoids IP parsing on every request)
|
||||||
|
if not self._is_trusted_proxy:
|
||||||
|
return
|
||||||
|
|
||||||
|
secure_scheme_headers = self.cfg.secure_scheme_headers
|
||||||
|
scheme_header = False
|
||||||
|
|
||||||
|
for name, value in result.headers:
|
||||||
|
if name == "EXPECT":
|
||||||
|
if value.lower() == "100-continue":
|
||||||
|
if result.version >= (1, 1):
|
||||||
|
result.expect_100_continue = True
|
||||||
|
else:
|
||||||
|
raise ExpectationFailed(value)
|
||||||
|
|
||||||
|
if name in secure_scheme_headers:
|
||||||
|
secure = value == secure_scheme_headers[name]
|
||||||
|
scheme = "https" if secure else "http"
|
||||||
|
if scheme_header:
|
||||||
|
if scheme != result.scheme:
|
||||||
|
raise InvalidSchemeHeaders()
|
||||||
|
else:
|
||||||
|
scheme_header = True
|
||||||
|
result.scheme = scheme
|
||||||
|
|
||||||
|
def _parse_body_info(self, result):
|
||||||
|
"""Parse Content-Length and Transfer-Encoding from headers."""
|
||||||
|
chunked = False
|
||||||
|
content_length = None
|
||||||
|
|
||||||
|
for name, value in result.headers:
|
||||||
|
if name == "CONTENT-LENGTH":
|
||||||
|
if content_length is not None:
|
||||||
|
raise InvalidHeader("CONTENT-LENGTH")
|
||||||
|
content_length = value
|
||||||
|
|
||||||
|
elif name == "TRANSFER-ENCODING":
|
||||||
|
vals = [v.strip() for v in value.split(',')]
|
||||||
|
for val in vals:
|
||||||
|
if val.lower() == "chunked":
|
||||||
|
if chunked:
|
||||||
|
raise InvalidHeader("TRANSFER-ENCODING")
|
||||||
|
chunked = True
|
||||||
|
elif val.lower() == "identity":
|
||||||
|
if chunked:
|
||||||
|
raise InvalidHeader("TRANSFER-ENCODING")
|
||||||
|
elif val.lower() in ('compress', 'deflate', 'gzip'):
|
||||||
|
if chunked:
|
||||||
|
raise InvalidHeader("TRANSFER-ENCODING")
|
||||||
|
result.must_close = True
|
||||||
|
else:
|
||||||
|
raise UnsupportedTransferCoding(value)
|
||||||
|
|
||||||
|
if chunked:
|
||||||
|
if result.version < (1, 1):
|
||||||
|
raise InvalidHeader("TRANSFER-ENCODING")
|
||||||
|
if content_length is not None:
|
||||||
|
raise InvalidHeader("CONTENT-LENGTH")
|
||||||
|
result.chunked = True
|
||||||
|
result.content_length = -1
|
||||||
|
elif content_length is not None:
|
||||||
|
try:
|
||||||
|
if str(content_length).isnumeric():
|
||||||
|
result.content_length = int(content_length)
|
||||||
|
else:
|
||||||
|
raise InvalidHeader("CONTENT-LENGTH")
|
||||||
|
except ValueError:
|
||||||
|
raise InvalidHeader("CONTENT-LENGTH")
|
||||||
|
|
||||||
|
if result.content_length < 0:
|
||||||
|
raise InvalidHeader("CONTENT-LENGTH")
|
||||||
|
else:
|
||||||
|
result.content_length = 0
|
||||||
|
|
||||||
|
def _should_keep_alive(self, result):
|
||||||
|
"""Determine if connection should be kept alive."""
|
||||||
|
if result.must_close:
|
||||||
|
return False
|
||||||
|
|
||||||
|
for name, value in result.headers:
|
||||||
|
if name == "CONNECTION":
|
||||||
|
v = value.lower().strip(" \t")
|
||||||
|
if v == "close":
|
||||||
|
return False
|
||||||
|
elif v == "keep-alive":
|
||||||
|
return True
|
||||||
|
break
|
||||||
|
|
||||||
|
return result.version > (1, 0)
|
||||||
|
|
||||||
|
def reset(self):
|
||||||
|
"""Reset parser state for next request on keep-alive connection."""
|
||||||
|
self._result = None
|
||||||
|
self.req_number += 1
|
||||||
@ -2772,6 +2772,21 @@ def validate_asgi_lifespan(val):
|
|||||||
return val
|
return val
|
||||||
|
|
||||||
|
|
||||||
|
def validate_http_parser(val):
|
||||||
|
"""Validate http_parser setting.
|
||||||
|
|
||||||
|
Accepts: auto, fast, python
|
||||||
|
"""
|
||||||
|
if val is None:
|
||||||
|
return "auto"
|
||||||
|
if not isinstance(val, str):
|
||||||
|
raise TypeError("http_parser must be a string")
|
||||||
|
val = val.lower().strip()
|
||||||
|
if val not in ("auto", "fast", "python"):
|
||||||
|
raise ValueError("http_parser must be: auto, fast, or python")
|
||||||
|
return val
|
||||||
|
|
||||||
|
|
||||||
class ASGILoop(Setting):
|
class ASGILoop(Setting):
|
||||||
name = "asgi_loop"
|
name = "asgi_loop"
|
||||||
section = "Worker Processes"
|
section = "Worker Processes"
|
||||||
@ -2845,6 +2860,30 @@ class ASGIDisconnectGracePeriod(Setting):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class HttpParser(Setting):
|
||||||
|
name = "http_parser"
|
||||||
|
section = "Worker Processes"
|
||||||
|
cli = ["--http-parser"]
|
||||||
|
meta = "STRING"
|
||||||
|
validator = validate_http_parser
|
||||||
|
default = "auto"
|
||||||
|
desc = """\
|
||||||
|
HTTP parser implementation for ASGI workers.
|
||||||
|
|
||||||
|
- auto: Use gunicorn_h1c if available, otherwise pure Python (default)
|
||||||
|
- fast: Require gunicorn_h1c C extension (fail if unavailable)
|
||||||
|
- python: Force pure Python parser
|
||||||
|
|
||||||
|
The gunicorn_h1c C extension provides significantly faster HTTP
|
||||||
|
parsing using picohttpparser with SIMD optimizations. Install it
|
||||||
|
with: pip install gunicorn[fast]
|
||||||
|
|
||||||
|
This setting only affects the ``asgi`` worker type.
|
||||||
|
|
||||||
|
.. versionadded:: 25.0.0
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RootPath(Setting):
|
class RootPath(Setting):
|
||||||
name = "root_path"
|
name = "root_path"
|
||||||
section = "Server Mechanics"
|
section = "Server Mechanics"
|
||||||
|
|||||||
@ -53,6 +53,7 @@ tornado = ["tornado>=6.5.0"]
|
|||||||
gthread = []
|
gthread = []
|
||||||
setproctitle = ["setproctitle"]
|
setproctitle = ["setproctitle"]
|
||||||
http2 = ["h2>=4.1.0"]
|
http2 = ["h2>=4.1.0"]
|
||||||
|
fast = ["gunicorn_h1c>=0.1.0"]
|
||||||
testing = [
|
testing = [
|
||||||
"gevent>=24.10.1",
|
"gevent>=24.10.1",
|
||||||
"eventlet>=0.40.3",
|
"eventlet>=0.40.3",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user