1"""Checker for CherryPy sites and mounted apps.""" 2import os 3import warnings 4 5import six 6from six.moves import builtins 7 8import cherrypy 9 10 11class Checker(object): 12 """A checker for CherryPy sites and their mounted applications. 13 14 When this object is called at engine startup, it executes each 15 of its own methods whose names start with ``check_``. If you wish 16 to disable selected checks, simply add a line in your global 17 config which sets the appropriate method to False:: 18 19 [global] 20 checker.check_skipped_app_config = False 21 22 You may also dynamically add or replace ``check_*`` methods in this way. 23 """ 24 25 on = True 26 """If True (the default), run all checks; if False, turn off all checks.""" 27 28 def __init__(self): 29 """Initialize Checker instance.""" 30 self._populate_known_types() 31 32 def __call__(self): 33 """Run all check_* methods.""" 34 if self.on: 35 oldformatwarning = warnings.formatwarning 36 warnings.formatwarning = self.formatwarning 37 try: 38 for name in dir(self): 39 if name.startswith('check_'): 40 method = getattr(self, name) 41 if method and hasattr(method, '__call__'): 42 method() 43 finally: 44 warnings.formatwarning = oldformatwarning 45 46 def formatwarning(self, message, category, filename, lineno, line=None): 47 """Format a warning.""" 48 return 'CherryPy Checker:\n%s\n\n' % message 49 50 # This value should be set inside _cpconfig. 51 global_config_contained_paths = False 52 53 def check_app_config_entries_dont_start_with_script_name(self): 54 """Check for App config with sections that repeat script_name.""" 55 for sn, app in cherrypy.tree.apps.items(): 56 if not isinstance(app, cherrypy.Application): 57 continue 58 if not app.config: 59 continue 60 if sn == '': 61 continue 62 sn_atoms = sn.strip('/').split('/') 63 for key in app.config.keys(): 64 key_atoms = key.strip('/').split('/') 65 if key_atoms[:len(sn_atoms)] == sn_atoms: 66 warnings.warn( 67 'The application mounted at %r has config ' 68 'entries that start with its script name: %r' % (sn, 69 key)) 70 71 def check_site_config_entries_in_app_config(self): 72 """Check for mounted Applications that have site-scoped config.""" 73 for sn, app in six.iteritems(cherrypy.tree.apps): 74 if not isinstance(app, cherrypy.Application): 75 continue 76 77 msg = [] 78 for section, entries in six.iteritems(app.config): 79 if section.startswith('/'): 80 for key, value in six.iteritems(entries): 81 for n in ('engine.', 'server.', 'tree.', 'checker.'): 82 if key.startswith(n): 83 msg.append('[%s] %s = %s' % 84 (section, key, value)) 85 if msg: 86 msg.insert(0, 87 'The application mounted at %r contains the ' 88 'following config entries, which are only allowed ' 89 'in site-wide config. Move them to a [global] ' 90 'section and pass them to cherrypy.config.update() ' 91 'instead of tree.mount().' % sn) 92 warnings.warn(os.linesep.join(msg)) 93 94 def check_skipped_app_config(self): 95 """Check for mounted Applications that have no config.""" 96 for sn, app in cherrypy.tree.apps.items(): 97 if not isinstance(app, cherrypy.Application): 98 continue 99 if not app.config: 100 msg = 'The Application mounted at %r has an empty config.' % sn 101 if self.global_config_contained_paths: 102 msg += (' It looks like the config you passed to ' 103 'cherrypy.config.update() contains application-' 104 'specific sections. You must explicitly pass ' 105 'application config via ' 106 'cherrypy.tree.mount(..., config=app_config)') 107 warnings.warn(msg) 108 return 109 110 def check_app_config_brackets(self): 111 """Check for App config with extraneous brackets in section names.""" 112 for sn, app in cherrypy.tree.apps.items(): 113 if not isinstance(app, cherrypy.Application): 114 continue 115 if not app.config: 116 continue 117 for key in app.config.keys(): 118 if key.startswith('[') or key.endswith(']'): 119 warnings.warn( 120 'The application mounted at %r has config ' 121 'section names with extraneous brackets: %r. ' 122 'Config *files* need brackets; config *dicts* ' 123 '(e.g. passed to tree.mount) do not.' % (sn, key)) 124 125 def check_static_paths(self): 126 """Check Application config for incorrect static paths.""" 127 # Use the dummy Request object in the main thread. 128 request = cherrypy.request 129 for sn, app in cherrypy.tree.apps.items(): 130 if not isinstance(app, cherrypy.Application): 131 continue 132 request.app = app 133 for section in app.config: 134 # get_resource will populate request.config 135 request.get_resource(section + '/dummy.html') 136 conf = request.config.get 137 138 if conf('tools.staticdir.on', False): 139 msg = '' 140 root = conf('tools.staticdir.root') 141 dir = conf('tools.staticdir.dir') 142 if dir is None: 143 msg = 'tools.staticdir.dir is not set.' 144 else: 145 fulldir = '' 146 if os.path.isabs(dir): 147 fulldir = dir 148 if root: 149 msg = ('dir is an absolute path, even ' 150 'though a root is provided.') 151 testdir = os.path.join(root, dir[1:]) 152 if os.path.exists(testdir): 153 msg += ( 154 '\nIf you meant to serve the ' 155 'filesystem folder at %r, remove the ' 156 'leading slash from dir.' % (testdir,)) 157 else: 158 if not root: 159 msg = ( 160 'dir is a relative path and ' 161 'no root provided.') 162 else: 163 fulldir = os.path.join(root, dir) 164 if not os.path.isabs(fulldir): 165 msg = ('%r is not an absolute path.' % ( 166 fulldir,)) 167 168 if fulldir and not os.path.exists(fulldir): 169 if msg: 170 msg += '\n' 171 msg += ('%r (root + dir) is not an existing ' 172 'filesystem path.' % fulldir) 173 174 if msg: 175 warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r' 176 % (msg, section, root, dir)) 177 178 # -------------------------- Compatibility -------------------------- # 179 obsolete = { 180 'server.default_content_type': 'tools.response_headers.headers', 181 'log_access_file': 'log.access_file', 182 'log_config_options': None, 183 'log_file': 'log.error_file', 184 'log_file_not_found': None, 185 'log_request_headers': 'tools.log_headers.on', 186 'log_to_screen': 'log.screen', 187 'show_tracebacks': 'request.show_tracebacks', 188 'throw_errors': 'request.throw_errors', 189 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' 190 'cherrypy.Application(Root())))'), 191 } 192 193 deprecated = {} 194 195 def _compat(self, config): 196 """Process config and warn on each obsolete or deprecated entry.""" 197 for section, conf in config.items(): 198 if isinstance(conf, dict): 199 for k in conf: 200 if k in self.obsolete: 201 warnings.warn('%r is obsolete. Use %r instead.\n' 202 'section: [%s]' % 203 (k, self.obsolete[k], section)) 204 elif k in self.deprecated: 205 warnings.warn('%r is deprecated. Use %r instead.\n' 206 'section: [%s]' % 207 (k, self.deprecated[k], section)) 208 else: 209 if section in self.obsolete: 210 warnings.warn('%r is obsolete. Use %r instead.' 211 % (section, self.obsolete[section])) 212 elif section in self.deprecated: 213 warnings.warn('%r is deprecated. Use %r instead.' 214 % (section, self.deprecated[section])) 215 216 def check_compatibility(self): 217 """Process config and warn on each obsolete or deprecated entry.""" 218 self._compat(cherrypy.config) 219 for sn, app in cherrypy.tree.apps.items(): 220 if not isinstance(app, cherrypy.Application): 221 continue 222 self._compat(app.config) 223 224 # ------------------------ Known Namespaces ------------------------ # 225 extra_config_namespaces = [] 226 227 def _known_ns(self, app): 228 ns = ['wsgi'] 229 ns.extend(app.toolboxes) 230 ns.extend(app.namespaces) 231 ns.extend(app.request_class.namespaces) 232 ns.extend(cherrypy.config.namespaces) 233 ns += self.extra_config_namespaces 234 235 for section, conf in app.config.items(): 236 is_path_section = section.startswith('/') 237 if is_path_section and isinstance(conf, dict): 238 for k in conf: 239 atoms = k.split('.') 240 if len(atoms) > 1: 241 if atoms[0] not in ns: 242 # Spit out a special warning if a known 243 # namespace is preceded by "cherrypy." 244 if atoms[0] == 'cherrypy' and atoms[1] in ns: 245 msg = ( 246 'The config entry %r is invalid; ' 247 'try %r instead.\nsection: [%s]' 248 % (k, '.'.join(atoms[1:]), section)) 249 else: 250 msg = ( 251 'The config entry %r is invalid, ' 252 'because the %r config namespace ' 253 'is unknown.\n' 254 'section: [%s]' % (k, atoms[0], section)) 255 warnings.warn(msg) 256 elif atoms[0] == 'tools': 257 if atoms[1] not in dir(cherrypy.tools): 258 msg = ( 259 'The config entry %r may be invalid, ' 260 'because the %r tool was not found.\n' 261 'section: [%s]' % (k, atoms[1], section)) 262 warnings.warn(msg) 263 264 def check_config_namespaces(self): 265 """Process config and warn on each unknown config namespace.""" 266 for sn, app in cherrypy.tree.apps.items(): 267 if not isinstance(app, cherrypy.Application): 268 continue 269 self._known_ns(app) 270 271 # -------------------------- Config Types -------------------------- # 272 known_config_types = {} 273 274 def _populate_known_types(self): 275 b = [x for x in vars(builtins).values() 276 if type(x) is type(str)] 277 278 def traverse(obj, namespace): 279 for name in dir(obj): 280 # Hack for 3.2's warning about body_params 281 if name == 'body_params': 282 continue 283 vtype = type(getattr(obj, name, None)) 284 if vtype in b: 285 self.known_config_types[namespace + '.' + name] = vtype 286 287 traverse(cherrypy.request, 'request') 288 traverse(cherrypy.response, 'response') 289 traverse(cherrypy.server, 'server') 290 traverse(cherrypy.engine, 'engine') 291 traverse(cherrypy.log, 'log') 292 293 def _known_types(self, config): 294 msg = ('The config entry %r in section %r is of type %r, ' 295 'which does not match the expected type %r.') 296 297 for section, conf in config.items(): 298 if not isinstance(conf, dict): 299 conf = {section: conf} 300 for k, v in conf.items(): 301 if v is not None: 302 expected_type = self.known_config_types.get(k, None) 303 vtype = type(v) 304 if expected_type and vtype != expected_type: 305 warnings.warn(msg % (k, section, vtype.__name__, 306 expected_type.__name__)) 307 308 def check_config_types(self): 309 """Assert that config values are of the same type as default values.""" 310 self._known_types(cherrypy.config) 311 for sn, app in cherrypy.tree.apps.items(): 312 if not isinstance(app, cherrypy.Application): 313 continue 314 self._known_types(app.config) 315 316 # -------------------- Specific config warnings -------------------- # 317 def check_localhost(self): 318 """Warn if any socket_host is 'localhost'. See #711.""" 319 for k, v in cherrypy.config.items(): 320 if k == 'server.socket_host' and v == 'localhost': 321 warnings.warn("The use of 'localhost' as a socket host can " 322 'cause problems on newer systems, since ' 323 "'localhost' can map to either an IPv4 or an " 324 "IPv6 address. You should use '127.0.0.1' " 325 "or '[::1]' instead.") 326