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