1 """CherryPy dispatchers.
3 A 'dispatcher' is the object which looks up the 'page handler' callable
4 and collects config for the current request based on the path_info, other
5 request attributes, and the application architecture. The core calls the
6 dispatcher as early as possible, passing it a 'path_info' argument.
8 The default dispatcher discovers the page handler by matching path_info
9 to a hierarchical arrangement of objects, starting at request.app.root.
19 class PageHandler(object):
20 """Callable which sets response.body."""
22 def __init__(self
, callable, *args
, **kwargs
):
23 self
.callable = callable
29 return self
.callable(*self
.args
, **self
.kwargs
)
33 test_callable_spec(self
.callable, self
.args
, self
.kwargs
)
34 except cherrypy
.HTTPError
:
35 raise sys
.exc_info()[1]
41 def test_callable_spec(callable, callable_args
, callable_kwargs
):
43 Inspect callable and test to see if the given args are suitable for it.
45 When an error occurs during the handler's invoking stage there are 2
47 1. Too many parameters passed to a function which doesn't define
48 one of *args or **kwargs.
49 2. Too little parameters are passed to the function.
51 There are 3 sources of parameters to a cherrypy handler.
52 1. query string parameters are passed as keyword parameters to the handler.
53 2. body parameters are also passed as keyword parameters.
54 3. when partial matching occurs, the final path atoms are passed as
56 Both the query string and path atoms are part of the URI. If they are
57 incorrect, then a 404 Not Found should be raised. Conversely the body
58 parameters are part of the request; if they are invalid a 400 Bad Request.
60 show_mismatched_params
= getattr(
61 cherrypy
.serving
.request
, 'show_mismatched_params', False)
63 (args
, varargs
, varkw
, defaults
) = inspect
.getargspec(callable)
65 if isinstance(callable, object) and hasattr(callable, '__call__'):
66 (args
, varargs
, varkw
, defaults
) = inspect
.getargspec(callable.__call
__)
68 # If it wasn't one of our own types, re-raise
72 if args
and args
[0] == 'self':
75 arg_usage
= dict([(arg
, 0,) for arg
in args
])
80 for i
, value
in enumerate(callable_args
):
82 arg_usage
[args
[i
]] += 1
86 for key
in callable_kwargs
.keys():
93 # figure out which args have defaults.
94 args_with_defaults
= args
[-len(defaults
or []):]
95 for i
, val
in enumerate(defaults
or []):
96 # Defaults take effect only when the arg hasn't been used yet.
97 if arg_usage
[args_with_defaults
[i
]] == 0:
98 arg_usage
[args_with_defaults
[i
]] += 1
102 for key
, usage
in arg_usage
.items():
104 missing_args
.append(key
)
106 multiple_args
.append(key
)
109 # In the case where the method allows body arguments
110 # there are 3 potential errors:
111 # 1. not enough query string parameters -> 404
112 # 2. not enough body parameters -> 400
113 # 3. not enough path parts (partial matches) -> 404
115 # We can't actually tell which case it is,
116 # so I'm raising a 404 because that covers 2/3 of the
119 # In the case where the method does not allow body
120 # arguments it's definitely a 404.
122 if show_mismatched_params
:
123 message
="Missing parameters: %s" % ",".join(missing_args
)
124 raise cherrypy
.HTTPError(404, message
=message
)
126 # the extra positional arguments come from the path - 404 Not Found
127 if not varargs
and vararg_usage
> 0:
128 raise cherrypy
.HTTPError(404)
130 body_params
= cherrypy
.serving
.request
.body
.params
or {}
131 body_params
= set(body_params
.keys())
132 qs_params
= set(callable_kwargs
.keys()) - body_params
135 if qs_params
.intersection(set(multiple_args
)):
136 # If any of the multiple parameters came from the query string then
137 # it's a 404 Not Found
140 # Otherwise it's a 400 Bad Request
144 if show_mismatched_params
:
145 message
="Multiple values for parameters: "\
146 "%s" % ",".join(multiple_args
)
147 raise cherrypy
.HTTPError(error
, message
=message
)
149 if not varkw
and varkw_usage
> 0:
151 # If there were extra query string parameters, it's a 404 Not Found
152 extra_qs_params
= set(qs_params
).intersection(extra_kwargs
)
155 if show_mismatched_params
:
156 message
="Unexpected query string "\
157 "parameters: %s" % ", ".join(extra_qs_params
)
158 raise cherrypy
.HTTPError(404, message
=message
)
160 # If there were any extra body parameters, it's a 400 Not Found
161 extra_body_params
= set(body_params
).intersection(extra_kwargs
)
162 if extra_body_params
:
164 if show_mismatched_params
:
165 message
="Unexpected body parameters: "\
166 "%s" % ", ".join(extra_body_params
)
167 raise cherrypy
.HTTPError(400, message
=message
)
173 test_callable_spec
= lambda callable, args
, kwargs
: None
177 class LateParamPageHandler(PageHandler
):
178 """When passing cherrypy.request.params to the page handler, we do not
179 want to capture that dict too early; we want to give tools like the
180 decoding tool a chance to modify the params dict in-between the lookup
181 of the handler and the actual calling of the handler. This subclass
182 takes that into account, and allows request.params to be 'bound late'
183 (it's more complicated than that, but that's the effect).
186 def _get_kwargs(self
):
187 kwargs
= cherrypy
.serving
.request
.params
.copy()
189 kwargs
.update(self
._kwargs
)
192 def _set_kwargs(self
, kwargs
):
193 self
._kwargs
= kwargs
195 kwargs
= property(_get_kwargs
, _set_kwargs
,
196 doc
='page handler kwargs (with '
197 'cherrypy.request.params copied in)')
200 punctuation_to_underscores
= string
.maketrans(
201 string
.punctuation
, '_' * len(string
.punctuation
))
203 class Dispatcher(object):
204 """CherryPy Dispatcher which walks a tree of objects to find a handler.
206 The tree is rooted at cherrypy.request.app.root, and each hierarchical
207 component in the path_info argument is matched to a corresponding nested
208 attribute of the root object. Matching handlers must have an 'exposed'
209 attribute which evaluates to True. The special method name "index"
210 matches a URI which ends in a slash ("/"). The special method name
211 "default" may match a portion of the path_info (but only when no longer
212 substring of the path_info matches some other object).
214 This is the default, built-in dispatcher for CherryPy.
217 dispatch_method_name
= '_cp_dispatch'
219 The name of the dispatch method that nodes may optionally implement
220 to provide their own dynamic dispatch algorithm.
223 def __init__(self
, dispatch_method_name
=None,
224 translate
=punctuation_to_underscores
):
225 if not isinstance(translate
, str) or len(translate
) != 256:
226 raise ValueError("The translate argument must be a str of len 256.")
227 self
.translate
= translate
228 if dispatch_method_name
:
229 self
.dispatch_method_name
= dispatch_method_name
231 def __call__(self
, path_info
):
232 """Set handler and config for the current request."""
233 request
= cherrypy
.serving
.request
234 func
, vpath
= self
.find_handler(path_info
)
237 # Decode any leftover %2F in the virtual_path atoms.
238 vpath
= [x
.replace("%2F", "/") for x
in vpath
]
239 request
.handler
= LateParamPageHandler(func
, *vpath
)
241 request
.handler
= cherrypy
.NotFound()
243 def find_handler(self
, path
):
244 """Return the appropriate page handler, plus any virtual path.
246 This will return two objects. The first will be a callable,
247 which can be used to generate page output. Any parameters from
248 the query string or request body will be sent to that callable
249 as keyword arguments.
251 The callable is found by traversing the application's tree,
252 starting from cherrypy.request.app.root, and matching path
253 components to successive objects in the tree. For example, the
254 URL "/path/to/handler" might return root.path.to.handler.
256 The second object returned will be a list of names which are
257 'virtual path' components: parts of the URL which are dynamic,
258 and were not used when looking up the handler.
259 These virtual path components are passed to the handler as
260 positional arguments.
262 request
= cherrypy
.serving
.request
265 dispatch_name
= self
.dispatch_method_name
267 # Get config for the root object/path.
268 fullpath
= [x
for x
in path
.strip('/').split('/') if x
] + ['index']
269 fullpath_len
= len(fullpath
)
270 segleft
= fullpath_len
272 if hasattr(root
, "_cp_config"):
273 nodeconf
.update(root
._cp
_config
)
274 if "/" in app
.config
:
275 nodeconf
.update(app
.config
["/"])
276 object_trail
= [['root', root
, nodeconf
, segleft
]]
279 iternames
= fullpath
[:]
282 # map to legal Python identifiers (e.g. replace '.' with '_')
283 objname
= name
.translate(self
.translate
)
286 subnode
= getattr(node
, objname
, None)
287 pre_len
= len(iternames
)
289 dispatch
= getattr(node
, dispatch_name
, None)
290 if dispatch
and hasattr(dispatch
, '__call__') and not \
291 getattr(dispatch
, 'exposed', False) and \
293 #Don't expose the hidden 'index' token to _cp_dispatch
294 #We skip this if pre_len == 1 since it makes no sense
295 #to call a dispatcher when we have no tokens left.
296 index_name
= iternames
.pop()
297 subnode
= dispatch(vpath
=iternames
)
298 iternames
.append(index_name
)
300 #We didn't find a path, but keep processing in case there
301 #is a default() handler.
304 #We found the path, remove the vpath entry
306 segleft
= len(iternames
)
307 if segleft
> pre_len
:
308 #No path segment was removed. Raise an error.
309 raise cherrypy
.CherryPyException(
310 "A vpath segment was added. Custom dispatchers may only "
311 + "remove elements. While trying to process "
312 + "{0} in {1}".format(name
, fullpath
)
314 elif segleft
== pre_len
:
315 #Assume that the handler used the current path segment, but
316 #did not pop it. This allows things like
317 #return getattr(self, vpath[0], None)
323 # Get _cp_config attached to this node.
324 if hasattr(node
, "_cp_config"):
325 nodeconf
.update(node
._cp
_config
)
327 # Mix in values from app.config for this path.
328 existing_len
= fullpath_len
- pre_len
329 if existing_len
!= 0:
330 curpath
= '/' + '/'.join(fullpath
[0:existing_len
])
333 new_segs
= fullpath
[fullpath_len
- pre_len
:fullpath_len
- segleft
]
336 if curpath
in app
.config
:
337 nodeconf
.update(app
.config
[curpath
])
339 object_trail
.append([name
, node
, nodeconf
, segleft
])
342 """Collapse all object_trail config into cherrypy.request.config."""
343 base
= cherrypy
.config
.copy()
344 # Note that we merge the config from each node
345 # even if that node was None.
346 for name
, obj
, conf
, segleft
in object_trail
:
348 if 'tools.staticdir.dir' in conf
:
349 base
['tools.staticdir.section'] = '/' + '/'.join(fullpath
[0:fullpath_len
- segleft
])
352 # Try successive objects (reverse order)
353 num_candidates
= len(object_trail
) - 1
354 for i
in range(num_candidates
, -1, -1):
356 name
, candidate
, nodeconf
, segleft
= object_trail
[i
]
357 if candidate
is None:
360 # Try a "default" method on the current leaf.
361 if hasattr(candidate
, "default"):
362 defhandler
= candidate
.default
363 if getattr(defhandler
, 'exposed', False):
364 # Insert any extra _cp_config from the default handler.
365 conf
= getattr(defhandler
, "_cp_config", {})
366 object_trail
.insert(i
+1, ["default", defhandler
, conf
, segleft
])
367 request
.config
= set_conf()
368 # See http://www.cherrypy.org/ticket/613
369 request
.is_index
= path
.endswith("/")
370 return defhandler
, fullpath
[fullpath_len
- segleft
:-1]
372 # Uncomment the next line to restrict positional params to "default".
373 # if i < num_candidates - 2: continue
375 # Try the current leaf.
376 if getattr(candidate
, 'exposed', False):
377 request
.config
= set_conf()
378 if i
== num_candidates
:
379 # We found the extra ".index". Mark request so tools
380 # can redirect if path_info has no trailing slash.
381 request
.is_index
= True
383 # We're not at an 'index' handler. Mark request so tools
384 # can redirect if path_info has NO trailing slash.
385 # Note that this also includes handlers which take
386 # positional parameters (virtual paths).
387 request
.is_index
= False
388 return candidate
, fullpath
[fullpath_len
- segleft
:-1]
390 # We didn't find anything
391 request
.config
= set_conf()
395 class MethodDispatcher(Dispatcher
):
396 """Additional dispatch based on cherrypy.request.method.upper().
398 Methods named GET, POST, etc will be called on an exposed class.
399 The method names must be all caps; the appropriate Allow header
400 will be output showing all capitalized method names as allowable
403 Note that the containing class must be exposed, not the methods.
406 def __call__(self
, path_info
):
407 """Set handler and config for the current request."""
408 request
= cherrypy
.serving
.request
409 resource
, vpath
= self
.find_handler(path_info
)
413 avail
= [m
for m
in dir(resource
) if m
.isupper()]
414 if "GET" in avail
and "HEAD" not in avail
:
417 cherrypy
.serving
.response
.headers
['Allow'] = ", ".join(avail
)
419 # Find the subhandler
420 meth
= request
.method
.upper()
421 func
= getattr(resource
, meth
, None)
422 if func
is None and meth
== "HEAD":
423 func
= getattr(resource
, "GET", None)
425 # Grab any _cp_config on the subhandler.
426 if hasattr(func
, "_cp_config"):
427 request
.config
.update(func
._cp
_config
)
429 # Decode any leftover %2F in the virtual_path atoms.
430 vpath
= [x
.replace("%2F", "/") for x
in vpath
]
431 request
.handler
= LateParamPageHandler(func
, *vpath
)
433 request
.handler
= cherrypy
.HTTPError(405)
435 request
.handler
= cherrypy
.NotFound()
438 class RoutesDispatcher(object):
439 """A Routes based dispatcher for CherryPy."""
441 def __init__(self
, full_result
=False):
445 Set full_result to True if you wish the controller
446 and the action to be passed on to the page handler
447 parameters. By default they won't be.
450 self
.full_result
= full_result
451 self
.controllers
= {}
452 self
.mapper
= routes
.Mapper()
453 self
.mapper
.controller_scan
= self
.controllers
.keys
455 def connect(self
, name
, route
, controller
, **kwargs
):
456 self
.controllers
[name
] = controller
457 self
.mapper
.connect(name
, route
, controller
=name
, **kwargs
)
459 def redirect(self
, url
):
460 raise cherrypy
.HTTPRedirect(url
)
462 def __call__(self
, path_info
):
463 """Set handler and config for the current request."""
464 func
= self
.find_handler(path_info
)
466 cherrypy
.serving
.request
.handler
= LateParamPageHandler(func
)
468 cherrypy
.serving
.request
.handler
= cherrypy
.NotFound()
470 def find_handler(self
, path_info
):
471 """Find the right page handler, and set request.config."""
474 request
= cherrypy
.serving
.request
476 config
= routes
.request_config()
477 config
.mapper
= self
.mapper
478 if hasattr(request
, 'wsgi_environ'):
479 config
.environ
= request
.wsgi_environ
480 config
.host
= request
.headers
.get('Host', None)
481 config
.protocol
= request
.scheme
482 config
.redirect
= self
.redirect
484 result
= self
.mapper
.match(path_info
)
486 config
.mapper_dict
= result
489 params
= result
.copy()
490 if not self
.full_result
:
491 params
.pop('controller', None)
492 params
.pop('action', None)
493 request
.params
.update(params
)
495 # Get config for the root object/path.
496 request
.config
= base
= cherrypy
.config
.copy()
500 if 'tools.staticdir.dir' in nodeconf
:
501 nodeconf
['tools.staticdir.section'] = curpath
or "/"
502 base
.update(nodeconf
)
506 if hasattr(root
, "_cp_config"):
507 merge(root
._cp
_config
)
508 if "/" in app
.config
:
509 merge(app
.config
["/"])
511 # Mix in values from app.config.
512 atoms
= [x
for x
in path_info
.split("/") if x
]
518 curpath
= "/".join((curpath
, atom
))
519 if curpath
in app
.config
:
520 merge(app
.config
[curpath
])
524 controller
= result
.get('controller')
525 controller
= self
.controllers
.get(controller
, controller
)
527 if isinstance(controller
, (type, types
.ClassType
)):
528 controller
= controller()
529 # Get config from the controller.
530 if hasattr(controller
, "_cp_config"):
531 merge(controller
._cp
_config
)
533 action
= result
.get('action')
534 if action
is not None:
535 handler
= getattr(controller
, action
, None)
536 # Get config from the handler
537 if hasattr(handler
, "_cp_config"):
538 merge(handler
._cp
_config
)
542 # Do the last path atom here so it can
543 # override the controller's _cp_config.
545 curpath
= "/".join((curpath
, last
))
546 if curpath
in app
.config
:
547 merge(app
.config
[curpath
])
552 def XMLRPCDispatcher(next_dispatcher
=Dispatcher()):
553 from cherrypy
.lib
import xmlrpc
554 def xmlrpc_dispatch(path_info
):
555 path_info
= xmlrpc
.patched_path(path_info
)
556 return next_dispatcher(path_info
)
557 return xmlrpc_dispatch
560 def VirtualHost(next_dispatcher
=Dispatcher(), use_x_forwarded_host
=True, **domains
):
562 Select a different handler based on the Host header.
564 This can be useful when running multiple sites within one CP server.
565 It allows several domains to point to different parts of a single
566 website structure. For example::
568 http://www.domain.example -> root
569 http://www.domain2.example -> root/domain2/
570 http://www.domain2.example:443 -> root/secure
572 can be accomplished via the following config::
575 request.dispatch = cherrypy.dispatch.VirtualHost(
576 **{'www.domain2.example': '/domain2',
577 'www.domain2.example:443': '/secure',
581 The next dispatcher object in the dispatch chain.
582 The VirtualHost dispatcher adds a prefix to the URL and calls
583 another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
586 If True (the default), any "X-Forwarded-Host"
587 request header will be used instead of the "Host" header. This
588 is commonly added by HTTP servers (such as Apache) when proxying.
591 A dict of {host header value: virtual prefix} pairs.
592 The incoming "Host" request header is looked up in this dict,
593 and, if a match is found, the corresponding "virtual prefix"
594 value will be prepended to the URL path before calling the
595 next dispatcher. Note that you often need separate entries
596 for "example.com" and "www.example.com". In addition, "Host"
597 headers may contain the port number.
599 from cherrypy
.lib
import httputil
600 def vhost_dispatch(path_info
):
601 request
= cherrypy
.serving
.request
602 header
= request
.headers
.get
604 domain
= header('Host', '')
605 if use_x_forwarded_host
:
606 domain
= header("X-Forwarded-Host", domain
)
608 prefix
= domains
.get(domain
, "")
610 path_info
= httputil
.urljoin(prefix
, path_info
)
612 result
= next_dispatcher(path_info
)
614 # Touch up staticdir config. See http://www.cherrypy.org/ticket/614.
615 section
= request
.config
.get('tools.staticdir.section')
617 section
= section
[len(prefix
):]
618 request
.config
['tools.staticdir.section'] = section
621 return vhost_dispatch