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