12 from eventlet import debug
13 from eventlet import event
14 from eventlet.green import socket as greensocket
15 from eventlet.green import ssl
16 from eventlet.green import subprocess
17 from eventlet import greenio
18 from eventlet import greenthread
19 from eventlet import support
20 from eventlet.support import bytes_to_str, capture_stderr, six
21 from eventlet import tpool
22 from eventlet import wsgi
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')
31 HttpReadResult = collections.namedtuple(
33 'status headers_lower body headers_original')
36 def hello_world(env, start_response):
37 if env['PATH_INFO'] == 'notexist':
38 start_response('404 Not Found', [('Content-type', 'text/plain')])
41 start_response('200 OK', [('Content-type', 'text/plain')])
42 return [b"hello world"]
45 def chunked_app(env, start_response):
46 start_response('200 OK', [('Content-type', 'text/plain')])
52 def chunked_fail_app(environ, start_response):
53 """http://rhodesmill.org/brandon/2013/chunked-wsgi/
55 headers = [('Content-Type', 'text/plain')]
56 start_response('200 OK', headers)
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"
62 # Then the back-end fails!
66 start_response('500 Error', headers, sys.exc_info())
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."
74 def big_chunks(env, start_response):
75 start_response('200 OK', [('Content-type', 'text/plain')])
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')])
86 if env['PATH_INFO'] == '/b':
87 write = start_response('200 OK', [('Content-type', 'text/plain')])
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'')]
102 def already_handled(env, start_response):
103 start_response('200 OK', [('Content-type', 'text/plain')])
104 return wsgi.ALREADY_HANDLED
109 self.application = hello_world
111 def __call__(self, env, start_response):
112 return self.application(env, start_response)
115 class IterableApp(object):
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
122 def __call__(self, env, start_response):
124 if self.send_start_response:
125 start_response('200 OK', [('Content-type', 'text/plain')])
126 return self.return_val
129 class IterableSite(Site):
130 def __call__(self, env, start_response):
131 it = self.application(env, start_response)
136 CONTENT_LENGTH = 'content-length'
148 def recvall(socket_):
151 chunk = socket_.recv()
159 class ConnectionClosed(Exception):
163 def send_expect_close(sock, buf):
164 # Some tests will induce behavior that causes the remote end to
165 # close the connection before all of the data has been written.
166 # With small kernel buffer sizes, this can cause an EPIPE error.
167 # Since the test expects an early close, this can be ignored.
170 except socket.error as exc:
171 if support.get_errno(exc) != errno.EPIPE:
176 fd = sock.makefile('rb')
178 response_line = bytes_to_str(fd.readline().rstrip(b'\r\n'))
179 except socket.error as exc:
180 # TODO find out whether 54 is ok here or not, I see it when running tests
182 if support.get_errno(exc) in (10053, 54):
183 raise ConnectionClosed
185 if not response_line:
186 raise ConnectionClosed(response_line)
194 header_lines.append(line)
196 headers_original = {}
198 for x in header_lines:
202 key, value = bytes_to_str(x).split(':', 1)
204 value = value.lstrip()
205 key_lower = key.lower()
206 # FIXME: Duplicate headers are allowed as per HTTP RFC standard,
207 # the client and/or intermediate proxies are supposed to treat them
208 # as a single header with values concatenated using space (' ') delimiter.
209 assert key_lower not in headers_lower, "header duplicated: {0}".format(key)
210 headers_original[key] = value
211 headers_lower[key_lower] = value
213 content_length_str = headers_lower.get(CONTENT_LENGTH.lower(), '')
214 if content_length_str:
215 num = int(content_length_str)
221 result = HttpReadResult(
222 status=response_line,
223 headers_lower=headers_lower,
225 headers_original=headers_original)
229 class _TestBase(tests.LimitedTestCase):
231 super(_TestBase, self).setUp()
232 self.logfile = six.StringIO()
239 greenthread.kill(self.killer)
241 super(_TestBase, self).tearDown()
243 def spawn_server(self, **kwargs):
244 """Spawns a new wsgi server with the given arguments using
245 :meth:`spawn_thread`.
247 Sets self.port to the port of the server
249 new_kwargs = dict(max_size=128,
252 new_kwargs.update(kwargs)
254 if 'sock' not in new_kwargs:
255 new_kwargs['sock'] = eventlet.listen(('localhost', 0))
257 self.port = new_kwargs['sock'].getsockname()[1]
258 self.spawn_thread(wsgi.server, **new_kwargs)
260 def spawn_thread(self, target, **kwargs):
261 """Spawns a new greenthread using specified target and arguments.
263 Kills any previously-running server and sets self.killer to the
264 greenthread running the target.
266 eventlet.sleep(0) # give previous server a chance to start
268 greenthread.kill(self.killer)
270 self.killer = eventlet.spawn_n(target, **kwargs)
273 raise NotImplementedError
276 class TestHttpd(_TestBase):
280 def test_001_server(self):
281 sock = eventlet.connect(
282 ('localhost', self.port))
284 fd = sock.makefile('rwb')
285 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
289 # The server responds with the maximum version it supports
290 assert result.startswith(b'HTTP'), result
291 assert result.endswith(b'hello world'), result
293 def test_002_keepalive(self):
294 sock = eventlet.connect(
295 ('localhost', self.port))
297 fd = sock.makefile('wb')
298 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
301 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
307 def test_003_passing_non_int_to_read(self):
308 # This should go in greenio_test
309 sock = eventlet.connect(
310 ('localhost', self.port))
312 fd = sock.makefile('rwb')
313 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
315 cancel = eventlet.Timeout(1, RuntimeError)
316 self.assertRaises(TypeError, fd.read, "This shouldn't work")
320 def test_004_close_keepalive(self):
321 sock = eventlet.connect(
322 ('localhost', self.port))
324 fd = sock.makefile('wb')
325 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
328 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
331 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
333 self.assertRaises(ConnectionClosed, read_http, sock)
337 def test_005_run_apachebench(self):
338 url = 'http://localhost:12346/'
341 [tests.find_command('ab'), '-c', '64', '-n', '1024', '-k', url],
342 stdout=subprocess.PIPE)
344 def test_006_reject_long_urls(self):
345 sock = eventlet.connect(
346 ('localhost', self.port))
348 for ii in range(3000):
349 path_parts.append('path')
350 path = '/'.join(path_parts)
351 request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
352 send_expect_close(sock, request.encode())
353 fd = sock.makefile('rb')
354 result = fd.readline()
356 # windows closes the socket before the data is flushed,
357 # so we never get anything back
358 status = result.split(b' ')[1]
359 self.assertEqual(status, b'414')
362 def test_007_get_arg(self):
363 # define a new handler that does a get_arg as well as a read_body
364 def new_app(env, start_response):
365 body = bytes_to_str(env['wsgi.input'].read())
366 a = cgi.parse_qs(body).get('a', [1])[0]
367 start_response('200 OK', [('Content-type', 'text/plain')])
368 return [six.b('a is %s, body is %s' % (a, body))]
370 self.site.application = new_app
371 sock = eventlet.connect(
372 ('localhost', self.port))
373 request = '\r\n'.join((
379 fd = sock.makefile('wb')
380 fd.write(request.encode())
383 # send some junk after the actual request
384 fd.write(b'01234567890123456789')
385 result = read_http(sock)
386 self.assertEqual(result.body, b'a is a, body is a=a')
389 def test_008_correctresponse(self):
390 sock = eventlet.connect(('localhost', self.port))
392 fd = sock.makefile('wb')
393 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
395 result_200 = read_http(sock)
396 fd.write(b'GET /notexist HTTP/1.1\r\nHost: localhost\r\n\r\n')
399 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
401 result_test = read_http(sock)
402 self.assertEqual(result_200.status, result_test.status)
406 def test_009_chunked_response(self):
407 self.site.application = chunked_app
408 sock = eventlet.connect(
409 ('localhost', self.port))
411 fd = sock.makefile('rwb')
412 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
414 assert b'Transfer-Encoding: chunked' in fd.read()
416 def test_010_no_chunked_http_1_0(self):
417 self.site.application = chunked_app
418 sock = eventlet.connect(
419 ('localhost', self.port))
421 fd = sock.makefile('rwb')
422 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n')
424 assert b'Transfer-Encoding: chunked' not in fd.read()
426 def test_011_multiple_chunks(self):
427 self.site.application = big_chunks
428 sock = eventlet.connect(
429 ('localhost', self.port))
431 fd = sock.makefile('rwb')
432 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
441 assert b'Transfer-Encoding: chunked' in headers
443 chunklen = int(fd.readline(), 16)
448 chunklen = int(fd.readline(), 16)
451 # Require a CRLF to close the message body
452 self.assertEqual(response, b'\r\n')
454 @tests.skip_if_no_ssl
455 def test_012_ssl_server(self):
456 def wsgi_app(environ, start_response):
457 start_response('200 OK', {})
458 return [environ['wsgi.input'].read()]
460 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
461 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
463 server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
464 certfile=certificate_file,
465 keyfile=private_key_file,
467 self.spawn_server(sock=server_sock, site=wsgi_app)
469 sock = eventlet.connect(('localhost', self.port))
470 sock = eventlet.wrap_ssl(sock)
472 b'POST /foo HTTP/1.1\r\nHost: localhost\r\n'
473 b'Connection: close\r\nContent-length:3\r\n\r\nabc')
474 result = recvall(sock)
475 assert result.endswith(b'abc')
477 @tests.skip_if_no_ssl
478 def test_013_empty_return(self):
479 def wsgi_app(environ, start_response):
480 start_response("200 OK", [])
483 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
484 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
485 server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
486 certfile=certificate_file,
487 keyfile=private_key_file,
489 self.spawn_server(sock=server_sock, site=wsgi_app)
491 sock = eventlet.connect(('localhost', server_sock.getsockname()[1]))
492 sock = eventlet.wrap_ssl(sock)
493 sock.write(b'GET /foo HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
494 result = recvall(sock)
495 assert result[-4:] == b'\r\n\r\n'
497 def test_014_chunked_post(self):
498 self.site.application = chunked_post
499 sock = eventlet.connect(('localhost', self.port))
500 fd = sock.makefile('rwb')
501 fd.write('PUT /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
502 'Transfer-Encoding: chunked\r\n\r\n'
503 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
506 if fd.readline() == b'\r\n':
509 assert response == b'oh hai', 'invalid response %s' % response
511 sock = eventlet.connect(('localhost', self.port))
512 fd = sock.makefile('rwb')
513 fd.write('PUT /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
514 'Transfer-Encoding: chunked\r\n\r\n'
515 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
518 if fd.readline() == b'\r\n':
521 assert response == b'oh hai', 'invalid response %s' % response
523 sock = eventlet.connect(('localhost', self.port))
524 fd = sock.makefile('rwb')
525 fd.write('PUT /c HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
526 'Transfer-Encoding: chunked\r\n\r\n'
527 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
530 if fd.readline() == b'\r\n':
532 response = fd.read(8192)
533 assert response == b'oh hai', 'invalid response %s' % response
535 def test_015_write(self):
536 self.site.application = use_write
537 sock = eventlet.connect(('localhost', self.port))
538 fd = sock.makefile('wb')
539 fd.write(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
541 result1 = read_http(sock)
542 assert 'content-length' in result1.headers_lower
544 sock = eventlet.connect(('localhost', self.port))
545 fd = sock.makefile('wb')
546 fd.write(b'GET /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
548 result2 = read_http(sock)
549 assert 'transfer-encoding' in result2.headers_lower
550 assert result2.headers_lower['transfer-encoding'] == 'chunked'
552 def test_016_repeated_content_length(self):
553 """content-length header was being doubled up if it was set in
554 start_response and could also be inferred from the iterator
556 def wsgi_app(environ, start_response):
557 start_response('200 OK', [('Content-Length', '7')])
559 self.site.application = wsgi_app
560 sock = eventlet.connect(('localhost', self.port))
561 fd = sock.makefile('rwb')
562 fd.write(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
570 header_lines.append(line)
571 self.assertEqual(1, len(
572 [l for l in header_lines if l.lower().startswith(b'content-length')]))
574 @tests.skip_if_no_ssl
575 def test_017_ssl_zeroreturnerror(self):
577 def server(sock, site, log):
579 serv = wsgi.Server(sock, sock.getsockname(), site, log)
580 client_socket = sock.accept()
581 serv.process_request(client_socket)
584 traceback.print_exc()
587 def wsgi_app(environ, start_response):
588 start_response('200 OK', [])
589 return [environ['wsgi.input'].read()]
591 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
592 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
594 sock = eventlet.wrap_ssl(
595 eventlet.listen(('localhost', 0)),
596 certfile=certificate_file, keyfile=private_key_file,
598 server_coro = eventlet.spawn(server, sock, wsgi_app, self.logfile)
600 client = eventlet.connect(('localhost', sock.getsockname()[1]))
601 client = eventlet.wrap_ssl(client)
602 client.write(b'X') # non-empty payload so that SSL handshake occurs
603 greenio.shutdown_safe(client)
606 success = server_coro.wait()
609 def test_018_http_10_keepalive(self):
610 # verify that if an http/1.0 client sends connection: keep-alive
611 # that we don't close the connection
612 sock = eventlet.connect(
613 ('localhost', self.port))
615 fd = sock.makefile('wb')
616 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
619 result1 = read_http(sock)
620 assert 'connection' in result1.headers_lower
621 self.assertEqual('keep-alive', result1.headers_lower['connection'])
622 # repeat request to verify connection is actually still open
623 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
625 result2 = read_http(sock)
626 assert 'connection' in result2.headers_lower
627 self.assertEqual('keep-alive', result2.headers_lower['connection'])
630 def test_019_fieldstorage_compat(self):
631 def use_fieldstorage(environ, start_response):
632 cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
633 start_response('200 OK', [('Content-type', 'text/plain')])
636 self.site.application = use_fieldstorage
637 sock = eventlet.connect(
638 ('localhost', self.port))
640 fd = sock.makefile('rwb')
641 fd.write('POST / HTTP/1.1\r\n'
642 'Host: localhost\r\n'
643 'Connection: close\r\n'
644 'Transfer-Encoding: chunked\r\n\r\n'
646 '4\r\n hai\r\n0\r\n\r\n'.encode())
648 assert b'hello!' in fd.read()
650 def test_020_x_forwarded_for(self):
652 b'GET / HTTP/1.1\r\nHost: localhost\r\n'
653 + b'X-Forwarded-For: 1.2.3.4, 5.6.7.8\r\n\r\n'
656 sock = eventlet.connect(('localhost', self.port))
657 sock.sendall(request_bytes)
660 assert '1.2.3.4,5.6.7.8,127.0.0.1' in self.logfile.getvalue()
662 # turning off the option should work too
663 self.logfile = six.StringIO()
664 self.spawn_server(log_x_forwarded_for=False)
666 sock = eventlet.connect(('localhost', self.port))
667 sock.sendall(request_bytes)
670 assert '1.2.3.4' not in self.logfile.getvalue()
671 assert '5.6.7.8' not in self.logfile.getvalue()
672 assert '127.0.0.1' in self.logfile.getvalue()
674 def test_socket_remains_open(self):
675 greenthread.kill(self.killer)
676 server_sock = eventlet.listen(('localhost', 0))
677 server_sock_2 = server_sock.dup()
678 self.spawn_server(sock=server_sock_2)
679 # do a single req/response to verify it's up
680 sock = eventlet.connect(('localhost', self.port))
681 fd = sock.makefile('rwb')
682 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
684 result = fd.read(1024)
686 assert result.startswith(b'HTTP'), result
687 assert result.endswith(b'hello world'), result
689 # shut down the server and verify the server_socket fd is still open,
690 # but the actual socketobject passed in to wsgi.server is closed
691 greenthread.kill(self.killer)
692 eventlet.sleep(0) # make the kill go through
694 server_sock_2.accept()
695 # shouldn't be able to use this one anymore
696 except socket.error as exc:
697 self.assertEqual(support.get_errno(exc), errno.EBADF)
698 self.spawn_server(sock=server_sock)
699 sock = eventlet.connect(('localhost', self.port))
700 fd = sock.makefile('rwb')
701 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
703 result = fd.read(1024)
705 assert result.startswith(b'HTTP'), result
706 assert result.endswith(b'hello world'), result
708 def test_021_environ_clobbering(self):
709 def clobberin_time(environ, start_response):
711 'wsgi.version', 'wsgi.url_scheme',
712 'wsgi.input', 'wsgi.errors', 'wsgi.multithread',
713 'wsgi.multiprocess', 'wsgi.run_once', 'REQUEST_METHOD',
714 'SCRIPT_NAME', 'RAW_PATH_INFO', 'PATH_INFO', 'QUERY_STRING',
715 'CONTENT_TYPE', 'CONTENT_LENGTH', 'SERVER_NAME', 'SERVER_PORT',
717 environ[environ_var] = None
718 start_response('200 OK', [('Content-type', 'text/plain')])
720 self.site.application = clobberin_time
721 sock = eventlet.connect(('localhost', self.port))
722 fd = sock.makefile('rwb')
723 fd.write('GET / HTTP/1.1\r\n'
724 'Host: localhost\r\n'
725 'Connection: close\r\n'
728 assert b'200 OK' in fd.read()
730 def test_022_custom_pool(self):
731 # just test that it accepts the parameter for now
732 # TODO(waitall): test that it uses the pool and that you can waitall() to
733 # ensure that all clients finished
734 p = eventlet.GreenPool(5)
735 self.spawn_server(custom_pool=p)
737 # this stuff is copied from test_001_server, could be better factored
738 sock = eventlet.connect(
739 ('localhost', self.port))
740 fd = sock.makefile('rwb')
741 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
745 assert result.startswith(b'HTTP'), result
746 assert result.endswith(b'hello world'), result
748 def test_023_bad_content_length(self):
749 sock = eventlet.connect(
750 ('localhost', self.port))
751 fd = sock.makefile('rwb')
752 fd.write(b'GET / HTTP/1.0\r\nHost: localhost\r\nContent-length: argh\r\n\r\n')
756 assert result.startswith(b'HTTP'), result
757 assert b'400 Bad Request' in result, result
758 assert b'500' not in result, result
760 def test_024_expect_100_continue(self):
761 def wsgi_app(environ, start_response):
762 if int(environ['CONTENT_LENGTH']) > 1024:
763 start_response('417 Expectation Failed', [('Content-Length', '7')])
766 text = environ['wsgi.input'].read()
767 start_response('200 OK', [('Content-Length', str(len(text)))])
769 self.site.application = wsgi_app
770 sock = eventlet.connect(('localhost', self.port))
771 fd = sock.makefile('rwb')
772 fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
773 b'Expect: 100-continue\r\n\r\n')
775 result = read_http(sock)
776 self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
777 self.assertEqual(result.body, b'failure')
779 b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
780 b'Expect: 100-continue\r\n\r\ntesting')
788 header_lines.append(line)
789 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
796 header_lines.append(line)
797 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
798 assert fd.read(7) == b'testing'
802 def test_024a_expect_100_continue_with_headers(self):
803 def wsgi_app(environ, start_response):
804 if int(environ['CONTENT_LENGTH']) > 1024:
805 start_response('417 Expectation Failed', [('Content-Length', '7')])
808 environ['wsgi.input'].set_hundred_continue_response_headers(
809 [('Hundred-Continue-Header-1', 'H1'),
810 ('Hundred-Continue-Header-2', 'H2'),
811 ('Hundred-Continue-Header-k', 'Hk')])
812 text = environ['wsgi.input'].read()
813 start_response('200 OK', [('Content-Length', str(len(text)))])
815 self.site.application = wsgi_app
816 sock = eventlet.connect(('localhost', self.port))
817 fd = sock.makefile('rwb')
818 fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
819 b'Expect: 100-continue\r\n\r\n')
821 result = read_http(sock)
822 self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
823 self.assertEqual(result.body, b'failure')
825 b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
826 b'Expect: 100-continue\r\n\r\ntesting')
834 header_lines.append(line.strip())
835 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
836 headers = dict((k, v) for k, v in (h.split(b': ', 1) for h in header_lines[1:]))
837 assert b'Hundred-Continue-Header-1' in headers
838 assert b'Hundred-Continue-Header-2' in headers
839 assert b'Hundred-Continue-Header-K' in headers
840 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
841 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
842 self.assertEqual(b'Hk', headers[b'Hundred-Continue-Header-K'])
849 header_lines.append(line)
850 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
851 self.assertEqual(fd.read(7), b'testing')
855 def test_024b_expect_100_continue_with_headers_multiple_chunked(self):
856 def wsgi_app(environ, start_response):
857 environ['wsgi.input'].set_hundred_continue_response_headers(
858 [('Hundred-Continue-Header-1', 'H1'),
859 ('Hundred-Continue-Header-2', 'H2')])
860 text = environ['wsgi.input'].read()
862 environ['wsgi.input'].set_hundred_continue_response_headers(
863 [('Hundred-Continue-Header-3', 'H3')])
864 environ['wsgi.input'].send_hundred_continue_response()
866 text += environ['wsgi.input'].read()
868 start_response('200 OK', [('Content-Length', str(len(text)))])
870 self.site.application = wsgi_app
871 sock = eventlet.connect(('localhost', self.port))
872 fd = sock.makefile('rwb')
873 fd.write(b'PUT /a HTTP/1.1\r\n'
874 b'Host: localhost\r\nConnection: close\r\n'
875 b'Transfer-Encoding: chunked\r\n'
876 b'Expect: 100-continue\r\n\r\n')
879 # Expect 1st 100-continue response
886 header_lines.append(line.strip())
887 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
888 headers = dict((k, v) for k, v in (h.split(b': ', 1)
889 for h in header_lines[1:]))
890 assert b'Hundred-Continue-Header-1' in headers
891 assert b'Hundred-Continue-Header-2' in headers
892 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
893 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
896 fd.write(b'5\r\nfirst\r\n8\r\n message\r\n0\r\n\r\n')
899 # Expect a 2nd 100-continue response
906 header_lines.append(line.strip())
907 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
908 headers = dict((k, v) for k, v in (h.split(b': ', 1)
909 for h in header_lines[1:]))
910 assert b'Hundred-Continue-Header-3' in headers
911 self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
914 fd.write(b'8\r\n, second\r\n8\r\n message\r\n0\r\n\r\n')
917 # Expect final 200-OK
924 header_lines.append(line.strip())
925 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
927 self.assertEqual(fd.read(29), b'first message, second message')
931 def test_024c_expect_100_continue_with_headers_multiple_nonchunked(self):
932 def wsgi_app(environ, start_response):
934 environ['wsgi.input'].set_hundred_continue_response_headers(
935 [('Hundred-Continue-Header-1', 'H1'),
936 ('Hundred-Continue-Header-2', 'H2')])
937 text = environ['wsgi.input'].read(13)
939 environ['wsgi.input'].set_hundred_continue_response_headers(
940 [('Hundred-Continue-Header-3', 'H3')])
941 environ['wsgi.input'].send_hundred_continue_response()
943 text += environ['wsgi.input'].read(16)
945 start_response('200 OK', [('Content-Length', str(len(text)))])
948 self.site.application = wsgi_app
949 sock = eventlet.connect(('localhost', self.port))
950 fd = sock.makefile('rwb')
951 fd.write(b'PUT /a HTTP/1.1\r\n'
952 b'Host: localhost\r\nConnection: close\r\n'
953 b'Content-Length: 29\r\n'
954 b'Expect: 100-continue\r\n\r\n')
957 # Expect 1st 100-continue response
964 header_lines.append(line.strip())
965 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
966 headers = dict((k, v) for k, v in (h.split(b': ', 1)
967 for h in header_lines[1:]))
968 assert b'Hundred-Continue-Header-1' in headers
969 assert b'Hundred-Continue-Header-2' in headers
970 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
971 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
974 fd.write(b'first message')
977 # Expect a 2nd 100-continue response
984 header_lines.append(line.strip())
985 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
986 headers = dict((k, v) for k, v in (h.split(b': ', 1)
987 for h in header_lines[1:]))
988 assert b'Hundred-Continue-Header-3' in headers
989 self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
992 fd.write(b', second message\r\n')
995 # Expect final 200-OK
1002 header_lines.append(line.strip())
1003 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
1005 self.assertEqual(fd.read(29), b'first message, second message')
1009 def test_025_accept_errors(self):
1010 debug.hub_exceptions(True)
1011 listener = greensocket.socket()
1012 listener.bind(('localhost', 0))
1013 # NOT calling listen, to trigger the error
1014 with capture_stderr() as log:
1015 self.spawn_server(sock=listener)
1016 eventlet.sleep(0) # need to enter server loop
1018 eventlet.connect(('localhost', self.port))
1019 self.fail("Didn't expect to connect")
1020 except socket.error as exc:
1021 self.assertEqual(support.get_errno(exc), errno.ECONNREFUSED)
1023 log_content = log.getvalue()
1024 assert 'Invalid argument' in log_content, log_content
1025 debug.hub_exceptions(False)
1027 def test_026_log_format(self):
1028 self.spawn_server(log_format="HI %(request_line)s HI")
1029 sock = eventlet.connect(('localhost', self.port))
1030 sock.sendall(b'GET /yo! HTTP/1.1\r\nHost: localhost\r\n\r\n')
1033 assert '\nHI GET /yo! HTTP/1.1 HI\n' in self.logfile.getvalue(), self.logfile.getvalue()
1035 def test_close_chunked_with_1_0_client(self):
1036 # verify that if we return a generator from our app
1037 # and we're not speaking with a 1.1 client, that we
1038 # close the connection
1039 self.site.application = chunked_app
1040 sock = eventlet.connect(('localhost', self.port))
1042 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1044 result = read_http(sock)
1045 self.assertEqual(result.headers_lower['connection'], 'close')
1046 self.assertNotEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1047 self.assertEqual(result.body, b"thisischunked")
1049 def test_minimum_chunk_size_parameter_leaves_httpprotocol_class_member_intact(self):
1050 start_size = wsgi.HttpProtocol.minimum_chunk_size
1052 self.spawn_server(minimum_chunk_size=start_size * 2)
1053 sock = eventlet.connect(('localhost', self.port))
1054 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1057 self.assertEqual(wsgi.HttpProtocol.minimum_chunk_size, start_size)
1060 def test_error_in_chunked_closes_connection(self):
1061 # From http://rhodesmill.org/brandon/2013/chunked-wsgi/
1062 self.spawn_server(minimum_chunk_size=1)
1064 self.site.application = chunked_fail_app
1065 sock = eventlet.connect(('localhost', self.port))
1067 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1069 result = read_http(sock)
1070 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1071 self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1073 b'27\r\nThe dwarves of yore made mighty spells,\r\n'
1074 b'25\r\nWhile hammers fell like ringing bells\r\n')
1075 self.assertEqual(result.body, expected_body)
1077 # verify that socket is closed by server
1078 self.assertEqual(sock.recv(1), b'')
1080 def test_026_http_10_nokeepalive(self):
1081 # verify that if an http/1.0 client sends connection: keep-alive
1082 # and the server doesn't accept keep-alives, we close the connection
1083 self.spawn_server(keepalive=False)
1084 sock = eventlet.connect(
1085 ('localhost', self.port))
1087 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1088 result = read_http(sock)
1089 self.assertEqual(result.headers_lower['connection'], 'close')
1091 def test_027_keepalive_chunked(self):
1092 self.site.application = chunked_post
1093 sock = eventlet.connect(('localhost', self.port))
1094 fd = sock.makefile('wb')
1096 b'Host: localhost\r\nTransfer-Encoding: chunked\r\n\r\n' +
1097 b'10\r\n0123456789abcdef\r\n0\r\n\r\n')
1098 fd.write(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1101 fd.write(b'PUT /b HTTP/1.1\r\n' + common_suffix)
1104 fd.write(b'PUT /c HTTP/1.1\r\n' + common_suffix)
1107 fd.write(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1112 @tests.skip_if_no_ssl
1113 def test_028_ssl_handshake_errors(self):
1118 wsgi.server(sock=sock, site=hello_world, log=self.logfile)
1119 errored[0] = 'SSL handshake error caused wsgi.server to exit.'
1120 except greenthread.greenlet.GreenletExit:
1122 except Exception as e:
1123 errored[0] = 'SSL handshake error raised exception %s.' % e
1125 for data in ('', 'GET /non-ssl-request HTTP/1.0\r\n\r\n'):
1126 srv_sock = eventlet.wrap_ssl(
1127 eventlet.listen(('localhost', 0)),
1128 certfile=certificate_file, keyfile=private_key_file,
1130 port = srv_sock.getsockname()[1]
1131 g = eventlet.spawn_n(server, srv_sock)
1132 client = eventlet.connect(('localhost', port))
1133 if data: # send non-ssl request
1134 client.sendall(data.encode())
1135 else: # close sock prematurely
1137 eventlet.sleep(0) # let context switch back to server
1138 assert not errored[0], errored[0]
1139 # make another request to ensure the server's still alive
1141 client = ssl.wrap_socket(eventlet.connect(('localhost', port)))
1142 client.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
1143 result = recvall(client)
1144 assert result.startswith(b'HTTP'), result
1145 assert result.endswith(b'hello world')
1147 pass # TODO(openssl): should test with OpenSSL
1150 def test_029_posthooks(self):
1151 posthook1_count = [0]
1152 posthook2_count = [0]
1154 def posthook1(env, value, multiplier=1):
1155 self.assertEqual(env['local.test'], 'test_029_posthooks')
1156 posthook1_count[0] += value * multiplier
1158 def posthook2(env, value, divisor=1):
1159 self.assertEqual(env['local.test'], 'test_029_posthooks')
1160 posthook2_count[0] += value / divisor
1162 def one_posthook_app(env, start_response):
1163 env['local.test'] = 'test_029_posthooks'
1164 if 'eventlet.posthooks' not in env:
1165 start_response('500 eventlet.posthooks not supported',
1166 [('Content-Type', 'text/plain')])
1168 env['eventlet.posthooks'].append(
1169 (posthook1, (2,), {'multiplier': 3}))
1170 start_response('200 OK', [('Content-Type', 'text/plain')])
1172 self.site.application = one_posthook_app
1173 sock = eventlet.connect(('localhost', self.port))
1174 fp = sock.makefile('rwb')
1175 fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1177 self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1180 self.assertEqual(posthook1_count[0], 6)
1181 self.assertEqual(posthook2_count[0], 0)
1183 def two_posthook_app(env, start_response):
1184 env['local.test'] = 'test_029_posthooks'
1185 if 'eventlet.posthooks' not in env:
1186 start_response('500 eventlet.posthooks not supported',
1187 [('Content-Type', 'text/plain')])
1189 env['eventlet.posthooks'].append(
1190 (posthook1, (4,), {'multiplier': 5}))
1191 env['eventlet.posthooks'].append(
1192 (posthook2, (100,), {'divisor': 4}))
1193 start_response('200 OK', [('Content-Type', 'text/plain')])
1195 self.site.application = two_posthook_app
1196 sock = eventlet.connect(('localhost', self.port))
1197 fp = sock.makefile('rwb')
1198 fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1200 self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1203 self.assertEqual(posthook1_count[0], 26)
1204 self.assertEqual(posthook2_count[0], 25)
1206 def test_030_reject_long_header_lines(self):
1207 sock = eventlet.connect(('localhost', self.port))
1208 request = 'GET / HTTP/1.0\r\nHost: localhost\r\nLong: %s\r\n\r\n' % \
1210 send_expect_close(sock, request.encode())
1211 result = read_http(sock)
1212 self.assertEqual(result.status, 'HTTP/1.0 400 Header Line Too Long')
1214 def test_031_reject_large_headers(self):
1215 sock = eventlet.connect(('localhost', self.port))
1216 headers = ('Name: %s\r\n' % ('a' * 7000,)) * 20
1217 request = 'GET / HTTP/1.0\r\nHost: localhost\r\n%s\r\n\r\n' % headers
1218 send_expect_close(sock, request.encode())
1219 result = read_http(sock)
1220 self.assertEqual(result.status, 'HTTP/1.0 400 Headers Too Large')
1222 def test_032_wsgi_input_as_iterable(self):
1223 # https://bitbucket.org/eventlet/eventlet/issue/150
1224 # env['wsgi.input'] returns a single byte at a time
1225 # when used as an iterator
1228 def echo_by_iterating(env, start_response):
1229 start_response('200 OK', [('Content-type', 'text/plain')])
1230 for chunk in env['wsgi.input']:
1234 self.site.application = echo_by_iterating
1235 upload_data = b'123456789abcdef' * 100
1237 'POST / HTTP/1.0\r\n'
1238 'Host: localhost\r\n'
1239 'Content-Length: %i\r\n\r\n%s'
1240 ) % (len(upload_data), bytes_to_str(upload_data))
1241 sock = eventlet.connect(('localhost', self.port))
1242 fd = sock.makefile('rwb')
1243 fd.write(request.encode())
1245 result = read_http(sock)
1246 self.assertEqual(result.body, upload_data)
1248 self.assertEqual(g[0], 1)
1250 def test_zero_length_chunked_response(self):
1251 def zero_chunked_app(env, start_response):
1252 start_response('200 OK', [('Content-type', 'text/plain')])
1255 self.site.application = zero_chunked_app
1256 sock = eventlet.connect(
1257 ('localhost', self.port))
1259 fd = sock.makefile('rwb')
1260 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1262 response = fd.read().split(b'\r\n')
1269 assert b'Transfer-Encoding: chunked' in b''.join(headers), headers
1270 # should only be one chunk of zero size with two blank lines
1271 # (one terminates the chunk, one terminates the body)
1272 self.assertEqual(response, [b'0', b'', b''])
1274 def test_configurable_url_length_limit(self):
1275 self.spawn_server(url_length_limit=20000)
1276 sock = eventlet.connect(
1277 ('localhost', self.port))
1279 request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
1280 fd = sock.makefile('rwb')
1281 fd.write(request.encode())
1283 result = fd.readline()
1285 # windows closes the socket before the data is flushed,
1286 # so we never get anything back
1287 status = result.split(b' ')[1]
1288 self.assertEqual(status, b'200')
1291 def test_aborted_chunked_post(self):
1292 read_content = event.Event()
1295 def chunk_reader(env, start_response):
1297 content = env['wsgi.input'].read(1024)
1301 read_content.send(content)
1302 start_response('200 OK', [('Content-Type', 'text/plain')])
1304 self.site.application = chunk_reader
1305 expected_body = 'a bunch of stuff'
1306 data = "\r\n".join(['PUT /somefile HTTP/1.0',
1307 'Transfer-Encoding: chunked',
1311 # start PUT-ing some chunked data but close prematurely
1312 sock = eventlet.connect(('127.0.0.1', self.port))
1313 sock.sendall(data.encode())
1315 # the test passes if we successfully get here, and read all the data
1316 # in spite of the early close
1317 self.assertEqual(read_content.wait(), b'ok')
1320 def test_exceptions_close_connection(self):
1321 def wsgi_app(environ, start_response):
1322 raise RuntimeError("intentional error")
1323 self.site.application = wsgi_app
1324 sock = eventlet.connect(('localhost', self.port))
1325 fd = sock.makefile('rwb')
1326 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1328 result = read_http(sock)
1329 self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1330 self.assertEqual(result.headers_lower['connection'], 'close')
1331 assert 'transfer-encoding' not in result.headers_lower
1333 def test_unicode_with_only_ascii_characters_works(self):
1334 def wsgi_app(environ, start_response):
1335 start_response("200 OK", [])
1338 self.site.application = wsgi_app
1339 sock = eventlet.connect(('localhost', self.port))
1340 fd = sock.makefile('rwb')
1341 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1343 result = read_http(sock)
1344 assert b'xxx' in result.body
1346 def test_unicode_with_nonascii_characters_raises_error(self):
1347 def wsgi_app(environ, start_response):
1348 start_response("200 OK", [])
1351 self.site.application = wsgi_app
1352 sock = eventlet.connect(('localhost', self.port))
1353 fd = sock.makefile('rwb')
1354 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1356 result = read_http(sock)
1357 self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1358 self.assertEqual(result.headers_lower['connection'], 'close')
1360 def test_path_info_decoding(self):
1361 def wsgi_app(environ, start_response):
1362 start_response("200 OK", [])
1363 yield six.b("decoded: %s" % environ['PATH_INFO'])
1364 yield six.b("raw: %s" % environ['RAW_PATH_INFO'])
1365 self.site.application = wsgi_app
1366 sock = eventlet.connect(('localhost', self.port))
1367 fd = sock.makefile('rwb')
1368 fd.write(b'GET /a*b@%40%233 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1370 result = read_http(sock)
1371 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1372 assert b'decoded: /a*b@@#3' in result.body
1373 assert b'raw: /a*b@%40%233' in result.body
1375 def test_ipv6(self):
1377 sock = eventlet.listen(('::1', 0), family=socket.AF_INET6)
1378 except (socket.gaierror, socket.error): # probably no ipv6
1380 log = six.StringIO()
1381 # first thing the server does is try to log the IP it's bound to
1385 wsgi.server(sock=sock, log=log, site=Site())
1387 log.write(b'broken')
1389 self.spawn_thread(run_server)
1391 logval = log.getvalue()
1394 logval = log.getvalue()
1395 if 'broked' in logval:
1396 self.fail('WSGI server raised exception with ipv6 socket')
1398 def test_debug(self):
1399 self.spawn_server(debug=False)
1401 def crasher(env, start_response):
1402 raise RuntimeError("intentional crash")
1403 self.site.application = crasher
1405 sock = eventlet.connect(('localhost', self.port))
1406 fd = sock.makefile('wb')
1407 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1409 result1 = read_http(sock)
1410 self.assertEqual(result1.status, 'HTTP/1.1 500 Internal Server Error')
1411 self.assertEqual(result1.body, b'')
1412 self.assertEqual(result1.headers_lower['connection'], 'close')
1413 assert 'transfer-encoding' not in result1.headers_lower
1415 # verify traceback when debugging enabled
1416 self.spawn_server(debug=True)
1417 self.site.application = crasher
1418 sock = eventlet.connect(('localhost', self.port))
1419 fd = sock.makefile('wb')
1420 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1422 result2 = read_http(sock)
1423 self.assertEqual(result2.status, 'HTTP/1.1 500 Internal Server Error')
1424 assert b'intentional crash' in result2.body, result2.body
1425 assert b'RuntimeError' in result2.body, result2.body
1426 assert b'Traceback' in result2.body, result2.body
1427 self.assertEqual(result2.headers_lower['connection'], 'close')
1428 assert 'transfer-encoding' not in result2.headers_lower
1430 def test_client_disconnect(self):
1431 """Issue #95 Server must handle disconnect from client in the middle of response
1433 def long_response(environ, start_response):
1434 start_response('200 OK', [('Content-Length', '9876')])
1437 server_sock = eventlet.listen(('localhost', 0))
1438 self.port = server_sock.getsockname()[1]
1439 server = wsgi.Server(server_sock, server_sock.getsockname(), long_response,
1443 sock = eventlet.connect(server_sock.getsockname())
1444 sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1447 request_thread = eventlet.spawn(make_request)
1448 server_conn = server_sock.accept()
1449 # Next line must not raise IOError -32 Broken pipe
1450 server.process_request(server_conn)
1451 request_thread.wait()
1454 def test_server_connection_timeout_exception(self):
1455 # Handle connection socket timeouts
1456 # https://bitbucket.org/eventlet/eventlet/issue/143/
1457 # Runs tests.wsgi_test_conntimeout in a separate process.
1458 tests.run_isolated('wsgi_connection_timeout.py')
1460 def test_server_socket_timeout(self):
1461 self.spawn_server(socket_timeout=0.1)
1462 sock = eventlet.connect(('localhost', self.port))
1463 sock.send(b'GET / HTTP/1.1\r\n')
1467 assert False, 'Expected ConnectionClosed exception'
1468 except ConnectionClosed:
1471 def test_disable_header_name_capitalization(self):
1472 # Disable HTTP header name capitalization
1474 # https://github.com/eventlet/eventlet/issues/80
1475 random_case_header = ('eTAg', 'TAg-VAluE')
1477 def wsgi_app(environ, start_response):
1478 start_response('200 oK', [random_case_header])
1481 self.spawn_server(site=wsgi_app, capitalize_response_headers=False)
1483 sock = eventlet.connect(('localhost', self.port))
1484 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1485 result = read_http(sock)
1487 self.assertEqual(result.status, 'HTTP/1.1 200 oK')
1488 self.assertEqual(result.headers_lower[random_case_header[0].lower()], random_case_header[1])
1489 self.assertEqual(result.headers_original[random_case_header[0]], random_case_header[1])
1492 def read_headers(sock):
1493 fd = sock.makefile('rb')
1495 response_line = fd.readline()
1496 except socket.error as exc:
1497 if support.get_errno(exc) == 10053:
1498 raise ConnectionClosed
1500 if not response_line:
1501 raise ConnectionClosed
1505 line = fd.readline()
1509 header_lines.append(line)
1511 for x in header_lines:
1515 key, value = x.split(b': ', 1)
1516 assert key.lower() not in headers, "%s header duplicated" % key
1517 headers[bytes_to_str(key.lower())] = bytes_to_str(value)
1518 return bytes_to_str(response_line), headers
1521 class IterableAlreadyHandledTest(_TestBase):
1523 self.site = IterableSite()
1526 return IterableApp(True)
1528 def test_iterable_app_keeps_socket_open_unless_connection_close_sent(self):
1529 self.site.application = self.get_app()
1530 sock = eventlet.connect(
1531 ('localhost', self.port))
1533 fd = sock.makefile('rwb')
1534 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1537 response_line, headers = read_headers(sock)
1538 self.assertEqual(response_line, 'HTTP/1.1 200 OK\r\n')
1539 assert 'connection' not in headers
1540 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1542 result = read_http(sock)
1543 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1544 self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1545 self.assertEqual(result.body, b'0\r\n\r\n') # Still coming back chunked
1548 class ProxiedIterableAlreadyHandledTest(IterableAlreadyHandledTest):
1549 # same thing as the previous test but ensuring that it works with tpooled
1550 # results as well as regular ones
1551 @tests.skip_with_pyevent
1553 return tpool.Proxy(super(ProxiedIterableAlreadyHandledTest, self).get_app())
1557 super(ProxiedIterableAlreadyHandledTest, self).tearDown()
1560 class TestChunkedInput(_TestBase):
1564 def application(self, env, start_response):
1565 input = env['wsgi.input']
1568 pi = env["PATH_INFO"]
1570 if pi == "/short-read":
1573 elif pi == "/lines":
1578 response.append(b"pong")
1579 elif pi.startswith("/yield_spaces"):
1580 if pi.endswith('override_min'):
1581 env['eventlet.minimum_write_chunk_size'] = 1
1582 self.yield_next_space = False
1584 def response_iter():
1587 while not self.yield_next_space and num_sleeps < 200:
1593 start_response('200 OK',
1594 [('Content-Type', 'text/plain'),
1595 ('Content-Length', '2')])
1596 return response_iter()
1598 raise RuntimeError("bad path")
1600 start_response('200 OK', [('Content-Type', 'text/plain')])
1604 return eventlet.connect(('localhost', self.port))
1608 self.site.application = self.application
1610 def chunk_encode(self, chunks, dirt=None):
1616 b += "%x%s\r\n%s\r\n" % (len(c), dirt, c)
1619 def body(self, dirt=None):
1620 return self.chunk_encode(["this", " is ", "chunked", "\nline",
1621 " 2", "\n", "line3", ""], dirt=dirt)
1624 fd.sendall(b"GET /ping HTTP/1.1\r\n\r\n")
1625 self.assertEqual(read_http(fd).body, b"pong")
1627 def test_short_read_with_content_length(self):
1629 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1630 "Content-Length:1000\r\n\r\n" + body
1633 fd.sendall(req.encode())
1634 self.assertEqual(read_http(fd).body, b"this is ch")
1639 def test_short_read_with_zero_content_length(self):
1641 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1642 "Content-Length:0\r\n\r\n" + body
1644 fd.sendall(req.encode())
1645 self.assertEqual(read_http(fd).body, b"this is ch")
1650 def test_short_read(self):
1652 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1655 fd.sendall(req.encode())
1656 self.assertEqual(read_http(fd).body, b"this is ch")
1661 def test_dirt(self):
1662 body = self.body(dirt="; here is dirt\0bla")
1663 req = "POST /ping HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1666 fd.sendall(req.encode())
1667 self.assertEqual(read_http(fd).body, b"pong")
1672 def test_chunked_readline(self):
1674 req = "POST /lines HTTP/1.1\r\nContent-Length: %s\r\n" \
1675 "transfer-encoding: Chunked\r\n\r\n%s" % (len(body), body)
1678 fd.sendall(req.encode())
1679 self.assertEqual(read_http(fd).body, b'this is chunked\nline 2\nline3')
1682 def test_chunked_readline_wsgi_override_minimum_chunk_size(self):
1685 fd.sendall(b"POST /yield_spaces/override_min HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1688 with eventlet.Timeout(.1):
1690 one_byte = fd.recv(1)
1691 resp_so_far += one_byte
1692 if resp_so_far.endswith(b'\r\n\r\n'):
1694 self.assertEqual(fd.recv(1), b' ')
1696 with eventlet.Timeout(.1):
1698 except eventlet.Timeout:
1702 self.yield_next_space = True
1704 with eventlet.Timeout(.1):
1705 self.assertEqual(fd.recv(1), b' ')
1707 def test_chunked_readline_wsgi_not_override_minimum_chunk_size(self):
1710 fd.sendall(b"POST /yield_spaces HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1714 with eventlet.Timeout(.1):
1716 one_byte = fd.recv(1)
1717 resp_so_far += one_byte
1718 if resp_so_far.endswith(b'\r\n\r\n'):
1720 self.assertEqual(fd.recv(1), b' ')
1721 except eventlet.Timeout:
1726 def test_close_before_finished(self):
1730 got_signal.append(1)
1731 raise KeyboardInterrupt()
1733 signal.signal(signal.SIGALRM, handler)
1738 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1741 fd.sendall(req.encode())
1746 # This is needed because on Python 3 GreenSocket.recv_into is called
1747 # rather than recv; recv_into right now (git 5ec3a3c) trampolines to
1748 # the hub *before* attempting to read anything from a file descriptor
1749 # therefore we need one extra context switch to let it notice closed
1750 # socket, die and leave the hub empty
1755 signal.signal(signal.SIGALRM, signal.SIG_DFL)
1757 assert not got_signal, "caught alarm signal. infinite loop detected."
1760 if __name__ == '__main__':