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