File CVE-2024-1135.patch of Package python-gunicorn.38005

From 559caf920537ece2ef058e1de5e36af44756bb19 Mon Sep 17 00:00:00 2001
From: "Paul J. Dorn" <pajod@users.noreply.github.com>
Date: Wed, 6 Dec 2023 15:30:50 +0100
Subject: [PATCH 01/16] pytest: raise on malformed test fixtures

and unbreak test depending on backslash escape
---
 tests/treq.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

Index: gunicorn-19.7.1/tests/treq.py
===================================================================
--- gunicorn-19.7.1.orig/tests/treq.py
+++ gunicorn-19.7.1/tests/treq.py
@@ -57,7 +57,9 @@ class request(object):
         with open(self.fname, 'rb') as handle:
             self.data = handle.read()
         self.data = self.data.replace(b"\n", b"").replace(b"\\r\\n", b"\r\n")
-        self.data = self.data.replace(b"\\0", b"\000")
+        self.data = self.data.replace(b"\\0", b"\000").replace(b"\\n", b"\n").replace(b"\\t", b"\t")
+        if b"\\" in self.data:
+            raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF")
 
     # Functions for sending data to the parser.
     # These functions mock out reading from a
@@ -252,8 +254,10 @@ class request(object):
     def check(self, cfg, sender, sizer, matcher):
         cases = self.expect[:]
         p = RequestParser(cfg, sender())
-        for req in p:
+        parsed_request_idx = -1
+        for parsed_request_idx, req in enumerate(p):
             self.same(req, sizer, matcher, cases.pop(0))
+        assert len(self.expect) == parsed_request_idx + 1
         assert len(cases) == 0
 
     def same(self, req, sizer, matcher, exp):
@@ -268,7 +272,8 @@ class request(object):
         assert req.trailers == exp.get("trailers", [])
 
 
-class badrequest(object):
+class badrequest:
+    # FIXME: no good reason why this cannot match what the more extensive mechanism above
     def __init__(self, fname):
         self.fname = fname
         self.name = os.path.basename(fname)
@@ -276,7 +281,9 @@ class badrequest(object):
         with open(self.fname) as handle:
             self.data = handle.read()
         self.data = self.data.replace("\n", "").replace("\\r\\n", "\r\n")
-        self.data = self.data.replace("\\0", "\000")
+        self.data = self.data.replace("\\0", "\000").replace("\\n", "\n").replace("\\t", "\t")
+        if "\\" in self.data:
+            raise AssertionError("Unexpected backslash in test data - only handling HTAB, NUL and CRLF")
         self.data = self.data.encode('latin1')
 
     def send(self):
@@ -289,4 +296,6 @@ class badrequest(object):
 
     def check(self, cfg):
         p = RequestParser(cfg, self.send())
-        six.next(p)
+        # must fully consume iterator, otherwise EOF errors could go unnoticed
+        for _ in p:
+            pass
Index: gunicorn-19.7.1/gunicorn/http/body.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/body.py
+++ gunicorn-19.7.1/gunicorn/http/body.py
@@ -49,7 +49,7 @@ class ChunkedReader(object):
         if done:
             unreader.unread(buf.getvalue()[2:])
             return b""
-        self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx])
+        self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True)
         unreader.unread(buf.getvalue()[idx + 4:])
 
     def parse_chunked(self, unreader):
@@ -83,11 +83,13 @@ class ChunkedReader(object):
         data = buf.getvalue()
         line, rest_chunk = data[:idx], data[idx + 2:]
 
-        chunk_size = line.split(b";", 1)[0].strip()
-        try:
-            chunk_size = int(chunk_size, 16)
-        except ValueError:
+        # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then
+        chunk_size, *chunk_ext = line.split(b";", 1)
+        if chunk_ext:
+            chunk_size = chunk_size.rstrip(b" \t")
+        if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size):
             raise InvalidChunkSize(chunk_size)
+        chunk_size = int(chunk_size, 16)
 
         if chunk_size == 0:
             try:
Index: gunicorn-19.7.1/gunicorn/http/message.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/message.py
+++ gunicorn-19.7.1/gunicorn/http/message.py
@@ -12,7 +12,7 @@ from gunicorn.http.unreader import Socke
 from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body
 from gunicorn.http.errors import (InvalidHeader, InvalidHeaderName, NoMoreData,
     InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion,
-    LimitRequestLine, LimitRequestHeaders)
+    LimitRequestLine, LimitRequestHeaders, UnsupportedTransferCoding)
 from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest
 from gunicorn.six import BytesIO
 from gunicorn._compat import urlsplit
@@ -21,9 +21,15 @@ MAX_REQUEST_LINE = 8190
 MAX_HEADERS = 32768
 DEFAULT_MAX_HEADERFIELD_SIZE = 8190
 
