Changeset 1786
- Timestamp:
- 10/27/07 19:55:13
- Files:
-
- trunk/cherrypy/_cprequest.py (modified) (2 diffs)
- trunk/cherrypy/_cpwsgi.py (modified) (3 diffs)
- trunk/cherrypy/lib/http.py (modified) (1 diff)
- trunk/cherrypy/test/test_conn.py (modified) (6 diffs)
- trunk/cherrypy/test/test_core.py (modified) (1 diff)
- trunk/cherrypy/wsgiserver/__init__.py (modified) (11 diffs)
Legend:
- Unmodified
- Added
- Removed
- Modified
- Copied
- Moved
trunk/cherrypy/_cprequest.py
r1768 r1786 555 555 if self.method not in self.methods_with_bodies: 556 556 self.process_request_body = False 557 558 if self.process_request_body:559 # Prepare the SizeCheckWrapper for the req body560 mbs = getattr(cherrypy.server,561 "max_request_body_size", 0)562 if mbs > 0:563 self.rfile = http.SizeCheckWrapper(self.rfile, mbs)564 557 565 558 self.hooks.run('before_request_body') … … 661 654 # the HTTP server to have supplied a Content-Length header 662 655 # which is valid for the decoded entity-body. 663 r eturn656 raise cherrypy.HTTPError(411) 664 657 665 658 # FieldStorage only recognizes POST, so fake it. trunk/cherrypy/_cpwsgi.py
r1764 r1786 333 333 class CPHTTPRequest(wsgiserver.HTTPRequest): 334 334 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) 359 340 360 341 … … 365 346 366 347 class CPWSGIServer(wsgiserver.CherryPyWSGIServer): 367 368 348 """Wrapper for wsgiserver.CherryPyWSGIServer. 369 349 … … 371 351 so that it can be used in other frameworks and applications. Therefore, 372 352 we wrap it here, so we can set our own mount points from cherrypy.tree. 373 374 353 """ 375 354 trunk/cherrypy/lib/http.py
r1687 r1786 377 377 378 378 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 379 from cherrypy.wsgiserver import SizeCheckWrapper, MaxSizeExceeded 443 380 444 381 trunk/cherrypy/test/test_conn.py
r1565 r1786 1 """Tests for TCP connection handling, including proper and timely close.""" 2 1 3 from cherrypy.test import test 2 4 test.prefer_parent_path() … … 16 18 17 19 def setup_server(): 20 21 def raise500(): 22 raise cherrypy.HTTPError(500) 23 18 24 class Root: 19 25 … … 41 47 stream._cp_config = {'response.stream': True} 42 48 49 def error(self, code=500): 50 raise cherrypy.HTTPError(code) 51 error.exposed = True 52 43 53 def upload(self): 44 54 return ("thanks for '%s' (%s)" % … … 51 61 return "Code = %s" % response_code 52 62 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} 53 68 54 69 cherrypy.tree.mount(Root()) 55 70 cherrypy.config.update({ 56 'server.max_request_body_size': 100 ,71 'server.max_request_body_size': 1001, 57 72 'environment': 'test_suite', 58 73 }) … … 345 360 self.assertBody("thanks for 'I am a small file' (text/plain)") 346 361 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 347 427 def test_No_Message_Body(self): 348 428 if cherrypy.server.protocol_version != "HTTP/1.1": … … 413 493 # Try a chunked request that exceeds server.max_request_body_size. 414 494 # 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" 416 496 conn.putrequest("POST", "/upload", skip_host=True) 417 497 conn.putheader("Host", self.HOST) 418 498 conn.putheader("Transfer-Encoding", "chunked") 419 499 conn.putheader("Content-Type", "text/plain") 500 # Chunked requests don't need a content-length 420 501 ## conn.putheader("Content-Length", len(body)) 421 502 conn.endheaders() trunk/cherrypy/test/test_core.py
r1769 r1786 796 796 self.assertBody('100-continue') 797 797 798 self.getPage("/expect/expectation_failed", [ ('Content-Length', '200'),e])798 self.getPage("/expect/expectation_failed", [e]) 799 799 self.assertStatus(417) 800 800 trunk/cherrypy/wsgiserver/__init__.py
r1767 r1786 120 120 121 121 122 class MaxSizeExceeded(Exception): 123 pass 124 125 class 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 122 188 class HTTPRequest(object): 123 189 """An HTTP Request (and response). … … 155 221 """ 156 222 223 max_request_header_size = 0 224 max_request_body_size = 0 225 157 226 def __init__(self, sendall, environ, wsgi_app): 158 227 self.rfile = environ['wsgi.input'] … … 171 240 def parse_request(self): 172 241 """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): 173 252 # HTTP/1.1 connections are persistent by default. If a client 174 253 # requests a page, then idles (leaves the connection open), … … 261 340 return 262 341 342 # Set AUTH_TYPE, REMOTE_USER 263 343 creds = environ.get("HTTP_AUTHORIZATION", "").split(" ", 1) 264 344 environ["AUTH_TYPE"] = creds[0] … … 283 363 te = [x.strip().lower() for x in te.split(",") if x.strip()] 284 364 285 read_chunked = False365 self.chunked_read = False 286 366 287 367 if te: 288 368 for enc in te: 289 369 if enc == "chunked": 290 read_chunked = True370 self.chunked_read = True 291 371 else: 292 372 # Note that, even if we see "chunked", we must reject … … 295 375 self.close_connection = True 296 376 return 297 298 if read_chunked:299 if not self.decode_chunked():300 return301 377 302 378 # From PEP 333: … … 386 462 def respond(self): 387 463 """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 388 487 response = self.wsgi_app(self.environ, self.start_response) 389 488 try: … … 400 499 if hasattr(response, "close"): 401 500 response.close() 501 402 502 if (self.ready and not self.sent_headers): 403 503 self.sent_headers = True … … 488 588 if not self.close_connection: 489 589 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) 490 607 491 608 if "date" not in hkeys: … … 613 730 self.sendall = sock.sendall 614 731 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) 616 737 617 738 def communicate(self): … … 625 746 req = self.RequestHandlerClass(self.sendall, self.environ, 626 747 self.wsgi_app) 748 627 749 # This order of operations should guarantee correct pipelining. 628 750 req.parse_request() 629 751 if not req.ready: 630 752 return 753 631 754 req.respond() 632 755 if req.close_connection: 633 756 return 757 634 758 except socket.error, e: 635 759 errnum = e.args[0]

