Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(776)

Side by Side Diff: third_party/cherrypy/lib/cptools.py

Issue 9368042: Add CherryPy to third_party. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build/
Patch Set: '' Created 8 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « third_party/cherrypy/lib/cpstats.py ('k') | third_party/cherrypy/lib/encoding.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Property Changes:
Added: svn:eol-style
+ LF
OLDNEW
(Empty)
1 """Functions for builtin CherryPy tools."""
2
3 import logging
4 import re
5
6 import cherrypy
7 from cherrypy._cpcompat import basestring, ntob, md5, set
8 from cherrypy.lib import httputil as _httputil
9
10
11 # Conditional HTTP request support #
12
13 def validate_etags(autotags=False, debug=False):
14 """Validate the current ETag against If-Match, If-None-Match headers.
15
16 If autotags is True, an ETag response-header value will be provided
17 from an MD5 hash of the response body (unless some other code has
18 already provided an ETag header). If False (the default), the ETag
19 will not be automatic.
20
21 WARNING: the autotags feature is not designed for URL's which allow
22 methods other than GET. For example, if a POST to the same URL returns
23 no content, the automatic ETag will be incorrect, breaking a fundamental
24 use for entity tags in a possibly destructive fashion. Likewise, if you
25 raise 304 Not Modified, the response body will be empty, the ETag hash
26 will be incorrect, and your application will break.
27 See :rfc:`2616` Section 14.24.
28 """
29 response = cherrypy.serving.response
30
31 # Guard against being run twice.
32 if hasattr(response, "ETag"):
33 return
34
35 status, reason, msg = _httputil.valid_status(response.status)
36
37 etag = response.headers.get('ETag')
38
39 # Automatic ETag generation. See warning in docstring.
40 if etag:
41 if debug:
42 cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
43 elif not autotags:
44 if debug:
45 cherrypy.log('Autotags off', 'TOOLS.ETAGS')
46 elif status != 200:
47 if debug:
48 cherrypy.log('Status not 200', 'TOOLS.ETAGS')
49 else:
50 etag = response.collapse_body()
51 etag = '"%s"' % md5(etag).hexdigest()
52 if debug:
53 cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
54 response.headers['ETag'] = etag
55
56 response.ETag = etag
57
58 # "If the request would, without the If-Match header field, result in
59 # anything other than a 2xx or 412 status, then the If-Match header
60 # MUST be ignored."
61 if debug:
62 cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
63 if status >= 200 and status <= 299:
64 request = cherrypy.serving.request
65
66 conditions = request.headers.elements('If-Match') or []
67 conditions = [str(x) for x in conditions]
68 if debug:
69 cherrypy.log('If-Match conditions: %s' % repr(conditions),
70 'TOOLS.ETAGS')
71 if conditions and not (conditions == ["*"] or etag in conditions):
72 raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did "
73 "not match %r" % (etag, conditions))
74
75 conditions = request.headers.elements('If-None-Match') or []
76 conditions = [str(x) for x in conditions]
77 if debug:
78 cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
79 'TOOLS.ETAGS')
80 if conditions == ["*"] or etag in conditions:
81 if debug:
82 cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS ')
83 if request.method in ("GET", "HEAD"):
84 raise cherrypy.HTTPRedirect([], 304)
85 else:
86 raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r "
87 "matched %r" % (etag, conditions))
88
89 def validate_since():
90 """Validate the current Last-Modified against If-Modified-Since headers.
91
92 If no code has set the Last-Modified response header, then no validation
93 will be performed.
94 """
95 response = cherrypy.serving.response
96 lastmod = response.headers.get('Last-Modified')
97 if lastmod:
98 status, reason, msg = _httputil.valid_status(response.status)
99
100 request = cherrypy.serving.request
101
102 since = request.headers.get('If-Unmodified-Since')
103 if since and since != lastmod:
104 if (status >= 200 and status <= 299) or status == 412:
105 raise cherrypy.HTTPError(412)
106
107 since = request.headers.get('If-Modified-Since')
108 if since and since == lastmod:
109 if (status >= 200 and status <= 299) or status == 304:
110 if request.method in ("GET", "HEAD"):
111 raise cherrypy.HTTPRedirect([], 304)
112 else:
113 raise cherrypy.HTTPError(412)
114
115
116 # Tool code #
117
118 def allow(methods=None, debug=False):
119 """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
120
121 The given methods are case-insensitive, and may be in any order.
122 If only one method is allowed, you may supply a single string;
123 if more than one, supply a list of strings.
124
125 Regardless of whether the current method is allowed or not, this
126 also emits an 'Allow' response header, containing the given methods.
127 """
128 if not isinstance(methods, (tuple, list)):
129 methods = [methods]
130 methods = [m.upper() for m in methods if m]
131 if not methods:
132 methods = ['GET', 'HEAD']
133 elif 'GET' in methods and 'HEAD' not in methods:
134 methods.append('HEAD')
135
136 cherrypy.response.headers['Allow'] = ', '.join(methods)
137 if cherrypy.request.method not in methods:
138 if debug:
139 cherrypy.log('request.method %r not in methods %r' %
140 (cherrypy.request.method, methods), 'TOOLS.ALLOW')
141 raise cherrypy.HTTPError(405)
142 else:
143 if debug:
144 cherrypy.log('request.method %r in methods %r' %
145 (cherrypy.request.method, methods), 'TOOLS.ALLOW')
146
147
148 def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
149 scheme='X-Forwarded-Proto', debug=False):
150 """Change the base URL (scheme://host[:port][/path]).
151
152 For running a CP server behind Apache, lighttpd, or other HTTP server.
153
154 For Apache and lighttpd, you should leave the 'local' argument at the
155 default value of 'X-Forwarded-Host'. For Squid, you probably want to set
156 tools.proxy.local = 'Origin'.
157
158 If you want the new request.base to include path info (not just the host),
159 you must explicitly set base to the full base path, and ALSO set 'local'
160 to '', so that the X-Forwarded-Host request header (which never includes
161 path info) does not override it. Regardless, the value for 'base' MUST
162 NOT end in a slash.
163
164 cherrypy.request.remote.ip (the IP address of the client) will be
165 rewritten if the header specified by the 'remote' arg is valid.
166 By default, 'remote' is set to 'X-Forwarded-For'. If you do not
167 want to rewrite remote.ip, set the 'remote' arg to an empty string.
168 """
169
170 request = cherrypy.serving.request
171
172 if scheme:
173 s = request.headers.get(scheme, None)
174 if debug:
175 cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
176 if s == 'on' and 'ssl' in scheme.lower():
177 # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
178 scheme = 'https'
179 else:
180 # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
181 scheme = s
182 if not scheme:
183 scheme = request.base[:request.base.find("://")]
184
185 if local:
186 lbase = request.headers.get(local, None)
187 if debug:
188 cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
189 if lbase is not None:
190 base = lbase.split(',')[0]
191 if not base:
192 port = request.local.port
193 if port == 80:
194 base = '127.0.0.1'
195 else:
196 base = '127.0.0.1:%s' % port
197
198 if base.find("://") == -1:
199 # add http:// or https:// if needed
200 base = scheme + "://" + base
201
202 request.base = base
203
204 if remote:
205 xff = request.headers.get(remote)
206 if debug:
207 cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
208 if xff:
209 if remote == 'X-Forwarded-For':
210 # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forw arded-for-caveat/
211 xff = xff.split(',')[-1].strip()
212 request.remote.ip = xff
213
214
215 def ignore_headers(headers=('Range',), debug=False):
216 """Delete request headers whose field names are included in 'headers'.
217
218 This is a useful tool for working behind certain HTTP servers;
219 for example, Apache duplicates the work that CP does for 'Range'
220 headers, and will doubly-truncate the response.
221 """
222 request = cherrypy.serving.request
223 for name in headers:
224 if name in request.headers:
225 if debug:
226 cherrypy.log('Ignoring request header %r' % name,
227 'TOOLS.IGNORE_HEADERS')
228 del request.headers[name]
229
230
231 def response_headers(headers=None, debug=False):
232 """Set headers on the response."""
233 if debug:
234 cherrypy.log('Setting response headers: %s' % repr(headers),
235 'TOOLS.RESPONSE_HEADERS')
236 for name, value in (headers or []):
237 cherrypy.serving.response.headers[name] = value
238 response_headers.failsafe = True
239
240
241 def referer(pattern, accept=True, accept_missing=False, error=403,
242 message='Forbidden Referer header.', debug=False):
243 """Raise HTTPError if Referer header does/does not match the given pattern.
244
245 pattern
246 A regular expression pattern to test against the Referer.
247
248 accept
249 If True, the Referer must match the pattern; if False,
250 the Referer must NOT match the pattern.
251
252 accept_missing
253 If True, permit requests with no Referer header.
254
255 error
256 The HTTP error code to return to the client on failure.
257
258 message
259 A string to include in the response body on failure.
260
261 """
262 try:
263 ref = cherrypy.serving.request.headers['Referer']
264 match = bool(re.match(pattern, ref))
265 if debug:
266 cherrypy.log('Referer %r matches %r' % (ref, pattern),
267 'TOOLS.REFERER')
268 if accept == match:
269 return
270 except KeyError:
271 if debug:
272 cherrypy.log('No Referer header', 'TOOLS.REFERER')
273 if accept_missing:
274 return
275
276 raise cherrypy.HTTPError(error, message)
277
278
279 class SessionAuth(object):
280 """Assert that the user is logged in."""
281
282 session_key = "username"
283 debug = False
284
285 def check_username_and_password(self, username, password):
286 pass
287
288 def anonymous(self):
289 """Provide a temporary user name for anonymous users."""
290 pass
291
292 def on_login(self, username):
293 pass
294
295 def on_logout(self, username):
296 pass
297
298 def on_check(self, username):
299 pass
300
301 def login_screen(self, from_page='..', username='', error_msg='', **kwargs):
302 return ntob("""<html><body>
303 Message: %(error_msg)s
304 <form method="post" action="do_login">
305 Login: <input type="text" name="username" value="%(username)s" size="10" />< br />
306 Password: <input type="password" name="password" size="10" /><br />
307 <input type="hidden" name="from_page" value="%(from_page)s" /><br />
308 <input type="submit" />
309 </form>
310 </body></html>""" % {'from_page': from_page, 'username': username,
311 'error_msg': error_msg}, "utf-8")
312
313 def do_login(self, username, password, from_page='..', **kwargs):
314 """Login. May raise redirect, or return True if request handled."""
315 response = cherrypy.serving.response
316 error_msg = self.check_username_and_password(username, password)
317 if error_msg:
318 body = self.login_screen(from_page, username, error_msg)
319 response.body = body
320 if "Content-Length" in response.headers:
321 # Delete Content-Length header so finalize() recalcs it.
322 del response.headers["Content-Length"]
323 return True
324 else:
325 cherrypy.serving.request.login = username
326 cherrypy.session[self.session_key] = username
327 self.on_login(username)
328 raise cherrypy.HTTPRedirect(from_page or "/")
329
330 def do_logout(self, from_page='..', **kwargs):
331 """Logout. May raise redirect, or return True if request handled."""
332 sess = cherrypy.session
333 username = sess.get(self.session_key)
334 sess[self.session_key] = None
335 if username:
336 cherrypy.serving.request.login = None
337 self.on_logout(username)
338 raise cherrypy.HTTPRedirect(from_page)
339
340 def do_check(self):
341 """Assert username. May raise redirect, or return True if request handle d."""
342 sess = cherrypy.session
343 request = cherrypy.serving.request
344 response = cherrypy.serving.response
345
346 username = sess.get(self.session_key)
347 if not username:
348 sess[self.session_key] = username = self.anonymous()
349 if self.debug:
350 cherrypy.log('No session[username], trying anonymous', 'TOOLS.SE SSAUTH')
351 if not username:
352 url = cherrypy.url(qs=request.query_string)
353 if self.debug:
354 cherrypy.log('No username, routing to login_screen with '
355 'from_page %r' % url, 'TOOLS.SESSAUTH')
356 response.body = self.login_screen(url)
357 if "Content-Length" in response.headers:
358 # Delete Content-Length header so finalize() recalcs it.
359 del response.headers["Content-Length"]
360 return True
361 if self.debug:
362 cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAU TH')
363 request.login = username
364 self.on_check(username)
365
366 def run(self):
367 request = cherrypy.serving.request
368 response = cherrypy.serving.response
369
370 path = request.path_info
371 if path.endswith('login_screen'):
372 if self.debug:
373 cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUT H')
374 return self.login_screen(**request.params)
375 elif path.endswith('do_login'):
376 if request.method != 'POST':
377 response.headers['Allow'] = "POST"
378 if self.debug:
379 cherrypy.log('do_login requires POST', 'TOOLS.SESSAUTH')
380 raise cherrypy.HTTPError(405)
381 if self.debug:
382 cherrypy.log('routing %r to do_login' % path, 'TOOLS.SESSAUTH')
383 return self.do_login(**request.params)
384 elif path.endswith('do_logout'):
385 if request.method != 'POST':
386 response.headers['Allow'] = "POST"
387 raise cherrypy.HTTPError(405)
388 if self.debug:
389 cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH')
390 return self.do_logout(**request.params)
391 else:
392 if self.debug:
393 cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUT H')
394 return self.do_check()
395
396
397 def session_auth(**kwargs):
398 sa = SessionAuth()
399 for k, v in kwargs.items():
400 setattr(sa, k, v)
401 return sa.run()
402 session_auth.__doc__ = """Session authentication hook.
403
404 Any attribute of the SessionAuth class may be overridden via a keyword arg
405 to this function:
406
407 """ + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__)
408 for k in dir(SessionAuth) if not k.startswith("__")])
409
410
411 def log_traceback(severity=logging.ERROR, debug=False):
412 """Write the last error's traceback to the cherrypy error log."""
413 cherrypy.log("", "HTTP", severity=severity, traceback=True)
414
415 def log_request_headers(debug=False):
416 """Write request headers to the cherrypy error log."""
417 h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list]
418 cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP")
419
420 def log_hooks(debug=False):
421 """Write request.hooks to the cherrypy error log."""
422 request = cherrypy.serving.request
423
424 msg = []
425 # Sort by the standard points if possible.
426 from cherrypy import _cprequest
427 points = _cprequest.hookpoints
428 for k in request.hooks.keys():
429 if k not in points:
430 points.append(k)
431
432 for k in points:
433 msg.append(" %s:" % k)
434 v = request.hooks.get(k, [])
435 v.sort()
436 for h in v:
437 msg.append(" %r" % h)
438 cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
439 ':\n' + '\n'.join(msg), "HTTP")
440
441 def redirect(url='', internal=True, debug=False):
442 """Raise InternalRedirect or HTTPRedirect to the given url."""
443 if debug:
444 cherrypy.log('Redirecting %sto: %s' %
445 ({True: 'internal ', False: ''}[internal], url),
446 'TOOLS.REDIRECT')
447 if internal:
448 raise cherrypy.InternalRedirect(url)
449 else:
450 raise cherrypy.HTTPRedirect(url)
451
452 def trailing_slash(missing=True, extra=False, status=None, debug=False):
453 """Redirect if path_info has (missing|extra) trailing slash."""
454 request = cherrypy.serving.request
455 pi = request.path_info
456
457 if debug:
458 cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
459 (request.is_index, missing, extra, pi),
460 'TOOLS.TRAILING_SLASH')
461 if request.is_index is True:
462 if missing:
463 if not pi.endswith('/'):
464 new_url = cherrypy.url(pi + '/', request.query_string)
465 raise cherrypy.HTTPRedirect(new_url, status=status or 301)
466 elif request.is_index is False:
467 if extra:
468 # If pi == '/', don't redirect to ''!
469 if pi.endswith('/') and pi != '/':
470 new_url = cherrypy.url(pi[:-1], request.query_string)
471 raise cherrypy.HTTPRedirect(new_url, status=status or 301)
472
473 def flatten(debug=False):
474 """Wrap response.body in a generator that recursively iterates over body.
475
476 This allows cherrypy.response.body to consist of 'nested generators';
477 that is, a set of generators that yield generators.
478 """
479 import types
480 def flattener(input):
481 numchunks = 0
482 for x in input:
483 if not isinstance(x, types.GeneratorType):
484 numchunks += 1
485 yield x
486 else:
487 for y in flattener(x):
488 numchunks += 1
489 yield y
490 if debug:
491 cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
492 response = cherrypy.serving.response
493 response.body = flattener(response.body)
494
495
496 def accept(media=None, debug=False):
497 """Return the client's preferred media-type (from the given Content-Types).
498
499 If 'media' is None (the default), no test will be performed.
500
501 If 'media' is provided, it should be the Content-Type value (as a string)
502 or values (as a list or tuple of strings) which the current resource
503 can emit. The client's acceptable media ranges (as declared in the
504 Accept request header) will be matched in order to these Content-Type
505 values; the first such string is returned. That is, the return value
506 will always be one of the strings provided in the 'media' arg (or None
507 if 'media' is None).
508
509 If no match is found, then HTTPError 406 (Not Acceptable) is raised.
510 Note that most web browsers send */* as a (low-quality) acceptable
511 media range, which should match any Content-Type. In addition, "...if
512 no Accept header field is present, then it is assumed that the client
513 accepts all media types."
514
515 Matching types are checked in order of client preference first,
516 and then in the order of the given 'media' values.
517
518 Note that this function does not honor accept-params (other than "q").
519 """
520 if not media:
521 return
522 if isinstance(media, basestring):
523 media = [media]
524 request = cherrypy.serving.request
525
526 # Parse the Accept request header, and try to match one
527 # of the requested media-ranges (in order of preference).
528 ranges = request.headers.elements('Accept')
529 if not ranges:
530 # Any media type is acceptable.
531 if debug:
532 cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
533 return media[0]
534 else:
535 # Note that 'ranges' is sorted in order of preference
536 for element in ranges:
537 if element.qvalue > 0:
538 if element.value == "*/*":
539 # Matches any type or subtype
540 if debug:
541 cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
542 return media[0]
543 elif element.value.endswith("/*"):
544 # Matches any subtype
545 mtype = element.value[:-1] # Keep the slash
546 for m in media:
547 if m.startswith(mtype):
548 if debug:
549 cherrypy.log('Match due to %s' % element.value,
550 'TOOLS.ACCEPT')
551 return m
552 else:
553 # Matches exact value
554 if element.value in media:
555 if debug:
556 cherrypy.log('Match due to %s' % element.value,
557 'TOOLS.ACCEPT')
558 return element.value
559
560 # No suitable media-range found.
561 ah = request.headers.get('Accept')
562 if ah is None:
563 msg = "Your client did not send an Accept header."
564 else:
565 msg = "Your client sent this Accept header: %s." % ah
566 msg += (" But this resource only emits these media types: %s." %
567 ", ".join(media))
568 raise cherrypy.HTTPError(406, msg)
569
570
571 class MonitoredHeaderMap(_httputil.HeaderMap):
572
573 def __init__(self):
574 self.accessed_headers = set()
575
576 def __getitem__(self, key):
577 self.accessed_headers.add(key)
578 return _httputil.HeaderMap.__getitem__(self, key)
579
580 def __contains__(self, key):
581 self.accessed_headers.add(key)
582 return _httputil.HeaderMap.__contains__(self, key)
583
584 def get(self, key, default=None):
585 self.accessed_headers.add(key)
586 return _httputil.HeaderMap.get(self, key, default=default)
587
588 if hasattr({}, 'has_key'):
589 # Python 2
590 def has_key(self, key):
591 self.accessed_headers.add(key)
592 return _httputil.HeaderMap.has_key(self, key)
593
594
595 def autovary(ignore=None, debug=False):
596 """Auto-populate the Vary response header based on request.header access."""
597 request = cherrypy.serving.request
598
599 req_h = request.headers
600 request.headers = MonitoredHeaderMap()
601 request.headers.update(req_h)
602 if ignore is None:
603 ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
604
605 def set_response_header():
606 resp_h = cherrypy.serving.response.headers
607 v = set([e.value for e in resp_h.elements('Vary')])
608 if debug:
609 cherrypy.log('Accessed headers: %s' % request.headers.accessed_heade rs,
610 'TOOLS.AUTOVARY')
611 v = v.union(request.headers.accessed_headers)
612 v = v.difference(ignore)
613 v = list(v)
614 v.sort()
615 resp_h['Vary'] = ', '.join(v)
616 request.hooks.attach('before_finalize', set_response_header, 95)
617
OLDNEW
« no previous file with comments | « third_party/cherrypy/lib/cpstats.py ('k') | third_party/cherrypy/lib/encoding.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698