-HEADER_RE = re.compile("[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]")
-METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}")
-VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)")
+#HEADER_RE = re.compile("[\x00-\x1F\x7F()<>@,;:\[\]={} \t\\\\\"]")
+#METH_RE = re.compile(r"[A-Z0-9$-_.]{3,20}")
+#VERSION_RE = re.compile(r"HTTP/(\d+)\.(\d+)")
+# 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)")
 
 
 class Message(object):
@@ -34,6 +40,7 @@ class Message(object):
         self.headers = []
         self.trailers = []
         self.body = None
+        self.must_close = False
 
         # set headers limits
         self.limit_request_fields = cfg.limit_request_fields
@@ -53,14 +60,17 @@ class Message(object):
         self.unreader.unread(unused)
         self.set_body_reader()
 
+    def force_close(self):
+        self.must_close = True
+
     def parse(self):
         raise NotImplementedError()
 
-    def parse_headers(self, data):
+    def parse_headers(self, data, from_trailer=False):
         headers = []
 
-        # Split lines on \r\n keeping the \r\n on each line
-        lines = [bytes_to_str(line) + "\r\n" for line in data.split(b"\r\n")]
+        # Split lines on \r\n
+        lines = [bytes_to_str(line) for line in data.split(b"\r\n")]
 
         # Parse headers into key/value pairs paying attention
         # to continuation lines.
@@ -70,28 +80,52 @@ class Message(object):
 
             # Parse initial header name : value pair.
             curr = lines.pop(0)
-            header_length = len(curr)
-            if curr.find(":") < 0:
-                raise InvalidHeader(curr.strip())
+            header_length = len(curr) + len("\r\n")
+            if curr.find(":") <= 0:
+                raise InvalidHeader(curr)
             name, value = curr.split(":", 1)
-            name = name.rstrip(" \t").upper()
-            if HEADER_RE.search(name):
+            name = name.rstrip(" \t")
+            if not TOKEN_RE.fullmatch(name):
                 raise InvalidHeaderName(name)
 
-            name, value = name.strip(), [value.lstrip()]
+            # 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()
+
+            value = [value.lstrip(" \t")]
 
             # Consume value continuation lines
             while len(lines) and lines[0].startswith((" ", "\t")):
                 curr = lines.pop(0)
-                header_length += len(curr)
+                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)
-            value = ''.join(value).rstrip()
+                value.append(curr.strip("\t "))
+            value = " ".join(value)
 
             if header_length > self.limit_request_field_size > 0:
                 raise LimitRequestHeaders("limit request headers fields size")
+
+            # 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 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
+                    continue
+                else:
+                    # fail-safe fallthrough: refuse
+                    raise InvalidHeaderName(name)
+
             headers.append((name, value))
         return headers
 
@@ -102,11 +136,50 @@ class Message(object):
             if name == "CONTENT-LENGTH":
                 content_length = value
             elif name == "TRANSFER-ENCODING":
-                chunked = value.lower() == "chunked"
+                if value.lower() == "chunked":
+                    # DANGER: transer codings stack, and stacked chunking is never intended
+                    if chunked:
+                        raise InvalidHeader("TRANSFER-ENCODING", req=self)
+                    chunked = True
+                elif value.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 value.lower() == "":
+                    # lacking security review on this case
+                    # offer the option to restore previous behaviour, but refuse by default, for now
+                    self.force_close()
+                    if not self.cfg.tolerate_dangerous_framing:
+                        raise UnsupportedTransferCoding(value)
+                # DANGER: do not change lightly; ref: request smuggling
+                # T-E is a list and we *could* support correctly parsing its elements
+                #  .. but that is only safe after getting all the edge cases right
+                #  .. for which no real-world need exists, so best to NOT open that can of worms
+                else:
+                    self.force_close()
+                    # even if parser is extended, retain this branch:
+                    #  the "chunked not last" case remains to be rejected!
+                    raise UnsupportedTransferCoding(value)
             elif name == "SEC-WEBSOCKET-KEY1":
                 content_length = 8
 
         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
+                self.force_close()
+                if not self.cfg.tolerate_dangerous_framing:
+                    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
+                self.force_close()
+                # either processing or rejecting is permitted in RFC 9112 Section 6.1
+                if not self.cfg.tolerate_dangerous_framing:
+                    raise InvalidHeader("CONTENT-LENGTH", req=self)
             self.body = Body(ChunkedReader(self, self.unreader))
         elif content_length is not None:
             try:
@@ -122,9 +195,11 @@ class Message(object):
             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()
+                v = v.lower().strip(" \t")
                 if v == "close":
                     return True
                 elif v == "keep-alive":
@@ -198,7 +273,7 @@ class Request(Message):
             self.unreader.unread(data[2:])
             return b""
 
