|
18 | 18 | )
|
19 | 19 | from tornado.iostream import IOStream
|
20 | 20 | from tornado.locks import Event
|
21 |
| -from tornado.log import gen_log |
| 21 | +from tornado.log import gen_log, app_log |
22 | 22 | from tornado.netutil import ssl_options_to_context
|
23 | 23 | from tornado.simple_httpclient import SimpleAsyncHTTPClient
|
24 | 24 | from tornado.testing import (
|
|
41 | 41 | import ssl
|
42 | 42 | import sys
|
43 | 43 | import tempfile
|
| 44 | +import textwrap |
44 | 45 | import unittest
|
45 | 46 | import urllib.parse
|
46 | 47 | from io import BytesIO
|
@@ -118,7 +119,7 @@ class SSLTestMixin(object):
|
118 | 119 | def get_ssl_options(self):
|
119 | 120 | return dict(
|
120 | 121 | ssl_version=self.get_ssl_version(),
|
121 |
| - **AsyncHTTPSTestCase.default_ssl_options() |
| 122 | + **AsyncHTTPSTestCase.default_ssl_options(), |
122 | 123 | )
|
123 | 124 |
|
124 | 125 | def get_ssl_version(self):
|
@@ -558,23 +559,60 @@ def test_chunked_request_uppercase(self):
|
558 | 559 | )
|
559 | 560 | self.assertEqual(json_decode(response), {"foo": ["bar"]})
|
560 | 561 |
|
561 |
| - @gen_test |
562 |
| - def test_invalid_content_length(self): |
563 |
| - with ExpectLog( |
564 |
| - gen_log, ".*Only integer Content-Length is allowed", level=logging.INFO |
565 |
| - ): |
566 |
| - self.stream.write( |
567 |
| - b"""\ |
| 562 | + def test_chunked_request_body_invalid_size(self): |
| 563 | + # Only hex digits are allowed in chunk sizes. Python's int() function |
| 564 | + # also accepts underscores, so make sure we reject them here. |
| 565 | + self.stream.write( |
| 566 | + b"""\ |
568 | 567 | POST /echo HTTP/1.1
|
569 |
| -Content-Length: foo |
| 568 | +Transfer-Encoding: chunked |
570 | 569 |
|
571 |
| -bar |
| 570 | +1_a |
| 571 | +1234567890abcdef1234567890 |
| 572 | +0 |
572 | 573 |
|
573 | 574 | """.replace(
|
574 |
| - b"\n", b"\r\n" |
575 |
| - ) |
| 575 | + b"\n", b"\r\n" |
| 576 | + ) |
| 577 | + ) |
| 578 | + with ExpectLog(gen_log, ".*invalid chunk size", level=logging.INFO): |
| 579 | + start_line, headers, response = self.io_loop.run_sync( |
| 580 | + lambda: read_stream_body(self.stream) |
576 | 581 | )
|
577 |
| - yield self.stream.read_until_close() |
| 582 | + self.assertEqual(400, start_line.code) |
| 583 | + |
| 584 | + @gen_test |
| 585 | + def test_invalid_content_length(self): |
| 586 | + # HTTP only allows decimal digits in content-length. Make sure we don't |
| 587 | + # accept anything else, with special attention to things accepted by the |
| 588 | + # python int() function (leading plus signs and internal underscores). |
| 589 | + test_cases = [ |
| 590 | + ("alphabetic", "foo"), |
| 591 | + ("leading plus", "+10"), |
| 592 | + ("internal underscore", "1_0"), |
| 593 | + ] |
| 594 | + for name, value in test_cases: |
| 595 | + with self.subTest(name=name), closing(IOStream(socket.socket())) as stream: |
| 596 | + with ExpectLog( |
| 597 | + gen_log, |
| 598 | + ".*Only integer Content-Length is allowed", |
| 599 | + level=logging.INFO, |
| 600 | + ): |
| 601 | + yield stream.connect(("127.0.0.1", self.get_http_port())) |
| 602 | + stream.write( |
| 603 | + utf8( |
| 604 | + textwrap.dedent( |
| 605 | + f"""\ |
| 606 | + POST /echo HTTP/1.1 |
| 607 | + Content-Length: {value} |
| 608 | + Connection: close |
| 609 | +
|
| 610 | + 1234567890 |
| 611 | + """ |
| 612 | + ).replace("\n", "\r\n") |
| 613 | + ) |
| 614 | + ) |
| 615 | + yield stream.read_until_close() |
578 | 616 |
|
579 | 617 |
|
580 | 618 | class XHeaderTest(HandlerBaseTestCase):
|
@@ -1123,6 +1161,46 @@ def body_producer(write):
|
1123 | 1161 | )
|
1124 | 1162 |
|
1125 | 1163 |
|
| 1164 | +class InvalidOutputContentLengthTest(AsyncHTTPTestCase): |
| 1165 | + class MessageDelegate(HTTPMessageDelegate): |
| 1166 | + def __init__(self, connection): |
| 1167 | + self.connection = connection |
| 1168 | + |
| 1169 | + def headers_received(self, start_line, headers): |
| 1170 | + content_lengths = { |
| 1171 | + "normal": "10", |
| 1172 | + "alphabetic": "foo", |
| 1173 | + "leading plus": "+10", |
| 1174 | + "underscore": "1_0", |
| 1175 | + } |
| 1176 | + self.connection.write_headers( |
| 1177 | + ResponseStartLine("HTTP/1.1", 200, "OK"), |
| 1178 | + HTTPHeaders({"Content-Length": content_lengths[headers["x-test"]]}), |
| 1179 | + ) |
| 1180 | + self.connection.write(b"1234567890") |
| 1181 | + self.connection.finish() |
| 1182 | + |
| 1183 | + def get_app(self): |
| 1184 | + class App(HTTPServerConnectionDelegate): |
| 1185 | + def start_request(self, server_conn, request_conn): |
| 1186 | + return InvalidOutputContentLengthTest.MessageDelegate(request_conn) |
| 1187 | + |
| 1188 | + return App() |
| 1189 | + |
| 1190 | + def test_invalid_output_content_length(self): |
| 1191 | + with self.subTest("normal"): |
| 1192 | + response = self.fetch("/", method="GET", headers={"x-test": "normal"}) |
| 1193 | + response.rethrow() |
| 1194 | + self.assertEqual(response.body, b"1234567890") |
| 1195 | + for test in ["alphabetic", "leading plus", "underscore"]: |
| 1196 | + with self.subTest(test): |
| 1197 | + # This log matching could be tighter but I think I'm already |
| 1198 | + # over-testing here. |
| 1199 | + with ExpectLog(app_log, "Uncaught exception"): |
| 1200 | + with self.assertRaises(HTTPError): |
| 1201 | + self.fetch("/", method="GET", headers={"x-test": test}) |
| 1202 | + |
| 1203 | + |
1126 | 1204 | class MaxHeaderSizeTest(AsyncHTTPTestCase):
|
1127 | 1205 | def get_app(self):
|
1128 | 1206 | return Application([("/", HelloWorldRequestHandler)])
|
|
0 commit comments