# Copyright (c) 2007, Linden Research, Inc. # Copyright (c) 2009-2010 gevent contributors # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # pylint: disable=too-many-lines,unused-argument,too-many-ancestors from __future__ import print_function from gevent import monkey monkey.patch_all() from contextlib import contextmanager try: from urllib.parse import parse_qs except ImportError: # Python 2 from urlparse import parse_qs import os import sys try: # On Python 2, we want the C-optimized version if # available; it has different corner-case behaviour than # the Python implementation, and it used by socket.makefile # by default. from cStringIO import StringIO except ImportError: from io import BytesIO as StringIO import weakref import unittest from wsgiref.validate import validator import gevent.testing as greentest import gevent from gevent.testing import PY3, PYPY from gevent import socket from gevent import pywsgi from gevent.pywsgi import Input CONTENT_LENGTH = 'Content-Length' CONN_ABORTED_ERRORS = greentest.CONN_ABORTED_ERRORS REASONS = { 200: 'OK', 500: 'Internal Server Error' } class ConnectionClosed(Exception): pass def read_headers(fd): response_line = fd.readline() if not response_line: raise ConnectionClosed response_line = response_line.decode('latin-1') headers = {} while True: line = fd.readline().strip() if not line: break line = line.decode('latin-1') try: key, value = line.split(': ', 1) except: print('Failed to split: %r' % (line, )) raise assert key.lower() not in {x.lower() for x in headers}, 'Header %r:%r sent more than once: %r' % (key, value, headers) headers[key] = value return response_line, headers def iread_chunks(fd): while True: line = fd.readline() chunk_size = line.strip() try: chunk_size = int(chunk_size, 16) except: print('Failed to parse chunk size: %r' % line) raise if chunk_size == 0: crlf = fd.read(2) assert crlf == b'\r\n', repr(crlf) break data = fd.read(chunk_size) yield data crlf = fd.read(2) assert crlf == b'\r\n', repr(crlf) class Response(object): def __init__(self, status_line, headers): self.status_line = status_line self.headers = headers self.body = None self.chunks = False try: version, code, self.reason = status_line[:-2].split(' ', 2) self.code = int(code) HTTP, self.version = version.split('/') assert HTTP == 'HTTP', repr(HTTP) assert self.version in ('1.0', '1.1'), repr(self.version) except Exception: print('Error: %r' % status_line) raise def __iter__(self): yield self.status_line yield self.headers yield self.body def __str__(self): args = (self.__class__.__name__, self.status_line, self.headers, self.body, self.chunks) return '<%s status_line=%r headers=%r body=%r chunks=%r>' % args def assertCode(self, code): if hasattr(code, '__contains__'): assert self.code in code, 'Unexpected code: %r (expected %r)\n%s' % (self.code, code, self) else: assert self.code == code, 'Unexpected code: %r (expected %r)\n%s' % (self.code, code, self) def assertReason(self, reason): assert self.reason == reason, 'Unexpected reason: %r (expected %r)\n%s' % (self.reason, reason, self) def assertVersion(self, version): assert self.version == version, 'Unexpected version: %r (expected %r)\n%s' % (self.version, version, self) def assertHeader(self, header, value): real_value = self.headers.get(header, False) assert real_value == value, \ 'Unexpected header %r: %r (expected %r)\n%s' % (header, real_value, value, self) def assertBody(self, body): if isinstance(body, str) and PY3: body = body.encode("ascii") assert self.body == body, 'Unexpected body: %r (expected %r)\n%s' % (self.body, body, self) @classmethod def read(cls, fd, code=200, reason='default', version='1.1', body=None, chunks=None, content_length=None): # pylint:disable=too-many-branches _status_line, headers = read_headers(fd) self = cls(_status_line, headers) if code is not None: self.assertCode(code) if reason == 'default': reason = REASONS.get(code) if reason is not None: self.assertReason(reason) if version is not None: self.assertVersion(version) if self.code == 100: return self if content_length is not None: if isinstance(content_length, int): content_length = str(content_length) self.assertHeader('Content-Length', content_length) try: if 'chunked' in headers.get('Transfer-Encoding', ''): if CONTENT_LENGTH in headers: print("WARNING: server used chunked transfer-encoding despite having Content-Length header (libevent 1.x's bug)") self.chunks = list(iread_chunks(fd)) self.body = b''.join(self.chunks) elif CONTENT_LENGTH in headers: num = int(headers[CONTENT_LENGTH]) self.body = fd.read(num) else: self.body = fd.read() except: print('Response.read failed to read the body:\n%s' % self) import traceback; traceback.print_exc() raise if body is not None: self.assertBody(body) if chunks is not None: assert chunks == self.chunks, (chunks, self.chunks) return self read_http = Response.read class TestCase(greentest.TestCase): server = None validator = staticmethod(validator) application = None # Bind to default address, which should give us ipv6 (when available) # and ipv4. (see self.connect()) listen_addr = greentest.DEFAULT_BIND_ADDR # connect on ipv4, even though we bound to ipv6 too # to prove ipv4 works...except on Windows, it apparently doesn't. # So use the hostname. connect_addr = greentest.DEFAULT_LOCAL_HOST_ADDR def init_logger(self): import logging logger = logging.getLogger('gevent.pywsgi') return logger def init_server(self, application): logger = self.logger = self.init_logger() self.server = pywsgi.WSGIServer((self.listen_addr, 0), application, log=logger, error_log=logger) def setUp(self): application = self.application if self.validator is not None: application = self.validator(application) self.init_server(application) self.server.start() while not self.server.server_port: print("Waiting on server port") self.port = self.server.server_port assert self.port greentest.TestCase.setUp(self) if greentest.CPYTHON and greentest.PY2: # Keeping raw sockets alive keeps SSL sockets # from being closed too, at least on CPython2, so we # need to use weakrefs. # In contrast, on PyPy, *only* having a weakref lets the # original socket die and leak def _close_on_teardown(self, resource): self.close_on_teardown.append(weakref.ref(resource)) return resource def _tearDownCloseOnTearDown(self): self.close_on_teardown = [r() for r in self.close_on_teardown if r() is not None] super(TestCase, self)._tearDownCloseOnTearDown() def tearDown(self): greentest.TestCase.tearDown(self) if self.server is not None: with gevent.Timeout.start_new(0.5): self.server.stop() self.server = None if greentest.PYPY: import gc gc.collect() gc.collect() @contextmanager def connect(self): conn = socket.create_connection((self.connect_addr, self.port)) result = conn if PY3: conn_makefile = conn.makefile def makefile(*args, **kwargs): if 'bufsize' in kwargs: kwargs['buffering'] = kwargs.pop('bufsize') if 'mode' in kwargs: return conn_makefile(*args, **kwargs) # Under Python3, you can't read and write to the same # makefile() opened in (default) r, and r+ is not allowed kwargs['mode'] = 'rwb' rconn = conn_makefile(*args, **kwargs) _rconn_write = rconn.write def write(data): if isinstance(data, str): data = data.encode('ascii') return _rconn_write(data) rconn.write = write self._close_on_teardown(rconn) return rconn class proxy(object): def __getattribute__(self, name): if name == 'makefile': return makefile return getattr(conn, name) result = proxy() try: yield result finally: result.close() @contextmanager def makefile(self): with self.connect() as sock: try: result = sock.makefile(bufsize=1) yield result finally: result.close() def urlopen(self, *args, **kwargs): with self.connect() as sock: with sock.makefile(bufsize=1) as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n') return read_http(fd, *args, **kwargs) HTTP_CLIENT_VERSION = '1.1' DEFAULT_EXTRA_CLIENT_HEADERS = {} def format_request(self, method='GET', path='/', **headers): def_headers = self.DEFAULT_EXTRA_CLIENT_HEADERS.copy() def_headers.update(headers) headers = def_headers headers = '\r\n'.join('%s: %s' % item for item in headers.items()) headers = headers + '\r\n' if headers else headers result = ( '%(method)s %(path)s HTTP/%(http_ver)s\r\n' 'Host: localhost\r\n' '%(headers)s' '\r\n' ) result = result % dict( method=method, path=path, http_ver=self.HTTP_CLIENT_VERSION, headers=headers ) return result class CommonTestMixin(object): PIPELINE_NOT_SUPPORTED_EXS = () EXPECT_CLOSE = False EXPECT_KEEPALIVE = False def test_basic(self): with self.makefile() as fd: fd.write(self.format_request()) response = read_http(fd, body='hello world') if response.headers.get('Connection') == 'close': self.assertTrue(self.EXPECT_CLOSE, "Server closed connection, not expecting that") return response, None self.assertFalse(self.EXPECT_CLOSE) if self.EXPECT_KEEPALIVE: response.assertHeader('Connection', 'keep-alive') fd.write(self.format_request(path='/notexist')) dne_response = read_http(fd, code=404, reason='Not Found', body='not found') fd.write(self.format_request()) response = read_http(fd, body='hello world') return response, dne_response def test_pipeline(self): exception = AssertionError('HTTP pipelining not supported; the second request is thrown away') with self.makefile() as fd: fd.write(self.format_request() + self.format_request(path='/notexist')) read_http(fd, body='hello world') try: timeout = gevent.Timeout.start_new(0.5, exception=exception) try: read_http(fd, code=404, reason='Not Found', body='not found') finally: timeout.close() except self.PIPELINE_NOT_SUPPORTED_EXS: pass except AssertionError as ex: if ex is not exception: raise def test_connection_close(self): with self.makefile() as fd: fd.write(self.format_request()) response = read_http(fd) if response.headers.get('Connection') == 'close': self.assertTrue(self.EXPECT_CLOSE, "Server closed connection, not expecting that") return self.assertFalse(self.EXPECT_CLOSE) if self.EXPECT_KEEPALIVE: response.assertHeader('Connection', 'keep-alive') fd.write(self.format_request(Connection='close')) read_http(fd) fd.write(self.format_request()) # This may either raise, or it may return an empty response, # depend on timing and the Python version. try: result = fd.readline() except socket.error as ex: if ex.args[0] not in CONN_ABORTED_ERRORS: raise else: self.assertFalse( result, 'The remote side is expected to close the connection, but it sent %r' % (result,)) @unittest.skip("Not sure") def test_006_reject_long_urls(self): path_parts = [] for _ in range(3000): path_parts.append('path') path = '/'.join(path_parts) with self.makefile() as fd: request = 'GET /%s HTTP/1.0\r\nHost: localhost\r\n\r\n' % path fd.write(request) result = fd.readline() status = result.split(' ')[1] self.assertEqual(status, '414') class TestNoChunks(CommonTestMixin, TestCase): # when returning a list of strings a shortcut is employed by the server: # it calculates the content-length and joins all the chunks before sending validator = None def application(self, env, start_response): self.assertTrue(env.get('wsgi.input_terminated')) path = env['PATH_INFO'] if path == '/': start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'hello ', b'world'] start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [b'not ', b'found'] def test_basic(self): response, dne_response = super(TestNoChunks, self).test_basic() self.assertFalse(response.chunks) response.assertHeader('Content-Length', '11') if dne_response is not None: self.assertFalse(dne_response.chunks) dne_response.assertHeader('Content-Length', '9') def test_dne(self): with self.makefile() as fd: fd.write(self.format_request(path='/notexist')) response = read_http(fd, code=404, reason='Not Found', body='not found') self.assertFalse(response.chunks) response.assertHeader('Content-Length', '9') class TestNoChunks10(TestNoChunks): HTTP_CLIENT_VERSION = '1.0' PIPELINE_NOT_SUPPORTED_EXS = (ConnectionClosed,) EXPECT_CLOSE = True class TestNoChunks10KeepAlive(TestNoChunks10): DEFAULT_EXTRA_CLIENT_HEADERS = { 'Connection': 'keep-alive', } EXPECT_CLOSE = False EXPECT_KEEPALIVE = True class TestExplicitContentLength(TestNoChunks): # pylint:disable=too-many-ancestors # when returning a list of strings a shortcut is empoyed by the # server - it caculates the content-length def application(self, env, start_response): self.assertTrue(env.get('wsgi.input_terminated')) path = env['PATH_INFO'] if path == '/': start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', '11')]) return [b'hello ', b'world'] start_response('404 Not Found', [('Content-Type', 'text/plain'), ('Content-Length', '9')]) return [b'not ', b'found'] class TestYield(CommonTestMixin, TestCase): @staticmethod def application(env, start_response): path = env['PATH_INFO'] if path == '/': start_response('200 OK', [('Content-Type', 'text/plain')]) yield b"hello world" else: start_response('404 Not Found', [('Content-Type', 'text/plain')]) yield b"not found" class TestBytearray(CommonTestMixin, TestCase): validator = None @staticmethod def application(env, start_response): path = env['PATH_INFO'] if path == '/': start_response('200 OK', [('Content-Type', 'text/plain')]) return [bytearray(b"hello "), bytearray(b"world")] start_response('404 Not Found', [('Content-Type', 'text/plain')]) return [bytearray(b"not found")] class TestMultiLineHeader(TestCase): @staticmethod def application(env, start_response): assert "test.submit" in env["CONTENT_TYPE"] start_response('200 OK', [('Content-Type', 'text/plain')]) return [b"ok"] def test_multiline_116(self): """issue #116""" request = '\r\n'.join(( 'POST / HTTP/1.0', 'Host: localhost', 'Content-Type: multipart/related; boundary="====XXXX====";', ' type="text/xml";start="test.submit"', 'Content-Length: 0', '', '')) with self.makefile() as fd: fd.write(request) read_http(fd) class TestGetArg(TestCase): @staticmethod def application(env, start_response): body = env['wsgi.input'].read(3) if PY3: body = body.decode('ascii') a = parse_qs(body).get('a', [1])[0] start_response('200 OK', [('Content-Type', 'text/plain')]) return [('a is %s, body is %s' % (a, body)).encode('ascii')] def test_007_get_arg(self): # define a new handler that does a get_arg as well as a read_body request = '\r\n'.join(( 'POST / HTTP/1.0', 'Host: localhost', 'Content-Length: 3', '', 'a=a')) with self.makefile() as fd: fd.write(request) # send some junk after the actual request fd.write('01234567890123456789') read_http(fd, body='a is a, body is a=a') class TestCloseIter(TestCase): # The *Validator* closes the iterators! validator = None def application(self, env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return self def __iter__(self): yield bytearray(b"Hello World") yield b"!" closed = False def close(self): self.closed += 1 def test_close_is_called(self): self.closed = False with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n') read_http(fd, body=b"Hello World!", chunks=[b'Hello World', b'!']) # We got closed exactly once. self.assertEqual(self.closed, 1) class TestChunkedApp(TestCase): chunks = [b'this', b'is', b'chunked'] def body(self): return b''.join(self.chunks) def application(self, env, start_response): self.assertTrue(env.get('wsgi.input_terminated')) start_response('200 OK', [('Content-Type', 'text/plain')]) for chunk in self.chunks: yield chunk def test_chunked_response(self): with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') response = read_http(fd, body=self.body(), chunks=None) response.assertHeader('Transfer-Encoding', 'chunked') self.assertEqual(response.chunks, self.chunks) def test_no_chunked_http_1_0(self): with self.makefile() as fd: fd.write('GET / HTTP/1.0\r\nHost: localhost\r\nConnection: close\r\n\r\n') response = read_http(fd) self.assertEqual(response.body, self.body()) self.assertEqual(response.headers.get('Transfer-Encoding'), None) content_length = response.headers.get('Content-Length') if content_length is not None: self.assertEqual(content_length, str(len(self.body()))) class TestBigChunks(TestChunkedApp): chunks = [b'a' * 8192] * 3 class TestNegativeRead(TestCase): def application(self, env, start_response): self.assertTrue(env.get('wsgi.input_terminated')) start_response('200 OK', [('Content-Type', 'text/plain')]) if env['PATH_INFO'] == '/read': data = env['wsgi.input'].read(-1) return [data] def test_negative_chunked_read(self): data = (b'POST /read HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') def test_negative_nonchunked_read(self): data = (b'POST /read HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Content-Length: 6\r\n\r\n' b'oh hai') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') class TestNegativeReadline(TestCase): validator = None @staticmethod def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) if env['PATH_INFO'] == '/readline': data = env['wsgi.input'].readline(-1) return [data] def test_negative_chunked_readline(self): data = (b'POST /readline HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') def test_negative_nonchunked_readline(self): data = (b'POST /readline HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Content-Length: 6\r\n\r\n' b'oh hai') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') class TestChunkedPost(TestCase): def application(self, env, start_response): self.assertTrue(env.get('wsgi.input_terminated')) start_response('200 OK', [('Content-Type', 'text/plain')]) if env['PATH_INFO'] == '/a': data = env['wsgi.input'].read(6) return [data] if env['PATH_INFO'] == '/b': lines = list(iter(lambda: env['wsgi.input'].read(6), b'')) return lines if env['PATH_INFO'] == '/c': return list(iter(lambda: env['wsgi.input'].read(1), b'')) def test_014_chunked_post(self): data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') # self.close_opened() # XXX: Why? with self.makefile() as fd: fd.write(data.replace(b'/a', b'/b')) read_http(fd, body='oh hai') with self.makefile() as fd: fd.write(data.replace(b'/a', b'/c')) read_http(fd, body='oh hai') def test_229_incorrect_chunk_no_newline(self): # Giving both a Content-Length and a Transfer-Encoding, # TE is preferred. But if the chunking is bad from the client, # missing its terminating newline, # the server doesn't hang data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Content-Length: 12\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'{"hi": "ho"}') with self.makefile() as fd: fd.write(data) read_http(fd, code=400) def test_229_incorrect_chunk_non_hex(self): # Giving both a Content-Length and a Transfer-Encoding, # TE is preferred. But if the chunking is bad from the client, # the server doesn't hang data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Content-Length: 12\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'{"hi": "ho"}\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, code=400) def test_229_correct_chunk_quoted_ext(self): data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2;token="oh hi"\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') def test_229_correct_chunk_token_ext(self): data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2;token=oh_hi\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') with self.makefile() as fd: fd.write(data) read_http(fd, body='oh hai') def test_229_incorrect_chunk_token_ext_too_long(self): data = (b'POST /a HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' b'Transfer-Encoding: chunked\r\n\r\n' b'2;token=oh_hi\r\noh\r\n4\r\n hai\r\n0\r\n\r\n') data = data.replace(b'oh_hi', b'_oh_hi' * 4000) with self.makefile() as fd: fd.write(data) read_http(fd, code=400) class TestUseWrite(TestCase): body = b'abcde' end = b'end' content_length = str(len(body + end)) def application(self, env, start_response): if env['PATH_INFO'] == '/explicit-content-length': write = start_response('200 OK', [('Content-Type', 'text/plain'), ('Content-Length', self.content_length)]) write(self.body) elif env['PATH_INFO'] == '/no-content-length': write = start_response('200 OK', [('Content-Type', 'text/plain')]) write(self.body) elif env['PATH_INFO'] == '/no-content-length-twice': write = start_response('200 OK', [('Content-Type', 'text/plain')]) write(self.body) write(self.body) else: raise Exception('Invalid url') return [self.end] def test_explicit_content_length(self): with self.makefile() as fd: fd.write('GET /explicit-content-length HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') response = read_http(fd, body=self.body + self.end) response.assertHeader('Content-Length', self.content_length) response.assertHeader('Transfer-Encoding', False) def test_no_content_length(self): with self.makefile() as fd: fd.write('GET /no-content-length HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') response = read_http(fd, body=self.body + self.end) response.assertHeader('Content-Length', False) response.assertHeader('Transfer-Encoding', 'chunked') def test_no_content_length_twice(self): with self.makefile() as fd: fd.write('GET /no-content-length-twice HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') response = read_http(fd, body=self.body + self.body + self.end) response.assertHeader('Content-Length', False) response.assertHeader('Transfer-Encoding', 'chunked') self.assertEqual(response.chunks, [self.body, self.body, self.end]) class HttpsTestCase(TestCase): certfile = os.path.join(os.path.dirname(__file__), 'test_server.crt') keyfile = os.path.join(os.path.dirname(__file__), 'test_server.key') def init_server(self, application): self.server = pywsgi.WSGIServer((self.listen_addr, 0), application, certfile=self.certfile, keyfile=self.keyfile) def urlopen(self, method='GET', post_body=None, **kwargs): # pylint:disable=arguments-differ import ssl with self.connect() as raw_sock: with ssl.wrap_socket(raw_sock) as sock: with sock.makefile(bufsize=1) as fd: # pylint:disable=unexpected-keyword-arg fd.write('%s / HTTP/1.1\r\nHost: localhost\r\n' % method) if post_body is not None: fd.write('Content-Length: %s\r\n\r\n' % len(post_body)) fd.write(post_body) if kwargs.get('body') is None: kwargs['body'] = post_body else: fd.write('\r\n') fd.flush() return read_http(fd, **kwargs) def application(self, environ, start_response): assert environ['wsgi.url_scheme'] == 'https', environ['wsgi.url_scheme'] start_response('200 OK', [('Content-Type', 'text/plain')]) return [environ['wsgi.input'].read(10)] import gevent.ssl HAVE_SSLCONTEXT = getattr(gevent.ssl, 'create_default_context') if HAVE_SSLCONTEXT: class HttpsSslContextTestCase(HttpsTestCase): def init_server(self, application): # On 2.7, our certs don't line up with hostname. # If we just use create_default_context as-is, we get # `ValueError: check_hostname requires server_hostname`. # If we set check_hostname to False, we get # `SSLError: [SSL: PEER_DID_NOT_RETURN_A_CERTIFICATE] peer did not return a certificate` # (Neither of which happens in Python 3.) But the unverified context # works both places. See also test___example_servers.py from gevent.ssl import _create_unverified_context # pylint:disable=no-name-in-module context = _create_unverified_context() context.load_cert_chain(certfile=self.certfile, keyfile=self.keyfile) self.server = pywsgi.WSGIServer((self.listen_addr, 0), application, ssl_context=context) class TestHttps(HttpsTestCase): if hasattr(socket, 'ssl'): def test_012_ssl_server(self): result = self.urlopen(method="POST", post_body='abc') self.assertEqual(result.body, 'abc') def test_013_empty_return(self): result = self.urlopen() self.assertEqual(result.body, '') if HAVE_SSLCONTEXT: class TestHttpsWithContext(HttpsSslContextTestCase, TestHttps): # pylint:disable=too-many-ancestors pass class TestInternational(TestCase): validator = None # wsgiref.validate.IteratorWrapper([]) does not have __len__ def application(self, environ, start_response): path_bytes = b'/\xd0\xbf\xd1\x80\xd0\xb8\xd0\xb2\xd0\xb5\xd1\x82' if PY3: # Under PY3, the escapes were decoded as latin-1 path_bytes = path_bytes.decode('latin-1') self.assertEqual(environ['PATH_INFO'], path_bytes) self.assertEqual(environ['QUERY_STRING'], '%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81=%D0%BE%D1%82%D0%B2%D0%B5%D1%82') start_response("200 PASSED", [('Content-Type', 'text/plain')]) return [] def test(self): with self.connect() as sock: sock.sendall( b'''GET /%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82?%D0%B2%D0%BE%D0%BF%D1%80%D0%BE%D1%81=%D0%BE%D1%82%D0%B2%D0%B5%D1%82 HTTP/1.1 Host: localhost Connection: close '''.replace(b'\n', b'\r\n')) with sock.makefile() as fd: read_http(fd, reason='PASSED', chunks=False, body='', content_length=0) class TestNonLatin1HeaderFromApplication(TestCase): error_fatal = False # Allow sending the exception response, don't kill the greenlet validator = None # Don't validate the application, it's deliberately bad header = b'\xe1\xbd\x8a3' # bomb in utf-8 bytes should_error = PY3 # non-native string under Py3 def setUp(self): super(TestNonLatin1HeaderFromApplication, self).setUp() self.errors = [] def tearDown(self): self.errors = [] super(TestNonLatin1HeaderFromApplication, self).tearDown() def application(self, environ, start_response): # We return a header that cannot be encoded in latin-1 try: start_response("200 PASSED", [('Content-Type', 'text/plain'), ('Custom-Header', self.header)]) except: self.errors.append(sys.exc_info()[:2]) raise return [] def test(self): with self.connect() as sock: self.expect_one_error() sock.sendall(b'''GET / HTTP/1.1\r\n\r\n''') with sock.makefile() as fd: if self.should_error: read_http(fd, code=500, reason='Internal Server Error') self.assert_error(where_type=pywsgi.SecureEnviron) self.assertEqual(len(self.errors), 1) _, v = self.errors[0] self.assertIsInstance(v, UnicodeError) else: read_http(fd, code=200, reason='PASSED') self.assertEqual(len(self.errors), 0) class TestNonLatin1UnicodeHeaderFromApplication(TestNonLatin1HeaderFromApplication): # Flip-flop of the superclass: Python 3 native string, Python 2 unicode object header = u"\u1f4a3" # bomb in unicode # Error both on py3 and py2. On py2, non-native string. On py3, native string # that cannot be encoded to latin-1 should_error = True class TestInputReadline(TestCase): # this test relies on the fact that readline() returns '' after it reached EOF # this behaviour is not mandated by WSGI spec, it's just happens that gevent.wsgi behaves like that # as such, this may change in the future validator = None def application(self, environ, start_response): input = environ['wsgi.input'] lines = [] while True: line = input.readline() if not line: break line = line.decode('ascii') if PY3 else line lines.append(repr(line) + ' ') start_response('200 hello', []) return [l.encode('ascii') for l in lines] if PY3 else lines def test(self): with self.makefile() as fd: content = 'hello\n\nworld\n123' fd.write('POST / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n' 'Content-Length: %s\r\n\r\n%s' % (len(content), content)) fd.flush() read_http(fd, reason='hello', body="'hello\\n' '\\n' 'world\\n' '123' ") class TestInputIter(TestInputReadline): def application(self, environ, start_response): input = environ['wsgi.input'] lines = [] for line in input: if not line: break line = line.decode('ascii') if PY3 else line lines.append(repr(line) + ' ') start_response('200 hello', []) return [l.encode('ascii') for l in lines] if PY3 else lines class TestInputReadlines(TestInputReadline): def application(self, environ, start_response): input = environ['wsgi.input'] lines = [l.decode('ascii') if PY3 else l for l in input.readlines()] lines = [repr(line) + ' ' for line in lines] start_response('200 hello', []) return [l.encode('ascii') for l in lines] if PY3 else lines class TestInputN(TestCase): # testing for this: # File "/home/denis/work/gevent/gevent/pywsgi.py", line 70, in _do_read # if length and length > self.content_length - self.position: # TypeError: unsupported operand type(s) for -: 'NoneType' and 'int' validator = None def application(self, environ, start_response): environ['wsgi.input'].read(5) start_response('200 OK', []) return [] def test(self): self.urlopen() class TestError(TestCase): error = object() error_fatal = False def application(self, env, start_response): self.error = greentest.ExpectedException('TestError.application') raise self.error def test(self): self.expect_one_error() self.urlopen(code=500) self.assert_error(greentest.ExpectedException, self.error) class TestError_after_start_response(TestError): def application(self, env, start_response): self.error = greentest.ExpectedException('TestError_after_start_response.application') start_response('200 OK', [('Content-Type', 'text/plain')]) raise self.error class TestEmptyYield(TestCase): @staticmethod def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) yield b"" yield b"" def test_err(self): chunks = [] with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') read_http(fd, body='', chunks=chunks) garbage = fd.read() self.assertEqual(garbage, b"", "got garbage: %r" % garbage) class TestFirstEmptyYield(TestCase): @staticmethod def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) yield b"" yield b"hello" def test_err(self): chunks = [b'hello'] with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') read_http(fd, body='hello', chunks=chunks) garbage = fd.read() self.assertEqual(garbage, b"") class TestEmptyYield304(TestCase): @staticmethod def application(env, start_response): start_response('304 Not modified', []) yield b"" yield b"" def test_err(self): with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') read_http(fd, code=304, body='', chunks=False) garbage = fd.read() self.assertEqual(garbage, b"") class TestContentLength304(TestCase): validator = None def application(self, env, start_response): try: start_response('304 Not modified', [('Content-Length', '100')]) except AssertionError as ex: start_response('200 Raised', []) return ex.args else: raise AssertionError('start_response did not fail but it should') def test_err(self): body = "Invalid Content-Length for 304 response: '100' (must be absent or zero)" with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') read_http(fd, code=200, reason='Raised', body=body, chunks=False) garbage = fd.read() self.assertEqual(garbage, b"") class TestBody304(TestCase): validator = None def application(self, env, start_response): start_response('304 Not modified', []) return [b'body'] def test_err(self): with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') try: read_http(fd) except AssertionError as ex: self.assertEqual(str(ex), 'The 304 response must have no body') else: raise AssertionError('AssertionError must be raised') class TestWrite304(TestCase): validator = None error_raised = None def application(self, env, start_response): write = start_response('304 Not modified', []) self.error_raised = False try: write('body') except AssertionError: self.error_raised = True raise def test_err(self): with self.makefile() as fd: fd.write(b'GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') with self.assertRaises(AssertionError) as exc: read_http(fd) ex = exc.exception self.assertEqual(str(ex), 'The 304 response must have no body') self.assertTrue(self.error_raised, 'write() must raise') class TestEmptyWrite(TestEmptyYield): @staticmethod def application(env, start_response): write = start_response('200 OK', [('Content-Type', 'text/plain')]) write(b"") write(b"") return [] class BadRequestTests(TestCase): validator = None # pywsgi checks content-length, but wsgi does not content_length = None def application(self, env, start_response): self.assertEqual(env['CONTENT_LENGTH'], self.content_length) start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'hello'] def test_negative_content_length(self): self.content_length = '-100' with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: %s\r\n\r\n' % self.content_length) read_http(fd, code=(200, 400)) def test_illegal_content_length(self): self.content_length = 'abc' with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nContent-Length: %s\r\n\r\n' % self.content_length) read_http(fd, code=(200, 400)) class ChunkedInputTests(TestCase): dirt = "" validator = None def application(self, env, start_response): input = env['wsgi.input'] response = [] pi = env["PATH_INFO"] if pi == "/short-read": d = input.read(10) response = [d] elif pi == "/lines": for x in input: response.append(x) elif pi == "/ping": input.read(1) response.append(b"pong") else: raise RuntimeError("bad path") start_response('200 OK', [('Content-Type', 'text/plain')]) return response def chunk_encode(self, chunks, dirt=None): if dirt is None: dirt = self.dirt return chunk_encode(chunks, dirt=dirt) def body(self, dirt=None): return self.chunk_encode(["this", " is ", "chunked", "\nline", " 2", "\n", "line3", ""], dirt=dirt) def ping(self, fd): fd.write("GET /ping HTTP/1.1\r\n\r\n") read_http(fd, body="pong") def ping_if_possible(self, fd): self.ping(fd) def test_short_read_with_content_length(self): body = self.body() req = b"POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\nContent-Length:1000\r\n\r\n" + body with self.connect() as conn: with conn.makefile(bufsize=1) as fd: # pylint:disable=unexpected-keyword-arg fd.write(req) read_http(fd, body="this is ch") self.ping_if_possible(fd) def test_short_read_with_zero_content_length(self): body = self.body() req = b"POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\nContent-Length:0\r\n\r\n" + body #print("REQUEST:", repr(req)) with self.makefile() as fd: fd.write(req) read_http(fd, body="this is ch") self.ping_if_possible(fd) def test_short_read(self): body = self.body() req = b"POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body with self.makefile() as fd: fd.write(req) read_http(fd, body="this is ch") self.ping_if_possible(fd) def test_dirt(self): body = self.body(dirt="; here is dirt\0bla") req = b"POST /ping HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body with self.makefile() as fd: fd.write(req) read_http(fd, body="pong") self.ping_if_possible(fd) def test_chunked_readline(self): body = self.body() req = "POST /lines HTTP/1.1\r\nContent-Length: %s\r\ntransfer-encoding: Chunked\r\n\r\n" % (len(body)) req = req.encode('latin-1') req += body with self.makefile() as fd: fd.write(req) read_http(fd, body='this is chunked\nline 2\nline3') def test_close_before_finished(self): self.expect_one_error() body = b'4\r\nthi' req = b"POST /short-read HTTP/1.1\r\ntransfer-encoding: Chunked\r\n\r\n" + body with self.connect() as sock: with sock.makefile(bufsize=1, mode='wb') as fd:# pylint:disable=unexpected-keyword-arg fd.write(req) fd.close() # Python 3 keeps the socket open even though the only # makefile is gone; python 2 closed them both (because there were # no outstanding references to the socket). Closing is essential for the server # to get the message that the read will fail. It's better to be explicit # to avoid a ResourceWarning sock.close() # Under Py2 it still needs to go away, which was implicit before del fd del sock gevent.get_hub().loop.update_now() gevent.sleep(0.01) # timing needed for cpython if greentest.PYPY: # XXX: Something is keeping the socket alive, # by which I mean, the close event is not propagating to the server # and waking up its recv() loop...we are stuck with the three bytes of # 'thi' in the buffer and trying to read the forth. No amount of tinkering # with the timing changes this...the only thing that does is running a # GC and letting some object get collected. Might this be a problem in real life? import gc gc.collect() gevent.sleep(0.01) gevent.get_hub().loop.update_now() gc.collect() gevent.sleep(0.01) # XXX2: Sometimes windows and PyPy/Travis fail to get this error, leading to a test failure. # This would have to be due to the socket being kept around and open, # not closed at the low levels. I haven't seen this locally. # In the PyPy case, I've seen the IOError reported on the console, but not # captured in the variables. # https://travis-ci.org/gevent/gevent/jobs/329232976#L1374 self.assert_error(IOError, 'unexpected end of file while parsing chunked data') class Expect100ContinueTests(TestCase): validator = None def application(self, environ, start_response): content_length = int(environ['CONTENT_LENGTH']) if content_length > 1024: start_response('417 Expectation Failed', [('Content-Length', '7'), ('Content-Type', 'text/plain')]) return [b'failure'] # pywsgi did sent a "100 continue" for each read # see http://code.google.com/p/gevent/issues/detail?id=93 text = environ['wsgi.input'].read(1) text += environ['wsgi.input'].read(content_length - 1) start_response('200 OK', [('Content-Length', str(len(text))), ('Content-Type', 'text/plain')]) return [text] def test_continue(self): with self.makefile() as fd: fd.write('PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 1025\r\nExpect: 100-continue\r\n\r\n') read_http(fd, code=417, body="failure") fd.write('PUT / HTTP/1.1\r\nHost: localhost\r\nContent-length: 7\r\nExpect: 100-continue\r\n\r\ntesting') read_http(fd, code=100) read_http(fd, body="testing") class MultipleCookieHeadersTest(TestCase): validator = None def application(self, environ, start_response): self.assertEqual(environ['HTTP_COOKIE'], 'name1="value1"; name2="value2"') self.assertEqual(environ['HTTP_COOKIE2'], 'nameA="valueA"; nameB="valueB"') start_response('200 OK', []) return [] def test(self): with self.makefile() as fd: fd.write('''GET / HTTP/1.1 Host: localhost Cookie: name1="value1" Cookie2: nameA="valueA" Cookie2: nameB="valueB" Cookie: name2="value2"\n\n'''.replace('\n', '\r\n')) read_http(fd) class TestLeakInput(TestCase): _leak_wsgi_input = None _leak_environ = None def tearDown(self): TestCase.tearDown(self) self._leak_wsgi_input = None self._leak_environ = None def application(self, environ, start_response): pi = environ["PATH_INFO"] self._leak_wsgi_input = environ["wsgi.input"] self._leak_environ = environ if pi == "/leak-frame": environ["_leak"] = sys._getframe(0) text = b"foobar" start_response('200 OK', [('Content-Length', str(len(text))), ('Content-Type', 'text/plain')]) return [text] def test_connection_close_leak_simple(self): with self.makefile() as fd: fd.write(b"GET / HTTP/1.0\r\nConnection: close\r\n\r\n") d = fd.read() self.assertTrue(d.startswith(b"HTTP/1.1 200 OK"), d) def test_connection_close_leak_frame(self): with self.makefile() as fd: fd.write(b"GET /leak-frame HTTP/1.0\r\nConnection: close\r\n\r\n") d = fd.read() self.assertTrue(d.startswith(b"HTTP/1.1 200 OK"), d) self._leak_environ.pop('_leak') class TestHTTPResponseSplitting(TestCase): # The validator would prevent the app from doing the # bad things it needs to do validator = None status = '200 OK' headers = () start_exc = None def setUp(self): TestCase.setUp(self) self.start_exc = None self.status = TestHTTPResponseSplitting.status self.headers = TestHTTPResponseSplitting.headers def tearDown(self): TestCase.tearDown(self) self.start_exc = None def application(self, environ, start_response): try: start_response(self.status, self.headers) except Exception as e: # pylint: disable=broad-except self.start_exc = e else: self.start_exc = None return () def _assert_failure(self, message): with self.makefile() as fd: fd.write('GET / HTTP/1.0\r\nHost: localhost\r\n\r\n') fd.read() self.assertIsInstance(self.start_exc, ValueError) self.assertEqual(self.start_exc.args[0], message) def test_newline_in_status(self): self.status = '200 OK\r\nConnection: close\r\nContent-Length: 0\r\n\r\n' self._assert_failure('carriage return or newline in status') def test_newline_in_header_value(self): self.headers = [('Test', 'Hi\r\nConnection: close')] self._assert_failure('carriage return or newline in header value') def test_newline_in_header_name(self): self.headers = [('Test\r\n', 'Hi')] self._assert_failure('carriage return or newline in header name') class TestInvalidEnviron(TestCase): validator = None # check that WSGIServer does not insert any default values for CONTENT_LENGTH def application(self, environ, start_response): for key, value in environ.items(): if key in ('CONTENT_LENGTH', 'CONTENT_TYPE') or key.startswith('HTTP_'): if key != 'HTTP_HOST': raise AssertionError('Unexpected environment variable: %s=%r' % (key, value)) start_response('200 OK', []) return [] def test(self): with self.makefile() as fd: fd.write('GET / HTTP/1.0\r\nHost: localhost\r\n\r\n') read_http(fd) with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\n\r\n') read_http(fd) class TestInvalidHeadersDropped(TestCase): validator = None # check that invalid headers with a _ are dropped def application(self, environ, start_response): self.assertNotIn('HTTP_X_AUTH_USER', environ) start_response('200 OK', []) return [] def test(self): with self.makefile() as fd: fd.write('GET / HTTP/1.0\r\nx-auth_user: bob\r\n\r\n') read_http(fd) class Handler(pywsgi.WSGIHandler): def read_requestline(self): data = self.rfile.read(7) if data[0] == b'<'[0]: # py3: indexing bytes returns ints. sigh. # Returning nothing stops handle_one_request() # Note that closing or even deleting self.socket() here # can lead to the read side throwing Connection Reset By Peer, # depending on the Python version and OS data += self.rfile.read(15) if data.lower() == b'': self.socket.sendall(b'HELLO') else: self.log_error('Invalid request: %r', data) return None return data + self.rfile.readline() class TestHandlerSubclass(TestCase): validator = None def application(self, environ, start_response): start_response('200 OK', []) return [] def init_server(self, application): self.server = pywsgi.WSGIServer((self.listen_addr, 0), application, handler_class=Handler) def test(self): with self.makefile() as fd: fd.write(b'\x00') fd.flush() # flush() is needed on PyPy, apparently it buffers slightly differently self.assertEqual(fd.read(), b'HELLO') with self.makefile() as fd: fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n') fd.flush() read_http(fd) with self.makefile() as fd: # Trigger an error fd.write('\x00') fd.flush() self.assertEqual(fd.read(), b'') class TestErrorAfterChunk(TestCase): validator = None @staticmethod def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) yield b"hello" raise greentest.ExpectedException('TestErrorAfterChunk') def test(self): with self.makefile() as fd: self.expect_one_error() fd.write('GET / HTTP/1.1\r\nHost: localhost\r\nConnection: keep-alive\r\n\r\n') with self.assertRaises(ValueError): read_http(fd) self.assert_error(greentest.ExpectedException) def chunk_encode(chunks, dirt=None): if dirt is None: dirt = "" b = b"" for c in chunks: x = "%x%s\r\n%s\r\n" % (len(c), dirt, c) b += x.encode('ascii') return b class TestInputRaw(greentest.BaseTestCase): def make_input(self, data, content_length=None, chunked_input=False): if isinstance(data, list): data = chunk_encode(data) chunked_input = True elif isinstance(data, str) and PY3: data = data.encode("ascii") return Input(StringIO(data), content_length=content_length, chunked_input=chunked_input) if PY3: def assertEqual(self, data, expected, *args): # pylint:disable=arguments-differ if isinstance(expected, str): expected = expected.encode('ascii') super(TestInputRaw, self).assertEqual(data, expected, *args) def test_short_post(self): i = self.make_input("1", content_length=2) self.assertRaises(IOError, i.read) def test_short_post_read_with_length(self): i = self.make_input("1", content_length=2) self.assertRaises(IOError, i.read, 2) def test_short_post_readline(self): i = self.make_input("1", content_length=2) self.assertRaises(IOError, i.readline) def test_post(self): i = self.make_input("12", content_length=2) data = i.read() self.assertEqual(data, "12") def test_post_read_with_length(self): i = self.make_input("12", content_length=2) data = i.read(10) self.assertEqual(data, "12") def test_chunked(self): i = self.make_input(["1", "2", ""]) data = i.read() self.assertEqual(data, "12") def test_chunked_read_with_length(self): i = self.make_input(["1", "2", ""]) data = i.read(10) self.assertEqual(data, "12") def test_chunked_missing_chunk(self): i = self.make_input(["1", "2"]) self.assertRaises(IOError, i.read) def test_chunked_missing_chunk_read_with_length(self): i = self.make_input(["1", "2"]) self.assertRaises(IOError, i.read, 10) def test_chunked_missing_chunk_readline(self): i = self.make_input(["1", "2"]) self.assertRaises(IOError, i.readline) def test_chunked_short_chunk(self): i = self.make_input("2\r\n1", chunked_input=True) self.assertRaises(IOError, i.read) def test_chunked_short_chunk_read_with_length(self): i = self.make_input("2\r\n1", chunked_input=True) self.assertRaises(IOError, i.read, 2) def test_chunked_short_chunk_readline(self): i = self.make_input("2\r\n1", chunked_input=True) self.assertRaises(IOError, i.readline) def test_32bit_overflow(self): # https://github.com/gevent/gevent/issues/289 # Should not raise an OverflowError on Python 2 print("BEGIN 32bit") data = b'asdf\nghij\n' long_data = b'a' * (pywsgi.MAX_REQUEST_LINE + 10) long_data += b'\n' data = data + long_data partial_data = b'qjk\n' # Note terminating \n n = 25 * 1000000000 print("N", n, "Data len", len(data)) if hasattr(n, 'bit_length'): self.assertEqual(n.bit_length(), 35) if not PY3 and not PYPY: # Make sure we have the impl we think we do self.assertRaises(OverflowError, StringIO(data).readline, n) i = self.make_input(data, content_length=n) # No size hint, but we have too large a content_length to fit self.assertEqual(i.readline(), b'asdf\n') # Large size hint self.assertEqual(i.readline(n), b'ghij\n') self.assertEqual(i.readline(n), long_data) # Now again with the real content length, assuring we can't read past it i = self.make_input(data + partial_data, content_length=len(data) + 1) self.assertEqual(i.readline(), b'asdf\n') self.assertEqual(i.readline(n), b'ghij\n') self.assertEqual(i.readline(n), long_data) # Now we've reached content_length so we shouldn't be able to # read anymore except the one byte remaining self.assertEqual(i.readline(n), b'q') class Test414(TestCase): @staticmethod def application(env, start_response): raise AssertionError('should not get there') def test(self): longline = 'x' * 20000 with self.makefile() as fd: fd.write(('''GET /%s HTTP/1.0\r\nHello: world\r\n\r\n''' % longline).encode('latin-1')) read_http(fd, code=414) class TestLogging(TestCase): # Something that gets wrapped in a LoggingLogAdapter class Logger(object): accessed = None logged = None thing = None def log(self, level, msg): self.logged = (level, msg) def access(self, msg): self.accessed = msg def get_thing(self): return self.thing def init_logger(self): return self.Logger() @staticmethod def application(env, start_response): start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'hello'] # Tests for issue #663 def test_proxy_methods_on_log(self): # An object that looks like a logger gets wrapped # with a proxy that self.assertTrue(isinstance(self.server.log, pywsgi.LoggingLogAdapter)) self.server.log.access("access") self.server.log.write("write") self.assertEqual(self.server.log.accessed, "access") self.assertEqual(self.server.log.logged, (20, "write")) def test_set_attributes(self): # Not defined by the wrapper, it goes to the logger self.server.log.thing = 42 self.assertEqual(self.server.log.get_thing(), 42) del self.server.log.thing self.assertEqual(self.server.log.get_thing(), None) def test_status_log(self): # Issue 664: Make sure we format the status line as a string self.urlopen() msg = self.server.log.logged[1] self.assertTrue('"GET / HTTP/1.1" 200 ' in msg, msg) # Issue 756: Make sure we don't throw a newline on the end self.assertTrue('\n' not in msg, msg) class TestEnviron(TestCase): # The wsgiref validator asserts type(environ) is dict. # https://mail.python.org/pipermail/web-sig/2016-March/005455.html validator = None def init_server(self, application): super(TestEnviron, self).init_server(application) self.server.environ_class = pywsgi.SecureEnviron def application(self, env, start_response): self.assertIsInstance(env, pywsgi.SecureEnviron) start_response('200 OK', [('Content-Type', 'text/plain')]) return [] def test_environ_is_secure_by_default(self): self.urlopen() def test_default_secure_repr(self): environ = pywsgi.SecureEnviron() self.assertIn('"}), str(environ)) self.assertEqual(repr({'key': ""}), repr(environ)) environ.whitelist_keys = ('key',) self.assertEqual(str({'key': 'value'}), str(environ)) self.assertEqual(repr({'key': 'value'}), repr(environ)) del environ.whitelist_keys def test_override_class_defaults(self): class EnvironClass(pywsgi.SecureEnviron): __slots__ = () environ = EnvironClass() self.assertTrue(environ.secure_repr) EnvironClass.default_secure_repr = False self.assertFalse(environ.secure_repr) self.assertEqual(str({}), str(environ)) self.assertEqual(repr({}), repr(environ)) EnvironClass.default_secure_repr = True EnvironClass.default_whitelist_keys = ('key',) environ['key'] = 1 self.assertEqual(str({'key': 1}), str(environ)) self.assertEqual(repr({'key': 1}), repr(environ)) # Clean up for leaktests del environ del EnvironClass import gc; gc.collect() def test_copy_still_secure(self): for cls in (pywsgi.Environ, pywsgi.SecureEnviron): self.assertIsInstance(cls().copy(), cls) def test_pickle_copy_returns_dict(self): # Anything going through copy.copy/pickle should # return the same pickle that a dict would. import pickle import json for cls in (pywsgi.Environ, pywsgi.SecureEnviron): bltin = {'key': 'value'} env = cls(bltin) self.assertIsInstance(env, cls) self.assertEqual(bltin, env) self.assertEqual(env, bltin) for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): # It's impossible to get a subclass of dict to pickle # identically, but it can restore identically env_dump = pickle.dumps(env, protocol) self.assertNotIn(b'Environ', env_dump) loaded = pickle.loads(env_dump) self.assertEqual(type(loaded), dict) self.assertEqual(json.dumps(bltin), json.dumps(env)) if __name__ == '__main__': greentest.main()