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.")