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