actualizado 3-sept
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
from gunicorn.http.message import Message, Request
|
||||
from gunicorn.http.parser import RequestParser
|
||||
|
||||
|
||||
def get_parser(cfg, source, source_addr, http2_connection=False):
|
||||
"""Get appropriate parser based on protocol config.
|
||||
|
||||
Args:
|
||||
cfg: Gunicorn config object
|
||||
source: Socket or iterable source
|
||||
source_addr: Source address tuple or None
|
||||
http2_connection: If True, create HTTP/2 connection handler
|
||||
|
||||
Returns:
|
||||
Parser instance (RequestParser, UWSGIParser, or HTTP2ServerConnection)
|
||||
"""
|
||||
# HTTP/2 connection
|
||||
if http2_connection:
|
||||
from gunicorn.http2.connection import HTTP2ServerConnection
|
||||
return HTTP2ServerConnection(cfg, source, source_addr)
|
||||
|
||||
# uWSGI protocol
|
||||
protocol = getattr(cfg, 'protocol', 'http')
|
||||
if protocol == 'uwsgi':
|
||||
from gunicorn.uwsgi.parser import UWSGIParser
|
||||
return UWSGIParser(cfg, source, source_addr)
|
||||
|
||||
# Default HTTP/1.x
|
||||
return RequestParser(cfg, source, source_addr)
|
||||
|
||||
|
||||
__all__ = ['Message', 'Request', 'RequestParser', 'get_parser']
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+274
@@ -0,0 +1,274 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
import io
|
||||
import sys
|
||||
|
||||
from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator,
|
||||
InvalidChunkSize, InvalidChunkExtension)
|
||||
|
||||
|
||||
class ChunkedReader:
|
||||
def __init__(self, req, unreader):
|
||||
self.req = req
|
||||
self.parser = self.parse_chunked(unreader)
|
||||
self.buf = io.BytesIO()
|
||||
|
||||
def read(self, size):
|
||||
if not isinstance(size, int):
|
||||
raise TypeError("size must be an integer type")
|
||||
if size < 0:
|
||||
raise ValueError("Size must be positive.")
|
||||
if size == 0:
|
||||
return b""
|
||||
|
||||
if self.parser:
|
||||
while self.buf.tell() < size:
|
||||
try:
|
||||
self.buf.write(next(self.parser))
|
||||
except StopIteration:
|
||||
self.parser = None
|
||||
break
|
||||
|
||||
data = self.buf.getvalue()
|
||||
ret, rest = data[:size], data[size:]
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(rest)
|
||||
return ret
|
||||
|
||||
def parse_trailers(self, unreader, data):
|
||||
buf = io.BytesIO()
|
||||
buf.write(data)
|
||||
|
||||
idx = buf.getvalue().find(b"\r\n\r\n")
|
||||
done = buf.getvalue()[:2] == b"\r\n"
|
||||
while idx < 0 and not done:
|
||||
try:
|
||||
self.get_data(unreader, buf)
|
||||
except NoMoreData:
|
||||
# RFC 9112 7.1.2: the last chunk (0 CRLF) must be followed by
|
||||
# a CRLF-terminated trailer section. Hitting EOF before that
|
||||
# means the chunked body was truncated, not cleanly ended.
|
||||
raise ChunkMissingTerminator(b"") from None
|
||||
idx = buf.getvalue().find(b"\r\n\r\n")
|
||||
done = buf.getvalue()[:2] == b"\r\n"
|
||||
if done:
|
||||
unreader.unread(buf.getvalue()[2:])
|
||||
return b""
|
||||
self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True)
|
||||
unreader.unread(buf.getvalue()[idx + 4:])
|
||||
|
||||
def parse_chunked(self, unreader):
|
||||
(size, rest) = self.parse_chunk_size(unreader)
|
||||
while size > 0:
|
||||
while size > len(rest):
|
||||
size -= len(rest)
|
||||
yield rest
|
||||
rest = unreader.read()
|
||||
if not rest:
|
||||
raise NoMoreData()
|
||||
yield rest[:size]
|
||||
# Remove \r\n after chunk
|
||||
rest = rest[size:]
|
||||
while len(rest) < 2:
|
||||
new_data = unreader.read()
|
||||
if not new_data:
|
||||
break
|
||||
rest += new_data
|
||||
if rest[:2] != b'\r\n':
|
||||
raise ChunkMissingTerminator(rest[:2])
|
||||
(size, rest) = self.parse_chunk_size(unreader, data=rest[2:])
|
||||
|
||||
def parse_chunk_size(self, unreader, data=None):
|
||||
buf = io.BytesIO()
|
||||
if data is not None:
|
||||
buf.write(data)
|
||||
|
||||
idx = buf.getvalue().find(b"\r\n")
|
||||
while idx < 0:
|
||||
self.get_data(unreader, buf)
|
||||
idx = buf.getvalue().find(b"\r\n")
|
||||
|
||||
data = buf.getvalue()
|
||||
line, rest_chunk = data[:idx], data[idx + 2:]
|
||||
|
||||
# RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
|
||||
chunk_size, *chunk_ext = line.split(b";", 1)
|
||||
if chunk_ext:
|
||||
# RFC 9112: chunk-ext must not contain bare CR
|
||||
if b'\r' in chunk_ext[0]:
|
||||
raise InvalidChunkExtension("bare CR not allowed")
|
||||
chunk_size = chunk_size.rstrip(b" \t")
|
||||
if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
|
||||
raise InvalidChunkSize(chunk_size)
|
||||
if len(chunk_size) == 0:
|
||||
raise InvalidChunkSize(chunk_size)
|
||||
chunk_size = int(chunk_size, 16)
|
||||
|
||||
if chunk_size == 0:
|
||||
self.parse_trailers(unreader, rest_chunk)
|
||||
return (0, None)
|
||||
return (chunk_size, rest_chunk)
|
||||
|
||||
def get_data(self, unreader, buf):
|
||||
data = unreader.read()
|
||||
if not data:
|
||||
raise NoMoreData()
|
||||
buf.write(data)
|
||||
|
||||
|
||||
class LengthReader:
|
||||
def __init__(self, unreader, length):
|
||||
self.unreader = unreader
|
||||
self.length = length
|
||||
|
||||
def read(self, size):
|
||||
if not isinstance(size, int):
|
||||
raise TypeError("size must be an integral type")
|
||||
|
||||
size = min(self.length, size)
|
||||
if size < 0:
|
||||
raise ValueError("Size must be positive.")
|
||||
if size == 0:
|
||||
return b""
|
||||
|
||||
buf = io.BytesIO()
|
||||
data = self.unreader.read()
|
||||
while data:
|
||||
buf.write(data)
|
||||
if buf.tell() >= size:
|
||||
break
|
||||
data = self.unreader.read()
|
||||
|
||||
buf = buf.getvalue()
|
||||
ret, rest = buf[:size], buf[size:]
|
||||
self.unreader.unread(rest)
|
||||
self.length -= size
|
||||
return ret
|
||||
|
||||
|
||||
class EOFReader:
|
||||
def __init__(self, unreader):
|
||||
self.unreader = unreader
|
||||
self.buf = io.BytesIO()
|
||||
self.finished = False
|
||||
|
||||
def read(self, size):
|
||||
if not isinstance(size, int):
|
||||
raise TypeError("size must be an integral type")
|
||||
if size < 0:
|
||||
raise ValueError("Size must be positive.")
|
||||
if size == 0:
|
||||
return b""
|
||||
|
||||
if self.finished:
|
||||
data = self.buf.getvalue()
|
||||
ret, rest = data[:size], data[size:]
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(rest)
|
||||
return ret
|
||||
|
||||
data = self.unreader.read()
|
||||
while data:
|
||||
self.buf.write(data)
|
||||
if self.buf.tell() > size:
|
||||
break
|
||||
data = self.unreader.read()
|
||||
|
||||
if not data:
|
||||
self.finished = True
|
||||
|
||||
data = self.buf.getvalue()
|
||||
ret, rest = data[:size], data[size:]
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(rest)
|
||||
return ret
|
||||
|
||||
|
||||
class Body:
|
||||
def __init__(self, reader):
|
||||
self.reader = reader
|
||||
self.buf = io.BytesIO()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
ret = self.readline()
|
||||
if not ret:
|
||||
raise StopIteration()
|
||||
return ret
|
||||
|
||||
next = __next__
|
||||
|
||||
def getsize(self, size):
|
||||
if size is None:
|
||||
return sys.maxsize
|
||||
elif not isinstance(size, int):
|
||||
raise TypeError("size must be an integral type")
|
||||
elif size < 0:
|
||||
return sys.maxsize
|
||||
return size
|
||||
|
||||
def read(self, size=None):
|
||||
size = self.getsize(size)
|
||||
if size == 0:
|
||||
return b""
|
||||
|
||||
if size < self.buf.tell():
|
||||
data = self.buf.getvalue()
|
||||
ret, rest = data[:size], data[size:]
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(rest)
|
||||
return ret
|
||||
|
||||
while size > self.buf.tell():
|
||||
data = self.reader.read(1024)
|
||||
if not data:
|
||||
break
|
||||
self.buf.write(data)
|
||||
|
||||
data = self.buf.getvalue()
|
||||
ret, rest = data[:size], data[size:]
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(rest)
|
||||
return ret
|
||||
|
||||
def readline(self, size=None):
|
||||
size = self.getsize(size)
|
||||
if size == 0:
|
||||
return b""
|
||||
|
||||
data = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
|
||||
ret = []
|
||||
while 1:
|
||||
idx = data.find(b"\n", 0, size)
|
||||
idx = idx + 1 if idx >= 0 else size if len(data) >= size else 0
|
||||
if idx:
|
||||
ret.append(data[:idx])
|
||||
self.buf.write(data[idx:])
|
||||
break
|
||||
|
||||
ret.append(data)
|
||||
size -= len(data)
|
||||
data = self.reader.read(min(1024, size))
|
||||
if not data:
|
||||
break
|
||||
|
||||
return b"".join(ret)
|
||||
|
||||
def readlines(self, size=None):
|
||||
ret = []
|
||||
data = self.read()
|
||||
while data:
|
||||
pos = data.find(b"\n")
|
||||
if pos < 0:
|
||||
ret.append(data)
|
||||
data = b""
|
||||
else:
|
||||
line, data = data[:pos + 1], data[pos + 1:]
|
||||
ret.append(line)
|
||||
return ret
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
# We don't need to call super() in __init__ methods of our
|
||||
# BaseException and Exception classes because we also define
|
||||
# our own __str__ methods so there is no need to pass 'message'
|
||||
# to the base class to get a meaningful output from 'str(exc)'.
|
||||
# pylint: disable=super-init-not-called
|
||||
|
||||
|
||||
class ParseException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoMoreData(IOError):
|
||||
def __init__(self, buf=None):
|
||||
self.buf = buf
|
||||
|
||||
def __str__(self):
|
||||
return "No more data after: %r" % self.buf
|
||||
|
||||
|
||||
class ConfigurationProblem(ParseException):
|
||||
def __init__(self, info):
|
||||
self.info = info
|
||||
self.code = 500
|
||||
|
||||
def __str__(self):
|
||||
return "Configuration problem: %s" % self.info
|
||||
|
||||
|
||||
class InvalidRequestLine(ParseException):
|
||||
def __init__(self, req):
|
||||
self.req = req
|
||||
self.code = 400
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid HTTP request line: %r" % self.req
|
||||
|
||||
|
||||
class InvalidRequestMethod(ParseException):
|
||||
def __init__(self, method):
|
||||
self.method = method
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid HTTP method: %r" % self.method
|
||||
|
||||
|
||||
class ExpectationFailed(ParseException):
|
||||
def __init__(self, expect):
|
||||
self.expect = expect
|
||||
|
||||
def __str__(self):
|
||||
return "Unable to comply with expectation: %r" % (self.expect, )
|
||||
|
||||
|
||||
class InvalidHTTPVersion(ParseException):
|
||||
def __init__(self, version):
|
||||
self.version = version
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid HTTP Version: %r" % (self.version,)
|
||||
|
||||
|
||||
class InvalidHeader(ParseException):
|
||||
def __init__(self, hdr, req=None):
|
||||
self.hdr = hdr
|
||||
self.req = req
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid HTTP Header: %r" % self.hdr
|
||||
|
||||
|
||||
class ObsoleteFolding(ParseException):
|
||||
def __init__(self, hdr):
|
||||
self.hdr = hdr
|
||||
|
||||
def __str__(self):
|
||||
return "Obsolete line folding is unacceptable: %r" % (self.hdr, )
|
||||
|
||||
|
||||
class InvalidHeaderName(ParseException):
|
||||
def __init__(self, hdr):
|
||||
self.hdr = hdr
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid HTTP header name: %r" % self.hdr
|
||||
|
||||
|
||||
class UnsupportedTransferCoding(ParseException):
|
||||
def __init__(self, hdr):
|
||||
self.hdr = hdr
|
||||
self.code = 501
|
||||
|
||||
def __str__(self):
|
||||
return "Unsupported transfer coding: %r" % self.hdr
|
||||
|
||||
|
||||
class InvalidChunkSize(IOError):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid chunk size: %r" % self.data
|
||||
|
||||
|
||||
class ChunkMissingTerminator(IOError):
|
||||
def __init__(self, term):
|
||||
self.term = term
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid chunk terminator is not '\\r\\n': %r" % self.term
|
||||
|
||||
|
||||
class InvalidChunkExtension(IOError):
|
||||
"""Invalid chunk extension per RFC 9112."""
|
||||
|
||||
def __init__(self, reason):
|
||||
self.reason = reason
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid chunk extension: %s" % self.reason
|
||||
|
||||
|
||||
class LimitRequestLine(ParseException):
|
||||
def __init__(self, size, max_size=None):
|
||||
self.size = size
|
||||
self.max_size = max_size
|
||||
|
||||
def __str__(self):
|
||||
if self.max_size is None:
|
||||
return str(self.size)
|
||||
return "Request Line is too large (%s > %s)" % (self.size, self.max_size)
|
||||
|
||||
|
||||
class LimitRequestHeaders(ParseException):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
|
||||
def __str__(self):
|
||||
return self.msg
|
||||
|
||||
|
||||
class InvalidProxyLine(ParseException):
|
||||
def __init__(self, line):
|
||||
self.line = line
|
||||
self.code = 400
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid PROXY line: %r" % self.line
|
||||
|
||||
|
||||
class InvalidProxyHeader(ParseException):
|
||||
def __init__(self, msg):
|
||||
self.msg = msg
|
||||
self.code = 400
|
||||
|
||||
def __str__(self):
|
||||
return "Invalid PROXY header: %s" % self.msg
|
||||
|
||||
|
||||
class ForbiddenProxyRequest(ParseException):
|
||||
def __init__(self, host):
|
||||
self.host = host
|
||||
self.code = 403
|
||||
|
||||
def __str__(self):
|
||||
return "Proxy request from %r not allowed" % self.host
|
||||
|
||||
|
||||
class InvalidSchemeHeaders(ParseException):
|
||||
def __str__(self):
|
||||
return "Contradictory scheme headers"
|
||||
|
||||
|
||||
class InvalidH2CPreface(ParseException):
|
||||
def __init__(self, data):
|
||||
self.data = data
|
||||
|
||||
def __str__(self):
|
||||
return "Expected HTTP/2 connection preface, got %r" % (self.data,)
|
||||
+939
@@ -0,0 +1,939 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
from enum import IntEnum
|
||||
import ipaddress
|
||||
import re
|
||||
import socket
|
||||
import struct
|
||||
|
||||
from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body
|
||||
from gunicorn.http.errors import (
|
||||
InvalidHeader, InvalidHeaderName, NoMoreData,
|
||||
InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
|
||||
LimitRequestLine, LimitRequestHeaders,
|
||||
UnsupportedTransferCoding, ObsoleteFolding,
|
||||
ExpectationFailed,
|
||||
)
|
||||
from gunicorn.http.errors import InvalidProxyLine, InvalidProxyHeader, ForbiddenProxyRequest
|
||||
from gunicorn.http.errors import InvalidSchemeHeaders
|
||||
from gunicorn.util import bytes_to_str, split_request_uri
|
||||
|
||||
|
||||
# Fast parser availability (cached at module level)
|
||||
_fast_parser_available = None
|
||||
_fast_parser_module = None
|
||||
|
||||
# Compatibility flags not supported by the fast parser
|
||||
_FAST_PARSER_INCOMPATIBLE_FLAGS = (
|
||||
'permit_obsolete_folding',
|
||||
'strip_header_spaces',
|
||||
)
|
||||
|
||||
|
||||
def _check_fast_parser(cfg):
|
||||
"""Check if fast C parser is available and should be used.
|
||||
|
||||
Returns False if:
|
||||
- http_parser='python' is explicitly set
|
||||
- gunicorn_h1c is not installed (in 'auto' mode)
|
||||
- gunicorn_h1c < 0.4.1 (in 'auto' mode)
|
||||
- Incompatible compatibility flags are enabled (in 'auto' mode)
|
||||
|
||||
Raises RuntimeError if:
|
||||
- http_parser='fast' but gunicorn_h1c is not installed
|
||||
- http_parser='fast' but gunicorn_h1c < 0.4.1
|
||||
- http_parser='fast' but incompatible flags are enabled
|
||||
"""
|
||||
global _fast_parser_available, _fast_parser_module # pylint: disable=global-statement
|
||||
|
||||
parser_setting = getattr(cfg, 'http_parser', 'auto')
|
||||
if parser_setting == 'python':
|
||||
return False
|
||||
|
||||
if _fast_parser_available is None:
|
||||
try:
|
||||
import gunicorn_h1c
|
||||
_fast_parser_available = True
|
||||
_fast_parser_module = gunicorn_h1c
|
||||
except ImportError:
|
||||
_fast_parser_available = False
|
||||
|
||||
if not _fast_parser_available and parser_setting == 'fast':
|
||||
raise RuntimeError("gunicorn_h1c not installed but http_parser='fast'")
|
||||
|
||||
if not _fast_parser_available:
|
||||
return False
|
||||
|
||||
# Require >= 0.4.1 for limit enforcement
|
||||
if not hasattr(_fast_parser_module, 'LimitRequestLine'):
|
||||
if parser_setting == 'fast':
|
||||
raise RuntimeError(
|
||||
"gunicorn_h1c >= 0.4.1 required for http_parser='fast'. "
|
||||
"Please upgrade: pip install --upgrade gunicorn_h1c"
|
||||
)
|
||||
# In 'auto' mode, fall back to Python parser
|
||||
return False
|
||||
|
||||
# Check for incompatible compatibility flags
|
||||
incompatible = []
|
||||
for flag in _FAST_PARSER_INCOMPATIBLE_FLAGS:
|
||||
if getattr(cfg, flag, False):
|
||||
incompatible.append(flag)
|
||||
|
||||
if incompatible:
|
||||
if parser_setting == 'fast':
|
||||
raise RuntimeError(
|
||||
"http_parser='fast' is incompatible with compatibility flags: %s. "
|
||||
"Use http_parser='python' or disable these flags."
|
||||
% ', '.join(incompatible)
|
||||
)
|
||||
# In 'auto' mode, fall back to Python parser
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# PROXY protocol v2 constants
|
||||
PP_V2_SIGNATURE = b"\x0D\x0A\x0D\x0A\x00\x0D\x0A\x51\x55\x49\x54\x0A"
|
||||
|
||||
|
||||
class PPCommand(IntEnum):
|
||||
"""PROXY protocol v2 commands."""
|
||||
LOCAL = 0x0
|
||||
PROXY = 0x1
|
||||
|
||||
|
||||
class PPFamily(IntEnum):
|
||||
"""PROXY protocol v2 address families."""
|
||||
UNSPEC = 0x0
|
||||
INET = 0x1 # IPv4
|
||||
INET6 = 0x2 # IPv6
|
||||
UNIX = 0x3
|
||||
|
||||
|
||||
class PPProtocol(IntEnum):
|
||||
"""PROXY protocol v2 transport protocols."""
|
||||
UNSPEC = 0x0
|
||||
STREAM = 0x1 # TCP
|
||||
DGRAM = 0x2 # UDP
|
||||
|
||||
|
||||
MAX_REQUEST_LINE = 8190
|
||||
MAX_HEADERS = 32768
|
||||
DEFAULT_MAX_HEADERFIELD_SIZE = 8190
|
||||
|
||||
# verbosely on purpose, avoid backslash ambiguity
|
||||
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#]")
|
||||
# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions
|
||||
VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)")
|
||||
# RFC 9110 section 5.5: field-vchar = VCHAR / obs-text; SP and HTAB are the
|
||||
# only non-VCHAR bytes allowed in a field-value. Anything else in the
|
||||
# control range (0x00-0x1F except HTAB, plus DEL 0x7F) must be rejected.
|
||||
RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]")
|
||||
|
||||
# RFC 9110 section 6.5.1: fields forbidden in trailers because they alter
|
||||
# routing, framing, or authentication. Using the uppercased names stored
|
||||
# by parse_headers.
|
||||
RFC9110_6_5_1_FORBIDDEN_TRAILER = frozenset((
|
||||
"HOST",
|
||||
"CONTENT-LENGTH",
|
||||
"TRANSFER-ENCODING",
|
||||
"TRAILER",
|
||||
"AUTHORIZATION",
|
||||
"TE",
|
||||
))
|
||||
|
||||
# RFC 9110 section 5.3: fields whose grammar admits only a single member, so a
|
||||
# second occurrence cannot be merged and makes the message ambiguous.
|
||||
# CONTENT-LENGTH is deliberately absent: duplicates are already rejected in
|
||||
# set_body_reader(), which reports them with the request attached.
|
||||
RFC9110_5_3_SINGLETON_FIELDS = frozenset((
|
||||
"HOST",
|
||||
"CONTENT-TYPE",
|
||||
))
|
||||
|
||||
|
||||
def _ip_in_allow_list(ip_str, allow_list, networks):
|
||||
"""Check if IP address is in the allow list.
|
||||
|
||||
Args:
|
||||
ip_str: The IP address string to check
|
||||
allow_list: The original allow list (strings, may contain "*")
|
||||
networks: Pre-computed ipaddress.ip_network objects from config
|
||||
"""
|
||||
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 HeaderPolicy:
|
||||
"""Header policy shared by every request path.
|
||||
|
||||
Applies to HTTP/1 (both parsers) and HTTP/2 alike, so a rule added here
|
||||
cannot be enforced on one protocol and quietly skipped on another.
|
||||
Requires ``self.cfg``, ``self.peer_addr``, ``self.scheme`` and
|
||||
``self.version`` to be set before any header is applied.
|
||||
"""
|
||||
|
||||
#: Set by whichever class mixes this in, before headers are applied.
|
||||
scheme = None
|
||||
version = None
|
||||
_expected_100_continue = False
|
||||
|
||||
#: HTTP/2 has no 100-continue handshake on the wire the way HTTP/1 does,
|
||||
#: and gunicorn answers one by writing HTTP/1 bytes straight to the socket
|
||||
#: (see wsgi.create), which would corrupt an HTTP/2 connection.
|
||||
_policy_expect_continue = True
|
||||
|
||||
def _peer_is_trusted_proxy(self):
|
||||
"""Whether the peer may set forwarding and scheme headers."""
|
||||
cfg = self.cfg
|
||||
return (not isinstance(self.peer_addr, tuple)
|
||||
or _ip_in_allow_list(self.peer_addr[0], cfg.forwarded_allow_ips,
|
||||
cfg.forwarded_allow_networks()))
|
||||
|
||||
def _peer_trusted_for_forwarded(self):
|
||||
"""Return the (secure_scheme_headers, forwarder_headers) the peer is allowed to set.
|
||||
|
||||
When the peer's address is not in ``forwarded_allow_ips`` (or networks),
|
||||
configured forwarding/secure-scheme policy must be ignored to prevent
|
||||
spoofing. Returns ``({}, [])`` when the peer is untrusted.
|
||||
"""
|
||||
cfg = self.cfg
|
||||
if self._peer_is_trusted_proxy():
|
||||
return cfg.secure_scheme_headers, cfg.forwarder_headers
|
||||
return {}, []
|
||||
|
||||
def _apply_header_policy(self, name, value, scheme_state, seen,
|
||||
secure_scheme_headers, forwarder_headers,
|
||||
from_trailer=False):
|
||||
"""Apply per-header policy shared between Python and fast parsers.
|
||||
|
||||
Mutates ``self._expected_100_continue`` and ``self.scheme`` as needed.
|
||||
``scheme_state`` is a single-element list used as a mutable sentinel
|
||||
so the caller can detect repeated scheme headers. ``seen`` is a set of
|
||||
the field names already accepted for this message, used the same way to
|
||||
detect repeated singleton fields.
|
||||
|
||||
Returns the (name, value) pair to retain, or ``None`` to drop the
|
||||
header (per ``header_map='drop'``). Raises the same exceptions the
|
||||
Python path raises so behavior is identical regardless of parser.
|
||||
"""
|
||||
# https://datatracker.ietf.org/doc/html/rfc9110#section-5.3
|
||||
# A singleton field cannot be combined into a list, so a repeat leaves
|
||||
# the message ambiguous and open to being read differently by us and by
|
||||
# anything downstream.
|
||||
if name in RFC9110_5_3_SINGLETON_FIELDS:
|
||||
if name in seen:
|
||||
raise InvalidHeader(name, req=self)
|
||||
seen.add(name)
|
||||
|
||||
if (self._policy_expect_continue and not from_trailer
|
||||
and name == "EXPECT"):
|
||||
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1
|
||||
# "The Expect field value is case-insensitive."
|
||||
if value.lower() == "100-continue":
|
||||
if self.version < (1, 1):
|
||||
# https://datatracker.ietf.org/doc/html/rfc9110#section-10.1.1-12
|
||||
# "A server that receives a 100-continue expectation
|
||||
# in an HTTP/1.0 request MUST ignore that expectation."
|
||||
pass
|
||||
else:
|
||||
self._expected_100_continue = True
|
||||
# N.B. understood but ignored expect header does not return 417
|
||||
else:
|
||||
raise ExpectationFailed(value)
|
||||
|
||||
if name in secure_scheme_headers:
|
||||
secure = value == secure_scheme_headers[name]
|
||||
scheme = "https" if secure else "http"
|
||||
if scheme_state[0]:
|
||||
if scheme != self.scheme:
|
||||
raise InvalidSchemeHeaders()
|
||||
else:
|
||||
scheme_state[0] = True
|
||||
self.scheme = scheme
|
||||
|
||||
# ambiguous mapping allows fooling downstream, e.g. merging non-identical headers:
|
||||
# X-Forwarded-For: 2001:db8::ha:cc:ed
|
||||
# X_Forwarded_For: 127.0.0.1,::1
|
||||
# HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1
|
||||
# Only modify after fixing *ALL* header transformations; network to wsgi env
|
||||
if "_" in name:
|
||||
if name in forwarder_headers or "*" in forwarder_headers:
|
||||
# This forwarder may override our environment
|
||||
pass
|
||||
elif self.cfg.header_map == "dangerous":
|
||||
# as if we did not know we cannot safely map this
|
||||
pass
|
||||
elif self.cfg.header_map == "drop":
|
||||
# almost as if it never had been there
|
||||
# but still counts against resource limits
|
||||
return None
|
||||
else:
|
||||
# fail-safe fallthrough: refuse
|
||||
raise InvalidHeaderName(name)
|
||||
|
||||
return (name, value)
|
||||
|
||||
|
||||
class Message(HeaderPolicy):
|
||||
def __init__(self, cfg, unreader, peer_addr):
|
||||
self.cfg = cfg
|
||||
self.unreader = unreader
|
||||
self.peer_addr = peer_addr
|
||||
self.remote_addr = peer_addr
|
||||
self.version = None
|
||||
self.headers = []
|
||||
self.trailers = []
|
||||
self.body = None
|
||||
self.scheme = "https" if cfg.is_ssl else "http"
|
||||
self.must_close = False
|
||||
self._expected_100_continue = False
|
||||
|
||||
# set headers limits
|
||||
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
|
||||
|
||||
# set max header buffer 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
|
||||
|
||||
unused = self.parse(self.unreader)
|
||||
self.unreader.unread(unused)
|
||||
self.set_body_reader()
|
||||
|
||||
def force_close(self):
|
||||
self.must_close = True
|
||||
|
||||
def parse(self, unreader):
|
||||
raise NotImplementedError()
|
||||
|
||||
def parse_headers(self, data, from_trailer=False):
|
||||
headers = []
|
||||
|
||||
# Split lines on \r\n
|
||||
lines = [bytes_to_str(line) for line in data.split(b"\r\n")]
|
||||
|
||||
# handle scheme headers
|
||||
scheme_state = [False]
|
||||
seen = set()
|
||||
if from_trailer:
|
||||
# nonsense. either a request is https from the beginning
|
||||
# .. or we are just behind a proxy who does not remove conflicting trailers
|
||||
secure_scheme_headers, forwarder_headers = {}, []
|
||||
else:
|
||||
secure_scheme_headers, forwarder_headers = self._peer_trusted_for_forwarded()
|
||||
|
||||
# Parse headers into key/value pairs paying attention
|
||||
# to continuation lines.
|
||||
while lines:
|
||||
if len(headers) >= self.limit_request_fields:
|
||||
raise LimitRequestHeaders("limit request headers fields")
|
||||
|
||||
# Parse initial header name: value pair.
|
||||
curr = lines.pop(0)
|
||||
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)
|
||||
|
||||
# this is still a dangerous place to do this
|
||||
# but it is more correct than doing it before the pattern match:
|
||||
# after we entered Unicode wonderland, 8bits could case-shift into ASCII:
|
||||
# b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS"
|
||||
name = name.upper()
|
||||
|
||||
# RFC 9110 section 6.5.1
|
||||
if from_trailer and name in RFC9110_6_5_1_FORBIDDEN_TRAILER:
|
||||
raise InvalidHeaderName(name)
|
||||
|
||||
value = [value.strip(" \t")]
|
||||
|
||||
# Consume value continuation lines..
|
||||
while lines and lines[0].startswith((" ", "\t")):
|
||||
# .. which is obsolete here, and no longer done by default
|
||||
if not self.cfg.permit_obsolete_folding:
|
||||
raise ObsoleteFolding(name)
|
||||
curr = lines.pop(0)
|
||||
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")
|
||||
|
||||
kept = self._apply_header_policy(
|
||||
name, value, scheme_state, seen,
|
||||
secure_scheme_headers, forwarder_headers,
|
||||
from_trailer=from_trailer,
|
||||
)
|
||||
if kept is None:
|
||||
continue
|
||||
headers.append(kept)
|
||||
|
||||
return headers
|
||||
|
||||
def set_body_reader(self):
|
||||
chunked = False
|
||||
content_length = None
|
||||
|
||||
for (name, value) in self.headers:
|
||||
if name == "CONTENT-LENGTH":
|
||||
if content_length is not None:
|
||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||
content_length = value
|
||||
elif name == "TRANSFER-ENCODING":
|
||||
# T-E can be a list
|
||||
# https://datatracker.ietf.org/doc/html/rfc9112#name-transfer-encoding
|
||||
vals = [v.strip() for v in value.split(',')]
|
||||
for val in vals:
|
||||
if val.lower() == "chunked":
|
||||
# DANGER: transfer codings stack, and stacked chunking is never intended
|
||||
if chunked:
|
||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||
chunked = True
|
||||
elif val.lower() == "identity":
|
||||
# does not do much, could still plausibly desync from what the proxy does
|
||||
# safe option: nuke it, its never needed
|
||||
if chunked:
|
||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||
elif val.lower() in ('compress', 'deflate', 'gzip'):
|
||||
# chunked should be the last one
|
||||
if chunked:
|
||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||
self.force_close()
|
||||
else:
|
||||
raise UnsupportedTransferCoding(value)
|
||||
|
||||
if chunked:
|
||||
# two potentially dangerous cases:
|
||||
# a) CL + TE (TE overrides CL.. only safe if the recipient sees it that way too)
|
||||
# b) chunked HTTP/1.0 (always faulty)
|
||||
if self.version < (1, 1):
|
||||
# framing wonky, see RFC 9112 Section 6.1
|
||||
raise InvalidHeader("TRANSFER-ENCODING", req=self)
|
||||
if content_length is not None:
|
||||
# we cannot be certain the message framing we understood matches proxy intent
|
||||
# -> whatever happens next, remaining input must not be trusted
|
||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||
self.body = Body(ChunkedReader(self, self.unreader))
|
||||
elif content_length is not None:
|
||||
try:
|
||||
if str(content_length).isnumeric():
|
||||
content_length = int(content_length)
|
||||
else:
|
||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||
except ValueError:
|
||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||
|
||||
if content_length < 0:
|
||||
raise InvalidHeader("CONTENT-LENGTH", req=self)
|
||||
|
||||
self.body = Body(LengthReader(self.unreader, content_length))
|
||||
else:
|
||||
self.body = Body(EOFReader(self.unreader))
|
||||
|
||||
def should_close(self):
|
||||
if self.must_close:
|
||||
return True
|
||||
for (h, v) in self.headers:
|
||||
if h == "CONNECTION":
|
||||
v = v.lower().strip(" \t")
|
||||
if v == "close":
|
||||
return True
|
||||
elif v == "keep-alive":
|
||||
return False
|
||||
break
|
||||
return self.version <= (1, 0)
|
||||
|
||||
|
||||
class Request(Message):
|
||||
def __init__(self, cfg, unreader, peer_addr, req_number=1):
|
||||
self.method = None
|
||||
self.uri = None
|
||||
self.path = None
|
||||
self.query = None
|
||||
self.fragment = None
|
||||
|
||||
# get max request line size (0 means unlimited per documentation)
|
||||
self.limit_request_line = cfg.limit_request_line
|
||||
if self.limit_request_line < 0:
|
||||
self.limit_request_line = MAX_REQUEST_LINE
|
||||
# For fast parser: use large value when unlimited (0), since C parser
|
||||
# doesn't support 0 as unlimited. 1MB should be more than enough.
|
||||
if self.limit_request_line == 0:
|
||||
self._fast_limit_request_line = 1024 * 1024 # 1MB
|
||||
elif self.limit_request_line >= MAX_REQUEST_LINE:
|
||||
self._fast_limit_request_line = MAX_REQUEST_LINE
|
||||
self.limit_request_line = MAX_REQUEST_LINE
|
||||
else:
|
||||
self._fast_limit_request_line = self.limit_request_line
|
||||
|
||||
self.req_number = req_number
|
||||
self.proxy_protocol_info = None
|
||||
|
||||
# Check if fast parser should be used
|
||||
self._use_fast = _check_fast_parser(cfg)
|
||||
|
||||
super().__init__(cfg, unreader, peer_addr)
|
||||
|
||||
def get_data(self, unreader, buf, stop=False):
|
||||
data = unreader.read()
|
||||
if not data:
|
||||
if stop:
|
||||
raise StopIteration()
|
||||
raise NoMoreData(buf.getvalue())
|
||||
buf.write(data)
|
||||
|
||||
def parse(self, unreader):
|
||||
buf = bytearray()
|
||||
self.read_into(unreader, buf, stop=True)
|
||||
|
||||
# Handle proxy protocol if enabled and this is the first request
|
||||
mode = self.cfg.proxy_protocol
|
||||
if mode != "off" and self.req_number == 1:
|
||||
buf = self._handle_proxy_protocol(unreader, buf, mode)
|
||||
|
||||
# Use fast parser if available
|
||||
if self._use_fast:
|
||||
return self._parse_fast(unreader, buf)
|
||||
|
||||
return self._parse_python(unreader, buf)
|
||||
|
||||
def _parse_fast(self, unreader, buf):
|
||||
"""Parse request using fast C parser (gunicorn_h1c >= 0.4.1)."""
|
||||
# Read until we have complete headers
|
||||
data = bytes(buf)
|
||||
last_len = 0
|
||||
|
||||
while True:
|
||||
try:
|
||||
# Pass all limit parameters to C parser
|
||||
# Use _fast_limit_request_line which handles 0=unlimited
|
||||
result = _fast_parser_module.parse_request(
|
||||
data,
|
||||
last_len=last_len,
|
||||
limit_request_line=self._fast_limit_request_line,
|
||||
limit_request_fields=self.limit_request_fields,
|
||||
limit_request_field_size=self.limit_request_field_size,
|
||||
permit_unconventional_http_method=self.cfg.permit_unconventional_http_method,
|
||||
permit_unconventional_http_version=self.cfg.permit_unconventional_http_version,
|
||||
)
|
||||
break
|
||||
except _fast_parser_module.IncompleteError:
|
||||
last_len = len(data)
|
||||
self.read_into(unreader, buf)
|
||||
data = bytes(buf)
|
||||
if len(data) > self.max_buffer_headers + self._fast_limit_request_line:
|
||||
raise LimitRequestHeaders("max buffer headers")
|
||||
except _fast_parser_module.LimitRequestLine as e:
|
||||
raise LimitRequestLine(str(e))
|
||||
except _fast_parser_module.LimitRequestHeaders as e:
|
||||
raise LimitRequestHeaders(str(e))
|
||||
except _fast_parser_module.InvalidRequestMethod as e:
|
||||
raise InvalidRequestMethod(str(e))
|
||||
except _fast_parser_module.InvalidHTTPVersion as e:
|
||||
raise InvalidHTTPVersion(str(e))
|
||||
except _fast_parser_module.InvalidHeaderName as e:
|
||||
raise InvalidHeaderName(str(e))
|
||||
except _fast_parser_module.InvalidHeader as e:
|
||||
raise InvalidHeader(str(e))
|
||||
except _fast_parser_module.ParseError as e:
|
||||
raise InvalidRequestLine(str(e))
|
||||
|
||||
# Extract parsed data
|
||||
self.method = bytes_to_str(result['method'])
|
||||
self.uri = bytes_to_str(result['path'])
|
||||
|
||||
# Casefold method if configured (validation done by C parser)
|
||||
if self.cfg.casefold_http_method:
|
||||
self.method = self.method.upper()
|
||||
|
||||
# Parse URI parts
|
||||
if len(self.uri) == 0:
|
||||
raise InvalidRequestLine(self.uri)
|
||||
try:
|
||||
parts = split_request_uri(self.uri)
|
||||
except ValueError:
|
||||
raise InvalidRequestLine(self.uri)
|
||||
self.path = parts.path or ""
|
||||
self.query = parts.query or ""
|
||||
self.fragment = parts.fragment or ""
|
||||
|
||||
# Version (validation done by C parser)
|
||||
self.version = (1, result['minor_version'])
|
||||
|
||||
# Headers - convert bytes to strings with uppercase names
|
||||
# gunicorn_h1c returns headers as (bytes, bytes) tuples
|
||||
# Header name/value validation done by C parser; policy (Expect,
|
||||
# secure_scheme_headers, forwarder trust gate, header_map) is enforced
|
||||
# below so the fast path mirrors parse_headers().
|
||||
self.headers = []
|
||||
scheme_state = [False]
|
||||
seen = set()
|
||||
secure_scheme_headers, forwarder_headers = self._peer_trusted_for_forwarded()
|
||||
for name_bytes, value_bytes in result['headers']:
|
||||
name = bytes_to_str(name_bytes).upper()
|
||||
value = bytes_to_str(value_bytes)
|
||||
|
||||
kept = self._apply_header_policy(
|
||||
name, value, scheme_state, seen,
|
||||
secure_scheme_headers, forwarder_headers,
|
||||
)
|
||||
if kept is None:
|
||||
continue
|
||||
self.headers.append(kept)
|
||||
|
||||
# Return remaining data after headers
|
||||
consumed = result['consumed']
|
||||
return data[consumed:]
|
||||
|
||||
def _parse_python(self, unreader, buf):
|
||||
"""Parse request using pure Python parser."""
|
||||
# Get request line
|
||||
line, buf = self.read_line(unreader, buf, self.limit_request_line)
|
||||
|
||||
self.parse_request_line(line)
|
||||
|
||||
# Headers
|
||||
data = bytes(buf)
|
||||
|
||||
done = data[:2] == b"\r\n"
|
||||
while True:
|
||||
idx = data.find(b"\r\n\r\n")
|
||||
done = data[:2] == b"\r\n"
|
||||
|
||||
if idx < 0 and not done:
|
||||
self.read_into(unreader, buf)
|
||||
data = bytes(buf)
|
||||
if len(data) > self.max_buffer_headers:
|
||||
raise LimitRequestHeaders("max buffer headers")
|
||||
else:
|
||||
break
|
||||
|
||||
if done:
|
||||
self.unreader.unread(data[2:])
|
||||
return b""
|
||||
|
||||
self.headers = self.parse_headers(data[:idx], from_trailer=False)
|
||||
|
||||
ret = data[idx + 4:]
|
||||
return ret
|
||||
|
||||
def read_into(self, unreader, buf, stop=False):
|
||||
"""Read data from unreader and append to bytearray buffer."""
|
||||
data = unreader.read()
|
||||
if not data:
|
||||
if stop:
|
||||
raise StopIteration()
|
||||
raise NoMoreData(bytes(buf))
|
||||
buf.extend(data)
|
||||
|
||||
def read_line(self, unreader, buf, limit=0):
|
||||
"""Read a line from buffer, returning (line, remaining_buffer)."""
|
||||
data = bytes(buf)
|
||||
|
||||
while True:
|
||||
idx = data.find(b"\r\n")
|
||||
if idx >= 0:
|
||||
# check if the request line is too large
|
||||
if idx > limit > 0:
|
||||
raise LimitRequestLine(idx, limit)
|
||||
break
|
||||
if len(data) - 2 > limit > 0:
|
||||
raise LimitRequestLine(len(data), limit)
|
||||
self.read_into(unreader, buf)
|
||||
data = bytes(buf)
|
||||
|
||||
return (data[:idx], # request line,
|
||||
bytearray(data[idx + 2:])) # residue in the buffer, skip \r\n
|
||||
|
||||
def read_bytes(self, unreader, buf, count):
|
||||
"""Read exactly count bytes from buffer/unreader."""
|
||||
while len(buf) < count:
|
||||
self.read_into(unreader, buf)
|
||||
return bytes(buf[:count]), bytearray(buf[count:])
|
||||
|
||||
def _handle_proxy_protocol(self, unreader, buf, mode):
|
||||
"""Handle PROXY protocol detection and parsing.
|
||||
|
||||
Returns the buffer with proxy protocol data consumed.
|
||||
"""
|
||||
# Ensure we have enough data to detect v2 signature (12 bytes)
|
||||
while len(buf) < 12:
|
||||
self.read_into(unreader, buf)
|
||||
|
||||
# Check for v2 signature first
|
||||
if mode in ("v2", "auto") and buf[:12] == PP_V2_SIGNATURE:
|
||||
self.proxy_protocol_access_check()
|
||||
return self._parse_proxy_protocol_v2(unreader, buf)
|
||||
|
||||
# Check for v1 prefix
|
||||
if mode in ("v1", "auto") and buf[:6] == b"PROXY ":
|
||||
self.proxy_protocol_access_check()
|
||||
return self._parse_proxy_protocol_v1(unreader, buf)
|
||||
|
||||
# Not proxy protocol - return buffer unchanged
|
||||
return buf
|
||||
|
||||
def proxy_protocol_access_check(self):
|
||||
"""Check if proxy protocol is allowed from this peer."""
|
||||
if (isinstance(self.peer_addr, tuple) and
|
||||
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_protocol_v1(self, unreader, buf):
|
||||
"""Parse PROXY protocol v1 (text format).
|
||||
|
||||
Returns buffer with v1 header consumed.
|
||||
"""
|
||||
# Read until we find \r\n
|
||||
data = bytes(buf)
|
||||
while b"\r\n" not in data:
|
||||
self.read_into(unreader, buf)
|
||||
data = bytes(buf)
|
||||
|
||||
idx = data.find(b"\r\n")
|
||||
line = bytes_to_str(data[:idx])
|
||||
remaining = bytearray(data[idx + 2:])
|
||||
|
||||
bits = line.split(" ")
|
||||
|
||||
if len(bits) != 6:
|
||||
raise InvalidProxyLine(line)
|
||||
|
||||
# Extract data
|
||||
proto = bits[1]
|
||||
s_addr = bits[2]
|
||||
d_addr = bits[3]
|
||||
|
||||
# Validation
|
||||
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)
|
||||
|
||||
# Set data
|
||||
self.proxy_protocol_info = {
|
||||
"proxy_protocol": proto,
|
||||
"client_addr": s_addr,
|
||||
"client_port": s_port,
|
||||
"proxy_addr": d_addr,
|
||||
"proxy_port": d_port
|
||||
}
|
||||
|
||||
return remaining
|
||||
|
||||
def _parse_proxy_protocol_v2(self, unreader, buf):
|
||||
"""Parse PROXY protocol v2 (binary format).
|
||||
|
||||
Returns buffer with v2 header consumed.
|
||||
"""
|
||||
# We need at least 16 bytes for the header (12 signature + 4 header)
|
||||
while len(buf) < 16:
|
||||
self.read_into(unreader, buf)
|
||||
|
||||
# Parse header fields (after 12-byte signature)
|
||||
ver_cmd = buf[12]
|
||||
fam_proto = buf[13]
|
||||
length = struct.unpack(">H", bytes(buf[14:16]))[0]
|
||||
|
||||
# Validate version (high nibble must be 0x2)
|
||||
version = (ver_cmd & 0xF0) >> 4
|
||||
if version != 2:
|
||||
raise InvalidProxyHeader("unsupported version %d" % version)
|
||||
|
||||
# Extract command (low nibble)
|
||||
command = ver_cmd & 0x0F
|
||||
if command not in (PPCommand.LOCAL, PPCommand.PROXY):
|
||||
raise InvalidProxyHeader("unsupported command %d" % command)
|
||||
|
||||
# Ensure we have the complete header
|
||||
total_header_size = 16 + length
|
||||
while len(buf) < total_header_size:
|
||||
self.read_into(unreader, buf)
|
||||
|
||||
# For LOCAL command, no address info is provided
|
||||
if command == PPCommand.LOCAL:
|
||||
self.proxy_protocol_info = {
|
||||
"proxy_protocol": "LOCAL",
|
||||
"client_addr": None,
|
||||
"client_port": None,
|
||||
"proxy_addr": None,
|
||||
"proxy_port": None
|
||||
}
|
||||
return bytearray(buf[total_header_size:])
|
||||
|
||||
# Extract address family and protocol
|
||||
family = (fam_proto & 0xF0) >> 4
|
||||
protocol = fam_proto & 0x0F
|
||||
|
||||
# We only support TCP (STREAM)
|
||||
if protocol != PPProtocol.STREAM:
|
||||
raise InvalidProxyHeader("only TCP protocol is supported")
|
||||
|
||||
addr_data = bytes(buf[16:16 + length])
|
||||
|
||||
if family == PPFamily.INET: # IPv4
|
||||
if length < 12: # 4+4+2+2
|
||||
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: # IPv6
|
||||
if length < 36: # 16+16+2+2
|
||||
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:
|
||||
# No address info provided with PROXY command
|
||||
self.proxy_protocol_info = {
|
||||
"proxy_protocol": "UNSPEC",
|
||||
"client_addr": None,
|
||||
"client_port": None,
|
||||
"proxy_addr": None,
|
||||
"proxy_port": None
|
||||
}
|
||||
return bytearray(buf[total_header_size:])
|
||||
|
||||
else:
|
||||
raise InvalidProxyHeader("unsupported address family %d" % family)
|
||||
|
||||
# Set data
|
||||
self.proxy_protocol_info = {
|
||||
"proxy_protocol": proto,
|
||||
"client_addr": s_addr,
|
||||
"client_port": s_port,
|
||||
"proxy_addr": d_addr,
|
||||
"proxy_port": d_port
|
||||
}
|
||||
|
||||
return bytearray(buf[total_header_size:])
|
||||
|
||||
def parse_request_line(self, line_bytes):
|
||||
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: RFC9110 Section 9
|
||||
self.method = bits[0]
|
||||
|
||||
# nonstandard restriction, suitable for all IANA registered methods
|
||||
# partially enforced in previous gunicorn versions
|
||||
if not self.cfg.permit_unconventional_http_method:
|
||||
if METHOD_BADCHAR_RE.search(self.method):
|
||||
raise InvalidRequestMethod(self.method)
|
||||
if not 3 <= len(bits[0]) <= 20:
|
||||
raise InvalidRequestMethod(self.method)
|
||||
# standard restriction: RFC9110 token
|
||||
if not TOKEN_RE.fullmatch(self.method):
|
||||
raise InvalidRequestMethod(self.method)
|
||||
# nonstandard and dangerous
|
||||
# methods are merely uppercase by convention, no case-insensitive treatment is intended
|
||||
if self.cfg.casefold_http_method:
|
||||
self.method = self.method.upper()
|
||||
|
||||
# URI
|
||||
self.uri = bits[1]
|
||||
|
||||
# Python stdlib explicitly tells us it will not perform validation.
|
||||
# https://docs.python.org/3/library/urllib.parse.html#url-parsing-security
|
||||
# There are *four* `request-target` forms in rfc9112, none of them can be empty:
|
||||
# 1. origin-form, which starts with a slash
|
||||
# 2. absolute-form, which starts with a non-empty scheme
|
||||
# 3. authority-form, (for CONNECT) which contains a colon after the host
|
||||
# 4. asterisk-form, which is an asterisk (`\x2A`)
|
||||
# => manually reject one always invalid URI: empty
|
||||
if len(self.uri) == 0:
|
||||
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||
|
||||
# RFC 9112 section 3.2.4: asterisk-form is only valid with OPTIONS.
|
||||
if self.uri == "*" and self.method != "OPTIONS":
|
||||
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||
|
||||
# RFC 9112 section 3.2.3: authority-form ("host:port") is only valid
|
||||
# with CONNECT. origin-form starts with "/"; absolute-form contains
|
||||
# "://". Anything else on a non-CONNECT request is authority-form.
|
||||
if (self.method != "CONNECT"
|
||||
and self.uri != "*"
|
||||
and not self.uri.startswith("/")
|
||||
and "://" not in self.uri):
|
||||
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||
|
||||
try:
|
||||
parts = split_request_uri(self.uri)
|
||||
except ValueError:
|
||||
raise InvalidRequestLine(bytes_to_str(line_bytes))
|
||||
self.path = parts.path or ""
|
||||
self.query = parts.query or ""
|
||||
self.fragment = parts.fragment or ""
|
||||
|
||||
# Version
|
||||
match = VERSION_RE.fullmatch(bits[2])
|
||||
if match is None:
|
||||
raise InvalidHTTPVersion(bits[2])
|
||||
self.version = (int(match.group(1)), int(match.group(2)))
|
||||
if not (1, 0) <= self.version < (2, 0):
|
||||
# if ever relaxing this, carefully review Content-Encoding processing
|
||||
if not self.cfg.permit_unconventional_http_version:
|
||||
raise InvalidHTTPVersion(self.version)
|
||||
|
||||
def set_body_reader(self):
|
||||
super().set_body_reader()
|
||||
if isinstance(self.body.reader, EOFReader):
|
||||
self.body = Body(LengthReader(self.unreader, 0))
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
#
|
||||
# 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=None):
|
||||
"""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; when a deadline is given
|
||||
and ``max_bytes`` is left at the default, ``_DRAIN_MAX_BYTES`` applies
|
||||
to defend against a slow client that keeps trickling under it. When
|
||||
called without a deadline (the default invocation from ``__next__``),
|
||||
no byte cap is applied so the prior unbounded drain semantics are
|
||||
preserved for callers that don't know how to react to a partial drain.
|
||||
|
||||
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
|
||||
|
||||
if max_bytes is None and deadline is not None:
|
||||
max_bytes = _DRAIN_MAX_BYTES
|
||||
|
||||
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 max_bytes is not None and 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
|
||||
@@ -0,0 +1,92 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
import io
|
||||
import os
|
||||
|
||||
# Classes that can undo reading data from
|
||||
# a given type of data source.
|
||||
|
||||
|
||||
class Unreader:
|
||||
def __init__(self):
|
||||
self.buf = io.BytesIO()
|
||||
|
||||
def chunk(self):
|
||||
raise NotImplementedError()
|
||||
|
||||
def take_buffered(self):
|
||||
"""Return read-ahead already held, without touching the source.
|
||||
|
||||
read() blocks on the source when the buffer is empty, which is wrong
|
||||
for a caller that only wants the bytes it has: an Upgrade: h2c
|
||||
handshake needs whatever the client pipelined behind the request,
|
||||
and must not wait for more.
|
||||
"""
|
||||
data = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
return data
|
||||
|
||||
def read(self, size=None):
|
||||
if size is not None and not isinstance(size, int):
|
||||
raise TypeError("size parameter must be an int or long.")
|
||||
|
||||
if size is not None:
|
||||
if size == 0:
|
||||
return b""
|
||||
if size < 0:
|
||||
size = None
|
||||
|
||||
self.buf.seek(0, os.SEEK_END)
|
||||
|
||||
if size is None and self.buf.tell():
|
||||
ret = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
return ret
|
||||
if size is None:
|
||||
d = self.chunk()
|
||||
return d
|
||||
|
||||
while self.buf.tell() < size:
|
||||
chunk = self.chunk()
|
||||
if not chunk:
|
||||
ret = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
return ret
|
||||
self.buf.write(chunk)
|
||||
data = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(data[size:])
|
||||
return data[:size]
|
||||
|
||||
def unread(self, data):
|
||||
rest = self.buf.getvalue()
|
||||
self.buf = io.BytesIO()
|
||||
self.buf.write(data)
|
||||
self.buf.write(rest)
|
||||
|
||||
|
||||
class SocketUnreader(Unreader):
|
||||
def __init__(self, sock, max_chunk=8192):
|
||||
super().__init__()
|
||||
self.sock = sock
|
||||
self.mxchunk = max_chunk
|
||||
|
||||
def chunk(self):
|
||||
return self.sock.recv(self.mxchunk)
|
||||
|
||||
|
||||
class IterUnreader(Unreader):
|
||||
def __init__(self, iterable):
|
||||
super().__init__()
|
||||
self.iter = iter(iterable)
|
||||
|
||||
def chunk(self):
|
||||
if not self.iter:
|
||||
return b""
|
||||
try:
|
||||
return next(self.iter)
|
||||
except StopIteration:
|
||||
self.iter = None
|
||||
return b""
|
||||
+551
@@ -0,0 +1,551 @@
|
||||
#
|
||||
# This file is part of gunicorn released under the MIT license.
|
||||
# See the NOTICE for more information.
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
from gunicorn.http.message import TOKEN_RE
|
||||
from gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName
|
||||
from gunicorn import SERVER_SOFTWARE, SERVER
|
||||
from gunicorn import util
|
||||
|
||||
# Send files in at most 1GB blocks as some operating systems can have problems
|
||||
# with sending files in blocks over 2GB.
|
||||
BLKSIZE = 0x3FFFFFFF
|
||||
|
||||
# RFC9110 5.5: field-vchar = VCHAR / obs-text
|
||||
# RFC4234 B.1: VCHAR = 0x21-x07E = printable ASCII
|
||||
HEADER_VALUE_RE = re.compile(r'[ \t\x21-\x7e\x80-\xff]*')
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileWrapper:
|
||||
|
||||
def __init__(self, filelike, blksize=8192):
|
||||
self.filelike = filelike
|
||||
self.blksize = blksize
|
||||
if hasattr(filelike, 'close'):
|
||||
self.close = filelike.close
|
||||
|
||||
def __getitem__(self, key):
|
||||
data = self.filelike.read(self.blksize)
|
||||
if data:
|
||||
return data
|
||||
raise IndexError
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
data = self.filelike.read(self.blksize)
|
||||
if data:
|
||||
return data
|
||||
raise StopIteration
|
||||
|
||||
|
||||
class WSGIErrorsWrapper(io.RawIOBase):
|
||||
|
||||
def __init__(self, cfg):
|
||||
# There is no public __init__ method for RawIOBase so
|
||||
# we don't need to call super() in the __init__ method.
|
||||
# pylint: disable=super-init-not-called
|
||||
errorlog = logging.getLogger("gunicorn.error")
|
||||
handlers = errorlog.handlers
|
||||
self.streams = []
|
||||
|
||||
if cfg.errorlog == "-":
|
||||
self.streams.append(sys.stderr)
|
||||
handlers = handlers[1:]
|
||||
|
||||
for h in handlers:
|
||||
if hasattr(h, "stream"):
|
||||
self.streams.append(h.stream)
|
||||
|
||||
def write(self, data):
|
||||
for stream in self.streams:
|
||||
try:
|
||||
stream.write(data)
|
||||
except UnicodeError:
|
||||
stream.write(data.encode("UTF-8"))
|
||||
stream.flush()
|
||||
|
||||
|
||||
def base_environ(cfg):
|
||||
return {
|
||||
"wsgi.errors": WSGIErrorsWrapper(cfg),
|
||||
"wsgi.version": (1, 0),
|
||||
"wsgi.multithread": False,
|
||||
"wsgi.multiprocess": (cfg.workers > 1),
|
||||
"wsgi.run_once": False,
|
||||
"wsgi.file_wrapper": FileWrapper,
|
||||
"wsgi.input_terminated": True,
|
||||
"SERVER_SOFTWARE": SERVER_SOFTWARE,
|
||||
}
|
||||
|
||||
|
||||
def default_environ(req, sock, cfg):
|
||||
env = base_environ(cfg)
|
||||
env.update({
|
||||
"wsgi.input": req.body,
|
||||
"gunicorn.socket": sock,
|
||||
"REQUEST_METHOD": req.method,
|
||||
"QUERY_STRING": req.query,
|
||||
"RAW_URI": req.uri,
|
||||
"SERVER_PROTOCOL": "HTTP/%s" % ".".join([str(v) for v in req.version])
|
||||
})
|
||||
return env
|
||||
|
||||
|
||||
def proxy_environ(req):
|
||||
info = req.proxy_protocol_info
|
||||
|
||||
if not info:
|
||||
return {}
|
||||
|
||||
return {
|
||||
"PROXY_PROTOCOL": info["proxy_protocol"],
|
||||
"REMOTE_ADDR": info["client_addr"],
|
||||
"REMOTE_PORT": str(info["client_port"]),
|
||||
"PROXY_ADDR": info["proxy_addr"],
|
||||
"PROXY_PORT": str(info["proxy_port"]),
|
||||
}
|
||||
|
||||
|
||||
def _make_early_hints_callback(req, sock, resp):
|
||||
"""Create a wsgi.early_hints callback for sending 103 Early Hints.
|
||||
|
||||
This allows WSGI applications to send 103 Early Hints responses
|
||||
before the final response, enabling browsers to preload resources.
|
||||
|
||||
Args:
|
||||
req: The request object
|
||||
sock: The socket to write to
|
||||
resp: The Response object to check if headers have been sent
|
||||
|
||||
Returns:
|
||||
A callback function that accepts a list of (name, value) header tuples
|
||||
and sends a 103 Early Hints response.
|
||||
|
||||
Note:
|
||||
- Early hints are only sent for HTTP/1.1 or later clients
|
||||
- HTTP/1.0 clients will silently ignore the callback
|
||||
- Multiple calls are allowed (sending multiple 103 responses)
|
||||
- Calls after response has started are silently ignored
|
||||
"""
|
||||
def send_early_hints(headers):
|
||||
"""Send 103 Early Hints response.
|
||||
|
||||
Args:
|
||||
headers: List of (name, value) header tuples, typically Link headers
|
||||
Example: [('Link', '</style.css>; rel=preload; as=style')]
|
||||
|
||||
Raises:
|
||||
InvalidHeaderName: If a header name is not a valid HTTP token.
|
||||
InvalidHeader: If a header value contains invalid characters.
|
||||
"""
|
||||
# Don't send after response has started - would break framing
|
||||
if resp.headers_sent:
|
||||
return
|
||||
|
||||
# Don't send to HTTP/1.0 clients - they don't support 1xx responses
|
||||
if req.version < (1, 1):
|
||||
return
|
||||
|
||||
# Build 103 response
|
||||
response = b"HTTP/1.1 103 Early Hints\r\n"
|
||||
for name, value in headers:
|
||||
if isinstance(name, bytes):
|
||||
name = name.decode('latin-1')
|
||||
if isinstance(value, bytes):
|
||||
value = value.decode('latin-1')
|
||||
|
||||
# Validate header name and value using the same checks as
|
||||
# Response.process_headers — defense-in-depth against
|
||||
# HTTP response splitting via CRLF injection.
|
||||
if not TOKEN_RE.fullmatch(name):
|
||||
raise InvalidHeaderName('%r' % name)
|
||||
if not HEADER_VALUE_RE.fullmatch(value):
|
||||
# Pass only the name — the invalid value may contain
|
||||
# sensitive data that shouldn't cross security boundaries
|
||||
# via exception propagation (browsers/proxies may forward
|
||||
# it to untrusted parties).
|
||||
raise InvalidHeader('%r' % name)
|
||||
|
||||
value = value.strip(" \t")
|
||||
response += f"{name}: {value}\r\n".encode('latin-1')
|
||||
response += b"\r\n"
|
||||
|
||||
util.write(sock, response)
|
||||
|
||||
return send_early_hints
|
||||
|
||||
|
||||
def create(req, sock, client, server, cfg, response_class=None,
|
||||
response_args=()):
|
||||
"""Build the (response, environ) pair for a request.
|
||||
|
||||
``response_class`` and ``response_args`` let a protocol supply its own
|
||||
writer: HTTP/2 passes HTTP2Response so the body is framed as HTTP/2
|
||||
instead of HTTP/1, while everything else here stays the same.
|
||||
"""
|
||||
if response_class is None:
|
||||
response_class = Response
|
||||
resp = response_class(req, sock, cfg, *response_args)
|
||||
|
||||
# set initial environ
|
||||
environ = default_environ(req, sock, cfg)
|
||||
|
||||
# default variables
|
||||
host = None
|
||||
script_name = os.environ.get("SCRIPT_NAME", "")
|
||||
|
||||
if req._expected_100_continue:
|
||||
sock.send(b"HTTP/1.1 100 Continue\r\n\r\n")
|
||||
# rfc9112: Expect MUST be forwarded if the request is forwarded
|
||||
# N.B. gunicorn just sends at most one - application might send another
|
||||
|
||||
# add the headers to the environ
|
||||
for hdr_name, hdr_value in req.headers:
|
||||
if hdr_name == 'HOST':
|
||||
host = hdr_value
|
||||
elif hdr_name == "SCRIPT_NAME":
|
||||
script_name = hdr_value
|
||||
elif hdr_name == "CONTENT-TYPE":
|
||||
environ['CONTENT_TYPE'] = hdr_value
|
||||
continue
|
||||
elif hdr_name == "CONTENT-LENGTH":
|
||||
environ['CONTENT_LENGTH'] = hdr_value
|
||||
continue
|
||||
|
||||
# do not change lightly, this is a common source of security problems
|
||||
# RFC9110 Section 17.10 discourages ambiguous or incomplete mappings
|
||||
key = 'HTTP_' + hdr_name.replace('-', '_')
|
||||
if key in environ:
|
||||
hdr_value = "%s,%s" % (environ[key], hdr_value)
|
||||
environ[key] = hdr_value
|
||||
|
||||
# set the url scheme
|
||||
environ['wsgi.url_scheme'] = req.scheme
|
||||
|
||||
# set the REMOTE_* keys in environ
|
||||
# authors should be aware that REMOTE_HOST and REMOTE_ADDR
|
||||
# may not qualify the remote addr:
|
||||
# http://www.ietf.org/rfc/rfc3875
|
||||
if isinstance(client, str):
|
||||
environ['REMOTE_ADDR'] = client
|
||||
elif isinstance(client, bytes):
|
||||
environ['REMOTE_ADDR'] = client.decode()
|
||||
else:
|
||||
environ['REMOTE_ADDR'] = client[0]
|
||||
environ['REMOTE_PORT'] = str(client[1])
|
||||
|
||||
# handle the SERVER_*
|
||||
# Normally only the application should use the Host header but since the
|
||||
# WSGI spec doesn't support unix sockets, we are using it to create
|
||||
# viable SERVER_* if possible.
|
||||
if isinstance(server, str):
|
||||
server = server.split(":")
|
||||
if len(server) == 1:
|
||||
# unix socket
|
||||
if host:
|
||||
server = host.split(':')
|
||||
if len(server) == 1:
|
||||
if req.scheme == "http":
|
||||
server.append(80)
|
||||
elif req.scheme == "https":
|
||||
server.append(443)
|
||||
else:
|
||||
server.append('')
|
||||
else:
|
||||
# no host header given which means that we are not behind a
|
||||
# proxy, so append an empty port.
|
||||
server.append('')
|
||||
environ['SERVER_NAME'] = server[0]
|
||||
environ['SERVER_PORT'] = str(server[1])
|
||||
|
||||
# set the path and script name
|
||||
path_info = req.path
|
||||
if script_name:
|
||||
if not path_info.startswith(script_name):
|
||||
raise ConfigurationProblem(
|
||||
"Request path %r does not start with SCRIPT_NAME %r" %
|
||||
(path_info, script_name))
|
||||
path_info = path_info[len(script_name):]
|
||||
environ['PATH_INFO'] = util.unquote_to_wsgi_str(path_info)
|
||||
environ['SCRIPT_NAME'] = script_name
|
||||
|
||||
# override the environ with the correct remote and server address if
|
||||
# we are behind a proxy using the proxy protocol.
|
||||
environ.update(proxy_environ(req))
|
||||
|
||||
# Add wsgi.early_hints callback for sending 103 Early Hints
|
||||
environ['wsgi.early_hints'] = _make_early_hints_callback(req, sock, resp)
|
||||
|
||||
# Add HTTP/2 stream priority if available
|
||||
if hasattr(req, 'priority_weight'):
|
||||
environ['gunicorn.http2.priority_weight'] = req.priority_weight
|
||||
environ['gunicorn.http2.priority_depends_on'] = req.priority_depends_on
|
||||
|
||||
return resp, environ
|
||||
|
||||
|
||||
class Response:
|
||||
|
||||
def __init__(self, req, sock, cfg):
|
||||
self.req = req
|
||||
self.sock = sock
|
||||
self.version = SERVER
|
||||
self.status = None
|
||||
self.chunked = False
|
||||
self.must_close = False
|
||||
self.headers = []
|
||||
self.headers_sent = False
|
||||
self.response_length = None
|
||||
self.sent = 0
|
||||
self.upgrade = False
|
||||
self.cfg = cfg
|
||||
self._omits_body = False
|
||||
self._omits_body_warned = False
|
||||
|
||||
def force_close(self):
|
||||
self.must_close = True
|
||||
|
||||
def should_close(self):
|
||||
if self.must_close or self.req.should_close():
|
||||
return True
|
||||
if self.response_length is not None or self.chunked:
|
||||
return False
|
||||
if self.req.method == 'HEAD':
|
||||
return False
|
||||
if self.status_code < 200 or self.status_code in (204, 304):
|
||||
return False
|
||||
return True
|
||||
|
||||
def start_response(self, status, headers, exc_info=None):
|
||||
if exc_info:
|
||||
try:
|
||||
if self.status and self.headers_sent:
|
||||
util.reraise(exc_info[0], exc_info[1], exc_info[2])
|
||||
finally:
|
||||
exc_info = None
|
||||
elif self.status is not None:
|
||||
raise AssertionError("Response headers already set!")
|
||||
|
||||
self.status = status
|
||||
|
||||
# get the status code from the response here so we can use it to check
|
||||
# the need for the connection header later without parsing the string
|
||||
# each time.
|
||||
try:
|
||||
self.status_code = int(self.status.split()[0])
|
||||
except ValueError:
|
||||
self.status_code = None
|
||||
|
||||
self.process_headers(headers)
|
||||
self._omits_body = self._response_omits_body(
|
||||
self.req.method, self.status_code)
|
||||
if self._omits_body and self._response_forbids_content_length(
|
||||
self.status_code):
|
||||
self.headers = [
|
||||
(k, v) for k, v in self.headers if k.lower() != "content-length"
|
||||
]
|
||||
self.response_length = None
|
||||
self.chunked = self.is_chunked()
|
||||
return self.write
|
||||
|
||||
@staticmethod
|
||||
def _response_omits_body(method, status):
|
||||
# RFC 9110: HEAD requests and 1xx/204/304 responses MUST NOT carry
|
||||
# a body, regardless of what the application emits.
|
||||
return (
|
||||
method == "HEAD"
|
||||
or status in (204, 304)
|
||||
or (status is not None and 100 <= status < 200)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _response_forbids_content_length(status):
|
||||
# RFC 9110 §6.4.2: a server MUST NOT send Content-Length on 1xx or
|
||||
# 204. HEAD MAY include the Content-Length the same GET would carry,
|
||||
# and 304 MAY include the Content-Length of the unconditional response.
|
||||
return status == 204 or (status is not None and 100 <= status < 200)
|
||||
|
||||
def process_headers(self, headers):
|
||||
for name, value in headers:
|
||||
if not isinstance(name, str):
|
||||
raise TypeError('%r is not a string' % name)
|
||||
|
||||
if not TOKEN_RE.fullmatch(name):
|
||||
raise InvalidHeaderName('%r' % name)
|
||||
|
||||
if not isinstance(value, str):
|
||||
raise TypeError('%r is not a string' % value)
|
||||
|
||||
if not HEADER_VALUE_RE.fullmatch(value):
|
||||
raise InvalidHeader('%r' % value)
|
||||
|
||||
# RFC9110 5.5
|
||||
value = value.strip(" \t")
|
||||
lname = name.lower()
|
||||
if lname == "content-length":
|
||||
self.response_length = int(value)
|
||||
elif util.is_hoppish(name):
|
||||
if lname == "connection":
|
||||
# handle websocket
|
||||
if value.lower() == "upgrade":
|
||||
self.upgrade = True
|
||||
elif lname == "upgrade":
|
||||
if value.lower() == "websocket":
|
||||
self.headers.append((name, value))
|
||||
|
||||
# ignore hopbyhop headers
|
||||
continue
|
||||
self.headers.append((name, value))
|
||||
|
||||
def is_chunked(self):
|
||||
# Only use chunked responses when the client is
|
||||
# speaking HTTP/1.1 or newer and there was
|
||||
# no Content-Length header set.
|
||||
if self.response_length is not None:
|
||||
return False
|
||||
elif self.req.version <= (1, 0):
|
||||
return False
|
||||
elif self._omits_body:
|
||||
# No body permitted (HEAD or 1xx/204/304), so no chunked framing.
|
||||
return False
|
||||
return True
|
||||
|
||||
def default_headers(self):
|
||||
# set the connection header
|
||||
if self.upgrade:
|
||||
connection = "upgrade"
|
||||
elif self.should_close():
|
||||
connection = "close"
|
||||
else:
|
||||
connection = "keep-alive"
|
||||
|
||||
headers = [
|
||||
"HTTP/%s.%s %s\r\n" % (self.req.version[0],
|
||||
self.req.version[1], self.status),
|
||||
"Server: %s\r\n" % self.version,
|
||||
"Date: %s\r\n" % util.http_date(),
|
||||
"Connection: %s\r\n" % connection
|
||||
]
|
||||
if self.chunked:
|
||||
headers.append("Transfer-Encoding: chunked\r\n")
|
||||
return headers
|
||||
|
||||
def send_headers(self):
|
||||
if self.headers_sent:
|
||||
return
|
||||
tosend = self.default_headers()
|
||||
tosend.extend(["%s: %s\r\n" % (k, v) for k, v in self.headers])
|
||||
|
||||
header_str = "%s\r\n" % "".join(tosend)
|
||||
util.write(self.sock, util.to_bytestring(header_str, "latin-1"))
|
||||
self.headers_sent = True
|
||||
|
||||
def write(self, arg):
|
||||
self.send_headers()
|
||||
if not isinstance(arg, bytes):
|
||||
raise TypeError('%r is not a byte' % arg)
|
||||
if self._omits_body:
|
||||
if arg and not self._omits_body_warned:
|
||||
log.warning(
|
||||
"WSGI app sent body bytes on a no-body response "
|
||||
"(method=%s status=%s); dropping per RFC 9110.",
|
||||
self.req.method, self.status_code,
|
||||
)
|
||||
self._omits_body_warned = True
|
||||
return
|
||||
arglen = len(arg)
|
||||
tosend = arglen
|
||||
if self.response_length is not None:
|
||||
if self.sent >= self.response_length:
|
||||
# Never write more than self.response_length bytes
|
||||
return
|
||||
|
||||
tosend = min(self.response_length - self.sent, tosend)
|
||||
if tosend < arglen:
|
||||
arg = arg[:tosend]
|
||||
|
||||
# Sending an empty chunk signals the end of the
|
||||
# response and prematurely closes the response
|
||||
if self.chunked and tosend == 0:
|
||||
return
|
||||
|
||||
self.sent += tosend
|
||||
self._emit_body(arg)
|
||||
|
||||
def _emit_body(self, data):
|
||||
"""Put body bytes on the wire.
|
||||
|
||||
The one place body framing happens, so a subclass can frame it
|
||||
differently without reimplementing write()'s bookkeeping.
|
||||
"""
|
||||
util.write(self.sock, data, self.chunked)
|
||||
|
||||
def can_sendfile(self):
|
||||
return self.cfg.sendfile is not False
|
||||
|
||||
def sendfile(self, respiter):
|
||||
if self.cfg.is_ssl or not self.can_sendfile():
|
||||
return False
|
||||
|
||||
if not util.has_fileno(respiter.filelike):
|
||||
return False
|
||||
|
||||
fileno = respiter.filelike.fileno()
|
||||
try:
|
||||
offset = os.lseek(fileno, 0, os.SEEK_CUR)
|
||||
if self.response_length is None:
|
||||
filesize = os.fstat(fileno).st_size
|
||||
nbytes = filesize - offset
|
||||
else:
|
||||
nbytes = self.response_length
|
||||
except (OSError, io.UnsupportedOperation):
|
||||
return False
|
||||
|
||||
if self._omits_body:
|
||||
self.send_headers()
|
||||
# Only complain when there really are body bytes to drop, the
|
||||
# same way write() only warns for a non-empty argument.
|
||||
if nbytes > 0 and not self._omits_body_warned:
|
||||
log.warning(
|
||||
"WSGI app sent body bytes on a no-body response "
|
||||
"(method=%s status=%s); dropping per RFC 9110.",
|
||||
self.req.method, self.status_code,
|
||||
)
|
||||
self._omits_body_warned = True
|
||||
return True
|
||||
|
||||
self.send_headers()
|
||||
|
||||
if self.is_chunked():
|
||||
chunk_size = "%X\r\n" % nbytes
|
||||
self.sock.sendall(chunk_size.encode('utf-8'))
|
||||
if nbytes > 0:
|
||||
self.sock.sendfile(respiter.filelike, offset=offset, count=nbytes)
|
||||
|
||||
if self.is_chunked():
|
||||
self.sock.sendall(b"\r\n")
|
||||
|
||||
os.lseek(fileno, offset, os.SEEK_SET)
|
||||
|
||||
return True
|
||||
|
||||
def write_file(self, respiter):
|
||||
if not self.sendfile(respiter):
|
||||
for item in respiter:
|
||||
self.write(item)
|
||||
|
||||
def close(self):
|
||||
if not self.headers_sent:
|
||||
self.send_headers()
|
||||
if self.chunked:
|
||||
util.write_chunk(self.sock, b"")
|
||||
Reference in New Issue
Block a user