mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-23 22:27:17 -04:00
WIP
This commit is contained in:
parent
02551f2486
commit
1b0e721a9d
181 changed files with 14500 additions and 32648 deletions
163
bazarr.py
163
bazarr.py
|
@ -2,10 +2,10 @@
|
|||
|
||||
import os
|
||||
import platform
|
||||
import signal
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import atexit
|
||||
|
||||
from bazarr.get_args import args
|
||||
|
||||
|
@ -16,12 +16,13 @@ def check_python_version():
|
|||
minimum_py3_str = ".".join(str(i) for i in minimum_py3_tuple)
|
||||
|
||||
if int(python_version[0]) < minimum_py3_tuple[0]:
|
||||
print("Python " + minimum_py3_str + " or greater required. Current version is " + platform.python_version() +
|
||||
". Please upgrade Python.")
|
||||
print("Python " + minimum_py3_str + " or greater required. "
|
||||
"Current version is " + platform.python_version() + ". Please upgrade Python.")
|
||||
sys.exit(1)
|
||||
elif int(python_version[1]) < minimum_py3_tuple[1]:
|
||||
print("Python " + minimum_py3_str + " or greater required. Current version is " + platform.python_version() +
|
||||
". Please upgrade Python.")
|
||||
elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \
|
||||
(int(python_version[0]) != minimum_py3_tuple[0]):
|
||||
print("Python " + minimum_py3_str + " or greater required. "
|
||||
"Current version is " + platform.python_version() + ". Please upgrade Python.")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
|
@ -30,133 +31,55 @@ check_python_version()
|
|||
dir_name = os.path.dirname(__file__)
|
||||
|
||||
|
||||
class ProcessRegistry:
|
||||
|
||||
def register(self, process):
|
||||
pass
|
||||
|
||||
def unregister(self, process):
|
||||
pass
|
||||
|
||||
|
||||
class DaemonStatus(ProcessRegistry):
|
||||
|
||||
def __init__(self):
|
||||
self.__should_stop = False
|
||||
self.__processes = set()
|
||||
|
||||
def register(self, process):
|
||||
self.__processes.add(process)
|
||||
|
||||
def unregister(self, process):
|
||||
self.__processes.remove(process)
|
||||
|
||||
@staticmethod
|
||||
def __send_signal(processes, signal_no, live_processes=None):
|
||||
"""
|
||||
Sends to every single of the specified processes the given signal and (if live_processes is not None) append to
|
||||
it processes which are still alive.
|
||||
"""
|
||||
for ep in processes:
|
||||
if ep.poll() is None:
|
||||
if live_processes is not None:
|
||||
live_processes.append(ep)
|
||||
try:
|
||||
ep.send_signal(signal_no)
|
||||
except Exception as e:
|
||||
print('Failed sending signal %s to process %s because of an unexpected error: %s' % (
|
||||
signal_no, ep.pid, e))
|
||||
return live_processes
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Flags this instance as should stop and terminates as smoothly as possible children processes.
|
||||
"""
|
||||
self.__should_stop = True
|
||||
DaemonStatus.__send_signal(self.__processes, signal.SIGINT, list())
|
||||
|
||||
def force_stop(self):
|
||||
self.__should_stop = True
|
||||
DaemonStatus.__send_signal(self.__processes, signal.SIGTERM)
|
||||
|
||||
def should_stop(self):
|
||||
return self.__should_stop
|
||||
|
||||
|
||||
def start_bazarr(process_registry=ProcessRegistry()):
|
||||
def start_bazarr():
|
||||
script = [sys.executable, "-u", os.path.normcase(os.path.join(dir_name, 'bazarr', 'main.py'))] + sys.argv[1:]
|
||||
|
||||
print("Bazarr starting...")
|
||||
ep = subprocess.Popen(script, stdout=None, stderr=None, stdin=subprocess.DEVNULL)
|
||||
process_registry.register(ep)
|
||||
try:
|
||||
ep.wait()
|
||||
process_registry.unregister(ep)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
atexit.register(lambda: ep.kill())
|
||||
|
||||
|
||||
def check_status():
|
||||
if os.path.exists(stopfile):
|
||||
try:
|
||||
os.remove(stopfile)
|
||||
except Exception:
|
||||
print('Unable to delete stop file.')
|
||||
finally:
|
||||
print('Bazarr exited.')
|
||||
sys.exit(0)
|
||||
|
||||
if os.path.exists(restartfile):
|
||||
try:
|
||||
os.remove(restartfile)
|
||||
except Exception:
|
||||
print('Unable to delete restart file.')
|
||||
else:
|
||||
print("Bazarr is restarting...")
|
||||
start_bazarr()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
restartfile = os.path.normcase(os.path.join(args.config_dir, 'bazarr.restart'))
|
||||
stopfile = os.path.normcase(os.path.join(args.config_dir, 'bazarr.stop'))
|
||||
restartfile = os.path.join(args.config_dir, 'bazarr.restart')
|
||||
stopfile = os.path.join(args.config_dir, 'bazarr.stop')
|
||||
|
||||
# Cleanup leftover files
|
||||
try:
|
||||
os.remove(restartfile)
|
||||
except Exception:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
try:
|
||||
os.remove(stopfile)
|
||||
except Exception:
|
||||
except FileNotFoundError:
|
||||
pass
|
||||
|
||||
|
||||
def daemon(bazarr_runner=lambda: start_bazarr()):
|
||||
if os.path.exists(stopfile):
|
||||
try:
|
||||
os.remove(stopfile)
|
||||
except Exception:
|
||||
print('Unable to delete stop file.')
|
||||
else:
|
||||
print('Bazarr exited.')
|
||||
sys.exit(0)
|
||||
|
||||
if os.path.exists(restartfile):
|
||||
try:
|
||||
os.remove(restartfile)
|
||||
except Exception:
|
||||
print('Unable to delete restart file.')
|
||||
else:
|
||||
bazarr_runner()
|
||||
|
||||
|
||||
bazarr_runner = lambda: start_bazarr()
|
||||
|
||||
should_stop = lambda: False
|
||||
|
||||
daemonStatus = DaemonStatus()
|
||||
|
||||
def force_shutdown():
|
||||
# force the killing of children processes
|
||||
daemonStatus.force_stop()
|
||||
# if a new SIGTERM signal is caught the standard behaviour should be followed
|
||||
signal.signal(signal.SIGTERM, signal.SIG_DFL)
|
||||
# emulate a Ctrl C command on itself (bypasses the signal thing but, then, emulates the "Ctrl+C break")
|
||||
os.kill(os.getpid(), signal.SIGINT)
|
||||
|
||||
def shutdown():
|
||||
# indicates that everything should stop
|
||||
daemonStatus.stop()
|
||||
# if a new sigterm signal is caught it should force the shutdown of children processes
|
||||
signal.signal(signal.SIGTERM, lambda signal_no, frame: force_shutdown())
|
||||
|
||||
signal.signal(signal.SIGTERM, lambda signal_no, frame: shutdown())
|
||||
|
||||
should_stop = lambda: daemonStatus.should_stop()
|
||||
bazarr_runner = lambda: start_bazarr(daemonStatus)
|
||||
bazarr_runner()
|
||||
# Initial start of main bazarr process
|
||||
print("Bazarr starting...")
|
||||
start_bazarr()
|
||||
|
||||
# Keep the script running forever until stop is requested through term or keyboard interrupt
|
||||
while not should_stop():
|
||||
daemon(bazarr_runner)
|
||||
time.sleep(1)
|
||||
while True:
|
||||
check_status()
|
||||
try:
|
||||
time.sleep(1)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
|
|
|
@ -140,7 +140,7 @@ class SystemTasks(Resource):
|
|||
|
||||
@authenticate
|
||||
def post(self):
|
||||
taskid = request.json['taskid']
|
||||
taskid = request.form.get('taskid')
|
||||
|
||||
scheduler.execute_job_now(taskid)
|
||||
|
||||
|
@ -308,15 +308,14 @@ class Series(Resource):
|
|||
class SeriesEditSave(Resource):
|
||||
@authenticate
|
||||
def post(self):
|
||||
changed_series = request.json
|
||||
lang = changed_series['languages']
|
||||
hi = changed_series['hi']
|
||||
forced = changed_series['forced']
|
||||
lang = request.form.getlist('languages[]')
|
||||
hi = request.form.getlist('hi[]')
|
||||
forced = request.form.getlist('forced[]')
|
||||
|
||||
if lang == ['None']:
|
||||
lang = 'None'
|
||||
|
||||
for item in changed_series['seriesid']:
|
||||
for item in request.form.getlist('seriesid[]'):
|
||||
seriesid = item.lstrip('row_')
|
||||
try:
|
||||
if len(lang):
|
||||
|
@ -726,15 +725,14 @@ class Movies(Resource):
|
|||
class MoviesEditSave(Resource):
|
||||
@authenticate
|
||||
def post(self):
|
||||
changed_movies = request.json
|
||||
lang = changed_movies['languages']
|
||||
hi = changed_movies['hi']
|
||||
forced = changed_movies['forced']
|
||||
lang = request.form.getlist('languages[]')
|
||||
hi = request.form.getlist('hi[]')
|
||||
forced = request.form.getlist('forced[]')
|
||||
|
||||
if lang == ['None']:
|
||||
lang = 'None'
|
||||
|
||||
for item in changed_movies['radarrid']:
|
||||
for item in request.form.getlist('radarrid[]'):
|
||||
radarrid = item.lstrip('row_')
|
||||
try:
|
||||
if len(lang):
|
||||
|
|
|
@ -95,6 +95,7 @@ def configure_logging(debug=False):
|
|||
logging.getLogger("engineio.server").setLevel(logging.ERROR)
|
||||
logging.getLogger("socketio.server").setLevel(logging.ERROR)
|
||||
|
||||
logging.getLogger("waitress").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("knowit").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("enzyme").setLevel(logging.CRITICAL)
|
||||
logging.getLogger("guessit").setLevel(logging.WARNING)
|
||||
|
|
|
@ -30,7 +30,7 @@ from database import database, dict_mapper
|
|||
|
||||
from notifier import update_notifier
|
||||
|
||||
from cherrypy.wsgiserver import CherryPyWSGIServer
|
||||
from waitress.server import create_server
|
||||
|
||||
from urllib.parse import unquote
|
||||
from datetime import datetime
|
||||
|
@ -176,9 +176,9 @@ def shutdown():
|
|||
|
||||
def doShutdown():
|
||||
try:
|
||||
server.stop()
|
||||
server.close()
|
||||
except:
|
||||
logging.error('BAZARR Cannot stop CherryPy.')
|
||||
logging.error('BAZARR Cannot stop Waitress.')
|
||||
else:
|
||||
database.close()
|
||||
try:
|
||||
|
@ -196,9 +196,9 @@ def doShutdown():
|
|||
@login_required
|
||||
def restart():
|
||||
try:
|
||||
server.stop()
|
||||
server.close()
|
||||
except:
|
||||
logging.error('BAZARR Cannot stop CherryPy.')
|
||||
logging.error('BAZARR Cannot stop Waitress.')
|
||||
else:
|
||||
database.close()
|
||||
try:
|
||||
|
@ -719,11 +719,13 @@ if args.dev:
|
|||
server = app.run(
|
||||
host=str(settings.general.ip), port=(int(args.port) if args.port else int(settings.general.port)))
|
||||
else:
|
||||
server = CherryPyWSGIServer((str(settings.general.ip), (int(args.port) if args.port else int(settings.general.port))), app)
|
||||
server = create_server(app,
|
||||
host=str(settings.general.ip),
|
||||
port=int(args.port) if args.port else int(settings.general.port))
|
||||
try:
|
||||
logging.info('BAZARR is started and waiting for request on http://' + str(settings.general.ip) + ':' + (str(
|
||||
args.port) if args.port else str(settings.general.port)) + str(base_url))
|
||||
if not args.dev:
|
||||
server.start()
|
||||
server.run()
|
||||
except KeyboardInterrupt:
|
||||
doShutdown()
|
||||
|
|
|
@ -1,374 +0,0 @@
|
|||
"""CherryPy is a pythonic, object-oriented HTTP framework.
|
||||
|
||||
|
||||
CherryPy consists of not one, but four separate API layers.
|
||||
|
||||
The APPLICATION LAYER is the simplest. CherryPy applications are written as
|
||||
a tree of classes and methods, where each branch in the tree corresponds to
|
||||
a branch in the URL path. Each method is a 'page handler', which receives
|
||||
GET and POST params as keyword arguments, and returns or yields the (HTML)
|
||||
body of the response. The special method name 'index' is used for paths
|
||||
that end in a slash, and the special method name 'default' is used to
|
||||
handle multiple paths via a single handler. This layer also includes:
|
||||
|
||||
* the 'exposed' attribute (and cherrypy.expose)
|
||||
* cherrypy.quickstart()
|
||||
* _cp_config attributes
|
||||
* cherrypy.tools (including cherrypy.session)
|
||||
* cherrypy.url()
|
||||
|
||||
The ENVIRONMENT LAYER is used by developers at all levels. It provides
|
||||
information about the current request and response, plus the application
|
||||
and server environment, via a (default) set of top-level objects:
|
||||
|
||||
* cherrypy.request
|
||||
* cherrypy.response
|
||||
* cherrypy.engine
|
||||
* cherrypy.server
|
||||
* cherrypy.tree
|
||||
* cherrypy.config
|
||||
* cherrypy.thread_data
|
||||
* cherrypy.log
|
||||
* cherrypy.HTTPError, NotFound, and HTTPRedirect
|
||||
* cherrypy.lib
|
||||
|
||||
The EXTENSION LAYER allows advanced users to construct and share their own
|
||||
plugins. It consists of:
|
||||
|
||||
* Hook API
|
||||
* Tool API
|
||||
* Toolbox API
|
||||
* Dispatch API
|
||||
* Config Namespace API
|
||||
|
||||
Finally, there is the CORE LAYER, which uses the core API's to construct
|
||||
the default components which are available at higher layers. You can think
|
||||
of the default components as the 'reference implementation' for CherryPy.
|
||||
Megaframeworks (and advanced users) may replace the default components
|
||||
with customized or extended components. The core API's are:
|
||||
|
||||
* Application API
|
||||
* Engine API
|
||||
* Request API
|
||||
* Server API
|
||||
* WSGI API
|
||||
|
||||
These API's are described in the `CherryPy specification <https://bitbucket.org/cherrypy/cherrypy/wiki/CherryPySpec>`_.
|
||||
"""
|
||||
|
||||
try:
|
||||
import pkg_resources
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from threading import local as _local
|
||||
|
||||
from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect # noqa
|
||||
from cherrypy._cperror import NotFound, CherryPyException, TimeoutError # noqa
|
||||
|
||||
from cherrypy import _cplogging
|
||||
|
||||
from cherrypy import _cpdispatch as dispatch # noqa
|
||||
|
||||
from cherrypy import _cptools
|
||||
from cherrypy._cptools import default_toolbox as tools, Tool
|
||||
|
||||
from cherrypy import _cprequest
|
||||
from cherrypy.lib import httputil as _httputil
|
||||
|
||||
from cherrypy import _cptree
|
||||
from cherrypy._cptree import Application # noqa
|
||||
from cherrypy import _cpwsgi as wsgi # noqa
|
||||
|
||||
from cherrypy import _cpserver
|
||||
from cherrypy import process
|
||||
try:
|
||||
from cherrypy.process import win32
|
||||
engine = win32.Win32Bus()
|
||||
engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
|
||||
del win32
|
||||
except ImportError:
|
||||
engine = process.bus
|
||||
|
||||
|
||||
tree = _cptree.Tree()
|
||||
|
||||
|
||||
try:
|
||||
__version__ = pkg_resources.require('cherrypy')[0].version
|
||||
except Exception:
|
||||
__version__ = 'unknown'
|
||||
|
||||
|
||||
# Timeout monitor. We add two channels to the engine
|
||||
# to which cherrypy.Application will publish.
|
||||
engine.listeners['before_request'] = set()
|
||||
engine.listeners['after_request'] = set()
|
||||
|
||||
|
||||
class _TimeoutMonitor(process.plugins.Monitor):
|
||||
|
||||
def __init__(self, bus):
|
||||
self.servings = []
|
||||
process.plugins.Monitor.__init__(self, bus, self.run)
|
||||
|
||||
def before_request(self):
|
||||
self.servings.append((serving.request, serving.response))
|
||||
|
||||
def after_request(self):
|
||||
try:
|
||||
self.servings.remove((serving.request, serving.response))
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def run(self):
|
||||
"""Check timeout on all responses. (Internal)"""
|
||||
for req, resp in self.servings:
|
||||
resp.check_timeout()
|
||||
engine.timeout_monitor = _TimeoutMonitor(engine)
|
||||
engine.timeout_monitor.subscribe()
|
||||
|
||||
engine.autoreload = process.plugins.Autoreloader(engine)
|
||||
engine.autoreload.subscribe()
|
||||
|
||||
engine.thread_manager = process.plugins.ThreadManager(engine)
|
||||
engine.thread_manager.subscribe()
|
||||
|
||||
engine.signal_handler = process.plugins.SignalHandler(engine)
|
||||
|
||||
|
||||
class _HandleSignalsPlugin(object):
|
||||
|
||||
"""Handle signals from other processes based on the configured
|
||||
platform handlers above."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def subscribe(self):
|
||||
"""Add the handlers based on the platform"""
|
||||
if hasattr(self.bus, 'signal_handler'):
|
||||
self.bus.signal_handler.subscribe()
|
||||
if hasattr(self.bus, 'console_control_handler'):
|
||||
self.bus.console_control_handler.subscribe()
|
||||
|
||||
engine.signals = _HandleSignalsPlugin(engine)
|
||||
|
||||
|
||||
server = _cpserver.Server()
|
||||
server.subscribe()
|
||||
|
||||
|
||||
def quickstart(root=None, script_name='', config=None):
|
||||
"""Mount the given root, start the builtin server (and engine), then block.
|
||||
|
||||
root: an instance of a "controller class" (a collection of page handler
|
||||
methods) which represents the root of the application.
|
||||
script_name: a string containing the "mount point" of the application.
|
||||
This should start with a slash, and be the path portion of the URL
|
||||
at which to mount the given root. For example, if root.index() will
|
||||
handle requests to "http://www.example.com:8080/dept/app1/", then
|
||||
the script_name argument would be "/dept/app1".
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the root
|
||||
of the URI, it MUST be an empty string (not "/").
|
||||
config: a file or dict containing application config. If this contains
|
||||
a [global] section, those entries will be used in the global
|
||||
(site-wide) config.
|
||||
"""
|
||||
if config:
|
||||
_global_conf_alias.update(config)
|
||||
|
||||
tree.mount(root, script_name, config)
|
||||
|
||||
engine.signals.subscribe()
|
||||
engine.start()
|
||||
engine.block()
|
||||
|
||||
|
||||
class _Serving(_local):
|
||||
|
||||
"""An interface for registering request and response objects.
|
||||
|
||||
Rather than have a separate "thread local" object for the request and
|
||||
the response, this class works as a single threadlocal container for
|
||||
both objects (and any others which developers wish to define). In this
|
||||
way, we can easily dump those objects when we stop/start a new HTTP
|
||||
conversation, yet still refer to them as module-level globals in a
|
||||
thread-safe way.
|
||||
"""
|
||||
|
||||
request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
|
||||
_httputil.Host('127.0.0.1', 1111))
|
||||
"""
|
||||
The request object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
response = _cprequest.Response()
|
||||
"""
|
||||
The response object for the current thread. In the main thread,
|
||||
and any threads which are not receiving HTTP requests, this is None."""
|
||||
|
||||
def load(self, request, response):
|
||||
self.request = request
|
||||
self.response = response
|
||||
|
||||
def clear(self):
|
||||
"""Remove all attributes of self."""
|
||||
self.__dict__.clear()
|
||||
|
||||
serving = _Serving()
|
||||
|
||||
|
||||
class _ThreadLocalProxy(object):
|
||||
|
||||
__slots__ = ['__attrname__', '__dict__']
|
||||
|
||||
def __init__(self, attrname):
|
||||
self.__attrname__ = attrname
|
||||
|
||||
def __getattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return getattr(child, name)
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
if name in ('__attrname__', ):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
child = getattr(serving, self.__attrname__)
|
||||
setattr(child, name, value)
|
||||
|
||||
def __delattr__(self, name):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
delattr(child, name)
|
||||
|
||||
def _get_dict(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
d = child.__class__.__dict__.copy()
|
||||
d.update(child.__dict__)
|
||||
return d
|
||||
__dict__ = property(_get_dict)
|
||||
|
||||
def __getitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return child[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
child[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
del child[key]
|
||||
|
||||
def __contains__(self, key):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return key in child
|
||||
|
||||
def __len__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return len(child)
|
||||
|
||||
def __nonzero__(self):
|
||||
child = getattr(serving, self.__attrname__)
|
||||
return bool(child)
|
||||
# Python 3
|
||||
__bool__ = __nonzero__
|
||||
|
||||
# Create request and response object (the same objects will be used
|
||||
# throughout the entire life of the webserver, but will redirect
|
||||
# to the "serving" object)
|
||||
request = _ThreadLocalProxy('request')
|
||||
response = _ThreadLocalProxy('response')
|
||||
|
||||
# Create thread_data object as a thread-specific all-purpose storage
|
||||
|
||||
|
||||
class _ThreadData(_local):
|
||||
|
||||
"""A container for thread-specific data."""
|
||||
thread_data = _ThreadData()
|
||||
|
||||
|
||||
# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
|
||||
# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
|
||||
# The only other way would be to change what is returned from type(request)
|
||||
# and that's not possible in pure Python (you'd have to fake ob_type).
|
||||
def _cherrypy_pydoc_resolve(thing, forceload=0):
|
||||
"""Given an object or a path to an object, get the object and its name."""
|
||||
if isinstance(thing, _ThreadLocalProxy):
|
||||
thing = getattr(serving, thing.__attrname__)
|
||||
return _pydoc._builtin_resolve(thing, forceload)
|
||||
|
||||
try:
|
||||
import pydoc as _pydoc
|
||||
_pydoc._builtin_resolve = _pydoc.resolve
|
||||
_pydoc.resolve = _cherrypy_pydoc_resolve
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class _GlobalLogManager(_cplogging.LogManager):
|
||||
|
||||
"""A site-wide LogManager; routes to app.log or global log as appropriate.
|
||||
|
||||
This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
|
||||
cherrypy.log() and cherrypy.log.access(). If either
|
||||
function is called during a request, the message will be sent to the
|
||||
logger for the current Application. If they are called outside of a
|
||||
request, the message will be sent to the site-wide logger.
|
||||
"""
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Log the given message to the app.log or global log as appropriate.
|
||||
"""
|
||||
# Do NOT use try/except here. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/945
|
||||
if hasattr(request, 'app') and hasattr(request.app, 'log'):
|
||||
log = request.app.log
|
||||
else:
|
||||
log = self
|
||||
return log.error(*args, **kwargs)
|
||||
|
||||
def access(self):
|
||||
"""Log an access message to the app.log or global log as appropriate.
|
||||
"""
|
||||
try:
|
||||
return request.app.log.access()
|
||||
except AttributeError:
|
||||
return _cplogging.LogManager.access(self)
|
||||
|
||||
|
||||
log = _GlobalLogManager()
|
||||
# Set a default screen handler on the global log.
|
||||
log.screen = True
|
||||
log.error_file = ''
|
||||
# Using an access file makes CP about 10% slower. Leave off by default.
|
||||
log.access_file = ''
|
||||
|
||||
|
||||
def _buslog(msg, level):
|
||||
log.error(msg, 'ENGINE', severity=level)
|
||||
engine.subscribe('log', _buslog)
|
||||
|
||||
from cherrypy._helper import expose, popargs, url # noqa
|
||||
|
||||
# import _cpconfig last so it can reference other top-level objects
|
||||
from cherrypy import _cpconfig # noqa
|
||||
# Use _global_conf_alias so quickstart can use 'config' as an arg
|
||||
# without shadowing cherrypy.config.
|
||||
config = _global_conf_alias = _cpconfig.Config()
|
||||
config.defaults = {
|
||||
'tools.log_tracebacks.on': True,
|
||||
'tools.log_headers.on': True,
|
||||
'tools.trailing_slash.on': True,
|
||||
'tools.encode.on': True
|
||||
}
|
||||
config.namespaces['log'] = lambda k, v: setattr(log, k, v)
|
||||
config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
|
||||
# Must reset to get our defaults applied.
|
||||
config.reset()
|
||||
|
||||
from cherrypy import _cpchecker # noqa
|
||||
checker = _cpchecker.Checker()
|
||||
engine.subscribe('start', checker)
|
|
@ -1,5 +0,0 @@
|
|||
from cherrypy.daemon import run
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run()
|
|
@ -1,332 +0,0 @@
|
|||
import os
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import iteritems, copykeys, builtins
|
||||
|
||||
|
||||
class Checker(object):
|
||||
|
||||
"""A checker for CherryPy sites and their mounted applications.
|
||||
|
||||
When this object is called at engine startup, it executes each
|
||||
of its own methods whose names start with ``check_``. If you wish
|
||||
to disable selected checks, simply add a line in your global
|
||||
config which sets the appropriate method to False::
|
||||
|
||||
[global]
|
||||
checker.check_skipped_app_config = False
|
||||
|
||||
You may also dynamically add or replace ``check_*`` methods in this way.
|
||||
"""
|
||||
|
||||
on = True
|
||||
"""If True (the default), run all checks; if False, turn off all checks."""
|
||||
|
||||
def __init__(self):
|
||||
self._populate_known_types()
|
||||
|
||||
def __call__(self):
|
||||
"""Run all check_* methods."""
|
||||
if self.on:
|
||||
oldformatwarning = warnings.formatwarning
|
||||
warnings.formatwarning = self.formatwarning
|
||||
try:
|
||||
for name in dir(self):
|
||||
if name.startswith('check_'):
|
||||
method = getattr(self, name)
|
||||
if method and hasattr(method, '__call__'):
|
||||
method()
|
||||
finally:
|
||||
warnings.formatwarning = oldformatwarning
|
||||
|
||||
def formatwarning(self, message, category, filename, lineno, line=None):
|
||||
"""Function to format a warning."""
|
||||
return 'CherryPy Checker:\n%s\n\n' % message
|
||||
|
||||
# This value should be set inside _cpconfig.
|
||||
global_config_contained_paths = False
|
||||
|
||||
def check_app_config_entries_dont_start_with_script_name(self):
|
||||
"""Check for Application config with sections that repeat script_name.
|
||||
"""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
if not app.config:
|
||||
continue
|
||||
if sn == '':
|
||||
continue
|
||||
sn_atoms = sn.strip('/').split('/')
|
||||
for key in app.config.keys():
|
||||
key_atoms = key.strip('/').split('/')
|
||||
if key_atoms[:len(sn_atoms)] == sn_atoms:
|
||||
warnings.warn(
|
||||
'The application mounted at %r has config '
|
||||
'entries that start with its script name: %r' % (sn,
|
||||
key))
|
||||
|
||||
def check_site_config_entries_in_app_config(self):
|
||||
"""Check for mounted Applications that have site-scoped config."""
|
||||
for sn, app in iteritems(cherrypy.tree.apps):
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
|
||||
msg = []
|
||||
for section, entries in iteritems(app.config):
|
||||
if section.startswith('/'):
|
||||
for key, value in iteritems(entries):
|
||||
for n in ('engine.', 'server.', 'tree.', 'checker.'):
|
||||
if key.startswith(n):
|
||||
msg.append('[%s] %s = %s' %
|
||||
(section, key, value))
|
||||
if msg:
|
||||
msg.insert(0,
|
||||
'The application mounted at %r contains the '
|
||||
'following config entries, which are only allowed '
|
||||
'in site-wide config. Move them to a [global] '
|
||||
'section and pass them to cherrypy.config.update() '
|
||||
'instead of tree.mount().' % sn)
|
||||
warnings.warn(os.linesep.join(msg))
|
||||
|
||||
def check_skipped_app_config(self):
|
||||
"""Check for mounted Applications that have no config."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
if not app.config:
|
||||
msg = 'The Application mounted at %r has an empty config.' % sn
|
||||
if self.global_config_contained_paths:
|
||||
msg += (' It looks like the config you passed to '
|
||||
'cherrypy.config.update() contains application-'
|
||||
'specific sections. You must explicitly pass '
|
||||
'application config via '
|
||||
'cherrypy.tree.mount(..., config=app_config)')
|
||||
warnings.warn(msg)
|
||||
return
|
||||
|
||||
def check_app_config_brackets(self):
|
||||
"""Check for Application config with extraneous brackets in section
|
||||
names.
|
||||
"""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
if not app.config:
|
||||
continue
|
||||
for key in app.config.keys():
|
||||
if key.startswith('[') or key.endswith(']'):
|
||||
warnings.warn(
|
||||
'The application mounted at %r has config '
|
||||
'section names with extraneous brackets: %r. '
|
||||
'Config *files* need brackets; config *dicts* '
|
||||
'(e.g. passed to tree.mount) do not.' % (sn, key))
|
||||
|
||||
def check_static_paths(self):
|
||||
"""Check Application config for incorrect static paths."""
|
||||
# Use the dummy Request object in the main thread.
|
||||
request = cherrypy.request
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
request.app = app
|
||||
for section in app.config:
|
||||
# get_resource will populate request.config
|
||||
request.get_resource(section + '/dummy.html')
|
||||
conf = request.config.get
|
||||
|
||||
if conf('tools.staticdir.on', False):
|
||||
msg = ''
|
||||
root = conf('tools.staticdir.root')
|
||||
dir = conf('tools.staticdir.dir')
|
||||
if dir is None:
|
||||
msg = 'tools.staticdir.dir is not set.'
|
||||
else:
|
||||
fulldir = ''
|
||||
if os.path.isabs(dir):
|
||||
fulldir = dir
|
||||
if root:
|
||||
msg = ('dir is an absolute path, even '
|
||||
'though a root is provided.')
|
||||
testdir = os.path.join(root, dir[1:])
|
||||
if os.path.exists(testdir):
|
||||
msg += (
|
||||
'\nIf you meant to serve the '
|
||||
'filesystem folder at %r, remove the '
|
||||
'leading slash from dir.' % (testdir,))
|
||||
else:
|
||||
if not root:
|
||||
msg = (
|
||||
'dir is a relative path and '
|
||||
'no root provided.')
|
||||
else:
|
||||
fulldir = os.path.join(root, dir)
|
||||
if not os.path.isabs(fulldir):
|
||||
msg = ('%r is not an absolute path.' % (
|
||||
fulldir,))
|
||||
|
||||
if fulldir and not os.path.exists(fulldir):
|
||||
if msg:
|
||||
msg += '\n'
|
||||
msg += ('%r (root + dir) is not an existing '
|
||||
'filesystem path.' % fulldir)
|
||||
|
||||
if msg:
|
||||
warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
|
||||
% (msg, section, root, dir))
|
||||
|
||||
# -------------------------- Compatibility -------------------------- #
|
||||
obsolete = {
|
||||
'server.default_content_type': 'tools.response_headers.headers',
|
||||
'log_access_file': 'log.access_file',
|
||||
'log_config_options': None,
|
||||
'log_file': 'log.error_file',
|
||||
'log_file_not_found': None,
|
||||
'log_request_headers': 'tools.log_headers.on',
|
||||
'log_to_screen': 'log.screen',
|
||||
'show_tracebacks': 'request.show_tracebacks',
|
||||
'throw_errors': 'request.throw_errors',
|
||||
'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
|
||||
'cherrypy.Application(Root())))'),
|
||||
}
|
||||
|
||||
deprecated = {}
|
||||
|
||||
def _compat(self, config):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
for section, conf in config.items():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.items():
|
||||
if k in self.obsolete:
|
||||
warnings.warn('%r is obsolete. Use %r instead.\n'
|
||||
'section: [%s]' %
|
||||
(k, self.obsolete[k], section))
|
||||
elif k in self.deprecated:
|
||||
warnings.warn('%r is deprecated. Use %r instead.\n'
|
||||
'section: [%s]' %
|
||||
(k, self.deprecated[k], section))
|
||||
else:
|
||||
if section in self.obsolete:
|
||||
warnings.warn('%r is obsolete. Use %r instead.'
|
||||
% (section, self.obsolete[section]))
|
||||
elif section in self.deprecated:
|
||||
warnings.warn('%r is deprecated. Use %r instead.'
|
||||
% (section, self.deprecated[section]))
|
||||
|
||||
def check_compatibility(self):
|
||||
"""Process config and warn on each obsolete or deprecated entry."""
|
||||
self._compat(cherrypy.config)
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._compat(app.config)
|
||||
|
||||
# ------------------------ Known Namespaces ------------------------ #
|
||||
extra_config_namespaces = []
|
||||
|
||||
def _known_ns(self, app):
|
||||
ns = ['wsgi']
|
||||
ns.extend(copykeys(app.toolboxes))
|
||||
ns.extend(copykeys(app.namespaces))
|
||||
ns.extend(copykeys(app.request_class.namespaces))
|
||||
ns.extend(copykeys(cherrypy.config.namespaces))
|
||||
ns += self.extra_config_namespaces
|
||||
|
||||
for section, conf in app.config.items():
|
||||
is_path_section = section.startswith('/')
|
||||
if is_path_section and isinstance(conf, dict):
|
||||
for k, v in conf.items():
|
||||
atoms = k.split('.')
|
||||
if len(atoms) > 1:
|
||||
if atoms[0] not in ns:
|
||||
# Spit out a special warning if a known
|
||||
# namespace is preceded by "cherrypy."
|
||||
if atoms[0] == 'cherrypy' and atoms[1] in ns:
|
||||
msg = (
|
||||
'The config entry %r is invalid; '
|
||||
'try %r instead.\nsection: [%s]'
|
||||
% (k, '.'.join(atoms[1:]), section))
|
||||
else:
|
||||
msg = (
|
||||
'The config entry %r is invalid, '
|
||||
'because the %r config namespace '
|
||||
'is unknown.\n'
|
||||
'section: [%s]' % (k, atoms[0], section))
|
||||
warnings.warn(msg)
|
||||
elif atoms[0] == 'tools':
|
||||
if atoms[1] not in dir(cherrypy.tools):
|
||||
msg = (
|
||||
'The config entry %r may be invalid, '
|
||||
'because the %r tool was not found.\n'
|
||||
'section: [%s]' % (k, atoms[1], section))
|
||||
warnings.warn(msg)
|
||||
|
||||
def check_config_namespaces(self):
|
||||
"""Process config and warn on each unknown config namespace."""
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_ns(app)
|
||||
|
||||
# -------------------------- Config Types -------------------------- #
|
||||
known_config_types = {}
|
||||
|
||||
def _populate_known_types(self):
|
||||
b = [x for x in vars(builtins).values()
|
||||
if type(x) is type(str)]
|
||||
|
||||
def traverse(obj, namespace):
|
||||
for name in dir(obj):
|
||||
# Hack for 3.2's warning about body_params
|
||||
if name == 'body_params':
|
||||
continue
|
||||
vtype = type(getattr(obj, name, None))
|
||||
if vtype in b:
|
||||
self.known_config_types[namespace + '.' + name] = vtype
|
||||
|
||||
traverse(cherrypy.request, 'request')
|
||||
traverse(cherrypy.response, 'response')
|
||||
traverse(cherrypy.server, 'server')
|
||||
traverse(cherrypy.engine, 'engine')
|
||||
traverse(cherrypy.log, 'log')
|
||||
|
||||
def _known_types(self, config):
|
||||
msg = ('The config entry %r in section %r is of type %r, '
|
||||
'which does not match the expected type %r.')
|
||||
|
||||
for section, conf in config.items():
|
||||
if isinstance(conf, dict):
|
||||
for k, v in conf.items():
|
||||
if v is not None:
|
||||
expected_type = self.known_config_types.get(k, None)
|
||||
vtype = type(v)
|
||||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
else:
|
||||
k, v = section, conf
|
||||
if v is not None:
|
||||
expected_type = self.known_config_types.get(k, None)
|
||||
vtype = type(v)
|
||||
if expected_type and vtype != expected_type:
|
||||
warnings.warn(msg % (k, section, vtype.__name__,
|
||||
expected_type.__name__))
|
||||
|
||||
def check_config_types(self):
|
||||
"""Assert that config values are of the same type as default values."""
|
||||
self._known_types(cherrypy.config)
|
||||
for sn, app in cherrypy.tree.apps.items():
|
||||
if not isinstance(app, cherrypy.Application):
|
||||
continue
|
||||
self._known_types(app.config)
|
||||
|
||||
# -------------------- Specific config warnings -------------------- #
|
||||
def check_localhost(self):
|
||||
"""Warn if any socket_host is 'localhost'. See #711."""
|
||||
for k, v in cherrypy.config.items():
|
||||
if k == 'server.socket_host' and v == 'localhost':
|
||||
warnings.warn("The use of 'localhost' as a socket host can "
|
||||
'cause problems on newer systems, since '
|
||||
"'localhost' can map to either an IPv4 or an "
|
||||
"IPv6 address. You should use '127.0.0.1' "
|
||||
"or '[::1]' instead.")
|
|
@ -1,350 +0,0 @@
|
|||
"""Compatibility code for using CherryPy with various versions of Python.
|
||||
|
||||
CherryPy 3.2 is compatible with Python versions 2.6+. This module provides a
|
||||
useful abstraction over the differences between Python versions, sometimes by
|
||||
preferring a newer idiom, sometimes an older one, and sometimes a custom one.
|
||||
|
||||
In particular, Python 2 uses str and '' for byte strings, while Python 3
|
||||
uses str and '' for unicode strings. We will call each of these the 'native
|
||||
string' type for each version. Because of this major difference, this module
|
||||
provides
|
||||
two functions: 'ntob', which translates native strings (of type 'str') into
|
||||
byte strings regardless of Python version, and 'ntou', which translates native
|
||||
strings to unicode strings. This also provides a 'BytesIO' name for dealing
|
||||
specifically with bytes, and a 'StringIO' name for dealing with native strings.
|
||||
It also provides a 'base64_decode' function with native strings as input and
|
||||
output.
|
||||
"""
|
||||
|
||||
import binascii
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
|
||||
import six
|
||||
|
||||
if six.PY3:
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a byte string in the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n.encode(encoding)
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a unicode string with the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 3, the native string type is unicode
|
||||
return n
|
||||
|
||||
def tonative(n, encoding='ISO-8859-1'):
|
||||
"""Return the given string as a native string in the given encoding."""
|
||||
# In Python 3, the native string type is unicode
|
||||
if isinstance(n, bytes):
|
||||
return n.decode(encoding)
|
||||
return n
|
||||
else:
|
||||
# Python 2
|
||||
def ntob(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a byte string in the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 2, the native string type is bytes. Assume it's already
|
||||
# in the given encoding, which for ISO-8859-1 is almost always what
|
||||
# was intended.
|
||||
return n
|
||||
|
||||
def ntou(n, encoding='ISO-8859-1'):
|
||||
"""Return the given native string as a unicode string with the given
|
||||
encoding.
|
||||
"""
|
||||
assert_native(n)
|
||||
# In Python 2, the native string type is bytes.
|
||||
# First, check for the special encoding 'escape'. The test suite uses
|
||||
# this to signal that it wants to pass a string with embedded \uXXXX
|
||||
# escapes, but without having to prefix it with u'' for Python 2,
|
||||
# but no prefix for Python 3.
|
||||
if encoding == 'escape':
|
||||
return unicode(
|
||||
re.sub(r'\\u([0-9a-zA-Z]{4})',
|
||||
lambda m: unichr(int(m.group(1), 16)),
|
||||
n.decode('ISO-8859-1')))
|
||||
# Assume it's already in the given encoding, which for ISO-8859-1
|
||||
# is almost always what was intended.
|
||||
return n.decode(encoding)
|
||||
|
||||
def tonative(n, encoding='ISO-8859-1'):
|
||||
"""Return the given string as a native string in the given encoding."""
|
||||
# In Python 2, the native string type is bytes.
|
||||
if isinstance(n, unicode):
|
||||
return n.encode(encoding)
|
||||
return n
|
||||
|
||||
|
||||
def assert_native(n):
|
||||
if not isinstance(n, str):
|
||||
raise TypeError('n must be a native str (got %s)' % type(n).__name__)
|
||||
|
||||
try:
|
||||
# Python 3.1+
|
||||
from base64 import decodebytes as _base64_decodebytes
|
||||
except ImportError:
|
||||
# Python 3.0-
|
||||
# since CherryPy claims compability with Python 2.3, we must use
|
||||
# the legacy API of base64
|
||||
from base64 import decodestring as _base64_decodebytes
|
||||
|
||||
|
||||
def base64_decode(n, encoding='ISO-8859-1'):
|
||||
"""Return the native string base64-decoded (as a native string)."""
|
||||
if isinstance(n, six.text_type):
|
||||
b = n.encode(encoding)
|
||||
else:
|
||||
b = n
|
||||
b = _base64_decodebytes(b)
|
||||
if str is six.text_type:
|
||||
return b.decode(encoding)
|
||||
else:
|
||||
return b
|
||||
|
||||
|
||||
try:
|
||||
sorted = sorted
|
||||
except NameError:
|
||||
def sorted(i):
|
||||
i = i[:]
|
||||
i.sort()
|
||||
return i
|
||||
|
||||
try:
|
||||
reversed = reversed
|
||||
except NameError:
|
||||
def reversed(x):
|
||||
i = len(x)
|
||||
while i > 0:
|
||||
i -= 1
|
||||
yield x[i]
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
from urllib.parse import urljoin, urlencode
|
||||
from urllib.parse import quote, quote_plus
|
||||
from urllib.request import unquote, urlopen
|
||||
from urllib.request import parse_http_list, parse_keqv_list
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urlparse import urljoin # noqa
|
||||
from urllib import urlencode, urlopen # noqa
|
||||
from urllib import quote, quote_plus # noqa
|
||||
from urllib import unquote # noqa
|
||||
from urllib2 import parse_http_list, parse_keqv_list # noqa
|
||||
|
||||
try:
|
||||
dict.iteritems
|
||||
# Python 2
|
||||
iteritems = lambda d: d.iteritems()
|
||||
copyitems = lambda d: d.items()
|
||||
except AttributeError:
|
||||
# Python 3
|
||||
iteritems = lambda d: d.items()
|
||||
copyitems = lambda d: list(d.items())
|
||||
|
||||
try:
|
||||
dict.iterkeys
|
||||
# Python 2
|
||||
iterkeys = lambda d: d.iterkeys()
|
||||
copykeys = lambda d: d.keys()
|
||||
except AttributeError:
|
||||
# Python 3
|
||||
iterkeys = lambda d: d.keys()
|
||||
copykeys = lambda d: list(d.keys())
|
||||
|
||||
try:
|
||||
dict.itervalues
|
||||
# Python 2
|
||||
itervalues = lambda d: d.itervalues()
|
||||
copyvalues = lambda d: d.values()
|
||||
except AttributeError:
|
||||
# Python 3
|
||||
itervalues = lambda d: d.values()
|
||||
copyvalues = lambda d: list(d.values())
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
import builtins
|
||||
except ImportError:
|
||||
# Python 2
|
||||
import __builtin__ as builtins # noqa
|
||||
|
||||
try:
|
||||
# Python 2. We try Python 2 first clients on Python 2
|
||||
# don't try to import the 'http' module from cherrypy.lib
|
||||
from Cookie import SimpleCookie, CookieError
|
||||
from httplib import BadStatusLine, HTTPConnection, IncompleteRead
|
||||
from httplib import NotConnected
|
||||
from BaseHTTPServer import BaseHTTPRequestHandler
|
||||
except ImportError:
|
||||
# Python 3
|
||||
from http.cookies import SimpleCookie, CookieError # noqa
|
||||
from http.client import BadStatusLine, HTTPConnection, IncompleteRead # noqa
|
||||
from http.client import NotConnected # noqa
|
||||
from http.server import BaseHTTPRequestHandler # noqa
|
||||
|
||||
# Some platforms don't expose HTTPSConnection, so handle it separately
|
||||
if six.PY3:
|
||||
try:
|
||||
from http.client import HTTPSConnection
|
||||
except ImportError:
|
||||
# Some platforms which don't have SSL don't expose HTTPSConnection
|
||||
HTTPSConnection = None
|
||||
else:
|
||||
try:
|
||||
from httplib import HTTPSConnection
|
||||
except ImportError:
|
||||
HTTPSConnection = None
|
||||
|
||||
try:
|
||||
# Python 2
|
||||
xrange = xrange
|
||||
except NameError:
|
||||
# Python 3
|
||||
xrange = range
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
from urllib.parse import unquote as parse_unquote
|
||||
|
||||
def unquote_qs(atom, encoding, errors='strict'):
|
||||
return parse_unquote(
|
||||
atom.replace('+', ' '),
|
||||
encoding=encoding,
|
||||
errors=errors)
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from urllib import unquote as parse_unquote
|
||||
|
||||
def unquote_qs(atom, encoding, errors='strict'):
|
||||
return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors)
|
||||
|
||||
try:
|
||||
# Prefer simplejson, which is usually more advanced than the builtin
|
||||
# module.
|
||||
import simplejson as json
|
||||
json_decode = json.JSONDecoder().decode
|
||||
_json_encode = json.JSONEncoder().iterencode
|
||||
except ImportError:
|
||||
if sys.version_info >= (2, 6):
|
||||
# Python >=2.6 : json is part of the standard library
|
||||
import json
|
||||
json_decode = json.JSONDecoder().decode
|
||||
_json_encode = json.JSONEncoder().iterencode
|
||||
else:
|
||||
json = None
|
||||
|
||||
def json_decode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
|
||||
def _json_encode(s):
|
||||
raise ValueError('No JSON library is available')
|
||||
finally:
|
||||
if json and six.PY3:
|
||||
# The two Python 3 implementations (simplejson/json)
|
||||
# outputs str. We need bytes.
|
||||
def json_encode(value):
|
||||
for chunk in _json_encode(value):
|
||||
yield chunk.encode('utf8')
|
||||
else:
|
||||
json_encode = _json_encode
|
||||
|
||||
text_or_bytes = six.text_type, six.binary_type
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
# In Python 2, pickle is a Python version.
|
||||
# In Python 3, pickle is the sped-up C version.
|
||||
import pickle # noqa
|
||||
|
||||
def random20():
|
||||
return binascii.hexlify(os.urandom(20)).decode('ascii')
|
||||
|
||||
try:
|
||||
from _thread import get_ident as get_thread_ident
|
||||
except ImportError:
|
||||
from thread import get_ident as get_thread_ident # noqa
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
next = next
|
||||
except NameError:
|
||||
# Python 2
|
||||
def next(i):
|
||||
return i.next()
|
||||
|
||||
if sys.version_info >= (3, 3):
|
||||
Timer = threading.Timer
|
||||
Event = threading.Event
|
||||
else:
|
||||
# Python 3.2 and earlier
|
||||
Timer = threading._Timer
|
||||
Event = threading._Event
|
||||
|
||||
try:
|
||||
# Python 2.7+
|
||||
from subprocess import _args_from_interpreter_flags
|
||||
except ImportError:
|
||||
def _args_from_interpreter_flags():
|
||||
"""Tries to reconstruct original interpreter args from sys.flags for Python 2.6
|
||||
|
||||
Backported from Python 3.5. Aims to return a list of
|
||||
command-line arguments reproducing the current
|
||||
settings in sys.flags and sys.warnoptions.
|
||||
"""
|
||||
flag_opt_map = {
|
||||
'debug': 'd',
|
||||
# 'inspect': 'i',
|
||||
# 'interactive': 'i',
|
||||
'optimize': 'O',
|
||||
'dont_write_bytecode': 'B',
|
||||
'no_user_site': 's',
|
||||
'no_site': 'S',
|
||||
'ignore_environment': 'E',
|
||||
'verbose': 'v',
|
||||
'bytes_warning': 'b',
|
||||
'quiet': 'q',
|
||||
'hash_randomization': 'R',
|
||||
'py3k_warning': '3',
|
||||
}
|
||||
|
||||
args = []
|
||||
for flag, opt in flag_opt_map.items():
|
||||
v = getattr(sys.flags, flag)
|
||||
if v > 0:
|
||||
if flag == 'hash_randomization':
|
||||
v = 1 # Handle specification of an exact seed
|
||||
args.append('-' + opt * v)
|
||||
for opt in sys.warnoptions:
|
||||
args.append('-W' + opt)
|
||||
|
||||
return args
|
||||
|
||||
# html module come in 3.2 version
|
||||
try:
|
||||
from html import escape
|
||||
except ImportError:
|
||||
from cgi import escape
|
||||
|
||||
# html module needed the argument quote=False because in cgi the default
|
||||
# is False. With quote=True the results differ.
|
||||
|
||||
def escape_html(s, escape_quote=False):
|
||||
"""Replace special characters "&", "<" and ">" to HTML-safe sequences.
|
||||
|
||||
When escape_quote=True, escape (') and (") chars.
|
||||
"""
|
||||
return escape(s, quote=escape_quote)
|
|
@ -1,303 +0,0 @@
|
|||
"""
|
||||
Configuration system for CherryPy.
|
||||
|
||||
Configuration in CherryPy is implemented via dictionaries. Keys are strings
|
||||
which name the mapped value, which may be of any type.
|
||||
|
||||
|
||||
Architecture
|
||||
------------
|
||||
|
||||
CherryPy Requests are part of an Application, which runs in a global context,
|
||||
and configuration data may apply to any of those three scopes:
|
||||
|
||||
Global
|
||||
Configuration entries which apply everywhere are stored in
|
||||
cherrypy.config.
|
||||
|
||||
Application
|
||||
Entries which apply to each mounted application are stored
|
||||
on the Application object itself, as 'app.config'. This is a two-level
|
||||
dict where each key is a path, or "relative URL" (for example, "/" or
|
||||
"/path/to/my/page"), and each value is a config dict. Usually, this
|
||||
data is provided in the call to tree.mount(root(), config=conf),
|
||||
although you may also use app.merge(conf).
|
||||
|
||||
Request
|
||||
Each Request object possesses a single 'Request.config' dict.
|
||||
Early in the request process, this dict is populated by merging global
|
||||
config entries, Application entries (whose path equals or is a parent
|
||||
of Request.path_info), and any config acquired while looking up the
|
||||
page handler (see next).
|
||||
|
||||
|
||||
Declaration
|
||||
-----------
|
||||
|
||||
Configuration data may be supplied as a Python dictionary, as a filename,
|
||||
or as an open file object. When you supply a filename or file, CherryPy
|
||||
uses Python's builtin ConfigParser; you declare Application config by
|
||||
writing each path as a section header::
|
||||
|
||||
[/path/to/my/page]
|
||||
request.stream = True
|
||||
|
||||
To declare global configuration entries, place them in a [global] section.
|
||||
|
||||
You may also declare config entries directly on the classes and methods
|
||||
(page handlers) that make up your CherryPy application via the ``_cp_config``
|
||||
attribute, set with the ``cherrypy.config`` decorator. For example::
|
||||
|
||||
@cherrypy.config(**{'tools.gzip.on': True})
|
||||
class Demo:
|
||||
|
||||
@cherrypy.expose
|
||||
@cherrypy.config(**{'request.show_tracebacks': False})
|
||||
def index(self):
|
||||
return "Hello world"
|
||||
|
||||
.. note::
|
||||
|
||||
This behavior is only guaranteed for the default dispatcher.
|
||||
Other dispatchers may have different restrictions on where
|
||||
you can attach config attributes.
|
||||
|
||||
|
||||
Namespaces
|
||||
----------
|
||||
|
||||
Configuration keys are separated into namespaces by the first "." in the key.
|
||||
Current namespaces:
|
||||
|
||||
engine
|
||||
Controls the 'application engine', including autoreload.
|
||||
These can only be declared in the global config.
|
||||
|
||||
tree
|
||||
Grafts cherrypy.Application objects onto cherrypy.tree.
|
||||
These can only be declared in the global config.
|
||||
|
||||
hooks
|
||||
Declares additional request-processing functions.
|
||||
|
||||
log
|
||||
Configures the logging for each application.
|
||||
These can only be declared in the global or / config.
|
||||
|
||||
request
|
||||
Adds attributes to each Request.
|
||||
|
||||
response
|
||||
Adds attributes to each Response.
|
||||
|
||||
server
|
||||
Controls the default HTTP server via cherrypy.server.
|
||||
These can only be declared in the global config.
|
||||
|
||||
tools
|
||||
Runs and configures additional request-processing packages.
|
||||
|
||||
wsgi
|
||||
Adds WSGI middleware to an Application's "pipeline".
|
||||
These can only be declared in the app's root config ("/").
|
||||
|
||||
checker
|
||||
Controls the 'checker', which looks for common errors in
|
||||
app state (including config) when the engine starts.
|
||||
Global config only.
|
||||
|
||||
The only key that does not exist in a namespace is the "environment" entry.
|
||||
This special entry 'imports' other config entries from a template stored in
|
||||
cherrypy._cpconfig.environments[environment]. It only applies to the global
|
||||
config, and only when you use cherrypy.config.update.
|
||||
|
||||
You can define your own namespaces to be called at the Global, Application,
|
||||
or Request level, by adding a named handler to cherrypy.config.namespaces,
|
||||
app.namespaces, or app.request_class.namespaces. The name can
|
||||
be any string, and the handler must be either a callable or a (Python 2.5
|
||||
style) context manager.
|
||||
"""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes
|
||||
from cherrypy.lib import reprconf
|
||||
|
||||
# Deprecated in CherryPy 3.2--remove in 3.3
|
||||
NamespaceSet = reprconf.NamespaceSet
|
||||
|
||||
|
||||
def merge(base, other):
|
||||
"""Merge one app config (from a dict, file, or filename) into another.
|
||||
|
||||
If the given config is a filename, it will be appended to
|
||||
the list of files to monitor for "autoreload" changes.
|
||||
"""
|
||||
if isinstance(other, text_or_bytes):
|
||||
cherrypy.engine.autoreload.files.add(other)
|
||||
|
||||
# Load other into base
|
||||
for section, value_map in reprconf.as_dict(other).items():
|
||||
if not isinstance(value_map, dict):
|
||||
raise ValueError(
|
||||
'Application config must include section headers, but the '
|
||||
"config you tried to merge doesn't have any sections. "
|
||||
'Wrap your config in another dict with paths as section '
|
||||
"headers, for example: {'/': config}.")
|
||||
base.setdefault(section, {}).update(value_map)
|
||||
|
||||
|
||||
class Config(reprconf.Config):
|
||||
|
||||
"""The 'global' configuration data for the entire CherryPy process."""
|
||||
|
||||
def update(self, config):
|
||||
"""Update self from a dict, file or filename."""
|
||||
if isinstance(config, text_or_bytes):
|
||||
# Filename
|
||||
cherrypy.engine.autoreload.files.add(config)
|
||||
reprconf.Config.update(self, config)
|
||||
|
||||
def _apply(self, config):
|
||||
"""Update self from a dict."""
|
||||
if isinstance(config.get('global'), dict):
|
||||
if len(config) > 1:
|
||||
cherrypy.checker.global_config_contained_paths = True
|
||||
config = config['global']
|
||||
if 'tools.staticdir.dir' in config:
|
||||
config['tools.staticdir.section'] = 'global'
|
||||
reprconf.Config._apply(self, config)
|
||||
|
||||
@staticmethod
|
||||
def __call__(*args, **kwargs):
|
||||
"""Decorator for page handlers to set _cp_config."""
|
||||
if args:
|
||||
raise TypeError(
|
||||
'The cherrypy.config decorator does not accept positional '
|
||||
'arguments; you must use keyword arguments.')
|
||||
|
||||
def tool_decorator(f):
|
||||
_Vars(f).setdefault('_cp_config', {}).update(kwargs)
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
|
||||
class _Vars(object):
|
||||
"""
|
||||
Adapter that allows setting a default attribute on a function
|
||||
or class.
|
||||
"""
|
||||
def __init__(self, target):
|
||||
self.target = target
|
||||
|
||||
def setdefault(self, key, default):
|
||||
if not hasattr(self.target, key):
|
||||
setattr(self.target, key, default)
|
||||
return getattr(self.target, key)
|
||||
|
||||
|
||||
# Sphinx begin config.environments
|
||||
Config.environments = environments = {
|
||||
'staging': {
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
},
|
||||
'production': {
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
'log.screen': False,
|
||||
},
|
||||
'embedded': {
|
||||
# For use with CherryPy embedded in another deployment stack.
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': False,
|
||||
'request.show_mismatched_params': False,
|
||||
'log.screen': False,
|
||||
'engine.SIGHUP': None,
|
||||
'engine.SIGTERM': None,
|
||||
},
|
||||
'test_suite': {
|
||||
'engine.autoreload.on': False,
|
||||
'checker.on': False,
|
||||
'tools.log_headers.on': False,
|
||||
'request.show_tracebacks': True,
|
||||
'request.show_mismatched_params': True,
|
||||
'log.screen': False,
|
||||
},
|
||||
}
|
||||
# Sphinx end config.environments
|
||||
|
||||
|
||||
def _server_namespace_handler(k, v):
|
||||
"""Config handler for the "server" namespace."""
|
||||
atoms = k.split('.', 1)
|
||||
if len(atoms) > 1:
|
||||
# Special-case config keys of the form 'server.servername.socket_port'
|
||||
# to configure additional HTTP servers.
|
||||
if not hasattr(cherrypy, 'servers'):
|
||||
cherrypy.servers = {}
|
||||
|
||||
servername, k = atoms
|
||||
if servername not in cherrypy.servers:
|
||||
from cherrypy import _cpserver
|
||||
cherrypy.servers[servername] = _cpserver.Server()
|
||||
# On by default, but 'on = False' can unsubscribe it (see below).
|
||||
cherrypy.servers[servername].subscribe()
|
||||
|
||||
if k == 'on':
|
||||
if v:
|
||||
cherrypy.servers[servername].subscribe()
|
||||
else:
|
||||
cherrypy.servers[servername].unsubscribe()
|
||||
else:
|
||||
setattr(cherrypy.servers[servername], k, v)
|
||||
else:
|
||||
setattr(cherrypy.server, k, v)
|
||||
Config.namespaces['server'] = _server_namespace_handler
|
||||
|
||||
|
||||
def _engine_namespace_handler(k, v):
|
||||
"""Config handler for the "engine" namespace."""
|
||||
engine = cherrypy.engine
|
||||
|
||||
if k == 'SIGHUP':
|
||||
engine.subscribe('SIGHUP', v)
|
||||
elif k == 'SIGTERM':
|
||||
engine.subscribe('SIGTERM', v)
|
||||
elif '.' in k:
|
||||
plugin, attrname = k.split('.', 1)
|
||||
plugin = getattr(engine, plugin)
|
||||
if attrname == 'on':
|
||||
if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'):
|
||||
plugin.subscribe()
|
||||
return
|
||||
elif (
|
||||
(not v) and
|
||||
hasattr(getattr(plugin, 'unsubscribe', None), '__call__')
|
||||
):
|
||||
plugin.unsubscribe()
|
||||
return
|
||||
setattr(plugin, attrname, v)
|
||||
else:
|
||||
setattr(engine, k, v)
|
||||
Config.namespaces['engine'] = _engine_namespace_handler
|
||||
|
||||
|
||||
def _tree_namespace_handler(k, v):
|
||||
"""Namespace handler for the 'tree' config namespace."""
|
||||
if isinstance(v, dict):
|
||||
for script_name, app in v.items():
|
||||
cherrypy.tree.graft(app, script_name)
|
||||
msg = 'Mounted: %s on %s' % (app, script_name or '/')
|
||||
cherrypy.engine.log(msg)
|
||||
else:
|
||||
cherrypy.tree.graft(v, v.script_name)
|
||||
cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
|
||||
Config.namespaces['tree'] = _tree_namespace_handler
|
|
@ -1,691 +0,0 @@
|
|||
"""CherryPy dispatchers.
|
||||
|
||||
A 'dispatcher' is the object which looks up the 'page handler' callable
|
||||
and collects config for the current request based on the path_info, other
|
||||
request attributes, and the application architecture. The core calls the
|
||||
dispatcher as early as possible, passing it a 'path_info' argument.
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
"""
|
||||
|
||||
import string
|
||||
import sys
|
||||
import types
|
||||
try:
|
||||
classtype = (type, types.ClassType)
|
||||
except AttributeError:
|
||||
classtype = type
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
class PageHandler(object):
|
||||
|
||||
"""Callable which sets response.body."""
|
||||
|
||||
def __init__(self, callable, *args, **kwargs):
|
||||
self.callable = callable
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
|
||||
def get_args(self):
|
||||
return cherrypy.serving.request.args
|
||||
|
||||
def set_args(self, args):
|
||||
cherrypy.serving.request.args = args
|
||||
return cherrypy.serving.request.args
|
||||
|
||||
args = property(
|
||||
get_args,
|
||||
set_args,
|
||||
doc='The ordered args should be accessible from post dispatch hooks'
|
||||
)
|
||||
|
||||
def get_kwargs(self):
|
||||
return cherrypy.serving.request.kwargs
|
||||
|
||||
def set_kwargs(self, kwargs):
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
return cherrypy.serving.request.kwargs
|
||||
|
||||
kwargs = property(
|
||||
get_kwargs,
|
||||
set_kwargs,
|
||||
doc='The named kwargs should be accessible from post dispatch hooks'
|
||||
)
|
||||
|
||||
def __call__(self):
|
||||
try:
|
||||
return self.callable(*self.args, **self.kwargs)
|
||||
except TypeError:
|
||||
x = sys.exc_info()[1]
|
||||
try:
|
||||
test_callable_spec(self.callable, self.args, self.kwargs)
|
||||
except cherrypy.HTTPError:
|
||||
raise sys.exc_info()[1]
|
||||
except:
|
||||
raise x
|
||||
raise
|
||||
|
||||
|
||||
def test_callable_spec(callable, callable_args, callable_kwargs):
|
||||
"""
|
||||
Inspect callable and test to see if the given args are suitable for it.
|
||||
|
||||
When an error occurs during the handler's invoking stage there are 2
|
||||
erroneous cases:
|
||||
1. Too many parameters passed to a function which doesn't define
|
||||
one of *args or **kwargs.
|
||||
2. Too little parameters are passed to the function.
|
||||
|
||||
There are 3 sources of parameters to a cherrypy handler.
|
||||
1. query string parameters are passed as keyword parameters to the
|
||||
handler.
|
||||
2. body parameters are also passed as keyword parameters.
|
||||
3. when partial matching occurs, the final path atoms are passed as
|
||||
positional args.
|
||||
Both the query string and path atoms are part of the URI. If they are
|
||||
incorrect, then a 404 Not Found should be raised. Conversely the body
|
||||
parameters are part of the request; if they are invalid a 400 Bad Request.
|
||||
"""
|
||||
show_mismatched_params = getattr(
|
||||
cherrypy.serving.request, 'show_mismatched_params', False)
|
||||
try:
|
||||
(args, varargs, varkw, defaults) = getargspec(callable)
|
||||
except TypeError:
|
||||
if isinstance(callable, object) and hasattr(callable, '__call__'):
|
||||
(args, varargs, varkw,
|
||||
defaults) = getargspec(callable.__call__)
|
||||
else:
|
||||
# If it wasn't one of our own types, re-raise
|
||||
# the original error
|
||||
raise
|
||||
|
||||
if args and (
|
||||
# For callable objects, which have a __call__(self) method
|
||||
hasattr(callable, '__call__') or
|
||||
# For normal methods
|
||||
inspect.ismethod(callable)
|
||||
):
|
||||
# Strip 'self'
|
||||
args = args[1:]
|
||||
|
||||
arg_usage = dict([(arg, 0,) for arg in args])
|
||||
vararg_usage = 0
|
||||
varkw_usage = 0
|
||||
extra_kwargs = set()
|
||||
|
||||
for i, value in enumerate(callable_args):
|
||||
try:
|
||||
arg_usage[args[i]] += 1
|
||||
except IndexError:
|
||||
vararg_usage += 1
|
||||
|
||||
for key in callable_kwargs.keys():
|
||||
try:
|
||||
arg_usage[key] += 1
|
||||
except KeyError:
|
||||
varkw_usage += 1
|
||||
extra_kwargs.add(key)
|
||||
|
||||
# figure out which args have defaults.
|
||||
args_with_defaults = args[-len(defaults or []):]
|
||||
for i, val in enumerate(defaults or []):
|
||||
# Defaults take effect only when the arg hasn't been used yet.
|
||||
if arg_usage[args_with_defaults[i]] == 0:
|
||||
arg_usage[args_with_defaults[i]] += 1
|
||||
|
||||
missing_args = []
|
||||
multiple_args = []
|
||||
for key, usage in arg_usage.items():
|
||||
if usage == 0:
|
||||
missing_args.append(key)
|
||||
elif usage > 1:
|
||||
multiple_args.append(key)
|
||||
|
||||
if missing_args:
|
||||
# In the case where the method allows body arguments
|
||||
# there are 3 potential errors:
|
||||
# 1. not enough query string parameters -> 404
|
||||
# 2. not enough body parameters -> 400
|
||||
# 3. not enough path parts (partial matches) -> 404
|
||||
#
|
||||
# We can't actually tell which case it is,
|
||||
# so I'm raising a 404 because that covers 2/3 of the
|
||||
# possibilities
|
||||
#
|
||||
# In the case where the method does not allow body
|
||||
# arguments it's definitely a 404.
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message = 'Missing parameters: %s' % ','.join(missing_args)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# the extra positional arguments come from the path - 404 Not Found
|
||||
if not varargs and vararg_usage > 0:
|
||||
raise cherrypy.HTTPError(404)
|
||||
|
||||
body_params = cherrypy.serving.request.body.params or {}
|
||||
body_params = set(body_params.keys())
|
||||
qs_params = set(callable_kwargs.keys()) - body_params
|
||||
|
||||
if multiple_args:
|
||||
if qs_params.intersection(set(multiple_args)):
|
||||
# If any of the multiple parameters came from the query string then
|
||||
# it's a 404 Not Found
|
||||
error = 404
|
||||
else:
|
||||
# Otherwise it's a 400 Bad Request
|
||||
error = 400
|
||||
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message = 'Multiple values for parameters: '\
|
||||
'%s' % ','.join(multiple_args)
|
||||
raise cherrypy.HTTPError(error, message=message)
|
||||
|
||||
if not varkw and varkw_usage > 0:
|
||||
|
||||
# If there were extra query string parameters, it's a 404 Not Found
|
||||
extra_qs_params = set(qs_params).intersection(extra_kwargs)
|
||||
if extra_qs_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message = 'Unexpected query string '\
|
||||
'parameters: %s' % ', '.join(extra_qs_params)
|
||||
raise cherrypy.HTTPError(404, message=message)
|
||||
|
||||
# If there were any extra body parameters, it's a 400 Not Found
|
||||
extra_body_params = set(body_params).intersection(extra_kwargs)
|
||||
if extra_body_params:
|
||||
message = None
|
||||
if show_mismatched_params:
|
||||
message = 'Unexpected body parameters: '\
|
||||
'%s' % ', '.join(extra_body_params)
|
||||
raise cherrypy.HTTPError(400, message=message)
|
||||
|
||||
|
||||
try:
|
||||
import inspect
|
||||
except ImportError:
|
||||
test_callable_spec = lambda callable, args, kwargs: None
|
||||
else:
|
||||
getargspec = inspect.getargspec
|
||||
# Python 3 requires using getfullargspec if keyword-only arguments are present
|
||||
if hasattr(inspect, 'getfullargspec'):
|
||||
def getargspec(callable):
|
||||
return inspect.getfullargspec(callable)[:4]
|
||||
|
||||
|
||||
class LateParamPageHandler(PageHandler):
|
||||
|
||||
"""When passing cherrypy.request.params to the page handler, we do not
|
||||
want to capture that dict too early; we want to give tools like the
|
||||
decoding tool a chance to modify the params dict in-between the lookup
|
||||
of the handler and the actual calling of the handler. This subclass
|
||||
takes that into account, and allows request.params to be 'bound late'
|
||||
(it's more complicated than that, but that's the effect).
|
||||
"""
|
||||
|
||||
def _get_kwargs(self):
|
||||
kwargs = cherrypy.serving.request.params.copy()
|
||||
if self._kwargs:
|
||||
kwargs.update(self._kwargs)
|
||||
return kwargs
|
||||
|
||||
def _set_kwargs(self, kwargs):
|
||||
cherrypy.serving.request.kwargs = kwargs
|
||||
self._kwargs = kwargs
|
||||
|
||||
kwargs = property(_get_kwargs, _set_kwargs,
|
||||
doc='page handler kwargs (with '
|
||||
'cherrypy.request.params copied in)')
|
||||
|
||||
|
||||
if sys.version_info < (3, 0):
|
||||
punctuation_to_underscores = string.maketrans(
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
if not isinstance(t, str) or len(t) != 256:
|
||||
raise ValueError(
|
||||
'The translate argument must be a str of len 256.')
|
||||
else:
|
||||
punctuation_to_underscores = str.maketrans(
|
||||
string.punctuation, '_' * len(string.punctuation))
|
||||
|
||||
def validate_translator(t):
|
||||
if not isinstance(t, dict):
|
||||
raise ValueError('The translate argument must be a dict.')
|
||||
|
||||
|
||||
class Dispatcher(object):
|
||||
|
||||
"""CherryPy Dispatcher which walks a tree of objects to find a handler.
|
||||
|
||||
The tree is rooted at cherrypy.request.app.root, and each hierarchical
|
||||
component in the path_info argument is matched to a corresponding nested
|
||||
attribute of the root object. Matching handlers must have an 'exposed'
|
||||
attribute which evaluates to True. The special method name "index"
|
||||
matches a URI which ends in a slash ("/"). The special method name
|
||||
"default" may match a portion of the path_info (but only when no longer
|
||||
substring of the path_info matches some other object).
|
||||
|
||||
This is the default, built-in dispatcher for CherryPy.
|
||||
"""
|
||||
|
||||
dispatch_method_name = '_cp_dispatch'
|
||||
"""
|
||||
The name of the dispatch method that nodes may optionally implement
|
||||
to provide their own dynamic dispatch algorithm.
|
||||
"""
|
||||
|
||||
def __init__(self, dispatch_method_name=None,
|
||||
translate=punctuation_to_underscores):
|
||||
validate_translator(translate)
|
||||
self.translate = translate
|
||||
if dispatch_method_name:
|
||||
self.dispatch_method_name = dispatch_method_name
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
func, vpath = self.find_handler(path_info)
|
||||
|
||||
if func:
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace('%2F', '/') for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
def find_handler(self, path):
|
||||
"""Return the appropriate page handler, plus any virtual path.
|
||||
|
||||
This will return two objects. The first will be a callable,
|
||||
which can be used to generate page output. Any parameters from
|
||||
the query string or request body will be sent to that callable
|
||||
as keyword arguments.
|
||||
|
||||
The callable is found by traversing the application's tree,
|
||||
starting from cherrypy.request.app.root, and matching path
|
||||
components to successive objects in the tree. For example, the
|
||||
URL "/path/to/handler" might return root.path.to.handler.
|
||||
|
||||
The second object returned will be a list of names which are
|
||||
'virtual path' components: parts of the URL which are dynamic,
|
||||
and were not used when looking up the handler.
|
||||
These virtual path components are passed to the handler as
|
||||
positional arguments.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
app = request.app
|
||||
root = app.root
|
||||
dispatch_name = self.dispatch_method_name
|
||||
|
||||
# Get config for the root object/path.
|
||||
fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
|
||||
fullpath_len = len(fullpath)
|
||||
segleft = fullpath_len
|
||||
nodeconf = {}
|
||||
if hasattr(root, '_cp_config'):
|
||||
nodeconf.update(root._cp_config)
|
||||
if '/' in app.config:
|
||||
nodeconf.update(app.config['/'])
|
||||
object_trail = [['root', root, nodeconf, segleft]]
|
||||
|
||||
node = root
|
||||
iternames = fullpath[:]
|
||||
while iternames:
|
||||
name = iternames[0]
|
||||
# map to legal Python identifiers (e.g. replace '.' with '_')
|
||||
objname = name.translate(self.translate)
|
||||
|
||||
nodeconf = {}
|
||||
subnode = getattr(node, objname, None)
|
||||
pre_len = len(iternames)
|
||||
if subnode is None:
|
||||
dispatch = getattr(node, dispatch_name, None)
|
||||
if dispatch and hasattr(dispatch, '__call__') and not \
|
||||
getattr(dispatch, 'exposed', False) and \
|
||||
pre_len > 1:
|
||||
# Don't expose the hidden 'index' token to _cp_dispatch
|
||||
# We skip this if pre_len == 1 since it makes no sense
|
||||
# to call a dispatcher when we have no tokens left.
|
||||
index_name = iternames.pop()
|
||||
subnode = dispatch(vpath=iternames)
|
||||
iternames.append(index_name)
|
||||
else:
|
||||
# We didn't find a path, but keep processing in case there
|
||||
# is a default() handler.
|
||||
iternames.pop(0)
|
||||
else:
|
||||
# We found the path, remove the vpath entry
|
||||
iternames.pop(0)
|
||||
segleft = len(iternames)
|
||||
if segleft > pre_len:
|
||||
# No path segment was removed. Raise an error.
|
||||
raise cherrypy.CherryPyException(
|
||||
'A vpath segment was added. Custom dispatchers may only '
|
||||
+ 'remove elements. While trying to process '
|
||||
+ '{0} in {1}'.format(name, fullpath)
|
||||
)
|
||||
elif segleft == pre_len:
|
||||
# Assume that the handler used the current path segment, but
|
||||
# did not pop it. This allows things like
|
||||
# return getattr(self, vpath[0], None)
|
||||
iternames.pop(0)
|
||||
segleft -= 1
|
||||
node = subnode
|
||||
|
||||
if node is not None:
|
||||
# Get _cp_config attached to this node.
|
||||
if hasattr(node, '_cp_config'):
|
||||
nodeconf.update(node._cp_config)
|
||||
|
||||
# Mix in values from app.config for this path.
|
||||
existing_len = fullpath_len - pre_len
|
||||
if existing_len != 0:
|
||||
curpath = '/' + '/'.join(fullpath[0:existing_len])
|
||||
else:
|
||||
curpath = ''
|
||||
new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
|
||||
for seg in new_segs:
|
||||
curpath += '/' + seg
|
||||
if curpath in app.config:
|
||||
nodeconf.update(app.config[curpath])
|
||||
|
||||
object_trail.append([name, node, nodeconf, segleft])
|
||||
|
||||
def set_conf():
|
||||
"""Collapse all object_trail config into cherrypy.request.config.
|
||||
"""
|
||||
base = cherrypy.config.copy()
|
||||
# Note that we merge the config from each node
|
||||
# even if that node was None.
|
||||
for name, obj, conf, segleft in object_trail:
|
||||
base.update(conf)
|
||||
if 'tools.staticdir.dir' in conf:
|
||||
base['tools.staticdir.section'] = '/' + \
|
||||
'/'.join(fullpath[0:fullpath_len - segleft])
|
||||
return base
|
||||
|
||||
# Try successive objects (reverse order)
|
||||
num_candidates = len(object_trail) - 1
|
||||
for i in range(num_candidates, -1, -1):
|
||||
|
||||
name, candidate, nodeconf, segleft = object_trail[i]
|
||||
if candidate is None:
|
||||
continue
|
||||
|
||||
# Try a "default" method on the current leaf.
|
||||
if hasattr(candidate, 'default'):
|
||||
defhandler = candidate.default
|
||||
if getattr(defhandler, 'exposed', False):
|
||||
# Insert any extra _cp_config from the default handler.
|
||||
conf = getattr(defhandler, '_cp_config', {})
|
||||
object_trail.insert(
|
||||
i + 1, ['default', defhandler, conf, segleft])
|
||||
request.config = set_conf()
|
||||
# See https://github.com/cherrypy/cherrypy/issues/613
|
||||
request.is_index = path.endswith('/')
|
||||
return defhandler, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
# Uncomment the next line to restrict positional params to
|
||||
# "default".
|
||||
# if i < num_candidates - 2: continue
|
||||
|
||||
# Try the current leaf.
|
||||
if getattr(candidate, 'exposed', False):
|
||||
request.config = set_conf()
|
||||
if i == num_candidates:
|
||||
# We found the extra ".index". Mark request so tools
|
||||
# can redirect if path_info has no trailing slash.
|
||||
request.is_index = True
|
||||
else:
|
||||
# We're not at an 'index' handler. Mark request so tools
|
||||
# can redirect if path_info has NO trailing slash.
|
||||
# Note that this also includes handlers which take
|
||||
# positional parameters (virtual paths).
|
||||
request.is_index = False
|
||||
return candidate, fullpath[fullpath_len - segleft:-1]
|
||||
|
||||
# We didn't find anything
|
||||
request.config = set_conf()
|
||||
return None, []
|
||||
|
||||
|
||||
class MethodDispatcher(Dispatcher):
|
||||
|
||||
"""Additional dispatch based on cherrypy.request.method.upper().
|
||||
|
||||
Methods named GET, POST, etc will be called on an exposed class.
|
||||
The method names must be all caps; the appropriate Allow header
|
||||
will be output showing all capitalized method names as allowable
|
||||
HTTP verbs.
|
||||
|
||||
Note that the containing class must be exposed, not the methods.
|
||||
"""
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
request = cherrypy.serving.request
|
||||
resource, vpath = self.find_handler(path_info)
|
||||
|
||||
if resource:
|
||||
# Set Allow header
|
||||
avail = [m for m in dir(resource) if m.isupper()]
|
||||
if 'GET' in avail and 'HEAD' not in avail:
|
||||
avail.append('HEAD')
|
||||
avail.sort()
|
||||
cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
|
||||
|
||||
# Find the subhandler
|
||||
meth = request.method.upper()
|
||||
func = getattr(resource, meth, None)
|
||||
if func is None and meth == 'HEAD':
|
||||
func = getattr(resource, 'GET', None)
|
||||
if func:
|
||||
# Grab any _cp_config on the subhandler.
|
||||
if hasattr(func, '_cp_config'):
|
||||
request.config.update(func._cp_config)
|
||||
|
||||
# Decode any leftover %2F in the virtual_path atoms.
|
||||
vpath = [x.replace('%2F', '/') for x in vpath]
|
||||
request.handler = LateParamPageHandler(func, *vpath)
|
||||
else:
|
||||
request.handler = cherrypy.HTTPError(405)
|
||||
else:
|
||||
request.handler = cherrypy.NotFound()
|
||||
|
||||
|
||||
class RoutesDispatcher(object):
|
||||
|
||||
"""A Routes based dispatcher for CherryPy."""
|
||||
|
||||
def __init__(self, full_result=False, **mapper_options):
|
||||
"""
|
||||
Routes dispatcher
|
||||
|
||||
Set full_result to True if you wish the controller
|
||||
and the action to be passed on to the page handler
|
||||
parameters. By default they won't be.
|
||||
"""
|
||||
import routes
|
||||
self.full_result = full_result
|
||||
self.controllers = {}
|
||||
self.mapper = routes.Mapper(**mapper_options)
|
||||
self.mapper.controller_scan = self.controllers.keys
|
||||
|
||||
def connect(self, name, route, controller, **kwargs):
|
||||
self.controllers[name] = controller
|
||||
self.mapper.connect(name, route, controller=name, **kwargs)
|
||||
|
||||
def redirect(self, url):
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
def __call__(self, path_info):
|
||||
"""Set handler and config for the current request."""
|
||||
func = self.find_handler(path_info)
|
||||
if func:
|
||||
cherrypy.serving.request.handler = LateParamPageHandler(func)
|
||||
else:
|
||||
cherrypy.serving.request.handler = cherrypy.NotFound()
|
||||
|
||||
def find_handler(self, path_info):
|
||||
"""Find the right page handler, and set request.config."""
|
||||
import routes
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
config = routes.request_config()
|
||||
config.mapper = self.mapper
|
||||
if hasattr(request, 'wsgi_environ'):
|
||||
config.environ = request.wsgi_environ
|
||||
config.host = request.headers.get('Host', None)
|
||||
config.protocol = request.scheme
|
||||
config.redirect = self.redirect
|
||||
|
||||
result = self.mapper.match(path_info)
|
||||
|
||||
config.mapper_dict = result
|
||||
params = {}
|
||||
if result:
|
||||
params = result.copy()
|
||||
if not self.full_result:
|
||||
params.pop('controller', None)
|
||||
params.pop('action', None)
|
||||
request.params.update(params)
|
||||
|
||||
# Get config for the root object/path.
|
||||
request.config = base = cherrypy.config.copy()
|
||||
curpath = ''
|
||||
|
||||
def merge(nodeconf):
|
||||
if 'tools.staticdir.dir' in nodeconf:
|
||||
nodeconf['tools.staticdir.section'] = curpath or '/'
|
||||
base.update(nodeconf)
|
||||
|
||||
app = request.app
|
||||
root = app.root
|
||||
if hasattr(root, '_cp_config'):
|
||||
merge(root._cp_config)
|
||||
if '/' in app.config:
|
||||
merge(app.config['/'])
|
||||
|
||||
# Mix in values from app.config.
|
||||
atoms = [x for x in path_info.split('/') if x]
|
||||
if atoms:
|
||||
last = atoms.pop()
|
||||
else:
|
||||
last = None
|
||||
for atom in atoms:
|
||||
curpath = '/'.join((curpath, atom))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
handler = None
|
||||
if result:
|
||||
controller = result.get('controller')
|
||||
controller = self.controllers.get(controller, controller)
|
||||
if controller:
|
||||
if isinstance(controller, classtype):
|
||||
controller = controller()
|
||||
# Get config from the controller.
|
||||
if hasattr(controller, '_cp_config'):
|
||||
merge(controller._cp_config)
|
||||
|
||||
action = result.get('action')
|
||||
if action is not None:
|
||||
handler = getattr(controller, action, None)
|
||||
# Get config from the handler
|
||||
if hasattr(handler, '_cp_config'):
|
||||
merge(handler._cp_config)
|
||||
else:
|
||||
handler = controller
|
||||
|
||||
# Do the last path atom here so it can
|
||||
# override the controller's _cp_config.
|
||||
if last:
|
||||
curpath = '/'.join((curpath, last))
|
||||
if curpath in app.config:
|
||||
merge(app.config[curpath])
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
|
||||
from cherrypy.lib import xmlrpcutil
|
||||
|
||||
def xmlrpc_dispatch(path_info):
|
||||
path_info = xmlrpcutil.patched_path(path_info)
|
||||
return next_dispatcher(path_info)
|
||||
return xmlrpc_dispatch
|
||||
|
||||
|
||||
def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
|
||||
**domains):
|
||||
"""
|
||||
Select a different handler based on the Host header.
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different parts of a single
|
||||
website structure. For example::
|
||||
|
||||
http://www.domain.example -> root
|
||||
http://www.domain2.example -> root/domain2/
|
||||
http://www.domain2.example:443 -> root/secure
|
||||
|
||||
can be accomplished via the following config::
|
||||
|
||||
[/]
|
||||
request.dispatch = cherrypy.dispatch.VirtualHost(
|
||||
**{'www.domain2.example': '/domain2',
|
||||
'www.domain2.example:443': '/secure',
|
||||
})
|
||||
|
||||
next_dispatcher
|
||||
The next dispatcher object in the dispatch chain.
|
||||
The VirtualHost dispatcher adds a prefix to the URL and calls
|
||||
another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
|
||||
|
||||
use_x_forwarded_host
|
||||
If True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying.
|
||||
|
||||
``**domains``
|
||||
A dict of {host header value: virtual prefix} pairs.
|
||||
The incoming "Host" request header is looked up in this dict,
|
||||
and, if a match is found, the corresponding "virtual prefix"
|
||||
value will be prepended to the URL path before calling the
|
||||
next dispatcher. Note that you often need separate entries
|
||||
for "example.com" and "www.example.com". In addition, "Host"
|
||||
headers may contain the port number.
|
||||
"""
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
def vhost_dispatch(path_info):
|
||||
request = cherrypy.serving.request
|
||||
header = request.headers.get
|
||||
|
||||
domain = header('Host', '')
|
||||
if use_x_forwarded_host:
|
||||
domain = header('X-Forwarded-Host', domain)
|
||||
|
||||
prefix = domains.get(domain, '')
|
||||
if prefix:
|
||||
path_info = httputil.urljoin(prefix, path_info)
|
||||
|
||||
result = next_dispatcher(path_info)
|
||||
|
||||
# Touch up staticdir config. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/614.
|
||||
section = request.config.get('tools.staticdir.section')
|
||||
if section:
|
||||
section = section[len(prefix):]
|
||||
request.config['tools.staticdir.section'] = section
|
||||
|
||||
return result
|
||||
return vhost_dispatch
|
|
@ -1,622 +0,0 @@
|
|||
"""Exception classes for CherryPy.
|
||||
|
||||
CherryPy provides (and uses) exceptions for declaring that the HTTP response
|
||||
should be a status other than the default "200 OK". You can ``raise`` them like
|
||||
normal Python exceptions. You can also call them and they will raise
|
||||
themselves; this means you can set an
|
||||
:class:`HTTPError<cherrypy._cperror.HTTPError>`
|
||||
or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
|
||||
:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
|
||||
|
||||
.. _redirectingpost:
|
||||
|
||||
Redirecting POST
|
||||
================
|
||||
|
||||
When you GET a resource and are redirected by the server to another Location,
|
||||
there's generally no problem since GET is both a "safe method" (there should
|
||||
be no side-effects) and an "idempotent method" (multiple calls are no different
|
||||
than a single call).
|
||||
|
||||
POST, however, is neither safe nor idempotent--if you
|
||||
charge a credit card, you don't want to be charged twice by a redirect!
|
||||
|
||||
For this reason, *none* of the 3xx responses permit a user-agent (browser) to
|
||||
resubmit a POST on redirection without first confirming the action with the
|
||||
user:
|
||||
|
||||
===== ================================= ===========
|
||||
300 Multiple Choices Confirm with the user
|
||||
301 Moved Permanently Confirm with the user
|
||||
302 Found (Object moved temporarily) Confirm with the user
|
||||
303 See Other GET the new URI--no confirmation
|
||||
304 Not modified (for conditional GET only--POST should not raise this error)
|
||||
305 Use Proxy Confirm with the user
|
||||
307 Temporary Redirect Confirm with the user
|
||||
===== ================================= ===========
|
||||
|
||||
However, browsers have historically implemented these restrictions poorly;
|
||||
in particular, many browsers do not force the user to confirm 301, 302
|
||||
or 307 when redirecting POST. For this reason, CherryPy defaults to 303,
|
||||
which most user-agents appear to have implemented correctly. Therefore, if
|
||||
you raise HTTPRedirect for a POST request, the user-agent will most likely
|
||||
attempt to GET the new URI (without asking for confirmation from the user).
|
||||
We realize this is confusing for developers, but it's the safest thing we
|
||||
could do. You are of course free to raise ``HTTPRedirect(uri, status=302)``
|
||||
or any other 3xx status if you know what you're doing, but given the
|
||||
environment, we couldn't let any of those be the default.
|
||||
|
||||
Custom Error Handling
|
||||
=====================
|
||||
|
||||
.. image:: /refman/cperrors.gif
|
||||
|
||||
Anticipated HTTP responses
|
||||
--------------------------
|
||||
|
||||
The 'error_page' config namespace can be used to provide custom HTML output for
|
||||
expected responses (like 404 Not Found). Supply a filename from which the
|
||||
output will be read. The contents will be interpolated with the values
|
||||
%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
|
||||
`string formatting <http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
|
||||
|
||||
::
|
||||
|
||||
_cp_config = {
|
||||
'error_page.404': os.path.join(localDir, "static/index.html")
|
||||
}
|
||||
|
||||
|
||||
Beginning in version 3.1, you may also provide a function or other callable as
|
||||
an error_page entry. It will be passed the same status, message, traceback and
|
||||
version arguments that are interpolated into templates::
|
||||
|
||||
def error_page_402(status, message, traceback, version):
|
||||
return "Error %s - Well, I'm very sorry but you haven't paid!" % status
|
||||
cherrypy.config.update({'error_page.402': error_page_402})
|
||||
|
||||
Also in 3.1, in addition to the numbered error codes, you may also supply
|
||||
"error_page.default" to handle all codes which do not have their own error_page
|
||||
entry.
|
||||
|
||||
|
||||
|
||||
Unanticipated errors
|
||||
--------------------
|
||||
|
||||
CherryPy also has a generic error handling mechanism: whenever an unanticipated
|
||||
error occurs in your code, it will call
|
||||
:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
|
||||
set the response status, headers, and body. By default, this is the same
|
||||
output as
|
||||
:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
|
||||
some other behavior, you generally replace "request.error_response".
|
||||
|
||||
Here is some sample code that shows how to display a custom error message and
|
||||
send an e-mail containing the error::
|
||||
|
||||
from cherrypy import _cperror
|
||||
|
||||
def handle_error():
|
||||
cherrypy.response.status = 500
|
||||
cherrypy.response.body = [
|
||||
"<html><body>Sorry, an error occured</body></html>"
|
||||
]
|
||||
sendMail('error@domain.com',
|
||||
'Error in your web app',
|
||||
_cperror.format_exc())
|
||||
|
||||
@cherrypy.config(**{'request.error_response': handle_error})
|
||||
class Root:
|
||||
pass
|
||||
|
||||
Note that you have to explicitly set
|
||||
:attr:`response.body <cherrypy._cprequest.Response.body>`
|
||||
and not simply return an error message as a result.
|
||||
"""
|
||||
|
||||
import contextlib
|
||||
from sys import exc_info as _exc_info
|
||||
from traceback import format_exception as _format_exception
|
||||
from xml.sax import saxutils
|
||||
|
||||
import six
|
||||
|
||||
from cherrypy._cpcompat import escape_html
|
||||
from cherrypy._cpcompat import text_or_bytes, iteritems, ntob
|
||||
from cherrypy._cpcompat import tonative, urljoin as _urljoin
|
||||
from cherrypy.lib import httputil as _httputil
|
||||
|
||||
|
||||
class CherryPyException(Exception):
|
||||
|
||||
"""A base class for CherryPy exceptions."""
|
||||
pass
|
||||
|
||||
|
||||
class TimeoutError(CherryPyException):
|
||||
|
||||
"""Exception raised when Response.timed_out is detected."""
|
||||
pass
|
||||
|
||||
|
||||
class InternalRedirect(CherryPyException):
|
||||
|
||||
"""Exception raised to switch to the handler for a different URL.
|
||||
|
||||
This exception will redirect processing to another path within the site
|
||||
(without informing the client). Provide the new path as an argument when
|
||||
raising the exception. Provide any params in the querystring for the new
|
||||
URL.
|
||||
"""
|
||||
|
||||
def __init__(self, path, query_string=''):
|
||||
import cherrypy
|
||||
self.request = cherrypy.serving.request
|
||||
|
||||
self.query_string = query_string
|
||||
if '?' in path:
|
||||
# Separate any params included in the path
|
||||
path, self.query_string = path.split('?', 1)
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a URL relative to root (e.g. "/dummy")
|
||||
# 2. a URL relative to the current path
|
||||
# Note that any query string will be discarded.
|
||||
path = _urljoin(self.request.path_info, path)
|
||||
|
||||
# Set a 'path' member attribute so that code which traps this
|
||||
# error can have access to it.
|
||||
self.path = path
|
||||
|
||||
CherryPyException.__init__(self, path, self.query_string)
|
||||
|
||||
|
||||
class HTTPRedirect(CherryPyException):
|
||||
|
||||
"""Exception raised when the request should be redirected.
|
||||
|
||||
This exception will force a HTTP redirect to the URL or URL's you give it.
|
||||
The new URL must be passed as the first argument to the Exception,
|
||||
e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
|
||||
If a URL is absolute, it will be used as-is. If it is relative, it is
|
||||
assumed to be relative to the current cherrypy.request.path_info.
|
||||
|
||||
If one of the provided URL is a unicode object, it will be encoded
|
||||
using the default encoding or the one passed in parameter.
|
||||
|
||||
There are multiple types of redirect, from which you can select via the
|
||||
``status`` argument. If you do not provide a ``status`` arg, it defaults to
|
||||
303 (or 302 if responding with HTTP/1.0).
|
||||
|
||||
Examples::
|
||||
|
||||
raise cherrypy.HTTPRedirect("")
|
||||
raise cherrypy.HTTPRedirect("/abs/path", 307)
|
||||
raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
|
||||
|
||||
See :ref:`redirectingpost` for additional caveats.
|
||||
"""
|
||||
|
||||
status = None
|
||||
"""The integer HTTP status code to emit."""
|
||||
|
||||
urls = None
|
||||
"""The list of URL's to emit."""
|
||||
|
||||
encoding = 'utf-8'
|
||||
"""The encoding when passed urls are not native strings"""
|
||||
|
||||
def __init__(self, urls, status=None, encoding=None):
|
||||
import cherrypy
|
||||
request = cherrypy.serving.request
|
||||
|
||||
if isinstance(urls, text_or_bytes):
|
||||
urls = [urls]
|
||||
|
||||
abs_urls = []
|
||||
for url in urls:
|
||||
url = tonative(url, encoding or self.encoding)
|
||||
|
||||
# Note that urljoin will "do the right thing" whether url is:
|
||||
# 1. a complete URL with host (e.g. "http://www.example.com/test")
|
||||
# 2. a URL relative to root (e.g. "/dummy")
|
||||
# 3. a URL relative to the current path
|
||||
# Note that any query string in cherrypy.request is discarded.
|
||||
url = _urljoin(cherrypy.url(), url)
|
||||
abs_urls.append(url)
|
||||
self.urls = abs_urls
|
||||
|
||||
# RFC 2616 indicates a 301 response code fits our goal; however,
|
||||
# browser support for 301 is quite messy. Do 302/303 instead. See
|
||||
# http://www.alanflavell.org.uk/www/post-redirect.html
|
||||
if status is None:
|
||||
if request.protocol >= (1, 1):
|
||||
status = 303
|
||||
else:
|
||||
status = 302
|
||||
else:
|
||||
status = int(status)
|
||||
if status < 300 or status > 399:
|
||||
raise ValueError('status must be between 300 and 399.')
|
||||
|
||||
self.status = status
|
||||
CherryPyException.__init__(self, abs_urls, status)
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPRedirect object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
response.status = status = self.status
|
||||
|
||||
if status in (300, 301, 302, 303, 307):
|
||||
response.headers['Content-Type'] = 'text/html;charset=utf-8'
|
||||
# "The ... URI SHOULD be given by the Location field
|
||||
# in the response."
|
||||
response.headers['Location'] = self.urls[0]
|
||||
|
||||
# "Unless the request method was HEAD, the entity of the response
|
||||
# SHOULD contain a short hypertext note with a hyperlink to the
|
||||
# new URI(s)."
|
||||
msg = {
|
||||
300: 'This resource can be found at ',
|
||||
301: 'This resource has permanently moved to ',
|
||||
302: 'This resource resides temporarily at ',
|
||||
303: 'This resource can be found at ',
|
||||
307: 'This resource has moved temporarily to ',
|
||||
}[status]
|
||||
msg += '<a href=%s>%s</a>.'
|
||||
msgs = [msg % (saxutils.quoteattr(u), u) for u in self.urls]
|
||||
response.body = ntob('<br />\n'.join(msgs), 'utf-8')
|
||||
# Previous code may have set C-L, so we have to reset it
|
||||
# (allow finalize to set it).
|
||||
response.headers.pop('Content-Length', None)
|
||||
elif status == 304:
|
||||
# Not Modified.
|
||||
# "The response MUST include the following header fields:
|
||||
# Date, unless its omission is required by section 14.18.1"
|
||||
# The "Date" header should have been set in Response.__init__
|
||||
|
||||
# "...the response SHOULD NOT include other entity-headers."
|
||||
for key in ('Allow', 'Content-Encoding', 'Content-Language',
|
||||
'Content-Length', 'Content-Location', 'Content-MD5',
|
||||
'Content-Range', 'Content-Type', 'Expires',
|
||||
'Last-Modified'):
|
||||
if key in response.headers:
|
||||
del response.headers[key]
|
||||
|
||||
# "The 304 response MUST NOT contain a message-body."
|
||||
response.body = None
|
||||
# Previous code may have set C-L, so we have to reset it.
|
||||
response.headers.pop('Content-Length', None)
|
||||
elif status == 305:
|
||||
# Use Proxy.
|
||||
# self.urls[0] should be the URI of the proxy.
|
||||
response.headers['Location'] = ntob(self.urls[0], 'utf-8')
|
||||
response.body = None
|
||||
# Previous code may have set C-L, so we have to reset it.
|
||||
response.headers.pop('Content-Length', None)
|
||||
else:
|
||||
raise ValueError('The %s status code is unknown.' % status)
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
||||
|
||||
def clean_headers(status):
|
||||
"""Remove any headers which should not apply to an error response."""
|
||||
import cherrypy
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# Remove headers which applied to the original content,
|
||||
# but do not apply to the error page.
|
||||
respheaders = response.headers
|
||||
for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
|
||||
'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
|
||||
'Content-Location', 'Content-MD5', 'Last-Modified']:
|
||||
if key in respheaders:
|
||||
del respheaders[key]
|
||||
|
||||
if status != 416:
|
||||
# A server sending a response with status code 416 (Requested
|
||||
# range not satisfiable) SHOULD include a Content-Range field
|
||||
# with a byte-range-resp-spec of "*". The instance-length
|
||||
# specifies the current length of the selected resource.
|
||||
# A response with status code 206 (Partial Content) MUST NOT
|
||||
# include a Content-Range field with a byte-range- resp-spec of "*".
|
||||
if 'Content-Range' in respheaders:
|
||||
del respheaders['Content-Range']
|
||||
|
||||
|
||||
class HTTPError(CherryPyException):
|
||||
|
||||
"""Exception used to return an HTTP error code (4xx-5xx) to the client.
|
||||
|
||||
This exception can be used to automatically send a response using a
|
||||
http status code, with an appropriate error page. It takes an optional
|
||||
``status`` argument (which must be between 400 and 599); it defaults to 500
|
||||
("Internal Server Error"). It also takes an optional ``message`` argument,
|
||||
which will be returned in the response body. See
|
||||
`RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
|
||||
for a complete list of available error codes and when to use them.
|
||||
|
||||
Examples::
|
||||
|
||||
raise cherrypy.HTTPError(403)
|
||||
raise cherrypy.HTTPError(
|
||||
"403 Forbidden", "You are not allowed to access this resource.")
|
||||
"""
|
||||
|
||||
status = None
|
||||
"""The HTTP status code. May be of type int or str (with a Reason-Phrase).
|
||||
"""
|
||||
|
||||
code = None
|
||||
"""The integer HTTP status code."""
|
||||
|
||||
reason = None
|
||||
"""The HTTP Reason-Phrase string."""
|
||||
|
||||
def __init__(self, status=500, message=None):
|
||||
self.status = status
|
||||
try:
|
||||
self.code, self.reason, defaultmsg = _httputil.valid_status(status)
|
||||
except ValueError:
|
||||
raise self.__class__(500, _exc_info()[1].args[0])
|
||||
|
||||
if self.code < 400 or self.code > 599:
|
||||
raise ValueError('status must be between 400 and 599.')
|
||||
|
||||
# See http://www.python.org/dev/peps/pep-0352/
|
||||
# self.message = message
|
||||
self._message = message or defaultmsg
|
||||
CherryPyException.__init__(self, status, message)
|
||||
|
||||
def set_response(self):
|
||||
"""Modify cherrypy.response status, headers, and body to represent
|
||||
self.
|
||||
|
||||
CherryPy uses this internally, but you can also use it to create an
|
||||
HTTPError object and set its output without *raising* the exception.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
clean_headers(self.code)
|
||||
|
||||
# In all cases, finalize will be called after this method,
|
||||
# so don't bother cleaning up response values here.
|
||||
response.status = self.status
|
||||
tb = None
|
||||
if cherrypy.serving.request.show_tracebacks:
|
||||
tb = format_exc()
|
||||
|
||||
response.headers.pop('Content-Length', None)
|
||||
|
||||
content = self.get_error_page(self.status, traceback=tb,
|
||||
message=self._message)
|
||||
response.body = content
|
||||
|
||||
_be_ie_unfriendly(self.code)
|
||||
|
||||
def get_error_page(self, *args, **kwargs):
|
||||
return get_error_page(*args, **kwargs)
|
||||
|
||||
def __call__(self):
|
||||
"""Use this exception as a request.handler (raise self)."""
|
||||
raise self
|
||||
|
||||
@classmethod
|
||||
@contextlib.contextmanager
|
||||
def handle(cls, exception, status=500, message=''):
|
||||
"""Translate exception into an HTTPError."""
|
||||
try:
|
||||
yield
|
||||
except exception as exc:
|
||||
raise cls(status, message or str(exc))
|
||||
|
||||
|
||||
class NotFound(HTTPError):
|
||||
|
||||
"""Exception raised when a URL could not be mapped to any handler (404).
|
||||
|
||||
This is equivalent to raising
|
||||
:class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
|
||||
"""
|
||||
|
||||
def __init__(self, path=None):
|
||||
if path is None:
|
||||
import cherrypy
|
||||
request = cherrypy.serving.request
|
||||
path = request.script_name + request.path_info
|
||||
self.args = (path,)
|
||||
HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
|
||||
|
||||
|
||||
_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
|
||||
"-//W3C//DTD XHTML 1.0 Transitional//EN"
|
||||
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
|
||||
<title>%(status)s</title>
|
||||
<style type="text/css">
|
||||
#powered_by {
|
||||
margin-top: 20px;
|
||||
border-top: 2px solid black;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
#traceback {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%(status)s</h2>
|
||||
<p>%(message)s</p>
|
||||
<pre id="traceback">%(traceback)s</pre>
|
||||
<div id="powered_by">
|
||||
<span>
|
||||
Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
|
||||
</span>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
def get_error_page(status, **kwargs):
|
||||
"""Return an HTML page, containing a pretty error response.
|
||||
|
||||
status should be an int or a str.
|
||||
kwargs will be interpolated into the page template.
|
||||
"""
|
||||
import cherrypy
|
||||
|
||||
try:
|
||||
code, reason, message = _httputil.valid_status(status)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
|
||||
|
||||
# We can't use setdefault here, because some
|
||||
# callers send None for kwarg values.
|
||||
if kwargs.get('status') is None:
|
||||
kwargs['status'] = '%s %s' % (code, reason)
|
||||
if kwargs.get('message') is None:
|
||||
kwargs['message'] = message
|
||||
if kwargs.get('traceback') is None:
|
||||
kwargs['traceback'] = ''
|
||||
if kwargs.get('version') is None:
|
||||
kwargs['version'] = cherrypy.__version__
|
||||
|
||||
for k, v in iteritems(kwargs):
|
||||
if v is None:
|
||||
kwargs[k] = ''
|
||||
else:
|
||||
kwargs[k] = escape_html(kwargs[k])
|
||||
|
||||
# Use a custom template or callable for the error page?
|
||||
pages = cherrypy.serving.request.error_page
|
||||
error_page = pages.get(code) or pages.get('default')
|
||||
|
||||
# Default template, can be overridden below.
|
||||
template = _HTTPErrorTemplate
|
||||
if error_page:
|
||||
try:
|
||||
if hasattr(error_page, '__call__'):
|
||||
# The caller function may be setting headers manually,
|
||||
# so we delegate to it completely. We may be returning
|
||||
# an iterator as well as a string here.
|
||||
#
|
||||
# We *must* make sure any content is not unicode.
|
||||
result = error_page(**kwargs)
|
||||
if cherrypy.lib.is_iterator(result):
|
||||
from cherrypy.lib.encoding import UTF8StreamEncoder
|
||||
return UTF8StreamEncoder(result)
|
||||
elif isinstance(result, six.text_type):
|
||||
return result.encode('utf-8')
|
||||
else:
|
||||
if not isinstance(result, bytes):
|
||||
raise ValueError('error page function did not '
|
||||
'return a bytestring, six.text_typeing or an '
|
||||
'iterator - returned object of type %s.'
|
||||
% (type(result).__name__))
|
||||
return result
|
||||
else:
|
||||
# Load the template from this path.
|
||||
template = tonative(open(error_page, 'rb').read())
|
||||
except:
|
||||
e = _format_exception(*_exc_info())[-1]
|
||||
m = kwargs['message']
|
||||
if m:
|
||||
m += '<br />'
|
||||
m += 'In addition, the custom error page failed:\n<br />%s' % e
|
||||
kwargs['message'] = m
|
||||
|
||||
response = cherrypy.serving.response
|
||||
response.headers['Content-Type'] = 'text/html;charset=utf-8'
|
||||
result = template % kwargs
|
||||
return result.encode('utf-8')
|
||||
|
||||
|
||||
|
||||
_ie_friendly_error_sizes = {
|
||||
400: 512, 403: 256, 404: 512, 405: 256,
|
||||
406: 512, 408: 512, 409: 512, 410: 256,
|
||||
500: 512, 501: 512, 505: 512,
|
||||
}
|
||||
|
||||
|
||||
def _be_ie_unfriendly(status):
|
||||
import cherrypy
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# For some statuses, Internet Explorer 5+ shows "friendly error
|
||||
# messages" instead of our response.body if the body is smaller
|
||||
# than a given size. Fix this by returning a body over that size
|
||||
# (by adding whitespace).
|
||||
# See http://support.microsoft.com/kb/q218155/
|
||||
s = _ie_friendly_error_sizes.get(status, 0)
|
||||
if s:
|
||||
s += 1
|
||||
# Since we are issuing an HTTP error status, we assume that
|
||||
# the entity is short, and we should just collapse it.
|
||||
content = response.collapse_body()
|
||||
l = len(content)
|
||||
if l and l < s:
|
||||
# IN ADDITION: the response must be written to IE
|
||||
# in one chunk or it will still get replaced! Bah.
|
||||
content = content + (ntob(' ') * (s - l))
|
||||
response.body = content
|
||||
response.headers['Content-Length'] = str(len(content))
|
||||
|
||||
|
||||
def format_exc(exc=None):
|
||||
"""Return exc (or sys.exc_info if None), formatted."""
|
||||
try:
|
||||
if exc is None:
|
||||
exc = _exc_info()
|
||||
if exc == (None, None, None):
|
||||
return ''
|
||||
import traceback
|
||||
return ''.join(traceback.format_exception(*exc))
|
||||
finally:
|
||||
del exc
|
||||
|
||||
|
||||
def bare_error(extrabody=None):
|
||||
"""Produce status, headers, body for a critical error.
|
||||
|
||||
Returns a triple without calling any other questionable functions,
|
||||
so it should be as error-free as possible. Call it from an HTTP server
|
||||
if you get errors outside of the request.
|
||||
|
||||
If extrabody is None, a friendly but rather unhelpful error message
|
||||
is set in the body. If extrabody is a string, it will be appended
|
||||
as-is to the body.
|
||||
"""
|
||||
|
||||
# The whole point of this function is to be a last line-of-defense
|
||||
# in handling errors. That is, it must not raise any errors itself;
|
||||
# it cannot be allowed to fail. Therefore, don't add to it!
|
||||
# In particular, don't call any other CP functions.
|
||||
|
||||
body = ntob('Unrecoverable error in the server.')
|
||||
if extrabody is not None:
|
||||
if not isinstance(extrabody, bytes):
|
||||
extrabody = extrabody.encode('utf-8')
|
||||
body += ntob('\n') + extrabody
|
||||
|
||||
return (ntob('500 Internal Server Error'),
|
||||
[(ntob('Content-Type'), ntob('text/plain')),
|
||||
(ntob('Content-Length'), ntob(str(len(body)), 'ISO-8859-1'))],
|
||||
[body])
|
|
@ -1,464 +0,0 @@
|
|||
"""
|
||||
Simple config
|
||||
=============
|
||||
|
||||
Although CherryPy uses the :mod:`Python logging module <logging>`, it does so
|
||||
behind the scenes so that simple logging is simple, but complicated logging
|
||||
is still possible. "Simple" logging means that you can log to the screen
|
||||
(i.e. console/stdout) or to a file, and that you can easily have separate
|
||||
error and access log files.
|
||||
|
||||
Here are the simplified logging settings. You use these by adding lines to
|
||||
your config file or dict. You should set these at either the global level or
|
||||
per application (see next), but generally not both.
|
||||
|
||||
* ``log.screen``: Set this to True to have both "error" and "access" messages
|
||||
printed to stdout.
|
||||
* ``log.access_file``: Set this to an absolute filename where you want
|
||||
"access" messages written.
|
||||
* ``log.error_file``: Set this to an absolute filename where you want "error"
|
||||
messages written.
|
||||
|
||||
Many events are automatically logged; to log your own application events, call
|
||||
:func:`cherrypy.log`.
|
||||
|
||||
Architecture
|
||||
============
|
||||
|
||||
Separate scopes
|
||||
---------------
|
||||
|
||||
CherryPy provides log managers at both the global and application layers.
|
||||
This means you can have one set of logging rules for your entire site,
|
||||
and another set of rules specific to each application. The global log
|
||||
manager is found at :func:`cherrypy.log`, and the log manager for each
|
||||
application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
|
||||
If you're inside a request, the latter is reachable from
|
||||
``cherrypy.request.app.log``; if you're outside a request, you'll have to
|
||||
obtain a reference to the ``app``: either the return value of
|
||||
:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
|
||||
:func:`quickstart()<cherrypy.quickstart>` instead, via
|
||||
``cherrypy.tree.apps['/']``.
|
||||
|
||||
By default, the global logs are named "cherrypy.error" and "cherrypy.access",
|
||||
and the application logs are named "cherrypy.error.2378745" and
|
||||
"cherrypy.access.2378745" (the number is the id of the Application object).
|
||||
This means that the application logs "bubble up" to the site logs, so if your
|
||||
application has no log handlers, the site-level handlers will still log the
|
||||
messages.
|
||||
|
||||
Errors vs. Access
|
||||
-----------------
|
||||
|
||||
Each log manager handles both "access" messages (one per HTTP request) and
|
||||
"error" messages (everything else). Note that the "error" log is not just for
|
||||
errors! The format of access messages is highly formalized, but the error log
|
||||
isn't--it receives messages from a variety of sources (including full error
|
||||
tracebacks, if enabled).
|
||||
|
||||
If you are logging the access log and error log to the same source, then there
|
||||
is a possibility that a specially crafted error message may replicate an access
|
||||
log message as described in CWE-117. In this case it is the application
|
||||
developer's responsibility to manually escape data before using CherryPy's log()
|
||||
functionality, or they may create an application that is vulnerable to CWE-117.
|
||||
This would be achieved by using a custom handler escape any special characters,
|
||||
and attached as described below.
|
||||
|
||||
Custom Handlers
|
||||
===============
|
||||
|
||||
The simple settings above work by manipulating Python's standard :mod:`logging`
|
||||
module. So when you need something more complex, the full power of the standard
|
||||
module is yours to exploit. You can borrow or create custom handlers, formats,
|
||||
filters, and much more. Here's an example that skips the standard FileHandler
|
||||
and uses a RotatingFileHandler instead:
|
||||
|
||||
::
|
||||
|
||||
#python
|
||||
log = app.log
|
||||
|
||||
# Remove the default FileHandlers if present.
|
||||
log.error_file = ""
|
||||
log.access_file = ""
|
||||
|
||||
maxBytes = getattr(log, "rot_maxBytes", 10000000)
|
||||
backupCount = getattr(log, "rot_backupCount", 1000)
|
||||
|
||||
# Make a new RotatingFileHandler for the error log.
|
||||
fname = getattr(log, "rot_error_file", "error.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
h.setLevel(DEBUG)
|
||||
h.setFormatter(_cplogging.logfmt)
|
||||
log.error_log.addHandler(h)
|
||||
|
||||
# Make a new RotatingFileHandler for the access log.
|
||||
fname = getattr(log, "rot_access_file", "access.log")
|
||||
h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
|
||||
h.setLevel(DEBUG)
|
||||
h.setFormatter(_cplogging.logfmt)
|
||||
log.access_log.addHandler(h)
|
||||
|
||||
|
||||
The ``rot_*`` attributes are pulled straight from the application log object.
|
||||
Since "log.*" config entries simply set attributes on the log object, you can
|
||||
add custom attributes to your heart's content. Note that these handlers are
|
||||
used ''instead'' of the default, simple handlers outlined above (so don't set
|
||||
the "log.error_file" config entry, for example).
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cperror
|
||||
from cherrypy._cpcompat import ntob
|
||||
|
||||
|
||||
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
|
||||
logging.Logger.manager.emittedNoHandlerWarning = 1
|
||||
logfmt = logging.Formatter('%(message)s')
|
||||
|
||||
|
||||
class NullHandler(logging.Handler):
|
||||
|
||||
"""A no-op logging handler to silence the logging.lastResort handler."""
|
||||
|
||||
def handle(self, record):
|
||||
pass
|
||||
|
||||
def emit(self, record):
|
||||
pass
|
||||
|
||||
def createLock(self):
|
||||
self.lock = None
|
||||
|
||||
|
||||
class LogManager(object):
|
||||
|
||||
"""An object to assist both simple and advanced logging.
|
||||
|
||||
``cherrypy.log`` is an instance of this class.
|
||||
"""
|
||||
|
||||
appid = None
|
||||
"""The id() of the Application object which owns this log manager. If this
|
||||
is a global log manager, appid is None."""
|
||||
|
||||
error_log = None
|
||||
"""The actual :class:`logging.Logger` instance for error messages."""
|
||||
|
||||
access_log = None
|
||||
"""The actual :class:`logging.Logger` instance for access messages."""
|
||||
|
||||
access_log_format = (
|
||||
'{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
|
||||
if six.PY3 else
|
||||
'%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
|
||||
)
|
||||
|
||||
logger_root = None
|
||||
"""The "top-level" logger name.
|
||||
|
||||
This string will be used as the first segment in the Logger names.
|
||||
The default is "cherrypy", for example, in which case the Logger names
|
||||
will be of the form::
|
||||
|
||||
cherrypy.error.<appid>
|
||||
cherrypy.access.<appid>
|
||||
"""
|
||||
|
||||
def __init__(self, appid=None, logger_root='cherrypy'):
|
||||
self.logger_root = logger_root
|
||||
self.appid = appid
|
||||
if appid is None:
|
||||
self.error_log = logging.getLogger('%s.error' % logger_root)
|
||||
self.access_log = logging.getLogger('%s.access' % logger_root)
|
||||
else:
|
||||
self.error_log = logging.getLogger(
|
||||
'%s.error.%s' % (logger_root, appid))
|
||||
self.access_log = logging.getLogger(
|
||||
'%s.access.%s' % (logger_root, appid))
|
||||
self.error_log.setLevel(logging.INFO)
|
||||
self.access_log.setLevel(logging.INFO)
|
||||
|
||||
# Silence the no-handlers "warning" (stderr write!) in stdlib logging
|
||||
self.error_log.addHandler(NullHandler())
|
||||
self.access_log.addHandler(NullHandler())
|
||||
|
||||
cherrypy.engine.subscribe('graceful', self.reopen_files)
|
||||
|
||||
def reopen_files(self):
|
||||
"""Close and reopen all file handlers."""
|
||||
for log in (self.error_log, self.access_log):
|
||||
for h in log.handlers:
|
||||
if isinstance(h, logging.FileHandler):
|
||||
h.acquire()
|
||||
h.stream.close()
|
||||
h.stream = open(h.baseFilename, h.mode)
|
||||
h.release()
|
||||
|
||||
def error(self, msg='', context='', severity=logging.INFO,
|
||||
traceback=False):
|
||||
"""Write the given ``msg`` to the error log.
|
||||
|
||||
This is not just for errors! Applications may call this at any time
|
||||
to log application-specific information.
|
||||
|
||||
If ``traceback`` is True, the traceback of the current exception
|
||||
(if any) will be appended to ``msg``.
|
||||
"""
|
||||
exc_info = None
|
||||
if traceback:
|
||||
exc_info = _cperror._exc_info()
|
||||
|
||||
self.error_log.log(severity, ' '.join((self.time(), context, msg)), exc_info=exc_info)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""An alias for ``error``."""
|
||||
return self.error(*args, **kwargs)
|
||||
|
||||
def access(self):
|
||||
"""Write to the access log (in Apache/NCSA Combined Log format).
|
||||
|
||||
See the
|
||||
`apache documentation <http://httpd.apache.org/docs/current/logs.html#combined>`_
|
||||
for format details.
|
||||
|
||||
CherryPy calls this automatically for you. Note there are no arguments;
|
||||
it collects the data itself from
|
||||
:class:`cherrypy.request<cherrypy._cprequest.Request>`.
|
||||
|
||||
Like Apache started doing in 2.0.46, non-printable and other special
|
||||
characters in %r (and we expand that to all parts) are escaped using
|
||||
\\xhh sequences, where hh stands for the hexadecimal representation
|
||||
of the raw byte. Exceptions from this rule are " and \\, which are
|
||||
escaped by prepending a backslash, and all whitespace characters,
|
||||
which are written in their C-style notation (\\n, \\t, etc).
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
remote = request.remote
|
||||
response = cherrypy.serving.response
|
||||
outheaders = response.headers
|
||||
inheaders = request.headers
|
||||
if response.output_status is None:
|
||||
status = '-'
|
||||
else:
|
||||
status = response.output_status.split(ntob(' '), 1)[0]
|
||||
if six.PY3:
|
||||
status = status.decode('ISO-8859-1')
|
||||
|
||||
atoms = {'h': remote.name or remote.ip,
|
||||
'l': '-',
|
||||
'u': getattr(request, 'login', None) or '-',
|
||||
't': self.time(),
|
||||
'r': request.request_line,
|
||||
's': status,
|
||||
'b': dict.get(outheaders, 'Content-Length', '') or '-',
|
||||
'f': dict.get(inheaders, 'Referer', ''),
|
||||
'a': dict.get(inheaders, 'User-Agent', ''),
|
||||
'o': dict.get(inheaders, 'Host', '-'),
|
||||
}
|
||||
if six.PY3:
|
||||
for k, v in atoms.items():
|
||||
if not isinstance(v, str):
|
||||
v = str(v)
|
||||
v = v.replace('"', '\\"').encode('utf8')
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[2:-1]
|
||||
|
||||
# in python 3.0 the repr of bytes (as returned by encode)
|
||||
# uses double \'s. But then the logger escapes them yet, again
|
||||
# resulting in quadruple slashes. Remove the extra one here.
|
||||
v = v.replace('\\\\', '\\')
|
||||
|
||||
# Escape double-quote.
|
||||
atoms[k] = v
|
||||
|
||||
try:
|
||||
self.access_log.log(
|
||||
logging.INFO, self.access_log_format.format(**atoms))
|
||||
except:
|
||||
self(traceback=True)
|
||||
else:
|
||||
for k, v in atoms.items():
|
||||
if isinstance(v, six.text_type):
|
||||
v = v.encode('utf8')
|
||||
elif not isinstance(v, str):
|
||||
v = str(v)
|
||||
# Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
|
||||
# and backslash for us. All we have to do is strip the quotes.
|
||||
v = repr(v)[1:-1]
|
||||
# Escape double-quote.
|
||||
atoms[k] = v.replace('"', '\\"')
|
||||
|
||||
try:
|
||||
self.access_log.log(
|
||||
logging.INFO, self.access_log_format % atoms)
|
||||
except:
|
||||
self(traceback=True)
|
||||
|
||||
def time(self):
|
||||
"""Return now() in Apache Common Log Format (no timezone)."""
|
||||
now = datetime.datetime.now()
|
||||
monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
|
||||
'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
|
||||
month = monthnames[now.month - 1].capitalize()
|
||||
return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
|
||||
(now.day, month, now.year, now.hour, now.minute, now.second))
|
||||
|
||||
def _get_builtin_handler(self, log, key):
|
||||
for h in log.handlers:
|
||||
if getattr(h, '_cpbuiltin', None) == key:
|
||||
return h
|
||||
|
||||
# ------------------------- Screen handlers ------------------------- #
|
||||
def _set_screen_handler(self, log, enable, stream=None):
|
||||
h = self._get_builtin_handler(log, 'screen')
|
||||
if enable:
|
||||
if not h:
|
||||
if stream is None:
|
||||
stream = sys.stderr
|
||||
h = logging.StreamHandler(stream)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = 'screen'
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_screen(self):
|
||||
h = self._get_builtin_handler
|
||||
has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
|
||||
return bool(has_h)
|
||||
|
||||
def _set_screen(self, newvalue):
|
||||
self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
|
||||
self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
|
||||
screen = property(_get_screen, _set_screen,
|
||||
doc="""Turn stderr/stdout logging on or off.
|
||||
|
||||
If you set this to True, it'll add the appropriate StreamHandler for
|
||||
you. If you set it to False, it will remove the handler.
|
||||
""")
|
||||
|
||||
# -------------------------- File handlers -------------------------- #
|
||||
|
||||
def _add_builtin_file_handler(self, log, fname):
|
||||
h = logging.FileHandler(fname)
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = 'file'
|
||||
log.addHandler(h)
|
||||
|
||||
def _set_file_handler(self, log, filename):
|
||||
h = self._get_builtin_handler(log, 'file')
|
||||
if filename:
|
||||
if h:
|
||||
if h.baseFilename != os.path.abspath(filename):
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
self._add_builtin_file_handler(log, filename)
|
||||
else:
|
||||
self._add_builtin_file_handler(log, filename)
|
||||
else:
|
||||
if h:
|
||||
h.close()
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_error_file(self):
|
||||
h = self._get_builtin_handler(self.error_log, 'file')
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
|
||||
def _set_error_file(self, newvalue):
|
||||
self._set_file_handler(self.error_log, newvalue)
|
||||
error_file = property(_get_error_file, _set_error_file,
|
||||
doc="""The filename for self.error_log.
|
||||
|
||||
If you set this to a string, it'll add the appropriate FileHandler for
|
||||
you. If you set it to ``None`` or ``''``, it will remove the handler.
|
||||
""")
|
||||
|
||||
def _get_access_file(self):
|
||||
h = self._get_builtin_handler(self.access_log, 'file')
|
||||
if h:
|
||||
return h.baseFilename
|
||||
return ''
|
||||
|
||||
def _set_access_file(self, newvalue):
|
||||
self._set_file_handler(self.access_log, newvalue)
|
||||
access_file = property(_get_access_file, _set_access_file,
|
||||
doc="""The filename for self.access_log.
|
||||
|
||||
If you set this to a string, it'll add the appropriate FileHandler for
|
||||
you. If you set it to ``None`` or ``''``, it will remove the handler.
|
||||
""")
|
||||
|
||||
# ------------------------- WSGI handlers ------------------------- #
|
||||
|
||||
def _set_wsgi_handler(self, log, enable):
|
||||
h = self._get_builtin_handler(log, 'wsgi')
|
||||
if enable:
|
||||
if not h:
|
||||
h = WSGIErrorHandler()
|
||||
h.setFormatter(logfmt)
|
||||
h._cpbuiltin = 'wsgi'
|
||||
log.addHandler(h)
|
||||
elif h:
|
||||
log.handlers.remove(h)
|
||||
|
||||
def _get_wsgi(self):
|
||||
return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
|
||||
|
||||
def _set_wsgi(self, newvalue):
|
||||
self._set_wsgi_handler(self.error_log, newvalue)
|
||||
wsgi = property(_get_wsgi, _set_wsgi,
|
||||
doc="""Write errors to wsgi.errors.
|
||||
|
||||
If you set this to True, it'll add the appropriate
|
||||
:class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
|
||||
(which writes errors to ``wsgi.errors``).
|
||||
If you set it to False, it will remove the handler.
|
||||
""")
|
||||
|
||||
|
||||
class WSGIErrorHandler(logging.Handler):
|
||||
|
||||
"A handler class which writes logging records to environ['wsgi.errors']."
|
||||
|
||||
def flush(self):
|
||||
"""Flushes the stream."""
|
||||
try:
|
||||
stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
else:
|
||||
stream.flush()
|
||||
|
||||
def emit(self, record):
|
||||
"""Emit a record."""
|
||||
try:
|
||||
stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
|
||||
except (AttributeError, KeyError):
|
||||
pass
|
||||
else:
|
||||
try:
|
||||
msg = self.format(record)
|
||||
fs = '%s\n'
|
||||
import types
|
||||
# if no unicode support...
|
||||
if not hasattr(types, 'UnicodeType'):
|
||||
stream.write(fs % msg)
|
||||
else:
|
||||
try:
|
||||
stream.write(fs % msg)
|
||||
except UnicodeError:
|
||||
stream.write(fs % msg.encode('UTF-8'))
|
||||
self.flush()
|
||||
except:
|
||||
self.handleError(record)
|
|
@ -1,354 +0,0 @@
|
|||
"""Native adapter for serving CherryPy via mod_python
|
||||
|
||||
Basic usage:
|
||||
|
||||
##########################################
|
||||
# Application in a module called myapp.py
|
||||
##########################################
|
||||
|
||||
import cherrypy
|
||||
|
||||
class Root:
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hi there, Ho there, Hey there'
|
||||
|
||||
|
||||
# We will use this method from the mod_python configuration
|
||||
# as the entry point to our application
|
||||
def setup_server():
|
||||
cherrypy.tree.mount(Root())
|
||||
cherrypy.config.update({'environment': 'production',
|
||||
'log.screen': False,
|
||||
'show_tracebacks': False})
|
||||
|
||||
##########################################
|
||||
# mod_python settings for apache2
|
||||
# This should reside in your httpd.conf
|
||||
# or a file that will be loaded at
|
||||
# apache startup
|
||||
##########################################
|
||||
|
||||
# Start
|
||||
DocumentRoot "/"
|
||||
Listen 8080
|
||||
LoadModule python_module /usr/lib/apache2/modules/mod_python.so
|
||||
|
||||
<Location "/">
|
||||
PythonPath "sys.path+['/path/to/my/application']"
|
||||
SetHandler python-program
|
||||
PythonHandler cherrypy._cpmodpy::handler
|
||||
PythonOption cherrypy.setup myapp::setup_server
|
||||
PythonDebug On
|
||||
</Location>
|
||||
# End
|
||||
|
||||
The actual path to your mod_python.so is dependent on your
|
||||
environment. In this case we suppose a global mod_python
|
||||
installation on a Linux distribution such as Ubuntu.
|
||||
|
||||
We do set the PythonPath configuration setting so that
|
||||
your application can be found by from the user running
|
||||
the apache2 instance. Of course if your application
|
||||
resides in the global site-package this won't be needed.
|
||||
|
||||
Then restart apache2 and access http://127.0.0.1:8080
|
||||
"""
|
||||
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import copyitems, ntob
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
# ------------------------------ Request-handling
|
||||
|
||||
|
||||
def setup(req):
|
||||
from mod_python import apache
|
||||
|
||||
# Run any setup functions defined by a "PythonOption cherrypy.setup"
|
||||
# directive.
|
||||
options = req.get_options()
|
||||
if 'cherrypy.setup' in options:
|
||||
for function in options['cherrypy.setup'].split():
|
||||
atoms = function.split('::', 1)
|
||||
if len(atoms) == 1:
|
||||
mod = __import__(atoms[0], globals(), locals())
|
||||
else:
|
||||
modname, fname = atoms
|
||||
mod = __import__(modname, globals(), locals(), [fname])
|
||||
func = getattr(mod, fname)
|
||||
func()
|
||||
|
||||
cherrypy.config.update({'log.screen': False,
|
||||
'tools.ignore_headers.on': True,
|
||||
'tools.ignore_headers.headers': ['Range'],
|
||||
})
|
||||
|
||||
engine = cherrypy.engine
|
||||
if hasattr(engine, 'signal_handler'):
|
||||
engine.signal_handler.unsubscribe()
|
||||
if hasattr(engine, 'console_control_handler'):
|
||||
engine.console_control_handler.unsubscribe()
|
||||
engine.autoreload.unsubscribe()
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
def _log(msg, level):
|
||||
newlevel = apache.APLOG_ERR
|
||||
if logging.DEBUG >= level:
|
||||
newlevel = apache.APLOG_DEBUG
|
||||
elif logging.INFO >= level:
|
||||
newlevel = apache.APLOG_INFO
|
||||
elif logging.WARNING >= level:
|
||||
newlevel = apache.APLOG_WARNING
|
||||
# On Windows, req.server is required or the msg will vanish. See
|
||||
# http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
|
||||
# Also, "When server is not specified...LogLevel does not apply..."
|
||||
apache.log_error(msg, newlevel, req.server)
|
||||
engine.subscribe('log', _log)
|
||||
|
||||
engine.start()
|
||||
|
||||
def cherrypy_cleanup(data):
|
||||
engine.exit()
|
||||
try:
|
||||
# apache.register_cleanup wasn't available until 3.1.4.
|
||||
apache.register_cleanup(cherrypy_cleanup)
|
||||
except AttributeError:
|
||||
req.server.register_cleanup(req, cherrypy_cleanup)
|
||||
|
||||
|
||||
class _ReadOnlyRequest:
|
||||
expose = ('read', 'readline', 'readlines')
|
||||
|
||||
def __init__(self, req):
|
||||
for method in self.expose:
|
||||
self.__dict__[method] = getattr(req, method)
|
||||
|
||||
|
||||
recursive = False
|
||||
|
||||
_isSetUp = False
|
||||
|
||||
|
||||
def handler(req):
|
||||
from mod_python import apache
|
||||
try:
|
||||
global _isSetUp
|
||||
if not _isSetUp:
|
||||
setup(req)
|
||||
_isSetUp = True
|
||||
|
||||
# Obtain a Request object from CherryPy
|
||||
local = req.connection.local_addr
|
||||
local = httputil.Host(
|
||||
local[0], local[1], req.connection.local_host or '')
|
||||
remote = req.connection.remote_addr
|
||||
remote = httputil.Host(
|
||||
remote[0], remote[1], req.connection.remote_host or '')
|
||||
|
||||
scheme = req.parsed_uri[0] or 'http'
|
||||
req.get_basic_auth_pw()
|
||||
|
||||
try:
|
||||
# apache.mpm_query only became available in mod_python 3.1
|
||||
q = apache.mpm_query
|
||||
threaded = q(apache.AP_MPMQ_IS_THREADED)
|
||||
forked = q(apache.AP_MPMQ_IS_FORKED)
|
||||
except AttributeError:
|
||||
bad_value = ("You must provide a PythonOption '%s', "
|
||||
"either 'on' or 'off', when running a version "
|
||||
'of mod_python < 3.1')
|
||||
|
||||
threaded = options.get('multithread', '').lower()
|
||||
if threaded == 'on':
|
||||
threaded = True
|
||||
elif threaded == 'off':
|
||||
threaded = False
|
||||
else:
|
||||
raise ValueError(bad_value % 'multithread')
|
||||
|
||||
forked = options.get('multiprocess', '').lower()
|
||||
if forked == 'on':
|
||||
forked = True
|
||||
elif forked == 'off':
|
||||
forked = False
|
||||
else:
|
||||
raise ValueError(bad_value % 'multiprocess')
|
||||
|
||||
sn = cherrypy.tree.script_name(req.uri or '/')
|
||||
if sn is None:
|
||||
send_response(req, '404 Not Found', [], '')
|
||||
else:
|
||||
app = cherrypy.tree.apps[sn]
|
||||
method = req.method
|
||||
path = req.uri
|
||||
qs = req.args or ''
|
||||
reqproto = req.protocol
|
||||
headers = copyitems(req.headers_in)
|
||||
rfile = _ReadOnlyRequest(req)
|
||||
prev = None
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
request, response = app.get_serving(local, remote, scheme,
|
||||
'HTTP/1.1')
|
||||
request.login = req.user
|
||||
request.multithread = bool(threaded)
|
||||
request.multiprocess = bool(forked)
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
# Run the CherryPy Request object and obtain the response
|
||||
try:
|
||||
request.run(method, path, qs, reqproto, headers, rfile)
|
||||
break
|
||||
except cherrypy.InternalRedirect:
|
||||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
if not recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError(
|
||||
'InternalRedirector visited the same URL '
|
||||
'twice: %r' % ir.path)
|
||||
else:
|
||||
# Add the *previous* path_info + qs to
|
||||
# redirections.
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
# Munge environment and try again.
|
||||
method = 'GET'
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = io.BytesIO()
|
||||
|
||||
send_response(
|
||||
req, response.output_status, response.header_list,
|
||||
response.body, response.stream)
|
||||
finally:
|
||||
app.release_serving()
|
||||
except:
|
||||
tb = format_exc()
|
||||
cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
|
||||
s, h, b = bare_error()
|
||||
send_response(req, s, h, b)
|
||||
return apache.OK
|
||||
|
||||
|
||||
def send_response(req, status, headers, body, stream=False):
|
||||
# Set response status
|
||||
req.status = int(status[:3])
|
||||
|
||||
# Set response headers
|
||||
req.content_type = 'text/plain'
|
||||
for header, value in headers:
|
||||
if header.lower() == 'content-type':
|
||||
req.content_type = value
|
||||
continue
|
||||
req.headers_out.add(header, value)
|
||||
|
||||
if stream:
|
||||
# Flush now so the status and headers are sent immediately.
|
||||
req.flush()
|
||||
|
||||
# Set response body
|
||||
if isinstance(body, text_or_bytes):
|
||||
req.write(body)
|
||||
else:
|
||||
for seg in body:
|
||||
req.write(seg)
|
||||
|
||||
|
||||
# --------------- Startup tools for CherryPy + mod_python --------------- #
|
||||
try:
|
||||
import subprocess
|
||||
|
||||
def popen(fullcmd):
|
||||
p = subprocess.Popen(fullcmd, shell=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
||||
close_fds=True)
|
||||
return p.stdout
|
||||
except ImportError:
|
||||
def popen(fullcmd):
|
||||
pipein, pipeout = os.popen4(fullcmd)
|
||||
return pipeout
|
||||
|
||||
|
||||
def read_process(cmd, args=''):
|
||||
fullcmd = '%s %s' % (cmd, args)
|
||||
pipeout = popen(fullcmd)
|
||||
try:
|
||||
firstline = pipeout.readline()
|
||||
cmd_not_found = re.search(
|
||||
ntob('(not recognized|No such file|not found)'),
|
||||
firstline,
|
||||
re.IGNORECASE
|
||||
)
|
||||
if cmd_not_found:
|
||||
raise IOError('%s must be on your system path.' % cmd)
|
||||
output = firstline + pipeout.read()
|
||||
finally:
|
||||
pipeout.close()
|
||||
return output
|
||||
|
||||
|
||||
class ModPythonServer(object):
|
||||
|
||||
template = """
|
||||
# Apache2 server configuration file for running CherryPy with mod_python.
|
||||
|
||||
DocumentRoot "/"
|
||||
Listen %(port)s
|
||||
LoadModule python_module modules/mod_python.so
|
||||
|
||||
<Location %(loc)s>
|
||||
SetHandler python-program
|
||||
PythonHandler %(handler)s
|
||||
PythonDebug On
|
||||
%(opts)s
|
||||
</Location>
|
||||
"""
|
||||
|
||||
def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
|
||||
handler='cherrypy._cpmodpy::handler'):
|
||||
self.loc = loc
|
||||
self.port = port
|
||||
self.opts = opts
|
||||
self.apache_path = apache_path
|
||||
self.handler = handler
|
||||
|
||||
def start(self):
|
||||
opts = ''.join([' PythonOption %s %s\n' % (k, v)
|
||||
for k, v in self.opts])
|
||||
conf_data = self.template % {'port': self.port,
|
||||
'loc': self.loc,
|
||||
'opts': opts,
|
||||
'handler': self.handler,
|
||||
}
|
||||
|
||||
mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
|
||||
f = open(mpconf, 'wb')
|
||||
try:
|
||||
f.write(conf_data)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
response = read_process(self.apache_path, '-k start -f %s' % mpconf)
|
||||
self.ready = True
|
||||
return response
|
||||
|
||||
def stop(self):
|
||||
os.popen('apache -k stop')
|
||||
self.ready = False
|
|
@ -1,154 +0,0 @@
|
|||
"""Native adapter for serving CherryPy via its builtin server."""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
import io
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import httputil
|
||||
from cherrypy import wsgiserver
|
||||
|
||||
|
||||
class NativeGateway(wsgiserver.Gateway):
|
||||
|
||||
recursive = False
|
||||
|
||||
def respond(self):
|
||||
req = self.req
|
||||
try:
|
||||
# Obtain a Request object from CherryPy
|
||||
local = req.server.bind_addr
|
||||
local = httputil.Host(local[0], local[1], '')
|
||||
remote = req.conn.remote_addr, req.conn.remote_port
|
||||
remote = httputil.Host(remote[0], remote[1], '')
|
||||
|
||||
scheme = req.scheme
|
||||
sn = cherrypy.tree.script_name(req.uri or '/')
|
||||
if sn is None:
|
||||
self.send_response('404 Not Found', [], [''])
|
||||
else:
|
||||
app = cherrypy.tree.apps[sn]
|
||||
method = req.method
|
||||
path = req.path
|
||||
qs = req.qs or ''
|
||||
headers = req.inheaders.items()
|
||||
rfile = req.rfile
|
||||
prev = None
|
||||
|
||||
try:
|
||||
redirections = []
|
||||
while True:
|
||||
request, response = app.get_serving(
|
||||
local, remote, scheme, 'HTTP/1.1')
|
||||
request.multithread = True
|
||||
request.multiprocess = False
|
||||
request.app = app
|
||||
request.prev = prev
|
||||
|
||||
# Run the CherryPy Request object and obtain the
|
||||
# response
|
||||
try:
|
||||
request.run(method, path, qs,
|
||||
req.request_protocol, headers, rfile)
|
||||
break
|
||||
except cherrypy.InternalRedirect:
|
||||
ir = sys.exc_info()[1]
|
||||
app.release_serving()
|
||||
prev = request
|
||||
|
||||
if not self.recursive:
|
||||
if ir.path in redirections:
|
||||
raise RuntimeError(
|
||||
'InternalRedirector visited the same '
|
||||
'URL twice: %r' % ir.path)
|
||||
else:
|
||||
# Add the *previous* path_info + qs to
|
||||
# redirections.
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
redirections.append(sn + path + qs)
|
||||
|
||||
# Munge environment and try again.
|
||||
method = 'GET'
|
||||
path = ir.path
|
||||
qs = ir.query_string
|
||||
rfile = io.BytesIO()
|
||||
|
||||
self.send_response(
|
||||
response.output_status, response.header_list,
|
||||
response.body)
|
||||
finally:
|
||||
app.release_serving()
|
||||
except:
|
||||
tb = format_exc()
|
||||
# print tb
|
||||
cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
|
||||
s, h, b = bare_error()
|
||||
self.send_response(s, h, b)
|
||||
|
||||
def send_response(self, status, headers, body):
|
||||
req = self.req
|
||||
|
||||
# Set response status
|
||||
req.status = str(status or '500 Server Error')
|
||||
|
||||
# Set response headers
|
||||
for header, value in headers:
|
||||
req.outheaders.append((header, value))
|
||||
if (req.ready and not req.sent_headers):
|
||||
req.sent_headers = True
|
||||
req.send_headers()
|
||||
|
||||
# Set response body
|
||||
for seg in body:
|
||||
req.write(seg)
|
||||
|
||||
|
||||
class CPHTTPServer(wsgiserver.HTTPServer):
|
||||
|
||||
"""Wrapper for wsgiserver.HTTPServer.
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
so that it can be used in other frameworks and applications.
|
||||
Therefore, we wrap it here, so we can apply some attributes
|
||||
from config -> cherrypy.server -> HTTPServer.
|
||||
"""
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
wsgiserver.HTTPServer.__init__(
|
||||
self, server_adapter.bind_addr, NativeGateway,
|
||||
minthreads=server_adapter.thread_pool,
|
||||
maxthreads=server_adapter.thread_pool_max,
|
||||
server_name=server_name)
|
||||
|
||||
self.max_request_header_size = (
|
||||
self.server_adapter.max_request_header_size or 0)
|
||||
self.max_request_body_size = (
|
||||
self.server_adapter.max_request_body_size or 0)
|
||||
self.request_queue_size = self.server_adapter.socket_queue_size
|
||||
self.timeout = self.server_adapter.socket_timeout
|
||||
self.shutdown_timeout = self.server_adapter.shutdown_timeout
|
||||
self.protocol = self.server_adapter.protocol_version
|
||||
self.nodelay = self.server_adapter.nodelay
|
||||
|
||||
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
|
||||
if self.server_adapter.ssl_context:
|
||||
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
|
||||
self.ssl_adapter = adapter_class(
|
||||
self.server_adapter.ssl_certificate,
|
||||
self.server_adapter.ssl_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
||||
self.ssl_adapter.context = self.server_adapter.ssl_context
|
||||
elif self.server_adapter.ssl_certificate:
|
||||
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
|
||||
self.ssl_adapter = adapter_class(
|
||||
self.server_adapter.ssl_certificate,
|
||||
self.server_adapter.ssl_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
File diff suppressed because it is too large
Load diff
|
@ -1,970 +0,0 @@
|
|||
import sys
|
||||
import time
|
||||
import warnings
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes, copykeys, ntob
|
||||
from cherrypy._cpcompat import SimpleCookie, CookieError
|
||||
from cherrypy import _cpreqbody, _cpconfig
|
||||
from cherrypy._cperror import format_exc, bare_error
|
||||
from cherrypy.lib import httputil, file_generator
|
||||
|
||||
|
||||
class Hook(object):
|
||||
|
||||
"""A callback and its metadata: failsafe, priority, and kwargs."""
|
||||
|
||||
callback = None
|
||||
"""
|
||||
The bare callable that this Hook object is wrapping, which will
|
||||
be called when the Hook is called."""
|
||||
|
||||
failsafe = False
|
||||
"""
|
||||
If True, the callback is guaranteed to run even if other callbacks
|
||||
from the same call point raise exceptions."""
|
||||
|
||||
priority = 50
|
||||
"""
|
||||
Defines the order of execution for a list of Hooks. Priority numbers
|
||||
should be limited to the closed interval [0, 100], but values outside
|
||||
this range are acceptable, as are fractional values."""
|
||||
|
||||
kwargs = {}
|
||||
"""
|
||||
A set of keyword arguments that will be passed to the
|
||||
callable on each call."""
|
||||
|
||||
def __init__(self, callback, failsafe=None, priority=None, **kwargs):
|
||||
self.callback = callback
|
||||
|
||||
if failsafe is None:
|
||||
failsafe = getattr(callback, 'failsafe', False)
|
||||
self.failsafe = failsafe
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, 'priority', 50)
|
||||
self.priority = priority
|
||||
|
||||
self.kwargs = kwargs
|
||||
|
||||
def __lt__(self, other):
|
||||
# Python 3
|
||||
return self.priority < other.priority
|
||||
|
||||
def __cmp__(self, other):
|
||||
# Python 2
|
||||
return cmp(self.priority, other.priority)
|
||||
|
||||
def __call__(self):
|
||||
"""Run self.callback(**self.kwargs)."""
|
||||
return self.callback(**self.kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
|
||||
% (cls.__module__, cls.__name__, self.callback,
|
||||
self.failsafe, self.priority,
|
||||
', '.join(['%s=%r' % (k, v)
|
||||
for k, v in self.kwargs.items()])))
|
||||
|
||||
|
||||
class HookMap(dict):
|
||||
|
||||
"""A map of call points to lists of callbacks (Hook objects)."""
|
||||
|
||||
def __new__(cls, points=None):
|
||||
d = dict.__new__(cls)
|
||||
for p in points or []:
|
||||
d[p] = []
|
||||
return d
|
||||
|
||||
def __init__(self, *a, **kw):
|
||||
pass
|
||||
|
||||
def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
|
||||
"""Append a new Hook made from the supplied arguments."""
|
||||
self[point].append(Hook(callback, failsafe, priority, **kwargs))
|
||||
|
||||
def run(self, point):
|
||||
"""Execute all registered Hooks (callbacks) for the given point."""
|
||||
exc = None
|
||||
hooks = self[point]
|
||||
hooks.sort()
|
||||
for hook in hooks:
|
||||
# Some hooks are guaranteed to run even if others at
|
||||
# the same hookpoint fail. We will still log the failure,
|
||||
# but proceed on to the next hook. The only way
|
||||
# to stop all processing from one of these hooks is
|
||||
# to raise SystemExit and stop the whole server.
|
||||
if exc is None or hook.failsafe:
|
||||
try:
|
||||
hook()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
|
||||
cherrypy.InternalRedirect):
|
||||
exc = sys.exc_info()[1]
|
||||
except:
|
||||
exc = sys.exc_info()[1]
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if exc:
|
||||
raise exc
|
||||
|
||||
def __copy__(self):
|
||||
newmap = self.__class__()
|
||||
# We can't just use 'update' because we want copies of the
|
||||
# mutable values (each is a list) as well.
|
||||
for k, v in self.items():
|
||||
newmap[k] = v[:]
|
||||
return newmap
|
||||
copy = __copy__
|
||||
|
||||
def __repr__(self):
|
||||
cls = self.__class__
|
||||
return '%s.%s(points=%r)' % (
|
||||
cls.__module__,
|
||||
cls.__name__,
|
||||
copykeys(self)
|
||||
)
|
||||
|
||||
|
||||
# Config namespace handlers
|
||||
|
||||
def hooks_namespace(k, v):
|
||||
"""Attach bare hooks declared in config."""
|
||||
# Use split again to allow multiple hooks for a single
|
||||
# hookpoint per path (e.g. "hooks.before_handler.1").
|
||||
# Little-known fact you only get from reading source ;)
|
||||
hookpoint = k.split('.', 1)[0]
|
||||
if isinstance(v, text_or_bytes):
|
||||
v = cherrypy.lib.attributes(v)
|
||||
if not isinstance(v, Hook):
|
||||
v = Hook(v)
|
||||
cherrypy.serving.request.hooks[hookpoint].append(v)
|
||||
|
||||
|
||||
def request_namespace(k, v):
|
||||
"""Attach request attributes declared in config."""
|
||||
# Provides config entries to set request.body attrs (like
|
||||
# attempt_charsets).
|
||||
if k[:5] == 'body.':
|
||||
setattr(cherrypy.serving.request.body, k[5:], v)
|
||||
else:
|
||||
setattr(cherrypy.serving.request, k, v)
|
||||
|
||||
|
||||
def response_namespace(k, v):
|
||||
"""Attach response attributes declared in config."""
|
||||
# Provides config entries to set default response headers
|
||||
# http://cherrypy.org/ticket/889
|
||||
if k[:8] == 'headers.':
|
||||
cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
|
||||
else:
|
||||
setattr(cherrypy.serving.response, k, v)
|
||||
|
||||
|
||||
def error_page_namespace(k, v):
|
||||
"""Attach error pages declared in config."""
|
||||
if k != 'default':
|
||||
k = int(k)
|
||||
cherrypy.serving.request.error_page[k] = v
|
||||
|
||||
|
||||
hookpoints = ['on_start_resource', 'before_request_body',
|
||||
'before_handler', 'before_finalize',
|
||||
'on_end_resource', 'on_end_request',
|
||||
'before_error_response', 'after_error_response']
|
||||
|
||||
|
||||
class Request(object):
|
||||
|
||||
"""An HTTP request.
|
||||
|
||||
This object represents the metadata of an HTTP request message;
|
||||
that is, it contains attributes which describe the environment
|
||||
in which the request URL, headers, and body were sent (if you
|
||||
want tools to interpret the headers and body, those are elsewhere,
|
||||
mostly in Tools). This 'metadata' consists of socket data,
|
||||
transport characteristics, and the Request-Line. This object
|
||||
also contains data regarding the configuration in effect for
|
||||
the given URL, and the execution plan for generating a response.
|
||||
"""
|
||||
|
||||
prev = None
|
||||
"""
|
||||
The previous Request object (if any). This should be None
|
||||
unless we are processing an InternalRedirect."""
|
||||
|
||||
# Conversation/connection attributes
|
||||
local = httputil.Host('127.0.0.1', 80)
|
||||
'An httputil.Host(ip, port, hostname) object for the server socket.'
|
||||
|
||||
remote = httputil.Host('127.0.0.1', 1111)
|
||||
'An httputil.Host(ip, port, hostname) object for the client socket.'
|
||||
|
||||
scheme = 'http'
|
||||
"""
|
||||
The protocol used between client and server. In most cases,
|
||||
this will be either 'http' or 'https'."""
|
||||
|
||||
server_protocol = 'HTTP/1.1'
|
||||
"""
|
||||
The HTTP version for which the HTTP server is at least
|
||||
conditionally compliant."""
|
||||
|
||||
base = ''
|
||||
"""The (scheme://host) portion of the requested URL.
|
||||
In some cases (e.g. when proxying via mod_rewrite), this may contain
|
||||
path segments which cherrypy.url uses when constructing url's, but
|
||||
which otherwise are ignored by CherryPy. Regardless, this value
|
||||
MUST NOT end in a slash."""
|
||||
|
||||
# Request-Line attributes
|
||||
request_line = ''
|
||||
"""
|
||||
The complete Request-Line received from the client. This is a
|
||||
single string consisting of the request method, URI, and protocol
|
||||
version (joined by spaces). Any final CRLF is removed."""
|
||||
|
||||
method = 'GET'
|
||||
"""
|
||||
Indicates the HTTP method to be performed on the resource identified
|
||||
by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
|
||||
DELETE. CherryPy allows any extension method; however, various HTTP
|
||||
servers and gateways may restrict the set of allowable methods.
|
||||
CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
|
||||
|
||||
query_string = ''
|
||||
"""
|
||||
The query component of the Request-URI, a string of information to be
|
||||
interpreted by the resource. The query portion of a URI follows the
|
||||
path component, and is separated by a '?'. For example, the URI
|
||||
'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
|
||||
'a=3&b=4'."""
|
||||
|
||||
query_string_encoding = 'utf8'
|
||||
"""
|
||||
The encoding expected for query string arguments after % HEX HEX decoding).
|
||||
If a query string is provided that cannot be decoded with this encoding,
|
||||
404 is raised (since technically it's a different URI). If you want
|
||||
arbitrary encodings to not error, set this to 'Latin-1'; you can then
|
||||
encode back to bytes and re-decode to whatever encoding you like later.
|
||||
"""
|
||||
|
||||
protocol = (1, 1)
|
||||
"""The HTTP protocol version corresponding to the set
|
||||
of features which should be allowed in the response. If BOTH
|
||||
the client's request message AND the server's level of HTTP
|
||||
compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
|
||||
If either is 1.0, this attribute will be the tuple (1, 0).
|
||||
Lower HTTP protocol versions are not explicitly supported."""
|
||||
|
||||
params = {}
|
||||
"""
|
||||
A dict which combines query string (GET) and request entity (POST)
|
||||
variables. This is populated in two stages: GET params are added
|
||||
before the 'on_start_resource' hook, and POST params are added
|
||||
between the 'before_request_body' and 'before_handler' hooks."""
|
||||
|
||||
# Message attributes
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP request headers as (name, value) tuples.
|
||||
In general, you should use request.headers (a dict) instead."""
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
A dict-like object containing the request headers. Keys are header
|
||||
names (in Title-Case format); however, you may get and set them in
|
||||
a case-insensitive manner. That is, headers['Content-Type'] and
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to :rfc:`2047` if necessary). See also:
|
||||
httputil.HeaderMap, httputil.HeaderElement."""
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
rfile = None
|
||||
"""
|
||||
If the request included an entity (body), it will be available
|
||||
as a stream in this attribute. However, the rfile will normally
|
||||
be read for you between the 'before_request_body' hook and the
|
||||
'before_handler' hook, and the resulting string is placed into
|
||||
either request.params or the request.body attribute.
|
||||
|
||||
You may disable the automatic consumption of the rfile by setting
|
||||
request.process_request_body to False, either in config for the desired
|
||||
path, or in an 'on_start_resource' or 'before_request_body' hook.
|
||||
|
||||
WARNING: In almost every case, you should not attempt to read from the
|
||||
rfile stream after CherryPy's automatic mechanism has read it. If you
|
||||
turn off the automatic parsing of rfile, you should read exactly the
|
||||
number of bytes specified in request.headers['Content-Length'].
|
||||
Ignoring either of these warnings may result in a hung request thread
|
||||
or in corruption of the next (pipelined) request.
|
||||
"""
|
||||
|
||||
process_request_body = True
|
||||
"""
|
||||
If True, the rfile (if any) is automatically read and parsed,
|
||||
and the result placed into request.params or request.body."""
|
||||
|
||||
methods_with_bodies = ('POST', 'PUT', 'PATCH')
|
||||
"""
|
||||
A sequence of HTTP methods for which CherryPy will automatically
|
||||
attempt to read a body from the rfile. If you are going to change
|
||||
this property, modify it on the configuration (recommended)
|
||||
or on the "hook point" `on_start_resource`.
|
||||
"""
|
||||
|
||||
body = None
|
||||
"""
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded'
|
||||
or multipart, this will be None. Otherwise, this will be an instance
|
||||
of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
|
||||
can .read()); this value is set between the 'before_request_body' and
|
||||
'before_handler' hooks (assuming that process_request_body is True)."""
|
||||
|
||||
# Dispatch attributes
|
||||
dispatch = cherrypy.dispatch.Dispatcher()
|
||||
"""
|
||||
The object which looks up the 'page handler' callable and collects
|
||||
config for the current request based on the path_info, other
|
||||
request attributes, and the application architecture. The core
|
||||
calls the dispatcher as early as possible, passing it a 'path_info'
|
||||
argument.
|
||||
|
||||
The default dispatcher discovers the page handler by matching path_info
|
||||
to a hierarchical arrangement of objects, starting at request.app.root.
|
||||
See help(cherrypy.dispatch) for more information."""
|
||||
|
||||
script_name = ''
|
||||
"""
|
||||
The 'mount point' of the application which is handling this request.
|
||||
|
||||
This attribute MUST NOT end in a slash. If the script_name refers to
|
||||
the root of the URI, it MUST be an empty string (not "/").
|
||||
"""
|
||||
|
||||
path_info = '/'
|
||||
"""
|
||||
The 'relative path' portion of the Request-URI. This is relative
|
||||
to the script_name ('mount point') of the application which is
|
||||
handling this request."""
|
||||
|
||||
login = None
|
||||
"""
|
||||
When authentication is used during the request processing this is
|
||||
set to 'False' if it failed and to the 'username' value if it succeeded.
|
||||
The default 'None' implies that no authentication happened."""
|
||||
|
||||
# Note that cherrypy.url uses "if request.app:" to determine whether
|
||||
# the call is during a real HTTP request or not. So leave this None.
|
||||
app = None
|
||||
"""The cherrypy.Application object which is handling this request."""
|
||||
|
||||
handler = None
|
||||
"""
|
||||
The function, method, or other callable which CherryPy will call to
|
||||
produce the response. The discovery of the handler and the arguments
|
||||
it will receive are determined by the request.dispatch object.
|
||||
By default, the handler is discovered by walking a tree of objects
|
||||
starting at request.app.root, and is then passed all HTTP params
|
||||
(from the query string and POST body) as keyword arguments."""
|
||||
|
||||
toolmaps = {}
|
||||
"""
|
||||
A nested dict of all Toolboxes and Tools in effect for this request,
|
||||
of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
|
||||
|
||||
config = None
|
||||
"""
|
||||
A flat dict of all configuration entries which apply to the
|
||||
current request. These entries are collected from global config,
|
||||
application config (based on request.path_info), and from handler
|
||||
config (exactly how is governed by the request.dispatch object in
|
||||
effect for this request; by default, handler config can be attached
|
||||
anywhere in the tree between request.app.root and the final handler,
|
||||
and inherits downward)."""
|
||||
|
||||
is_index = None
|
||||
"""
|
||||
This will be True if the current request is mapped to an 'index'
|
||||
resource handler (also, a 'default' handler if path_info ends with
|
||||
a slash). The value may be used to automatically redirect the
|
||||
user-agent to a 'more canonical' URL which either adds or removes
|
||||
the trailing slash. See cherrypy.tools.trailing_slash."""
|
||||
|
||||
hooks = HookMap(hookpoints)
|
||||
"""
|
||||
A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
|
||||
Each key is a str naming the hook point, and each value is a list
|
||||
of hooks which will be called at that hook point during this request.
|
||||
The list of hooks is generally populated as early as possible (mostly
|
||||
from Tools specified in config), but may be extended at any time.
|
||||
See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
|
||||
|
||||
error_response = cherrypy.HTTPError(500).set_response
|
||||
"""
|
||||
The no-arg callable which will handle unexpected, untrapped errors
|
||||
during request processing. This is not used for expected exceptions
|
||||
(like NotFound, HTTPError, or HTTPRedirect) which are raised in
|
||||
response to expected conditions (those should be customized either
|
||||
via request.error_page or by overriding HTTPError.set_response).
|
||||
By default, error_response uses HTTPError(500) to return a generic
|
||||
error response to the user-agent."""
|
||||
|
||||
error_page = {}
|
||||
"""
|
||||
A dict of {error code: response filename or callable} pairs.
|
||||
|
||||
The error code must be an int representing a given HTTP error code,
|
||||
or the string 'default', which will be used if no matching entry
|
||||
is found for a given numeric code.
|
||||
|
||||
If a filename is provided, the file should contain a Python string-
|
||||
formatting template, and can expect by default to receive format
|
||||
values with the mapping keys %(status)s, %(message)s, %(traceback)s,
|
||||
and %(version)s. The set of format mappings can be extended by
|
||||
overriding HTTPError.set_response.
|
||||
|
||||
If a callable is provided, it will be called by default with keyword
|
||||
arguments 'status', 'message', 'traceback', and 'version', as for a
|
||||
string-formatting template. The callable must return a string or
|
||||
iterable of strings which will be set to response.body. It may also
|
||||
override headers or perform any other processing.
|
||||
|
||||
If no entry is given for an error code, and no 'default' entry exists,
|
||||
a default template will be used.
|
||||
"""
|
||||
|
||||
show_tracebacks = True
|
||||
"""
|
||||
If True, unexpected errors encountered during request processing will
|
||||
include a traceback in the response body."""
|
||||
|
||||
show_mismatched_params = True
|
||||
"""
|
||||
If True, mismatched parameters encountered during PageHandler invocation
|
||||
processing will be included in the response body."""
|
||||
|
||||
throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
|
||||
"""The sequence of exceptions which Request.run does not trap."""
|
||||
|
||||
throw_errors = False
|
||||
"""
|
||||
If True, Request.run will not trap any errors (except HTTPRedirect and
|
||||
HTTPError, which are more properly called 'exceptions', not errors)."""
|
||||
|
||||
closed = False
|
||||
"""True once the close method has been called, False otherwise."""
|
||||
|
||||
stage = None
|
||||
"""
|
||||
A string containing the stage reached in the request-handling process.
|
||||
This is useful when debugging a live server with hung requests."""
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet(
|
||||
**{'hooks': hooks_namespace,
|
||||
'request': request_namespace,
|
||||
'response': response_namespace,
|
||||
'error_page': error_page_namespace,
|
||||
'tools': cherrypy.tools,
|
||||
})
|
||||
|
||||
def __init__(self, local_host, remote_host, scheme='http',
|
||||
server_protocol='HTTP/1.1'):
|
||||
"""Populate a new Request object.
|
||||
|
||||
local_host should be an httputil.Host object with the server info.
|
||||
remote_host should be an httputil.Host object with the client info.
|
||||
scheme should be a string, either "http" or "https".
|
||||
"""
|
||||
self.local = local_host
|
||||
self.remote = remote_host
|
||||
self.scheme = scheme
|
||||
self.server_protocol = server_protocol
|
||||
|
||||
self.closed = False
|
||||
|
||||
# Put a *copy* of the class error_page into self.
|
||||
self.error_page = self.error_page.copy()
|
||||
|
||||
# Put a *copy* of the class namespaces into self.
|
||||
self.namespaces = self.namespaces.copy()
|
||||
|
||||
self.stage = None
|
||||
|
||||
def close(self):
|
||||
"""Run cleanup code. (Core)"""
|
||||
if not self.closed:
|
||||
self.closed = True
|
||||
self.stage = 'on_end_request'
|
||||
self.hooks.run('on_end_request')
|
||||
self.stage = 'close'
|
||||
|
||||
def run(self, method, path, query_string, req_protocol, headers, rfile):
|
||||
r"""Process the Request. (Core)
|
||||
|
||||
method, path, query_string, and req_protocol should be pulled directly
|
||||
from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
|
||||
|
||||
path
|
||||
This should be %XX-unquoted, but query_string should not be.
|
||||
|
||||
When using Python 2, they both MUST be byte strings,
|
||||
not unicode strings.
|
||||
|
||||
When using Python 3, they both MUST be unicode strings,
|
||||
not byte strings, and preferably not bytes \x00-\xFF
|
||||
disguised as unicode.
|
||||
|
||||
headers
|
||||
A list of (name, value) tuples.
|
||||
|
||||
rfile
|
||||
A file-like object containing the HTTP request entity.
|
||||
|
||||
When run() is done, the returned object should have 3 attributes:
|
||||
|
||||
* status, e.g. "200 OK"
|
||||
* header_list, a list of (name, value) tuples
|
||||
* body, an iterable yielding strings
|
||||
|
||||
Consumer code (HTTP servers) should then access these response
|
||||
attributes to build the outbound stream.
|
||||
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
self.stage = 'run'
|
||||
try:
|
||||
self.error_response = cherrypy.HTTPError(500).set_response
|
||||
|
||||
self.method = method
|
||||
path = path or '/'
|
||||
self.query_string = query_string or ''
|
||||
self.params = {}
|
||||
|
||||
# Compare request and server HTTP protocol versions, in case our
|
||||
# server does not support the requested protocol. Limit our output
|
||||
# to min(req, server). We want the following output:
|
||||
# request server actual written supported response
|
||||
# protocol protocol response protocol feature set
|
||||
# a 1.0 1.0 1.0 1.0
|
||||
# b 1.0 1.1 1.1 1.0
|
||||
# c 1.1 1.0 1.0 1.0
|
||||
# d 1.1 1.1 1.1 1.1
|
||||
# Notice that, in (b), the response will be "HTTP/1.1" even though
|
||||
# the client only understands 1.0. RFC 2616 10.5.6 says we should
|
||||
# only return 505 if the _major_ version is different.
|
||||
rp = int(req_protocol[5]), int(req_protocol[7])
|
||||
sp = int(self.server_protocol[5]), int(self.server_protocol[7])
|
||||
self.protocol = min(rp, sp)
|
||||
response.headers.protocol = self.protocol
|
||||
|
||||
# Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
|
||||
url = path
|
||||
if query_string:
|
||||
url += '?' + query_string
|
||||
self.request_line = '%s %s %s' % (method, url, req_protocol)
|
||||
|
||||
self.header_list = list(headers)
|
||||
self.headers = httputil.HeaderMap()
|
||||
|
||||
self.rfile = rfile
|
||||
self.body = None
|
||||
|
||||
self.cookie = SimpleCookie()
|
||||
self.handler = None
|
||||
|
||||
# path_info should be the path from the
|
||||
# app root (script_name) to the handler.
|
||||
self.script_name = self.app.script_name
|
||||
self.path_info = pi = path[len(self.script_name):]
|
||||
|
||||
self.stage = 'respond'
|
||||
self.respond(pi)
|
||||
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
if self.throw_errors:
|
||||
raise
|
||||
else:
|
||||
# Failure in setup, error handler or finalize. Bypass them.
|
||||
# Can't use handle_error because we may not have hooks yet.
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
if self.show_tracebacks:
|
||||
body = format_exc()
|
||||
else:
|
||||
body = ''
|
||||
r = bare_error(body)
|
||||
response.output_status, response.header_list, response.body = r
|
||||
|
||||
if self.method == 'HEAD':
|
||||
# HEAD requests MUST NOT return a message-body in the response.
|
||||
response.body = []
|
||||
|
||||
try:
|
||||
cherrypy.log.access()
|
||||
except:
|
||||
cherrypy.log.error(traceback=True)
|
||||
|
||||
if response.timed_out:
|
||||
raise cherrypy.TimeoutError()
|
||||
|
||||
return response
|
||||
|
||||
# Uncomment for stage debugging
|
||||
# stage = property(lambda self: self._stage, lambda self, v: print(v))
|
||||
|
||||
def respond(self, path_info):
|
||||
"""Generate a response for the resource at self.path_info. (Core)"""
|
||||
response = cherrypy.serving.response
|
||||
try:
|
||||
try:
|
||||
try:
|
||||
if self.app is None:
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Get the 'Host' header, so we can HTTPRedirect properly.
|
||||
self.stage = 'process_headers'
|
||||
self.process_headers()
|
||||
|
||||
# Make a copy of the class hooks
|
||||
self.hooks = self.__class__.hooks.copy()
|
||||
self.toolmaps = {}
|
||||
|
||||
self.stage = 'get_resource'
|
||||
self.get_resource(path_info)
|
||||
|
||||
self.body = _cpreqbody.RequestBody(
|
||||
self.rfile, self.headers, request_params=self.params)
|
||||
|
||||
self.namespaces(self.config)
|
||||
|
||||
self.stage = 'on_start_resource'
|
||||
self.hooks.run('on_start_resource')
|
||||
|
||||
# Parse the querystring
|
||||
self.stage = 'process_query_string'
|
||||
self.process_query_string()
|
||||
|
||||
# Process the body
|
||||
if self.process_request_body:
|
||||
if self.method not in self.methods_with_bodies:
|
||||
self.process_request_body = False
|
||||
self.stage = 'before_request_body'
|
||||
self.hooks.run('before_request_body')
|
||||
if self.process_request_body:
|
||||
self.body.process()
|
||||
|
||||
# Run the handler
|
||||
self.stage = 'before_handler'
|
||||
self.hooks.run('before_handler')
|
||||
if self.handler:
|
||||
self.stage = 'handler'
|
||||
response.body = self.handler()
|
||||
|
||||
# Finalize
|
||||
self.stage = 'before_finalize'
|
||||
self.hooks.run('before_finalize')
|
||||
response.finalize()
|
||||
except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
|
||||
inst = sys.exc_info()[1]
|
||||
inst.set_response()
|
||||
self.stage = 'before_finalize (HTTPError)'
|
||||
self.hooks.run('before_finalize')
|
||||
response.finalize()
|
||||
finally:
|
||||
self.stage = 'on_end_resource'
|
||||
self.hooks.run('on_end_resource')
|
||||
except self.throws:
|
||||
raise
|
||||
except:
|
||||
if self.throw_errors:
|
||||
raise
|
||||
self.handle_error()
|
||||
|
||||
def process_query_string(self):
|
||||
"""Parse the query string into Python structures. (Core)"""
|
||||
try:
|
||||
p = httputil.parse_query_string(
|
||||
self.query_string, encoding=self.query_string_encoding)
|
||||
except UnicodeDecodeError:
|
||||
raise cherrypy.HTTPError(
|
||||
404, 'The given query string could not be processed. Query '
|
||||
'strings for this resource must be encoded with %r.' %
|
||||
self.query_string_encoding)
|
||||
|
||||
# Python 2 only: keyword arguments must be byte strings (type 'str').
|
||||
if six.PY2:
|
||||
for key, value in p.items():
|
||||
if isinstance(key, six.text_type):
|
||||
del p[key]
|
||||
p[key.encode(self.query_string_encoding)] = value
|
||||
self.params.update(p)
|
||||
|
||||
def process_headers(self):
|
||||
"""Parse HTTP header data into Python structures. (Core)"""
|
||||
# Process the headers into self.headers
|
||||
headers = self.headers
|
||||
for name, value in self.header_list:
|
||||
# Call title() now (and use dict.__method__(headers))
|
||||
# so title doesn't have to be called twice.
|
||||
name = name.title()
|
||||
value = value.strip()
|
||||
|
||||
# Warning: if there is more than one header entry for cookies
|
||||
# (AFAIK, only Konqueror does that), only the last one will
|
||||
# remain in headers (but they will be correctly stored in
|
||||
# request.cookie).
|
||||
if '=?' in value:
|
||||
dict.__setitem__(headers, name, httputil.decode_TEXT(value))
|
||||
else:
|
||||
dict.__setitem__(headers, name, value)
|
||||
|
||||
# Handle cookies differently because on Konqueror, multiple
|
||||
# cookies come on different lines with the same key
|
||||
if name == 'Cookie':
|
||||
try:
|
||||
self.cookie.load(value)
|
||||
except CookieError:
|
||||
msg = 'Illegal cookie name %s' % value.split('=')[0]
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
|
||||
if not dict.__contains__(headers, 'Host'):
|
||||
# All Internet-based HTTP/1.1 servers MUST respond with a 400
|
||||
# (Bad Request) status code to any HTTP/1.1 request message
|
||||
# which lacks a Host header field.
|
||||
if self.protocol >= (1, 1):
|
||||
msg = "HTTP/1.1 requires a 'Host' request header."
|
||||
raise cherrypy.HTTPError(400, msg)
|
||||
host = dict.get(headers, 'Host')
|
||||
if not host:
|
||||
host = self.local.name or self.local.ip
|
||||
self.base = '%s://%s' % (self.scheme, host)
|
||||
|
||||
def get_resource(self, path):
|
||||
"""Call a dispatcher (which sets self.handler and .config). (Core)"""
|
||||
# First, see if there is a custom dispatch at this URI. Custom
|
||||
# dispatchers can only be specified in app.config, not in _cp_config
|
||||
# (since custom dispatchers may not even have an app.root).
|
||||
dispatch = self.app.find_config(
|
||||
path, 'request.dispatch', self.dispatch)
|
||||
|
||||
# dispatch() should set self.handler and self.config
|
||||
dispatch(path)
|
||||
|
||||
def handle_error(self):
|
||||
"""Handle the last unanticipated exception. (Core)"""
|
||||
try:
|
||||
self.hooks.run('before_error_response')
|
||||
if self.error_response:
|
||||
self.error_response()
|
||||
self.hooks.run('after_error_response')
|
||||
cherrypy.serving.response.finalize()
|
||||
except cherrypy.HTTPRedirect:
|
||||
inst = sys.exc_info()[1]
|
||||
inst.set_response()
|
||||
cherrypy.serving.response.finalize()
|
||||
|
||||
# ------------------------- Properties ------------------------- #
|
||||
|
||||
def _get_body_params(self):
|
||||
warnings.warn(
|
||||
'body_params is deprecated in CherryPy 3.2, will be removed in '
|
||||
'CherryPy 3.3.',
|
||||
DeprecationWarning
|
||||
)
|
||||
return self.body.params
|
||||
body_params = property(_get_body_params,
|
||||
doc="""
|
||||
If the request Content-Type is 'application/x-www-form-urlencoded' or
|
||||
multipart, this will be a dict of the params pulled from the entity
|
||||
body; that is, it will be the portion of request.params that come
|
||||
from the message body (sometimes called "POST params", although they
|
||||
can be sent with various HTTP method verbs). This value is set between
|
||||
the 'before_request_body' and 'before_handler' hooks (assuming that
|
||||
process_request_body is True).
|
||||
|
||||
Deprecated in 3.2, will be removed for 3.3 in favor of
|
||||
:attr:`request.body.params<cherrypy._cprequest.RequestBody.params>`.""")
|
||||
|
||||
|
||||
class ResponseBody(object):
|
||||
|
||||
"""The body of the HTTP response (the response entity)."""
|
||||
|
||||
if six.PY3:
|
||||
unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
|
||||
'if you wish to return unicode.')
|
||||
|
||||
def __get__(self, obj, objclass=None):
|
||||
if obj is None:
|
||||
# When calling on the class instead of an instance...
|
||||
return self
|
||||
else:
|
||||
return obj._body
|
||||
|
||||
def __set__(self, obj, value):
|
||||
# Convert the given value to an iterable object.
|
||||
if six.PY3 and isinstance(value, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
|
||||
if isinstance(value, text_or_bytes):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# item list is much faster than iterating over every character
|
||||
# in a long string.
|
||||
if value:
|
||||
value = [value]
|
||||
else:
|
||||
# [''] doesn't evaluate to False, so replace it with [].
|
||||
value = []
|
||||
elif six.PY3 and isinstance(value, list):
|
||||
# every item in a list must be bytes...
|
||||
for i, item in enumerate(value):
|
||||
if isinstance(item, str):
|
||||
raise ValueError(self.unicode_err)
|
||||
# Don't use isinstance here; io.IOBase which has an ABC takes
|
||||
# 1000 times as long as, say, isinstance(value, str)
|
||||
elif hasattr(value, 'read'):
|
||||
value = file_generator(value)
|
||||
elif value is None:
|
||||
value = []
|
||||
obj._body = value
|
||||
|
||||
|
||||
class Response(object):
|
||||
|
||||
"""An HTTP Response, including status, headers, and body."""
|
||||
|
||||
status = ''
|
||||
"""The HTTP Status-Code and Reason-Phrase."""
|
||||
|
||||
header_list = []
|
||||
"""
|
||||
A list of the HTTP response headers as (name, value) tuples.
|
||||
In general, you should use response.headers (a dict) instead. This
|
||||
attribute is generated from response.headers and is not valid until
|
||||
after the finalize phase."""
|
||||
|
||||
headers = httputil.HeaderMap()
|
||||
"""
|
||||
A dict-like object containing the response headers. Keys are header
|
||||
names (in Title-Case format); however, you may get and set them in
|
||||
a case-insensitive manner. That is, headers['Content-Type'] and
|
||||
headers['content-type'] refer to the same value. Values are header
|
||||
values (decoded according to :rfc:`2047` if necessary).
|
||||
|
||||
.. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
|
||||
"""
|
||||
|
||||
cookie = SimpleCookie()
|
||||
"""See help(Cookie)."""
|
||||
|
||||
body = ResponseBody()
|
||||
"""The body (entity) of the HTTP response."""
|
||||
|
||||
time = None
|
||||
"""The value of time.time() when created. Use in HTTP dates."""
|
||||
|
||||
timeout = 300
|
||||
"""Seconds after which the response will be aborted."""
|
||||
|
||||
timed_out = False
|
||||
"""
|
||||
Flag to indicate the response should be aborted, because it has
|
||||
exceeded its timeout."""
|
||||
|
||||
stream = False
|
||||
"""If False, buffer the response body."""
|
||||
|
||||
def __init__(self):
|
||||
self.status = None
|
||||
self.header_list = None
|
||||
self._body = []
|
||||
self.time = time.time()
|
||||
|
||||
self.headers = httputil.HeaderMap()
|
||||
# Since we know all our keys are titled strings, we can
|
||||
# bypass HeaderMap.update and get a big speed boost.
|
||||
dict.update(self.headers, {
|
||||
'Content-Type': 'text/html',
|
||||
'Server': 'CherryPy/' + cherrypy.__version__,
|
||||
'Date': httputil.HTTPDate(self.time),
|
||||
})
|
||||
self.cookie = SimpleCookie()
|
||||
|
||||
def collapse_body(self):
|
||||
"""Collapse self.body to a single string; replace it and return it."""
|
||||
if isinstance(self.body, text_or_bytes):
|
||||
return self.body
|
||||
|
||||
newbody = []
|
||||
for chunk in self.body:
|
||||
if six.PY3 and not isinstance(chunk, bytes):
|
||||
raise TypeError("Chunk %s is not of type 'bytes'." %
|
||||
repr(chunk))
|
||||
newbody.append(chunk)
|
||||
newbody = ntob('').join(newbody)
|
||||
|
||||
self.body = newbody
|
||||
return newbody
|
||||
|
||||
def finalize(self):
|
||||
"""Transform headers (and cookies) into self.header_list. (Core)"""
|
||||
try:
|
||||
code, reason, _ = httputil.valid_status(self.status)
|
||||
except ValueError:
|
||||
raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
|
||||
|
||||
headers = self.headers
|
||||
|
||||
self.status = '%s %s' % (code, reason)
|
||||
self.output_status = ntob(str(code), 'ascii') + \
|
||||
ntob(' ') + headers.encode(reason)
|
||||
|
||||
if self.stream:
|
||||
# The upshot: wsgiserver will chunk the response if
|
||||
# you pop Content-Length (or set it explicitly to None).
|
||||
# Note that lib.static sets C-L to the file's st_size.
|
||||
if dict.get(headers, 'Content-Length') is None:
|
||||
dict.pop(headers, 'Content-Length', None)
|
||||
elif code < 200 or code in (204, 205, 304):
|
||||
# "All 1xx (informational), 204 (no content),
|
||||
# and 304 (not modified) responses MUST NOT
|
||||
# include a message-body."
|
||||
dict.pop(headers, 'Content-Length', None)
|
||||
self.body = ntob('')
|
||||
else:
|
||||
# Responses which are not streamed should have a Content-Length,
|
||||
# but allow user code to set Content-Length if desired.
|
||||
if dict.get(headers, 'Content-Length') is None:
|
||||
content = self.collapse_body()
|
||||
dict.__setitem__(headers, 'Content-Length', len(content))
|
||||
|
||||
# Transform our header dict into a list of tuples.
|
||||
self.header_list = h = headers.output()
|
||||
|
||||
cookie = self.cookie.output()
|
||||
if cookie:
|
||||
for line in cookie.split('\r\n'):
|
||||
name, value = line.split(': ', 1)
|
||||
if isinstance(name, six.text_type):
|
||||
name = name.encode('ISO-8859-1')
|
||||
if isinstance(value, six.text_type):
|
||||
value = headers.encode(value)
|
||||
h.append((name, value))
|
||||
|
||||
def check_timeout(self):
|
||||
"""If now > self.time + self.timeout, set self.timed_out.
|
||||
|
||||
This purposefully sets a flag, rather than raising an error,
|
||||
so that a monitor thread can interrupt the Response thread.
|
||||
"""
|
||||
if time.time() > self.time + self.timeout:
|
||||
self.timed_out = True
|
|
@ -1,235 +0,0 @@
|
|||
"""Manage HTTP servers with CherryPy."""
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib.reprconf import attributes
|
||||
from cherrypy._cpcompat import text_or_bytes
|
||||
from cherrypy.process.servers import ServerAdapter
|
||||
|
||||
# export some names from here
|
||||
from cherrypy.process.servers import (
|
||||
client_host, check_port, wait_for_free_port, wait_for_occupied_port,
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Server',
|
||||
'client_host', 'check_port', 'wait_for_free_port',
|
||||
'wait_for_occupied_port',
|
||||
]
|
||||
|
||||
|
||||
class Server(ServerAdapter):
|
||||
|
||||
"""An adapter for an HTTP server.
|
||||
|
||||
You can set attributes (like socket_host and socket_port)
|
||||
on *this* object (which is probably cherrypy.server), and call
|
||||
quickstart. For example::
|
||||
|
||||
cherrypy.server.socket_port = 80
|
||||
cherrypy.quickstart()
|
||||
"""
|
||||
|
||||
socket_port = 8080
|
||||
"""The TCP port on which to listen for connections."""
|
||||
|
||||
_socket_host = '127.0.0.1'
|
||||
|
||||
def _get_socket_host(self):
|
||||
return self._socket_host
|
||||
|
||||
def _set_socket_host(self, value):
|
||||
if value == '':
|
||||
raise ValueError("The empty string ('') is not an allowed value. "
|
||||
"Use '0.0.0.0' instead to listen on all active "
|
||||
'interfaces (INADDR_ANY).')
|
||||
self._socket_host = value
|
||||
socket_host = property(
|
||||
_get_socket_host,
|
||||
_set_socket_host,
|
||||
doc="""The hostname or IP address on which to listen for connections.
|
||||
|
||||
Host values may be any IPv4 or IPv6 address, or any valid hostname.
|
||||
The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
|
||||
your hosts file prefers IPv6). The string '0.0.0.0' is a special
|
||||
IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
|
||||
is the similar IN6ADDR_ANY for IPv6. The empty string or None are
|
||||
not allowed.""")
|
||||
|
||||
socket_file = None
|
||||
"""If given, the name of the UNIX socket to use instead of TCP/IP.
|
||||
|
||||
When this option is not None, the `socket_host` and `socket_port` options
|
||||
are ignored."""
|
||||
|
||||
socket_queue_size = 5
|
||||
"""The 'backlog' argument to socket.listen(); specifies the maximum number
|
||||
of queued connections (default 5)."""
|
||||
|
||||
socket_timeout = 10
|
||||
"""The timeout in seconds for accepted connections (default 10)."""
|
||||
|
||||
accepted_queue_size = -1
|
||||
"""The maximum number of requests which will be queued up before
|
||||
the server refuses to accept it (default -1, meaning no limit)."""
|
||||
|
||||
accepted_queue_timeout = 10
|
||||
"""The timeout in seconds for attempting to add a request to the
|
||||
queue when the queue is full (default 10)."""
|
||||
|
||||
shutdown_timeout = 5
|
||||
"""The time to wait for HTTP worker threads to clean up."""
|
||||
|
||||
protocol_version = 'HTTP/1.1'
|
||||
"""The version string to write in the Status-Line of all HTTP responses,
|
||||
for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
|
||||
this should also limit the supported features used in the response."""
|
||||
|
||||
thread_pool = 10
|
||||
"""The number of worker threads to start up in the pool."""
|
||||
|
||||
thread_pool_max = -1
|
||||
"""The maximum size of the worker-thread pool. Use -1 to indicate no limit.
|
||||
"""
|
||||
|
||||
max_request_header_size = 500 * 1024
|
||||
"""The maximum number of bytes allowable in the request headers.
|
||||
If exceeded, the HTTP server should return "413 Request Entity Too Large".
|
||||
"""
|
||||
|
||||
max_request_body_size = 100 * 1024 * 1024
|
||||
"""The maximum number of bytes allowable in the request body. If exceeded,
|
||||
the HTTP server should return "413 Request Entity Too Large"."""
|
||||
|
||||
instance = None
|
||||
"""If not None, this should be an HTTP server instance (such as
|
||||
CPWSGIServer) which cherrypy.server will control. Use this when you need
|
||||
more control over object instantiation than is available in the various
|
||||
configuration options."""
|
||||
|
||||
ssl_context = None
|
||||
"""When using PyOpenSSL, an instance of SSL.Context."""
|
||||
|
||||
ssl_certificate = None
|
||||
"""The filename of the SSL certificate to use."""
|
||||
|
||||
ssl_certificate_chain = None
|
||||
"""When using PyOpenSSL, the certificate chain to pass to
|
||||
Context.load_verify_locations."""
|
||||
|
||||
ssl_private_key = None
|
||||
"""The filename of the private key to use with SSL."""
|
||||
|
||||
if six.PY3:
|
||||
ssl_module = 'builtin'
|
||||
"""The name of a registered SSL adaptation module to use with
|
||||
the builtin WSGI server. Builtin options are: 'builtin' (to
|
||||
use the SSL library built into recent versions of Python).
|
||||
You may also register your own classes in the
|
||||
wsgiserver.ssl_adapters dict."""
|
||||
else:
|
||||
ssl_module = 'pyopenssl'
|
||||
"""The name of a registered SSL adaptation module to use with the
|
||||
builtin WSGI server. Builtin options are 'builtin' (to use the SSL
|
||||
library built into recent versions of Python) and 'pyopenssl' (to
|
||||
use the PyOpenSSL project, which you must install separately). You
|
||||
may also register your own classes in the wsgiserver.ssl_adapters
|
||||
dict."""
|
||||
|
||||
statistics = False
|
||||
"""Turns statistics-gathering on or off for aware HTTP servers."""
|
||||
|
||||
nodelay = True
|
||||
"""If True (the default since 3.1), sets the TCP_NODELAY socket option."""
|
||||
|
||||
wsgi_version = (1, 0)
|
||||
"""The WSGI version tuple to use with the builtin WSGI server.
|
||||
The provided options are (1, 0) [which includes support for PEP 3333,
|
||||
which declares it covers WSGI version 1.0.1 but still mandates the
|
||||
wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
|
||||
You may create and register your own experimental versions of the WSGI
|
||||
protocol by adding custom classes to the wsgiserver.wsgi_gateways dict."""
|
||||
|
||||
def __init__(self):
|
||||
self.bus = cherrypy.engine
|
||||
self.httpserver = None
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
def httpserver_from_self(self, httpserver=None):
|
||||
"""Return a (httpserver, bind_addr) pair based on self attributes."""
|
||||
if httpserver is None:
|
||||
httpserver = self.instance
|
||||
if httpserver is None:
|
||||
from cherrypy import _cpwsgi_server
|
||||
httpserver = _cpwsgi_server.CPWSGIServer(self)
|
||||
if isinstance(httpserver, text_or_bytes):
|
||||
# Is anyone using this? Can I add an arg?
|
||||
httpserver = attributes(httpserver)(self)
|
||||
return httpserver, self.bind_addr
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if not self.httpserver:
|
||||
self.httpserver, self.bind_addr = self.httpserver_from_self()
|
||||
super(Server, self).start()
|
||||
start.priority = 75
|
||||
|
||||
def _get_bind_addr(self):
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
if self.socket_host is None and self.socket_port is None:
|
||||
return None
|
||||
return (self.socket_host, self.socket_port)
|
||||
|
||||
def _set_bind_addr(self, value):
|
||||
if value is None:
|
||||
self.socket_file = None
|
||||
self.socket_host = None
|
||||
self.socket_port = None
|
||||
elif isinstance(value, text_or_bytes):
|
||||
self.socket_file = value
|
||||
self.socket_host = None
|
||||
self.socket_port = None
|
||||
else:
|
||||
try:
|
||||
self.socket_host, self.socket_port = value
|
||||
self.socket_file = None
|
||||
except ValueError:
|
||||
raise ValueError('bind_addr must be a (host, port) tuple '
|
||||
'(for TCP sockets) or a string (for Unix '
|
||||
'domain sockets), not %r' % value)
|
||||
bind_addr = property(
|
||||
_get_bind_addr,
|
||||
_set_bind_addr,
|
||||
doc='A (host, port) tuple for TCP sockets or '
|
||||
'a str for Unix domain sockets.')
|
||||
|
||||
def base(self):
|
||||
"""Return the base (scheme://host[:port] or sock file) for this server.
|
||||
"""
|
||||
if self.socket_file:
|
||||
return self.socket_file
|
||||
|
||||
host = self.socket_host
|
||||
if host in ('0.0.0.0', '::'):
|
||||
# 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
|
||||
# Look up the host name, which should be the
|
||||
# safest thing to spit out in a URL.
|
||||
import socket
|
||||
host = socket.gethostname()
|
||||
|
||||
port = self.socket_port
|
||||
|
||||
if self.ssl_certificate:
|
||||
scheme = 'https'
|
||||
if port != 443:
|
||||
host += ':%s' % port
|
||||
else:
|
||||
scheme = 'http'
|
||||
if port != 80:
|
||||
host += ':%s' % port
|
||||
|
||||
return '%s://%s' % (scheme, host)
|
|
@ -1,538 +0,0 @@
|
|||
"""CherryPy tools. A "tool" is any helper, adapted to CP.
|
||||
|
||||
Tools are usually designed to be used in a variety of ways (although some
|
||||
may only offer one if they choose):
|
||||
|
||||
Library calls
|
||||
All tools are callables that can be used wherever needed.
|
||||
The arguments are straightforward and should be detailed within the
|
||||
docstring.
|
||||
|
||||
Function decorators
|
||||
All tools, when called, may be used as decorators which configure
|
||||
individual CherryPy page handlers (methods on the CherryPy tree).
|
||||
That is, "@tools.anytool()" should "turn on" the tool via the
|
||||
decorated function's _cp_config attribute.
|
||||
|
||||
CherryPy config
|
||||
If a tool exposes a "_setup" callable, it will be called
|
||||
once per Request (if the feature is "turned on" via config).
|
||||
|
||||
Tools may be implemented as any object with a namespace. The builtins
|
||||
are generally either modules or instances of the tools.Tool class.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._helper import expose
|
||||
|
||||
from cherrypy.lib import cptools, encoding, auth, static, jsontools
|
||||
from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
|
||||
from cherrypy.lib import caching as _caching
|
||||
from cherrypy.lib import auth_basic, auth_digest
|
||||
|
||||
|
||||
def _getargs(func):
|
||||
"""Return the names of all static arguments to the given function."""
|
||||
# Use this instead of importing inspect for less mem overhead.
|
||||
import types
|
||||
if sys.version_info >= (3, 0):
|
||||
if isinstance(func, types.MethodType):
|
||||
func = func.__func__
|
||||
co = func.__code__
|
||||
else:
|
||||
if isinstance(func, types.MethodType):
|
||||
func = func.im_func
|
||||
co = func.func_code
|
||||
return co.co_varnames[:co.co_argcount]
|
||||
|
||||
|
||||
_attr_error = (
|
||||
'CherryPy Tools cannot be turned on directly. Instead, turn them '
|
||||
'on via config, or use them as decorators on your page handlers.'
|
||||
)
|
||||
|
||||
|
||||
class Tool(object):
|
||||
|
||||
"""A registered function for use with CherryPy request-processing hooks.
|
||||
|
||||
help(tool.callable) should give you more information about this Tool.
|
||||
"""
|
||||
|
||||
namespace = 'tools'
|
||||
|
||||
def __init__(self, point, callable, name=None, priority=50):
|
||||
self._point = point
|
||||
self.callable = callable
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
self.__doc__ = self.callable.__doc__
|
||||
self._setargs()
|
||||
|
||||
def _get_on(self):
|
||||
raise AttributeError(_attr_error)
|
||||
|
||||
def _set_on(self, value):
|
||||
raise AttributeError(_attr_error)
|
||||
on = property(_get_on, _set_on)
|
||||
|
||||
def _setargs(self):
|
||||
"""Copy func parameter names to obj attributes."""
|
||||
try:
|
||||
for arg in _getargs(self.callable):
|
||||
setattr(self, arg, None)
|
||||
except (TypeError, AttributeError):
|
||||
if hasattr(self.callable, '__call__'):
|
||||
for arg in _getargs(self.callable.__call__):
|
||||
setattr(self, arg, None)
|
||||
# IronPython 1.0 raises NotImplementedError because
|
||||
# inspect.getargspec tries to access Python bytecode
|
||||
# in co_code attribute.
|
||||
except NotImplementedError:
|
||||
pass
|
||||
# IronPython 1B1 may raise IndexError in some cases,
|
||||
# but if we trap it here it doesn't prevent CP from working.
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
def _merged_args(self, d=None):
|
||||
"""Return a dict of configuration entries for this Tool."""
|
||||
if d:
|
||||
conf = d.copy()
|
||||
else:
|
||||
conf = {}
|
||||
|
||||
tm = cherrypy.serving.request.toolmaps[self.namespace]
|
||||
if self._name in tm:
|
||||
conf.update(tm[self._name])
|
||||
|
||||
if 'on' in conf:
|
||||
del conf['on']
|
||||
|
||||
return conf
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
"""Compile-time decorator (turn on the tool in config).
|
||||
|
||||
For example::
|
||||
|
||||
@expose
|
||||
@tools.proxy()
|
||||
def whats_my_base(self):
|
||||
return cherrypy.request.base
|
||||
"""
|
||||
if args:
|
||||
raise TypeError('The %r Tool does not accept positional '
|
||||
'arguments; you must use keyword arguments.'
|
||||
% self._name)
|
||||
|
||||
def tool_decorator(f):
|
||||
if not hasattr(f, '_cp_config'):
|
||||
f._cp_config = {}
|
||||
subspace = self.namespace + '.' + self._name + '.'
|
||||
f._cp_config[subspace + 'on'] = True
|
||||
for k, v in kwargs.items():
|
||||
f._cp_config[subspace + k] = v
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
conf = self._merged_args()
|
||||
p = conf.pop('priority', None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, 'priority', self._priority)
|
||||
cherrypy.serving.request.hooks.attach(self._point, self.callable,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
class HandlerTool(Tool):
|
||||
|
||||
"""Tool which is called 'before main', that may skip normal handlers.
|
||||
|
||||
If the tool successfully handles the request (by setting response.body),
|
||||
if should return True. This will cause CherryPy to skip any 'normal' page
|
||||
handler. If the tool did not handle the request, it should return False
|
||||
to tell CherryPy to continue on and call the normal page handler. If the
|
||||
tool is declared AS a page handler (see the 'handler' method), returning
|
||||
False will raise NotFound.
|
||||
"""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, 'before_handler', callable, name)
|
||||
|
||||
def handler(self, *args, **kwargs):
|
||||
"""Use this tool as a CherryPy page handler.
|
||||
|
||||
For example::
|
||||
|
||||
class Root:
|
||||
nav = tools.staticdir.handler(section="/nav", dir="nav",
|
||||
root=absDir)
|
||||
"""
|
||||
@expose
|
||||
def handle_func(*a, **kw):
|
||||
handled = self.callable(*args, **self._merged_args(kwargs))
|
||||
if not handled:
|
||||
raise cherrypy.NotFound()
|
||||
return cherrypy.serving.response.body
|
||||
return handle_func
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
if self.callable(**kwargs):
|
||||
cherrypy.serving.request.handler = None
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
conf = self._merged_args()
|
||||
p = conf.pop('priority', None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, 'priority', self._priority)
|
||||
cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
class HandlerWrapperTool(Tool):
|
||||
|
||||
"""Tool which wraps request.handler in a provided wrapper function.
|
||||
|
||||
The 'newhandler' arg must be a handler wrapper function that takes a
|
||||
'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
|
||||
page handler
|
||||
functions, it must return an iterable for use as cherrypy.response.body.
|
||||
|
||||
For example, to allow your 'inner' page handlers to return dicts
|
||||
which then get interpolated into a template::
|
||||
|
||||
def interpolator(next_handler, *args, **kwargs):
|
||||
filename = cherrypy.request.config.get('template')
|
||||
cherrypy.response.template = env.get_template(filename)
|
||||
response_dict = next_handler(*args, **kwargs)
|
||||
return cherrypy.response.template.render(**response_dict)
|
||||
cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
|
||||
"""
|
||||
|
||||
def __init__(self, newhandler, point='before_handler', name=None,
|
||||
priority=50):
|
||||
self.newhandler = newhandler
|
||||
self._point = point
|
||||
self._name = name
|
||||
self._priority = priority
|
||||
|
||||
def callable(self, *args, **kwargs):
|
||||
innerfunc = cherrypy.serving.request.handler
|
||||
|
||||
def wrap(*args, **kwargs):
|
||||
return self.newhandler(innerfunc, *args, **kwargs)
|
||||
cherrypy.serving.request.handler = wrap
|
||||
|
||||
|
||||
class ErrorTool(Tool):
|
||||
|
||||
"""Tool which is used to replace the default request.error_response."""
|
||||
|
||||
def __init__(self, callable, name=None):
|
||||
Tool.__init__(self, None, callable, name)
|
||||
|
||||
def _wrapper(self):
|
||||
self.callable(**self._merged_args())
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
cherrypy.serving.request.error_response = self._wrapper
|
||||
|
||||
|
||||
# Builtin tools #
|
||||
|
||||
|
||||
class SessionTool(Tool):
|
||||
|
||||
"""Session Tool for CherryPy.
|
||||
|
||||
sessions.locking
|
||||
When 'implicit' (the default), the session will be locked for you,
|
||||
just before running the page handler.
|
||||
|
||||
When 'early', the session will be locked before reading the request
|
||||
body. This is off by default for safety reasons; for example,
|
||||
a large upload would block the session, denying an AJAX
|
||||
progress meter
|
||||
(`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
|
||||
|
||||
When 'explicit' (or any other value), you need to call
|
||||
cherrypy.session.acquire_lock() yourself before using
|
||||
session data.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# _sessions.init must be bound after headers are read
|
||||
Tool.__init__(self, 'before_request_body', _sessions.init)
|
||||
|
||||
def _lock_session(self):
|
||||
cherrypy.serving.session.acquire_lock()
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
hooks = cherrypy.serving.request.hooks
|
||||
|
||||
conf = self._merged_args()
|
||||
|
||||
p = conf.pop('priority', None)
|
||||
if p is None:
|
||||
p = getattr(self.callable, 'priority', self._priority)
|
||||
|
||||
hooks.attach(self._point, self.callable, priority=p, **conf)
|
||||
|
||||
locking = conf.pop('locking', 'implicit')
|
||||
if locking == 'implicit':
|
||||
hooks.attach('before_handler', self._lock_session)
|
||||
elif locking == 'early':
|
||||
# Lock before the request body (but after _sessions.init runs!)
|
||||
hooks.attach('before_request_body', self._lock_session,
|
||||
priority=60)
|
||||
else:
|
||||
# Don't lock
|
||||
pass
|
||||
|
||||
hooks.attach('before_finalize', _sessions.save)
|
||||
hooks.attach('on_end_request', _sessions.close)
|
||||
|
||||
def regenerate(self):
|
||||
"""Drop the current session and make a new one (with a new id)."""
|
||||
sess = cherrypy.serving.session
|
||||
sess.regenerate()
|
||||
|
||||
# Grab cookie-relevant tool args
|
||||
conf = dict([(k, v) for k, v in self._merged_args().items()
|
||||
if k in ('path', 'path_header', 'name', 'timeout',
|
||||
'domain', 'secure')])
|
||||
_sessions.set_response_cookie(**conf)
|
||||
|
||||
|
||||
class XMLRPCController(object):
|
||||
|
||||
"""A Controller (page handler collection) for XML-RPC.
|
||||
|
||||
To use it, have your controllers subclass this base class (it will
|
||||
turn on the tool for you).
|
||||
|
||||
You can also supply the following optional config entries::
|
||||
|
||||
tools.xmlrpc.encoding: 'utf-8'
|
||||
tools.xmlrpc.allow_none: 0
|
||||
|
||||
XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
|
||||
appropriate handler must first be performed according to the URL, and
|
||||
then a second dispatch step must take place according to the RPC method
|
||||
specified in the request body. It also allows a superfluous "/RPC2"
|
||||
prefix in the URL, supplies its own handler args in the body, and
|
||||
requires a 200 OK "Fault" response instead of 404 when the desired
|
||||
method is not found.
|
||||
|
||||
Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
|
||||
This Controller acts as the dispatch target for the first half (based
|
||||
on the URL); it then reads the RPC method from the request body and
|
||||
does its own second dispatch step based on that method. It also reads
|
||||
body params, and returns a Fault on error.
|
||||
|
||||
The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
|
||||
in your URL's, you can safely skip turning on the XMLRPCDispatcher.
|
||||
Otherwise, you need to use declare it in config::
|
||||
|
||||
request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
|
||||
"""
|
||||
|
||||
# Note we're hard-coding this into the 'tools' namespace. We could do
|
||||
# a huge amount of work to make it relocatable, but the only reason why
|
||||
# would be if someone actually disabled the default_toolbox. Meh.
|
||||
_cp_config = {'tools.xmlrpc.on': True}
|
||||
|
||||
@expose
|
||||
def default(self, *vpath, **params):
|
||||
rpcparams, rpcmethod = _xmlrpc.process_body()
|
||||
|
||||
subhandler = self
|
||||
for attr in str(rpcmethod).split('.'):
|
||||
subhandler = getattr(subhandler, attr, None)
|
||||
|
||||
if subhandler and getattr(subhandler, 'exposed', False):
|
||||
body = subhandler(*(vpath + rpcparams), **params)
|
||||
|
||||
else:
|
||||
# https://github.com/cherrypy/cherrypy/issues/533
|
||||
# if a method is not found, an xmlrpclib.Fault should be returned
|
||||
# raising an exception here will do that; see
|
||||
# cherrypy.lib.xmlrpcutil.on_error
|
||||
raise Exception('method "%s" is not supported' % attr)
|
||||
|
||||
conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {})
|
||||
_xmlrpc.respond(body,
|
||||
conf.get('encoding', 'utf-8'),
|
||||
conf.get('allow_none', 0))
|
||||
return cherrypy.serving.response.body
|
||||
|
||||
|
||||
class SessionAuthTool(HandlerTool):
|
||||
|
||||
def _setargs(self):
|
||||
for name in dir(cptools.SessionAuth):
|
||||
if not name.startswith('__'):
|
||||
setattr(self, name, None)
|
||||
|
||||
|
||||
class CachingTool(Tool):
|
||||
|
||||
"""Caching Tool for CherryPy."""
|
||||
|
||||
def _wrapper(self, **kwargs):
|
||||
request = cherrypy.serving.request
|
||||
if _caching.get(**kwargs):
|
||||
request.handler = None
|
||||
else:
|
||||
if request.cacheable:
|
||||
# Note the devious technique here of adding hooks on the fly
|
||||
request.hooks.attach('before_finalize', _caching.tee_output,
|
||||
priority=90)
|
||||
_wrapper.priority = 20
|
||||
|
||||
def _setup(self):
|
||||
"""Hook caching into cherrypy.request."""
|
||||
conf = self._merged_args()
|
||||
|
||||
p = conf.pop('priority', None)
|
||||
cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
|
||||
priority=p, **conf)
|
||||
|
||||
|
||||
class Toolbox(object):
|
||||
|
||||
"""A collection of Tools.
|
||||
|
||||
This object also functions as a config namespace handler for itself.
|
||||
Custom toolboxes should be added to each Application's toolboxes dict.
|
||||
"""
|
||||
|
||||
def __init__(self, namespace):
|
||||
self.namespace = namespace
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# If the Tool._name is None, supply it from the attribute name.
|
||||
if isinstance(value, Tool):
|
||||
if value._name is None:
|
||||
value._name = name
|
||||
value.namespace = self.namespace
|
||||
object.__setattr__(self, name, value)
|
||||
|
||||
def __enter__(self):
|
||||
"""Populate request.toolmaps from tools specified in config."""
|
||||
cherrypy.serving.request.toolmaps[self.namespace] = map = {}
|
||||
|
||||
def populate(k, v):
|
||||
toolname, arg = k.split('.', 1)
|
||||
bucket = map.setdefault(toolname, {})
|
||||
bucket[arg] = v
|
||||
return populate
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Run tool._setup() for each tool in our toolmap."""
|
||||
map = cherrypy.serving.request.toolmaps.get(self.namespace)
|
||||
if map:
|
||||
for name, settings in map.items():
|
||||
if settings.get('on', False):
|
||||
tool = getattr(self, name)
|
||||
tool._setup()
|
||||
|
||||
def register(self, point, **kwargs):
|
||||
"""Return a decorator which registers the function at the given hook point."""
|
||||
def decorator(func):
|
||||
setattr(self, kwargs.get('name', func.__name__), Tool(point, func, **kwargs))
|
||||
return func
|
||||
return decorator
|
||||
|
||||
|
||||
class DeprecatedTool(Tool):
|
||||
|
||||
_name = None
|
||||
warnmsg = 'This Tool is deprecated.'
|
||||
|
||||
def __init__(self, point, warnmsg=None):
|
||||
self.point = point
|
||||
if warnmsg is not None:
|
||||
self.warnmsg = warnmsg
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
warnings.warn(self.warnmsg)
|
||||
|
||||
def tool_decorator(f):
|
||||
return f
|
||||
return tool_decorator
|
||||
|
||||
def _setup(self):
|
||||
warnings.warn(self.warnmsg)
|
||||
|
||||
|
||||
default_toolbox = _d = Toolbox('tools')
|
||||
_d.session_auth = SessionAuthTool(cptools.session_auth)
|
||||
_d.allow = Tool('on_start_resource', cptools.allow)
|
||||
_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
|
||||
_d.response_headers = Tool('on_start_resource', cptools.response_headers)
|
||||
_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
|
||||
_d.log_headers = Tool('before_error_response', cptools.log_request_headers)
|
||||
_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
|
||||
_d.err_redirect = ErrorTool(cptools.redirect)
|
||||
_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
|
||||
_d.decode = Tool('before_request_body', encoding.decode)
|
||||
# the order of encoding, gzip, caching is important
|
||||
_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
|
||||
_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
|
||||
_d.staticdir = HandlerTool(static.staticdir)
|
||||
_d.staticfile = HandlerTool(static.staticfile)
|
||||
_d.sessions = SessionTool()
|
||||
_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
|
||||
_d.caching = CachingTool('before_handler', _caching.get, 'caching')
|
||||
_d.expires = Tool('before_finalize', _caching.expires)
|
||||
_d.tidy = DeprecatedTool(
|
||||
'before_finalize',
|
||||
'The tidy tool has been removed from the standard distribution of '
|
||||
'CherryPy. The most recent version can be found at '
|
||||
'http://tools.cherrypy.org/browser.')
|
||||
_d.nsgmls = DeprecatedTool(
|
||||
'before_finalize',
|
||||
'The nsgmls tool has been removed from the standard distribution of '
|
||||
'CherryPy. The most recent version can be found at '
|
||||
'http://tools.cherrypy.org/browser.')
|
||||
_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
|
||||
_d.referer = Tool('before_request_body', cptools.referer)
|
||||
_d.basic_auth = Tool('on_start_resource', auth.basic_auth)
|
||||
_d.digest_auth = Tool('on_start_resource', auth.digest_auth)
|
||||
_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
|
||||
_d.flatten = Tool('before_finalize', cptools.flatten)
|
||||
_d.accept = Tool('on_start_resource', cptools.accept)
|
||||
_d.redirect = Tool('on_start_resource', cptools.redirect)
|
||||
_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
|
||||
_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
|
||||
_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
|
||||
_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
|
||||
_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
|
||||
_d.params = Tool('before_handler', cptools.convert_params)
|
||||
|
||||
del _d, cptools, encoding, auth, static
|
|
@ -1,287 +0,0 @@
|
|||
"""CherryPy Application and Tree objects."""
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntou
|
||||
from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
class Application(object):
|
||||
|
||||
"""A CherryPy Application.
|
||||
|
||||
Servers and gateways should not instantiate Request objects directly.
|
||||
Instead, they should ask an Application object for a request object.
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object) for itself.
|
||||
"""
|
||||
|
||||
root = None
|
||||
"""The top-most container of page handlers for this app. Handlers should
|
||||
be arranged in a hierarchy of attributes, matching the expected URI
|
||||
hierarchy; the default dispatcher then searches this hierarchy for a
|
||||
matching handler. When using a dispatcher other than the default,
|
||||
this value may be None."""
|
||||
|
||||
config = {}
|
||||
"""A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
|
||||
of {key: value} pairs."""
|
||||
|
||||
namespaces = _cpconfig.NamespaceSet()
|
||||
toolboxes = {'tools': cherrypy.tools}
|
||||
|
||||
log = None
|
||||
"""A LogManager instance. See _cplogging."""
|
||||
|
||||
wsgiapp = None
|
||||
"""A CPWSGIApp instance. See _cpwsgi."""
|
||||
|
||||
request_class = _cprequest.Request
|
||||
response_class = _cprequest.Response
|
||||
|
||||
relative_urls = False
|
||||
|
||||
def __init__(self, root, script_name='', config=None):
|
||||
self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
|
||||
self.root = root
|
||||
self.script_name = script_name
|
||||
self.wsgiapp = _cpwsgi.CPWSGIApp(self)
|
||||
|
||||
self.namespaces = self.namespaces.copy()
|
||||
self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
|
||||
self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
|
||||
|
||||
self.config = self.__class__.config.copy()
|
||||
if config:
|
||||
self.merge(config)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
|
||||
self.root, self.script_name)
|
||||
|
||||
script_name_doc = """The URI "mount point" for this app. A mount point
|
||||
is that portion of the URI which is constant for all URIs that are
|
||||
serviced by this application; it does not include scheme, host, or proxy
|
||||
("virtual host") portions of the URI.
|
||||
|
||||
For example, if script_name is "/my/cool/app", then the URL
|
||||
"http://www.example.com/my/cool/app/page1" might be handled by a
|
||||
"page1" method on the root object.
|
||||
|
||||
The value of script_name MUST NOT end in a slash. If the script_name
|
||||
refers to the root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
If script_name is explicitly set to None, then the script_name will be
|
||||
provided for each call from request.wsgi_environ['SCRIPT_NAME'].
|
||||
"""
|
||||
|
||||
def _get_script_name(self):
|
||||
if self._script_name is not None:
|
||||
return self._script_name
|
||||
|
||||
# A `_script_name` with a value of None signals that the script name
|
||||
# should be pulled from WSGI environ.
|
||||
return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
|
||||
|
||||
def _set_script_name(self, value):
|
||||
if value:
|
||||
value = value.rstrip('/')
|
||||
self._script_name = value
|
||||
script_name = property(fget=_get_script_name, fset=_set_script_name,
|
||||
doc=script_name_doc)
|
||||
|
||||
def merge(self, config):
|
||||
"""Merge the given config into self.config."""
|
||||
_cpconfig.merge(self.config, config)
|
||||
|
||||
# Handle namespaces specified in config.
|
||||
self.namespaces(self.config.get('/', {}))
|
||||
|
||||
def find_config(self, path, key, default=None):
|
||||
"""Return the most-specific value for key along path, or default."""
|
||||
trail = path or '/'
|
||||
while trail:
|
||||
nodeconf = self.config.get(trail, {})
|
||||
|
||||
if key in nodeconf:
|
||||
return nodeconf[key]
|
||||
|
||||
lastslash = trail.rfind('/')
|
||||
if lastslash == -1:
|
||||
break
|
||||
elif lastslash == 0 and trail != '/':
|
||||
trail = '/'
|
||||
else:
|
||||
trail = trail[:lastslash]
|
||||
|
||||
return default
|
||||
|
||||
def get_serving(self, local, remote, scheme, sproto):
|
||||
"""Create and return a Request and Response object."""
|
||||
req = self.request_class(local, remote, scheme, sproto)
|
||||
req.app = self
|
||||
|
||||
for name, toolbox in self.toolboxes.items():
|
||||
req.namespaces[name] = toolbox
|
||||
|
||||
resp = self.response_class()
|
||||
cherrypy.serving.load(req, resp)
|
||||
cherrypy.engine.publish('acquire_thread')
|
||||
cherrypy.engine.publish('before_request')
|
||||
|
||||
return req, resp
|
||||
|
||||
def release_serving(self):
|
||||
"""Release the current serving (request and response)."""
|
||||
req = cherrypy.serving.request
|
||||
|
||||
cherrypy.engine.publish('after_request')
|
||||
|
||||
try:
|
||||
req.close()
|
||||
except:
|
||||
cherrypy.log(traceback=True, severity=40)
|
||||
|
||||
cherrypy.serving.clear()
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return self.wsgiapp(environ, start_response)
|
||||
|
||||
|
||||
class Tree(object):
|
||||
|
||||
"""A registry of CherryPy applications, mounted at diverse points.
|
||||
|
||||
An instance of this class may also be used as a WSGI callable
|
||||
(WSGI application object), in which case it dispatches to all
|
||||
mounted apps.
|
||||
"""
|
||||
|
||||
apps = {}
|
||||
"""
|
||||
A dict of the form {script name: application}, where "script name"
|
||||
is a string declaring the URI mount point (no trailing slash), and
|
||||
"application" is an instance of cherrypy.Application (or an arbitrary
|
||||
WSGI callable if you happen to be using a WSGI server)."""
|
||||
|
||||
def __init__(self):
|
||||
self.apps = {}
|
||||
|
||||
def mount(self, root, script_name='', config=None):
|
||||
"""Mount a new app from a root object, script_name, and config.
|
||||
|
||||
root
|
||||
An instance of a "controller class" (a collection of page
|
||||
handler methods) which represents the root of the application.
|
||||
This may also be an Application instance, or None if using
|
||||
a dispatcher other than the default.
|
||||
|
||||
script_name
|
||||
A string containing the "mount point" of the application.
|
||||
This should start with a slash, and be the path portion of the
|
||||
URL at which to mount the given root. For example, if root.index()
|
||||
will handle requests to "http://www.example.com:8080/dept/app1/",
|
||||
then the script_name argument would be "/dept/app1".
|
||||
|
||||
It MUST NOT end in a slash. If the script_name refers to the
|
||||
root of the URI, it MUST be an empty string (not "/").
|
||||
|
||||
config
|
||||
A file or dict containing application config.
|
||||
"""
|
||||
if script_name is None:
|
||||
raise TypeError(
|
||||
"The 'script_name' argument may not be None. Application "
|
||||
'objects may, however, possess a script_name of None (in '
|
||||
'order to inpect the WSGI environ for SCRIPT_NAME upon each '
|
||||
'request). You cannot mount such Applications on this Tree; '
|
||||
'you must pass them to a WSGI server interface directly.')
|
||||
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip('/')
|
||||
|
||||
if isinstance(root, Application):
|
||||
app = root
|
||||
if script_name != '' and script_name != app.script_name:
|
||||
raise ValueError(
|
||||
'Cannot specify a different script name and pass an '
|
||||
'Application instance to cherrypy.mount')
|
||||
script_name = app.script_name
|
||||
else:
|
||||
app = Application(root, script_name)
|
||||
|
||||
# If mounted at "", add favicon.ico
|
||||
if (script_name == '' and root is not None
|
||||
and not hasattr(root, 'favicon_ico')):
|
||||
favicon = os.path.join(os.getcwd(), os.path.dirname(__file__),
|
||||
'favicon.ico')
|
||||
root.favicon_ico = tools.staticfile.handler(favicon)
|
||||
|
||||
if config:
|
||||
app.merge(config)
|
||||
|
||||
self.apps[script_name] = app
|
||||
|
||||
return app
|
||||
|
||||
def graft(self, wsgi_callable, script_name=''):
|
||||
"""Mount a wsgi callable at the given script_name."""
|
||||
# Next line both 1) strips trailing slash and 2) maps "/" -> "".
|
||||
script_name = script_name.rstrip('/')
|
||||
self.apps[script_name] = wsgi_callable
|
||||
|
||||
def script_name(self, path=None):
|
||||
"""The script_name of the app at the given path, or None.
|
||||
|
||||
If path is None, cherrypy.request is used.
|
||||
"""
|
||||
if path is None:
|
||||
try:
|
||||
request = cherrypy.serving.request
|
||||
path = httputil.urljoin(request.script_name,
|
||||
request.path_info)
|
||||
except AttributeError:
|
||||
return None
|
||||
|
||||
while True:
|
||||
if path in self.apps:
|
||||
return path
|
||||
|
||||
if path == '':
|
||||
return None
|
||||
|
||||
# Move one node up the tree and try again.
|
||||
path = path[:path.rfind('/')]
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
# If you're calling this, then you're probably setting SCRIPT_NAME
|
||||
# to '' (some WSGI servers always set SCRIPT_NAME to '').
|
||||
# Try to look up the app using the full path.
|
||||
env1x = environ
|
||||
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
||||
env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
|
||||
path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
|
||||
env1x.get('PATH_INFO', ''))
|
||||
sn = self.script_name(path or '/')
|
||||
if sn is None:
|
||||
start_response('404 Not Found', [])
|
||||
return []
|
||||
|
||||
app = self.apps[sn]
|
||||
|
||||
# Correct the SCRIPT_NAME and PATH_INFO environ entries.
|
||||
environ = environ.copy()
|
||||
if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
||||
# Python 2/WSGI u.0: all strings MUST be of type unicode
|
||||
enc = environ[ntou('wsgi.url_encoding')]
|
||||
environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
|
||||
environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
|
||||
else:
|
||||
environ['SCRIPT_NAME'] = sn
|
||||
environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
|
||||
return app(environ, start_response)
|
|
@ -1,466 +0,0 @@
|
|||
"""WSGI interface (see PEP 333 and 3333).
|
||||
|
||||
Note that WSGI environ keys and values are 'native strings'; that is,
|
||||
whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
|
||||
it's a unicode string. But PEP 3333 says: "even if Python's str type is
|
||||
actually Unicode "under the hood", the content of native strings must
|
||||
still be translatable to bytes via the Latin-1 encoding!"
|
||||
"""
|
||||
|
||||
import sys as _sys
|
||||
import io
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy as _cherrypy
|
||||
from cherrypy._cpcompat import ntob, ntou
|
||||
from cherrypy import _cperror
|
||||
from cherrypy.lib import httputil
|
||||
from cherrypy.lib import is_closable_iterator
|
||||
|
||||
def downgrade_wsgi_ux_to_1x(environ):
|
||||
"""Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
|
||||
"""
|
||||
env1x = {}
|
||||
|
||||
url_encoding = environ[ntou('wsgi.url_encoding')]
|
||||
for k, v in list(environ.items()):
|
||||
if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
|
||||
v = v.encode(url_encoding)
|
||||
elif isinstance(v, six.text_type):
|
||||
v = v.encode('ISO-8859-1')
|
||||
env1x[k.encode('ISO-8859-1')] = v
|
||||
|
||||
return env1x
|
||||
|
||||
|
||||
class VirtualHost(object):
|
||||
|
||||
"""Select a different WSGI application based on the Host header.
|
||||
|
||||
This can be useful when running multiple sites within one CP server.
|
||||
It allows several domains to point to different applications. For example::
|
||||
|
||||
root = Root()
|
||||
RootApp = cherrypy.Application(root)
|
||||
Domain2App = cherrypy.Application(root)
|
||||
SecureApp = cherrypy.Application(Secure())
|
||||
|
||||
vhost = cherrypy._cpwsgi.VirtualHost(
|
||||
RootApp,
|
||||
domains={
|
||||
'www.domain2.example': Domain2App,
|
||||
'www.domain2.example:443': SecureApp,
|
||||
},
|
||||
)
|
||||
|
||||
cherrypy.tree.graft(vhost)
|
||||
"""
|
||||
default = None
|
||||
"""Required. The default WSGI application."""
|
||||
|
||||
use_x_forwarded_host = True
|
||||
"""If True (the default), any "X-Forwarded-Host"
|
||||
request header will be used instead of the "Host" header. This
|
||||
is commonly added by HTTP servers (such as Apache) when proxying."""
|
||||
|
||||
domains = {}
|
||||
"""A dict of {host header value: application} pairs.
|
||||
The incoming "Host" request header is looked up in this dict,
|
||||
and, if a match is found, the corresponding WSGI application
|
||||
will be called instead of the default. Note that you often need
|
||||
separate entries for "example.com" and "www.example.com".
|
||||
In addition, "Host" headers may contain the port number.
|
||||
"""
|
||||
|
||||
def __init__(self, default, domains=None, use_x_forwarded_host=True):
|
||||
self.default = default
|
||||
self.domains = domains or {}
|
||||
self.use_x_forwarded_host = use_x_forwarded_host
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
domain = environ.get('HTTP_HOST', '')
|
||||
if self.use_x_forwarded_host:
|
||||
domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
|
||||
|
||||
nextapp = self.domains.get(domain)
|
||||
if nextapp is None:
|
||||
nextapp = self.default
|
||||
return nextapp(environ, start_response)
|
||||
|
||||
|
||||
class InternalRedirector(object):
|
||||
|
||||
"""WSGI middleware that handles raised cherrypy.InternalRedirect."""
|
||||
|
||||
def __init__(self, nextapp, recursive=False):
|
||||
self.nextapp = nextapp
|
||||
self.recursive = recursive
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
redirections = []
|
||||
while True:
|
||||
environ = environ.copy()
|
||||
try:
|
||||
return self.nextapp(environ, start_response)
|
||||
except _cherrypy.InternalRedirect:
|
||||
ir = _sys.exc_info()[1]
|
||||
sn = environ.get('SCRIPT_NAME', '')
|
||||
path = environ.get('PATH_INFO', '')
|
||||
qs = environ.get('QUERY_STRING', '')
|
||||
|
||||
# Add the *previous* path_info + qs to redirections.
|
||||
old_uri = sn + path
|
||||
if qs:
|
||||
old_uri += '?' + qs
|
||||
redirections.append(old_uri)
|
||||
|
||||
if not self.recursive:
|
||||
# Check to see if the new URI has been redirected to
|
||||
# already
|
||||
new_uri = sn + ir.path
|
||||
if ir.query_string:
|
||||
new_uri += '?' + ir.query_string
|
||||
if new_uri in redirections:
|
||||
ir.request.close()
|
||||
tmpl = (
|
||||
'InternalRedirector visited the same URL twice: %r'
|
||||
)
|
||||
raise RuntimeError(tmpl % new_uri)
|
||||
|
||||
# Munge the environment and try again.
|
||||
environ['REQUEST_METHOD'] = 'GET'
|
||||
environ['PATH_INFO'] = ir.path
|
||||
environ['QUERY_STRING'] = ir.query_string
|
||||
environ['wsgi.input'] = io.BytesIO()
|
||||
environ['CONTENT_LENGTH'] = '0'
|
||||
environ['cherrypy.previous_request'] = ir.request
|
||||
|
||||
|
||||
class ExceptionTrapper(object):
|
||||
|
||||
"""WSGI middleware that traps exceptions."""
|
||||
|
||||
def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
|
||||
self.nextapp = nextapp
|
||||
self.throws = throws
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
return _TrappedResponse(
|
||||
self.nextapp,
|
||||
environ,
|
||||
start_response,
|
||||
self.throws
|
||||
)
|
||||
|
||||
|
||||
class _TrappedResponse(object):
|
||||
|
||||
response = iter([])
|
||||
|
||||
def __init__(self, nextapp, environ, start_response, throws):
|
||||
self.nextapp = nextapp
|
||||
self.environ = environ
|
||||
self.start_response = start_response
|
||||
self.throws = throws
|
||||
self.started_response = False
|
||||
self.response = self.trap(
|
||||
self.nextapp, self.environ, self.start_response,
|
||||
)
|
||||
self.iter_response = iter(self.response)
|
||||
|
||||
def __iter__(self):
|
||||
self.started_response = True
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return self.trap(next, self.iter_response)
|
||||
|
||||
# todo: https://pythonhosted.org/six/#six.Iterator
|
||||
if six.PY2:
|
||||
next = __next__
|
||||
|
||||
def close(self):
|
||||
if hasattr(self.response, 'close'):
|
||||
self.response.close()
|
||||
|
||||
def trap(self, func, *args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except self.throws:
|
||||
raise
|
||||
except StopIteration:
|
||||
raise
|
||||
except:
|
||||
tb = _cperror.format_exc()
|
||||
_cherrypy.log(tb, severity=40)
|
||||
if not _cherrypy.request.show_tracebacks:
|
||||
tb = ''
|
||||
s, h, b = _cperror.bare_error(tb)
|
||||
if six.PY3:
|
||||
# What fun.
|
||||
s = s.decode('ISO-8859-1')
|
||||
h = [
|
||||
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
|
||||
for k, v in h
|
||||
]
|
||||
if self.started_response:
|
||||
# Empty our iterable (so future calls raise StopIteration)
|
||||
self.iter_response = iter([])
|
||||
else:
|
||||
self.iter_response = iter(b)
|
||||
|
||||
try:
|
||||
self.start_response(s, h, _sys.exc_info())
|
||||
except:
|
||||
# "The application must not trap any exceptions raised by
|
||||
# start_response, if it called start_response with exc_info.
|
||||
# Instead, it should allow such exceptions to propagate
|
||||
# back to the server or gateway."
|
||||
# But we still log and call close() to clean up ourselves.
|
||||
_cherrypy.log(traceback=True, severity=40)
|
||||
raise
|
||||
|
||||
if self.started_response:
|
||||
return ntob('').join(b)
|
||||
else:
|
||||
return b
|
||||
|
||||
|
||||
# WSGI-to-CP Adapter #
|
||||
|
||||
|
||||
class AppResponse(object):
|
||||
|
||||
"""WSGI response iterable for CherryPy applications."""
|
||||
|
||||
def __init__(self, environ, start_response, cpapp):
|
||||
self.cpapp = cpapp
|
||||
try:
|
||||
if six.PY2:
|
||||
if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
|
||||
environ = downgrade_wsgi_ux_to_1x(environ)
|
||||
self.environ = environ
|
||||
self.run()
|
||||
|
||||
r = _cherrypy.serving.response
|
||||
|
||||
outstatus = r.output_status
|
||||
if not isinstance(outstatus, bytes):
|
||||
raise TypeError('response.output_status is not a byte string.')
|
||||
|
||||
outheaders = []
|
||||
for k, v in r.header_list:
|
||||
if not isinstance(k, bytes):
|
||||
tmpl = 'response.header_list key %r is not a byte string.'
|
||||
raise TypeError(tmpl % k)
|
||||
if not isinstance(v, bytes):
|
||||
tmpl = (
|
||||
'response.header_list value %r is not a byte string.'
|
||||
)
|
||||
raise TypeError(tmpl % v)
|
||||
outheaders.append((k, v))
|
||||
|
||||
if six.PY3:
|
||||
# According to PEP 3333, when using Python 3, the response
|
||||
# status and headers must be bytes masquerading as unicode;
|
||||
# that is, they must be of type "str" but are restricted to
|
||||
# code points in the "latin-1" set.
|
||||
outstatus = outstatus.decode('ISO-8859-1')
|
||||
outheaders = [
|
||||
(k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
|
||||
for k, v in outheaders
|
||||
]
|
||||
|
||||
self.iter_response = iter(r.body)
|
||||
self.write = start_response(outstatus, outheaders)
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
return next(self.iter_response)
|
||||
|
||||
# todo: https://pythonhosted.org/six/#six.Iterator
|
||||
if six.PY2:
|
||||
next = __next__
|
||||
|
||||
def close(self):
|
||||
"""Close and de-reference the current request and response. (Core)"""
|
||||
streaming = _cherrypy.serving.response.stream
|
||||
self.cpapp.release_serving()
|
||||
|
||||
# We avoid the expense of examining the iterator to see if it's
|
||||
# closable unless we are streaming the response, as that's the
|
||||
# only situation where we are going to have an iterator which
|
||||
# may not have been exhausted yet.
|
||||
if streaming and is_closable_iterator(self.iter_response):
|
||||
iter_close = self.iter_response.close
|
||||
try:
|
||||
iter_close()
|
||||
except Exception:
|
||||
_cherrypy.log(traceback=True, severity=40)
|
||||
|
||||
def run(self):
|
||||
"""Create a Request object using environ."""
|
||||
env = self.environ.get
|
||||
|
||||
local = httputil.Host(
|
||||
'',
|
||||
int(env('SERVER_PORT', 80) or -1),
|
||||
env('SERVER_NAME', ''),
|
||||
)
|
||||
remote = httputil.Host(
|
||||
env('REMOTE_ADDR', ''),
|
||||
int(env('REMOTE_PORT', -1) or -1),
|
||||
env('REMOTE_HOST', ''),
|
||||
)
|
||||
scheme = env('wsgi.url_scheme')
|
||||
sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
|
||||
request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
|
||||
|
||||
# LOGON_USER is served by IIS, and is the name of the
|
||||
# user after having been mapped to a local account.
|
||||
# Both IIS and Apache set REMOTE_USER, when possible.
|
||||
request.login = env('LOGON_USER') or env('REMOTE_USER') or None
|
||||
request.multithread = self.environ['wsgi.multithread']
|
||||
request.multiprocess = self.environ['wsgi.multiprocess']
|
||||
request.wsgi_environ = self.environ
|
||||
request.prev = env('cherrypy.previous_request', None)
|
||||
|
||||
meth = self.environ['REQUEST_METHOD']
|
||||
|
||||
path = httputil.urljoin(
|
||||
self.environ.get('SCRIPT_NAME', ''),
|
||||
self.environ.get('PATH_INFO', ''),
|
||||
)
|
||||
qs = self.environ.get('QUERY_STRING', '')
|
||||
|
||||
path, qs = self.recode_path_qs(path, qs) or (path, qs)
|
||||
|
||||
rproto = self.environ.get('SERVER_PROTOCOL')
|
||||
headers = self.translate_headers(self.environ)
|
||||
rfile = self.environ['wsgi.input']
|
||||
request.run(meth, path, qs, rproto, headers, rfile)
|
||||
|
||||
headerNames = {
|
||||
'HTTP_CGI_AUTHORIZATION': 'Authorization',
|
||||
'CONTENT_LENGTH': 'Content-Length',
|
||||
'CONTENT_TYPE': 'Content-Type',
|
||||
'REMOTE_HOST': 'Remote-Host',
|
||||
'REMOTE_ADDR': 'Remote-Addr',
|
||||
}
|
||||
|
||||
def recode_path_qs(self, path, qs):
|
||||
if not six.PY3:
|
||||
return
|
||||
|
||||
# This isn't perfect; if the given PATH_INFO is in the
|
||||
# wrong encoding, it may fail to match the appropriate config
|
||||
# section URI. But meh.
|
||||
old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
|
||||
new_enc = self.cpapp.find_config(
|
||||
self.environ.get('PATH_INFO', ''),
|
||||
'request.uri_encoding', 'utf-8',
|
||||
)
|
||||
if new_enc.lower() == old_enc.lower():
|
||||
return
|
||||
|
||||
# Even though the path and qs are unicode, the WSGI server
|
||||
# is required by PEP 3333 to coerce them to ISO-8859-1
|
||||
# masquerading as unicode. So we have to encode back to
|
||||
# bytes and then decode again using the "correct" encoding.
|
||||
try:
|
||||
return (
|
||||
path.encode(old_enc).decode(new_enc),
|
||||
qs.encode(old_enc).decode(new_enc),
|
||||
)
|
||||
except (UnicodeEncodeError, UnicodeDecodeError):
|
||||
# Just pass them through without transcoding and hope.
|
||||
pass
|
||||
|
||||
def translate_headers(self, environ):
|
||||
"""Translate CGI-environ header names to HTTP header names."""
|
||||
for cgiName in environ:
|
||||
# We assume all incoming header keys are uppercase already.
|
||||
if cgiName in self.headerNames:
|
||||
yield self.headerNames[cgiName], environ[cgiName]
|
||||
elif cgiName[:5] == 'HTTP_':
|
||||
# Hackish attempt at recovering original header names.
|
||||
translatedHeader = cgiName[5:].replace('_', '-')
|
||||
yield translatedHeader, environ[cgiName]
|
||||
|
||||
|
||||
class CPWSGIApp(object):
|
||||
|
||||
"""A WSGI application object for a CherryPy Application."""
|
||||
|
||||
pipeline = [
|
||||
('ExceptionTrapper', ExceptionTrapper),
|
||||
('InternalRedirector', InternalRedirector),
|
||||
]
|
||||
"""A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
|
||||
constructor that takes an initial, positional 'nextapp' argument,
|
||||
plus optional keyword arguments, and returns a WSGI application
|
||||
(that takes environ and start_response arguments). The 'name' can
|
||||
be any you choose, and will correspond to keys in self.config."""
|
||||
|
||||
head = None
|
||||
"""Rather than nest all apps in the pipeline on each call, it's only
|
||||
done the first time, and the result is memoized into self.head. Set
|
||||
this to None again if you change self.pipeline after calling self."""
|
||||
|
||||
config = {}
|
||||
"""A dict whose keys match names listed in the pipeline. Each
|
||||
value is a further dict which will be passed to the corresponding
|
||||
named WSGI callable (from the pipeline) as keyword arguments."""
|
||||
|
||||
response_class = AppResponse
|
||||
"""The class to instantiate and return as the next app in the WSGI chain.
|
||||
"""
|
||||
|
||||
def __init__(self, cpapp, pipeline=None):
|
||||
self.cpapp = cpapp
|
||||
self.pipeline = self.pipeline[:]
|
||||
if pipeline:
|
||||
self.pipeline.extend(pipeline)
|
||||
self.config = self.config.copy()
|
||||
|
||||
def tail(self, environ, start_response):
|
||||
"""WSGI application callable for the actual CherryPy application.
|
||||
|
||||
You probably shouldn't call this; call self.__call__ instead,
|
||||
so that any WSGI middleware in self.pipeline can run first.
|
||||
"""
|
||||
return self.response_class(environ, start_response, self.cpapp)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
head = self.head
|
||||
if head is None:
|
||||
# Create and nest the WSGI apps in our pipeline (in reverse order).
|
||||
# Then memoize the result in self.head.
|
||||
head = self.tail
|
||||
for name, callable in self.pipeline[::-1]:
|
||||
conf = self.config.get(name, {})
|
||||
head = callable(head, **conf)
|
||||
self.head = head
|
||||
return head(environ, start_response)
|
||||
|
||||
def namespace_handler(self, k, v):
|
||||
"""Config handler for the 'wsgi' namespace."""
|
||||
if k == 'pipeline':
|
||||
# Note this allows multiple 'wsgi.pipeline' config entries
|
||||
# (but each entry will be processed in a 'random' order).
|
||||
# It should also allow developers to set default middleware
|
||||
# in code (passed to self.__init__) that deployers can add to
|
||||
# (but not remove) via config.
|
||||
self.pipeline.extend(v)
|
||||
elif k == 'response_class':
|
||||
self.response_class = v
|
||||
else:
|
||||
name, arg = k.split('.', 1)
|
||||
bucket = self.config.setdefault(name, {})
|
||||
bucket[arg] = v
|
|
@ -1,70 +0,0 @@
|
|||
"""WSGI server interface (see PEP 333). This adds some CP-specific bits to
|
||||
the framework-agnostic wsgiserver package.
|
||||
"""
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import wsgiserver
|
||||
|
||||
|
||||
class CPWSGIServer(wsgiserver.CherryPyWSGIServer):
|
||||
|
||||
"""Wrapper for wsgiserver.CherryPyWSGIServer.
|
||||
|
||||
wsgiserver has been designed to not reference CherryPy in any way,
|
||||
so that it can be used in other frameworks and applications. Therefore,
|
||||
we wrap it here, so we can set our own mount points from cherrypy.tree
|
||||
and apply some attributes from config -> cherrypy.server -> wsgiserver.
|
||||
"""
|
||||
|
||||
def __init__(self, server_adapter=cherrypy.server):
|
||||
self.server_adapter = server_adapter
|
||||
self.max_request_header_size = (
|
||||
self.server_adapter.max_request_header_size or 0
|
||||
)
|
||||
self.max_request_body_size = (
|
||||
self.server_adapter.max_request_body_size or 0
|
||||
)
|
||||
|
||||
server_name = (self.server_adapter.socket_host or
|
||||
self.server_adapter.socket_file or
|
||||
None)
|
||||
|
||||
self.wsgi_version = self.server_adapter.wsgi_version
|
||||
s = wsgiserver.CherryPyWSGIServer
|
||||
s.__init__(self, server_adapter.bind_addr, cherrypy.tree,
|
||||
self.server_adapter.thread_pool,
|
||||
server_name,
|
||||
max=self.server_adapter.thread_pool_max,
|
||||
request_queue_size=self.server_adapter.socket_queue_size,
|
||||
timeout=self.server_adapter.socket_timeout,
|
||||
shutdown_timeout=self.server_adapter.shutdown_timeout,
|
||||
accepted_queue_size=self.server_adapter.accepted_queue_size,
|
||||
accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
|
||||
)
|
||||
self.protocol = self.server_adapter.protocol_version
|
||||
self.nodelay = self.server_adapter.nodelay
|
||||
|
||||
if sys.version_info >= (3, 0):
|
||||
ssl_module = self.server_adapter.ssl_module or 'builtin'
|
||||
else:
|
||||
ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
|
||||
if self.server_adapter.ssl_context:
|
||||
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
|
||||
self.ssl_adapter = adapter_class(
|
||||
self.server_adapter.ssl_certificate,
|
||||
self.server_adapter.ssl_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
||||
self.ssl_adapter.context = self.server_adapter.ssl_context
|
||||
elif self.server_adapter.ssl_certificate:
|
||||
adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module)
|
||||
self.ssl_adapter = adapter_class(
|
||||
self.server_adapter.ssl_certificate,
|
||||
self.server_adapter.ssl_private_key,
|
||||
self.server_adapter.ssl_certificate_chain)
|
||||
|
||||
self.stats['Enabled'] = getattr(
|
||||
self.server_adapter, 'statistics', False)
|
||||
|
||||
def error_log(self, msg='', level=20, traceback=False):
|
||||
cherrypy.engine.log(msg, level, traceback)
|
|
@ -1,298 +0,0 @@
|
|||
"""
|
||||
Helper functions for CP apps
|
||||
"""
|
||||
|
||||
import six
|
||||
|
||||
from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode
|
||||
from cherrypy._cpcompat import text_or_bytes
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
def expose(func=None, alias=None):
|
||||
"""
|
||||
Expose the function or class, optionally providing an alias or set of aliases.
|
||||
"""
|
||||
def expose_(func):
|
||||
func.exposed = True
|
||||
if alias is not None:
|
||||
if isinstance(alias, text_or_bytes):
|
||||
parents[alias.replace('.', '_')] = func
|
||||
else:
|
||||
for a in alias:
|
||||
parents[a.replace('.', '_')] = func
|
||||
return func
|
||||
|
||||
import sys
|
||||
import types
|
||||
decoratable_types = types.FunctionType, types.MethodType, type,
|
||||
if six.PY2:
|
||||
# Old-style classes are type types.ClassType.
|
||||
decoratable_types += types.ClassType,
|
||||
if isinstance(func, decoratable_types):
|
||||
if alias is None:
|
||||
# @expose
|
||||
func.exposed = True
|
||||
return func
|
||||
else:
|
||||
# func = expose(func, alias)
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_(func)
|
||||
elif func is None:
|
||||
if alias is None:
|
||||
# @expose()
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_
|
||||
else:
|
||||
# @expose(alias="alias") or
|
||||
# @expose(alias=["alias1", "alias2"])
|
||||
parents = sys._getframe(1).f_locals
|
||||
return expose_
|
||||
else:
|
||||
# @expose("alias") or
|
||||
# @expose(["alias1", "alias2"])
|
||||
parents = sys._getframe(1).f_locals
|
||||
alias = func
|
||||
return expose_
|
||||
|
||||
|
||||
def popargs(*args, **kwargs):
|
||||
"""A decorator for _cp_dispatch
|
||||
(cherrypy.dispatch.Dispatcher.dispatch_method_name).
|
||||
|
||||
Optional keyword argument: handler=(Object or Function)
|
||||
|
||||
Provides a _cp_dispatch function that pops off path segments into
|
||||
cherrypy.request.params under the names specified. The dispatch
|
||||
is then forwarded on to the next vpath element.
|
||||
|
||||
Note that any existing (and exposed) member function of the class that
|
||||
popargs is applied to will override that value of the argument. For
|
||||
instance, if you have a method named "list" on the class decorated with
|
||||
popargs, then accessing "/list" will call that function instead of popping
|
||||
it off as the requested parameter. This restriction applies to all
|
||||
_cp_dispatch functions. The only way around this restriction is to create
|
||||
a "blank class" whose only function is to provide _cp_dispatch.
|
||||
|
||||
If there are path elements after the arguments, or more arguments
|
||||
are requested than are available in the vpath, then the 'handler'
|
||||
keyword argument specifies the next object to handle the parameterized
|
||||
request. If handler is not specified or is None, then self is used.
|
||||
If handler is a function rather than an instance, then that function
|
||||
will be called with the args specified and the return value from that
|
||||
function used as the next object INSTEAD of adding the parameters to
|
||||
cherrypy.request.args.
|
||||
|
||||
This decorator may be used in one of two ways:
|
||||
|
||||
As a class decorator:
|
||||
@cherrypy.popargs('year', 'month', 'day')
|
||||
class Blog:
|
||||
def index(self, year=None, month=None, day=None):
|
||||
#Process the parameters here; any url like
|
||||
#/, /2009, /2009/12, or /2009/12/31
|
||||
#will fill in the appropriate parameters.
|
||||
|
||||
def create(self):
|
||||
#This link will still be available at /create. Defined functions
|
||||
#take precedence over arguments.
|
||||
|
||||
Or as a member of a class:
|
||||
class Blog:
|
||||
_cp_dispatch = cherrypy.popargs('year', 'month', 'day')
|
||||
#...
|
||||
|
||||
The handler argument may be used to mix arguments with built in functions.
|
||||
For instance, the following setup allows different activities at the
|
||||
day, month, and year level:
|
||||
|
||||
class DayHandler:
|
||||
def index(self, year, month, day):
|
||||
#Do something with this day; probably list entries
|
||||
|
||||
def delete(self, year, month, day):
|
||||
#Delete all entries for this day
|
||||
|
||||
@cherrypy.popargs('day', handler=DayHandler())
|
||||
class MonthHandler:
|
||||
def index(self, year, month):
|
||||
#Do something with this month; probably list entries
|
||||
|
||||
def delete(self, year, month):
|
||||
#Delete all entries for this month
|
||||
|
||||
@cherrypy.popargs('month', handler=MonthHandler())
|
||||
class YearHandler:
|
||||
def index(self, year):
|
||||
#Do something with this year
|
||||
|
||||
#...
|
||||
|
||||
@cherrypy.popargs('year', handler=YearHandler())
|
||||
class Root:
|
||||
def index(self):
|
||||
#...
|
||||
|
||||
"""
|
||||
|
||||
# Since keyword arg comes after *args, we have to process it ourselves
|
||||
# for lower versions of python.
|
||||
|
||||
handler = None
|
||||
handler_call = False
|
||||
for k, v in kwargs.items():
|
||||
if k == 'handler':
|
||||
handler = v
|
||||
else:
|
||||
raise TypeError(
|
||||
"cherrypy.popargs() got an unexpected keyword argument '{0}'"
|
||||
.format(k)
|
||||
)
|
||||
|
||||
import inspect
|
||||
|
||||
if handler is not None \
|
||||
and (hasattr(handler, '__call__') or inspect.isclass(handler)):
|
||||
handler_call = True
|
||||
|
||||
def decorated(cls_or_self=None, vpath=None):
|
||||
if inspect.isclass(cls_or_self):
|
||||
# cherrypy.popargs is a class decorator
|
||||
cls = cls_or_self
|
||||
setattr(cls, cherrypy.dispatch.Dispatcher.dispatch_method_name, decorated)
|
||||
return cls
|
||||
|
||||
# We're in the actual function
|
||||
self = cls_or_self
|
||||
parms = {}
|
||||
for arg in args:
|
||||
if not vpath:
|
||||
break
|
||||
parms[arg] = vpath.pop(0)
|
||||
|
||||
if handler is not None:
|
||||
if handler_call:
|
||||
return handler(**parms)
|
||||
else:
|
||||
cherrypy.request.params.update(parms)
|
||||
return handler
|
||||
|
||||
cherrypy.request.params.update(parms)
|
||||
|
||||
# If we are the ultimate handler, then to prevent our _cp_dispatch
|
||||
# from being called again, we will resolve remaining elements through
|
||||
# getattr() directly.
|
||||
if vpath:
|
||||
return getattr(self, vpath.pop(0), None)
|
||||
else:
|
||||
return self
|
||||
|
||||
return decorated
|
||||
|
||||
|
||||
def url(path='', qs='', script_name=None, base=None, relative=None):
|
||||
"""Create an absolute URL for the given path.
|
||||
|
||||
If 'path' starts with a slash ('/'), this will return
|
||||
(base + script_name + path + qs).
|
||||
If it does not start with a slash, this returns
|
||||
(base + script_name [+ request.path_info] + path + qs).
|
||||
|
||||
If script_name is None, cherrypy.request will be used
|
||||
to find a script_name, if available.
|
||||
|
||||
If base is None, cherrypy.request.base will be used (if available).
|
||||
Note that you can use cherrypy.tools.proxy to change this.
|
||||
|
||||
Finally, note that this function can be used to obtain an absolute URL
|
||||
for the current request path (minus the querystring) by passing no args.
|
||||
If you call url(qs=cherrypy.request.query_string), you should get the
|
||||
original browser URL (assuming no internal redirections).
|
||||
|
||||
If relative is None or not provided, request.app.relative_urls will
|
||||
be used (if available, else False). If False, the output will be an
|
||||
absolute URL (including the scheme, host, vhost, and script_name).
|
||||
If True, the output will instead be a URL that is relative to the
|
||||
current request path, perhaps including '..' atoms. If relative is
|
||||
the string 'server', the output will instead be a URL that is
|
||||
relative to the server root; i.e., it will start with a slash.
|
||||
"""
|
||||
if isinstance(qs, (tuple, list, dict)):
|
||||
qs = _urlencode(qs)
|
||||
if qs:
|
||||
qs = '?' + qs
|
||||
|
||||
if cherrypy.request.app:
|
||||
if not path.startswith('/'):
|
||||
# Append/remove trailing slash from path_info as needed
|
||||
# (this is to support mistyped URL's without redirecting;
|
||||
# if you want to redirect, use tools.trailing_slash).
|
||||
pi = cherrypy.request.path_info
|
||||
if cherrypy.request.is_index is True:
|
||||
if not pi.endswith('/'):
|
||||
pi = pi + '/'
|
||||
elif cherrypy.request.is_index is False:
|
||||
if pi.endswith('/') and pi != '/':
|
||||
pi = pi[:-1]
|
||||
|
||||
if path == '':
|
||||
path = pi
|
||||
else:
|
||||
path = _urljoin(pi, path)
|
||||
|
||||
if script_name is None:
|
||||
script_name = cherrypy.request.script_name
|
||||
if base is None:
|
||||
base = cherrypy.request.base
|
||||
|
||||
newurl = base + script_name + path + qs
|
||||
else:
|
||||
# No request.app (we're being called outside a request).
|
||||
# We'll have to guess the base from server.* attributes.
|
||||
# This will produce very different results from the above
|
||||
# if you're using vhosts or tools.proxy.
|
||||
if base is None:
|
||||
base = cherrypy.server.base()
|
||||
|
||||
path = (script_name or '') + path
|
||||
newurl = base + path + qs
|
||||
|
||||
if './' in newurl:
|
||||
# Normalize the URL by removing ./ and ../
|
||||
atoms = []
|
||||
for atom in newurl.split('/'):
|
||||
if atom == '.':
|
||||
pass
|
||||
elif atom == '..':
|
||||
atoms.pop()
|
||||
else:
|
||||
atoms.append(atom)
|
||||
newurl = '/'.join(atoms)
|
||||
|
||||
# At this point, we should have a fully-qualified absolute URL.
|
||||
|
||||
if relative is None:
|
||||
relative = getattr(cherrypy.request.app, 'relative_urls', False)
|
||||
|
||||
# See http://www.ietf.org/rfc/rfc2396.txt
|
||||
if relative == 'server':
|
||||
# "A relative reference beginning with a single slash character is
|
||||
# termed an absolute-path reference, as defined by <abs_path>..."
|
||||
# This is also sometimes called "server-relative".
|
||||
newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
|
||||
elif relative:
|
||||
# "A relative reference that does not begin with a scheme name
|
||||
# or a slash character is termed a relative-path reference."
|
||||
old = url(relative=False).split('/')[:-1]
|
||||
new = newurl.split('/')
|
||||
while old and new:
|
||||
a, b = old[0], new[0]
|
||||
if a != b:
|
||||
break
|
||||
old.pop(0)
|
||||
new.pop(0)
|
||||
new = (['..'] * len(old)) + new
|
||||
newurl = '/'.join(new)
|
||||
|
||||
return newurl
|
|
@ -1,106 +0,0 @@
|
|||
"""The CherryPy daemon."""
|
||||
|
||||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.process import plugins, servers
|
||||
from cherrypy import Application
|
||||
|
||||
|
||||
def start(configfiles=None, daemonize=False, environment=None,
|
||||
fastcgi=False, scgi=False, pidfile=None, imports=None,
|
||||
cgi=False):
|
||||
"""Subscribe all engine plugins and start the engine."""
|
||||
sys.path = [''] + sys.path
|
||||
for i in imports or []:
|
||||
exec('import %s' % i)
|
||||
|
||||
for c in configfiles or []:
|
||||
cherrypy.config.update(c)
|
||||
# If there's only one app mounted, merge config into it.
|
||||
if len(cherrypy.tree.apps) == 1:
|
||||
for app in cherrypy.tree.apps.values():
|
||||
if isinstance(app, Application):
|
||||
app.merge(c)
|
||||
|
||||
engine = cherrypy.engine
|
||||
|
||||
if environment is not None:
|
||||
cherrypy.config.update({'environment': environment})
|
||||
|
||||
# Only daemonize if asked to.
|
||||
if daemonize:
|
||||
# Don't print anything to stdout/sterr.
|
||||
cherrypy.config.update({'log.screen': False})
|
||||
plugins.Daemonizer(engine).subscribe()
|
||||
|
||||
if pidfile:
|
||||
plugins.PIDFile(engine, pidfile).subscribe()
|
||||
|
||||
if hasattr(engine, 'signal_handler'):
|
||||
engine.signal_handler.subscribe()
|
||||
if hasattr(engine, 'console_control_handler'):
|
||||
engine.console_control_handler.subscribe()
|
||||
|
||||
if (fastcgi and (scgi or cgi)) or (scgi and cgi):
|
||||
cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
|
||||
'scgi options.', 'ENGINE')
|
||||
sys.exit(1)
|
||||
elif fastcgi or scgi or cgi:
|
||||
# Turn off autoreload when using *cgi.
|
||||
cherrypy.config.update({'engine.autoreload.on': False})
|
||||
# Turn off the default HTTP server (which is subscribed by default).
|
||||
cherrypy.server.unsubscribe()
|
||||
|
||||
addr = cherrypy.server.bind_addr
|
||||
cls = (
|
||||
servers.FlupFCGIServer if fastcgi else
|
||||
servers.FlupSCGIServer if scgi else
|
||||
servers.FlupCGIServer
|
||||
)
|
||||
f = cls(application=cherrypy.tree, bindAddress=addr)
|
||||
s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
|
||||
s.subscribe()
|
||||
|
||||
# Always start the engine; this will start all other services
|
||||
try:
|
||||
engine.start()
|
||||
except:
|
||||
# Assume the error has been logged already via bus.log.
|
||||
sys.exit(1)
|
||||
else:
|
||||
engine.block()
|
||||
|
||||
|
||||
def run():
|
||||
from optparse import OptionParser
|
||||
|
||||
p = OptionParser()
|
||||
p.add_option('-c', '--config', action='append', dest='config',
|
||||
help='specify config file(s)')
|
||||
p.add_option('-d', action='store_true', dest='daemonize',
|
||||
help='run the server as a daemon')
|
||||
p.add_option('-e', '--environment', dest='environment', default=None,
|
||||
help='apply the given config environment')
|
||||
p.add_option('-f', action='store_true', dest='fastcgi',
|
||||
help='start a fastcgi server instead of the default HTTP '
|
||||
'server')
|
||||
p.add_option('-s', action='store_true', dest='scgi',
|
||||
help='start a scgi server instead of the default HTTP server')
|
||||
p.add_option('-x', action='store_true', dest='cgi',
|
||||
help='start a cgi server instead of the default HTTP server')
|
||||
p.add_option('-i', '--import', action='append', dest='imports',
|
||||
help='specify modules to import')
|
||||
p.add_option('-p', '--pidfile', dest='pidfile', default=None,
|
||||
help='store the process id in the given file')
|
||||
p.add_option('-P', '--Path', action='append', dest='Path',
|
||||
help='add the given paths to sys.path')
|
||||
options, args = p.parse_args()
|
||||
|
||||
if options.Path:
|
||||
for p in options.Path:
|
||||
sys.path.insert(0, p)
|
||||
|
||||
start(options.config, options.daemonize,
|
||||
options.environment, options.fastcgi, options.scgi,
|
||||
options.pidfile, options.imports, options.cgi)
|
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,85 +0,0 @@
|
|||
"""CherryPy Library"""
|
||||
|
||||
|
||||
def is_iterator(obj):
|
||||
'''Returns a boolean indicating if the object provided implements
|
||||
the iterator protocol (i.e. like a generator). This will return
|
||||
false for objects which iterable, but not iterators themselves.'''
|
||||
from types import GeneratorType
|
||||
if isinstance(obj, GeneratorType):
|
||||
return True
|
||||
elif not hasattr(obj, '__iter__'):
|
||||
return False
|
||||
else:
|
||||
# Types which implement the protocol must return themselves when
|
||||
# invoking 'iter' upon them.
|
||||
return iter(obj) is obj
|
||||
|
||||
|
||||
def is_closable_iterator(obj):
|
||||
|
||||
# Not an iterator.
|
||||
if not is_iterator(obj):
|
||||
return False
|
||||
|
||||
# A generator - the easiest thing to deal with.
|
||||
import inspect
|
||||
if inspect.isgenerator(obj):
|
||||
return True
|
||||
|
||||
# A custom iterator. Look for a close method...
|
||||
if not (hasattr(obj, 'close') and callable(obj.close)):
|
||||
return False
|
||||
|
||||
# ... which doesn't require any arguments.
|
||||
try:
|
||||
inspect.getcallargs(obj.close)
|
||||
except TypeError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
|
||||
class file_generator(object):
|
||||
|
||||
"""Yield the given input (a file object) in chunks (default 64k). (Core)"""
|
||||
|
||||
def __init__(self, input, chunkSize=65536):
|
||||
self.input = input
|
||||
self.chunkSize = chunkSize
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
chunk = self.input.read(self.chunkSize)
|
||||
if chunk:
|
||||
return chunk
|
||||
else:
|
||||
if hasattr(self.input, 'close'):
|
||||
self.input.close()
|
||||
raise StopIteration()
|
||||
next = __next__
|
||||
|
||||
|
||||
def file_generator_limited(fileobj, count, chunk_size=65536):
|
||||
"""Yield the given file object in chunks, stopping after `count`
|
||||
bytes has been emitted. Default chunk size is 64kB. (Core)
|
||||
"""
|
||||
remaining = count
|
||||
while remaining > 0:
|
||||
chunk = fileobj.read(min(chunk_size, remaining))
|
||||
chunklen = len(chunk)
|
||||
if chunklen == 0:
|
||||
return
|
||||
remaining -= chunklen
|
||||
yield chunk
|
||||
|
||||
|
||||
def set_vary_header(response, header_name):
|
||||
'Add a Vary header to a response'
|
||||
varies = response.headers.get('Vary', '')
|
||||
varies = [x.strip() for x in varies.split(',') if x.strip()]
|
||||
if header_name not in varies:
|
||||
varies.append(header_name)
|
||||
response.headers['Vary'] = ', '.join(varies)
|
|
@ -1,97 +0,0 @@
|
|||
import cherrypy
|
||||
from cherrypy.lib import httpauth
|
||||
|
||||
|
||||
def check_auth(users, encrypt=None, realm=None):
|
||||
"""If an authorization header contains credentials, return True or False.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if 'authorization' in request.headers:
|
||||
# make sure the provided credentials are correctly set
|
||||
ah = httpauth.parseAuthorization(request.headers['authorization'])
|
||||
if ah is None:
|
||||
raise cherrypy.HTTPError(400, 'Bad Request')
|
||||
|
||||
if not encrypt:
|
||||
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
|
||||
|
||||
if hasattr(users, '__call__'):
|
||||
try:
|
||||
# backward compatibility
|
||||
users = users() # expect it to return a dictionary
|
||||
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError(
|
||||
'Authentication users must be a dictionary')
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah['username'], None)
|
||||
except TypeError:
|
||||
# returns a password (encrypted or clear text)
|
||||
password = users(ah['username'])
|
||||
else:
|
||||
if not isinstance(users, dict):
|
||||
raise ValueError('Authentication users must be a dictionary')
|
||||
|
||||
# fetch the user password
|
||||
password = users.get(ah['username'], None)
|
||||
|
||||
# validate the authorization by re-computing it here
|
||||
# and compare it with what the user-agent provided
|
||||
if httpauth.checkResponse(ah, password, method=request.method,
|
||||
encrypt=encrypt, realm=realm):
|
||||
request.login = ah['username']
|
||||
return True
|
||||
|
||||
request.login = False
|
||||
return False
|
||||
|
||||
|
||||
def basic_auth(realm, users, encrypt=None, debug=False):
|
||||
"""If auth fails, raise 401 with a basic authentication header.
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
|
||||
users
|
||||
A dict of the form: {username: password} or a callable returning
|
||||
a dict.
|
||||
|
||||
encrypt
|
||||
callable used to encrypt the password returned from the user-agent.
|
||||
if None it defaults to a md5 encryption.
|
||||
|
||||
"""
|
||||
if check_auth(users, encrypt):
|
||||
if debug:
|
||||
cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH')
|
||||
return
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.serving.response.headers[
|
||||
'www-authenticate'] = httpauth.basicAuth(realm)
|
||||
|
||||
raise cherrypy.HTTPError(
|
||||
401, 'You are not authorized to access that resource')
|
||||
|
||||
|
||||
def digest_auth(realm, users, debug=False):
|
||||
"""If auth fails, raise 401 with a digest authentication header.
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
users
|
||||
A dict of the form: {username: password} or a callable returning
|
||||
a dict.
|
||||
"""
|
||||
if check_auth(users, realm=realm):
|
||||
if debug:
|
||||
cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH')
|
||||
return
|
||||
|
||||
# inform the user-agent this path is protected
|
||||
cherrypy.serving.response.headers[
|
||||
'www-authenticate'] = httpauth.digestAuth(realm)
|
||||
|
||||
raise cherrypy.HTTPError(
|
||||
401, 'You are not authorized to access that resource')
|
|
@ -1,90 +0,0 @@
|
|||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
import binascii
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import base64_decode
|
||||
|
||||
|
||||
__doc__ = """This module provides a CherryPy 3.x tool which implements
|
||||
the server-side of HTTP Basic Access Authentication, as described in
|
||||
:rfc:`2617`.
|
||||
|
||||
Example usage, using the built-in checkpassword_dict function which uses a dict
|
||||
as the credentials store::
|
||||
|
||||
userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'}
|
||||
checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict)
|
||||
basic_auth = {'tools.auth_basic.on': True,
|
||||
'tools.auth_basic.realm': 'earth',
|
||||
'tools.auth_basic.checkpassword': checkpassword,
|
||||
}
|
||||
app_config = { '/' : basic_auth }
|
||||
|
||||
"""
|
||||
|
||||
__author__ = 'visteya'
|
||||
__date__ = 'April 2009'
|
||||
|
||||
|
||||
def checkpassword_dict(user_password_dict):
|
||||
"""Returns a checkpassword function which checks credentials
|
||||
against a dictionary of the form: {username : password}.
|
||||
|
||||
If you want a simple dictionary-based authentication scheme, use
|
||||
checkpassword_dict(my_credentials_dict) as the value for the
|
||||
checkpassword argument to basic_auth().
|
||||
"""
|
||||
def checkpassword(realm, user, password):
|
||||
p = user_password_dict.get(user)
|
||||
return p and p == password or False
|
||||
|
||||
return checkpassword
|
||||
|
||||
|
||||
def basic_auth(realm, checkpassword, debug=False):
|
||||
"""A CherryPy tool which hooks at before_handler to perform
|
||||
HTTP Basic Access Authentication, as specified in :rfc:`2617`.
|
||||
|
||||
If the request has an 'authorization' header with a 'Basic' scheme, this
|
||||
tool attempts to authenticate the credentials supplied in that header. If
|
||||
the request has no 'authorization' header, or if it does but the scheme is
|
||||
not 'Basic', or if authentication fails, the tool sends a 401 response with
|
||||
a 'WWW-Authenticate' Basic header.
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
|
||||
checkpassword
|
||||
A callable which checks the authentication credentials.
|
||||
Its signature is checkpassword(realm, username, password). where
|
||||
username and password are the values obtained from the request's
|
||||
'authorization' header. If authentication succeeds, checkpassword
|
||||
returns True, else it returns False.
|
||||
|
||||
"""
|
||||
|
||||
if '"' in realm:
|
||||
raise ValueError('Realm cannot contain the " (quote) character.')
|
||||
request = cherrypy.serving.request
|
||||
|
||||
auth_header = request.headers.get('authorization')
|
||||
if auth_header is not None:
|
||||
# split() error, base64.decodestring() error
|
||||
with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, 'Bad Request'):
|
||||
scheme, params = auth_header.split(' ', 1)
|
||||
if scheme.lower() == 'basic':
|
||||
username, password = base64_decode(params).split(':', 1)
|
||||
if checkpassword(realm, username, password):
|
||||
if debug:
|
||||
cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
|
||||
request.login = username
|
||||
return # successful authentication
|
||||
|
||||
# Respond with 401 status and a WWW-Authenticate header
|
||||
cherrypy.serving.response.headers[
|
||||
'www-authenticate'] = 'Basic realm="%s"' % realm
|
||||
raise cherrypy.HTTPError(
|
||||
401, 'You are not authorized to access that resource')
|
|
@ -1,390 +0,0 @@
|
|||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
import time
|
||||
from hashlib import md5
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, parse_http_list, parse_keqv_list
|
||||
|
||||
|
||||
__doc__ = """An implementation of the server-side of HTTP Digest Access
|
||||
Authentication, which is described in :rfc:`2617`.
|
||||
|
||||
Example usage, using the built-in get_ha1_dict_plain function which uses a dict
|
||||
of plaintext passwords as the credentials store::
|
||||
|
||||
userpassdict = {'alice' : '4x5istwelve'}
|
||||
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict)
|
||||
digest_auth = {'tools.auth_digest.on': True,
|
||||
'tools.auth_digest.realm': 'wonderland',
|
||||
'tools.auth_digest.get_ha1': get_ha1,
|
||||
'tools.auth_digest.key': 'a565c27146791cfb',
|
||||
}
|
||||
app_config = { '/' : digest_auth }
|
||||
"""
|
||||
|
||||
__author__ = 'visteya'
|
||||
__date__ = 'April 2009'
|
||||
|
||||
|
||||
md5_hex = lambda s: md5(ntob(s)).hexdigest()
|
||||
|
||||
qop_auth = 'auth'
|
||||
qop_auth_int = 'auth-int'
|
||||
valid_qops = (qop_auth, qop_auth_int)
|
||||
|
||||
valid_algorithms = ('MD5', 'MD5-sess')
|
||||
|
||||
|
||||
def TRACE(msg):
|
||||
cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
|
||||
|
||||
# Three helper functions for users of the tool, providing three variants
|
||||
# of get_ha1() functions for three different kinds of credential stores.
|
||||
|
||||
|
||||
def get_ha1_dict_plain(user_password_dict):
|
||||
"""Returns a get_ha1 function which obtains a plaintext password from a
|
||||
dictionary of the form: {username : password}.
|
||||
|
||||
If you want a simple dictionary-based authentication scheme, with plaintext
|
||||
passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
|
||||
get_ha1 argument to digest_auth().
|
||||
"""
|
||||
def get_ha1(realm, username):
|
||||
password = user_password_dict.get(username)
|
||||
if password:
|
||||
return md5_hex('%s:%s:%s' % (username, realm, password))
|
||||
return None
|
||||
|
||||
return get_ha1
|
||||
|
||||
|
||||
def get_ha1_dict(user_ha1_dict):
|
||||
"""Returns a get_ha1 function which obtains a HA1 password hash from a
|
||||
dictionary of the form: {username : HA1}.
|
||||
|
||||
If you want a dictionary-based authentication scheme, but with
|
||||
pre-computed HA1 hashes instead of plain-text passwords, use
|
||||
get_ha1_dict(my_userha1_dict) as the value for the get_ha1
|
||||
argument to digest_auth().
|
||||
"""
|
||||
def get_ha1(realm, username):
|
||||
return user_ha1_dict.get(username)
|
||||
|
||||
return get_ha1
|
||||
|
||||
|
||||
def get_ha1_file_htdigest(filename):
|
||||
"""Returns a get_ha1 function which obtains a HA1 password hash from a
|
||||
flat file with lines of the same format as that produced by the Apache
|
||||
htdigest utility. For example, for realm 'wonderland', username 'alice',
|
||||
and password '4x5istwelve', the htdigest line would be::
|
||||
|
||||
alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c
|
||||
|
||||
If you want to use an Apache htdigest file as the credentials store,
|
||||
then use get_ha1_file_htdigest(my_htdigest_file) as the value for the
|
||||
get_ha1 argument to digest_auth(). It is recommended that the filename
|
||||
argument be an absolute path, to avoid problems.
|
||||
"""
|
||||
def get_ha1(realm, username):
|
||||
result = None
|
||||
f = open(filename, 'r')
|
||||
for line in f:
|
||||
u, r, ha1 = line.rstrip().split(':')
|
||||
if u == username and r == realm:
|
||||
result = ha1
|
||||
break
|
||||
f.close()
|
||||
return result
|
||||
|
||||
return get_ha1
|
||||
|
||||
|
||||
def synthesize_nonce(s, key, timestamp=None):
|
||||
"""Synthesize a nonce value which resists spoofing and can be checked
|
||||
for staleness. Returns a string suitable as the value for 'nonce' in
|
||||
the www-authenticate header.
|
||||
|
||||
s
|
||||
A string related to the resource, such as the hostname of the server.
|
||||
|
||||
key
|
||||
A secret string known only to the server.
|
||||
|
||||
timestamp
|
||||
An integer seconds-since-the-epoch timestamp
|
||||
|
||||
"""
|
||||
if timestamp is None:
|
||||
timestamp = int(time.time())
|
||||
h = md5_hex('%s:%s:%s' % (timestamp, s, key))
|
||||
nonce = '%s:%s' % (timestamp, h)
|
||||
return nonce
|
||||
|
||||
|
||||
def H(s):
|
||||
"""The hash function H"""
|
||||
return md5_hex(s)
|
||||
|
||||
|
||||
class HttpDigestAuthorization (object):
|
||||
|
||||
"""Class to parse a Digest Authorization header and perform re-calculation
|
||||
of the digest.
|
||||
"""
|
||||
|
||||
def errmsg(self, s):
|
||||
return 'Digest Authorization header: %s' % s
|
||||
|
||||
def __init__(self, auth_header, http_method, debug=False):
|
||||
self.http_method = http_method
|
||||
self.debug = debug
|
||||
scheme, params = auth_header.split(' ', 1)
|
||||
self.scheme = scheme.lower()
|
||||
if self.scheme != 'digest':
|
||||
raise ValueError('Authorization scheme is not "Digest"')
|
||||
|
||||
self.auth_header = auth_header
|
||||
|
||||
# make a dict of the params
|
||||
items = parse_http_list(params)
|
||||
paramsd = parse_keqv_list(items)
|
||||
|
||||
self.realm = paramsd.get('realm')
|
||||
self.username = paramsd.get('username')
|
||||
self.nonce = paramsd.get('nonce')
|
||||
self.uri = paramsd.get('uri')
|
||||
self.method = paramsd.get('method')
|
||||
self.response = paramsd.get('response') # the response digest
|
||||
self.algorithm = paramsd.get('algorithm', 'MD5').upper()
|
||||
self.cnonce = paramsd.get('cnonce')
|
||||
self.opaque = paramsd.get('opaque')
|
||||
self.qop = paramsd.get('qop') # qop
|
||||
self.nc = paramsd.get('nc') # nonce count
|
||||
|
||||
# perform some correctness checks
|
||||
if self.algorithm not in valid_algorithms:
|
||||
raise ValueError(
|
||||
self.errmsg("Unsupported value for algorithm: '%s'" %
|
||||
self.algorithm))
|
||||
|
||||
has_reqd = (
|
||||
self.username and
|
||||
self.realm and
|
||||
self.nonce and
|
||||
self.uri and
|
||||
self.response
|
||||
)
|
||||
if not has_reqd:
|
||||
raise ValueError(
|
||||
self.errmsg('Not all required parameters are present.'))
|
||||
|
||||
if self.qop:
|
||||
if self.qop not in valid_qops:
|
||||
raise ValueError(
|
||||
self.errmsg("Unsupported value for qop: '%s'" % self.qop))
|
||||
if not (self.cnonce and self.nc):
|
||||
raise ValueError(
|
||||
self.errmsg('If qop is sent then '
|
||||
'cnonce and nc MUST be present'))
|
||||
else:
|
||||
if self.cnonce or self.nc:
|
||||
raise ValueError(
|
||||
self.errmsg('If qop is not sent, '
|
||||
'neither cnonce nor nc can be present'))
|
||||
|
||||
def __str__(self):
|
||||
return 'authorization : %s' % self.auth_header
|
||||
|
||||
def validate_nonce(self, s, key):
|
||||
"""Validate the nonce.
|
||||
Returns True if nonce was generated by synthesize_nonce() and the
|
||||
timestamp is not spoofed, else returns False.
|
||||
|
||||
s
|
||||
A string related to the resource, such as the hostname of
|
||||
the server.
|
||||
|
||||
key
|
||||
A secret string known only to the server.
|
||||
|
||||
Both s and key must be the same values which were used to synthesize
|
||||
the nonce we are trying to validate.
|
||||
"""
|
||||
try:
|
||||
timestamp, hashpart = self.nonce.split(':', 1)
|
||||
s_timestamp, s_hashpart = synthesize_nonce(
|
||||
s, key, timestamp).split(':', 1)
|
||||
is_valid = s_hashpart == hashpart
|
||||
if self.debug:
|
||||
TRACE('validate_nonce: %s' % is_valid)
|
||||
return is_valid
|
||||
except ValueError: # split() error
|
||||
pass
|
||||
return False
|
||||
|
||||
def is_nonce_stale(self, max_age_seconds=600):
|
||||
"""Returns True if a validated nonce is stale. The nonce contains a
|
||||
timestamp in plaintext and also a secure hash of the timestamp.
|
||||
You should first validate the nonce to ensure the plaintext
|
||||
timestamp is not spoofed.
|
||||
"""
|
||||
try:
|
||||
timestamp, hashpart = self.nonce.split(':', 1)
|
||||
if int(timestamp) + max_age_seconds > int(time.time()):
|
||||
return False
|
||||
except ValueError: # int() error
|
||||
pass
|
||||
if self.debug:
|
||||
TRACE('nonce is stale')
|
||||
return True
|
||||
|
||||
def HA2(self, entity_body=''):
|
||||
"""Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
|
||||
# RFC 2617 3.2.2.3
|
||||
# If the "qop" directive's value is "auth" or is unspecified,
|
||||
# then A2 is:
|
||||
# A2 = method ":" digest-uri-value
|
||||
#
|
||||
# If the "qop" value is "auth-int", then A2 is:
|
||||
# A2 = method ":" digest-uri-value ":" H(entity-body)
|
||||
if self.qop is None or self.qop == 'auth':
|
||||
a2 = '%s:%s' % (self.http_method, self.uri)
|
||||
elif self.qop == 'auth-int':
|
||||
a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
|
||||
else:
|
||||
# in theory, this should never happen, since I validate qop in
|
||||
# __init__()
|
||||
raise ValueError(self.errmsg('Unrecognized value for qop!'))
|
||||
return H(a2)
|
||||
|
||||
def request_digest(self, ha1, entity_body=''):
|
||||
"""Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
|
||||
|
||||
ha1
|
||||
The HA1 string obtained from the credentials store.
|
||||
|
||||
entity_body
|
||||
If 'qop' is set to 'auth-int', then A2 includes a hash
|
||||
of the "entity body". The entity body is the part of the
|
||||
message which follows the HTTP headers. See :rfc:`2617` section
|
||||
4.3. This refers to the entity the user agent sent in the
|
||||
request which has the Authorization header. Typically GET
|
||||
requests don't have an entity, and POST requests do.
|
||||
|
||||
"""
|
||||
ha2 = self.HA2(entity_body)
|
||||
# Request-Digest -- RFC 2617 3.2.2.1
|
||||
if self.qop:
|
||||
req = '%s:%s:%s:%s:%s' % (
|
||||
self.nonce, self.nc, self.cnonce, self.qop, ha2)
|
||||
else:
|
||||
req = '%s:%s' % (self.nonce, ha2)
|
||||
|
||||
# RFC 2617 3.2.2.2
|
||||
#
|
||||
# If the "algorithm" directive's value is "MD5" or is unspecified,
|
||||
# then A1 is:
|
||||
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
|
||||
#
|
||||
# If the "algorithm" directive's value is "MD5-sess", then A1 is
|
||||
# calculated only once - on the first request by the client following
|
||||
# receipt of a WWW-Authenticate challenge from the server.
|
||||
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
|
||||
# ":" unq(nonce-value) ":" unq(cnonce-value)
|
||||
if self.algorithm == 'MD5-sess':
|
||||
ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce))
|
||||
|
||||
digest = H('%s:%s' % (ha1, req))
|
||||
return digest
|
||||
|
||||
|
||||
def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
|
||||
stale=False):
|
||||
"""Constructs a WWW-Authenticate header for Digest authentication."""
|
||||
if qop not in valid_qops:
|
||||
raise ValueError("Unsupported value for qop: '%s'" % qop)
|
||||
if algorithm not in valid_algorithms:
|
||||
raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
|
||||
|
||||
if nonce is None:
|
||||
nonce = synthesize_nonce(realm, key)
|
||||
s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
|
||||
realm, nonce, algorithm, qop)
|
||||
if stale:
|
||||
s += ', stale="true"'
|
||||
return s
|
||||
|
||||
|
||||
def digest_auth(realm, get_ha1, key, debug=False):
|
||||
"""A CherryPy tool which hooks at before_handler to perform
|
||||
HTTP Digest Access Authentication, as specified in :rfc:`2617`.
|
||||
|
||||
If the request has an 'authorization' header with a 'Digest' scheme,
|
||||
this tool authenticates the credentials supplied in that header.
|
||||
If the request has no 'authorization' header, or if it does but the
|
||||
scheme is not "Digest", or if authentication fails, the tool sends
|
||||
a 401 response with a 'WWW-Authenticate' Digest header.
|
||||
|
||||
realm
|
||||
A string containing the authentication realm.
|
||||
|
||||
get_ha1
|
||||
A callable which looks up a username in a credentials store
|
||||
and returns the HA1 string, which is defined in the RFC to be
|
||||
MD5(username : realm : password). The function's signature is:
|
||||
``get_ha1(realm, username)``
|
||||
where username is obtained from the request's 'authorization' header.
|
||||
If username is not found in the credentials store, get_ha1() returns
|
||||
None.
|
||||
|
||||
key
|
||||
A secret string known only to the server, used in the synthesis
|
||||
of nonces.
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
auth_header = request.headers.get('authorization')
|
||||
nonce_is_stale = False
|
||||
if auth_header is not None:
|
||||
with cherrypy.HTTPError.handle(ValueError, 400,
|
||||
'The Authorization header could not be parsed.'):
|
||||
auth = HttpDigestAuthorization(
|
||||
auth_header, request.method, debug=debug)
|
||||
|
||||
if debug:
|
||||
TRACE(str(auth))
|
||||
|
||||
if auth.validate_nonce(realm, key):
|
||||
ha1 = get_ha1(realm, auth.username)
|
||||
if ha1 is not None:
|
||||
# note that for request.body to be available we need to
|
||||
# hook in at before_handler, not on_start_resource like
|
||||
# 3.1.x digest_auth does.
|
||||
digest = auth.request_digest(ha1, entity_body=request.body)
|
||||
if digest == auth.response: # authenticated
|
||||
if debug:
|
||||
TRACE('digest matches auth.response')
|
||||
# Now check if nonce is stale.
|
||||
# The choice of ten minutes' lifetime for nonce is somewhat
|
||||
# arbitrary
|
||||
nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600)
|
||||
if not nonce_is_stale:
|
||||
request.login = auth.username
|
||||
if debug:
|
||||
TRACE('authentication of %s successful' %
|
||||
auth.username)
|
||||
return
|
||||
|
||||
# Respond with 401 status and a WWW-Authenticate header
|
||||
header = www_authenticate(realm, key, stale=nonce_is_stale)
|
||||
if debug:
|
||||
TRACE(header)
|
||||
cherrypy.serving.response.headers['WWW-Authenticate'] = header
|
||||
raise cherrypy.HTTPError(
|
||||
401, 'You are not authorized to access that resource')
|
|
@ -1,470 +0,0 @@
|
|||
"""
|
||||
CherryPy implements a simple caching system as a pluggable Tool. This tool
|
||||
tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
|
||||
yet, but it's probably good enough for most sites.
|
||||
|
||||
In general, GET responses are cached (along with selecting headers) and, if
|
||||
another request arrives for the same resource, the caching Tool will return 304
|
||||
Not Modified if possible, or serve the cached response otherwise. It also sets
|
||||
request.cached to True if serving a cached representation, and sets
|
||||
request.cacheable to False (so it doesn't get cached again).
|
||||
|
||||
If POST, PUT, or DELETE requests are made for a cached resource, they
|
||||
invalidate (delete) any cached response.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Configuration file example::
|
||||
|
||||
[/]
|
||||
tools.caching.on = True
|
||||
tools.caching.delay = 3600
|
||||
|
||||
You may use a class other than the default
|
||||
:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
|
||||
entry ``cache_class``; supply the full dotted name of the replacement class
|
||||
as the config value. It must implement the basic methods ``get``, ``put``,
|
||||
``delete``, and ``clear``.
|
||||
|
||||
You may set any attribute, including overriding methods, on the cache
|
||||
instance by providing them in config. The above sets the
|
||||
:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import cptools, httputil
|
||||
from cherrypy._cpcompat import copyitems, ntob, sorted, Event
|
||||
|
||||
|
||||
class Cache(object):
|
||||
|
||||
"""Base class for Cache implementations."""
|
||||
|
||||
def get(self):
|
||||
"""Return the current variant if in the cache, else None."""
|
||||
raise NotImplemented
|
||||
|
||||
def put(self, obj, size):
|
||||
"""Store the current variant in the cache."""
|
||||
raise NotImplemented
|
||||
|
||||
def delete(self):
|
||||
"""Remove ALL cached variants of the current resource."""
|
||||
raise NotImplemented
|
||||
|
||||
def clear(self):
|
||||
"""Reset the cache to its initial, empty state."""
|
||||
raise NotImplemented
|
||||
|
||||
|
||||
# ------------------------------ Memory Cache ------------------------------- #
|
||||
class AntiStampedeCache(dict):
|
||||
|
||||
"""A storage system for cached items which reduces stampede collisions."""
|
||||
|
||||
def wait(self, key, timeout=5, debug=False):
|
||||
"""Return the cached value for the given key, or None.
|
||||
|
||||
If timeout is not None, and the value is already
|
||||
being calculated by another thread, wait until the given timeout has
|
||||
elapsed. If the value is available before the timeout expires, it is
|
||||
returned. If not, None is returned, and a sentinel placed in the cache
|
||||
to signal other threads to wait.
|
||||
|
||||
If timeout is None, no waiting is performed nor sentinels used.
|
||||
"""
|
||||
value = self.get(key)
|
||||
if isinstance(value, Event):
|
||||
if timeout is None:
|
||||
# Ignore the other thread and recalc it ourselves.
|
||||
if debug:
|
||||
cherrypy.log('No timeout', 'TOOLS.CACHING')
|
||||
return None
|
||||
|
||||
# Wait until it's done or times out.
|
||||
if debug:
|
||||
cherrypy.log('Waiting up to %s seconds' %
|
||||
timeout, 'TOOLS.CACHING')
|
||||
value.wait(timeout)
|
||||
if value.result is not None:
|
||||
# The other thread finished its calculation. Use it.
|
||||
if debug:
|
||||
cherrypy.log('Result!', 'TOOLS.CACHING')
|
||||
return value.result
|
||||
# Timed out. Stick an Event in the slot so other threads wait
|
||||
# on this one to finish calculating the value.
|
||||
if debug:
|
||||
cherrypy.log('Timed out', 'TOOLS.CACHING')
|
||||
e = threading.Event()
|
||||
e.result = None
|
||||
dict.__setitem__(self, key, e)
|
||||
|
||||
return None
|
||||
elif value is None:
|
||||
# Stick an Event in the slot so other threads wait
|
||||
# on this one to finish calculating the value.
|
||||
if debug:
|
||||
cherrypy.log('Timed out', 'TOOLS.CACHING')
|
||||
e = threading.Event()
|
||||
e.result = None
|
||||
dict.__setitem__(self, key, e)
|
||||
return value
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
"""Set the cached value for the given key."""
|
||||
existing = self.get(key)
|
||||
dict.__setitem__(self, key, value)
|
||||
if isinstance(existing, Event):
|
||||
# Set Event.result so other threads waiting on it have
|
||||
# immediate access without needing to poll the cache again.
|
||||
existing.result = value
|
||||
existing.set()
|
||||
|
||||
|
||||
class MemoryCache(Cache):
|
||||
|
||||
"""An in-memory cache for varying response content.
|
||||
|
||||
Each key in self.store is a URI, and each value is an AntiStampedeCache.
|
||||
The response for any given URI may vary based on the values of
|
||||
"selecting request headers"; that is, those named in the Vary
|
||||
response header. We assume the list of header names to be constant
|
||||
for each URI throughout the lifetime of the application, and store
|
||||
that list in ``self.store[uri].selecting_headers``.
|
||||
|
||||
The items contained in ``self.store[uri]`` have keys which are tuples of
|
||||
request header values (in the same order as the names in its
|
||||
selecting_headers), and values which are the actual responses.
|
||||
"""
|
||||
|
||||
maxobjects = 1000
|
||||
"""The maximum number of cached objects; defaults to 1000."""
|
||||
|
||||
maxobj_size = 100000
|
||||
"""The maximum size of each cached object in bytes; defaults to 100 KB."""
|
||||
|
||||
maxsize = 10000000
|
||||
"""The maximum size of the entire cache in bytes; defaults to 10 MB."""
|
||||
|
||||
delay = 600
|
||||
"""Seconds until the cached content expires; defaults to 600 (10 minutes).
|
||||
"""
|
||||
|
||||
antistampede_timeout = 5
|
||||
"""Seconds to wait for other threads to release a cache lock."""
|
||||
|
||||
expire_freq = 0.1
|
||||
"""Seconds to sleep between cache expiration sweeps."""
|
||||
|
||||
debug = False
|
||||
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
# Run self.expire_cache in a separate daemon thread.
|
||||
t = threading.Thread(target=self.expire_cache, name='expire_cache')
|
||||
self.expiration_thread = t
|
||||
t.daemon = True
|
||||
t.start()
|
||||
|
||||
def clear(self):
|
||||
"""Reset the cache to its initial, empty state."""
|
||||
self.store = {}
|
||||
self.expirations = {}
|
||||
self.tot_puts = 0
|
||||
self.tot_gets = 0
|
||||
self.tot_hist = 0
|
||||
self.tot_expires = 0
|
||||
self.tot_non_modified = 0
|
||||
self.cursize = 0
|
||||
|
||||
def expire_cache(self):
|
||||
"""Continuously examine cached objects, expiring stale ones.
|
||||
|
||||
This function is designed to be run in its own daemon thread,
|
||||
referenced at ``self.expiration_thread``.
|
||||
"""
|
||||
# It's possible that "time" will be set to None
|
||||
# arbitrarily, so we check "while time" to avoid exceptions.
|
||||
# See tickets #99 and #180 for more information.
|
||||
while time:
|
||||
now = time.time()
|
||||
# Must make a copy of expirations so it doesn't change size
|
||||
# during iteration
|
||||
for expiration_time, objects in copyitems(self.expirations):
|
||||
if expiration_time <= now:
|
||||
for obj_size, uri, sel_header_values in objects:
|
||||
try:
|
||||
del self.store[uri][tuple(sel_header_values)]
|
||||
self.tot_expires += 1
|
||||
self.cursize -= obj_size
|
||||
except KeyError:
|
||||
# the key may have been deleted elsewhere
|
||||
pass
|
||||
del self.expirations[expiration_time]
|
||||
time.sleep(self.expire_freq)
|
||||
|
||||
def get(self):
|
||||
"""Return the current variant if in the cache, else None."""
|
||||
request = cherrypy.serving.request
|
||||
self.tot_gets += 1
|
||||
|
||||
uri = cherrypy.url(qs=request.query_string)
|
||||
uricache = self.store.get(uri)
|
||||
if uricache is None:
|
||||
return None
|
||||
|
||||
header_values = [request.headers.get(h, '')
|
||||
for h in uricache.selecting_headers]
|
||||
variant = uricache.wait(key=tuple(sorted(header_values)),
|
||||
timeout=self.antistampede_timeout,
|
||||
debug=self.debug)
|
||||
if variant is not None:
|
||||
self.tot_hist += 1
|
||||
return variant
|
||||
|
||||
def put(self, variant, size):
|
||||
"""Store the current variant in the cache."""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
uri = cherrypy.url(qs=request.query_string)
|
||||
uricache = self.store.get(uri)
|
||||
if uricache is None:
|
||||
uricache = AntiStampedeCache()
|
||||
uricache.selecting_headers = [
|
||||
e.value for e in response.headers.elements('Vary')]
|
||||
self.store[uri] = uricache
|
||||
|
||||
if len(self.store) < self.maxobjects:
|
||||
total_size = self.cursize + size
|
||||
|
||||
# checks if there's space for the object
|
||||
if (size < self.maxobj_size and total_size < self.maxsize):
|
||||
# add to the expirations list
|
||||
expiration_time = response.time + self.delay
|
||||
bucket = self.expirations.setdefault(expiration_time, [])
|
||||
bucket.append((size, uri, uricache.selecting_headers))
|
||||
|
||||
# add to the cache
|
||||
header_values = [request.headers.get(h, '')
|
||||
for h in uricache.selecting_headers]
|
||||
uricache[tuple(sorted(header_values))] = variant
|
||||
self.tot_puts += 1
|
||||
self.cursize = total_size
|
||||
|
||||
def delete(self):
|
||||
"""Remove ALL cached variants of the current resource."""
|
||||
uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
|
||||
self.store.pop(uri, None)
|
||||
|
||||
|
||||
def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
|
||||
"""Try to obtain cached output. If fresh enough, raise HTTPError(304).
|
||||
|
||||
If POST, PUT, or DELETE:
|
||||
* invalidates (deletes) any cached response for this resource
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = False
|
||||
|
||||
else if a cached copy exists:
|
||||
* sets request.cached = True
|
||||
* sets request.cacheable = False
|
||||
* sets response.headers to the cached values
|
||||
* checks the cached Last-Modified response header against the
|
||||
current If-(Un)Modified-Since request headers; raises 304
|
||||
if necessary.
|
||||
* sets response.status and response.body to the cached values
|
||||
* returns True
|
||||
|
||||
otherwise:
|
||||
* sets request.cached = False
|
||||
* sets request.cacheable = True
|
||||
* returns False
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
if not hasattr(cherrypy, '_cache'):
|
||||
# Make a process-wide Cache object.
|
||||
cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
|
||||
|
||||
# Take all remaining kwargs and set them on the Cache object.
|
||||
for k, v in kwargs.items():
|
||||
setattr(cherrypy._cache, k, v)
|
||||
cherrypy._cache.debug = debug
|
||||
|
||||
# POST, PUT, DELETE should invalidate (delete) the cached copy.
|
||||
# See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
|
||||
if request.method in invalid_methods:
|
||||
if debug:
|
||||
cherrypy.log('request.method %r in invalid_methods %r' %
|
||||
(request.method, invalid_methods), 'TOOLS.CACHING')
|
||||
cherrypy._cache.delete()
|
||||
request.cached = False
|
||||
request.cacheable = False
|
||||
return False
|
||||
|
||||
if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
cache_data = cherrypy._cache.get()
|
||||
request.cached = bool(cache_data)
|
||||
request.cacheable = not request.cached
|
||||
if request.cached:
|
||||
# Serve the cached copy.
|
||||
max_age = cherrypy._cache.delay
|
||||
for v in [e.value for e in request.headers.elements('Cache-Control')]:
|
||||
atoms = v.split('=', 1)
|
||||
directive = atoms.pop(0)
|
||||
if directive == 'max-age':
|
||||
if len(atoms) != 1 or not atoms[0].isdigit():
|
||||
raise cherrypy.HTTPError(
|
||||
400, 'Invalid Cache-Control header')
|
||||
max_age = int(atoms[0])
|
||||
break
|
||||
elif directive == 'no-cache':
|
||||
if debug:
|
||||
cherrypy.log(
|
||||
'Ignoring cache due to Cache-Control: no-cache',
|
||||
'TOOLS.CACHING')
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
|
||||
s, h, b, create_time = cache_data
|
||||
age = int(response.time - create_time)
|
||||
if (age > max_age):
|
||||
if debug:
|
||||
cherrypy.log('Ignoring cache due to age > %d' % max_age,
|
||||
'TOOLS.CACHING')
|
||||
request.cached = False
|
||||
request.cacheable = True
|
||||
return False
|
||||
|
||||
# Copy the response headers. See
|
||||
# https://github.com/cherrypy/cherrypy/issues/721.
|
||||
response.headers = rh = httputil.HeaderMap()
|
||||
for k in h:
|
||||
dict.__setitem__(rh, k, dict.__getitem__(h, k))
|
||||
|
||||
# Add the required Age header
|
||||
response.headers['Age'] = str(age)
|
||||
|
||||
try:
|
||||
# Note that validate_since depends on a Last-Modified header;
|
||||
# this was put into the cached copy, and should have been
|
||||
# resurrected just above (response.headers = cache_data[1]).
|
||||
cptools.validate_since()
|
||||
except cherrypy.HTTPRedirect:
|
||||
x = sys.exc_info()[1]
|
||||
if x.status == 304:
|
||||
cherrypy._cache.tot_non_modified += 1
|
||||
raise
|
||||
|
||||
# serve it & get out from the request
|
||||
response.status = s
|
||||
response.body = b
|
||||
else:
|
||||
if debug:
|
||||
cherrypy.log('request is not cached', 'TOOLS.CACHING')
|
||||
return request.cached
|
||||
|
||||
|
||||
def tee_output():
|
||||
"""Tee response output to cache storage. Internal."""
|
||||
# Used by CachingTool by attaching to request.hooks
|
||||
|
||||
request = cherrypy.serving.request
|
||||
if 'no-store' in request.headers.values('Cache-Control'):
|
||||
return
|
||||
|
||||
def tee(body):
|
||||
"""Tee response.body into a list."""
|
||||
if ('no-cache' in response.headers.values('Pragma') or
|
||||
'no-store' in response.headers.values('Cache-Control')):
|
||||
for chunk in body:
|
||||
yield chunk
|
||||
return
|
||||
|
||||
output = []
|
||||
for chunk in body:
|
||||
output.append(chunk)
|
||||
yield chunk
|
||||
|
||||
# save the cache data
|
||||
body = ntob('').join(output)
|
||||
cherrypy._cache.put((response.status, response.headers or {},
|
||||
body, response.time), len(body))
|
||||
|
||||
response = cherrypy.serving.response
|
||||
response.body = tee(response.body)
|
||||
|
||||
|
||||
def expires(secs=0, force=False, debug=False):
|
||||
"""Tool for influencing cache mechanisms using the 'Expires' header.
|
||||
|
||||
secs
|
||||
Must be either an int or a datetime.timedelta, and indicates the
|
||||
number of seconds between response.time and when the response should
|
||||
expire. The 'Expires' header will be set to response.time + secs.
|
||||
If secs is zero, the 'Expires' header is set one year in the past, and
|
||||
the following "cache prevention" headers are also set:
|
||||
|
||||
* Pragma: no-cache
|
||||
* Cache-Control': no-cache, must-revalidate
|
||||
|
||||
force
|
||||
If False, the following headers are checked:
|
||||
|
||||
* Etag
|
||||
* Last-Modified
|
||||
* Age
|
||||
* Expires
|
||||
|
||||
If any are already present, none of the above response headers are set.
|
||||
|
||||
"""
|
||||
|
||||
response = cherrypy.serving.response
|
||||
headers = response.headers
|
||||
|
||||
cacheable = False
|
||||
if not force:
|
||||
# some header names that indicate that the response can be cached
|
||||
for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
|
||||
if indicator in headers:
|
||||
cacheable = True
|
||||
break
|
||||
|
||||
if not cacheable and not force:
|
||||
if debug:
|
||||
cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
|
||||
else:
|
||||
if debug:
|
||||
cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
|
||||
if isinstance(secs, datetime.timedelta):
|
||||
secs = (86400 * secs.days) + secs.seconds
|
||||
|
||||
if secs == 0:
|
||||
if force or ('Pragma' not in headers):
|
||||
headers['Pragma'] = 'no-cache'
|
||||
if cherrypy.serving.request.protocol >= (1, 1):
|
||||
if force or 'Cache-Control' not in headers:
|
||||
headers['Cache-Control'] = 'no-cache, must-revalidate'
|
||||
# Set an explicit Expires date in the past.
|
||||
expiry = httputil.HTTPDate(1169942400.0)
|
||||
else:
|
||||
expiry = httputil.HTTPDate(response.time + secs)
|
||||
if force or 'Expires' not in headers:
|
||||
headers['Expires'] = expiry
|
|
@ -1,391 +0,0 @@
|
|||
"""Code-coverage tools for CherryPy.
|
||||
|
||||
To use this module, or the coverage tools in the test suite,
|
||||
you need to download 'coverage.py', either Gareth Rees' `original
|
||||
implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
|
||||
or Ned Batchelder's `enhanced version:
|
||||
<http://www.nedbatchelder.com/code/modules/coverage.html>`_
|
||||
|
||||
To turn on coverage tracing, use the following code::
|
||||
|
||||
cherrypy.engine.subscribe('start', covercp.start)
|
||||
|
||||
DO NOT subscribe anything on the 'start_thread' channel, as previously
|
||||
recommended. Calling start once in the main thread should be sufficient
|
||||
to start coverage on all threads. Calling start again in each thread
|
||||
effectively clears any coverage data gathered up to that point.
|
||||
|
||||
Run your code, then use the ``covercp.serve()`` function to browse the
|
||||
results in a web browser. If you run this module from the command line,
|
||||
it will call ``serve()`` for you.
|
||||
"""
|
||||
|
||||
import re
|
||||
import sys
|
||||
import cgi
|
||||
import os
|
||||
import os.path
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import quote_plus
|
||||
|
||||
|
||||
localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
|
||||
|
||||
the_coverage = None
|
||||
try:
|
||||
from coverage import coverage
|
||||
the_coverage = coverage(data_file=localFile)
|
||||
|
||||
def start():
|
||||
the_coverage.start()
|
||||
except ImportError:
|
||||
# Setting the_coverage to None will raise errors
|
||||
# that need to be trapped downstream.
|
||||
the_coverage = None
|
||||
|
||||
import warnings
|
||||
warnings.warn(
|
||||
'No code coverage will be performed; '
|
||||
'coverage.py could not be imported.')
|
||||
|
||||
def start():
|
||||
pass
|
||||
start.priority = 20
|
||||
|
||||
TEMPLATE_MENU = """<html>
|
||||
<head>
|
||||
<title>CherryPy Coverage Menu</title>
|
||||
<style>
|
||||
body {font: 9pt Arial, serif;}
|
||||
#tree {
|
||||
font-size: 8pt;
|
||||
font-family: Andale Mono, monospace;
|
||||
white-space: pre;
|
||||
}
|
||||
#tree a:active, a:focus {
|
||||
background-color: black;
|
||||
padding: 1px;
|
||||
color: white;
|
||||
border: 0px solid #9999FF;
|
||||
-moz-outline-style: none;
|
||||
}
|
||||
.fail { color: red;}
|
||||
.pass { color: #888;}
|
||||
#pct { text-align: right;}
|
||||
h3 {
|
||||
font-size: small;
|
||||
font-weight: bold;
|
||||
font-style: italic;
|
||||
margin-top: 5px;
|
||||
}
|
||||
input { border: 1px solid #ccc; padding: 2px; }
|
||||
.directory {
|
||||
color: #933;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
font-size: 10pt;
|
||||
}
|
||||
.file {
|
||||
color: #400;
|
||||
}
|
||||
a { text-decoration: none; }
|
||||
#crumbs {
|
||||
color: white;
|
||||
font-size: 8pt;
|
||||
font-family: Andale Mono, monospace;
|
||||
width: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
#crumbs a {
|
||||
color: #f88;
|
||||
}
|
||||
#options {
|
||||
line-height: 2.3em;
|
||||
border: 1px solid black;
|
||||
background-color: #eee;
|
||||
padding: 4px;
|
||||
}
|
||||
#exclude {
|
||||
width: 100%;
|
||||
margin-bottom: 3px;
|
||||
border: 1px solid #999;
|
||||
}
|
||||
#submit {
|
||||
background-color: black;
|
||||
color: white;
|
||||
border: 0;
|
||||
margin-bottom: -9px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>CherryPy Coverage</h2>"""
|
||||
|
||||
TEMPLATE_FORM = """
|
||||
<div id="options">
|
||||
<form action='menu' method=GET>
|
||||
<input type='hidden' name='base' value='%(base)s' />
|
||||
Show percentages
|
||||
<input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
|
||||
Hide files over
|
||||
<input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
|
||||
Exclude files matching<br />
|
||||
<input type='text' id='exclude' name='exclude'
|
||||
value='%(exclude)s' size='20' />
|
||||
<br />
|
||||
|
||||
<input type='submit' value='Change view' id="submit"/>
|
||||
</form>
|
||||
</div>"""
|
||||
|
||||
TEMPLATE_FRAMESET = """<html>
|
||||
<head><title>CherryPy coverage data</title></head>
|
||||
<frameset cols='250, 1*'>
|
||||
<frame src='menu?base=%s' />
|
||||
<frame name='main' src='' />
|
||||
</frameset>
|
||||
</html>
|
||||
"""
|
||||
|
||||
TEMPLATE_COVERAGE = """<html>
|
||||
<head>
|
||||
<title>Coverage for %(name)s</title>
|
||||
<style>
|
||||
h2 { margin-bottom: .25em; }
|
||||
p { margin: .25em; }
|
||||
.covered { color: #000; background-color: #fff; }
|
||||
.notcovered { color: #fee; background-color: #500; }
|
||||
.excluded { color: #00f; background-color: #fff; }
|
||||
table .covered, table .notcovered, table .excluded
|
||||
{ font-family: Andale Mono, monospace;
|
||||
font-size: 10pt; white-space: pre; }
|
||||
|
||||
.lineno { background-color: #eee;}
|
||||
.notcovered .lineno { background-color: #000;}
|
||||
table { border-collapse: collapse;
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>%(name)s</h2>
|
||||
<p>%(fullpath)s</p>
|
||||
<p>Coverage: %(pc)s%%</p>"""
|
||||
|
||||
TEMPLATE_LOC_COVERED = """<tr class="covered">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
|
||||
<td class="lineno">%s </td>
|
||||
<td>%s</td>
|
||||
</tr>\n"""
|
||||
|
||||
TEMPLATE_ITEM = (
|
||||
"%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
|
||||
)
|
||||
|
||||
|
||||
def _percent(statements, missing):
|
||||
s = len(statements)
|
||||
e = s - len(missing)
|
||||
if s > 0:
|
||||
return int(round(100.0 * e / s))
|
||||
return 0
|
||||
|
||||
|
||||
def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
|
||||
coverage=the_coverage):
|
||||
|
||||
# Show the directory name and any of our children
|
||||
dirs = [k for k, v in root.items() if v]
|
||||
dirs.sort()
|
||||
for name in dirs:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
if newpath.lower().startswith(base):
|
||||
relpath = newpath[len(base):]
|
||||
yield '| ' * relpath.count(os.sep)
|
||||
yield (
|
||||
"<a class='directory' "
|
||||
"href='menu?base=%s&exclude=%s'>%s</a>\n" %
|
||||
(newpath, quote_plus(exclude), name)
|
||||
)
|
||||
|
||||
for chunk in _show_branch(
|
||||
root[name], base, newpath, pct, showpct,
|
||||
exclude, coverage=coverage
|
||||
):
|
||||
yield chunk
|
||||
|
||||
# Now list the files
|
||||
if path.lower().startswith(base):
|
||||
relpath = path[len(base):]
|
||||
files = [k for k, v in root.items() if not v]
|
||||
files.sort()
|
||||
for name in files:
|
||||
newpath = os.path.join(path, name)
|
||||
|
||||
pc_str = ''
|
||||
if showpct:
|
||||
try:
|
||||
_, statements, _, missing, _ = coverage.analysis2(newpath)
|
||||
except:
|
||||
# Yes, we really want to pass on all errors.
|
||||
pass
|
||||
else:
|
||||
pc = _percent(statements, missing)
|
||||
pc_str = ('%3d%% ' % pc).replace(' ', ' ')
|
||||
if pc < float(pct) or pc == -1:
|
||||
pc_str = "<span class='fail'>%s</span>" % pc_str
|
||||
else:
|
||||
pc_str = "<span class='pass'>%s</span>" % pc_str
|
||||
|
||||
yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
|
||||
pc_str, newpath, name)
|
||||
|
||||
|
||||
def _skip_file(path, exclude):
|
||||
if exclude:
|
||||
return bool(re.search(exclude, path))
|
||||
|
||||
|
||||
def _graft(path, tree):
|
||||
d = tree
|
||||
|
||||
p = path
|
||||
atoms = []
|
||||
while True:
|
||||
p, tail = os.path.split(p)
|
||||
if not tail:
|
||||
break
|
||||
atoms.append(tail)
|
||||
atoms.append(p)
|
||||
if p != '/':
|
||||
atoms.append('/')
|
||||
|
||||
atoms.reverse()
|
||||
for node in atoms:
|
||||
if node:
|
||||
d = d.setdefault(node, {})
|
||||
|
||||
|
||||
def get_tree(base, exclude, coverage=the_coverage):
|
||||
"""Return covered module names as a nested dict."""
|
||||
tree = {}
|
||||
runs = coverage.data.executed_files()
|
||||
for path in runs:
|
||||
if not _skip_file(path, exclude) and not os.path.isdir(path):
|
||||
_graft(path, tree)
|
||||
return tree
|
||||
|
||||
|
||||
class CoverStats(object):
|
||||
|
||||
def __init__(self, coverage, root=None):
|
||||
self.coverage = coverage
|
||||
if root is None:
|
||||
# Guess initial depth. Files outside this path will not be
|
||||
# reachable from the web interface.
|
||||
import cherrypy
|
||||
root = os.path.dirname(cherrypy.__file__)
|
||||
self.root = root
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return TEMPLATE_FRAMESET % self.root.lower()
|
||||
|
||||
@cherrypy.expose
|
||||
def menu(self, base='/', pct='50', showpct='',
|
||||
exclude=r'python\d\.\d|test|tut\d|tutorial'):
|
||||
|
||||
# The coverage module uses all-lower-case names.
|
||||
base = base.lower().rstrip(os.sep)
|
||||
|
||||
yield TEMPLATE_MENU
|
||||
yield TEMPLATE_FORM % locals()
|
||||
|
||||
# Start by showing links for parent paths
|
||||
yield "<div id='crumbs'>"
|
||||
path = ''
|
||||
atoms = base.split(os.sep)
|
||||
atoms.pop()
|
||||
for atom in atoms:
|
||||
path += atom + os.sep
|
||||
yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
|
||||
% (path, quote_plus(exclude), atom, os.sep))
|
||||
yield '</div>'
|
||||
|
||||
yield "<div id='tree'>"
|
||||
|
||||
# Then display the tree
|
||||
tree = get_tree(base, exclude, self.coverage)
|
||||
if not tree:
|
||||
yield '<p>No modules covered.</p>'
|
||||
else:
|
||||
for chunk in _show_branch(tree, base, '/', pct,
|
||||
showpct == 'checked', exclude,
|
||||
coverage=self.coverage):
|
||||
yield chunk
|
||||
|
||||
yield '</div>'
|
||||
yield '</body></html>'
|
||||
|
||||
def annotated_file(self, filename, statements, excluded, missing):
|
||||
source = open(filename, 'r')
|
||||
buffer = []
|
||||
for lineno, line in enumerate(source.readlines()):
|
||||
lineno += 1
|
||||
line = line.strip('\n\r')
|
||||
empty_the_buffer = True
|
||||
if lineno in excluded:
|
||||
template = TEMPLATE_LOC_EXCLUDED
|
||||
elif lineno in missing:
|
||||
template = TEMPLATE_LOC_NOT_COVERED
|
||||
elif lineno in statements:
|
||||
template = TEMPLATE_LOC_COVERED
|
||||
else:
|
||||
empty_the_buffer = False
|
||||
buffer.append((lineno, line))
|
||||
if empty_the_buffer:
|
||||
for lno, pastline in buffer:
|
||||
yield template % (lno, cgi.escape(pastline))
|
||||
buffer = []
|
||||
yield template % (lineno, cgi.escape(line))
|
||||
|
||||
@cherrypy.expose
|
||||
def report(self, name):
|
||||
filename, statements, excluded, missing, _ = self.coverage.analysis2(
|
||||
name)
|
||||
pc = _percent(statements, missing)
|
||||
yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
|
||||
fullpath=name,
|
||||
pc=pc)
|
||||
yield '<table>\n'
|
||||
for line in self.annotated_file(filename, statements, excluded,
|
||||
missing):
|
||||
yield line
|
||||
yield '</table>'
|
||||
yield '</body>'
|
||||
yield '</html>'
|
||||
|
||||
|
||||
def serve(path=localFile, port=8080, root=None):
|
||||
if coverage is None:
|
||||
raise ImportError('The coverage module could not be imported.')
|
||||
from coverage import coverage
|
||||
cov = coverage(data_file=path)
|
||||
cov.load()
|
||||
|
||||
import cherrypy
|
||||
cherrypy.config.update({'server.socket_port': int(port),
|
||||
'server.thread_pool': 10,
|
||||
'environment': 'production',
|
||||
})
|
||||
cherrypy.quickstart(CoverStats(cov, root))
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve(*tuple(sys.argv[1:]))
|
|
@ -1,690 +0,0 @@
|
|||
"""CPStats, a package for collecting and reporting on program statistics.
|
||||
|
||||
Overview
|
||||
========
|
||||
|
||||
Statistics about program operation are an invaluable monitoring and debugging
|
||||
tool. Unfortunately, the gathering and reporting of these critical values is
|
||||
usually ad-hoc. This package aims to add a centralized place for gathering
|
||||
statistical performance data, a structure for recording that data which
|
||||
provides for extrapolation of that data into more useful information,
|
||||
and a method of serving that data to both human investigators and
|
||||
monitoring software. Let's examine each of those in more detail.
|
||||
|
||||
Data Gathering
|
||||
--------------
|
||||
|
||||
Just as Python's `logging` module provides a common importable for gathering
|
||||
and sending messages, performance statistics would benefit from a similar
|
||||
common mechanism, and one that does *not* require each package which wishes
|
||||
to collect stats to import a third-party module. Therefore, we choose to
|
||||
re-use the `logging` module by adding a `statistics` object to it.
|
||||
|
||||
That `logging.statistics` object is a nested dict. It is not a custom class,
|
||||
because that would:
|
||||
|
||||
1. require libraries and applications to import a third-party module in
|
||||
order to participate
|
||||
2. inhibit innovation in extrapolation approaches and in reporting tools, and
|
||||
3. be slow.
|
||||
|
||||
There are, however, some specifications regarding the structure of the dict.::
|
||||
|
||||
{
|
||||
+----"SQLAlchemy": {
|
||||
| "Inserts": 4389745,
|
||||
| "Inserts per Second":
|
||||
| lambda s: s["Inserts"] / (time() - s["Start"]),
|
||||
| C +---"Table Statistics": {
|
||||
| o | "widgets": {-----------+
|
||||
N | l | "Rows": 1.3M, | Record
|
||||
a | l | "Inserts": 400, |
|
||||
m | e | },---------------------+
|
||||
e | c | "froobles": {
|
||||
s | t | "Rows": 7845,
|
||||
p | i | "Inserts": 0,
|
||||
a | o | },
|
||||
c | n +---},
|
||||
e | "Slow Queries":
|
||||
| [{"Query": "SELECT * FROM widgets;",
|
||||
| "Processing Time": 47.840923343,
|
||||
| },
|
||||
| ],
|
||||
+----},
|
||||
}
|
||||
|
||||
The `logging.statistics` dict has four levels. The topmost level is nothing
|
||||
more than a set of names to introduce modularity, usually along the lines of
|
||||
package names. If the SQLAlchemy project wanted to participate, for example,
|
||||
it might populate the item `logging.statistics['SQLAlchemy']`, whose value
|
||||
would be a second-layer dict we call a "namespace". Namespaces help multiple
|
||||
packages to avoid collisions over key names, and make reports easier to read,
|
||||
to boot. The maintainers of SQLAlchemy should feel free to use more than one
|
||||
namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case
|
||||
or other syntax constraints on the namespace names; they should be chosen
|
||||
to be maximally readable by humans (neither too short nor too long).
|
||||
|
||||
Each namespace, then, is a dict of named statistical values, such as
|
||||
'Requests/sec' or 'Uptime'. You should choose names which will look
|
||||
good on a report: spaces and capitalization are just fine.
|
||||
|
||||
In addition to scalars, values in a namespace MAY be a (third-layer)
|
||||
dict, or a list, called a "collection". For example, the CherryPy
|
||||
:class:`StatsTool` keeps track of what each request is doing (or has most
|
||||
recently done) in a 'Requests' collection, where each key is a thread ID; each
|
||||
value in the subdict MUST be a fourth dict (whew!) of statistical data about
|
||||
each thread. We call each subdict in the collection a "record". Similarly,
|
||||
the :class:`StatsTool` also keeps a list of slow queries, where each record
|
||||
contains data about each slow query, in order.
|
||||
|
||||
Values in a namespace or record may also be functions, which brings us to:
|
||||
|
||||
Extrapolation
|
||||
-------------
|
||||
|
||||
The collection of statistical data needs to be fast, as close to unnoticeable
|
||||
as possible to the host program. That requires us to minimize I/O, for example,
|
||||
but in Python it also means we need to minimize function calls. So when you
|
||||
are designing your namespace and record values, try to insert the most basic
|
||||
scalar values you already have on hand.
|
||||
|
||||
When it comes time to report on the gathered data, however, we usually have
|
||||
much more freedom in what we can calculate. Therefore, whenever reporting
|
||||
tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
|
||||
of `logging.statistics` for reporting, they first call
|
||||
`extrapolate_statistics` (passing the whole `statistics` dict as the only
|
||||
argument). This makes a deep copy of the statistics dict so that the
|
||||
reporting tool can both iterate over it and even change it without harming
|
||||
the original. But it also expands any functions in the dict by calling them.
|
||||
For example, you might have a 'Current Time' entry in the namespace with the
|
||||
value "lambda scope: time.time()". The "scope" parameter is the current
|
||||
namespace dict (or record, if we're currently expanding one of those
|
||||
instead), allowing you access to existing static entries. If you're truly
|
||||
evil, you can even modify more than one entry at a time.
|
||||
|
||||
However, don't try to calculate an entry and then use its value in further
|
||||
extrapolations; the order in which the functions are called is not guaranteed.
|
||||
This can lead to a certain amount of duplicated work (or a redesign of your
|
||||
schema), but that's better than complicating the spec.
|
||||
|
||||
After the whole thing has been extrapolated, it's time for:
|
||||
|
||||
Reporting
|
||||
---------
|
||||
|
||||
The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
|
||||
it all, and then transforms it to HTML for easy viewing. Each namespace gets
|
||||
its own header and attribute table, plus an extra table for each collection.
|
||||
This is NOT part of the statistics specification; other tools can format how
|
||||
they like.
|
||||
|
||||
You can control which columns are output and how they are formatted by updating
|
||||
StatsPage.formatting, which is a dict that mirrors the keys and nesting of
|
||||
`logging.statistics`. The difference is that, instead of data values, it has
|
||||
formatting values. Use None for a given key to indicate to the StatsPage that a
|
||||
given column should not be output. Use a string with formatting
|
||||
(such as '%.3f') to interpolate the value(s), or use a callable (such as
|
||||
lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
|
||||
mentioned in the formatting dict is output unchanged.
|
||||
|
||||
Monitoring
|
||||
----------
|
||||
|
||||
Although the HTML output takes pains to assign unique id's to each <td> with
|
||||
statistical data, you're probably better off fetching /cpstats/data, which
|
||||
outputs the whole (extrapolated) `logging.statistics` dict in JSON format.
|
||||
That is probably easier to parse, and doesn't have any formatting controls,
|
||||
so you get the "original" data in a consistently-serialized format.
|
||||
Note: there's no treatment yet for datetime objects. Try time.time() instead
|
||||
for now if you can. Nagios will probably thank you.
|
||||
|
||||
Turning Collection Off
|
||||
----------------------
|
||||
|
||||
It is recommended each namespace have an "Enabled" item which, if False,
|
||||
stops collection (but not reporting) of statistical data. Applications
|
||||
SHOULD provide controls to pause and resume collection by setting these
|
||||
entries to False or True, if present.
|
||||
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
To collect statistics on CherryPy applications::
|
||||
|
||||
from cherrypy.lib import cpstats
|
||||
appconfig['/']['tools.cpstats.on'] = True
|
||||
|
||||
To collect statistics on your own code::
|
||||
|
||||
import logging
|
||||
# Initialize the repository
|
||||
if not hasattr(logging, 'statistics'): logging.statistics = {}
|
||||
# Initialize my namespace
|
||||
mystats = logging.statistics.setdefault('My Stuff', {})
|
||||
# Initialize my namespace's scalars and collections
|
||||
mystats.update({
|
||||
'Enabled': True,
|
||||
'Start Time': time.time(),
|
||||
'Important Events': 0,
|
||||
'Events/Second': lambda s: (
|
||||
(s['Important Events'] / (time.time() - s['Start Time']))),
|
||||
})
|
||||
...
|
||||
for event in events:
|
||||
...
|
||||
# Collect stats
|
||||
if mystats.get('Enabled', False):
|
||||
mystats['Important Events'] += 1
|
||||
|
||||
To report statistics::
|
||||
|
||||
root.cpstats = cpstats.StatsPage()
|
||||
|
||||
To format statistics reports::
|
||||
|
||||
See 'Reporting', above.
|
||||
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import json
|
||||
|
||||
# ------------------------------- Statistics -------------------------------- #
|
||||
|
||||
if not hasattr(logging, 'statistics'):
|
||||
logging.statistics = {}
|
||||
|
||||
|
||||
def extrapolate_statistics(scope):
|
||||
"""Return an extrapolated copy of the given scope."""
|
||||
c = {}
|
||||
for k, v in list(scope.items()):
|
||||
if isinstance(v, dict):
|
||||
v = extrapolate_statistics(v)
|
||||
elif isinstance(v, (list, tuple)):
|
||||
v = [extrapolate_statistics(record) for record in v]
|
||||
elif hasattr(v, '__call__'):
|
||||
v = v(scope)
|
||||
c[k] = v
|
||||
return c
|
||||
|
||||
|
||||
# -------------------- CherryPy Applications Statistics --------------------- #
|
||||
|
||||
appstats = logging.statistics.setdefault('CherryPy Applications', {})
|
||||
appstats.update({
|
||||
'Enabled': True,
|
||||
'Bytes Read/Request': lambda s: (
|
||||
s['Total Requests'] and
|
||||
(s['Total Bytes Read'] / float(s['Total Requests'])) or
|
||||
0.0
|
||||
),
|
||||
'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
|
||||
'Bytes Written/Request': lambda s: (
|
||||
s['Total Requests'] and
|
||||
(s['Total Bytes Written'] / float(s['Total Requests'])) or
|
||||
0.0
|
||||
),
|
||||
'Bytes Written/Second': lambda s: (
|
||||
s['Total Bytes Written'] / s['Uptime'](s)
|
||||
),
|
||||
'Current Time': lambda s: time.time(),
|
||||
'Current Requests': 0,
|
||||
'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
|
||||
'Server Version': cherrypy.__version__,
|
||||
'Start Time': time.time(),
|
||||
'Total Bytes Read': 0,
|
||||
'Total Bytes Written': 0,
|
||||
'Total Requests': 0,
|
||||
'Total Time': 0,
|
||||
'Uptime': lambda s: time.time() - s['Start Time'],
|
||||
'Requests': {},
|
||||
})
|
||||
|
||||
proc_time = lambda s: time.time() - s['Start Time']
|
||||
|
||||
|
||||
class ByteCountWrapper(object):
|
||||
|
||||
"""Wraps a file-like object, counting the number of bytes read."""
|
||||
|
||||
def __init__(self, rfile):
|
||||
self.rfile = rfile
|
||||
self.bytes_read = 0
|
||||
|
||||
def read(self, size=-1):
|
||||
data = self.rfile.read(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
def readline(self, size=-1):
|
||||
data = self.rfile.readline(size)
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
def readlines(self, sizehint=0):
|
||||
# Shamelessly stolen from StringIO
|
||||
total = 0
|
||||
lines = []
|
||||
line = self.readline()
|
||||
while line:
|
||||
lines.append(line)
|
||||
total += len(line)
|
||||
if 0 < sizehint <= total:
|
||||
break
|
||||
line = self.readline()
|
||||
return lines
|
||||
|
||||
def close(self):
|
||||
self.rfile.close()
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
data = self.rfile.next()
|
||||
self.bytes_read += len(data)
|
||||
return data
|
||||
|
||||
|
||||
average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0
|
||||
|
||||
|
||||
def _get_threading_ident():
|
||||
if sys.version_info >= (3, 3):
|
||||
return threading.get_ident()
|
||||
return threading._get_ident()
|
||||
|
||||
class StatsTool(cherrypy.Tool):
|
||||
|
||||
"""Record various information about the current request."""
|
||||
|
||||
def __init__(self):
|
||||
cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
|
||||
|
||||
def _setup(self):
|
||||
"""Hook this tool into cherrypy.request.
|
||||
|
||||
The standard CherryPy request object will automatically call this
|
||||
method when the tool is "turned on" in config.
|
||||
"""
|
||||
if appstats.get('Enabled', False):
|
||||
cherrypy.Tool._setup(self)
|
||||
self.record_start()
|
||||
|
||||
def record_start(self):
|
||||
"""Record the beginning of a request."""
|
||||
request = cherrypy.serving.request
|
||||
if not hasattr(request.rfile, 'bytes_read'):
|
||||
request.rfile = ByteCountWrapper(request.rfile)
|
||||
request.body.fp = request.rfile
|
||||
|
||||
r = request.remote
|
||||
|
||||
appstats['Current Requests'] += 1
|
||||
appstats['Total Requests'] += 1
|
||||
appstats['Requests'][_get_threading_ident()] = {
|
||||
'Bytes Read': None,
|
||||
'Bytes Written': None,
|
||||
# Use a lambda so the ip gets updated by tools.proxy later
|
||||
'Client': lambda s: '%s:%s' % (r.ip, r.port),
|
||||
'End Time': None,
|
||||
'Processing Time': proc_time,
|
||||
'Request-Line': request.request_line,
|
||||
'Response Status': None,
|
||||
'Start Time': time.time(),
|
||||
}
|
||||
|
||||
def record_stop(
|
||||
self, uriset=None, slow_queries=1.0, slow_queries_count=100,
|
||||
debug=False, **kwargs):
|
||||
"""Record the end of a request."""
|
||||
resp = cherrypy.serving.response
|
||||
w = appstats['Requests'][_get_threading_ident()]
|
||||
|
||||
r = cherrypy.request.rfile.bytes_read
|
||||
w['Bytes Read'] = r
|
||||
appstats['Total Bytes Read'] += r
|
||||
|
||||
if resp.stream:
|
||||
w['Bytes Written'] = 'chunked'
|
||||
else:
|
||||
cl = int(resp.headers.get('Content-Length', 0))
|
||||
w['Bytes Written'] = cl
|
||||
appstats['Total Bytes Written'] += cl
|
||||
|
||||
w['Response Status'] = getattr(
|
||||
resp, 'output_status', None) or resp.status
|
||||
|
||||
w['End Time'] = time.time()
|
||||
p = w['End Time'] - w['Start Time']
|
||||
w['Processing Time'] = p
|
||||
appstats['Total Time'] += p
|
||||
|
||||
appstats['Current Requests'] -= 1
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
|
||||
|
||||
if uriset:
|
||||
rs = appstats.setdefault('URI Set Tracking', {})
|
||||
r = rs.setdefault(uriset, {
|
||||
'Min': None, 'Max': None, 'Count': 0, 'Sum': 0,
|
||||
'Avg': average_uriset_time})
|
||||
if r['Min'] is None or p < r['Min']:
|
||||
r['Min'] = p
|
||||
if r['Max'] is None or p > r['Max']:
|
||||
r['Max'] = p
|
||||
r['Count'] += 1
|
||||
r['Sum'] += p
|
||||
|
||||
if slow_queries and p > slow_queries:
|
||||
sq = appstats.setdefault('Slow Queries', [])
|
||||
sq.append(w.copy())
|
||||
if len(sq) > slow_queries_count:
|
||||
sq.pop(0)
|
||||
|
||||
|
||||
cherrypy.tools.cpstats = StatsTool()
|
||||
|
||||
|
||||
# ---------------------- CherryPy Statistics Reporting ---------------------- #
|
||||
|
||||
thisdir = os.path.abspath(os.path.dirname(__file__))
|
||||
|
||||
missing = object()
|
||||
|
||||
locale_date = lambda v: time.strftime('%c', time.gmtime(v))
|
||||
iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
|
||||
|
||||
|
||||
def pause_resume(ns):
|
||||
def _pause_resume(enabled):
|
||||
pause_disabled = ''
|
||||
resume_disabled = ''
|
||||
if enabled:
|
||||
resume_disabled = 'disabled="disabled" '
|
||||
else:
|
||||
pause_disabled = 'disabled="disabled" '
|
||||
return """
|
||||
<form action="pause" method="POST" style="display:inline">
|
||||
<input type="hidden" name="namespace" value="%s" />
|
||||
<input type="submit" value="Pause" %s/>
|
||||
</form>
|
||||
<form action="resume" method="POST" style="display:inline">
|
||||
<input type="hidden" name="namespace" value="%s" />
|
||||
<input type="submit" value="Resume" %s/>
|
||||
</form>
|
||||
""" % (ns, pause_disabled, ns, resume_disabled)
|
||||
return _pause_resume
|
||||
|
||||
|
||||
class StatsPage(object):
|
||||
|
||||
formatting = {
|
||||
'CherryPy Applications': {
|
||||
'Enabled': pause_resume('CherryPy Applications'),
|
||||
'Bytes Read/Request': '%.3f',
|
||||
'Bytes Read/Second': '%.3f',
|
||||
'Bytes Written/Request': '%.3f',
|
||||
'Bytes Written/Second': '%.3f',
|
||||
'Current Time': iso_format,
|
||||
'Requests/Second': '%.3f',
|
||||
'Start Time': iso_format,
|
||||
'Total Time': '%.3f',
|
||||
'Uptime': '%.3f',
|
||||
'Slow Queries': {
|
||||
'End Time': None,
|
||||
'Processing Time': '%.3f',
|
||||
'Start Time': iso_format,
|
||||
},
|
||||
'URI Set Tracking': {
|
||||
'Avg': '%.3f',
|
||||
'Max': '%.3f',
|
||||
'Min': '%.3f',
|
||||
'Sum': '%.3f',
|
||||
},
|
||||
'Requests': {
|
||||
'Bytes Read': '%s',
|
||||
'Bytes Written': '%s',
|
||||
'End Time': None,
|
||||
'Processing Time': '%.3f',
|
||||
'Start Time': None,
|
||||
},
|
||||
},
|
||||
'CherryPy WSGIServer': {
|
||||
'Enabled': pause_resume('CherryPy WSGIServer'),
|
||||
'Connections/second': '%.3f',
|
||||
'Start time': iso_format,
|
||||
},
|
||||
}
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
# Transform the raw data into pretty output for HTML
|
||||
yield """
|
||||
<html>
|
||||
<head>
|
||||
<title>Statistics</title>
|
||||
<style>
|
||||
|
||||
th, td {
|
||||
padding: 0.25em 0.5em;
|
||||
border: 1px solid #666699;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
table.stats1 {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
table.stats1 th {
|
||||
font-weight: bold;
|
||||
text-align: right;
|
||||
background-color: #CCD5DD;
|
||||
}
|
||||
|
||||
table.stats2, h2 {
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
table.stats2 th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
background-color: #CCD5DD;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
"""
|
||||
for title, scalars, collections in self.get_namespaces():
|
||||
yield """
|
||||
<h1>%s</h1>
|
||||
|
||||
<table class='stats1'>
|
||||
<tbody>
|
||||
""" % title
|
||||
for i, (key, value) in enumerate(scalars):
|
||||
colnum = i % 3
|
||||
if colnum == 0:
|
||||
yield """
|
||||
<tr>"""
|
||||
yield (
|
||||
"""
|
||||
<th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
|
||||
vars()
|
||||
)
|
||||
if colnum == 2:
|
||||
yield """
|
||||
</tr>"""
|
||||
|
||||
if colnum == 0:
|
||||
yield """
|
||||
<th></th><td></td>
|
||||
<th></th><td></td>
|
||||
</tr>"""
|
||||
elif colnum == 1:
|
||||
yield """
|
||||
<th></th><td></td>
|
||||
</tr>"""
|
||||
yield """
|
||||
</tbody>
|
||||
</table>"""
|
||||
|
||||
for subtitle, headers, subrows in collections:
|
||||
yield """
|
||||
<h2>%s</h2>
|
||||
<table class='stats2'>
|
||||
<thead>
|
||||
<tr>""" % subtitle
|
||||
for key in headers:
|
||||
yield """
|
||||
<th>%s</th>""" % key
|
||||
yield """
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>"""
|
||||
for subrow in subrows:
|
||||
yield """
|
||||
<tr>"""
|
||||
for value in subrow:
|
||||
yield """
|
||||
<td>%s</td>""" % value
|
||||
yield """
|
||||
</tr>"""
|
||||
yield """
|
||||
</tbody>
|
||||
</table>"""
|
||||
yield """
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
def get_namespaces(self):
|
||||
"""Yield (title, scalars, collections) for each namespace."""
|
||||
s = extrapolate_statistics(logging.statistics)
|
||||
for title, ns in sorted(s.items()):
|
||||
scalars = []
|
||||
collections = []
|
||||
ns_fmt = self.formatting.get(title, {})
|
||||
for k, v in sorted(ns.items()):
|
||||
fmt = ns_fmt.get(k, {})
|
||||
if isinstance(v, dict):
|
||||
headers, subrows = self.get_dict_collection(v, fmt)
|
||||
collections.append((k, ['ID'] + headers, subrows))
|
||||
elif isinstance(v, (list, tuple)):
|
||||
headers, subrows = self.get_list_collection(v, fmt)
|
||||
collections.append((k, headers, subrows))
|
||||
else:
|
||||
format = ns_fmt.get(k, missing)
|
||||
if format is None:
|
||||
# Don't output this column.
|
||||
continue
|
||||
if hasattr(format, '__call__'):
|
||||
v = format(v)
|
||||
elif format is not missing:
|
||||
v = format % v
|
||||
scalars.append((k, v))
|
||||
yield title, scalars, collections
|
||||
|
||||
def get_dict_collection(self, v, formatting):
|
||||
"""Return ([headers], [rows]) for the given collection."""
|
||||
# E.g., the 'Requests' dict.
|
||||
headers = []
|
||||
try:
|
||||
# python2
|
||||
vals = v.itervalues()
|
||||
except AttributeError:
|
||||
# python3
|
||||
vals = v.values()
|
||||
for record in vals:
|
||||
for k3 in record:
|
||||
format = formatting.get(k3, missing)
|
||||
if format is None:
|
||||
# Don't output this column.
|
||||
continue
|
||||
if k3 not in headers:
|
||||
headers.append(k3)
|
||||
headers.sort()
|
||||
|
||||
subrows = []
|
||||
for k2, record in sorted(v.items()):
|
||||
subrow = [k2]
|
||||
for k3 in headers:
|
||||
v3 = record.get(k3, '')
|
||||
format = formatting.get(k3, missing)
|
||||
if format is None:
|
||||
# Don't output this column.
|
||||
continue
|
||||
if hasattr(format, '__call__'):
|
||||
v3 = format(v3)
|
||||
elif format is not missing:
|
||||
v3 = format % v3
|
||||
subrow.append(v3)
|
||||
subrows.append(subrow)
|
||||
|
||||
return headers, subrows
|
||||
|
||||
def get_list_collection(self, v, formatting):
|
||||
"""Return ([headers], [subrows]) for the given collection."""
|
||||
# E.g., the 'Slow Queries' list.
|
||||
headers = []
|
||||
for record in v:
|
||||
for k3 in record:
|
||||
format = formatting.get(k3, missing)
|
||||
if format is None:
|
||||
# Don't output this column.
|
||||
continue
|
||||
if k3 not in headers:
|
||||
headers.append(k3)
|
||||
headers.sort()
|
||||
|
||||
subrows = []
|
||||
for record in v:
|
||||
subrow = []
|
||||
for k3 in headers:
|
||||
v3 = record.get(k3, '')
|
||||
format = formatting.get(k3, missing)
|
||||
if format is None:
|
||||
# Don't output this column.
|
||||
continue
|
||||
if hasattr(format, '__call__'):
|
||||
v3 = format(v3)
|
||||
elif format is not missing:
|
||||
v3 = format % v3
|
||||
subrow.append(v3)
|
||||
subrows.append(subrow)
|
||||
|
||||
return headers, subrows
|
||||
|
||||
if json is not None:
|
||||
@cherrypy.expose
|
||||
def data(self):
|
||||
s = extrapolate_statistics(logging.statistics)
|
||||
cherrypy.response.headers['Content-Type'] = 'application/json'
|
||||
return json.dumps(s, sort_keys=True, indent=4)
|
||||
|
||||
@cherrypy.expose
|
||||
def pause(self, namespace):
|
||||
logging.statistics.get(namespace, {})['Enabled'] = False
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
pause.cp_config = {'tools.allow.on': True,
|
||||
'tools.allow.methods': ['POST']}
|
||||
|
||||
@cherrypy.expose
|
||||
def resume(self, namespace):
|
||||
logging.statistics.get(namespace, {})['Enabled'] = True
|
||||
raise cherrypy.HTTPRedirect('./')
|
||||
resume.cp_config = {'tools.allow.on': True,
|
||||
'tools.allow.methods': ['POST']}
|
|
@ -1,648 +0,0 @@
|
|||
"""Functions for builtin CherryPy tools."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
from hashlib import md5
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes
|
||||
from cherrypy.lib import httputil as _httputil
|
||||
from cherrypy.lib import is_iterator
|
||||
|
||||
|
||||
# Conditional HTTP request support #
|
||||
|
||||
def validate_etags(autotags=False, debug=False):
|
||||
"""Validate the current ETag against If-Match, If-None-Match headers.
|
||||
|
||||
If autotags is True, an ETag response-header value will be provided
|
||||
from an MD5 hash of the response body (unless some other code has
|
||||
already provided an ETag header). If False (the default), the ETag
|
||||
will not be automatic.
|
||||
|
||||
WARNING: the autotags feature is not designed for URL's which allow
|
||||
methods other than GET. For example, if a POST to the same URL returns
|
||||
no content, the automatic ETag will be incorrect, breaking a fundamental
|
||||
use for entity tags in a possibly destructive fashion. Likewise, if you
|
||||
raise 304 Not Modified, the response body will be empty, the ETag hash
|
||||
will be incorrect, and your application will break.
|
||||
See :rfc:`2616` Section 14.24.
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# Guard against being run twice.
|
||||
if hasattr(response, 'ETag'):
|
||||
return
|
||||
|
||||
status, reason, msg = _httputil.valid_status(response.status)
|
||||
|
||||
etag = response.headers.get('ETag')
|
||||
|
||||
# Automatic ETag generation. See warning in docstring.
|
||||
if etag:
|
||||
if debug:
|
||||
cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
|
||||
elif not autotags:
|
||||
if debug:
|
||||
cherrypy.log('Autotags off', 'TOOLS.ETAGS')
|
||||
elif status != 200:
|
||||
if debug:
|
||||
cherrypy.log('Status not 200', 'TOOLS.ETAGS')
|
||||
else:
|
||||
etag = response.collapse_body()
|
||||
etag = '"%s"' % md5(etag).hexdigest()
|
||||
if debug:
|
||||
cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
|
||||
response.headers['ETag'] = etag
|
||||
|
||||
response.ETag = etag
|
||||
|
||||
# "If the request would, without the If-Match header field, result in
|
||||
# anything other than a 2xx or 412 status, then the If-Match header
|
||||
# MUST be ignored."
|
||||
if debug:
|
||||
cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
|
||||
if status >= 200 and status <= 299:
|
||||
request = cherrypy.serving.request
|
||||
|
||||
conditions = request.headers.elements('If-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if debug:
|
||||
cherrypy.log('If-Match conditions: %s' % repr(conditions),
|
||||
'TOOLS.ETAGS')
|
||||
if conditions and not (conditions == ['*'] or etag in conditions):
|
||||
raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
|
||||
'not match %r' % (etag, conditions))
|
||||
|
||||
conditions = request.headers.elements('If-None-Match') or []
|
||||
conditions = [str(x) for x in conditions]
|
||||
if debug:
|
||||
cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
|
||||
'TOOLS.ETAGS')
|
||||
if conditions == ['*'] or etag in conditions:
|
||||
if debug:
|
||||
cherrypy.log('request.method: %s' %
|
||||
request.method, 'TOOLS.ETAGS')
|
||||
if request.method in ('GET', 'HEAD'):
|
||||
raise cherrypy.HTTPRedirect([], 304)
|
||||
else:
|
||||
raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
|
||||
'matched %r' % (etag, conditions))
|
||||
|
||||
|
||||
def validate_since():
|
||||
"""Validate the current Last-Modified against If-Modified-Since headers.
|
||||
|
||||
If no code has set the Last-Modified response header, then no validation
|
||||
will be performed.
|
||||
"""
|
||||
response = cherrypy.serving.response
|
||||
lastmod = response.headers.get('Last-Modified')
|
||||
if lastmod:
|
||||
status, reason, msg = _httputil.valid_status(response.status)
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
since = request.headers.get('If-Unmodified-Since')
|
||||
if since and since != lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 412:
|
||||
raise cherrypy.HTTPError(412)
|
||||
|
||||
since = request.headers.get('If-Modified-Since')
|
||||
if since and since == lastmod:
|
||||
if (status >= 200 and status <= 299) or status == 304:
|
||||
if request.method in ('GET', 'HEAD'):
|
||||
raise cherrypy.HTTPRedirect([], 304)
|
||||
else:
|
||||
raise cherrypy.HTTPError(412)
|
||||
|
||||
|
||||
# Tool code #
|
||||
|
||||
def allow(methods=None, debug=False):
|
||||
"""Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
|
||||
|
||||
The given methods are case-insensitive, and may be in any order.
|
||||
If only one method is allowed, you may supply a single string;
|
||||
if more than one, supply a list of strings.
|
||||
|
||||
Regardless of whether the current method is allowed or not, this
|
||||
also emits an 'Allow' response header, containing the given methods.
|
||||
"""
|
||||
if not isinstance(methods, (tuple, list)):
|
||||
methods = [methods]
|
||||
methods = [m.upper() for m in methods if m]
|
||||
if not methods:
|
||||
methods = ['GET', 'HEAD']
|
||||
elif 'GET' in methods and 'HEAD' not in methods:
|
||||
methods.append('HEAD')
|
||||
|
||||
cherrypy.response.headers['Allow'] = ', '.join(methods)
|
||||
if cherrypy.request.method not in methods:
|
||||
if debug:
|
||||
cherrypy.log('request.method %r not in methods %r' %
|
||||
(cherrypy.request.method, methods), 'TOOLS.ALLOW')
|
||||
raise cherrypy.HTTPError(405)
|
||||
else:
|
||||
if debug:
|
||||
cherrypy.log('request.method %r in methods %r' %
|
||||
(cherrypy.request.method, methods), 'TOOLS.ALLOW')
|
||||
|
||||
|
||||
def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
|
||||
scheme='X-Forwarded-Proto', debug=False):
|
||||
"""Change the base URL (scheme://host[:port][/path]).
|
||||
|
||||
For running a CP server behind Apache, lighttpd, or other HTTP server.
|
||||
|
||||
For Apache and lighttpd, you should leave the 'local' argument at the
|
||||
default value of 'X-Forwarded-Host'. For Squid, you probably want to set
|
||||
tools.proxy.local = 'Origin'.
|
||||
|
||||
If you want the new request.base to include path info (not just the host),
|
||||
you must explicitly set base to the full base path, and ALSO set 'local'
|
||||
to '', so that the X-Forwarded-Host request header (which never includes
|
||||
path info) does not override it. Regardless, the value for 'base' MUST
|
||||
NOT end in a slash.
|
||||
|
||||
cherrypy.request.remote.ip (the IP address of the client) will be
|
||||
rewritten if the header specified by the 'remote' arg is valid.
|
||||
By default, 'remote' is set to 'X-Forwarded-For'. If you do not
|
||||
want to rewrite remote.ip, set the 'remote' arg to an empty string.
|
||||
"""
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
if scheme:
|
||||
s = request.headers.get(scheme, None)
|
||||
if debug:
|
||||
cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
|
||||
if s == 'on' and 'ssl' in scheme.lower():
|
||||
# This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
|
||||
scheme = 'https'
|
||||
else:
|
||||
# This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
|
||||
scheme = s
|
||||
if not scheme:
|
||||
scheme = request.base[:request.base.find('://')]
|
||||
|
||||
if local:
|
||||
lbase = request.headers.get(local, None)
|
||||
if debug:
|
||||
cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
|
||||
if lbase is not None:
|
||||
base = lbase.split(',')[0]
|
||||
if not base:
|
||||
base = request.headers.get('Host', '127.0.0.1')
|
||||
port = request.local.port
|
||||
if port != 80:
|
||||
base += ':%s' % port
|
||||
|
||||
if base.find('://') == -1:
|
||||
# add http:// or https:// if needed
|
||||
base = scheme + '://' + base
|
||||
|
||||
request.base = base
|
||||
|
||||
if remote:
|
||||
xff = request.headers.get(remote)
|
||||
if debug:
|
||||
cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
|
||||
if xff:
|
||||
if remote == 'X-Forwarded-For':
|
||||
# Bug #1268
|
||||
xff = xff.split(',')[0].strip()
|
||||
request.remote.ip = xff
|
||||
|
||||
|
||||
def ignore_headers(headers=('Range',), debug=False):
|
||||
"""Delete request headers whose field names are included in 'headers'.
|
||||
|
||||
This is a useful tool for working behind certain HTTP servers;
|
||||
for example, Apache duplicates the work that CP does for 'Range'
|
||||
headers, and will doubly-truncate the response.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
for name in headers:
|
||||
if name in request.headers:
|
||||
if debug:
|
||||
cherrypy.log('Ignoring request header %r' % name,
|
||||
'TOOLS.IGNORE_HEADERS')
|
||||
del request.headers[name]
|
||||
|
||||
|
||||
def response_headers(headers=None, debug=False):
|
||||
"""Set headers on the response."""
|
||||
if debug:
|
||||
cherrypy.log('Setting response headers: %s' % repr(headers),
|
||||
'TOOLS.RESPONSE_HEADERS')
|
||||
for name, value in (headers or []):
|
||||
cherrypy.serving.response.headers[name] = value
|
||||
response_headers.failsafe = True
|
||||
|
||||
|
||||
def referer(pattern, accept=True, accept_missing=False, error=403,
|
||||
message='Forbidden Referer header.', debug=False):
|
||||
"""Raise HTTPError if Referer header does/does not match the given pattern.
|
||||
|
||||
pattern
|
||||
A regular expression pattern to test against the Referer.
|
||||
|
||||
accept
|
||||
If True, the Referer must match the pattern; if False,
|
||||
the Referer must NOT match the pattern.
|
||||
|
||||
accept_missing
|
||||
If True, permit requests with no Referer header.
|
||||
|
||||
error
|
||||
The HTTP error code to return to the client on failure.
|
||||
|
||||
message
|
||||
A string to include in the response body on failure.
|
||||
|
||||
"""
|
||||
try:
|
||||
ref = cherrypy.serving.request.headers['Referer']
|
||||
match = bool(re.match(pattern, ref))
|
||||
if debug:
|
||||
cherrypy.log('Referer %r matches %r' % (ref, pattern),
|
||||
'TOOLS.REFERER')
|
||||
if accept == match:
|
||||
return
|
||||
except KeyError:
|
||||
if debug:
|
||||
cherrypy.log('No Referer header', 'TOOLS.REFERER')
|
||||
if accept_missing:
|
||||
return
|
||||
|
||||
raise cherrypy.HTTPError(error, message)
|
||||
|
||||
|
||||
class SessionAuth(object):
|
||||
|
||||
"""Assert that the user is logged in."""
|
||||
|
||||
session_key = 'username'
|
||||
debug = False
|
||||
|
||||
def check_username_and_password(self, username, password):
|
||||
pass
|
||||
|
||||
def anonymous(self):
|
||||
"""Provide a temporary user name for anonymous users."""
|
||||
pass
|
||||
|
||||
def on_login(self, username):
|
||||
pass
|
||||
|
||||
def on_logout(self, username):
|
||||
pass
|
||||
|
||||
def on_check(self, username):
|
||||
pass
|
||||
|
||||
def login_screen(self, from_page='..', username='', error_msg='',
|
||||
**kwargs):
|
||||
return (six.text_type("""<html><body>
|
||||
Message: %(error_msg)s
|
||||
<form method="post" action="do_login">
|
||||
Login: <input type="text" name="username" value="%(username)s" size="10" />
|
||||
<br />
|
||||
Password: <input type="password" name="password" size="10" />
|
||||
<br />
|
||||
<input type="hidden" name="from_page" value="%(from_page)s" />
|
||||
<br />
|
||||
<input type="submit" />
|
||||
</form>
|
||||
</body></html>""") % vars()).encode('utf-8')
|
||||
|
||||
def do_login(self, username, password, from_page='..', **kwargs):
|
||||
"""Login. May raise redirect, or return True if request handled."""
|
||||
response = cherrypy.serving.response
|
||||
error_msg = self.check_username_and_password(username, password)
|
||||
if error_msg:
|
||||
body = self.login_screen(from_page, username, error_msg)
|
||||
response.body = body
|
||||
if 'Content-Length' in response.headers:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers['Content-Length']
|
||||
return True
|
||||
else:
|
||||
cherrypy.serving.request.login = username
|
||||
cherrypy.session[self.session_key] = username
|
||||
self.on_login(username)
|
||||
raise cherrypy.HTTPRedirect(from_page or '/')
|
||||
|
||||
def do_logout(self, from_page='..', **kwargs):
|
||||
"""Logout. May raise redirect, or return True if request handled."""
|
||||
sess = cherrypy.session
|
||||
username = sess.get(self.session_key)
|
||||
sess[self.session_key] = None
|
||||
if username:
|
||||
cherrypy.serving.request.login = None
|
||||
self.on_logout(username)
|
||||
raise cherrypy.HTTPRedirect(from_page)
|
||||
|
||||
def do_check(self):
|
||||
"""Assert username. Raise redirect, or return True if request handled.
|
||||
"""
|
||||
sess = cherrypy.session
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
username = sess.get(self.session_key)
|
||||
if not username:
|
||||
sess[self.session_key] = username = self.anonymous()
|
||||
self._debug_message('No session[username], trying anonymous')
|
||||
if not username:
|
||||
url = cherrypy.url(qs=request.query_string)
|
||||
self._debug_message(
|
||||
'No username, routing to login_screen with from_page %(url)r',
|
||||
locals(),
|
||||
)
|
||||
response.body = self.login_screen(url)
|
||||
if 'Content-Length' in response.headers:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers['Content-Length']
|
||||
return True
|
||||
self._debug_message('Setting request.login to %(username)r', locals())
|
||||
request.login = username
|
||||
self.on_check(username)
|
||||
|
||||
def _debug_message(self, template, context={}):
|
||||
if not self.debug:
|
||||
return
|
||||
cherrypy.log(template % context, 'TOOLS.SESSAUTH')
|
||||
|
||||
def run(self):
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
path = request.path_info
|
||||
if path.endswith('login_screen'):
|
||||
self._debug_message('routing %(path)r to login_screen', locals())
|
||||
response.body = self.login_screen()
|
||||
return True
|
||||
elif path.endswith('do_login'):
|
||||
if request.method != 'POST':
|
||||
response.headers['Allow'] = 'POST'
|
||||
self._debug_message('do_login requires POST')
|
||||
raise cherrypy.HTTPError(405)
|
||||
self._debug_message('routing %(path)r to do_login', locals())
|
||||
return self.do_login(**request.params)
|
||||
elif path.endswith('do_logout'):
|
||||
if request.method != 'POST':
|
||||
response.headers['Allow'] = 'POST'
|
||||
raise cherrypy.HTTPError(405)
|
||||
self._debug_message('routing %(path)r to do_logout', locals())
|
||||
return self.do_logout(**request.params)
|
||||
else:
|
||||
self._debug_message('No special path, running do_check')
|
||||
return self.do_check()
|
||||
|
||||
|
||||
def session_auth(**kwargs):
|
||||
sa = SessionAuth()
|
||||
for k, v in kwargs.items():
|
||||
setattr(sa, k, v)
|
||||
return sa.run()
|
||||
session_auth.__doc__ = """Session authentication hook.
|
||||
|
||||
Any attribute of the SessionAuth class may be overridden via a keyword arg
|
||||
to this function:
|
||||
|
||||
""" + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
|
||||
for k in dir(SessionAuth) if not k.startswith('__')])
|
||||
|
||||
|
||||
def log_traceback(severity=logging.ERROR, debug=False):
|
||||
"""Write the last error's traceback to the cherrypy error log."""
|
||||
cherrypy.log('', 'HTTP', severity=severity, traceback=True)
|
||||
|
||||
|
||||
def log_request_headers(debug=False):
|
||||
"""Write request headers to the cherrypy error log."""
|
||||
h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
|
||||
cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
|
||||
|
||||
|
||||
def log_hooks(debug=False):
|
||||
"""Write request.hooks to the cherrypy error log."""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
msg = []
|
||||
# Sort by the standard points if possible.
|
||||
from cherrypy import _cprequest
|
||||
points = _cprequest.hookpoints
|
||||
for k in request.hooks.keys():
|
||||
if k not in points:
|
||||
points.append(k)
|
||||
|
||||
for k in points:
|
||||
msg.append(' %s:' % k)
|
||||
v = request.hooks.get(k, [])
|
||||
v.sort()
|
||||
for h in v:
|
||||
msg.append(' %r' % h)
|
||||
cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
|
||||
':\n' + '\n'.join(msg), 'HTTP')
|
||||
|
||||
|
||||
def redirect(url='', internal=True, debug=False):
|
||||
"""Raise InternalRedirect or HTTPRedirect to the given url."""
|
||||
if debug:
|
||||
cherrypy.log('Redirecting %sto: %s' %
|
||||
({True: 'internal ', False: ''}[internal], url),
|
||||
'TOOLS.REDIRECT')
|
||||
if internal:
|
||||
raise cherrypy.InternalRedirect(url)
|
||||
else:
|
||||
raise cherrypy.HTTPRedirect(url)
|
||||
|
||||
|
||||
def trailing_slash(missing=True, extra=False, status=None, debug=False):
|
||||
"""Redirect if path_info has (missing|extra) trailing slash."""
|
||||
request = cherrypy.serving.request
|
||||
pi = request.path_info
|
||||
|
||||
if debug:
|
||||
cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
|
||||
(request.is_index, missing, extra, pi),
|
||||
'TOOLS.TRAILING_SLASH')
|
||||
if request.is_index is True:
|
||||
if missing:
|
||||
if not pi.endswith('/'):
|
||||
new_url = cherrypy.url(pi + '/', request.query_string)
|
||||
raise cherrypy.HTTPRedirect(new_url, status=status or 301)
|
||||
elif request.is_index is False:
|
||||
if extra:
|
||||
# If pi == '/', don't redirect to ''!
|
||||
if pi.endswith('/') and pi != '/':
|
||||
new_url = cherrypy.url(pi[:-1], request.query_string)
|
||||
raise cherrypy.HTTPRedirect(new_url, status=status or 301)
|
||||
|
||||
|
||||
def flatten(debug=False):
|
||||
"""Wrap response.body in a generator that recursively iterates over body.
|
||||
|
||||
This allows cherrypy.response.body to consist of 'nested generators';
|
||||
that is, a set of generators that yield generators.
|
||||
"""
|
||||
def flattener(input):
|
||||
numchunks = 0
|
||||
for x in input:
|
||||
if not is_iterator(x):
|
||||
numchunks += 1
|
||||
yield x
|
||||
else:
|
||||
for y in flattener(x):
|
||||
numchunks += 1
|
||||
yield y
|
||||
if debug:
|
||||
cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
|
||||
response = cherrypy.serving.response
|
||||
response.body = flattener(response.body)
|
||||
|
||||
|
||||
def accept(media=None, debug=False):
|
||||
"""Return the client's preferred media-type (from the given Content-Types).
|
||||
|
||||
If 'media' is None (the default), no test will be performed.
|
||||
|
||||
If 'media' is provided, it should be the Content-Type value (as a string)
|
||||
or values (as a list or tuple of strings) which the current resource
|
||||
can emit. The client's acceptable media ranges (as declared in the
|
||||
Accept request header) will be matched in order to these Content-Type
|
||||
values; the first such string is returned. That is, the return value
|
||||
will always be one of the strings provided in the 'media' arg (or None
|
||||
if 'media' is None).
|
||||
|
||||
If no match is found, then HTTPError 406 (Not Acceptable) is raised.
|
||||
Note that most web browsers send */* as a (low-quality) acceptable
|
||||
media range, which should match any Content-Type. In addition, "...if
|
||||
no Accept header field is present, then it is assumed that the client
|
||||
accepts all media types."
|
||||
|
||||
Matching types are checked in order of client preference first,
|
||||
and then in the order of the given 'media' values.
|
||||
|
||||
Note that this function does not honor accept-params (other than "q").
|
||||
"""
|
||||
if not media:
|
||||
return
|
||||
if isinstance(media, text_or_bytes):
|
||||
media = [media]
|
||||
request = cherrypy.serving.request
|
||||
|
||||
# Parse the Accept request header, and try to match one
|
||||
# of the requested media-ranges (in order of preference).
|
||||
ranges = request.headers.elements('Accept')
|
||||
if not ranges:
|
||||
# Any media type is acceptable.
|
||||
if debug:
|
||||
cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
|
||||
return media[0]
|
||||
else:
|
||||
# Note that 'ranges' is sorted in order of preference
|
||||
for element in ranges:
|
||||
if element.qvalue > 0:
|
||||
if element.value == '*/*':
|
||||
# Matches any type or subtype
|
||||
if debug:
|
||||
cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
|
||||
return media[0]
|
||||
elif element.value.endswith('/*'):
|
||||
# Matches any subtype
|
||||
mtype = element.value[:-1] # Keep the slash
|
||||
for m in media:
|
||||
if m.startswith(mtype):
|
||||
if debug:
|
||||
cherrypy.log('Match due to %s' % element.value,
|
||||
'TOOLS.ACCEPT')
|
||||
return m
|
||||
else:
|
||||
# Matches exact value
|
||||
if element.value in media:
|
||||
if debug:
|
||||
cherrypy.log('Match due to %s' % element.value,
|
||||
'TOOLS.ACCEPT')
|
||||
return element.value
|
||||
|
||||
# No suitable media-range found.
|
||||
ah = request.headers.get('Accept')
|
||||
if ah is None:
|
||||
msg = 'Your client did not send an Accept header.'
|
||||
else:
|
||||
msg = 'Your client sent this Accept header: %s.' % ah
|
||||
msg += (' But this resource only emits these media types: %s.' %
|
||||
', '.join(media))
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
|
||||
class MonitoredHeaderMap(_httputil.HeaderMap):
|
||||
|
||||
def __init__(self):
|
||||
self.accessed_headers = set()
|
||||
|
||||
def __getitem__(self, key):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.__getitem__(self, key)
|
||||
|
||||
def __contains__(self, key):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.__contains__(self, key)
|
||||
|
||||
def get(self, key, default=None):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.get(self, key, default=default)
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
# Python 2
|
||||
def has_key(self, key):
|
||||
self.accessed_headers.add(key)
|
||||
return _httputil.HeaderMap.has_key(self, key)
|
||||
|
||||
|
||||
def autovary(ignore=None, debug=False):
|
||||
"""Auto-populate the Vary response header based on request.header access.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
|
||||
req_h = request.headers
|
||||
request.headers = MonitoredHeaderMap()
|
||||
request.headers.update(req_h)
|
||||
if ignore is None:
|
||||
ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
|
||||
|
||||
def set_response_header():
|
||||
resp_h = cherrypy.serving.response.headers
|
||||
v = set([e.value for e in resp_h.elements('Vary')])
|
||||
if debug:
|
||||
cherrypy.log(
|
||||
'Accessed headers: %s' % request.headers.accessed_headers,
|
||||
'TOOLS.AUTOVARY')
|
||||
v = v.union(request.headers.accessed_headers)
|
||||
v = v.difference(ignore)
|
||||
v = list(v)
|
||||
v.sort()
|
||||
resp_h['Vary'] = ', '.join(v)
|
||||
request.hooks.attach('before_finalize', set_response_header, 95)
|
||||
|
||||
|
||||
def convert_params(exception=ValueError, error=400):
|
||||
"""Convert request params based on function annotations, with error handling.
|
||||
|
||||
exception
|
||||
Exception class to catch.
|
||||
|
||||
status
|
||||
The HTTP error code to return to the client on failure.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
types = request.handler.callable.__annotations__
|
||||
with cherrypy.HTTPError.handle(exception, error):
|
||||
for key in set(types).intersection(request.params):
|
||||
request.params[key] = types[key](request.params[key])
|
|
@ -1,424 +0,0 @@
|
|||
import struct
|
||||
import time
|
||||
import io
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes, ntob
|
||||
from cherrypy.lib import file_generator
|
||||
from cherrypy.lib import is_closable_iterator
|
||||
from cherrypy.lib import set_vary_header
|
||||
|
||||
|
||||
def decode(encoding=None, default_encoding='utf-8'):
|
||||
"""Replace or extend the list of charsets used to decode a request entity.
|
||||
|
||||
Either argument may be a single string or a list of strings.
|
||||
|
||||
encoding
|
||||
If not None, restricts the set of charsets attempted while decoding
|
||||
a request entity to the given set (even if a different charset is
|
||||
given in the Content-Type request header).
|
||||
|
||||
default_encoding
|
||||
Only in effect if the 'encoding' argument is not given.
|
||||
If given, the set of charsets attempted while decoding a request
|
||||
entity is *extended* with the given value(s).
|
||||
|
||||
"""
|
||||
body = cherrypy.request.body
|
||||
if encoding is not None:
|
||||
if not isinstance(encoding, list):
|
||||
encoding = [encoding]
|
||||
body.attempt_charsets = encoding
|
||||
elif default_encoding:
|
||||
if not isinstance(default_encoding, list):
|
||||
default_encoding = [default_encoding]
|
||||
body.attempt_charsets = body.attempt_charsets + default_encoding
|
||||
|
||||
class UTF8StreamEncoder:
|
||||
def __init__(self, iterator):
|
||||
self._iterator = iterator
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def next(self):
|
||||
return self.__next__()
|
||||
|
||||
def __next__(self):
|
||||
res = next(self._iterator)
|
||||
if isinstance(res, six.text_type):
|
||||
res = res.encode('utf-8')
|
||||
return res
|
||||
|
||||
def close(self):
|
||||
if is_closable_iterator(self._iterator):
|
||||
self._iterator.close()
|
||||
|
||||
def __getattr__(self, attr):
|
||||
if attr.startswith('__'):
|
||||
raise AttributeError(self, attr)
|
||||
return getattr(self._iterator, attr)
|
||||
|
||||
|
||||
class ResponseEncoder:
|
||||
|
||||
default_encoding = 'utf-8'
|
||||
failmsg = 'Response body could not be encoded with %r.'
|
||||
encoding = None
|
||||
errors = 'strict'
|
||||
text_only = True
|
||||
add_charset = True
|
||||
debug = False
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
self.attempted_charsets = set()
|
||||
request = cherrypy.serving.request
|
||||
if request.handler is not None:
|
||||
# Replace request.handler with self
|
||||
if self.debug:
|
||||
cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
|
||||
self.oldhandler = request.handler
|
||||
request.handler = self
|
||||
|
||||
def encode_stream(self, encoding):
|
||||
"""Encode a streaming response body.
|
||||
|
||||
Use a generator wrapper, and just pray it works as the stream is
|
||||
being written out.
|
||||
"""
|
||||
if encoding in self.attempted_charsets:
|
||||
return False
|
||||
self.attempted_charsets.add(encoding)
|
||||
|
||||
def encoder(body):
|
||||
for chunk in body:
|
||||
if isinstance(chunk, six.text_type):
|
||||
chunk = chunk.encode(encoding, self.errors)
|
||||
yield chunk
|
||||
self.body = encoder(self.body)
|
||||
return True
|
||||
|
||||
def encode_string(self, encoding):
|
||||
"""Encode a buffered response body."""
|
||||
if encoding in self.attempted_charsets:
|
||||
return False
|
||||
self.attempted_charsets.add(encoding)
|
||||
body = []
|
||||
for chunk in self.body:
|
||||
if isinstance(chunk, six.text_type):
|
||||
try:
|
||||
chunk = chunk.encode(encoding, self.errors)
|
||||
except (LookupError, UnicodeError):
|
||||
return False
|
||||
body.append(chunk)
|
||||
self.body = body
|
||||
return True
|
||||
|
||||
def find_acceptable_charset(self):
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
if self.debug:
|
||||
cherrypy.log('response.stream %r' %
|
||||
response.stream, 'TOOLS.ENCODE')
|
||||
if response.stream:
|
||||
encoder = self.encode_stream
|
||||
else:
|
||||
encoder = self.encode_string
|
||||
if 'Content-Length' in response.headers:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
# Encoded strings may be of different lengths from their
|
||||
# unicode equivalents, and even from each other. For example:
|
||||
# >>> t = u"\u7007\u3040"
|
||||
# >>> len(t)
|
||||
# 2
|
||||
# >>> len(t.encode("UTF-8"))
|
||||
# 6
|
||||
# >>> len(t.encode("utf7"))
|
||||
# 8
|
||||
del response.headers['Content-Length']
|
||||
|
||||
# Parse the Accept-Charset request header, and try to provide one
|
||||
# of the requested charsets (in order of user preference).
|
||||
encs = request.headers.elements('Accept-Charset')
|
||||
charsets = [enc.value.lower() for enc in encs]
|
||||
if self.debug:
|
||||
cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
|
||||
|
||||
if self.encoding is not None:
|
||||
# If specified, force this encoding to be used, or fail.
|
||||
encoding = self.encoding.lower()
|
||||
if self.debug:
|
||||
cherrypy.log('Specified encoding %r' %
|
||||
encoding, 'TOOLS.ENCODE')
|
||||
if (not charsets) or '*' in charsets or encoding in charsets:
|
||||
if self.debug:
|
||||
cherrypy.log('Attempting encoding %r' %
|
||||
encoding, 'TOOLS.ENCODE')
|
||||
if encoder(encoding):
|
||||
return encoding
|
||||
else:
|
||||
if not encs:
|
||||
if self.debug:
|
||||
cherrypy.log('Attempting default encoding %r' %
|
||||
self.default_encoding, 'TOOLS.ENCODE')
|
||||
# Any character-set is acceptable.
|
||||
if encoder(self.default_encoding):
|
||||
return self.default_encoding
|
||||
else:
|
||||
raise cherrypy.HTTPError(500, self.failmsg %
|
||||
self.default_encoding)
|
||||
else:
|
||||
for element in encs:
|
||||
if element.qvalue > 0:
|
||||
if element.value == '*':
|
||||
# Matches any charset. Try our default.
|
||||
if self.debug:
|
||||
cherrypy.log('Attempting default encoding due '
|
||||
'to %r' % element, 'TOOLS.ENCODE')
|
||||
if encoder(self.default_encoding):
|
||||
return self.default_encoding
|
||||
else:
|
||||
encoding = element.value
|
||||
if self.debug:
|
||||
cherrypy.log('Attempting encoding %s (qvalue >'
|
||||
'0)' % element, 'TOOLS.ENCODE')
|
||||
if encoder(encoding):
|
||||
return encoding
|
||||
|
||||
if '*' not in charsets:
|
||||
# If no "*" is present in an Accept-Charset field, then all
|
||||
# character sets not explicitly mentioned get a quality
|
||||
# value of 0, except for ISO-8859-1, which gets a quality
|
||||
# value of 1 if not explicitly mentioned.
|
||||
iso = 'iso-8859-1'
|
||||
if iso not in charsets:
|
||||
if self.debug:
|
||||
cherrypy.log('Attempting ISO-8859-1 encoding',
|
||||
'TOOLS.ENCODE')
|
||||
if encoder(iso):
|
||||
return iso
|
||||
|
||||
# No suitable encoding found.
|
||||
ac = request.headers.get('Accept-Charset')
|
||||
if ac is None:
|
||||
msg = 'Your client did not send an Accept-Charset header.'
|
||||
else:
|
||||
msg = 'Your client sent this Accept-Charset header: %s.' % ac
|
||||
_charsets = ', '.join(sorted(self.attempted_charsets))
|
||||
msg += ' We tried these charsets: %s.' % (_charsets,)
|
||||
raise cherrypy.HTTPError(406, msg)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
response = cherrypy.serving.response
|
||||
self.body = self.oldhandler(*args, **kwargs)
|
||||
|
||||
if isinstance(self.body, text_or_bytes):
|
||||
# strings get wrapped in a list because iterating over a single
|
||||
# item list is much faster than iterating over every character
|
||||
# in a long string.
|
||||
if self.body:
|
||||
self.body = [self.body]
|
||||
else:
|
||||
# [''] doesn't evaluate to False, so replace it with [].
|
||||
self.body = []
|
||||
elif hasattr(self.body, 'read'):
|
||||
self.body = file_generator(self.body)
|
||||
elif self.body is None:
|
||||
self.body = []
|
||||
|
||||
ct = response.headers.elements('Content-Type')
|
||||
if self.debug:
|
||||
cherrypy.log('Content-Type: %r' % [str(h)
|
||||
for h in ct], 'TOOLS.ENCODE')
|
||||
if ct and self.add_charset:
|
||||
ct = ct[0]
|
||||
if self.text_only:
|
||||
if ct.value.lower().startswith('text/'):
|
||||
if self.debug:
|
||||
cherrypy.log(
|
||||
'Content-Type %s starts with "text/"' % ct,
|
||||
'TOOLS.ENCODE')
|
||||
do_find = True
|
||||
else:
|
||||
if self.debug:
|
||||
cherrypy.log('Not finding because Content-Type %s '
|
||||
'does not start with "text/"' % ct,
|
||||
'TOOLS.ENCODE')
|
||||
do_find = False
|
||||
else:
|
||||
if self.debug:
|
||||
cherrypy.log('Finding because not text_only',
|
||||
'TOOLS.ENCODE')
|
||||
do_find = True
|
||||
|
||||
if do_find:
|
||||
# Set "charset=..." param on response Content-Type header
|
||||
ct.params['charset'] = self.find_acceptable_charset()
|
||||
if self.debug:
|
||||
cherrypy.log('Setting Content-Type %s' % ct,
|
||||
'TOOLS.ENCODE')
|
||||
response.headers['Content-Type'] = str(ct)
|
||||
|
||||
return self.body
|
||||
|
||||
# GZIP
|
||||
|
||||
|
||||
def compress(body, compress_level):
|
||||
"""Compress 'body' at the given compress_level."""
|
||||
import zlib
|
||||
|
||||
# See http://www.gzip.org/zlib/rfc-gzip.html
|
||||
yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker
|
||||
yield ntob('\x08') # CM: compression method
|
||||
yield ntob('\x00') # FLG: none set
|
||||
# MTIME: 4 bytes
|
||||
yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
|
||||
yield ntob('\x02') # XFL: max compression, slowest algo
|
||||
yield ntob('\xff') # OS: unknown
|
||||
|
||||
crc = zlib.crc32(ntob(''))
|
||||
size = 0
|
||||
zobj = zlib.compressobj(compress_level,
|
||||
zlib.DEFLATED, -zlib.MAX_WBITS,
|
||||
zlib.DEF_MEM_LEVEL, 0)
|
||||
for line in body:
|
||||
size += len(line)
|
||||
crc = zlib.crc32(line, crc)
|
||||
yield zobj.compress(line)
|
||||
yield zobj.flush()
|
||||
|
||||
# CRC32: 4 bytes
|
||||
yield struct.pack('<L', crc & int('FFFFFFFF', 16))
|
||||
# ISIZE: 4 bytes
|
||||
yield struct.pack('<L', size & int('FFFFFFFF', 16))
|
||||
|
||||
|
||||
def decompress(body):
|
||||
import gzip
|
||||
|
||||
zbuf = io.BytesIO()
|
||||
zbuf.write(body)
|
||||
zbuf.seek(0)
|
||||
zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
|
||||
data = zfile.read()
|
||||
zfile.close()
|
||||
return data
|
||||
|
||||
|
||||
def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
|
||||
debug=False):
|
||||
"""Try to gzip the response body if Content-Type in mime_types.
|
||||
|
||||
cherrypy.response.headers['Content-Type'] must be set to one of the
|
||||
values in the mime_types arg before calling this function.
|
||||
|
||||
The provided list of mime-types must be of one of the following form:
|
||||
* type/subtype
|
||||
* type/*
|
||||
* type/*+subtype
|
||||
|
||||
No compression is performed if any of the following hold:
|
||||
* The client sends no Accept-Encoding request header
|
||||
* No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
|
||||
* No 'gzip' or 'x-gzip' with a qvalue > 0 is present
|
||||
* The 'identity' value is given with a qvalue > 0.
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
set_vary_header(response, 'Accept-Encoding')
|
||||
|
||||
if not response.body:
|
||||
# Response body is empty (might be a 304 for instance)
|
||||
if debug:
|
||||
cherrypy.log('No response body', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
# If returning cached content (which should already have been gzipped),
|
||||
# don't re-zip.
|
||||
if getattr(request, 'cached', False):
|
||||
if debug:
|
||||
cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
acceptable = request.headers.elements('Accept-Encoding')
|
||||
if not acceptable:
|
||||
# If no Accept-Encoding field is present in a request,
|
||||
# the server MAY assume that the client will accept any
|
||||
# content coding. In this case, if "identity" is one of
|
||||
# the available content-codings, then the server SHOULD use
|
||||
# the "identity" content-coding, unless it has additional
|
||||
# information that a different content-coding is meaningful
|
||||
# to the client.
|
||||
if debug:
|
||||
cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
ct = response.headers.get('Content-Type', '').split(';')[0]
|
||||
for coding in acceptable:
|
||||
if coding.value == 'identity' and coding.qvalue != 0:
|
||||
if debug:
|
||||
cherrypy.log('Non-zero identity qvalue: %s' % coding,
|
||||
context='TOOLS.GZIP')
|
||||
return
|
||||
if coding.value in ('gzip', 'x-gzip'):
|
||||
if coding.qvalue == 0:
|
||||
if debug:
|
||||
cherrypy.log('Zero gzip qvalue: %s' % coding,
|
||||
context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
if ct not in mime_types:
|
||||
# If the list of provided mime-types contains tokens
|
||||
# such as 'text/*' or 'application/*+xml',
|
||||
# we go through them and find the most appropriate one
|
||||
# based on the given content-type.
|
||||
# The pattern matching is only caring about the most
|
||||
# common cases, as stated above, and doesn't support
|
||||
# for extra parameters.
|
||||
found = False
|
||||
if '/' in ct:
|
||||
ct_media_type, ct_sub_type = ct.split('/')
|
||||
for mime_type in mime_types:
|
||||
if '/' in mime_type:
|
||||
media_type, sub_type = mime_type.split('/')
|
||||
if ct_media_type == media_type:
|
||||
if sub_type == '*':
|
||||
found = True
|
||||
break
|
||||
elif '+' in sub_type and '+' in ct_sub_type:
|
||||
ct_left, ct_right = ct_sub_type.split('+')
|
||||
left, right = sub_type.split('+')
|
||||
if left == '*' and ct_right == right:
|
||||
found = True
|
||||
break
|
||||
|
||||
if not found:
|
||||
if debug:
|
||||
cherrypy.log('Content-Type %s not in mime_types %r' %
|
||||
(ct, mime_types), context='TOOLS.GZIP')
|
||||
return
|
||||
|
||||
if debug:
|
||||
cherrypy.log('Gzipping', context='TOOLS.GZIP')
|
||||
# Return a generator that compresses the page
|
||||
response.headers['Content-Encoding'] = 'gzip'
|
||||
response.body = compress(response.body, compress_level)
|
||||
if 'Content-Length' in response.headers:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers['Content-Length']
|
||||
|
||||
return
|
||||
|
||||
if debug:
|
||||
cherrypy.log('No acceptable encoding found.', context='GZIP')
|
||||
cherrypy.HTTPError(406, 'identity, gzip').set_response()
|
|
@ -1,216 +0,0 @@
|
|||
import gc
|
||||
import inspect
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
import objgraph
|
||||
except ImportError:
|
||||
objgraph = None
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import _cprequest, _cpwsgi
|
||||
from cherrypy.process.plugins import SimplePlugin
|
||||
|
||||
|
||||
class ReferrerTree(object):
|
||||
|
||||
"""An object which gathers all referrers of an object to a given depth."""
|
||||
|
||||
peek_length = 40
|
||||
|
||||
def __init__(self, ignore=None, maxdepth=2, maxparents=10):
|
||||
self.ignore = ignore or []
|
||||
self.ignore.append(inspect.currentframe().f_back)
|
||||
self.maxdepth = maxdepth
|
||||
self.maxparents = maxparents
|
||||
|
||||
def ascend(self, obj, depth=1):
|
||||
"""Return a nested list containing referrers of the given object."""
|
||||
depth += 1
|
||||
parents = []
|
||||
|
||||
# Gather all referrers in one step to minimize
|
||||
# cascading references due to repr() logic.
|
||||
refs = gc.get_referrers(obj)
|
||||
self.ignore.append(refs)
|
||||
if len(refs) > self.maxparents:
|
||||
return [('[%s referrers]' % len(refs), [])]
|
||||
|
||||
try:
|
||||
ascendcode = self.ascend.__code__
|
||||
except AttributeError:
|
||||
ascendcode = self.ascend.im_func.func_code
|
||||
for parent in refs:
|
||||
if inspect.isframe(parent) and parent.f_code is ascendcode:
|
||||
continue
|
||||
if parent in self.ignore:
|
||||
continue
|
||||
if depth <= self.maxdepth:
|
||||
parents.append((parent, self.ascend(parent, depth)))
|
||||
else:
|
||||
parents.append((parent, []))
|
||||
|
||||
return parents
|
||||
|
||||
def peek(self, s):
|
||||
"""Return s, restricted to a sane length."""
|
||||
if len(s) > (self.peek_length + 3):
|
||||
half = self.peek_length // 2
|
||||
return s[:half] + '...' + s[-half:]
|
||||
else:
|
||||
return s
|
||||
|
||||
def _format(self, obj, descend=True):
|
||||
"""Return a string representation of a single object."""
|
||||
if inspect.isframe(obj):
|
||||
filename, lineno, func, context, index = inspect.getframeinfo(obj)
|
||||
return "<frame of function '%s'>" % func
|
||||
|
||||
if not descend:
|
||||
return self.peek(repr(obj))
|
||||
|
||||
if isinstance(obj, dict):
|
||||
return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
|
||||
self._format(v, descend=False))
|
||||
for k, v in obj.items()]) + '}'
|
||||
elif isinstance(obj, list):
|
||||
return '[' + ', '.join([self._format(item, descend=False)
|
||||
for item in obj]) + ']'
|
||||
elif isinstance(obj, tuple):
|
||||
return '(' + ', '.join([self._format(item, descend=False)
|
||||
for item in obj]) + ')'
|
||||
|
||||
r = self.peek(repr(obj))
|
||||
if isinstance(obj, (str, int, float)):
|
||||
return r
|
||||
return '%s: %s' % (type(obj), r)
|
||||
|
||||
def format(self, tree):
|
||||
"""Return a list of string reprs from a nested list of referrers."""
|
||||
output = []
|
||||
|
||||
def ascend(branch, depth=1):
|
||||
for parent, grandparents in branch:
|
||||
output.append((' ' * depth) + self._format(parent))
|
||||
if grandparents:
|
||||
ascend(grandparents, depth + 1)
|
||||
ascend(tree)
|
||||
return output
|
||||
|
||||
|
||||
def get_instances(cls):
|
||||
return [x for x in gc.get_objects() if isinstance(x, cls)]
|
||||
|
||||
|
||||
class RequestCounter(SimplePlugin):
|
||||
|
||||
def start(self):
|
||||
self.count = 0
|
||||
|
||||
def before_request(self):
|
||||
self.count += 1
|
||||
|
||||
def after_request(self):
|
||||
self.count -= 1
|
||||
request_counter = RequestCounter(cherrypy.engine)
|
||||
request_counter.subscribe()
|
||||
|
||||
|
||||
def get_context(obj):
|
||||
if isinstance(obj, _cprequest.Request):
|
||||
return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
|
||||
elif isinstance(obj, _cprequest.Response):
|
||||
return 'status=%s' % obj.status
|
||||
elif isinstance(obj, _cpwsgi.AppResponse):
|
||||
return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
|
||||
elif hasattr(obj, 'tb_lineno'):
|
||||
return 'tb_lineno=%s' % obj.tb_lineno
|
||||
return ''
|
||||
|
||||
|
||||
class GCRoot(object):
|
||||
|
||||
"""A CherryPy page handler for testing reference leaks."""
|
||||
|
||||
classes = [
|
||||
(_cprequest.Request, 2, 2,
|
||||
'Should be 1 in this request thread and 1 in the main thread.'),
|
||||
(_cprequest.Response, 2, 2,
|
||||
'Should be 1 in this request thread and 1 in the main thread.'),
|
||||
(_cpwsgi.AppResponse, 1, 1,
|
||||
'Should be 1 in this request thread only.'),
|
||||
]
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'Hello, world!'
|
||||
|
||||
@cherrypy.expose
|
||||
def stats(self):
|
||||
output = ['Statistics:']
|
||||
|
||||
for trial in range(10):
|
||||
if request_counter.count > 0:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
output.append('\nNot all requests closed properly.')
|
||||
|
||||
# gc_collect isn't perfectly synchronous, because it may
|
||||
# break reference cycles that then take time to fully
|
||||
# finalize. Call it thrice and hope for the best.
|
||||
gc.collect()
|
||||
gc.collect()
|
||||
unreachable = gc.collect()
|
||||
if unreachable:
|
||||
if objgraph is not None:
|
||||
final = objgraph.by_type('Nondestructible')
|
||||
if final:
|
||||
objgraph.show_backrefs(final, filename='finalizers.png')
|
||||
|
||||
trash = {}
|
||||
for x in gc.garbage:
|
||||
trash[type(x)] = trash.get(type(x), 0) + 1
|
||||
if trash:
|
||||
output.insert(0, '\n%s unreachable objects:' % unreachable)
|
||||
trash = [(v, k) for k, v in trash.items()]
|
||||
trash.sort()
|
||||
for pair in trash:
|
||||
output.append(' ' + repr(pair))
|
||||
|
||||
# Check declared classes to verify uncollected instances.
|
||||
# These don't have to be part of a cycle; they can be
|
||||
# any objects that have unanticipated referrers that keep
|
||||
# them from being collected.
|
||||
allobjs = {}
|
||||
for cls, minobj, maxobj, msg in self.classes:
|
||||
allobjs[cls] = get_instances(cls)
|
||||
|
||||
for cls, minobj, maxobj, msg in self.classes:
|
||||
objs = allobjs[cls]
|
||||
lenobj = len(objs)
|
||||
if lenobj < minobj or lenobj > maxobj:
|
||||
if minobj == maxobj:
|
||||
output.append(
|
||||
'\nExpected %s %r references, got %s.' %
|
||||
(minobj, cls, lenobj))
|
||||
else:
|
||||
output.append(
|
||||
'\nExpected %s to %s %r references, got %s.' %
|
||||
(minobj, maxobj, cls, lenobj))
|
||||
|
||||
for obj in objs:
|
||||
if objgraph is not None:
|
||||
ig = [id(objs), id(inspect.currentframe())]
|
||||
fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
|
||||
objgraph.show_backrefs(
|
||||
obj, extra_ignore=ig, max_depth=4, too_many=20,
|
||||
filename=fname, extra_info=get_context)
|
||||
output.append('\nReferrers for %s (refcount=%s):' %
|
||||
(repr(obj), sys.getrefcount(obj)))
|
||||
t = ReferrerTree(ignore=[objs], maxdepth=3)
|
||||
tree = t.ascend(obj)
|
||||
output.extend(t.format(tree))
|
||||
|
||||
return '\n'.join(output)
|
|
@ -1,378 +0,0 @@
|
|||
"""
|
||||
This module defines functions to implement HTTP Digest Authentication
|
||||
(:rfc:`2617`).
|
||||
This has full compliance with 'Digest' and 'Basic' authentication methods. In
|
||||
'Digest' it supports both MD5 and MD5-sess algorithms.
|
||||
|
||||
Usage:
|
||||
First use 'doAuth' to request the client authentication for a
|
||||
certain resource. You should send an httplib.UNAUTHORIZED response to the
|
||||
client so he knows he has to authenticate itself.
|
||||
|
||||
Then use 'parseAuthorization' to retrieve the 'auth_map' used in
|
||||
'checkResponse'.
|
||||
|
||||
To use 'checkResponse' you must have already verified the password
|
||||
associated with the 'username' key in 'auth_map' dict. Then you use the
|
||||
'checkResponse' function to verify if the password matches the one sent
|
||||
by the client.
|
||||
|
||||
SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms
|
||||
SUPPORTED_QOP - list of supported 'Digest' 'qop'.
|
||||
"""
|
||||
|
||||
import time
|
||||
from hashlib import md5
|
||||
|
||||
from cherrypy._cpcompat import (
|
||||
base64_decode, ntob,
|
||||
parse_http_list, parse_keqv_list
|
||||
)
|
||||
|
||||
|
||||
__version__ = 1, 0, 1
|
||||
__author__ = 'Tiago Cogumbreiro <cogumbreiro@users.sf.net>'
|
||||
__credits__ = """
|
||||
Peter van Kampen for its recipe which implement most of Digest
|
||||
authentication:
|
||||
http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378
|
||||
"""
|
||||
|
||||
__license__ = """
|
||||
Copyright (c) 2005, Tiago Cogumbreiro <cogumbreiro@users.sf.net>
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
* Neither the name of Sylvain Hellegouarch nor the names of his
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
"""
|
||||
|
||||
__all__ = ('digestAuth', 'basicAuth', 'doAuth', 'checkResponse',
|
||||
'parseAuthorization', 'SUPPORTED_ALGORITHM', 'md5SessionKey',
|
||||
'calculateNonce', 'SUPPORTED_QOP')
|
||||
|
||||
##########################################################################
|
||||
|
||||
MD5 = 'MD5'
|
||||
MD5_SESS = 'MD5-sess'
|
||||
AUTH = 'auth'
|
||||
AUTH_INT = 'auth-int'
|
||||
|
||||
SUPPORTED_ALGORITHM = (MD5, MD5_SESS)
|
||||
SUPPORTED_QOP = (AUTH, AUTH_INT)
|
||||
|
||||
##########################################################################
|
||||
# doAuth
|
||||
#
|
||||
DIGEST_AUTH_ENCODERS = {
|
||||
MD5: lambda val: md5(ntob(val)).hexdigest(),
|
||||
MD5_SESS: lambda val: md5(ntob(val)).hexdigest(),
|
||||
# SHA: lambda val: sha.new(ntob(val)).hexdigest (),
|
||||
}
|
||||
|
||||
|
||||
def calculateNonce(realm, algorithm=MD5):
|
||||
"""This is an auxaliary function that calculates 'nonce' value. It is used
|
||||
to handle sessions."""
|
||||
|
||||
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS
|
||||
assert algorithm in SUPPORTED_ALGORITHM
|
||||
|
||||
try:
|
||||
encoder = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
except KeyError:
|
||||
raise NotImplementedError('The chosen algorithm (%s) does not have '
|
||||
'an implementation yet' % algorithm)
|
||||
|
||||
return encoder('%d:%s' % (time.time(), realm))
|
||||
|
||||
|
||||
def digestAuth(realm, algorithm=MD5, nonce=None, qop=AUTH):
|
||||
"""Challenges the client for a Digest authentication."""
|
||||
global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP
|
||||
assert algorithm in SUPPORTED_ALGORITHM
|
||||
assert qop in SUPPORTED_QOP
|
||||
|
||||
if nonce is None:
|
||||
nonce = calculateNonce(realm, algorithm)
|
||||
|
||||
return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % (
|
||||
realm, nonce, algorithm, qop
|
||||
)
|
||||
|
||||
|
||||
def basicAuth(realm):
|
||||
"""Challengenes the client for a Basic authentication."""
|
||||
assert '"' not in realm, "Realms cannot contain the \" (quote) character."
|
||||
|
||||
return 'Basic realm="%s"' % realm
|
||||
|
||||
|
||||
def doAuth(realm):
|
||||
"""'doAuth' function returns the challenge string b giving priority over
|
||||
Digest and fallback to Basic authentication when the browser doesn't
|
||||
support the first one.
|
||||
|
||||
This should be set in the HTTP header under the key 'WWW-Authenticate'."""
|
||||
|
||||
return digestAuth(realm) + ' ' + basicAuth(realm)
|
||||
|
||||
|
||||
##########################################################################
|
||||
# Parse authorization parameters
|
||||
#
|
||||
def _parseDigestAuthorization(auth_params):
|
||||
# Convert the auth params to a dict
|
||||
items = parse_http_list(auth_params)
|
||||
params = parse_keqv_list(items)
|
||||
|
||||
# Now validate the params
|
||||
|
||||
# Check for required parameters
|
||||
required = ['username', 'realm', 'nonce', 'uri', 'response']
|
||||
for k in required:
|
||||
if k not in params:
|
||||
return None
|
||||
|
||||
# If qop is sent then cnonce and nc MUST be present
|
||||
if 'qop' in params and not ('cnonce' in params
|
||||
and 'nc' in params):
|
||||
return None
|
||||
|
||||
# If qop is not sent, neither cnonce nor nc can be present
|
||||
if ('cnonce' in params or 'nc' in params) and \
|
||||
'qop' not in params:
|
||||
return None
|
||||
|
||||
return params
|
||||
|
||||
|
||||
def _parseBasicAuthorization(auth_params):
|
||||
username, password = base64_decode(auth_params).split(':', 1)
|
||||
return {'username': username, 'password': password}
|
||||
|
||||
AUTH_SCHEMES = {
|
||||
'basic': _parseBasicAuthorization,
|
||||
'digest': _parseDigestAuthorization,
|
||||
}
|
||||
|
||||
|
||||
def parseAuthorization(credentials):
|
||||
"""parseAuthorization will convert the value of the 'Authorization' key in
|
||||
the HTTP header to a map itself. If the parsing fails 'None' is returned.
|
||||
"""
|
||||
|
||||
global AUTH_SCHEMES
|
||||
|
||||
auth_scheme, auth_params = credentials.split(' ', 1)
|
||||
auth_scheme = auth_scheme.lower()
|
||||
|
||||
parser = AUTH_SCHEMES[auth_scheme]
|
||||
params = parser(auth_params)
|
||||
|
||||
if params is None:
|
||||
return
|
||||
|
||||
assert 'auth_scheme' not in params
|
||||
params['auth_scheme'] = auth_scheme
|
||||
return params
|
||||
|
||||
|
||||
##########################################################################
|
||||
# Check provided response for a valid password
|
||||
#
|
||||
def md5SessionKey(params, password):
|
||||
"""
|
||||
If the "algorithm" directive's value is "MD5-sess", then A1
|
||||
[the session key] is calculated only once - on the first request by the
|
||||
client following receipt of a WWW-Authenticate challenge from the server.
|
||||
|
||||
This creates a 'session key' for the authentication of subsequent
|
||||
requests and responses which is different for each "authentication
|
||||
session", thus limiting the amount of material hashed with any one
|
||||
key.
|
||||
|
||||
Because the server need only use the hash of the user
|
||||
credentials in order to create the A1 value, this construction could
|
||||
be used in conjunction with a third party authentication service so
|
||||
that the web server would not need the actual password value. The
|
||||
specification of such a protocol is beyond the scope of this
|
||||
specification.
|
||||
"""
|
||||
|
||||
keys = ('username', 'realm', 'nonce', 'cnonce')
|
||||
params_copy = {}
|
||||
for key in keys:
|
||||
params_copy[key] = params[key]
|
||||
|
||||
params_copy['algorithm'] = MD5_SESS
|
||||
return _A1(params_copy, password)
|
||||
|
||||
|
||||
def _A1(params, password):
|
||||
algorithm = params.get('algorithm', MD5)
|
||||
H = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
|
||||
if algorithm == MD5:
|
||||
# If the "algorithm" directive's value is "MD5" or is
|
||||
# unspecified, then A1 is:
|
||||
# A1 = unq(username-value) ":" unq(realm-value) ":" passwd
|
||||
return '%s:%s:%s' % (params['username'], params['realm'], password)
|
||||
|
||||
elif algorithm == MD5_SESS:
|
||||
|
||||
# This is A1 if qop is set
|
||||
# A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
|
||||
# ":" unq(nonce-value) ":" unq(cnonce-value)
|
||||
h_a1 = H('%s:%s:%s' % (params['username'], params['realm'], password))
|
||||
return '%s:%s:%s' % (h_a1, params['nonce'], params['cnonce'])
|
||||
|
||||
|
||||
def _A2(params, method, kwargs):
|
||||
# If the "qop" directive's value is "auth" or is unspecified, then A2 is:
|
||||
# A2 = Method ":" digest-uri-value
|
||||
|
||||
qop = params.get('qop', 'auth')
|
||||
if qop == 'auth':
|
||||
return method + ':' + params['uri']
|
||||
elif qop == 'auth-int':
|
||||
# If the "qop" value is "auth-int", then A2 is:
|
||||
# A2 = Method ":" digest-uri-value ":" H(entity-body)
|
||||
entity_body = kwargs.get('entity_body', '')
|
||||
H = kwargs['H']
|
||||
|
||||
return '%s:%s:%s' % (
|
||||
method,
|
||||
params['uri'],
|
||||
H(entity_body)
|
||||
)
|
||||
|
||||
else:
|
||||
raise NotImplementedError("The 'qop' method is unknown: %s" % qop)
|
||||
|
||||
|
||||
def _computeDigestResponse(auth_map, password, method='GET', A1=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Generates a response respecting the algorithm defined in RFC 2617
|
||||
"""
|
||||
params = auth_map
|
||||
|
||||
algorithm = params.get('algorithm', MD5)
|
||||
|
||||
H = DIGEST_AUTH_ENCODERS[algorithm]
|
||||
KD = lambda secret, data: H(secret + ':' + data)
|
||||
|
||||
qop = params.get('qop', None)
|
||||
|
||||
H_A2 = H(_A2(params, method, kwargs))
|
||||
|
||||
if algorithm == MD5_SESS and A1 is not None:
|
||||
H_A1 = H(A1)
|
||||
else:
|
||||
H_A1 = H(_A1(params, password))
|
||||
|
||||
if qop in ('auth', 'auth-int'):
|
||||
# If the "qop" value is "auth" or "auth-int":
|
||||
# request-digest = <"> < KD ( H(A1), unq(nonce-value)
|
||||
# ":" nc-value
|
||||
# ":" unq(cnonce-value)
|
||||
# ":" unq(qop-value)
|
||||
# ":" H(A2)
|
||||
# ) <">
|
||||
request = '%s:%s:%s:%s:%s' % (
|
||||
params['nonce'],
|
||||
params['nc'],
|
||||
params['cnonce'],
|
||||
params['qop'],
|
||||
H_A2,
|
||||
)
|
||||
elif qop is None:
|
||||
# If the "qop" directive is not present (this construction is
|
||||
# for compatibility with RFC 2069):
|
||||
# request-digest =
|
||||
# <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <">
|
||||
request = '%s:%s' % (params['nonce'], H_A2)
|
||||
|
||||
return KD(H_A1, request)
|
||||
|
||||
|
||||
def _checkDigestResponse(auth_map, password, method='GET', A1=None, **kwargs):
|
||||
"""This function is used to verify the response given by the client when
|
||||
he tries to authenticate.
|
||||
Optional arguments:
|
||||
entity_body - when 'qop' is set to 'auth-int' you MUST provide the
|
||||
raw data you are going to send to the client (usually the
|
||||
HTML page.
|
||||
request_uri - the uri from the request line compared with the 'uri'
|
||||
directive of the authorization map. They must represent
|
||||
the same resource (unused at this time).
|
||||
"""
|
||||
|
||||
if auth_map['realm'] != kwargs.get('realm', None):
|
||||
return False
|
||||
|
||||
response = _computeDigestResponse(
|
||||
auth_map, password, method, A1, **kwargs)
|
||||
|
||||
return response == auth_map['response']
|
||||
|
||||
|
||||
def _checkBasicResponse(auth_map, password, method='GET', encrypt=None,
|
||||
**kwargs):
|
||||
# Note that the Basic response doesn't provide the realm value so we cannot
|
||||
# test it
|
||||
pass_through = lambda password, username=None: password
|
||||
encrypt = encrypt or pass_through
|
||||
try:
|
||||
candidate = encrypt(auth_map['password'], auth_map['username'])
|
||||
except TypeError:
|
||||
# if encrypt only takes one parameter, it's the password
|
||||
candidate = encrypt(auth_map['password'])
|
||||
return candidate == password
|
||||
|
||||
AUTH_RESPONSES = {
|
||||
'basic': _checkBasicResponse,
|
||||
'digest': _checkDigestResponse,
|
||||
}
|
||||
|
||||
|
||||
def checkResponse(auth_map, password, method='GET', encrypt=None, **kwargs):
|
||||
"""'checkResponse' compares the auth_map with the password and optionally
|
||||
other arguments that each implementation might need.
|
||||
|
||||
If the response is of type 'Basic' then the function has the following
|
||||
signature::
|
||||
|
||||
checkBasicResponse(auth_map, password) -> bool
|
||||
|
||||
If the response is of type 'Digest' then the function has the following
|
||||
signature::
|
||||
|
||||
checkDigestResponse(auth_map, password, method='GET', A1=None) -> bool
|
||||
|
||||
The 'A1' argument is only used in MD5_SESS algorithm based responses.
|
||||
Check md5SessionKey() for more info.
|
||||
"""
|
||||
checker = AUTH_RESPONSES[auth_map['auth_scheme']]
|
||||
return checker(auth_map, password, method=method, encrypt=encrypt,
|
||||
**kwargs)
|
|
@ -1,530 +0,0 @@
|
|||
"""HTTP library functions.
|
||||
|
||||
This module contains functions for building an HTTP application
|
||||
framework: any one, not just one whose name starts with "Ch". ;) If you
|
||||
reference any modules from some popular framework inside *this* module,
|
||||
FuManChu will personally hang you up by your thumbs and submit you
|
||||
to a public caning.
|
||||
"""
|
||||
|
||||
import functools
|
||||
import email.utils
|
||||
import re
|
||||
from binascii import b2a_base64
|
||||
from cgi import parse_header
|
||||
try:
|
||||
# Python 3
|
||||
from email.header import decode_header
|
||||
except ImportError:
|
||||
from email.Header import decode_header
|
||||
|
||||
import six
|
||||
|
||||
from cherrypy._cpcompat import BaseHTTPRequestHandler, ntob, ntou
|
||||
from cherrypy._cpcompat import text_or_bytes, iteritems
|
||||
from cherrypy._cpcompat import reversed, sorted, unquote_qs
|
||||
|
||||
response_codes = BaseHTTPRequestHandler.responses.copy()
|
||||
|
||||
# From https://github.com/cherrypy/cherrypy/issues/361
|
||||
response_codes[500] = ('Internal Server Error',
|
||||
'The server encountered an unexpected condition '
|
||||
'which prevented it from fulfilling the request.')
|
||||
response_codes[503] = ('Service Unavailable',
|
||||
'The server is currently unable to handle the '
|
||||
'request due to a temporary overloading or '
|
||||
'maintenance of the server.')
|
||||
|
||||
|
||||
HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
|
||||
|
||||
|
||||
def urljoin(*atoms):
|
||||
"""Return the given path \*atoms, joined into a single URL.
|
||||
|
||||
This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
||||
original URL, even if either atom is blank.
|
||||
"""
|
||||
url = '/'.join([x for x in atoms if x])
|
||||
while '//' in url:
|
||||
url = url.replace('//', '/')
|
||||
# Special-case the final url of "", and return "/" instead.
|
||||
return url or '/'
|
||||
|
||||
|
||||
def urljoin_bytes(*atoms):
|
||||
"""Return the given path *atoms, joined into a single URL.
|
||||
|
||||
This will correctly join a SCRIPT_NAME and PATH_INFO into the
|
||||
original URL, even if either atom is blank.
|
||||
"""
|
||||
url = ntob('/').join([x for x in atoms if x])
|
||||
while ntob('//') in url:
|
||||
url = url.replace(ntob('//'), ntob('/'))
|
||||
# Special-case the final url of "", and return "/" instead.
|
||||
return url or ntob('/')
|
||||
|
||||
|
||||
def protocol_from_http(protocol_str):
|
||||
"""Return a protocol tuple from the given 'HTTP/x.y' string."""
|
||||
return int(protocol_str[5]), int(protocol_str[7])
|
||||
|
||||
|
||||
def get_ranges(headervalue, content_length):
|
||||
"""Return a list of (start, stop) indices from a Range header, or None.
|
||||
|
||||
Each (start, stop) tuple will be composed of two ints, which are suitable
|
||||
for use in a slicing operation. That is, the header "Range: bytes=3-6",
|
||||
if applied against a Python string, is requesting resource[3:7]. This
|
||||
function will return the list [(3, 7)].
|
||||
|
||||
If this function returns an empty list, you should return HTTP 416.
|
||||
"""
|
||||
|
||||
if not headervalue:
|
||||
return None
|
||||
|
||||
result = []
|
||||
bytesunit, byteranges = headervalue.split('=', 1)
|
||||
for brange in byteranges.split(','):
|
||||
start, stop = [x.strip() for x in brange.split('-', 1)]
|
||||
if start:
|
||||
if not stop:
|
||||
stop = content_length - 1
|
||||
start, stop = int(start), int(stop)
|
||||
if start >= content_length:
|
||||
# From rfc 2616 sec 14.16:
|
||||
# "If the server receives a request (other than one
|
||||
# including an If-Range request-header field) with an
|
||||
# unsatisfiable Range request-header field (that is,
|
||||
# all of whose byte-range-spec values have a first-byte-pos
|
||||
# value greater than the current length of the selected
|
||||
# resource), it SHOULD return a response code of 416
|
||||
# (Requested range not satisfiable)."
|
||||
continue
|
||||
if stop < start:
|
||||
# From rfc 2616 sec 14.16:
|
||||
# "If the server ignores a byte-range-spec because it
|
||||
# is syntactically invalid, the server SHOULD treat
|
||||
# the request as if the invalid Range header field
|
||||
# did not exist. (Normally, this means return a 200
|
||||
# response containing the full entity)."
|
||||
return None
|
||||
result.append((start, stop + 1))
|
||||
else:
|
||||
if not stop:
|
||||
# See rfc quote above.
|
||||
return None
|
||||
# Negative subscript (last N bytes)
|
||||
#
|
||||
# RFC 2616 Section 14.35.1:
|
||||
# If the entity is shorter than the specified suffix-length,
|
||||
# the entire entity-body is used.
|
||||
if int(stop) > content_length:
|
||||
result.append((0, content_length))
|
||||
else:
|
||||
result.append((content_length - int(stop), content_length))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class HeaderElement(object):
|
||||
|
||||
"""An element (with parameters) from an HTTP header's element list."""
|
||||
|
||||
def __init__(self, value, params=None):
|
||||
self.value = value
|
||||
if params is None:
|
||||
params = {}
|
||||
self.params = params
|
||||
|
||||
def __cmp__(self, other):
|
||||
return cmp(self.value, other.value)
|
||||
|
||||
def __lt__(self, other):
|
||||
return self.value < other.value
|
||||
|
||||
def __str__(self):
|
||||
p = [';%s=%s' % (k, v) for k, v in iteritems(self.params)]
|
||||
return str('%s%s' % (self.value, ''.join(p)))
|
||||
|
||||
def __bytes__(self):
|
||||
return ntob(self.__str__())
|
||||
|
||||
def __unicode__(self):
|
||||
return ntou(self.__str__())
|
||||
|
||||
@staticmethod
|
||||
def parse(elementstr):
|
||||
"""Transform 'token;key=val' to ('token', {'key': 'val'})."""
|
||||
initial_value, params = parse_header(elementstr)
|
||||
return initial_value, params
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, elementstr):
|
||||
"""Construct an instance from a string of the form 'token;key=val'."""
|
||||
ival, params = cls.parse(elementstr)
|
||||
return cls(ival, params)
|
||||
|
||||
|
||||
q_separator = re.compile(r'; *q *=')
|
||||
|
||||
|
||||
class AcceptElement(HeaderElement):
|
||||
|
||||
"""An element (with parameters) from an Accept* header's element list.
|
||||
|
||||
AcceptElement objects are comparable; the more-preferred object will be
|
||||
"less than" the less-preferred object. They are also therefore sortable;
|
||||
if you sort a list of AcceptElement objects, they will be listed in
|
||||
priority order; the most preferred value will be first. Yes, it should
|
||||
have been the other way around, but it's too late to fix now.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_str(cls, elementstr):
|
||||
qvalue = None
|
||||
# The first "q" parameter (if any) separates the initial
|
||||
# media-range parameter(s) (if any) from the accept-params.
|
||||
atoms = q_separator.split(elementstr, 1)
|
||||
media_range = atoms.pop(0).strip()
|
||||
if atoms:
|
||||
# The qvalue for an Accept header can have extensions. The other
|
||||
# headers cannot, but it's easier to parse them as if they did.
|
||||
qvalue = HeaderElement.from_str(atoms[0].strip())
|
||||
|
||||
media_type, params = cls.parse(media_range)
|
||||
if qvalue is not None:
|
||||
params['q'] = qvalue
|
||||
return cls(media_type, params)
|
||||
|
||||
@property
|
||||
def qvalue(self):
|
||||
'The qvalue, or priority, of this value.'
|
||||
val = self.params.get('q', '1')
|
||||
if isinstance(val, HeaderElement):
|
||||
val = val.value
|
||||
return float(val)
|
||||
|
||||
def __cmp__(self, other):
|
||||
diff = cmp(self.qvalue, other.qvalue)
|
||||
if diff == 0:
|
||||
diff = cmp(str(self), str(other))
|
||||
return diff
|
||||
|
||||
def __lt__(self, other):
|
||||
if self.qvalue == other.qvalue:
|
||||
return str(self) < str(other)
|
||||
else:
|
||||
return self.qvalue < other.qvalue
|
||||
|
||||
RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
|
||||
def header_elements(fieldname, fieldvalue):
|
||||
"""Return a sorted HeaderElement list from a comma-separated header string.
|
||||
"""
|
||||
if not fieldvalue:
|
||||
return []
|
||||
|
||||
result = []
|
||||
for element in RE_HEADER_SPLIT.split(fieldvalue):
|
||||
if fieldname.startswith('Accept') or fieldname == 'TE':
|
||||
hv = AcceptElement.from_str(element)
|
||||
else:
|
||||
hv = HeaderElement.from_str(element)
|
||||
result.append(hv)
|
||||
|
||||
return list(reversed(sorted(result)))
|
||||
|
||||
|
||||
def decode_TEXT(value):
|
||||
r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr")."""
|
||||
atoms = decode_header(value)
|
||||
decodedvalue = ''
|
||||
for atom, charset in atoms:
|
||||
if charset is not None:
|
||||
atom = atom.decode(charset)
|
||||
decodedvalue += atom
|
||||
return decodedvalue
|
||||
|
||||
|
||||
def valid_status(status):
|
||||
"""Return legal HTTP status Code, Reason-phrase and Message.
|
||||
|
||||
The status arg must be an int, or a str that begins with an int.
|
||||
|
||||
If status is an int, or a str and no reason-phrase is supplied,
|
||||
a default reason-phrase will be provided.
|
||||
"""
|
||||
|
||||
if not status:
|
||||
status = 200
|
||||
|
||||
status = str(status)
|
||||
parts = status.split(' ', 1)
|
||||
if len(parts) == 1:
|
||||
# No reason supplied.
|
||||
code, = parts
|
||||
reason = None
|
||||
else:
|
||||
code, reason = parts
|
||||
reason = reason.strip()
|
||||
|
||||
try:
|
||||
code = int(code)
|
||||
except ValueError:
|
||||
raise ValueError('Illegal response status from server '
|
||||
'(%s is non-numeric).' % repr(code))
|
||||
|
||||
if code < 100 or code > 599:
|
||||
raise ValueError('Illegal response status from server '
|
||||
'(%s is out of range).' % repr(code))
|
||||
|
||||
if code not in response_codes:
|
||||
# code is unknown but not illegal
|
||||
default_reason, message = '', ''
|
||||
else:
|
||||
default_reason, message = response_codes[code]
|
||||
|
||||
if reason is None:
|
||||
reason = default_reason
|
||||
|
||||
return code, reason, message
|
||||
|
||||
|
||||
# NOTE: the parse_qs functions that follow are modified version of those
|
||||
# in the python3.0 source - we need to pass through an encoding to the unquote
|
||||
# method, but the default parse_qs function doesn't allow us to. These do.
|
||||
|
||||
def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
|
||||
"""Parse a query given as a string argument.
|
||||
|
||||
Arguments:
|
||||
|
||||
qs: URL-encoded query string to be parsed
|
||||
|
||||
keep_blank_values: flag indicating whether blank values in
|
||||
URL encoded queries should be treated as blank strings. A
|
||||
true value indicates that blanks should be retained as blank
|
||||
strings. The default false value indicates that blank values
|
||||
are to be ignored and treated as if they were not included.
|
||||
|
||||
strict_parsing: flag indicating what to do with parsing errors. If
|
||||
false (the default), errors are silently ignored. If true,
|
||||
errors raise a ValueError exception.
|
||||
|
||||
Returns a dict, as G-d intended.
|
||||
"""
|
||||
pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
|
||||
d = {}
|
||||
for name_value in pairs:
|
||||
if not name_value and not strict_parsing:
|
||||
continue
|
||||
nv = name_value.split('=', 1)
|
||||
if len(nv) != 2:
|
||||
if strict_parsing:
|
||||
raise ValueError('bad query field: %r' % (name_value,))
|
||||
# Handle case of a control-name with no equal sign
|
||||
if keep_blank_values:
|
||||
nv.append('')
|
||||
else:
|
||||
continue
|
||||
if len(nv[1]) or keep_blank_values:
|
||||
name = unquote_qs(nv[0], encoding)
|
||||
value = unquote_qs(nv[1], encoding)
|
||||
if name in d:
|
||||
if not isinstance(d[name], list):
|
||||
d[name] = [d[name]]
|
||||
d[name].append(value)
|
||||
else:
|
||||
d[name] = value
|
||||
return d
|
||||
|
||||
|
||||
image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
|
||||
|
||||
|
||||
def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
|
||||
"""Build a params dictionary from a query_string.
|
||||
|
||||
Duplicate key/value pairs in the provided query_string will be
|
||||
returned as {'key': [val1, val2, ...]}. Single key/values will
|
||||
be returned as strings: {'key': 'value'}.
|
||||
"""
|
||||
if image_map_pattern.match(query_string):
|
||||
# Server-side image map. Map the coords to 'x' and 'y'
|
||||
# (like CGI::Request does).
|
||||
pm = query_string.split(',')
|
||||
pm = {'x': int(pm[0]), 'y': int(pm[1])}
|
||||
else:
|
||||
pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
|
||||
return pm
|
||||
|
||||
|
||||
class CaseInsensitiveDict(dict):
|
||||
|
||||
"""A case-insensitive dict subclass.
|
||||
|
||||
Each key is changed on entry to str(key).title().
|
||||
"""
|
||||
|
||||
def __getitem__(self, key):
|
||||
return dict.__getitem__(self, str(key).title())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(self, str(key).title(), value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, str(key).title())
|
||||
|
||||
def __contains__(self, key):
|
||||
return dict.__contains__(self, str(key).title())
|
||||
|
||||
def get(self, key, default=None):
|
||||
return dict.get(self, str(key).title(), default)
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
def has_key(self, key):
|
||||
return str(key).title() in self
|
||||
|
||||
def update(self, E):
|
||||
for k in E.keys():
|
||||
self[str(k).title()] = E[k]
|
||||
|
||||
@classmethod
|
||||
def fromkeys(cls, seq, value=None):
|
||||
newdict = cls()
|
||||
for k in seq:
|
||||
newdict[str(k).title()] = value
|
||||
return newdict
|
||||
|
||||
def setdefault(self, key, x=None):
|
||||
key = str(key).title()
|
||||
try:
|
||||
return self[key]
|
||||
except KeyError:
|
||||
self[key] = x
|
||||
return x
|
||||
|
||||
def pop(self, key, default):
|
||||
return dict.pop(self, str(key).title(), default)
|
||||
|
||||
|
||||
# TEXT = <any OCTET except CTLs, but including LWS>
|
||||
#
|
||||
# A CRLF is allowed in the definition of TEXT only as part of a header
|
||||
# field continuation. It is expected that the folding LWS will be
|
||||
# replaced with a single SP before interpretation of the TEXT value."
|
||||
if str == bytes:
|
||||
header_translate_table = ''.join([chr(i) for i in xrange(256)])
|
||||
header_translate_deletechars = ''.join(
|
||||
[chr(i) for i in xrange(32)]) + chr(127)
|
||||
else:
|
||||
header_translate_table = None
|
||||
header_translate_deletechars = bytes(range(32)) + bytes([127])
|
||||
|
||||
|
||||
class HeaderMap(CaseInsensitiveDict):
|
||||
|
||||
"""A dict subclass for HTTP request and response headers.
|
||||
|
||||
Each key is changed on entry to str(key).title(). This allows headers
|
||||
to be case-insensitive and avoid duplicates.
|
||||
|
||||
Values are header values (decoded according to :rfc:`2047` if necessary).
|
||||
"""
|
||||
|
||||
protocol = (1, 1)
|
||||
encodings = ['ISO-8859-1']
|
||||
|
||||
# Someday, when http-bis is done, this will probably get dropped
|
||||
# since few servers, clients, or intermediaries do it. But until then,
|
||||
# we're going to obey the spec as is.
|
||||
# "Words of *TEXT MAY contain characters from character sets other than
|
||||
# ISO-8859-1 only when encoded according to the rules of RFC 2047."
|
||||
use_rfc_2047 = True
|
||||
|
||||
def elements(self, key):
|
||||
"""Return a sorted list of HeaderElements for the given header."""
|
||||
key = str(key).title()
|
||||
value = self.get(key)
|
||||
return header_elements(key, value)
|
||||
|
||||
def values(self, key):
|
||||
"""Return a sorted list of HeaderElement.value for the given header."""
|
||||
return [e.value for e in self.elements(key)]
|
||||
|
||||
def output(self):
|
||||
"""Transform self into a list of (name, value) tuples."""
|
||||
return list(self.encode_header_items(self.items()))
|
||||
|
||||
@classmethod
|
||||
def encode_header_items(cls, header_items):
|
||||
"""
|
||||
Prepare the sequence of name, value tuples into a form suitable for
|
||||
transmitting on the wire for HTTP.
|
||||
"""
|
||||
for k, v in header_items:
|
||||
if isinstance(k, six.text_type):
|
||||
k = cls.encode(k)
|
||||
|
||||
if not isinstance(v, text_or_bytes):
|
||||
v = str(v)
|
||||
|
||||
if isinstance(v, six.text_type):
|
||||
v = cls.encode(v)
|
||||
|
||||
# See header_translate_* constants above.
|
||||
# Replace only if you really know what you're doing.
|
||||
k = k.translate(header_translate_table,
|
||||
header_translate_deletechars)
|
||||
v = v.translate(header_translate_table,
|
||||
header_translate_deletechars)
|
||||
|
||||
yield (k, v)
|
||||
|
||||
@classmethod
|
||||
def encode(cls, v):
|
||||
"""Return the given header name or value, encoded for HTTP output."""
|
||||
for enc in cls.encodings:
|
||||
try:
|
||||
return v.encode(enc)
|
||||
except UnicodeEncodeError:
|
||||
continue
|
||||
|
||||
if cls.protocol == (1, 1) and cls.use_rfc_2047:
|
||||
# Encode RFC-2047 TEXT
|
||||
# (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
|
||||
# We do our own here instead of using the email module
|
||||
# because we never want to fold lines--folding has
|
||||
# been deprecated by the HTTP working group.
|
||||
v = b2a_base64(v.encode('utf-8'))
|
||||
return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?='))
|
||||
|
||||
raise ValueError('Could not encode header part %r using '
|
||||
'any of the encodings %r.' %
|
||||
(v, cls.encodings))
|
||||
|
||||
|
||||
class Host(object):
|
||||
|
||||
"""An internet address.
|
||||
|
||||
name
|
||||
Should be the client's host name. If not available (because no DNS
|
||||
lookup is performed), the IP address should be used instead.
|
||||
|
||||
"""
|
||||
|
||||
ip = '0.0.0.0'
|
||||
port = 80
|
||||
name = 'unknown.tld'
|
||||
|
||||
def __init__(self, ip, port, name=None):
|
||||
self.ip = ip
|
||||
self.port = port
|
||||
if name is None:
|
||||
name = ip
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
|
|
@ -1,94 +0,0 @@
|
|||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
|
||||
|
||||
|
||||
def json_processor(entity):
|
||||
"""Read application/json data into request.json."""
|
||||
if not entity.headers.get(ntou('Content-Length'), ntou('')):
|
||||
raise cherrypy.HTTPError(411)
|
||||
|
||||
body = entity.fp.read()
|
||||
with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
|
||||
cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
|
||||
|
||||
|
||||
def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
|
||||
force=True, debug=False, processor=json_processor):
|
||||
"""Add a processor to parse JSON request entities:
|
||||
The default processor places the parsed data into request.json.
|
||||
|
||||
Incoming request entities which match the given content_type(s) will
|
||||
be deserialized from JSON to the Python equivalent, and the result
|
||||
stored at cherrypy.request.json. The 'content_type' argument may
|
||||
be a Content-Type string or a list of allowable Content-Type strings.
|
||||
|
||||
If the 'force' argument is True (the default), then entities of other
|
||||
content types will not be allowed; "415 Unsupported Media Type" is
|
||||
raised instead.
|
||||
|
||||
Supply your own processor to use a custom decoder, or to handle the parsed
|
||||
data differently. The processor can be configured via
|
||||
tools.json_in.processor or via the decorator method.
|
||||
|
||||
Note that the deserializer requires the client send a Content-Length
|
||||
request header, or it will raise "411 Length Required". If for any
|
||||
other reason the request entity cannot be deserialized from JSON,
|
||||
it will raise "400 Bad Request: Invalid JSON document".
|
||||
|
||||
You must be using Python 2.6 or greater, or have the 'simplejson'
|
||||
package importable; otherwise, ValueError is raised during processing.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if isinstance(content_type, text_or_bytes):
|
||||
content_type = [content_type]
|
||||
|
||||
if force:
|
||||
if debug:
|
||||
cherrypy.log('Removing body processors %s' %
|
||||
repr(request.body.processors.keys()), 'TOOLS.JSON_IN')
|
||||
request.body.processors.clear()
|
||||
request.body.default_proc = cherrypy.HTTPError(
|
||||
415, 'Expected an entity of content type %s' %
|
||||
', '.join(content_type))
|
||||
|
||||
for ct in content_type:
|
||||
if debug:
|
||||
cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
|
||||
request.body.processors[ct] = processor
|
||||
|
||||
|
||||
def json_handler(*args, **kwargs):
|
||||
value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
|
||||
return json_encode(value)
|
||||
|
||||
|
||||
def json_out(content_type='application/json', debug=False,
|
||||
handler=json_handler):
|
||||
"""Wrap request.handler to serialize its output to JSON. Sets Content-Type.
|
||||
|
||||
If the given content_type is None, the Content-Type response header
|
||||
is not set.
|
||||
|
||||
Provide your own handler to use a custom encoder. For example
|
||||
cherrypy.config['tools.json_out.handler'] = <function>, or
|
||||
@json_out(handler=function).
|
||||
|
||||
You must be using Python 2.6 or greater, or have the 'simplejson'
|
||||
package importable; otherwise, ValueError is raised during processing.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
# request.handler may be set to None by e.g. the caching tool
|
||||
# to signal to all components that a response body has already
|
||||
# been attached, in which case we don't need to wrap anything.
|
||||
if request.handler is None:
|
||||
return
|
||||
if debug:
|
||||
cherrypy.log('Replacing %s with JSON handler' % request.handler,
|
||||
'TOOLS.JSON_OUT')
|
||||
request._json_inner_handler = request.handler
|
||||
request.handler = handler
|
||||
if content_type is not None:
|
||||
if debug:
|
||||
cherrypy.log('Setting Content-Type to %s' %
|
||||
content_type, 'TOOLS.JSON_OUT')
|
||||
cherrypy.serving.response.headers['Content-Type'] = content_type
|
|
@ -1,142 +0,0 @@
|
|||
"""
|
||||
Platform-independent file locking. Inspired by and modeled after zc.lockfile.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
try:
|
||||
import msvcrt
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
class LockError(Exception):
|
||||
|
||||
'Could not obtain a lock'
|
||||
|
||||
msg = 'Unable to lock %r'
|
||||
|
||||
def __init__(self, path):
|
||||
super(LockError, self).__init__(self.msg % path)
|
||||
|
||||
|
||||
class UnlockError(LockError):
|
||||
|
||||
'Could not release a lock'
|
||||
|
||||
msg = 'Unable to unlock %r'
|
||||
|
||||
|
||||
# first, a default, naive locking implementation
|
||||
class LockFile(object):
|
||||
|
||||
"""
|
||||
A default, naive locking implementation. Always fails if the file
|
||||
already exists.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
try:
|
||||
fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_EXCL)
|
||||
except OSError:
|
||||
raise LockError(self.path)
|
||||
os.close(fd)
|
||||
|
||||
def release(self):
|
||||
os.remove(self.path)
|
||||
|
||||
def remove(self):
|
||||
pass
|
||||
|
||||
|
||||
class SystemLockFile(object):
|
||||
|
||||
"""
|
||||
An abstract base class for platform-specific locking.
|
||||
"""
|
||||
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
try:
|
||||
# Open lockfile for writing without truncation:
|
||||
self.fp = open(path, 'r+')
|
||||
except IOError:
|
||||
# If the file doesn't exist, IOError is raised; Use a+ instead.
|
||||
# Note that there may be a race here. Multiple processes
|
||||
# could fail on the r+ open and open the file a+, but only
|
||||
# one will get the the lock and write a pid.
|
||||
self.fp = open(path, 'a+')
|
||||
|
||||
try:
|
||||
self._lock_file()
|
||||
except:
|
||||
self.fp.seek(1)
|
||||
self.fp.close()
|
||||
del self.fp
|
||||
raise
|
||||
|
||||
self.fp.write(' %s\n' % os.getpid())
|
||||
self.fp.truncate()
|
||||
self.fp.flush()
|
||||
|
||||
def release(self):
|
||||
if not hasattr(self, 'fp'):
|
||||
return
|
||||
self._unlock_file()
|
||||
self.fp.close()
|
||||
del self.fp
|
||||
|
||||
def remove(self):
|
||||
"""
|
||||
Attempt to remove the file
|
||||
"""
|
||||
try:
|
||||
os.remove(self.path)
|
||||
except:
|
||||
pass
|
||||
|
||||
def _unlock_file(self):
|
||||
"""Attempt to obtain the lock on self.fp. Raise UnlockError if not
|
||||
released."""
|
||||
|
||||
|
||||
class WindowsLockFile(SystemLockFile):
|
||||
|
||||
def _lock_file(self):
|
||||
# Lock just the first byte
|
||||
try:
|
||||
msvcrt.locking(self.fp.fileno(), msvcrt.LK_NBLCK, 1)
|
||||
except IOError:
|
||||
raise LockError(self.fp.name)
|
||||
|
||||
def _unlock_file(self):
|
||||
try:
|
||||
self.fp.seek(0)
|
||||
msvcrt.locking(self.fp.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
except IOError:
|
||||
raise UnlockError(self.fp.name)
|
||||
|
||||
if 'msvcrt' in globals():
|
||||
LockFile = WindowsLockFile
|
||||
|
||||
|
||||
class UnixLockFile(SystemLockFile):
|
||||
|
||||
def _lock_file(self):
|
||||
flags = fcntl.LOCK_EX | fcntl.LOCK_NB
|
||||
try:
|
||||
fcntl.flock(self.fp.fileno(), flags)
|
||||
except IOError:
|
||||
raise LockError(self.fp.name)
|
||||
|
||||
# no need to implement _unlock_file, it will be unlocked on close()
|
||||
|
||||
if 'fcntl' in globals():
|
||||
LockFile = UnixLockFile
|
|
@ -1,47 +0,0 @@
|
|||
import datetime
|
||||
|
||||
|
||||
class NeverExpires(object):
|
||||
def expired(self):
|
||||
return False
|
||||
|
||||
|
||||
class Timer(object):
|
||||
"""
|
||||
A simple timer that will indicate when an expiration time has passed.
|
||||
"""
|
||||
def __init__(self, expiration):
|
||||
'Create a timer that expires at `expiration` (UTC datetime)'
|
||||
self.expiration = expiration
|
||||
|
||||
@classmethod
|
||||
def after(cls, elapsed):
|
||||
"""
|
||||
Return a timer that will expire after `elapsed` passes.
|
||||
"""
|
||||
return cls(datetime.datetime.utcnow() + elapsed)
|
||||
|
||||
def expired(self):
|
||||
return datetime.datetime.utcnow() >= self.expiration
|
||||
|
||||
|
||||
class LockTimeout(Exception):
|
||||
'An exception when a lock could not be acquired before a timeout period'
|
||||
|
||||
|
||||
class LockChecker(object):
|
||||
"""
|
||||
Keep track of the time and detect if a timeout has expired
|
||||
"""
|
||||
def __init__(self, session_id, timeout):
|
||||
self.session_id = session_id
|
||||
if timeout:
|
||||
self.timer = Timer.after(timeout)
|
||||
else:
|
||||
self.timer = NeverExpires()
|
||||
|
||||
def expired(self):
|
||||
if self.timer.expired():
|
||||
raise LockTimeout(
|
||||
'Timeout acquiring lock for %(session_id)s' % vars(self))
|
||||
return False
|
|
@ -1,217 +0,0 @@
|
|||
"""Profiler tools for CherryPy.
|
||||
|
||||
CherryPy users
|
||||
==============
|
||||
|
||||
You can profile any of your pages as follows::
|
||||
|
||||
from cherrypy.lib import profiler
|
||||
|
||||
class Root:
|
||||
p = profiler.Profiler("/path/to/profile/dir")
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
self.p.run(self._index)
|
||||
|
||||
def _index(self):
|
||||
return "Hello, world!"
|
||||
|
||||
cherrypy.tree.mount(Root())
|
||||
|
||||
You can also turn on profiling for all requests
|
||||
using the ``make_app`` function as WSGI middleware.
|
||||
|
||||
CherryPy developers
|
||||
===================
|
||||
|
||||
This module can be used whenever you make changes to CherryPy,
|
||||
to get a quick sanity-check on overall CP performance. Use the
|
||||
``--profile`` flag when running the test suite. Then, use the ``serve()``
|
||||
function to browse the results in a web browser. If you run this
|
||||
module from the command line, it will call ``serve()`` for you.
|
||||
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import os.path
|
||||
import sys
|
||||
import warnings
|
||||
|
||||
import cherrypy
|
||||
|
||||
|
||||
try:
|
||||
import profile
|
||||
import pstats
|
||||
|
||||
def new_func_strip_path(func_name):
|
||||
"""Make profiler output more readable by adding `__init__` modules' parents
|
||||
"""
|
||||
filename, line, name = func_name
|
||||
if filename.endswith('__init__.py'):
|
||||
return os.path.basename(filename[:-12]) + filename[-12:], line, name
|
||||
return os.path.basename(filename), line, name
|
||||
|
||||
pstats.func_strip_path = new_func_strip_path
|
||||
except ImportError:
|
||||
profile = None
|
||||
pstats = None
|
||||
|
||||
|
||||
_count = 0
|
||||
|
||||
|
||||
class Profiler(object):
|
||||
|
||||
def __init__(self, path=None):
|
||||
if not path:
|
||||
path = os.path.join(os.path.dirname(__file__), 'profile')
|
||||
self.path = path
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
def run(self, func, *args, **params):
|
||||
"""Dump profile data into self.path."""
|
||||
global _count
|
||||
c = _count = _count + 1
|
||||
path = os.path.join(self.path, 'cp_%04d.prof' % c)
|
||||
prof = profile.Profile()
|
||||
result = prof.runcall(func, *args, **params)
|
||||
prof.dump_stats(path)
|
||||
return result
|
||||
|
||||
def statfiles(self):
|
||||
""":rtype: list of available profiles.
|
||||
"""
|
||||
return [f for f in os.listdir(self.path)
|
||||
if f.startswith('cp_') and f.endswith('.prof')]
|
||||
|
||||
def stats(self, filename, sortby='cumulative'):
|
||||
""":rtype stats(index): output of print_stats() for the given profile.
|
||||
"""
|
||||
sio = io.StringIO()
|
||||
if sys.version_info >= (2, 5):
|
||||
s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
|
||||
s.strip_dirs()
|
||||
s.sort_stats(sortby)
|
||||
s.print_stats()
|
||||
else:
|
||||
# pstats.Stats before Python 2.5 didn't take a 'stream' arg,
|
||||
# but just printed to stdout. So re-route stdout.
|
||||
s = pstats.Stats(os.path.join(self.path, filename))
|
||||
s.strip_dirs()
|
||||
s.sort_stats(sortby)
|
||||
oldout = sys.stdout
|
||||
try:
|
||||
sys.stdout = sio
|
||||
s.print_stats()
|
||||
finally:
|
||||
sys.stdout = oldout
|
||||
response = sio.getvalue()
|
||||
sio.close()
|
||||
return response
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return """<html>
|
||||
<head><title>CherryPy profile data</title></head>
|
||||
<frameset cols='200, 1*'>
|
||||
<frame src='menu' />
|
||||
<frame name='main' src='' />
|
||||
</frameset>
|
||||
</html>
|
||||
"""
|
||||
|
||||
@cherrypy.expose
|
||||
def menu(self):
|
||||
yield '<h2>Profiling runs</h2>'
|
||||
yield '<p>Click on one of the runs below to see profiling data.</p>'
|
||||
runs = self.statfiles()
|
||||
runs.sort()
|
||||
for i in runs:
|
||||
yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
|
||||
i, i)
|
||||
|
||||
@cherrypy.expose
|
||||
def report(self, filename):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
return self.stats(filename)
|
||||
|
||||
|
||||
class ProfileAggregator(Profiler):
|
||||
|
||||
def __init__(self, path=None):
|
||||
Profiler.__init__(self, path)
|
||||
global _count
|
||||
self.count = _count = _count + 1
|
||||
self.profiler = profile.Profile()
|
||||
|
||||
def run(self, func, *args, **params):
|
||||
path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
|
||||
result = self.profiler.runcall(func, *args, **params)
|
||||
self.profiler.dump_stats(path)
|
||||
return result
|
||||
|
||||
|
||||
class make_app:
|
||||
|
||||
def __init__(self, nextapp, path=None, aggregate=False):
|
||||
"""Make a WSGI middleware app which wraps 'nextapp' with profiling.
|
||||
|
||||
nextapp
|
||||
the WSGI application to wrap, usually an instance of
|
||||
cherrypy.Application.
|
||||
|
||||
path
|
||||
where to dump the profiling output.
|
||||
|
||||
aggregate
|
||||
if True, profile data for all HTTP requests will go in
|
||||
a single file. If False (the default), each HTTP request will
|
||||
dump its profile data into a separate file.
|
||||
|
||||
"""
|
||||
if profile is None or pstats is None:
|
||||
msg = ('Your installation of Python does not have a profile '
|
||||
"module. If you're on Debian, try "
|
||||
'`sudo apt-get install python-profiler`. '
|
||||
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
|
||||
'for details.')
|
||||
warnings.warn(msg)
|
||||
|
||||
self.nextapp = nextapp
|
||||
self.aggregate = aggregate
|
||||
if aggregate:
|
||||
self.profiler = ProfileAggregator(path)
|
||||
else:
|
||||
self.profiler = Profiler(path)
|
||||
|
||||
def __call__(self, environ, start_response):
|
||||
def gather():
|
||||
result = []
|
||||
for line in self.nextapp(environ, start_response):
|
||||
result.append(line)
|
||||
return result
|
||||
return self.profiler.run(gather)
|
||||
|
||||
|
||||
def serve(path=None, port=8080):
|
||||
if profile is None or pstats is None:
|
||||
msg = ('Your installation of Python does not have a profile module. '
|
||||
"If you're on Debian, try "
|
||||
'`sudo apt-get install python-profiler`. '
|
||||
'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
|
||||
'for details.')
|
||||
warnings.warn(msg)
|
||||
|
||||
cherrypy.config.update({'server.socket_port': int(port),
|
||||
'server.thread_pool': 10,
|
||||
'environment': 'production',
|
||||
})
|
||||
cherrypy.quickstart(Profiler(path))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
serve(*tuple(sys.argv[1:]))
|
|
@ -1,534 +0,0 @@
|
|||
"""Generic configuration system using unrepr.
|
||||
|
||||
Configuration data may be supplied as a Python dictionary, as a filename,
|
||||
or as an open file object. When you supply a filename or file, Python's
|
||||
builtin ConfigParser is used (with some extensions).
|
||||
|
||||
Namespaces
|
||||
----------
|
||||
|
||||
Configuration keys are separated into namespaces by the first "." in the key.
|
||||
|
||||
The only key that cannot exist in a namespace is the "environment" entry.
|
||||
This special entry 'imports' other config entries from a template stored in
|
||||
the Config.environments dict.
|
||||
|
||||
You can define your own namespaces to be called when new config is merged
|
||||
by adding a named handler to Config.namespaces. The name can be any string,
|
||||
and the handler must be either a callable or a context manager.
|
||||
"""
|
||||
|
||||
try:
|
||||
# Python 3.0+
|
||||
from configparser import ConfigParser
|
||||
except ImportError:
|
||||
from ConfigParser import ConfigParser
|
||||
|
||||
try:
|
||||
text_or_bytes
|
||||
except NameError:
|
||||
text_or_bytes = str
|
||||
|
||||
try:
|
||||
# Python 3
|
||||
import builtins
|
||||
except ImportError:
|
||||
# Python 2
|
||||
import __builtin__ as builtins
|
||||
|
||||
import operator as _operator
|
||||
import sys
|
||||
|
||||
|
||||
def as_dict(config):
|
||||
"""Return a dict from 'config' whether it is a dict, file, or filename."""
|
||||
if isinstance(config, text_or_bytes):
|
||||
config = Parser().dict_from_file(config)
|
||||
elif hasattr(config, 'read'):
|
||||
config = Parser().dict_from_file(config)
|
||||
return config
|
||||
|
||||
|
||||
class NamespaceSet(dict):
|
||||
|
||||
"""A dict of config namespace names and handlers.
|
||||
|
||||
Each config entry should begin with a namespace name; the corresponding
|
||||
namespace handler will be called once for each config entry in that
|
||||
namespace, and will be passed two arguments: the config key (with the
|
||||
namespace removed) and the config value.
|
||||
|
||||
Namespace handlers may be any Python callable; they may also be
|
||||
Python 2.5-style 'context managers', in which case their __enter__
|
||||
method should return a callable to be used as the handler.
|
||||
See cherrypy.tools (the Toolbox class) for an example.
|
||||
"""
|
||||
|
||||
def __call__(self, config):
|
||||
"""Iterate through config and pass it to each namespace handler.
|
||||
|
||||
config
|
||||
A flat dict, where keys use dots to separate
|
||||
namespaces, and values are arbitrary.
|
||||
|
||||
The first name in each config key is used to look up the corresponding
|
||||
namespace handler. For example, a config entry of {'tools.gzip.on': v}
|
||||
will call the 'tools' namespace handler with the args: ('gzip.on', v)
|
||||
"""
|
||||
# Separate the given config into namespaces
|
||||
ns_confs = {}
|
||||
for k in config:
|
||||
if '.' in k:
|
||||
ns, name = k.split('.', 1)
|
||||
bucket = ns_confs.setdefault(ns, {})
|
||||
bucket[name] = config[k]
|
||||
|
||||
# I chose __enter__ and __exit__ so someday this could be
|
||||
# rewritten using Python 2.5's 'with' statement:
|
||||
# for ns, handler in self.iteritems():
|
||||
# with handler as callable:
|
||||
# for k, v in ns_confs.get(ns, {}).iteritems():
|
||||
# callable(k, v)
|
||||
for ns, handler in self.items():
|
||||
exit = getattr(handler, '__exit__', None)
|
||||
if exit:
|
||||
callable = handler.__enter__()
|
||||
no_exc = True
|
||||
try:
|
||||
try:
|
||||
for k, v in ns_confs.get(ns, {}).items():
|
||||
callable(k, v)
|
||||
except:
|
||||
# The exceptional case is handled here
|
||||
no_exc = False
|
||||
if exit is None:
|
||||
raise
|
||||
if not exit(*sys.exc_info()):
|
||||
raise
|
||||
# The exception is swallowed if exit() returns true
|
||||
finally:
|
||||
# The normal and non-local-goto cases are handled here
|
||||
if no_exc and exit:
|
||||
exit(None, None, None)
|
||||
else:
|
||||
for k, v in ns_confs.get(ns, {}).items():
|
||||
handler(k, v)
|
||||
|
||||
def __repr__(self):
|
||||
return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
|
||||
dict.__repr__(self))
|
||||
|
||||
def __copy__(self):
|
||||
newobj = self.__class__()
|
||||
newobj.update(self)
|
||||
return newobj
|
||||
copy = __copy__
|
||||
|
||||
|
||||
class Config(dict):
|
||||
|
||||
"""A dict-like set of configuration data, with defaults and namespaces.
|
||||
|
||||
May take a file, filename, or dict.
|
||||
"""
|
||||
|
||||
defaults = {}
|
||||
environments = {}
|
||||
namespaces = NamespaceSet()
|
||||
|
||||
def __init__(self, file=None, **kwargs):
|
||||
self.reset()
|
||||
if file is not None:
|
||||
self.update(file)
|
||||
if kwargs:
|
||||
self.update(kwargs)
|
||||
|
||||
def reset(self):
|
||||
"""Reset self to default values."""
|
||||
self.clear()
|
||||
dict.update(self, self.defaults)
|
||||
|
||||
def update(self, config):
|
||||
"""Update self from a dict, file or filename."""
|
||||
if isinstance(config, text_or_bytes):
|
||||
# Filename
|
||||
config = Parser().dict_from_file(config)
|
||||
elif hasattr(config, 'read'):
|
||||
# Open file object
|
||||
config = Parser().dict_from_file(config)
|
||||
else:
|
||||
config = config.copy()
|
||||
self._apply(config)
|
||||
|
||||
def _apply(self, config):
|
||||
"""Update self from a dict."""
|
||||
which_env = config.get('environment')
|
||||
if which_env:
|
||||
env = self.environments[which_env]
|
||||
for k in env:
|
||||
if k not in config:
|
||||
config[k] = env[k]
|
||||
|
||||
dict.update(self, config)
|
||||
self.namespaces(config)
|
||||
|
||||
def __setitem__(self, k, v):
|
||||
dict.__setitem__(self, k, v)
|
||||
self.namespaces({k: v})
|
||||
|
||||
|
||||
class Parser(ConfigParser):
|
||||
|
||||
"""Sub-class of ConfigParser that keeps the case of options and that
|
||||
raises an exception if the file cannot be read.
|
||||
"""
|
||||
|
||||
def optionxform(self, optionstr):
|
||||
return optionstr
|
||||
|
||||
def read(self, filenames):
|
||||
if isinstance(filenames, text_or_bytes):
|
||||
filenames = [filenames]
|
||||
for filename in filenames:
|
||||
# try:
|
||||
# fp = open(filename)
|
||||
# except IOError:
|
||||
# continue
|
||||
fp = open(filename)
|
||||
try:
|
||||
self._read(fp, filename)
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
def as_dict(self, raw=False, vars=None):
|
||||
"""Convert an INI file to a dictionary"""
|
||||
# Load INI file into a dict
|
||||
result = {}
|
||||
for section in self.sections():
|
||||
if section not in result:
|
||||
result[section] = {}
|
||||
for option in self.options(section):
|
||||
value = self.get(section, option, raw=raw, vars=vars)
|
||||
try:
|
||||
value = unrepr(value)
|
||||
except Exception:
|
||||
x = sys.exc_info()[1]
|
||||
msg = ('Config error in section: %r, option: %r, '
|
||||
'value: %r. Config values must be valid Python.' %
|
||||
(section, option, value))
|
||||
raise ValueError(msg, x.__class__.__name__, x.args)
|
||||
result[section][option] = value
|
||||
return result
|
||||
|
||||
def dict_from_file(self, file):
|
||||
if hasattr(file, 'read'):
|
||||
self.readfp(file)
|
||||
else:
|
||||
self.read(file)
|
||||
return self.as_dict()
|
||||
|
||||
|
||||
# public domain "unrepr" implementation, found on the web and then improved.
|
||||
|
||||
|
||||
class _Builder2:
|
||||
|
||||
def build(self, o):
|
||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||
if m is None:
|
||||
raise TypeError('unrepr does not recognize %s' %
|
||||
repr(o.__class__.__name__))
|
||||
return m(o)
|
||||
|
||||
def astnode(self, s):
|
||||
"""Return a Python2 ast Node compiled from a string."""
|
||||
try:
|
||||
import compiler
|
||||
except ImportError:
|
||||
# Fallback to eval when compiler package is not available,
|
||||
# e.g. IronPython 1.0.
|
||||
return eval(s)
|
||||
|
||||
p = compiler.parse('__tempvalue__ = ' + s)
|
||||
return p.getChildren()[1].getChildren()[0].getChildren()[1]
|
||||
|
||||
def build_Subscript(self, o):
|
||||
expr, flags, subs = o.getChildren()
|
||||
expr = self.build(expr)
|
||||
subs = self.build(subs)
|
||||
return expr[subs]
|
||||
|
||||
def build_CallFunc(self, o):
|
||||
children = o.getChildren()
|
||||
# Build callee from first child
|
||||
callee = self.build(children[0])
|
||||
# Build args and kwargs from remaining children
|
||||
args = []
|
||||
kwargs = {}
|
||||
for child in children[1:]:
|
||||
class_name = child.__class__.__name__
|
||||
# None is ignored
|
||||
if class_name == 'NoneType':
|
||||
continue
|
||||
# Keywords become kwargs
|
||||
if class_name == 'Keyword':
|
||||
kwargs.update(self.build(child))
|
||||
# Everything else becomes args
|
||||
else :
|
||||
args.append(self.build(child))
|
||||
|
||||
return callee(*args, **kwargs)
|
||||
|
||||
def build_Keyword(self, o):
|
||||
key, value_obj = o.getChildren()
|
||||
value = self.build(value_obj)
|
||||
kw_dict = {key: value}
|
||||
return kw_dict
|
||||
|
||||
def build_List(self, o):
|
||||
return map(self.build, o.getChildren())
|
||||
|
||||
def build_Const(self, o):
|
||||
return o.value
|
||||
|
||||
def build_Dict(self, o):
|
||||
d = {}
|
||||
i = iter(map(self.build, o.getChildren()))
|
||||
for el in i:
|
||||
d[el] = i.next()
|
||||
return d
|
||||
|
||||
def build_Tuple(self, o):
|
||||
return tuple(self.build_List(o))
|
||||
|
||||
def build_Name(self, o):
|
||||
name = o.name
|
||||
if name == 'None':
|
||||
return None
|
||||
if name == 'True':
|
||||
return True
|
||||
if name == 'False':
|
||||
return False
|
||||
|
||||
# See if the Name is a package or module. If it is, import it.
|
||||
try:
|
||||
return modules(name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# See if the Name is in builtins.
|
||||
try:
|
||||
return getattr(builtins, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError('unrepr could not resolve the name %s' % repr(name))
|
||||
|
||||
def build_Add(self, o):
|
||||
left, right = map(self.build, o.getChildren())
|
||||
return left + right
|
||||
|
||||
def build_Mul(self, o):
|
||||
left, right = map(self.build, o.getChildren())
|
||||
return left * right
|
||||
|
||||
def build_Getattr(self, o):
|
||||
parent = self.build(o.expr)
|
||||
return getattr(parent, o.attrname)
|
||||
|
||||
def build_NoneType(self, o):
|
||||
return None
|
||||
|
||||
def build_UnarySub(self, o):
|
||||
return -self.build(o.getChildren()[0])
|
||||
|
||||
def build_UnaryAdd(self, o):
|
||||
return self.build(o.getChildren()[0])
|
||||
|
||||
|
||||
class _Builder3:
|
||||
|
||||
def build(self, o):
|
||||
m = getattr(self, 'build_' + o.__class__.__name__, None)
|
||||
if m is None:
|
||||
raise TypeError('unrepr does not recognize %s' %
|
||||
repr(o.__class__.__name__))
|
||||
return m(o)
|
||||
|
||||
def astnode(self, s):
|
||||
"""Return a Python3 ast Node compiled from a string."""
|
||||
try:
|
||||
import ast
|
||||
except ImportError:
|
||||
# Fallback to eval when ast package is not available,
|
||||
# e.g. IronPython 1.0.
|
||||
return eval(s)
|
||||
|
||||
p = ast.parse('__tempvalue__ = ' + s)
|
||||
return p.body[0].value
|
||||
|
||||
def build_Subscript(self, o):
|
||||
return self.build(o.value)[self.build(o.slice)]
|
||||
|
||||
def build_Index(self, o):
|
||||
return self.build(o.value)
|
||||
|
||||
def _build_call35(self, o):
|
||||
"""
|
||||
Workaround for python 3.5 _ast.Call signature, docs found here
|
||||
https://greentreesnakes.readthedocs.org/en/latest/nodes.html
|
||||
"""
|
||||
import ast
|
||||
callee = self.build(o.func)
|
||||
args = []
|
||||
if o.args is not None:
|
||||
for a in o.args:
|
||||
if isinstance(a, ast.Starred):
|
||||
args.append(self.build(a.value))
|
||||
else:
|
||||
args.append(self.build(a))
|
||||
kwargs = {}
|
||||
for kw in o.keywords:
|
||||
if kw.arg is None: # double asterix `**`
|
||||
rst = self.build(kw.value)
|
||||
if not isinstance(rst, dict):
|
||||
raise TypeError('Invalid argument for call.'
|
||||
'Must be a mapping object.')
|
||||
# give preference to the keys set directly from arg=value
|
||||
for k, v in rst.items():
|
||||
if k not in kwargs:
|
||||
kwargs[k] = v
|
||||
else: # defined on the call as: arg=value
|
||||
kwargs[kw.arg] = self.build(kw.value)
|
||||
return callee(*args, **kwargs)
|
||||
|
||||
def build_Call(self, o):
|
||||
if sys.version_info >= (3, 5):
|
||||
return self._build_call35(o)
|
||||
|
||||
callee = self.build(o.func)
|
||||
|
||||
if o.args is None:
|
||||
args = ()
|
||||
else:
|
||||
args = tuple([self.build(a) for a in o.args])
|
||||
|
||||
if o.starargs is None:
|
||||
starargs = ()
|
||||
else:
|
||||
starargs = tuple(self.build(o.starargs))
|
||||
|
||||
if o.kwargs is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = self.build(o.kwargs)
|
||||
if o.keywords is not None: # direct a=b keywords
|
||||
for kw in o.keywords:
|
||||
# preference because is a direct keyword against **kwargs
|
||||
kwargs[kw.arg] = self.build(kw.value)
|
||||
return callee(*(args + starargs), **kwargs)
|
||||
|
||||
def build_List(self, o):
|
||||
return list(map(self.build, o.elts))
|
||||
|
||||
def build_Str(self, o):
|
||||
return o.s
|
||||
|
||||
def build_Num(self, o):
|
||||
return o.n
|
||||
|
||||
def build_Dict(self, o):
|
||||
return dict([(self.build(k), self.build(v))
|
||||
for k, v in zip(o.keys, o.values)])
|
||||
|
||||
def build_Tuple(self, o):
|
||||
return tuple(self.build_List(o))
|
||||
|
||||
def build_Name(self, o):
|
||||
name = o.id
|
||||
if name == 'None':
|
||||
return None
|
||||
if name == 'True':
|
||||
return True
|
||||
if name == 'False':
|
||||
return False
|
||||
|
||||
# See if the Name is a package or module. If it is, import it.
|
||||
try:
|
||||
return modules(name)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# See if the Name is in builtins.
|
||||
try:
|
||||
import builtins
|
||||
return getattr(builtins, name)
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
raise TypeError('unrepr could not resolve the name %s' % repr(name))
|
||||
|
||||
def build_NameConstant(self, o):
|
||||
return o.value
|
||||
|
||||
def build_UnaryOp(self, o):
|
||||
op, operand = map(self.build, [o.op, o.operand])
|
||||
return op(operand)
|
||||
|
||||
def build_BinOp(self, o):
|
||||
left, op, right = map(self.build, [o.left, o.op, o.right])
|
||||
return op(left, right)
|
||||
|
||||
def build_Add(self, o):
|
||||
return _operator.add
|
||||
|
||||
def build_Mult(self, o):
|
||||
return _operator.mul
|
||||
|
||||
def build_USub(self, o):
|
||||
return _operator.neg
|
||||
|
||||
def build_Attribute(self, o):
|
||||
parent = self.build(o.value)
|
||||
return getattr(parent, o.attr)
|
||||
|
||||
def build_NoneType(self, o):
|
||||
return None
|
||||
|
||||
|
||||
def unrepr(s):
|
||||
"""Return a Python object compiled from a string."""
|
||||
if not s:
|
||||
return s
|
||||
if sys.version_info < (3, 0):
|
||||
b = _Builder2()
|
||||
else:
|
||||
b = _Builder3()
|
||||
obj = b.astnode(s)
|
||||
return b.build(obj)
|
||||
|
||||
|
||||
def modules(modulePath):
|
||||
"""Load a module and retrieve a reference to that module."""
|
||||
__import__(modulePath)
|
||||
return sys.modules[modulePath]
|
||||
|
||||
|
||||
def attributes(full_attribute_name):
|
||||
"""Load a module and retrieve an attribute of that module."""
|
||||
|
||||
# Parse out the path, module, and attribute
|
||||
last_dot = full_attribute_name.rfind('.')
|
||||
attr_name = full_attribute_name[last_dot + 1:]
|
||||
mod_path = full_attribute_name[:last_dot]
|
||||
|
||||
mod = modules(mod_path)
|
||||
# Let an AttributeError propagate outward.
|
||||
try:
|
||||
attr = getattr(mod, attr_name)
|
||||
except AttributeError:
|
||||
raise AttributeError("'%s' object has no attribute '%s'"
|
||||
% (mod_path, attr_name))
|
||||
|
||||
# Return a reference to the attribute.
|
||||
return attr
|
|
@ -1,883 +0,0 @@
|
|||
"""Session implementation for CherryPy.
|
||||
|
||||
You need to edit your config file to use sessions. Here's an example::
|
||||
|
||||
[/]
|
||||
tools.sessions.on = True
|
||||
tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
|
||||
tools.sessions.storage_path = "/home/site/sessions"
|
||||
tools.sessions.timeout = 60
|
||||
|
||||
This sets the session to be stored in files in the directory
|
||||
/home/site/sessions, and the session timeout to 60 minutes. If you omit
|
||||
``storage_class``, the sessions will be saved in RAM.
|
||||
``tools.sessions.on`` is the only required line for working sessions,
|
||||
the rest are optional.
|
||||
|
||||
By default, the session ID is passed in a cookie, so the client's browser must
|
||||
have cookies enabled for your site.
|
||||
|
||||
To set data for the current session, use
|
||||
``cherrypy.session['fieldname'] = 'fieldvalue'``;
|
||||
to get data use ``cherrypy.session.get('fieldname')``.
|
||||
|
||||
================
|
||||
Locking sessions
|
||||
================
|
||||
|
||||
By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
|
||||
the session is locked early and unlocked late. Be mindful of this default mode
|
||||
for any requests that take a long time to process (streaming responses,
|
||||
expensive calculations, database lookups, API calls, etc), as other concurrent
|
||||
requests that also utilize sessions will hang until the session is unlocked.
|
||||
|
||||
If you want to control when the session data is locked and unlocked,
|
||||
set ``tools.sessions.locking = 'explicit'``. Then call
|
||||
``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
|
||||
Regardless of which mode you use, the session is guaranteed to be unlocked when
|
||||
the request is complete.
|
||||
|
||||
=================
|
||||
Expiring Sessions
|
||||
=================
|
||||
|
||||
You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
|
||||
Simply call that function at the point you want the session to expire, and it
|
||||
will cause the session cookie to expire client-side.
|
||||
|
||||
===========================
|
||||
Session Fixation Protection
|
||||
===========================
|
||||
|
||||
If CherryPy receives, via a request cookie, a session id that it does not
|
||||
recognize, it will reject that id and create a new one to return in the
|
||||
response cookie. This `helps prevent session fixation attacks
|
||||
<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
|
||||
However, CherryPy "recognizes" a session id by looking up the saved session
|
||||
data for that id. Therefore, if you never save any session data,
|
||||
**you will get a new session id for every request**.
|
||||
|
||||
================
|
||||
Sharing Sessions
|
||||
================
|
||||
|
||||
If you run multiple instances of CherryPy (for example via mod_python behind
|
||||
Apache prefork), you most likely cannot use the RAM session backend, since each
|
||||
instance of CherryPy will have its own memory space. Use a different backend
|
||||
instead, and verify that all instances are pointing at the same file or db
|
||||
location. Alternately, you might try a load balancer which makes sessions
|
||||
"sticky". Google is your friend, there.
|
||||
|
||||
================
|
||||
Expiration Dates
|
||||
================
|
||||
|
||||
The response cookie will possess an expiration date to inform the client at
|
||||
which point to stop sending the cookie back in requests. If the server time
|
||||
and client time differ, expect sessions to be unreliable. **Make sure the
|
||||
system time of your server is accurate**.
|
||||
|
||||
CherryPy defaults to a 60-minute session timeout, which also applies to the
|
||||
cookie which is sent to the client. Unfortunately, some versions of Safari
|
||||
("4 public beta" on Windows XP at least) appear to have a bug in their parsing
|
||||
of the GMT expiration date--they appear to interpret the date as one hour in
|
||||
the past. Sixty minutes minus one hour is pretty close to zero, so you may
|
||||
experience this bug as a new session id for every request, unless the requests
|
||||
are less than one second apart. To fix, try increasing the session.timeout.
|
||||
|
||||
On the other extreme, some users report Firefox sending cookies after their
|
||||
expiration date, although this was on a system with an inaccurate system time.
|
||||
Maybe FF doesn't trust system time.
|
||||
"""
|
||||
import sys
|
||||
import datetime
|
||||
import os
|
||||
import time
|
||||
import threading
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import copyitems, pickle, random20
|
||||
from cherrypy.lib import httputil
|
||||
from cherrypy.lib import lockfile
|
||||
from cherrypy.lib import locking
|
||||
from cherrypy.lib import is_iterator
|
||||
|
||||
missing = object()
|
||||
|
||||
|
||||
class Session(object):
|
||||
|
||||
"""A CherryPy dict-like Session object (one per request)."""
|
||||
|
||||
_id = None
|
||||
|
||||
id_observers = None
|
||||
"A list of callbacks to which to pass new id's."
|
||||
|
||||
def _get_id(self):
|
||||
return self._id
|
||||
|
||||
def _set_id(self, value):
|
||||
self._id = value
|
||||
for o in self.id_observers:
|
||||
o(value)
|
||||
id = property(_get_id, _set_id, doc='The current session ID.')
|
||||
|
||||
timeout = 60
|
||||
'Number of minutes after which to delete session data.'
|
||||
|
||||
locked = False
|
||||
"""
|
||||
If True, this session instance has exclusive read/write access
|
||||
to session data."""
|
||||
|
||||
loaded = False
|
||||
"""
|
||||
If True, data has been retrieved from storage. This should happen
|
||||
automatically on the first attempt to access session data."""
|
||||
|
||||
clean_thread = None
|
||||
'Class-level Monitor which calls self.clean_up.'
|
||||
|
||||
clean_freq = 5
|
||||
'The poll rate for expired session cleanup in minutes.'
|
||||
|
||||
originalid = None
|
||||
'The session id passed by the client. May be missing or unsafe.'
|
||||
|
||||
missing = False
|
||||
'True if the session requested by the client did not exist.'
|
||||
|
||||
regenerated = False
|
||||
"""
|
||||
True if the application called session.regenerate(). This is not set by
|
||||
internal calls to regenerate the session id."""
|
||||
|
||||
debug = False
|
||||
'If True, log debug information.'
|
||||
|
||||
# --------------------- Session management methods --------------------- #
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
self.id_observers = []
|
||||
self._data = {}
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
self.originalid = id
|
||||
self.missing = False
|
||||
if id is None:
|
||||
if self.debug:
|
||||
cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
|
||||
self._regenerate()
|
||||
else:
|
||||
self.id = id
|
||||
if self._exists():
|
||||
if self.debug:
|
||||
cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS')
|
||||
else:
|
||||
if self.debug:
|
||||
cherrypy.log('Expired or malicious session %r; '
|
||||
'making a new one' % id, 'TOOLS.SESSIONS')
|
||||
# Expired or malicious session. Make a new one.
|
||||
# See https://github.com/cherrypy/cherrypy/issues/709.
|
||||
self.id = None
|
||||
self.missing = True
|
||||
self._regenerate()
|
||||
|
||||
def now(self):
|
||||
"""Generate the session specific concept of 'now'.
|
||||
|
||||
Other session providers can override this to use alternative,
|
||||
possibly timezone aware, versions of 'now'.
|
||||
"""
|
||||
return datetime.datetime.now()
|
||||
|
||||
def regenerate(self):
|
||||
"""Replace the current session (with a new id)."""
|
||||
self.regenerated = True
|
||||
self._regenerate()
|
||||
|
||||
def _regenerate(self):
|
||||
if self.id is not None:
|
||||
if self.debug:
|
||||
cherrypy.log(
|
||||
'Deleting the existing session %r before '
|
||||
'regeneration.' % self.id,
|
||||
'TOOLS.SESSIONS')
|
||||
self.delete()
|
||||
|
||||
old_session_was_locked = self.locked
|
||||
if old_session_was_locked:
|
||||
self.release_lock()
|
||||
if self.debug:
|
||||
cherrypy.log('Old lock released.', 'TOOLS.SESSIONS')
|
||||
|
||||
self.id = None
|
||||
while self.id is None:
|
||||
self.id = self.generate_id()
|
||||
# Assert that the generated id is not already stored.
|
||||
if self._exists():
|
||||
self.id = None
|
||||
if self.debug:
|
||||
cherrypy.log('Set id to generated %s.' % self.id,
|
||||
'TOOLS.SESSIONS')
|
||||
|
||||
if old_session_was_locked:
|
||||
self.acquire_lock()
|
||||
if self.debug:
|
||||
cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS')
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
pass
|
||||
|
||||
def generate_id(self):
|
||||
"""Return a new session id."""
|
||||
return random20()
|
||||
|
||||
def save(self):
|
||||
"""Save session data."""
|
||||
try:
|
||||
# If session data has never been loaded then it's never been
|
||||
# accessed: no need to save it
|
||||
if self.loaded:
|
||||
t = datetime.timedelta(seconds=self.timeout * 60)
|
||||
expiration_time = self.now() + t
|
||||
if self.debug:
|
||||
cherrypy.log('Saving session %r with expiry %s' %
|
||||
(self.id, expiration_time),
|
||||
'TOOLS.SESSIONS')
|
||||
self._save(expiration_time)
|
||||
else:
|
||||
if self.debug:
|
||||
cherrypy.log(
|
||||
'Skipping save of session %r (no session loaded).' %
|
||||
self.id, 'TOOLS.SESSIONS')
|
||||
finally:
|
||||
if self.locked:
|
||||
# Always release the lock if the user didn't release it
|
||||
self.release_lock()
|
||||
if self.debug:
|
||||
cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS')
|
||||
|
||||
def load(self):
|
||||
"""Copy stored session data into this session instance."""
|
||||
data = self._load()
|
||||
# data is either None or a tuple (session_data, expiration_time)
|
||||
if data is None or data[1] < self.now():
|
||||
if self.debug:
|
||||
cherrypy.log('Expired session %r, flushing data.' % self.id,
|
||||
'TOOLS.SESSIONS')
|
||||
self._data = {}
|
||||
else:
|
||||
if self.debug:
|
||||
cherrypy.log('Data loaded for session %r.' % self.id,
|
||||
'TOOLS.SESSIONS')
|
||||
self._data = data[0]
|
||||
self.loaded = True
|
||||
|
||||
# Stick the clean_thread in the class, not the instance.
|
||||
# The instances are created and destroyed per-request.
|
||||
cls = self.__class__
|
||||
if self.clean_freq and not cls.clean_thread:
|
||||
# clean_up is an instancemethod and not a classmethod,
|
||||
# so that tool config can be accessed inside the method.
|
||||
t = cherrypy.process.plugins.Monitor(
|
||||
cherrypy.engine, self.clean_up, self.clean_freq * 60,
|
||||
name='Session cleanup')
|
||||
t.subscribe()
|
||||
cls.clean_thread = t
|
||||
t.start()
|
||||
if self.debug:
|
||||
cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS')
|
||||
|
||||
def delete(self):
|
||||
"""Delete stored session data."""
|
||||
self._delete()
|
||||
if self.debug:
|
||||
cherrypy.log('Deleted session %s.' % self.id,
|
||||
'TOOLS.SESSIONS')
|
||||
|
||||
# -------------------- Application accessor methods -------------------- #
|
||||
|
||||
def __getitem__(self, key):
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data[key]
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
self._data[key] = value
|
||||
|
||||
def __delitem__(self, key):
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
del self._data[key]
|
||||
|
||||
def pop(self, key, default=missing):
|
||||
"""Remove the specified key and return the corresponding value.
|
||||
If key is not found, default is returned if given,
|
||||
otherwise KeyError is raised.
|
||||
"""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
if default is missing:
|
||||
return self._data.pop(key)
|
||||
else:
|
||||
return self._data.pop(key, default)
|
||||
|
||||
def __contains__(self, key):
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return key in self._data
|
||||
|
||||
if hasattr({}, 'has_key'):
|
||||
def has_key(self, key):
|
||||
"""D.has_key(k) -> True if D has a key k, else False."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return key in self._data
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.get(key, default)
|
||||
|
||||
def update(self, d):
|
||||
"""D.update(E) -> None. Update D from E: for k in E: D[k] = E[k]."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
self._data.update(d)
|
||||
|
||||
def setdefault(self, key, default=None):
|
||||
"""D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.setdefault(key, default)
|
||||
|
||||
def clear(self):
|
||||
"""D.clear() -> None. Remove all items from D."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
self._data.clear()
|
||||
|
||||
def keys(self):
|
||||
"""D.keys() -> list of D's keys."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.keys()
|
||||
|
||||
def items(self):
|
||||
"""D.items() -> list of D's (key, value) pairs, as 2-tuples."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.items()
|
||||
|
||||
def values(self):
|
||||
"""D.values() -> list of D's values."""
|
||||
if not self.loaded:
|
||||
self.load()
|
||||
return self._data.values()
|
||||
|
||||
|
||||
class RamSession(Session):
|
||||
|
||||
# Class-level objects. Don't rebind these!
|
||||
cache = {}
|
||||
locks = {}
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
|
||||
now = self.now()
|
||||
for _id, (data, expiration_time) in copyitems(self.cache):
|
||||
if expiration_time <= now:
|
||||
try:
|
||||
del self.cache[_id]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
if self.locks[_id].acquire(blocking=False):
|
||||
lock = self.locks.pop(_id)
|
||||
lock.release()
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
# added to remove obsolete lock objects
|
||||
for _id in list(self.locks):
|
||||
if _id not in self.cache and self.locks[_id].acquire(blocking=False):
|
||||
lock = self.locks.pop(_id)
|
||||
lock.release()
|
||||
|
||||
def _exists(self):
|
||||
return self.id in self.cache
|
||||
|
||||
def _load(self):
|
||||
return self.cache.get(self.id)
|
||||
|
||||
def _save(self, expiration_time):
|
||||
self.cache[self.id] = (self._data, expiration_time)
|
||||
|
||||
def _delete(self):
|
||||
self.cache.pop(self.id, None)
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len(self.cache)
|
||||
|
||||
|
||||
class FileSession(Session):
|
||||
|
||||
"""Implementation of the File backend for sessions
|
||||
|
||||
storage_path
|
||||
The folder where session data will be saved. Each session
|
||||
will be saved as pickle.dump(data, expiration_time) in its own file;
|
||||
the filename will be self.SESSION_PREFIX + self.id.
|
||||
|
||||
lock_timeout
|
||||
A timedelta or numeric seconds indicating how long
|
||||
to block acquiring a lock. If None (default), acquiring a lock
|
||||
will block indefinitely.
|
||||
"""
|
||||
|
||||
SESSION_PREFIX = 'session-'
|
||||
LOCK_SUFFIX = '.lock'
|
||||
pickle_protocol = pickle.HIGHEST_PROTOCOL
|
||||
|
||||
def __init__(self, id=None, **kwargs):
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
kwargs.setdefault('lock_timeout', None)
|
||||
|
||||
Session.__init__(self, id=id, **kwargs)
|
||||
|
||||
# validate self.lock_timeout
|
||||
if isinstance(self.lock_timeout, (int, float)):
|
||||
self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
|
||||
if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
|
||||
raise ValueError('Lock timeout must be numeric seconds or '
|
||||
'a timedelta instance.')
|
||||
|
||||
@classmethod
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for file-based sessions.
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
# The 'storage_path' arg is required for file-based sessions.
|
||||
kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
|
||||
|
||||
for k, v in kwargs.items():
|
||||
setattr(cls, k, v)
|
||||
|
||||
def _get_file_path(self):
|
||||
f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
|
||||
if not os.path.abspath(f).startswith(self.storage_path):
|
||||
raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
|
||||
return f
|
||||
|
||||
def _exists(self):
|
||||
path = self._get_file_path()
|
||||
return os.path.exists(path)
|
||||
|
||||
def _load(self, path=None):
|
||||
assert self.locked, ('The session load without being locked. '
|
||||
"Check your tools' priority levels.")
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
try:
|
||||
f = open(path, 'rb')
|
||||
try:
|
||||
return pickle.load(f)
|
||||
finally:
|
||||
f.close()
|
||||
except (IOError, EOFError):
|
||||
e = sys.exc_info()[1]
|
||||
if self.debug:
|
||||
cherrypy.log('Error loading the session pickle: %s' %
|
||||
e, 'TOOLS.SESSIONS')
|
||||
return None
|
||||
|
||||
def _save(self, expiration_time):
|
||||
assert self.locked, ('The session was saved without being locked. '
|
||||
"Check your tools' priority levels.")
|
||||
f = open(self._get_file_path(), 'wb')
|
||||
try:
|
||||
pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
def _delete(self):
|
||||
assert self.locked, ('The session deletion without being locked. '
|
||||
"Check your tools' priority levels.")
|
||||
try:
|
||||
os.unlink(self._get_file_path())
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def acquire_lock(self, path=None):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
if path is None:
|
||||
path = self._get_file_path()
|
||||
path += self.LOCK_SUFFIX
|
||||
checker = locking.LockChecker(self.id, self.lock_timeout)
|
||||
while not checker.expired():
|
||||
try:
|
||||
self.lock = lockfile.LockFile(path)
|
||||
except lockfile.LockError:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
self.locked = True
|
||||
if self.debug:
|
||||
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
|
||||
|
||||
def release_lock(self, path=None):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.lock.release()
|
||||
self.lock.remove()
|
||||
self.locked = False
|
||||
|
||||
def clean_up(self):
|
||||
"""Clean up expired sessions."""
|
||||
now = self.now()
|
||||
# Iterate over all session files in self.storage_path
|
||||
for fname in os.listdir(self.storage_path):
|
||||
if (fname.startswith(self.SESSION_PREFIX)
|
||||
and not fname.endswith(self.LOCK_SUFFIX)):
|
||||
# We have a session file: lock and load it and check
|
||||
# if it's expired. If it fails, nevermind.
|
||||
path = os.path.join(self.storage_path, fname)
|
||||
self.acquire_lock(path)
|
||||
if self.debug:
|
||||
# This is a bit of a hack, since we're calling clean_up
|
||||
# on the first instance rather than the entire class,
|
||||
# so depending on whether you have "debug" set on the
|
||||
# path of the first session called, this may not run.
|
||||
cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS')
|
||||
|
||||
try:
|
||||
contents = self._load(path)
|
||||
# _load returns None on IOError
|
||||
if contents is not None:
|
||||
data, expiration_time = contents
|
||||
if expiration_time < now:
|
||||
# Session expired: deleting it
|
||||
os.unlink(path)
|
||||
finally:
|
||||
self.release_lock(path)
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
return len([fname for fname in os.listdir(self.storage_path)
|
||||
if (fname.startswith(self.SESSION_PREFIX)
|
||||
and not fname.endswith(self.LOCK_SUFFIX))])
|
||||
|
||||
|
||||
class MemcachedSession(Session):
|
||||
|
||||
# The most popular memcached client for Python isn't thread-safe.
|
||||
# Wrap all .get and .set operations in a single lock.
|
||||
mc_lock = threading.RLock()
|
||||
|
||||
# This is a seperate set of locks per session id.
|
||||
locks = {}
|
||||
|
||||
servers = ['127.0.0.1:11211']
|
||||
|
||||
@classmethod
|
||||
def setup(cls, **kwargs):
|
||||
"""Set up the storage system for memcached-based sessions.
|
||||
|
||||
This should only be called once per process; this will be done
|
||||
automatically when using sessions.init (as the built-in Tool does).
|
||||
"""
|
||||
for k, v in kwargs.items():
|
||||
setattr(cls, k, v)
|
||||
|
||||
import memcache
|
||||
cls.cache = memcache.Client(cls.servers)
|
||||
|
||||
def _exists(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return bool(self.cache.get(self.id))
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _load(self):
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
return self.cache.get(self.id)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _save(self, expiration_time):
|
||||
# Send the expiration time as "Unix time" (seconds since 1/1/1970)
|
||||
td = int(time.mktime(expiration_time.timetuple()))
|
||||
self.mc_lock.acquire()
|
||||
try:
|
||||
if not self.cache.set(self.id, (self._data, expiration_time), td):
|
||||
raise AssertionError(
|
||||
'Session data for id %r not set.' % self.id)
|
||||
finally:
|
||||
self.mc_lock.release()
|
||||
|
||||
def _delete(self):
|
||||
self.cache.delete(self.id)
|
||||
|
||||
def acquire_lock(self):
|
||||
"""Acquire an exclusive lock on the currently-loaded session data."""
|
||||
self.locked = True
|
||||
self.locks.setdefault(self.id, threading.RLock()).acquire()
|
||||
if self.debug:
|
||||
cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
|
||||
|
||||
def release_lock(self):
|
||||
"""Release the lock on the currently-loaded session data."""
|
||||
self.locks[self.id].release()
|
||||
self.locked = False
|
||||
|
||||
def __len__(self):
|
||||
"""Return the number of active sessions."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
# Hook functions (for CherryPy tools)
|
||||
|
||||
def save():
|
||||
"""Save any changed session data."""
|
||||
|
||||
if not hasattr(cherrypy.serving, 'session'):
|
||||
return
|
||||
request = cherrypy.serving.request
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(request, '_sessionsaved'):
|
||||
return
|
||||
request._sessionsaved = True
|
||||
|
||||
if response.stream:
|
||||
# If the body is being streamed, we have to save the data
|
||||
# *after* the response has been written out
|
||||
request.hooks.attach('on_end_request', cherrypy.session.save)
|
||||
else:
|
||||
# If the body is not being streamed, we save the data now
|
||||
# (so we can release the lock).
|
||||
if is_iterator(response.body):
|
||||
response.collapse_body()
|
||||
cherrypy.session.save()
|
||||
save.failsafe = True
|
||||
|
||||
|
||||
def close():
|
||||
"""Close the session object for this request."""
|
||||
sess = getattr(cherrypy.serving, 'session', None)
|
||||
if getattr(sess, 'locked', False):
|
||||
# If the session is still locked we release the lock
|
||||
sess.release_lock()
|
||||
if sess.debug:
|
||||
cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
|
||||
close.failsafe = True
|
||||
close.priority = 90
|
||||
|
||||
|
||||
def init(storage_type=None, path=None, path_header=None, name='session_id',
|
||||
timeout=60, domain=None, secure=False, clean_freq=5,
|
||||
persistent=True, httponly=False, debug=False,
|
||||
# Py27 compat
|
||||
# *, storage_class=RamSession,
|
||||
**kwargs):
|
||||
"""Initialize session object (using cookies).
|
||||
|
||||
storage_class
|
||||
The Session subclass to use. Defaults to RamSession.
|
||||
|
||||
storage_type
|
||||
(deprecated)
|
||||
One of 'ram', 'file', memcached'. This will be
|
||||
used to look up the corresponding class in cherrypy.lib.sessions
|
||||
globals. For example, 'file' will use the FileSession class.
|
||||
|
||||
path
|
||||
The 'path' value to stick in the response cookie metadata.
|
||||
|
||||
path_header
|
||||
If 'path' is None (the default), then the response
|
||||
cookie 'path' will be pulled from request.headers[path_header].
|
||||
|
||||
name
|
||||
The name of the cookie.
|
||||
|
||||
timeout
|
||||
The expiration timeout (in minutes) for the stored session data.
|
||||
If 'persistent' is True (the default), this is also the timeout
|
||||
for the cookie.
|
||||
|
||||
domain
|
||||
The cookie domain.
|
||||
|
||||
secure
|
||||
If False (the default) the cookie 'secure' value will not
|
||||
be set. If True, the cookie 'secure' value will be set (to 1).
|
||||
|
||||
clean_freq (minutes)
|
||||
The poll rate for expired session cleanup.
|
||||
|
||||
persistent
|
||||
If True (the default), the 'timeout' argument will be used
|
||||
to expire the cookie. If False, the cookie will not have an expiry,
|
||||
and the cookie will be a "session cookie" which expires when the
|
||||
browser is closed.
|
||||
|
||||
httponly
|
||||
If False (the default) the cookie 'httponly' value will not be set.
|
||||
If True, the cookie 'httponly' value will be set (to 1).
|
||||
|
||||
Any additional kwargs will be bound to the new Session instance,
|
||||
and may be specific to the storage type. See the subclass of Session
|
||||
you're using for more information.
|
||||
"""
|
||||
|
||||
# Py27 compat
|
||||
storage_class = kwargs.pop('storage_class', RamSession)
|
||||
|
||||
request = cherrypy.serving.request
|
||||
|
||||
# Guard against running twice
|
||||
if hasattr(request, '_session_init_flag'):
|
||||
return
|
||||
request._session_init_flag = True
|
||||
|
||||
# Check if request came with a session ID
|
||||
id = None
|
||||
if name in request.cookie:
|
||||
id = request.cookie[name].value
|
||||
if debug:
|
||||
cherrypy.log('ID obtained from request.cookie: %r' % id,
|
||||
'TOOLS.SESSIONS')
|
||||
|
||||
first_time = not hasattr(cherrypy, 'session')
|
||||
|
||||
if storage_type:
|
||||
if first_time:
|
||||
msg = 'storage_type is deprecated. Supply storage_class instead'
|
||||
cherrypy.log(msg)
|
||||
storage_class = storage_type.title() + 'Session'
|
||||
storage_class = globals()[storage_class]
|
||||
|
||||
# call setup first time only
|
||||
if first_time:
|
||||
if hasattr(storage_class, 'setup'):
|
||||
storage_class.setup(**kwargs)
|
||||
|
||||
# Create and attach a new Session instance to cherrypy.serving.
|
||||
# It will possess a reference to (and lock, and lazily load)
|
||||
# the requested session data.
|
||||
kwargs['timeout'] = timeout
|
||||
kwargs['clean_freq'] = clean_freq
|
||||
cherrypy.serving.session = sess = storage_class(id, **kwargs)
|
||||
sess.debug = debug
|
||||
|
||||
def update_cookie(id):
|
||||
"""Update the cookie every time the session id changes."""
|
||||
cherrypy.serving.response.cookie[name] = id
|
||||
sess.id_observers.append(update_cookie)
|
||||
|
||||
# Create cherrypy.session which will proxy to cherrypy.serving.session
|
||||
if not hasattr(cherrypy, 'session'):
|
||||
cherrypy.session = cherrypy._ThreadLocalProxy('session')
|
||||
|
||||
if persistent:
|
||||
cookie_timeout = timeout
|
||||
else:
|
||||
# See http://support.microsoft.com/kb/223799/EN-US/
|
||||
# and http://support.mozilla.com/en-US/kb/Cookies
|
||||
cookie_timeout = None
|
||||
set_response_cookie(path=path, path_header=path_header, name=name,
|
||||
timeout=cookie_timeout, domain=domain, secure=secure,
|
||||
httponly=httponly)
|
||||
|
||||
|
||||
def set_response_cookie(path=None, path_header=None, name='session_id',
|
||||
timeout=60, domain=None, secure=False, httponly=False):
|
||||
"""Set a response cookie for the client.
|
||||
|
||||
path
|
||||
the 'path' value to stick in the response cookie metadata.
|
||||
|
||||
path_header
|
||||
if 'path' is None (the default), then the response
|
||||
cookie 'path' will be pulled from request.headers[path_header].
|
||||
|
||||
name
|
||||
the name of the cookie.
|
||||
|
||||
timeout
|
||||
the expiration timeout for the cookie. If 0 or other boolean
|
||||
False, no 'expires' param will be set, and the cookie will be a
|
||||
"session cookie" which expires when the browser is closed.
|
||||
|
||||
domain
|
||||
the cookie domain.
|
||||
|
||||
secure
|
||||
if False (the default) the cookie 'secure' value will not
|
||||
be set. If True, the cookie 'secure' value will be set (to 1).
|
||||
|
||||
httponly
|
||||
If False (the default) the cookie 'httponly' value will not be set.
|
||||
If True, the cookie 'httponly' value will be set (to 1).
|
||||
|
||||
"""
|
||||
# Set response cookie
|
||||
cookie = cherrypy.serving.response.cookie
|
||||
cookie[name] = cherrypy.serving.session.id
|
||||
cookie[name]['path'] = (
|
||||
path or
|
||||
cherrypy.serving.request.headers.get(path_header) or
|
||||
'/'
|
||||
)
|
||||
|
||||
# We'd like to use the "max-age" param as indicated in
|
||||
# http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
|
||||
# save it to disk and the session is lost if people close
|
||||
# the browser. So we have to use the old "expires" ... sigh ...
|
||||
## cookie[name]['max-age'] = timeout * 60
|
||||
if timeout:
|
||||
e = time.time() + (timeout * 60)
|
||||
cookie[name]['expires'] = httputil.HTTPDate(e)
|
||||
if domain is not None:
|
||||
cookie[name]['domain'] = domain
|
||||
if secure:
|
||||
cookie[name]['secure'] = 1
|
||||
if httponly:
|
||||
if not cookie[name].isReservedKey('httponly'):
|
||||
raise ValueError('The httponly cookie token is not supported.')
|
||||
cookie[name]['httponly'] = 1
|
||||
|
||||
|
||||
def expire():
|
||||
"""Expire the current session cookie."""
|
||||
name = cherrypy.serving.request.config.get(
|
||||
'tools.sessions.name', 'session_id')
|
||||
one_year = 60 * 60 * 24 * 365
|
||||
e = time.time() - one_year
|
||||
cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
|
|
@ -1,381 +0,0 @@
|
|||
import os
|
||||
import re
|
||||
import stat
|
||||
import mimetypes
|
||||
|
||||
try:
|
||||
from io import UnsupportedOperation
|
||||
except ImportError:
|
||||
UnsupportedOperation = object()
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, unquote
|
||||
from cherrypy.lib import cptools, httputil, file_generator_limited
|
||||
|
||||
|
||||
mimetypes.init()
|
||||
mimetypes.types_map['.dwg'] = 'image/x-dwg'
|
||||
mimetypes.types_map['.ico'] = 'image/x-icon'
|
||||
mimetypes.types_map['.bz2'] = 'application/x-bzip2'
|
||||
mimetypes.types_map['.gz'] = 'application/x-gzip'
|
||||
|
||||
|
||||
def serve_file(path, content_type=None, disposition=None, name=None,
|
||||
debug=False):
|
||||
"""Set status, headers, and body in order to serve the given path.
|
||||
|
||||
The Content-Type header will be set to the content_type arg, if provided.
|
||||
If not provided, the Content-Type will be guessed by the file extension
|
||||
of the 'path' argument.
|
||||
|
||||
If disposition is not None, the Content-Disposition header will be set
|
||||
to "<disposition>; filename=<name>". If name is None, it will be set
|
||||
to the basename of path. If disposition is None, no Content-Disposition
|
||||
header will be written.
|
||||
"""
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# If path is relative, users should fix it by making path absolute.
|
||||
# That is, CherryPy should not guess where the application root is.
|
||||
# It certainly should *not* use cwd (since CP may be invoked from a
|
||||
# variety of paths). If using tools.staticdir, you can make your relative
|
||||
# paths become absolute by supplying a value for "tools.staticdir.root".
|
||||
if not os.path.isabs(path):
|
||||
msg = "'%s' is not an absolute path." % path
|
||||
if debug:
|
||||
cherrypy.log(msg, 'TOOLS.STATICFILE')
|
||||
raise ValueError(msg)
|
||||
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except (OSError, TypeError, ValueError):
|
||||
# OSError when file fails to stat
|
||||
# TypeError on Python 2 when there's a null byte
|
||||
# ValueError on Python 3 when there's a null byte
|
||||
if debug:
|
||||
cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Check if path is a directory.
|
||||
if stat.S_ISDIR(st.st_mode):
|
||||
# Let the caller deal with it as they like.
|
||||
if debug:
|
||||
cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
|
||||
raise cherrypy.NotFound()
|
||||
|
||||
# Set the Last-Modified response header, so that
|
||||
# modified-since validation code can work.
|
||||
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
|
||||
cptools.validate_since()
|
||||
|
||||
if content_type is None:
|
||||
# Set content-type based on filename extension
|
||||
ext = ''
|
||||
i = path.rfind('.')
|
||||
if i != -1:
|
||||
ext = path[i:].lower()
|
||||
content_type = mimetypes.types_map.get(ext, None)
|
||||
if content_type is not None:
|
||||
response.headers['Content-Type'] = content_type
|
||||
if debug:
|
||||
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
|
||||
|
||||
cd = None
|
||||
if disposition is not None:
|
||||
if name is None:
|
||||
name = os.path.basename(path)
|
||||
cd = '%s; filename="%s"' % (disposition, name)
|
||||
response.headers['Content-Disposition'] = cd
|
||||
if debug:
|
||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||
|
||||
# Set Content-Length and use an iterable (file object)
|
||||
# this way CP won't load the whole file in memory
|
||||
content_length = st.st_size
|
||||
fileobj = open(path, 'rb')
|
||||
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
|
||||
|
||||
|
||||
def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
|
||||
debug=False):
|
||||
"""Set status, headers, and body in order to serve the given file object.
|
||||
|
||||
The Content-Type header will be set to the content_type arg, if provided.
|
||||
|
||||
If disposition is not None, the Content-Disposition header will be set
|
||||
to "<disposition>; filename=<name>". If name is None, 'filename' will
|
||||
not be set. If disposition is None, no Content-Disposition header will
|
||||
be written.
|
||||
|
||||
CAUTION: If the request contains a 'Range' header, one or more seek()s will
|
||||
be performed on the file object. This may cause undesired behavior if
|
||||
the file object is not seekable. It could also produce undesired results
|
||||
if the caller set the read position of the file object prior to calling
|
||||
serve_fileobj(), expecting that the data would be served starting from that
|
||||
position.
|
||||
"""
|
||||
|
||||
response = cherrypy.serving.response
|
||||
|
||||
try:
|
||||
st = os.fstat(fileobj.fileno())
|
||||
except AttributeError:
|
||||
if debug:
|
||||
cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
|
||||
content_length = None
|
||||
except UnsupportedOperation:
|
||||
content_length = None
|
||||
else:
|
||||
# Set the Last-Modified response header, so that
|
||||
# modified-since validation code can work.
|
||||
response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
|
||||
cptools.validate_since()
|
||||
content_length = st.st_size
|
||||
|
||||
if content_type is not None:
|
||||
response.headers['Content-Type'] = content_type
|
||||
if debug:
|
||||
cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
|
||||
|
||||
cd = None
|
||||
if disposition is not None:
|
||||
if name is None:
|
||||
cd = disposition
|
||||
else:
|
||||
cd = '%s; filename="%s"' % (disposition, name)
|
||||
response.headers['Content-Disposition'] = cd
|
||||
if debug:
|
||||
cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
|
||||
|
||||
return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
|
||||
|
||||
|
||||
def _serve_fileobj(fileobj, content_type, content_length, debug=False):
|
||||
"""Internal. Set response.body to the given file object, perhaps ranged."""
|
||||
response = cherrypy.serving.response
|
||||
|
||||
# HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
|
||||
request = cherrypy.serving.request
|
||||
if request.protocol >= (1, 1):
|
||||
response.headers['Accept-Ranges'] = 'bytes'
|
||||
r = httputil.get_ranges(request.headers.get('Range'), content_length)
|
||||
if r == []:
|
||||
response.headers['Content-Range'] = 'bytes */%s' % content_length
|
||||
message = ('Invalid Range (first-byte-pos greater than '
|
||||
'Content-Length)')
|
||||
if debug:
|
||||
cherrypy.log(message, 'TOOLS.STATIC')
|
||||
raise cherrypy.HTTPError(416, message)
|
||||
|
||||
if r:
|
||||
if len(r) == 1:
|
||||
# Return a single-part response.
|
||||
start, stop = r[0]
|
||||
if stop > content_length:
|
||||
stop = content_length
|
||||
r_len = stop - start
|
||||
if debug:
|
||||
cherrypy.log(
|
||||
'Single part; start: %r, stop: %r' % (start, stop),
|
||||
'TOOLS.STATIC')
|
||||
response.status = '206 Partial Content'
|
||||
response.headers['Content-Range'] = (
|
||||
'bytes %s-%s/%s' % (start, stop - 1, content_length))
|
||||
response.headers['Content-Length'] = r_len
|
||||
fileobj.seek(start)
|
||||
response.body = file_generator_limited(fileobj, r_len)
|
||||
else:
|
||||
# Return a multipart/byteranges response.
|
||||
response.status = '206 Partial Content'
|
||||
try:
|
||||
# Python 3
|
||||
from email.generator import _make_boundary as make_boundary
|
||||
except ImportError:
|
||||
# Python 2
|
||||
from mimetools import choose_boundary as make_boundary
|
||||
boundary = make_boundary()
|
||||
ct = 'multipart/byteranges; boundary=%s' % boundary
|
||||
response.headers['Content-Type'] = ct
|
||||
if 'Content-Length' in response.headers:
|
||||
# Delete Content-Length header so finalize() recalcs it.
|
||||
del response.headers['Content-Length']
|
||||
|
||||
def file_ranges():
|
||||
# Apache compatibility:
|
||||
yield ntob('\r\n')
|
||||
|
||||
for start, stop in r:
|
||||
if debug:
|
||||
cherrypy.log(
|
||||
'Multipart; start: %r, stop: %r' % (
|
||||
start, stop),
|
||||
'TOOLS.STATIC')
|
||||
yield ntob('--' + boundary, 'ascii')
|
||||
yield ntob('\r\nContent-type: %s' % content_type,
|
||||
'ascii')
|
||||
yield ntob(
|
||||
'\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % (
|
||||
start, stop - 1, content_length),
|
||||
'ascii')
|
||||
fileobj.seek(start)
|
||||
gen = file_generator_limited(fileobj, stop - start)
|
||||
for chunk in gen:
|
||||
yield chunk
|
||||
yield ntob('\r\n')
|
||||
# Final boundary
|
||||
yield ntob('--' + boundary + '--', 'ascii')
|
||||
|
||||
# Apache compatibility:
|
||||
yield ntob('\r\n')
|
||||
response.body = file_ranges()
|
||||
return response.body
|
||||
else:
|
||||
if debug:
|
||||
cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
|
||||
|
||||
# Set Content-Length and use an iterable (file object)
|
||||
# this way CP won't load the whole file in memory
|
||||
response.headers['Content-Length'] = content_length
|
||||
response.body = fileobj
|
||||
return response.body
|
||||
|
||||
|
||||
def serve_download(path, name=None):
|
||||
"""Serve 'path' as an application/x-download attachment."""
|
||||
# This is such a common idiom I felt it deserved its own wrapper.
|
||||
return serve_file(path, 'application/x-download', 'attachment', name)
|
||||
|
||||
|
||||
def _attempt(filename, content_types, debug=False):
|
||||
if debug:
|
||||
cherrypy.log('Attempting %r (content_types %r)' %
|
||||
(filename, content_types), 'TOOLS.STATICDIR')
|
||||
try:
|
||||
# you can set the content types for a
|
||||
# complete directory per extension
|
||||
content_type = None
|
||||
if content_types:
|
||||
r, ext = os.path.splitext(filename)
|
||||
content_type = content_types.get(ext[1:], None)
|
||||
serve_file(filename, content_type=content_type, debug=debug)
|
||||
return True
|
||||
except cherrypy.NotFound:
|
||||
# If we didn't find the static file, continue handling the
|
||||
# request. We might find a dynamic handler instead.
|
||||
if debug:
|
||||
cherrypy.log('NotFound', 'TOOLS.STATICFILE')
|
||||
return False
|
||||
|
||||
|
||||
def staticdir(section, dir, root='', match='', content_types=None, index='',
|
||||
debug=False):
|
||||
"""Serve a static resource from the given (root +) dir.
|
||||
|
||||
match
|
||||
If given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
content_types
|
||||
If given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
|
||||
index
|
||||
If provided, it should be the (relative) name of a file to
|
||||
serve for directory requests. For example, if the dir argument is
|
||||
'/home/me', the Request-URI is 'myapp', and the index arg is
|
||||
'index.html', the file '/home/me/myapp/index.html' will be sought.
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if request.method not in ('GET', 'HEAD'):
|
||||
if debug:
|
||||
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
|
||||
return False
|
||||
|
||||
if match and not re.search(match, request.path_info):
|
||||
if debug:
|
||||
cherrypy.log('request.path_info %r does not match pattern %r' %
|
||||
(request.path_info, match), 'TOOLS.STATICDIR')
|
||||
return False
|
||||
|
||||
# Allow the use of '~' to refer to a user's home directory.
|
||||
dir = os.path.expanduser(dir)
|
||||
|
||||
# If dir is relative, make absolute using "root".
|
||||
if not os.path.isabs(dir):
|
||||
if not root:
|
||||
msg = 'Static dir requires an absolute dir (or root).'
|
||||
if debug:
|
||||
cherrypy.log(msg, 'TOOLS.STATICDIR')
|
||||
raise ValueError(msg)
|
||||
dir = os.path.join(root, dir)
|
||||
|
||||
# Determine where we are in the object tree relative to 'section'
|
||||
# (where the static tool was defined).
|
||||
if section == 'global':
|
||||
section = '/'
|
||||
section = section.rstrip(r'\/')
|
||||
branch = request.path_info[len(section) + 1:]
|
||||
branch = unquote(branch.lstrip(r'\/'))
|
||||
|
||||
# If branch is "", filename will end in a slash
|
||||
filename = os.path.join(dir, branch)
|
||||
if debug:
|
||||
cherrypy.log('Checking file %r to fulfill %r' %
|
||||
(filename, request.path_info), 'TOOLS.STATICDIR')
|
||||
|
||||
# There's a chance that the branch pulled from the URL might
|
||||
# have ".." or similar uplevel attacks in it. Check that the final
|
||||
# filename is a child of dir.
|
||||
if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
|
||||
raise cherrypy.HTTPError(403) # Forbidden
|
||||
|
||||
handled = _attempt(filename, content_types)
|
||||
if not handled:
|
||||
# Check for an index file if a folder was requested.
|
||||
if index:
|
||||
handled = _attempt(os.path.join(filename, index), content_types)
|
||||
if handled:
|
||||
request.is_index = filename[-1] in (r'\/')
|
||||
return handled
|
||||
|
||||
|
||||
def staticfile(filename, root=None, match='', content_types=None, debug=False):
|
||||
"""Serve a static resource from the given (root +) filename.
|
||||
|
||||
match
|
||||
If given, request.path_info will be searched for the given
|
||||
regular expression before attempting to serve static content.
|
||||
|
||||
content_types
|
||||
If given, it should be a Python dictionary of
|
||||
{file-extension: content-type} pairs, where 'file-extension' is
|
||||
a string (e.g. "gif") and 'content-type' is the value to write
|
||||
out in the Content-Type response header (e.g. "image/gif").
|
||||
|
||||
"""
|
||||
request = cherrypy.serving.request
|
||||
if request.method not in ('GET', 'HEAD'):
|
||||
if debug:
|
||||
cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
|
||||
return False
|
||||
|
||||
if match and not re.search(match, request.path_info):
|
||||
if debug:
|
||||
cherrypy.log('request.path_info %r does not match pattern %r' %
|
||||
(request.path_info, match), 'TOOLS.STATICFILE')
|
||||
return False
|
||||
|
||||
# If filename is relative, make absolute using "root".
|
||||
if not os.path.isabs(filename):
|
||||
if not root:
|
||||
msg = "Static tool requires an absolute filename (got '%s')." % (
|
||||
filename,)
|
||||
if debug:
|
||||
cherrypy.log(msg, 'TOOLS.STATICFILE')
|
||||
raise ValueError(msg)
|
||||
filename = os.path.join(root, filename)
|
||||
|
||||
return _attempt(filename, content_types, debug=debug)
|
|
@ -1,57 +0,0 @@
|
|||
import sys
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
|
||||
|
||||
def get_xmlrpclib():
|
||||
try:
|
||||
import xmlrpc.client as x
|
||||
except ImportError:
|
||||
import xmlrpclib as x
|
||||
return x
|
||||
|
||||
|
||||
def process_body():
|
||||
"""Return (params, method) from request body."""
|
||||
try:
|
||||
return get_xmlrpclib().loads(cherrypy.request.body.read())
|
||||
except Exception:
|
||||
return ('ERROR PARAMS', ), 'ERRORMETHOD'
|
||||
|
||||
|
||||
def patched_path(path):
|
||||
"""Return 'path', doctored for RPC."""
|
||||
if not path.endswith('/'):
|
||||
path += '/'
|
||||
if path.startswith('/RPC2/'):
|
||||
# strip the first /rpc2
|
||||
path = path[5:]
|
||||
return path
|
||||
|
||||
|
||||
def _set_response(body):
|
||||
# The XML-RPC spec (http://www.xmlrpc.com/spec) says:
|
||||
# "Unless there's a lower-level error, always return 200 OK."
|
||||
# Since Python's xmlrpclib interprets a non-200 response
|
||||
# as a "Protocol Error", we'll just return 200 every time.
|
||||
response = cherrypy.response
|
||||
response.status = '200 OK'
|
||||
response.body = ntob(body, 'utf-8')
|
||||
response.headers['Content-Type'] = 'text/xml'
|
||||
response.headers['Content-Length'] = len(body)
|
||||
|
||||
|
||||
def respond(body, encoding='utf-8', allow_none=0):
|
||||
xmlrpclib = get_xmlrpclib()
|
||||
if not isinstance(body, xmlrpclib.Fault):
|
||||
body = (body,)
|
||||
_set_response(xmlrpclib.dumps(body, methodresponse=1,
|
||||
encoding=encoding,
|
||||
allow_none=allow_none))
|
||||
|
||||
|
||||
def on_error(*args, **kwargs):
|
||||
body = str(sys.exc_info()[1])
|
||||
xmlrpclib = get_xmlrpclib()
|
||||
_set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body)))
|
|
@ -1,14 +0,0 @@
|
|||
"""Site container for an HTTP server.
|
||||
|
||||
A Web Site Process Bus object is used to connect applications, servers,
|
||||
and frameworks with site-wide services such as daemonization, process
|
||||
reload, signal handling, drop privileges, PID file management, logging
|
||||
for all of these, and many more.
|
||||
|
||||
The 'plugins' module defines a few abstract and concrete services for
|
||||
use with the bus. Some use tool-specific channels; see the documentation
|
||||
for each class.
|
||||
"""
|
||||
|
||||
from cherrypy.process.wspbus import bus # noqa
|
||||
from cherrypy.process import plugins, servers # noqa
|
|
@ -1,739 +0,0 @@
|
|||
"""Site services for use with a Web Site Process Bus."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import signal as _signal
|
||||
import sys
|
||||
import time
|
||||
import threading
|
||||
|
||||
from cherrypy._cpcompat import text_or_bytes, get_thread_ident
|
||||
from cherrypy._cpcompat import ntob, Timer
|
||||
|
||||
# _module__file__base is used by Autoreload to make
|
||||
# absolute any filenames retrieved from sys.modules which are not
|
||||
# already absolute paths. This is to work around Python's quirk
|
||||
# of importing the startup script and using a relative filename
|
||||
# for it in sys.modules.
|
||||
#
|
||||
# Autoreload examines sys.modules afresh every time it runs. If an application
|
||||
# changes the current directory by executing os.chdir(), then the next time
|
||||
# Autoreload runs, it will not be able to find any filenames which are
|
||||
# not absolute paths, because the current directory is not the same as when the
|
||||
# module was first imported. Autoreload will then wrongly conclude the file
|
||||
# has "changed", and initiate the shutdown/re-exec sequence.
|
||||
# See ticket #917.
|
||||
# For this workaround to have a decent probability of success, this module
|
||||
# needs to be imported as early as possible, before the app has much chance
|
||||
# to change the working directory.
|
||||
_module__file__base = os.getcwd()
|
||||
|
||||
|
||||
class SimplePlugin(object):
|
||||
|
||||
"""Plugin base class which auto-subscribes methods for known channels."""
|
||||
|
||||
bus = None
|
||||
"""A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
|
||||
"""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
|
||||
def subscribe(self):
|
||||
"""Register this object as a (multi-channel) listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
# Subscribe self.start, self.exit, etc. if present.
|
||||
method = getattr(self, channel, None)
|
||||
if method is not None:
|
||||
self.bus.subscribe(channel, method)
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unregister this object as a listener on the bus."""
|
||||
for channel in self.bus.listeners:
|
||||
# Unsubscribe self.start, self.exit, etc. if present.
|
||||
method = getattr(self, channel, None)
|
||||
if method is not None:
|
||||
self.bus.unsubscribe(channel, method)
|
||||
|
||||
|
||||
class SignalHandler(object):
|
||||
|
||||
"""Register bus channels (and listeners) for system signals.
|
||||
|
||||
You can modify what signals your application listens for, and what it does
|
||||
when it receives signals, by modifying :attr:`SignalHandler.handlers`,
|
||||
a dict of {signal name: callback} pairs. The default set is::
|
||||
|
||||
handlers = {'SIGTERM': self.bus.exit,
|
||||
'SIGHUP': self.handle_SIGHUP,
|
||||
'SIGUSR1': self.bus.graceful,
|
||||
}
|
||||
|
||||
The :func:`SignalHandler.handle_SIGHUP`` method calls
|
||||
:func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
|
||||
if the process is daemonized, but
|
||||
:func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
|
||||
if the process is attached to a TTY. This is because Unix window
|
||||
managers tend to send SIGHUP to terminal windows when the user closes them.
|
||||
|
||||
Feel free to add signals which are not available on every platform.
|
||||
The :class:`SignalHandler` will ignore errors raised from attempting
|
||||
to register handlers for unknown signals.
|
||||
"""
|
||||
|
||||
handlers = {}
|
||||
"""A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
|
||||
|
||||
signals = {}
|
||||
"""A map from signal numbers to names."""
|
||||
|
||||
for k, v in vars(_signal).items():
|
||||
if k.startswith('SIG') and not k.startswith('SIG_'):
|
||||
signals[v] = k
|
||||
del k, v
|
||||
|
||||
def __init__(self, bus):
|
||||
self.bus = bus
|
||||
# Set default handlers
|
||||
self.handlers = {'SIGTERM': self.bus.exit,
|
||||
'SIGHUP': self.handle_SIGHUP,
|
||||
'SIGUSR1': self.bus.graceful,
|
||||
}
|
||||
|
||||
if sys.platform[:4] == 'java':
|
||||
del self.handlers['SIGUSR1']
|
||||
self.handlers['SIGUSR2'] = self.bus.graceful
|
||||
self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
|
||||
'Using SIGUSR2 instead.')
|
||||
self.handlers['SIGINT'] = self._jython_SIGINT_handler
|
||||
|
||||
self._previous_handlers = {}
|
||||
# used to determine is the process is a daemon in `self._is_daemonized`
|
||||
self._original_pid = os.getpid()
|
||||
|
||||
|
||||
def _jython_SIGINT_handler(self, signum=None, frame=None):
|
||||
# See http://bugs.jython.org/issue1313
|
||||
self.bus.log('Keyboard Interrupt: shutting down bus')
|
||||
self.bus.exit()
|
||||
|
||||
def _is_daemonized(self):
|
||||
"""Return boolean indicating if the current process is
|
||||
running as a daemon.
|
||||
|
||||
The criteria to determine the `daemon` condition is to verify
|
||||
if the current pid is not the same as the one that got used on
|
||||
the initial construction of the plugin *and* the stdin is not
|
||||
connected to a terminal.
|
||||
|
||||
The sole validation of the tty is not enough when the plugin
|
||||
is executing inside other process like in a CI tool
|
||||
(Buildbot, Jenkins).
|
||||
"""
|
||||
if (self._original_pid != os.getpid() and
|
||||
not os.isatty(sys.stdin.fileno())):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def subscribe(self):
|
||||
"""Subscribe self.handlers to signals."""
|
||||
for sig, func in self.handlers.items():
|
||||
try:
|
||||
self.set_handler(sig, func)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
def unsubscribe(self):
|
||||
"""Unsubscribe self.handlers from signals."""
|
||||
for signum, handler in self._previous_handlers.items():
|
||||
signame = self.signals[signum]
|
||||
|
||||
if handler is None:
|
||||
self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
|
||||
handler = _signal.SIG_DFL
|
||||
else:
|
||||
self.bus.log('Restoring %s handler %r.' % (signame, handler))
|
||||
|
||||
try:
|
||||
our_handler = _signal.signal(signum, handler)
|
||||
if our_handler is None:
|
||||
self.bus.log('Restored old %s handler %r, but our '
|
||||
'handler was not registered.' %
|
||||
(signame, handler), level=30)
|
||||
except ValueError:
|
||||
self.bus.log('Unable to restore %s handler %r.' %
|
||||
(signame, handler), level=40, traceback=True)
|
||||
|
||||
def set_handler(self, signal, listener=None):
|
||||
"""Subscribe a handler for the given signal (number or name).
|
||||
|
||||
If the optional 'listener' argument is provided, it will be
|
||||
subscribed as a listener for the given signal's channel.
|
||||
|
||||
If the given signal name or number is not available on the current
|
||||
platform, ValueError is raised.
|
||||
"""
|
||||
if isinstance(signal, text_or_bytes):
|
||||
signum = getattr(_signal, signal, None)
|
||||
if signum is None:
|
||||
raise ValueError('No such signal: %r' % signal)
|
||||
signame = signal
|
||||
else:
|
||||
try:
|
||||
signame = self.signals[signal]
|
||||
except KeyError:
|
||||
raise ValueError('No such signal: %r' % signal)
|
||||
signum = signal
|
||||
|
||||
prev = _signal.signal(signum, self._handle_signal)
|
||||
self._previous_handlers[signum] = prev
|
||||
|
||||
if listener is not None:
|
||||
self.bus.log('Listening for %s.' % signame)
|
||||
self.bus.subscribe(signame, listener)
|
||||
|
||||
def _handle_signal(self, signum=None, frame=None):
|
||||
"""Python signal handler (self.set_handler subscribes it for you)."""
|
||||
signame = self.signals[signum]
|
||||
self.bus.log('Caught signal %s.' % signame)
|
||||
self.bus.publish(signame)
|
||||
|
||||
def handle_SIGHUP(self):
|
||||
"""Restart if daemonized, else exit."""
|
||||
if self._is_daemonized():
|
||||
self.bus.log('SIGHUP caught while daemonized. Restarting.')
|
||||
self.bus.restart()
|
||||
else:
|
||||
# not daemonized (may be foreground or background)
|
||||
self.bus.log('SIGHUP caught but not daemonized. Exiting.')
|
||||
self.bus.exit()
|
||||
|
||||
|
||||
try:
|
||||
import pwd
|
||||
import grp
|
||||
except ImportError:
|
||||
pwd, grp = None, None
|
||||
|
||||
|
||||
class DropPrivileges(SimplePlugin):
|
||||
|
||||
"""Drop privileges. uid/gid arguments not available on Windows.
|
||||
|
||||
Special thanks to `Gavin Baker <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
|
||||
"""
|
||||
|
||||
def __init__(self, bus, umask=None, uid=None, gid=None):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.finalized = False
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.umask = umask
|
||||
|
||||
def _get_uid(self):
|
||||
return self._uid
|
||||
|
||||
def _set_uid(self, val):
|
||||
if val is not None:
|
||||
if pwd is None:
|
||||
self.bus.log('pwd module not available; ignoring uid.',
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, text_or_bytes):
|
||||
val = pwd.getpwnam(val)[2]
|
||||
self._uid = val
|
||||
uid = property(_get_uid, _set_uid,
|
||||
doc='The uid under which to run. Availability: Unix.')
|
||||
|
||||
def _get_gid(self):
|
||||
return self._gid
|
||||
|
||||
def _set_gid(self, val):
|
||||
if val is not None:
|
||||
if grp is None:
|
||||
self.bus.log('grp module not available; ignoring gid.',
|
||||
level=30)
|
||||
val = None
|
||||
elif isinstance(val, text_or_bytes):
|
||||
val = grp.getgrnam(val)[2]
|
||||
self._gid = val
|
||||
gid = property(_get_gid, _set_gid,
|
||||
doc='The gid under which to run. Availability: Unix.')
|
||||
|
||||
def _get_umask(self):
|
||||
return self._umask
|
||||
|
||||
def _set_umask(self, val):
|
||||
if val is not None:
|
||||
try:
|
||||
os.umask
|
||||
except AttributeError:
|
||||
self.bus.log('umask function not available; ignoring umask.',
|
||||
level=30)
|
||||
val = None
|
||||
self._umask = val
|
||||
umask = property(
|
||||
_get_umask,
|
||||
_set_umask,
|
||||
doc="""The default permission mode for newly created files and
|
||||
directories.
|
||||
|
||||
Usually expressed in octal format, for example, ``0644``.
|
||||
Availability: Unix, Windows.
|
||||
""")
|
||||
|
||||
def start(self):
|
||||
# uid/gid
|
||||
def current_ids():
|
||||
"""Return the current (uid, gid) if available."""
|
||||
name, group = None, None
|
||||
if pwd:
|
||||
name = pwd.getpwuid(os.getuid())[0]
|
||||
if grp:
|
||||
group = grp.getgrgid(os.getgid())[0]
|
||||
return name, group
|
||||
|
||||
if self.finalized:
|
||||
if not (self.uid is None and self.gid is None):
|
||||
self.bus.log('Already running as uid: %r gid: %r' %
|
||||
current_ids())
|
||||
else:
|
||||
if self.uid is None and self.gid is None:
|
||||
if pwd or grp:
|
||||
self.bus.log('uid/gid not set', level=30)
|
||||
else:
|
||||
self.bus.log('Started as uid: %r gid: %r' % current_ids())
|
||||
if self.gid is not None:
|
||||
os.setgid(self.gid)
|
||||
os.setgroups([])
|
||||
if self.uid is not None:
|
||||
os.setuid(self.uid)
|
||||
self.bus.log('Running as uid: %r gid: %r' % current_ids())
|
||||
|
||||
# umask
|
||||
if self.finalized:
|
||||
if self.umask is not None:
|
||||
self.bus.log('umask already set to: %03o' % self.umask)
|
||||
else:
|
||||
if self.umask is None:
|
||||
self.bus.log('umask not set', level=30)
|
||||
else:
|
||||
old_umask = os.umask(self.umask)
|
||||
self.bus.log('umask old: %03o, new: %03o' %
|
||||
(old_umask, self.umask))
|
||||
|
||||
self.finalized = True
|
||||
# This is slightly higher than the priority for server.start
|
||||
# in order to facilitate the most common use: starting on a low
|
||||
# port (which requires root) and then dropping to another user.
|
||||
start.priority = 77
|
||||
|
||||
|
||||
class Daemonizer(SimplePlugin):
|
||||
|
||||
"""Daemonize the running script.
|
||||
|
||||
Use this with a Web Site Process Bus via::
|
||||
|
||||
Daemonizer(bus).subscribe()
|
||||
|
||||
When this component finishes, the process is completely decoupled from
|
||||
the parent environment. Please note that when this component is used,
|
||||
the return code from the parent process will still be 0 if a startup
|
||||
error occurs in the forked children. Errors in the initial daemonizing
|
||||
process still return proper exit codes. Therefore, if you use this
|
||||
plugin to daemonize, don't use the return code as an accurate indicator
|
||||
of whether the process fully started. In fact, that return code only
|
||||
indicates if the process succesfully finished the first fork.
|
||||
"""
|
||||
|
||||
def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
|
||||
stderr='/dev/null'):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.stderr = stderr
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
if self.finalized:
|
||||
self.bus.log('Already deamonized.')
|
||||
|
||||
# forking has issues with threads:
|
||||
# http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
|
||||
# "The general problem with making fork() work in a multi-threaded
|
||||
# world is what to do with all of the threads..."
|
||||
# So we check for active threads:
|
||||
if threading.activeCount() != 1:
|
||||
self.bus.log('There are %r active threads. '
|
||||
'Daemonizing now may cause strange failures.' %
|
||||
threading.enumerate(), level=30)
|
||||
|
||||
# See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
|
||||
# (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
|
||||
# and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
|
||||
|
||||
# Finish up with the current stdout/stderr
|
||||
sys.stdout.flush()
|
||||
sys.stderr.flush()
|
||||
|
||||
# Do first fork.
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid == 0:
|
||||
# This is the child process. Continue.
|
||||
pass
|
||||
else:
|
||||
# This is the first parent. Exit, now that we've forked.
|
||||
self.bus.log('Forking once.')
|
||||
os._exit(0)
|
||||
except OSError:
|
||||
# Python raises OSError rather than returning negative numbers.
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit('%s: fork #1 failed: (%d) %s\n'
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
os.setsid()
|
||||
|
||||
# Do second fork
|
||||
try:
|
||||
pid = os.fork()
|
||||
if pid > 0:
|
||||
self.bus.log('Forking twice.')
|
||||
os._exit(0) # Exit second parent
|
||||
except OSError:
|
||||
exc = sys.exc_info()[1]
|
||||
sys.exit('%s: fork #2 failed: (%d) %s\n'
|
||||
% (sys.argv[0], exc.errno, exc.strerror))
|
||||
|
||||
os.umask(0)
|
||||
|
||||
si = open(self.stdin, 'r')
|
||||
so = open(self.stdout, 'a+')
|
||||
se = open(self.stderr, 'a+')
|
||||
|
||||
# os.dup2(fd, fd2) will close fd2 if necessary,
|
||||
# so we don't explicitly close stdin/out/err.
|
||||
# See http://docs.python.org/lib/os-fd-ops.html
|
||||
os.dup2(si.fileno(), sys.stdin.fileno())
|
||||
os.dup2(so.fileno(), sys.stdout.fileno())
|
||||
os.dup2(se.fileno(), sys.stderr.fileno())
|
||||
|
||||
self.bus.log('Daemonized to PID: %s' % os.getpid())
|
||||
self.finalized = True
|
||||
start.priority = 65
|
||||
|
||||
|
||||
class PIDFile(SimplePlugin):
|
||||
|
||||
"""Maintain a PID file via a WSPBus."""
|
||||
|
||||
def __init__(self, bus, pidfile):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.pidfile = pidfile
|
||||
self.finalized = False
|
||||
|
||||
def start(self):
|
||||
pid = os.getpid()
|
||||
if self.finalized:
|
||||
self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
|
||||
else:
|
||||
open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
|
||||
self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
|
||||
self.finalized = True
|
||||
start.priority = 70
|
||||
|
||||
def exit(self):
|
||||
try:
|
||||
os.remove(self.pidfile)
|
||||
self.bus.log('PID file removed: %r.' % self.pidfile)
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
class PerpetualTimer(Timer):
|
||||
|
||||
"""A responsive subclass of threading.Timer whose run() method repeats.
|
||||
|
||||
Use this timer only when you really need a very interruptible timer;
|
||||
this checks its 'finished' condition up to 20 times a second, which can
|
||||
results in pretty high CPU usage
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"Override parent constructor to allow 'bus' to be provided."
|
||||
self.bus = kwargs.pop('bus', None)
|
||||
super(PerpetualTimer, self).__init__(*args, **kwargs)
|
||||
|
||||
def run(self):
|
||||
while True:
|
||||
self.finished.wait(self.interval)
|
||||
if self.finished.isSet():
|
||||
return
|
||||
try:
|
||||
self.function(*self.args, **self.kwargs)
|
||||
except Exception:
|
||||
if self.bus:
|
||||
self.bus.log(
|
||||
'Error in perpetual timer thread function %r.' %
|
||||
self.function, level=40, traceback=True)
|
||||
# Quit on first error to avoid massive logs.
|
||||
raise
|
||||
|
||||
|
||||
class BackgroundTask(threading.Thread):
|
||||
|
||||
"""A subclass of threading.Thread whose run() method repeats.
|
||||
|
||||
Use this class for most repeating tasks. It uses time.sleep() to wait
|
||||
for each interval, which isn't very responsive; that is, even if you call
|
||||
self.cancel(), you'll have to wait until the sleep() call finishes before
|
||||
the thread stops. To compensate, it defaults to being daemonic, which means
|
||||
it won't delay stopping the whole process.
|
||||
"""
|
||||
|
||||
def __init__(self, interval, function, args=[], kwargs={}, bus=None):
|
||||
super(BackgroundTask, self).__init__()
|
||||
self.interval = interval
|
||||
self.function = function
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.running = False
|
||||
self.bus = bus
|
||||
|
||||
# default to daemonic
|
||||
self.daemon = True
|
||||
|
||||
def cancel(self):
|
||||
self.running = False
|
||||
|
||||
def run(self):
|
||||
self.running = True
|
||||
while self.running:
|
||||
time.sleep(self.interval)
|
||||
if not self.running:
|
||||
return
|
||||
try:
|
||||
self.function(*self.args, **self.kwargs)
|
||||
except Exception:
|
||||
if self.bus:
|
||||
self.bus.log('Error in background task thread function %r.'
|
||||
% self.function, level=40, traceback=True)
|
||||
# Quit on first error to avoid massive logs.
|
||||
raise
|
||||
|
||||
|
||||
class Monitor(SimplePlugin):
|
||||
|
||||
"""WSPBus listener to periodically run a callback in its own thread."""
|
||||
|
||||
callback = None
|
||||
"""The function to call at intervals."""
|
||||
|
||||
frequency = 60
|
||||
"""The time in seconds between callback runs."""
|
||||
|
||||
thread = None
|
||||
"""A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
|
||||
thread.
|
||||
"""
|
||||
|
||||
def __init__(self, bus, callback, frequency=60, name=None):
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.callback = callback
|
||||
self.frequency = frequency
|
||||
self.thread = None
|
||||
self.name = name
|
||||
|
||||
def start(self):
|
||||
"""Start our callback in its own background thread."""
|
||||
if self.frequency > 0:
|
||||
threadname = self.name or self.__class__.__name__
|
||||
if self.thread is None:
|
||||
self.thread = BackgroundTask(self.frequency, self.callback,
|
||||
bus=self.bus)
|
||||
self.thread.setName(threadname)
|
||||
self.thread.start()
|
||||
self.bus.log('Started monitor thread %r.' % threadname)
|
||||
else:
|
||||
self.bus.log('Monitor thread %r already started.' % threadname)
|
||||
start.priority = 70
|
||||
|
||||
def stop(self):
|
||||
"""Stop our callback's background task thread."""
|
||||
if self.thread is None:
|
||||
self.bus.log('No thread running for %s.' %
|
||||
self.name or self.__class__.__name__)
|
||||
else:
|
||||
if self.thread is not threading.currentThread():
|
||||
name = self.thread.getName()
|
||||
self.thread.cancel()
|
||||
if not self.thread.daemon:
|
||||
self.bus.log('Joining %r' % name)
|
||||
self.thread.join()
|
||||
self.bus.log('Stopped thread %r.' % name)
|
||||
self.thread = None
|
||||
|
||||
def graceful(self):
|
||||
"""Stop the callback's background task thread and restart it."""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
|
||||
class Autoreloader(Monitor):
|
||||
|
||||
"""Monitor which re-executes the process when files change.
|
||||
|
||||
This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
|
||||
if any of the files it monitors change (or is deleted). By default, the
|
||||
autoreloader monitors all imported modules; you can add to the
|
||||
set by adding to ``autoreload.files``::
|
||||
|
||||
cherrypy.engine.autoreload.files.add(myFile)
|
||||
|
||||
If there are imported files you do *not* wish to monitor, you can
|
||||
adjust the ``match`` attribute, a regular expression. For example,
|
||||
to stop monitoring cherrypy itself::
|
||||
|
||||
cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
|
||||
|
||||
Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
|
||||
the autoreload plugin takes a ``frequency`` argument. The default is
|
||||
1 second; that is, the autoreloader will examine files once each second.
|
||||
"""
|
||||
|
||||
files = None
|
||||
"""The set of files to poll for modifications."""
|
||||
|
||||
frequency = 1
|
||||
"""The interval in seconds at which to poll for modified files."""
|
||||
|
||||
match = '.*'
|
||||
"""A regular expression by which to match filenames."""
|
||||
|
||||
def __init__(self, bus, frequency=1, match='.*'):
|
||||
self.mtimes = {}
|
||||
self.files = set()
|
||||
self.match = match
|
||||
Monitor.__init__(self, bus, self.run, frequency)
|
||||
|
||||
def start(self):
|
||||
"""Start our own background task thread for self.run."""
|
||||
if self.thread is None:
|
||||
self.mtimes = {}
|
||||
Monitor.start(self)
|
||||
start.priority = 70
|
||||
|
||||
def sysfiles(self):
|
||||
"""Return a Set of sys.modules filenames to monitor."""
|
||||
files = set()
|
||||
for k, m in list(sys.modules.items()):
|
||||
if re.match(self.match, k):
|
||||
if (
|
||||
hasattr(m, '__loader__') and
|
||||
hasattr(m.__loader__, 'archive')
|
||||
):
|
||||
f = m.__loader__.archive
|
||||
else:
|
||||
f = getattr(m, '__file__', None)
|
||||
if f is not None and not os.path.isabs(f):
|
||||
# ensure absolute paths so a os.chdir() in the app
|
||||
# doesn't break me
|
||||
f = os.path.normpath(
|
||||
os.path.join(_module__file__base, f))
|
||||
files.add(f)
|
||||
return files
|
||||
|
||||
def run(self):
|
||||
"""Reload the process if registered files have been modified."""
|
||||
for filename in self.sysfiles() | self.files:
|
||||
if filename:
|
||||
if filename.endswith('.pyc'):
|
||||
filename = filename[:-1]
|
||||
|
||||
oldtime = self.mtimes.get(filename, 0)
|
||||
if oldtime is None:
|
||||
# Module with no .py file. Skip it.
|
||||
continue
|
||||
|
||||
try:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
except OSError:
|
||||
# Either a module with no .py file, or it's been deleted.
|
||||
mtime = None
|
||||
|
||||
if filename not in self.mtimes:
|
||||
# If a module has no .py file, this will be None.
|
||||
self.mtimes[filename] = mtime
|
||||
else:
|
||||
if mtime is None or mtime > oldtime:
|
||||
# The file has been deleted or modified.
|
||||
self.bus.log('Restarting because %s changed.' %
|
||||
filename)
|
||||
self.thread.cancel()
|
||||
self.bus.log('Stopped thread %r.' %
|
||||
self.thread.getName())
|
||||
self.bus.restart()
|
||||
return
|
||||
|
||||
|
||||
class ThreadManager(SimplePlugin):
|
||||
|
||||
"""Manager for HTTP request threads.
|
||||
|
||||
If you have control over thread creation and destruction, publish to
|
||||
the 'acquire_thread' and 'release_thread' channels (for each thread).
|
||||
This will register/unregister the current thread and publish to
|
||||
'start_thread' and 'stop_thread' listeners in the bus as needed.
|
||||
|
||||
If threads are created and destroyed by code you do not control
|
||||
(e.g., Apache), then, at the beginning of every HTTP request,
|
||||
publish to 'acquire_thread' only. You should not publish to
|
||||
'release_thread' in this case, since you do not know whether
|
||||
the thread will be re-used or not. The bus will call
|
||||
'stop_thread' listeners for you when it stops.
|
||||
"""
|
||||
|
||||
threads = None
|
||||
"""A map of {thread ident: index number} pairs."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.threads = {}
|
||||
SimplePlugin.__init__(self, bus)
|
||||
self.bus.listeners.setdefault('acquire_thread', set())
|
||||
self.bus.listeners.setdefault('start_thread', set())
|
||||
self.bus.listeners.setdefault('release_thread', set())
|
||||
self.bus.listeners.setdefault('stop_thread', set())
|
||||
|
||||
def acquire_thread(self):
|
||||
"""Run 'start_thread' listeners for the current thread.
|
||||
|
||||
If the current thread has already been seen, any 'start_thread'
|
||||
listeners will not be run again.
|
||||
"""
|
||||
thread_ident = get_thread_ident()
|
||||
if thread_ident not in self.threads:
|
||||
# We can't just use get_ident as the thread ID
|
||||
# because some platforms reuse thread ID's.
|
||||
i = len(self.threads) + 1
|
||||
self.threads[thread_ident] = i
|
||||
self.bus.publish('start_thread', i)
|
||||
|
||||
def release_thread(self):
|
||||
"""Release the current thread and run 'stop_thread' listeners."""
|
||||
thread_ident = get_thread_ident()
|
||||
i = self.threads.pop(thread_ident, None)
|
||||
if i is not None:
|
||||
self.bus.publish('stop_thread', i)
|
||||
|
||||
def stop(self):
|
||||
"""Release all threads and run all 'stop_thread' listeners."""
|
||||
for thread_ident, i in self.threads.items():
|
||||
self.bus.publish('stop_thread', i)
|
||||
self.threads.clear()
|
||||
graceful = stop
|
|
@ -1,491 +0,0 @@
|
|||
"""
|
||||
Starting in CherryPy 3.1, cherrypy.server is implemented as an
|
||||
:ref:`Engine Plugin<plugins>`. It's an instance of
|
||||
:class:`cherrypy._cpserver.Server`, which is a subclass of
|
||||
:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
|
||||
is designed to control other servers, as well.
|
||||
|
||||
Multiple servers/ports
|
||||
======================
|
||||
|
||||
If you need to start more than one HTTP server (to serve on multiple ports, or
|
||||
protocols, etc.), you can manually register each one and then start them all
|
||||
with engine.start::
|
||||
|
||||
s1 = ServerAdapter(
|
||||
cherrypy.engine,
|
||||
MyWSGIServer(host='0.0.0.0', port=80)
|
||||
)
|
||||
s2 = ServerAdapter(
|
||||
cherrypy.engine,
|
||||
another.HTTPServer(host='127.0.0.1', SSL=True)
|
||||
)
|
||||
s1.subscribe()
|
||||
s2.subscribe()
|
||||
cherrypy.engine.start()
|
||||
|
||||
.. index:: SCGI
|
||||
|
||||
FastCGI/SCGI
|
||||
============
|
||||
|
||||
There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
|
||||
:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
|
||||
wrap an instance of it in a ServerAdapter::
|
||||
|
||||
addr = ('0.0.0.0', 4000)
|
||||
f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
|
||||
s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
|
||||
s.subscribe()
|
||||
|
||||
The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
|
||||
you via its `-f` flag.
|
||||
Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
|
||||
yourself, whether you use ``cherryd`` or not.
|
||||
|
||||
.. _fastcgi:
|
||||
.. index:: FastCGI
|
||||
|
||||
FastCGI
|
||||
-------
|
||||
|
||||
A very simple setup lets your cherry run with FastCGI.
|
||||
You just need the flup library,
|
||||
plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
|
||||
|
||||
CherryPy code
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
hello.py::
|
||||
|
||||
#!/usr/bin/python
|
||||
import cherrypy
|
||||
|
||||
class HelloWorld:
|
||||
'''Sample request handler class.'''
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello world!"
|
||||
|
||||
cherrypy.tree.mount(HelloWorld())
|
||||
# CherryPy autoreload must be disabled for the flup server to work
|
||||
cherrypy.config.update({'engine.autoreload.on':False})
|
||||
|
||||
Then run :doc:`/deployguide/cherryd` with the '-f' arg::
|
||||
|
||||
cherryd -c <myconfig> -d -f -i hello.py
|
||||
|
||||
Apache
|
||||
^^^^^^
|
||||
|
||||
At the top level in httpd.conf::
|
||||
|
||||
FastCgiIpcDir /tmp
|
||||
FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
|
||||
|
||||
And inside the relevant VirtualHost section::
|
||||
|
||||
# FastCGI config
|
||||
AddHandler fastcgi-script .fcgi
|
||||
ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
|
||||
|
||||
Lighttpd
|
||||
^^^^^^^^
|
||||
|
||||
For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
|
||||
instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
|
||||
active within ``server.modules``. Then, within your ``$HTTP["host"]``
|
||||
directive, configure your fastcgi script like the following::
|
||||
|
||||
$HTTP["url"] =~ "" {
|
||||
fastcgi.server = (
|
||||
"/" => (
|
||||
"script.fcgi" => (
|
||||
"bin-path" => "/path/to/your/script.fcgi",
|
||||
"socket" => "/tmp/script.sock",
|
||||
"check-local" => "disable",
|
||||
"disable-time" => 1,
|
||||
"min-procs" => 1,
|
||||
"max-procs" => 1, # adjust as needed
|
||||
),
|
||||
),
|
||||
)
|
||||
} # end of $HTTP["url"] =~ "^/"
|
||||
|
||||
Please see `Lighttpd FastCGI Docs
|
||||
<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
|
||||
an explanation of the possible configuration options.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
|
||||
|
||||
class ServerAdapter(object):
|
||||
|
||||
"""Adapter for an HTTP server.
|
||||
|
||||
If you need to start more than one HTTP server (to serve on multiple
|
||||
ports, or protocols, etc.), you can manually register each one and then
|
||||
start them all with bus.start::
|
||||
|
||||
s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
|
||||
s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
|
||||
s1.subscribe()
|
||||
s2.subscribe()
|
||||
bus.start()
|
||||
"""
|
||||
|
||||
def __init__(self, bus, httpserver=None, bind_addr=None):
|
||||
self.bus = bus
|
||||
self.httpserver = httpserver
|
||||
self.bind_addr = bind_addr
|
||||
self.interrupt = None
|
||||
self.running = False
|
||||
|
||||
def subscribe(self):
|
||||
self.bus.subscribe('start', self.start)
|
||||
self.bus.subscribe('stop', self.stop)
|
||||
|
||||
def unsubscribe(self):
|
||||
self.bus.unsubscribe('start', self.start)
|
||||
self.bus.unsubscribe('stop', self.stop)
|
||||
|
||||
def start(self):
|
||||
"""Start the HTTP server."""
|
||||
if self.running:
|
||||
self.bus.log('Already serving on %s' % self.description)
|
||||
return
|
||||
|
||||
self.interrupt = None
|
||||
if not self.httpserver:
|
||||
raise ValueError('No HTTP server has been created.')
|
||||
|
||||
if not os.environ.get('LISTEN_PID', None):
|
||||
# Start the httpserver in a new thread.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bind_addr)
|
||||
|
||||
import threading
|
||||
t = threading.Thread(target=self._start_http_thread)
|
||||
t.setName('HTTPServer ' + t.getName())
|
||||
t.start()
|
||||
|
||||
self.wait()
|
||||
self.running = True
|
||||
self.bus.log('Serving on %s' % self.description)
|
||||
start.priority = 75
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
"""
|
||||
A description about where this server is bound.
|
||||
"""
|
||||
if self.bind_addr is None:
|
||||
on_what = 'unknown interface (dynamic?)'
|
||||
elif isinstance(self.bind_addr, tuple):
|
||||
on_what = self._get_base()
|
||||
else:
|
||||
on_what = 'socket file: %s' % self.bind_addr
|
||||
return on_what
|
||||
|
||||
def _get_base(self):
|
||||
if not self.httpserver:
|
||||
return ''
|
||||
host, port = self.bound_addr
|
||||
if getattr(self.httpserver, 'ssl_adapter', None):
|
||||
scheme = 'https'
|
||||
if port != 443:
|
||||
host += ':%s' % port
|
||||
else:
|
||||
scheme = 'http'
|
||||
if port != 80:
|
||||
host += ':%s' % port
|
||||
|
||||
return '%s://%s' % (scheme, host)
|
||||
|
||||
def _start_http_thread(self):
|
||||
"""HTTP servers MUST be running in new threads, so that the
|
||||
main thread persists to receive KeyboardInterrupt's. If an
|
||||
exception is raised in the httpserver's thread then it's
|
||||
trapped here, and the bus (and therefore our httpserver)
|
||||
are shut down.
|
||||
"""
|
||||
try:
|
||||
self.httpserver.start()
|
||||
except KeyboardInterrupt:
|
||||
self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.exit()
|
||||
except SystemExit:
|
||||
self.bus.log('SystemExit raised: shutting down HTTP server')
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.exit()
|
||||
raise
|
||||
except:
|
||||
self.interrupt = sys.exc_info()[1]
|
||||
self.bus.log('Error in HTTP server: shutting down',
|
||||
traceback=True, level=40)
|
||||
self.bus.exit()
|
||||
raise
|
||||
|
||||
def wait(self):
|
||||
"""Wait until the HTTP server is ready to receive requests."""
|
||||
while not getattr(self.httpserver, 'ready', False):
|
||||
if self.interrupt:
|
||||
raise self.interrupt
|
||||
time.sleep(.1)
|
||||
|
||||
# Wait for port to be occupied
|
||||
if not os.environ.get('LISTEN_PID', None):
|
||||
# Wait for port to be occupied if not running via socket-activation
|
||||
# (for socket-activation the port will be managed by systemd )
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_occupied_port(*self.bound_addr)
|
||||
|
||||
@property
|
||||
def bound_addr(self):
|
||||
"""
|
||||
The bind address, or if it's an ephemeral port and the
|
||||
socket has been bound, return the actual port bound.
|
||||
"""
|
||||
host, port = self.bind_addr
|
||||
if port == 0 and self.httpserver.socket:
|
||||
# Bound to ephemeral port. Get the actual port allocated.
|
||||
port = self.httpserver.socket.getsockname()[1]
|
||||
return host, port
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
if self.running:
|
||||
# stop() MUST block until the server is *truly* stopped.
|
||||
self.httpserver.stop()
|
||||
# Wait for the socket to be truly freed.
|
||||
if isinstance(self.bind_addr, tuple):
|
||||
wait_for_free_port(*self.bound_addr)
|
||||
self.running = False
|
||||
self.bus.log('HTTP Server %s shut down' % self.httpserver)
|
||||
else:
|
||||
self.bus.log('HTTP Server %s already shut down' % self.httpserver)
|
||||
stop.priority = 25
|
||||
|
||||
def restart(self):
|
||||
"""Restart the HTTP server."""
|
||||
self.stop()
|
||||
self.start()
|
||||
|
||||
|
||||
class FlupCGIServer(object):
|
||||
|
||||
"""Adapter for a flup.server.cgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
def start(self):
|
||||
"""Start the CGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.cgi import WSGIServer
|
||||
|
||||
self.cgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
self.ready = True
|
||||
self.cgiserver.run()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
self.ready = False
|
||||
|
||||
|
||||
class FlupFCGIServer(object):
|
||||
|
||||
"""Adapter for a flup.server.fcgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if kwargs.get('bindAddress', None) is None:
|
||||
import socket
|
||||
if not hasattr(socket, 'fromfd'):
|
||||
raise ValueError(
|
||||
'Dynamic FCGI server not available on this platform. '
|
||||
'You must use a static or external one by providing a '
|
||||
'legal bindAddress.')
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
def start(self):
|
||||
"""Start the FCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.fcgi import WSGIServer
|
||||
self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
# TODO: report this bug upstream to flup.
|
||||
# If we don't set _oldSIGs on Windows, we get:
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 108, in run
|
||||
# self._restoreSignalHandlers()
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 156, in _restoreSignalHandlers
|
||||
# for signum,handler in self._oldSIGs:
|
||||
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
|
||||
self.fcgiserver._installSignalHandlers = lambda: None
|
||||
self.fcgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.fcgiserver.run()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
# Forcibly stop the fcgi server main event loop.
|
||||
self.fcgiserver._keepGoing = False
|
||||
# Force all worker threads to die off.
|
||||
self.fcgiserver._threadPool.maxSpare = (
|
||||
self.fcgiserver._threadPool._idleCount)
|
||||
self.ready = False
|
||||
|
||||
|
||||
class FlupSCGIServer(object):
|
||||
|
||||
"""Adapter for a flup.server.scgi.WSGIServer."""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.args = args
|
||||
self.kwargs = kwargs
|
||||
self.ready = False
|
||||
|
||||
def start(self):
|
||||
"""Start the SCGI server."""
|
||||
# We have to instantiate the server class here because its __init__
|
||||
# starts a threadpool. If we do it too early, daemonize won't work.
|
||||
from flup.server.scgi import WSGIServer
|
||||
self.scgiserver = WSGIServer(*self.args, **self.kwargs)
|
||||
# TODO: report this bug upstream to flup.
|
||||
# If we don't set _oldSIGs on Windows, we get:
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 108, in run
|
||||
# self._restoreSignalHandlers()
|
||||
# File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
|
||||
# line 156, in _restoreSignalHandlers
|
||||
# for signum,handler in self._oldSIGs:
|
||||
# AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
|
||||
self.scgiserver._installSignalHandlers = lambda: None
|
||||
self.scgiserver._oldSIGs = []
|
||||
self.ready = True
|
||||
self.scgiserver.run()
|
||||
|
||||
def stop(self):
|
||||
"""Stop the HTTP server."""
|
||||
self.ready = False
|
||||
# Forcibly stop the scgi server main event loop.
|
||||
self.scgiserver._keepGoing = False
|
||||
# Force all worker threads to die off.
|
||||
self.scgiserver._threadPool.maxSpare = 0
|
||||
|
||||
|
||||
def client_host(server_host):
|
||||
"""Return the host on which a client can connect to the given listener."""
|
||||
if server_host == '0.0.0.0':
|
||||
# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
|
||||
return '127.0.0.1'
|
||||
if server_host in ('::', '::0', '::0.0.0.0'):
|
||||
# :: is IN6ADDR_ANY, which should answer on localhost.
|
||||
# ::0 and ::0.0.0.0 are non-canonical but common
|
||||
# ways to write IN6ADDR_ANY.
|
||||
return '::1'
|
||||
return server_host
|
||||
|
||||
|
||||
def check_port(host, port, timeout=1.0):
|
||||
"""Raise an error if the given port is not free on the given host."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
host = client_host(host)
|
||||
port = int(port)
|
||||
|
||||
import socket
|
||||
|
||||
# AF_INET or AF_INET6 socket
|
||||
# Get the correct address family for our host (allows IPv6 addresses)
|
||||
try:
|
||||
info = socket.getaddrinfo(host, port, socket.AF_UNSPEC,
|
||||
socket.SOCK_STREAM)
|
||||
except socket.gaierror:
|
||||
if ':' in host:
|
||||
info = [(
|
||||
socket.AF_INET6, socket.SOCK_STREAM, 0, '', (host, port, 0, 0)
|
||||
)]
|
||||
else:
|
||||
info = [(socket.AF_INET, socket.SOCK_STREAM, 0, '', (host, port))]
|
||||
|
||||
for res in info:
|
||||
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(timeout)
|
||||
s.connect((host, port))
|
||||
s.close()
|
||||
except socket.error:
|
||||
if s:
|
||||
s.close()
|
||||
else:
|
||||
raise IOError('Port %s is in use on %s; perhaps the previous '
|
||||
'httpserver did not shut down properly.' %
|
||||
(repr(port), repr(host)))
|
||||
|
||||
|
||||
# Feel free to increase these defaults on slow systems:
|
||||
free_port_timeout = 0.1
|
||||
occupied_port_timeout = 1.0
|
||||
|
||||
|
||||
def wait_for_free_port(host, port, timeout=None):
|
||||
"""Wait for the specified port to become free (drop requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
if timeout is None:
|
||||
timeout = free_port_timeout
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
# we are expecting a free port, so reduce the timeout
|
||||
check_port(host, port, timeout=timeout)
|
||||
except IOError:
|
||||
# Give the old server thread time to free the port.
|
||||
time.sleep(timeout)
|
||||
else:
|
||||
return
|
||||
|
||||
raise IOError('Port %r not free on %r' % (port, host))
|
||||
|
||||
|
||||
def wait_for_occupied_port(host, port, timeout=None):
|
||||
"""Wait for the specified port to become active (receive requests)."""
|
||||
if not host:
|
||||
raise ValueError("Host values of '' or None are not allowed.")
|
||||
if timeout is None:
|
||||
timeout = occupied_port_timeout
|
||||
|
||||
for trial in range(50):
|
||||
try:
|
||||
check_port(host, port, timeout=timeout)
|
||||
except IOError:
|
||||
# port is occupied
|
||||
return
|
||||
else:
|
||||
time.sleep(timeout)
|
||||
|
||||
if host == client_host(host):
|
||||
raise IOError('Port %r not bound on %r' % (port, host))
|
||||
|
||||
# On systems where a loopback interface is not available and the
|
||||
# server is bound to all interfaces, it's difficult to determine
|
||||
# whether the server is in fact occupying the port. In this case,
|
||||
# just issue a warning and move on. See issue #1100.
|
||||
msg = 'Unable to verify that the server is bound on %r' % port
|
||||
warnings.warn(msg)
|
|
@ -1,180 +0,0 @@
|
|||
"""Windows service. Requires pywin32."""
|
||||
|
||||
import os
|
||||
import win32api
|
||||
import win32con
|
||||
import win32event
|
||||
import win32service
|
||||
import win32serviceutil
|
||||
|
||||
from cherrypy.process import wspbus, plugins
|
||||
|
||||
|
||||
class ConsoleCtrlHandler(plugins.SimplePlugin):
|
||||
|
||||
"""A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
|
||||
|
||||
def __init__(self, bus):
|
||||
self.is_set = False
|
||||
plugins.SimplePlugin.__init__(self, bus)
|
||||
|
||||
def start(self):
|
||||
if self.is_set:
|
||||
self.bus.log('Handler for console events already set.', level=40)
|
||||
return
|
||||
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 1)
|
||||
if result == 0:
|
||||
self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
|
||||
win32api.GetLastError(), level=40)
|
||||
else:
|
||||
self.bus.log('Set handler for console events.', level=40)
|
||||
self.is_set = True
|
||||
|
||||
def stop(self):
|
||||
if not self.is_set:
|
||||
self.bus.log('Handler for console events already off.', level=40)
|
||||
return
|
||||
|
||||
try:
|
||||
result = win32api.SetConsoleCtrlHandler(self.handle, 0)
|
||||
except ValueError:
|
||||
# "ValueError: The object has not been registered"
|
||||
result = 1
|
||||
|
||||
if result == 0:
|
||||
self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
|
||||
win32api.GetLastError(), level=40)
|
||||
else:
|
||||
self.bus.log('Removed handler for console events.', level=40)
|
||||
self.is_set = False
|
||||
|
||||
def handle(self, event):
|
||||
"""Handle console control events (like Ctrl-C)."""
|
||||
if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
|
||||
win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
|
||||
win32con.CTRL_CLOSE_EVENT):
|
||||
self.bus.log('Console event %s: shutting down bus' % event)
|
||||
|
||||
# Remove self immediately so repeated Ctrl-C doesn't re-call it.
|
||||
try:
|
||||
self.stop()
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self.bus.exit()
|
||||
# 'First to return True stops the calls'
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
||||
class Win32Bus(wspbus.Bus):
|
||||
|
||||
"""A Web Site Process Bus implementation for Win32.
|
||||
|
||||
Instead of time.sleep, this bus blocks using native win32event objects.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.events = {}
|
||||
wspbus.Bus.__init__(self)
|
||||
|
||||
def _get_state_event(self, state):
|
||||
"""Return a win32event for the given state (creating it if needed)."""
|
||||
try:
|
||||
return self.events[state]
|
||||
except KeyError:
|
||||
event = win32event.CreateEvent(None, 0, 0,
|
||||
'WSPBus %s Event (pid=%r)' %
|
||||
(state.name, os.getpid()))
|
||||
self.events[state] = event
|
||||
return event
|
||||
|
||||
def _get_state(self):
|
||||
return self._state
|
||||
|
||||
def _set_state(self, value):
|
||||
self._state = value
|
||||
event = self._get_state_event(value)
|
||||
win32event.PulseEvent(event)
|
||||
state = property(_get_state, _set_state)
|
||||
|
||||
def wait(self, state, interval=0.1, channel=None):
|
||||
"""Wait for the given state(s), KeyboardInterrupt or SystemExit.
|
||||
|
||||
Since this class uses native win32event objects, the interval
|
||||
argument is ignored.
|
||||
"""
|
||||
if isinstance(state, (tuple, list)):
|
||||
# Don't wait for an event that beat us to the punch ;)
|
||||
if self.state not in state:
|
||||
events = tuple([self._get_state_event(s) for s in state])
|
||||
win32event.WaitForMultipleObjects(
|
||||
events, 0, win32event.INFINITE)
|
||||
else:
|
||||
# Don't wait for an event that beat us to the punch ;)
|
||||
if self.state != state:
|
||||
event = self._get_state_event(state)
|
||||
win32event.WaitForSingleObject(event, win32event.INFINITE)
|
||||
|
||||
|
||||
class _ControlCodes(dict):
|
||||
|
||||
"""Control codes used to "signal" a service via ControlService.
|
||||
|
||||
User-defined control codes are in the range 128-255. We generally use
|
||||
the standard Python value for the Linux signal and add 128. Example:
|
||||
|
||||
>>> signal.SIGUSR1
|
||||
10
|
||||
control_codes['graceful'] = 128 + 10
|
||||
"""
|
||||
|
||||
def key_for(self, obj):
|
||||
"""For the given value, return its corresponding key."""
|
||||
for key, val in self.items():
|
||||
if val is obj:
|
||||
return key
|
||||
raise ValueError('The given object could not be found: %r' % obj)
|
||||
|
||||
control_codes = _ControlCodes({'graceful': 138})
|
||||
|
||||
|
||||
def signal_child(service, command):
|
||||
if command == 'stop':
|
||||
win32serviceutil.StopService(service)
|
||||
elif command == 'restart':
|
||||
win32serviceutil.RestartService(service)
|
||||
else:
|
||||
win32serviceutil.ControlService(service, control_codes[command])
|
||||
|
||||
|
||||
class PyWebService(win32serviceutil.ServiceFramework):
|
||||
|
||||
"""Python Web Service."""
|
||||
|
||||
_svc_name_ = 'Python Web Service'
|
||||
_svc_display_name_ = 'Python Web Service'
|
||||
_svc_deps_ = None # sequence of service names on which this depends
|
||||
_exe_name_ = 'pywebsvc'
|
||||
_exe_args_ = None # Default to no arguments
|
||||
|
||||
# Only exists on Windows 2000 or later, ignored on windows NT
|
||||
_svc_description_ = 'Python Web Service'
|
||||
|
||||
def SvcDoRun(self):
|
||||
from cherrypy import process
|
||||
process.bus.start()
|
||||
process.bus.block()
|
||||
|
||||
def SvcStop(self):
|
||||
from cherrypy import process
|
||||
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
|
||||
process.bus.exit()
|
||||
|
||||
def SvcOther(self, control):
|
||||
process.bus.publish(control_codes.key_for(control))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
win32serviceutil.HandleCommandLine(PyWebService)
|
|
@ -1,590 +0,0 @@
|
|||
"""An implementation of the Web Site Process Bus.
|
||||
|
||||
This module is completely standalone, depending only on the stdlib.
|
||||
|
||||
Web Site Process Bus
|
||||
--------------------
|
||||
|
||||
A Bus object is used to contain and manage site-wide behavior:
|
||||
daemonization, HTTP server start/stop, process reload, signal handling,
|
||||
drop privileges, PID file management, logging for all of these,
|
||||
and many more.
|
||||
|
||||
In addition, a Bus object provides a place for each web framework
|
||||
to register code that runs in response to site-wide events (like
|
||||
process start and stop), or which controls or otherwise interacts with
|
||||
the site-wide components mentioned above. For example, a framework which
|
||||
uses file-based templates would add known template filenames to an
|
||||
autoreload component.
|
||||
|
||||
Ideally, a Bus object will be flexible enough to be useful in a variety
|
||||
of invocation scenarios:
|
||||
|
||||
1. The deployer starts a site from the command line via a
|
||||
framework-neutral deployment script; applications from multiple frameworks
|
||||
are mixed in a single site. Command-line arguments and configuration
|
||||
files are used to define site-wide components such as the HTTP server,
|
||||
WSGI component graph, autoreload behavior, signal handling, etc.
|
||||
2. The deployer starts a site via some other process, such as Apache;
|
||||
applications from multiple frameworks are mixed in a single site.
|
||||
Autoreload and signal handling (from Python at least) are disabled.
|
||||
3. The deployer starts a site via a framework-specific mechanism;
|
||||
for example, when running tests, exploring tutorials, or deploying
|
||||
single applications from a single framework. The framework controls
|
||||
which site-wide components are enabled as it sees fit.
|
||||
|
||||
The Bus object in this package uses topic-based publish-subscribe
|
||||
messaging to accomplish all this. A few topic channels are built in
|
||||
('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and
|
||||
site containers are free to define their own. If a message is sent to a
|
||||
channel that has not been defined or has no listeners, there is no effect.
|
||||
|
||||
In general, there should only ever be a single Bus object per process.
|
||||
Frameworks and site containers share a single Bus object by publishing
|
||||
messages and subscribing listeners.
|
||||
|
||||
The Bus object works as a finite state machine which models the current
|
||||
state of the process. Bus methods move it from one state to another;
|
||||
those methods then publish to subscribed listeners on the channel for
|
||||
the new state.::
|
||||
|
||||
O
|
||||
|
|
||||
V
|
||||
STOPPING --> STOPPED --> EXITING -> X
|
||||
A A |
|
||||
| \___ |
|
||||
| \ |
|
||||
| V V
|
||||
STARTED <-- STARTING
|
||||
|
||||
"""
|
||||
|
||||
import atexit
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
except ImportError:
|
||||
"""Google AppEngine is shipped without ctypes
|
||||
|
||||
:seealso: http://stackoverflow.com/a/6523777/70170
|
||||
"""
|
||||
ctypes = None
|
||||
|
||||
import operator
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import traceback as _traceback
|
||||
import warnings
|
||||
|
||||
import six
|
||||
|
||||
from cherrypy._cpcompat import _args_from_interpreter_flags
|
||||
|
||||
|
||||
# Here I save the value of os.getcwd(), which, if I am imported early enough,
|
||||
# will be the directory from which the startup script was run. This is needed
|
||||
# by _do_execv(), to change back to the original directory before execv()ing a
|
||||
# new process. This is a defense against the application having changed the
|
||||
# current working directory (which could make sys.executable "not found" if
|
||||
# sys.executable is a relative-path, and/or cause other problems).
|
||||
_startup_cwd = os.getcwd()
|
||||
|
||||
|
||||
class ChannelFailures(Exception):
|
||||
|
||||
"""Exception raised when errors occur in a listener during Bus.publish().
|
||||
"""
|
||||
delimiter = '\n'
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Exception, self).__init__(*args, **kwargs)
|
||||
self._exceptions = list()
|
||||
|
||||
def handle_exception(self):
|
||||
"""Append the current exception to self."""
|
||||
self._exceptions.append(sys.exc_info()[1])
|
||||
|
||||
def get_instances(self):
|
||||
"""Return a list of seen exception instances."""
|
||||
return self._exceptions[:]
|
||||
|
||||
def __str__(self):
|
||||
exception_strings = map(repr, self.get_instances())
|
||||
return self.delimiter.join(exception_strings)
|
||||
|
||||
__repr__ = __str__
|
||||
|
||||
def __bool__(self):
|
||||
return bool(self._exceptions)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
# Use a flag to indicate the state of the bus.
|
||||
|
||||
|
||||
class _StateEnum(object):
|
||||
|
||||
class State(object):
|
||||
name = None
|
||||
|
||||
def __repr__(self):
|
||||
return 'states.%s' % self.name
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
if isinstance(value, self.State):
|
||||
value.name = key
|
||||
object.__setattr__(self, key, value)
|
||||
states = _StateEnum()
|
||||
states.STOPPED = states.State()
|
||||
states.STARTING = states.State()
|
||||
states.STARTED = states.State()
|
||||
states.STOPPING = states.State()
|
||||
states.EXITING = states.State()
|
||||
|
||||
|
||||
try:
|
||||
import fcntl
|
||||
except ImportError:
|
||||
max_files = 0
|
||||
else:
|
||||
try:
|
||||
max_files = os.sysconf('SC_OPEN_MAX')
|
||||
except AttributeError:
|
||||
max_files = 1024
|
||||
|
||||
|
||||
class Bus(object):
|
||||
|
||||
"""Process state-machine and messenger for HTTP site deployment.
|
||||
|
||||
All listeners for a given channel are guaranteed to be called even
|
||||
if others at the same channel fail. Each failure is logged, but
|
||||
execution proceeds on to the next listener. The only way to stop all
|
||||
processing from inside a listener is to raise SystemExit and stop the
|
||||
whole server.
|
||||
"""
|
||||
|
||||
states = states
|
||||
state = states.STOPPED
|
||||
execv = False
|
||||
max_cloexec_files = max_files
|
||||
|
||||
def __init__(self):
|
||||
self.execv = False
|
||||
self.state = states.STOPPED
|
||||
channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
|
||||
self.listeners = dict(
|
||||
(channel, set())
|
||||
for channel in channels
|
||||
)
|
||||
self._priorities = {}
|
||||
|
||||
def subscribe(self, channel, callback, priority=None):
|
||||
"""Add the given callback at the given channel (if not present)."""
|
||||
ch_listeners = self.listeners.setdefault(channel, set())
|
||||
ch_listeners.add(callback)
|
||||
|
||||
if priority is None:
|
||||
priority = getattr(callback, 'priority', 50)
|
||||
self._priorities[(channel, callback)] = priority
|
||||
|
||||
def unsubscribe(self, channel, callback):
|
||||
"""Discard the given callback (if present)."""
|
||||
listeners = self.listeners.get(channel)
|
||||
if listeners and callback in listeners:
|
||||
listeners.discard(callback)
|
||||
del self._priorities[(channel, callback)]
|
||||
|
||||
def publish(self, channel, *args, **kwargs):
|
||||
"""Return output of all subscribers for the given channel."""
|
||||
if channel not in self.listeners:
|
||||
return []
|
||||
|
||||
exc = ChannelFailures()
|
||||
output = []
|
||||
|
||||
raw_items = (
|
||||
(self._priorities[(channel, listener)], listener)
|
||||
for listener in self.listeners[channel]
|
||||
)
|
||||
items = sorted(raw_items, key=operator.itemgetter(0))
|
||||
for priority, listener in items:
|
||||
try:
|
||||
output.append(listener(*args, **kwargs))
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except SystemExit:
|
||||
e = sys.exc_info()[1]
|
||||
# If we have previous errors ensure the exit code is non-zero
|
||||
if exc and e.code == 0:
|
||||
e.code = 1
|
||||
raise
|
||||
except:
|
||||
exc.handle_exception()
|
||||
if channel == 'log':
|
||||
# Assume any further messages to 'log' will fail.
|
||||
pass
|
||||
else:
|
||||
self.log('Error in %r listener %r' % (channel, listener),
|
||||
level=40, traceback=True)
|
||||
if exc:
|
||||
raise exc
|
||||
return output
|
||||
|
||||
def _clean_exit(self):
|
||||
"""An atexit handler which asserts the Bus is not running."""
|
||||
if self.state != states.EXITING:
|
||||
warnings.warn(
|
||||
'The main thread is exiting, but the Bus is in the %r state; '
|
||||
'shutting it down automatically now. You must either call '
|
||||
'bus.block() after start(), or call bus.exit() before the '
|
||||
'main thread exits.' % self.state, RuntimeWarning)
|
||||
self.exit()
|
||||
|
||||
def start(self):
|
||||
"""Start all services."""
|
||||
atexit.register(self._clean_exit)
|
||||
|
||||
self.state = states.STARTING
|
||||
self.log('Bus STARTING')
|
||||
try:
|
||||
self.publish('start')
|
||||
self.state = states.STARTED
|
||||
self.log('Bus STARTED')
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
raise
|
||||
except:
|
||||
self.log('Shutting down due to error in start listener:',
|
||||
level=40, traceback=True)
|
||||
e_info = sys.exc_info()[1]
|
||||
try:
|
||||
self.exit()
|
||||
except:
|
||||
# Any stop/exit errors will be logged inside publish().
|
||||
pass
|
||||
# Re-raise the original error
|
||||
raise e_info
|
||||
|
||||
def exit(self):
|
||||
"""Stop all services and prepare to exit the process."""
|
||||
exitstate = self.state
|
||||
try:
|
||||
self.stop()
|
||||
|
||||
self.state = states.EXITING
|
||||
self.log('Bus EXITING')
|
||||
self.publish('exit')
|
||||
# This isn't strictly necessary, but it's better than seeing
|
||||
# "Waiting for child threads to terminate..." and then nothing.
|
||||
self.log('Bus EXITED')
|
||||
except:
|
||||
# This method is often called asynchronously (whether thread,
|
||||
# signal handler, console handler, or atexit handler), so we
|
||||
# can't just let exceptions propagate out unhandled.
|
||||
# Assume it's been logged and just die.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
|
||||
if exitstate == states.STARTING:
|
||||
# exit() was called before start() finished, possibly due to
|
||||
# Ctrl-C because a start listener got stuck. In this case,
|
||||
# we could get stuck in a loop where Ctrl-C never exits the
|
||||
# process, so we just call os.exit here.
|
||||
os._exit(70) # EX_SOFTWARE
|
||||
|
||||
def restart(self):
|
||||
"""Restart the process (may close connections).
|
||||
|
||||
This method does not restart the process from the calling thread;
|
||||
instead, it stops the bus and asks the main thread to call execv.
|
||||
"""
|
||||
self.execv = True
|
||||
self.exit()
|
||||
|
||||
def graceful(self):
|
||||
"""Advise all services to reload."""
|
||||
self.log('Bus graceful')
|
||||
self.publish('graceful')
|
||||
|
||||
def block(self, interval=0.1):
|
||||
"""Wait for the EXITING state, KeyboardInterrupt or SystemExit.
|
||||
|
||||
This function is intended to be called only by the main thread.
|
||||
After waiting for the EXITING state, it also waits for all threads
|
||||
to terminate, and then calls os.execv if self.execv is True. This
|
||||
design allows another thread to call bus.restart, yet have the main
|
||||
thread perform the actual execv call (required on some platforms).
|
||||
"""
|
||||
try:
|
||||
self.wait(states.EXITING, interval=interval, channel='main')
|
||||
except (KeyboardInterrupt, IOError):
|
||||
# The time.sleep call might raise
|
||||
# "IOError: [Errno 4] Interrupted function call" on KBInt.
|
||||
self.log('Keyboard Interrupt: shutting down bus')
|
||||
self.exit()
|
||||
except SystemExit:
|
||||
self.log('SystemExit raised: shutting down bus')
|
||||
self.exit()
|
||||
raise
|
||||
|
||||
# Waiting for ALL child threads to finish is necessary on OS X.
|
||||
# See https://github.com/cherrypy/cherrypy/issues/581.
|
||||
# It's also good to let them all shut down before allowing
|
||||
# the main thread to call atexit handlers.
|
||||
# See https://github.com/cherrypy/cherrypy/issues/751.
|
||||
self.log('Waiting for child threads to terminate...')
|
||||
for t in threading.enumerate():
|
||||
# Validate the we're not trying to join the MainThread
|
||||
# that will cause a deadlock and the case exist when
|
||||
# implemented as a windows service and in any other case
|
||||
# that another thread executes cherrypy.engine.exit()
|
||||
if (
|
||||
t != threading.currentThread() and
|
||||
t.isAlive() and
|
||||
not isinstance(t, threading._MainThread)
|
||||
):
|
||||
# Note that any dummy (external) threads are always daemonic.
|
||||
if hasattr(threading.Thread, 'daemon'):
|
||||
# Python 2.6+
|
||||
d = t.daemon
|
||||
else:
|
||||
d = t.isDaemon()
|
||||
if not d:
|
||||
self.log('Waiting for thread %s.' % t.getName())
|
||||
t.join()
|
||||
|
||||
if self.execv:
|
||||
self._do_execv()
|
||||
|
||||
def wait(self, state, interval=0.1, channel=None):
|
||||
"""Poll for the given state(s) at intervals; publish to channel."""
|
||||
if isinstance(state, (tuple, list)):
|
||||
states = state
|
||||
else:
|
||||
states = [state]
|
||||
|
||||
def _wait():
|
||||
while self.state not in states:
|
||||
time.sleep(interval)
|
||||
self.publish(channel)
|
||||
|
||||
# From http://psyco.sourceforge.net/psycoguide/bugs.html:
|
||||
# "The compiled machine code does not include the regular polling
|
||||
# done by Python, meaning that a KeyboardInterrupt will not be
|
||||
# detected before execution comes back to the regular Python
|
||||
# interpreter. Your program cannot be interrupted if caught
|
||||
# into an infinite Psyco-compiled loop."
|
||||
try:
|
||||
sys.modules['psyco'].cannotcompile(_wait)
|
||||
except (KeyError, AttributeError):
|
||||
pass
|
||||
|
||||
_wait()
|
||||
|
||||
def _do_execv(self):
|
||||
"""Re-execute the current process.
|
||||
|
||||
This must be called from the main thread, because certain platforms
|
||||
(OS X) don't allow execv to be called in a child thread very well.
|
||||
"""
|
||||
try:
|
||||
args = self._get_true_argv()
|
||||
except NotImplementedError:
|
||||
"""It's probably win32 or GAE"""
|
||||
args = [sys.executable] + self._get_interpreter_argv() + sys.argv
|
||||
|
||||
self.log('Re-spawning %s' % ' '.join(args))
|
||||
|
||||
self._extend_pythonpath(os.environ)
|
||||
|
||||
if sys.platform[:4] == 'java':
|
||||
from _systemrestart import SystemRestart
|
||||
raise SystemRestart
|
||||
else:
|
||||
if sys.platform == 'win32':
|
||||
args = ['"%s"' % arg for arg in args]
|
||||
|
||||
os.chdir(_startup_cwd)
|
||||
if self.max_cloexec_files:
|
||||
self._set_cloexec()
|
||||
os.execv(sys.executable, args)
|
||||
|
||||
@staticmethod
|
||||
def _get_interpreter_argv():
|
||||
"""Retrieve current Python interpreter's arguments
|
||||
|
||||
Returns empty tuple in case of frozen mode, uses built-in arguments
|
||||
reproduction function otherwise.
|
||||
|
||||
Frozen mode is possible for the app has been packaged into a binary
|
||||
executable using py2exe. In this case the interpreter's arguments are
|
||||
already built-in into that executable.
|
||||
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1526
|
||||
Ref: https://pythonhosted.org/PyInstaller/runtime-information.html
|
||||
"""
|
||||
return ([]
|
||||
if getattr(sys, 'frozen', False)
|
||||
else _args_from_interpreter_flags())
|
||||
|
||||
@staticmethod
|
||||
def _get_true_argv():
|
||||
"""Retrieves all real arguments of the python interpreter
|
||||
|
||||
...even those not listed in ``sys.argv``
|
||||
|
||||
:seealso: http://stackoverflow.com/a/28338254/595220
|
||||
:seealso: http://stackoverflow.com/a/6683222/595220
|
||||
:seealso: http://stackoverflow.com/a/28414807/595220
|
||||
"""
|
||||
|
||||
try:
|
||||
char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
|
||||
|
||||
argv = ctypes.POINTER(char_p)()
|
||||
argc = ctypes.c_int()
|
||||
|
||||
ctypes.pythonapi.Py_GetArgcArgv(ctypes.byref(argc), ctypes.byref(argv))
|
||||
|
||||
_argv = argv[:argc.value]
|
||||
|
||||
# The code below is trying to correctly handle special cases.
|
||||
# `-c`'s argument interpreted by Python itself becomes `-c` as
|
||||
# well. Same applies to `-m`. This snippet is trying to survive
|
||||
# at least the case with `-m`
|
||||
# Ref: https://github.com/cherrypy/cherrypy/issues/1545
|
||||
# Ref: python/cpython@418baf9
|
||||
argv_len, is_command, is_module = len(_argv), False, False
|
||||
|
||||
try:
|
||||
m_ind = _argv.index('-m')
|
||||
if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'):
|
||||
"""
|
||||
In some older Python versions `-m`'s argument may be
|
||||
substituted with `-c`, not `-m`
|
||||
"""
|
||||
is_module = True
|
||||
except (IndexError, ValueError):
|
||||
m_ind = None
|
||||
|
||||
try:
|
||||
c_ind = _argv.index('-c')
|
||||
if m_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
|
||||
is_command = True
|
||||
except (IndexError, ValueError):
|
||||
c_ind = None
|
||||
|
||||
if is_module:
|
||||
"""It's containing `-m -m` sequence of arguments"""
|
||||
if is_command and c_ind < m_ind:
|
||||
"""There's `-c -c` before `-m`"""
|
||||
raise RuntimeError(
|
||||
"Cannot reconstruct command from '-c'. Ref: "
|
||||
'https://github.com/cherrypy/cherrypy/issues/1545')
|
||||
# Survive module argument here
|
||||
original_module = sys.argv[0]
|
||||
if not os.access(original_module, os.R_OK):
|
||||
"""There's no such module exist"""
|
||||
raise AttributeError(
|
||||
"{} doesn't seem to be a module "
|
||||
"accessible by current user".format(original_module))
|
||||
del _argv[m_ind:m_ind + 2] # remove `-m -m`
|
||||
# ... and substitute it with the original module path:
|
||||
_argv.insert(m_ind, original_module)
|
||||
elif is_command:
|
||||
"""It's containing just `-c -c` sequence of arguments"""
|
||||
raise RuntimeError(
|
||||
"Cannot reconstruct command from '-c'. "
|
||||
'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
|
||||
except AttributeError:
|
||||
"""It looks Py_GetArgcArgv is completely absent in some environments
|
||||
|
||||
It is known, that there's no Py_GetArgcArgv in MS Windows and
|
||||
``ctypes`` module is completely absent in Google AppEngine
|
||||
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1506
|
||||
:seealso: https://github.com/cherrypy/cherrypy/issues/1512
|
||||
:ref: https://chromium.googlesource.com/infra/infra/+/69eb0279c12bcede5937ce9298020dd4581e38dd%5E!/
|
||||
"""
|
||||
raise NotImplementedError
|
||||
else:
|
||||
return _argv
|
||||
|
||||
@staticmethod
|
||||
def _extend_pythonpath(env):
|
||||
"""
|
||||
If sys.path[0] is an empty string, the interpreter was likely
|
||||
invoked with -m and the effective path is about to change on
|
||||
re-exec. Add the current directory to $PYTHONPATH to ensure
|
||||
that the new process sees the same path.
|
||||
|
||||
This issue cannot be addressed in the general case because
|
||||
Python cannot reliably reconstruct the
|
||||
original command line (http://bugs.python.org/issue14208).
|
||||
|
||||
(This idea filched from tornado.autoreload)
|
||||
"""
|
||||
path_prefix = '.' + os.pathsep
|
||||
existing_path = env.get('PYTHONPATH', '')
|
||||
needs_patch = (
|
||||
sys.path[0] == '' and
|
||||
not existing_path.startswith(path_prefix)
|
||||
)
|
||||
|
||||
if needs_patch:
|
||||
env['PYTHONPATH'] = path_prefix + existing_path
|
||||
|
||||
def _set_cloexec(self):
|
||||
"""Set the CLOEXEC flag on all open files (except stdin/out/err).
|
||||
|
||||
If self.max_cloexec_files is an integer (the default), then on
|
||||
platforms which support it, it represents the max open files setting
|
||||
for the operating system. This function will be called just before
|
||||
the process is restarted via os.execv() to prevent open files
|
||||
from persisting into the new process.
|
||||
|
||||
Set self.max_cloexec_files to 0 to disable this behavior.
|
||||
"""
|
||||
for fd in range(3, self.max_cloexec_files): # skip stdin/out/err
|
||||
try:
|
||||
flags = fcntl.fcntl(fd, fcntl.F_GETFD)
|
||||
except IOError:
|
||||
continue
|
||||
fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
|
||||
|
||||
def stop(self):
|
||||
"""Stop all services."""
|
||||
self.state = states.STOPPING
|
||||
self.log('Bus STOPPING')
|
||||
self.publish('stop')
|
||||
self.state = states.STOPPED
|
||||
self.log('Bus STOPPED')
|
||||
|
||||
def start_with_callback(self, func, args=None, kwargs=None):
|
||||
"""Start 'func' in a new thread T, then start self (and return T)."""
|
||||
if args is None:
|
||||
args = ()
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
args = (func,) + args
|
||||
|
||||
def _callback(func, *a, **kw):
|
||||
self.wait(states.STARTED)
|
||||
func(*a, **kw)
|
||||
t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
|
||||
t.setName('Bus Callback ' + t.getName())
|
||||
t.start()
|
||||
|
||||
self.start()
|
||||
|
||||
return t
|
||||
|
||||
def log(self, msg='', level=20, traceback=False):
|
||||
"""Log the given message. Append the last traceback if requested."""
|
||||
if traceback:
|
||||
msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
|
||||
self.publish('log', msg, level)
|
||||
|
||||
bus = Bus()
|
|
@ -1,59 +0,0 @@
|
|||
"""<MyProject>, a CherryPy application.
|
||||
|
||||
Use this as a base for creating new CherryPy applications. When you want
|
||||
to make a new app, copy and paste this folder to some other location
|
||||
(maybe site-packages) and rename it to the name of your project,
|
||||
then tweak as desired.
|
||||
|
||||
Even before any tweaking, this should serve a few demonstration pages.
|
||||
Change to this directory and run:
|
||||
|
||||
cherryd -c site.conf
|
||||
|
||||
"""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import tools, url
|
||||
|
||||
import os
|
||||
local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
@cherrypy.config(**{'tools.log_tracebacks.on': True})
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return """<html>
|
||||
<body>Try some <a href='%s?a=7'>other</a> path,
|
||||
or a <a href='%s?n=14'>default</a> path.<br />
|
||||
Or, just look at the pretty picture:<br />
|
||||
<img src='%s' />
|
||||
</body></html>""" % (url('other'), url('else'),
|
||||
url('files/made_with_cherrypy_small.png'))
|
||||
|
||||
@cherrypy.expose
|
||||
def default(self, *args, **kwargs):
|
||||
return 'args: %s kwargs: %s' % (args, kwargs)
|
||||
|
||||
@cherrypy.expose
|
||||
def other(self, a=2, b='bananas', c=None):
|
||||
cherrypy.response.headers['Content-Type'] = 'text/plain'
|
||||
if c is None:
|
||||
return 'Have %d %s.' % (int(a), b)
|
||||
else:
|
||||
return 'Have %d %s, %s.' % (int(a), b, c)
|
||||
|
||||
files = tools.staticdir.handler(
|
||||
section='/files',
|
||||
dir=os.path.join(local_dir, 'static'),
|
||||
# Ignore .php files, etc.
|
||||
match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
|
||||
)
|
||||
|
||||
|
||||
root = Root()
|
||||
|
||||
# Uncomment the following to use your own favicon instead of CP's default.
|
||||
# favicon_path = os.path.join(local_dir, "favicon.ico")
|
||||
# root.favicon_ico = tools.staticfile.handler(filename=favicon_path)
|
|
@ -1,22 +0,0 @@
|
|||
# Apache2 server conf file for using CherryPy with mod_fcgid.
|
||||
|
||||
# This doesn't have to be "C:/", but it has to be a directory somewhere, and
|
||||
# MUST match the directory used in the FastCgiExternalServer directive, below.
|
||||
DocumentRoot "C:/"
|
||||
|
||||
ServerName 127.0.0.1
|
||||
Listen 80
|
||||
LoadModule fastcgi_module modules/mod_fastcgi.dll
|
||||
LoadModule rewrite_module modules/mod_rewrite.so
|
||||
|
||||
Options ExecCGI
|
||||
SetHandler fastcgi-script
|
||||
RewriteEngine On
|
||||
# Send requests for any URI to our fastcgi handler.
|
||||
RewriteRule ^(.*)$ /fastcgi.pyc [L]
|
||||
|
||||
# The FastCgiExternalServer directive defines filename as an external FastCGI application.
|
||||
# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot.
|
||||
# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this
|
||||
# filename will be handled by this external FastCGI application.
|
||||
FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088
|
|
@ -1,3 +0,0 @@
|
|||
[/]
|
||||
log.error_file: "error.log"
|
||||
log.access_file: "access.log"
|
|
@ -1,14 +0,0 @@
|
|||
[global]
|
||||
# Uncomment this when you're done developing
|
||||
#environment: "production"
|
||||
|
||||
server.socket_host: "0.0.0.0"
|
||||
server.socket_port: 8088
|
||||
|
||||
# Uncomment the following lines to run on HTTPS at the same time
|
||||
#server.2.socket_host: "0.0.0.0"
|
||||
#server.2.socket_port: 8433
|
||||
#server.2.ssl_certificate: '../test/test.pem'
|
||||
#server.2.ssl_private_key: '../test/test.pem'
|
||||
|
||||
tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")
|
Binary file not shown.
Before Width: | Height: | Size: 7.3 KiB |
|
@ -1,24 +0,0 @@
|
|||
"""
|
||||
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
|
|
@ -1,40 +0,0 @@
|
|||
"""Test module for the @-decorator syntax, which is version-specific"""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy import expose, tools
|
||||
from cherrypy._cpcompat import ntob
|
||||
|
||||
|
||||
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 ntob('blah')
|
|
@ -1,69 +0,0 @@
|
|||
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()
|
||||
|
||||
|
||||
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', unsub_sig, priority=100)
|
||||
|
||||
|
||||
def starterror():
|
||||
if cherrypy.config.get('starterror', False):
|
||||
zerodiv = 1 / 0
|
||||
cherrypy.engine.subscribe('start', starterror, 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.engine.subscribe('start', log_test_case_name, priority=6)
|
||||
|
||||
|
||||
cherrypy.tree.mount(Root(), '/', {'/': {}})
|
|
@ -1,421 +0,0 @@
|
|||
"""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._cpcompat import ntob
|
||||
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', 'startup', '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
|
||||
|
||||
|
||||
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,
|
||||
'engine.timeout_monitor.frequency': 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,
|
||||
},
|
||||
}
|
||||
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',
|
||||
ntob(r'^Complete requests:\s*(\d+)')),
|
||||
('failed_requests', 'Failed',
|
||||
ntob(r'^Failed requests:\s*(\d+)')),
|
||||
('requests_per_second', 'req/sec',
|
||||
ntob(r'^Requests per second:\s*([0-9.]+)')),
|
||||
('time_per_request_concurrent', 'msec/req',
|
||||
ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')),
|
||||
('transfer_rate', 'KB/sec',
|
||||
ntob(r'^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:
|
||||
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 i, val in enumerate(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__':
|
||||
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:
|
||||
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()
|
|
@ -1,48 +0,0 @@
|
|||
"""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)
|
|
@ -1,18 +0,0 @@
|
|||
|
||||
# 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
|
|
@ -1,14 +0,0 @@
|
|||
|
||||
# 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
|
|
@ -1,559 +0,0 @@
|
|||
"""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 nose
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import text_or_bytes, copyitems, HTTPSConnection, ntob
|
||||
from cherrypy.lib import httputil
|
||||
from cherrypy.lib import gctools
|
||||
from cherrypy.lib.reprconf import unrepr
|
||||
from cherrypy.test import webtest
|
||||
|
||||
_testconfig = None
|
||||
log = logging.getLogger(__name__)
|
||||
thisdir = os.path.abspath(os.path.dirname(__file__))
|
||||
serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
|
||||
|
||||
|
||||
def get_tst_config(overconf={}):
|
||||
global _testconfig
|
||||
if _testconfig is None:
|
||||
conf = {
|
||||
'scheme': 'http',
|
||||
'protocol': 'HTTP/1.1',
|
||||
'port': 54583,
|
||||
'host': '127.0.0.1',
|
||||
'validate': False,
|
||||
'server': 'wsgi',
|
||||
}
|
||||
try:
|
||||
import testconfig
|
||||
_conf = testconfig.config.get('supervisor', None)
|
||||
if _conf is not None:
|
||||
for k, v in _conf.items():
|
||||
if isinstance(v, text_or_bytes):
|
||||
_conf[k] = unrepr(v)
|
||||
conf.update(_conf)
|
||||
except ImportError:
|
||||
pass
|
||||
_testconfig = conf
|
||||
conf = _testconfig.copy()
|
||||
conf.update(overconf)
|
||||
|
||||
return conf
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
log_to_stderr = lambda msg, level: 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()
|
||||
|
||||
for name, server in copyitems(getattr(cherrypy, 'servers', {})):
|
||||
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 = get_tst_config()
|
||||
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 '):
|
||||
raise nose.SkipTest(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">') + ntob('(.*)') + 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."""
|
||||
cherrypy._cpserver.wait_for_free_port(self.host, self.port)
|
||||
|
||||
args = [
|
||||
'-m',
|
||||
'cherrypy.__main__', # __main__ is needed for `-m` in Python 2.6
|
||||
'-c', self.config_file,
|
||||
'-p', self.pid_file,
|
||||
]
|
||||
"""
|
||||
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:
|
||||
cherrypy._cpserver.wait_for_occupied_port(self.host, self.port)
|
||||
|
||||
# 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
|
|
@ -1,200 +0,0 @@
|
|||
"""logtest, a unittest.TestCase helper for testing log output."""
|
||||
|
||||
import sys
|
||||
import time
|
||||
|
||||
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 = ntob('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 not marker 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 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)
|
|
@ -1,136 +0,0 @@
|
|||
"""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)
|
|
@ -1,124 +0,0 @@
|
|||
"""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()
|
|
@ -1,165 +0,0 @@
|
|||
"""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
|
||||
|
||||
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()
|
||||
|
||||
import cherrypy
|
||||
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()
|
||||
|
||||
import cherrypy
|
||||
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
|
|
@ -1,151 +0,0 @@
|
|||
"""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 cherrypy
|
||||
from cherrypy.test import helper, webtest
|
||||
|
||||
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
|
||||
"""
|
||||
|
||||
|
||||
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.
|
||||
cherrypy._cpserver.wait_for_occupied_port('127.0.0.1', self.port)
|
||||
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):
|
||||
import cherrypy
|
||||
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)
|
|
@ -1,158 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
"""A session demonstration app."""
|
||||
|
||||
import calendar
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import cherrypy
|
||||
from cherrypy.lib import sessions
|
||||
from cherrypy._cpcompat import copyitems
|
||||
|
||||
|
||||
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'> </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>
|
||||
"""
|
||||
|
||||
|
||||
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': copyitems(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())
|
|
@ -1,5 +0,0 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>I couldn't find that thing you were looking for!</h1>
|
||||
</body>
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 18 KiB |
|
@ -1 +0,0 @@
|
|||
Hello, world
|
|
@ -1 +0,0 @@
|
|||
Dummy stylesheet
|
|
@ -1,38 +0,0 @@
|
|||
-----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-----
|
|
@ -1,95 +0,0 @@
|
|||
# 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)
|
||||
|
||||
userpassdict = {'xuser': 'xpassword'}
|
||||
userhashdict = {'xuser': md5(ntob('xpassword')).hexdigest()}
|
||||
|
||||
def checkpasshash(realm, user, password):
|
||||
p = userhashdict.get(user)
|
||||
return p and p == md5(ntob(password)).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
|
||||
},
|
||||
}
|
||||
|
||||
root = Root()
|
||||
root.basic = BasicProtected()
|
||||
root.basic2 = BasicProtected2()
|
||||
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"')
|
||||
|
||||
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.")
|
|
@ -1,139 +0,0 @@
|
|||
# This file is part of CherryPy <http://www.cherrypy.org/>
|
||||
# -*- coding: utf-8 -*-
|
||||
# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
|
||||
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.lib import auth_digest
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
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):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
def fetch_users():
|
||||
return {'test': 'test'}
|
||||
|
||||
get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users())
|
||||
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'}}
|
||||
|
||||
root = Root()
|
||||
root.digest = DigestProtected()
|
||||
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 testDigest(self):
|
||||
self.getPage('/digest/')
|
||||
self.assertStatus(401)
|
||||
|
||||
value = None
|
||||
for k, v in self.headers:
|
||||
if k.lower() == 'www-authenticate':
|
||||
if v.startswith('Digest'):
|
||||
value = v
|
||||
break
|
||||
|
||||
if value is None:
|
||||
self._handlewebError(
|
||||
'Digest authentification scheme was not found')
|
||||
|
||||
value = value[7:]
|
||||
items = value.split(', ')
|
||||
tokens = {}
|
||||
for item in items:
|
||||
key, value = item.split('=')
|
||||
tokens[key.lower()] = value
|
||||
|
||||
missing_msg = '%s is missing'
|
||||
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
|
||||
nonce = None
|
||||
if 'realm' not in tokens:
|
||||
self._handlewebError(missing_msg % 'realm')
|
||||
elif tokens['realm'] != '"localhost"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('realm', '"localhost"', tokens['realm']))
|
||||
if 'nonce' not in tokens:
|
||||
self._handlewebError(missing_msg % 'nonce')
|
||||
else:
|
||||
nonce = tokens['nonce'].strip('"')
|
||||
if 'algorithm' not in tokens:
|
||||
self._handlewebError(missing_msg % 'algorithm')
|
||||
elif tokens['algorithm'] != '"MD5"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('algorithm', '"MD5"', tokens['algorithm']))
|
||||
if 'qop' not in tokens:
|
||||
self._handlewebError(missing_msg % 'qop')
|
||||
elif tokens['qop'] != '"auth"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('qop', '"auth"', tokens['qop']))
|
||||
|
||||
get_ha1 = auth_digest.get_ha1_dict_plain({'test': 'test'})
|
||||
|
||||
# Test user agent response with a wrong value for 'realm'
|
||||
base_auth = ('Digest username="test", '
|
||||
'realm="wrong realm", '
|
||||
'nonce="%s", '
|
||||
'uri="/digest/", '
|
||||
'algorithm=MD5, '
|
||||
'response="%s", '
|
||||
'qop=auth, '
|
||||
'nc=%s, '
|
||||
'cnonce="1522e61005789929"')
|
||||
|
||||
auth_header = base_auth % (
|
||||
nonce, '11111111111111111111111111111111', '00000001')
|
||||
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
|
||||
# calculate the response digest
|
||||
ha1 = get_ha1(auth.realm, 'test')
|
||||
response = auth.request_digest(ha1)
|
||||
# send response with correct response digest, but wrong realm
|
||||
auth_header = base_auth % (nonce, response, '00000001')
|
||||
self.getPage('/digest/', [('Authorization', auth_header)])
|
||||
self.assertStatus(401)
|
||||
|
||||
# Test that must pass
|
||||
base_auth = ('Digest username="test", '
|
||||
'realm="localhost", '
|
||||
'nonce="%s", '
|
||||
'uri="/digest/", '
|
||||
'algorithm=MD5, '
|
||||
'response="%s", '
|
||||
'qop=auth, '
|
||||
'nc=%s, '
|
||||
'cnonce="1522e61005789929"')
|
||||
|
||||
auth_header = base_auth % (
|
||||
nonce, '11111111111111111111111111111111', '00000001')
|
||||
auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
|
||||
# calculate the response digest
|
||||
ha1 = get_ha1('localhost', 'test')
|
||||
response = auth.request_digest(ha1)
|
||||
# send response with correct response digest
|
||||
auth_header = base_auth % (nonce, response, '00000001')
|
||||
self.getPage('/digest/', [('Authorization', auth_header)])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello test, you've been authorized.")
|
|
@ -1,274 +0,0 @@
|
|||
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()
|
|
@ -1,342 +0,0 @@
|
|||
import datetime
|
||||
from itertools import count
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import next, ntob, quote, xrange
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
gif_bytes = ntob(
|
||||
'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
|
||||
'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
|
||||
'\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.tree.mount(Root())
|
||||
cherrypy.tree.mount(UnCached(), '/expires')
|
||||
cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
|
||||
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), ntob('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), ntob('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 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!')
|
||||
self.getPage('/clear_cache?path=' + quote(slow_url, safe=''))
|
||||
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 xrange(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')
|
|
@ -1,31 +0,0 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
import nose
|
||||
|
||||
from cherrypy import _cpcompat as compat
|
||||
|
||||
|
||||
class StringTester(unittest.TestCase):
|
||||
|
||||
def test_ntob_non_native(self):
|
||||
"""
|
||||
ntob should raise an Exception on unicode.
|
||||
(Python 2 only)
|
||||
|
||||
See #1132 for discussion.
|
||||
"""
|
||||
if six.PY3:
|
||||
raise nose.SkipTest('Only useful on Python 2')
|
||||
self.assertRaises(Exception, 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&<>"aa'""", compat.escape_html("""xx&<>"aa'"""))
|
|
@ -1,304 +0,0 @@
|
|||
"""Tests for the CherryPy configuration system."""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
import cherrypy._cpcompat as compat
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
StringIOFromNative = lambda x: 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(unicode('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=compat.ntob('\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
|
|
@ -1,126 +0,0 @@
|
|||
"""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)
|
|
@ -1,862 +0,0 @@
|
|||
"""Tests for TCP connection handling, including proper and timely close."""
|
||||
|
||||
import errno
|
||||
import socket
|
||||
import sys
|
||||
import time
|
||||
|
||||
import six
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import (
|
||||
HTTPConnection, HTTPSConnection,
|
||||
NotConnected, BadStatusLine,
|
||||
ntob, tonative,
|
||||
urlopen,
|
||||
)
|
||||
from cherrypy.test import helper, webtest
|
||||
|
||||
|
||||
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(ntob('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(ntob('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(ntob('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:
|
||||
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(ntob('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:
|
||||
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(ntob('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 ``reponse`` 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, ntob('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, ntob('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 = ntob('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(ntob('POST /upload HTTP/1.1'))
|
||||
conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
|
||||
conn._output(ntob('Content-Type: text/plain'))
|
||||
conn._output(ntob('Content-Length: 17'))
|
||||
conn._output(ntob('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 = ntob('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'" % ntob('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):
|
||||
remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' %
|
||||
(self.scheme, self.HOST, self.PORT,))
|
||||
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(ntob('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(ntob('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(ntob('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()
|
|
@ -1,782 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
"""Basic tests for the CherryPy core: request handling."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import itervalues, ntob, 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 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 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_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 ntob('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=[
|
||||
(ntob('WWW-Authenticate'), ntob('Negotiate')),
|
||||
(ntob('WWW-Authenticate'), ntob('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 as e:
|
||||
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_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())
|
||||
|
||||
# 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())
|
||||
|
||||
# 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):
|
||||
st = 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
|
|
@ -1,424 +0,0 @@
|
|||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import sorted
|
||||
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 unicode(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:
|
||||
prefix = 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')
|
|
@ -1,414 +0,0 @@
|
|||
# coding: utf-8
|
||||
|
||||
import gzip
|
||||
import io
|
||||
from unittest import mock
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import IncompleteRead, 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'
|
||||
return 'Any content'
|
||||
|
||||
@cherrypy.expose
|
||||
def reqparams(self, *args, **kwargs):
|
||||
return ntob(', ').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):
|
||||
if six.PY3:
|
||||
# This test fails on Python 3. See #1443
|
||||
return
|
||||
europoundUtf8 = europoundUnicode.encode('utf-8')
|
||||
self.getPage(ntob('/?param=') + europoundUtf8)
|
||||
self.assertBody(europoundUtf8)
|
||||
|
||||
# Encoded utf8 query strings MUST be parsed correctly.
|
||||
# Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
|
||||
self.getPage('/reqparams?q=%C2%A3')
|
||||
# The return value will be encoded as utf8.
|
||||
self.assertBody(ntob('q: \xc2\xa3'))
|
||||
|
||||
# Query strings that are incorrectly encoded MUST raise 404.
|
||||
# Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX
|
||||
self.getPage('/reqparams?q=%A3')
|
||||
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 = ntob('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 = ntob('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(ntob('q: \xc2\xa3'))
|
||||
|
||||
# ...and in utf16, which is not in the default attempt_charsets list:
|
||||
body = ntob('\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(ntob('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 = ntob('\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 = ntob('\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(ntob('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 = ntob('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(ntob('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 = ntob('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(ntob('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(ntob('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')
|
||||
|
||||
def testGzip(self):
|
||||
zbuf = io.BytesIO()
|
||||
zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
|
||||
zfile.write(ntob('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')
|
||||
|
||||
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')
|
|
@ -1,84 +0,0 @@
|
|||
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)
|
|
@ -1,260 +0,0 @@
|
|||
"""Tests for managing HTTP issues (malformed requests, etc)."""
|
||||
|
||||
import errno
|
||||
import mimetypes
|
||||
import socket
|
||||
import sys
|
||||
from unittest import mock
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
|
||||
L = []
|
||||
for key, filename, value in files:
|
||||
L.append('--' + BOUNDARY)
|
||||
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' %
|
||||
(key, filename))
|
||||
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(ntob('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)
|
||||
self.assertBody(', '.join(['%s * 65536' % c for c in alphabet]))
|
||||
|
||||
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'''
|
||||
# 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']
|
||||
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(ntob('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), ntob('Malformed Request-Line'))
|
||||
c.close()
|
||||
|
||||
def test_request_line_split_issue_1220(self):
|
||||
Request_URI = (
|
||||
'/index?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'
|
||||
)
|
||||
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(ntob('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(ntob('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),
|
||||
ntob('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
|
|
@ -1,195 +0,0 @@
|
|||
from hashlib import md5, sha1
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob
|
||||
from cherrypy.lib import httpauth
|
||||
|
||||
from cherrypy.test import helper
|
||||
|
||||
|
||||
class HTTPAuthTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
class Root:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return 'This is public.'
|
||||
|
||||
class DigestProtected:
|
||||
|
||||
@cherrypy.expose
|
||||
def index(self):
|
||||
return "Hello %s, you've been authorized." % (
|
||||
cherrypy.request.login)
|
||||
|
||||
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)
|
||||
|
||||
def fetch_users():
|
||||
return {'test': 'test'}
|
||||
|
||||
def sha_password_encrypter(password):
|
||||
return sha1(ntob(password)).hexdigest()
|
||||
|
||||
def fetch_password(username):
|
||||
return sha1(ntob('test')).hexdigest()
|
||||
|
||||
conf = {
|
||||
'/digest': {
|
||||
'tools.digest_auth.on': True,
|
||||
'tools.digest_auth.realm': 'localhost',
|
||||
'tools.digest_auth.users': fetch_users
|
||||
},
|
||||
'/basic': {
|
||||
'tools.basic_auth.on': True,
|
||||
'tools.basic_auth.realm': 'localhost',
|
||||
'tools.basic_auth.users': {
|
||||
'test': md5(ntob('test')).hexdigest()
|
||||
}
|
||||
},
|
||||
'/basic2': {
|
||||
'tools.basic_auth.on': True,
|
||||
'tools.basic_auth.realm': 'localhost',
|
||||
'tools.basic_auth.users': fetch_password,
|
||||
'tools.basic_auth.encrypt': sha_password_encrypter
|
||||
}
|
||||
}
|
||||
|
||||
root = Root()
|
||||
root.digest = DigestProtected()
|
||||
root.basic = BasicProtected()
|
||||
root.basic2 = BasicProtected2()
|
||||
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="localhost"')
|
||||
|
||||
self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')])
|
||||
self.assertStatus(401)
|
||||
|
||||
self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello test, you've been authorized.")
|
||||
|
||||
def testBasic2(self):
|
||||
self.getPage('/basic2/')
|
||||
self.assertStatus(401)
|
||||
self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"')
|
||||
|
||||
self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')])
|
||||
self.assertStatus(401)
|
||||
|
||||
self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello test, you've been authorized.")
|
||||
|
||||
def testDigest(self):
|
||||
self.getPage('/digest/')
|
||||
self.assertStatus(401)
|
||||
|
||||
value = None
|
||||
for k, v in self.headers:
|
||||
if k.lower() == 'www-authenticate':
|
||||
if v.startswith('Digest'):
|
||||
value = v
|
||||
break
|
||||
|
||||
if value is None:
|
||||
self._handlewebError(
|
||||
'Digest authentification scheme was not found')
|
||||
|
||||
value = value[7:]
|
||||
items = value.split(', ')
|
||||
tokens = {}
|
||||
for item in items:
|
||||
key, value = item.split('=')
|
||||
tokens[key.lower()] = value
|
||||
|
||||
missing_msg = '%s is missing'
|
||||
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
|
||||
nonce = None
|
||||
if 'realm' not in tokens:
|
||||
self._handlewebError(missing_msg % 'realm')
|
||||
elif tokens['realm'] != '"localhost"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('realm', '"localhost"', tokens['realm']))
|
||||
if 'nonce' not in tokens:
|
||||
self._handlewebError(missing_msg % 'nonce')
|
||||
else:
|
||||
nonce = tokens['nonce'].strip('"')
|
||||
if 'algorithm' not in tokens:
|
||||
self._handlewebError(missing_msg % 'algorithm')
|
||||
elif tokens['algorithm'] != '"MD5"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('algorithm', '"MD5"', tokens['algorithm']))
|
||||
if 'qop' not in tokens:
|
||||
self._handlewebError(missing_msg % 'qop')
|
||||
elif tokens['qop'] != '"auth"':
|
||||
self._handlewebError(bad_value_msg %
|
||||
('qop', '"auth"', tokens['qop']))
|
||||
|
||||
# Test a wrong 'realm' value
|
||||
base_auth = (
|
||||
'Digest '
|
||||
'username="test", '
|
||||
'realm="wrong realm", '
|
||||
'nonce="%s", '
|
||||
'uri="/digest/", '
|
||||
'algorithm=MD5, '
|
||||
'response="%s", '
|
||||
'qop=auth, '
|
||||
'nc=%s, '
|
||||
'cnonce="1522e61005789929"'
|
||||
)
|
||||
|
||||
auth = base_auth % (nonce, '', '00000001')
|
||||
params = httpauth.parseAuthorization(auth)
|
||||
response = httpauth._computeDigestResponse(params, 'test')
|
||||
|
||||
auth = base_auth % (nonce, response, '00000001')
|
||||
self.getPage('/digest/', [('Authorization', auth)])
|
||||
self.assertStatus(401)
|
||||
|
||||
# Test that must pass
|
||||
base_auth = (
|
||||
'Digest '
|
||||
'username="test", '
|
||||
'realm="localhost", '
|
||||
'nonce="%s", '
|
||||
'uri="/digest/", '
|
||||
'algorithm=MD5, '
|
||||
'response="%s", '
|
||||
'qop=auth, '
|
||||
'nc=%s, '
|
||||
'cnonce="1522e61005789929"'
|
||||
)
|
||||
|
||||
auth = base_auth % (nonce, '', '00000001')
|
||||
params = httpauth.parseAuthorization(auth)
|
||||
response = httpauth._computeDigestResponse(params, 'test')
|
||||
|
||||
auth = base_auth % (nonce, response, '00000001')
|
||||
self.getPage('/digest/', [('Authorization', auth)])
|
||||
self.assertStatus('200 OK')
|
||||
self.assertBody("Hello test, you've been authorized.")
|
|
@ -1,29 +0,0 @@
|
|||
"""Tests for cherrypy/lib/httputil.py."""
|
||||
|
||||
import unittest
|
||||
from cherrypy.lib import httputil
|
||||
|
||||
|
||||
class UtilityTests(unittest.TestCase):
|
||||
|
||||
def test_urljoin(self):
|
||||
# Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO
|
||||
self.assertEqual(httputil.urljoin('/sn/', '/pi/'), '/sn/pi/')
|
||||
self.assertEqual(httputil.urljoin('/sn/', '/pi'), '/sn/pi')
|
||||
self.assertEqual(httputil.urljoin('/sn/', '/'), '/sn/')
|
||||
self.assertEqual(httputil.urljoin('/sn/', ''), '/sn/')
|
||||
self.assertEqual(httputil.urljoin('/sn', '/pi/'), '/sn/pi/')
|
||||
self.assertEqual(httputil.urljoin('/sn', '/pi'), '/sn/pi')
|
||||
self.assertEqual(httputil.urljoin('/sn', '/'), '/sn/')
|
||||
self.assertEqual(httputil.urljoin('/sn', ''), '/sn')
|
||||
self.assertEqual(httputil.urljoin('/', '/pi/'), '/pi/')
|
||||
self.assertEqual(httputil.urljoin('/', '/pi'), '/pi')
|
||||
self.assertEqual(httputil.urljoin('/', '/'), '/')
|
||||
self.assertEqual(httputil.urljoin('/', ''), '/')
|
||||
self.assertEqual(httputil.urljoin('', '/pi/'), '/pi/')
|
||||
self.assertEqual(httputil.urljoin('', '/pi'), '/pi')
|
||||
self.assertEqual(httputil.urljoin('', '/'), '/')
|
||||
self.assertEqual(httputil.urljoin('', ''), '/')
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
|
@ -1,189 +0,0 @@
|
|||
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':
|
||||
assert header_value == six.text_type(1024 * 16 * 256), 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)
|
||||
)
|
|
@ -1,101 +0,0 @@
|
|||
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"')
|
|
@ -1,177 +0,0 @@
|
|||
"""Basic tests for the CherryPy core: request handling."""
|
||||
|
||||
import os
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, 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())
|
||||
|
||||
def testCustomLogFormat(self):
|
||||
'''Test a customized access_log_format string,
|
||||
which is a feature of _cplogging.LogManager.access() '''
|
||||
|
||||
original_logformat = cherrypy._cplogging.LogManager.access_log_format
|
||||
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'
|
||||
|
||||
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')
|
||||
|
||||
cherrypy._cplogging.LogManager.access_log_format = original_logformat
|
||||
|
||||
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, ntob('"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()
|
|
@ -1,134 +0,0 @@
|
|||
"""Tests for various MIME issues, including the safe_multipart Tool."""
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import ntob, ntou, sorted
|
||||
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 = ntob('<?xml version="1.0" encoding="UTF-8"?>\r\n'
|
||||
'<projectDescription>\r\n'
|
||||
'</projectDescription>\r\n')
|
||||
body = (ntob(
|
||||
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
'Content-Disposition: form-data; name="Filename"\r\n'
|
||||
'\r\n'
|
||||
'.project\r\n'
|
||||
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
'Content-Disposition: form-data; '
|
||||
'name="Filedata"; filename=".project"\r\n'
|
||||
'Content-Type: application/octet-stream\r\n'
|
||||
'\r\n')
|
||||
+ filedata +
|
||||
ntob('\r\n'
|
||||
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
|
||||
'Content-Disposition: form-data; name="Upload"\r\n'
|
||||
'\r\n'
|
||||
'Submit Query\r\n'
|
||||
# Flash apps omit the trailing \r\n on the last line:
|
||||
'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
|
||||
))
|
||||
self.getPage('/flashupload', headers, 'POST', body)
|
||||
self.assertBody('Upload: Submit Query, Filename: .project, '
|
||||
'Filedata: %r' % filedata)
|
|
@ -1,215 +0,0 @@
|
|||
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'
|
||||
ae = cherrypy.request.headers.get('Accept-Encoding')
|
||||
# Read a header directly with '__getitem__'
|
||||
cl = cherrypy.request.headers['Host']
|
||||
# Read a header directly with '__contains__'
|
||||
hasif = 'If-Modified-Since' in cherrypy.request.headers
|
||||
# Read a header directly with 'has_key'
|
||||
if hasattr(dict, 'has_key'):
|
||||
# Python 2
|
||||
has = cherrypy.request.headers.has_key('Range')
|
||||
else:
|
||||
# Python 3
|
||||
has = 'Range' in cherrypy.request.headers
|
||||
# Call a lib function
|
||||
mtype = 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'
|
||||
)
|
|
@ -1,430 +0,0 @@
|
|||
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:
|
||||
prefix = 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 diferent 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:
|
||||
prefix = 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)
|
|
@ -1,60 +0,0 @@
|
|||
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.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')
|
|
@ -1,138 +0,0 @@
|
|||
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'))
|
||||
|
||||
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)
|
|
@ -1,64 +0,0 @@
|
|||
"""Tests for refleaks."""
|
||||
|
||||
import itertools
|
||||
import platform
|
||||
import threading
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import HTTPConnection, 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)
|
|
@ -1,861 +0,0 @@
|
|||
"""Basic tests for the cherrypy.Request object."""
|
||||
|
||||
from functools import wraps
|
||||
import os
|
||||
import sys
|
||||
import types
|
||||
|
||||
import six
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import IncompleteRead, ntob, 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
|
||||
|
||||
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 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¬athing=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¬athing=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¶m2=bar',
|
||||
'/paramerrors/one_positional_args_kwargs/foo?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_kwargs?'
|
||||
'param1=foo¶m2=bar¶m3=baz',
|
||||
'/paramerrors/one_positional_kwargs/foo?'
|
||||
'param4=foo¶m2=bar¶m3=baz',
|
||||
'/paramerrors/no_positional',
|
||||
'/paramerrors/no_positional_args/foo',
|
||||
'/paramerrors/no_positional_args/foo/bar/baz',
|
||||
'/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar',
|
||||
'/paramerrors/no_positional_args_kwargs/foo?param2=bar',
|
||||
'/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
|
||||
'param2=bar¶m3=baz',
|
||||
'/paramerrors/no_positional_kwargs?param1=foo¶m2=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¶m2=foo',
|
||||
error_msgs[2]),
|
||||
('/paramerrors/one_positional_args/foo?param1=foo¶m2=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¶m3=baz',
|
||||
error_msgs[2]),
|
||||
('/paramerrors/one_positional_kwargs/foo?'
|
||||
'param1=foo¶m2=bar¶m3=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¶m2=foo', error_msgs[2]),
|
||||
('/paramerrors/one_positional_args/foo',
|
||||
'param1=foo¶m2=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¶m3=baz', error_msgs[2]),
|
||||
('/paramerrors/one_positional_kwargs/foo',
|
||||
'param1=foo¶m2=bar¶m3=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¶m3=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, <b>really</b>, 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(ntob('\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(ntob('\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], ntob('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 = ntob('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 = ntob('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):
|
||||
if getattr(cherrypy.server, 'using_apache', False):
|
||||
return self.skip('skipped due to known Apache differences... ')
|
||||
|
||||
self.getPage('/method/', method='CONNECT')
|
||||
self.assertBody('CONNECT')
|
||||
|
||||
def testEmptyThreadlocals(self):
|
||||
results = []
|
||||
for x in range(20):
|
||||
self.getPage('/threadlocal/')
|
||||
results.append(self.body)
|
||||
self.assertEqual(results, [ntob('None')] * 20)
|
|
@ -1,76 +0,0 @@
|
|||
import os
|
||||
|
||||
import nose
|
||||
|
||||
import cherrypy
|
||||
from cherrypy.test import helper
|
||||
|
||||
curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
|
||||
|
||||
|
||||
class RoutesDispatchTest(helper.CPWebCase):
|
||||
|
||||
@staticmethod
|
||||
def setup_server():
|
||||
|
||||
try:
|
||||
import routes # noqa
|
||||
except ImportError:
|
||||
raise nose.SkipTest('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):
|
||||
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')
|
|
@ -1,503 +0,0 @@
|
|||
import os
|
||||
import threading
|
||||
import time
|
||||
import socket
|
||||
|
||||
import pytest
|
||||
|
||||
import cherrypy
|
||||
from cherrypy._cpcompat import (
|
||||
copykeys, json_decode,
|
||||
HTTPConnection, 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):
|
||||
os.unlink(os.path.join(localDir, fname))
|
||||
|
||||
@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')
|
||||
f = lambda: [
|
||||
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('/')
|
||||
f = lambda: [
|
||||
x for x in os.listdir(localDir) if x.startswith('session-')]
|
||||
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')
|
||||
|
||||
@pytest.mark.xfail(reason='#1540')
|
||||
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(copykeys(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(copykeys(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 = copykeys(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()
|
||||
assert len(sessions.RamSession.locks) == 1, 'Clean up should not remove active lock'
|
||||
t.join()
|
||||
|
||||
|
||||
try:
|
||||
import memcache # NOQA
|
||||
|
||||
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])
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue