Add python-eventlet 0.16.1
[packages/trusty/python-eventlet.git] / eventlet / tests / wsgi_test.py
1 import cgi
2 import collections
3 import errno
4 import os
5 import signal
6 import socket
7 import sys
8 import traceback
9 import unittest
10
11 import eventlet
12 from eventlet import debug
13 from eventlet import event
14 from eventlet.green import socket as greensocket
15 from eventlet.green import ssl
16 from eventlet.green import subprocess
17 from eventlet import greenio
18 from eventlet import greenthread
19 from eventlet import support
20 from eventlet.support import bytes_to_str, capture_stderr, six
21 from eventlet import tpool
22 from eventlet import wsgi
23
24 import tests
25
26
27 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
28 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
29
30
31 HttpReadResult = collections.namedtuple(
32     'HttpReadResult',
33     'status headers_lower body headers_original')
34
35
36 def hello_world(env, start_response):
37     if env['PATH_INFO'] == 'notexist':
38         start_response('404 Not Found', [('Content-type', 'text/plain')])
39         return [b"not found"]
40
41     start_response('200 OK', [('Content-type', 'text/plain')])
42     return [b"hello world"]
43
44
45 def chunked_app(env, start_response):
46     start_response('200 OK', [('Content-type', 'text/plain')])
47     yield b"this"
48     yield b"is"
49     yield b"chunked"
50
51
52 def chunked_fail_app(environ, start_response):
53     """http://rhodesmill.org/brandon/2013/chunked-wsgi/
54     """
55     headers = [('Content-Type', 'text/plain')]
56     start_response('200 OK', headers)
57
58     # We start streaming data just fine.
59     yield b"The dwarves of yore made mighty spells,"
60     yield b"While hammers fell like ringing bells"
61
62     # Then the back-end fails!
63     try:
64         1 / 0
65     except Exception:
66         start_response('500 Error', headers, sys.exc_info())
67         return
68
69     # So rest of the response data is not available.
70     yield b"In places deep, where dark things sleep,"
71     yield b"In hollow halls beneath the fells."
72
73
74 def big_chunks(env, start_response):
75     start_response('200 OK', [('Content-type', 'text/plain')])
76     line = b'a' * 8192
77     for x in range(10):
78         yield line
79
80
81 def use_write(env, start_response):
82     if env['PATH_INFO'] == '/a':
83         write = start_response('200 OK', [('Content-type', 'text/plain'),
84                                           ('Content-Length', '5')])
85         write(b'abcde')
86     if env['PATH_INFO'] == '/b':
87         write = start_response('200 OK', [('Content-type', 'text/plain')])
88         write(b'abcde')
89     return []
90
91
92 def chunked_post(env, start_response):
93     start_response('200 OK', [('Content-type', 'text/plain')])
94     if env['PATH_INFO'] == '/a':
95         return [env['wsgi.input'].read()]
96     elif env['PATH_INFO'] == '/b':
97         return [x for x in iter(lambda: env['wsgi.input'].read(4096), b'')]
98     elif env['PATH_INFO'] == '/c':
99         return [x for x in iter(lambda: env['wsgi.input'].read(1), b'')]
100
101
102 def already_handled(env, start_response):
103     start_response('200 OK', [('Content-type', 'text/plain')])
104     return wsgi.ALREADY_HANDLED
105
106
107 class Site(object):
108     def __init__(self):
109         self.application = hello_world
110
111     def __call__(self, env, start_response):
112         return self.application(env, start_response)
113
114
115 class IterableApp(object):
116
117     def __init__(self, send_start_response=False, return_val=wsgi.ALREADY_HANDLED):
118         self.send_start_response = send_start_response
119         self.return_val = return_val
120         self.env = {}
121
122     def __call__(self, env, start_response):
123         self.env = env
124         if self.send_start_response:
125             start_response('200 OK', [('Content-type', 'text/plain')])
126         return self.return_val
127
128
129 class IterableSite(Site):
130     def __call__(self, env, start_response):
131         it = self.application(env, start_response)
132         for i in it:
133             yield i
134
135
136 CONTENT_LENGTH = 'content-length'
137
138
139 """
140 HTTP/1.1 200 OK
141 Date: foo
142 Content-length: 11
143
144 hello world
145 """
146
147
148 def recvall(socket_):
149     result = b''
150     while True:
151         chunk = socket_.recv()
152         result += chunk
153         if chunk == b'':
154             break
155
156     return result
157
158
159 class ConnectionClosed(Exception):
160     pass
161
162
163 def send_expect_close(sock, buf):
164     # Some tests will induce behavior that causes the remote end to
165     # close the connection before all of the data has been written.
166     # With small kernel buffer sizes, this can cause an EPIPE error.
167     # Since the test expects an early close, this can be ignored.
168     try:
169         sock.sendall(buf)
170     except socket.error as exc:
171         if support.get_errno(exc) != errno.EPIPE:
172             raise
173
174
175 def read_http(sock):
176     fd = sock.makefile('rb')
177     try:
178         response_line = bytes_to_str(fd.readline().rstrip(b'\r\n'))
179     except socket.error as exc:
180         # TODO find out whether 54 is ok here or not, I see it when running tests
181         # on Python 3
182         if support.get_errno(exc) in (10053, 54):
183             raise ConnectionClosed
184         raise
185     if not response_line:
186         raise ConnectionClosed(response_line)
187
188     header_lines = []
189     while True:
190         line = fd.readline()
191         if line == b'\r\n':
192             break
193         else:
194             header_lines.append(line)
195
196     headers_original = {}
197     headers_lower = {}
198     for x in header_lines:
199         x = x.strip()
200         if not x:
201             continue
202         key, value = bytes_to_str(x).split(':', 1)
203         key = key.rstrip()
204         value = value.lstrip()
205         key_lower = key.lower()
206         # FIXME: Duplicate headers are allowed as per HTTP RFC standard,
207         # the client and/or intermediate proxies are supposed to treat them
208         # as a single header with values concatenated using space (' ') delimiter.
209         assert key_lower not in headers_lower, "header duplicated: {0}".format(key)
210         headers_original[key] = value
211         headers_lower[key_lower] = value
212
213     content_length_str = headers_lower.get(CONTENT_LENGTH.lower(), '')
214     if content_length_str:
215         num = int(content_length_str)
216         body = fd.read(num)
217     else:
218         # read until EOF
219         body = fd.read()
220
221     result = HttpReadResult(
222         status=response_line,
223         headers_lower=headers_lower,
224         body=body,
225         headers_original=headers_original)
226     return result
227
228
229 class _TestBase(tests.LimitedTestCase):
230     def setUp(self):
231         super(_TestBase, self).setUp()
232         self.logfile = six.StringIO()
233         self.site = Site()
234         self.killer = None
235         self.set_site()
236         self.spawn_server()
237
238     def tearDown(self):
239         greenthread.kill(self.killer)
240         eventlet.sleep(0)
241         super(_TestBase, self).tearDown()
242
243     def spawn_server(self, **kwargs):
244         """Spawns a new wsgi server with the given arguments using
245         :meth:`spawn_thread`.
246
247         Sets self.port to the port of the server
248         """
249         new_kwargs = dict(max_size=128,
250                           log=self.logfile,
251                           site=self.site)
252         new_kwargs.update(kwargs)
253
254         if 'sock' not in new_kwargs:
255             new_kwargs['sock'] = eventlet.listen(('localhost', 0))
256
257         self.port = new_kwargs['sock'].getsockname()[1]
258         self.spawn_thread(wsgi.server, **new_kwargs)
259
260     def spawn_thread(self, target, **kwargs):
261         """Spawns a new greenthread using specified target and arguments.
262
263         Kills any previously-running server and sets self.killer to the
264         greenthread running the target.
265         """
266         eventlet.sleep(0)  # give previous server a chance to start
267         if self.killer:
268             greenthread.kill(self.killer)
269
270         self.killer = eventlet.spawn_n(target, **kwargs)
271
272     def set_site(self):
273         raise NotImplementedError
274
275
276 class TestHttpd(_TestBase):
277     def set_site(self):
278         self.site = Site()
279
280     def test_001_server(self):
281         sock = eventlet.connect(
282             ('localhost', self.port))
283
284         fd = sock.makefile('rwb')
285         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
286         fd.flush()
287         result = fd.read()
288         fd.close()
289         # The server responds with the maximum version it supports
290         assert result.startswith(b'HTTP'), result
291         assert result.endswith(b'hello world'), result
292
293     def test_002_keepalive(self):
294         sock = eventlet.connect(
295             ('localhost', self.port))
296
297         fd = sock.makefile('wb')
298         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
299         fd.flush()
300         read_http(sock)
301         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
302         fd.flush()
303         read_http(sock)
304         fd.close()
305         sock.close()
306
307     def test_003_passing_non_int_to_read(self):
308         # This should go in greenio_test
309         sock = eventlet.connect(
310             ('localhost', self.port))
311
312         fd = sock.makefile('rwb')
313         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
314         fd.flush()
315         cancel = eventlet.Timeout(1, RuntimeError)
316         self.assertRaises(TypeError, fd.read, "This shouldn't work")
317         cancel.cancel()
318         fd.close()
319
320     def test_004_close_keepalive(self):
321         sock = eventlet.connect(
322             ('localhost', self.port))
323
324         fd = sock.makefile('wb')
325         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
326         fd.flush()
327         read_http(sock)
328         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
329         fd.flush()
330         read_http(sock)
331         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
332         fd.flush()
333         self.assertRaises(ConnectionClosed, read_http, sock)
334         fd.close()
335
336     @tests.skipped
337     def test_005_run_apachebench(self):
338         url = 'http://localhost:12346/'
339         # ab is apachebench
340         subprocess.call(
341             [tests.find_command('ab'), '-c', '64', '-n', '1024', '-k', url],
342             stdout=subprocess.PIPE)
343
344     def test_006_reject_long_urls(self):
345         sock = eventlet.connect(
346             ('localhost', self.port))
347         path_parts = []
348         for ii in range(3000):
349             path_parts.append('path')
350         path = '/'.join(path_parts)
351         request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
352         send_expect_close(sock, request.encode())
353         fd = sock.makefile('rb')
354         result = fd.readline()
355         if result:
356             # windows closes the socket before the data is flushed,
357             # so we never get anything back
358             status = result.split(b' ')[1]
359             self.assertEqual(status, b'414')
360         fd.close()
361
362     def test_007_get_arg(self):
363         # define a new handler that does a get_arg as well as a read_body
364         def new_app(env, start_response):
365             body = bytes_to_str(env['wsgi.input'].read())
366             a = cgi.parse_qs(body).get('a', [1])[0]
367             start_response('200 OK', [('Content-type', 'text/plain')])
368             return [six.b('a is %s, body is %s' % (a, body))]
369
370         self.site.application = new_app
371         sock = eventlet.connect(
372             ('localhost', self.port))
373         request = '\r\n'.join((
374             'POST / HTTP/1.0',
375             'Host: localhost',
376             'Content-Length: 3',
377             '',
378             'a=a'))
379         fd = sock.makefile('wb')
380         fd.write(request.encode())
381         fd.flush()
382
383         # send some junk after the actual request
384         fd.write(b'01234567890123456789')
385         result = read_http(sock)
386         self.assertEqual(result.body, b'a is a, body is a=a')
387         fd.close()
388
389     def test_008_correctresponse(self):
390         sock = eventlet.connect(('localhost', self.port))
391
392         fd = sock.makefile('wb')
393         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
394         fd.flush()
395         result_200 = read_http(sock)
396         fd.write(b'GET /notexist HTTP/1.1\r\nHost: localhost\r\n\r\n')
397         fd.flush()
398         read_http(sock)
399         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
400         fd.flush()
401         result_test = read_http(sock)
402         self.assertEqual(result_200.status, result_test.status)
403         fd.close()
404         sock.close()
405
406     def test_009_chunked_response(self):
407         self.site.application = chunked_app
408         sock = eventlet.connect(
409             ('localhost', self.port))
410
411         fd = sock.makefile('rwb')
412         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
413         fd.flush()
414         assert b'Transfer-Encoding: chunked' in fd.read()
415
416     def test_010_no_chunked_http_1_0(self):
417         self.site.application = chunked_app
418         sock = eventlet.connect(
419             ('localhost', self.port))
420
421         fd = sock.makefile('rwb')
422         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n')
423         fd.flush()
424         assert b'Transfer-Encoding: chunked' not in fd.read()
425
426     def test_011_multiple_chunks(self):
427         self.site.application = big_chunks
428         sock = eventlet.connect(
429             ('localhost', self.port))
430
431         fd = sock.makefile('rwb')
432         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
433         fd.flush()
434         headers = b''
435         while True:
436             line = fd.readline()
437             if line == b'\r\n':
438                 break
439             else:
440                 headers += line
441         assert b'Transfer-Encoding: chunked' in headers
442         chunks = 0
443         chunklen = int(fd.readline(), 16)
444         while chunklen:
445             chunks += 1
446             fd.read(chunklen)
447             fd.readline()  # CRLF
448             chunklen = int(fd.readline(), 16)
449         assert chunks > 1
450         response = fd.read()
451         # Require a CRLF to close the message body
452         self.assertEqual(response, b'\r\n')
453
454     @tests.skip_if_no_ssl
455     def test_012_ssl_server(self):
456         def wsgi_app(environ, start_response):
457             start_response('200 OK', {})
458             return [environ['wsgi.input'].read()]
459
460         certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
461         private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
462
463         server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
464                                         certfile=certificate_file,
465                                         keyfile=private_key_file,
466                                         server_side=True)
467         self.spawn_server(sock=server_sock, site=wsgi_app)
468
469         sock = eventlet.connect(('localhost', self.port))
470         sock = eventlet.wrap_ssl(sock)
471         sock.write(
472             b'POST /foo HTTP/1.1\r\nHost: localhost\r\n'
473             b'Connection: close\r\nContent-length:3\r\n\r\nabc')
474         result = recvall(sock)
475         assert result.endswith(b'abc')
476
477     @tests.skip_if_no_ssl
478     def test_013_empty_return(self):
479         def wsgi_app(environ, start_response):
480             start_response("200 OK", [])
481             return [b""]
482
483         certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
484         private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
485         server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
486                                         certfile=certificate_file,
487                                         keyfile=private_key_file,
488                                         server_side=True)
489         self.spawn_server(sock=server_sock, site=wsgi_app)
490
491         sock = eventlet.connect(('localhost', server_sock.getsockname()[1]))
492         sock = eventlet.wrap_ssl(sock)
493         sock.write(b'GET /foo HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
494         result = recvall(sock)
495         assert result[-4:] == b'\r\n\r\n'
496
497     def test_014_chunked_post(self):
498         self.site.application = chunked_post
499         sock = eventlet.connect(('localhost', self.port))
500         fd = sock.makefile('rwb')
501         fd.write('PUT /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
502                  'Transfer-Encoding: chunked\r\n\r\n'
503                  '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
504         fd.flush()
505         while True:
506             if fd.readline() == b'\r\n':
507                 break
508         response = fd.read()
509         assert response == b'oh hai', 'invalid response %s' % response
510
511         sock = eventlet.connect(('localhost', self.port))
512         fd = sock.makefile('rwb')
513         fd.write('PUT /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
514                  'Transfer-Encoding: chunked\r\n\r\n'
515                  '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
516         fd.flush()
517         while True:
518             if fd.readline() == b'\r\n':
519                 break
520         response = fd.read()
521         assert response == b'oh hai', 'invalid response %s' % response
522
523         sock = eventlet.connect(('localhost', self.port))
524         fd = sock.makefile('rwb')
525         fd.write('PUT /c HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
526                  'Transfer-Encoding: chunked\r\n\r\n'
527                  '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
528         fd.flush()
529         while True:
530             if fd.readline() == b'\r\n':
531                 break
532         response = fd.read(8192)
533         assert response == b'oh hai', 'invalid response %s' % response
534
535     def test_015_write(self):
536         self.site.application = use_write
537         sock = eventlet.connect(('localhost', self.port))
538         fd = sock.makefile('wb')
539         fd.write(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
540         fd.flush()
541         result1 = read_http(sock)
542         assert 'content-length' in result1.headers_lower
543
544         sock = eventlet.connect(('localhost', self.port))
545         fd = sock.makefile('wb')
546         fd.write(b'GET /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
547         fd.flush()
548         result2 = read_http(sock)
549         assert 'transfer-encoding' in result2.headers_lower
550         assert result2.headers_lower['transfer-encoding'] == 'chunked'
551
552     def test_016_repeated_content_length(self):
553         """content-length header was being doubled up if it was set in
554         start_response and could also be inferred from the iterator
555         """
556         def wsgi_app(environ, start_response):
557             start_response('200 OK', [('Content-Length', '7')])
558             return [b'testing']
559         self.site.application = wsgi_app
560         sock = eventlet.connect(('localhost', self.port))
561         fd = sock.makefile('rwb')
562         fd.write(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
563         fd.flush()
564         header_lines = []
565         while True:
566             line = fd.readline()
567             if line == b'\r\n':
568                 break
569             else:
570                 header_lines.append(line)
571         self.assertEqual(1, len(
572             [l for l in header_lines if l.lower().startswith(b'content-length')]))
573
574     @tests.skip_if_no_ssl
575     def test_017_ssl_zeroreturnerror(self):
576
577         def server(sock, site, log):
578             try:
579                 serv = wsgi.Server(sock, sock.getsockname(), site, log)
580                 client_socket = sock.accept()
581                 serv.process_request(client_socket)
582                 return True
583             except Exception:
584                 traceback.print_exc()
585                 return False
586
587         def wsgi_app(environ, start_response):
588             start_response('200 OK', [])
589             return [environ['wsgi.input'].read()]
590
591         certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
592         private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
593
594         sock = eventlet.wrap_ssl(
595             eventlet.listen(('localhost', 0)),
596             certfile=certificate_file, keyfile=private_key_file,
597             server_side=True)
598         server_coro = eventlet.spawn(server, sock, wsgi_app, self.logfile)
599
600         client = eventlet.connect(('localhost', sock.getsockname()[1]))
601         client = eventlet.wrap_ssl(client)
602         client.write(b'X')  # non-empty payload so that SSL handshake occurs
603         greenio.shutdown_safe(client)
604         client.close()
605
606         success = server_coro.wait()
607         assert success
608
609     def test_018_http_10_keepalive(self):
610         # verify that if an http/1.0 client sends connection: keep-alive
611         # that we don't close the connection
612         sock = eventlet.connect(
613             ('localhost', self.port))
614
615         fd = sock.makefile('wb')
616         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
617         fd.flush()
618
619         result1 = read_http(sock)
620         assert 'connection' in result1.headers_lower
621         self.assertEqual('keep-alive', result1.headers_lower['connection'])
622         # repeat request to verify connection is actually still open
623         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
624         fd.flush()
625         result2 = read_http(sock)
626         assert 'connection' in result2.headers_lower
627         self.assertEqual('keep-alive', result2.headers_lower['connection'])
628         sock.close()
629
630     def test_019_fieldstorage_compat(self):
631         def use_fieldstorage(environ, start_response):
632             cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
633             start_response('200 OK', [('Content-type', 'text/plain')])
634             return [b'hello!']
635
636         self.site.application = use_fieldstorage
637         sock = eventlet.connect(
638             ('localhost', self.port))
639
640         fd = sock.makefile('rwb')
641         fd.write('POST / HTTP/1.1\r\n'
642                  'Host: localhost\r\n'
643                  'Connection: close\r\n'
644                  'Transfer-Encoding: chunked\r\n\r\n'
645                  '2\r\noh\r\n'
646                  '4\r\n hai\r\n0\r\n\r\n'.encode())
647         fd.flush()
648         assert b'hello!' in fd.read()
649
650     def test_020_x_forwarded_for(self):
651         request_bytes = (
652             b'GET / HTTP/1.1\r\nHost: localhost\r\n'
653             + b'X-Forwarded-For: 1.2.3.4, 5.6.7.8\r\n\r\n'
654         )
655
656         sock = eventlet.connect(('localhost', self.port))
657         sock.sendall(request_bytes)
658         sock.recv(1024)
659         sock.close()
660         assert '1.2.3.4,5.6.7.8,127.0.0.1' in self.logfile.getvalue()
661
662         # turning off the option should work too
663         self.logfile = six.StringIO()
664         self.spawn_server(log_x_forwarded_for=False)
665
666         sock = eventlet.connect(('localhost', self.port))
667         sock.sendall(request_bytes)
668         sock.recv(1024)
669         sock.close()
670         assert '1.2.3.4' not in self.logfile.getvalue()
671         assert '5.6.7.8' not in self.logfile.getvalue()
672         assert '127.0.0.1' in self.logfile.getvalue()
673
674     def test_socket_remains_open(self):
675         greenthread.kill(self.killer)
676         server_sock = eventlet.listen(('localhost', 0))
677         server_sock_2 = server_sock.dup()
678         self.spawn_server(sock=server_sock_2)
679         # do a single req/response to verify it's up
680         sock = eventlet.connect(('localhost', self.port))
681         fd = sock.makefile('rwb')
682         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
683         fd.flush()
684         result = fd.read(1024)
685         fd.close()
686         assert result.startswith(b'HTTP'), result
687         assert result.endswith(b'hello world'), result
688
689         # shut down the server and verify the server_socket fd is still open,
690         # but the actual socketobject passed in to wsgi.server is closed
691         greenthread.kill(self.killer)
692         eventlet.sleep(0)  # make the kill go through
693         try:
694             server_sock_2.accept()
695             # shouldn't be able to use this one anymore
696         except socket.error as exc:
697             self.assertEqual(support.get_errno(exc), errno.EBADF)
698         self.spawn_server(sock=server_sock)
699         sock = eventlet.connect(('localhost', self.port))
700         fd = sock.makefile('rwb')
701         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
702         fd.flush()
703         result = fd.read(1024)
704         fd.close()
705         assert result.startswith(b'HTTP'), result
706         assert result.endswith(b'hello world'), result
707
708     def test_021_environ_clobbering(self):
709         def clobberin_time(environ, start_response):
710             for environ_var in [
711                     'wsgi.version', 'wsgi.url_scheme',
712                     'wsgi.input', 'wsgi.errors', 'wsgi.multithread',
713                     'wsgi.multiprocess', 'wsgi.run_once', 'REQUEST_METHOD',
714                     'SCRIPT_NAME', 'RAW_PATH_INFO', 'PATH_INFO', 'QUERY_STRING',
715                     'CONTENT_TYPE', 'CONTENT_LENGTH', 'SERVER_NAME', 'SERVER_PORT',
716                     'SERVER_PROTOCOL']:
717                 environ[environ_var] = None
718             start_response('200 OK', [('Content-type', 'text/plain')])
719             return []
720         self.site.application = clobberin_time
721         sock = eventlet.connect(('localhost', self.port))
722         fd = sock.makefile('rwb')
723         fd.write('GET / HTTP/1.1\r\n'
724                  'Host: localhost\r\n'
725                  'Connection: close\r\n'
726                  '\r\n\r\n'.encode())
727         fd.flush()
728         assert b'200 OK' in fd.read()
729
730     def test_022_custom_pool(self):
731         # just test that it accepts the parameter for now
732         # TODO(waitall): test that it uses the pool and that you can waitall() to
733         # ensure that all clients finished
734         p = eventlet.GreenPool(5)
735         self.spawn_server(custom_pool=p)
736
737         # this stuff is copied from test_001_server, could be better factored
738         sock = eventlet.connect(
739             ('localhost', self.port))
740         fd = sock.makefile('rwb')
741         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
742         fd.flush()
743         result = fd.read()
744         fd.close()
745         assert result.startswith(b'HTTP'), result
746         assert result.endswith(b'hello world'), result
747
748     def test_023_bad_content_length(self):
749         sock = eventlet.connect(
750             ('localhost', self.port))
751         fd = sock.makefile('rwb')
752         fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nContent-length: argh\r\n\r\n')
753         fd.flush()
754         result = fd.read()
755         fd.close()
756         assert result.startswith(b'HTTP'), result
757         assert b'400 Bad Request' in result, result
758         assert b'500' not in result, result
759
760     def test_024_expect_100_continue(self):
761         def wsgi_app(environ, start_response):
762             if int(environ['CONTENT_LENGTH']) > 1024:
763                 start_response('417 Expectation Failed', [('Content-Length', '7')])
764                 return [b'failure']
765             else:
766                 text = environ['wsgi.input'].read()
767                 start_response('200 OK', [('Content-Length', str(len(text)))])
768                 return [text]
769         self.site.application = wsgi_app
770         sock = eventlet.connect(('localhost', self.port))
771         fd = sock.makefile('rwb')
772         fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
773                  b'Expect: 100-continue\r\n\r\n')
774         fd.flush()
775         result = read_http(sock)
776         self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
777         self.assertEqual(result.body, b'failure')
778         fd.write(
779             b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
780             b'Expect: 100-continue\r\n\r\ntesting')
781         fd.flush()
782         header_lines = []
783         while True:
784             line = fd.readline()
785             if line == b'\r\n':
786                 break
787             else:
788                 header_lines.append(line)
789         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
790         header_lines = []
791         while True:
792             line = fd.readline()
793             if line == b'\r\n':
794                 break
795             else:
796                 header_lines.append(line)
797         assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
798         assert fd.read(7) == b'testing'
799         fd.close()
800         sock.close()
801
802     def test_024a_expect_100_continue_with_headers(self):
803         def wsgi_app(environ, start_response):
804             if int(environ['CONTENT_LENGTH']) > 1024:
805                 start_response('417 Expectation Failed', [('Content-Length', '7')])
806                 return [b'failure']
807             else:
808                 environ['wsgi.input'].set_hundred_continue_response_headers(
809                     [('Hundred-Continue-Header-1', 'H1'),
810                      ('Hundred-Continue-Header-2', 'H2'),
811                      ('Hundred-Continue-Header-k', 'Hk')])
812                 text = environ['wsgi.input'].read()
813                 start_response('200 OK', [('Content-Length', str(len(text)))])
814                 return [text]
815         self.site.application = wsgi_app
816         sock = eventlet.connect(('localhost', self.port))
817         fd = sock.makefile('rwb')
818         fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
819                  b'Expect: 100-continue\r\n\r\n')
820         fd.flush()
821         result = read_http(sock)
822         self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
823         self.assertEqual(result.body, b'failure')
824         fd.write(
825             b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
826             b'Expect: 100-continue\r\n\r\ntesting')
827         fd.flush()
828         header_lines = []
829         while True:
830             line = fd.readline()
831             if line == b'\r\n':
832                 break
833             else:
834                 header_lines.append(line.strip())
835         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
836         headers = dict((k, v) for k, v in (h.split(b': ', 1) for h in header_lines[1:]))
837         assert b'Hundred-Continue-Header-1' in headers
838         assert b'Hundred-Continue-Header-2' in headers
839         assert b'Hundred-Continue-Header-K' in headers
840         self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
841         self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
842         self.assertEqual(b'Hk', headers[b'Hundred-Continue-Header-K'])
843         header_lines = []
844         while True:
845             line = fd.readline()
846             if line == b'\r\n':
847                 break
848             else:
849                 header_lines.append(line)
850         assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
851         self.assertEqual(fd.read(7), b'testing')
852         fd.close()
853         sock.close()
854
855     def test_024b_expect_100_continue_with_headers_multiple_chunked(self):
856         def wsgi_app(environ, start_response):
857             environ['wsgi.input'].set_hundred_continue_response_headers(
858                 [('Hundred-Continue-Header-1', 'H1'),
859                  ('Hundred-Continue-Header-2', 'H2')])
860             text = environ['wsgi.input'].read()
861
862             environ['wsgi.input'].set_hundred_continue_response_headers(
863                 [('Hundred-Continue-Header-3', 'H3')])
864             environ['wsgi.input'].send_hundred_continue_response()
865
866             text += environ['wsgi.input'].read()
867
868             start_response('200 OK', [('Content-Length', str(len(text)))])
869             return [text]
870         self.site.application = wsgi_app
871         sock = eventlet.connect(('localhost', self.port))
872         fd = sock.makefile('rwb')
873         fd.write(b'PUT /a HTTP/1.1\r\n'
874                  b'Host: localhost\r\nConnection: close\r\n'
875                  b'Transfer-Encoding: chunked\r\n'
876                  b'Expect: 100-continue\r\n\r\n')
877         fd.flush()
878
879         # Expect 1st 100-continue response
880         header_lines = []
881         while True:
882             line = fd.readline()
883             if line == b'\r\n':
884                 break
885             else:
886                 header_lines.append(line.strip())
887         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
888         headers = dict((k, v) for k, v in (h.split(b': ', 1)
889                                            for h in header_lines[1:]))
890         assert b'Hundred-Continue-Header-1' in headers
891         assert b'Hundred-Continue-Header-2' in headers
892         self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
893         self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
894
895         # Send message 1
896         fd.write(b'5\r\nfirst\r\n8\r\n message\r\n0\r\n\r\n')
897         fd.flush()
898
899         # Expect a 2nd 100-continue response
900         header_lines = []
901         while True:
902             line = fd.readline()
903             if line == b'\r\n':
904                 break
905             else:
906                 header_lines.append(line.strip())
907         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
908         headers = dict((k, v) for k, v in (h.split(b': ', 1)
909                                            for h in header_lines[1:]))
910         assert b'Hundred-Continue-Header-3' in headers
911         self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
912
913         # Send message 2
914         fd.write(b'8\r\n, second\r\n8\r\n message\r\n0\r\n\r\n')
915         fd.flush()
916
917         # Expect final 200-OK
918         header_lines = []
919         while True:
920             line = fd.readline()
921             if line == b'\r\n':
922                 break
923             else:
924                 header_lines.append(line.strip())
925         assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
926
927         self.assertEqual(fd.read(29), b'first message, second message')
928         fd.close()
929         sock.close()
930
931     def test_024c_expect_100_continue_with_headers_multiple_nonchunked(self):
932         def wsgi_app(environ, start_response):
933
934             environ['wsgi.input'].set_hundred_continue_response_headers(
935                 [('Hundred-Continue-Header-1', 'H1'),
936                  ('Hundred-Continue-Header-2', 'H2')])
937             text = environ['wsgi.input'].read(13)
938
939             environ['wsgi.input'].set_hundred_continue_response_headers(
940                 [('Hundred-Continue-Header-3', 'H3')])
941             environ['wsgi.input'].send_hundred_continue_response()
942
943             text += environ['wsgi.input'].read(16)
944
945             start_response('200 OK', [('Content-Length', str(len(text)))])
946             return [text]
947
948         self.site.application = wsgi_app
949         sock = eventlet.connect(('localhost', self.port))
950         fd = sock.makefile('rwb')
951         fd.write(b'PUT /a HTTP/1.1\r\n'
952                  b'Host: localhost\r\nConnection: close\r\n'
953                  b'Content-Length: 29\r\n'
954                  b'Expect: 100-continue\r\n\r\n')
955         fd.flush()
956
957         # Expect 1st 100-continue response
958         header_lines = []
959         while True:
960             line = fd.readline()
961             if line == b'\r\n':
962                 break
963             else:
964                 header_lines.append(line.strip())
965         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
966         headers = dict((k, v) for k, v in (h.split(b': ', 1)
967                                            for h in header_lines[1:]))
968         assert b'Hundred-Continue-Header-1' in headers
969         assert b'Hundred-Continue-Header-2' in headers
970         self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
971         self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
972
973         # Send message 1
974         fd.write(b'first message')
975         fd.flush()
976
977         # Expect a 2nd 100-continue response
978         header_lines = []
979         while True:
980             line = fd.readline()
981             if line == b'\r\n':
982                 break
983             else:
984                 header_lines.append(line.strip())
985         assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
986         headers = dict((k, v) for k, v in (h.split(b': ', 1)
987                                            for h in header_lines[1:]))
988         assert b'Hundred-Continue-Header-3' in headers
989         self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
990
991         # Send message 2
992         fd.write(b', second message\r\n')
993         fd.flush()
994
995         # Expect final 200-OK
996         header_lines = []
997         while True:
998             line = fd.readline()
999             if line == b'\r\n':
1000                 break
1001             else:
1002                 header_lines.append(line.strip())
1003         assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
1004
1005         self.assertEqual(fd.read(29), b'first message, second message')
1006         fd.close()
1007         sock.close()
1008
1009     def test_025_accept_errors(self):
1010         debug.hub_exceptions(True)
1011         listener = greensocket.socket()
1012         listener.bind(('localhost', 0))
1013         # NOT calling listen, to trigger the error
1014         with capture_stderr() as log:
1015             self.spawn_server(sock=listener)
1016             eventlet.sleep(0)  # need to enter server loop
1017             try:
1018                 eventlet.connect(('localhost', self.port))
1019                 self.fail("Didn't expect to connect")
1020             except socket.error as exc:
1021                 self.assertEqual(support.get_errno(exc), errno.ECONNREFUSED)
1022
1023         log_content = log.getvalue()
1024         assert 'Invalid argument' in log_content, log_content
1025         debug.hub_exceptions(False)
1026
1027     def test_026_log_format(self):
1028         self.spawn_server(log_format="HI %(request_line)s HI")
1029         sock = eventlet.connect(('localhost', self.port))
1030         sock.sendall(b'GET /yo! HTTP/1.1\r\nHost: localhost\r\n\r\n')
1031         sock.recv(1024)
1032         sock.close()
1033         assert '\nHI GET /yo! HTTP/1.1 HI\n' in self.logfile.getvalue(), self.logfile.getvalue()
1034
1035     def test_close_chunked_with_1_0_client(self):
1036         # verify that if we return a generator from our app
1037         # and we're not speaking with a 1.1 client, that we
1038         # close the connection
1039         self.site.application = chunked_app
1040         sock = eventlet.connect(('localhost', self.port))
1041
1042         sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1043
1044         result = read_http(sock)
1045         self.assertEqual(result.headers_lower['connection'], 'close')
1046         self.assertNotEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1047         self.assertEqual(result.body, b"thisischunked")
1048
1049     def test_minimum_chunk_size_parameter_leaves_httpprotocol_class_member_intact(self):
1050         start_size = wsgi.HttpProtocol.minimum_chunk_size
1051
1052         self.spawn_server(minimum_chunk_size=start_size * 2)
1053         sock = eventlet.connect(('localhost', self.port))
1054         sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1055         read_http(sock)
1056
1057         self.assertEqual(wsgi.HttpProtocol.minimum_chunk_size, start_size)
1058         sock.close()
1059
1060     def test_error_in_chunked_closes_connection(self):
1061         # From http://rhodesmill.org/brandon/2013/chunked-wsgi/
1062         self.spawn_server(minimum_chunk_size=1)
1063
1064         self.site.application = chunked_fail_app
1065         sock = eventlet.connect(('localhost', self.port))
1066
1067         sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1068
1069         result = read_http(sock)
1070         self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1071         self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1072         expected_body = (
1073             b'27\r\nThe dwarves of yore made mighty spells,\r\n'
1074             b'25\r\nWhile hammers fell like ringing bells\r\n')
1075         self.assertEqual(result.body, expected_body)
1076
1077         # verify that socket is closed by server
1078         self.assertEqual(sock.recv(1), b'')
1079
1080     def test_026_http_10_nokeepalive(self):
1081         # verify that if an http/1.0 client sends connection: keep-alive
1082         # and the server doesn't accept keep-alives, we close the connection
1083         self.spawn_server(keepalive=False)
1084         sock = eventlet.connect(
1085             ('localhost', self.port))
1086
1087         sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1088         result = read_http(sock)
1089         self.assertEqual(result.headers_lower['connection'], 'close')
1090
1091     def test_027_keepalive_chunked(self):
1092         self.site.application = chunked_post
1093         sock = eventlet.connect(('localhost', self.port))
1094         fd = sock.makefile('wb')
1095         common_suffix = (
1096             b'Host: localhost\r\nTransfer-Encoding: chunked\r\n\r\n' +
1097             b'10\r\n0123456789abcdef\r\n0\r\n\r\n')
1098         fd.write(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1099         fd.flush()
1100         read_http(sock)
1101         fd.write(b'PUT /b HTTP/1.1\r\n' + common_suffix)
1102         fd.flush()
1103         read_http(sock)
1104         fd.write(b'PUT /c HTTP/1.1\r\n' + common_suffix)
1105         fd.flush()
1106         read_http(sock)
1107         fd.write(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1108         fd.flush()
1109         read_http(sock)
1110         sock.close()
1111
1112     @tests.skip_if_no_ssl
1113     def test_028_ssl_handshake_errors(self):
1114         errored = [False]
1115
1116         def server(sock):
1117             try:
1118                 wsgi.server(sock=sock, site=hello_world, log=self.logfile)
1119                 errored[0] = 'SSL handshake error caused wsgi.server to exit.'
1120             except greenthread.greenlet.GreenletExit:
1121                 pass
1122             except Exception as e:
1123                 errored[0] = 'SSL handshake error raised exception %s.' % e
1124                 raise
1125         for data in ('', 'GET /non-ssl-request HTTP/1.0\r\n\r\n'):
1126             srv_sock = eventlet.wrap_ssl(
1127                 eventlet.listen(('localhost', 0)),
1128                 certfile=certificate_file, keyfile=private_key_file,
1129                 server_side=True)
1130             port = srv_sock.getsockname()[1]
1131             g = eventlet.spawn_n(server, srv_sock)
1132             client = eventlet.connect(('localhost', port))
1133             if data:  # send non-ssl request
1134                 client.sendall(data.encode())
1135             else:  # close sock prematurely
1136                 client.close()
1137             eventlet.sleep(0)  # let context switch back to server
1138             assert not errored[0], errored[0]
1139             # make another request to ensure the server's still alive
1140             try:
1141                 client = ssl.wrap_socket(eventlet.connect(('localhost', port)))
1142                 client.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
1143                 result = recvall(client)
1144                 assert result.startswith(b'HTTP'), result
1145                 assert result.endswith(b'hello world')
1146             except ImportError:
1147                 pass  # TODO(openssl): should test with OpenSSL
1148             greenthread.kill(g)
1149
1150     def test_029_posthooks(self):
1151         posthook1_count = [0]
1152         posthook2_count = [0]
1153
1154         def posthook1(env, value, multiplier=1):
1155             self.assertEqual(env['local.test'], 'test_029_posthooks')
1156             posthook1_count[0] += value * multiplier
1157
1158         def posthook2(env, value, divisor=1):
1159             self.assertEqual(env['local.test'], 'test_029_posthooks')
1160             posthook2_count[0] += value / divisor
1161
1162         def one_posthook_app(env, start_response):
1163             env['local.test'] = 'test_029_posthooks'
1164             if 'eventlet.posthooks' not in env:
1165                 start_response('500 eventlet.posthooks not supported',
1166                                [('Content-Type', 'text/plain')])
1167             else:
1168                 env['eventlet.posthooks'].append(
1169                     (posthook1, (2,), {'multiplier': 3}))
1170                 start_response('200 OK', [('Content-Type', 'text/plain')])
1171             yield b''
1172         self.site.application = one_posthook_app
1173         sock = eventlet.connect(('localhost', self.port))
1174         fp = sock.makefile('rwb')
1175         fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1176         fp.flush()
1177         self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1178         fp.close()
1179         sock.close()
1180         self.assertEqual(posthook1_count[0], 6)
1181         self.assertEqual(posthook2_count[0], 0)
1182
1183         def two_posthook_app(env, start_response):
1184             env['local.test'] = 'test_029_posthooks'
1185             if 'eventlet.posthooks' not in env:
1186                 start_response('500 eventlet.posthooks not supported',
1187                                [('Content-Type', 'text/plain')])
1188             else:
1189                 env['eventlet.posthooks'].append(
1190                     (posthook1, (4,), {'multiplier': 5}))
1191                 env['eventlet.posthooks'].append(
1192                     (posthook2, (100,), {'divisor': 4}))
1193                 start_response('200 OK', [('Content-Type', 'text/plain')])
1194             yield b''
1195         self.site.application = two_posthook_app
1196         sock = eventlet.connect(('localhost', self.port))
1197         fp = sock.makefile('rwb')
1198         fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1199         fp.flush()
1200         self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1201         fp.close()
1202         sock.close()
1203         self.assertEqual(posthook1_count[0], 26)
1204         self.assertEqual(posthook2_count[0], 25)
1205
1206     def test_030_reject_long_header_lines(self):
1207         sock = eventlet.connect(('localhost', self.port))
1208         request = 'GET / HTTP/1.0\r\nHost: localhost\r\nLong: %s\r\n\r\n' % \
1209             ('a' * 10000)
1210         send_expect_close(sock, request.encode())
1211         result = read_http(sock)
1212         self.assertEqual(result.status, 'HTTP/1.0 400 Header Line Too Long')
1213
1214     def test_031_reject_large_headers(self):
1215         sock = eventlet.connect(('localhost', self.port))
1216         headers = ('Name: %s\r\n' % ('a' * 7000,)) * 20
1217         request = 'GET / HTTP/1.0\r\nHost: localhost\r\n%s\r\n\r\n' % headers
1218         send_expect_close(sock, request.encode())
1219         result = read_http(sock)
1220         self.assertEqual(result.status, 'HTTP/1.0 400 Headers Too Large')
1221
1222     def test_032_wsgi_input_as_iterable(self):
1223         # https://bitbucket.org/eventlet/eventlet/issue/150
1224         # env['wsgi.input'] returns a single byte at a time
1225         # when used as an iterator
1226         g = [0]
1227
1228         def echo_by_iterating(env, start_response):
1229             start_response('200 OK', [('Content-type', 'text/plain')])
1230             for chunk in env['wsgi.input']:
1231                 g[0] += 1
1232                 yield chunk
1233
1234         self.site.application = echo_by_iterating
1235         upload_data = b'123456789abcdef' * 100
1236         request = (
1237             'POST / HTTP/1.0\r\n'
1238             'Host: localhost\r\n'
1239             'Content-Length: %i\r\n\r\n%s'
1240         ) % (len(upload_data), bytes_to_str(upload_data))
1241         sock = eventlet.connect(('localhost', self.port))
1242         fd = sock.makefile('rwb')
1243         fd.write(request.encode())
1244         fd.flush()
1245         result = read_http(sock)
1246         self.assertEqual(result.body, upload_data)
1247         fd.close()
1248         self.assertEqual(g[0], 1)
1249
1250     def test_zero_length_chunked_response(self):
1251         def zero_chunked_app(env, start_response):
1252             start_response('200 OK', [('Content-type', 'text/plain')])
1253             yield b""
1254
1255         self.site.application = zero_chunked_app
1256         sock = eventlet.connect(
1257             ('localhost', self.port))
1258
1259         fd = sock.makefile('rwb')
1260         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1261         fd.flush()
1262         response = fd.read().split(b'\r\n')
1263         headers = []
1264         while True:
1265             h = response.pop(0)
1266             headers.append(h)
1267             if h == b'':
1268                 break
1269         assert b'Transfer-Encoding: chunked' in b''.join(headers), headers
1270         # should only be one chunk of zero size with two blank lines
1271         # (one terminates the chunk, one terminates the body)
1272         self.assertEqual(response, [b'0', b'', b''])
1273
1274     def test_configurable_url_length_limit(self):
1275         self.spawn_server(url_length_limit=20000)
1276         sock = eventlet.connect(
1277             ('localhost', self.port))
1278         path = 'x' * 15000
1279         request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
1280         fd = sock.makefile('rwb')
1281         fd.write(request.encode())
1282         fd.flush()
1283         result = fd.readline()
1284         if result:
1285             # windows closes the socket before the data is flushed,
1286             # so we never get anything back
1287             status = result.split(b' ')[1]
1288             self.assertEqual(status, b'200')
1289         fd.close()
1290
1291     def test_aborted_chunked_post(self):
1292         read_content = event.Event()
1293         blew_up = [False]
1294
1295         def chunk_reader(env, start_response):
1296             try:
1297                 content = env['wsgi.input'].read(1024)
1298             except IOError:
1299                 blew_up[0] = True
1300                 content = b'ok'
1301             read_content.send(content)
1302             start_response('200 OK', [('Content-Type', 'text/plain')])
1303             return [content]
1304         self.site.application = chunk_reader
1305         expected_body = 'a bunch of stuff'
1306         data = "\r\n".join(['PUT /somefile HTTP/1.0',
1307                             'Transfer-Encoding: chunked',
1308                             '',
1309                             'def',
1310                             expected_body])
1311         # start PUT-ing some chunked data but close prematurely
1312         sock = eventlet.connect(('127.0.0.1', self.port))
1313         sock.sendall(data.encode())
1314         sock.close()
1315         # the test passes if we successfully get here, and read all the data
1316         # in spite of the early close
1317         self.assertEqual(read_content.wait(), b'ok')
1318         assert blew_up[0]
1319
1320     def test_exceptions_close_connection(self):
1321         def wsgi_app(environ, start_response):
1322             raise RuntimeError("intentional error")
1323         self.site.application = wsgi_app
1324         sock = eventlet.connect(('localhost', self.port))
1325         fd = sock.makefile('rwb')
1326         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1327         fd.flush()
1328         result = read_http(sock)
1329         self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1330         self.assertEqual(result.headers_lower['connection'], 'close')
1331         assert 'transfer-encoding' not in result.headers_lower
1332
1333     def test_unicode_raises_error(self):
1334         def wsgi_app(environ, start_response):
1335             start_response("200 OK", [])
1336             yield u"oh hai"
1337             yield u"non-encodable unicode: \u0230"
1338         self.site.application = wsgi_app
1339         sock = eventlet.connect(('localhost', self.port))
1340         fd = sock.makefile('rwb')
1341         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1342         fd.flush()
1343         result = read_http(sock)
1344         self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1345         self.assertEqual(result.headers_lower['connection'], 'close')
1346         assert b'unicode' in result.body
1347
1348     def test_path_info_decoding(self):
1349         def wsgi_app(environ, start_response):
1350             start_response("200 OK", [])
1351             yield six.b("decoded: %s" % environ['PATH_INFO'])
1352             yield six.b("raw: %s" % environ['RAW_PATH_INFO'])
1353         self.site.application = wsgi_app
1354         sock = eventlet.connect(('localhost', self.port))
1355         fd = sock.makefile('rwb')
1356         fd.write(b'GET /a*b@%40%233 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1357         fd.flush()
1358         result = read_http(sock)
1359         self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1360         assert b'decoded: /a*b@@#3' in result.body
1361         assert b'raw: /a*b@%40%233' in result.body
1362
1363     def test_ipv6(self):
1364         try:
1365             sock = eventlet.listen(('::1', 0), family=socket.AF_INET6)
1366         except (socket.gaierror, socket.error):  # probably no ipv6
1367             return
1368         log = six.StringIO()
1369         # first thing the server does is try to log the IP it's bound to
1370
1371         def run_server():
1372             try:
1373                 wsgi.server(sock=sock, log=log, site=Site())
1374             except ValueError:
1375                 log.write(b'broken')
1376
1377         self.spawn_thread(run_server)
1378
1379         logval = log.getvalue()
1380         while not logval:
1381             eventlet.sleep(0.0)
1382             logval = log.getvalue()
1383         if 'broked' in logval:
1384             self.fail('WSGI server raised exception with ipv6 socket')
1385
1386     def test_debug(self):
1387         self.spawn_server(debug=False)
1388
1389         def crasher(env, start_response):
1390             raise RuntimeError("intentional crash")
1391         self.site.application = crasher
1392
1393         sock = eventlet.connect(('localhost', self.port))
1394         fd = sock.makefile('wb')
1395         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1396         fd.flush()
1397         result1 = read_http(sock)
1398         self.assertEqual(result1.status, 'HTTP/1.1 500 Internal Server Error')
1399         self.assertEqual(result1.body, b'')
1400         self.assertEqual(result1.headers_lower['connection'], 'close')
1401         assert 'transfer-encoding' not in result1.headers_lower
1402
1403         # verify traceback when debugging enabled
1404         self.spawn_server(debug=True)
1405         self.site.application = crasher
1406         sock = eventlet.connect(('localhost', self.port))
1407         fd = sock.makefile('wb')
1408         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1409         fd.flush()
1410         result2 = read_http(sock)
1411         self.assertEqual(result2.status, 'HTTP/1.1 500 Internal Server Error')
1412         assert b'intentional crash' in result2.body, result2.body
1413         assert b'RuntimeError' in result2.body, result2.body
1414         assert b'Traceback' in result2.body, result2.body
1415         self.assertEqual(result2.headers_lower['connection'], 'close')
1416         assert 'transfer-encoding' not in result2.headers_lower
1417
1418     def test_client_disconnect(self):
1419         """Issue #95 Server must handle disconnect from client in the middle of response
1420         """
1421         def long_response(environ, start_response):
1422             start_response('200 OK', [('Content-Length', '9876')])
1423             yield b'a' * 9876
1424
1425         server_sock = eventlet.listen(('localhost', 0))
1426         self.port = server_sock.getsockname()[1]
1427         server = wsgi.Server(server_sock, server_sock.getsockname(), long_response,
1428                              log=self.logfile)
1429
1430         def make_request():
1431             sock = eventlet.connect(server_sock.getsockname())
1432             sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1433             sock.close()
1434
1435         request_thread = eventlet.spawn(make_request)
1436         server_conn = server_sock.accept()
1437         # Next line must not raise IOError -32 Broken pipe
1438         server.process_request(server_conn)
1439         request_thread.wait()
1440         server_sock.close()
1441
1442     def test_server_connection_timeout_exception(self):
1443         # Handle connection socket timeouts
1444         # https://bitbucket.org/eventlet/eventlet/issue/143/
1445         # Runs tests.wsgi_test_conntimeout in a separate process.
1446         testcode_path = os.path.join(
1447             os.path.dirname(os.path.abspath(__file__)),
1448             'wsgi_test_conntimeout.py')
1449         output = tests.run_python(testcode_path)
1450         sections = output.split(b"SEPERATOR_SENTINEL")
1451         # first section is empty
1452         self.assertEqual(3, len(sections), output)
1453         # if the "BOOM" check fails, it's because our timeout didn't happen
1454         # (if eventlet stops using file.readline() to read HTTP headers,
1455         # for instance)
1456         for runlog in sections[1:]:
1457             debug = False if "debug set to: False" in runlog else True
1458             if debug:
1459                 self.assertTrue("timed out" in runlog)
1460             self.assertTrue("BOOM" in runlog)
1461             self.assertFalse("Traceback" in runlog)
1462
1463     def test_server_socket_timeout(self):
1464         self.spawn_server(socket_timeout=0.1)
1465         sock = eventlet.connect(('localhost', self.port))
1466         sock.send(b'GET / HTTP/1.1\r\n')
1467         eventlet.sleep(0.1)
1468         try:
1469             read_http(sock)
1470             assert False, 'Expected ConnectionClosed exception'
1471         except ConnectionClosed:
1472             pass
1473
1474     def test_disable_header_name_capitalization(self):
1475         # Disable HTTP header name capitalization
1476         #
1477         # https://github.com/eventlet/eventlet/issues/80
1478         random_case_header = ('eTAg', 'TAg-VAluE')
1479
1480         def wsgi_app(environ, start_response):
1481             start_response('200 oK', [random_case_header])
1482             return [b'']
1483
1484         self.spawn_server(site=wsgi_app, capitalize_response_headers=False)
1485
1486         sock = eventlet.connect(('localhost', self.port))
1487         sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1488         result = read_http(sock)
1489         sock.close()
1490         self.assertEqual(result.status, 'HTTP/1.1 200 oK')
1491         self.assertEqual(result.headers_lower[random_case_header[0].lower()], random_case_header[1])
1492         self.assertEqual(result.headers_original[random_case_header[0]], random_case_header[1])
1493
1494
1495 def read_headers(sock):
1496     fd = sock.makefile('rb')
1497     try:
1498         response_line = fd.readline()
1499     except socket.error as exc:
1500         if support.get_errno(exc) == 10053:
1501             raise ConnectionClosed
1502         raise
1503     if not response_line:
1504         raise ConnectionClosed
1505
1506     header_lines = []
1507     while True:
1508         line = fd.readline()
1509         if line == b'\r\n':
1510             break
1511         else:
1512             header_lines.append(line)
1513     headers = dict()
1514     for x in header_lines:
1515         x = x.strip()
1516         if not x:
1517             continue
1518         key, value = x.split(b': ', 1)
1519         assert key.lower() not in headers, "%s header duplicated" % key
1520         headers[bytes_to_str(key.lower())] = bytes_to_str(value)
1521     return bytes_to_str(response_line), headers
1522
1523
1524 class IterableAlreadyHandledTest(_TestBase):
1525     def set_site(self):
1526         self.site = IterableSite()
1527
1528     def get_app(self):
1529         return IterableApp(True)
1530
1531     def test_iterable_app_keeps_socket_open_unless_connection_close_sent(self):
1532         self.site.application = self.get_app()
1533         sock = eventlet.connect(
1534             ('localhost', self.port))
1535
1536         fd = sock.makefile('rwb')
1537         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1538
1539         fd.flush()
1540         response_line, headers = read_headers(sock)
1541         self.assertEqual(response_line, 'HTTP/1.1 200 OK\r\n')
1542         assert 'connection' not in headers
1543         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1544         fd.flush()
1545         result = read_http(sock)
1546         self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1547         self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1548         self.assertEqual(result.body, b'0\r\n\r\n')  # Still coming back chunked
1549
1550
1551 class ProxiedIterableAlreadyHandledTest(IterableAlreadyHandledTest):
1552     # same thing as the previous test but ensuring that it works with tpooled
1553     # results as well as regular ones
1554     @tests.skip_with_pyevent
1555     def get_app(self):
1556         return tpool.Proxy(super(ProxiedIterableAlreadyHandledTest, self).get_app())
1557
1558     def tearDown(self):
1559         tpool.killall()
1560         super(ProxiedIterableAlreadyHandledTest, self).tearDown()
1561
1562
1563 class TestChunkedInput(_TestBase):
1564     dirt = ""
1565     validator = None
1566
1567     def application(self, env, start_response):
1568         input = env['wsgi.input']
1569         response = []
1570
1571         pi = env["PATH_INFO"]
1572
1573         if pi == "/short-read":
1574             d = input.read(10)
1575             response = [d]
1576         elif pi == "/lines":
1577             for x in input:
1578                 response.append(x)
1579         elif pi == "/ping":
1580             input.read()
1581             response.append(b"pong")
1582         elif pi.startswith("/yield_spaces"):
1583             if pi.endswith('override_min'):
1584                 env['eventlet.minimum_write_chunk_size'] = 1
1585             self.yield_next_space = False
1586
1587             def response_iter():
1588                 yield b' '
1589                 num_sleeps = 0
1590                 while not self.yield_next_space and num_sleeps < 200:
1591                     eventlet.sleep(.01)
1592                     num_sleeps += 1
1593
1594                 yield b' '
1595
1596             start_response('200 OK',
1597                            [('Content-Type', 'text/plain'),
1598                             ('Content-Length', '2')])
1599             return response_iter()
1600         else:
1601             raise RuntimeError("bad path")
1602
1603         start_response('200 OK', [('Content-Type', 'text/plain')])
1604         return response
1605
1606     def connect(self):
1607         return eventlet.connect(('localhost', self.port))
1608
1609     def set_site(self):
1610         self.site = Site()
1611         self.site.application = self.application
1612
1613     def chunk_encode(self, chunks, dirt=None):
1614         if dirt is None:
1615             dirt = self.dirt
1616
1617         b = ""
1618         for c in chunks:
1619             b += "%x%s\r\n%s\r\n" % (len(c), dirt, c)
1620         return b
1621
1622     def body(self, dirt=None):
1623         return self.chunk_encode(["this", " is ", "chunked", "\nline",
1624                                   " 2", "\n", "line3", ""], dirt=dirt)
1625
1626     def ping(self, fd):
1627         fd.sendall(b"GET /ping HTTP/1.1\r\n\r\n")
1628         self.assertEqual(read_http(fd).body, b"pong")
1629
1630     def test_short_read_with_content_length(self):
1631         body = self.body()
1632         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1633               "Content-Length:1000\r\n\r\n" + body
1634
1635         fd = self.connect()
1636         fd.sendall(req.encode())
1637         self.assertEqual(read_http(fd).body, b"this is ch")
1638
1639         self.ping(fd)
1640         fd.close()
1641
1642     def test_short_read_with_zero_content_length(self):
1643         body = self.body()
1644         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1645               "Content-Length:0\r\n\r\n" + body
1646         fd = self.connect()
1647         fd.sendall(req.encode())
1648         self.assertEqual(read_http(fd).body, b"this is ch")
1649
1650         self.ping(fd)
1651         fd.close()
1652
1653     def test_short_read(self):
1654         body = self.body()
1655         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1656
1657         fd = self.connect()
1658         fd.sendall(req.encode())
1659         self.assertEqual(read_http(fd).body, b"this is ch")
1660
1661         self.ping(fd)
1662         fd.close()
1663
1664     def test_dirt(self):
1665         body = self.body(dirt="; here is dirt\0bla")
1666         req = "POST /ping HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1667
1668         fd = self.connect()
1669         fd.sendall(req.encode())
1670         self.assertEqual(read_http(fd).body, b"pong")
1671
1672         self.ping(fd)
1673         fd.close()
1674
1675     def test_chunked_readline(self):
1676         body = self.body()
1677         req = "POST /lines HTTP/1.1\r\nContent-Length: %s\r\n" \
1678               "transfer-encoding: Chunked\r\n\r\n%s" % (len(body), body)
1679
1680         fd = self.connect()
1681         fd.sendall(req.encode())
1682         self.assertEqual(read_http(fd).body, b'this is chunked\nline 2\nline3')
1683         fd.close()
1684
1685     def test_chunked_readline_wsgi_override_minimum_chunk_size(self):
1686
1687         fd = self.connect()
1688         fd.sendall(b"POST /yield_spaces/override_min HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1689
1690         resp_so_far = b''
1691         with eventlet.Timeout(.1):
1692             while True:
1693                 one_byte = fd.recv(1)
1694                 resp_so_far += one_byte
1695                 if resp_so_far.endswith(b'\r\n\r\n'):
1696                     break
1697             self.assertEqual(fd.recv(1), b' ')
1698         try:
1699             with eventlet.Timeout(.1):
1700                 fd.recv(1)
1701         except eventlet.Timeout:
1702             pass
1703         else:
1704             assert False
1705         self.yield_next_space = True
1706
1707         with eventlet.Timeout(.1):
1708             self.assertEqual(fd.recv(1), b' ')
1709
1710     def test_chunked_readline_wsgi_not_override_minimum_chunk_size(self):
1711
1712         fd = self.connect()
1713         fd.sendall(b"POST /yield_spaces HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1714
1715         resp_so_far = b''
1716         try:
1717             with eventlet.Timeout(.1):
1718                 while True:
1719                     one_byte = fd.recv(1)
1720                     resp_so_far += one_byte
1721                     if resp_so_far.endswith(b'\r\n\r\n'):
1722                         break
1723                 self.assertEqual(fd.recv(1), b' ')
1724         except eventlet.Timeout:
1725             pass
1726         else:
1727             assert False
1728
1729     def test_close_before_finished(self):
1730         got_signal = []
1731
1732         def handler(*args):
1733             got_signal.append(1)
1734             raise KeyboardInterrupt()
1735
1736         signal.signal(signal.SIGALRM, handler)
1737         signal.alarm(1)
1738
1739         try:
1740             body = '4\r\nthi'
1741             req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1742
1743             fd = self.connect()
1744             fd.sendall(req.encode())
1745             fd.close()
1746             eventlet.sleep(0.0)
1747         finally:
1748             signal.alarm(0)
1749             signal.signal(signal.SIGALRM, signal.SIG_DFL)
1750
1751         assert not got_signal, "caught alarm signal. infinite loop detected."
1752
1753
1754 if __name__ == '__main__':
1755     unittest.main()