Web   ·   Wiki   ·   Activities   ·   Blog   ·   Lists   ·   Chat   ·   Meeting   ·   Bugs   ·   Git   ·   Translate   ·   Archive   ·   People   ·   Donate
summaryrefslogtreecommitdiffstats
path: root/cherrypy/_cpchecker.py
blob: 7ccfd89dc23702d854bb89e40c94180ecc120790 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
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.")