Update cherrpy to 17.4.2

This commit is contained in:
JonnyWong16
2019-11-23 18:55:19 -08:00
parent f28e741ad7
commit 4d6279a626
131 changed files with 15864 additions and 10389 deletions

View File

@@ -0,0 +1,24 @@
"""
Regression test suite for CherryPy.
"""
import os
import sys
def newexit():
os._exit(1)
def setup():
# We want to monkey patch sys.exit so that we can get some
# information about where exit is being called.
newexit._old = sys.exit
sys.exit = newexit
def teardown():
try:
sys.exit = sys.exit._old
except AttributeError:
sys.exit = sys._exit

View File

@@ -0,0 +1,39 @@
"""Test module for the @-decorator syntax, which is version-specific"""
import cherrypy
from cherrypy import expose, tools
class ExposeExamples(object):
@expose
def no_call(self):
return 'Mr E. R. Bradshaw'
@expose()
def call_empty(self):
return 'Mrs. B.J. Smegma'
@expose('call_alias')
def nesbitt(self):
return 'Mr Nesbitt'
@expose(['alias1', 'alias2'])
def andrews(self):
return 'Mr Ken Andrews'
@expose(alias='alias3')
def watson(self):
return 'Mr. and Mrs. Watson'
class ToolExamples(object):
@expose
# This is here to demonstrate that using the config decorator
# does not overwrite other config attributes added by the Tool
# decorator (in this case response_headers).
@cherrypy.config(**{'response.stream': True})
@tools.response_headers(headers=[('Content-Type', 'application/data')])
def blah(self):
yield b'blah'

View File

@@ -0,0 +1,69 @@
import os
import sys
import time
import cherrypy
starttime = time.time()
class Root:
@cherrypy.expose
def index(self):
return 'Hello World'
@cherrypy.expose
def mtimes(self):
return repr(cherrypy.engine.publish('Autoreloader', 'mtimes'))
@cherrypy.expose
def pid(self):
return str(os.getpid())
@cherrypy.expose
def start(self):
return repr(starttime)
@cherrypy.expose
def exit(self):
# This handler might be called before the engine is STARTED if an
# HTTP worker thread handles it before the HTTP server returns
# control to engine.start. We avoid that race condition here
# by waiting for the Bus to be STARTED.
cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
cherrypy.engine.exit()
@cherrypy.engine.subscribe('start', priority=100)
def unsub_sig():
cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False))
if cherrypy.config.get('unsubsig', False):
cherrypy.log('Unsubscribing the default cherrypy signal handler')
cherrypy.engine.signal_handler.unsubscribe()
try:
from signal import signal, SIGTERM
except ImportError:
pass
else:
def old_term_handler(signum=None, frame=None):
cherrypy.log('I am an old SIGTERM handler.')
sys.exit(0)
cherrypy.log('Subscribing the new one.')
signal(SIGTERM, old_term_handler)
@cherrypy.engine.subscribe('start', priority=6)
def starterror():
if cherrypy.config.get('starterror', False):
1 / 0
@cherrypy.engine.subscribe('start', priority=6)
def log_test_case_name():
if cherrypy.config.get('test_case_name', False):
cherrypy.log('STARTED FROM: %s' %
cherrypy.config.get('test_case_name'))
cherrypy.tree.mount(Root(), '/', {'/': {}})

View File

@@ -0,0 +1,425 @@
"""CherryPy Benchmark Tool
Usage:
benchmark.py [options]
--null: use a null Request object (to bench the HTTP server only)
--notests: start the server but do not run the tests; this allows
you to check the tested pages with a browser
--help: show this help message
--cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy)
--modpython: run tests via apache on 54583 (with modpython_gateway)
--ab=path: Use the ab script/executable at 'path' (see below)
--apache=path: Use the apache script/exe at 'path' (see below)
To run the benchmarks, the Apache Benchmark tool "ab" must either be on
your system path, or specified via the --ab=path option.
To run the modpython tests, the "apache" executable or script must be
on your system path, or provided via the --apache=path option. On some
platforms, "apache" may be called "apachectl" or "apache2ctl"--create
a symlink to them if needed.
"""
import getopt
import os
import re
import sys
import time
import cherrypy
from cherrypy import _cperror, _cpmodpy
from cherrypy.lib import httputil
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
AB_PATH = ''
APACHE_PATH = 'apache'
SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog'
__all__ = ['ABSession', 'Root', 'print_report',
'run_standard_benchmarks', 'safe_threads',
'size_report', 'thread_report',
]
size_cache = {}
class Root:
@cherrypy.expose
def index(self):
return """<html>
<head>
<title>CherryPy Benchmark</title>
</head>
<body>
<ul>
<li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
<li><a href="static/index.html">Static file (14 bytes static)</a></li>
<li><form action="sizer">Response of length:
<input type='text' name='size' value='10' /></form>
</li>
</ul>
</body>
</html>"""
@cherrypy.expose
def hello(self):
return 'Hello, world\r\n'
@cherrypy.expose
def sizer(self, size):
resp = size_cache.get(size, None)
if resp is None:
size_cache[size] = resp = 'X' * int(size)
return resp
def init():
cherrypy.config.update({
'log.error.file': '',
'environment': 'production',
'server.socket_host': '127.0.0.1',
'server.socket_port': 54583,
'server.max_request_header_size': 0,
'server.max_request_body_size': 0,
})
# Cheat mode on ;)
del cherrypy.config['tools.log_tracebacks.on']
del cherrypy.config['tools.log_headers.on']
del cherrypy.config['tools.trailing_slash.on']
appconf = {
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
},
}
globals().update(
app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf),
)
class NullRequest:
"""A null HTTP request class, returning 200 and an empty body."""
def __init__(self, local, remote, scheme='http'):
pass
def close(self):
pass
def run(self, method, path, query_string, protocol, headers, rfile):
cherrypy.response.status = '200 OK'
cherrypy.response.header_list = [('Content-Type', 'text/html'),
('Server', 'Null CherryPy'),
('Date', httputil.HTTPDate()),
('Content-Length', '0'),
]
cherrypy.response.body = ['']
return cherrypy.response
class NullResponse:
pass
class ABSession:
"""A session of 'ab', the Apache HTTP server benchmarking tool.
Example output from ab:
This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Completed 200 requests
Completed 300 requests
Completed 400 requests
Completed 500 requests
Completed 600 requests
Completed 700 requests
Completed 800 requests
Completed 900 requests
Server Software: CherryPy/3.1beta
Server Hostname: 127.0.0.1
Server Port: 54583
Document Path: /static/index.html
Document Length: 14 bytes
Concurrency Level: 10
Time taken for tests: 9.643867 seconds
Complete requests: 1000
Failed requests: 0
Write errors: 0
Total transferred: 189000 bytes
HTML transferred: 14000 bytes
Requests per second: 103.69 [#/sec] (mean)
Time per request: 96.439 [ms] (mean)
Time per request: 9.644 [ms] (mean, across all concurrent requests)
Transfer rate: 19.08 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 0 2.9 0 10
Processing: 20 94 7.3 90 130
Waiting: 0 43 28.1 40 100
Total: 20 95 7.3 100 130
Percentage of the requests served within a certain time (ms)
50% 100
66% 100
75% 100
80% 100
90% 100
95% 100
98% 100
99% 110
100% 130 (longest request)
Finished 1000 requests
"""
parse_patterns = [
('complete_requests', 'Completed',
br'^Complete requests:\s*(\d+)'),
('failed_requests', 'Failed',
br'^Failed requests:\s*(\d+)'),
('requests_per_second', 'req/sec',
br'^Requests per second:\s*([0-9.]+)'),
('time_per_request_concurrent', 'msec/req',
br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'),
('transfer_rate', 'KB/sec',
br'^Transfer rate:\s*([0-9.]+)')
]
def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000,
concurrency=10):
self.path = path
self.requests = requests
self.concurrency = concurrency
def args(self):
port = cherrypy.server.socket_port
assert self.concurrency > 0
assert self.requests > 0
# Don't use "localhost".
# Cf
# http://mail.python.org/pipermail/python-win32/2008-March/007050.html
return ('-k -n %s -c %s http://127.0.0.1:%s%s' %
(self.requests, self.concurrency, port, self.path))
def run(self):
# Parse output of ab, setting attributes on self
try:
self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args())
except Exception:
print(_cperror.format_exc())
raise
for attr, name, pattern in self.parse_patterns:
val = re.search(pattern, self.output, re.MULTILINE)
if val:
val = val.group(1)
setattr(self, attr, val)
else:
setattr(self, attr, None)
safe_threads = (25, 50, 100, 200, 400)
if sys.platform in ('win32',):
# For some reason, ab crashes with > 50 threads on my Win2k laptop.
safe_threads = (10, 20, 30, 40, 50)
def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads):
sess = ABSession(path)
attrs, names, patterns = list(zip(*sess.parse_patterns))
avg = dict.fromkeys(attrs, 0.0)
yield ('threads',) + names
for c in concurrency:
sess.concurrency = c
sess.run()
row = [c]
for attr in attrs:
val = getattr(sess, attr)
if val is None:
print(sess.output)
row = None
break
val = float(val)
avg[attr] += float(val)
row.append(val)
if row:
yield row
# Add a row of averages.
yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs]
def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
concurrency=50):
sess = ABSession(concurrency=concurrency)
attrs, names, patterns = list(zip(*sess.parse_patterns))
yield ('bytes',) + names
for sz in sizes:
sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz)
sess.run()
yield [sz] + [getattr(sess, attr) for attr in attrs]
def print_report(rows):
for row in rows:
print('')
for val in row:
sys.stdout.write(str(val).rjust(10) + ' | ')
print('')
def run_standard_benchmarks():
print('')
print('Client Thread Report (1000 requests, 14 byte response body, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(thread_report())
print('')
print('Client Thread Report (1000 requests, 14 bytes via staticdir, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(thread_report('%s/static/index.html' % SCRIPT_NAME))
print('')
print('Size Report (1000 requests, 50 client threads, '
'%s server threads):' % cherrypy.server.thread_pool)
print_report(size_report())
# modpython and other WSGI #
def startup_modpython(req=None):
"""Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
"""
if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
if req:
if 'nullreq' in req.get_options():
cherrypy.engine.request_class = NullRequest
cherrypy.engine.response_class = NullResponse
ab_opt = req.get_options().get('ab', '')
if ab_opt:
global AB_PATH
AB_PATH = ab_opt
cherrypy.engine.start()
if cherrypy.engine.state == cherrypy._cpengine.STARTING:
cherrypy.engine.wait()
return 0 # apache.OK
def run_modpython(use_wsgi=False):
print('Starting mod_python...')
pyopts = []
# Pass the null and ab=path options through Apache
if '--null' in opts:
pyopts.append(('nullreq', ''))
if '--ab' in opts:
pyopts.append(('ab', opts['--ab']))
s = _cpmodpy.ModPythonServer
if use_wsgi:
pyopts.append(('wsgi.application', 'cherrypy::tree'))
pyopts.append(
('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython'))
handler = 'modpython_gateway::handler'
s = s(port=54583, opts=pyopts,
apache_path=APACHE_PATH, handler=handler)
else:
pyopts.append(
('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython'))
s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
try:
s.start()
run()
finally:
s.stop()
if __name__ == '__main__':
init()
longopts = ['cpmodpy', 'modpython', 'null', 'notests',
'help', 'ab=', 'apache=']
try:
switches, args = getopt.getopt(sys.argv[1:], '', longopts)
opts = dict(switches)
except getopt.GetoptError:
print(__doc__)
sys.exit(2)
if '--help' in opts:
print(__doc__)
sys.exit(0)
if '--ab' in opts:
AB_PATH = opts['--ab']
if '--notests' in opts:
# Return without stopping the server, so that the pages
# can be tested from a standard web browser.
def run():
port = cherrypy.server.socket_port
print('You may now open http://127.0.0.1:%s%s/' %
(port, SCRIPT_NAME))
if '--null' in opts:
print('Using null Request object')
else:
def run():
end = time.time() - start
print('Started in %s seconds' % end)
if '--null' in opts:
print('\nUsing null Request object')
try:
try:
run_standard_benchmarks()
except Exception:
print(_cperror.format_exc())
raise
finally:
cherrypy.engine.exit()
print('Starting CherryPy app server...')
class NullWriter(object):
"""Suppresses the printing of socket errors."""
def write(self, data):
pass
sys.stderr = NullWriter()
start = time.time()
if '--cpmodpy' in opts:
run_modpython()
elif '--modpython' in opts:
run_modpython(use_wsgi=True)
else:
if '--null' in opts:
cherrypy.server.request_class = NullRequest
cherrypy.server.response_class = NullResponse
cherrypy.engine.start_with_callback(run)
cherrypy.engine.block()

View File

@@ -0,0 +1,49 @@
"""Demonstration app for cherrypy.checker.
This application is intentionally broken and badly designed.
To demonstrate the output of the CherryPy Checker, simply execute
this module.
"""
import os
import cherrypy
thisdir = os.path.dirname(os.path.abspath(__file__))
class Root:
pass
if __name__ == '__main__':
conf = {'/base': {'tools.staticdir.root': thisdir,
# Obsolete key.
'throw_errors': True,
},
# This entry should be OK.
'/base/static': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'},
# Warn on missing folder.
'/base/js': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'js'},
# Warn on dir with an abs path even though we provide root.
'/base/static2': {'tools.staticdir.on': True,
'tools.staticdir.dir': '/static'},
# Warn on dir with a relative path with no root.
'/static3': {'tools.staticdir.on': True,
'tools.staticdir.dir': 'static'},
# Warn on unknown namespace
'/unknown': {'toobles.gzip.on': True},
# Warn special on cherrypy.<known ns>.*
'/cpknown': {'cherrypy.tools.encode.on': True},
# Warn on mismatched types
'/conftype': {'request.show_tracebacks': 14},
# Warn on unknown tool.
'/web': {'tools.unknown.on': True},
# Warn on server.* in app config.
'/app1': {'server.socket_host': '0.0.0.0'},
# Warn on 'localhost'
'global': {'server.socket_host': 'localhost'},
# Warn on '[name]'
'[/extra_brackets]': {},
}
cherrypy.quickstart(Root(), config=conf)

View File

@@ -0,0 +1,18 @@
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
ServerRoot /usr/lib/apache2
User #1000
ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log
DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test"
ServerName 127.0.0.1
Listen 8080
LoadModule fastcgi_module modules/mod_fastcgi.so
LoadModule rewrite_module modules/mod_rewrite.so
Options +ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000

View File

@@ -0,0 +1,14 @@
# Apache2 server conf file for testing CherryPy with mod_fcgid.
DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test"
ServerName 127.0.0.1
Listen 8080
LoadModule fastcgi_module modules/mod_fastcgi.dll
LoadModule rewrite_module modules/mod_rewrite.so
Options ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000

542
lib/cherrypy/test/helper.py Normal file
View File

@@ -0,0 +1,542 @@
"""A library of helper functions for the CherryPy test suite."""
import datetime
import io
import logging
import os
import re
import subprocess
import sys
import time
import unittest
import warnings
import portend
import pytest
import six
from cheroot.test import webtest
import cherrypy
from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob
from cherrypy.lib import httputil
from cherrypy.lib import gctools
log = logging.getLogger(__name__)
thisdir = os.path.abspath(os.path.dirname(__file__))
serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
class Supervisor(object):
"""Base class for modeling and controlling servers during testing."""
def __init__(self, **kwargs):
for k, v in kwargs.items():
if k == 'port':
setattr(self, k, int(v))
setattr(self, k, v)
def log_to_stderr(msg, level):
return sys.stderr.write(msg + os.linesep)
class LocalSupervisor(Supervisor):
"""Base class for modeling/controlling servers which run in the same
process.
When the server side runs in a different process, start/stop can dump all
state between each test module easily. When the server side runs in the
same process as the client, however, we have to do a bit more work to
ensure config and mounted apps are reset between tests.
"""
using_apache = False
using_wsgi = False
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
cherrypy.server.httpserver = self.httpserver_class
# This is perhaps the wrong place for this call but this is the only
# place that i've found so far that I KNOW is early enough to set this.
cherrypy.config.update({'log.screen': False})
engine = cherrypy.engine
if hasattr(engine, 'signal_handler'):
engine.signal_handler.subscribe()
if hasattr(engine, 'console_control_handler'):
engine.console_control_handler.subscribe()
def start(self, modulename=None):
"""Load and start the HTTP server."""
if modulename:
# Unhook httpserver so cherrypy.server.start() creates a new
# one (with config from setup_server, if declared).
cherrypy.server.httpserver = None
cherrypy.engine.start()
self.sync_apps()
def sync_apps(self):
"""Tell the server about any apps which the setup functions mounted."""
pass
def stop(self):
td = getattr(self, 'teardown', None)
if td:
td()
cherrypy.engine.exit()
servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
for name, server in servers_copy:
server.unsubscribe()
del cherrypy.servers[name]
class NativeServerSupervisor(LocalSupervisor):
"""Server supervisor for the builtin HTTP server."""
httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
using_apache = False
using_wsgi = False
def __str__(self):
return 'Builtin HTTP Server on %s:%s' % (self.host, self.port)
class LocalWSGISupervisor(LocalSupervisor):
"""Server supervisor for the builtin WSGI server."""
httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
using_apache = False
using_wsgi = True
def __str__(self):
return 'Builtin WSGI Server on %s:%s' % (self.host, self.port)
def sync_apps(self):
"""Hook a new WSGI app into the origin server."""
cherrypy.server.httpserver.wsgi_app = self.get_app()
def get_app(self, app=None):
"""Obtain a new (decorated) WSGI app to hook into the origin server."""
if app is None:
app = cherrypy.tree
if self.validate:
try:
from wsgiref import validate
except ImportError:
warnings.warn(
'Error importing wsgiref. The validator will not run.')
else:
# wraps the app in the validator
app = validate.validator(app)
return app
def get_cpmodpy_supervisor(**options):
from cherrypy.test import modpy
sup = modpy.ModPythonSupervisor(**options)
sup.template = modpy.conf_cpmodpy
return sup
def get_modpygw_supervisor(**options):
from cherrypy.test import modpy
sup = modpy.ModPythonSupervisor(**options)
sup.template = modpy.conf_modpython_gateway
sup.using_wsgi = True
return sup
def get_modwsgi_supervisor(**options):
from cherrypy.test import modwsgi
return modwsgi.ModWSGISupervisor(**options)
def get_modfcgid_supervisor(**options):
from cherrypy.test import modfcgid
return modfcgid.ModFCGISupervisor(**options)
def get_modfastcgi_supervisor(**options):
from cherrypy.test import modfastcgi
return modfastcgi.ModFCGISupervisor(**options)
def get_wsgi_u_supervisor(**options):
cherrypy.server.wsgi_version = ('u', 0)
return LocalWSGISupervisor(**options)
class CPWebCase(webtest.WebCase):
script_name = ''
scheme = 'http'
available_servers = {'wsgi': LocalWSGISupervisor,
'wsgi_u': get_wsgi_u_supervisor,
'native': NativeServerSupervisor,
'cpmodpy': get_cpmodpy_supervisor,
'modpygw': get_modpygw_supervisor,
'modwsgi': get_modwsgi_supervisor,
'modfcgid': get_modfcgid_supervisor,
'modfastcgi': get_modfastcgi_supervisor,
}
default_server = 'wsgi'
@classmethod
def _setup_server(cls, supervisor, conf):
v = sys.version.split()[0]
log.info('Python version used to run this test script: %s' % v)
log.info('CherryPy version: %s' % cherrypy.__version__)
if supervisor.scheme == 'https':
ssl = ' (ssl)'
else:
ssl = ''
log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl))
log.info('PID: %s' % os.getpid())
cherrypy.server.using_apache = supervisor.using_apache
cherrypy.server.using_wsgi = supervisor.using_wsgi
if sys.platform[:4] == 'java':
cherrypy.config.update({'server.nodelay': False})
if isinstance(conf, text_or_bytes):
parser = cherrypy.lib.reprconf.Parser()
conf = parser.dict_from_file(conf).get('global', {})
else:
conf = conf or {}
baseconf = conf.copy()
baseconf.update({'server.socket_host': supervisor.host,
'server.socket_port': supervisor.port,
'server.protocol_version': supervisor.protocol,
'environment': 'test_suite',
})
if supervisor.scheme == 'https':
# baseconf['server.ssl_module'] = 'builtin'
baseconf['server.ssl_certificate'] = serverpem
baseconf['server.ssl_private_key'] = serverpem
# helper must be imported lazily so the coverage tool
# can run against module-level statements within cherrypy.
# Also, we have to do "from cherrypy.test import helper",
# exactly like each test module does, because a relative import
# would stick a second instance of webtest in sys.modules,
# and we wouldn't be able to globally override the port anymore.
if supervisor.scheme == 'https':
webtest.WebCase.HTTP_CONN = HTTPSConnection
return baseconf
@classmethod
def setup_class(cls):
''
# Creates a server
conf = {
'scheme': 'http',
'protocol': 'HTTP/1.1',
'port': 54583,
'host': '127.0.0.1',
'validate': False,
'server': 'wsgi',
}
supervisor_factory = cls.available_servers.get(
conf.get('server', 'wsgi'))
if supervisor_factory is None:
raise RuntimeError('Unknown server in config: %s' % conf['server'])
supervisor = supervisor_factory(**conf)
# Copied from "run_test_suite"
cherrypy.config.reset()
baseconf = cls._setup_server(supervisor, conf)
cherrypy.config.update(baseconf)
setup_client()
if hasattr(cls, 'setup_server'):
# Clear the cherrypy tree and clear the wsgi server so that
# it can be updated with the new root
cherrypy.tree = cherrypy._cptree.Tree()
cherrypy.server.httpserver = None
cls.setup_server()
# Add a resource for verifying there are no refleaks
# to *every* test class.
cherrypy.tree.mount(gctools.GCRoot(), '/gc')
cls.do_gc_test = True
supervisor.start(cls.__module__)
cls.supervisor = supervisor
@classmethod
def teardown_class(cls):
''
if hasattr(cls, 'setup_server'):
cls.supervisor.stop()
do_gc_test = False
def test_gc(self):
if not self.do_gc_test:
return
self.getPage('/gc/stats')
try:
self.assertBody('Statistics:')
except Exception:
'Failures occur intermittently. See #1420'
def prefix(self):
return self.script_name.rstrip('/')
def base(self):
if ((self.scheme == 'http' and self.PORT == 80) or
(self.scheme == 'https' and self.PORT == 443)):
port = ''
else:
port = ':%s' % self.PORT
return '%s://%s%s%s' % (self.scheme, self.HOST, port,
self.script_name.rstrip('/'))
def exit(self):
sys.exit()
def getPage(self, url, headers=None, method='GET', body=None,
protocol=None, raise_subcls=None):
"""Open the url. Return status, headers, body.
`raise_subcls` must be a tuple with the exceptions classes
or a single exception class that are not going to be considered
a socket.error regardless that they were are subclass of a
socket.error and therefore not considered for a connection retry.
"""
if self.script_name:
url = httputil.urljoin(self.script_name, url)
return webtest.WebCase.getPage(self, url, headers, method, body,
protocol, raise_subcls)
def skip(self, msg='skipped '):
pytest.skip(msg)
def assertErrorPage(self, status, message=None, pattern=''):
"""Compare the response body with a built in error page.
The function will optionally look for the regexp pattern,
within the exception embedded in the error page."""
# This will never contain a traceback
page = cherrypy._cperror.get_error_page(status, message=message)
# First, test the response body without checking the traceback.
# Stick a match-all group (.*) in to grab the traceback.
def esc(text):
return re.escape(ntob(text))
epage = re.escape(page)
epage = epage.replace(
esc('<pre id="traceback"></pre>'),
esc('<pre id="traceback">') + b'(.*)' + esc('</pre>'))
m = re.match(epage, self.body, re.DOTALL)
if not m:
self._handlewebError(
'Error page does not match; expected:\n' + page)
return
# Now test the pattern against the traceback
if pattern is None:
# Special-case None to mean that there should be *no* traceback.
if m and m.group(1):
self._handlewebError('Error page contains traceback')
else:
if (m is None) or (
not re.search(ntob(re.escape(pattern), self.encoding),
m.group(1))):
msg = 'Error page does not contain %s in traceback'
self._handlewebError(msg % repr(pattern))
date_tolerance = 2
def assertEqualDates(self, dt1, dt2, seconds=None):
"""Assert abs(dt1 - dt2) is within Y seconds."""
if seconds is None:
seconds = self.date_tolerance
if dt1 > dt2:
diff = dt1 - dt2
else:
diff = dt2 - dt1
if not diff < datetime.timedelta(seconds=seconds):
raise AssertionError('%r and %r are not within %r seconds.' %
(dt1, dt2, seconds))
def _test_method_sorter(_, x, y):
"""Monkeypatch the test sorter to always run test_gc last in each suite."""
if x == 'test_gc':
return 1
if y == 'test_gc':
return -1
if x > y:
return 1
if x < y:
return -1
return 0
unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
def setup_client():
"""Set up the WebCase classes to match the server's socket settings."""
webtest.WebCase.PORT = cherrypy.server.socket_port
webtest.WebCase.HOST = cherrypy.server.socket_host
if cherrypy.server.ssl_certificate:
CPWebCase.scheme = 'https'
# --------------------------- Spawning helpers --------------------------- #
class CPProcess(object):
pid_file = os.path.join(thisdir, 'test.pid')
config_file = os.path.join(thisdir, 'test.conf')
config_template = """[global]
server.socket_host: '%(host)s'
server.socket_port: %(port)s
checker.on: False
log.screen: False
log.error_file: r'%(error_log)s'
log.access_file: r'%(access_log)s'
%(ssl)s
%(extra)s
"""
error_log = os.path.join(thisdir, 'test.error.log')
access_log = os.path.join(thisdir, 'test.access.log')
def __init__(self, wait=False, daemonize=False, ssl=False,
socket_host=None, socket_port=None):
self.wait = wait
self.daemonize = daemonize
self.ssl = ssl
self.host = socket_host or cherrypy.server.socket_host
self.port = socket_port or cherrypy.server.socket_port
def write_conf(self, extra=''):
if self.ssl:
serverpem = os.path.join(thisdir, 'test.pem')
ssl = """
server.ssl_certificate: r'%s'
server.ssl_private_key: r'%s'
""" % (serverpem, serverpem)
else:
ssl = ''
conf = self.config_template % {
'host': self.host,
'port': self.port,
'error_log': self.error_log,
'access_log': self.access_log,
'ssl': ssl,
'extra': extra,
}
with io.open(self.config_file, 'w', encoding='utf-8') as f:
f.write(six.text_type(conf))
def start(self, imports=None):
"""Start cherryd in a subprocess."""
portend.free(self.host, self.port, timeout=1)
args = [
'-m',
'cherrypy',
'-c', self.config_file,
'-p', self.pid_file,
]
r"""
Command for running cherryd server with autoreload enabled
Using
```
['-c',
"__requires__ = 'CherryPy'; \
import pkg_resources, re, sys; \
sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
sys.exit(\
pkg_resources.load_entry_point(\
'CherryPy', 'console_scripts', 'cherryd')())"]
```
doesn't work as it's impossible to reconstruct the `-c`'s contents.
Ref: https://github.com/cherrypy/cherrypy/issues/1545
"""
if not isinstance(imports, (list, tuple)):
imports = [imports]
for i in imports:
if i:
args.append('-i')
args.append(i)
if self.daemonize:
args.append('-d')
env = os.environ.copy()
# Make sure we import the cherrypy package in which this module is
# defined.
grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
if env.get('PYTHONPATH', ''):
env['PYTHONPATH'] = os.pathsep.join(
(grandparentdir, env['PYTHONPATH']))
else:
env['PYTHONPATH'] = grandparentdir
self._proc = subprocess.Popen([sys.executable] + args, env=env)
if self.wait:
self.exit_code = self._proc.wait()
else:
portend.occupied(self.host, self.port, timeout=5)
# Give the engine a wee bit more time to finish STARTING
if self.daemonize:
time.sleep(2)
else:
time.sleep(1)
def get_pid(self):
if self.daemonize:
return int(open(self.pid_file, 'rb').read())
return self._proc.pid
def join(self):
"""Wait for the process to exit."""
if self.daemonize:
return self._join_daemon()
self._proc.wait()
def _join_daemon(self):
try:
try:
# Mac, UNIX
os.wait()
except AttributeError:
# Windows
try:
pid = self.get_pid()
except IOError:
# Assume the subprocess deleted the pidfile on shutdown.
pass
else:
os.waitpid(pid, 0)
except OSError:
x = sys.exc_info()[1]
if x.args != (10, 'No child processes'):
raise

View File

@@ -0,0 +1,228 @@
"""logtest, a unittest.TestCase helper for testing log output."""
import sys
import time
from uuid import UUID
import six
from cherrypy._cpcompat import text_or_bytes, ntob
try:
# On Windows, msvcrt.getch reads a single char without output.
import msvcrt
def getchar():
return msvcrt.getch()
except ImportError:
# Unix getchr
import tty
import termios
def getchar():
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
try:
tty.setraw(sys.stdin.fileno())
ch = sys.stdin.read(1)
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
return ch
class LogCase(object):
"""unittest.TestCase mixin for testing log messages.
logfile: a filename for the desired log. Yes, I know modes are evil,
but it makes the test functions so much cleaner to set this once.
lastmarker: the last marker in the log. This can be used to search for
messages since the last marker.
markerPrefix: a string with which to prefix log markers. This should be
unique enough from normal log output to use for marker identification.
"""
logfile = None
lastmarker = None
markerPrefix = b'test suite marker: '
def _handleLogError(self, msg, data, marker, pattern):
print('')
print(' ERROR: %s' % msg)
if not self.interactive:
raise self.failureException(msg)
p = (' Show: '
'[L]og [M]arker [P]attern; '
'[I]gnore, [R]aise, or sys.e[X]it >> ')
sys.stdout.write(p + ' ')
# ARGH
sys.stdout.flush()
while True:
i = getchar().upper()
if i not in 'MPLIRX':
continue
print(i.upper()) # Also prints new line
if i == 'L':
for x, line in enumerate(data):
if (x + 1) % self.console_height == 0:
# The \r and comma should make the next line overwrite
sys.stdout.write('<-- More -->\r ')
m = getchar().lower()
# Erase our "More" prompt
sys.stdout.write(' \r ')
if m == 'q':
break
print(line.rstrip())
elif i == 'M':
print(repr(marker or self.lastmarker))
elif i == 'P':
print(repr(pattern))
elif i == 'I':
# return without raising the normal exception
return
elif i == 'R':
raise self.failureException(msg)
elif i == 'X':
self.exit()
sys.stdout.write(p + ' ')
def exit(self):
sys.exit()
def emptyLog(self):
"""Overwrite self.logfile with 0 bytes."""
open(self.logfile, 'wb').write('')
def markLog(self, key=None):
"""Insert a marker line into the log and set self.lastmarker."""
if key is None:
key = str(time.time())
self.lastmarker = key
open(self.logfile, 'ab+').write(
ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
def _read_marked_region(self, marker=None):
"""Return lines from self.logfile in the marked region.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be returned.
"""
# Give the logger time to finish writing?
# time.sleep(0.5)
logfile = self.logfile
marker = marker or self.lastmarker
if marker is None:
return open(logfile, 'rb').readlines()
if isinstance(marker, six.text_type):
marker = marker.encode('utf-8')
data = []
in_region = False
for line in open(logfile, 'rb'):
if in_region:
if line.startswith(self.markerPrefix) and marker not in line:
break
else:
data.append(line)
elif marker in line:
in_region = True
return data
def assertInLog(self, line, marker=None):
"""Fail if the given (partial) line is not in the log.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
for logline in data:
if line in logline:
return
msg = '%r not found in log' % line
self._handleLogError(msg, data, marker, line)
def assertNotInLog(self, line, marker=None):
"""Fail if the given (partial) line is in the log.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
for logline in data:
if line in logline:
msg = '%r found in log' % line
self._handleLogError(msg, data, marker, line)
def assertValidUUIDv4(self, marker=None):
"""Fail if the given UUIDv4 is not valid.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
data = [
chunk.decode('utf-8').rstrip('\n').rstrip('\r')
for chunk in data
]
for log_chunk in data:
try:
uuid_log = data[-1]
uuid_obj = UUID(uuid_log, version=4)
except (TypeError, ValueError):
pass # it might be in other chunk
else:
if str(uuid_obj) == uuid_log:
return
msg = '%r is not a valid UUIDv4' % uuid_log
self._handleLogError(msg, data, marker, log_chunk)
msg = 'UUIDv4 not found in log'
self._handleLogError(msg, data, marker, log_chunk)
def assertLog(self, sliceargs, lines, marker=None):
"""Fail if log.readlines()[sliceargs] is not contained in 'lines'.
The log will be searched from the given marker to the next marker.
If marker is None, self.lastmarker is used. If the log hasn't
been marked (using self.markLog), the entire log will be searched.
"""
data = self._read_marked_region(marker)
if isinstance(sliceargs, int):
# Single arg. Use __getitem__ and allow lines to be str or list.
if isinstance(lines, (tuple, list)):
lines = lines[0]
if isinstance(lines, six.text_type):
lines = lines.encode('utf-8')
if lines not in data[sliceargs]:
msg = '%r not found on log line %r' % (lines, sliceargs)
self._handleLogError(
msg,
[data[sliceargs], '--EXTRA CONTEXT--'] + data[
sliceargs + 1:sliceargs + 6],
marker,
lines)
else:
# Multiple args. Use __getslice__ and require lines to be list.
if isinstance(lines, tuple):
lines = list(lines)
elif isinstance(lines, text_or_bytes):
raise TypeError("The 'lines' arg must be a list when "
"'sliceargs' is a tuple.")
start, stop = sliceargs
for line, logline in zip(lines, data[start:stop]):
if isinstance(line, six.text_type):
line = line.encode('utf-8')
if line not in logline:
msg = '%r not found in log' % line
self._handleLogError(msg, data[start:stop], marker, line)

View File

@@ -0,0 +1,136 @@
"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
To autostart fastcgi, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl", "apache2ctl",
or "httpd"--create a symlink to them if needed.
You'll also need the WSGIServer from flup.servers.
See http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy.process import servers
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'apache2ctl'
CONF_PATH = 'fastcgi.conf'
conf_fastcgi = """
# Apache2 server conf file for testing CherryPy with mod_fastcgi.
# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
ServerRoot /usr/lib/apache2
User #1000
ErrorLog %(root)s/mod_fastcgi.error.log
DocumentRoot "%(root)s"
ServerName 127.0.0.1
Listen %(port)s
LoadModule fastcgi_module modules/mod_fastcgi.so
LoadModule rewrite_module modules/mod_rewrite.so
Options +ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
"""
def erase_script_name(environ, start_response):
environ['SCRIPT_NAME'] = ''
return cherrypy.tree(environ, start_response)
class ModFCGISupervisor(helper.LocalWSGISupervisor):
httpserver_class = 'cherrypy.process.servers.FlupFCGIServer'
using_apache = True
using_wsgi = True
template = conf_fastcgi
def __str__(self):
return 'FCGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
cherrypy.server.httpserver = servers.FlupFCGIServer(
application=erase_script_name, bindAddress=('127.0.0.1', 4000))
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
cherrypy.server.socket_port = 4000
# For FCGI, we both start apache...
self.start_apache()
# ...and our local server
cherrypy.engine.start()
self.sync_apps()
def start_apache(self):
fcgiconf = CONF_PATH
if not os.path.isabs(fcgiconf):
fcgiconf = os.path.join(curdir, fcgiconf)
# Write the Apache conf file.
f = open(fcgiconf, 'wb')
try:
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
output = self.template % {'port': self.port, 'root': curdir,
'server': server}
output = output.replace('\r\n', '\n')
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
helper.LocalWSGISupervisor.stop(self)
def sync_apps(self):
cherrypy.server.httpserver.fcgiserver.application = self.get_app(
erase_script_name)

View File

@@ -0,0 +1,124 @@
"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
To autostart fcgid, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl", "apache2ctl",
or "httpd"--create a symlink to them if needed.
You'll also need the WSGIServer from flup.servers.
See http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.process import servers
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'httpd'
CONF_PATH = 'fcgi.conf'
conf_fcgid = """
# Apache2 server conf file for testing CherryPy with mod_fcgid.
DocumentRoot "%(root)s"
ServerName 127.0.0.1
Listen %(port)s
LoadModule fastcgi_module modules/mod_fastcgi.dll
LoadModule rewrite_module modules/mod_rewrite.so
Options ExecCGI
SetHandler fastcgi-script
RewriteEngine On
RewriteRule ^(.*)$ /fastcgi.pyc [L]
FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
"""
class ModFCGISupervisor(helper.LocalSupervisor):
using_apache = True
using_wsgi = True
template = conf_fcgid
def __str__(self):
return 'FCGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
cherrypy.server.httpserver = servers.FlupFCGIServer(
application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
# For FCGI, we both start apache...
self.start_apache()
# ...and our local server
helper.LocalServer.start(self, modulename)
def start_apache(self):
fcgiconf = CONF_PATH
if not os.path.isabs(fcgiconf):
fcgiconf = os.path.join(curdir, fcgiconf)
# Write the Apache conf file.
f = open(fcgiconf, 'wb')
try:
server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
output = self.template % {'port': self.port, 'root': curdir,
'server': server}
output = ntob(output.replace('\r\n', '\n'))
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
helper.LocalServer.stop(self)
def sync_apps(self):
cherrypy.server.httpserver.fcgiserver.application = self.get_app()

164
lib/cherrypy/test/modpy.py Normal file
View File

@@ -0,0 +1,164 @@
"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
To autostart modpython, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
create a symlink to them if needed.
If you wish to test the WSGI interface instead of our _cpmodpy interface,
you also need the 'modpython_gateway' module at:
http://projects.amor.org/misc/wiki/ModPythonGateway
KNOWN BUGS
==========
1. Apache processes Range headers automatically; CherryPy's truncated
output is then truncated again by Apache. See test_core.testRanges.
This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
4. Apache replaces status "reason phrases" automatically. For example,
CherryPy may set "304 Not modified" but Apache will write out
"304 Not Modified" (capital "M").
5. Apache does not allow custom error codes as per the spec.
6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
Request-URI too early.
7. mod_python will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. Apache will output a "Content-Length: 0" response header even if there's
no response entity body. This isn't really a bug; it just differs from
the CherryPy default.
"""
import os
import re
import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
APACHE_PATH = 'httpd'
CONF_PATH = 'test_mp.conf'
conf_modpython_gateway = """
# Apache2 server conf file for testing CherryPy with modpython_gateway.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
LoadModule python_module modules/mod_python.so
SetHandler python-program
PythonFixupHandler cherrypy.test.modpy::wsgisetup
PythonOption testmod %(modulename)s
PythonHandler modpython_gateway::handler
PythonOption wsgi.application cherrypy::tree
PythonOption socket_host %(host)s
PythonDebug On
"""
conf_cpmodpy = """
# Apache2 server conf file for testing CherryPy with _cpmodpy.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
LoadModule python_module modules/mod_python.so
SetHandler python-program
PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
PythonHandler cherrypy._cpmodpy::handler
PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
PythonOption socket_host %(host)s
PythonDebug On
"""
class ModPythonSupervisor(helper.Supervisor):
using_apache = True
using_wsgi = False
template = None
def __str__(self):
return 'ModPython Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
mpconf = CONF_PATH
if not os.path.isabs(mpconf):
mpconf = os.path.join(curdir, mpconf)
f = open(mpconf, 'wb')
try:
f.write(self.template %
{'port': self.port, 'modulename': modulename,
'host': self.host})
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
if result:
print(result)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
loaded = False
def wsgisetup(req):
global loaded
if not loaded:
loaded = True
options = req.get_options()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
'server.socket_host': options['socket_host'],
})
modname = options['testmod']
mod = __import__(modname, globals(), locals(), [''])
mod.setup_server()
cherrypy.server.unsubscribe()
cherrypy.engine.start()
from mod_python import apache
return apache.OK
def cpmodpysetup(req):
global loaded
if not loaded:
loaded = True
options = req.get_options()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.log'),
'environment': 'test_suite',
'server.socket_host': options['socket_host'],
})
from mod_python import apache
return apache.OK