-        self.headers = self.parse_headers(data[:idx])
+        self.headers = self.parse_headers(data[:idx], from_trailer=False)
 
         ret = data[idx + 4:]
         buf = BytesIO()
@@ -257,7 +332,7 @@ class Request(Message):
                 raise ForbiddenProxyRequest(remote_host)
 
     def parse_proxy_protocol(self, line):
-        bits = line.split()
+        bits = line.split(" ")
 
         if len(bits) != 6:
             raise InvalidProxyLine(line)
@@ -302,14 +377,28 @@ class Request(Message):
         }
 
     def parse_request_line(self, line):
-        bits = line.split(None, 2)
+        line_bytes = line.encode()
+        bits = line.split(" ", 2)
         if len(bits) != 3:
             raise InvalidRequestLine(line)
 
-        # Method
-        if not METH_RE.match(bits[0]):
-            raise InvalidRequestMethod(bits[0])
-        self.method = bits[0].upper()
+        # 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
         # When the path starts with //, urlsplit considers it as a
@@ -331,10 +420,14 @@ class Request(Message):
         self.fragment = parts.fragment or ""
 
         # Version
-        match = VERSION_RE.match(bits[2])
+        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(Request, self).set_body_reader()
Index: gunicorn-19.7.1/gunicorn/http/wsgi.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/wsgi.py
+++ gunicorn-19.7.1/gunicorn/http/wsgi.py
@@ -10,8 +10,8 @@ import re
 import sys
 
 from gunicorn._compat import unquote_to_wsgi_str
-from gunicorn.http.message import HEADER_RE
-from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
+from gunicorn.http.message import TOKEN_RE
+from gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName
 from gunicorn.six import string_types, binary_type, reraise
 from gunicorn import SERVER_SOFTWARE
 import gunicorn.util as util
@@ -29,7 +29,9 @@ except ImportError:
 # with sending files in blocks over 2GB.
 BLKSIZE = 0x3FFFFFFF
 
-HEADER_VALUE_RE = re.compile(r'[\x00-\x1F\x7F]')
+# 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__)
 
@@ -151,6 +153,8 @@ def create(req, sock, client, server, cf
             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)
