1 """Tests for TCP connection handling, including proper and timely close."""
10 from cherrypy
._cpcompat
import HTTPConnection
, HTTPSConnection
, NotConnected
, BadStatusLine
11 from cherrypy
._cpcompat
import ntob
, urlopen
, unicodestr
12 from cherrypy
.test
import webtest
13 from cherrypy
import _cperror
16 pov
= 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
21 raise cherrypy
.HTTPError(500)
33 return "Hello, world!"
37 return str(cherrypy
.server
.httpserver
.timeout
)
38 timeout
.exposed
= True
40 def stream(self
, set_cl
=False):
42 cherrypy
.response
.headers
['Content-Length'] = 10
50 stream
._cp
_config
= {'response.stream': True}
52 def error(self
, code
=500):
53 raise cherrypy
.HTTPError(code
)
57 if not cherrypy
.request
.method
== 'POST':
58 raise AssertionError("'POST' != request.method %r" %
59 cherrypy
.request
.method
)
60 return "thanks for '%s'" % cherrypy
.request
.body
.read()
63 def custom(self
, response_code
):
64 cherrypy
.response
.status
= response_code
65 return "Code = %s" % response_code
68 def err_before_read(self
):
70 err_before_read
.exposed
= True
71 err_before_read
._cp
_config
= {'hooks.on_start_resource': raise500
}
73 def one_megabyte_of_a(self
):
74 return ["a" * 1024] * 1024
75 one_megabyte_of_a
.exposed
= True
77 def custom_cl(self
, body
, cl
):
78 cherrypy
.response
.headers
['Content-Length'] = cl
79 if not isinstance(body
, list):
83 if isinstance(chunk
, unicodestr
):
84 chunk
= chunk
.encode('ISO-8859-1')
87 custom_cl
.exposed
= True
88 # Turn off the encoding tool so it doens't collapse
89 # our response body and reclaculate the Content-Length.
90 custom_cl
._cp
_config
= {'tools.encode.on': False}
92 cherrypy
.tree
.mount(Root())
93 cherrypy
.config
.update({
94 'server.max_request_body_size': 1001,
95 'server.socket_timeout': timeout
,
99 from cherrypy
.test
import helper
101 class ConnectionCloseTests(helper
.CPWebCase
):
102 setup_server
= staticmethod(setup_server
)
104 def test_HTTP11(self
):
105 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
108 self
.PROTOCOL
= "HTTP/1.1"
110 self
.persistent
= True
112 # Make the first request and assert there's no "Connection: close".
114 self
.assertStatus('200 OK')
116 self
.assertNoHeader("Connection")
118 # Make another request on the same connection.
119 self
.getPage("/page1")
120 self
.assertStatus('200 OK')
122 self
.assertNoHeader("Connection")
124 # Test client-side close.
125 self
.getPage("/page2", headers
=[("Connection", "close")])
126 self
.assertStatus('200 OK')
128 self
.assertHeader("Connection", "close")
130 # Make another request on the same connection, which should error.
131 self
.assertRaises(NotConnected
, self
.getPage
, "/")
133 def test_Streaming_no_len(self
):
134 self
._streaming
(set_cl
=False)
136 def test_Streaming_with_len(self
):
137 self
._streaming
(set_cl
=True)
139 def _streaming(self
, set_cl
):
140 if cherrypy
.server
.protocol_version
== "HTTP/1.1":
141 self
.PROTOCOL
= "HTTP/1.1"
143 self
.persistent
= True
145 # Make the first request and assert there's no "Connection: close".
147 self
.assertStatus('200 OK')
149 self
.assertNoHeader("Connection")
151 # Make another, streamed request on the same connection.
153 # When a Content-Length is provided, the content should stream
154 # without closing the connection.
155 self
.getPage("/stream?set_cl=Yes")
156 self
.assertHeader("Content-Length")
157 self
.assertNoHeader("Connection", "close")
158 self
.assertNoHeader("Transfer-Encoding")
160 self
.assertStatus('200 OK')
161 self
.assertBody('0123456789')
163 # When no Content-Length response header is provided,
164 # streamed output will either close the connection, or use
165 # chunked encoding, to determine transfer-length.
166 self
.getPage("/stream")
167 self
.assertNoHeader("Content-Length")
168 self
.assertStatus('200 OK')
169 self
.assertBody('0123456789')
171 chunked_response
= False
172 for k
, v
in self
.headers
:
173 if k
.lower() == "transfer-encoding":
174 if str(v
) == "chunked":
175 chunked_response
= True
178 self
.assertNoHeader("Connection", "close")
180 self
.assertHeader("Connection", "close")
182 # Make another request on the same connection, which should error.
183 self
.assertRaises(NotConnected
, self
.getPage
, "/")
185 # Try HEAD. See http://www.cherrypy.org/ticket/864.
186 self
.getPage("/stream", method
='HEAD')
187 self
.assertStatus('200 OK')
189 self
.assertNoHeader("Transfer-Encoding")
191 self
.PROTOCOL
= "HTTP/1.0"
193 self
.persistent
= True
195 # Make the first request and assert Keep-Alive.
196 self
.getPage("/", headers
=[("Connection", "Keep-Alive")])
197 self
.assertStatus('200 OK')
199 self
.assertHeader("Connection", "Keep-Alive")
201 # Make another, streamed request on the same connection.
203 # When a Content-Length is provided, the content should
204 # stream without closing the connection.
205 self
.getPage("/stream?set_cl=Yes",
206 headers
=[("Connection", "Keep-Alive")])
207 self
.assertHeader("Content-Length")
208 self
.assertHeader("Connection", "Keep-Alive")
209 self
.assertNoHeader("Transfer-Encoding")
210 self
.assertStatus('200 OK')
211 self
.assertBody('0123456789')
213 # When a Content-Length is not provided,
214 # the server should close the connection.
215 self
.getPage("/stream", headers
=[("Connection", "Keep-Alive")])
216 self
.assertStatus('200 OK')
217 self
.assertBody('0123456789')
219 self
.assertNoHeader("Content-Length")
220 self
.assertNoHeader("Connection", "Keep-Alive")
221 self
.assertNoHeader("Transfer-Encoding")
223 # Make another request on the same connection, which should error.
224 self
.assertRaises(NotConnected
, self
.getPage
, "/")
226 def test_HTTP10_KeepAlive(self
):
227 self
.PROTOCOL
= "HTTP/1.0"
228 if self
.scheme
== "https":
229 self
.HTTP_CONN
= HTTPSConnection
231 self
.HTTP_CONN
= HTTPConnection
233 # Test a normal HTTP/1.0 request.
234 self
.getPage("/page2")
235 self
.assertStatus('200 OK')
237 # Apache, for example, may emit a Connection header even for HTTP/1.0
238 ## self.assertNoHeader("Connection")
240 # Test a keep-alive HTTP/1.0 request.
241 self
.persistent
= True
243 self
.getPage("/page3", headers
=[("Connection", "Keep-Alive")])
244 self
.assertStatus('200 OK')
246 self
.assertHeader("Connection", "Keep-Alive")
248 # Remove the keep-alive header again.
249 self
.getPage("/page3")
250 self
.assertStatus('200 OK')
252 # Apache, for example, may emit a Connection header even for HTTP/1.0
253 ## self.assertNoHeader("Connection")
256 class PipelineTests(helper
.CPWebCase
):
257 setup_server
= staticmethod(setup_server
)
259 def test_HTTP11_Timeout(self
):
260 # If we timeout without sending any data,
261 # the server will close the conn with a 408.
262 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
265 self
.PROTOCOL
= "HTTP/1.1"
267 # Connect but send nothing.
268 self
.persistent
= True
269 conn
= self
.HTTP_CONN
270 conn
.auto_open
= False
273 # Wait for our socket timeout
274 time
.sleep(timeout
* 2)
276 # The request should have returned 408 already.
277 response
= conn
.response_class(conn
.sock
, method
="GET")
279 self
.assertEqual(response
.status
, 408)
282 # Connect but send half the headers only.
283 self
.persistent
= True
284 conn
= self
.HTTP_CONN
285 conn
.auto_open
= False
287 conn
.send(ntob('GET /hello HTTP/1.1'))
288 conn
.send(("Host: %s" % self
.HOST
).encode('ascii'))
290 # Wait for our socket timeout
291 time
.sleep(timeout
* 2)
293 # The conn should have already sent 408.
294 response
= conn
.response_class(conn
.sock
, method
="GET")
296 self
.assertEqual(response
.status
, 408)
299 def test_HTTP11_Timeout_after_request(self
):
300 # If we timeout after at least one request has succeeded,
301 # the server will close the conn without 408.
302 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
305 self
.PROTOCOL
= "HTTP/1.1"
307 # Make an initial request
308 self
.persistent
= True
309 conn
= self
.HTTP_CONN
310 conn
.putrequest("GET", "/timeout?t=%s" % timeout
, skip_host
=True)
311 conn
.putheader("Host", self
.HOST
)
313 response
= conn
.response_class(conn
.sock
, method
="GET")
315 self
.assertEqual(response
.status
, 200)
316 self
.body
= response
.read()
317 self
.assertBody(str(timeout
))
319 # Make a second request on the same socket
320 conn
._output
(ntob('GET /hello HTTP/1.1'))
321 conn
._output
(ntob("Host: %s" % self
.HOST
, 'ascii'))
323 response
= conn
.response_class(conn
.sock
, method
="GET")
325 self
.assertEqual(response
.status
, 200)
326 self
.body
= response
.read()
327 self
.assertBody("Hello, world!")
329 # Wait for our socket timeout
330 time
.sleep(timeout
* 2)
332 # Make another request on the same socket, which should error
333 conn
._output
(ntob('GET /hello HTTP/1.1'))
334 conn
._output
(ntob("Host: %s" % self
.HOST
, 'ascii'))
336 response
= conn
.response_class(conn
.sock
, method
="GET")
340 if not isinstance(sys
.exc_info()[1],
341 (socket
.error
, BadStatusLine
)):
342 self
.fail("Writing to timed out socket didn't fail"
343 " as it should have: %s" % sys
.exc_info()[1])
345 if response
.status
!= 408:
346 self
.fail("Writing to timed out socket didn't fail"
347 " as it should have: %s" %
352 # Make another request on a new socket, which should work
353 self
.persistent
= True
354 conn
= self
.HTTP_CONN
355 conn
.putrequest("GET", "/", skip_host
=True)
356 conn
.putheader("Host", self
.HOST
)
358 response
= conn
.response_class(conn
.sock
, method
="GET")
360 self
.assertEqual(response
.status
, 200)
361 self
.body
= response
.read()
365 # Make another request on the same socket,
366 # but timeout on the headers
367 conn
.send(ntob('GET /hello HTTP/1.1'))
368 # Wait for our socket timeout
369 time
.sleep(timeout
* 2)
370 response
= conn
.response_class(conn
.sock
, method
="GET")
374 if not isinstance(sys
.exc_info()[1],
375 (socket
.error
, BadStatusLine
)):
376 self
.fail("Writing to timed out socket didn't fail"
377 " as it should have: %s" % sys
.exc_info()[1])
379 self
.fail("Writing to timed out socket didn't fail"
380 " as it should have: %s" %
385 # Retry the request on a new connection, which should work
386 self
.persistent
= True
387 conn
= self
.HTTP_CONN
388 conn
.putrequest("GET", "/", skip_host
=True)
389 conn
.putheader("Host", self
.HOST
)
391 response
= conn
.response_class(conn
.sock
, method
="GET")
393 self
.assertEqual(response
.status
, 200)
394 self
.body
= response
.read()
398 def test_HTTP11_pipelining(self
):
399 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
402 self
.PROTOCOL
= "HTTP/1.1"
404 # Test pipelining. httplib doesn't support this directly.
405 self
.persistent
= True
406 conn
= self
.HTTP_CONN
409 conn
.putrequest("GET", "/hello", skip_host
=True)
410 conn
.putheader("Host", self
.HOST
)
413 for trial
in range(5):
415 conn
._output
(ntob('GET /hello HTTP/1.1'))
416 conn
._output
(ntob("Host: %s" % self
.HOST
, 'ascii'))
419 # Retrieve previous response
420 response
= conn
.response_class(conn
.sock
, method
="GET")
422 body
= response
.read(13)
423 self
.assertEqual(response
.status
, 200)
424 self
.assertEqual(body
, ntob("Hello, world!"))
426 # Retrieve final response
427 response
= conn
.response_class(conn
.sock
, method
="GET")
429 body
= response
.read()
430 self
.assertEqual(response
.status
, 200)
431 self
.assertEqual(body
, ntob("Hello, world!"))
435 def test_100_Continue(self
):
436 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
439 self
.PROTOCOL
= "HTTP/1.1"
441 self
.persistent
= True
442 conn
= self
.HTTP_CONN
444 # Try a page without an Expect request header first.
445 # Note that httplib's response.begin automatically ignores
446 # 100 Continue responses, so we must manually check for it.
447 conn
.putrequest("POST", "/upload", skip_host
=True)
448 conn
.putheader("Host", self
.HOST
)
449 conn
.putheader("Content-Type", "text/plain")
450 conn
.putheader("Content-Length", "4")
452 conn
.send(ntob("d'oh"))
453 response
= conn
.response_class(conn
.sock
, method
="POST")
454 version
, status
, reason
= response
._read
_status
()
455 self
.assertNotEqual(status
, 100)
458 # Now try a page with an Expect header...
460 conn
.putrequest("POST", "/upload", skip_host
=True)
461 conn
.putheader("Host", self
.HOST
)
462 conn
.putheader("Content-Type", "text/plain")
463 conn
.putheader("Content-Length", "17")
464 conn
.putheader("Expect", "100-continue")
466 response
= conn
.response_class(conn
.sock
, method
="POST")
468 # ...assert and then skip the 100 response
469 version
, status
, reason
= response
._read
_status
()
470 self
.assertEqual(status
, 100)
472 line
= response
.fp
.readline().strip()
474 self
.fail("100 Continue should not output any headers. Got %r" % line
)
479 body
= ntob("I am a small file")
482 # ...get the final response
484 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
485 self
.assertStatus(200)
486 self
.assertBody("thanks for '%s'" % body
)
490 class ConnectionTests(helper
.CPWebCase
):
491 setup_server
= staticmethod(setup_server
)
493 def test_readall_or_close(self
):
494 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
497 self
.PROTOCOL
= "HTTP/1.1"
499 if self
.scheme
== "https":
500 self
.HTTP_CONN
= HTTPSConnection
502 self
.HTTP_CONN
= HTTPConnection
504 # Test a max of 0 (the default) and then reset to what it was above.
505 old_max
= cherrypy
.server
.max_request_body_size
506 for new_max
in (0, old_max
):
507 cherrypy
.server
.max_request_body_size
= new_max
509 self
.persistent
= True
510 conn
= self
.HTTP_CONN
512 # Get a POST page with an error
513 conn
.putrequest("POST", "/err_before_read", skip_host
=True)
514 conn
.putheader("Host", self
.HOST
)
515 conn
.putheader("Content-Type", "text/plain")
516 conn
.putheader("Content-Length", "1000")
517 conn
.putheader("Expect", "100-continue")
519 response
= conn
.response_class(conn
.sock
, method
="POST")
521 # ...assert and then skip the 100 response
522 version
, status
, reason
= response
._read
_status
()
523 self
.assertEqual(status
, 100)
525 skip
= response
.fp
.readline().strip()
530 conn
.send(ntob("x" * 1000))
532 # ...get the final response
534 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
535 self
.assertStatus(500)
537 # Now try a working page with an Expect header...
538 conn
._output
(ntob('POST /upload HTTP/1.1'))
539 conn
._output
(ntob("Host: %s" % self
.HOST
, 'ascii'))
540 conn
._output
(ntob("Content-Type: text/plain"))
541 conn
._output
(ntob("Content-Length: 17"))
542 conn
._output
(ntob("Expect: 100-continue"))
544 response
= conn
.response_class(conn
.sock
, method
="POST")
546 # ...assert and then skip the 100 response
547 version
, status
, reason
= response
._read
_status
()
548 self
.assertEqual(status
, 100)
550 skip
= response
.fp
.readline().strip()
555 body
= ntob("I am a small file")
558 # ...get the final response
560 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
561 self
.assertStatus(200)
562 self
.assertBody("thanks for '%s'" % body
)
565 def test_No_Message_Body(self
):
566 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
569 self
.PROTOCOL
= "HTTP/1.1"
571 # Set our HTTP_CONN to an instance so it persists between requests.
572 self
.persistent
= True
574 # Make the first request and assert there's no "Connection: close".
576 self
.assertStatus('200 OK')
578 self
.assertNoHeader("Connection")
580 # Make a 204 request on the same connection.
581 self
.getPage("/custom/204")
582 self
.assertStatus(204)
583 self
.assertNoHeader("Content-Length")
585 self
.assertNoHeader("Connection")
587 # Make a 304 request on the same connection.
588 self
.getPage("/custom/304")
589 self
.assertStatus(304)
590 self
.assertNoHeader("Content-Length")
592 self
.assertNoHeader("Connection")
594 def test_Chunked_Encoding(self
):
595 if cherrypy
.server
.protocol_version
!= "HTTP/1.1":
598 if (hasattr(self
, 'harness') and
599 "modpython" in self
.harness
.__class
__.__name
__.lower()):
600 # mod_python forbids chunked encoding
603 self
.PROTOCOL
= "HTTP/1.1"
605 # Set our HTTP_CONN to an instance so it persists between requests.
606 self
.persistent
= True
607 conn
= self
.HTTP_CONN
609 # Try a normal chunked request (with extensions)
610 body
= ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n"
611 "Content-Type: application/json\r\n"
613 conn
.putrequest("POST", "/upload", skip_host
=True)
614 conn
.putheader("Host", self
.HOST
)
615 conn
.putheader("Transfer-Encoding", "chunked")
616 conn
.putheader("Trailer", "Content-Type")
617 # Note that this is somewhat malformed:
618 # we shouldn't be sending Content-Length.
619 # RFC 2616 says the server should ignore it.
620 conn
.putheader("Content-Length", "3")
623 response
= conn
.getresponse()
624 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
625 self
.assertStatus('200 OK')
626 self
.assertBody("thanks for '%s'" % ntob('xx\r\nxxxxyyyyy'))
628 # Try a chunked request that exceeds server.max_request_body_size.
629 # Note that the delimiters and trailer are included.
630 body
= ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n")
631 conn
.putrequest("POST", "/upload", skip_host
=True)
632 conn
.putheader("Host", self
.HOST
)
633 conn
.putheader("Transfer-Encoding", "chunked")
634 conn
.putheader("Content-Type", "text/plain")
635 # Chunked requests don't need a content-length
636 ## conn.putheader("Content-Length", len(body))
639 response
= conn
.getresponse()
640 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
641 self
.assertStatus(413)
644 def test_Content_Length_in(self
):
645 # Try a non-chunked request where Content-Length exceeds
646 # server.max_request_body_size. Assert error before body send.
647 self
.persistent
= True
648 conn
= self
.HTTP_CONN
649 conn
.putrequest("POST", "/upload", skip_host
=True)
650 conn
.putheader("Host", self
.HOST
)
651 conn
.putheader("Content-Type", "text/plain")
652 conn
.putheader("Content-Length", "9999")
654 response
= conn
.getresponse()
655 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
656 self
.assertStatus(413)
657 self
.assertBody("The entity sent with the request exceeds "
658 "the maximum allowed bytes.")
661 def test_Content_Length_out_preheaders(self
):
662 # Try a non-chunked response where Content-Length is less than
663 # the actual bytes in the response body.
664 self
.persistent
= True
665 conn
= self
.HTTP_CONN
666 conn
.putrequest("GET", "/custom_cl?body=I+have+too+many+bytes&cl=5",
668 conn
.putheader("Host", self
.HOST
)
670 response
= conn
.getresponse()
671 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
672 self
.assertStatus(500)
674 "The requested resource returned more bytes than the "
675 "declared Content-Length.")
678 def test_Content_Length_out_postheaders(self
):
679 # Try a non-chunked response where Content-Length is less than
680 # the actual bytes in the response body.
681 self
.persistent
= True
682 conn
= self
.HTTP_CONN
683 conn
.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5",
685 conn
.putheader("Host", self
.HOST
)
687 response
= conn
.getresponse()
688 self
.status
, self
.headers
, self
.body
= webtest
.shb(response
)
689 self
.assertStatus(200)
690 self
.assertBody("I too")
694 remote_data_conn
= urlopen('%s://%s:%s/one_megabyte_of_a/' %
695 (self
.scheme
, self
.HOST
, self
.PORT
,))
696 buf
= remote_data_conn
.read(512)
697 time
.sleep(timeout
* 0.6)
698 remaining
= (1024 * 1024) - 512
700 data
= remote_data_conn
.read(remaining
)
705 remaining
-= len(data
)
707 self
.assertEqual(len(buf
), 1024 * 1024)
708 self
.assertEqual(buf
, ntob("a" * 1024 * 1024))
709 self
.assertEqual(remaining
, 0)
710 remote_data_conn
.close()
713 class BadRequestTests(helper
.CPWebCase
):
714 setup_server
= staticmethod(setup_server
)
716 def test_No_CRLF(self
):
717 self
.persistent
= True
719 conn
= self
.HTTP_CONN
720 conn
.send(ntob('GET /hello HTTP/1.1\n\n'))
721 response
= conn
.response_class(conn
.sock
, method
="GET")
723 self
.body
= response
.read()
724 self
.assertBody("HTTP requires CRLF terminators")
728 conn
.send(ntob('GET /hello HTTP/1.1\r\n\n'))
729 response
= conn
.response_class(conn
.sock
, method
="GET")
731 self
.body
= response
.read()
732 self
.assertBody("HTTP requires CRLF terminators")