Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

Changeset 1786

Show
Ignore:
Timestamp:
10/27/07 19:55:13
Author:
fumanchu
Message:

Fix for #622, #742, #736. The wsgiserver would respond without closing connection and without reading the full request. Fixed now.

Files:

Legend:

Unmodified
Added
Removed
Modified
Copied
Moved
  • trunk/cherrypy/_cprequest.py

    r1768 r1786  
    555555                        if self.method not in self.methods_with_bodies: 
    556556                            self.process_request_body = False 
    557                          
    558                         if self.process_request_body: 
    559                             # Prepare the SizeCheckWrapper for the req body 
    560                             mbs = getattr(cherrypy.server, 
    561                                           "max_request_body_size", 0) 
    562                             if mbs > 0: 
    563                                 self.rfile = http.SizeCheckWrapper(self.rfile, mbs) 
    564557                     
    565558                    self.hooks.run('before_request_body') 
     
    661654            # the HTTP server to have supplied a Content-Length header 
    662655            # which is valid for the decoded entity-body. 
    663             return 
     656            raise cherrypy.HTTPError(411) 
    664657         
    665658        # FieldStorage only recognizes POST, so fake it. 
  • trunk/cherrypy/_cpwsgi.py

    r1764 r1786  
    333333class CPHTTPRequest(wsgiserver.HTTPRequest): 
    334334     
    335     def parse_request(self): 
    336         mhs = _cherrypy.server.max_request_header_size 
    337         if mhs > 0: 
    338             self.rfile = _http.SizeCheckWrapper(self.rfile, mhs) 
    339          
    340         try: 
    341             wsgiserver.HTTPRequest.parse_request(self) 
    342         except _http.MaxSizeExceeded: 
    343             self.simple_response("413 Request Entity Too Large") 
    344             _cherrypy.log(traceback=True) 
    345      
    346     def decode_chunked(self): 
    347         """Decode the 'chunked' transfer coding.""" 
    348         if isinstance(self.rfile, _http.SizeCheckWrapper): 
    349             self.rfile = self.rfile.rfile 
    350         mbs = _cherrypy.server.max_request_body_size 
    351         if mbs > 0: 
    352             self.rfile = _http.SizeCheckWrapper(self.rfile, mbs) 
    353         try: 
    354             return wsgiserver.HTTPRequest.decode_chunked(self) 
    355         except _http.MaxSizeExceeded: 
    356             self.simple_response("413 Request Entity Too Large") 
    357             _cherrypy.log(traceback=True) 
    358             return False 
     335    def __init__(self, sendall, environ, wsgi_app): 
     336        s = _cherrypy.server 
     337        self.max_request_header_size = s.max_request_header_size or 0 
     338        self.max_request_body_size = s.max_request_body_size or 0 
     339        wsgiserver.HTTPRequest.__init__(self, sendall, environ, wsgi_app) 
    359340 
    360341 
     
    365346 
    366347class CPWSGIServer(wsgiserver.CherryPyWSGIServer): 
    367      
    368348    """Wrapper for wsgiserver.CherryPyWSGIServer. 
    369349     
     
    371351    so that it can be used in other frameworks and applications. Therefore, 
    372352    we wrap it here, so we can set our own mount points from cherrypy.tree. 
    373      
    374353    """ 
    375354     
  • trunk/cherrypy/lib/http.py

    r1687 r1786  
    377377 
    378378 
    379 class MaxSizeExceeded(Exception): 
    380     pass 
    381  
    382 class SizeCheckWrapper(object): 
    383     """Wraps a file-like object, raising MaxSizeExceeded if too large.""" 
    384      
    385     def __init__(self, rfile, maxlen): 
    386         self.rfile = rfile 
    387         self.maxlen = maxlen 
    388         self.bytes_read = 0 
    389      
    390     def _check_length(self): 
    391         if self.maxlen and self.bytes_read > self.maxlen: 
    392             raise MaxSizeExceeded() 
    393      
    394     def read(self, size = None): 
    395         data = self.rfile.read(size) 
    396         self.bytes_read += len(data) 
    397         self._check_length() 
    398         return data 
    399      
    400     def readline(self, size = None): 
    401         if size is not None: 
    402             data = self.rfile.readline(size) 
    403             self.bytes_read += len(data) 
    404             self._check_length() 
    405             return data 
    406          
    407         # User didn't specify a size ... 
    408         # We read the line in chunks to make sure it's not a 100MB line ! 
    409         res = [] 
    410         while True: 
    411             data = self.rfile.readline(256) 
    412             self.bytes_read += len(data) 
    413             self._check_length() 
    414             res.append(data) 
    415             # See http://www.cherrypy.org/ticket/421 
    416             if len(data) < 256 or data[-1:] == "\n": 
    417                 return ''.join(res) 
    418      
    419     def readlines(self, sizehint = 0): 
    420         # Shamelessly stolen from StringIO 
    421         total = 0 
    422         lines = [] 
    423         line = self.readline() 
    424         while line: 
    425             lines.append(line) 
    426             total += len(line) 
    427             if 0 < sizehint <= total: 
    428                 break 
    429             line = self.readline() 
    430         return lines 
    431      
    432     def close(self): 
    433         self.rfile.close() 
    434      
    435     def __iter__(self): 
    436         return self 
    437      
    438     def next(self): 
    439         data = self.rfile.next() 
    440         self.bytes_read += len(data) 
    441         self._check_length() 
    442         return data 
     379from cherrypy.wsgiserver import SizeCheckWrapper, MaxSizeExceeded 
    443380 
    444381 
  • trunk/cherrypy/test/test_conn.py

    r1565 r1786  
     1"""Tests for TCP connection handling, including proper and timely close.""" 
     2 
    13from cherrypy.test import test 
    24test.prefer_parent_path() 
     
    1618 
    1719def setup_server(): 
     20     
     21    def raise500(): 
     22        raise cherrypy.HTTPError(500) 
     23     
    1824    class Root: 
    1925         
     
    4147        stream._cp_config = {'response.stream': True} 
    4248         
     49        def error(self, code=500): 
     50            raise cherrypy.HTTPError(code) 
     51        error.exposed = True 
     52         
    4353        def upload(self): 
    4454            return ("thanks for '%s' (%s)" % 
     
    5161            return "Code = %s" % response_code 
    5262        custom.exposed = True 
     63         
     64        def err_before_read(self): 
     65            return "ok" 
     66        err_before_read.exposed = True 
     67        err_before_read._cp_config = {'hooks.on_start_resource': raise500} 
    5368     
    5469    cherrypy.tree.mount(Root()) 
    5570    cherrypy.config.update({ 
    56         'server.max_request_body_size': 100
     71        'server.max_request_body_size': 1001
    5772        'environment': 'test_suite', 
    5873        }) 
     
    345360        self.assertBody("thanks for 'I am a small file' (text/plain)") 
    346361     
     362    def test_readall_or_close(self): 
     363        if cherrypy.server.protocol_version != "HTTP/1.1": 
     364            self.PROTOCOL = "HTTP/1.0" 
     365        else: 
     366            self.PROTOCOL = "HTTP/1.1" 
     367         
     368        if self.scheme == "https": 
     369            self.HTTP_CONN = httplib.HTTPSConnection 
     370        else: 
     371            self.HTTP_CONN = httplib.HTTPConnection 
     372         
     373        self.persistent = True 
     374        conn = self.HTTP_CONN 
     375         
     376        # Get a POST page with an error 
     377        conn.putrequest("POST", "/err_before_read", skip_host=True) 
     378        conn.putheader("Host", self.HOST) 
     379        conn.putheader("Content-Type", "text/plain") 
     380        conn.putheader("Content-Length", "1000") 
     381        conn.putheader("Expect", "100-continue") 
     382        conn.endheaders() 
     383        response = conn.response_class(conn.sock, method="POST") 
     384         
     385        # ...assert and then skip the 100 response 
     386        version, status, reason = response._read_status() 
     387        self.assertEqual(status, 100) 
     388        while True: 
     389            skip = response.fp.readline().strip() 
     390            if not skip: 
     391                break 
     392         
     393        # ...send the body 
     394        conn.send("x" * 1000) 
     395         
     396        # ...get the final response 
     397        response.begin() 
     398        self.status, self.headers, self.body = webtest.shb(response) 
     399        self.assertStatus(500) 
     400         
     401        # Now try a working page with an Expect header... 
     402        conn._output('POST /upload HTTP/1.1') 
     403        conn._output("Host: %s" % self.HOST) 
     404        conn._output("Content-Type: text/plain") 
     405        conn._output("Content-Length: 17") 
     406        conn._output("Expect: 100-continue") 
     407        conn._send_output() 
     408        response = conn.response_class(conn.sock, method="POST") 
     409         
     410        # ...assert and then skip the 100 response 
     411        version, status, reason = response._read_status() 
     412        self.assertEqual(status, 100) 
     413        while True: 
     414            skip = response.fp.readline().strip() 
     415            if not skip: 
     416                break 
     417         
     418        # ...send the body 
     419        conn.send("I am a small file") 
     420         
     421        # ...get the final response 
     422        response.begin() 
     423        self.status, self.headers, self.body = webtest.shb(response) 
     424        self.assertStatus(200) 
     425        self.assertBody("thanks for 'I am a small file' (text/plain)") 
     426     
    347427    def test_No_Message_Body(self): 
    348428        if cherrypy.server.protocol_version != "HTTP/1.1": 
     
    413493        # Try a chunked request that exceeds server.max_request_body_size. 
    414494        # Note that the delimiters and trailer are included. 
    415         body = "5f\r\n" + ("x" * 95) + "\r\n0\r\n\r\n" 
     495        body = "3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n" 
    416496        conn.putrequest("POST", "/upload", skip_host=True) 
    417497        conn.putheader("Host", self.HOST) 
    418498        conn.putheader("Transfer-Encoding", "chunked") 
    419499        conn.putheader("Content-Type", "text/plain") 
     500        # Chunked requests don't need a content-length 
    420501##        conn.putheader("Content-Length", len(body)) 
    421502        conn.endheaders() 
  • trunk/cherrypy/test/test_core.py

    r1769 r1786  
    796796        self.assertBody('100-continue') 
    797797         
    798         self.getPage("/expect/expectation_failed", [('Content-Length', '200'), e]) 
     798        self.getPage("/expect/expectation_failed", [e]) 
    799799        self.assertStatus(417) 
    800800     
  • trunk/cherrypy/wsgiserver/__init__.py

    r1767 r1786  
    120120 
    121121 
     122class MaxSizeExceeded(Exception): 
     123    pass 
     124 
     125class SizeCheckWrapper(object): 
     126    """Wraps a file-like object, raising MaxSizeExceeded if too large.""" 
     127     
     128    def __init__(self, rfile, maxlen): 
     129        self.rfile = rfile 
     130        self.maxlen = maxlen 
     131        self.bytes_read = 0 
     132     
     133    def _check_length(self): 
     134        if self.maxlen and self.bytes_read > self.maxlen: 
     135            raise MaxSizeExceeded() 
     136     
     137    def read(self, size=None): 
     138        data = self.rfile.read(size) 
     139        self.bytes_read += len(data) 
     140        self._check_length() 
     141        return data 
     142     
     143    def readline(self, size=None): 
     144        if size is not None: 
     145            data = self.rfile.readline(size) 
     146            self.bytes_read += len(data) 
     147            self._check_length() 
     148            return data 
     149         
     150        # User didn't specify a size ... 
     151        # We read the line in chunks to make sure it's not a 100MB line ! 
     152        res = [] 
     153        while True: 
     154            data = self.rfile.readline(256) 
     155            self.bytes_read += len(data) 
     156            self._check_length() 
     157            res.append(data) 
     158            # See http://www.cherrypy.org/ticket/421 
     159            if len(data) < 256 or data[-1:] == "\n": 
     160                return ''.join(res) 
     161     
     162    def readlines(self, sizehint=0): 
     163        # Shamelessly stolen from StringIO 
     164        total = 0 
     165        lines = [] 
     166        line = self.readline() 
     167        while line: 
     168            lines.append(line) 
     169            total += len(line) 
     170            if 0 < sizehint <= total: 
     171                break 
     172            line = self.readline() 
     173        return lines 
     174     
     175    def close(self): 
     176        self.rfile.close() 
     177     
     178    def __iter__(self): 
     179        return self 
     180     
     181    def next(self): 
     182        data = self.rfile.next() 
     183        self.bytes_read += len(data) 
     184        self._check_length() 
     185        return data 
     186 
     187 
    122188class HTTPRequest(object): 
    123189    """An HTTP Request (and response). 
     
    155221    """ 
    156222     
     223    max_request_header_size = 0 
     224    max_request_body_size = 0 
     225     
    157226    def __init__(self, sendall, environ, wsgi_app): 
    158227        self.rfile = environ['wsgi.input'] 
     
    171240    def parse_request(self): 
    172241        """Parse the next HTTP request start-line and message-headers.""" 
     242        self.rfile.maxlen = self.max_request_header_size 
     243        self.rfile.bytes_read = 0 
     244         
     245        try: 
     246            self._parse_request() 
     247        except MaxSizeExceeded: 
     248            self.simple_response("413 Request Entity Too Large") 
     249            return 
     250     
     251    def _parse_request(self): 
    173252        # HTTP/1.1 connections are persistent by default. If a client 
    174253        # requests a page, then idles (leaves the connection open), 
     
    261340            return 
    262341         
     342        # Set AUTH_TYPE, REMOTE_USER 
    263343        creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1) 
    264344        environ["AUTH_TYPE"] = creds[0] 
     
    283363                te = [x.strip().lower() for x in te.split(",") if x.strip()] 
    284364         
    285         read_chunked = False 
     365        self.chunked_read = False 
    286366         
    287367        if te: 
    288368            for enc in te: 
    289369                if enc == "chunked": 
    290                     read_chunked = True 
     370                    self.chunked_read = True 
    291371                else: 
    292372                    # Note that, even if we see "chunked", we must reject 
     
    295375                    self.close_connection = True 
    296376                    return 
    297          
    298         if read_chunked: 
    299             if not self.decode_chunked(): 
    300                 return 
    301377         
    302378        # From PEP 333: 
     
    386462    def respond(self): 
    387463        """Call the appropriate WSGI app and write its iterable output.""" 
     464        # Set rfile.maxlen to ensure we don't read past Content-Length. 
     465        # This will also be used to read the entire request body if errors 
     466        # are raised before the app can read the body. 
     467        if self.chunked_read: 
     468            # If chunked, Content-Length will be 0. 
     469            self.rfile.maxlen = self.max_request_body_size 
     470        else: 
     471            cl = int(self.environ.get("CONTENT_LENGTH", 0)) 
     472            self.rfile.maxlen = min(cl, self.max_request_body_size) 
     473        self.rfile.bytes_read = 0 
     474         
     475        try: 
     476            self._respond() 
     477        except MaxSizeExceeded: 
     478            self.simple_response("413 Request Entity Too Large") 
     479            return 
     480     
     481    def _respond(self): 
     482        if self.chunked_read: 
     483            if not self.decode_chunked(): 
     484                self.close_connection = True 
     485                return 
     486         
    388487        response = self.wsgi_app(self.environ, self.start_response) 
    389488        try: 
     
    400499            if hasattr(response, "close"): 
    401500                response.close() 
     501         
    402502        if (self.ready and not self.sent_headers): 
    403503            self.sent_headers = True 
     
    488588                if not self.close_connection: 
    489589                    self.outheaders.append(("Connection", "Keep-Alive")) 
     590         
     591        if (not self.close_connection) and (not self.chunked_read): 
     592            # Read any remaining request body data on the socket. 
     593            # "If an origin server receives a request that does not include an 
     594            # Expect request-header field with the "100-continue" expectation, 
     595            # the request includes a request body, and the server responds 
     596            # with a final status code before reading the entire request body 
     597            # from the transport connection, then the server SHOULD NOT close 
     598            # the transport connection until it has read the entire request, 
     599            # or until the client closes the connection. Otherwise, the client 
     600            # might not reliably receive the response message. However, this 
     601            # requirement is not be construed as preventing a server from 
     602            # defending itself against denial-of-service attacks, or from 
     603            # badly broken client implementations." 
     604            size = self.rfile.maxlen - self.rfile.bytes_read 
     605            if size > 0: 
     606                self.rfile.read(size) 
    490607         
    491608        if "date" not in hkeys: 
     
    613730            self.sendall = sock.sendall 
    614731         
    615         self.environ["wsgi.input"] = self.rfile 
     732        # Wrap wsgi.input but not HTTPConnection.rfile itself. 
     733        # We're also not setting maxlen yet; we'll do that separately 
     734        # for headers and body for each iteration of self.communicate 
     735        # (if maxlen is 0 the wrapper doesn't check length). 
     736        self.environ["wsgi.input"] = SizeCheckWrapper(self.rfile, 0) 
    616737     
    617738    def communicate(self): 
     
    625746                req = self.RequestHandlerClass(self.sendall, self.environ, 
    626747                                               self.wsgi_app) 
     748                 
    627749                # This order of operations should guarantee correct pipelining. 
    628750                req.parse_request() 
    629751                if not req.ready: 
    630752                    return 
     753                 
    631754                req.respond() 
    632755                if req.close_connection: 
    633756                    return 
     757         
    634758        except socket.error, e: 
    635759            errnum = e.args[0] 

Hosted by WebFaction

Log in as guest/cpguest to create tickets