View File

@@ -0,0 +1,154 @@
"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
To autostart modwsgi, the "apache" executable or script must be
on your system path, or you must override the global APACHE_PATH.
On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
create a symlink to them if needed.
KNOWN BUGS
==========
##1. Apache processes Range headers automatically; CherryPy's truncated
## output is then truncated again by Apache. See test_core.testRanges.
## This was worked around in http://www.cherrypy.org/changeset/1319.
2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
See test_core.testHTTPMethods.
3. Max request header and body settings do not work with Apache.
##4. Apache replaces status "reason phrases" automatically. For example,
## CherryPy may set "304 Not modified" but Apache will write out
## "304 Not Modified" (capital "M").
##5. Apache does not allow custom error codes as per the spec.
##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
## Request-URI too early.
7. mod_wsgi will not read request bodies which use the "chunked"
transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
mod_python's requestobject.c).
8. When responding with 204 No Content, mod_wsgi adds a Content-Length
header for you.
9. When an error is raised, mod_wsgi has no facility for printing a
traceback as the response content (it's sent to the Apache log instead).
10. Startup and shutdown of Apache when running mod_wsgi seems slow.
"""
import os
import re
import sys
import time
import portend
from cheroot.test import webtest
import cherrypy
from cherrypy.test import helper
curdir = os.path.abspath(os.path.dirname(__file__))
def read_process(cmd, args=''):
pipein, pipeout = os.popen4('%s %s' % (cmd, args))
try:
firstline = pipeout.readline()
if (re.search(r'(not recognized|No such file|not found)', firstline,
re.IGNORECASE)):
raise IOError('%s must be on your system path.' % cmd)
output = firstline + pipeout.read()
finally:
pipeout.close()
return output
if sys.platform == 'win32':
APACHE_PATH = 'httpd'
else:
APACHE_PATH = 'apache'
CONF_PATH = 'test_mw.conf'
conf_modwsgi = r"""
# Apache2 server conf file for testing CherryPy with modpython_gateway.
ServerName 127.0.0.1
DocumentRoot "/"
Listen %(port)s
AllowEncodedSlashes On
LoadModule rewrite_module modules/mod_rewrite.so
RewriteEngine on
RewriteMap escaping int:escape
LoadModule log_config_module modules/mod_log_config.so
LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
CustomLog "%(curdir)s/apache.access.log" combined
ErrorLog "%(curdir)s/apache.error.log"
LogLevel debug
LoadModule wsgi_module modules/mod_wsgi.so
LoadModule env_module modules/mod_env.so
WSGIScriptAlias / "%(curdir)s/modwsgi.py"
SetEnv testmod %(testmod)s
""" # noqa E501
class ModWSGISupervisor(helper.Supervisor):
"""Server Controller for ModWSGI and CherryPy."""
using_apache = True
using_wsgi = True
template = conf_modwsgi
def __str__(self):
return 'ModWSGI Server on %s:%s' % (self.host, self.port)
def start(self, modulename):
mpconf = CONF_PATH
if not os.path.isabs(mpconf):
mpconf = os.path.join(curdir, mpconf)
f = open(mpconf, 'wb')
try:
output = (self.template %
{'port': self.port, 'testmod': modulename,
'curdir': curdir})
f.write(output)
finally:
f.close()
result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
if result:
print(result)
# Make a request so mod_wsgi starts up our app.
# If we don't, concurrent initial requests will 404.
portend.occupied('127.0.0.1', self.port, timeout=5)
webtest.openURL('/ihopetheresnodefault', port=self.port)
time.sleep(1)
def stop(self):
"""Gracefully shutdown a server that is serving forever."""
read_process(APACHE_PATH, '-k stop')
loaded = False
def application(environ, start_response):
global loaded
if not loaded:
loaded = True
modname = 'cherrypy.test.' + environ['testmod']
mod = __import__(modname, globals(), locals(), [''])
mod.setup_server()
cherrypy.config.update({
'log.error_file': os.path.join(curdir, 'test.error.log'),
'log.access_file': os.path.join(curdir, 'test.access.log'),
'environment': 'test_suite',
'engine.SIGHUP': None,
'engine.SIGTERM': None,
})
return cherrypy.tree(environ, start_response)

View File

@@ -0,0 +1,161 @@
#!/usr/bin/python
"""A session demonstration app."""
import calendar
from datetime import datetime
import sys
import six
import cherrypy
from cherrypy.lib import sessions
page = """
<html>
<head>
<style type='text/css'>
table { border-collapse: collapse; border: 1px solid #663333; }
th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
border: 1px solid #663333; }
.warn { font-family: serif; color: #990000; }
</style>
<script type="text/javascript">
<!--
function twodigit(d) { return d < 10 ? "0" + d : d; }
function formattime(t) {
var month = t.getUTCMonth() + 1;
var day = t.getUTCDate();
var year = t.getUTCFullYear();
var hours = t.getUTCHours();
var minutes = t.getUTCMinutes();
return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
hours + ":" + twodigit(minutes) + " UTC");
}
function interval(s) {
// Return the given interval (in seconds) as an English phrase
var seconds = s %% 60;
s = Math.floor(s / 60);
var minutes = s %% 60;
s = Math.floor(s / 60);
var hours = s %% 24;
var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
var days = Math.floor(s / 24);
if (days != 0) v = days + ' days, ' + v;
return v;
}
var fudge_seconds = 5;
function init() {
// Set the content of the 'btime' cell.
var currentTime = new Date();
var bunixtime = Math.floor(currentTime.getTime() / 1000);
var v = formattime(currentTime);
v += " (Unix time: " + bunixtime + ")";
var diff = Math.abs(%(serverunixtime)s - bunixtime);
if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
document.getElementById('btime').innerHTML = v;
// Warn if response cookie expires is not close to one hour in the future.
// Yes, we want this to happen when wit hit the 'Expire' link, too.
var expires = Date.parse("%(expires)s") / 1000;
var onehour = (60 * 60);
if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
diff = Math.floor(expires - bunixtime);
if (expires > (bunixtime + onehour)) {
var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
} else {
var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
}
document.getElementById('respcookiewarn').innerHTML = msg;
}
}
//-->
</script>
</head>
<body onload='init()'>
<h2>Session Demo</h2>
<p>Reload this page. The session ID should not change from one reload to the next</p>
<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
<table>
<tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
<tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
<tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
<tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
<tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
<tr><th>Browser Time</th><td id='btime'>&nbsp;</td></tr>
<tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
<tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
</table>
</body></html>
""" # noqa E501
class Root(object):
def page(self):
changemsg = []
if cherrypy.session.id != cherrypy.session.originalid:
if cherrypy.session.originalid is None:
changemsg.append(
'Created new session because no session id was given.')
if cherrypy.session.missing:
changemsg.append(
'Created new session due to missing '
'(expired or malicious) session.')
if cherrypy.session.regenerated:
changemsg.append('Application generated a new session.')
try:
expires = cherrypy.response.cookie['session_id']['expires']
except KeyError:
expires = ''
return page % {
'sessionid': cherrypy.session.id,
'changemsg': '<br>'.join(changemsg),
'respcookie': cherrypy.response.cookie.output(),
'reqcookie': cherrypy.request.cookie.output(),
'sessiondata': list(six.iteritems(cherrypy.session)),
'servertime': (
datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
),
'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
'cpversion': cherrypy.__version__,
'pyversion': sys.version,
'expires': expires,
}
@cherrypy.expose
def index(self):
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'green'
return self.page()
@cherrypy.expose
def expire(self):
sessions.expire()
return self.page()
@cherrypy.expose
def regen(self):
cherrypy.session.regenerate()
# Must modify data or the session will not be saved.
cherrypy.session['color'] = 'yellow'
return self.page()
if __name__ == '__main__':
cherrypy.config.update({
# 'environment': 'production',
'log.screen': True,
'tools.sessions.on': True,
})
cherrypy.quickstart(Root())

View File

@@ -0,0 +1,5 @@
<html>
<body>
<h1>I couldn't find that thing you were looking for!</h1>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1 @@
Hello, world

View File

@@ -0,0 +1 @@
Dummy stylesheet

View File

@@ -0,0 +1,38 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
-----END RSA PRIVATE KEY-----
-----BEGIN CERTIFICATE-----
MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
NluCaWQys3MS
-----END CERTIFICATE-----

View File

@@ -0,0 +1,135 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
from hashlib import md5
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.lib import auth_basic
from cherrypy.test import helper
class BasicAuthTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'This is public.'
class BasicProtected:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
class BasicProtected2:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
class BasicProtected2_u:
@cherrypy.expose
def index(self):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
userpassdict = {'xuser': 'xpassword'}
userhashdict = {'xuser': md5(b'xpassword').hexdigest()}
userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()}
def checkpasshash(realm, user, password):
p = userhashdict.get(user)
return p and p == md5(ntob(password)).hexdigest() or False
def checkpasshash_u(realm, user, password):
p = userhashdict_u.get(user)
return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False
basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
conf = {
'/basic': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': basic_checkpassword_dict
},
'/basic2': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': checkpasshash,
'tools.auth_basic.accept_charset': 'ISO-8859-1',
},
'/basic2_u': {
'tools.auth_basic.on': True,
'tools.auth_basic.realm': 'wonderland',
'tools.auth_basic.checkpassword': checkpasshash_u,
'tools.auth_basic.accept_charset': 'UTF-8',
},
}
root = Root()
root.basic = BasicProtected()
root.basic2 = BasicProtected2()
root.basic2_u = BasicProtected2_u()
cherrypy.tree.mount(root, config=conf)
def testPublic(self):
self.getPage('/')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('This is public.')
def testBasic(self):
self.getPage('/basic/')
self.assertStatus(401)
self.assertHeader(
'WWW-Authenticate',
'Basic realm="wonderland", charset="UTF-8"'
)
self.getPage('/basic/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
self.assertStatus(401)
self.getPage('/basic/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
self.assertStatus('200 OK')
self.assertBody("Hello xuser, you've been authorized.")
def testBasic2(self):
self.getPage('/basic2/')
self.assertStatus(401)
self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
self.getPage('/basic2/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
self.assertStatus(401)
self.getPage('/basic2/',
[('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
self.assertStatus('200 OK')
self.assertBody("Hello xuser, you've been authorized.")
def testBasic2_u(self):
self.getPage('/basic2_u/')
self.assertStatus(401)
self.assertHeader(
'WWW-Authenticate',
'Basic realm="wonderland", charset="UTF-8"'
)
self.getPage('/basic2_u/',
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')])
self.assertStatus(401)
self.getPage('/basic2_u/',
[('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')])
self.assertStatus('200 OK')
self.assertBody("Hello xюзер, you've been authorized.")

View File

@@ -0,0 +1,134 @@
# This file is part of CherryPy <http://www.cherrypy.org/>
# -*- coding: utf-8 -*-
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
import six
import cherrypy
from cherrypy.lib import auth_digest
from cherrypy._cpcompat import ntob
from cherrypy.test import helper
def _fetch_users():
return {'test': 'test', '☃йюзер': 'їпароль'}
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users())
class DigestAuthTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'This is public.'
class DigestProtected:
@cherrypy.expose
def index(self, *args, **kwargs):
return "Hello %s, you've been authorized." % (
cherrypy.request.login)
conf = {'/digest': {'tools.auth_digest.on': True,
'tools.auth_digest.realm': 'localhost',
'tools.auth_digest.get_ha1': get_ha1,
'tools.auth_digest.key': 'a565c27146791cfb',
'tools.auth_digest.debug': True,
'tools.auth_digest.accept_charset': 'UTF-8'}}
root = Root()
root.digest = DigestProtected()
cherrypy.tree.mount(root, config=conf)
def testPublic(self):
self.getPage('/')
assert self.status == '200 OK'
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
assert self.body == b'This is public.'
def _test_parametric_digest(self, username, realm):
test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path'
self.getPage(test_uri)
assert self.status_code == 401
msg = 'Digest authentification scheme was not found'
www_auth_digest = tuple(filter(
lambda kv: kv[0].lower() == 'www-authenticate'
and kv[1].startswith('Digest '),
self.headers,
))
assert len(www_auth_digest) == 1, msg
items = www_auth_digest[0][-1][7:].split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
assert tokens['realm'] == '"localhost"'
assert tokens['algorithm'] == '"MD5"'
assert tokens['qop'] == '"auth"'
assert tokens['charset'] == '"UTF-8"'
nonce = tokens['nonce'].strip('"')
# Test user agent response with a wrong value for 'realm'
base_auth = ('Digest username="%s", '
'realm="%s", '
'nonce="%s", '
'uri="%s", '
'algorithm=MD5, '
'response="%s", '
'qop=auth, '
'nc=%s, '
'cnonce="1522e61005789929"')
encoded_user = username
if six.PY3:
encoded_user = encoded_user.encode('utf-8')
encoded_user = encoded_user.decode('latin1')
auth_header = base_auth % (
encoded_user, realm, nonce, test_uri,
'11111111111111111111111111111111', '00000001',
)
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
# calculate the response digest
ha1 = get_ha1(auth.realm, auth.username)
response = auth.request_digest(ha1)
auth_header = base_auth % (
encoded_user, realm, nonce, test_uri,
response, '00000001',
)
self.getPage(test_uri, [('Authorization', auth_header)])
def test_wrong_realm(self):
# send response with correct response digest, but wrong realm
self._test_parametric_digest(username='test', realm='wrong realm')
assert self.status_code == 401
def test_ascii_user(self):
self._test_parametric_digest(username='test', realm='localhost')
assert self.status == '200 OK'
assert self.body == b"Hello test, you've been authorized."
def test_unicode_user(self):
self._test_parametric_digest(username='☃йюзер', realm='localhost')
assert self.status == '200 OK'
assert self.body == ntob(
"Hello ☃йюзер, you've been authorized.", 'utf-8',
)
def test_wrong_scheme(self):
basic_auth = {
'Authorization': 'Basic foo:bar',
}
self.getPage('/digest/', headers=list(basic_auth.items()))
assert self.status_code == 401

View File

@@ -0,0 +1,274 @@
import threading
import time
import unittest
from cherrypy.process import wspbus
msg = 'Listener %d on channel %s: %s.'
class PublishSubscribeTests(unittest.TestCase):
def get_listener(self, channel, index):
def listener(arg=None):
self.responses.append(msg % (index, channel, arg))
return listener
def test_builtin_channels(self):
b = wspbus.Bus()
self.responses, expected = [], []
for channel in b.listeners:
for index, priority in enumerate([100, 50, 0, 51]):
b.subscribe(channel,
self.get_listener(channel, index), priority)
for channel in b.listeners:
b.publish(channel)
expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
b.publish(channel, arg=79347)
expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
self.assertEqual(self.responses, expected)
def test_custom_channels(self):
b = wspbus.Bus()
self.responses, expected = [], []
custom_listeners = ('hugh', 'louis', 'dewey')
for channel in custom_listeners:
for index, priority in enumerate([None, 10, 60, 40]):
b.subscribe(channel,
self.get_listener(channel, index), priority)
for channel in custom_listeners:
b.publish(channel, 'ah so')
expected.extend([msg % (i, channel, 'ah so')
for i in (1, 3, 0, 2)])
b.publish(channel)
expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
self.assertEqual(self.responses, expected)
def test_listener_errors(self):
b = wspbus.Bus()
self.responses, expected = [], []
channels = [c for c in b.listeners if c != 'log']
for channel in channels:
b.subscribe(channel, self.get_listener(channel, 1))
# This will break since the lambda takes no args.
b.subscribe(channel, lambda: None, priority=20)
for channel in channels:
self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
expected.append(msg % (1, channel, 123))
self.assertEqual(self.responses, expected)
class BusMethodTests(unittest.TestCase):
def log(self, bus):
self._log_entries = []
def logit(msg, level):
self._log_entries.append(msg)
bus.subscribe('log', logit)
def assertLog(self, entries):
self.assertEqual(self._log_entries, entries)
def get_listener(self, channel, index):
def listener(arg=None):
self.responses.append(msg % (index, channel, arg))
return listener
def test_start(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('start', self.get_listener('start', index))
b.start()
try:
# The start method MUST call all 'start' listeners.
self.assertEqual(
set(self.responses),
set([msg % (i, 'start', None) for i in range(num)]))
# The start method MUST move the state to STARTED
# (or EXITING, if errors occur)
self.assertEqual(b.state, b.states.STARTED)
# The start method MUST log its states.
self.assertLog(['Bus STARTING', 'Bus STARTED'])
finally:
# Exit so the atexit handler doesn't complain.
b.exit()
def test_stop(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.stop()
# The stop method MUST call all 'stop' listeners.
self.assertEqual(set(self.responses),
set([msg % (i, 'stop', None) for i in range(num)]))
# The stop method MUST move the state to STOPPED
self.assertEqual(b.state, b.states.STOPPED)
# The stop method MUST log its states.
self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
def test_graceful(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('graceful', self.get_listener('graceful', index))
b.graceful()
# The graceful method MUST call all 'graceful' listeners.
self.assertEqual(
set(self.responses),
set([msg % (i, 'graceful', None) for i in range(num)]))
# The graceful method MUST log its states.
self.assertLog(['Bus graceful'])
def test_exit(self):
b = wspbus.Bus()
self.log(b)
self.responses = []
num = 3
for index in range(num):
b.subscribe('stop', self.get_listener('stop', index))
b.subscribe('exit', self.get_listener('exit', index))
b.exit()
# The exit method MUST call all 'stop' listeners,
# and then all 'exit' listeners.
self.assertEqual(set(self.responses),
set([msg % (i, 'stop', None) for i in range(num)] +
[msg % (i, 'exit', None) for i in range(num)]))
# The exit method MUST move the state to EXITING
self.assertEqual(b.state, b.states.EXITING)
# The exit method MUST log its states.
self.assertLog(
['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
def test_wait(self):
b = wspbus.Bus()
def f(method):
time.sleep(0.2)
getattr(b, method)()
for method, states in [('start', [b.states.STARTED]),
('stop', [b.states.STOPPED]),
('start',
[b.states.STARTING, b.states.STARTED]),
('exit', [b.states.EXITING]),
]:
threading.Thread(target=f, args=(method,)).start()
b.wait(states)
# The wait method MUST wait for the given state(s).
if b.state not in states:
self.fail('State %r not in %r' % (b.state, states))
def test_block(self):
b = wspbus.Bus()
self.log(b)
def f():
time.sleep(0.2)
b.exit()
def g():
time.sleep(0.4)
threading.Thread(target=f).start()
threading.Thread(target=g).start()
threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 3)
b.block()
# The block method MUST wait for the EXITING state.
self.assertEqual(b.state, b.states.EXITING)
# The block method MUST wait for ALL non-main, non-daemon threads to
# finish.
threads = [t for t in threading.enumerate() if not t.daemon]
self.assertEqual(len(threads), 1)
# The last message will mention an indeterminable thread name; ignore
# it
self.assertEqual(self._log_entries[:-1],
['Bus STOPPING', 'Bus STOPPED',
'Bus EXITING', 'Bus EXITED',
'Waiting for child threads to terminate...'])
def test_start_with_callback(self):
b = wspbus.Bus()
self.log(b)
try:
events = []
def f(*args, **kwargs):
events.append(('f', args, kwargs))
def g():
events.append('g')
b.subscribe('start', g)
b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
# Give wait() time to run f()
time.sleep(0.2)
# The callback method MUST wait for the STARTED state.
self.assertEqual(b.state, b.states.STARTED)
# The callback method MUST run after all start methods.
self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
finally:
b.exit()
def test_log(self):
b = wspbus.Bus()
self.log(b)
self.assertLog([])
# Try a normal message.
expected = []
for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
b.log(msg)
expected.append(msg)
self.assertLog(expected)
# Try an error message
try:
foo
except NameError:
b.log('You are lost and gone forever', traceback=True)
lastmsg = self._log_entries[-1]
if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
self.fail('Last log message %r did not contain '
'the expected traceback.' % lastmsg)
else:
self.fail('NameError was not raised as expected.')
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,392 @@
import datetime
from itertools import count
import os
import threading
import time
from six.moves import range
from six.moves import urllib
import pytest
import cherrypy
from cherrypy.lib import httputil
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
gif_bytes = (
b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
)
class CacheTest(helper.CPWebCase):
@staticmethod
def setup_server():
@cherrypy.config(**{'tools.caching.on': True})
class Root:
def __init__(self):
self.counter = 0
self.control_counter = 0
self.longlock = threading.Lock()
@cherrypy.expose
def index(self):
self.counter += 1
msg = 'visit #%s' % self.counter
return msg
@cherrypy.expose
def control(self):
self.control_counter += 1
return 'visit #%s' % self.control_counter
@cherrypy.expose
def a_gif(self):
cherrypy.response.headers[
'Last-Modified'] = httputil.HTTPDate()
return gif_bytes
@cherrypy.expose
def long_process(self, seconds='1'):
try:
self.longlock.acquire()
time.sleep(float(seconds))
finally:
self.longlock.release()
return 'success!'
@cherrypy.expose
def clear_cache(self, path):
cherrypy._cache.store[cherrypy.request.base + path].clear()
@cherrypy.config(**{
'tools.caching.on': True,
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Vary', 'Our-Varying-Header')
],
})
class VaryHeaderCachingServer(object):
def __init__(self):
self.counter = count(1)
@cherrypy.expose
def index(self):
return 'visit #%s' % next(self.counter)
@cherrypy.config(**{
'tools.expires.on': True,
'tools.expires.secs': 60,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
})
class UnCached(object):
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': 0})
def force(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
self._cp_config['tools.expires.force'] = True
self._cp_config['tools.expires.secs'] = 0
return 'being forceful'
@cherrypy.expose
def dynamic(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
cherrypy.response.headers['Cache-Control'] = 'private'
return 'D-d-d-dynamic!'
@cherrypy.expose
def cacheable(self):
cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
return "Hi, I'm cacheable."
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': 86400})
def specific(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return 'I am being specific'
class Foo(object):
pass
@cherrypy.expose
@cherrypy.config(**{'tools.expires.secs': Foo()})
def wrongtype(self):
cherrypy.response.headers[
'Etag'] = 'need_this_to_make_me_cacheable'
return 'Woops'
@cherrypy.config(**{
'tools.gzip.mime_types': ['text/*', 'image/*'],
'tools.caching.on': True,
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir
})
class GzipStaticCache(object):
pass
cherrypy.tree.mount(Root())
cherrypy.tree.mount(UnCached(), '/expires')
cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache')
cherrypy.config.update({'tools.gzip.on': True})
def testCaching(self):
elapsed = 0.0
for trial in range(10):
self.getPage('/')
# The response should be the same every time,
# except for the Age response header.
self.assertBody('visit #1')
if trial != 0:
age = int(self.assertHeader('Age'))
self.assert_(age >= elapsed)
elapsed = age
# POST, PUT, DELETE should not be cached.
self.getPage('/', method='POST')
self.assertBody('visit #2')
# Because gzip is turned on, the Vary header should always Vary for
# content-encoding
self.assertHeader('Vary', 'Accept-Encoding')
# The previous request should have invalidated the cache,
# so this request will recalc the response.
self.getPage('/', method='GET')
self.assertBody('visit #3')
# ...but this request should get the cached copy.
self.getPage('/', method='GET')
self.assertBody('visit #3')
self.getPage('/', method='DELETE')
self.assertBody('visit #4')
# The previous request should have invalidated the cache,
# so this request will recalc the response.
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertHeader('Vary')
self.assertEqual(
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
# Now check that a second request gets the gzip header and gzipped body
# This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
# response body was being gzipped a second time.
self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertEqual(
cherrypy.lib.encoding.decompress(self.body), b'visit #5')
# Now check that a third request that doesn't accept gzip
# skips the cache (because the 'Vary' header denies it).
self.getPage('/', method='GET')
self.assertNoHeader('Content-Encoding')
self.assertBody('visit #6')
def testVaryHeader(self):
self.getPage('/varying_headers/')
self.assertStatus('200 OK')
self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
self.assertBody('visit #1')
# Now check that different 'Vary'-fields don't evict each other.
# This test creates 2 requests with different 'Our-Varying-Header'
# and then tests if the first one still exists.
self.getPage('/varying_headers/',
headers=[('Our-Varying-Header', 'request 2')])
self.assertStatus('200 OK')
self.assertBody('visit #2')
self.getPage('/varying_headers/',
headers=[('Our-Varying-Header', 'request 2')])
self.assertStatus('200 OK')
self.assertBody('visit #2')
self.getPage('/varying_headers/')
self.assertStatus('200 OK')
self.assertBody('visit #1')
def testExpiresTool(self):
# test setting an expires header
self.getPage('/expires/specific')
self.assertStatus('200 OK')
self.assertHeader('Expires')
# test exceptions for bad time values
self.getPage('/expires/wrongtype')
self.assertStatus(500)
self.assertInBody('TypeError')
# static content should not have "cache prevention" headers
self.getPage('/expires/index.html')
self.assertStatus('200 OK')
self.assertNoHeader('Pragma')
self.assertNoHeader('Cache-Control')
self.assertHeader('Expires')
# dynamic content that sets indicators should not have
# "cache prevention" headers
self.getPage('/expires/cacheable')
self.assertStatus('200 OK')
self.assertNoHeader('Pragma')
self.assertNoHeader('Cache-Control')
self.assertHeader('Expires')
self.getPage('/expires/dynamic')
self.assertBody('D-d-d-dynamic!')
# the Cache-Control header should be untouched
self.assertHeader('Cache-Control', 'private')
self.assertHeader('Expires')
# configure the tool to ignore indicators and replace existing headers
self.getPage('/expires/force')
self.assertStatus('200 OK')
# This also gives us a chance to test 0 expiry with no other headers
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
# static content should now have "cache prevention" headers
self.getPage('/expires/index.html')
self.assertStatus('200 OK')
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
# the cacheable handler should now have "cache prevention" headers
self.getPage('/expires/cacheable')
self.assertStatus('200 OK')
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
self.getPage('/expires/dynamic')
self.assertBody('D-d-d-dynamic!')
# dynamic sets Cache-Control to private but it should be
# overwritten here ...
self.assertHeader('Pragma', 'no-cache')
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
def _assert_resp_len_and_enc_for_gzip(self, uri):
"""
Test that after querying gzipped content it's remains valid in
cache and available non-gzipped as well.
"""
ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')]
content_len = None
for _ in range(3):
self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS)
if content_len is not None:
# all requests should get the same length
self.assertHeader('Content-Length', content_len)
self.assertHeader('Content-Encoding', 'gzip')
content_len = dict(self.headers)['Content-Length']
# check that we can still get non-gzipped version
self.getPage(uri, method='GET')
self.assertNoHeader('Content-Encoding')
# non-gzipped version should have a different content length
self.assertNoHeaderItemValue('Content-Length', content_len)
def testGzipStaticCache(self):
"""Test that cache and gzip tools play well together when both enabled.
Ref GitHub issue #1190.
"""
GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}'
resource_files = ('index.html', 'dirback.jpg')
for f in resource_files:
uri = GZIP_STATIC_CACHE_TMPL.format(f)
self._assert_resp_len_and_enc_for_gzip(uri)
def testLastModified(self):
self.getPage('/a.gif')
self.assertStatus(200)
self.assertBody(gif_bytes)
lm1 = self.assertHeader('Last-Modified')
# this request should get the cached copy.
self.getPage('/a.gif')
self.assertStatus(200)
self.assertBody(gif_bytes)
self.assertHeader('Age')
lm2 = self.assertHeader('Last-Modified')
self.assertEqual(lm1, lm2)
# this request should match the cached copy, but raise 304.
self.getPage('/a.gif', [('If-Modified-Since', lm1)])
self.assertStatus(304)
self.assertNoHeader('Last-Modified')
if not getattr(cherrypy.server, 'using_apache', False):
self.assertHeader('Age')
@pytest.mark.xfail(reason='#1536')
def test_antistampede(self):
SECONDS = 4
slow_url = '/long_process?seconds={SECONDS}'.format(**locals())
# We MUST make an initial synchronous request in order to create the
# AntiStampedeCache object, and populate its selecting_headers,
# before the actual stampede.
self.getPage(slow_url)
self.assertBody('success!')
path = urllib.parse.quote(slow_url, safe='')
self.getPage('/clear_cache?path=' + path)
self.assertStatus(200)
start = datetime.datetime.now()
def run():
self.getPage(slow_url)
# The response should be the same every time
self.assertBody('success!')
ts = [threading.Thread(target=run) for i in range(100)]
for t in ts:
t.start()
for t in ts:
t.join()
finish = datetime.datetime.now()
# Allow for overhead, two seconds for slow hosts
allowance = SECONDS + 2
self.assertEqualDates(start, finish, seconds=allowance)
def test_cache_control(self):
self.getPage('/control')
self.assertBody('visit #1')
self.getPage('/control')
self.assertBody('visit #1')
self.getPage('/control', headers=[('Cache-Control', 'no-cache')])
self.assertBody('visit #2')
self.getPage('/control')
self.assertBody('visit #2')
self.getPage('/control', headers=[('Pragma', 'no-cache')])
self.assertBody('visit #3')
self.getPage('/control')
self.assertBody('visit #3')
time.sleep(1)
self.getPage('/control', headers=[('Cache-Control', 'max-age=0')])
self.assertBody('visit #4')
self.getPage('/control')
self.assertBody('visit #4')

View File

@@ -0,0 +1,34 @@
"""Test Python 2/3 compatibility module."""
from __future__ import unicode_literals
import unittest
import pytest
import six
from cherrypy import _cpcompat as compat
class StringTester(unittest.TestCase):
"""Tests for string conversion."""
@pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
def test_ntob_non_native(self):
"""ntob should raise an Exception on unicode.
(Python 2 only)
See #1132 for discussion.
"""
self.assertRaises(TypeError, compat.ntob, 'fight')
class EscapeTester(unittest.TestCase):
"""Class to test escape_html function from _cpcompat."""
def test_escape_quote(self):
"""test_escape_quote - Verify the output for &<>"' chars."""
self.assertEqual(
"""xx&amp;&lt;&gt;"aa'""",
compat.escape_html("""xx&<>"aa'"""),
)

View File

@@ -0,0 +1,303 @@
"""Tests for the CherryPy configuration system."""
import io
import os
import sys
import unittest
import six
import cherrypy
from cherrypy.test import helper
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
def StringIOFromNative(x):
return io.StringIO(six.text_type(x))
def setup_server():
@cherrypy.config(foo='this', bar='that')
class Root:
def __init__(self):
cherrypy.config.namespaces['db'] = self.db_namespace
def db_namespace(self, k, v):
if k == 'scheme':
self.db = v
@cherrypy.expose(alias=('global_', 'xyz'))
def index(self, key):
return cherrypy.request.config.get(key, 'None')
@cherrypy.expose
def repr(self, key):
return repr(cherrypy.request.config.get(key, None))
@cherrypy.expose
def dbscheme(self):
return self.db
@cherrypy.expose
@cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']})
def plain(self, x):
return x
favicon_ico = cherrypy.tools.staticfile.handler(
filename=os.path.join(localDir, '../favicon.ico'))
@cherrypy.config(foo='this2', baz='that2')
class Foo:
@cherrypy.expose
def index(self, key):
return cherrypy.request.config.get(key, 'None')
nex = index
@cherrypy.expose
@cherrypy.config(**{'response.headers.X-silly': 'sillyval'})
def silly(self):
return 'Hello world'
# Test the expose and config decorators
@cherrypy.config(foo='this3', **{'bax': 'this4'})
@cherrypy.expose
def bar(self, key):
return repr(cherrypy.request.config.get(key, None))
class Another:
@cherrypy.expose
def index(self, key):
return str(cherrypy.request.config.get(key, 'None'))
def raw_namespace(key, value):
if key == 'input.map':
handler = cherrypy.request.handler
def wrapper():
params = cherrypy.request.params
for name, coercer in list(value.items()):
try:
params[name] = coercer(params[name])
except KeyError:
pass
return handler()
cherrypy.request.handler = wrapper
elif key == 'output':
handler = cherrypy.request.handler
def wrapper():
# 'value' is a type (like int or str).
return value(handler())
cherrypy.request.handler = wrapper
@cherrypy.config(**{'raw.output': repr})
class Raw:
@cherrypy.expose
@cherrypy.config(**{'raw.input.map': {'num': int}})
def incr(self, num):
return num + 1
if not six.PY3:
thing3 = "thing3: unicode('test', errors='ignore')"
else:
thing3 = ''
ioconf = StringIOFromNative("""
[/]
neg: -1234
filename: os.path.join(sys.prefix, "hello.py")
thing1: cherrypy.lib.httputil.response_codes[404]
thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
%s
complex: 3+2j
mul: 6*3
ones: "11"
twos: "22"
stradd: %%(ones)s + %%(twos)s + "33"
[/favicon.ico]
tools.staticfile.filename = %r
""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
root = Root()
root.foo = Foo()
root.raw = Raw()
app = cherrypy.tree.mount(root, config=ioconf)
app.request_class.namespaces['raw'] = raw_namespace
cherrypy.tree.mount(Another(), '/another')
cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
'db.scheme': r'sqlite///memory',
})
# Client-side code #
class ConfigTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testConfig(self):
tests = [
('/', 'nex', 'None'),
('/', 'foo', 'this'),
('/', 'bar', 'that'),
('/xyz', 'foo', 'this'),
('/foo/', 'foo', 'this2'),
('/foo/', 'bar', 'that'),
('/foo/', 'bax', 'None'),
('/foo/bar', 'baz', "'that2'"),
('/foo/nex', 'baz', 'that2'),
# If 'foo' == 'this', then the mount point '/another' leaks into
# '/'.
('/another/', 'foo', 'None'),
]
for path, key, expected in tests:
self.getPage(path + '?key=' + key)
self.assertBody(expected)
expectedconf = {
# From CP defaults
'tools.log_headers.on': False,
'tools.log_tracebacks.on': True,
'request.show_tracebacks': True,
'log.screen': False,
'environment': 'test_suite',
'engine.autoreload.on': False,
# From global config
'luxuryyacht': 'throatwobblermangrove',
# From Root._cp_config
'bar': 'that',
# From Foo._cp_config
'baz': 'that2',
# From Foo.bar._cp_config
'foo': 'this3',
'bax': 'this4',
}
for key, expected in expectedconf.items():
self.getPage('/foo/bar?key=' + key)
self.assertBody(repr(expected))
def testUnrepr(self):
self.getPage('/repr?key=neg')
self.assertBody('-1234')
self.getPage('/repr?key=filename')
self.assertBody(repr(os.path.join(sys.prefix, 'hello.py')))
self.getPage('/repr?key=thing1')
self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
if not getattr(cherrypy.server, 'using_apache', False):
# The object ID's won't match up when using Apache, since the
# server and client are running in different processes.
self.getPage('/repr?key=thing2')
from cherrypy.tutorial import thing2
self.assertBody(repr(thing2))
if not six.PY3:
self.getPage('/repr?key=thing3')
self.assertBody(repr(six.text_type('test')))
self.getPage('/repr?key=complex')
self.assertBody('(3+2j)')
self.getPage('/repr?key=mul')
self.assertBody('18')
self.getPage('/repr?key=stradd')
self.assertBody(repr('112233'))
def testRespNamespaces(self):
self.getPage('/foo/silly')
self.assertHeader('X-silly', 'sillyval')
self.assertBody('Hello world')
def testCustomNamespaces(self):
self.getPage('/raw/incr?num=12')
self.assertBody('13')
self.getPage('/dbscheme')
self.assertBody(r'sqlite///memory')
def testHandlerToolConfigOverride(self):
# Assert that config overrides tool constructor args. Above, we set
# the favicon in the page handler to be '../favicon.ico',
# but then overrode it in config to be './static/dirback.jpg'.
self.getPage('/favicon.ico')
self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
'rb').read())
def test_request_body_namespace(self):
self.getPage('/plain', method='POST', headers=[
('Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', '13')],
body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')
self.assertBody('abc')
class VariableSubstitutionTests(unittest.TestCase):
setup_server = staticmethod(setup_server)
def test_config(self):
from textwrap import dedent
# variable substitution with [DEFAULT]
conf = dedent("""
[DEFAULT]
dir = "/some/dir"
my.dir = %(dir)s + "/sub"
[my]
my.dir = %(dir)s + "/my/dir"
my.dir2 = %(my.dir)s + '/dir2'
""")
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir')
self.assertEqual(cherrypy.config['my']
['my.dir2'], '/some/dir/my/dir/dir2')
class CallablesInConfigTest(unittest.TestCase):
setup_server = staticmethod(setup_server)
def test_call_with_literal_dict(self):
from textwrap import dedent
conf = dedent("""
[my]
value = dict(**{'foo': 'bar'})
""")
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
def test_call_with_kwargs(self):
from textwrap import dedent
conf = dedent("""
[my]
value = dict(foo="buzz", **cherrypy._test_dict)
""")
test_dict = {
'foo': 'bar',
'bar': 'foo',
'fizz': 'buzz'
}
cherrypy._test_dict = test_dict
fp = StringIOFromNative(conf)
cherrypy.config.update(fp)
test_dict['foo'] = 'buzz'
self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
self.assertEqual(cherrypy.config['my']['value'], test_dict)
del cherrypy._test_dict

View File

@@ -0,0 +1,126 @@
"""Tests for the CherryPy configuration system."""
import os
import cherrypy
from cherrypy.test import helper
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
# Client-side code #
class ServerConfigTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return cherrypy.request.wsgi_environ['SERVER_PORT']
@cherrypy.expose
def upload(self, file):
return 'Size: %s' % len(file.file.read())
@cherrypy.expose
@cherrypy.config(**{'request.body.maxbytes': 100})
def tinyupload(self):
return cherrypy.request.body.read()
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.socket_host': '0.0.0.0',
'server.socket_port': 9876,
'server.max_request_body_size': 200,
'server.max_request_header_size': 500,
'server.socket_timeout': 0.5,
# Test explicit server.instance
'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
'server.2.socket_port': 9877,
# Test non-numeric <servername>
# Also test default server.instance = builtin server
'server.yetanother.socket_port': 9878,
})
PORT = 9876
def testBasicConfig(self):
self.getPage('/')
self.assertBody(str(self.PORT))
def testAdditionalServers(self):
if self.scheme == 'https':
return self.skip('not available under ssl')
self.PORT = 9877
self.getPage('/')
self.assertBody(str(self.PORT))
self.PORT = 9878
self.getPage('/')
self.assertBody(str(self.PORT))
def testMaxRequestSizePerHandler(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences... ')
self.getPage('/tinyupload', method='POST',
headers=[('Content-Type', 'text/plain'),
('Content-Length', '100')],
body='x' * 100)
self.assertStatus(200)
self.assertBody('x' * 100)
self.getPage('/tinyupload', method='POST',
headers=[('Content-Type', 'text/plain'),
('Content-Length', '101')],
body='x' * 101)
self.assertStatus(413)
def testMaxRequestSize(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences... ')
for size in (500, 5000, 50000):
self.getPage('/', headers=[('From', 'x' * 500)])
self.assertStatus(413)
# Test for https://github.com/cherrypy/cherrypy/issues/421
# (Incorrect border condition in readline of SizeCheckWrapper).
# This hangs in rev 891 and earlier.
lines256 = 'x' * 248
self.getPage('/',
headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
('From', lines256)])
# Test upload
cd = (
'Content-Disposition: form-data; '
'name="file"; '
'filename="hello.txt"'
)
body = '\r\n'.join([
'--x',
cd,
'Content-Type: text/plain',
'',
'%s',
'--x--'])
partlen = 200 - len(body)
b = body % ('x' * partlen)
h = [('Content-type', 'multipart/form-data; boundary=x'),
('Content-Length', '%s' % len(b))]
self.getPage('/upload', h, 'POST', b)
self.assertBody('Size: %d' % partlen)
b = body % ('x' * 200)
h = [('Content-type', 'multipart/form-data; boundary=x'),
('Content-Length', '%s' % len(b))]
self.getPage('/upload', h, 'POST', b)
self.assertStatus(413)

View File

@@ -0,0 +1,873 @@
"""Tests for TCP connection handling, including proper and timely close."""
import errno
import socket
import sys
import time
import six
from six.moves import urllib
from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
import pytest
from cheroot.test import webtest
import cherrypy
from cherrypy._cpcompat import HTTPSConnection, ntob, tonative
from cherrypy.test import helper
timeout = 1
pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
def setup_server():
def raise500():
raise cherrypy.HTTPError(500)
class Root:
@cherrypy.expose
def index(self):
return pov
page1 = index
page2 = index
page3 = index
@cherrypy.expose
def hello(self):
return 'Hello, world!'
@cherrypy.expose
def timeout(self, t):
return str(cherrypy.server.httpserver.timeout)
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def stream(self, set_cl=False):
if set_cl:
cherrypy.response.headers['Content-Length'] = 10
def content():
for x in range(10):
yield str(x)
return content()
@cherrypy.expose
def error(self, code=500):
raise cherrypy.HTTPError(code)
@cherrypy.expose
def upload(self):
if not cherrypy.request.method == 'POST':
raise AssertionError("'POST' != request.method %r" %
cherrypy.request.method)
return "thanks for '%s'" % cherrypy.request.body.read()
@cherrypy.expose
def custom(self, response_code):
cherrypy.response.status = response_code
return 'Code = %s' % response_code
@cherrypy.expose
@cherrypy.config(**{'hooks.on_start_resource': raise500})
def err_before_read(self):
return 'ok'
@cherrypy.expose
def one_megabyte_of_a(self):
return ['a' * 1024] * 1024
@cherrypy.expose
# Turn off the encoding tool so it doens't collapse
# our response body and reclaculate the Content-Length.
@cherrypy.config(**{'tools.encode.on': False})
def custom_cl(self, body, cl):
cherrypy.response.headers['Content-Length'] = cl
if not isinstance(body, list):
body = [body]
newbody = []
for chunk in body:
if isinstance(chunk, six.text_type):
chunk = chunk.encode('ISO-8859-1')
newbody.append(chunk)
return newbody
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.max_request_body_size': 1001,
'server.socket_timeout': timeout,
})
class ConnectionCloseTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_HTTP11(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make another request on the same connection.
self.getPage('/page1')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Test client-side close.
self.getPage('/page2', headers=[('Connection', 'close')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'close')
# Make another request on the same connection, which should error.
self.assertRaises(NotConnected, self.getPage, '/')
def test_Streaming_no_len(self):
try:
self._streaming(set_cl=False)
finally:
try:
self.HTTP_CONN.close()
except (TypeError, AttributeError):
pass
def test_Streaming_with_len(self):
try:
self._streaming(set_cl=True)
finally:
try:
self.HTTP_CONN.close()
except (TypeError, AttributeError):
pass
def _streaming(self, set_cl):
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make another, streamed request on the same connection.
if set_cl:
# When a Content-Length is provided, the content should stream
# without closing the connection.
self.getPage('/stream?set_cl=Yes')
self.assertHeader('Content-Length')
self.assertNoHeader('Connection', 'close')
self.assertNoHeader('Transfer-Encoding')
self.assertStatus('200 OK')
self.assertBody('0123456789')
else:
# When no Content-Length response header is provided,
# streamed output will either close the connection, or use
# chunked encoding, to determine transfer-length.
self.getPage('/stream')
self.assertNoHeader('Content-Length')
self.assertStatus('200 OK')
self.assertBody('0123456789')
chunked_response = False
for k, v in self.headers:
if k.lower() == 'transfer-encoding':
if str(v) == 'chunked':
chunked_response = True
if chunked_response:
self.assertNoHeader('Connection', 'close')
else:
self.assertHeader('Connection', 'close')
# Make another request on the same connection, which should
# error.
self.assertRaises(NotConnected, self.getPage, '/')
# Try HEAD. See
# https://github.com/cherrypy/cherrypy/issues/864.
self.getPage('/stream', method='HEAD')
self.assertStatus('200 OK')
self.assertBody('')
self.assertNoHeader('Transfer-Encoding')
else:
self.PROTOCOL = 'HTTP/1.0'
self.persistent = True
# Make the first request and assert Keep-Alive.
self.getPage('/', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'Keep-Alive')
# Make another, streamed request on the same connection.
if set_cl:
# When a Content-Length is provided, the content should
# stream without closing the connection.
self.getPage('/stream?set_cl=Yes',
headers=[('Connection', 'Keep-Alive')])
self.assertHeader('Content-Length')
self.assertHeader('Connection', 'Keep-Alive')
self.assertNoHeader('Transfer-Encoding')
self.assertStatus('200 OK')
self.assertBody('0123456789')
else:
# When a Content-Length is not provided,
# the server should close the connection.
self.getPage('/stream', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody('0123456789')
self.assertNoHeader('Content-Length')
self.assertNoHeader('Connection', 'Keep-Alive')
self.assertNoHeader('Transfer-Encoding')
# Make another request on the same connection, which should
# error.
self.assertRaises(NotConnected, self.getPage, '/')
def test_HTTP10_KeepAlive(self):
self.PROTOCOL = 'HTTP/1.0'
if self.scheme == 'https':
self.HTTP_CONN = HTTPSConnection
else:
self.HTTP_CONN = HTTPConnection
# Test a normal HTTP/1.0 request.
self.getPage('/page2')
self.assertStatus('200 OK')
self.assertBody(pov)
# Apache, for example, may emit a Connection header even for HTTP/1.0
# self.assertNoHeader("Connection")
# Test a keep-alive HTTP/1.0 request.
self.persistent = True
self.getPage('/page3', headers=[('Connection', 'Keep-Alive')])
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertHeader('Connection', 'Keep-Alive')
# Remove the keep-alive header again.
self.getPage('/page3')
self.assertStatus('200 OK')
self.assertBody(pov)
# Apache, for example, may emit a Connection header even for HTTP/1.0
# self.assertNoHeader("Connection")
class PipelineTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_HTTP11_Timeout(self):
# If we timeout without sending any data,
# the server will close the conn with a 408.
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Connect but send nothing.
self.persistent = True
conn = self.HTTP_CONN
conn.auto_open = False
conn.connect()
# Wait for our socket timeout
time.sleep(timeout * 2)
# The request should have returned 408 already.
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 408)
conn.close()
# Connect but send half the headers only.
self.persistent = True
conn = self.HTTP_CONN
conn.auto_open = False
conn.connect()
conn.send(b'GET /hello HTTP/1.1')
conn.send(('Host: %s' % self.HOST).encode('ascii'))
# Wait for our socket timeout
time.sleep(timeout * 2)
# The conn should have already sent 408.
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 408)
conn.close()
def test_HTTP11_Timeout_after_request(self):
# If we timeout after at least one request has succeeded,
# the server will close the conn without 408.
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Make an initial request
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(str(timeout))
# Make a second request on the same socket
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody('Hello, world!')
# Wait for our socket timeout
time.sleep(timeout * 2)
# Make another request on the same socket, which should error
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
except Exception:
if not isinstance(sys.exc_info()[1],
(socket.error, BadStatusLine)):
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' % sys.exc_info()[1])
else:
if response.status != 408:
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' %
response.read())
conn.close()
# Make another request on a new socket, which should work
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(pov)
# Make another request on the same socket,
# but timeout on the headers
conn.send(b'GET /hello HTTP/1.1')
# Wait for our socket timeout
time.sleep(timeout * 2)
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
except Exception:
if not isinstance(sys.exc_info()[1],
(socket.error, BadStatusLine)):
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' % sys.exc_info()[1])
else:
self.fail("Writing to timed out socket didn't fail"
' as it should have: %s' %
response.read())
conn.close()
# Retry the request on a new connection, which should work
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(pov)
conn.close()
def test_HTTP11_pipelining(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Test pipelining. httplib doesn't support this directly.
self.persistent = True
conn = self.HTTP_CONN
# Put request 1
conn.putrequest('GET', '/hello', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
for trial in range(5):
# Put next request
conn._output(b'GET /hello HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._send_output()
# Retrieve previous response
response = conn.response_class(conn.sock, method='GET')
# there is a bug in python3 regarding the buffering of
# ``conn.sock``. Until that bug get's fixed we will
# monkey patch the ``response`` instance.
# https://bugs.python.org/issue23377
if six.PY3:
response.fp = conn.sock.makefile('rb', 0)
response.begin()
body = response.read(13)
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello, world!')
# Retrieve final response
response = conn.response_class(conn.sock, method='GET')
response.begin()
body = response.read()
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello, world!')
conn.close()
def test_100_Continue(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
self.persistent = True
conn = self.HTTP_CONN
# Try a page without an Expect request header first.
# Note that httplib's response.begin automatically ignores
# 100 Continue responses, so we must manually check for it.
try:
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '4')
conn.endheaders()
conn.send(ntob("d'oh"))
response = conn.response_class(conn.sock, method='POST')
version, status, reason = response._read_status()
self.assertNotEqual(status, 100)
finally:
conn.close()
# Now try a page with an Expect header...
try:
conn.connect()
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '17')
conn.putheader('Expect', '100-continue')
conn.endheaders()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
line = response.fp.readline().strip()
if line:
self.fail(
'100 Continue should not output any headers. Got %r' %
line)
else:
break
# ...send the body
body = b'I am a small file'
conn.send(body)
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody("thanks for '%s'" % body)
finally:
conn.close()
class ConnectionTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_readall_or_close(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
if self.scheme == 'https':
self.HTTP_CONN = HTTPSConnection
else:
self.HTTP_CONN = HTTPConnection
# Test a max of 0 (the default) and then reset to what it was above.
old_max = cherrypy.server.max_request_body_size
for new_max in (0, old_max):
cherrypy.server.max_request_body_size = new_max
self.persistent = True
conn = self.HTTP_CONN
# Get a POST page with an error
conn.putrequest('POST', '/err_before_read', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '1000')
conn.putheader('Expect', '100-continue')
conn.endheaders()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
skip = response.fp.readline().strip()
if not skip:
break
# ...send the body
conn.send(ntob('x' * 1000))
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(500)
# Now try a working page with an Expect header...
conn._output(b'POST /upload HTTP/1.1')
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
conn._output(b'Content-Type: text/plain')
conn._output(b'Content-Length: 17')
conn._output(b'Expect: 100-continue')
conn._send_output()
response = conn.response_class(conn.sock, method='POST')
# ...assert and then skip the 100 response
version, status, reason = response._read_status()
self.assertEqual(status, 100)
while True:
skip = response.fp.readline().strip()
if not skip:
break
# ...send the body
body = b'I am a small file'
conn.send(body)
# ...get the final response
response.begin()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody("thanks for '%s'" % body)
conn.close()
def test_No_Message_Body(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Set our HTTP_CONN to an instance so it persists between requests.
self.persistent = True
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody(pov)
self.assertNoHeader('Connection')
# Make a 204 request on the same connection.
self.getPage('/custom/204')
self.assertStatus(204)
self.assertNoHeader('Content-Length')
self.assertBody('')
self.assertNoHeader('Connection')
# Make a 304 request on the same connection.
self.getPage('/custom/304')
self.assertStatus(304)
self.assertNoHeader('Content-Length')
self.assertBody('')
self.assertNoHeader('Connection')
def test_Chunked_Encoding(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
if (hasattr(self, 'harness') and
'modpython' in self.harness.__class__.__name__.lower()):
# mod_python forbids chunked encoding
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Set our HTTP_CONN to an instance so it persists between requests.
self.persistent = True
conn = self.HTTP_CONN
# Try a normal chunked request (with extensions)
body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
'Content-Type: application/json\r\n'
'\r\n')
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Transfer-Encoding', 'chunked')
conn.putheader('Trailer', 'Content-Type')
# Note that this is somewhat malformed:
# we shouldn't be sending Content-Length.
# RFC 2616 says the server should ignore it.
conn.putheader('Content-Length', '3')
conn.endheaders()
conn.send(body)
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus('200 OK')
self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy')
# Try a chunked request that exceeds server.max_request_body_size.
# Note that the delimiters and trailer are included.
body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n')
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Transfer-Encoding', 'chunked')
conn.putheader('Content-Type', 'text/plain')
# Chunked requests don't need a content-length
# # conn.putheader("Content-Length", len(body))
conn.endheaders()
conn.send(body)
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(413)
conn.close()
def test_Content_Length_in(self):
# Try a non-chunked request where Content-Length exceeds
# server.max_request_body_size. Assert error before body send.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '9999')
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(413)
self.assertBody('The entity sent with the request exceeds '
'the maximum allowed bytes.')
conn.close()
def test_Content_Length_out_preheaders(self):
# Try a non-chunked response where Content-Length is less than
# the actual bytes in the response body.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5',
skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(500)
self.assertBody(
'The requested resource returned more bytes than the '
'declared Content-Length.')
conn.close()
def test_Content_Length_out_postheaders(self):
# Try a non-chunked response where Content-Length is less than
# the actual bytes in the response body.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest(
'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5',
skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.getresponse()
self.status, self.headers, self.body = webtest.shb(response)
self.assertStatus(200)
self.assertBody('I too')
conn.close()
def test_598(self):
tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/'
url = tmpl.format(
scheme=self.scheme,
host=self.HOST,
port=self.PORT,
)
remote_data_conn = urllib.request.urlopen(url)
buf = remote_data_conn.read(512)
time.sleep(timeout * 0.6)
remaining = (1024 * 1024) - 512
while remaining:
data = remote_data_conn.read(remaining)
if not data:
break
else:
buf += data
remaining -= len(data)
self.assertEqual(len(buf), 1024 * 1024)
self.assertEqual(buf, ntob('a' * 1024 * 1024))
self.assertEqual(remaining, 0)
remote_data_conn.close()
def setup_upload_server():
class Root:
@cherrypy.expose
def upload(self):
if not cherrypy.request.method == 'POST':
raise AssertionError("'POST' != request.method %r" %
cherrypy.request.method)
return "thanks for '%s'" % tonative(cherrypy.request.body.read())
cherrypy.tree.mount(Root())
cherrypy.config.update({
'server.max_request_body_size': 1001,
'server.socket_timeout': 10,
'server.accepted_queue_size': 5,
'server.accepted_queue_timeout': 0.1,
})
reset_names = 'ECONNRESET', 'WSAECONNRESET'
socket_reset_errors = [
getattr(errno, name)
for name in reset_names
if hasattr(errno, name)
]
'reset error numbers available on this platform'
socket_reset_errors += [
# Python 3.5 raises an http.client.RemoteDisconnected
# with this message
'Remote end closed connection without response',
]
class LimitedRequestQueueTests(helper.CPWebCase):
setup_server = staticmethod(setup_upload_server)
@pytest.mark.xfail(reason='#1535')
def test_queue_full(self):
conns = []
overflow_conn = None
try:
# Make 15 initial requests and leave them open, which should use
# all of wsgiserver's WorkerThreads and fill its Queue.
for i in range(15):
conn = self.HTTP_CONN(self.HOST, self.PORT)
conn.putrequest('POST', '/upload', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Type', 'text/plain')
conn.putheader('Content-Length', '4')
conn.endheaders()
conns.append(conn)
# Now try a 16th conn, which should be closed by the
# server immediately.
overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
# Manually connect since httplib won't let us set a timeout
for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
overflow_conn.sock = socket.socket(af, socktype, proto)
overflow_conn.sock.settimeout(5)
overflow_conn.sock.connect(sa)
break
overflow_conn.putrequest('GET', '/', skip_host=True)
overflow_conn.putheader('Host', self.HOST)
overflow_conn.endheaders()
response = overflow_conn.response_class(
overflow_conn.sock,
method='GET',
)
try:
response.begin()
except socket.error as exc:
if exc.args[0] in socket_reset_errors:
pass # Expected.
else:
tmpl = (
'Overflow conn did not get RST. '
'Got {exc.args!r} instead'
)
raise AssertionError(tmpl.format(**locals()))
except BadStatusLine:
# This is a special case in OS X. Linux and Windows will
# RST correctly.
assert sys.platform == 'darwin'
else:
raise AssertionError('Overflow conn did not get RST ')
finally:
for conn in conns:
conn.send(b'done')
response = conn.response_class(conn.sock, method='POST')
response.begin()
self.body = response.read()
self.assertBody("thanks for 'done'")
self.assertEqual(response.status, 200)
conn.close()
if overflow_conn:
overflow_conn.close()
class BadRequestTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_No_CRLF(self):
self.persistent = True
conn = self.HTTP_CONN
conn.send(b'GET /hello HTTP/1.1\n\n')
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.body = response.read()
self.assertBody('HTTP requires CRLF terminators')
conn.close()
conn.connect()
conn.send(b'GET /hello HTTP/1.1\r\n\n')
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.body = response.read()
self.assertBody('HTTP requires CRLF terminators')
conn.close()

View File

@@ -0,0 +1,823 @@
# coding: utf-8
"""Basic tests for the CherryPy core: request handling."""
import os
import sys
import types
import six
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy import _cptools, tools
from cherrypy.lib import httputil, static
from cherrypy.test._test_decorators import ExposeExamples
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico')
# Client-side code #
class CoreRequestHandlingTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
favicon_ico = tools.staticfile.handler(filename=favicon_path)
@cherrypy.expose
def defct(self, newct):
newct = 'text/%s' % newct
cherrypy.config.update({'tools.response_headers.on': True,
'tools.response_headers.headers':
[('Content-Type', newct)]})
@cherrypy.expose
def baseurl(self, path_info, relative=None):
return cherrypy.url(path_info, relative=bool(relative))
root = Root()
root.expose_dec = ExposeExamples()
class TestType(type):
"""Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute
of root.
"""
def __init__(cls, name, bases, dct):
type.__init__(cls, name, bases, dct)
for value in six.itervalues(dct):
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object, ), {})
@cherrypy.config(**{'tools.trailing_slash.on': False})
class URL(Test):
def index(self, path_info, relative=None):
if relative != 'server':
relative = bool(relative)
return cherrypy.url(path_info, relative=relative)
def leaf(self, path_info, relative=None):
if relative != 'server':
relative = bool(relative)
return cherrypy.url(path_info, relative=relative)
def qs(self, qs):
return cherrypy.url(qs=qs)
def log_status():
Status.statuses.append(cherrypy.response.status)
cherrypy.tools.log_status = cherrypy.Tool(
'on_end_resource', log_status)
class Status(Test):
def index(self):
return 'normal'
def blank(self):
cherrypy.response.status = ''
# According to RFC 2616, new status codes are OK as long as they
# are between 100 and 599.
# Here is an illegal code...
def illegal(self):
cherrypy.response.status = 781
return 'oops'
# ...and here is an unknown but legal code.
def unknown(self):
cherrypy.response.status = '431 My custom error'
return 'funky'
# Non-numeric code
def bad(self):
cherrypy.response.status = 'error'
return 'bad news'
statuses = []
@cherrypy.config(**{'tools.log_status.on': True})
def on_end_resource_stage(self):
return repr(self.statuses)
class Redirect(Test):
@cherrypy.config(**{
'tools.err_redirect.on': True,
'tools.err_redirect.url': '/errpage',
'tools.err_redirect.internal': False,
})
class Error:
@cherrypy.expose
def index(self):
raise NameError('redirect_test')
error = Error()
def index(self):
return 'child'
def custom(self, url, code):
raise cherrypy.HTTPRedirect(url, code)
@cherrypy.config(**{'tools.trailing_slash.extra': True})
def by_code(self, code):
raise cherrypy.HTTPRedirect('somewhere%20else', code)
def nomodify(self):
raise cherrypy.HTTPRedirect('', 304)
def proxy(self):
raise cherrypy.HTTPRedirect('proxy', 305)
def stringify(self):
return str(cherrypy.HTTPRedirect('/'))
def fragment(self, frag):
raise cherrypy.HTTPRedirect('/some/url#%s' % frag)
def url_with_quote(self):
raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
def url_with_xss(self):
raise cherrypy.HTTPRedirect(
"/some<script>alert(1);</script>url/that'we/want")
def url_with_unicode(self):
raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8'))
def login_redir():
if not getattr(cherrypy.request, 'login', None):
raise cherrypy.InternalRedirect('/internalredirect/login')
tools.login_redir = _cptools.Tool('before_handler', login_redir)
def redir_custom():
raise cherrypy.InternalRedirect('/internalredirect/custom_err')
class InternalRedirect(Test):
def index(self):
raise cherrypy.InternalRedirect('/')
@cherrypy.expose
@cherrypy.config(**{'hooks.before_error_response': redir_custom})
def choke(self):
return 3 / 0
def relative(self, a, b):
raise cherrypy.InternalRedirect('cousin?t=6')
def cousin(self, t):
assert cherrypy.request.prev.closed
return cherrypy.request.prev.query_string
def petshop(self, user_id):
if user_id == 'parrot':
# Trade it for a slug when redirecting
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=slug')
elif user_id == 'terrier':
# Trade it for a fish when redirecting
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=fish')
else:
# This should pass the user_id through to getImagesByUser
raise cherrypy.InternalRedirect(
'/image/getImagesByUser?user_id=%s' % str(user_id))
# We support Python 2.3, but the @-deco syntax would look like
# this:
# @tools.login_redir()
def secure(self):
return 'Welcome!'
secure = tools.login_redir()(secure)
# Since calling the tool returns the same function you pass in,
# you could skip binding the return value, and just write:
# tools.login_redir()(secure)
def login(self):
return 'Please log in'
def custom_err(self):
return 'Something went horribly wrong.'
@cherrypy.config(**{'hooks.before_request_body': redir_custom})
def early_ir(self, arg):
return 'whatever'
class Image(Test):
def getImagesByUser(self, user_id):
return '0 images for %s' % user_id
class Flatten(Test):
def as_string(self):
return 'content'
def as_list(self):
return ['con', 'tent']
def as_yield(self):
yield b'content'
@cherrypy.config(**{'tools.flatten.on': True})
def as_dblyield(self):
yield self.as_yield()
def as_refyield(self):
for chunk in self.as_yield():
yield chunk
class Ranges(Test):
def get_ranges(self, bytes):
return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
def slice_file(self):
path = os.path.join(os.getcwd(), os.path.dirname(__file__))
return static.serve_file(
os.path.join(path, 'static/index.html'))
class Cookies(Test):
def single(self, name):
cookie = cherrypy.request.cookie[name]
# Python2's SimpleCookie.__setitem__ won't take unicode keys.
cherrypy.response.cookie[str(name)] = cookie.value
def multiple(self, names):
list(map(self.single, names))
def append_headers(header_list, debug=False):
if debug:
cherrypy.log(
'Extending response headers with %s' % repr(header_list),
'TOOLS.APPEND_HEADERS')
cherrypy.serving.response.header_list.extend(header_list)
cherrypy.tools.append_headers = cherrypy.Tool(
'on_end_resource', append_headers)
class MultiHeader(Test):
def header_list(self):
pass
header_list = cherrypy.tools.append_headers(header_list=[
(b'WWW-Authenticate', b'Negotiate'),
(b'WWW-Authenticate', b'Basic realm="foo"'),
])(header_list)
def commas(self):
cherrypy.response.headers[
'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
cherrypy.tree.mount(root)
def testStatus(self):
self.getPage('/status/')
self.assertBody('normal')
self.assertStatus(200)
self.getPage('/status/blank')
self.assertBody('')
self.assertStatus(200)
self.getPage('/status/illegal')
self.assertStatus(500)
msg = 'Illegal response status from server (781 is out of range).'
self.assertErrorPage(500, msg)
if not getattr(cherrypy.server, 'using_apache', False):
self.getPage('/status/unknown')
self.assertBody('funky')
self.assertStatus(431)
self.getPage('/status/bad')
self.assertStatus(500)
msg = "Illegal response status from server ('error' is non-numeric)."
self.assertErrorPage(500, msg)
def test_on_end_resource_status(self):
self.getPage('/status/on_end_resource_stage')
self.assertBody('[]')
self.getPage('/status/on_end_resource_stage')
self.assertBody(repr(['200 OK']))
def testSlashes(self):
# Test that requests for index methods without a trailing slash
# get redirected to the same URI path with a trailing slash.
# Make sure GET params are preserved.
self.getPage('/redirect?id=3')
self.assertStatus(301)
self.assertMatchesBody(
'<a href=([\'"])%s/redirect/[?]id=3\\1>'
'%s/redirect/[?]id=3</a>' % (self.base(), self.base())
)
if self.prefix():
# Corner case: the "trailing slash" redirect could be tricky if
# we're using a virtual root and the URI is "/vroot" (no slash).
self.getPage('')
self.assertStatus(301)
self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
(self.base(), self.base()))
# Test that requests for NON-index methods WITH a trailing slash
# get redirected to the same URI path WITHOUT a trailing slash.
# Make sure GET params are preserved.
self.getPage('/redirect/by_code/?code=307')
self.assertStatus(301)
self.assertMatchesBody(
"<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
'%s/redirect/by_code[?]code=307</a>'
% (self.base(), self.base())
)
# If the trailing_slash tool is off, CP should just continue
# as if the slashes were correct. But it needs some help
# inside cherrypy.url to form correct output.
self.getPage('/url?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf/?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
def testRedirect(self):
self.getPage('/redirect/')
self.assertBody('child')
self.assertStatus(200)
self.getPage('/redirect/by_code?code=300')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(300)
self.getPage('/redirect/by_code?code=301')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(301)
self.getPage('/redirect/by_code?code=302')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(302)
self.getPage('/redirect/by_code?code=303')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(303)
self.getPage('/redirect/by_code?code=307')
self.assertMatchesBody(
r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
self.assertStatus(307)
self.getPage('/redirect/nomodify')
self.assertBody('')
self.assertStatus(304)
self.getPage('/redirect/proxy')
self.assertBody('')
self.assertStatus(305)
# HTTPRedirect on error
self.getPage('/redirect/error/')
self.assertStatus(('302 Found', '303 See Other'))
self.assertInBody('/errpage')
# Make sure str(HTTPRedirect()) works.
self.getPage('/redirect/stringify', protocol='HTTP/1.0')
self.assertStatus(200)
self.assertBody("(['%s/'], 302)" % self.base())
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.getPage('/redirect/stringify', protocol='HTTP/1.1')
self.assertStatus(200)
self.assertBody("(['%s/'], 303)" % self.base())
# check that #fragments are handled properly
# http://skrb.org/ietf/http_errata.html#location-fragments
frag = 'foo'
self.getPage('/redirect/fragment/%s' % frag)
self.assertMatchesBody(
r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
frag, frag))
loc = self.assertHeader('Location')
assert loc.endswith('#%s' % frag)
self.assertStatus(('302 Found', '303 See Other'))
# check injection protection
# See https://github.com/cherrypy/cherrypy/issues/1003
self.getPage(
'/redirect/custom?'
'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval')
self.assertStatus(303)
loc = self.assertHeader('Location')
assert 'Set-Cookie' in loc
self.assertNoHeader('Set-Cookie')
def assertValidXHTML():
from xml.etree import ElementTree
try:
ElementTree.fromstring(
'<html><body>%s</body></html>' % self.body,
)
except ElementTree.ParseError:
self._handlewebError(
'automatically generated redirect did not '
'generate well-formed html',
)
# check redirects to URLs generated valid HTML - we check this
# by seeing if it appears as valid XHTML.
self.getPage('/redirect/by_code?code=303')
self.assertStatus(303)
assertValidXHTML()
# do the same with a url containing quote characters.
self.getPage('/redirect/url_with_quote')
self.assertStatus(303)
assertValidXHTML()
def test_redirect_with_xss(self):
"""A redirect to a URL with HTML injected should result
in page contents escaped."""
self.getPage('/redirect/url_with_xss')
self.assertStatus(303)
assert b'<script>' not in self.body
assert b'&lt;script&gt;' in self.body
def test_redirect_with_unicode(self):
"""
A redirect to a URL with Unicode should return a Location
header containing that Unicode URL.
"""
# test disabled due to #1440
return
self.getPage('/redirect/url_with_unicode')
self.assertStatus(303)
loc = self.assertHeader('Location')
assert ntou('тест', encoding='utf-8') in loc
def test_InternalRedirect(self):
# InternalRedirect
self.getPage('/internalredirect/')
self.assertBody('hello')
self.assertStatus(200)
# Test passthrough
self.getPage(
'/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film')
self.assertBody('0 images for Sir-not-appearing-in-this-film')
self.assertStatus(200)
# Test args
self.getPage('/internalredirect/petshop?user_id=parrot')
self.assertBody('0 images for slug')
self.assertStatus(200)
# Test POST
self.getPage('/internalredirect/petshop', method='POST',
body='user_id=terrier')
self.assertBody('0 images for fish')
self.assertStatus(200)
# Test ir before body read
self.getPage('/internalredirect/early_ir', method='POST',
body='arg=aha!')
self.assertBody('Something went horribly wrong.')
self.assertStatus(200)
self.getPage('/internalredirect/secure')
self.assertBody('Please log in')
self.assertStatus(200)
# Relative path in InternalRedirect.
# Also tests request.prev.
self.getPage('/internalredirect/relative?a=3&b=5')
self.assertBody('a=3&b=5')
self.assertStatus(200)
# InternalRedirect on error
self.getPage('/internalredirect/choke')
self.assertStatus(200)
self.assertBody('Something went horribly wrong.')
def testFlatten(self):
for url in ['/flatten/as_string', '/flatten/as_list',
'/flatten/as_yield', '/flatten/as_dblyield',
'/flatten/as_refyield']:
self.getPage(url)
self.assertBody('content')
def testRanges(self):
self.getPage('/ranges/get_ranges?bytes=3-6')
self.assertBody('[(3, 7)]')
# Test multiple ranges and a suffix-byte-range-spec, for good measure.
self.getPage('/ranges/get_ranges?bytes=2-4,-1')
self.assertBody('[(2, 5), (7, 8)]')
# Test a suffix-byte-range longer than the content
# length. Note that in this test, the content length
# is 8 bytes.
self.getPage('/ranges/get_ranges?bytes=-100')
self.assertBody('[(0, 8)]')
# Get a partial file.
if cherrypy.server.protocol_version == 'HTTP/1.1':
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
self.assertStatus(206)
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertHeader('Content-Range', 'bytes 2-5/14')
self.assertBody('llo,')
# What happens with overlapping ranges (and out of order, too)?
self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')])
self.assertStatus(206)
ct = self.assertHeader('Content-Type')
expected_type = 'multipart/byteranges; boundary='
self.assert_(ct.startswith(expected_type))
boundary = ct[len(expected_type):]
expected_body = ('\r\n--%s\r\n'
'Content-type: text/html\r\n'
'Content-range: bytes 4-6/14\r\n'
'\r\n'
'o, \r\n'
'--%s\r\n'
'Content-type: text/html\r\n'
'Content-range: bytes 2-5/14\r\n'
'\r\n'
'llo,\r\n'
'--%s--\r\n' % (boundary, boundary, boundary))
self.assertBody(expected_body)
self.assertHeader('Content-Length')
# Test "416 Requested Range Not Satisfiable"
self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')])
self.assertStatus(416)
# "When this status code is returned for a byte-range request,
# the response SHOULD include a Content-Range entity-header
# field specifying the current length of the selected resource"
self.assertHeader('Content-Range', 'bytes */14')
elif cherrypy.server.protocol_version == 'HTTP/1.0':
# Test Range behavior with HTTP/1.0 request
self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
self.assertStatus(200)
self.assertBody('Hello, world\r\n')
def testFavicon(self):
# favicon.ico is served by staticfile.
icofilename = os.path.join(localDir, '../favicon.ico')
icofile = open(icofilename, 'rb')
data = icofile.read()
icofile.close()
self.getPage('/favicon.ico')
self.assertBody(data)
def skip_if_bad_cookies(self):
"""
cookies module fails to reject invalid cookies
https://github.com/cherrypy/cherrypy/issues/1405
"""
cookies = sys.modules.get('http.cookies')
_is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
if not _is_legal_key(','):
return
issue = 'http://bugs.python.org/issue26302'
tmpl = 'Broken cookies module ({issue})'
self.skip(tmpl.format(**locals()))
def testCookies(self):
self.skip_if_bad_cookies()
self.getPage('/cookies/single?name=First',
[('Cookie', 'First=Dinsdale;')])
self.assertHeader('Set-Cookie', 'First=Dinsdale')
self.getPage('/cookies/multiple?names=First&names=Last',
[('Cookie', 'First=Dinsdale; Last=Piranha;'),
])
self.assertHeader('Set-Cookie', 'First=Dinsdale')
self.assertHeader('Set-Cookie', 'Last=Piranha')
self.getPage('/cookies/single?name=Something-With%2CComma',
[('Cookie', 'Something-With,Comma=some-value')])
self.assertStatus(400)
def testDefaultContentType(self):
self.getPage('/')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.getPage('/defct/plain')
self.getPage('/')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
self.getPage('/defct/html')
def test_multiple_headers(self):
self.getPage('/multiheader/header_list')
self.assertEqual(
[(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
[('WWW-Authenticate', 'Negotiate'),
('WWW-Authenticate', 'Basic realm="foo"'),
])
self.getPage('/multiheader/commas')
self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
def test_cherrypy_url(self):
# Input relative to current
self.getPage('/url/leaf?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/?path_info=page1')
self.assertBody('%s/url/page1' % self.base())
# Other host header
host = 'www.mydomain.example'
self.getPage('/url/leaf?path_info=page1',
headers=[('Host', host)])
self.assertBody('%s://%s/url/page1' % (self.scheme, host))
# Input is 'absolute'; that is, relative to script_name
self.getPage('/url/leaf?path_info=/page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/?path_info=/page1')
self.assertBody('%s/page1' % self.base())
# Single dots
self.getPage('/url/leaf?path_info=./page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf?path_info=other/./page1')
self.assertBody('%s/url/other/page1' % self.base())
self.getPage('/url/?path_info=/other/./page1')
self.assertBody('%s/other/page1' % self.base())
self.getPage('/url/?path_info=/other/././././page1')
self.assertBody('%s/other/page1' % self.base())
# Double dots
self.getPage('/url/leaf?path_info=../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=other/../page1')
self.assertBody('%s/url/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../../../page1')
self.assertBody('%s/page1' % self.base())
self.getPage('/url/leaf?path_info=/other/../../../../../page1')
self.assertBody('%s/page1' % self.base())
# qs param is not normalized as a path
self.getPage('/url/qs?qs=/other')
self.assertBody('%s/url/qs?/other' % self.base())
self.getPage('/url/qs?qs=/other/../page1')
self.assertBody('%s/url/qs?/other/../page1' % self.base())
self.getPage('/url/qs?qs=../page1')
self.assertBody('%s/url/qs?../page1' % self.base())
self.getPage('/url/qs?qs=../../page1')
self.assertBody('%s/url/qs?../../page1' % self.base())
# Output relative to current path or script_name
self.getPage('/url/?path_info=page1&relative=True')
self.assertBody('page1')
self.getPage('/url/leaf?path_info=/page1&relative=True')
self.assertBody('../page1')
self.getPage('/url/leaf?path_info=page1&relative=True')
self.assertBody('page1')
self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
self.assertBody('leaf/page1')
self.getPage('/url/leaf?path_info=../page1&relative=True')
self.assertBody('../page1')
self.getPage('/url/?path_info=other/../page1&relative=True')
self.assertBody('page1')
# Output relative to /
self.getPage('/baseurl?path_info=ab&relative=True')
self.assertBody('ab')
# Output relative to /
self.getPage('/baseurl?path_info=/ab&relative=True')
self.assertBody('ab')
# absolute-path references ("server-relative")
# Input relative to current
self.getPage('/url/leaf?path_info=page1&relative=server')
self.assertBody('/url/page1')
self.getPage('/url/?path_info=page1&relative=server')
self.assertBody('/url/page1')
# Input is 'absolute'; that is, relative to script_name
self.getPage('/url/leaf?path_info=/page1&relative=server')
self.assertBody('/page1')
self.getPage('/url/?path_info=/page1&relative=server')
self.assertBody('/page1')
def test_expose_decorator(self):
# Test @expose
self.getPage('/expose_dec/no_call')
self.assertStatus(200)
self.assertBody('Mr E. R. Bradshaw')
# Test @expose()
self.getPage('/expose_dec/call_empty')
self.assertStatus(200)
self.assertBody('Mrs. B.J. Smegma')
# Test @expose("alias")
self.getPage('/expose_dec/call_alias')
self.assertStatus(200)
self.assertBody('Mr Nesbitt')
# Does the original name work?
self.getPage('/expose_dec/nesbitt')
self.assertStatus(200)
self.assertBody('Mr Nesbitt')
# Test @expose(["alias1", "alias2"])
self.getPage('/expose_dec/alias1')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
self.getPage('/expose_dec/alias2')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
# Does the original name work?
self.getPage('/expose_dec/andrews')
self.assertStatus(200)
self.assertBody('Mr Ken Andrews')
# Test @expose(alias="alias")
self.getPage('/expose_dec/alias3')
self.assertStatus(200)
self.assertBody('Mr. and Mrs. Watson')
class ErrorTests(helper.CPWebCase):
@staticmethod
def setup_server():
def break_header():
# Add a header after finalize that is invalid
cherrypy.serving.response.header_list.append((2, 3))
cherrypy.tools.break_header = cherrypy.Tool(
'on_end_resource', break_header)
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.config(**{'tools.break_header.on': True})
def start_response_error(self):
return 'salud!'
@cherrypy.expose
def stat(self, path):
with cherrypy.HTTPError.handle(OSError, 404):
os.stat(path)
root = Root()
cherrypy.tree.mount(root)
def test_start_response_error(self):
self.getPage('/start_response_error')
self.assertStatus(500)
self.assertInBody(
'TypeError: response.header_list key 2 is not a byte string.')
def test_contextmanager(self):
self.getPage('/stat/missing')
self.assertStatus(404)
body_text = self.body.decode('utf-8')
assert (
'No such file or directory' in body_text or
'cannot find the file specified' in body_text
)
class TestBinding:
def test_bind_ephemeral_port(self):
"""
A server configured to bind to port 0 will bind to an ephemeral
port and indicate that port number on startup.
"""
cherrypy.config.reset()
bind_ephemeral_conf = {
'server.socket_port': 0,
}
cherrypy.config.update(bind_ephemeral_conf)
cherrypy.engine.start()
assert cherrypy.server.bound_addr != cherrypy.server.bind_addr
_host, port = cherrypy.server.bound_addr
assert port > 0
cherrypy.engine.stop()
assert cherrypy.server.bind_addr == cherrypy.server.bound_addr

View File

@@ -0,0 +1,424 @@
import six
import cherrypy
from cherrypy.test import helper
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
def setup_server():
class SubSubRoot:
@cherrypy.expose
def index(self):
return 'SubSubRoot index'
@cherrypy.expose
def default(self, *args):
return 'SubSubRoot default'
@cherrypy.expose
def handler(self):
return 'SubSubRoot handler'
@cherrypy.expose
def dispatch(self):
return 'SubSubRoot dispatch'
subsubnodes = {
'1': SubSubRoot(),
'2': SubSubRoot(),
}
class SubRoot:
@cherrypy.expose
def index(self):
return 'SubRoot index'
@cherrypy.expose
def default(self, *args):
return 'SubRoot %s' % (args,)
@cherrypy.expose
def handler(self):
return 'SubRoot handler'
def _cp_dispatch(self, vpath):
return subsubnodes.get(vpath[0], None)
subnodes = {
'1': SubRoot(),
'2': SubRoot(),
}
class Root:
@cherrypy.expose
def index(self):
return 'index'
@cherrypy.expose
def default(self, *args):
return 'default %s' % (args,)
@cherrypy.expose
def handler(self):
return 'handler'
def _cp_dispatch(self, vpath):
return subnodes.get(vpath[0])
# -------------------------------------------------------------------------
# DynamicNodeAndMethodDispatcher example.
# This example exposes a fairly naive HTTP api
class User(object):
def __init__(self, id, name):
self.id = id
self.name = name
def __unicode__(self):
return six.text_type(self.name)
def __str__(self):
return str(self.name)
user_lookup = {
1: User(1, 'foo'),
2: User(2, 'bar'),
}
def make_user(name, id=None):
if not id:
id = max(*list(user_lookup.keys())) + 1
user_lookup[id] = User(id, name)
return id
@cherrypy.expose
class UserContainerNode(object):
def POST(self, name):
"""
Allow the creation of a new Object
"""
return 'POST %d' % make_user(name)
def GET(self):
return six.text_type(sorted(user_lookup.keys()))
def dynamic_dispatch(self, vpath):
try:
id = int(vpath[0])
except (ValueError, IndexError):
return None
return UserInstanceNode(id)
@cherrypy.expose
class UserInstanceNode(object):
def __init__(self, id):
self.id = id
self.user = user_lookup.get(id, None)
# For all but PUT methods there MUST be a valid user identified
# by self.id
if not self.user and cherrypy.request.method != 'PUT':
raise cherrypy.HTTPError(404)
def GET(self, *args, **kwargs):
"""
Return the appropriate representation of the instance.
"""
return six.text_type(self.user)
def POST(self, name):
"""
Update the fields of the user instance.
"""
self.user.name = name
return 'POST %d' % self.user.id
def PUT(self, name):
"""
Create a new user with the specified id, or edit it if it already
exists
"""
if self.user:
# Edit the current user
self.user.name = name
return 'PUT %d' % self.user.id
else:
# Make a new user with said attributes.
return 'PUT %d' % make_user(name, self.id)
def DELETE(self):
"""
Delete the user specified at the id.
"""
id = self.user.id
del user_lookup[self.user.id]
del self.user
return 'DELETE %d' % id
class ABHandler:
class CustomDispatch:
@cherrypy.expose
def index(self, a, b):
return 'custom'
def _cp_dispatch(self, vpath):
"""Make sure that if we don't pop anything from vpath,
processing still works.
"""
return self.CustomDispatch()
@cherrypy.expose
def index(self, a, b=None):
body = ['a:' + str(a)]
if b is not None:
body.append(',b:' + str(b))
return ''.join(body)
@cherrypy.expose
def delete(self, a, b):
return 'deleting ' + str(a) + ' and ' + str(b)
class IndexOnly:
def _cp_dispatch(self, vpath):
"""Make sure that popping ALL of vpath still shows the index
handler.
"""
while vpath:
vpath.pop()
return self
@cherrypy.expose
def index(self):
return 'IndexOnly index'
class DecoratedPopArgs:
"""Test _cp_dispatch with @cherrypy.popargs."""
@cherrypy.expose
def index(self):
return 'no params'
@cherrypy.expose
def hi(self):
return "hi was not interpreted as 'a' param"
DecoratedPopArgs = cherrypy.popargs(
'a', 'b', handler=ABHandler())(DecoratedPopArgs)
class NonDecoratedPopArgs:
"""Test _cp_dispatch = cherrypy.popargs()"""
_cp_dispatch = cherrypy.popargs('a')
@cherrypy.expose
def index(self, a):
return 'index: ' + str(a)
class ParameterizedHandler:
"""Special handler created for each request"""
def __init__(self, a):
self.a = a
@cherrypy.expose
def index(self):
if 'a' in cherrypy.request.params:
raise Exception(
'Parameterized handler argument ended up in '
'request.params')
return self.a
class ParameterizedPopArgs:
"""Test cherrypy.popargs() with a function call handler"""
ParameterizedPopArgs = cherrypy.popargs(
'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
Root.decorated = DecoratedPopArgs()
Root.undecorated = NonDecoratedPopArgs()
Root.index_only = IndexOnly()
Root.parameter_test = ParameterizedPopArgs()
Root.users = UserContainerNode()
md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
for url in script_names:
conf = {
'/': {
'user': (url or '/').split('/')[-2],
},
'/users': {
'request.dispatch': md
},
}
cherrypy.tree.mount(Root(), url, conf)
class DynamicObjectMappingTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testObjectMapping(self):
for url in script_names:
self.script_name = url
self.getPage('/')
self.assertBody('index')
self.getPage('/handler')
self.assertBody('handler')
# Dynamic dispatch will succeed here for the subnodes
# so the subroot gets called
self.getPage('/1/')
self.assertBody('SubRoot index')
self.getPage('/2/')
self.assertBody('SubRoot index')
self.getPage('/1/handler')
self.assertBody('SubRoot handler')
self.getPage('/2/handler')
self.assertBody('SubRoot handler')
# Dynamic dispatch will fail here for the subnodes
# so the default gets called
self.getPage('/asdf/')
self.assertBody("default ('asdf',)")
self.getPage('/asdf/asdf')
self.assertBody("default ('asdf', 'asdf')")
self.getPage('/asdf/handler')
self.assertBody("default ('asdf', 'handler')")
# Dynamic dispatch will succeed here for the subsubnodes
# so the subsubroot gets called
self.getPage('/1/1/')
self.assertBody('SubSubRoot index')
self.getPage('/2/2/')
self.assertBody('SubSubRoot index')
self.getPage('/1/1/handler')
self.assertBody('SubSubRoot handler')
self.getPage('/2/2/handler')
self.assertBody('SubSubRoot handler')
self.getPage('/2/2/dispatch')
self.assertBody('SubSubRoot dispatch')
# The exposed dispatch will not be called as a dispatch
# method.
self.getPage('/2/2/foo/foo')
self.assertBody('SubSubRoot default')
# Dynamic dispatch will fail here for the subsubnodes
# so the SubRoot gets called
self.getPage('/1/asdf/')
self.assertBody("SubRoot ('asdf',)")
self.getPage('/1/asdf/asdf')
self.assertBody("SubRoot ('asdf', 'asdf')")
self.getPage('/1/asdf/handler')
self.assertBody("SubRoot ('asdf', 'handler')")
def testMethodDispatch(self):
# GET acts like a container
self.getPage('/users')
self.assertBody('[1, 2]')
self.assertHeader('Allow', 'GET, HEAD, POST')
# POST to the container URI allows creation
self.getPage('/users', method='POST', body='name=baz')
self.assertBody('POST 3')
self.assertHeader('Allow', 'GET, HEAD, POST')
# POST to a specific instanct URI results in a 404
# as the resource does not exit.
self.getPage('/users/5', method='POST', body='name=baz')
self.assertStatus(404)
# PUT to a specific instanct URI results in creation
self.getPage('/users/5', method='PUT', body='name=boris')
self.assertBody('PUT 5')
self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
# GET acts like a container
self.getPage('/users')
self.assertBody('[1, 2, 3, 5]')
self.assertHeader('Allow', 'GET, HEAD, POST')
test_cases = (
(1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
(2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
(3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
(5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
)
for id, name, updatedname, headers in test_cases:
self.getPage('/users/%d' % id)
self.assertBody(name)
self.assertHeader('Allow', headers)
# Make sure POSTs update already existings resources
self.getPage('/users/%d' %
id, method='POST', body='name=%s' % updatedname)
self.assertBody('POST %d' % id)
self.assertHeader('Allow', headers)
# Make sure PUTs Update already existing resources.
self.getPage('/users/%d' %
id, method='PUT', body='name=%s' % updatedname)
self.assertBody('PUT %d' % id)
self.assertHeader('Allow', headers)
# Make sure DELETES Remove already existing resources.
self.getPage('/users/%d' % id, method='DELETE')
self.assertBody('DELETE %d' % id)
self.assertHeader('Allow', headers)
# GET acts like a container
self.getPage('/users')
self.assertBody('[]')
self.assertHeader('Allow', 'GET, HEAD, POST')
def testVpathDispatch(self):
self.getPage('/decorated/')
self.assertBody('no params')
self.getPage('/decorated/hi')
self.assertBody("hi was not interpreted as 'a' param")
self.getPage('/decorated/yo/')
self.assertBody('a:yo')
self.getPage('/decorated/yo/there/')
self.assertBody('a:yo,b:there')
self.getPage('/decorated/yo/there/delete')
self.assertBody('deleting yo and there')
self.getPage('/decorated/yo/there/handled_by_dispatch/')
self.assertBody('custom')
self.getPage('/undecorated/blah/')
self.assertBody('index: blah')
self.getPage('/index_only/a/b/c/d/e/f/g/')
self.assertBody('IndexOnly index')
self.getPage('/parameter_test/argument2/')
self.assertBody('argument2')

View File

@@ -0,0 +1,433 @@
# coding: utf-8
import gzip
import io
from unittest import mock
from six.moves.http_client import IncompleteRead
from six.moves.urllib.parse import quote as url_quote
import cherrypy
from cherrypy._cpcompat import ntob, ntou
from cherrypy.test import helper
europoundUnicode = ntou('£', encoding='utf-8')
sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8')
sing8 = sing.encode('utf-8')
sing16 = sing.encode('utf-16')
class EncodingTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, param):
assert param == europoundUnicode, '%r != %r' % (
param, europoundUnicode)
yield europoundUnicode
@cherrypy.expose
def mao_zedong(self):
return sing
@cherrypy.expose
@cherrypy.config(**{'tools.encode.encoding': 'utf-8'})
def utf8(self):
return sing8
@cherrypy.expose
def cookies_and_headers(self):
# if the headers have non-ascii characters and a cookie has
# any part which is unicode (even ascii), the response
# should not fail.
cherrypy.response.cookie['candy'] = 'bar'
cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
cherrypy.response.headers[
'Some-Header'] = 'My d\xc3\xb6g has fleas'
cherrypy.response.headers[
'Bytes-Header'] = b'Bytes given header'
return 'Any content'
@cherrypy.expose
def reqparams(self, *args, **kwargs):
return b', '.join(
[': '.join((k, v)).encode('utf8')
for k, v in sorted(cherrypy.request.params.items())]
)
@cherrypy.expose
@cherrypy.config(**{
'tools.encode.text_only': False,
'tools.encode.add_charset': True,
})
def nontext(self, *args, **kwargs):
cherrypy.response.headers[
'Content-Type'] = 'application/binary'
return '\x00\x01\x02\x03'
class GZIP:
@cherrypy.expose
def index(self):
yield 'Hello, world'
@cherrypy.expose
# Turn encoding off so the gzip tool is the one doing the collapse.
@cherrypy.config(**{'tools.encode.on': False})
def noshow(self):
# Test for ticket #147, where yield showed no exceptions
# (content-encoding was still gzip even though traceback
# wasn't zipped).
raise IndexError()
yield 'Here be dragons'
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def noshow_stream(self):
# Test for ticket #147, where yield showed no exceptions
# (content-encoding was still gzip even though traceback
# wasn't zipped).
raise IndexError()
yield 'Here be dragons'
class Decode:
@cherrypy.expose
@cherrypy.config(**{
'tools.decode.on': True,
'tools.decode.default_encoding': ['utf-16'],
})
def extra_charset(self, *args, **kwargs):
return ', '.join([': '.join((k, v))
for k, v in cherrypy.request.params.items()])
@cherrypy.expose
@cherrypy.config(**{
'tools.decode.on': True,
'tools.decode.encoding': 'utf-16',
})
def force_charset(self, *args, **kwargs):
return ', '.join([': '.join((k, v))
for k, v in cherrypy.request.params.items()])
root = Root()
root.gzip = GZIP()
root.decode = Decode()
cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
def test_query_string_decoding(self):
URI_TMPL = '/reqparams?q={q}'
europoundUtf8_2_bytes = europoundUnicode.encode('utf-8')
europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2]
# Encoded utf8 query strings MUST be parsed correctly.
# Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes)))
# The return value will be encoded as utf8.
self.assertBody(b'q: ' + europoundUtf8_2_bytes)
# Query strings that are incorrectly encoded MUST raise 404.
# Here, q is the second byte of POUND SIGN U+A3 encoded in utf8
# and then %HEX
# TODO: check whether this shouldn't raise 400 Bad Request instead
self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte)))
self.assertStatus(404)
self.assertErrorPage(
404,
'The given query string could not be processed. Query '
"strings for this resource must be encoded with 'utf8'.")
def test_urlencoded_decoding(self):
# Test the decoding of an application/x-www-form-urlencoded entity.
europoundUtf8 = europoundUnicode.encode('utf-8')
body = b'param=' + europoundUtf8
self.getPage('/',
method='POST',
headers=[
('Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(europoundUtf8)
# Encoded utf8 entities MUST be parsed and decoded correctly.
# Here, q is the POUND SIGN U+00A3 encoded in utf8
body = b'q=\xc2\xa3'
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# ...and in utf16, which is not in the default attempt_charsets list:
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/reqparams',
method='POST',
headers=[
('Content-Type',
'application/x-www-form-urlencoded;charset=utf-16'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# Entities that are incorrectly encoded MUST raise 400.
# Here, q is the POUND SIGN U+00A3 encoded in utf16, but
# the Content-Type incorrectly labels it utf-8.
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/reqparams',
method='POST',
headers=[
('Content-Type',
'application/x-www-form-urlencoded;charset=utf-8'),
('Content-Length', str(len(body))),
],
body=body),
self.assertStatus(400)
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['utf-8']")
def test_decode_tool(self):
# An extra charset should be tried first, and succeed if it matches.
# Here, we add utf-16 as a charset and pass a utf-16 body.
body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
self.getPage('/decode/extra_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# An extra charset should be tried first, and continue to other default
# charsets if it doesn't match.
# Here, we add utf-16 as a charset but still pass a utf-8 body.
body = b'q=\xc2\xa3'
self.getPage('/decode/extra_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'q: \xc2\xa3')
# An extra charset should error if force is True and it doesn't match.
# Here, we force utf-16 as a charset but still pass a utf-8 body.
body = b'q=\xc2\xa3'
self.getPage('/decode/force_charset', method='POST',
headers=[(
'Content-Type', 'application/x-www-form-urlencoded'),
('Content-Length', str(len(body))),
],
body=body),
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['utf-16']")
def test_multipart_decoding(self):
# Test the decoding of a multipart entity when the charset (utf16) is
# explicitly given.
body = ntob('\r\n'.join([
'--X',
'Content-Type: text/plain;charset=utf-16',
'Content-Disposition: form-data; name="text"',
'',
'\xff\xfea\x00b\x00\x1c c\x00',
'--X',
'Content-Type: text/plain;charset=utf-16',
'Content-Disposition: form-data; name="submit"',
'',
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc')
@mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1)
def test_multipart_decoding_bigger_maxrambytes(self):
"""
Decoding of a multipart entity should also pass when
the entity is bigger than maxrambytes. See ticket #1352.
"""
self.test_multipart_decoding()
def test_multipart_decoding_no_charset(self):
# Test the decoding of a multipart entity when the charset (utf8) is
# NOT explicitly given, but is in the list of charsets to attempt.
body = ntob('\r\n'.join([
'--X',
'Content-Disposition: form-data; name="text"',
'',
'\xe2\x80\x9c',
'--X',
'Content-Disposition: form-data; name="submit"',
'',
'Create',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(b'submit: Create, text: \xe2\x80\x9c')
def test_multipart_decoding_no_successful_charset(self):
# Test the decoding of a multipart entity when the charset (utf16) is
# NOT explicitly given, and is NOT in the list of charsets to attempt.
body = ntob('\r\n'.join([
'--X',
'Content-Disposition: form-data; name="text"',
'',
'\xff\xfea\x00b\x00\x1c c\x00',
'--X',
'Content-Disposition: form-data; name="submit"',
'',
'\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
'--X--'
]))
self.getPage('/reqparams', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertStatus(400)
self.assertErrorPage(
400,
'The request entity could not be decoded. The following charsets '
"were attempted: ['us-ascii', 'utf-8']")
def test_nontext(self):
self.getPage('/nontext')
self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
self.assertBody('\x00\x01\x02\x03')
def testEncoding(self):
# Default encoding should be utf-8
self.getPage('/mao_zedong')
self.assertBody(sing8)
# Ask for utf-16.
self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
self.assertHeader('Content-Type', 'text/html;charset=utf-16')
self.assertBody(sing16)
# Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
# should be produced.
self.getPage('/mao_zedong', [('Accept-Charset',
'iso-8859-1;q=1, utf-16;q=0.5')])
self.assertBody(sing16)
# The "*" value should default to our default_encoding, utf-8
self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
self.assertBody(sing8)
# Only allow iso-8859-1, which should fail and raise 406.
self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
self.assertStatus('406 Not Acceptable')
self.assertInBody('Your client sent this Accept-Charset header: '
'iso-8859-1, *;q=0. We tried these charsets: '
'iso-8859-1.')
# Ask for x-mac-ce, which should be unknown. See ticket #569.
self.getPage('/mao_zedong', [('Accept-Charset',
'us-ascii, ISO-8859-1, x-mac-ce')])
self.assertStatus('406 Not Acceptable')
self.assertInBody('Your client sent this Accept-Charset header: '
'us-ascii, ISO-8859-1, x-mac-ce. We tried these '
'charsets: ISO-8859-1, us-ascii, x-mac-ce.')
# Test the 'encoding' arg to encode.
self.getPage('/utf8')
self.assertBody(sing8)
self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
self.assertStatus('406 Not Acceptable')
# Test malformed quality value, which should raise 400.
self.getPage('/mao_zedong', [('Accept-Charset',
'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')])
self.assertStatus('400 Bad Request')
def testGzip(self):
zbuf = io.BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
zfile.write(b'Hello, world')
zfile.close()
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')])
self.assertInBody(zbuf.getvalue()[:3])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertHeader('Content-Encoding', 'gzip')
# Test when gzip is denied.
self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertNoHeader('Content-Encoding')
self.assertBody('Hello, world')
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')])
self.assertHeader('Vary', 'Accept-Encoding')
self.assertNoHeader('Content-Encoding')
self.assertBody('Hello, world')
# Test that trailing comma doesn't cause IndexError
# Ref: https://github.com/cherrypy/cherrypy/issues/988
self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')])
self.assertStatus(200)
self.assertNotInBody('IndexError')
self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')])
self.assertStatus(406)
self.assertNoHeader('Content-Encoding')
self.assertErrorPage(406, 'identity, gzip')
# Test for ticket #147
self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')])
self.assertNoHeader('Content-Encoding')
self.assertStatus(500)
self.assertErrorPage(500, pattern='IndexError\n')
# In this case, there's nothing we can do to deliver a
# readable page, since 1) the gzip header is already set,
# and 2) we may have already written some of the body.
# The fix is to never stream yields when using gzip.
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
getattr(cherrypy.server, 'using_apache', False)):
self.getPage('/gzip/noshow_stream',
headers=[('Accept-Encoding', 'gzip')])
self.assertHeader('Content-Encoding', 'gzip')
self.assertInBody('\x1f\x8b\x08\x00')
else:
# The wsgiserver will simply stop sending data, and the HTTP client
# will error due to an incomplete chunk-encoded stream.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
'/gzip/noshow_stream',
headers=[('Accept-Encoding', 'gzip')])
def test_UnicodeHeaders(self):
self.getPage('/cookies_and_headers')
self.assertBody('Any content')
def test_BytesHeaders(self):
self.getPage('/cookies_and_headers')
self.assertBody('Any content')
self.assertHeader('Bytes-Header', 'Bytes given header')

View File

@@ -0,0 +1,84 @@
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper
class ETagTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def resource(self):
return 'Oh wah ta goo Siam.'
@cherrypy.expose
def fail(self, code):
code = int(code)
if 300 <= code <= 399:
raise cherrypy.HTTPRedirect([], code)
else:
raise cherrypy.HTTPError(code)
@cherrypy.expose
# In Python 3, tools.encode is on by default
@cherrypy.config(**{'tools.encode.on': True})
def unicoded(self):
return ntou('I am a \u1ee4nicode string.', 'escape')
conf = {'/': {'tools.etags.on': True,
'tools.etags.autotags': True,
}}
cherrypy.tree.mount(Root(), config=conf)
def test_etags(self):
self.getPage('/resource')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('Oh wah ta goo Siam.')
etag = self.assertHeader('ETag')
# Test If-Match (both valid and invalid)
self.getPage('/resource', headers=[('If-Match', etag)])
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', '*')])
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', '*')], method='POST')
self.assertStatus('200 OK')
self.getPage('/resource', headers=[('If-Match', 'a bogus tag')])
self.assertStatus('412 Precondition Failed')
# Test If-None-Match (both valid and invalid)
self.getPage('/resource', headers=[('If-None-Match', etag)])
self.assertStatus(304)
self.getPage('/resource', method='POST',
headers=[('If-None-Match', etag)])
self.assertStatus('412 Precondition Failed')
self.getPage('/resource', headers=[('If-None-Match', '*')])
self.assertStatus(304)
self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')])
self.assertStatus('200 OK')
def test_errors(self):
self.getPage('/resource')
self.assertStatus(200)
etag = self.assertHeader('ETag')
# Test raising errors in page handler
self.getPage('/fail/412', headers=[('If-Match', etag)])
self.assertStatus(412)
self.getPage('/fail/304', headers=[('If-Match', etag)])
self.assertStatus(304)
self.getPage('/fail/412', headers=[('If-None-Match', '*')])
self.assertStatus(412)
self.getPage('/fail/304', headers=[('If-None-Match', '*')])
self.assertStatus(304)
def test_unicode_body(self):
self.getPage('/unicoded')
self.assertStatus(200)
etag1 = self.assertHeader('ETag')
self.getPage('/unicoded', headers=[('If-Match', etag1)])
self.assertStatus(200)
self.assertHeader('ETag', etag1)

View File

@@ -0,0 +1,307 @@
# coding: utf-8
"""Tests for managing HTTP issues (malformed requests, etc)."""
import errno
import mimetypes
import socket
import sys
from unittest import mock
import six
from six.moves.http_client import HTTPConnection
from six.moves import urllib
import cherrypy
from cherrypy._cpcompat import HTTPSConnection, quote
from cherrypy.test import helper
def is_ascii(text):
"""
Return True if the text encodes as ascii.
"""
try:
text.encode('ascii')
return True
except Exception:
pass
return False
def encode_filename(filename):
"""
Given a filename to be used in a multipart/form-data,
encode the name. Return the key and encoded filename.
"""
if is_ascii(filename):
return 'filename', '"{filename}"'.format(**locals())
encoded = quote(filename, encoding='utf-8')
return 'filename*', "'".join((
'UTF-8',
'', # lang
encoded,
))
def encode_multipart_formdata(files):
"""Return (content_type, body) ready for httplib.HTTP instance.
files: a sequence of (name, filename, value) tuples for multipart uploads.
filename can be a string or a tuple ('filename string', 'encoding')
"""
BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
L = []
for key, filename, value in files:
L.append('--' + BOUNDARY)
fn_key, encoded = encode_filename(filename)
tmpl = \
'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}'
L.append(tmpl.format(**locals()))
ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
L.append('Content-Type: %s' % ct)
L.append('')
L.append(value)
L.append('--' + BOUNDARY + '--')
L.append('')
body = '\r\n'.join(L)
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
return content_type, body
class HTTPTests(helper.CPWebCase):
def make_connection(self):
if self.scheme == 'https':
return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, *args, **kwargs):
return 'Hello world!'
@cherrypy.expose
@cherrypy.config(**{'request.process_request_body': False})
def no_body(self, *args, **kwargs):
return 'Hello world!'
@cherrypy.expose
def post_multipart(self, file):
"""Return a summary ("a * 65536\nb * 65536") of the uploaded
file.
"""
contents = file.file.read()
summary = []
curchar = None
count = 0
for c in contents:
if c == curchar:
count += 1
else:
if count:
if six.PY3:
curchar = chr(curchar)
summary.append('%s * %d' % (curchar, count))
count = 1
curchar = c
if count:
if six.PY3:
curchar = chr(curchar)
summary.append('%s * %d' % (curchar, count))
return ', '.join(summary)
@cherrypy.expose
def post_filename(self, myfile):
'''Return the name of the file which was uploaded.'''
return myfile.filename
cherrypy.tree.mount(Root())
cherrypy.config.update({'server.max_request_body_size': 30000000})
def test_no_content_length(self):
# "The presence of a message-body in a request is signaled by the
# inclusion of a Content-Length or Transfer-Encoding header field in
# the request's message-headers."
#
# Send a message with neither header and no body. Even though
# the request is of method POST, this should be OK because we set
# request.process_request_body to False for our handler.
c = self.make_connection()
c.request('POST', '/no_body')
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
self.assertBody(b'Hello world!')
# Now send a message that has no Content-Length, but does send a body.
# Verify that CP times out the socket and responds
# with 411 Length Required.
if self.scheme == 'https':
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
# `_get_content_length` is needed for Python 3.6+
with mock.patch.object(
c,
'_get_content_length',
lambda body, method: None,
create=True):
# `_set_content_length` is needed for Python 2.7-3.5
with mock.patch.object(c, '_set_content_length', create=True):
c.request('POST', '/')
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(411)
def test_post_multipart(self):
alphabet = 'abcdefghijklmnopqrstuvwxyz'
# generate file contents for a large post
contents = ''.join([c * 65536 for c in alphabet])
# encode as multipart form data
files = [('file', 'file.txt', contents)]
content_type, body = encode_multipart_formdata(files)
body = body.encode('Latin-1')
# post file
c = self.make_connection()
c.putrequest('POST', '/post_multipart')
c.putheader('Content-Type', content_type)
c.putheader('Content-Length', str(len(body)))
c.endheaders()
c.send(body)
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
parts = ['%s * 65536' % ch for ch in alphabet]
self.assertBody(', '.join(parts))
def test_post_filename_with_special_characters(self):
'''Testing that we can handle filenames with special characters. This
was reported as a bug in:
https://github.com/cherrypy/cherrypy/issues/1146/
https://github.com/cherrypy/cherrypy/issues/1397/
https://github.com/cherrypy/cherrypy/issues/1694/
'''
# We'll upload a bunch of files with differing names.
fnames = [
'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
'file;name.csv', 'file; name.csv', u'test_łóąä.txt',
]
for fname in fnames:
files = [('myfile', fname, 'yunyeenyunyue')]
content_type, body = encode_multipart_formdata(files)
body = body.encode('Latin-1')
# post file
c = self.make_connection()
c.putrequest('POST', '/post_filename')
c.putheader('Content-Type', content_type)
c.putheader('Content-Length', str(len(body)))
c.endheaders()
c.send(body)
response = c.getresponse()
self.body = response.fp.read()
self.status = str(response.status)
self.assertStatus(200)
self.assertBody(fname)
def test_malformed_request_line(self):
if getattr(cherrypy.server, 'using_apache', False):
return self.skip('skipped due to known Apache differences...')
# Test missing version in Request-Line
c = self.make_connection()
c._output(b'geT /')
c._send_output()
if hasattr(c, 'strict'):
response = c.response_class(c.sock, strict=c.strict, method='GET')
else:
# Python 3.2 removed the 'strict' feature, saying:
# "http.client now always assumes HTTP/1.x compliant servers."
response = c.response_class(c.sock, method='GET')
response.begin()
self.assertEqual(response.status, 400)
self.assertEqual(response.fp.read(22), b'Malformed Request-Line')
c.close()
def test_request_line_split_issue_1220(self):
params = {
'intervenant-entreprise-evenement_classaction':
'evenement-mailremerciements',
'_path': 'intervenant-entreprise-evenement',
'intervenant-entreprise-evenement_action-id': 19404,
'intervenant-entreprise-evenement_id': 19404,
'intervenant-entreprise_id': 28092,
}
Request_URI = '/index?' + urllib.parse.urlencode(params)
self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256)
self.getPage(Request_URI)
self.assertBody('Hello world!')
def test_malformed_header(self):
c = self.make_connection()
c.putrequest('GET', '/')
c.putheader('Content-Type', 'text/plain')
# See https://github.com/cherrypy/cherrypy/issues/941
c._output(b're, 1.2.3.4#015#012')
c.endheaders()
response = c.getresponse()
self.status = str(response.status)
self.assertStatus(400)
self.body = response.fp.read(20)
self.assertBody('Illegal header line.')
def test_http_over_https(self):
if self.scheme != 'https':
return self.skip('skipped (not running HTTPS)... ')
# Try connecting without SSL.
conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
conn.putrequest('GET', '/', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
try:
response.begin()
self.assertEqual(response.status, 400)
self.body = response.read()
self.assertBody('The client sent a plain HTTP request, but this '
'server only speaks HTTPS on this port.')
except socket.error:
e = sys.exc_info()[1]
# "Connection reset by peer" is also acceptable.
if e.errno != errno.ECONNRESET:
raise
def test_garbage_in(self):
# Connect without SSL regardless of server.scheme
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
c._output(b'gjkgjklsgjklsgjkljklsg')
c._send_output()
response = c.response_class(c.sock, method='GET')
try:
response.begin()
self.assertEqual(response.status, 400)
self.assertEqual(response.fp.read(22),
b'Malformed Request-Line')
c.close()
except socket.error:
e = sys.exc_info()[1]
# "Connection reset by peer" is also acceptable.
if e.errno != errno.ECONNRESET:
raise

View File

@@ -0,0 +1,80 @@
"""Test helpers from ``cherrypy.lib.httputil`` module."""
import pytest
from six.moves import http_client
from cherrypy.lib import httputil
@pytest.mark.parametrize(
'script_name,path_info,expected_url',
[
('/sn/', '/pi/', '/sn/pi/'),
('/sn/', '/pi', '/sn/pi'),
('/sn/', '/', '/sn/'),
('/sn/', '', '/sn/'),
('/sn', '/pi/', '/sn/pi/'),
('/sn', '/pi', '/sn/pi'),
('/sn', '/', '/sn/'),
('/sn', '', '/sn'),
('/', '/pi/', '/pi/'),
('/', '/pi', '/pi'),
('/', '/', '/'),
('/', '', '/'),
('', '/pi/', '/pi/'),
('', '/pi', '/pi'),
('', '/', '/'),
('', '', '/'),
]
)
def test_urljoin(script_name, path_info, expected_url):
"""Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO."""
actual_url = httputil.urljoin(script_name, path_info)
assert actual_url == expected_url
EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows')
EXPECTED_500 = (
500,
'Internal Server Error',
'The server encountered an unexpected condition which '
'prevented it from fulfilling the request.',
)
EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI')
EXPECTED_444 = (444, 'Non-existent reason', '')
@pytest.mark.parametrize(
'status,expected_status',
[
(None, EXPECTED_200),
(200, EXPECTED_200),
('500', EXPECTED_500),
(http_client.NOT_FOUND, EXPECTED_404),
('444 Non-existent reason', EXPECTED_444),
]
)
def test_valid_status(status, expected_status):
"""Check valid int, string and http_client-constants
statuses processing."""
assert httputil.valid_status(status) == expected_status
@pytest.mark.parametrize(
'status_code,error_msg',
[
('hey', "Illegal response status from server ('hey' is non-numeric)."),
(
{'hey': 'hi'},
'Illegal response status from server '
"({'hey': 'hi'} is non-numeric).",
),
(1, 'Illegal response status from server (1 is out of range).'),
(600, 'Illegal response status from server (600 is out of range).'),
]
)
def test_invalid_status(status_code, error_msg):
"""Check that invalid status cause certain errors."""
with pytest.raises(ValueError) as excinfo:
httputil.valid_status(status_code)
assert error_msg in str(excinfo)

View File

@@ -0,0 +1,196 @@
import six
import cherrypy
from cherrypy.test import helper
class IteratorBase(object):
created = 0
datachunk = 'butternut squash' * 256
@classmethod
def incr(cls):
cls.created += 1
@classmethod
def decr(cls):
cls.created -= 1
class OurGenerator(IteratorBase):
def __iter__(self):
self.incr()
try:
for i in range(1024):
yield self.datachunk
finally:
self.decr()
class OurIterator(IteratorBase):
started = False
closed_off = False
count = 0
def increment(self):
self.incr()
def decrement(self):
if not self.closed_off:
self.closed_off = True
self.decr()
def __iter__(self):
return self
def __next__(self):
if not self.started:
self.started = True
self.increment()
self.count += 1
if self.count > 1024:
raise StopIteration
return self.datachunk
next = __next__
def __del__(self):
self.decrement()
class OurClosableIterator(OurIterator):
def close(self):
self.decrement()
class OurNotClosableIterator(OurIterator):
# We can't close something which requires an additional argument.
def close(self, somearg):
self.decrement()
class OurUnclosableIterator(OurIterator):
close = 'close' # not callable!
class IteratorTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root(object):
@cherrypy.expose
def count(self, clsname):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return six.text_type(globals()[clsname].created)
@cherrypy.expose
def getall(self, clsname):
cherrypy.response.headers['Content-Type'] = 'text/plain'
return globals()[clsname]()
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def stream(self, clsname):
return self.getall(clsname)
cherrypy.tree.mount(Root())
def test_iterator(self):
try:
self._test_iterator()
except Exception:
'Test fails intermittently. See #1419'
def _test_iterator(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Check the counts of all the classes, they should be zero.
closables = ['OurClosableIterator', 'OurGenerator']
unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator']
all_classes = closables + unclosables
import random
random.shuffle(all_classes)
for clsname in all_classes:
self.getPage('/count/' + clsname)
self.assertStatus(200)
self.assertBody('0')
# We should also be able to read the entire content body
# successfully, though we don't need to, we just want to
# check the header.
for clsname in all_classes:
itr_conn = self.get_conn()
itr_conn.putrequest('GET', '/getall/' + clsname)
itr_conn.endheaders()
response = itr_conn.getresponse()
self.assertEqual(response.status, 200)
headers = response.getheaders()
for header_name, header_value in headers:
if header_name.lower() == 'content-length':
expected = six.text_type(1024 * 16 * 256)
assert header_value == expected, header_value
break
else:
raise AssertionError('No Content-Length header found')
# As the response should be fully consumed by CherryPy
# before sending back, the count should still be at zero
# by the time the response has been sent.
self.getPage('/count/' + clsname)
self.assertStatus(200)
self.assertBody('0')
# Now we do the same check with streaming - some classes will
# be automatically closed, while others cannot.
stream_counts = {}
for clsname in all_classes:
itr_conn = self.get_conn()
itr_conn.putrequest('GET', '/stream/' + clsname)
itr_conn.endheaders()
response = itr_conn.getresponse()
self.assertEqual(response.status, 200)
response.fp.read(65536)
# Let's check the count - this should always be one.
self.getPage('/count/' + clsname)
self.assertBody('1')
# Now if we close the connection, the count should go back
# to zero.
itr_conn.close()
self.getPage('/count/' + clsname)
# If this is a response which should be easily closed, then
# we will test to see if the value has gone back down to
# zero.
if clsname in closables:
# Sometimes we try to get the answer too quickly - we
# will wait for 100 ms before asking again if we didn't
# get the answer we wanted.
if self.body != '0':
import time
time.sleep(0.1)
self.getPage('/count/' + clsname)
stream_counts[clsname] = int(self.body)
# Check that we closed off the classes which should provide
# easy mechanisms for doing so.
for clsname in closables:
assert stream_counts[clsname] == 0, (
'did not close off stream response correctly, expected '
'count of zero for %s: %s' % (clsname, stream_counts)
)

View File

@@ -0,0 +1,102 @@
import cherrypy
from cherrypy.test import helper
from cherrypy._cpcompat import json
json_out = cherrypy.config(**{'tools.json_out.on': True})
json_in = cherrypy.config(**{'tools.json_in.on': True})
class JsonTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root(object):
@cherrypy.expose
def plain(self):
return 'hello'
@cherrypy.expose
@json_out
def json_string(self):
return 'hello'
@cherrypy.expose
@json_out
def json_list(self):
return ['a', 'b', 42]
@cherrypy.expose
@json_out
def json_dict(self):
return {'answer': 42}
@cherrypy.expose
@json_in
def json_post(self):
if cherrypy.request.json == [13, 'c']:
return 'ok'
else:
return 'nok'
@cherrypy.expose
@json_out
@cherrypy.config(**{'tools.caching.on': True})
def json_cached(self):
return 'hello there'
root = Root()
cherrypy.tree.mount(root)
def test_json_output(self):
if json is None:
self.skip('json not found ')
return
self.getPage('/plain')
self.assertBody('hello')
self.getPage('/json_string')
self.assertBody('"hello"')
self.getPage('/json_list')
self.assertBody('["a", "b", 42]')
self.getPage('/json_dict')
self.assertBody('{"answer": 42}')
def test_json_input(self):
if json is None:
self.skip('json not found ')
return
body = '[13, "c"]'
headers = [('Content-Type', 'application/json'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertBody('ok')
body = '[13, "c"]'
headers = [('Content-Type', 'text/plain'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertStatus(415, 'Expected an application/json content type')
body = '[13, -]'
headers = [('Content-Type', 'application/json'),
('Content-Length', str(len(body)))]
self.getPage('/json_post', method='POST', headers=headers, body=body)
self.assertStatus(400, 'Invalid JSON document')
def test_cached(self):
if json is None:
self.skip('json not found ')
return
self.getPage('/json_cached')
self.assertStatus(200, '"hello"')
self.getPage('/json_cached') # 2'nd time to hit cache
self.assertStatus(200, '"hello"')

View File

@@ -0,0 +1,209 @@
"""Basic tests for the CherryPy core: request handling."""
import os
from unittest import mock
import six
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper, logtest
localDir = os.path.dirname(__file__)
access_log = os.path.join(localDir, 'access.log')
error_log = os.path.join(localDir, 'error.log')
# Some unicode strings.
tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.expose
def uni_code(self):
cherrypy.request.login = tartaros
cherrypy.request.remote.name = erebos
@cherrypy.expose
def slashes(self):
cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
@cherrypy.expose
def whitespace(self):
# User-Agent = "User-Agent" ":" 1*( product | comment )
# comment = "(" *( ctext | quoted-pair | comment ) ")"
# ctext = <any TEXT excluding "(" and ")">
# TEXT = <any OCTET except CTLs, but including LWS>
# LWS = [CRLF] 1*( SP | HT )
cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
@cherrypy.expose
def as_string(self):
return 'content'
@cherrypy.expose
def as_yield(self):
yield 'content'
@cherrypy.expose
@cherrypy.config(**{'tools.log_tracebacks.on': True})
def error(self):
raise ValueError()
root = Root()
cherrypy.config.update({
'log.error_file': error_log,
'log.access_file': access_log,
})
cherrypy.tree.mount(root)
class AccessLogTests(helper.CPWebCase, logtest.LogCase):
setup_server = staticmethod(setup_server)
logfile = access_log
def testNormalReturn(self):
self.markLog()
self.getPage('/as_string',
headers=[('Referer', 'http://www.cherrypy.org/'),
('User-Agent', 'Mozilla/5.0')])
self.assertBody('content')
self.assertStatus(200)
intro = '%s - - [' % self.interface()
self.assertLog(-1, intro)
if [k for k, v in self.headers if k.lower() == 'content-length']:
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
'"http://www.cherrypy.org/" "Mozilla/5.0"'
% self.prefix())
else:
self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
'"http://www.cherrypy.org/" "Mozilla/5.0"'
% self.prefix())
def testNormalYield(self):
self.markLog()
self.getPage('/as_yield')
self.assertBody('content')
self.assertStatus(200)
intro = '%s - - [' % self.interface()
self.assertLog(-1, intro)
if [k for k, v in self.headers if k.lower() == 'content-length']:
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
self.prefix())
else:
self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
% self.prefix())
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
if six.PY3 else
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
)
def testCustomLogFormat(self):
"""Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access()."""
self.markLog()
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
('User-Agent', 'USERAGENT'),
('Host', 'HOST')])
self.assertLog(-1, '%s - - [' % self.interface())
self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
'200 7 "REFERER" "USERAGENT" HOST')
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
if six.PY3 else
'%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
)
def testTimezLogFormat(self):
"""Test a customized access_log_format string, which is a
feature of _cplogging.LogManager.access()."""
self.markLog()
expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
with mock.patch(
'cherrypy._cplogging.LazyRfc3339UtcTime',
lambda: expected_time):
self.getPage('/as_string', headers=[('Referer', 'REFERER'),
('User-Agent', 'USERAGENT'),
('Host', 'HOST')])
self.assertLog(-1, '%s - - ' % self.interface())
self.assertLog(-1, expected_time)
self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
'200 7 "REFERER" "USERAGENT" HOST')
@mock.patch(
'cherrypy._cplogging.LogManager.access_log_format',
'{i}' if six.PY3 else '%(i)s'
)
def testUUIDv4ParameterLogFormat(self):
"""Test rendering of UUID4 within access log."""
self.markLog()
self.getPage('/as_string')
self.assertValidUUIDv4()
def testEscapedOutput(self):
# Test unicode in access log pieces.
self.markLog()
self.getPage('/uni_code')
self.assertStatus(200)
if six.PY3:
# The repr of a bytestring in six.PY3 includes a b'' prefix
self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
else:
self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
# Test the erebos value. Included inline for your enlightenment.
# Note the 'r' prefix--those backslashes are literals.
self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
# Test backslashes in output.
self.markLog()
self.getPage('/slashes')
self.assertStatus(200)
if six.PY3:
self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
else:
self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
# Test whitespace in output.
self.markLog()
self.getPage('/whitespace')
self.assertStatus(200)
# Again, note the 'r' prefix.
self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
setup_server = staticmethod(setup_server)
logfile = error_log
def testTracebacks(self):
# Test that tracebacks get written to the error log.
self.markLog()
ignore = helper.webtest.ignored_exceptions
ignore.append(ValueError)
try:
self.getPage('/error')
self.assertInBody('raise ValueError()')
self.assertLog(0, 'HTTP')
self.assertLog(1, 'Traceback (most recent call last):')
self.assertLog(-2, 'raise ValueError()')
finally:
ignore.pop()

View File

@@ -0,0 +1,134 @@
"""Tests for various MIME issues, including the safe_multipart Tool."""
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.test import helper
def setup_server():
class Root:
@cherrypy.expose
def multipart(self, parts):
return repr(parts)
@cherrypy.expose
def multipart_form_data(self, **kwargs):
return repr(list(sorted(kwargs.items())))
@cherrypy.expose
def flashupload(self, Filedata, Upload, Filename):
return ('Upload: %s, Filename: %s, Filedata: %r' %
(Upload, Filename, Filedata.file.read()))
cherrypy.config.update({'server.max_request_body_size': 0})
cherrypy.tree.mount(Root())
# Client-side code #
class MultipartTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_multipart(self):
text_part = ntou('This is the text version')
html_part = ntou(
"""<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
</head>
<body bgcolor="#ffffff" text="#000000">
This is the <strong>HTML</strong> version
</body>
</html>
""")
body = '\r\n'.join([
'--123456789',
"Content-Type: text/plain; charset='ISO-8859-1'",
'Content-Transfer-Encoding: 7bit',
'',
text_part,
'--123456789',
"Content-Type: text/html; charset='ISO-8859-1'",
'',
html_part,
'--123456789--'])
headers = [
('Content-Type', 'multipart/mixed; boundary=123456789'),
('Content-Length', str(len(body))),
]
self.getPage('/multipart', headers, 'POST', body)
self.assertBody(repr([text_part, html_part]))
def test_multipart_form_data(self):
body = '\r\n'.join([
'--X',
'Content-Disposition: form-data; name="foo"',
'',
'bar',
'--X',
# Test a param with more than one value.
# See
# https://github.com/cherrypy/cherrypy/issues/1028
'Content-Disposition: form-data; name="baz"',
'',
'111',
'--X',
'Content-Disposition: form-data; name="baz"',
'',
'333',
'--X--'
])
self.getPage('/multipart_form_data', method='POST',
headers=[(
'Content-Type', 'multipart/form-data;boundary=X'),
('Content-Length', str(len(body))),
],
body=body),
self.assertBody(
repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
class SafeMultipartHandlingTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_Flash_Upload(self):
headers = [
('Accept', 'text/*'),
('Content-Type', 'multipart/form-data; '
'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
('User-Agent', 'Shockwave Flash'),
('Host', 'www.example.com:54583'),
('Content-Length', '499'),
('Connection', 'Keep-Alive'),
('Cache-Control', 'no-cache'),
]
filedata = (b'<?xml version="1.0" encoding="UTF-8"?>\r\n'
b'<projectDescription>\r\n'
b'</projectDescription>\r\n')
body = (
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; name="Filename"\r\n'
b'\r\n'
b'.project\r\n'
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; '
b'name="Filedata"; filename=".project"\r\n'
b'Content-Type: application/octet-stream\r\n'
b'\r\n' +
filedata +
b'\r\n'
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
b'Content-Disposition: form-data; name="Upload"\r\n'
b'\r\n'
b'Submit Query\r\n'
# Flash apps omit the trailing \r\n on the last line:
b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
)
self.getPage('/flashupload', headers, 'POST', body)
self.assertBody('Upload: Submit Query, Filename: .project, '
'Filedata: %r' % filedata)

View File

@@ -0,0 +1,210 @@
import os
import cherrypy
from cherrypy import tools
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
logfile = os.path.join(localDir, 'test_misc_tools.log')
def setup_server():
class Root:
@cherrypy.expose
def index(self):
yield 'Hello, world'
h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')]
tools.response_headers(headers=h)(index)
@cherrypy.expose
@cherrypy.config(**{
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Content-Language', 'fr'),
('Content-Type', 'text/plain'),
],
'tools.log_hooks.on': True,
})
def other(self):
return 'salut'
@cherrypy.config(**{'tools.accept.on': True})
class Accept:
@cherrypy.expose
def index(self):
return '<a href="feed">Atom feed</a>'
@cherrypy.expose
@tools.accept(media='application/atom+xml')
def feed(self):
return """<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Unknown Blog</title>
</feed>"""
@cherrypy.expose
def select(self):
# We could also write this: mtype = cherrypy.lib.accept.accept(...)
mtype = tools.accept.callable(['text/html', 'text/plain'])
if mtype == 'text/html':
return '<h2>Page Title</h2>'
else:
return 'PAGE TITLE'
class Referer:
@cherrypy.expose
def accept(self):
return 'Accepted!'
reject = accept
class AutoVary:
@cherrypy.expose
def index(self):
# Read a header directly with 'get'
cherrypy.request.headers.get('Accept-Encoding')
# Read a header directly with '__getitem__'
cherrypy.request.headers['Host']
# Read a header directly with '__contains__'
'If-Modified-Since' in cherrypy.request.headers
# Read a header directly
'Range' in cherrypy.request.headers
# Call a lib function
tools.accept.callable(['text/html', 'text/plain'])
return 'Hello, world!'
conf = {'/referer': {'tools.referer.on': True,
'tools.referer.pattern': r'http://[^/]*example\.com',
},
'/referer/reject': {'tools.referer.accept': False,
'tools.referer.accept_missing': True,
},
'/autovary': {'tools.autovary.on': True},
}
root = Root()
root.referer = Referer()
root.accept = Accept()
root.autovary = AutoVary()
cherrypy.tree.mount(root, config=conf)
cherrypy.config.update({'log.error_file': logfile})
class ResponseHeadersTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testResponseHeadersDecorator(self):
self.getPage('/')
self.assertHeader('Content-Language', 'en-GB')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
def testResponseHeaders(self):
self.getPage('/other')
self.assertHeader('Content-Language', 'fr')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
class RefererTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testReferer(self):
self.getPage('/referer/accept')
self.assertErrorPage(403, 'Forbidden Referer header.')
self.getPage('/referer/accept',
headers=[('Referer', 'http://www.example.com/')])
self.assertStatus(200)
self.assertBody('Accepted!')
# Reject
self.getPage('/referer/reject')
self.assertStatus(200)
self.assertBody('Accepted!')
self.getPage('/referer/reject',
headers=[('Referer', 'http://www.example.com/')])
self.assertErrorPage(403, 'Forbidden Referer header.')
class AcceptTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_Accept_Tool(self):
# Test with no header provided
self.getPage('/accept/feed')
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify exact media type
self.getPage('/accept/feed',
headers=[('Accept', 'application/atom+xml')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify matching media range
self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify all media ranges
self.getPage('/accept/feed', headers=[('Accept', '*/*')])
self.assertStatus(200)
self.assertInBody('<title>Unknown Blog</title>')
# Specify unacceptable media types
self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
self.assertErrorPage(406,
'Your client sent this Accept header: text/html. '
'But this resource only emits these media types: '
'application/atom+xml.')
# Test resource where tool is 'on' but media is None (not set).
self.getPage('/accept/')
self.assertStatus(200)
self.assertBody('<a href="feed">Atom feed</a>')
def test_accept_selection(self):
# Try both our expected media types
self.getPage('/accept/select', [('Accept', 'text/html')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
self.getPage('/accept/select', [('Accept', 'text/plain')])
self.assertStatus(200)
self.assertBody('PAGE TITLE')
self.getPage('/accept/select',
[('Accept', 'text/plain, text/*;q=0.5')])
self.assertStatus(200)
self.assertBody('PAGE TITLE')
# text/* and */* should prefer text/html since it comes first
# in our 'media' argument to tools.accept
self.getPage('/accept/select', [('Accept', 'text/*')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
self.getPage('/accept/select', [('Accept', '*/*')])
self.assertStatus(200)
self.assertBody('<h2>Page Title</h2>')
# Try unacceptable media types
self.getPage('/accept/select', [('Accept', 'application/xml')])
self.assertErrorPage(
406,
'Your client sent this Accept header: application/xml. '
'But this resource only emits these media types: '
'text/html, text/plain.')
class AutoVaryTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testAutoVary(self):
self.getPage('/autovary/')
self.assertHeader(
'Vary',
'Accept, Accept-Charset, Accept-Encoding, '
'Host, If-Modified-Since, Range'
)

View File

@@ -0,0 +1,38 @@
"""Test the native server."""
import pytest
from requests_toolbelt import sessions
import cherrypy._cpnative_server
pytestmark = pytest.mark.skipif(
'sys.platform == "win32"',
reason='tests fail on Windows',
)
@pytest.fixture
def cp_native_server(request):
"""A native server."""
class Root(object):
@cherrypy.expose
def index(self):
return 'Hello World!'
cls = cherrypy._cpnative_server.CPHTTPServer
cherrypy.server.httpserver = cls(cherrypy.server)
cherrypy.tree.mount(Root(), '/')
cherrypy.engine.start()
request.addfinalizer(cherrypy.engine.stop)
url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals())
return sessions.BaseUrlSession(url)
def test_basic_request(cp_native_server):
"""A request to a native server should succeed."""
resp = cp_native_server.get('/')
assert resp.ok
assert resp.status_code == 200
assert resp.text == 'Hello World!'

View File

@@ -0,0 +1,430 @@
import sys
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy._cptree import Application
from cherrypy.test import helper
script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
class ObjectMappingTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, name='world'):
return name
@cherrypy.expose
def foobar(self):
return 'bar'
@cherrypy.expose
def default(self, *params, **kwargs):
return 'default:' + repr(params)
@cherrypy.expose
def other(self):
return 'other'
@cherrypy.expose
def extra(self, *p):
return repr(p)
@cherrypy.expose
def redirect(self):
raise cherrypy.HTTPRedirect('dir1/', 302)
def notExposed(self):
return 'not exposed'
@cherrypy.expose
def confvalue(self):
return cherrypy.request.config.get('user')
@cherrypy.expose
def redirect_via_url(self, path):
raise cherrypy.HTTPRedirect(cherrypy.url(path))
@cherrypy.expose
def translate_html(self):
return 'OK'
@cherrypy.expose
def mapped_func(self, ID=None):
return 'ID is %s' % ID
setattr(Root, 'Von B\xfclow', mapped_func)
class Exposing:
@cherrypy.expose
def base(self):
return 'expose works!'
cherrypy.expose(base, '1')
cherrypy.expose(base, '2')
class ExposingNewStyle(object):
@cherrypy.expose
def base(self):
return 'expose works!'
cherrypy.expose(base, '1')
cherrypy.expose(base, '2')
class Dir1:
@cherrypy.expose
def index(self):
return 'index for dir1'
@cherrypy.expose
@cherrypy.config(**{'tools.trailing_slash.extra': True})
def myMethod(self):
return 'myMethod from dir1, path_info is:' + repr(
cherrypy.request.path_info)
@cherrypy.expose
def default(self, *params):
return 'default for dir1, param is:' + repr(params)
class Dir2:
@cherrypy.expose
def index(self):
return 'index for dir2, path is:' + cherrypy.request.path_info
@cherrypy.expose
def script_name(self):
return cherrypy.tree.script_name()
@cherrypy.expose
def cherrypy_url(self):
return cherrypy.url('/extra')
@cherrypy.expose
def posparam(self, *vpath):
return '/'.join(vpath)
class Dir3:
def default(self):
return 'default for dir3, not exposed'
class Dir4:
def index(self):
return 'index for dir4, not exposed'
class DefNoIndex:
@cherrypy.expose
def default(self, *args):
raise cherrypy.HTTPRedirect('contact')
# MethodDispatcher code
@cherrypy.expose
class ByMethod:
def __init__(self, *things):
self.things = list(things)
def GET(self):
return repr(self.things)
def POST(self, thing):
self.things.append(thing)
class Collection:
default = ByMethod('a', 'bit')
Root.exposing = Exposing()
Root.exposingnew = ExposingNewStyle()
Root.dir1 = Dir1()
Root.dir1.dir2 = Dir2()
Root.dir1.dir2.dir3 = Dir3()
Root.dir1.dir2.dir3.dir4 = Dir4()
Root.defnoindex = DefNoIndex()
Root.bymethod = ByMethod('another')
Root.collection = Collection()
d = cherrypy.dispatch.MethodDispatcher()
for url in script_names:
conf = {'/': {'user': (url or '/').split('/')[-2]},
'/bymethod': {'request.dispatch': d},
'/collection': {'request.dispatch': d},
}
cherrypy.tree.mount(Root(), url, conf)
class Isolated:
@cherrypy.expose
def index(self):
return 'made it!'
cherrypy.tree.mount(Isolated(), '/isolated')
@cherrypy.expose
class AnotherApp:
def GET(self):
return 'milk'
cherrypy.tree.mount(AnotherApp(), '/app',
{'/': {'request.dispatch': d}})
def testObjectMapping(self):
for url in script_names:
self.script_name = url
self.getPage('/')
self.assertBody('world')
self.getPage('/dir1/myMethod')
self.assertBody(
"myMethod from dir1, path_info is:'/dir1/myMethod'")
self.getPage('/this/method/does/not/exist')
self.assertBody(
"default:('this', 'method', 'does', 'not', 'exist')")
self.getPage('/extra/too/much')
self.assertBody("('too', 'much')")
self.getPage('/other')
self.assertBody('other')
self.getPage('/notExposed')
self.assertBody("default:('notExposed',)")
self.getPage('/dir1/dir2/')
self.assertBody('index for dir2, path is:/dir1/dir2/')
# Test omitted trailing slash (should be redirected by default).
self.getPage('/dir1/dir2')
self.assertStatus(301)
self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
# Test extra trailing slash (should be redirected if configured).
self.getPage('/dir1/myMethod/')
self.assertStatus(301)
self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
# Test that default method must be exposed in order to match.
self.getPage('/dir1/dir2/dir3/dir4/index')
self.assertBody(
"default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
# Test *vpath when default() is defined but not index()
# This also tests HTTPRedirect with default.
self.getPage('/defnoindex')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/contact' % self.base())
self.getPage('/defnoindex/')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/defnoindex/contact' %
self.base())
self.getPage('/defnoindex/page')
self.assertStatus((302, 303))
self.assertHeader('Location', '%s/defnoindex/contact' %
self.base())
self.getPage('/redirect')
self.assertStatus('302 Found')
self.assertHeader('Location', '%s/dir1/' % self.base())
if not getattr(cherrypy.server, 'using_apache', False):
# Test that we can use URL's which aren't all valid Python
# identifiers
# This should also test the %XX-unquoting of URL's.
self.getPage('/Von%20B%fclow?ID=14')
self.assertBody('ID is 14')
# Test that %2F in the path doesn't get unquoted too early;
# that is, it should not be used to separate path components.
# See ticket #393.
self.getPage('/page%2Fname')
self.assertBody("default:('page/name',)")
self.getPage('/dir1/dir2/script_name')
self.assertBody(url)
self.getPage('/dir1/dir2/cherrypy_url')
self.assertBody('%s/extra' % self.base())
# Test that configs don't overwrite each other from different apps
self.getPage('/confvalue')
self.assertBody((url or '/').split('/')[-2])
self.script_name = ''
# Test absoluteURI's in the Request-Line
self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
self.assertBody('world')
self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
(self.interface(), self.PORT))
self.assertBody("default:('abs',)")
self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
self.assertBody("default:('rel',)")
# Test that the "isolated" app doesn't leak url's into the root app.
# If it did leak, Root.default() would answer with
# "default:('isolated', 'doesnt', 'exist')".
self.getPage('/isolated/')
self.assertStatus('200 OK')
self.assertBody('made it!')
self.getPage('/isolated/doesnt/exist')
self.assertStatus('404 Not Found')
# Make sure /foobar maps to Root.foobar and not to the app
# mounted at /foo. See
# https://github.com/cherrypy/cherrypy/issues/573
self.getPage('/foobar')
self.assertBody('bar')
def test_translate(self):
self.getPage('/translate_html')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/translate.html')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/translate-html')
self.assertStatus('200 OK')
self.assertBody('OK')
def test_redir_using_url(self):
for url in script_names:
self.script_name = url
# Test the absolute path to the parent (leading slash)
self.getPage('/redirect_via_url?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the relative path to the parent (no leading slash)
self.getPage('/redirect_via_url?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the absolute path to the parent (leading slash)
self.getPage('/redirect_via_url/?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
# Test the relative path to the parent (no leading slash)
self.getPage('/redirect_via_url/?path=./')
self.assertStatus(('302 Found', '303 See Other'))
self.assertHeader('Location', '%s/' % self.base())
def testPositionalParams(self):
self.getPage('/dir1/dir2/posparam/18/24/hut/hike')
self.assertBody('18/24/hut/hike')
# intermediate index methods should not receive posparams;
# only the "final" index method should do so.
self.getPage('/dir1/dir2/5/3/sir')
self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
# test that extra positional args raises an 404 Not Found
# See https://github.com/cherrypy/cherrypy/issues/733.
self.getPage('/dir1/dir2/script_name/extra/stuff')
self.assertStatus(404)
def testExpose(self):
# Test the cherrypy.expose function/decorator
self.getPage('/exposing/base')
self.assertBody('expose works!')
self.getPage('/exposing/1')
self.assertBody('expose works!')
self.getPage('/exposing/2')
self.assertBody('expose works!')
self.getPage('/exposingnew/base')
self.assertBody('expose works!')
self.getPage('/exposingnew/1')
self.assertBody('expose works!')
self.getPage('/exposingnew/2')
self.assertBody('expose works!')
def testMethodDispatch(self):
self.getPage('/bymethod')
self.assertBody("['another']")
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='HEAD')
self.assertBody('')
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='POST', body='thing=one')
self.assertBody('')
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod')
self.assertBody(repr(['another', ntou('one')]))
self.assertHeader('Allow', 'GET, HEAD, POST')
self.getPage('/bymethod', method='PUT')
self.assertErrorPage(405)
self.assertHeader('Allow', 'GET, HEAD, POST')
# Test default with posparams
self.getPage('/collection/silly', method='POST')
self.getPage('/collection', method='GET')
self.assertBody("['a', 'bit', 'silly']")
# Test custom dispatcher set on app root (see #737).
self.getPage('/app')
self.assertBody('milk')
def testTreeMounting(self):
class Root(object):
@cherrypy.expose
def hello(self):
return 'Hello world!'
# When mounting an application instance,
# we can't specify a different script name in the call to mount.
a = Application(Root(), '/somewhere')
self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
# When mounting an application instance...
a = Application(Root(), '/somewhere')
# ...we MUST allow in identical script name in the call to mount...
cherrypy.tree.mount(a, '/somewhere')
self.getPage('/somewhere/hello')
self.assertStatus(200)
# ...and MUST allow a missing script_name.
del cherrypy.tree.apps['/somewhere']
cherrypy.tree.mount(a)
self.getPage('/somewhere/hello')
self.assertStatus(200)
# In addition, we MUST be able to create an Application using
# script_name == None for access to the wsgi_environ.
a = Application(Root(), script_name=None)
# However, this does not apply to tree.mount
self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
def testKeywords(self):
if sys.version_info < (3,):
return self.skip('skipped (Python 3 only)')
exec("""class Root(object):
@cherrypy.expose
def hello(self, *, name='world'):
return 'Hello %s!' % name
cherrypy.tree.mount(Application(Root(), '/keywords'))""")
self.getPage('/keywords/hello')
self.assertStatus(200)
self.getPage('/keywords/hello/extra')
self.assertStatus(404)

View File

@@ -0,0 +1,61 @@
import sys
import textwrap
import cherrypy
from cherrypy.test import helper
class ParamsTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
@cherrypy.tools.json_out()
@cherrypy.tools.params()
def resource(self, limit=None, sort=None):
return type(limit).__name__
# for testing on Py 2
resource.__annotations__ = {'limit': int}
conf = {'/': {'tools.params.on': True}}
cherrypy.tree.mount(Root(), config=conf)
def test_pass(self):
self.getPage('/resource')
self.assertStatus(200)
self.assertBody('"NoneType"')
self.getPage('/resource?limit=0')
self.assertStatus(200)
self.assertBody('"int"')
def test_error(self):
self.getPage('/resource?limit=')
self.assertStatus(400)
self.assertInBody('invalid literal for int')
cherrypy.config['tools.params.error'] = 422
self.getPage('/resource?limit=')
self.assertStatus(422)
self.assertInBody('invalid literal for int')
cherrypy.config['tools.params.exception'] = TypeError
self.getPage('/resource?limit=')
self.assertStatus(500)
def test_syntax(self):
if sys.version_info < (3,):
return self.skip('skipped (Python 3 only)')
code = textwrap.dedent("""
class Root:
@cherrypy.expose
@cherrypy.tools.params()
def resource(self, limit: int):
return type(limit).__name__
conf = {'/': {'tools.params.on': True}}
cherrypy.tree.mount(Root(), config=conf)
""")
exec(code)
self.getPage('/resource?limit=0')
self.assertStatus(200)
self.assertBody('int')

View File

@@ -0,0 +1,14 @@
from cherrypy.process import plugins
__metaclass__ = type
class TestAutoreloader:
def test_file_for_file_module_when_None(self):
"""No error when module.__file__ is None.
"""
class test_module:
__file__ = None
assert plugins.Autoreloader._file_for_file_module(test_module) is None

View File

@@ -0,0 +1,154 @@
import cherrypy
from cherrypy.test import helper
script_names = ['', '/path/to/myapp']
class ProxyTest(helper.CPWebCase):
@staticmethod
def setup_server():
# Set up site
cherrypy.config.update({
'tools.proxy.on': True,
'tools.proxy.base': 'www.mydomain.test',
})
# Set up application
class Root:
def __init__(self, sn):
# Calculate a URL outside of any requests.
self.thisnewpage = cherrypy.url(
'/this/new/page', script_name=sn)
@cherrypy.expose
def pageurl(self):
return self.thisnewpage
@cherrypy.expose
def index(self):
raise cherrypy.HTTPRedirect('dummy')
@cherrypy.expose
def remoteip(self):
return cherrypy.request.remote.ip
@cherrypy.expose
@cherrypy.config(**{
'tools.proxy.local': 'X-Host',
'tools.trailing_slash.extra': True,
})
def xhost(self):
raise cherrypy.HTTPRedirect('blah')
@cherrypy.expose
def base(self):
return cherrypy.request.base
@cherrypy.expose
@cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'})
def ssl(self):
return cherrypy.request.base
@cherrypy.expose
def newurl(self):
return ("Browse to <a href='%s'>this page</a>."
% cherrypy.url('/this/new/page'))
@cherrypy.expose
@cherrypy.config(**{
'tools.proxy.base': None,
})
def base_no_base(self):
return cherrypy.request.base
for sn in script_names:
cherrypy.tree.mount(Root(sn), sn)
def testProxy(self):
self.getPage('/')
self.assertHeader('Location',
'%s://www.mydomain.test%s/dummy' %
(self.scheme, self.prefix()))
# Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
self.getPage(
'/', headers=[('X-Forwarded-Host', 'http://www.example.test')])
self.assertHeader('Location', 'http://www.example.test/dummy')
self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/dummy' %
self.scheme)
# Test multiple X-Forwarded-Host headers
self.getPage('/', headers=[
('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
])
self.assertHeader('Location', 'http://www.example.test/dummy')
# Test X-Forwarded-For (Apache2)
self.getPage('/remoteip',
headers=[('X-Forwarded-For', '192.168.0.20')])
self.assertBody('192.168.0.20')
# Fix bug #1268
self.getPage('/remoteip',
headers=[
('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
])
self.assertBody('67.15.36.43')
# Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
self.getPage('/xhost', headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/blah' %
self.scheme)
# Test X-Forwarded-Proto (lighttpd)
self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')])
self.assertBody('https://www.mydomain.test')
# Test X-Forwarded-Ssl (webfaction?)
self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')])
self.assertBody('https://www.mydomain.test')
# Test cherrypy.url()
for sn in script_names:
# Test the value inside requests
self.getPage(sn + '/newurl')
self.assertBody(
"Browse to <a href='%s://www.mydomain.test" % self.scheme +
sn + "/this/new/page'>this page</a>.")
self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host',
'http://www.example.test')])
self.assertBody("Browse to <a href='http://www.example.test" +
sn + "/this/new/page'>this page</a>.")
# Test the value outside requests
port = ''
if self.scheme == 'http' and self.PORT != 80:
port = ':%s' % self.PORT
elif self.scheme == 'https' and self.PORT != 443:
port = ':%s' % self.PORT
host = self.HOST
if host in ('0.0.0.0', '::'):
import socket
host = socket.gethostname()
expected = ('%s://%s%s%s/this/new/page'
% (self.scheme, host, port, sn))
self.getPage(sn + '/pageurl')
self.assertBody(expected)
# Test trailing slash (see
# https://github.com/cherrypy/cherrypy/issues/562).
self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')])
self.assertHeader('Location', '%s://www.example.test/xhost'
% self.scheme)
def test_no_base_port_in_host(self):
"""
If no base is indicated, and the host header is used to resolve
the base, it should rely on the host header for the port also.
"""
headers = {'Host': 'localhost:8080'}.items()
self.getPage('/base_no_base', headers=headers)
self.assertBody('http://localhost:8080')

View File

@@ -0,0 +1,66 @@
"""Tests for refleaks."""
import itertools
import platform
import threading
from six.moves.http_client import HTTPConnection
import cherrypy
from cherrypy._cpcompat import HTTPSConnection
from cherrypy.test import helper
data = object()
class ReferenceTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self, *args, **kwargs):
cherrypy.request.thing = data
return 'Hello world!'
cherrypy.tree.mount(Root())
def test_threadlocal_garbage(self):
if platform.system() == 'Darwin':
self.skip('queue issues; see #1474')
success = itertools.count()
def getpage():
host = '%s:%s' % (self.interface(), self.PORT)
if self.scheme == 'https':
c = HTTPSConnection(host)
else:
c = HTTPConnection(host)
try:
c.putrequest('GET', '/')
c.endheaders()
response = c.getresponse()
body = response.read()
self.assertEqual(response.status, 200)
self.assertEqual(body, b'Hello world!')
finally:
c.close()
next(success)
ITERATIONS = 25
ts = [
threading.Thread(target=getpage)
for _ in range(ITERATIONS)
]
for t in ts:
t.start()
for t in ts:
t.join()
self.assertEqual(next(success), ITERATIONS)

View File

@@ -0,0 +1,932 @@
"""Basic tests for the cherrypy.Request object."""
from functools import wraps
import os
import sys
import types
import uuid
import six
from six.moves.http_client import IncompleteRead
import cherrypy
from cherrypy._cpcompat import ntou
from cherrypy.lib import httputil
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
'TRACE', 'PROPFIND', 'PATCH')
# Client-side code #
class RequestObjectTests(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'hello'
@cherrypy.expose
def scheme(self):
return cherrypy.request.scheme
@cherrypy.expose
def created_example_com_3128(self):
"""Handle CONNECT method."""
cherrypy.response.status = 204
@cherrypy.expose
def body_example_com_3128(self):
"""Handle CONNECT method."""
return (
cherrypy.request.method
+ 'ed to '
+ cherrypy.request.path_info
)
@cherrypy.expose
def request_uuid4(self):
return [
str(cherrypy.request.unique_id),
' ',
str(cherrypy.request.unique_id),
]
root = Root()
class TestType(type):
"""Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute
of root.
"""
def __init__(cls, name, bases, dct):
type.__init__(cls, name, bases, dct)
for value in dct.values():
if isinstance(value, types.FunctionType):
value.exposed = True
setattr(root, name.lower(), cls())
Test = TestType('Test', (object,), {})
class PathInfo(Test):
def default(self, *args):
return cherrypy.request.path_info
class Params(Test):
def index(self, thing):
return repr(thing)
def ismap(self, x, y):
return 'Coordinates: %s, %s' % (x, y)
@cherrypy.config(**{'request.query_string_encoding': 'latin1'})
def default(self, *args, **kwargs):
return 'args: %s kwargs: %s' % (args, sorted(kwargs.items()))
@cherrypy.expose
class ParamErrorsCallable(object):
def __call__(self):
return 'data'
def handler_dec(f):
@wraps(f)
def wrapper(handler, *args, **kwargs):
return f(handler, *args, **kwargs)
return wrapper
class ParamErrors(Test):
@cherrypy.expose
def one_positional(self, param1):
return 'data'
@cherrypy.expose
def one_positional_args(self, param1, *args):
return 'data'
@cherrypy.expose
def one_positional_args_kwargs(self, param1, *args, **kwargs):
return 'data'
@cherrypy.expose
def one_positional_kwargs(self, param1, **kwargs):
return 'data'
@cherrypy.expose
def no_positional(self):
return 'data'
@cherrypy.expose
def no_positional_args(self, *args):
return 'data'
@cherrypy.expose
def no_positional_args_kwargs(self, *args, **kwargs):
return 'data'
@cherrypy.expose
def no_positional_kwargs(self, **kwargs):
return 'data'
callable_object = ParamErrorsCallable()
@cherrypy.expose
def raise_type_error(self, **kwargs):
raise TypeError('Client Error')
@cherrypy.expose
def raise_type_error_with_default_param(self, x, y=None):
return '%d' % 'a' # throw an exception
@cherrypy.expose
@handler_dec
def raise_type_error_decorated(self, *args, **kwargs):
raise TypeError('Client Error')
def callable_error_page(status, **kwargs):
return "Error %s - Well, I'm very sorry but you haven't paid!" % (
status)
@cherrypy.config(**{'tools.log_tracebacks.on': True})
class Error(Test):
def reason_phrase(self):
raise cherrypy.HTTPError("410 Gone fishin'")
@cherrypy.config(**{
'error_page.404': os.path.join(localDir, 'static/index.html'),
'error_page.401': callable_error_page,
})
def custom(self, err='404'):
raise cherrypy.HTTPError(
int(err), 'No, <b>really</b>, not found!')
@cherrypy.config(**{
'error_page.default': callable_error_page,
})
def custom_default(self):
return 1 + 'a' # raise an unexpected error
@cherrypy.config(**{'error_page.404': 'nonexistent.html'})
def noexist(self):
raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!')
def page_method(self):
raise ValueError()
def page_yield(self):
yield 'howdy'
raise ValueError()
@cherrypy.config(**{'response.stream': True})
def page_streamed(self):
yield 'word up'
raise ValueError()
yield 'very oops'
@cherrypy.config(**{'request.show_tracebacks': False})
def cause_err_in_finalize(self):
# Since status must start with an int, this should error.
cherrypy.response.status = 'ZOO OK'
@cherrypy.config(**{'request.throw_errors': True})
def rethrow(self):
"""Test that an error raised here will be thrown out to
the server.
"""
raise ValueError()
class Expect(Test):
def expectation_failed(self):
expect = cherrypy.request.headers.elements('Expect')
if expect and expect[0].value != '100-continue':
raise cherrypy.HTTPError(400)
raise cherrypy.HTTPError(417, 'Expectation Failed')
class Headers(Test):
def default(self, headername):
"""Spit back out the value for the requested header."""
return cherrypy.request.headers[headername]
def doubledheaders(self):
# From https://github.com/cherrypy/cherrypy/issues/165:
# "header field names should not be case sensitive sayes the
# rfc. if i set a headerfield in complete lowercase i end up
# with two header fields, one in lowercase, the other in
# mixed-case."
# Set the most common headers
hMap = cherrypy.response.headers
hMap['content-type'] = 'text/html'
hMap['content-length'] = 18
hMap['server'] = 'CherryPy headertest'
hMap['location'] = ('%s://%s:%s/headers/'
% (cherrypy.request.local.ip,
cherrypy.request.local.port,
cherrypy.request.scheme))
# Set a rare header for fun
hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
return 'double header test'
def ifmatch(self):
val = cherrypy.request.headers['If-Match']
assert isinstance(val, six.text_type)
cherrypy.response.headers['ETag'] = val
return val
class HeaderElements(Test):
def get_elements(self, headername):
e = cherrypy.request.headers.elements(headername)
return '\n'.join([six.text_type(x) for x in e])
class Method(Test):
def index(self):
m = cherrypy.request.method
if m in defined_http_methods or m == 'CONNECT':
return m
if m == 'LINK':
raise cherrypy.HTTPError(405)
else:
raise cherrypy.HTTPError(501)
def parameterized(self, data):
return data
def request_body(self):
# This should be a file object (temp file),
# which CP will just pipe back out if we tell it to.
return cherrypy.request.body
def reachable(self):
return 'success'
class Divorce(Test):
"""HTTP Method handlers shouldn't collide with normal method names.
For example, a GET-handler shouldn't collide with a method named
'get'.
If you build HTTP method dispatching into CherryPy, rewrite this
class to use your new dispatch mechanism and make sure that:
"GET /divorce HTTP/1.1" maps to divorce.index() and
"GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
"""
documents = {}
@cherrypy.expose
def index(self):
yield '<h1>Choose your document</h1>\n'
yield '<ul>\n'
for id, contents in self.documents.items():
yield (
" <li><a href='/divorce/get?ID=%s'>%s</a>:"
' %s</li>\n' % (id, id, contents))
yield '</ul>'
@cherrypy.expose
def get(self, ID):
return ('Divorce document %s: %s' %
(ID, self.documents.get(ID, 'empty')))
class ThreadLocal(Test):
def index(self):
existing = repr(getattr(cherrypy.request, 'asdf', None))
cherrypy.request.asdf = 'rassfrassin'
return existing
appconf = {
'/method': {
'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND',
'PATCH')
},
}
cherrypy.tree.mount(root, config=appconf)
def test_scheme(self):
self.getPage('/scheme')
self.assertBody(self.scheme)
def test_per_request_uuid4(self):
self.getPage('/request_uuid4')
first_uuid4, _, second_uuid4 = self.body.decode().partition(' ')
assert (
uuid.UUID(first_uuid4, version=4)
== uuid.UUID(second_uuid4, version=4)
)
self.getPage('/request_uuid4')
third_uuid4, _, _ = self.body.decode().partition(' ')
assert (
uuid.UUID(first_uuid4, version=4)
!= uuid.UUID(third_uuid4, version=4)
)
def testRelativeURIPathInfo(self):
self.getPage('/pathinfo/foo/bar')
self.assertBody('/pathinfo/foo/bar')
def testAbsoluteURIPathInfo(self):
# http://cherrypy.org/ticket/1061
self.getPage('http://localhost/pathinfo/foo/bar')
self.assertBody('/pathinfo/foo/bar')
def testParams(self):
self.getPage('/params/?thing=a')
self.assertBody(repr(ntou('a')))
self.getPage('/params/?thing=a&thing=b&thing=c')
self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')]))
# Test friendly error message when given params are not accepted.
cherrypy.config.update({'request.show_mismatched_params': True})
self.getPage('/params/?notathing=meeting')
self.assertInBody('Missing parameters: thing')
self.getPage('/params/?thing=meeting&notathing=meeting')
self.assertInBody('Unexpected query string parameters: notathing')
# Test ability to turn off friendly error messages
cherrypy.config.update({'request.show_mismatched_params': False})
self.getPage('/params/?notathing=meeting')
self.assertInBody('Not Found')
self.getPage('/params/?thing=meeting&notathing=meeting')
self.assertInBody('Not Found')
# Test "% HEX HEX"-encoded URL, param keys, and values
self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville')
self.assertBody('args: %s kwargs: %s' %
(('\xd4 \xe3', 'cheese'),
[('Gruy\xe8re', ntou('Bulgn\xe9ville'))]))
# Make sure that encoded = and & get parsed correctly
self.getPage(
'/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2')
self.assertBody('args: %s kwargs: %s' %
(('code',),
[('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
# Test coordinates sent by <img ismap>
self.getPage('/params/ismap?223,114')
self.assertBody('Coordinates: 223, 114')
# Test "name[key]" dict-like params
self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz')
self.assertBody('args: %s kwargs: %s' %
(('dictlike',),
[('a[1]', ntou('1')), ('a[2]', ntou('2')),
('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
def testParamErrors(self):
# test that all of the handlers work when given
# the correct parameters in order to ensure that the
# errors below aren't coming from some other source.
for uri in (
'/paramerrors/one_positional?param1=foo',
'/paramerrors/one_positional_args?param1=foo',
'/paramerrors/one_positional_args/foo',
'/paramerrors/one_positional_args/foo/bar/baz',
'/paramerrors/one_positional_args_kwargs?'
'param1=foo&param2=bar',
'/paramerrors/one_positional_args_kwargs/foo?'
'param2=bar&param3=baz',
'/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
'param2=bar&param3=baz',
'/paramerrors/one_positional_kwargs?'
'param1=foo&param2=bar&param3=baz',
'/paramerrors/one_positional_kwargs/foo?'
'param4=foo&param2=bar&param3=baz',
'/paramerrors/no_positional',
'/paramerrors/no_positional_args/foo',
'/paramerrors/no_positional_args/foo/bar/baz',
'/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
'/paramerrors/no_positional_args_kwargs/foo?param2=bar',
'/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
'param2=bar&param3=baz',
'/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
'/paramerrors/callable_object',
):
self.getPage(uri)
self.assertStatus(200)
error_msgs = [
'Missing parameters',
'Nothing matches the given URI',
'Multiple values for parameters',
'Unexpected query string parameters',
'Unexpected body parameters',
'Invalid path in Request-URI',
'Illegal #fragment in Request-URI',
]
# uri should be tested for valid absolute path, the status must be 400.
for uri, error_idx in (
('invalid/path/without/leading/slash', 5),
('/valid/path#invalid=fragment', 6),
):
self.getPage(uri)
self.assertStatus(400)
self.assertInBody(error_msgs[error_idx])
# query string parameters are part of the URI, so if they are wrong
# for a particular handler, the status MUST be a 404.
for uri, msg in (
('/paramerrors/one_positional', error_msgs[0]),
('/paramerrors/one_positional?foo=foo', error_msgs[0]),
('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
('/paramerrors/one_positional/foo?param1=foo&param2=foo',
error_msgs[2]),
('/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
error_msgs[2]),
('/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
error_msgs[3]),
('/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
'param1=bar&param3=baz',
error_msgs[2]),
('/paramerrors/one_positional_kwargs/foo?'
'param1=foo&param2=bar&param3=baz',
error_msgs[2]),
('/paramerrors/no_positional/boo', error_msgs[1]),
('/paramerrors/no_positional?param1=foo', error_msgs[3]),
('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
('/paramerrors/no_positional_kwargs/boo?param1=foo',
error_msgs[1]),
('/paramerrors/callable_object?param1=foo', error_msgs[3]),
('/paramerrors/callable_object/boo', error_msgs[1]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri)
self.assertStatus(404)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('Not Found')
# if body parameters are wrong, a 400 must be returned.
for uri, body, msg in (
('/paramerrors/one_positional/foo',
'param1=foo', error_msgs[2]),
('/paramerrors/one_positional/foo',
'param1=foo&param2=foo', error_msgs[2]),
('/paramerrors/one_positional_args/foo',
'param1=foo&param2=foo', error_msgs[2]),
('/paramerrors/one_positional_args/foo/bar/baz',
'param2=foo', error_msgs[4]),
('/paramerrors/one_positional_args_kwargs/foo/bar/baz',
'param1=bar&param3=baz', error_msgs[2]),
('/paramerrors/one_positional_kwargs/foo',
'param1=foo&param2=bar&param3=baz', error_msgs[2]),
('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
('/paramerrors/no_positional_args/boo',
'param1=foo', error_msgs[4]),
('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri, method='POST', body=body)
self.assertStatus(400)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('400 Bad')
# even if body parameters are wrong, if we get the uri wrong, then
# it's a 404
for uri, body, msg in (
('/paramerrors/one_positional?param2=foo',
'param1=foo', error_msgs[3]),
('/paramerrors/one_positional/foo/bar',
'param2=foo', error_msgs[1]),
('/paramerrors/one_positional_args/foo/bar?param2=foo',
'param3=foo', error_msgs[3]),
('/paramerrors/one_positional_kwargs/foo/bar',
'param2=bar&param3=baz', error_msgs[1]),
('/paramerrors/no_positional?param1=foo',
'param2=foo', error_msgs[3]),
('/paramerrors/no_positional_args/boo?param2=foo',
'param1=foo', error_msgs[3]),
('/paramerrors/callable_object?param2=bar',
'param1=foo', error_msgs[3]),
):
for show_mismatched_params in (True, False):
cherrypy.config.update(
{'request.show_mismatched_params': show_mismatched_params})
self.getPage(uri, method='POST', body=body)
self.assertStatus(404)
if show_mismatched_params:
self.assertInBody(msg)
else:
self.assertInBody('Not Found')
# In the case that a handler raises a TypeError we should
# let that type error through.
for uri in (
'/paramerrors/raise_type_error',
'/paramerrors/raise_type_error_with_default_param?x=0',
'/paramerrors/raise_type_error_with_default_param?x=0&y=0',
'/paramerrors/raise_type_error_decorated',
):
self.getPage(uri, method='GET')
self.assertStatus(500)
self.assertTrue('Client Error', self.body)
def testErrorHandling(self):
self.getPage('/error/missing')
self.assertStatus(404)
self.assertErrorPage(404, "The path '/error/missing' was not found.")
ignore = helper.webtest.ignored_exceptions
ignore.append(ValueError)
try:
valerr = '\n raise ValueError()\nValueError'
self.getPage('/error/page_method')
self.assertErrorPage(500, pattern=valerr)
self.getPage('/error/page_yield')
self.assertErrorPage(500, pattern=valerr)
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
getattr(cherrypy.server, 'using_apache', False)):
self.getPage('/error/page_streamed')
# Because this error is raised after the response body has
# started, the status should not change to an error status.
self.assertStatus(200)
self.assertBody('word up')
else:
# Under HTTP/1.1, the chunked transfer-coding is used.
# The HTTP client will choke when the output is incomplete.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
'/error/page_streamed')
# No traceback should be present
self.getPage('/error/cause_err_in_finalize')
msg = "Illegal response status from server ('ZOO' is non-numeric)."
self.assertErrorPage(500, msg, None)
finally:
ignore.pop()
# Test HTTPError with a reason-phrase in the status arg.
self.getPage('/error/reason_phrase')
self.assertStatus("410 Gone fishin'")
# Test custom error page for a specific error.
self.getPage('/error/custom')
self.assertStatus(404)
self.assertBody('Hello, world\r\n' + (' ' * 499))
# Test custom error page for a specific error.
self.getPage('/error/custom?err=401')
self.assertStatus(401)
self.assertBody(
'Error 401 Unauthorized - '
"Well, I'm very sorry but you haven't paid!")
# Test default custom error page.
self.getPage('/error/custom_default')
self.assertStatus(500)
self.assertBody(
'Error 500 Internal Server Error - '
"Well, I'm very sorry but you haven't paid!".ljust(513))
# Test error in custom error page (ticket #305).
# Note that the message is escaped for HTML (ticket #310).
self.getPage('/error/noexist')
self.assertStatus(404)
if sys.version_info >= (3, 3):
exc_name = 'FileNotFoundError'
else:
exc_name = 'IOError'
msg = ('No, &lt;b&gt;really&lt;/b&gt;, not found!<br />'
'In addition, the custom error page failed:\n<br />'
'%s: [Errno 2] '
"No such file or directory: 'nonexistent.html'") % (exc_name,)
self.assertInBody(msg)
if getattr(cherrypy.server, 'using_apache', False):
pass
else:
# Test throw_errors (ticket #186).
self.getPage('/error/rethrow')
self.assertInBody('raise ValueError()')
def testExpect(self):
e = ('Expect', '100-continue')
self.getPage('/headerelements/get_elements?headername=Expect', [e])
self.assertBody('100-continue')
self.getPage('/expect/expectation_failed', [e])
self.assertStatus(417)
def testHeaderElements(self):
# Accept-* header elements should be sorted, with most preferred first.
h = [('Accept', 'audio/*; q=0.2, audio/basic')]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('audio/basic\n'
'audio/*;q=0.2')
h = [
('Accept',
'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('text/x-c\n'
'text/html\n'
'text/x-dvi;q=0.8\n'
'text/plain;q=0.5')
# Test that more specific media ranges get priority.
h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
self.getPage('/headerelements/get_elements?headername=Accept', h)
self.assertStatus(200)
self.assertBody('text/html;level=1\n'
'text/html\n'
'text/*\n'
'*/*')
# Test Accept-Charset
h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Charset', h)
self.assertStatus('200 OK')
self.assertBody('iso-8859-5\n'
'unicode-1-1;q=0.8')
# Test Accept-Encoding
h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Encoding', h)
self.assertStatus('200 OK')
self.assertBody('gzip;q=1.0\n'
'identity;q=0.5\n'
'*;q=0')
# Test Accept-Language
h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
self.getPage(
'/headerelements/get_elements?headername=Accept-Language', h)
self.assertStatus('200 OK')
self.assertBody('da\n'
'en-gb;q=0.8\n'
'en;q=0.7')
# Test malformed header parsing. See
# https://github.com/cherrypy/cherrypy/issues/763.
self.getPage('/headerelements/get_elements?headername=Content-Type',
# Note the illegal trailing ";"
headers=[('Content-Type', 'text/html; charset=utf-8;')])
self.assertStatus(200)
self.assertBody('text/html;charset=utf-8')
def test_repeated_headers(self):
# Test that two request headers are collapsed into one.
# See https://github.com/cherrypy/cherrypy/issues/542.
self.getPage('/headers/Accept-Charset',
headers=[('Accept-Charset', 'iso-8859-5'),
('Accept-Charset', 'unicode-1-1;q=0.8')])
self.assertBody('iso-8859-5, unicode-1-1;q=0.8')
# Tests that each header only appears once, regardless of case.
self.getPage('/headers/doubledheaders')
self.assertBody('double header test')
hnames = [name.title() for name, val in self.headers]
for key in ['Content-Length', 'Content-Type', 'Date',
'Expires', 'Location', 'Server']:
self.assertEqual(hnames.count(key), 1, self.headers)
def test_encoded_headers(self):
# First, make sure the innards work like expected.
self.assertEqual(
httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr'))
if cherrypy.server.protocol_version == 'HTTP/1.1':
# Test RFC-2047-encoded request and response header values
u = ntou('\u212bngstr\xf6m', 'escape')
c = ntou('=E2=84=ABngstr=C3=B6m')
self.getPage('/headers/ifmatch',
[('If-Match', ntou('=?utf-8?q?%s?=') % c)])
# The body should be utf-8 encoded.
self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m')
# But the Etag header should be RFC-2047 encoded (binary)
self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?='))
# Test a *LONG* RFC-2047-encoded request and response header value
self.getPage('/headers/ifmatch',
[('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))])
self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10)
# Note: this is different output for Python3, but it decodes fine.
etag = self.assertHeader(
'ETag',
'=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
'4oSrbmdzdHLDtm0=?=')
self.assertEqual(httputil.decode_TEXT(etag), u * 10)
def test_header_presence(self):
# If we don't pass a Content-Type header, it should not be present
# in cherrypy.request.headers
self.getPage('/headers/Content-Type',
headers=[])
self.assertStatus(500)
# If Content-Type is present in the request, it should be present in
# cherrypy.request.headers
self.getPage('/headers/Content-Type',
headers=[('Content-type', 'application/json')])
self.assertBody('application/json')
def test_basic_HTTPMethods(self):
helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND',
'PATCH')
# Test that all defined HTTP methods work.
for m in defined_http_methods:
self.getPage('/method/', method=m)
# HEAD requests should not return any body.
if m == 'HEAD':
self.assertBody('')
elif m == 'TRACE':
# Some HTTP servers (like modpy) have their own TRACE support
self.assertEqual(self.body[:5], b'TRACE')
else:
self.assertBody(m)
# test of PATCH requests
# Request a PATCH method with a form-urlencoded body
self.getPage('/method/parameterized', method='PATCH',
body='data=on+top+of+other+things')
self.assertBody('on top of other things')
# Request a PATCH method with a file body
b = 'one thing on top of another'
h = [('Content-Type', 'text/plain'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h, method='PATCH', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a PATCH method with a file body but no Content-Type.
# See https://github.com/cherrypy/cherrypy/issues/790.
b = b'one thing on top of another'
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest('PATCH', '/method/request_body', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Length', str(len(b)))
conn.endheaders()
conn.send(b)
response = conn.response_class(conn.sock, method='PATCH')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b)
finally:
self.persistent = False
# Request a PATCH method with no body whatsoever (not an empty one).
# See https://github.com/cherrypy/cherrypy/issues/650.
# Provide a C-T or webtest will provide one (and a C-L) for us.
h = [('Content-Type', 'text/plain')]
self.getPage('/method/reachable', headers=h, method='PATCH')
self.assertStatus(411)
# HTTP PUT tests
# Request a PUT method with a form-urlencoded body
self.getPage('/method/parameterized', method='PUT',
body='data=on+top+of+other+things')
self.assertBody('on top of other things')
# Request a PUT method with a file body
b = 'one thing on top of another'
h = [('Content-Type', 'text/plain'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h, method='PUT', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a PUT method with a file body but no Content-Type.
# See https://github.com/cherrypy/cherrypy/issues/790.
b = b'one thing on top of another'
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest('PUT', '/method/request_body', skip_host=True)
conn.putheader('Host', self.HOST)
conn.putheader('Content-Length', str(len(b)))
conn.endheaders()
conn.send(b)
response = conn.response_class(conn.sock, method='PUT')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b)
finally:
self.persistent = False
# Request a PUT method with no body whatsoever (not an empty one).
# See https://github.com/cherrypy/cherrypy/issues/650.
# Provide a C-T or webtest will provide one (and a C-L) for us.
h = [('Content-Type', 'text/plain')]
self.getPage('/method/reachable', headers=h, method='PUT')
self.assertStatus(411)
# Request a custom method with a request body
b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
'<propfind xmlns="DAV:"><prop><getlastmodified/>'
'</prop></propfind>')
h = [('Content-Type', 'text/xml'),
('Content-Length', str(len(b)))]
self.getPage('/method/request_body', headers=h,
method='PROPFIND', body=b)
self.assertStatus(200)
self.assertBody(b)
# Request a disallowed method
self.getPage('/method/', method='LINK')
self.assertStatus(405)
# Request an unknown method
self.getPage('/method/', method='SEARCH')
self.assertStatus(501)
# For method dispatchers: make sure that an HTTP method doesn't
# collide with a virtual path atom. If you build HTTP-method
# dispatching into the core, rewrite these handlers to use
# your dispatch idioms.
self.getPage('/divorce/get?ID=13')
self.assertBody('Divorce document 13: empty')
self.assertStatus(200)
self.getPage('/divorce/', method='GET')
self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
self.assertStatus(200)
def test_CONNECT_method(self):
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', 'created.example.com:3128')
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 204)
finally:
self.persistent = False
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', 'body.example.com:3128')
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 200)
self.body = response.read()
self.assertBody(b'CONNECTed to /body.example.com:3128')
finally:
self.persistent = False
def test_CONNECT_method_invalid_authority(self):
for request_target in ['example.com', 'http://example.com:33',
'/path/', 'path/', '/?q=f', '#f']:
self.persistent = True
try:
conn = self.HTTP_CONN
conn.request('CONNECT', request_target)
response = conn.response_class(conn.sock, method='CONNECT')
response.begin()
self.assertEqual(response.status, 400)
self.body = response.read()
self.assertBody(b'Invalid path in Request-URI: request-target '
b'must match authority-form.')
finally:
self.persistent = False
def testEmptyThreadlocals(self):
results = []
for x in range(20):
self.getPage('/threadlocal/')
results.append(self.body)
self.assertEqual(results, [b'None'] * 20)

View File

@@ -0,0 +1,80 @@
"""Test Routes dispatcher."""
import os
import importlib
import pytest
import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class RoutesDispatchTest(helper.CPWebCase):
"""Routes dispatcher test suite."""
@staticmethod
def setup_server():
"""Set up cherrypy test instance."""
try:
importlib.import_module('routes')
except ImportError:
pytest.skip('Install routes to test RoutesDispatcher code')
class Dummy:
def index(self):
return 'I said good day!'
class City:
def __init__(self, name):
self.name = name
self.population = 10000
@cherrypy.config(**{
'tools.response_headers.on': True,
'tools.response_headers.headers': [
('Content-Language', 'en-GB'),
],
})
def index(self, **kwargs):
return 'Welcome to %s, pop. %s' % (self.name, self.population)
def update(self, **kwargs):
self.population = kwargs['pop']
return 'OK'
d = cherrypy.dispatch.RoutesDispatcher()
d.connect(action='index', name='hounslow', route='/hounslow',
controller=City('Hounslow'))
d.connect(
name='surbiton', route='/surbiton', controller=City('Surbiton'),
action='index', conditions=dict(method=['GET']))
d.mapper.connect('/surbiton', controller='surbiton',
action='update', conditions=dict(method=['POST']))
d.connect('main', ':action', controller=Dummy())
conf = {'/': {'request.dispatch': d}}
cherrypy.tree.mount(root=None, config=conf)
def test_Routes_Dispatch(self):
"""Check that routes package based URI dispatching works correctly."""
self.getPage('/hounslow')
self.assertStatus('200 OK')
self.assertBody('Welcome to Hounslow, pop. 10000')
self.getPage('/foo')
self.assertStatus('404 Not Found')
self.getPage('/surbiton')
self.assertStatus('200 OK')
self.assertBody('Welcome to Surbiton, pop. 10000')
self.getPage('/surbiton', method='POST', body='pop=1327')
self.assertStatus('200 OK')
self.assertBody('OK')
self.getPage('/surbiton')
self.assertStatus('200 OK')
self.assertHeader('Content-Language', 'en-GB')
self.assertBody('Welcome to Surbiton, pop. 1327')

View File

@@ -0,0 +1,512 @@
import os
import threading
import time
import socket
import importlib
from six.moves.http_client import HTTPConnection
import pytest
from path import Path
import cherrypy
from cherrypy._cpcompat import (
json_decode,
HTTPSConnection,
)
from cherrypy.lib import sessions
from cherrypy.lib import reprconf
from cherrypy.lib.httputil import response_codes
from cherrypy.test import helper
localDir = os.path.dirname(__file__)
def http_methods_allowed(methods=['GET', 'HEAD']):
method = cherrypy.request.method.upper()
if method not in methods:
cherrypy.response.headers['Allow'] = ', '.join(methods)
raise cherrypy.HTTPError(405)
cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
def setup_server():
@cherrypy.config(**{
'tools.sessions.on': True,
'tools.sessions.storage_class': sessions.RamSession,
'tools.sessions.storage_path': localDir,
'tools.sessions.timeout': (1.0 / 60),
'tools.sessions.clean_freq': (1.0 / 60),
})
class Root:
@cherrypy.expose
def clear(self):
cherrypy.session.cache.clear()
@cherrypy.expose
def data(self):
cherrypy.session['aha'] = 'foo'
return repr(cherrypy.session._data)
@cherrypy.expose
def testGen(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
yield str(counter)
@cherrypy.expose
def testStr(self):
counter = cherrypy.session.get('counter', 0) + 1
cherrypy.session['counter'] = counter
return str(counter)
@cherrypy.expose
@cherrypy.config(**{'tools.sessions.on': False})
def set_session_cls(self, new_cls_name):
new_cls = reprconf.attributes(new_cls_name)
cfg = {'tools.sessions.storage_class': new_cls}
self.__class__._cp_config.update(cfg)
if hasattr(cherrypy, 'session'):
del cherrypy.session
if new_cls.clean_thread:
new_cls.clean_thread.stop()
new_cls.clean_thread.unsubscribe()
del new_cls.clean_thread
@cherrypy.expose
def index(self):
sess = cherrypy.session
c = sess.get('counter', 0) + 1
time.sleep(0.01)
sess['counter'] = c
return str(c)
@cherrypy.expose
def keyin(self, key):
return str(key in cherrypy.session)
@cherrypy.expose
def delete(self):
cherrypy.session.delete()
sessions.expire()
return 'done'
@cherrypy.expose
def delkey(self, key):
del cherrypy.session[key]
return 'OK'
@cherrypy.expose
def redir_target(self):
return self._cp_config['tools.sessions.storage_class'].__name__
@cherrypy.expose
def iredir(self):
raise cherrypy.InternalRedirect('/redir_target')
@cherrypy.expose
@cherrypy.config(**{
'tools.allow.on': True,
'tools.allow.methods': ['GET'],
})
def restricted(self):
return cherrypy.request.method
@cherrypy.expose
def regen(self):
cherrypy.tools.sessions.regenerate()
return 'logged in'
@cherrypy.expose
def length(self):
return str(len(cherrypy.session))
@cherrypy.expose
@cherrypy.config(**{
'tools.sessions.path': '/session_cookie',
'tools.sessions.name': 'temp',
'tools.sessions.persistent': False,
})
def session_cookie(self):
# Must load() to start the clean thread.
cherrypy.session.load()
return cherrypy.session.id
cherrypy.tree.mount(Root())
class SessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def tearDown(self):
# Clean up sessions.
for fname in os.listdir(localDir):
if fname.startswith(sessions.FileSession.SESSION_PREFIX):
path = Path(localDir) / fname
path.remove_p()
@pytest.mark.xfail(reason='#1534')
def test_0_Session(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self.getPage('/clear')
# Test that a normal request gets the same id in the cookies.
# Note: this wouldn't work if /data didn't load the session.
self.getPage('/data')
self.assertBody("{'aha': 'foo'}")
c = self.cookies[0]
self.getPage('/data', self.cookies)
self.assertEqual(self.cookies[0], c)
self.getPage('/testStr')
self.assertBody('1')
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is an 'expires' param
self.assertEqual(set(cookie_parts.keys()),
set(['session_id', 'expires', 'Path']))
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/data', self.cookies)
self.assertDictEqual(json_decode(self.body),
{'counter': 3, 'aha': 'foo'})
self.getPage('/length', self.cookies)
self.assertBody('2')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
self.getPage('/testStr')
self.assertBody('1')
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
# Wait for the session.timeout (1 second)
time.sleep(2)
self.getPage('/')
self.assertBody('1')
self.getPage('/length', self.cookies)
self.assertBody('1')
# Test session __contains__
self.getPage('/keyin?key=counter', self.cookies)
self.assertBody('True')
cookieset1 = self.cookies
# Make a new session and test __len__ again
self.getPage('/')
self.getPage('/length', self.cookies)
self.assertBody('2')
# Test session delete
self.getPage('/delete', self.cookies)
self.assertBody('done')
self.getPage('/delete', cookieset1)
self.assertBody('done')
def f():
return [
x
for x in os.listdir(localDir)
if x.startswith('session-')
]
self.assertEqual(f(), [])
# Wait for the cleanup thread to delete remaining session files
self.getPage('/')
self.assertNotEqual(f(), [])
time.sleep(2)
self.assertEqual(f(), [])
def test_1_Ram_Concurrency(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self._test_Concurrency()
@pytest.mark.xfail(reason='#1306')
def test_2_File_Concurrency(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
self._test_Concurrency()
def _test_Concurrency(self):
client_thread_count = 5
request_count = 30
# Get initial cookie
self.getPage('/')
self.assertBody('1')
cookies = self.cookies
data_dict = {}
errors = []
def request(index):
if self.scheme == 'https':
c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
else:
c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
for i in range(request_count):
c.putrequest('GET', '/')
for k, v in cookies:
c.putheader(k, v)
c.endheaders()
response = c.getresponse()
body = response.read()
if response.status != 200 or not body.isdigit():
errors.append((response.status, body))
else:
data_dict[index] = max(data_dict[index], int(body))
# Uncomment the following line to prove threads overlap.
# sys.stdout.write("%d " % index)
# Start <request_count> requests from each of
# <client_thread_count> concurrent clients
ts = []
for c in range(client_thread_count):
data_dict[c] = 0
t = threading.Thread(target=request, args=(c,))
ts.append(t)
t.start()
for t in ts:
t.join()
hitcount = max(data_dict.values())
expected = 1 + (client_thread_count * request_count)
for e in errors:
print(e)
self.assertEqual(hitcount, expected)
def test_3_Redirect(self):
# Start a new session
self.getPage('/testStr')
self.getPage('/iredir', self.cookies)
self.assertBody('FileSession')
def test_4_File_deletion(self):
# Start a new session
self.getPage('/testStr')
# Delete the session file manually and retry.
id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
path = os.path.join(localDir, 'session-' + id)
os.unlink(path)
self.getPage('/testStr', self.cookies)
def test_5_Error_paths(self):
self.getPage('/unknown/page')
self.assertErrorPage(404, "The path '/unknown/page' was not found.")
# Note: this path is *not* the same as above. The above
# takes a normal route through the session code; this one
# skips the session code's before_handler and only calls
# before_finalize (save) and on_end (close). So the session
# code has to survive calling save/close without init.
self.getPage('/restricted', self.cookies, method='POST')
self.assertErrorPage(405, response_codes[405][1])
def test_6_regenerate(self):
self.getPage('/testStr')
# grab the cookie ID
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.getPage('/regen')
self.assertBody('logged in')
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.assertNotEqual(id1, id2)
self.getPage('/testStr')
# grab the cookie ID
id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.getPage('/testStr',
headers=[
('Cookie',
'session_id=maliciousid; '
'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
self.assertNotEqual(id1, id2)
self.assertNotEqual(id2, 'maliciousid')
def test_7_session_cookies(self):
self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
self.getPage('/clear')
self.getPage('/session_cookie')
# grab the cookie ID
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
id1 = cookie_parts['temp']
self.assertEqual(list(sessions.RamSession.cache), [id1])
# Send another request in the same "browser session".
self.getPage('/session_cookie', self.cookies)
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
self.assertBody(id1)
self.assertEqual(list(sessions.RamSession.cache), [id1])
# Simulate a browser close by just not sending the cookies
self.getPage('/session_cookie')
# grab the cookie ID
cookie_parts = dict([p.strip().split('=')
for p in self.cookies[0][1].split(';')])
# Assert there is no 'expires' param
self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
# Assert a new id has been generated...
id2 = cookie_parts['temp']
self.assertNotEqual(id1, id2)
self.assertEqual(set(sessions.RamSession.cache.keys()),
set([id1, id2]))
# Wait for the session.timeout on both sessions
time.sleep(2.5)
cache = list(sessions.RamSession.cache)
if cache:
if cache == [id2]:
self.fail('The second session did not time out.')
else:
self.fail('Unknown session id in cache: %r', cache)
def test_8_Ram_Cleanup(self):
def lock():
s1 = sessions.RamSession()
s1.acquire_lock()
time.sleep(1)
s1.release_lock()
t = threading.Thread(target=lock)
t.start()
start = time.time()
while not sessions.RamSession.locks and time.time() - start < 5:
time.sleep(0.01)
assert len(sessions.RamSession.locks) == 1, 'Lock not acquired'
s2 = sessions.RamSession()
s2.clean_up()
msg = 'Clean up should not remove active lock'
assert len(sessions.RamSession.locks) == 1, msg
t.join()
try:
importlib.import_module('memcache')
host, port = '127.0.0.1', 11211
for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
s = None
try:
s = socket.socket(af, socktype, proto)
# See http://groups.google.com/group/cherrypy-users/
# browse_frm/thread/bbfe5eb39c904fe0
s.settimeout(1.0)
s.connect((host, port))
s.close()
except socket.error:
if s:
s.close()
raise
break
except (ImportError, socket.error):
class MemcachedSessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test(self):
return self.skip('memcached not reachable ')
else:
class MemcachedSessionTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def test_0_Session(self):
self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
self.getPage('/testStr')
self.assertBody('1')
self.getPage('/testGen', self.cookies)
self.assertBody('2')
self.getPage('/testStr', self.cookies)
self.assertBody('3')
self.getPage('/length', self.cookies)
self.assertErrorPage(500)
self.assertInBody('NotImplementedError')
self.getPage('/delkey?key=counter', self.cookies)
self.assertStatus(200)
# Wait for the session.timeout (1 second)
time.sleep(1.25)
self.getPage('/')
self.assertBody('1')
# Test session __contains__
self.getPage('/keyin?key=counter', self.cookies)
self.assertBody('True')
# Test session delete
self.getPage('/delete', self.cookies)
self.assertBody('done')
def test_1_Concurrency(self):
client_thread_count = 5
request_count = 30
# Get initial cookie
self.getPage('/')
self.assertBody('1')
cookies = self.cookies
data_dict = {}
def request(index):
for i in range(request_count):
self.getPage('/', cookies)
# Uncomment the following line to prove threads overlap.
# sys.stdout.write("%d " % index)
if not self.body.isdigit():
self.fail(self.body)
data_dict[index] = int(self.body)
# Start <request_count> concurrent requests from
# each of <client_thread_count> clients
ts = []
for c in range(client_thread_count):
data_dict[c] = 0
t = threading.Thread(target=request, args=(c,))
ts.append(t)
t.start()
for t in ts:
t.join()
hitcount = max(data_dict.values())
expected = 1 + (client_thread_count * request_count)
self.assertEqual(hitcount, expected)
def test_3_Redirect(self):
# Start a new session
self.getPage('/testStr')
self.getPage('/iredir', self.cookies)
self.assertBody('memcached')
def test_5_Error_paths(self):
self.getPage('/unknown/page')
self.assertErrorPage(
404, "The path '/unknown/page' was not found.")
# Note: this path is *not* the same as above. The above
# takes a normal route through the session code; this one
# skips the session code's before_handler and only calls
# before_finalize (save) and on_end (close). So the session
# code has to survive calling save/close without init.
self.getPage('/restricted', self.cookies, method='POST')
self.assertErrorPage(405, response_codes[405][1])

View File

@@ -0,0 +1,61 @@
import cherrypy
from cherrypy.test import helper
class SessionAuthenticateTest(helper.CPWebCase):
@staticmethod
def setup_server():
def check(username, password):
# Dummy check_username_and_password function
if username != 'test' or password != 'password':
return 'Wrong login/password'
def augment_params():
# A simple tool to add some things to request.params
# This is to check to make sure that session_auth can handle
# request params (ticket #780)
cherrypy.request.params['test'] = 'test'
cherrypy.tools.augment_params = cherrypy.Tool(
'before_handler', augment_params, None, priority=30)
class Test:
_cp_config = {
'tools.sessions.on': True,
'tools.session_auth.on': True,
'tools.session_auth.check_username_and_password': check,
'tools.augment_params.on': True,
}
@cherrypy.expose
def index(self, **kwargs):
return 'Hi %s, you are logged in' % cherrypy.request.login
cherrypy.tree.mount(Test())
def testSessionAuthenticate(self):
# request a page and check for login form
self.getPage('/')
self.assertInBody('<form method="post" action="do_login">')
# setup credentials
login_body = 'username=test&password=password&from_page=/'
# attempt a login
self.getPage('/do_login', method='POST', body=login_body)
self.assertStatus((302, 303))
# get the page now that we are logged in
self.getPage('/', self.cookies)
self.assertBody('Hi test, you are logged in')
# do a logout
self.getPage('/do_logout', self.cookies, method='POST')
self.assertStatus((302, 303))
# verify we are logged out
self.getPage('/', self.cookies)
self.assertInBody('<form method="post" action="do_login">')

View File

@@ -0,0 +1,473 @@
import os
import signal
import time
import unittest
import warnings
from six.moves.http_client import BadStatusLine
import pytest
import portend
import cherrypy
import cherrypy.process.servers
from cherrypy.test import helper
engine = cherrypy.engine
thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class Dependency:
def __init__(self, bus):
self.bus = bus
self.running = False
self.startcount = 0
self.gracecount = 0
self.threads = {}
def subscribe(self):
self.bus.subscribe('start', self.start)
self.bus.subscribe('stop', self.stop)
self.bus.subscribe('graceful', self.graceful)
self.bus.subscribe('start_thread', self.startthread)
self.bus.subscribe('stop_thread', self.stopthread)
def start(self):
self.running = True
self.startcount += 1
def stop(self):
self.running = False
def graceful(self):
self.gracecount += 1
def startthread(self, thread_id):
self.threads[thread_id] = None
def stopthread(self, thread_id):
del self.threads[thread_id]
db_connection = Dependency(engine)
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'Hello World'
@cherrypy.expose
def ctrlc(self):
raise KeyboardInterrupt()
@cherrypy.expose
def graceful(self):
engine.graceful()
return 'app was (gracefully) restarted succesfully'
cherrypy.tree.mount(Root())
cherrypy.config.update({
'environment': 'test_suite',
})
db_connection.subscribe()
# ------------ Enough helpers. Time for real live test cases. ------------ #
class ServerStateTests(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def setUp(self):
cherrypy.server.socket_timeout = 0.1
self.do_gc_test = False
def test_0_NormalStateFlow(self):
engine.stop()
# Our db_connection should not be running
self.assertEqual(db_connection.running, False)
self.assertEqual(db_connection.startcount, 1)
self.assertEqual(len(db_connection.threads), 0)
# Test server start
engine.start()
self.assertEqual(engine.state, engine.states.STARTED)
host = cherrypy.server.socket_host
port = cherrypy.server.socket_port
portend.occupied(host, port, timeout=0.1)
# The db_connection should be running now
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.startcount, 2)
self.assertEqual(len(db_connection.threads), 0)
self.getPage('/')
self.assertBody('Hello World')
self.assertEqual(len(db_connection.threads), 1)
# Test engine stop. This will also stop the HTTP server.
engine.stop()
self.assertEqual(engine.state, engine.states.STOPPED)
# Verify that our custom stop function was called
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
# Block the main thread now and verify that exit() works.
def exittest():
self.getPage('/')
self.assertBody('Hello World')
engine.exit()
cherrypy.server.start()
engine.start_with_callback(exittest)
engine.block()
self.assertEqual(engine.state, engine.states.EXITING)
def test_1_Restart(self):
cherrypy.server.start()
engine.start()
# The db_connection should be running now
self.assertEqual(db_connection.running, True)
grace = db_connection.gracecount
self.getPage('/')
self.assertBody('Hello World')
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from this thread
engine.graceful()
self.assertEqual(engine.state, engine.states.STARTED)
self.getPage('/')
self.assertBody('Hello World')
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.gracecount, grace + 1)
self.assertEqual(len(db_connection.threads), 1)
# Test server restart from inside a page handler
self.getPage('/graceful')
self.assertEqual(engine.state, engine.states.STARTED)
self.assertBody('app was (gracefully) restarted succesfully')
self.assertEqual(db_connection.running, True)
self.assertEqual(db_connection.gracecount, grace + 2)
# Since we are requesting synchronously, is only one thread used?
# Note that the "/graceful" request has been flushed.
self.assertEqual(len(db_connection.threads), 0)
engine.stop()
self.assertEqual(engine.state, engine.states.STOPPED)
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
def test_2_KeyboardInterrupt(self):
# Raise a keyboard interrupt in the HTTP server's main thread.
# We must start the server in this, the main thread
engine.start()
cherrypy.server.start()
self.persistent = True
try:
# Make the first request and assert there's no "Connection: close".
self.getPage('/')
self.assertStatus('200 OK')
self.assertBody('Hello World')
self.assertNoHeader('Connection')
cherrypy.server.httpserver.interrupt = KeyboardInterrupt
engine.block()
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
self.assertEqual(engine.state, engine.states.EXITING)
finally:
self.persistent = False
# Raise a keyboard interrupt in a page handler; on multithreaded
# servers, this should occur in one of the worker threads.
# This should raise a BadStatusLine error, since the worker
# thread will just die without writing a response.
engine.start()
cherrypy.server.start()
# From python3.5 a new exception is retuned when the connection
# ends abruptly:
# http.client.RemoteDisconnected
# RemoteDisconnected is a subclass of:
# (ConnectionResetError, http.client.BadStatusLine)
# and ConnectionResetError is an indirect subclass of:
# OSError
# From python 3.3 an up socket.error is an alias to OSError
# following PEP-3151, therefore http.client.RemoteDisconnected
# is considered a socket.error.
#
# raise_subcls specifies the classes that are not going
# to be considered as a socket.error for the retries.
# Given that RemoteDisconnected is part BadStatusLine
# we can use the same call for all py3 versions without
# sideffects. python < 3.5 will raise directly BadStatusLine
# which is not a subclass for socket.error/OSError.
try:
self.getPage('/ctrlc', raise_subcls=BadStatusLine)
except BadStatusLine:
pass
else:
print(self.body)
self.fail('AssertionError: BadStatusLine not raised')
engine.block()
self.assertEqual(db_connection.running, False)
self.assertEqual(len(db_connection.threads), 0)
@pytest.mark.xfail(
'sys.platform == "Darwin" '
'and sys.version_info > (3, 7) '
'and os.environ["TRAVIS"]',
reason='https://github.com/cherrypy/cherrypy/issues/1693',
)
def test_4_Autoreload(self):
# If test_3 has not been executed, the server won't be stopped,
# so we'll have to do it.
if engine.state != engine.states.EXITING:
engine.exit()
# Start the demo script in a new process
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(extra='test_case_name: "test_4_Autoreload"')
p.start(imports='cherrypy.test._test_states_demo')
try:
self.getPage('/start')
start = float(self.body)
# Give the autoreloader time to cache the file time.
time.sleep(2)
# Touch the file
os.utime(os.path.join(thisdir, '_test_states_demo.py'), None)
# Give the autoreloader time to re-exec the process
time.sleep(2)
host = cherrypy.server.socket_host
port = cherrypy.server.socket_port
portend.occupied(host, port, timeout=5)
self.getPage('/start')
if not (float(self.body) > start):
raise AssertionError('start time %s not greater than %s' %
(float(self.body), start))
finally:
# Shut down the spawned process
self.getPage('/exit')
p.join()
def test_5_Start_Error(self):
# If test_3 has not been executed, the server won't be stopped,
# so we'll have to do it.
if engine.state != engine.states.EXITING:
engine.exit()
# If a process errors during start, it should stop the engine
# and exit with a non-zero exit code.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True)
p.write_conf(
extra="""starterror: True
test_case_name: "test_5_Start_Error"
"""
)
p.start(imports='cherrypy.test._test_states_demo')
if p.exit_code == 0:
self.fail('Process failed to return nonzero exit code.')
class PluginTests(helper.CPWebCase):
def test_daemonize(self):
if os.name not in ['posix']:
return self.skip('skipped (not on posix) ')
self.HOST = '127.0.0.1'
self.PORT = 8081
# Spawn the process and wait, when this returns, the original process
# is finished. If it daemonized properly, we should still be able
# to access pages.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True,
socket_host='127.0.0.1',
socket_port=8081)
p.write_conf(
extra='test_case_name: "test_daemonize"')
p.start(imports='cherrypy.test._test_states_demo')
try:
# Just get the pid of the daemonization process.
self.getPage('/pid')
self.assertStatus(200)
page_pid = int(self.body)
self.assertEqual(page_pid, p.get_pid())
finally:
# Shut down the spawned process
self.getPage('/exit')
p.join()
# Wait until here to test the exit code because we want to ensure
# that we wait for the daemon to finish running before we fail.
if p.exit_code != 0:
self.fail('Daemonized parent process failed to exit cleanly.')
class SignalHandlingTests(helper.CPWebCase):
def test_SIGHUP_tty(self):
# When not daemonized, SIGHUP should shut down the server.
try:
from signal import SIGHUP
except ImportError:
return self.skip('skipped (no SIGHUP) ')
# Spawn the process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra='test_case_name: "test_SIGHUP_tty"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGHUP
os.kill(p.get_pid(), SIGHUP)
# This might hang if things aren't working right, but meh.
p.join()
def test_SIGHUP_daemonized(self):
# When daemonized, SIGHUP should restart the server.
try:
from signal import SIGHUP
except ImportError:
return self.skip('skipped (no SIGHUP) ')
if os.name not in ['posix']:
return self.skip('skipped (not on posix) ')
# Spawn the process and wait, when this returns, the original process
# is finished. If it daemonized properly, we should still be able
# to access pages.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True)
p.write_conf(
extra='test_case_name: "test_SIGHUP_daemonized"')
p.start(imports='cherrypy.test._test_states_demo')
pid = p.get_pid()
try:
# Send a SIGHUP
os.kill(pid, SIGHUP)
# Give the server some time to restart
time.sleep(2)
self.getPage('/pid')
self.assertStatus(200)
new_pid = int(self.body)
self.assertNotEqual(new_pid, pid)
finally:
# Shut down the spawned process
self.getPage('/exit')
p.join()
def _require_signal_and_kill(self, signal_name):
if not hasattr(signal, signal_name):
self.skip('skipped (no %(signal_name)s)' % vars())
if not hasattr(os, 'kill'):
self.skip('skipped (no os.kill)')
def test_SIGTERM(self):
'SIGTERM should shut down the server whether daemonized or not.'
self._require_signal_and_kill('SIGTERM')
# Spawn a normal, undaemonized process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra='test_case_name: "test_SIGTERM"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGTERM
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
if os.name in ['posix']:
# Spawn a daemonized process and test again.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
wait=True, daemonize=True)
p.write_conf(
extra='test_case_name: "test_SIGTERM_2"')
p.start(imports='cherrypy.test._test_states_demo')
# Send a SIGTERM
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
def test_signal_handler_unsubscribe(self):
self._require_signal_and_kill('SIGTERM')
# Although Windows has `os.kill` and SIGTERM is defined, the
# platform does not implement signals and sending SIGTERM
# will result in a forced termination of the process.
# Therefore, this test is not suitable for Windows.
if os.name == 'nt':
self.skip('SIGTERM not available')
# Spawn a normal, undaemonized process.
p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
p.write_conf(
extra="""unsubsig: True
test_case_name: "test_signal_handler_unsubscribe"
""")
p.start(imports='cherrypy.test._test_states_demo')
# Ask the process to quit
os.kill(p.get_pid(), signal.SIGTERM)
# This might hang if things aren't working right, but meh.
p.join()
# Assert the old handler ran.
log_lines = list(open(p.error_log, 'rb'))
assert any(
line.endswith(b'I am an old SIGTERM handler.\n')
for line in log_lines
)
class WaitTests(unittest.TestCase):
def test_safe_wait_INADDR_ANY(self):
"""
Wait on INADDR_ANY should not raise IOError
In cases where the loopback interface does not exist, CherryPy cannot
effectively determine if a port binding to INADDR_ANY was effected.
In this situation, CherryPy should assume that it failed to detect
the binding (not that the binding failed) and only warn that it could
not verify it.
"""
# At such a time that CherryPy can reliably determine one or more
# viable IP addresses of the host, this test may be removed.
# Simulate the behavior we observe when no loopback interface is
# present by: finding a port that's not occupied, then wait on it.
free_port = portend.find_available_local_port()
servers = cherrypy.process.servers
inaddr_any = '0.0.0.0'
# Wait on the free port that's unbound
with warnings.catch_warnings(record=True) as w:
with servers._safe_wait(inaddr_any, free_port):
portend.occupied(inaddr_any, free_port, timeout=1)
self.assertEqual(len(w), 1)
self.assertTrue(isinstance(w[0], warnings.WarningMessage))
self.assertTrue(
'Unable to verify that the server is bound on ' in str(w[0]))
# The wait should still raise an IO error if INADDR_ANY was
# not supplied.
with pytest.raises(IOError):
with servers._safe_wait('127.0.0.1', free_port):
portend.occupied('127.0.0.1', free_port, timeout=1)

View File

@@ -0,0 +1,438 @@
# -*- coding: utf-8 -*-
import contextlib
import io
import os
import sys
import platform
import tempfile
from six import text_type as str
from six.moves import urllib
from six.moves.http_client import HTTPConnection
import pytest
import py.path
import cherrypy
from cherrypy.lib import static
from cherrypy._cpcompat import HTTPSConnection, ntou, tonative
from cherrypy.test import helper
@pytest.fixture
def unicode_filesystem(tmpdir):
_check_unicode_filesystem(tmpdir)
def _check_unicode_filesystem(tmpdir):
filename = tmpdir / ntou('', 'utf-8')
tmpl = 'File system encoding ({encoding}) cannot support unicode filenames'
msg = tmpl.format(encoding=sys.getfilesystemencoding())
try:
io.open(str(filename), 'w').close()
except UnicodeEncodeError:
pytest.skip(msg)
def ensure_unicode_filesystem():
"""
TODO: replace with simply pytest fixtures once webtest.TestCase
no longer implies unittest.
"""
tmpdir = py.path.local(tempfile.mkdtemp())
try:
_check_unicode_filesystem(tmpdir)
finally:
tmpdir.remove()
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log')
# The file size needs to be big enough such that half the size of it
# won't be socket-buffered (or server-buffered) all in one go. See
# test_file_stream.
MB = 2 ** 20
BIGFILE_SIZE = 32 * MB
class StaticTest(helper.CPWebCase):
@staticmethod
def setup_server():
if not os.path.exists(has_space_filepath):
with open(has_space_filepath, 'wb') as f:
f.write(b'Hello, world\r\n')
needs_bigfile = (
not os.path.exists(bigfile_filepath) or
os.path.getsize(bigfile_filepath) != BIGFILE_SIZE
)
if needs_bigfile:
with open(bigfile_filepath, 'wb') as f:
f.write(b'x' * BIGFILE_SIZE)
class Root:
@cherrypy.expose
@cherrypy.config(**{'response.stream': True})
def bigfile(self):
self.f = static.serve_file(bigfile_filepath)
return self.f
@cherrypy.expose
def tell(self):
if self.f.input.closed:
return ''
return repr(self.f.input.tell()).rstrip('L')
@cherrypy.expose
def fileobj(self):
f = open(os.path.join(curdir, 'style.css'), 'rb')
return static.serve_fileobj(f, content_type='text/css')
@cherrypy.expose
def bytesio(self):
f = io.BytesIO(b'Fee\nfie\nfo\nfum')
return static.serve_fileobj(f, content_type='text/plain')
class Static:
@cherrypy.expose
def index(self):
return 'You want the Baron? You can have the Baron!'
@cherrypy.expose
def dynamic(self):
return 'This is a DYNAMIC page'
root = Root()
root.static = Static()
rootconf = {
'/static': {
'tools.staticdir.on': True,
'tools.staticdir.dir': 'static',
'tools.staticdir.root': curdir,
},
'/static-long': {
'tools.staticdir.on': True,
'tools.staticdir.dir': r'\\?\%s' % curdir,
},
'/style.css': {
'tools.staticfile.on': True,
'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
},
'/docroot': {
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
'tools.staticdir.index': 'index.html',
},
'/error': {
'tools.staticdir.on': True,
'request.show_tracebacks': True,
},
'/404test': {
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
'error_page.404': error_page_404,
}
}
rootApp = cherrypy.Application(root)
rootApp.merge(rootconf)
test_app_conf = {
'/test': {
'tools.staticdir.index': 'index.html',
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
},
}
testApp = cherrypy.Application(Static())
testApp.merge(test_app_conf)
vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
cherrypy.tree.graft(vhost)
@staticmethod
def teardown_server():
for f in (has_space_filepath, bigfile_filepath):
if os.path.exists(f):
try:
os.unlink(f)
except Exception:
pass
def test_static(self):
self.getPage('/static/index.html')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# Using a staticdir.root value in a subdir...
self.getPage('/docroot/index.html')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# Check a filename with spaces in it
self.getPage('/static/has%20space.html')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
self.getPage('/style.css')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/css')
# Note: The body should be exactly 'Dummy stylesheet\n', but
# unfortunately some tools such as WinZip sometimes turn \n
# into \r\n on Windows when extracting the CherryPy tarball so
# we just check the content
self.assertMatchesBody('^Dummy stylesheet')
@pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
def test_static_longpath(self):
"""Test serving of a file in subdir of a Windows long-path
staticdir."""
self.getPage('/static-long/static/index.html')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
def test_fallthrough(self):
# Test that NotFound will then try dynamic handlers (see [878]).
self.getPage('/static/dynamic')
self.assertBody('This is a DYNAMIC page')
# Check a directory via fall-through to dynamic handler.
self.getPage('/static/')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html;charset=utf-8')
self.assertBody('You want the Baron? You can have the Baron!')
def test_index(self):
# Check a directory via "staticdir.index".
self.getPage('/docroot/')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/html')
self.assertBody('Hello, world\r\n')
# The same page should be returned even if redirected.
self.getPage('/docroot')
self.assertStatus(301)
self.assertHeader('Location', '%s/docroot/' % self.base())
self.assertMatchesBody(
"This resource .* <a href=(['\"])%s/docroot/\\1>"
'%s/docroot/</a>.'
% (self.base(), self.base())
)
def test_config_errors(self):
# Check that we get an error if no .file or .dir
self.getPage('/error/thing.html')
self.assertErrorPage(500)
if sys.version_info >= (3, 3):
errmsg = (
r'TypeError: staticdir\(\) missing 2 '
'required positional arguments'
)
else:
errmsg = (
r'TypeError: staticdir\(\) takes at least 2 '
r'(positional )?arguments \(0 given\)'
)
self.assertMatchesBody(errmsg.encode('ascii'))
def test_security(self):
# Test up-level security
self.getPage('/static/../../test/style.css')
self.assertStatus((400, 403))
def test_modif(self):
# Test modified-since on a reasonably-large file
self.getPage('/static/dirback.jpg')
self.assertStatus('200 OK')
lastmod = ''
for k, v in self.headers:
if k == 'Last-Modified':
lastmod = v
ims = ('If-Modified-Since', lastmod)
self.getPage('/static/dirback.jpg', headers=[ims])
self.assertStatus(304)
self.assertNoHeader('Content-Type')
self.assertNoHeader('Content-Length')
self.assertNoHeader('Content-Disposition')
self.assertBody('')
def test_755_vhost(self):
self.getPage('/test/', [('Host', 'virt.net')])
self.assertStatus(200)
self.getPage('/test', [('Host', 'virt.net')])
self.assertStatus(301)
self.assertHeader('Location', self.scheme + '://virt.net/test/')
def test_serve_fileobj(self):
self.getPage('/fileobj')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/css;charset=utf-8')
self.assertMatchesBody('^Dummy stylesheet')
def test_serve_bytesio(self):
self.getPage('/bytesio')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
self.assertHeader('Content-Length', 14)
self.assertMatchesBody('Fee\nfie\nfo\nfum')
@pytest.mark.xfail(reason='#1475')
def test_file_stream(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Make an initial request
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/bigfile', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
body = b''
remaining = BIGFILE_SIZE
while remaining > 0:
data = response.fp.read(65536)
if not data:
break
body += data
remaining -= len(data)
if self.scheme == 'https':
newconn = HTTPSConnection
else:
newconn = HTTPConnection
s, h, b = helper.webtest.openURL(
b'/tell', headers=[], host=self.HOST, port=self.PORT,
http_conn=newconn)
if not b:
# The file was closed on the server.
tell_position = BIGFILE_SIZE
else:
tell_position = int(b)
read_so_far = len(body)
# It is difficult for us to force the server to only read
# the bytes that we ask for - there are going to be buffers
# inbetween.
#
# CherryPy will attempt to write as much data as it can to
# the socket, and we don't have a way to determine what that
# size will be. So we make the following assumption - by
# the time we have read in the entire file on the server,
# we will have at least received half of it. If this is not
# the case, then this is an indicator that either:
# - machines that are running this test are using buffer
# sizes greater than half of BIGFILE_SIZE; or
# - streaming is broken.
#
# At the time of writing, we seem to have encountered
# buffer sizes bigger than 512K, so we've increased
# BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB.
# This test is going to keep failing according to the
# improvements in hardware and OS buffers.
if tell_position >= BIGFILE_SIZE:
if read_so_far < (BIGFILE_SIZE / 2):
self.fail(
'The file should have advanced to position %r, but '
'has already advanced to the end of the file. It '
'may not be streamed as intended, or at the wrong '
'chunk size (64k)' % read_so_far)
elif tell_position < read_so_far:
self.fail(
'The file should have advanced to position %r, but has '
'only advanced to position %r. It may not be streamed '
'as intended, or at the wrong chunk size (64k)' %
(read_so_far, tell_position))
if body != b'x' * BIGFILE_SIZE:
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(BIGFILE_SIZE, body[:50], len(body)))
conn.close()
def test_file_stream_deadlock(self):
if cherrypy.server.protocol_version != 'HTTP/1.1':
return self.skip()
self.PROTOCOL = 'HTTP/1.1'
# Make an initial request but abort early.
self.persistent = True
conn = self.HTTP_CONN
conn.putrequest('GET', '/bigfile', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
response = conn.response_class(conn.sock, method='GET')
response.begin()
self.assertEqual(response.status, 200)
body = response.fp.read(65536)
if body != b'x' * len(body):
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(65536, body[:50], len(body)))
response.close()
conn.close()
# Make a second request, which should fetch the whole file.
self.persistent = False
self.getPage('/bigfile')
if self.body != b'x' * BIGFILE_SIZE:
self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
(BIGFILE_SIZE, self.body[:50], len(body)))
def test_error_page_with_serve_file(self):
self.getPage('/404test/yunyeen')
self.assertStatus(404)
self.assertInBody("I couldn't find that thing")
def test_null_bytes(self):
self.getPage('/static/\x00')
self.assertStatus('404 Not Found')
@staticmethod
@contextlib.contextmanager
def unicode_file():
filename = ntou('Слава Україні.html', 'utf-8')
filepath = os.path.join(curdir, 'static', filename)
with io.open(filepath, 'w', encoding='utf-8') as strm:
strm.write(ntou('Героям Слава!', 'utf-8'))
try:
yield
finally:
os.remove(filepath)
py27_on_windows = (
platform.system() == 'Windows' and
sys.version_info < (3,)
)
@pytest.mark.xfail(py27_on_windows, reason='#1544') # noqa: E301
def test_unicode(self):
ensure_unicode_filesystem()
with self.unicode_file():
url = ntou('/static/Слава Україні.html', 'utf-8')
# quote function requires str
url = tonative(url, 'utf-8')
url = urllib.parse.quote(url)
self.getPage(url)
expected = ntou('Героям Слава!', 'utf-8')
self.assertInBody(expected)
def error_page_404(status, message, traceback, version):
path = os.path.join(curdir, 'static', '404.html')
return static.serve_file(path, content_type='text/html')

View File

@@ -0,0 +1,468 @@
"""Test the various means of instantiating and invoking tools."""
import gzip
import io
import sys
import time
import types
import unittest
import operator
import six
from six.moves import range, map
from six.moves.http_client import IncompleteRead
import cherrypy
from cherrypy import tools
from cherrypy._cpcompat import ntou
from cherrypy.test import helper, _test_decorators
timeout = 0.2
europoundUnicode = ntou('\x80\xa3')
# Client-side code #
class ToolTests(helper.CPWebCase):
@staticmethod
def setup_server():
# Put check_access in a custom toolbox with its own namespace
myauthtools = cherrypy._cptools.Toolbox('myauth')
def check_access(default=False):
if not getattr(cherrypy.request, 'userid', default):
raise cherrypy.HTTPError(401)
myauthtools.check_access = cherrypy.Tool(
'before_request_body', check_access)
def numerify():
def number_it(body):
for chunk in body:
for k, v in cherrypy.request.numerify_map:
chunk = chunk.replace(k, v)
yield chunk
cherrypy.response.body = number_it(cherrypy.response.body)
class NumTool(cherrypy.Tool):
def _setup(self):
def makemap():
m = self._merged_args().get('map', {})
cherrypy.request.numerify_map = list(six.iteritems(m))
cherrypy.request.hooks.attach('on_start_resource', makemap)
def critical():
cherrypy.request.error_response = cherrypy.HTTPError(
502).set_response
critical.failsafe = True
cherrypy.request.hooks.attach('on_start_resource', critical)
cherrypy.request.hooks.attach(self._point, self.callable)
tools.numerify = NumTool('before_finalize', numerify)
# It's not mandatory to inherit from cherrypy.Tool.
class NadsatTool:
def __init__(self):
self.ended = {}
self._name = 'nadsat'
def nadsat(self):
def nadsat_it_up(body):
for chunk in body:
chunk = chunk.replace(b'good', b'horrorshow')
chunk = chunk.replace(b'piece', b'lomtick')
yield chunk
cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
nadsat.priority = 0
def cleanup(self):
# This runs after the request has been completely written out.
cherrypy.response.body = [b'razdrez']
id = cherrypy.request.params.get('id')
if id:
self.ended[id] = True
cleanup.failsafe = True
def _setup(self):
cherrypy.request.hooks.attach('before_finalize', self.nadsat)
cherrypy.request.hooks.attach('on_end_request', self.cleanup)
tools.nadsat = NadsatTool()
def pipe_body():
cherrypy.request.process_request_body = False
clen = int(cherrypy.request.headers['Content-Length'])
cherrypy.request.body = cherrypy.request.rfile.read(clen)
# Assert that we can use a callable object instead of a function.
class Rotator(object):
def __call__(self, scale):
r = cherrypy.response
r.collapse_body()
if six.PY3:
r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
else:
r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
def stream_handler(next_handler, *args, **kwargs):
actual = cherrypy.request.config.get('tools.streamer.arg')
assert actual == 'arg value'
cherrypy.response.output = o = io.BytesIO()
try:
next_handler(*args, **kwargs)
# Ignore the response and return our accumulated output
# instead.
return o.getvalue()
finally:
o.close()
cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
stream_handler)
class Root:
@cherrypy.expose
def index(self):
return 'Howdy earth!'
@cherrypy.expose
@cherrypy.config(**{
'tools.streamer.on': True,
'tools.streamer.arg': 'arg value',
})
def tarfile(self):
actual = cherrypy.request.config.get('tools.streamer.arg')
assert actual == 'arg value'
cherrypy.response.output.write(b'I am ')
cherrypy.response.output.write(b'a tarfile')
@cherrypy.expose
def euro(self):
hooks = list(cherrypy.request.hooks['before_finalize'])
hooks.sort()
cbnames = [x.callback.__name__ for x in hooks]
assert cbnames == ['gzip'], cbnames
priorities = [x.priority for x in hooks]
assert priorities == [80], priorities
yield ntou('Hello,')
yield ntou('world')
yield europoundUnicode
# Bare hooks
@cherrypy.expose
@cherrypy.config(**{'hooks.before_request_body': pipe_body})
def pipe(self):
return cherrypy.request.body
# Multiple decorators; include kwargs just for fun.
# Note that rotator must run before gzip.
@cherrypy.expose
def decorated_euro(self, *vpath):
yield ntou('Hello,')
yield ntou('world')
yield europoundUnicode
decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
decorated_euro = tools.rotator(scale=3)(decorated_euro)
root = Root()
class TestType(type):
"""Metaclass which automatically exposes all functions in each
subclass, and adds an instance of the subclass as an attribute
of root.
"""
def __init__(cls, name, bases, dct):
type.__init__(cls, name, bases, dct)
for value in six.itervalues(dct):
if isinstance(value, types.FunctionType):
cherrypy.expose(value)
setattr(root, name.lower(), cls())
Test = TestType('Test', (object,), {})
# METHOD ONE:
# Declare Tools in _cp_config
@cherrypy.config(**{'tools.nadsat.on': True})
class Demo(Test):
def index(self, id=None):
return 'A good piece of cherry pie'
def ended(self, id):
return repr(tools.nadsat.ended[id])
def err(self, id=None):
raise ValueError()
def errinstream(self, id=None):
yield 'nonconfidential'
raise ValueError()
yield 'confidential'
# METHOD TWO: decorator using Tool()
# We support Python 2.3, but the @-deco syntax would look like
# this:
# @tools.check_access()
def restricted(self):
return 'Welcome!'
restricted = myauthtools.check_access()(restricted)
userid = restricted
def err_in_onstart(self):
return 'success!'
@cherrypy.config(**{'response.stream': True})
def stream(self, id=None):
for x in range(100000000):
yield str(x)
conf = {
# METHOD THREE:
# Declare Tools in detached config
'/demo': {
'tools.numerify.on': True,
'tools.numerify.map': {b'pie': b'3.14159'},
},
'/demo/restricted': {
'request.show_tracebacks': False,
},
'/demo/userid': {
'request.show_tracebacks': False,
'myauth.check_access.default': True,
},
'/demo/errinstream': {
'response.stream': True,
},
'/demo/err_in_onstart': {
# Because this isn't a dict, on_start_resource will error.
'tools.numerify.map': 'pie->3.14159'
},
# Combined tools
'/euro': {
'tools.gzip.on': True,
'tools.encode.on': True,
},
# Priority specified in config
'/decorated_euro/subpath': {
'tools.gzip.priority': 10,
},
# Handler wrappers
'/tarfile': {'tools.streamer.on': True}
}
app = cherrypy.tree.mount(root, config=conf)
app.request_class.namespaces['myauth'] = myauthtools
root.tooldecs = _test_decorators.ToolExamples()
def testHookErrors(self):
self.getPage('/demo/?id=1')
# If body is "razdrez", then on_end_request is being called too early.
self.assertBody('A horrorshow lomtick of cherry 3.14159')
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage('/demo/ended/1')
self.assertBody('True')
valerr = '\n raise ValueError()\nValueError'
self.getPage('/demo/err?id=3')
# If body is "razdrez", then on_end_request is being called too early.
self.assertErrorPage(502, pattern=valerr)
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage('/demo/ended/3')
self.assertBody('True')
# If body is "razdrez", then on_end_request is being called too early.
if (cherrypy.server.protocol_version == 'HTTP/1.0' or
getattr(cherrypy.server, 'using_apache', False)):
self.getPage('/demo/errinstream?id=5')
# Because this error is raised after the response body has
# started, the status should not change to an error status.
self.assertStatus('200 OK')
self.assertBody('nonconfidential')
else:
# Because this error is raised after the response body has
# started, and because it's chunked output, an error is raised by
# the HTTP client when it encounters incomplete output.
self.assertRaises((ValueError, IncompleteRead), self.getPage,
'/demo/errinstream?id=5')
# If this fails, then on_end_request isn't being called at all.
time.sleep(0.1)
self.getPage('/demo/ended/5')
self.assertBody('True')
# Test the "__call__" technique (compile-time decorator).
self.getPage('/demo/restricted')
self.assertErrorPage(401)
# Test compile-time decorator with kwargs from config.
self.getPage('/demo/userid')
self.assertBody('Welcome!')
def testEndRequestOnDrop(self):
old_timeout = None
try:
httpserver = cherrypy.server.httpserver
old_timeout = httpserver.timeout
except (AttributeError, IndexError):
return self.skip()
try:
httpserver.timeout = timeout
# Test that on_end_request is called even if the client drops.
self.persistent = True
try:
conn = self.HTTP_CONN
conn.putrequest('GET', '/demo/stream?id=9', skip_host=True)
conn.putheader('Host', self.HOST)
conn.endheaders()
# Skip the rest of the request and close the conn. This will
# cause the server's active socket to error, which *should*
# result in the request being aborted, and request.close being
# called all the way up the stack (including WSGI middleware),
# eventually calling our on_end_request hook.
finally:
self.persistent = False
time.sleep(timeout * 2)
# Test that the on_end_request hook was called.
self.getPage('/demo/ended/9')
self.assertBody('True')
finally:
if old_timeout is not None:
httpserver.timeout = old_timeout
def testGuaranteedHooks(self):
# The 'critical' on_start_resource hook is 'failsafe' (guaranteed
# to run even if there are failures in other on_start methods).
# This is NOT true of the other hooks.
# Here, we have set up a failure in NumerifyTool.numerify_map,
# but our 'critical' hook should run and set the error to 502.
self.getPage('/demo/err_in_onstart')
self.assertErrorPage(502)
tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems')
self.assertInBody(expected_msg)
def testCombinedTools(self):
expectedResult = (ntou('Hello,world') +
europoundUnicode).encode('utf-8')
zbuf = io.BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
zfile.write(expectedResult)
zfile.close()
self.getPage('/euro',
headers=[
('Accept-Encoding', 'gzip'),
('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
self.assertInBody(zbuf.getvalue()[:3])
zbuf = io.BytesIO()
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
zfile.write(expectedResult)
zfile.close()
self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')])
self.assertInBody(zbuf.getvalue()[:3])
# This returns a different value because gzip's priority was
# lowered in conf, allowing the rotator to run after gzip.
# Of course, we don't want breakage in production apps,
# but it proves the priority was changed.
self.getPage('/decorated_euro/subpath',
headers=[('Accept-Encoding', 'gzip')])
if six.PY3:
self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
else:
self.assertInBody(''.join([chr((ord(x) + 3) % 256)
for x in zbuf.getvalue()]))
def testBareHooks(self):
content = 'bit of a pain in me gulliver'
self.getPage('/pipe',
headers=[('Content-Length', str(len(content))),
('Content-Type', 'text/plain')],
method='POST', body=content)
self.assertBody(content)
def testHandlerWrapperTool(self):
self.getPage('/tarfile')
self.assertBody('I am a tarfile')
def testToolWithConfig(self):
if not sys.version_info >= (2, 5):
return self.skip('skipped (Python 2.5+ only)')
self.getPage('/tooldecs/blah')
self.assertHeader('Content-Type', 'application/data')
def testWarnToolOn(self):
# get
try:
cherrypy.tools.numerify.on
except AttributeError:
pass
else:
raise AssertionError('Tool.on did not error as it should have.')
# set
try:
cherrypy.tools.numerify.on = True
except AttributeError:
pass
else:
raise AssertionError('Tool.on did not error as it should have.')
def testDecorator(self):
@cherrypy.tools.register('on_start_resource')
def example():
pass
self.assertTrue(isinstance(cherrypy.tools.example, cherrypy.Tool))
self.assertEqual(cherrypy.tools.example._point, 'on_start_resource')
@cherrypy.tools.register( # noqa: F811
'before_finalize', name='renamed', priority=60,
)
def example():
pass
self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
self.assertEqual(cherrypy.tools.renamed._name, 'renamed')
self.assertEqual(cherrypy.tools.renamed._priority, 60)
class SessionAuthTest(unittest.TestCase):
def test_login_screen_returns_bytes(self):
"""
login_screen must return bytes even if unicode parameters are passed.
Issue 1132 revealed that login_screen would return unicode if the
username and password were unicode.
"""
sa = cherrypy.lib.cptools.SessionAuth()
res = sa.login_screen(None, username=six.text_type('nobody'),
password=six.text_type('anypass'))
self.assertTrue(isinstance(res, bytes))
class TestHooks:
def test_priorities(self):
"""
Hooks should sort by priority order.
"""
Hook = cherrypy._cprequest.Hook
hooks = [
Hook(None, priority=48),
Hook(None),
Hook(None, priority=49),
]
hooks.sort()
by_priority = operator.attrgetter('priority')
priorities = list(map(by_priority, hooks))
assert priorities == [48, 49, 50]

View File

@@ -0,0 +1,210 @@
import sys
import imp
import types
import importlib
import six
import cherrypy
from cherrypy.test import helper
class TutorialTest(helper.CPWebCase):
@classmethod
def setup_server(cls):
"""
Mount something so the engine starts.
"""
class Dummy:
pass
cherrypy.tree.mount(Dummy())
@staticmethod
def load_module(name):
"""
Import or reload tutorial module as needed.
"""
target = 'cherrypy.tutorial.' + name
if target in sys.modules:
module = imp.reload(sys.modules[target])
else:
module = importlib.import_module(target)
return module
@classmethod
def setup_tutorial(cls, name, root_name, config={}):
cherrypy.config.reset()
module = cls.load_module(name)
root = getattr(module, root_name)
conf = getattr(module, 'tutconf')
class_types = type,
if six.PY2:
class_types += types.ClassType,
if isinstance(root, class_types):
root = root()
cherrypy.tree.mount(root, config=conf)
cherrypy.config.update(config)
def test01HelloWorld(self):
self.setup_tutorial('tut01_helloworld', 'HelloWorld')
self.getPage('/')
self.assertBody('Hello world!')
def test02ExposeMethods(self):
self.setup_tutorial('tut02_expose_methods', 'HelloWorld')
self.getPage('/show_msg')
self.assertBody('Hello world!')
def test03GetAndPost(self):
self.setup_tutorial('tut03_get_and_post', 'WelcomePage')
# Try different GET queries
self.getPage('/greetUser?name=Bob')
self.assertBody("Hey Bob, what's up?")
self.getPage('/greetUser')
self.assertBody('Please enter your name <a href="./">here</a>.')
self.getPage('/greetUser?name=')
self.assertBody('No, really, enter your name <a href="./">here</a>.')
# Try the same with POST
self.getPage('/greetUser', method='POST', body='name=Bob')
self.assertBody("Hey Bob, what's up?")
self.getPage('/greetUser', method='POST', body='name=')
self.assertBody('No, really, enter your name <a href="./">here</a>.')
def test04ComplexSite(self):
self.setup_tutorial('tut04_complex_site', 'root')
msg = '''
<p>Here are some extra useful links:</p>
<ul>
<li><a href="http://del.icio.us">del.icio.us</a></li>
<li><a href="http://www.cherrypy.org">CherryPy</a></li>
</ul>
<p>[<a href="../">Return to links page</a>]</p>'''
self.getPage('/links/extra/')
self.assertBody(msg)
def test05DerivedObjects(self):
self.setup_tutorial('tut05_derived_objects', 'HomePage')
msg = '''
<html>
<head>
<title>Another Page</title>
<head>
<body>
<h2>Another Page</h2>
<p>
And this is the amazing second page!
</p>
</body>
</html>
'''
# the tutorial has some annoying spaces in otherwise blank lines
msg = msg.replace('</h2>\n\n', '</h2>\n \n')
msg = msg.replace('</p>\n\n', '</p>\n \n')
self.getPage('/another/')
self.assertBody(msg)
def test06DefaultMethod(self):
self.setup_tutorial('tut06_default_method', 'UsersPage')
self.getPage('/hendrik')
self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
'(<a href="./">back</a>)')
def test07Sessions(self):
self.setup_tutorial('tut07_sessions', 'HitCounter')
self.getPage('/')
self.assertBody(
"\n During your current session, you've viewed this"
'\n page 1 times! Your life is a patio of fun!'
'\n ')
self.getPage('/', self.cookies)
self.assertBody(
"\n During your current session, you've viewed this"
'\n page 2 times! Your life is a patio of fun!'
'\n ')
def test08GeneratorsAndYield(self):
self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo')
self.getPage('/')
self.assertBody('<html><body><h2>Generators rule!</h2>'
'<h3>List of users:</h3>'
'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
'</body></html>')
def test09Files(self):
self.setup_tutorial('tut09_files', 'FileDemo')
# Test upload
filesize = 5
h = [('Content-type', 'multipart/form-data; boundary=x'),
('Content-Length', str(105 + filesize))]
b = ('--x\n'
'Content-Disposition: form-data; name="myFile"; '
'filename="hello.txt"\r\n'
'Content-Type: text/plain\r\n'
'\r\n')
b += 'a' * filesize + '\n' + '--x--\n'
self.getPage('/upload', h, 'POST', b)
self.assertBody('''<html>
<body>
myFile length: %d<br />
myFile filename: hello.txt<br />
myFile mime-type: text/plain
</body>
</html>''' % filesize)
# Test download
self.getPage('/download')
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'application/x-download')
self.assertHeader('Content-Disposition',
# Make sure the filename is quoted.
'attachment; filename="pdf_file.pdf"')
self.assertEqual(len(self.body), 85698)
def test10HTTPErrors(self):
self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo')
@cherrypy.expose
def traceback_setting():
return repr(cherrypy.request.show_tracebacks)
cherrypy.tree.mount(traceback_setting, '/traceback_setting')
self.getPage('/')
self.assertInBody("""<a href="toggleTracebacks">""")
self.assertInBody("""<a href="/doesNotExist">""")
self.assertInBody("""<a href="/error?code=403">""")
self.assertInBody("""<a href="/error?code=500">""")
self.assertInBody("""<a href="/messageArg">""")
self.getPage('/traceback_setting')
setting = self.body
self.getPage('/toggleTracebacks')
self.assertStatus((302, 303))
self.getPage('/traceback_setting')
self.assertBody(str(not eval(setting)))
self.getPage('/error?code=500')
self.assertStatus(500)
self.assertInBody('The server encountered an unexpected condition '
'which prevented it from fulfilling the request.')
self.getPage('/error?code=403')
self.assertStatus(403)
self.assertInBody("<h2>You can't do that!</h2>")
self.getPage('/messageArg')
self.assertStatus(500)
self.assertInBody("If you construct an HTTPError with a 'message'")

View File

@@ -0,0 +1,113 @@
import os
import cherrypy
from cherrypy.test import helper
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
class VirtualHostTest(helper.CPWebCase):
@staticmethod
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return 'Hello, world'
@cherrypy.expose
def dom4(self):
return 'Under construction'
@cherrypy.expose
def method(self, value):
return 'You sent %s' % value
class VHost:
def __init__(self, sitename):
self.sitename = sitename
@cherrypy.expose
def index(self):
return 'Welcome to %s' % self.sitename
@cherrypy.expose
def vmethod(self, value):
return 'You sent %s' % value
@cherrypy.expose
def url(self):
return cherrypy.url('nextpage')
# Test static as a handler (section must NOT include vhost prefix)
static = cherrypy.tools.staticdir.handler(
section='/static', dir=curdir)
root = Root()
root.mydom2 = VHost('Domain 2')
root.mydom3 = VHost('Domain 3')
hostmap = {'www.mydom2.com': '/mydom2',
'www.mydom3.com': '/mydom3',
'www.mydom4.com': '/dom4',
}
cherrypy.tree.mount(root, config={
'/': {
'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)
},
# Test static in config (section must include vhost prefix)
'/mydom2/static2': {
'tools.staticdir.on': True,
'tools.staticdir.root': curdir,
'tools.staticdir.dir': 'static',
'tools.staticdir.index': 'index.html',
},
})
def testVirtualHost(self):
self.getPage('/', [('Host', 'www.mydom1.com')])
self.assertBody('Hello, world')
self.getPage('/mydom2/', [('Host', 'www.mydom1.com')])
self.assertBody('Welcome to Domain 2')
self.getPage('/', [('Host', 'www.mydom2.com')])
self.assertBody('Welcome to Domain 2')
self.getPage('/', [('Host', 'www.mydom3.com')])
self.assertBody('Welcome to Domain 3')
self.getPage('/', [('Host', 'www.mydom4.com')])
self.assertBody('Under construction')
# Test GET, POST, and positional params
self.getPage('/method?value=root')
self.assertBody('You sent root')
self.getPage('/vmethod?value=dom2+GET', [('Host', 'www.mydom2.com')])
self.assertBody('You sent dom2 GET')
self.getPage('/vmethod', [('Host', 'www.mydom3.com')], method='POST',
body='value=dom3+POST')
self.assertBody('You sent dom3 POST')
self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')])
self.assertBody('You sent pos')
# Test that cherrypy.url uses the browser url, not the virtual url
self.getPage('/url', [('Host', 'www.mydom2.com')])
self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme)
def test_VHost_plus_Static(self):
# Test static as a handler
self.getPage('/static/style.css', [('Host', 'www.mydom2.com')])
self.assertStatus('200 OK')
self.assertHeader('Content-Type', 'text/css;charset=utf-8')
# Test static in config
self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')])
self.assertStatus('200 OK')
self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg'])
# Test static config with "index" arg
self.getPage('/static2/', [('Host', 'www.mydom2.com')])
self.assertStatus('200 OK')
self.assertBody('Hello, world\r\n')
# Since tools.trailing_slash is on by default, this should redirect
self.getPage('/static2', [('Host', 'www.mydom2.com')])
self.assertStatus(301)

View File

@@ -0,0 +1,93 @@
import cherrypy
from cherrypy.test import helper
class WSGI_Namespace_Test(helper.CPWebCase):
@staticmethod
def setup_server():
class WSGIResponse(object):
def __init__(self, appresults):
self.appresults = appresults
self.iter = iter(appresults)
def __iter__(self):
return self
def next(self):
return self.iter.next()
def __next__(self):
return next(self.iter)
def close(self):
if hasattr(self.appresults, 'close'):
self.appresults.close()
class ChangeCase(object):
def __init__(self, app, to=None):
self.app = app
self.to = to
def __call__(self, environ, start_response):
res = self.app(environ, start_response)
class CaseResults(WSGIResponse):
def next(this):
return getattr(this.iter.next(), self.to)()
def __next__(this):
return getattr(next(this.iter), self.to)()
return CaseResults(res)
class Replacer(object):
def __init__(self, app, map={}):
self.app = app
self.map = map
def __call__(self, environ, start_response):
res = self.app(environ, start_response)
class ReplaceResults(WSGIResponse):
def next(this):
line = this.iter.next()
for k, v in self.map.iteritems():
line = line.replace(k, v)
return line
def __next__(this):
line = next(this.iter)
for k, v in self.map.items():
line = line.replace(k, v)
return line
return ReplaceResults(res)
class Root(object):
@cherrypy.expose
def index(self):
return 'HellO WoRlD!'
root_conf = {'wsgi.pipeline': [('replace', Replacer)],
'wsgi.replace.map': {b'L': b'X',
b'l': b'r'},
}
app = cherrypy.Application(Root())
app.wsgiapp.pipeline.append(('changecase', ChangeCase))
app.wsgiapp.config['changecase'] = {'to': 'upper'}
cherrypy.tree.mount(app, config={'/': root_conf})
def test_pipeline(self):
if not cherrypy.server.httpserver:
return self.skip()
self.getPage('/')
# If body is "HEXXO WORXD!", the middleware was applied out of order.
self.assertBody('HERRO WORRD!')

View File

@@ -0,0 +1,93 @@
import os
import socket
import atexit
import tempfile
from six.moves.http_client import HTTPConnection
import pytest
import cherrypy
from cherrypy.test import helper
def usocket_path():
fd, path = tempfile.mkstemp('cp_test.sock')
os.close(fd)
os.remove(path)
return path
USOCKET_PATH = usocket_path()
class USocketHTTPConnection(HTTPConnection):
"""
HTTPConnection over a unix socket.
"""
def __init__(self, path):
HTTPConnection.__init__(self, 'localhost')
self.path = path
def __call__(self, *args, **kwargs):
"""
Catch-all method just to present itself as a constructor for the
HTTPConnection.
"""
return self
def connect(self):
"""
Override the connect method and assign a unix socket as a transport.
"""
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(self.path)
self.sock = sock
atexit.register(lambda: os.remove(self.path))
@pytest.mark.skipif("sys.platform == 'win32'")
class WSGI_UnixSocket_Test(helper.CPWebCase):
"""
Test basic behavior on a cherrypy wsgi server listening
on a unix socket.
It exercises the config option `server.socket_file`.
"""
HTTP_CONN = USocketHTTPConnection(USOCKET_PATH)
@staticmethod
def setup_server():
class Root(object):
@cherrypy.expose
def index(self):
return 'Test OK'
@cherrypy.expose
def error(self):
raise Exception('Invalid page')
config = {
'server.socket_file': USOCKET_PATH
}
cherrypy.config.update(config)
cherrypy.tree.mount(Root())
def tearDown(self):
cherrypy.config.update({'server.socket_file': None})
def test_simple_request(self):
self.getPage('/')
self.assertStatus('200 OK')
self.assertInBody('Test OK')
def test_not_found(self):
self.getPage('/invalid_path')
self.assertStatus('404 Not Found')
def test_internal_error(self):
self.getPage('/error')
self.assertStatus('500 Internal Server Error')
self.assertInBody('Invalid page')

View File

@@ -0,0 +1,35 @@
import cherrypy
from cherrypy.test import helper
class WSGI_VirtualHost_Test(helper.CPWebCase):
@staticmethod
def setup_server():
class ClassOfRoot(object):
def __init__(self, name):
self.name = name
@cherrypy.expose
def index(self):
return 'Welcome to the %s website!' % self.name
default = cherrypy.Application(None)
domains = {}
for year in range(1997, 2008):
app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
domains['www.classof%s.example' % year] = app
cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
def test_welcome(self):
if not cherrypy.server.using_wsgi:
return self.skip('skipped (not using WSGI)... ')
for year in range(1997, 2008):
self.getPage(
'/', headers=[('Host', 'www.classof%s.example' % year)])
self.assertBody('Welcome to the Class of %s website!' % year)

View File

@@ -0,0 +1,120 @@
import sys
import cherrypy
from cherrypy._cpcompat import ntob
from cherrypy.test import helper
class WSGIGraftTests(helper.CPWebCase):
@staticmethod
def setup_server():
def test_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
output = ['Hello, world!\n',
'This is a wsgi app running within CherryPy!\n\n']
keys = list(environ.keys())
keys.sort()
for k in keys:
output.append('%s: %s\n' % (k, environ[k]))
return [ntob(x, 'utf-8') for x in output]
def test_empty_string_app(environ, start_response):
status = '200 OK'
response_headers = [('Content-type', 'text/plain')]
start_response(status, response_headers)
return [
b'Hello', b'', b' ', b'', b'world',
]
class WSGIResponse(object):
def __init__(self, appresults):
self.appresults = appresults
self.iter = iter(appresults)
def __iter__(self):
return self
if sys.version_info >= (3, 0):
def __next__(self):
return next(self.iter)
else:
def next(self):
return self.iter.next()
def close(self):
if hasattr(self.appresults, 'close'):
self.appresults.close()
class ReversingMiddleware(object):
def __init__(self, app):
self.app = app
def __call__(self, environ, start_response):
results = app(environ, start_response)
class Reverser(WSGIResponse):
if sys.version_info >= (3, 0):
def __next__(this):
line = list(next(this.iter))
line.reverse()
return bytes(line)
else:
def next(this):
line = list(this.iter.next())
line.reverse()
return ''.join(line)
return Reverser(results)
class Root:
@cherrypy.expose
def index(self):
return ntob("I'm a regular CherryPy page handler!")
cherrypy.tree.mount(Root())
cherrypy.tree.graft(test_app, '/hosted/app1')
cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
# Set script_name explicitly to None to signal CP that it should
# be pulled from the WSGI environ each time.
app = cherrypy.Application(Root(), script_name=None)
cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2')
wsgi_output = '''Hello, world!
This is a wsgi app running within CherryPy!'''
def test_01_standard_app(self):
self.getPage('/')
self.assertBody("I'm a regular CherryPy page handler!")
def test_04_pure_wsgi(self):
if not cherrypy.server.using_wsgi:
return self.skip('skipped (not using WSGI)... ')
self.getPage('/hosted/app1')
self.assertHeader('Content-Type', 'text/plain')
self.assertInBody(self.wsgi_output)
def test_05_wrapped_cp_app(self):
if not cherrypy.server.using_wsgi:
return self.skip('skipped (not using WSGI)... ')
self.getPage('/hosted/app2/')
body = list("I'm a regular CherryPy page handler!")
body.reverse()
body = ''.join(body)
self.assertInBody(body)
def test_06_empty_string_app(self):
if not cherrypy.server.using_wsgi:
return self.skip('skipped (not using WSGI)... ')
self.getPage('/hosted/app3')
self.assertHeader('Content-Type', 'text/plain')
self.assertInBody('Hello world')

View File

@@ -0,0 +1,183 @@
import sys
import six
from six.moves.xmlrpc_client import (
DateTime, Fault,
ProtocolError, ServerProxy, SafeTransport
)
import cherrypy
from cherrypy import _cptools
from cherrypy.test import helper
if six.PY3:
HTTPSTransport = SafeTransport
# Python 3.0's SafeTransport still mistakenly checks for socket.ssl
import socket
if not hasattr(socket, 'ssl'):
socket.ssl = True
else:
class HTTPSTransport(SafeTransport):
"""Subclass of SafeTransport to fix sock.recv errors (by using file).
"""
def request(self, host, handler, request_body, verbose=0):
# issue XML-RPC request
h = self.make_connection(host)
if verbose:
h.set_debuglevel(1)
self.send_request(h, handler, request_body)
self.send_host(h, host)
self.send_user_agent(h)
self.send_content(h, request_body)
errcode, errmsg, headers = h.getreply()
if errcode != 200:
raise ProtocolError(host + handler, errcode, errmsg, headers)
self.verbose = verbose
# Here's where we differ from the superclass. It says:
# try:
# sock = h._conn.sock
# except AttributeError:
# sock = None
# return self._parse_response(h.getfile(), sock)
return self.parse_response(h.getfile())
def setup_server():
class Root:
@cherrypy.expose
def index(self):
return "I'm a standard index!"
class XmlRpc(_cptools.XMLRPCController):
@cherrypy.expose
def foo(self):
return 'Hello world!'
@cherrypy.expose
def return_single_item_list(self):
return [42]
@cherrypy.expose
def return_string(self):
return 'here is a string'
@cherrypy.expose
def return_tuple(self):
return ('here', 'is', 1, 'tuple')
@cherrypy.expose
def return_dict(self):
return dict(a=1, b=2, c=3)
@cherrypy.expose
def return_composite(self):
return dict(a=1, z=26), 'hi', ['welcome', 'friend']
@cherrypy.expose
def return_int(self):
return 42
@cherrypy.expose
def return_float(self):
return 3.14
@cherrypy.expose
def return_datetime(self):
return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
@cherrypy.expose
def return_boolean(self):
return True
@cherrypy.expose
def test_argument_passing(self, num):
return num * 2
@cherrypy.expose
def test_returning_Fault(self):
return Fault(1, 'custom Fault response')
root = Root()
root.xmlrpc = XmlRpc()
cherrypy.tree.mount(root, config={'/': {
'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
'tools.xmlrpc.allow_none': 0,
}})
class XmlRpcTest(helper.CPWebCase):
setup_server = staticmethod(setup_server)
def testXmlRpc(self):
scheme = self.scheme
if scheme == 'https':
url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
proxy = ServerProxy(url, transport=HTTPSTransport())
else:
url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
proxy = ServerProxy(url)
# begin the tests ...
self.getPage('/xmlrpc/foo')
self.assertBody('Hello world!')
self.assertEqual(proxy.return_single_item_list(), [42])
self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
self.assertEqual(proxy.return_string(), 'here is a string')
self.assertEqual(proxy.return_tuple(),
list(('here', 'is', 1, 'tuple')))
self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
self.assertEqual(proxy.return_composite(),
[{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
self.assertEqual(proxy.return_int(), 42)
self.assertEqual(proxy.return_float(), 3.14)
self.assertEqual(proxy.return_datetime(),
DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
self.assertEqual(proxy.return_boolean(), True)
self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
# Test an error in the page handler (should raise an xmlrpclib.Fault)
try:
proxy.test_argument_passing({})
except Exception:
x = sys.exc_info()[1]
self.assertEqual(x.__class__, Fault)
self.assertEqual(x.faultString, ('unsupported operand type(s) '
"for *: 'dict' and 'int'"))
else:
self.fail('Expected xmlrpclib.Fault')
# https://github.com/cherrypy/cherrypy/issues/533
# if a method is not found, an xmlrpclib.Fault should be raised
try:
proxy.non_method()
except Exception:
x = sys.exc_info()[1]
self.assertEqual(x.__class__, Fault)
self.assertEqual(x.faultString,
'method "non_method" is not supported')
else:
self.fail('Expected xmlrpclib.Fault')
# Test returning a Fault from the page handler.
try:
proxy.test_returning_Fault()
except Exception:
x = sys.exc_info()[1]
self.assertEqual(x.__class__, Fault)
self.assertEqual(x.faultString, ('custom Fault response'))
else:
self.fail('Expected xmlrpclib.Fault')

View File

@@ -0,0 +1,11 @@
# for compatibility, expose cheroot webtest here
import warnings
from cheroot.test.webtest import ( # noqa
interface,
WebCase, cleanHeaders, shb, openURL,
ServerError, server_error,
)
warnings.warn('Use cheroot.test.webtest', DeprecationWarning)