14 from eventlet import debug
15 from eventlet import event
16 from eventlet import greenio
17 from eventlet import greenthread
18 from eventlet import support
19 from eventlet import tpool
20 from eventlet import wsgi
21 from eventlet.green import socket as greensocket
22 from eventlet.green import ssl
23 from eventlet.support import bytes_to_str, capture_stderr, six
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'
142 chunk = sock.recv(16 << 10)
148 class ConnectionClosed(Exception):
152 def send_expect_close(sock, buf):
153 # Some tests will induce behavior that causes the remote end to
154 # close the connection before all of the data has been written.
155 # With small kernel buffer sizes, this can cause an EPIPE error.
156 # Since the test expects an early close, this can be ignored.
159 except socket.error as exc:
160 if support.get_errno(exc) != errno.EPIPE:
165 fd = sock.makefile('rb')
167 response_line = bytes_to_str(fd.readline().rstrip(b'\r\n'))
168 except socket.error as exc:
169 # TODO find out whether 54 is ok here or not, I see it when running tests
171 if support.get_errno(exc) in (10053, 54):
172 raise ConnectionClosed
174 if not response_line:
175 raise ConnectionClosed(response_line)
183 header_lines.append(line)
185 headers_original = {}
187 for x in header_lines:
191 key, value = bytes_to_str(x).split(':', 1)
193 value = value.lstrip()
194 key_lower = key.lower()
195 # FIXME: Duplicate headers are allowed as per HTTP RFC standard,
196 # the client and/or intermediate proxies are supposed to treat them
197 # as a single header with values concatenated using space (' ') delimiter.
198 assert key_lower not in headers_lower, "header duplicated: {0}".format(key)
199 headers_original[key] = value
200 headers_lower[key_lower] = value
202 content_length_str = headers_lower.get(CONTENT_LENGTH.lower(), '')
203 if content_length_str:
204 num = int(content_length_str)
210 result = HttpReadResult(
211 status=response_line,
212 headers_lower=headers_lower,
214 headers_original=headers_original)
218 class _TestBase(tests.LimitedTestCase):
220 super(_TestBase, self).setUp()
221 self.logfile = six.StringIO()
228 greenthread.kill(self.killer)
230 super(_TestBase, self).tearDown()
232 def spawn_server(self, **kwargs):
233 """Spawns a new wsgi server with the given arguments using
234 :meth:`spawn_thread`.
236 Sets `self.server_addr` to (host, port) tuple suitable for `socket.connect`.
238 new_kwargs = dict(max_size=128,
241 new_kwargs.update(kwargs)
243 if 'sock' not in new_kwargs:
244 new_kwargs['sock'] = eventlet.listen(('localhost', 0))
246 self.server_addr = new_kwargs['sock'].getsockname()
247 self.spawn_thread(wsgi.server, **new_kwargs)
249 def spawn_thread(self, target, **kwargs):
250 """Spawns a new greenthread using specified target and arguments.
252 Kills any previously-running server and sets self.killer to the
253 greenthread running the target.
255 eventlet.sleep(0) # give previous server a chance to start
257 greenthread.kill(self.killer)
259 self.killer = eventlet.spawn_n(target, **kwargs)
262 raise NotImplementedError
265 class TestHttpd(_TestBase):
269 def test_001_server(self):
270 sock = eventlet.connect(self.server_addr)
272 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
273 result = recvall(sock)
274 # The server responds with the maximum version it supports
275 assert result.startswith(b'HTTP'), result
276 assert result.endswith(b'hello world'), result
278 def test_002_keepalive(self):
279 sock = eventlet.connect(self.server_addr)
281 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
283 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
286 def test_004_close_keepalive(self):
287 sock = eventlet.connect(self.server_addr)
289 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
290 result1 = read_http(sock)
291 assert result1.status == 'HTTP/1.1 200 OK'
292 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
293 result2 = read_http(sock)
294 assert result2.status == 'HTTP/1.1 200 OK'
295 assert result2.headers_lower['connection'] == 'close'
296 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
297 self.assertRaises(ConnectionClosed, read_http, sock)
299 def test_006_reject_long_urls(self):
300 sock = eventlet.connect(self.server_addr)
302 for ii in range(3000):
303 path_parts.append('path')
304 path = '/'.join(path_parts)
305 request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
306 send_expect_close(sock, request.encode())
307 fd = sock.makefile('rb')
308 result = fd.readline()
310 # windows closes the socket before the data is flushed,
311 # so we never get anything back
312 status = result.split(b' ')[1]
313 self.assertEqual(status, b'414')
316 def test_007_get_arg(self):
317 # define a new handler that does a get_arg as well as a read_body
318 def new_app(env, start_response):
319 body = bytes_to_str(env['wsgi.input'].read())
320 a = cgi.parse_qs(body).get('a', [1])[0]
321 start_response('200 OK', [('Content-type', 'text/plain')])
322 return [six.b('a is %s, body is %s' % (a, body))]
324 self.site.application = new_app
325 sock = eventlet.connect(self.server_addr)
326 request = b'\r\n'.join((
329 b'Content-Length: 3',
332 sock.sendall(request)
334 # send some junk after the actual request
335 sock.sendall(b'01234567890123456789')
336 result = read_http(sock)
337 self.assertEqual(result.body, b'a is a, body is a=a')
339 def test_008_correctresponse(self):
340 sock = eventlet.connect(self.server_addr)
342 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
343 result_200 = read_http(sock)
344 sock.sendall(b'GET /notexist HTTP/1.1\r\nHost: localhost\r\n\r\n')
346 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
347 result_test = read_http(sock)
348 self.assertEqual(result_200.status, result_test.status)
350 def test_009_chunked_response(self):
351 self.site.application = chunked_app
352 sock = eventlet.connect(self.server_addr)
354 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
355 assert b'Transfer-Encoding: chunked' in recvall(sock)
357 def test_010_no_chunked_http_1_0(self):
358 self.site.application = chunked_app
359 sock = eventlet.connect(self.server_addr)
361 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n')
362 assert b'Transfer-Encoding: chunked' not in recvall(sock)
364 def test_011_multiple_chunks(self):
365 self.site.application = big_chunks
366 sock = eventlet.connect(self.server_addr)
368 fd = sock.makefile('rwb')
369 fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
378 assert b'Transfer-Encoding: chunked' in headers
380 chunklen = int(fd.readline(), 16)
385 chunklen = int(fd.readline(), 16)
388 # Require a CRLF to close the message body
389 self.assertEqual(response, b'\r\n')
391 def test_partial_writes_are_handled(self):
392 # https://github.com/eventlet/eventlet/issues/295
393 # Eventlet issue: "Python 3: wsgi doesn't handle correctly partial
394 # write of socket send() when using writelines()".
396 # The bug was caused by the default writelines() implementaiton
397 # (used by the wsgi module) which doesn't check if write()
398 # successfully completed sending *all* data therefore data could be
399 # lost and the client could be left hanging forever.
401 # Switching wsgi wfile to buffered mode fixes the issue.
403 # Related CPython issue: "Raw I/O writelines() broken",
404 # http://bugs.python.org/issue26292
406 # Custom accept() and send() in order to simulate a connection that
407 # only sends one byte at a time so that any code that doesn't handle
408 # partial writes correctly has to fail.
409 listen_socket = eventlet.listen(('localhost', 0))
410 original_accept = listen_socket.accept
413 connection, address = original_accept()
414 original_send = connection.send
418 return original_send(b, *args)
420 connection.send = send
421 return connection, address
423 listen_socket.accept = accept
425 def application(env, start_response):
426 # Sending content-length is important here so that the client knows
427 # exactly how many bytes does it need to wait for.
428 start_response('200 OK', [('Content-length', 3)])
431 self.spawn_server(sock=listen_socket)
432 self.site.application = application
433 sock = eventlet.connect(self.server_addr)
434 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
435 # This would previously hang forever
436 result = read_http(sock)
437 assert result.body == b'asd'
439 @tests.skip_if_no_ssl
440 def test_012_ssl_server(self):
441 def wsgi_app(environ, start_response):
442 start_response('200 OK', {})
443 return [environ['wsgi.input'].read()]
445 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
446 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
448 server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
449 certfile=certificate_file,
450 keyfile=private_key_file,
452 self.spawn_server(sock=server_sock, site=wsgi_app)
454 sock = eventlet.connect(self.server_addr)
455 sock = eventlet.wrap_ssl(sock)
457 b'POST /foo HTTP/1.1\r\nHost: localhost\r\n'
458 b'Connection: close\r\nContent-length:3\r\n\r\nabc')
459 result = recvall(sock)
460 assert result.endswith(b'abc')
462 @tests.skip_if_no_ssl
463 def test_013_empty_return(self):
464 def wsgi_app(environ, start_response):
465 start_response("200 OK", [])
468 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
469 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
470 server_sock = eventlet.wrap_ssl(eventlet.listen(('localhost', 0)),
471 certfile=certificate_file,
472 keyfile=private_key_file,
474 self.spawn_server(sock=server_sock, site=wsgi_app)
476 sock = eventlet.connect(('localhost', server_sock.getsockname()[1]))
477 sock = eventlet.wrap_ssl(sock)
478 sock.write(b'GET /foo HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
479 result = recvall(sock)
480 assert result[-4:] == b'\r\n\r\n'
482 def test_014_chunked_post(self):
483 self.site.application = chunked_post
484 sock = eventlet.connect(self.server_addr)
485 fd = sock.makefile('rwb')
486 fd.write('PUT /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
487 'Transfer-Encoding: chunked\r\n\r\n'
488 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
491 if fd.readline() == b'\r\n':
494 assert response == b'oh hai', 'invalid response %s' % response
496 sock = eventlet.connect(self.server_addr)
497 fd = sock.makefile('rwb')
498 fd.write('PUT /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
499 'Transfer-Encoding: chunked\r\n\r\n'
500 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
503 if fd.readline() == b'\r\n':
506 assert response == b'oh hai', 'invalid response %s' % response
508 sock = eventlet.connect(self.server_addr)
509 fd = sock.makefile('rwb')
510 fd.write('PUT /c HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
511 'Transfer-Encoding: chunked\r\n\r\n'
512 '2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n'.encode())
515 if fd.readline() == b'\r\n':
517 response = fd.read(8192)
518 assert response == b'oh hai', 'invalid response %s' % response
520 def test_015_write(self):
521 self.site.application = use_write
522 sock = eventlet.connect(self.server_addr)
523 sock.sendall(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
524 result1 = read_http(sock)
525 assert 'content-length' in result1.headers_lower
527 sock = eventlet.connect(self.server_addr)
528 sock.sendall(b'GET /b HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
529 result2 = read_http(sock)
530 assert 'transfer-encoding' in result2.headers_lower
531 assert result2.headers_lower['transfer-encoding'] == 'chunked'
533 def test_016_repeated_content_length(self):
534 """content-length header was being doubled up if it was set in
535 start_response and could also be inferred from the iterator
537 def wsgi_app(environ, start_response):
538 start_response('200 OK', [('Content-Length', '7')])
540 self.site.application = wsgi_app
541 sock = eventlet.connect(self.server_addr)
542 fd = sock.makefile('rwb')
543 fd.write(b'GET /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
551 header_lines.append(line)
552 self.assertEqual(1, len(
553 [l for l in header_lines if l.lower().startswith(b'content-length')]))
555 @tests.skip_if_no_ssl
556 def test_017_ssl_zeroreturnerror(self):
558 def server(sock, site, log):
560 serv = wsgi.Server(sock, sock.getsockname(), site, log)
561 client_socket = sock.accept()
562 serv.process_request(client_socket)
565 traceback.print_exc()
568 def wsgi_app(environ, start_response):
569 start_response('200 OK', [])
570 return [environ['wsgi.input'].read()]
572 certificate_file = os.path.join(os.path.dirname(__file__), 'test_server.crt')
573 private_key_file = os.path.join(os.path.dirname(__file__), 'test_server.key')
575 sock = eventlet.wrap_ssl(
576 eventlet.listen(('localhost', 0)),
577 certfile=certificate_file, keyfile=private_key_file,
579 server_coro = eventlet.spawn(server, sock, wsgi_app, self.logfile)
581 client = eventlet.connect(('localhost', sock.getsockname()[1]))
582 client = eventlet.wrap_ssl(client)
583 client.write(b'X') # non-empty payload so that SSL handshake occurs
584 greenio.shutdown_safe(client)
587 success = server_coro.wait()
590 def test_018_http_10_keepalive(self):
591 # verify that if an http/1.0 client sends connection: keep-alive
592 # that we don't close the connection
593 sock = eventlet.connect(self.server_addr)
595 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
596 result1 = read_http(sock)
597 assert 'connection' in result1.headers_lower
598 self.assertEqual('keep-alive', result1.headers_lower['connection'])
600 # repeat request to verify connection is actually still open
601 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
602 result2 = read_http(sock)
603 assert 'connection' in result2.headers_lower
604 self.assertEqual('keep-alive', result2.headers_lower['connection'])
607 def test_019_fieldstorage_compat(self):
608 def use_fieldstorage(environ, start_response):
609 cgi.FieldStorage(fp=environ['wsgi.input'], environ=environ)
610 start_response('200 OK', [('Content-type', 'text/plain')])
613 self.site.application = use_fieldstorage
614 sock = eventlet.connect(self.server_addr)
616 sock.sendall(b'POST / HTTP/1.1\r\n'
617 b'Host: localhost\r\n'
618 b'Connection: close\r\n'
619 b'Transfer-Encoding: chunked\r\n\r\n'
621 b'4\r\n hai\r\n0\r\n\r\n')
622 assert b'hello!' in recvall(sock)
624 def test_020_x_forwarded_for(self):
626 b'GET / HTTP/1.1\r\nHost: localhost\r\n'
627 + b'X-Forwarded-For: 1.2.3.4, 5.6.7.8\r\n\r\n'
630 sock = eventlet.connect(self.server_addr)
631 sock.sendall(request_bytes)
634 assert '1.2.3.4,5.6.7.8,127.0.0.1' in self.logfile.getvalue()
636 # turning off the option should work too
637 self.logfile = six.StringIO()
638 self.spawn_server(log_x_forwarded_for=False)
640 sock = eventlet.connect(self.server_addr)
641 sock.sendall(request_bytes)
644 assert '1.2.3.4' not in self.logfile.getvalue()
645 assert '5.6.7.8' not in self.logfile.getvalue()
646 assert '127.0.0.1' in self.logfile.getvalue()
648 def test_socket_remains_open(self):
649 greenthread.kill(self.killer)
650 server_sock = eventlet.listen(('localhost', 0))
651 server_sock_2 = server_sock.dup()
652 self.spawn_server(sock=server_sock_2)
653 # do a single req/response to verify it's up
654 sock = eventlet.connect(server_sock.getsockname())
655 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
656 result = sock.recv(1024)
657 assert result.startswith(b'HTTP'), result
658 assert result.endswith(b'hello world'), result
660 # shut down the server and verify the server_socket fd is still open,
661 # but the actual socketobject passed in to wsgi.server is closed
662 greenthread.kill(self.killer)
663 eventlet.sleep(0) # make the kill go through
665 server_sock_2.accept()
666 # shouldn't be able to use this one anymore
667 except socket.error as exc:
668 self.assertEqual(support.get_errno(exc), errno.EBADF)
669 self.spawn_server(sock=server_sock)
670 sock = eventlet.connect(server_sock.getsockname())
671 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
672 result = sock.recv(1024)
673 assert result.startswith(b'HTTP'), result
674 assert result.endswith(b'hello world'), result
676 def test_021_environ_clobbering(self):
677 def clobberin_time(environ, start_response):
679 'wsgi.version', 'wsgi.url_scheme',
680 'wsgi.input', 'wsgi.errors', 'wsgi.multithread',
681 'wsgi.multiprocess', 'wsgi.run_once', 'REQUEST_METHOD',
682 'SCRIPT_NAME', 'RAW_PATH_INFO', 'PATH_INFO', 'QUERY_STRING',
683 'CONTENT_TYPE', 'CONTENT_LENGTH', 'SERVER_NAME', 'SERVER_PORT',
685 environ[environ_var] = None
686 start_response('200 OK', [('Content-type', 'text/plain')])
688 self.site.application = clobberin_time
689 sock = eventlet.connect(self.server_addr)
690 sock.sendall(b'GET / HTTP/1.1\r\n'
691 b'Host: localhost\r\n'
692 b'Connection: close\r\n'
694 assert b'200 OK' in recvall(sock)
696 def test_022_custom_pool(self):
697 # just test that it accepts the parameter for now
698 # TODO(waitall): test that it uses the pool and that you can waitall() to
699 # ensure that all clients finished
700 p = eventlet.GreenPool(5)
701 self.spawn_server(custom_pool=p)
703 # this stuff is copied from test_001_server, could be better factored
704 sock = eventlet.connect(self.server_addr)
705 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
706 result = recvall(sock)
707 assert result.startswith(b'HTTP'), result
708 assert result.endswith(b'hello world'), result
710 def test_023_bad_content_length(self):
711 sock = eventlet.connect(self.server_addr)
712 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nContent-length: argh\r\n\r\n')
713 result = recvall(sock)
714 assert result.startswith(b'HTTP'), result
715 assert b'400 Bad Request' in result, result
716 assert b'500' not in result, result
718 def test_024_expect_100_continue(self):
719 def wsgi_app(environ, start_response):
720 if int(environ['CONTENT_LENGTH']) > 1024:
721 start_response('417 Expectation Failed', [('Content-Length', '7')])
724 text = environ['wsgi.input'].read()
725 start_response('200 OK', [('Content-Length', str(len(text)))])
727 self.site.application = wsgi_app
728 sock = eventlet.connect(self.server_addr)
729 fd = sock.makefile('rwb')
730 fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
731 b'Expect: 100-continue\r\n\r\n')
733 result = read_http(sock)
734 self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
735 self.assertEqual(result.body, b'failure')
737 b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
738 b'Expect: 100-continue\r\n\r\ntesting')
746 header_lines.append(line)
747 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
754 header_lines.append(line)
755 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
756 assert fd.read(7) == b'testing'
760 def test_024a_expect_100_continue_with_headers(self):
761 def wsgi_app(environ, start_response):
762 if int(environ['CONTENT_LENGTH']) > 1024:
763 start_response('417 Expectation Failed', [('Content-Length', '7')])
766 environ['wsgi.input'].set_hundred_continue_response_headers(
767 [('Hundred-Continue-Header-1', 'H1'),
768 ('Hundred-Continue-Header-2', 'H2'),
769 ('Hundred-Continue-Header-k', 'Hk')])
770 text = environ['wsgi.input'].read()
771 start_response('200 OK', [('Content-Length', str(len(text)))])
773 self.site.application = wsgi_app
774 sock = eventlet.connect(self.server_addr)
775 fd = sock.makefile('rwb')
776 fd.write(b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\n'
777 b'Expect: 100-continue\r\n\r\n')
779 result = read_http(sock)
780 self.assertEqual(result.status, 'HTTP/1.1 417 Expectation Failed')
781 self.assertEqual(result.body, b'failure')
783 b'PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\n'
784 b'Expect: 100-continue\r\n\r\ntesting')
792 header_lines.append(line.strip())
793 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
794 headers = dict((k, v) for k, v in (h.split(b': ', 1) for h in header_lines[1:]))
795 assert b'Hundred-Continue-Header-1' in headers
796 assert b'Hundred-Continue-Header-2' in headers
797 assert b'Hundred-Continue-Header-K' in headers
798 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
799 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
800 self.assertEqual(b'Hk', headers[b'Hundred-Continue-Header-K'])
807 header_lines.append(line)
808 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
809 self.assertEqual(fd.read(7), b'testing')
813 def test_024b_expect_100_continue_with_headers_multiple_chunked(self):
814 def wsgi_app(environ, start_response):
815 environ['wsgi.input'].set_hundred_continue_response_headers(
816 [('Hundred-Continue-Header-1', 'H1'),
817 ('Hundred-Continue-Header-2', 'H2')])
818 text = environ['wsgi.input'].read()
820 environ['wsgi.input'].set_hundred_continue_response_headers(
821 [('Hundred-Continue-Header-3', 'H3')])
822 environ['wsgi.input'].send_hundred_continue_response()
824 text += environ['wsgi.input'].read()
826 start_response('200 OK', [('Content-Length', str(len(text)))])
828 self.site.application = wsgi_app
829 sock = eventlet.connect(self.server_addr)
830 fd = sock.makefile('rwb')
831 fd.write(b'PUT /a HTTP/1.1\r\n'
832 b'Host: localhost\r\nConnection: close\r\n'
833 b'Transfer-Encoding: chunked\r\n'
834 b'Expect: 100-continue\r\n\r\n')
837 # Expect 1st 100-continue response
844 header_lines.append(line.strip())
845 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
846 headers = dict((k, v) for k, v in (h.split(b': ', 1)
847 for h in header_lines[1:]))
848 assert b'Hundred-Continue-Header-1' in headers
849 assert b'Hundred-Continue-Header-2' in headers
850 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
851 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
854 fd.write(b'5\r\nfirst\r\n8\r\n message\r\n0\r\n\r\n')
857 # Expect a 2nd 100-continue response
864 header_lines.append(line.strip())
865 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
866 headers = dict((k, v) for k, v in (h.split(b': ', 1)
867 for h in header_lines[1:]))
868 assert b'Hundred-Continue-Header-3' in headers
869 self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
872 fd.write(b'8\r\n, second\r\n8\r\n message\r\n0\r\n\r\n')
875 # Expect final 200-OK
882 header_lines.append(line.strip())
883 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
885 self.assertEqual(fd.read(29), b'first message, second message')
889 def test_024c_expect_100_continue_with_headers_multiple_nonchunked(self):
890 def wsgi_app(environ, start_response):
892 environ['wsgi.input'].set_hundred_continue_response_headers(
893 [('Hundred-Continue-Header-1', 'H1'),
894 ('Hundred-Continue-Header-2', 'H2')])
895 text = environ['wsgi.input'].read(13)
897 environ['wsgi.input'].set_hundred_continue_response_headers(
898 [('Hundred-Continue-Header-3', 'H3')])
899 environ['wsgi.input'].send_hundred_continue_response()
901 text += environ['wsgi.input'].read(16)
903 start_response('200 OK', [('Content-Length', str(len(text)))])
906 self.site.application = wsgi_app
907 sock = eventlet.connect(self.server_addr)
908 fd = sock.makefile('rwb')
909 fd.write(b'PUT /a HTTP/1.1\r\n'
910 b'Host: localhost\r\nConnection: close\r\n'
911 b'Content-Length: 29\r\n'
912 b'Expect: 100-continue\r\n\r\n')
915 # Expect 1st 100-continue response
922 header_lines.append(line.strip())
923 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
924 headers = dict((k, v) for k, v in (h.split(b': ', 1)
925 for h in header_lines[1:]))
926 assert b'Hundred-Continue-Header-1' in headers
927 assert b'Hundred-Continue-Header-2' in headers
928 self.assertEqual(b'H1', headers[b'Hundred-Continue-Header-1'])
929 self.assertEqual(b'H2', headers[b'Hundred-Continue-Header-2'])
932 fd.write(b'first message')
935 # Expect a 2nd 100-continue response
942 header_lines.append(line.strip())
943 assert header_lines[0].startswith(b'HTTP/1.1 100 Continue')
944 headers = dict((k, v) for k, v in (h.split(b': ', 1)
945 for h in header_lines[1:]))
946 assert b'Hundred-Continue-Header-3' in headers
947 self.assertEqual(b'H3', headers[b'Hundred-Continue-Header-3'])
950 fd.write(b', second message\r\n')
953 # Expect final 200-OK
960 header_lines.append(line.strip())
961 assert header_lines[0].startswith(b'HTTP/1.1 200 OK')
963 self.assertEqual(fd.read(29), b'first message, second message')
967 def test_025_accept_errors(self):
968 debug.hub_exceptions(True)
969 listener = greensocket.socket()
970 listener.bind(('localhost', 0))
971 # NOT calling listen, to trigger the error
972 with capture_stderr() as log:
973 self.spawn_server(sock=listener)
974 eventlet.sleep(0) # need to enter server loop
976 eventlet.connect(self.server_addr)
977 self.fail("Didn't expect to connect")
978 except socket.error as exc:
979 self.assertEqual(support.get_errno(exc), errno.ECONNREFUSED)
981 log_content = log.getvalue()
982 assert 'Invalid argument' in log_content, log_content
983 debug.hub_exceptions(False)
985 def test_026_log_format(self):
986 self.spawn_server(log_format="HI %(request_line)s HI")
987 sock = eventlet.connect(self.server_addr)
988 sock.sendall(b'GET /yo! HTTP/1.1\r\nHost: localhost\r\n\r\n')
991 assert '\nHI GET /yo! HTTP/1.1 HI\n' in self.logfile.getvalue(), self.logfile.getvalue()
993 def test_close_chunked_with_1_0_client(self):
994 # verify that if we return a generator from our app
995 # and we're not speaking with a 1.1 client, that we
996 # close the connection
997 self.site.application = chunked_app
998 sock = eventlet.connect(self.server_addr)
1000 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1002 result = read_http(sock)
1003 self.assertEqual(result.headers_lower['connection'], 'close')
1004 self.assertNotEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1005 self.assertEqual(result.body, b"thisischunked")
1007 def test_chunked_response_when_app_yields_empty_string(self):
1008 def empty_string_chunked_app(env, start_response):
1009 env['eventlet.minimum_write_chunk_size'] = 0 # no buffering
1010 start_response('200 OK', [('Content-type', 'text/plain')])
1011 return iter([b"stuff", b"", b"more stuff"])
1013 self.site.application = empty_string_chunked_app
1014 sock = eventlet.connect(self.server_addr)
1016 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1018 result = read_http(sock)
1019 self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1020 self.assertEqual(result.body, b"5\r\nstuff\r\na\r\nmore stuff\r\n0\r\n\r\n")
1022 def test_minimum_chunk_size_parameter_leaves_httpprotocol_class_member_intact(self):
1023 start_size = wsgi.HttpProtocol.minimum_chunk_size
1025 self.spawn_server(minimum_chunk_size=start_size * 2)
1026 sock = eventlet.connect(self.server_addr)
1027 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1030 self.assertEqual(wsgi.HttpProtocol.minimum_chunk_size, start_size)
1033 def test_error_in_chunked_closes_connection(self):
1034 # From http://rhodesmill.org/brandon/2013/chunked-wsgi/
1035 self.spawn_server(minimum_chunk_size=1)
1037 self.site.application = chunked_fail_app
1038 sock = eventlet.connect(self.server_addr)
1040 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1042 result = read_http(sock)
1043 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1044 self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1046 b'27\r\nThe dwarves of yore made mighty spells,\r\n'
1047 b'25\r\nWhile hammers fell like ringing bells\r\n')
1048 self.assertEqual(result.body, expected_body)
1050 # verify that socket is closed by server
1051 self.assertEqual(sock.recv(1), b'')
1053 def test_026_http_10_nokeepalive(self):
1054 # verify that if an http/1.0 client sends connection: keep-alive
1055 # and the server doesn't accept keep-alives, we close the connection
1056 self.spawn_server(keepalive=False)
1057 sock = eventlet.connect(self.server_addr)
1059 sock.sendall(b'GET / HTTP/1.0\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n')
1060 result = read_http(sock)
1061 self.assertEqual(result.headers_lower['connection'], 'close')
1063 def test_027_keepalive_chunked(self):
1064 self.site.application = chunked_post
1065 sock = eventlet.connect(self.server_addr)
1067 b'Host: localhost\r\nTransfer-Encoding: chunked\r\n\r\n' +
1068 b'10\r\n0123456789abcdef\r\n0\r\n\r\n')
1069 sock.sendall(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1071 sock.sendall(b'PUT /b HTTP/1.1\r\n' + common_suffix)
1073 sock.sendall(b'PUT /c HTTP/1.1\r\n' + common_suffix)
1075 sock.sendall(b'PUT /a HTTP/1.1\r\n' + common_suffix)
1079 @tests.skip_if_no_ssl
1080 def test_028_ssl_handshake_errors(self):
1085 wsgi.server(sock=sock, site=hello_world, log=self.logfile)
1086 errored[0] = 'SSL handshake error caused wsgi.server to exit.'
1087 except greenthread.greenlet.GreenletExit:
1089 except Exception as e:
1090 errored[0] = 'SSL handshake error raised exception %s.' % e
1092 for data in ('', 'GET /non-ssl-request HTTP/1.0\r\n\r\n'):
1093 srv_sock = eventlet.wrap_ssl(
1094 eventlet.listen(('localhost', 0)),
1095 certfile=certificate_file, keyfile=private_key_file,
1097 addr = srv_sock.getsockname()
1098 g = eventlet.spawn_n(server, srv_sock)
1099 client = eventlet.connect(addr)
1100 if data: # send non-ssl request
1101 client.sendall(data.encode())
1102 else: # close sock prematurely
1104 eventlet.sleep(0) # let context switch back to server
1105 assert not errored[0], errored[0]
1106 # make another request to ensure the server's still alive
1108 client = ssl.wrap_socket(eventlet.connect(addr))
1109 client.write(b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n')
1110 result = recvall(client)
1111 assert result.startswith(b'HTTP'), result
1112 assert result.endswith(b'hello world')
1114 pass # TODO(openssl): should test with OpenSSL
1117 def test_029_posthooks(self):
1118 posthook1_count = [0]
1119 posthook2_count = [0]
1121 def posthook1(env, value, multiplier=1):
1122 self.assertEqual(env['local.test'], 'test_029_posthooks')
1123 posthook1_count[0] += value * multiplier
1125 def posthook2(env, value, divisor=1):
1126 self.assertEqual(env['local.test'], 'test_029_posthooks')
1127 posthook2_count[0] += value / divisor
1129 def one_posthook_app(env, start_response):
1130 env['local.test'] = 'test_029_posthooks'
1131 if 'eventlet.posthooks' not in env:
1132 start_response('500 eventlet.posthooks not supported',
1133 [('Content-Type', 'text/plain')])
1135 env['eventlet.posthooks'].append(
1136 (posthook1, (2,), {'multiplier': 3}))
1137 start_response('200 OK', [('Content-Type', 'text/plain')])
1139 self.site.application = one_posthook_app
1140 sock = eventlet.connect(self.server_addr)
1141 fp = sock.makefile('rwb')
1142 fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1144 self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1147 self.assertEqual(posthook1_count[0], 6)
1148 self.assertEqual(posthook2_count[0], 0)
1150 def two_posthook_app(env, start_response):
1151 env['local.test'] = 'test_029_posthooks'
1152 if 'eventlet.posthooks' not in env:
1153 start_response('500 eventlet.posthooks not supported',
1154 [('Content-Type', 'text/plain')])
1156 env['eventlet.posthooks'].append(
1157 (posthook1, (4,), {'multiplier': 5}))
1158 env['eventlet.posthooks'].append(
1159 (posthook2, (100,), {'divisor': 4}))
1160 start_response('200 OK', [('Content-Type', 'text/plain')])
1162 self.site.application = two_posthook_app
1163 sock = eventlet.connect(self.server_addr)
1164 fp = sock.makefile('rwb')
1165 fp.write(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1167 self.assertEqual(fp.readline(), b'HTTP/1.1 200 OK\r\n')
1170 self.assertEqual(posthook1_count[0], 26)
1171 self.assertEqual(posthook2_count[0], 25)
1173 def test_030_reject_long_header_lines(self):
1174 sock = eventlet.connect(self.server_addr)
1175 request = 'GET / HTTP/1.0\r\nHost: localhost\r\nLong: %s\r\n\r\n' % \
1177 send_expect_close(sock, request.encode())
1178 result = read_http(sock)
1179 self.assertEqual(result.status, 'HTTP/1.0 400 Header Line Too Long')
1181 def test_031_reject_large_headers(self):
1182 sock = eventlet.connect(self.server_addr)
1183 headers = ('Name: %s\r\n' % ('a' * 7000,)) * 20
1184 request = 'GET / HTTP/1.0\r\nHost: localhost\r\n%s\r\n\r\n' % headers
1185 send_expect_close(sock, request.encode())
1186 result = read_http(sock)
1187 self.assertEqual(result.status, 'HTTP/1.0 400 Headers Too Large')
1189 def test_032_wsgi_input_as_iterable(self):
1190 # https://bitbucket.org/eventlet/eventlet/issue/150
1191 # env['wsgi.input'] returns a single byte at a time
1192 # when used as an iterator
1195 def echo_by_iterating(env, start_response):
1196 start_response('200 OK', [('Content-type', 'text/plain')])
1197 for chunk in env['wsgi.input']:
1201 self.site.application = echo_by_iterating
1202 upload_data = b'123456789abcdef' * 100
1204 'POST / HTTP/1.0\r\n'
1205 'Host: localhost\r\n'
1206 'Content-Length: %i\r\n\r\n%s'
1207 ) % (len(upload_data), bytes_to_str(upload_data))
1208 sock = eventlet.connect(self.server_addr)
1209 sock.sendall(request.encode())
1210 result = read_http(sock)
1211 self.assertEqual(result.body, upload_data)
1212 self.assertEqual(g[0], 1)
1214 def test_zero_length_chunked_response(self):
1215 def zero_chunked_app(env, start_response):
1216 start_response('200 OK', [('Content-type', 'text/plain')])
1219 self.site.application = zero_chunked_app
1220 sock = eventlet.connect(self.server_addr)
1222 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1223 response = recvall(sock).split(b'\r\n')
1230 assert b'Transfer-Encoding: chunked' in b''.join(headers), headers
1231 # should only be one chunk of zero size with two blank lines
1232 # (one terminates the chunk, one terminates the body)
1233 self.assertEqual(response, [b'0', b'', b''])
1235 def test_configurable_url_length_limit(self):
1236 self.spawn_server(url_length_limit=20000)
1237 sock = eventlet.connect(self.server_addr)
1239 request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path
1240 fd = sock.makefile('rwb')
1241 fd.write(request.encode())
1243 result = fd.readline()
1245 # windows closes the socket before the data is flushed,
1246 # so we never get anything back
1247 status = result.split(b' ')[1]
1248 self.assertEqual(status, b'200')
1251 def test_aborted_chunked_post(self):
1252 read_content = event.Event()
1255 def chunk_reader(env, start_response):
1257 content = env['wsgi.input'].read(1024)
1261 read_content.send(content)
1262 start_response('200 OK', [('Content-Type', 'text/plain')])
1265 self.site.application = chunk_reader
1266 expected_body = 'a bunch of stuff'
1267 data = "\r\n".join(['PUT /somefile HTTP/1.0',
1268 'Transfer-Encoding: chunked',
1272 # start PUT-ing some chunked data but close prematurely
1273 sock = eventlet.connect(self.server_addr)
1274 sock.sendall(data.encode())
1276 # the test passes if we successfully get here, and read all the data
1277 # in spite of the early close
1278 self.assertEqual(read_content.wait(), b'ok')
1281 def test_aborted_chunked_post_between_chunks(self):
1282 read_content = event.Event()
1285 def chunk_reader(env, start_response):
1287 content = env['wsgi.input'].read(1024)
1288 except wsgi.ChunkReadError:
1291 except Exception as err:
1293 content = b'wrong exception: ' + str(err).encode()
1294 read_content.send(content)
1295 start_response('200 OK', [('Content-Type', 'text/plain')])
1297 self.site.application = chunk_reader
1298 expected_body = 'A' * 0xdb
1299 data = "\r\n".join(['PUT /somefile HTTP/1.0',
1300 'Transfer-Encoding: chunked',
1304 # start PUT-ing some chunked data but close prematurely
1305 sock = eventlet.connect(self.server_addr)
1306 sock.sendall(data.encode())
1308 # the test passes if we successfully get here, and read all the data
1309 # in spite of the early close
1310 self.assertEqual(read_content.wait(), b'ok')
1313 def test_aborted_chunked_post_bad_chunks(self):
1314 read_content = event.Event()
1317 def chunk_reader(env, start_response):
1319 content = env['wsgi.input'].read(1024)
1320 except wsgi.ChunkReadError:
1323 except Exception as err:
1325 content = b'wrong exception: ' + str(err).encode()
1326 read_content.send(content)
1327 start_response('200 OK', [('Content-Type', 'text/plain')])
1329 self.site.application = chunk_reader
1330 expected_body = 'look here is some data for you'
1331 data = "\r\n".join(['PUT /somefile HTTP/1.0',
1332 'Transfer-Encoding: chunked',
1336 # start PUT-ing some garbage
1337 sock = eventlet.connect(self.server_addr)
1338 sock.sendall(data.encode())
1340 # the test passes if we successfully get here, and read all the data
1341 # in spite of the early close
1342 self.assertEqual(read_content.wait(), b'ok')
1345 def test_exceptions_close_connection(self):
1346 def wsgi_app(environ, start_response):
1347 raise RuntimeError("intentional error")
1348 self.site.application = wsgi_app
1349 sock = eventlet.connect(self.server_addr)
1350 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1351 result = read_http(sock)
1352 self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1353 self.assertEqual(result.headers_lower['connection'], 'close')
1354 assert 'transfer-encoding' not in result.headers_lower
1356 def test_unicode_with_only_ascii_characters_works(self):
1357 def wsgi_app(environ, start_response):
1358 start_response("200 OK", [])
1361 self.site.application = wsgi_app
1362 sock = eventlet.connect(self.server_addr)
1363 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1364 result = read_http(sock)
1365 assert b'xxx' in result.body
1367 def test_unicode_with_nonascii_characters_raises_error(self):
1368 def wsgi_app(environ, start_response):
1369 start_response("200 OK", [])
1372 self.site.application = wsgi_app
1373 sock = eventlet.connect(self.server_addr)
1374 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1375 result = read_http(sock)
1376 self.assertEqual(result.status, 'HTTP/1.1 500 Internal Server Error')
1377 self.assertEqual(result.headers_lower['connection'], 'close')
1379 def test_path_info_decoding(self):
1380 def wsgi_app(environ, start_response):
1381 start_response("200 OK", [])
1382 yield six.b("decoded: %s" % environ['PATH_INFO'])
1383 yield six.b("raw: %s" % environ['RAW_PATH_INFO'])
1384 self.site.application = wsgi_app
1385 sock = eventlet.connect(self.server_addr)
1386 sock.sendall(b'GET /a*b@%40%233 HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1387 result = read_http(sock)
1388 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1389 assert b'decoded: /a*b@@#3' in result.body
1390 assert b'raw: /a*b@%40%233' in result.body
1392 def test_ipv6(self):
1394 sock = eventlet.listen(('::1', 0), family=socket.AF_INET6)
1395 except (socket.gaierror, socket.error): # probably no ipv6
1397 log = six.StringIO()
1398 # first thing the server does is try to log the IP it's bound to
1402 wsgi.server(sock=sock, log=log, site=Site())
1404 log.write(b'broken')
1406 self.spawn_thread(run_server)
1408 logval = log.getvalue()
1411 logval = log.getvalue()
1412 if 'broked' in logval:
1413 self.fail('WSGI server raised exception with ipv6 socket')
1415 def test_debug(self):
1416 self.spawn_server(debug=False)
1418 def crasher(env, start_response):
1419 raise RuntimeError("intentional crash")
1420 self.site.application = crasher
1422 sock = eventlet.connect(self.server_addr)
1423 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1424 result1 = read_http(sock)
1425 self.assertEqual(result1.status, 'HTTP/1.1 500 Internal Server Error')
1426 self.assertEqual(result1.body, b'')
1427 self.assertEqual(result1.headers_lower['connection'], 'close')
1428 assert 'transfer-encoding' not in result1.headers_lower
1430 # verify traceback when debugging enabled
1431 self.spawn_server(debug=True)
1432 self.site.application = crasher
1433 sock = eventlet.connect(self.server_addr)
1434 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1435 result2 = read_http(sock)
1436 self.assertEqual(result2.status, 'HTTP/1.1 500 Internal Server Error')
1437 assert b'intentional crash' in result2.body, result2.body
1438 assert b'RuntimeError' in result2.body, result2.body
1439 assert b'Traceback' in result2.body, result2.body
1440 self.assertEqual(result2.headers_lower['connection'], 'close')
1441 assert 'transfer-encoding' not in result2.headers_lower
1443 def test_client_disconnect(self):
1444 """Issue #95 Server must handle disconnect from client in the middle of response
1446 def long_response(environ, start_response):
1447 start_response('200 OK', [('Content-Length', '9876')])
1450 server_sock = eventlet.listen(('localhost', 0))
1451 self.server_addr = server_sock.getsockname()
1452 server = wsgi.Server(server_sock, server_sock.getsockname(), long_response,
1456 sock = eventlet.connect(server_sock.getsockname())
1457 sock.send(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1460 request_thread = eventlet.spawn(make_request)
1461 server_conn = server_sock.accept()
1462 # Next line must not raise IOError -32 Broken pipe
1463 server.process_request(server_conn)
1464 request_thread.wait()
1467 def test_server_connection_timeout_exception(self):
1468 # Handle connection socket timeouts
1469 # https://bitbucket.org/eventlet/eventlet/issue/143/
1470 # Runs tests.wsgi_test_conntimeout in a separate process.
1471 tests.run_isolated('wsgi_connection_timeout.py')
1473 def test_server_socket_timeout(self):
1474 self.spawn_server(socket_timeout=0.1)
1475 sock = eventlet.connect(self.server_addr)
1476 sock.send(b'GET / HTTP/1.1\r\n')
1480 assert False, 'Expected ConnectionClosed exception'
1481 except ConnectionClosed:
1484 def test_disable_header_name_capitalization(self):
1485 # Disable HTTP header name capitalization
1487 # https://github.com/eventlet/eventlet/issues/80
1488 random_case_header = ('eTAg', 'TAg-VAluE')
1490 def wsgi_app(environ, start_response):
1491 start_response('200 oK', [random_case_header])
1494 self.spawn_server(site=wsgi_app, capitalize_response_headers=False)
1496 sock = eventlet.connect(self.server_addr)
1497 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1498 result = read_http(sock)
1500 self.assertEqual(result.status, 'HTTP/1.1 200 oK')
1501 self.assertEqual(result.headers_lower[random_case_header[0].lower()], random_case_header[1])
1502 self.assertEqual(result.headers_original[random_case_header[0]], random_case_header[1])
1504 def test_log_unix_address(self):
1505 tempdir = tempfile.mkdtemp('eventlet_test_log_unix_address')
1508 sock = eventlet.listen(tempdir + '/socket', socket.AF_UNIX)
1509 path = sock.getsockname()
1511 log = six.StringIO()
1512 self.spawn_server(sock=sock, log=log)
1513 eventlet.sleep(0) # need to enter server loop
1514 assert 'http:' + path in log.getvalue()
1516 shutil.rmtree(tempdir)
1519 def read_headers(sock):
1520 fd = sock.makefile('rb')
1522 response_line = fd.readline()
1523 except socket.error as exc:
1524 if support.get_errno(exc) == 10053:
1525 raise ConnectionClosed
1527 if not response_line:
1528 raise ConnectionClosed
1532 line = fd.readline()
1536 header_lines.append(line)
1538 for x in header_lines:
1542 key, value = x.split(b': ', 1)
1543 assert key.lower() not in headers, "%s header duplicated" % key
1544 headers[bytes_to_str(key.lower())] = bytes_to_str(value)
1545 return bytes_to_str(response_line), headers
1548 class IterableAlreadyHandledTest(_TestBase):
1550 self.site = IterableSite()
1553 return IterableApp(True)
1555 def test_iterable_app_keeps_socket_open_unless_connection_close_sent(self):
1556 self.site.application = self.get_app()
1557 sock = eventlet.connect(self.server_addr)
1559 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\n\r\n')
1560 response_line, headers = read_headers(sock)
1561 self.assertEqual(response_line, 'HTTP/1.1 200 OK\r\n')
1562 assert 'connection' not in headers
1564 sock.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n')
1565 result = read_http(sock)
1566 self.assertEqual(result.status, 'HTTP/1.1 200 OK')
1567 self.assertEqual(result.headers_lower.get('transfer-encoding'), 'chunked')
1568 self.assertEqual(result.body, b'0\r\n\r\n') # Still coming back chunked
1571 class ProxiedIterableAlreadyHandledTest(IterableAlreadyHandledTest):
1572 # same thing as the previous test but ensuring that it works with tpooled
1573 # results as well as regular ones
1574 @tests.skip_with_pyevent
1576 return tpool.Proxy(super(ProxiedIterableAlreadyHandledTest, self).get_app())
1580 super(ProxiedIterableAlreadyHandledTest, self).tearDown()
1583 class TestChunkedInput(_TestBase):
1586 def application(self, env, start_response):
1587 input = env['wsgi.input']
1590 pi = env["PATH_INFO"]
1592 if pi == "/short-read":
1595 elif pi == "/lines":
1600 response.append(b"pong")
1601 elif pi.startswith("/yield_spaces"):
1602 if pi.endswith('override_min'):
1603 env['eventlet.minimum_write_chunk_size'] = 1
1604 self.yield_next_space = False
1606 def response_iter():
1609 while not self.yield_next_space and num_sleeps < 200:
1615 start_response('200 OK',
1616 [('Content-Type', 'text/plain'),
1617 ('Content-Length', '2')])
1618 return response_iter()
1620 raise RuntimeError("bad path")
1622 start_response('200 OK', [('Content-Type', 'text/plain')])
1626 return eventlet.connect(self.server_addr)
1630 self.site.application = self.application
1632 def chunk_encode(self, chunks, dirt=""):
1635 b += "%x%s\r\n%s\r\n" % (len(c), dirt, c)
1638 def body(self, dirt=""):
1639 return self.chunk_encode(["this", " is ", "chunked", "\nline",
1640 " 2", "\n", "line3", ""], dirt=dirt)
1643 fd.sendall(b"GET /ping HTTP/1.1\r\n\r\n")
1644 self.assertEqual(read_http(fd).body, b"pong")
1646 def test_short_read_with_content_length(self):
1648 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1649 "Content-Length:1000\r\n\r\n" + body
1652 fd.sendall(req.encode())
1653 self.assertEqual(read_http(fd).body, b"this is ch")
1658 def test_short_read_with_zero_content_length(self):
1660 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n" \
1661 "Content-Length:0\r\n\r\n" + body
1663 fd.sendall(req.encode())
1664 self.assertEqual(read_http(fd).body, b"this is ch")
1669 def test_short_read(self):
1671 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1674 fd.sendall(req.encode())
1675 self.assertEqual(read_http(fd).body, b"this is ch")
1680 def test_dirt(self):
1681 body = self.body(dirt="; here is dirt\0bla")
1682 req = "POST /ping HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1685 fd.sendall(req.encode())
1686 self.assertEqual(read_http(fd).body, b"pong")
1691 def test_chunked_readline(self):
1693 req = "POST /lines HTTP/1.1\r\nContent-Length: %s\r\n" \
1694 "transfer-encoding: Chunked\r\n\r\n%s" % (len(body), body)
1697 fd.sendall(req.encode())
1698 self.assertEqual(read_http(fd).body, b'this is chunked\nline 2\nline3')
1701 def test_chunked_readline_wsgi_override_minimum_chunk_size(self):
1704 fd.sendall(b"POST /yield_spaces/override_min HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1707 with eventlet.Timeout(.1):
1709 one_byte = fd.recv(1)
1710 resp_so_far += one_byte
1711 if resp_so_far.endswith(b'\r\n\r\n'):
1713 self.assertEqual(fd.recv(1), b' ')
1715 with eventlet.Timeout(.1):
1717 except eventlet.Timeout:
1721 self.yield_next_space = True
1723 with eventlet.Timeout(.1):
1724 self.assertEqual(fd.recv(1), b' ')
1726 def test_chunked_readline_wsgi_not_override_minimum_chunk_size(self):
1729 fd.sendall(b"POST /yield_spaces HTTP/1.1\r\nContent-Length: 0\r\n\r\n")
1733 with eventlet.Timeout(.1):
1735 one_byte = fd.recv(1)
1736 resp_so_far += one_byte
1737 if resp_so_far.endswith(b'\r\n\r\n'):
1739 self.assertEqual(fd.recv(1), b' ')
1740 except eventlet.Timeout:
1745 def test_close_before_finished(self):
1749 got_signal.append(1)
1750 raise KeyboardInterrupt()
1752 signal.signal(signal.SIGALRM, handler)
1757 req = "POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body
1760 fd.sendall(req.encode())
1765 # This is needed because on Python 3 GreenSocket.recv_into is called
1766 # rather than recv; recv_into right now (git 5ec3a3c) trampolines to
1767 # the hub *before* attempting to read anything from a file descriptor
1768 # therefore we need one extra context switch to let it notice closed
1769 # socket, die and leave the hub empty
1774 signal.signal(signal.SIGALRM, signal.SIG_DFL)
1776 assert not got_signal, "caught alarm signal. infinite loop detected."
1779 if __name__ == '__main__':