Package cherrypy :: Package lib :: Module cptools
[hide private]
[frames] | no frames]

Source Code for Module cherrypy.lib.cptools

  1  """Functions for builtin CherryPy tools.""" 
  2   
  3  import logging 
  4   
  5  try: 
  6      # Python 2.5+ 
  7      from hashlib import md5 
  8  except ImportError: 
  9      from md5 import new as md5 
 10   
 11  import re 
 12   
 13  import cherrypy 
 14  from cherrypy.lib import http as _http 
 15   
 16   
 17  #                     Conditional HTTP request support                     # 
 18   
19 -def validate_etags(autotags=False):
20 """Validate the current ETag against If-Match, If-None-Match headers. 21 22 If autotags is True, an ETag response-header value will be provided 23 from an MD5 hash of the response body (unless some other code has 24 already provided an ETag header). If False (the default), the ETag 25 will not be automatic. 26 27 WARNING: the autotags feature is not designed for URL's which allow 28 methods other than GET. For example, if a POST to the same URL returns 29 no content, the automatic ETag will be incorrect, breaking a fundamental 30 use for entity tags in a possibly destructive fashion. Likewise, if you 31 raise 304 Not Modified, the response body will be empty, the ETag hash 32 will be incorrect, and your application will break. 33 See http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24 34 """ 35 response = cherrypy.response 36 37 # Guard against being run twice. 38 if hasattr(response, "ETag"): 39 return 40 41 status, reason, msg = _http.valid_status(response.status) 42 43 etag = response.headers.get('ETag') 44 45 # Automatic ETag generation. See warning in docstring. 46 if (not etag) and autotags: 47 if status == 200: 48 etag = response.collapse_body() 49 etag = '"%s"' % md5(etag).hexdigest() 50 response.headers['ETag'] = etag 51 52 response.ETag = etag 53 54 # "If the request would, without the If-Match header field, result in 55 # anything other than a 2xx or 412 status, then the If-Match header 56 # MUST be ignored." 57 if status >= 200 and status <= 299: 58 request = cherrypy.request 59 60 conditions = request.headers.elements('If-Match') or [] 61 conditions = [str(x) for x in conditions] 62 if conditions and not (conditions == ["*"] or etag in conditions): 63 raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " 64 "not match %r" % (etag, conditions)) 65 66 conditions = request.headers.elements('If-None-Match') or [] 67 conditions = [str(x) for x in conditions] 68 if conditions == ["*"] or etag in conditions: 69 if request.method in ("GET", "HEAD"): 70 raise cherrypy.HTTPRedirect([], 304) 71 else: 72 raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r " 73 "matched %r" % (etag, conditions))
74
75 -def validate_since():
76 """Validate the current Last-Modified against If-Modified-Since headers. 77 78 If no code has set the Last-Modified response header, then no validation 79 will be performed. 80 """ 81 response = cherrypy.response 82 lastmod = response.headers.get('Last-Modified') 83 if lastmod: 84 status, reason, msg = _http.valid_status(response.status) 85 86 request = cherrypy.request 87 88 since = request.headers.get('If-Unmodified-Since') 89 if since and since != lastmod: 90 if (status >= 200 and status <= 299) or status == 412: 91 raise cherrypy.HTTPError(412) 92 93 since = request.headers.get('If-Modified-Since') 94 if since and since == lastmod: 95 if (status >= 200 and status <= 299) or status == 304: 96 if request.method in ("GET", "HEAD"): 97 raise cherrypy.HTTPRedirect([], 304) 98 else: 99 raise cherrypy.HTTPError(412)
100 101 102 # Tool code # 103
104 -def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', 105 scheme='X-Forwarded-Proto'):
106 """Change the base URL (scheme://host[:port][/path]). 107 108 For running a CP server behind Apache, lighttpd, or other HTTP server. 109 110 If you want the new request.base to include path info (not just the host), 111 you must explicitly set base to the full base path, and ALSO set 'local' 112 to '', so that the X-Forwarded-Host request header (which never includes 113 path info) does not override it. 114 115 cherrypy.request.remote.ip (the IP address of the client) will be 116 rewritten if the header specified by the 'remote' arg is valid. 117 By default, 'remote' is set to 'X-Forwarded-For'. If you do not 118 want to rewrite remote.ip, set the 'remote' arg to an empty string. 119 """ 120 121 request = cherrypy.request 122 123 if scheme: 124 s = request.headers.get(scheme, None) 125 if s == 'on' and 'ssl' in scheme.lower(): 126 # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header 127 scheme = 'https' 128 else: 129 # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' 130 scheme = s 131 if not scheme: 132 scheme = request.base[:request.base.find("://")] 133 134 if local: 135 base = request.headers.get(local, base) 136 if not base: 137 port = cherrypy.request.local.port 138 if port == 80: 139 base = '127.0.0.1' 140 else: 141 base = '127.0.0.1:%s' % port 142 143 if base.find("://") == -1: 144 # add http:// or https:// if needed 145 base = scheme + "://" + base 146 147 request.base = base 148 149 if remote: 150 xff = request.headers.get(remote) 151 if xff: 152 if remote == 'X-Forwarded-For': 153 # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ 154 xff = xff.split(',')[-1].strip() 155 request.remote.ip = xff
156 157
158 -def ignore_headers(headers=('Range',)):
159 """Delete request headers whose field names are included in 'headers'. 160 161 This is a useful tool for working behind certain HTTP servers; 162 for example, Apache duplicates the work that CP does for 'Range' 163 headers, and will doubly-truncate the response. 164 """ 165 request = cherrypy.request 166 for name in headers: 167 if name in request.headers: 168 del request.headers[name]
169 170
171 -def response_headers(headers=None):
172 """Set headers on the response.""" 173 for name, value in (headers or []): 174 cherrypy.response.headers[name] = value
175 response_headers.failsafe = True 176 177
178 -def referer(pattern, accept=True, accept_missing=False, error=403, 179 message='Forbidden Referer header.'):
180 """Raise HTTPError if Referer header does/does not match the given pattern. 181 182 pattern: a regular expression pattern to test against the Referer. 183 accept: if True, the Referer must match the pattern; if False, 184 the Referer must NOT match the pattern. 185 accept_missing: if True, permit requests with no Referer header. 186 error: the HTTP error code to return to the client on failure. 187 message: a string to include in the response body on failure. 188 """ 189 try: 190 match = bool(re.match(pattern, cherrypy.request.headers['Referer'])) 191 if accept == match: 192 return 193 except KeyError: 194 if accept_missing: 195 return 196 197 raise cherrypy.HTTPError(error, message)
198 199
200 -class SessionAuth(object):
201 """Assert that the user is logged in.""" 202 203 session_key = "username" 204
205 - def check_username_and_password(self, username, password):
206 pass
207
208 - def anonymous(self):
209 """Provide a temporary user name for anonymous users.""" 210 pass
211
212 - def on_login(self, username):
213 pass
214
215 - def on_logout(self, username):
216 pass
217
218 - def on_check(self, username):
219 pass
220
221 - def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
222 return """<html><body> 223 Message: %(error_msg)s 224 <form method="post" action="do_login"> 225 Login: <input type="text" name="username" value="%(username)s" size="10" /><br /> 226 Password: <input type="password" name="password" size="10" /><br /> 227 <input type="hidden" name="from_page" value="%(from_page)s" /><br /> 228 <input type="submit" /> 229 </form> 230 </body></html>""" % {'from_page': from_page, 'username': username, 231 'error_msg': error_msg}
232
233 - def do_login(self, username, password, from_page='..', **kwargs):
234 """Login. May raise redirect, or return True if request handled.""" 235 error_msg = self.check_username_and_password(username, password) 236 if error_msg: 237 body = self.login_screen(from_page, username, error_msg) 238 cherrypy.response.body = body 239 if cherrypy.response.headers.has_key("Content-Length"): 240 # Delete Content-Length header so finalize() recalcs it. 241 del cherrypy.response.headers["Content-Length"] 242 return True 243 else: 244 cherrypy.session[self.session_key] = cherrypy.request.login = username 245 self.on_login(username) 246 raise cherrypy.HTTPRedirect(from_page or "/")
247
248 - def do_logout(self, from_page='..', **kwargs):
249 """Logout. May raise redirect, or return True if request handled.""" 250 sess = cherrypy.session 251 username = sess.get(self.session_key) 252 sess[self.session_key] = None 253 if username: 254 cherrypy.request.login = None 255 self.on_logout(username) 256 raise cherrypy.HTTPRedirect(from_page)
257
258 - def do_check(self):
259 """Assert username. May raise redirect, or return True if request handled.""" 260 sess = cherrypy.session 261 request = cherrypy.request 262 263 username = sess.get(self.session_key) 264 if not username: 265 sess[self.session_key] = username = self.anonymous() 266 if not username: 267 cherrypy.response.body = self.login_screen(cherrypy.url(qs=request.query_string)) 268 if cherrypy.response.headers.has_key("Content-Length"): 269 # Delete Content-Length header so finalize() recalcs it. 270 del cherrypy.response.headers["Content-Length"] 271 return True 272 cherrypy.request.login = username 273 self.on_check(username)
274
275 - def run(self):
276 request = cherrypy.request 277 path = request.path_info 278 if path.endswith('login_screen'): 279 return self.login_screen(**request.params) 280 elif path.endswith('do_login'): 281 return self.do_login(**request.params) 282 elif path.endswith('do_logout'): 283 return self.do_logout(**request.params) 284 else: 285 return self.do_check()
286 287
288 -def session_auth(**kwargs):
289 sa = SessionAuth() 290 for k, v in kwargs.iteritems(): 291 setattr(sa, k, v) 292 return sa.run()
293 session_auth.__doc__ = """Session authentication hook. 294 295 Any attribute of the SessionAuth class may be overridden via a keyword arg 296 to this function: 297 298 """ + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__) 299 for k in dir(SessionAuth) if not k.startswith("__")]) 300 301
302 -def log_traceback(severity=logging.DEBUG):
303 """Write the last error's traceback to the cherrypy error log.""" 304 cherrypy.log("", "HTTP", severity=severity, traceback=True)
305
306 -def log_request_headers():
307 """Write request headers to the cherrypy error log.""" 308 h = [" %s: %s" % (k, v) for k, v in cherrypy.request.header_list] 309 cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
310
311 -def log_hooks():
312 """Write request.hooks to the cherrypy error log.""" 313 msg = [] 314 # Sort by the standard points if possible. 315 from cherrypy import _cprequest 316 points = _cprequest.hookpoints 317 for k in cherrypy.request.hooks.keys(): 318 if k not in points: 319 points.append(k) 320 321 for k in points: 322 msg.append(" %s:" % k) 323 v = cherrypy.request.hooks.get(k, []) 324 v.sort() 325 for h in v: 326 msg.append(" %r" % h) 327 cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + 328 ':\n' + '\n'.join(msg), "HTTP")
329
330 -def redirect(url='', internal=True):
331 """Raise InternalRedirect or HTTPRedirect to the given url.""" 332 if internal: 333 raise cherrypy.InternalRedirect(url) 334 else: 335 raise cherrypy.HTTPRedirect(url)
336
337 -def trailing_slash(missing=True, extra=False):
338 """Redirect if path_info has (missing|extra) trailing slash.""" 339 request = cherrypy.request 340 pi = request.path_info 341 342 if request.is_index is True: 343 if missing: 344 if not pi.endswith('/'): 345 new_url = cherrypy.url(pi + '/', request.query_string) 346 raise cherrypy.HTTPRedirect(new_url) 347 elif request.is_index is False: 348 if extra: 349 # If pi == '/', don't redirect to ''! 350 if pi.endswith('/') and pi != '/': 351 new_url = cherrypy.url(pi[:-1], request.query_string) 352 raise cherrypy.HTTPRedirect(new_url)
353
354 -def flatten():
355 """Wrap response.body in a generator that recursively iterates over body. 356 357 This allows cherrypy.response.body to consist of 'nested generators'; 358 that is, a set of generators that yield generators. 359 """ 360 import types 361 def flattener(input): 362 for x in input: 363 if not isinstance(x, types.GeneratorType): 364 yield x 365 else: 366 for y in flattener(x): 367 yield y
368 response = cherrypy.response 369 response.body = flattener(response.body) 370 371
372 -def accept(media=None):
373 """Return the client's preferred media-type (from the given Content-Types). 374 375 If 'media' is None (the default), no test will be performed. 376 377 If 'media' is provided, it should be the Content-Type value (as a string) 378 or values (as a list or tuple of strings) which the current request 379 can emit. The client's acceptable media ranges (as declared in the 380 Accept request header) will be matched in order to these Content-Type 381 values; the first such string is returned. That is, the return value 382 will always be one of the strings provided in the 'media' arg (or None 383 if 'media' is None). 384 385 If no match is found, then HTTPError 406 (Not Acceptable) is raised. 386 Note that most web browsers send */* as a (low-quality) acceptable 387 media range, which should match any Content-Type. In addition, "...if 388 no Accept header field is present, then it is assumed that the client 389 accepts all media types." 390 391 Matching types are checked in order of client preference first, 392 and then in the order of the given 'media' values. 393 394 Note that this function does not honor accept-params (other than "q"). 395 """ 396 if not media: 397 return 398 if isinstance(media, basestring): 399 media = [media] 400 401 # Parse the Accept request header, and try to match one 402 # of the requested media-ranges (in order of preference). 403 ranges = cherrypy.request.headers.elements('Accept') 404 if not ranges: 405 # Any media type is acceptable. 406 return media[0] 407 else: 408 # Note that 'ranges' is sorted in order of preference 409 for element in ranges: 410 if element.qvalue > 0: 411 if element.value == "*/*": 412 # Matches any type or subtype 413 return media[0] 414 elif element.value.endswith("/*"): 415 # Matches any subtype 416 mtype = element.value[:-1] # Keep the slash 417 for m in media: 418 if m.startswith(mtype): 419 return m 420 else: 421 # Matches exact value 422 if element.value in media: 423 return element.value 424 425 # No suitable media-range found. 426 ah = cherrypy.request.headers.get('Accept') 427 if ah is None: 428 msg = "Your client did not send an Accept header." 429 else: 430 msg = "Your client sent this Accept header: %s." % ah 431 msg += (" But this resource only emits these media types: %s." % 432 ", ".join(media)) 433 raise cherrypy.HTTPError(406, msg)
434