CVE-2019-16785 (Python, published 2019-12-20T23:15Z)
Description: value: Waitress through version 1.3.1 implemented a "MAY" part of the RFC7230 which states: "Although the line terminator for the start-line and header fields is the sequence CRLF, a recipient MAY recognize a single LF as a line terminator and ignore any preceding CR." Unfortunately if a front-end server does not parse header fields with an LF the same way as it does those with a CRLF it can lead to the front-end and the back-end server parsing the same HTTP message in two different ways. This can lead to a potential for HTTP request smuggling/splitting whereby Waitress may see two requests while the front-end server only sees a single HTTP message. This issue is fixed in Waitress 1.4.0.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: CWE-444 \u2014 Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')
The product acts as an intermediary HTTP agent
         (such as a proxy or firewall) in the data flow between two
         entities such as a client and server, but it does not
         interpret malformed HTTP requests or responses in ways that
         are consistent with how the messages will be processed by
         those entities that are at the ultimate destination.
Repository: https://github.com/Pylons/waitress
Files changed:
waitress/parser.py
waitress/receiver.py
waitress/tests/test_channel.py
waitress/tests/test_functional.py
waitress/tests/test_parser.py
waitress/tests/test_receiver.py
waitress/tests/test_utilities.py
waitress/utilities.py
Commit message: Remove support for non CRLF line endings

https://tools.ietf.org/html/rfc7230#section-3.5 says that servers MAY
implement their parsers to use only the LF as a delimeter between lines,
however if the frontend server does NOT do the same you can potentially
allow a single HTTP request to be treated differently by the two
servers.

This issue can be used to cause HTTP request smuggling or HTTP desync
which may lead to vulnerabilities.

To increase robustness Waitress will no longer allow bare LF for HTTP
messages/headers and chunked encoding and instead now enforces that the
line endings at CRLF.


Vulnerable code:
from waitress.compat import (
    tostr,
    urlparse,
    unquote_bytes_to_wsgi,
)


from waitress.receiver import (
    FixedStreamReceiver,
    ChunkedReceiver,
)

    find_double_newline,
    BadRequest,
                # Remove preceeding blank lines.
        index = header_plus.find(b"\n")
            header = header_plus[index + 1 :]
            first_line = header_plus.rstrip()
            header = b""
    lines = header.split(b"\n")
from waitress.utilities import find_double_newline

