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