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