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

root/branches/cherrypy-3.0.x/cherrypy/lib/cptools.py

Revision 1614 (checked in by lawouach, 2 years ago)

Implemented a login attribute for the request and answered #653

  • 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] = 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
Note: See TracBrowser for help on using the browser.

Hosted by WebFaction

Log in as guest/cpguest to create tickets