from waitress.utilities import BadRequest
                pos = s.find(b"\n")
                    s = s[pos + 1 :]
                elif trailer.startswith(b"\n"):
                    # No trailer.
                    self.completed = True
                    return orig_size - (len(trailer) - 1)
        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n" + b"a" * 50000)
    def test_received_preq_completed_n_lt_data(self):
        inst, sock, map = self._makeOneWithMap()
        inst.server = DummyServer()
        preq = DummyParser()
        inst.request = preq
        preq.completed = True
        preq.empty = False
        line = b"GET / HTTP/1.1\n\n"
        preq.retval = len(line)
        inst.received(line + line)
        self.assertEqual(inst.request, None)
        self.assertEqual(len(inst.requests), 2)
        self.assertEqual(len(inst.server.tasks), 1)

        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n")
        inst.received(b"GET / HTTP/1.1\n\n")
        to_send = "GET / HTTP/1.0\n" "Content-Length: 0\n\n"
        to_send = "GET / HTTP/1.0\n" " Host: 0\n\n"
        to_send = "GET / HTTP/1.0\n" "Content-Length: 5\n\n"
        to_send = "GET / HTTP/1.0\n" "Content-Length: 0\n\n"
        data = "Echo\nthis\r\nplease"
            "GET / HTTP/1.0\n"
            "Connection: close\n"
            "Content-Length: %d\n"
            "\n"
            "GET / HTTP/1.0\n" "Content-Length: %d\n" "\n" "%s" % (len(body), body)
        header = tobytes("GET / HTTP/1.1\n" "Transfer-Encoding: chunked\n\n")
        header = tobytes("GET / HTTP/1.1\n" "Transfer-Encoding: chunked\n\n")
        to_send = "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n"
        to_send += control_line + s
        to_send += "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n"
            "GET / HTTP/1.0\n" "Content-Length: %d\n" "\n" "%s" % (len(data), data)
            "GET / HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: %d\n"
            "\n"
            "GET / HTTP/1.1\n" "Content-Length: %d\n" "\n" "%s" % (len(data), data)
            "GET / HTTP/1.1\n"
            "Connection: keep-alive\n"
            "Content-Length: %d\n"
            "\n"
            "GET / HTTP/1.1\n"
            "Connection: close\n"
            "Content-Length: %d\n"
            "\n"
            "GET / HTTP/1.0\n"
            "Content-Length: 0\n"
            "Host: www.google.com:8080\n"
            "X-Forwarded-For: 192.168.1.1\n"
            "X-Forwarded-Proto: https\n"
            "X-Forwarded-Port: 5000\n\n"
            "GET / HTTP/1.1\n"
            "Connection: close\n"
            "Content-Length: %d\n"
            "Expect: 100-continue\n"
            "\n"
            "GET /short_body HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
            "GET /long_body HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
            "GET / HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: %d\n\n" % len(body)
            "GET /list HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: %d\n\n" % len(body)
            "GET /list_lentwo HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: %d\n\n" % len(body)
        to_send = "GET / HTTP/1.1\n" "Content-Length: %s\n\n" % len(body)
        to_send = "GET /list HTTP/1.1\n" "Content-Length: %d\n\n" % len(body)
        to_send = "GET /list_lentwo HTTP/1.1\n" "Content-Length: %s\n\n" % len(body)
            "GET /short_body HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
            "GET /long_body HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
            "GET /equal_body HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
            "GET /no_content_length HTTP/1.0\n"
            "Connection: Keep-Alive\n"
            "Content-Length: 0\n"
            "\n"
        to_send = "GET / HTTP/1.0\n" "Content-Length: 5\n\n"
        to_send = "GET / HTTP/1.0\n" "Content-Length: 5\n" "Connection: Keep-Alive\n\n"
        to_send = "GET / HTTP/1.0\n\n"
        to_send = "GET / HTTP/1.0\nConnection: Keep-Alive\n\n"
        to_send = "GET / HTTP/1.1\n" "Content-Length: 5\n\n"
        to_send = "GET / HTTP/1.1\nContent-Length: 5\nConnection: close\n\n"
        to_send = "GET / HTTP/1.1\n\n"
        to_send = "GET / HTTP/1.1\nConnection: close\n\n"
        to_send = "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n"
        to_send = "GET /before_start_response HTTP/1.0\n\n"
        to_send = "GET /before_start_response HTTP/1.1\n\n"
            "GET /before_start_response HTTP/1.1\n" "Connection: close\n\n"
        to_send = "GET /after_start_response HTTP/1.0\n\n"
        to_send = "GET /after_start_response HTTP/1.1\n\n"
            "GET /after_start_response HTTP/1.1\n" "Connection: close\n\n"
        to_send = "GET /after_write_cb HTTP/1.1\n\n"
        to_send = "GET /in_generator HTTP/1.1\n\n"
        to_send = "GET /filelike HTTP/1.1\n\n"
        to_send = "GET /filelike_nocl HTTP/1.1\n\n"
        to_send = "GET /filelike_shortcl HTTP/1.1\n\n"
        to_send = "GET /filelike_longcl HTTP/1.1\n\n"
        to_send = "GET /notfilelike HTTP/1.1\n\n"
        to_send = "GET /notfilelike_iobase HTTP/1.1\n\n"
        to_send = "GET /notfilelike_nocl HTTP/1.1\n\n"
        to_send = "GET /notfilelike_shortcl HTTP/1.1\n\n"
        to_send = "GET /notfilelike_longcl HTTP/1.1\n\n"
        to_send = "GET /filelike HTTP/1.0\n\n"
        to_send = "GET /filelike_nocl HTTP/1.0\n\n"
        to_send = "GET /notfilelike HTTP/1.0\n\n"
        to_send = "GET /notfilelike_nocl HTTP/1.0\n\n"
        if line in (b"\r\n", b"\n", b""):
