You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
333 lines
15 KiB
333 lines
15 KiB
5 years ago
|
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.")
|