1 """Functions for builtin CherryPy tools."""
2
3 import logging
4 import re
5
6 import cherrypy
7 from cherrypy._cpcompat import basestring, md5, set, unicodestr
8 from cherrypy.lib import httputil as _httputil
9 from cherrypy.lib import is_iterator
10
11
12
13
90
91
117
118
119
120
121 -def allow(methods=None, debug=False):
122 """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
123
124 The given methods are case-insensitive, and may be in any order.
125 If only one method is allowed, you may supply a single string;
126 if more than one, supply a list of strings.
127
128 Regardless of whether the current method is allowed or not, this
129 also emits an 'Allow' response header, containing the given methods.
130 """
131 if not isinstance(methods, (tuple, list)):
132 methods = [methods]
133 methods = [m.upper() for m in methods if m]
134 if not methods:
135 methods = ['GET', 'HEAD']
136 elif 'GET' in methods and 'HEAD' not in methods:
137 methods.append('HEAD')
138
139 cherrypy.response.headers['Allow'] = ', '.join(methods)
140 if cherrypy.request.method not in methods:
141 if debug:
142 cherrypy.log('request.method %r not in methods %r' %
143 (cherrypy.request.method, methods), 'TOOLS.ALLOW')
144 raise cherrypy.HTTPError(405)
145 else:
146 if debug:
147 cherrypy.log('request.method %r in methods %r' %
148 (cherrypy.request.method, methods), 'TOOLS.ALLOW')
149
150
151 -def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
152 scheme='X-Forwarded-Proto', debug=False):
153 """Change the base URL (scheme://host[:port][/path]).
154
155 For running a CP server behind Apache, lighttpd, or other HTTP server.
156
157 For Apache and lighttpd, you should leave the 'local' argument at the
158 default value of 'X-Forwarded-Host'. For Squid, you probably want to set
159 tools.proxy.local = 'Origin'.
160
161 If you want the new request.base to include path info (not just the host),
162 you must explicitly set base to the full base path, and ALSO set 'local'
163 to '', so that the X-Forwarded-Host request header (which never includes
164 path info) does not override it. Regardless, the value for 'base' MUST
165 NOT end in a slash.
166
167 cherrypy.request.remote.ip (the IP address of the client) will be
168 rewritten if the header specified by the 'remote' arg is valid.
169 By default, 'remote' is set to 'X-Forwarded-For'. If you do not
170 want to rewrite remote.ip, set the 'remote' arg to an empty string.
171 """
172
173 request = cherrypy.serving.request
174
175 if scheme:
176 s = request.headers.get(scheme, None)
177 if debug:
178 cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
179 if s == 'on' and 'ssl' in scheme.lower():
180
181 scheme = 'https'
182 else:
183
184 scheme = s
185 if not scheme:
186 scheme = request.base[:request.base.find("://")]
187
188 if local:
189 lbase = request.headers.get(local, None)
190 if debug:
191 cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
192 if lbase is not None:
193 base = lbase.split(',')[0]
194 if not base:
195 port = request.local.port
196 if port == 80:
197 base = '127.0.0.1'
198 else:
199 base = '127.0.0.1:%s' % port
200
201 if base.find("://") == -1:
202
203 base = scheme + "://" + base
204
205 request.base = base
206
207 if remote:
208 xff = request.headers.get(remote)
209 if debug:
210 cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
211 if xff:
212 if remote == 'X-Forwarded-For':
213
214 xff = xff.split(',')[0].strip()
215 request.remote.ip = xff
216
217
219 """Delete request headers whose field names are included in 'headers'.
220
221 This is a useful tool for working behind certain HTTP servers;
222 for example, Apache duplicates the work that CP does for 'Range'
223 headers, and will doubly-truncate the response.
224 """
225 request = cherrypy.serving.request
226 for name in headers:
227 if name in request.headers:
228 if debug:
229 cherrypy.log('Ignoring request header %r' % name,
230 'TOOLS.IGNORE_HEADERS')
231 del request.headers[name]
232
233
241 response_headers.failsafe = True
242
243
244 -def referer(pattern, accept=True, accept_missing=False, error=403,
245 message='Forbidden Referer header.', debug=False):
246 """Raise HTTPError if Referer header does/does not match the given pattern.
247
248 pattern
249 A regular expression pattern to test against the Referer.
250
251 accept
252 If True, the Referer must match the pattern; if False,
253 the Referer must NOT match the pattern.
254
255 accept_missing
256 If True, permit requests with no Referer header.
257
258 error
259 The HTTP error code to return to the client on failure.
260
261 message
262 A string to include in the response body on failure.
263
264 """
265 try:
266 ref = cherrypy.serving.request.headers['Referer']
267 match = bool(re.match(pattern, ref))
268 if debug:
269 cherrypy.log('Referer %r matches %r' % (ref, pattern),
270 'TOOLS.REFERER')
271 if accept == match:
272 return
273 except KeyError:
274 if debug:
275 cherrypy.log('No Referer header', 'TOOLS.REFERER')
276 if accept_missing:
277 return
278
279 raise cherrypy.HTTPError(error, message)
280
281
283
284 """Assert that the user is logged in."""
285
286 session_key = "username"
287 debug = False
288
291
293 """Provide a temporary user name for anonymous users."""
294 pass
295
298
301
304
305 - def login_screen(self, from_page='..', username='', error_msg='',
306 **kwargs):
307 return (unicodestr("""<html><body>
308 Message: %(error_msg)s
309 <form method="post" action="do_login">
310 Login: <input type="text" name="username" value="%(username)s" size="10" />
311 <br />
312 Password: <input type="password" name="password" size="10" />
313 <br />
314 <input type="hidden" name="from_page" value="%(from_page)s" />
315 <br />
316 <input type="submit" />
317 </form>
318 </body></html>""") % vars()).encode("utf-8")
319
320 - def do_login(self, username, password, from_page='..', **kwargs):
336
337 - def do_logout(self, from_page='..', **kwargs):
346
375
408
409
415 session_auth.__doc__ = """Session authentication hook.
416
417 Any attribute of the SessionAuth class may be overridden via a keyword arg
418 to this function:
419
420 """ + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
421 for k in dir(SessionAuth) if not k.startswith("__")])
422
423
425 """Write the last error's traceback to the cherrypy error log."""
426 cherrypy.log("", "HTTP", severity=severity, traceback=True)
427
428
433
434
455
456
457 -def redirect(url='', internal=True, debug=False):
467
468
469 -def trailing_slash(missing=True, extra=False, status=None, debug=False):
470 """Redirect if path_info has (missing|extra) trailing slash."""
471 request = cherrypy.serving.request
472 pi = request.path_info
473
474 if debug:
475 cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
476 (request.is_index, missing, extra, pi),
477 'TOOLS.TRAILING_SLASH')
478 if request.is_index is True:
479 if missing:
480 if not pi.endswith('/'):
481 new_url = cherrypy.url(pi + '/', request.query_string)
482 raise cherrypy.HTTPRedirect(new_url, status=status or 301)
483 elif request.is_index is False:
484 if extra:
485
486 if pi.endswith('/') and pi != '/':
487 new_url = cherrypy.url(pi[:-1], request.query_string)
488 raise cherrypy.HTTPRedirect(new_url, status=status or 301)
489
490
492 """Wrap response.body in a generator that recursively iterates over body.
493
494 This allows cherrypy.response.body to consist of 'nested generators';
495 that is, a set of generators that yield generators.
496 """
497 def flattener(input):
498 numchunks = 0
499 for x in input:
500 if not is_iterator(x):
501 numchunks += 1
502 yield x
503 else:
504 for y in flattener(x):
505 numchunks += 1
506 yield y
507 if debug:
508 cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
509 response = cherrypy.serving.response
510 response.body = flattener(response.body)
511
512
513 -def accept(media=None, debug=False):
514 """Return the client's preferred media-type (from the given Content-Types).
515
516 If 'media' is None (the default), no test will be performed.
517
518 If 'media' is provided, it should be the Content-Type value (as a string)
519 or values (as a list or tuple of strings) which the current resource
520 can emit. The client's acceptable media ranges (as declared in the
521 Accept request header) will be matched in order to these Content-Type
522 values; the first such string is returned. That is, the return value
523 will always be one of the strings provided in the 'media' arg (or None
524 if 'media' is None).
525
526 If no match is found, then HTTPError 406 (Not Acceptable) is raised.
527 Note that most web browsers send */* as a (low-quality) acceptable
528 media range, which should match any Content-Type. In addition, "...if
529 no Accept header field is present, then it is assumed that the client
530 accepts all media types."
531
532 Matching types are checked in order of client preference first,
533 and then in the order of the given 'media' values.
534
535 Note that this function does not honor accept-params (other than "q").
536 """
537 if not media:
538 return
539 if isinstance(media, basestring):
540 media = [media]
541 request = cherrypy.serving.request
542
543
544
545 ranges = request.headers.elements('Accept')
546 if not ranges:
547
548 if debug:
549 cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
550 return media[0]
551 else:
552
553 for element in ranges:
554 if element.qvalue > 0:
555 if element.value == "*/*":
556
557 if debug:
558 cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
559 return media[0]
560 elif element.value.endswith("/*"):
561
562 mtype = element.value[:-1]
563 for m in media:
564 if m.startswith(mtype):
565 if debug:
566 cherrypy.log('Match due to %s' % element.value,
567 'TOOLS.ACCEPT')
568 return m
569 else:
570
571 if element.value in media:
572 if debug:
573 cherrypy.log('Match due to %s' % element.value,
574 'TOOLS.ACCEPT')
575 return element.value
576
577
578 ah = request.headers.get('Accept')
579 if ah is None:
580 msg = "Your client did not send an Accept header."
581 else:
582 msg = "Your client sent this Accept header: %s." % ah
583 msg += (" But this resource only emits these media types: %s." %
584 ", ".join(media))
585 raise cherrypy.HTTPError(406, msg)
586
587
589
591 self.accessed_headers = set()
592
596
600
604
605 if hasattr({}, 'has_key'):
606
608 self.accessed_headers.add(key)
609 return _httputil.HeaderMap.has_key(self, key)
610
611
635 request.hooks.attach('before_finalize', set_response_header, 95)
636