from waitress.compat import (
    text_,
    tobytes,
)
    def test_received_nonsense_with_double_cr(self):
        data = b"""\
HTTP/1.0 GET /foobar


"""
        self.assertEqual(result, 22)
        data = b"""\
HTTP/1.0 GET /foobar
 Host: foo


"""
        self.assertEqual(result, 33)
        data = b"""\


"""
        self.assertEqual(result, 2)
        data = b"""\
GET /foobar HTTP/8.4
"""
        self.assertEqual(result, 21)
        data = b"""\
GET /foobar HTTP/8.4
Content-Length: 10

"""
        self.assertEqual(result, 41)
        data = b"""\
GET /foobar HTTP/8.4
X-Foo: 1
"""
        self.assertEqual(result, 30)
        data = b"""\
GET /foobar HTTP/1.1
Transfer-Encoding: chunked
X-Foo: 1

20;\r\n
This string has 32 characters\r\n
0\r\n\r\n"""
        self.assertEqual(result, 58)
        data = b"""\
GET /foobar HTTP/1.1
Transfer-Encoding: chunked
X-Foo: 1

garbage
"""
        self.assertEqual(result, 8)
        data = b"""\
GET /foobar HTTP/1.1
Transfer-Encoding: chunked
X-Foo: 1

20;\r\n
This string has 32 characters\r\n
0\r\n\r\n"""
        self.assertEqual(result, 58)
        self.assertEqual(self.parser.headers["CONTENT_LENGTH"], "32")
        data = b"""\
GET /foobar HTTP/8.4
foo: bar"""
        self.parser.parse_header(data)
        self.assertEqual(self.parser.first_line, data)
        data = b"GET /foobar HTTP/8.4\ncontent-length: abc"
        data = b"GET /foobar HTTP/1.1\ntransfer-encoding: ChUnKed"
        data = b"GET /foobar HTTP/1.1\nexpect: 100-continue"
        data = b"GET /foobar HTTP/1.1\nConnection: close\n\n"
        result = self._callFUT(b"slam\nslim")
        result = self._callFUT(b"slim\n slam")
        result = self._callFUT(b"slam\n\tslim")
        data = b"""\
GET /foobar HTTP/8.4
FirstName: mickey
lastname: Mouse
content-length: 7

Hello.
"""
            {"FIRSTNAME": "mickey", "LASTNAME": "Mouse", "CONTENT_LENGTH": "7",},
        self.assertEqual(parser.get_body_stream().getvalue(), b"Hello.\n")
        data = b"""\
GET /foo/a+%2B%2F%C3%A4%3D%26a%3Aint?d=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6 HTTP/8.4
FirstName: mickey
lastname: Mouse
content-length: 10

Hello mickey.
"""
            {"FIRSTNAME": "mickey", "LASTNAME": "Mouse", "CONTENT_LENGTH": "10",},
        data = b"""\
GET https://example.com:8080/foobar HTTP/8.4
content-length: 7

Hello.
"""
        self.assertEqual(parser.headers, {"CONTENT_LENGTH": "7",})
        self.assertEqual(parser.get_body_stream().getvalue(), b"Hello.\n")
        data = b"""\
GET /foobar HTTP/8.4
x-forwarded-for: 10.11.12.13
x-forwarded-for: unknown,127.0.0.1
X-Forwarded_for: 255.255.255.255
content-length: 7

Hello.
"""
                "CONTENT_LENGTH": "7",
        data = b"""\
GET /foobar HTTP/8.4
x-auth_user: bob
content-length: 7

Hello.
"""
        self.assertEqual(self.parser.headers, {"CONTENT_LENGTH": "7",})
        result = inst.received(b"garbage\n")
        self.assertEqual(result, 8)
        result = inst.received(b"a;discard\n")
        self.assertEqual(result, 10)
        result = inst.received(b"0;discard\n")
        self.assertEqual(result, 10)
        self.assertEqual(inst.completed, True)
        self.assertEqual(self._callFUT(b"\n\n"), 2)
        self.assertEqual(self._callFUT(b"\n\n00\r\n\r\n"), 2)
    pos1 = s.find(b"\n\r\n")  # One kind of double newline
    if pos1 >= 0:
        pos1 += 3
    pos2 = s.find(b"\n\n")  # Another kind of double newline
    if pos2 >= 0:
        pos2 += 2

    if pos1 >= 0:
        if pos2 >= 0:
            return min(pos1, pos2)
        else:
            return pos1
    else:
        return pos2