@@ -198,7 +202,11 @@ def create(req, sock, client, server, cf
     # set the path and script name
     path_info = req.path
     if script_name:
-        path_info = path_info.split(script_name, 1)[1]
+        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'] = unquote_to_wsgi_str(path_info)
     environ['SCRIPT_NAME'] = script_name
 
@@ -267,28 +275,29 @@ class Response(object):
             if not isinstance(name, string_types):
                 raise TypeError('%r is not a string' % name)
 
-            if HEADER_RE.search(name):
+            if not TOKEN_RE.fullmatch(name):
                 raise InvalidHeaderName('%r' % name)
 
-            if HEADER_VALUE_RE.search(value):
+            if not HEADER_VALUE_RE.fullmatch(value):
                 raise InvalidHeader('%r' % value)
 
-            value = str(value).strip()
-            lname = name.lower().strip()
+            # RFC9110 5.5
+            value = str(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().strip() == "upgrade":
+                    if value.lower() == "upgrade":
                         self.upgrade = True
                 elif lname == "upgrade":
-                    if value.lower().strip() == "websocket":
-                        self.headers.append((name.strip(), value))
+                    if value.lower() == "websocket":
+                        self.headers.append((name, value))
 
                 # ignore hopbyhop headers
                 continue
-            self.headers.append((name.strip(), value))
+            self.headers.append((name, value))
 
     def is_chunked(self):
         # Only use chunked responses when the client is
Index: gunicorn-19.7.1/tests/requests/invalid/003.http
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/invalid/003.http
+++ gunicorn-19.7.1/tests/requests/invalid/003.http
@@ -1,2 +1,2 @@
--blargh /foo HTTP/1.1\r\n
-\r\n
\ No newline at end of file
+GET\n/\nHTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/invalid/003.py
+++ gunicorn-19.7.1/tests/requests/invalid/003.py
@@ -1,2 +1,2 @@
-from gunicorn.http.errors import InvalidRequestMethod
-request = InvalidRequestMethod
\ No newline at end of file
+from gunicorn.http.errors import InvalidRequestLine
+request = InvalidRequestLine
Index: gunicorn-19.7.1/tests/requests/valid/031.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031.http
@@ -0,0 +1,2 @@
+-BLARGH /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/031.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031.py
@@ -0,0 +1,7 @@
+request = {
+    "method": "-BLARGH",
+    "uri": uri("/foo"),
+    "version": (1, 1),
+    "headers": [],
+    "body": b""
+}
Index: gunicorn-19.7.1/gunicorn/http/errors.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/http/errors.py
+++ gunicorn-19.7.1/gunicorn/http/errors.py
@@ -16,6 +16,15 @@ class NoMoreData(IOError):
         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
@@ -58,6 +67,15 @@ class InvalidHeaderName(ParseException):
         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
Index: gunicorn-19.7.1/SECURITY.md
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/SECURITY.md
@@ -0,0 +1,22 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+**Please note that public Github issues are open for everyone to see!**
+
+If you believe you are found a problem in Gunicorn software, examples or documentation, we encourage you to send your report privately via email, or via Github using the *Report a vulnerability* button in the [Security](https://github.com/benoitc/gunicorn/security) section.
+
+## Supported Releases
+
+At this time, **only the latest release** receives any security attention whatsoever.
+
+| Version | Status          |
+| ------- | ------------------ |
+| latest release  | :white_check_mark: |
+| 21.2.0  | :x: |
+| 20.0.0  | :x: |
+| < 20.0  | :x: |
+
+## Python Versions
+
+Gunicorn runs on Python 3.7+, we *highly recommend* the latest release of a [supported series](https://devguide.python.org/version/) and will not prioritize issues exclusively affecting in EoL environments.
Index: gunicorn-19.7.1/tests/test_http.py
===================================================================
--- gunicorn-19.7.1.orig/tests/test_http.py
+++ gunicorn-19.7.1/tests/test_http.py
@@ -9,6 +9,17 @@ from gunicorn.http.wsgi import Response
 from gunicorn.http.unreader import Unreader, IterUnreader, SocketUnreader
 from gunicorn.six import BytesIO
 from gunicorn.http.errors import InvalidHeader, InvalidHeaderName
+from gunicorn.http.message import TOKEN_RE
+
+
+def test_method_pattern():
+    assert TOKEN_RE.fullmatch("GET")
+    assert TOKEN_RE.fullmatch("MKCALENDAR")
+    assert not TOKEN_RE.fullmatch("GET:")
+    assert not TOKEN_RE.fullmatch("GET;")
+    RFC9110_5_6_2_TOKEN_DELIM = r'"(),/:;<=>?@[\]{}'
+    for bad_char in RFC9110_5_6_2_TOKEN_DELIM:
+        assert not TOKEN_RE.match(bad_char)
 
 try:
     import unittest.mock as mock
Index: gunicorn-19.7.1/gunicorn/config.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/config.py
+++ gunicorn-19.7.1/gunicorn/config.py
@@ -1858,3 +1858,127 @@ class PasteGlobalConf(Setting):
 
         .. versionadded:: 19.7
         """
+
+
+class PermitUnconventionalHTTPMethod(Setting):
+    name = "permit_unconventional_http_method"
+    section = "Server Mechanics"
+    cli = ["--permit-unconventional-http-method"]
+    validator = validate_bool
+    action = "store_true"
+    default = False
+    desc = """\
+        Permit HTTP methods not matching conventions, such as IANA registration guidelines
+
+        This permits request methods of length less than 3 or more than 20,
+        methods with lowercase characters or methods containing the # character.
+        HTTP methods are case sensitive by definition, and merely uppercase by convention.
+
+        This option is provided to diagnose backwards-incompatible changes.
+
+        Use with care and only if necessary. May be removed in a future version.
+
+        .. versionadded:: 22.0.0
+        """
+
+
+class PermitUnconventionalHTTPVersion(Setting):
+    name = "permit_unconventional_http_version"
+    section = "Server Mechanics"
+    cli = ["--permit-unconventional-http-version"]
+    validator = validate_bool
+    action = "store_true"
+    default = False
+    desc = """\
+        Permit HTTP version not matching conventions of 2023
+
+        This disables the refusal of likely malformed request lines.
+        It is unusual to specify HTTP 1 versions other than 1.0 and 1.1.
+
+        This option is provided to diagnose backwards-incompatible changes.
+        Use with care and only if necessary. May be removed in a future version.
+
+        .. versionadded:: 22.0.0
+        """
+
+
+class CasefoldHTTPMethod(Setting):
+    name = "casefold_http_method"
+    section = "Server Mechanics"
+    cli = ["--casefold-http-method"]
+    validator = validate_bool
+    action = "store_true"
+    default = False
+    desc = """\
+         Transform received HTTP methods to uppercase
+
+         HTTP methods are case sensitive by definition, and merely uppercase by convention.
+
+         This option is provided because previous versions of gunicorn defaulted to this behaviour.
+
+         Use with care and only if necessary. May be removed in a future version.
+
+         .. versionadded:: 22.0.0
+         """
+
+
+def validate_header_map_behaviour(val):
+    # FIXME: refactor all of this subclassing stdlib argparse
+
+    if val is None:
+        return
+
+    if not isinstance(val, str):
+        raise TypeError("Invalid type for casting: %s" % val)
+    if val.lower().strip() == "drop":
+        return "drop"
+    elif val.lower().strip() == "refuse":
+        return "refuse"
+    elif val.lower().strip() == "dangerous":
+        return "dangerous"
+    else:
+        raise ValueError("Invalid header map behaviour: %s" % val)
+
+
+class HeaderMap(Setting):
+    name = "header_map"
+    section = "Server Mechanics"
+    cli = ["--header-map"]
+    validator = validate_header_map_behaviour
+    default = "drop"
+    desc = """\
+        Configure how header field names are mapped into environ
+
+        Headers containing underscores are permitted by RFC9110,
+        but gunicorn joining headers of different names into
+        the same environment variable will dangerously confuse applications as to which is which.
+
+        The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped.
+        The value ``refuse`` will return an error if a request contains *any* such header.
+        The value ``dangerous`` matches the previous, not advisabble, behaviour of mapping different
+        header field names into the same environ name.
+
+        Use with care and only if necessary and after considering if your problem could
+        instead be solved by specifically renaming or rewriting only the intended headers
+        on a proxy in front of Gunicorn.
+
+        .. versionadded:: 22.0.0
+        """
+
+
+class TolerateDangerousFraming(Setting):
+    name = "tolerate_dangerous_framing"
+    section = "Server Mechanics"
+    cli = ["--tolerate-dangerous-framing"]
+    validator = validate_bool
+    action = "store_true"
+    default = False
+    desc = """\
+        Process requests with both Transfer-Encoding and Content-Length
+
+        This is known to induce vulnerabilities, but not strictly forbidden by RFC9112.
+
+        Use with care and only if necessary. May be removed in a future version.
+
+        .. versionadded:: 22.0.0
+        """
Index: gunicorn-19.7.1/tests/requests/invalid/003b.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003b.http
@@ -0,0 +1,2 @@
+bla:rgh /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003b.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003b.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/003c.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003c.http
@@ -0,0 +1,2 @@
+-bl /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/003c.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/003c.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/valid/031compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat.http
@@ -0,0 +1,2 @@
+-blargh /foo HTTP/1.1\r\n
+\r\n
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/valid/031compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat.py
@@ -0,0 +1,13 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("permit_unconventional_http_method", True)
+cfg.set("casefold_http_method", True)
+
+request = {
+    "method": "-BLARGH",
+    "uri": uri("/foo"),
+    "version": (1, 1),
+    "headers": [],
+    "body": b""
+}
Index: gunicorn-19.7.1/tests/requests/valid/031compat2.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat2.http
@@ -0,0 +1,2 @@
+-blargh /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/031compat2.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/031compat2.py
@@ -0,0 +1,12 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("permit_unconventional_http_method", True)
+
+request = {
+    "method": "-blargh",
+    "uri": uri("/foo"),
+    "version": (1, 1),
+    "headers": [],
+    "body": b""
+}
Index: gunicorn-19.7.1/tests/requests/invalid/040.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/040.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/040.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/040.py
@@ -0,0 +1,7 @@
+from gunicorn.http.errors import InvalidHeaderName
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "refuse")
+
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_07.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_07.http
@@ -0,0 +1,10 @@
+POST /chunked_ambiguous_header_mapping HTTP/1.1\r\n
+Transfer_Encoding: gzip\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_07.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_07.py
@@ -0,0 +1,7 @@
+from gunicorn.http.errors import InvalidHeaderName
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "refuse")
+
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/valid/040.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/valid/040.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040.py
@@ -0,0 +1,9 @@
+request = {
+    "method": "GET",
+    "uri": uri("/keep/same/as?invalid/040"),
+    "version": (1, 0),
+    "headers": [
+        ("CONTENT-LENGTH", "7")
+    ],
+    "body": b'tricked'
+}
Index: gunicorn-19.7.1/tests/requests/valid/040_compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040_compat.http
@@ -0,0 +1,6 @@
+GET /keep/same/as?invalid/040 HTTP/1.0\r\n
+Transfer_Encoding: tricked\r\n
+Content-Length: 7\r\n
+Content_Length: -1E23\r\n
+\r\n
+tricked\r\n
Index: gunicorn-19.7.1/tests/requests/valid/040_compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/040_compat.py
@@ -0,0 +1,16 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("header_map", "dangerous")
+
+request = {
+    "method": "GET",
+    "uri": uri("/keep/same/as?invalid/040"),
+    "version": (1, 0),
+    "headers": [
+        ("TRANSFER_ENCODING", "tricked"),
+        ("CONTENT-LENGTH", "7"),
+        ("CONTENT_LENGTH", "-1E23"),
+    ],
+    "body": b'tricked'
+}
Index: gunicorn-19.7.1/gunicorn/workers/base.py
===================================================================
--- gunicorn-19.7.1.orig/gunicorn/workers/base.py
+++ gunicorn-19.7.1/gunicorn/workers/base.py
@@ -233,6 +233,8 @@ class Worker(object):
         else:
             if hasattr(req, "uri"):
                 self.log.exception("Error handling request %s", req.uri)
+            else:
+                self.log.exception("Error handling request (no URI read)")
             status_int = 500
             reason = "Internal Server Error"
             mesg = ""
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_01.http
@@ -0,0 +1,12 @@
+POST /chunked_w_underscore_chunk_size HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6_0\r\n
+ world\r\n
+0\r\n
+\r\n
+POST /after HTTP/1.1\r\n
+Transfer-Encoding: identity\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_02.http
@@ -0,0 +1,9 @@
+POST /chunked_with_prefixed_value HTTP/1.1\r\n
+Content-Length: 12\r\n
+Transfer-Encoding: \tchunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_03.http
@@ -0,0 +1,8 @@
+POST /double_chunked HTTP/1.1\r\n
+Transfer-Encoding: identity, chunked, identity, chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_03.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import UnsupportedTransferCoding
+request = UnsupportedTransferCoding
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_04.http
@@ -0,0 +1,11 @@
+POST /chunked_twice HTTP/1.1\r\n
+Transfer-Encoding: identity\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: identity\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_04.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_05.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_05.http
@@ -0,0 +1,11 @@
+POST /chunked_HTTP_1.0 HTTP/1.0\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+0\r\n
+Vary: *\r\n
+Content-Type: text/plain\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_05.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_05.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_06.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_06.http
@@ -0,0 +1,9 @@
+POST /chunked_not_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: gzip\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_06.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_06.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import UnsupportedTransferCoding
+request = UnsupportedTransferCoding
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_08.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_08.http
@@ -0,0 +1,9 @@
+POST /chunked_not_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Transfer-Encoding: identity\r\n
+\r\n
+5\r\n
+hello\r\n
+6\r\n
+ world\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_08.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_08.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHeader
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_01.http
@@ -0,0 +1,4 @@
+GETß /germans.. HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_01.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_02.http
@@ -0,0 +1,4 @@
+GETÿ /french.. HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_02.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_04.http
@@ -0,0 +1,5 @@
+GET /french.. HTTP/1.1\r\n
+Content-Lengthÿ: 3\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_04.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeaderName
+
+cfg = Config()
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_01.http
@@ -0,0 +1,2 @@
+GET\0PROXY /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_02.http
@@ -0,0 +1,2 @@
+GET\0 /foo HTTP/1.1\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidRequestMethod
+request = InvalidRequestMethod
\ No newline at end of file
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_03.http
@@ -0,0 +1,4 @@
+GET /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 0 1\r\n
+\r\n
+x
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_03.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_04.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_04.http
@@ -0,0 +1,5 @@
+GET /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 3 1\r\n
+\r\n
+xyz
+abc123
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_04.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_04.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeader
+
+cfg = Config()
+request = InvalidHeader
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_05.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_05.http
@@ -0,0 +1,4 @@
+GET: /stuff/here?foo=bar HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+xyz
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_05.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_05.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidRequestMethod
+
+cfg = Config()
+request = InvalidRequestMethod
Index: gunicorn-19.7.1/tests/requests/valid/025.http
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/025.http
+++ gunicorn-19.7.1/tests/requests/valid/025.http
@@ -1,10 +1,9 @@
 POST /chunked_cont_h_at_first HTTP/1.1\r\n
-Content-Length: -1\r\n
 Transfer-Encoding: chunked\r\n
 \r\n
 5; some; parameters=stuff\r\n
 hello\r\n
-6; blahblah; blah\r\n
+6 \t;\tblahblah; blah\r\n
  world\r\n
 0\r\n
 \r\n
@@ -16,4 +15,10 @@ Content-Length: -1\r\n
 hello\r\n
 6; blahblah; blah\r\n
  world\r\n
-0\r\n
\ No newline at end of file
+0\r\n
+\r\n
+PUT /ignored_after_dangerous_framing HTTP/1.1\r\n
+Content-Length: 3\r\n
+\r\n
+foo\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/valid/025.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/025.py
+++ gunicorn-19.7.1/tests/requests/valid/025.py
@@ -1,9 +1,13 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("tolerate_dangerous_framing", True)
+
 req1 = {
     "method": "POST",
     "uri": uri("/chunked_cont_h_at_first"),
     "version": (1, 1),
     "headers": [
-        ("CONTENT-LENGTH", "-1"),
         ("TRANSFER-ENCODING", "chunked")
     ],
     "body": b"hello world"
Index: gunicorn-19.7.1/tests/requests/valid/025compat.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/025compat.http
@@ -0,0 +1,18 @@
+POST /chunked_cont_h_at_first HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5; some; parameters=stuff\r\n
+hello\r\n
+6; blahblah; blah\r\n
+ world\r\n
+0\r\n
+\r\n
+PUT /chunked_cont_h_at_last HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+Content-Length: -1\r\n
+\r\n
+5; some; parameters=stuff\r\n
+hello\r\n
+6; blahblah; blah\r\n
+ world\r\n
+0\r\n
Index: gunicorn-19.7.1/tests/requests/valid/025compat.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/valid/025compat.py
@@ -0,0 +1,27 @@
+from gunicorn.config import Config
+
+cfg = Config()
+cfg.set("tolerate_dangerous_framing", True)
+
+req1 = {
+    "method": "POST",
+    "uri": uri("/chunked_cont_h_at_first"),
+    "version": (1, 1),
+    "headers": [
+        ("TRANSFER-ENCODING", "chunked")
+    ],
+    "body": b"hello world"
+}
+
+req2 = {
+    "method": "PUT",
+    "uri": uri("/chunked_cont_h_at_last"),
+    "version": (1, 1),
+    "headers": [
+        ("TRANSFER-ENCODING", "chunked"),
+        ("CONTENT-LENGTH", "-1"),
+    ],
+    "body": b"hello world"
+}
+
+request = [req1, req2]
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_03.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_03.http
@@ -0,0 +1,5 @@
+GET /germans.. HTTP/1.1\r\n
+Content-Lengthß: 3\r\n
+Content-Length: 3\r\n
+\r\n
+ÄÄÄ
Index: gunicorn-19.7.1/tests/requests/invalid/nonascii_03.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/nonascii_03.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHeaderName
+
+cfg = Config()
+request = InvalidHeaderName
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_06.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_06.http
@@ -0,0 +1,4 @@
+GET /the/future HTTP/1111111111111111111111111111111111111111111111111111111111111111111111111111111111111111.1\r\n
+Content-Length: 7\r\n
+\r\n
+Old Man
Index: gunicorn-19.7.1/tests/requests/invalid/prefix_06.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/prefix_06.py
@@ -0,0 +1,5 @@
+from gunicorn.config import Config
+from gunicorn.http.errors import InvalidHTTPVersion
+
+cfg = Config()
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/version_01.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_01.http
@@ -0,0 +1,2 @@
+GET /foo HTTP/0.99\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/version_01.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_01.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHTTPVersion
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/version_02.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_02.http
@@ -0,0 +1,2 @@
+GET /foo HTTP/2.0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/version_02.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/version_02.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidHTTPVersion
+request = InvalidHTTPVersion
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_09.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_09.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_without_ext HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+0 \r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_09.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_09.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_10.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_10.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_before HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\r\n
+hello\r\n
+ 0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_10.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_10.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_11.http
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_11.http
@@ -0,0 +1,7 @@
+POST /chunked_ows_before HTTP/1.1\r\n
+Transfer-Encoding: chunked\r\n
+\r\n
+5\n;\r\n
+hello\r\n
+0\r\n
+\r\n
Index: gunicorn-19.7.1/tests/requests/invalid/chunked_11.py
===================================================================
--- /dev/null
+++ gunicorn-19.7.1/tests/requests/invalid/chunked_11.py
@@ -0,0 +1,2 @@
+from gunicorn.http.errors import InvalidChunkSize
+request = InvalidChunkSize
Index: gunicorn-19.7.1/tests/requests/valid/016.py
===================================================================
--- gunicorn-19.7.1.orig/tests/requests/valid/016.py
+++ gunicorn-19.7.1/tests/requests/valid/016.py
@@ -1,35 +1,35 @@
-certificate = """-----BEGIN CERTIFICATE-----\r\n
-    MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx\r\n
-    ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT\r\n
-    AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu\r\n
-    dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV\r\n
-    SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV\r\n
-    BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB\r\n
-    BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF\r\n
-    W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR\r\n
-    gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL\r\n
-    0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP\r\n
-    u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR\r\n
-    wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG\r\n
-    1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs\r\n
-    BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD\r\n
-    VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj\r\n
-    loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj\r\n
-    aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG\r\n
-    9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE\r\n
-    IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO\r\n
-    BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1\r\n
-    cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg\r\n
-    EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC\r\n
-    5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv\r\n
-    Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3\r\n
-    XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8\r\n
-    UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk\r\n
-    hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK\r\n
-    wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu\r\n
-    Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3\r\n
-    RA==\r\n
-    -----END CERTIFICATE-----""".replace("\n\n", "\n")
+certificate = """-----BEGIN CERTIFICATE-----
+ MIIFbTCCBFWgAwIBAgICH4cwDQYJKoZIhvcNAQEFBQAwcDELMAkGA1UEBhMCVUsx
+ ETAPBgNVBAoTCGVTY2llbmNlMRIwEAYDVQQLEwlBdXRob3JpdHkxCzAJBgNVBAMT
+ AkNBMS0wKwYJKoZIhvcNAQkBFh5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMu
+ dWswHhcNMDYwNzI3MTQxMzI4WhcNMDcwNzI3MTQxMzI4WjBbMQswCQYDVQQGEwJV
+ SzERMA8GA1UEChMIZVNjaWVuY2UxEzARBgNVBAsTCk1hbmNoZXN0ZXIxCzAJBgNV
+ BAcTmrsogriqMWLAk1DMRcwFQYDVQQDEw5taWNoYWVsIHBhcmQYJKoZIhvcNAQEB
+ BQADggEPADCCAQoCggEBANPEQBgl1IaKdSS1TbhF3hEXSl72G9J+WC/1R64fAcEF
+ W51rEyFYiIeZGx/BVzwXbeBoNUK41OK65sxGuflMo5gLflbwJtHBRIEKAfVVp3YR
+ gW7cMA/s/XKgL1GEC7rQw8lIZT8RApukCGqOVHSi/F1SiFlPDxuDfmdiNzL31+sL
+ 0iwHDdNkGjy5pyBSB8Y79dsSJtCW/iaLB0/n8Sj7HgvvZJ7x0fr+RQjYOUUfrePP
+ u2MSpFyf+9BbC/aXgaZuiCvSR+8Snv3xApQY+fULK/xY8h8Ua51iXoQ5jrgu2SqR
+ wgA7BUi3G8LFzMBl8FRCDYGUDy7M6QaHXx1ZWIPWNKsCAwEAAaOCAiQwggIgMAwG
+ 1UdEwEB/wQCMAAwEQYJYIZIAYb4QgHTTPAQDAgWgMA4GA1UdDwEB/wQEAwID6DAs
+ BglghkgBhvhCAQ0EHxYdVUsgZS1TY2llbmNlIFVzZXIgQ2VydGlmaWNhdGUwHQYD
+ VR0OBBYEFDTt/sf9PeMaZDHkUIldrDYMNTBZMIGaBgNVHSMEgZIwgY+AFAI4qxGj
+ loCLDdMVKwiljjDastqooXSkcjBwMQswCQYDVQQGEwJVSzERMA8GA1UEChMIZVNj
+ aWVuY2UxEjAQBgNVBAsTCUF1dGhvcml0eTELMAkGA1UEAxMCQ0ExLTArBgkqhkiG
+ 9w0BCQEWHmNhLW9wZXJhdG9yQGdyaWQtc3VwcG9ydC5hYy51a4IBADApBgNVHRIE
+ IjAggR5jYS1vcGVyYXRvckBncmlkLXN1cHBvcnQuYWMudWswGQYDVR0gBBIwEDAO
+ BgwrBgEEAdkvAQEBAQYwPQYJYIZIAYb4QgEEBDAWLmh0dHA6Ly9jYS5ncmlkLXN1
+ cHBvcnQuYWMudmT4sopwqlBWsvcHViL2NybC9jYWNybC5jcmwwPQYJYIZIAYb4Qg
+ EDBDAWLmh0dHA6Ly9jYS5ncmlkLXN1cHBvcnQuYWMudWsvcHViL2NybC9jYWNybC
+ 5jcmwwPwYDVR0fBDgwNjA0oDKgMIYuaHR0cDovL2NhLmdyaWQt5hYy51ay9wdWIv
+ Y3JsL2NhY3JsLmNybDANBgkqhkiG9w0BAQUFAAOCAQEAS/U4iiooBENGW/Hwmmd3
+ XCy6Zrt08YjKCzGNjorT98g8uGsqYjSxv/hmi0qlnlHs+k/3Iobc3LjS5AMYr5L8
+ UO7OSkgFFlLHQyC9JzPfmLCAugvzEbyv4Olnsr8hbxF1MbKZoQxUZtMVu29wjfXk
+ hTeApBv7eaKCWpSp7MCbvgzm74izKhu3vlDk9w6qVrxePfGgpKPqfHiOoGhFnbTK
+ wTC6o2xq5y0qZ03JonF7OJspEd3I5zKY3E+ov7/ZhW6DqT8UFvsAdjvQbXyhV8Eu
+ Yhixw1aKEPzNjNowuIseVogKOLXxWI5vAi5HgXdS0/ES5gDGsABo4fqovUKlgop3
+ RA==
+ -----END CERTIFICATE-----""".replace("\n", "")
 
 request = {
     "method": "GET",
openSUSE Build Service is sponsored by