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.