Fixed code:
from waitress.compat import tostr, unquote_bytes_to_wsgi, urlparse
from waitress.receiver import ChunkedReceiver, FixedStreamReceiver
    BadRequest,
    find_double_newline,

                # Remove preceeding blank lines. This is suggested by
                # https://tools.ietf.org/html/rfc7230#section-3.5 to support
                # clients sending an extra CR LF after another request when
                # using HTTP pipelining

        index = header_plus.find(b"\r\n")
            header = header_plus[index + 2 :]
            raise ParsingError("HTTP message header invalid")

        if b"\r" in first_line or b"\n" in first_line:
            raise ParsingError("Bare CR or LF found in HTTP message")
    lines = header.split(b"\r\n")
        if b"\r" in line or b"\n" in line:
            raise ParsingError('Bare CR or LF found in header line "%s"' % tostr(line))

from waitress.utilities import BadRequest, find_double_newline





    validate_chunk_end = False





                if self.chunk_remainder == 0:
                    self.validate_chunk_end = True
            elif self.validate_chunk_end:
                pos = s.find(b"\r\n")

                if pos == 0:
                    # Chop off the terminating CR LF from the chunk
                    s = s[2:]
                else:
                    self.error = BadRequest("Chunk not properly terminated")
                    self.all_chunks_received = True

                # Always exit this loop
                self.validate_chunk_end = False
                pos = s.find(b"\r\n")

                    s = s[pos + 2 :]








        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n" + b"a" * 50000)
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        inst.received(b"GET / HTTP/1.1\r\n\r\n")
        to_send = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\n Host: 0\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\nContent-Length: 0\r\n\r\n"
        data = "Echo\r\nthis\r\nplease"
            "GET / HTTP/1.0\r\n"
            "Connection: close\r\n"
            "Content-Length: %d\r\n"
            "\r\n"
            "GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(body), body)
        header = tobytes("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")
        header = tobytes("GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n")
            self.sock.send(b"\r\n")  # End the chunk
        to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
        to_send += control_line + s + "\r\n"
        to_send += "garbage\r\n"
        to_send = tobytes(to_send)
        self.connect()
        self.sock.send(to_send)
        fp = self.sock.makefile("rb", 0)
        line, headers, response_body = read_http(fp)
        # receiver caught garbage and turned it into a 400
        self.assertline(line, "400", "Bad Request", "HTTP/1.1")
        cl = int(headers["content-length"])
        self.assertEqual(cl, len(response_body))
        self.assertEqual(
            sorted(headers.keys()), ["connection", "content-length", "content-type", "date", "server"]
        )
        self.assertEqual(headers["content-type"], "text/plain")
        # connection has been closed
        self.send_check_error(to_send)
        self.assertRaises(ConnectionClosed, read_http, fp)

    def test_broken_chunked_encoding_missing_chunk_end(self):
        control_line = "20;\r\n"  # 20 hex = 32 dec
        s = "This string has 32 characters.\r\n"
        to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
        # garbage in input
        to_send += "garbage"
        self.assertTrue(b"Chunk not properly terminated" in response_body)
            "GET / HTTP/1.0\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data)
            "GET / HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: %d\r\n"
            "\r\n"
            "GET / HTTP/1.1\r\nContent-Length: %d\r\n\r\n%s" % (len(data), data)
            "GET / HTTP/1.1\r\n"
            "Connection: keep-alive\r\n"
            "Content-Length: %d\r\n"
            "\r\n"
            "GET / HTTP/1.1\r\n"
            "Connection: close\r\n"
            "Content-Length: %d\r\n"
            "\r\n"
            "GET / HTTP/1.0\r\n"
            "Content-Length: 0\r\n"
            "Host: www.google.com:8080\r\n"
            "X-Forwarded-For: 192.168.1.1\r\n"
            "X-Forwarded-Proto: https\r\n"
            "X-Forwarded-Port: 5000\r\n\r\n"
            "GET / HTTP/1.1\r\n"
            "Connection: close\r\n"
            "Content-Length: %d\r\n"
            "Expect: 100-continue\r\n"
            "\r\n"
            "GET /short_body HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
            "GET /long_body HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
            "GET / HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: %d\r\n\r\n" % len(body)
            "GET /list HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: %d\r\n\r\n" % len(body)
            "GET /list_lentwo HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: %d\r\n\r\n" % len(body)
        to_send = "GET / HTTP/1.1\r\nContent-Length: %s\r\n\r\n" % len(body)
        to_send = "GET /list HTTP/1.1\r\nContent-Length: %d\r\n\r\n" % len(body)
        to_send = "GET /list_lentwo HTTP/1.1\r\nContent-Length: %s\r\n\r\n" % len(body)
            "GET /short_body HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
            "GET /long_body HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
            "GET /equal_body HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
            "GET /no_content_length HTTP/1.0\r\n"
            "Connection: Keep-Alive\r\n"
            "Content-Length: 0\r\n"
            "\r\n"
        to_send = "GET / HTTP/1.0\r\nContent-Length: 5\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\nContent-Length: 5\r\nConnection: Keep-Alive\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\n\r\n"
        to_send = "GET / HTTP/1.0\r\nConnection: Keep-Alive\r\n\r\n"
        to_send = "GET / HTTP/1.1\r\nContent-Length: 5\r\n\r\n"
        to_send = "GET / HTTP/1.1\r\nContent-Length: 5\r\nConnection: close\r\n\r\n"
        to_send = "GET / HTTP/1.1\r\n\r\n"
        to_send = "GET / HTTP/1.1\r\nConnection: close\r\n\r\n"
        to_send = "GET / HTTP/1.1\r\nTransfer-Encoding: chunked\r\n\r\n"
        to_send = "GET /before_start_response HTTP/1.0\r\n\r\n"
        to_send = "GET /before_start_response HTTP/1.1\r\n\r\n"
            "GET /before_start_response HTTP/1.1\r\nConnection: close\r\n\r\n"
        to_send = "GET /after_start_response HTTP/1.0\r\n\r\n"
        to_send = "GET /after_start_response HTTP/1.1\r\n\r\n"
            "GET /after_start_response HTTP/1.1\r\nConnection: close\r\n\r\n"
        to_send = "GET /after_write_cb HTTP/1.1\r\n\r\n"
        to_send = "GET /in_generator HTTP/1.1\r\n\r\n"
        to_send = "GET /filelike HTTP/1.1\r\n\r\n"
        to_send = "GET /filelike_nocl HTTP/1.1\r\n\r\n"
        to_send = "GET /filelike_shortcl HTTP/1.1\r\n\r\n"
        to_send = "GET /filelike_longcl HTTP/1.1\r\n\r\n"
        to_send = "GET /notfilelike HTTP/1.1\r\n\r\n"
        to_send = "GET /notfilelike_iobase HTTP/1.1\r\n\r\n"
        to_send = "GET /notfilelike_nocl HTTP/1.1\r\n\r\n"
        to_send = "GET /notfilelike_shortcl HTTP/1.1\r\n\r\n"
        to_send = "GET /notfilelike_longcl HTTP/1.1\r\n\r\n"
        to_send = "GET /filelike HTTP/1.0\r\n\r\n"
        to_send = "GET /filelike_nocl HTTP/1.0\r\n\r\n"
        to_send = "GET /notfilelike HTTP/1.0\r\n\r\n"
        to_send = "GET /notfilelike_nocl HTTP/1.0\r\n\r\n"
        if line in (b"\r\n", b"\r\n", b""):
