You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1844 lines
64 KiB
Python
1844 lines
64 KiB
Python
5 years ago
|
# 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'<policy-file-request/>':
|
||
|
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'<policy-file-request/>\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('<policy-file-XXXuest/>\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('<pywsgi.SecureEnviron dict (keys: 0) at', repr(environ))
|
||
|
self.assertIn('<pywsgi.SecureEnviron dict (keys: 0) at', str(environ))
|
||
|
|
||
|
environ['key'] = 'value'
|
||
|
self.assertIn('<pywsgi.SecureEnviron dict (keys: 1) at', repr(environ))
|
||
|
self.assertIn('<pywsgi.SecureEnviron dict (keys: 1) at', str(environ))
|
||
|
|
||
|
environ.secure_repr = False
|
||
|
self.assertEqual(str({'key': 'value'}), str(environ))
|
||
|
self.assertEqual(repr({'key': 'value'}), repr(environ))
|
||
|
|
||
|
del environ.secure_repr
|
||
|
|
||
|
environ.whitelist_keys = ('missing value',)
|
||
|
self.assertEqual(str({'key': "<MASKED>"}), str(environ))
|
||
|
self.assertEqual(repr({'key': "<MASKED>"}), 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()
|