Anonymous avatar Anonymous committed 00f620b

Implemented ticket #90 (still need to write docs though)

Comments (0)

Files changed (5)

cherrypy/_cphttptools.py

 from urlparse import urlparse
 
 import cherrypy
-from cherrypy import _cputil, _cpcgifs
+from cherrypy import _cputil, _cpcgifs, _cperror, _cpwsgiserver
 from cherrypy.lib import cptools
 
 
         
         # FieldStorage only recognizes POST, so fake it.
         methenv = {'REQUEST_METHOD': "POST"}
-        forms = _cpcgifs.FieldStorage(fp=request.rfile,
+        try:
+            forms = _cpcgifs.FieldStorage(fp=request.rfile,
                                       headers=lowerHeaderMap,
                                       environ=methenv,
                                       keep_blank_values=1)
+        except _cpwsgiserver.MaxSizeExceeded:
+            # Post data is too big
+            raise _cperror.HTTPStatusError(413)
         
         if forms.file:
             # request body was a content-type other than form params.

cherrypy/_cpwsgi.py

                         wsgiApp,
                         conf("server.threadPool"),
                         conf("server.socketHost"),
+                        config = cherrypy.config
                         )

cherrypy/_cpwsgiserver.py

 monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                    'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
 
+class MaxSizeExceeded(Exception):
+    pass
+
+class SizeCheckWrapper(object):
+    """ Wrapper around the rfile object. For each data reading method,
+        it reads the data but it checks that the size of the data doesn't
+        except a certain limit
+    """
+    def __init__(self, rfile, maxlen):
+        self.rfile = rfile
+        self.maxlen = maxlen
+        self.bytes_read = 0
+    def _check_length(self):
+        if self.maxlen and self.bytes_read > self.maxlen:
+            raise MaxSizeExceeded()
+    def read(self, size = None):
+        data = self.rfile.read(size)
+        self.bytes_read += len(data)
+        self._check_length()
+        return data
+    def readline(self, size = None):
+        if size is not None:
+            data = self.rfile.readline(size)
+            self.bytes_read += len(data)
+            self._check_length()
+            return data
+
+        # User didn't specify a size ...
+        # We read the line in chunks to make sure it's not a 100MB line !
+        res = []
+        while True:
+            data = self.rfile.readline(256)
+            self.bytes_read += len(data)
+            self._check_length()
+            res.append(data)
+            if len(data) < 256:
+                return ''.join(data)
+    def close(self):
+        self.rfile.close()
+
 
 class HTTPRequest(object):
     def __init__(self, socket, addr, server):
         self.outheaders = None
         self.outheaderkeys = None
         self.rfile = self.socket.makefile("r", self.server.bufsize)
+        if self.server.config:
+            mhs = self.server.config.get(
+                'server.maxRequestHeaderSize',
+                500 * 1024) # 500KB by default
+            self.rfile = SizeCheckWrapper(self.rfile, mhs)
         self.wfile = self.socket.makefile("w", self.server.bufsize)
         self.sent_headers = False
     def parse_request(self):
             envname = "HTTP_" + k.upper().replace("-","_")
             self.environ[envname] = v
         self.ready = True
+
+        # Request header is parsed
+        # We prepare the SizeCheckWrapper for the request body
+        if self.server.config:
+            mbs = self.server.config.get(
+                'server.maxRequestBodySize',
+                100 * 1024 * 1024, # 100MB by default
+                path = path)
+            self.rfile.bytes_read = 0
+            self.rfile.maxlen = mbs
     
     def start_response(self, status, headers, exc_info = None):
         if self.started_response:
                         pass
                     else:
                         raise
+                except MaxSizeExceeded:
+                    str = "Request Entity Too Large"
+                    proto = request.environ.get("SERVER_PROTOCOL", "HTTP/1.0")
+                    request.wfile.write("%s 413 %s\r\n" % (proto, str))
+                    request.wfile.write("Content-Length: %s\r\n\r\n" % len(str))
+                    request.wfile.write(str)
+                    request.wfile.flush()
                 except:
                     traceback.print_exc()
             finally:
 class CherryPyWSGIServer(object):
     version = "CherryPy/2.1.0-beta"
     def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None,
-                 stderr=sys.stderr, bufsize=-1, max=-1):
+                 stderr=sys.stderr, bufsize=-1, max=-1,
+                 config = None):
         '''
         be careful w/ max
         '''
         self.wsgi_app = wsgi_app
         self.bind_addr = bind_addr
         self.numthreads = numthreads or 1
+        self.config = config
         if server_name:
             self.server_name = server_name
         else:

cherrypy/config.py

     'server.logFile': '',
     'server.reverseDNS': False,
     'server.threadPool': 0,
-
-    'server.maxRequestSize' : 0, # 0 == unlimited
     }
 
 def reset(useDefaults=True):
             autoreload.reloadFiles.append(file)
         _load(file, override)
 
-def get(key, defaultValue=None, returnSection=False):
+def get(key, defaultValue=None, returnSection=False, path = None):
     """Return the configuration value corresponding to key
     If specified, return defaultValue on lookup failure. If returnSection is
     specified, return the path to the value, instead of the value itself.
     """
     # Look, ma, no Python function calls! Uber-fast.
 
-    try:
-        path = cherrypy.request.path
-    except AttributeError:
-        # There's no request.path yet, so use the global settings.
-        path = "global"
-    
+    if path is None:
+        try:
+            path = cherrypy.request.path
+        except AttributeError:
+            # There's no request.path yet, so use the global settings.
+            path = "global"
+
     while True:
         if path == "":
             path = "/"

cherrypy/test/test_core.py

         # which CP will just pipe back out if we tell it to.
         return cherrypy.request.body
 
-
 class Cookies(Test):
     
     def single(self, name):
             cookie = cherrypy.request.simpleCookie[name]
             cherrypy.response.simpleCookie[name] = cookie.value
 
+class MaxRequestSize(Test):
+    
+    def index(self):
+        return "OK"
+
+    def upload(self, file):
+        return "Size: %s" % len(file.file.read())
 
 logFile = os.path.join(localDir, "error.log")
 logAccessFile = os.path.join(localDir, "access.log")
         self.assertHeader('Set-Cookie', 'First=Dinsdale;')
         self.assertHeader('Set-Cookie', 'Last=Piranha;')
 
+    def testMaxRequestSize(self):
+        self.getPage("/maxrequestsize/index")
+        self.assertBody("OK")
+        cherrypy.config.update({'server.maxRequestHeaderSize': 10})
+        self.getPage("/maxrequestsize/index")
+        self.assertStatus("413 Request Entity Too Large")
+        self.assertBody("Request Entity Too Large")
+        cherrypy.config.update({'server.maxRequestHeaderSize': 0})
+
+        # Test upload
+        h = [("Content-type", "multipart/form-data; boundary=x"),
+             ("Content-Length", "110")]
+        b = """--x
+Content-Disposition: form-data; name="file"; filename="hello.txt"
+Content-Type: text/plain
+
+hello
+--x--
+"""
+        self.getPage('/maxrequestsize/upload', h, "POST", b)
+        self.assertBody('Size: 5')
+        cherrypy.config.update({
+            '/maxrequestsize': {'server.maxRequestBodySize': 3}})
+        self.getPage('/maxrequestsize/upload', h, "POST", b)
+        self.assertStatus("413 Request Entity Too Large")
+        self.assertInBody("Request Entity Too Large")
 
 if __name__ == '__main__':
     helper.testmain()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.