from waitress.compat import text_, tobytes
    def test_received_get_no_headers(self):
        data = b"HTTP/1.0 GET /foobar\r\n\r\n"
        self.assertEqual(result, 24)
        data = b"HTTP/1.0 GET /foobar\r\n Host: foo\r\n\r\n"
        self.assertEqual(result, 36)
        data = b"\r\n\r\n"
        self.assertEqual(result, 4)
        data = b"GET /foobar HTTP/8.4\r\n"
        self.assertEqual(result, 22)
        data = b"GET /foobar HTTP/8.4\r\nContent-Length: 10\r\n\r\n"
        self.assertEqual(result, 44)
        data = b"GET /foobar HTTP/8.4\r\nX-Foo: 1\r\n\r\n"
        self.assertEqual(result, 34)
        data = (
            b"GET /foobar HTTP/1.1\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"X-Foo: 1\r\n"
            b"\r\n"
            b"1d;\r\n"
            b"This string has 29 characters\r\n"
            b"0\r\n\r\n"
        )

        self.assertEqual(result, 62)
        data = (
            b"GET /foobar HTTP/1.1\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"X-Foo: 1\r\n"
            b"\r\n"
            b"garbage\r\n"
        )
        self.assertEqual(result, 9)
        data = (
            b"GET /foobar HTTP/1.1\r\n"
            b"Transfer-Encoding: chunked\r\n"
            b"X-Foo: 1\r\n"
            b"\r\n"
            b"1d;\r\n"
            b"This string has 29 characters\r\n"
            b"0\r\n\r\n"
        )
        self.assertEqual(result, 62)
        self.assertEqual(self.parser.headers["CONTENT_LENGTH"], "29")
        data = b"GET /foobar HTTP/8.4\r\nfoo: bar\r\n"
        from waitress.parser import ParsingError


        try:
            self.parser.parse_header(data)
        except ParsingError:
            pass
        else:  # pragma: nocover
            self.assertTrue(False)
        data = b"GET /foobar HTTP/8.4\r\ncontent-length: abc\r\n"
        data = b"GET /foobar HTTP/1.1\r\ntransfer-encoding: ChUnKed\r\n"
        data = b"GET /foobar HTTP/1.1\r\nexpect: 100-continue\r\n"
        data = b"GET /foobar HTTP/1.1\r\nConnection: close\r\n"
    def test_parse_header_lf_only(self):
        from waitress.parser import ParsingError

        data = b"GET /foobar HTTP/8.4\nfoo: bar"

        try:
            self.parser.parse_header(data)
        except ParsingError:
            pass
        else:  # pragma: nocover
            self.assertTrue(False)

    def test_parse_header_cr_only(self):
        from waitress.parser import ParsingError

        data = b"GET /foobar HTTP/8.4\rfoo: bar"
        try:
            self.parser.parse_header(data)
        except ParsingError:
            pass
        else:  # pragma: nocover
            self.assertTrue(False)

    def test_parse_header_extra_lf_in_header(self):
        from waitress.parser import ParsingError

        data = b"GET /foobar HTTP/8.4\r\nfoo: \nbar\r\n"
        try:
            self.parser.parse_header(data)
        except ParsingError as e:
            self.assertIn("Bare CR or LF found in header line", e.args[0])
        else:  # pragma: nocover
            self.assertTrue(False)

    def test_parse_header_extra_lf_in_first_line(self):
        from waitress.parser import ParsingError

        data = b"GET /foobar\n HTTP/8.4\r\n"
        try:
            self.parser.parse_header(data)
        except ParsingError as e:
            self.assertIn("Bare CR or LF found in HTTP message", e.args[0])
        else:  # pragma: nocover
            self.assertTrue(False)
        result = self._callFUT(b"slam\r\nslim")
        result = self._callFUT(b"slim\r\n slam")
        result = self._callFUT(b"slam\r\n\tslim")


        data = (
            b"GET /foobar HTTP/8.4\r\n"
            b"FirstName: mickey\r\n"
            b"lastname: Mouse\r\n"
            b"content-length: 6\r\n"
            b"\r\n"
            b"Hello."
        )
            {"FIRSTNAME": "mickey", "LASTNAME": "Mouse", "CONTENT_LENGTH": "6",},
        self.assertEqual(parser.get_body_stream().getvalue(), b"Hello.")
        data = (
            b"GET /foo/a+%2B%2F%C3%A4%3D%26a%3Aint?d=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6 HTTP/8.4\r\n"
            b"FirstName: mickey\r\n"
            b"lastname: Mouse\r\n"
            b"content-length: 10\r\n"
            b"\r\n"
            b"Hello mickey."
        )
            {"FIRSTNAME": "mickey", "LASTNAME": "Mouse", "CONTENT_LENGTH": "10"},
        data = (
            b"GET https://example.com:8080/foobar HTTP/8.4\r\n"
            b"content-length: 6\r\n"
            b"\r\n"
            b"Hello."
        )
        self.assertEqual(parser.headers, {"CONTENT_LENGTH": "6"})
        self.assertEqual(parser.get_body_stream().getvalue(), b"Hello.")
        data = (
            b"GET /foobar HTTP/8.4\r\n"
            b"x-forwarded-for: 10.11.12.13\r\n"
            b"x-forwarded-for: unknown,127.0.0.1\r\n"
            b"X-Forwarded_for: 255.255.255.255\r\n"
            b"content-length: 6\r\n"
            b"\r\n"
            b"Hello."
        )
                "CONTENT_LENGTH": "6",
        data = (
            b"GET /foobar HTTP/8.4\r\n"
            b"x-auth_user: bob\r\n"
            b"content-length: 6\r\n"
            b"\r\n"
            b"Hello."
        )
        self.assertEqual(self.parser.headers, {"CONTENT_LENGTH": "6",})
        result = inst.received(b"garbage\r\n")
        self.assertEqual(result, 9)
        result = inst.received(b"a;discard\r\n")
        self.assertEqual(result, 11)
        result = inst.received(b"0;discard\r\n")
        self.assertEqual(result, 11)
        self.assertEqual(inst.completed, False)
    def test_received_chunk_is_properly_terminated(self):
        buf = DummyBuffer()
        inst = self._makeOne(buf)
        data = b"4\r\nWiki\r\n"
        result = inst.received(data)
        self.assertEqual(result, len(data))
        self.assertEqual(inst.completed, False)
        self.assertEqual(buf.data[0], b"Wiki")

    def test_received_chunk_not_properly_terminated(self):
        from waitress.utilities import BadRequest

        buf = DummyBuffer()
        inst = self._makeOne(buf)
        data = b"4\r\nWikibadchunk\r\n"
        result = inst.received(data)
        self.assertEqual(result, len(data))
        self.assertEqual(inst.completed, False)
        self.assertEqual(buf.data[0], b"Wiki")
        self.assertEqual(inst.error.__class__, BadRequest)

    def test_received_multiple_chunks(self):
        from waitress.utilities import BadRequest

        buf = DummyBuffer()
        inst = self._makeOne(buf)
        data = (
            b"4\r\n"
            b"Wiki\r\n"
            b"5\r\n"
            b"pedia\r\n"
            b"E\r\n"
            b" in\r\n"
            b"\r\n"
            b"chunks.\r\n"
            b"0\r\n"
            b"\r\n"
        )
        result = inst.received(data)
        self.assertEqual(result, len(data))
        self.assertEqual(inst.completed, True)
        self.assertEqual(b"".join(buf.data), b"Wikipedia in\r\n\r\nchunks.")
        self.assertEqual(inst.error, None)

        self.assertEqual(self._callFUT(b"\n\n"), -1)
        self.assertEqual(self._callFUT(b"\n\n00\r\n\r\n"), 8)
    pos = s.find(b"\r\n\r\n")

    if pos >= 0:
        pos += 4

    return pos