From 5b624ab729824346dfc1e6958b81992a82ce462b Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Sun, 6 Apr 2014 13:10:16 +0100 Subject: [PATCH] Add a configurable header size limit to HTTPServer and simple_httpclient. --- tornado/http1connection.py | 8 +++++-- tornado/httpserver.py | 6 ++++-- tornado/simple_httpclient.py | 13 ++++++++---- tornado/test/httpserver_test.py | 18 ++++++++++++++++ tornado/test/simple_httpclient_test.py | 29 ++++++++++++++++++++++++++ tornado/websocket.py | 2 +- 6 files changed, 67 insertions(+), 9 deletions(-) diff --git a/tornado/http1connection.py b/tornado/http1connection.py index 7e1ab3d72..eb1f30906 100644 --- a/tornado/http1connection.py +++ b/tornado/http1connection.py @@ -35,7 +35,8 @@ class HTTP1Connection(object): until the HTTP conection is closed. """ def __init__(self, stream, address, is_client, - no_keep_alive=False, protocol=None, chunk_size=None): + no_keep_alive=False, protocol=None, chunk_size=None, + max_header_size=None): self.is_client = is_client self.stream = stream self.address = address @@ -58,6 +59,7 @@ class HTTP1Connection(object): else: self.protocol = "http" self._chunk_size = chunk_size or 65536 + self._max_header_size = max_header_size or 65536 self._disconnect_on_finish = False self._clear_request_state() self.stream.set_close_callback(self._on_connection_close) @@ -112,7 +114,9 @@ class HTTP1Connection(object): assert isinstance(delegate, httputil.HTTPMessageDelegate) self.message_delegate = delegate try: - header_data = yield self.stream.read_until_regex(b"\r?\n\r?\n") + header_data = yield self.stream.read_until_regex( + b"\r?\n\r?\n", + max_bytes=self._max_header_size) self._reading = True self._finish_future = Future() start_line, headers = self._parse_headers(header_data) diff --git a/tornado/httpserver.py b/tornado/httpserver.py index 1a0aff297..6f9b9db23 100644 --- a/tornado/httpserver.py +++ b/tornado/httpserver.py @@ -136,13 +136,14 @@ class HTTPServer(TCPServer, httputil.HTTPServerConnectionDelegate): """ def __init__(self, request_callback, no_keep_alive=False, io_loop=None, xheaders=False, ssl_options=None, protocol=None, gzip=False, - chunk_size=None, **kwargs): + chunk_size=None, max_header_size=None, **kwargs): self.request_callback = request_callback self.no_keep_alive = no_keep_alive self.xheaders = xheaders self.protocol = protocol self.gzip = gzip self.chunk_size = chunk_size + self.max_header_size = max_header_size TCPServer.__init__(self, io_loop=io_loop, ssl_options=ssl_options, **kwargs) @@ -150,7 +151,8 @@ class HTTPServer(TCPServer, httputil.HTTPServerConnectionDelegate): conn = HTTP1Connection(stream, address=address, is_client=False, no_keep_alive=self.no_keep_alive, protocol=self.protocol, - chunk_size=self.chunk_size) + chunk_size=self.chunk_size, + max_header_size=self.max_header_size) conn.start_serving(self, gzip=self.gzip) def start_request(self, connection): diff --git a/tornado/simple_httpclient.py b/tornado/simple_httpclient.py index 4f562226b..bb59b30f7 100644 --- a/tornado/simple_httpclient.py +++ b/tornado/simple_httpclient.py @@ -48,7 +48,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): """ def initialize(self, io_loop, max_clients=10, hostname_mapping=None, max_buffer_size=104857600, - resolver=None, defaults=None): + resolver=None, defaults=None, max_header_size=None): """Creates a AsyncHTTPClient. Only a single AsyncHTTPClient instance exists per IOLoop @@ -75,6 +75,7 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): self.active = {} self.waiting = {} self.max_buffer_size = max_buffer_size + self.max_header_size = max_header_size if resolver: self.resolver = resolver self.own_resolver = False @@ -120,7 +121,8 @@ class SimpleAsyncHTTPClient(AsyncHTTPClient): def _handle_request(self, request, release_callback, final_callback): _HTTPConnection(self.io_loop, self, request, release_callback, - final_callback, self.max_buffer_size, self.resolver) + final_callback, self.max_buffer_size, self.resolver, + self.max_header_size) def _release_fetch(self, key): del self.active[key] @@ -147,7 +149,8 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): _SUPPORTED_METHODS = set(["GET", "HEAD", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) def __init__(self, io_loop, client, request, release_callback, - final_callback, max_buffer_size, resolver): + final_callback, max_buffer_size, resolver, + max_header_size): self.start_time = io_loop.time() self.io_loop = io_loop self.client = client @@ -156,6 +159,7 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): self.final_callback = final_callback self.max_buffer_size = max_buffer_size self.resolver = resolver + self.max_header_size = max_header_size self.code = None self.headers = None self.chunks = [] @@ -330,7 +334,8 @@ class _HTTPConnection(httputil.HTTPMessageDelegate): self.stream.set_nodelay(True) self.connection = HTTP1Connection( self.stream, self._sockaddr, is_client=True, - no_keep_alive=True, protocol=self.parsed.scheme) + no_keep_alive=True, protocol=self.parsed.scheme, + max_header_size=self.max_header_size) start_line = httputil.RequestStartLine(self.request.method, req_path, 'HTTP/1.1') self.connection.write_headers( diff --git a/tornado/test/httpserver_test.py b/tornado/test/httpserver_test.py index 9cadb042b..7a2d651da 100644 --- a/tornado/test/httpserver_test.py +++ b/tornado/test/httpserver_test.py @@ -826,3 +826,21 @@ class StreamingChunkSizeTest(AsyncHTTPTestCase): write(compressed[20:]) self.fetch_chunk_sizes(body_producer=body_producer, headers={'Content-Encoding': 'gzip'}) + + +class MaxHeaderSizeTest(AsyncHTTPTestCase): + def get_app(self): + return Application([('/', HelloWorldRequestHandler)]) + + def get_httpserver_options(self): + return dict(max_header_size=1024) + + def test_small_headers(self): + response = self.fetch("/", headers={'X-Filler': 'a' * 100}) + response.rethrow() + self.assertEqual(response.body, b"Hello world") + + def test_large_headers(self): + with ExpectLog(gen_log, "Unsatisfiable read"): + response = self.fetch("/", headers={'X-Filler': 'a' * 1000}) + self.assertEqual(response.code, 599) diff --git a/tornado/test/simple_httpclient_test.py b/tornado/test/simple_httpclient_test.py index ad905552e..5ca795599 100644 --- a/tornado/test/simple_httpclient_test.py +++ b/tornado/test/simple_httpclient_test.py @@ -496,3 +496,32 @@ class ResolveTimeoutTestCase(AsyncHTTPTestCase): def test_resolve_timeout(self): response = self.fetch('/hello', connect_timeout=0.1) self.assertEqual(response.code, 599) + + +class MaxHeaderSizeTest(AsyncHTTPTestCase): + def get_app(self): + class SmallHeaders(RequestHandler): + def get(self): + self.set_header("X-Filler", "a" * 100) + self.write("ok") + + class LargeHeaders(RequestHandler): + def get(self): + self.set_header("X-Filler", "a" * 1000) + self.write("ok") + + return Application([('/small', SmallHeaders), + ('/large', LargeHeaders)]) + + def get_http_client(self): + return SimpleAsyncHTTPClient(io_loop=self.io_loop, max_header_size=1024) + + def test_small_headers(self): + response = self.fetch('/small') + response.rethrow() + self.assertEqual(response.body, b'ok') + + def test_large_headers(self): + with ExpectLog(gen_log, "Unsatisfiable read"): + response = self.fetch('/large') + self.assertEqual(response.code, 599) diff --git a/tornado/websocket.py b/tornado/websocket.py index 40d04e65a..66a4fedf9 100644 --- a/tornado/websocket.py +++ b/tornado/websocket.py @@ -823,7 +823,7 @@ class WebSocketClientConnection(simple_httpclient._HTTPConnection): self.resolver = Resolver(io_loop=io_loop) super(WebSocketClientConnection, self).__init__( io_loop, None, request, lambda: None, self._on_http_response, - 104857600, self.resolver) + 104857600, self.resolver, 65536) def close(self, code=None, reason=None): """Closes the websocket connection. -- 2.47.2