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