diff --git a/src/twisted/newsfragments/9757.misc b/src/twisted/newsfragments/9757.misc new file mode 100644 index 000000000..e69de29bb diff --git a/src/twisted/web/http.py b/src/twisted/web/http.py index d1b201978..f0fb05b4d 100644 --- a/src/twisted/web/http.py +++ b/src/twisted/web/http.py @@ -873,7 +873,8 @@ class Request: @type version: C{bytes} @param version: The HTTP version of this request. """ - self.content.seek(0,0) + clength = self.content.tell() + self.content.seek(0, 0) self.args = {} self.method, self.uri = command, path @@ -889,16 +890,16 @@ class Request: # Argument processing args = self.args ctype = self.requestHeaders.getRawHeaders(b'content-type') - clength = self.requestHeaders.getRawHeaders(b'content-length') if ctype is not None: ctype = ctype[0] - if clength is not None: - clength = clength[0] - if self.method == b"POST" and ctype and clength: mfd = b'multipart/form-data' key, pdict = _parseHeader(ctype) + # This weird CONTENT-LENGTH param is required by + # cgi.parse_multipart() in some versions of Python 3.7+, see + # bpo-29979. It looks like this will be relaxed and backported, see + # https://github.com/python/cpython/pull/8530. pdict["CONTENT-LENGTH"] = clength if key == b'application/x-www-form-urlencoded': args.update(parse_qs(self.content.read(), 1)) diff --git a/src/twisted/web/newsfragments/9678.bugfix b/src/twisted/web/newsfragments/9678.bugfix new file mode 100644 index 000000000..141578b7d --- /dev/null +++ b/src/twisted/web/newsfragments/9678.bugfix @@ -0,0 +1 @@ +twisted.web.http.Request now correctly parses multipart-encoded form data submitted as a chunked request on Python 3.7+. diff --git a/src/twisted/web/test/test_http.py b/src/twisted/web/test/test_http.py index 6c1b14897..0a0db09b7 100644 --- a/src/twisted/web/test/test_http.py +++ b/src/twisted/web/test/test_http.py @@ -1426,6 +1426,73 @@ class ChunkingTests(unittest.TestCase, ResponseTestMixin): b"Transfer-Encoding: chunked", b"5\r\nHello\r\n6\r\nWorld!\r\n")]) + def runChunkedRequest(self, httpRequest, requestFactory=None, + chunkSize=1): + """ + Execute a web request based on plain text content, chunking + the request payload. + + This is a stripped-down, chunking version of ParsingTests.runRequest. + """ + channel = http.HTTPChannel() + + if requestFactory: + channel.requestFactory = _makeRequestProxyFactory(requestFactory) + + httpRequest = httpRequest.replace(b"\n", b"\r\n") + header, body = httpRequest.split(b"\r\n\r\n", 1) + + transport = StringTransport() + + channel.makeConnection(transport) + channel.dataReceived(header+b"\r\n\r\n") + + for pos in range(len(body)//chunkSize+1): + if channel.transport.disconnecting: + break + channel.dataReceived(b"".join( + http.toChunk(body[pos*chunkSize:(pos+1)*chunkSize]))) + + channel.dataReceived(b"".join(http.toChunk(b""))) + channel.connectionLost(IOError("all done")) + + return channel + + def test_multipartFormData(self): + """ + Test that chunked uploads are actually processed into args. + + This is essentially a copy of ParsingTests.test_multipartFormData, + just with chunking put in. + + This fails as of twisted version 18.9.0 because of bug #9678. + """ + processed = [] + + class MyRequest(http.Request): + def process(self): + processed.append(self) + self.write(b"done") + self.finish() + req = b'''\ +POST / HTTP/1.0 +Content-Type: multipart/form-data; boundary=AaB03x +Transfer-Encoding: chunked + +--AaB03x +Content-Type: text/plain +Content-Disposition: form-data; name="text" +Content-Transfer-Encoding: quoted-printable + +abasdfg +--AaB03x-- +''' + channel = self.runChunkedRequest(req, MyRequest, chunkSize=5) + self.assertEqual(channel.transport.value(), + b"HTTP/1.0 200 OK\r\n\r\ndone") + self.assertEqual(len(processed), 1) + self.assertEqual(processed[0].args, {b"text": [b"abasdfg"]}) + class ParsingTests(unittest.TestCase): diff --git a/tox.ini b/tox.ini index 328ac0354..e5fdf779c 100644 --- a/tox.ini +++ b/tox.ini @@ -48,9 +48,9 @@ extras = deps = py27-alldeps-{posix,macos}: pysqlite - ; Coverage 5.0 does not work with codecov. - {withcov}: coverage<5.0 - {coverage-prepare,codecov-publish}: coverage<5.0 + {withcov}: coverage + + {coverage-prepare,codecov-publish}: coverage {codecov-push,codecov-publish}: codecov