From e889c85dae4164de7f6414fe76dc55c4102cf30a Mon Sep 17 00:00:00 2001 From: Gregory Holt Date: Thu, 20 Oct 2011 03:11:57 +0000 Subject: [PATCH 1/4] Limit HTTP header line length --- eventlet/wsgi.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/eventlet/wsgi.py b/eventlet/wsgi.py index e9959e1..15fcba3 100644 --- a/eventlet/wsgi.py +++ b/eventlet/wsgi.py @@ -15,6 +15,7 @@ from eventlet.support import get_errno DEFAULT_MAX_SIMULTANEOUS_REQUESTS = 1024 DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1' MAX_REQUEST_LINE = 8192 +MAX_HEADER_LINE = 8192 MINIMUM_CHUNK_SIZE = 4096 DEFAULT_LOG_FORMAT= ('%(client_ip)s - - [%(date_time)s] "%(request_line)s"' ' %(status_code)s %(body_length)s %(wall_seconds).6f') @@ -163,6 +164,25 @@ class Input(object): return self.rfile._sock +class ReadlineTooLong(Exception): + pass + + +class FileObjectForHeaders(object): + + def __init__(self, fp): + self.fp = fp + + def readline(self, size=-1): + sz = size + if size < 0: + sz = MAX_HEADER_LINE + rv = self.fp.readline(sz) + if size < 0 and len(rv) == MAX_HEADER_LINE: + raise ReadlineTooLong() + return rv + + class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' minimum_chunk_size = MINIMUM_CHUNK_SIZE @@ -210,8 +230,19 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): self.close_connection = 1 return - if not self.parse_request(): + orig_rfile = self.rfile + try: + self.rfile = FileObjectForHeaders(self.rfile) + if not self.parse_request(): + return + except ReadlineTooLong: + self.wfile.write( + "HTTP/1.0 400 Header Line Too Long\r\n" + "Connection: close\r\nContent-length: 0\r\n\r\n") + self.close_connection = 1 return + finally: + self.rfile = orig_rfile content_length = self.headers.getheader('content-length') if content_length: From fa59a3b339e103bd2660c3cf2204bbd1da297ba3 Mon Sep 17 00:00:00 2001 From: Gregory Holt Date: Thu, 20 Oct 2011 14:37:22 +0000 Subject: [PATCH 2/4] Limit overall HTTP header length and tests --- eventlet/wsgi.py | 23 +++++++++++++++++++---- tests/wsgi_test.py | 24 ++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/eventlet/wsgi.py b/eventlet/wsgi.py index 15fcba3..6003664 100644 --- a/eventlet/wsgi.py +++ b/eventlet/wsgi.py @@ -16,6 +16,7 @@ DEFAULT_MAX_SIMULTANEOUS_REQUESTS = 1024 DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1' MAX_REQUEST_LINE = 8192 MAX_HEADER_LINE = 8192 +MAX_TOTAL_HEADER_SIZE = 65536 MINIMUM_CHUNK_SIZE = 4096 DEFAULT_LOG_FORMAT= ('%(client_ip)s - - [%(date_time)s] "%(request_line)s"' ' %(status_code)s %(body_length)s %(wall_seconds).6f') @@ -164,7 +165,11 @@ class Input(object): return self.rfile._sock -class ReadlineTooLong(Exception): +class HeaderLineTooLong(Exception): + pass + + +class HeadersTooLarge(Exception): pass @@ -172,14 +177,18 @@ class FileObjectForHeaders(object): def __init__(self, fp): self.fp = fp + self.total_header_size = 0 def readline(self, size=-1): sz = size if size < 0: sz = MAX_HEADER_LINE rv = self.fp.readline(sz) - if size < 0 and len(rv) == MAX_HEADER_LINE: - raise ReadlineTooLong() + if size < 0 and len(rv) >= MAX_HEADER_LINE: + raise HeaderLineTooLong() + self.total_header_size += len(rv) + if self.total_header_size > MAX_TOTAL_HEADER_SIZE: + raise HeadersTooLarge() return rv @@ -235,12 +244,18 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): self.rfile = FileObjectForHeaders(self.rfile) if not self.parse_request(): return - except ReadlineTooLong: + except HeaderLineTooLong: self.wfile.write( "HTTP/1.0 400 Header Line Too Long\r\n" "Connection: close\r\nContent-length: 0\r\n\r\n") self.close_connection = 1 return + except HeadersTooLarge: + self.wfile.write( + "HTTP/1.0 400 Headers Too Large\r\n" + "Connection: close\r\nContent-length: 0\r\n\r\n") + self.close_connection = 1 + return finally: self.rfile = orig_rfile diff --git a/tests/wsgi_test.py b/tests/wsgi_test.py index b257efb..ecdb688 100644 --- a/tests/wsgi_test.py +++ b/tests/wsgi_test.py @@ -871,6 +871,30 @@ class TestHttpd(_TestBase): self.assertEquals(posthook1_count[0], 26) self.assertEquals(posthook2_count[0], 25) + def test_030_reject_long_header_lines(self): + sock = eventlet.connect(('localhost', self.port)) + request = 'GET / HTTP/1.0\r\nHost: localhost\r\nLong: %s\r\n\r\n' % \ + ('a' * 10000) + fd = sock.makefile('rw') + fd.write(request) + fd.flush() + response_line, headers, body = read_http(sock) + self.assertEquals(response_line, + 'HTTP/1.0 400 Header Line Too Long\r\n') + fd.close() + + def test_031_reject_large_headers(self): + sock = eventlet.connect(('localhost', self.port)) + headers = 'Name: Value\r\n' * 5050 + request = 'GET / HTTP/1.0\r\nHost: localhost\r\n%s\r\n\r\n' % headers + fd = sock.makefile('rw') + fd.write(request) + fd.flush() + response_line, headers, body = read_http(sock) + self.assertEquals(response_line, + 'HTTP/1.0 400 Headers Too Large\r\n') + fd.close() + def test_zero_length_chunked_response(self): def zero_chunked_app(env, start_response): start_response('200 OK', [('Content-type', 'text/plain')]) From f1bed7d9f95cbe4d44e1dd0a22645002d6183869 Mon Sep 17 00:00:00 2001 From: Duncan McGreggor Date: Fri, 28 Oct 2011 23:12:12 -0700 Subject: [PATCH 3/4] Added a missing traceback import. --- tests/mysqldb_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/mysqldb_test.py b/tests/mysqldb_test.py index 7eb3e52..f6f45cf 100644 --- a/tests/mysqldb_test.py +++ b/tests/mysqldb_test.py @@ -1,6 +1,7 @@ import os import sys import time +import traceback from tests import skipped, skip_unless, using_pyevent, get_database_auth, LimitedTestCase import eventlet from eventlet import event From 21f905dbc6f7acccffa9e74711e1a57885e0e9c9 Mon Sep 17 00:00:00 2001 From: "jmg.utn" Date: Sun, 6 Nov 2011 14:33:56 -0300 Subject: [PATCH 4/4] log_output flag parameter added to the wsgi server --- eventlet/wsgi.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/eventlet/wsgi.py b/eventlet/wsgi.py index e9959e1..0ae5389 100644 --- a/eventlet/wsgi.py +++ b/eventlet/wsgi.py @@ -383,14 +383,16 @@ class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): for hook, args, kwargs in self.environ['eventlet.posthooks']: hook(self.environ, *args, **kwargs) - - self.server.log_message(self.server.log_format % dict( - client_ip=self.get_client_ip(), - date_time=self.log_date_time_string(), - request_line=self.requestline, - status_code=status_code[0], - body_length=length[0], - wall_seconds=finish - start)) + + if self.server.log_output: + + self.server.log_message(self.server.log_format % dict( + client_ip=self.get_client_ip(), + date_time=self.log_date_time_string(), + request_line=self.requestline, + status_code=status_code[0], + body_length=length[0], + wall_seconds=finish - start)) def get_client_ip(self): client_ip = self.client_address[0] @@ -471,6 +473,7 @@ class Server(BaseHTTPServer.HTTPServer): minimum_chunk_size=None, log_x_forwarded_for=True, keepalive=True, + log_output=True, log_format=DEFAULT_LOG_FORMAT, debug=True): @@ -490,6 +493,7 @@ class Server(BaseHTTPServer.HTTPServer): if minimum_chunk_size is not None: protocol.minimum_chunk_size = minimum_chunk_size self.log_x_forwarded_for = log_x_forwarded_for + self.log_output = log_output self.log_format = log_format self.debug = debug @@ -536,7 +540,8 @@ def server(sock, site, minimum_chunk_size=None, log_x_forwarded_for=True, custom_pool=None, - keepalive=True, + keepalive=True, + log_output=True, log_format=DEFAULT_LOG_FORMAT, debug=True): """ Start up a wsgi server handling requests from the supplied server @@ -556,6 +561,7 @@ def server(sock, site, :param log_x_forwarded_for: If True (the default), logs the contents of the x-forwarded-for header in addition to the actual client ip address in the 'client_ip' field of the log line. :param custom_pool: A custom GreenPool instance which is used to spawn client green threads. If this is supplied, max_size is ignored. :param keepalive: If set to False, disables keepalives on the server; all connections will be closed after serving one request. + :param log_output: A Boolean indicating if the server will log data or not. :param log_format: A python format string that is used as the template to generate log lines. The following values can be formatted into it: client_ip, date_time, request_line, status_code, body_length, wall_seconds. The default is a good example of how to use it. :param debug: True if the server should send exception tracebacks to the clients on 500 errors. If False, the server will respond with empty bodies. """ @@ -567,6 +573,7 @@ def server(sock, site, minimum_chunk_size=minimum_chunk_size, log_x_forwarded_for=log_x_forwarded_for, keepalive=keepalive, + log_output=log_output, log_format=log_format, debug=debug) if server_event is not None: