d5cea188f212505f28bc55b495b7a44a13b70019
[packages/trusty/python-eventlet.git] / python-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_with_only_ascii_characters_works(self):
1334         def wsgi_app(environ, start_response):
1335             start_response("200 OK", [])
1336             yield b"oh hai, "
1337             yield u"xxx"
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         assert b'xxx' in result.body
1345
1346     def test_unicode_with_nonascii_characters_raises_error(self):
1347         def wsgi_app(environ, start_response):
1348             start_response("200 OK", [])
1349             yield b"oh hai, "
1350             yield u"xxx \u0230"
1351         self.site.application = wsgi_app
1352         sock = eventlet.connect(('localhost', self.port))
1353         fd = sock.makefile('rwb')
1354         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1355         fd.flush()
1356         result = read_http(sock)
1357         self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1358         self.assertEqual(result.headers_lower['connection'], 'close')
1359
1360     def test_path_info_decoding(self):
1361         def wsgi_app(environ, start_response):
1362             start_response("200 OK", [])
1363             yield six.b("decoded: %s" % environ['PATH_INFO'])
1364             yield six.b("raw: %s" % environ['RAW_PATH_INFO'])
1365         self.site.application = wsgi_app
1366         sock = eventlet.connect(('localhost', self.port))
1367         fd = sock.makefile('rwb')
1368         fd.write(b'GET /a*b@%40%233 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1369         fd.flush()
1370         result = read_http(sock)
1371         self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1372         assert b'decoded: /a*b@@#3' in result.body
1373         assert b'raw: /a*b@%40%233' in result.body
1374
1375     def test_ipv6(self):
1376         try:
1377             sock = eventlet.listen(('::1', 0), family=socket.AF_INET6)
1378         except (socket.gaierror, socket.error):  # probably no ipv6
1379             return
1380         log = six.StringIO()
1381         # first thing the server does is try to log the IP it's bound to
1382
1383         def run_server():
1384             try:
1385                 wsgi.server(sock=sock, log=log, site=Site())
1386             except ValueError:
1387                 log.write(b'broken')
1388
1389         self.spawn_thread(run_server)
1390
1391         logval = log.getvalue()
1392         while not logval:
1393             eventlet.sleep(0.0)
1394             logval = log.getvalue()
1395         if 'broked' in logval:
1396             self.fail('WSGI server raised exception with ipv6 socket')
1397
1398     def test_debug(self):
1399         self.spawn_server(debug=False)
1400
1401         def crasher(env, start_response):
1402             raise RuntimeError("intentional crash")
1403         self.site.application = crasher
1404
1405         sock = eventlet.connect(('localhost', self.port))
1406         fd = sock.makefile('wb')
1407         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1408         fd.flush()
1409         result1 = read_http(sock)
1410         self.assertEqual(result1.status, 'HTTP/1.1 500 Internal Server Error')
1411         self.assertEqual(result1.body, b'')
1412         self.assertEqual(result1.headers_lower['connection'], 'close')
1413         assert 'transfer-encoding' not in result1.headers_lower
1414
1415         # verify traceback when debugging enabled
1416         self.spawn_server(debug=True)
1417         self.site.application = crasher
1418         sock = eventlet.connect(('localhost', self.port))
1419         fd = sock.makefile('wb')
1420         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1421         fd.flush()
1422         result2 = read_http(sock)
1423         self.assertEqual(result2.status, 'HTTP/1.1 500 Internal Server Error')
1424         assert b'intentional crash' in result2.body, result2.body
1425         assert b'RuntimeError' in result2.body, result2.body
1426         assert b'Traceback' in result2.body, result2.body
1427         self.assertEqual(result2.headers_lower['connection'], 'close')
1428         assert 'transfer-encoding' not in result2.headers_lower
1429
1430     def test_client_disconnect(self):
1431         """Issue #95 Server must handle disconnect from client in the middle of response
1432         """
1433         def long_response(environ, start_response):
1434             start_response('200 OK', [('Content-Length', '9876')])
1435             yield b'a' * 9876
1436
1437         server_sock = eventlet.listen(('localhost', 0))
1438         self.port = server_sock.getsockname()[1]
1439         server = wsgi.Server(server_sock, server_sock.getsockname(), long_response,
1440                              log=self.logfile)
1441
1442         def make_request():
1443             sock = eventlet.connect(server_sock.getsockname())
1444             sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1445             sock.close()
1446
1447         request_thread = eventlet.spawn(make_request)
1448         server_conn = server_sock.accept()
1449         # Next line must not raise IOError -32 Broken pipe
1450         server.process_request(server_conn)
1451         request_thread.wait()
1452         server_sock.close()
1453
1454     def test_server_connection_timeout_exception(self):
1455         # Handle connection socket timeouts
1456         # https://bitbucket.org/eventlet/eventlet/issue/143/
1457         # Runs tests.wsgi_test_conntimeout in a separate process.
1458         tests.run_isolated('wsgi_connection_timeout.py')
1459
1460     def test_server_socket_timeout(self):
1461         self.spawn_server(socket_timeout=0.1)
1462         sock = eventlet.connect(('localhost', self.port))
1463         sock.send(b'GET / HTTP/1.1\r\n')
1464         eventlet.sleep(0.1)
1465         try:
1466             read_http(sock)
1467             assert False, 'Expected ConnectionClosed exception'
1468         except ConnectionClosed:
1469             pass
1470
1471     def test_disable_header_name_capitalization(self):
1472         # Disable HTTP header name capitalization
1473         #
1474         # https://github.com/eventlet/eventlet/issues/80
1475         random_case_header = ('eTAg', 'TAg-VAluE')
1476
1477         def wsgi_app(environ, start_response):
1478             start_response('200 oK', [random_case_header])
1479             return [b'']
1480
1481         self.spawn_server(site=wsgi_app, capitalize_response_headers=False)
1482
1483         sock = eventlet.connect(('localhost', self.port))
1484         sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1485         result = read_http(sock)
1486         sock.close()
1487         self.assertEqual(result.status, 'HTTP/1.1 200 oK')
1488         self.assertEqual(result.headers_lower[random_case_header[0].lower()], random_case_header[1])
1489         self.assertEqual(result.headers_original[random_case_header[0]], random_case_header[1])
1490
1491
1492 def read_headers(sock):
1493     fd = sock.makefile('rb')
1494     try:
1495         response_line = fd.readline()
1496     except socket.error as exc:
1497         if support.get_errno(exc) == 10053:
1498             raise ConnectionClosed
1499         raise
1500     if not response_line:
1501         raise ConnectionClosed
1502
1503     header_lines = []
1504     while True:
1505         line = fd.readline()
1506         if line == b'\r\n':
1507             break
1508         else:
1509             header_lines.append(line)
1510     headers = dict()
1511     for x in header_lines:
1512         x = x.strip()
1513         if not x:
1514             continue
1515         key, value = x.split(b': ', 1)
1516         assert key.lower() not in headers, "%s header duplicated" % key
1517         headers[bytes_to_str(key.lower())] = bytes_to_str(value)
1518     return bytes_to_str(response_line), headers
1519
1520
1521 class IterableAlreadyHandledTest(_TestBase):
1522     def set_site(self):
1523         self.site = IterableSite()
1524
1525     def get_app(self):
1526         return IterableApp(True)
1527
1528     def test_iterable_app_keeps_socket_open_unless_connection_close_sent(self):
1529         self.site.application = self.get_app()
1530         sock = eventlet.connect(
1531             ('localhost', self.port))
1532
1533         fd = sock.makefile('rwb')
1534         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1535
1536         fd.flush()
1537         response_line, headers = read_headers(sock)
1538         self.assertEqual(response_line, 'HTTP/1.1 200 OK\r\n')
1539         assert 'connection' not in headers
1540         fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1541         fd.flush()
1542         result = read_http(sock)
1543         self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1544         self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1545         self.assertEqual(result.body, b'0\r\n\r\n')  # Still coming back chunked
1546
1547
1548 class ProxiedIterableAlreadyHandledTest(IterableAlreadyHandledTest):
1549     # same thing as the previous test but ensuring that it works with tpooled
1550     # results as well as regular ones
1551     @tests.skip_with_pyevent
1552     def get_app(self):
1553         return tpool.Proxy(super(ProxiedIterableAlreadyHandledTest, self).get_app())
1554
1555     def tearDown(self):
1556         tpool.killall()
1557         super(ProxiedIterableAlreadyHandledTest, self).tearDown()
1558
1559
1560 class TestChunkedInput(_TestBase):
1561     dirt = ""
1562     validator = None
1563
1564     def application(self, env, start_response):
1565         input = env['wsgi.input']
1566         response = []
1567
1568         pi = env["PATH_INFO"]
1569
1570         if pi == "/short-read":
1571             d = input.read(10)
1572             response = [d]
1573         elif pi == "/lines":
1574             for x in input:
1575                 response.append(x)
1576         elif pi == "/ping":
1577             input.read()
1578             response.append(b"pong")
1579         elif pi.startswith("/yield_spaces"):
1580             if pi.endswith('override_min'):
1581                 env['eventlet.minimum_write_chunk_size'] = 1
1582             self.yield_next_space = False
1583
1584             def response_iter():
1585                 yield b' '
1586                 num_sleeps = 0
1587                 while not self.yield_next_space and num_sleeps < 200:
1588                     eventlet.sleep(.01)
1589                     num_sleeps += 1
1590
1591                 yield b' '
1592
1593             start_response('200 OK',
1594                            [('Content-Type', 'text/plain'),
1595                             ('Content-Length', '2')])
1596             return response_iter()
1597         else:
1598             raise RuntimeError("bad path")
1599
1600         start_response('200 OK', [('Content-Type', 'text/plain')])
1601         return response
1602
1603     def connect(self):
1604         return eventlet.connect(('localhost', self.port))
1605
1606     def set_site(self):
1607         self.site = Site()
1608         self.site.application = self.application
1609
1610     def chunk_encode(self, chunks, dirt=None):
1611         if dirt is None:
1612             dirt = self.dirt
1613
1614         b = ""
1615         for c in chunks:
1616             b += "%x%s\r\n%s\r\n" % (len(c), dirt, c)
1617         return b
1618
1619     def body(self, dirt=None):
1620         return self.chunk_encode(["this", " is ", "chunked", "\nline",
1621                                   " 2", "\n", "line3", ""], dirt=dirt)
1622
1623     def ping(self, fd):
1624         fd.sendall(b"GET /ping HTTP/1.1\r\n\r\n")
1625         self.assertEqual(read_http(fd).body, b"pong")
1626
1627     def test_short_read_with_content_length(self):
1628         body = self.body()
1629         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1630               "Content-Length:1000\r\n\r\n" + body
1631
1632         fd = self.connect()
1633         fd.sendall(req.encode())
1634         self.assertEqual(read_http(fd).body, b"this is ch")
1635
1636         self.ping(fd)
1637         fd.close()
1638
1639     def test_short_read_with_zero_content_length(self):
1640         body = self.body()
1641         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1642               "Content-Length:0\r\n\r\n" + body
1643         fd = self.connect()
1644         fd.sendall(req.encode())
1645         self.assertEqual(read_http(fd).body, b"this is ch")
1646
1647         self.ping(fd)
1648         fd.close()
1649
1650     def test_short_read(self):
1651         body = self.body()
1652         req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1653
1654         fd = self.connect()
1655         fd.sendall(req.encode())
1656         self.assertEqual(read_http(fd).body, b"this is ch")
1657
1658         self.ping(fd)
1659         fd.close()
1660
1661     def test_dirt(self):
1662         body = self.body(dirt="; here is dirt\0bla")
1663         req = "POST /ping HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1664
1665         fd = self.connect()
1666         fd.sendall(req.encode())
1667         self.assertEqual(read_http(fd).body, b"pong")
1668
1669         self.ping(fd)
1670         fd.close()
1671
1672     def test_chunked_readline(self):
1673         body = self.body()
1674         req = "POST /lines HTTP/1.1\r\nContent-Length: %s\r\n" \
1675               "transfer-encoding: Chunked\r\n\r\n%s" % (len(body), body)
1676
1677         fd = self.connect()
1678         fd.sendall(req.encode())
1679         self.assertEqual(read_http(fd).body, b'this is chunked\nline 2\nline3')
1680         fd.close()
1681
1682     def test_chunked_readline_wsgi_override_minimum_chunk_size(self):
1683
1684         fd = self.connect()
1685         fd.sendall(b"POST /yield_spaces/override_min HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1686
1687         resp_so_far = b''
1688         with eventlet.Timeout(.1):
1689             while True:
1690                 one_byte = fd.recv(1)
1691                 resp_so_far += one_byte
1692                 if resp_so_far.endswith(b'\r\n\r\n'):
1693                     break
1694             self.assertEqual(fd.recv(1), b' ')
1695         try:
1696             with eventlet.Timeout(.1):
1697                 fd.recv(1)
1698         except eventlet.Timeout:
1699             pass
1700         else:
1701             assert False
1702         self.yield_next_space = True
1703
1704         with eventlet.Timeout(.1):
1705             self.assertEqual(fd.recv(1), b' ')
1706
1707     def test_chunked_readline_wsgi_not_override_minimum_chunk_size(self):
1708
1709         fd = self.connect()
1710         fd.sendall(b"POST /yield_spaces HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1711
1712         resp_so_far = b''
1713         try:
1714             with eventlet.Timeout(.1):
1715                 while True:
1716                     one_byte = fd.recv(1)
1717                     resp_so_far += one_byte
1718                     if resp_so_far.endswith(b'\r\n\r\n'):
1719                         break
1720                 self.assertEqual(fd.recv(1), b' ')
1721         except eventlet.Timeout:
1722             pass
1723         else:
1724             assert False
1725
1726     def test_close_before_finished(self):
1727         got_signal = []
1728
1729         def handler(*args):
1730             got_signal.append(1)
1731             raise KeyboardInterrupt()
1732
1733         signal.signal(signal.SIGALRM, handler)
1734         signal.alarm(1)
1735
1736         try:
1737             body = '4\r\nthi'
1738             req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1739
1740             fd = self.connect()
1741             fd.sendall(req.encode())
1742             fd.close()
1743
1744             eventlet.sleep(0)
1745
1746             # This is needed because on Python 3 GreenSocket.recv_into is called
1747             # rather than recv; recv_into right now (git 5ec3a3c) trampolines to
1748             # the hub *before* attempting to read anything from a file descriptor
1749             # therefore we need one extra context switch to let it notice closed
1750             # socket, die and leave the hub empty
1751             if six.PY3:
1752                 eventlet.sleep(0)
1753         finally:
1754             signal.alarm(0)
1755             signal.signal(signal.SIGALRM, signal.SIG_DFL)
1756
1757         assert not got_signal, "caught alarm signal. infinite loop detected."
1758
1759
1760 if __name__ == '__main__':
1761     unittest.main()