Skip to content

Commit

Permalink
#11997 Fix for Twisted web client support of trailer Server-Timing (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
adiroiban committed Jan 29, 2024
2 parents f35f891 + a758438 commit dd96b6f
Show file tree
Hide file tree
Showing 3 changed files with 114 additions and 29 deletions.
1 change: 1 addition & 0 deletions src/twisted/newsfragments/11997.bugfix
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
twisted.web.http.HTTPChannel now ignores the trailer headers provided in the last chunk of a chunked encoded response, rather than raising an exception.
54 changes: 39 additions & 15 deletions src/twisted/web/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1805,7 +1805,6 @@ def noMoreData(self):

maxChunkSizeLineLength = 1024


_chunkExtChars = (
b"\t !\"#$%&'()*+,-./0123456789:;<=>?@"
b"ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^_`"
Expand Down Expand Up @@ -1889,12 +1888,20 @@ class _ChunkedTransferDecoder:
state transition this is truncated at the front so that index 0 is
where the next state shall begin.
@ivar _start: While in the C{'CHUNK_LENGTH'} state, tracks the index into
the buffer at which search for CRLF should resume. Resuming the search
at this position avoids doing quadratic work if the chunk length line
arrives over many calls to C{dataReceived}.
@ivar _start: While in the C{'CHUNK_LENGTH'} and C{'TRAILER'} states,
tracks the index into the buffer at which search for CRLF should resume.
Resuming the search at this position avoids doing quadratic work if the
chunk length line arrives over many calls to C{dataReceived}.
@ivar _trailerHeaders: Accumulates raw/unparsed trailer headers.
See https://github.com/twisted/twisted/issues/12014
@ivar _maxTrailerHeadersSize: Maximum bytes for trailer header from the
response.
@type _maxTrailerHeadersSize: C{int}
Not used in any other state.
@ivar _receivedTrailerHeadersSize: Bytes received so far for the tailer headers.
@type _receivedTrailerHeadersSize: C{int}
"""

state = "CHUNK_LENGTH"
Expand All @@ -1908,6 +1915,9 @@ def __init__(
self.finishCallback = finishCallback
self._buffer = bytearray()
self._start = 0
self._trailerHeaders: List[bytearray] = []
self._maxTrailerHeadersSize = 2**16
self._receivedTrailerHeadersSize = 0

def _dataReceived_CHUNK_LENGTH(self) -> bool:
"""
Expand Down Expand Up @@ -1984,23 +1994,37 @@ def _dataReceived_CRLF(self) -> bool:

def _dataReceived_TRAILER(self) -> bool:
"""
Await the carriage return and line feed characters that follow the
terminal zero-length chunk. Then invoke C{finishCallback} and switch to
state C{'FINISHED'}.
Collect trailer headers if received and finish at the terminal zero-length
chunk. Then invoke C{finishCallback} and switch to state C{'FINISHED'}.
@returns: C{False}, as there is either insufficient data to continue,
or no data remains.
@raises _MalformedChunkedDataError: when anything other than CRLF is
received.
"""
if len(self._buffer) < 2:
if (
self._receivedTrailerHeadersSize + len(self._buffer)
> self._maxTrailerHeadersSize
):
raise _MalformedChunkedDataError("Trailer headers data is too long.")

eolIndex = self._buffer.find(b"\r\n", self._start)

if eolIndex == -1:
# Still no end of network line marker found.
# Continue processing more data.
return False

if not self._buffer.startswith(b"\r\n"):
raise _MalformedChunkedDataError("Chunk did not end with CRLF")
if eolIndex > 0:
# A trailer header was detected.
self._trailerHeaders.append(self._buffer[0:eolIndex])
del self._buffer[0 : eolIndex + 2]
self._start = 0
self._receivedTrailerHeadersSize += eolIndex + 2
return True

# eolIndex in this part of code is equal to 0

data = memoryview(self._buffer)[2:].tobytes()

del self._buffer[:]
self.state = "FINISHED"
self.finishCallback(data)
Expand Down
88 changes: 74 additions & 14 deletions src/twisted/web/test/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,7 @@ def test_short(self):
p.dataReceived(s)
self.assertEqual(L, [b"a", b"b", b"c", b"1", b"2", b"3", b"4", b"5"])
self.assertEqual(finished, [b""])
self.assertEqual(p._trailerHeaders, [])

def test_long(self):
"""
Expand Down Expand Up @@ -1381,20 +1382,6 @@ def test_malformedChunkEnd(self):
http._MalformedChunkedDataError, p.dataReceived, b"3\r\nabc!!!!"
)

def test_malformedChunkEndFinal(self):
r"""
L{_ChunkedTransferDecoder.dataReceived} raises
L{_MalformedChunkedDataError} when the terminal zero-length chunk is
followed by characters other than C{\r\n}.
"""
p = http._ChunkedTransferDecoder(
lambda b: None,
lambda b: None, # pragma: nocov
)
self.assertRaises(
http._MalformedChunkedDataError, p.dataReceived, b"3\r\nabc\r\n0\r\n!!"
)

def test_finish(self):
"""
L{_ChunkedTransferDecoder.dataReceived} interprets a zero-length
Expand Down Expand Up @@ -1469,6 +1456,79 @@ def finished(extra):
self.assertEqual(errors, [])
self.assertEqual(successes, [True])

def test_trailerHeaders(self):
"""
L{_ChunkedTransferDecoder.dataReceived} decodes chunked-encoded data
and ignores trailer headers which come after the terminating zero-length
chunk.
"""
L = []
finished = []
p = http._ChunkedTransferDecoder(L.append, finished.append)
p.dataReceived(b"3\r\nabc\r\n5\r\n12345\r\n")
p.dataReceived(
b"a\r\n0123456789\r\n0\r\nServer-Timing: total;dur=123.4\r\nExpires: Wed, 21 Oct 2015 07:28:00 GMT\r\n\r\n"
)
self.assertEqual(L, [b"abc", b"12345", b"0123456789"])
self.assertEqual(finished, [b""])
self.assertEqual(
p._trailerHeaders,
[
b"Server-Timing: total;dur=123.4",
b"Expires: Wed, 21 Oct 2015 07:28:00 GMT",
],
)

def test_shortTrailerHeader(self):
"""
L{_ChunkedTransferDecoder.dataReceived} decodes chunks of input with
tailer header broken up and delivered in multiple calls.
"""
L = []
finished = []
p = http._ChunkedTransferDecoder(L.append, finished.append)
for s in iterbytes(
b"3\r\nabc\r\n5\r\n12345\r\n0\r\nServer-Timing: total;dur=123.4\r\n\r\n"
):
p.dataReceived(s)
self.assertEqual(L, [b"a", b"b", b"c", b"1", b"2", b"3", b"4", b"5"])
self.assertEqual(finished, [b""])
self.assertEqual(p._trailerHeaders, [b"Server-Timing: total;dur=123.4"])

def test_tooLongTrailerHeader(self):
r"""
L{_ChunkedTransferDecoder.dataReceived} raises
L{_MalformedChunkedDataError} when the trailing headers data is too long.
"""
p = http._ChunkedTransferDecoder(
lambda b: None,
lambda b: None, # pragma: nocov
)
p._maxTrailerHeadersSize = 10
self.assertRaises(
http._MalformedChunkedDataError,
p.dataReceived,
b"3\r\nabc\r\n0\r\nTotal-Trailer: header;greater-then=10\r\n\r\n",
)

def test_unfinishedTrailerHeader(self):
r"""
L{_ChunkedTransferDecoder.dataReceived} raises
L{_MalformedChunkedDataError} when the trailing headers data is too long
and doesn't have final CRLF characters.
"""
p = http._ChunkedTransferDecoder(
lambda b: None,
lambda b: None, # pragma: nocov
)
p._maxTrailerHeadersSize = 10
p.dataReceived(b"3\r\nabc\r\n0\r\n0123456789")
self.assertRaises(
http._MalformedChunkedDataError,
p.dataReceived,
b"A",
)


class ChunkingTests(unittest.TestCase, ResponseTestMixin):
strings = [b"abcv", b"", b"fdfsd423", b"Ffasfas\r\n", b"523523\n\rfsdf", b"4234"]
Expand Down

0 comments on commit dd96b6f

Please sign in to comment.