Ticket #3795: secure-chunked-3795-3.diff
| File secure-chunked-3795-3.diff, 12.7 KB (added by ivank, 4 years ago) |
|---|
-
twisted/web/http.py
=== modified file 'twisted/web/http.py'
9 9 10 10 Future Plans: 11 11 - HTTP client support will at some point be refactored to support HTTP/1.1. 12 - Accept chunked data from clients in server.13 12 - Other missing HTTP features from the RFC. 14 13 15 14 Maintainer: Itamar Shtull-Trauring … … 36 35 from twisted.python import log 37 36 try: # try importing the fast, C version 38 37 from twisted.protocols._c_urlarg import unquote 38 unquote # shut up pyflakes 39 39 except ImportError: 40 40 from urllib import unquote 41 41 … … 1279 1279 Protocol for decoding I{chunked} Transfer-Encoding, as defined by RFC 2616, 1280 1280 section 3.6.1. This protocol can interpret the contents of a request or 1281 1281 response body which uses the I{chunked} Transfer-Encoding. It cannot 1282 interpret any of the rest of the HTTP protocol. 1282 interpret any of the rest of the HTTP protocol. It ignores trailers. 1283 1283 1284 1284 It may make sense for _ChunkedTransferDecoder to be an actual IProtocol 1285 1285 implementation. Currently, the only user of this class will only ever … … 1298 1298 @ivar finishCallback: A one-argument callable which will be invoked when 1299 1299 the terminal chunk is received. It will be invoked with all bytes 1300 1300 which were delivered to this protocol which came after the terminal 1301 chunk. 1301 chunk. These bytes are *not* the trailer; they might be the beginning 1302 of the next request or response. 1302 1303 1303 1304 @ivar length: Counter keeping track of how many more bytes in a chunk there 1304 1305 are to receive. … … 1315 1316 will be accepted. 1316 1317 """ 1317 1318 state = 'chunk-length' 1318 finish = False1319 1319 1320 1320 def __init__(self, dataCallback, finishCallback): 1321 1321 self.dataCallback = dataCallback 1322 1322 self.finishCallback = finishCallback 1323 1323 self._buffer = '' 1324 1325 # While an HTTP/1.1 chunk has no size limit in the specification, a 1326 # reasonable limit must be established to prevent untrusted input from 1327 # causing excessive string concatenation in the parser. A limit of 17 bytes 1328 # (max FFFFFFFFFFFFFFFFF) can support chunks up to 2**68-1 bytes. 1329 self._maximumChunkSizeStringLength = 17 1324 1330 1325 1331 1326 1332 def dataReceived(self, data): … … 1335 1341 if '\r\n' in data: 1336 1342 line, rest = data.split('\r\n', 1) 1337 1343 parts = line.split(';') 1338 self.length = int(parts[0], 16) 1344 # HEX in RFC 2616 section 2.2 does not include the minus 1345 # sign; negative numbers and negative zero are not allowed. 1346 if parts[0][0] == '-': 1347 raise RuntimeError( 1348 "_ChunkedTransferDecoder.dataReceived received " 1349 "negative chunk length in parts %s" % (repr(parts),)) 1350 if len(parts[0]) > self._maximumChunkSizeStringLength: 1351 raise RuntimeError( 1352 "_ChunkedTransferDecoder.dataReceived received " 1353 "too-long chunk length in parts %s" % (repr(parts),)) 1354 try: 1355 self.length = int(parts[0], 16) 1356 except ValueError: 1357 raise RuntimeError( 1358 "_ChunkedTransferDecoder.dataReceived received " 1359 "unparsable chunk length in parts %s" % (repr(parts),)) 1339 1360 if self.length == 0: 1340 1361 self.state = 'trailer' 1341 self.finish = True1342 1362 else: 1343 1363 self.state = 'body' 1344 1364 data = rest 1345 1365 else: 1366 # Throw away HTTP/1.1 chunk-extensions every time, 1367 # but keep the semicolon so that additional chunk-extension 1368 # data doesn't get interpreted as part of the chunk-length. 1369 if ';' in data: 1370 reattachCR = (data[-1] == '\r') 1371 data = data[:data.find(';')+1].strip() 1372 if reattachCR: 1373 data += '\r' 1374 extraByte = 1 1375 else: 1376 extraByte = 0 1377 1378 if len(data) > (self._maximumChunkSizeStringLength + extraByte): 1379 raise RuntimeError( 1380 "_ChunkedTransferDecoder.dataReceived buffered " 1381 "too-long chunk length %s" % (repr(data),)) 1346 1382 self._buffer = data 1347 1383 data = '' 1348 elif self.state == ' trailer':1384 elif self.state == 'crlf': 1349 1385 if data.startswith('\r\n'): 1350 1386 data = data[2:] 1351 if self.finish: 1352 self.state = 'finished' 1353 self.finishCallback(data) 1354 data = '' 1355 else: 1356 self.state = 'chunk-length' 1357 else: 1387 self.state = 'chunk-length' 1388 elif data == '\r': 1358 1389 self._buffer = data 1359 1390 data = '' 1391 else: 1392 raise RuntimeError( 1393 "_ChunkedTransferDecoder.dataReceived was looking for " 1394 "CRLF, not %s" % (repr(data),)) 1395 elif self.state == 'trailer': 1396 # The goal is to throw away as much of the trailer as 1397 # possible every time, while hoping to get the end-of-trailer. 1398 CRLF_at = data.find('\r\n') 1399 if CRLF_at != -1: 1400 # The *first* '\r\n' ends the trailer. 1401 data = data[CRLF_at+2:] 1402 self.state = 'finished' 1403 self.finishCallback(data) 1404 elif data[-1] == '\r': 1405 self._buffer = data[-1] 1406 data = '' 1360 1407 elif self.state == 'body': 1361 1408 if len(data) >= self.length: 1362 1409 chunk, data = data[:self.length], data[self.length:] 1363 1410 self.dataCallback(chunk) 1364 self.state = ' trailer'1411 self.state = 'crlf' 1365 1412 elif len(data) < self.length: 1366 1413 self.length -= len(data) 1367 1414 self.dataCallback(data) -
twisted/web/test/test_http.py
=== modified file 'twisted/web/test/test_http.py'
527 527 """ 528 528 p = http._ChunkedTransferDecoder(None, lambda bytes: None) 529 529 p.dataReceived('0\r\n\r\n') 530 self.assertRaises(RuntimeError, p.dataReceived, 'hello') 531 530 exc = self.assertRaises(RuntimeError, p.dataReceived, 'hello') 531 self.assertEqual( 532 str(exc), 533 "_ChunkedTransferDecoder.dataReceived called after last " 534 "chunk was processed") 535 532 536 533 537 def test_earlyConnectionLose(self): 534 538 """ … … 574 578 self.assertEqual(successes, [True]) 575 579 576 580 581 def test_trailerUsesNoMemory(self): 582 """ 583 L{_ChunkedTransferDecoder.dataReceived} does not waste memory 584 buffering pieces of the trailer, which is always ignored anyway. 585 586 This test is very implementation-specific because the parser exhibits 587 no public behavior while ignoring the trailer. 588 """ 589 L = [] 590 finished = [] 591 p = http._ChunkedTransferDecoder(L.append, finished.append) 592 p.dataReceived('3\r\nabc\r\n0\r\nTRAILER') 593 self.assertEqual(len(p._buffer), 0) 594 p.dataReceived('MORE TRAILER') 595 self.assertEqual(len(p._buffer), 0) 596 p.dataReceived('Here comes a CR: \r') 597 self.assertEqual(len(p._buffer), 1) 598 p.dataReceived('But no newline!') 599 self.assertEqual(len(p._buffer), 0) 600 p.dataReceived('Really finish the trailer now: \r\n') 601 self.assertEqual(len(p._buffer), 0) 602 self.assertEqual(L, ['abc']) 603 self.assertEqual(finished, ['']) 604 605 606 def test_chunkExtensionsUseNoMemory(self): 607 """ 608 L{_ChunkedTransferDecoder.dataReceived} does not waste memory 609 buffering pieces of chunk extensions, which are always ignored anyway. 610 611 This test is very implementation-specific because the parser exhibits 612 no public behavior while ignoring the chunk extensions. 613 """ 614 L = [] 615 finished = [] 616 p = http._ChunkedTransferDecoder(L.append, finished.append) 617 p.dataReceived('3\r\nabc\r\n4; hello=yes') 618 originalLength = len(p._buffer) 619 # feed it some more ignored chunk-extension 620 p.dataReceived('-still-ignored') 621 self.assertEqual(len(p._buffer), originalLength) 622 623 624 def test_limitedChunkLengthBuffering(self): 625 """ 626 L{_ChunkedTransferDecoder.dataReceived} does not allow input 627 to endlessly fill its buffer with a chunk length string. 628 """ 629 L = [] 630 p = http._ChunkedTransferDecoder(L.append, None) 631 max = p._maximumChunkSizeStringLength 632 633 p.dataReceived('2\r\nab\r\n') 634 exc = self.assertRaises(RuntimeError, lambda: p.dataReceived('3' * (max+1))) 635 self.assertEqual( 636 str(exc), 637 "_ChunkedTransferDecoder.dataReceived buffered too-long " 638 "chunk length '333333333333333333'") 639 640 641 def test_limitedChunkLengthBufferingShort(self): 642 """ 643 L{_ChunkedTransferDecoder.dataReceived} does not allow input 644 to endlessly fill its buffer with a chunk length string, even when 645 the data is delivered with multiple calls. 646 """ 647 L = [] 648 p = http._ChunkedTransferDecoder(L.append, None) 649 max = p._maximumChunkSizeStringLength 650 651 p.dataReceived('2\r\nab\r\n') 652 for s in '3' * max: 653 p.dataReceived(s) 654 exc = self.assertRaises(RuntimeError, lambda: p.dataReceived('3' * 1)) 655 self.assertEqual( 656 str(exc), 657 "_ChunkedTransferDecoder.dataReceived buffered too-long " 658 "chunk length '333333333333333333'") 659 660 661 def test_chunkLengthNotTooLong(self): 662 """ 663 664 """ 665 L = [] 666 p = http._ChunkedTransferDecoder(L.append, None) 667 max = p._maximumChunkSizeStringLength 668 669 p.dataReceived('2\r\nab\r\n') 670 671 chunkLenString = ('3' * (max+1)) 672 exc = self.assertRaises(RuntimeError, 673 lambda: p.dataReceived(chunkLenString + '\r\n')) 674 675 self.assertEqual( 676 str(exc), 677 "_ChunkedTransferDecoder.dataReceived received " 678 "too-long chunk length in parts %s" % (repr([chunkLenString]),)) 679 680 681 def test_chunkLengthSemicolonMath(self): 682 """ 683 L{_ChunkedTransferDecoder.dataReceived} doesn't include 684 the length of the semicolon or chunk-extension data when 685 determining the length of the chunk-length bytes. 686 """ 687 L = [] 688 p = http._ChunkedTransferDecoder(L.append, None) 689 max = p._maximumChunkSizeStringLength 690 691 p.dataReceived((('3' * (max)) + '; long-extension-completely-ignored=yes')) 692 693 694 def test_chunkLengthNotUnparsable(self): 695 """ 696 697 """ 698 L = [] 699 p = http._ChunkedTransferDecoder(L.append, None) 700 701 p.dataReceived('2\r\nab\r\n') 702 703 chunkLenString = ('G') 704 exc = self.assertRaises(RuntimeError, 705 lambda: p.dataReceived(chunkLenString + '\r\n')) 706 707 self.assertEqual( 708 str(exc), 709 "_ChunkedTransferDecoder.dataReceived received " 710 "unparsable chunk length in parts %s" % (repr([chunkLenString]),)) 711 712 713 def test_chunkLengthNotNegative(self): 714 """ 715 716 """ 717 L = [] 718 p = http._ChunkedTransferDecoder(L.append, None) 719 720 p.dataReceived('2\r\nab\r\n') 721 exc = self.assertRaises(RuntimeError, lambda: p.dataReceived('-1\r\n')) 722 self.assertEqual( 723 str(exc), 724 "_ChunkedTransferDecoder.dataReceived received " 725 "negative chunk length in parts %s" % (repr(['-1']),)) 726 727 728 def test_chunkLengthNotNegativeZero(self): 729 """ 730 731 """ 732 L = [] 733 p = http._ChunkedTransferDecoder(L.append, None) 734 735 p.dataReceived('2\r\nab\r\n') 736 exc = self.assertRaises(RuntimeError, lambda: p.dataReceived('-0\r\n')) 737 self.assertEqual( 738 str(exc), 739 "_ChunkedTransferDecoder.dataReceived received " 740 "negative chunk length in parts %s" % (repr(['-0']),)) 741 742 577 743 578 744 class ChunkingTestCase(unittest.TestCase): 579 745
