Download Install Tutorial Docs FAQ Tools WikiLicense Team IRC Planet Involvement Shop Book

root/tags/cherrypy-3.0.0/cherrypy/lib/cptools.py

Revision 1462 (checked in by fumanchu, 2 years ago)

Clearer trailing-slash logic using endswith instead of slicing.

  • Property svn:eol-style set to native
Line 
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] = 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             self.on_logout(username)
242         raise cherrypy.HTTPRedirect(from_page)
243    
244     def do_check(self):
245         """Assert username. May raise redirect, or return True if request handled."""
246         sess = cherrypy.session
247         request = cherrypy.request
248        
249         username = sess.get(self.session_key)
250         if not username:
251             sess[self.session_key] = username = self.anonymous()
252         if not username:
253             cherrypy.response.body = self.login_screen(cherrypy.url(qs=request.query_string))
254             if cherrypy.response.headers.has_key("Content-Length"):
255                 # Delete Content-Length header so finalize() recalcs it.
256                 del cherrypy.response.headers["Content-Length"]
257             return True
258        
259         self.on_check(username)
260    
261     def run(self):
262         request = cherrypy.request
263         path = request.path_info
264         if path.endswith('login_screen'):
265             return self.login_screen(**request.params)
266         elif path.endswith('do_login'):
267             return self.do_login(**request.params)
268         elif path.endswith('do_logout'):
269             return self.do_logout(**request.params)
270         else:
271             return self.do_check()
272
273
274 def session_auth(**kwargs):
275     sa = SessionAuth()
276     for k, v in kwargs.iteritems():
277         setattr(sa, k, v)
278     return sa.run()
279 session_auth.__doc__ = """Session authentication hook.
280
281 Any attribute of the SessionAuth class may be overridden via a keyword arg
282 to this function:
283
284 """ + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
285                  for k in dir(SessionAuth) if not k.startswith("__")])
286
287
288 def log_traceback():
289     """Write the last error's traceback to the cherrypy error log."""
290     from cherrypy import _cperror
291     cherrypy.log(_cperror.format_exc(), "HTTP")
292
293 def log_request_headers():
294     """Write request headers to the cherrypy error log."""
295     h = ["  %s: %s" % (k, v) for k, v in cherrypy.request.header_list]
296     cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
297
298 def redirect(url='', internal=True):
299     """Raise InternalRedirect or HTTPRedirect to the given url."""
300     if internal:
301         raise cherrypy.InternalRedirect(url)
302     else:
303         raise cherrypy.HTTPRedirect(url)
304
305 def trailing_slash(missing=True, extra=False):
306     """Redirect if path_info has (missing|extra) trailing slash."""
307     request = cherrypy.request
308     pi = request.path_info
309    
310     if request.is_index is True:
311         if missing:
312             if not pi.endswith('/'):
313                 new_url = cherrypy.url(pi + '/', request.query_string)
314                 raise cherrypy.HTTPRedirect(new_url)
315     elif request.is_index is False:
316         if extra:
317             # If pi == '/', don't redirect to ''!
318             if pi.endswith('/') and pi != '/':
319                 new_url = cherrypy.url(pi[:-1], request.query_string)
320                 raise cherrypy.HTTPRedirect(new_url)
321
322 def flatten():
323     """Wrap response.body in a generator that recursively iterates over body.
324     
325     This allows cherrypy.response.body to consist of 'nested generators';
326     that is, a set of generators that yield generators.
327     """
328     import types
329     def flattener(input):
330         for x in input:
331             if not isinstance(x, types.GeneratorType):
332                 yield x
333             else:
334                 for y in flattener(x):
335                     yield y
336     response = cherrypy.response
337     response.body = flattener(response.body)
338
339
340 def accept(media=None):
341     """Return the client's preferred media-type (from the given Content-Types).
342     
343     If 'media' is None (the default), no test will be performed.
344     
345     If 'media' is provided, it should be the Content-Type value (as a string)
346     or values (as a list or tuple of strings) which the current request
347     can emit. The client's acceptable media ranges (as declared in the
348     Accept request header) will be matched in order to these Content-Type
349     values; the first such string is returned. That is, the return value
350     will always be one of the strings provided in the 'media' arg (or None
351     if 'media' is None).
352     
353     If no match is found, then HTTPError 406 (Not Acceptable) is raised.
354     Note that most web browsers send */* as a (low-quality) acceptable
355     media range, which should match any Content-Type. In addition, "...if
356     no Accept header field is present, then it is assumed that the client
357     accepts all media types."
358     
359     Matching types are checked in order of client preference first,
360     and then in the order of the given 'media' values.
361     
362     Note that this function does not honor accept-params (other than "q").
363     """
364     if not media:
365         return
366     if isinstance(media, basestring):
367         media = [media]
368    
369     # Parse the Accept request header, and try to match one
370     # of the requested media-ranges (in order of preference).
371     ranges = cherrypy.request.headers.elements('Accept')
372     if not ranges:
373         # Any media type is acceptable.
374         return media[0]
375     else:
376         # Note that 'ranges' is sorted in order of preference
377         for element in ranges:
378             if element.qvalue > 0:
379                 if element.value == "*/*":
380                     # Matches any type or subtype
381                     return media[0]
382                 elif element.value.endswith("/*"):
383                     # Matches any subtype
384                     mtype = element.value[:-1]  # Keep the slash
385                     for m in media:
386                         if m.startswith(mtype):
387                             return m
388                 else:
389                     # Matches exact value
390                     if element.value in media:
391                         return element.value
392    
393     # No suitable media-range found.
394     ah = cherrypy.request.headers.get('Accept')
395     if ah is None:
396         msg = "Your client did not send an Accept header."
397     else:
398         msg = "Your client sent this Accept header: %s." % ah
399     msg += (" But this resource only emits these media types: %s." %
400             ", ".join(media))
401     raise cherrypy.HTTPError(406, msg)
402
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets