1
"""Tests for TCP connection handling, including proper and timely close."""
3
from cherrypy.test import test
4
test.prefer_parent_path()
6
from httplib import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine
15
from cherrypy.test import webtest
16
from cherrypy import _cperror
19
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
24
raise cherrypy.HTTPError(500)
36
return "Hello, world!"
40
return str(cherrypy.server.httpserver.timeout)
41
timeout.exposed = True
43
def stream(self, set_cl=False):
45
cherrypy.response.headers['Content-Length'] = 10
53
stream._cp_config = {'response.stream': True}
55
def error(self, code=500):
56
raise cherrypy.HTTPError(code)
60
if not cherrypy.request.method == 'POST':
61
raise AssertionError("'POST' != request.method %r" %
62
cherrypy.request.method)
63
return "thanks for '%s'" % cherrypy.request.body.read()
66
def custom(self, response_code):
67
cherrypy.response.status = response_code
68
return "Code = %s" % response_code
71
def err_before_read(self):
73
err_before_read.exposed = True
74
err_before_read._cp_config = {'hooks.on_start_resource': raise500}
76
def one_megabyte_of_a(self):
77
return ["a" * 1024] * 1024
78
one_megabyte_of_a.exposed = True
80
cherrypy.tree.mount(Root())
81
cherrypy.config.update({
82
'server.max_request_body_size': 1001,
83
'server.socket_timeout': timeout,
87
from cherrypy.test import helper
89
class ConnectionCloseTests(helper.CPWebCase):
91
def test_HTTP11(self):
92
if cherrypy.server.protocol_version != "HTTP/1.1":
95
self.PROTOCOL = "HTTP/1.1"
97
self.persistent = True
99
# Make the first request and assert there's no "Connection: close".
101
self.assertStatus('200 OK')
103
self.assertNoHeader("Connection")
105
# Make another request on the same connection.
106
self.getPage("/page1")
107
self.assertStatus('200 OK')
109
self.assertNoHeader("Connection")
111
# Test client-side close.
112
self.getPage("/page2", headers=[("Connection", "close")])
113
self.assertStatus('200 OK')
115
self.assertHeader("Connection", "close")
117
# Make another request on the same connection, which should error.
118
self.assertRaises(NotConnected, self.getPage, "/")
120
def test_Streaming_no_len(self):
121
self._streaming(set_cl=False)
123
def test_Streaming_with_len(self):
124
self._streaming(set_cl=True)
126
def _streaming(self, set_cl):
127
if cherrypy.server.protocol_version == "HTTP/1.1":
128
self.PROTOCOL = "HTTP/1.1"
130
self.persistent = True
132
# Make the first request and assert there's no "Connection: close".
134
self.assertStatus('200 OK')
136
self.assertNoHeader("Connection")
138
# Make another, streamed request on the same connection.
140
# When a Content-Length is provided, the content should stream
141
# without closing the connection.
142
self.getPage("/stream?set_cl=Yes")
143
self.assertHeader("Content-Length")
144
self.assertNoHeader("Connection", "close")
145
self.assertNoHeader("Transfer-Encoding")
147
self.assertStatus('200 OK')
148
self.assertBody('0123456789')
150
# When no Content-Length response header is provided,
151
# streamed output will either close the connection, or use
152
# chunked encoding, to determine transfer-length.
153
self.getPage("/stream")
154
self.assertNoHeader("Content-Length")
155
self.assertStatus('200 OK')
156
self.assertBody('0123456789')
158
chunked_response = False
159
for k, v in self.headers:
160
if k.lower() == "transfer-encoding":
161
if str(v) == "chunked":
162
chunked_response = True
165
self.assertNoHeader("Connection", "close")
167
self.assertHeader("Connection", "close")
169
# Make another request on the same connection, which should error.
170
self.assertRaises(NotConnected, self.getPage, "/")
172
# Try HEAD. See http://www.cherrypy.org/ticket/864.
173
self.getPage("/stream", method='HEAD')
174
self.assertStatus('200 OK')
176
self.assertNoHeader("Transfer-Encoding")
178
self.PROTOCOL = "HTTP/1.0"
180
self.persistent = True
182
# Make the first request and assert Keep-Alive.
183
self.getPage("/", headers=[("Connection", "Keep-Alive")])
184
self.assertStatus('200 OK')
186
self.assertHeader("Connection", "Keep-Alive")
188
# Make another, streamed request on the same connection.
190
# When a Content-Length is provided, the content should
191
# stream without closing the connection.
192
self.getPage("/stream?set_cl=Yes",
193
headers=[("Connection", "Keep-Alive")])
194
self.assertHeader("Content-Length")
195
self.assertHeader("Connection", "Keep-Alive")
196
self.assertNoHeader("Transfer-Encoding")
197
self.assertStatus('200 OK')
198
self.assertBody('0123456789')
200
# When a Content-Length is not provided,
201
# the server should close the connection.
202
self.getPage("/stream", headers=[("Connection", "Keep-Alive")])
203
self.assertStatus('200 OK')
204
self.assertBody('0123456789')
206
self.assertNoHeader("Content-Length")
207
self.assertNoHeader("Connection", "Keep-Alive")
208
self.assertNoHeader("Transfer-Encoding")
210
# Make another request on the same connection, which should error.
211
self.assertRaises(NotConnected, self.getPage, "/")
213
def test_HTTP10_KeepAlive(self):
214
self.PROTOCOL = "HTTP/1.0"
215
if self.scheme == "https":
216
self.HTTP_CONN = HTTPSConnection
218
self.HTTP_CONN = HTTPConnection
220
# Test a normal HTTP/1.0 request.
221
self.getPage("/page2")
222
self.assertStatus('200 OK')
224
# Apache, for example, may emit a Connection header even for HTTP/1.0
225
## self.assertNoHeader("Connection")
227
# Test a keep-alive HTTP/1.0 request.
228
self.persistent = True
230
self.getPage("/page3", headers=[("Connection", "Keep-Alive")])
231
self.assertStatus('200 OK')
233
self.assertHeader("Connection", "Keep-Alive")
235
# Remove the keep-alive header again.
236
self.getPage("/page3")
237
self.assertStatus('200 OK')
239
# Apache, for example, may emit a Connection header even for HTTP/1.0
240
## self.assertNoHeader("Connection")
243
class PipelineTests(helper.CPWebCase):
245
def test_HTTP11_Timeout(self):
246
# If we timeout without sending any data,
247
# the server will close the conn with a 408.
248
if cherrypy.server.protocol_version != "HTTP/1.1":
251
self.PROTOCOL = "HTTP/1.1"
253
# Connect but send nothing.
254
self.persistent = True
255
conn = self.HTTP_CONN
256
conn.auto_open = False
259
# Wait for our socket timeout
260
time.sleep(timeout * 2)
262
# The request should have returned 408 already.
263
response = conn.response_class(conn.sock, method="GET")
265
self.assertEqual(response.status, 408)
268
# Connect but send half the headers only.
269
self.persistent = True
270
conn = self.HTTP_CONN
271
conn.auto_open = False
273
conn.send('GET /hello HTTP/1.1')
274
conn.send(("Host: %s" % self.HOST).encode('ascii'))
276
# Wait for our socket timeout
277
time.sleep(timeout * 2)
279
# The conn should have already sent 408.
280
response = conn.response_class(conn.sock, method="GET")
282
self.assertEqual(response.status, 408)
285
def test_HTTP11_Timeout_after_request(self):
286
# If we timeout after at least one request has succeeded,
287
# the server will close the conn without 408.
288
if cherrypy.server.protocol_version != "HTTP/1.1":
291
self.PROTOCOL = "HTTP/1.1"
293
# Make an initial request
294
self.persistent = True
295
conn = self.HTTP_CONN
296
conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True)
297
conn.putheader("Host", self.HOST)
299
response = conn.response_class(conn.sock, method="GET")
301
self.assertEqual(response.status, 200)
302
self.body = response.read()
303
self.assertBody(str(timeout))
305
# Make a second request on the same socket
306
conn._output('GET /hello HTTP/1.1')
307
conn._output("Host: %s" % self.HOST)
309
response = conn.response_class(conn.sock, method="GET")
311
self.assertEqual(response.status, 200)
312
self.body = response.read()
313
self.assertBody("Hello, world!")
315
# Wait for our socket timeout
316
time.sleep(timeout * 2)
318
# Make another request on the same socket, which should error
319
conn._output('GET /hello HTTP/1.1')
320
conn._output("Host: %s" % self.HOST)
322
response = conn.response_class(conn.sock, method="GET")
326
if not isinstance(sys.exc_info()[1],
327
(socket.error, BadStatusLine)):
328
self.fail("Writing to timed out socket didn't fail"
329
" as it should have: %s" % sys.exc_info()[1])
331
if response.status != 408:
332
self.fail("Writing to timed out socket didn't fail"
333
" as it should have: %s" %
338
# Make another request on a new socket, which should work
339
self.persistent = True
340
conn = self.HTTP_CONN
341
conn.putrequest("GET", "/", skip_host=True)
342
conn.putheader("Host", self.HOST)
344
response = conn.response_class(conn.sock, method="GET")
346
self.assertEqual(response.status, 200)
347
self.body = response.read()
351
# Make another request on the same socket,
352
# but timeout on the headers
353
conn.send('GET /hello HTTP/1.1')
354
# Wait for our socket timeout
355
time.sleep(timeout * 2)
356
response = conn.response_class(conn.sock, method="GET")
360
if not isinstance(sys.exc_info()[1],
361
(socket.error, BadStatusLine)):
362
self.fail("Writing to timed out socket didn't fail"
363
" as it should have: %s" % sys.exc_info()[1])
365
self.fail("Writing to timed out socket didn't fail"
366
" as it should have: %s" %
371
# Retry the request on a new connection, which should work
372
self.persistent = True
373
conn = self.HTTP_CONN
374
conn.putrequest("GET", "/", skip_host=True)
375
conn.putheader("Host", self.HOST)
377
response = conn.response_class(conn.sock, method="GET")
379
self.assertEqual(response.status, 200)
380
self.body = response.read()
384
def test_HTTP11_pipelining(self):
385
if cherrypy.server.protocol_version != "HTTP/1.1":
388
self.PROTOCOL = "HTTP/1.1"
390
# Test pipelining. httplib doesn't support this directly.
391
self.persistent = True
392
conn = self.HTTP_CONN
395
conn.putrequest("GET", "/hello", skip_host=True)
396
conn.putheader("Host", self.HOST)
399
for trial in range(5):
401
conn._output('GET /hello HTTP/1.1')
402
conn._output("Host: %s" % self.HOST)
405
# Retrieve previous response
406
response = conn.response_class(conn.sock, method="GET")
408
body = response.read()
409
self.assertEqual(response.status, 200)
410
self.assertEqual(body, "Hello, world!")
412
# Retrieve final response
413
response = conn.response_class(conn.sock, method="GET")
415
body = response.read()
416
self.assertEqual(response.status, 200)
417
self.assertEqual(body, "Hello, world!")
421
def test_100_Continue(self):
422
if cherrypy.server.protocol_version != "HTTP/1.1":
425
self.PROTOCOL = "HTTP/1.1"
427
self.persistent = True
428
conn = self.HTTP_CONN
430
# Try a page without an Expect request header first.
431
# Note that httplib's response.begin automatically ignores
432
# 100 Continue responses, so we must manually check for it.
433
conn.putrequest("POST", "/upload", skip_host=True)
434
conn.putheader("Host", self.HOST)
435
conn.putheader("Content-Type", "text/plain")
436
conn.putheader("Content-Length", "4")
439
response = conn.response_class(conn.sock, method="POST")
440
version, status, reason = response._read_status()
441
self.assertNotEqual(status, 100)
444
# Now try a page with an Expect header...
446
conn.putrequest("POST", "/upload", skip_host=True)
447
conn.putheader("Host", self.HOST)
448
conn.putheader("Content-Type", "text/plain")
449
conn.putheader("Content-Length", "17")
450
conn.putheader("Expect", "100-continue")
452
response = conn.response_class(conn.sock, method="POST")
454
# ...assert and then skip the 100 response
455
version, status, reason = response._read_status()
456
self.assertEqual(status, 100)
458
line = response.fp.readline().strip()
460
self.fail("100 Continue should not output any headers. Got %r" % line)
465
conn.send("I am a small file")
467
# ...get the final response
469
self.status, self.headers, self.body = webtest.shb(response)
470
self.assertStatus(200)
471
self.assertBody("thanks for 'I am a small file'")
475
class ConnectionTests(helper.CPWebCase):
477
def test_readall_or_close(self):
478
if cherrypy.server.protocol_version != "HTTP/1.1":
481
self.PROTOCOL = "HTTP/1.1"
483
if self.scheme == "https":
484
self.HTTP_CONN = HTTPSConnection
486
self.HTTP_CONN = HTTPConnection
488
# Test a max of 0 (the default) and then reset to what it was above.
489
old_max = cherrypy.server.max_request_body_size
490
for new_max in (0, old_max):
491
cherrypy.server.max_request_body_size = new_max
493
self.persistent = True
494
conn = self.HTTP_CONN
496
# Get a POST page with an error
497
conn.putrequest("POST", "/err_before_read", skip_host=True)
498
conn.putheader("Host", self.HOST)
499
conn.putheader("Content-Type", "text/plain")
500
conn.putheader("Content-Length", "1000")
501
conn.putheader("Expect", "100-continue")
503
response = conn.response_class(conn.sock, method="POST")
505
# ...assert and then skip the 100 response
506
version, status, reason = response._read_status()
507
self.assertEqual(status, 100)
509
skip = response.fp.readline().strip()
514
conn.send("x" * 1000)
516
# ...get the final response
518
self.status, self.headers, self.body = webtest.shb(response)
519
self.assertStatus(500)
521
# Now try a working page with an Expect header...
522
conn._output('POST /upload HTTP/1.1')
523
conn._output("Host: %s" % self.HOST)
524
conn._output("Content-Type: text/plain")
525
conn._output("Content-Length: 17")
526
conn._output("Expect: 100-continue")
528
response = conn.response_class(conn.sock, method="POST")
530
# ...assert and then skip the 100 response
531
version, status, reason = response._read_status()
532
self.assertEqual(status, 100)
534
skip = response.fp.readline().strip()
539
conn.send("I am a small file")
541
# ...get the final response
543
self.status, self.headers, self.body = webtest.shb(response)
544
self.assertStatus(200)
545
self.assertBody("thanks for 'I am a small file'")
548
def test_No_Message_Body(self):
549
if cherrypy.server.protocol_version != "HTTP/1.1":
552
self.PROTOCOL = "HTTP/1.1"
554
# Set our HTTP_CONN to an instance so it persists between requests.
555
self.persistent = True
557
# Make the first request and assert there's no "Connection: close".
559
self.assertStatus('200 OK')
561
self.assertNoHeader("Connection")
563
# Make a 204 request on the same connection.
564
self.getPage("/custom/204")
565
self.assertStatus(204)
566
self.assertNoHeader("Content-Length")
568
self.assertNoHeader("Connection")
570
# Make a 304 request on the same connection.
571
self.getPage("/custom/304")
572
self.assertStatus(304)
573
self.assertNoHeader("Content-Length")
575
self.assertNoHeader("Connection")
577
def test_Chunked_Encoding(self):
578
if cherrypy.server.protocol_version != "HTTP/1.1":
581
if (hasattr(self, 'harness') and
582
"modpython" in self.harness.__class__.__name__.lower()):
583
# mod_python forbids chunked encoding
586
self.PROTOCOL = "HTTP/1.1"
588
# Set our HTTP_CONN to an instance so it persists between requests.
589
self.persistent = True
590
conn = self.HTTP_CONN
592
# Try a normal chunked request (with extensions)
593
body = ("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n"
594
"Content-Type: application/json\r\n"
596
conn.putrequest("POST", "/upload", skip_host=True)
597
conn.putheader("Host", self.HOST)
598
conn.putheader("Transfer-Encoding", "chunked")
599
conn.putheader("Trailer", "Content-Type")
600
# Note that this is somewhat malformed:
601
# we shouldn't be sending Content-Length.
602
# RFC 2616 says the server should ignore it.
603
conn.putheader("Content-Length", "3")
606
response = conn.getresponse()
607
self.status, self.headers, self.body = webtest.shb(response)
608
self.assertStatus('200 OK')
609
self.assertBody("thanks for 'xx\r\nxxxxyyyyy'")
611
# Try a chunked request that exceeds server.max_request_body_size.
612
# Note that the delimiters and trailer are included.
613
body = "3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n"
614
conn.putrequest("POST", "/upload", skip_host=True)
615
conn.putheader("Host", self.HOST)
616
conn.putheader("Transfer-Encoding", "chunked")
617
conn.putheader("Content-Type", "text/plain")
618
# Chunked requests don't need a content-length
619
## conn.putheader("Content-Length", len(body))
622
response = conn.getresponse()
623
self.status, self.headers, self.body = webtest.shb(response)
624
self.assertStatus(413)
627
def test_Content_Length(self):
628
# Try a non-chunked request where Content-Length exceeds
629
# server.max_request_body_size. Assert error before body send.
630
self.persistent = True
631
conn = self.HTTP_CONN
632
conn.putrequest("POST", "/upload", skip_host=True)
633
conn.putheader("Host", self.HOST)
634
conn.putheader("Content-Type", "text/plain")
635
conn.putheader("Content-Length", "9999")
637
response = conn.getresponse()
638
self.status, self.headers, self.body = webtest.shb(response)
639
self.assertStatus(413)
644
remote_data_conn = urllib.urlopen('%s://%s:%s/one_megabyte_of_a/' %
645
(self.scheme, self.HOST, self.PORT,))
646
buf = remote_data_conn.read(512)
647
time.sleep(timeout * 0.6)
648
remaining = (1024 * 1024) - 512
650
data = remote_data_conn.read(remaining)
655
remaining -= len(data)
657
self.assertEqual(len(buf), 1024 * 1024)
658
self.assertEqual(buf, "a" * 1024 * 1024)
659
self.assertEqual(remaining, 0)
660
remote_data_conn.close()
663
class BadRequestTests(helper.CPWebCase):
665
def test_No_CRLF(self):
666
self.persistent = True
668
conn = self.HTTP_CONN
669
conn.send('GET /hello HTTP/1.1\n\n')
670
response = conn.response_class(conn.sock, method="GET")
672
self.body = response.read()
673
self.assertBody("HTTP requires CRLF terminators")
677
conn.send('GET /hello HTTP/1.1\r\n\n')
678
response = conn.response_class(conn.sock, method="GET")
680
self.body = response.read()
681
self.assertBody("HTTP requires CRLF terminators")
686
if __name__ == "__main__":