jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 1 | from __future__ import generators |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 2 | """ |
| 3 | httplib2 |
| 4 | |
| 5 | A caching http interface that supports ETags and gzip |
| 6 | to conserve bandwidth. |
| 7 | |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 8 | Requires Python 2.3 or later |
| 9 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 10 | """ |
| 11 | |
| 12 | __author__ = "Joe Gregorio (joe@bitworking.org)" |
| 13 | __copyright__ = "Copyright 2006, Joe Gregorio" |
jcgregorio | a0713ab | 2006-07-01 05:21:34 +0000 | [diff] [blame] | 14 | __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", |
| 15 | "James Antill", |
jcgregorio | 9208892 | 2006-07-01 05:53:21 +0000 | [diff] [blame] | 16 | "Xavier Verges Farrero", |
| 17 | "Jonathan Feinberg", |
jcgregorio | cd1c27d | 2006-11-16 04:49:30 +0000 | [diff] [blame] | 18 | "Blair Zajac", |
jcgregorio | 093bae6 | 2007-01-18 15:24:52 +0000 | [diff] [blame] | 19 | "Sam Ruby", |
| 20 | "Louis Nyffenegger"] |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 21 | __license__ = "MIT" |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 22 | __version__ = "$Rev$" |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 23 | |
| 24 | import re |
jcgregorio | debceec | 2006-12-12 20:26:02 +0000 | [diff] [blame] | 25 | import sys |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 26 | import md5 |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 27 | import email |
| 28 | import email.Utils |
| 29 | import email.Message |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 30 | import StringIO |
| 31 | import gzip |
| 32 | import zlib |
| 33 | import httplib |
| 34 | import urlparse |
| 35 | import base64 |
| 36 | import os |
| 37 | import copy |
| 38 | import calendar |
| 39 | import time |
| 40 | import random |
| 41 | import sha |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 42 | import hmac |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 43 | from gettext import gettext as _ |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 44 | import socket |
jcgregorio | debceec | 2006-12-12 20:26:02 +0000 | [diff] [blame] | 45 | |
| 46 | if sys.version_info >= (2,3): |
| 47 | from iri2uri import iri2uri |
| 48 | else: |
| 49 | def iri2uri(uri): |
| 50 | return uri |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 51 | |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 52 | __all__ = ['Http', 'Response', 'HttpLib2Error', |
| 53 | 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent', |
jcgregorio | 076f54d | 2006-07-03 17:34:16 +0000 | [diff] [blame] | 54 | 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError', |
| 55 | 'debuglevel'] |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 56 | |
| 57 | |
jcgregorio | d62c5d2 | 2006-03-18 04:39:31 +0000 | [diff] [blame] | 58 | # The httplib debug level, set to a non-zero value to get debug output |
| 59 | debuglevel = 0 |
| 60 | |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 61 | # Python 2.3 support |
jcgregorio | 89e6edb | 2006-12-18 16:26:18 +0000 | [diff] [blame] | 62 | if sys.version_info < (2,4): |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 63 | def sorted(seq): |
| 64 | seq.sort() |
| 65 | return seq |
| 66 | |
| 67 | # Python 2.3 support |
| 68 | def HTTPResponse__getheaders(self): |
| 69 | """Return list of (header, value) tuples.""" |
| 70 | if self.msg is None: |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 71 | raise httplib.ResponseNotReady() |
| 72 | return self.msg.items() |
| 73 | |
| 74 | if not hasattr(httplib.HTTPResponse, 'getheaders'): |
| 75 | httplib.HTTPResponse.getheaders = HTTPResponse__getheaders |
| 76 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 77 | # All exceptions raised here derive from HttpLib2Error |
| 78 | class HttpLib2Error(Exception): pass |
| 79 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 80 | # Some exceptions can be caught and optionally |
| 81 | # be turned back into responses. |
| 82 | class HttpLib2ErrorWithResponse(HttpLib2Error): |
| 83 | def __init__(self, desc, response, content): |
| 84 | self.response = response |
| 85 | self.content = content |
| 86 | HttpLib2Error.__init__(self, desc) |
| 87 | |
| 88 | class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass |
| 89 | class RedirectLimit(HttpLib2ErrorWithResponse): pass |
| 90 | class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass |
| 91 | class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass |
| 92 | class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass |
| 93 | |
jcgregorio | 132d28e | 2007-01-23 16:22:53 +0000 | [diff] [blame] | 94 | class RelativeURIError(HttpLib2Error): pass |
jcgregorio | 6a63817 | 2007-01-23 16:40:23 +0000 | [diff] [blame] | 95 | class ServerNotFoundError(HttpLib2Error): pass |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 96 | |
| 97 | # Open Items: |
| 98 | # ----------- |
| 99 | # Proxy support |
| 100 | |
| 101 | # Are we removing the cached content too soon on PUT (only delete on 200 Maybe?) |
| 102 | |
| 103 | # Pluggable cache storage (supports storing the cache in |
| 104 | # flat files by default. We need a plug-in architecture |
| 105 | # that can support Berkeley DB and Squid) |
| 106 | |
| 107 | # == Known Issues == |
| 108 | # Does not handle a resource that uses conneg and Last-Modified but no ETag as a cache validator. |
| 109 | # Does not handle Cache-Control: max-stale |
| 110 | # Does not use Age: headers when calculating cache freshness. |
| 111 | |
| 112 | |
| 113 | # The number of redirections to follow before giving up. |
| 114 | # Note that only GET redirects are automatically followed. |
| 115 | # Will also honor 301 requests by saving that info and never |
| 116 | # requesting that URI again. |
| 117 | DEFAULT_MAX_REDIRECTS = 5 |
| 118 | |
| 119 | # Which headers are hop-by-hop headers by default |
| 120 | HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] |
| 121 | |
jcgregorio | db8dfc8 | 2006-03-31 14:59:46 +0000 | [diff] [blame] | 122 | def _get_end2end_headers(response): |
jcgregorio | 6cb373b | 2006-04-03 13:51:00 +0000 | [diff] [blame] | 123 | hopbyhop = list(HOP_BY_HOP) |
jcgregorio | db8dfc8 | 2006-03-31 14:59:46 +0000 | [diff] [blame] | 124 | hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) |
| 125 | return [header for header in response.keys() if header not in hopbyhop] |
| 126 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 127 | URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") |
| 128 | |
| 129 | def parse_uri(uri): |
| 130 | """Parses a URI using the regex given in Appendix B of RFC 3986. |
| 131 | |
| 132 | (scheme, authority, path, query, fragment) = parse_uri(uri) |
| 133 | """ |
| 134 | groups = URI.match(uri).groups() |
| 135 | return (groups[1], groups[3], groups[4], groups[6], groups[8]) |
| 136 | |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 137 | def urlnorm(uri): |
| 138 | (scheme, authority, path, query, fragment) = parse_uri(uri) |
jcgregorio | 132d28e | 2007-01-23 16:22:53 +0000 | [diff] [blame] | 139 | if not scheme or not authority: |
| 140 | raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri) |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 141 | authority = authority.lower() |
jcgregorio | b4e9ab0 | 2006-11-17 15:53:15 +0000 | [diff] [blame] | 142 | scheme = scheme.lower() |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 143 | if not path: |
| 144 | path = "/" |
| 145 | # Could do syntax based normalization of the URI before |
| 146 | # computing the digest. See Section 6.2.2 of Std 66. |
| 147 | request_uri = query and "?".join([path, query]) or path |
jcgregorio | a898f8f | 2006-12-12 17:16:55 +0000 | [diff] [blame] | 148 | scheme = scheme.lower() |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 149 | defrag_uri = scheme + "://" + authority + request_uri |
| 150 | return scheme, authority, request_uri, defrag_uri |
| 151 | |
| 152 | |
| 153 | # Cache filename construction (original borrowed from Venus http://intertwingly.net/code/venus/) |
| 154 | re_url_scheme = re.compile(r'^\w+://') |
| 155 | re_slash = re.compile(r'[?/:|]+') |
| 156 | |
| 157 | def safename(filename): |
| 158 | """Return a filename suitable for the cache. |
| 159 | |
| 160 | Strips dangerous and common characters to create a filename we |
| 161 | can use to store the cache in. |
| 162 | """ |
| 163 | |
| 164 | try: |
| 165 | if re_url_scheme.match(filename): |
| 166 | if isinstance(filename,str): |
jcgregorio | a898f8f | 2006-12-12 17:16:55 +0000 | [diff] [blame] | 167 | filename = filename.decode('utf-8') |
| 168 | filename = filename.encode('idna') |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 169 | else: |
jcgregorio | a898f8f | 2006-12-12 17:16:55 +0000 | [diff] [blame] | 170 | filename = filename.encode('idna') |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 171 | except: |
| 172 | pass |
| 173 | if isinstance(filename,unicode): |
| 174 | filename=filename.encode('utf-8') |
| 175 | filemd5 = md5.new(filename).hexdigest() |
| 176 | filename = re_url_scheme.sub("", filename) |
| 177 | filename = re_slash.sub(",", filename) |
| 178 | |
| 179 | # limit length of filename |
| 180 | if len(filename)>200: |
| 181 | filename=filename[:200] |
| 182 | return ",".join((filename, filemd5)) |
| 183 | |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 184 | NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 185 | def _normalize_headers(headers): |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 186 | return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 187 | |
| 188 | def _parse_cache_control(headers): |
| 189 | retval = {} |
| 190 | if headers.has_key('cache-control'): |
| 191 | parts = headers['cache-control'].split(',') |
| 192 | parts_with_args = [tuple([x.strip() for x in part.split("=")]) for part in parts if -1 != part.find("=")] |
| 193 | parts_wo_args = [(name.strip(), 1) for name in parts if -1 == name.find("=")] |
| 194 | retval = dict(parts_with_args + parts_wo_args) |
| 195 | return retval |
| 196 | |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 197 | # Whether to use a strict mode to parse WWW-Authenticate headers |
| 198 | # Might lead to bad results in case of ill-formed header value, |
| 199 | # so disabled by default, falling back to relaxed parsing. |
| 200 | # Set to true to turn on, usefull for testing servers. |
| 201 | USE_WWW_AUTH_STRICT_PARSING = 0 |
| 202 | |
| 203 | # In regex below: |
| 204 | # [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+ matches a "token" as defined by HTTP |
| 205 | # "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?" matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space |
| 206 | # Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both: |
| 207 | # \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08\x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"? |
| 208 | WWW_AUTH_STRICT = re.compile(r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$") |
| 209 | WWW_AUTH_RELAXED = re.compile(r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$") |
| 210 | UNQUOTE_PAIRS = re.compile(r'\\(.)') |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 211 | def _parse_www_authenticate(headers, headername='www-authenticate'): |
| 212 | """Returns a dictionary of dictionaries, one dict |
| 213 | per auth_scheme.""" |
| 214 | retval = {} |
| 215 | if headers.has_key(headername): |
| 216 | authenticate = headers[headername].strip() |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 217 | www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 218 | while authenticate: |
| 219 | # Break off the scheme at the beginning of the line |
| 220 | if headername == 'authentication-info': |
| 221 | (auth_scheme, the_rest) = ('digest', authenticate) |
| 222 | else: |
| 223 | (auth_scheme, the_rest) = authenticate.split(" ", 1) |
| 224 | # Now loop over all the key value pairs that come after the scheme, |
| 225 | # being careful not to roll into the next scheme |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 226 | match = www_auth.search(the_rest) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 227 | auth_params = {} |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 228 | while match: |
| 229 | if match and len(match.groups()) == 3: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 230 | (key, value, the_rest) = match.groups() |
jcgregorio | fd22e43 | 2006-04-27 02:00:08 +0000 | [diff] [blame] | 231 | auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')]) |
| 232 | match = www_auth.search(the_rest) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 233 | retval[auth_scheme.lower()] = auth_params |
| 234 | authenticate = the_rest.strip() |
| 235 | return retval |
| 236 | |
| 237 | |
| 238 | def _entry_disposition(response_headers, request_headers): |
| 239 | """Determine freshness from the Date, Expires and Cache-Control headers. |
| 240 | |
| 241 | We don't handle the following: |
| 242 | |
| 243 | 1. Cache-Control: max-stale |
| 244 | 2. Age: headers are not used in the calculations. |
| 245 | |
| 246 | Not that this algorithm is simpler than you might think |
| 247 | because we are operating as a private (non-shared) cache. |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 248 | This lets us ignore 's-maxage'. We can also ignore |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 249 | 'proxy-invalidate' since we aren't a proxy. |
| 250 | We will never return a stale document as |
| 251 | fresh as a design decision, and thus the non-implementation |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 252 | of 'max-stale'. This also lets us safely ignore 'must-revalidate' |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 253 | since we operate as if every server has sent 'must-revalidate'. |
| 254 | Since we are private we get to ignore both 'public' and |
| 255 | 'private' parameters. We also ignore 'no-transform' since |
| 256 | we don't do any transformations. |
| 257 | The 'no-store' parameter is handled at a higher level. |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 258 | So the only Cache-Control parameters we look at are: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 259 | |
| 260 | no-cache |
| 261 | only-if-cached |
| 262 | max-age |
| 263 | min-fresh |
| 264 | """ |
| 265 | |
| 266 | retval = "STALE" |
| 267 | cc = _parse_cache_control(request_headers) |
| 268 | cc_response = _parse_cache_control(response_headers) |
| 269 | |
| 270 | if request_headers.has_key('pragma') and request_headers['pragma'].lower().find('no-cache') != -1: |
| 271 | retval = "TRANSPARENT" |
| 272 | if 'cache-control' not in request_headers: |
| 273 | request_headers['cache-control'] = 'no-cache' |
| 274 | elif cc.has_key('no-cache'): |
| 275 | retval = "TRANSPARENT" |
| 276 | elif cc_response.has_key('no-cache'): |
| 277 | retval = "STALE" |
| 278 | elif cc.has_key('only-if-cached'): |
| 279 | retval = "FRESH" |
| 280 | elif response_headers.has_key('date'): |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 281 | date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date'])) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 282 | now = time.time() |
| 283 | current_age = max(0, now - date) |
| 284 | if cc_response.has_key('max-age'): |
jcgregorio | ac4c753 | 2007-01-18 16:25:01 +0000 | [diff] [blame] | 285 | try: |
| 286 | freshness_lifetime = int(cc_response['max-age']) |
| 287 | except: |
| 288 | freshness_lifetime = 0 |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 289 | elif response_headers.has_key('expires'): |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 290 | expires = email.Utils.parsedate_tz(response_headers['expires']) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 291 | freshness_lifetime = max(0, calendar.timegm(expires) - date) |
| 292 | else: |
| 293 | freshness_lifetime = 0 |
| 294 | if cc.has_key('max-age'): |
jcgregorio | ac4c753 | 2007-01-18 16:25:01 +0000 | [diff] [blame] | 295 | try: |
| 296 | freshness_lifetime = int(cc['max-age']) |
| 297 | except: |
| 298 | freshness_lifetime = 0 |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 299 | if cc.has_key('min-fresh'): |
jcgregorio | ac4c753 | 2007-01-18 16:25:01 +0000 | [diff] [blame] | 300 | try: |
| 301 | min_fresh = int(cc['min-fresh']) |
| 302 | except: |
| 303 | min_fresh = 0 |
| 304 | current_age += min_fresh |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 305 | if freshness_lifetime > current_age: |
| 306 | retval = "FRESH" |
| 307 | return retval |
| 308 | |
| 309 | def _decompressContent(response, new_content): |
| 310 | content = new_content |
| 311 | try: |
jcgregorio | 90fb4a4 | 2006-11-17 16:19:47 +0000 | [diff] [blame] | 312 | encoding = response.get('content-encoding', None) |
| 313 | if encoding in ['gzip', 'deflate']: |
| 314 | if encoding == 'gzip': |
| 315 | content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() |
| 316 | if encoding == 'deflate': |
| 317 | content = zlib.decompress(content) |
jcgregorio | 153f588 | 2006-11-06 03:33:24 +0000 | [diff] [blame] | 318 | response['content-length'] = str(len(content)) |
jcgregorio | 90fb4a4 | 2006-11-17 16:19:47 +0000 | [diff] [blame] | 319 | del response['content-encoding'] |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 320 | except: |
| 321 | content = "" |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 322 | raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 323 | return content |
| 324 | |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 325 | def _updateCache(request_headers, response_headers, content, cache, cachekey): |
| 326 | if cachekey: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 327 | cc = _parse_cache_control(request_headers) |
| 328 | cc_response = _parse_cache_control(response_headers) |
| 329 | if cc.has_key('no-store') or cc_response.has_key('no-store'): |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 330 | cache.delete(cachekey) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 331 | else: |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 332 | info = email.Message.Message() |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 333 | for key, value in response_headers.iteritems(): |
jcgregorio | 90fb4a4 | 2006-11-17 16:19:47 +0000 | [diff] [blame] | 334 | if key not in ['status','content-encoding','transfer-encoding']: |
| 335 | info[key] = value |
| 336 | |
| 337 | status = response_headers.status |
| 338 | if status == 304: |
| 339 | status = 200 |
| 340 | |
| 341 | status_header = 'status: %d\r\n' % response_headers.status |
| 342 | |
| 343 | header_str = info.as_string() |
| 344 | |
| 345 | header_str = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", header_str) |
| 346 | text = "".join([status_header, header_str, content]) |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 347 | |
| 348 | cache.set(cachekey, text) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 349 | |
| 350 | def _cnonce(): |
| 351 | dig = md5.new("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest() |
| 352 | return dig[:16] |
| 353 | |
| 354 | def _wsse_username_token(cnonce, iso_now, password): |
| 355 | return base64.encodestring(sha.new("%s%s%s" % (cnonce, iso_now, password)).digest()).strip() |
| 356 | |
| 357 | |
| 358 | # For credentials we need two things, first |
| 359 | # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.) |
| 360 | # Then we also need a list of URIs that have already demanded authentication |
| 361 | # That list is tricky since sub-URIs can take the same auth, or the |
| 362 | # auth scheme may change as you descend the tree. |
| 363 | # So we also need each Auth instance to be able to tell us |
| 364 | # how close to the 'top' it is. |
| 365 | |
| 366 | class Authentication: |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 367 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 368 | (scheme, authority, path, query, fragment) = parse_uri(request_uri) |
| 369 | self.path = path |
| 370 | self.host = host |
| 371 | self.credentials = credentials |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 372 | self.http = http |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 373 | |
| 374 | def depth(self, request_uri): |
| 375 | (scheme, authority, path, query, fragment) = parse_uri(request_uri) |
| 376 | return request_uri[len(self.path):].count("/") |
| 377 | |
| 378 | def inscope(self, host, request_uri): |
| 379 | # XXX Should we normalize the request_uri? |
| 380 | (scheme, authority, path, query, fragment) = parse_uri(request_uri) |
| 381 | return (host == self.host) and path.startswith(self.path) |
| 382 | |
| 383 | def request(self, method, request_uri, headers, content): |
| 384 | """Modify the request headers to add the appropriate |
| 385 | Authorization header. Over-rise this in sub-classes.""" |
| 386 | pass |
| 387 | |
| 388 | def response(self, response, content): |
| 389 | """Gives us a chance to update with new nonces |
| 390 | or such returned from the last authorized response. |
| 391 | Over-rise this in sub-classes if necessary. |
| 392 | |
| 393 | Return TRUE is the request is to be retried, for |
| 394 | example Digest may return stale=true. |
| 395 | """ |
| 396 | return False |
| 397 | |
| 398 | |
| 399 | |
| 400 | class BasicAuthentication(Authentication): |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 401 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
| 402 | Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 403 | |
| 404 | def request(self, method, request_uri, headers, content): |
| 405 | """Modify the request headers to add the appropriate |
| 406 | Authorization header.""" |
| 407 | headers['authorization'] = 'Basic ' + base64.encodestring("%s:%s" % self.credentials).strip() |
| 408 | |
| 409 | |
| 410 | class DigestAuthentication(Authentication): |
| 411 | """Only do qop='auth' and MD5, since that |
| 412 | is all Apache currently implements""" |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 413 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
| 414 | Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 415 | challenge = _parse_www_authenticate(response, 'www-authenticate') |
| 416 | self.challenge = challenge['digest'] |
| 417 | qop = self.challenge.get('qop') |
| 418 | self.challenge['qop'] = ('auth' in [x.strip() for x in qop.split()]) and 'auth' or None |
| 419 | if self.challenge['qop'] is None: |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 420 | raise UnimplementedDigestAuthOptionError( _("Unsupported value for qop: %s." % qop)) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 421 | self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5') |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 422 | if self.challenge['algorithm'] != 'MD5': |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 423 | raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 424 | self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]]) |
| 425 | self.challenge['nc'] = 1 |
| 426 | |
| 427 | def request(self, method, request_uri, headers, content, cnonce = None): |
| 428 | """Modify the request headers""" |
| 429 | H = lambda x: md5.new(x).hexdigest() |
| 430 | KD = lambda s, d: H("%s:%s" % (s, d)) |
| 431 | A2 = "".join([method, ":", request_uri]) |
| 432 | self.challenge['cnonce'] = cnonce or _cnonce() |
| 433 | request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'], |
| 434 | '%08x' % self.challenge['nc'], |
| 435 | self.challenge['cnonce'], |
| 436 | self.challenge['qop'], H(A2) |
| 437 | )) |
| 438 | headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % ( |
| 439 | self.credentials[0], |
| 440 | self.challenge['realm'], |
| 441 | self.challenge['nonce'], |
| 442 | request_uri, |
| 443 | self.challenge['algorithm'], |
| 444 | request_digest, |
| 445 | self.challenge['qop'], |
| 446 | self.challenge['nc'], |
| 447 | self.challenge['cnonce'], |
| 448 | ) |
| 449 | self.challenge['nc'] += 1 |
| 450 | |
| 451 | def response(self, response, content): |
| 452 | if not response.has_key('authentication-info'): |
jcgregorio | cd140be | 2007-01-18 14:44:09 +0000 | [diff] [blame] | 453 | challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 454 | if 'true' == challenge.get('stale'): |
| 455 | self.challenge['nonce'] = challenge['nonce'] |
| 456 | self.challenge['nc'] = 1 |
| 457 | return True |
| 458 | else: |
jcgregorio | cd140be | 2007-01-18 14:44:09 +0000 | [diff] [blame] | 459 | updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 460 | |
| 461 | if updated_challenge.has_key('nextnonce'): |
| 462 | self.challenge['nonce'] = updated_challenge['nextnonce'] |
| 463 | self.challenge['nc'] = 1 |
| 464 | return False |
| 465 | |
| 466 | |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 467 | class HmacDigestAuthentication(Authentication): |
| 468 | """Adapted from Robert Sayre's code and DigestAuthentication above.""" |
| 469 | __author__ = "Thomas Broyer (t.broyer@ltgt.net)" |
| 470 | |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 471 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
| 472 | Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 473 | challenge = _parse_www_authenticate(response, 'www-authenticate') |
| 474 | self.challenge = challenge['hmacdigest'] |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 475 | # TODO: self.challenge['domain'] |
| 476 | self.challenge['reason'] = self.challenge.get('reason', 'unauthorized') |
| 477 | if self.challenge['reason'] not in ['unauthorized', 'integrity']: |
| 478 | self.challenge['reason'] = 'unauthorized' |
| 479 | self.challenge['salt'] = self.challenge.get('salt', '') |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 480 | if not self.challenge.get('snonce'): |
| 481 | raise UnimplementedHmacDigestAuthOptionError( _("The challenge doesn't contain a server nonce, or this one is empty.")) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 482 | self.challenge['algorithm'] = self.challenge.get('algorithm', 'HMAC-SHA-1') |
| 483 | if self.challenge['algorithm'] not in ['HMAC-SHA-1', 'HMAC-MD5']: |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 484 | raise UnimplementedHmacDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm'])) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 485 | self.challenge['pw-algorithm'] = self.challenge.get('pw-algorithm', 'SHA-1') |
| 486 | if self.challenge['pw-algorithm'] not in ['SHA-1', 'MD5']: |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 487 | raise UnimplementedHmacDigestAuthOptionError( _("Unsupported value for pw-algorithm: %s." % self.challenge['pw-algorithm'])) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 488 | if self.challenge['algorithm'] == 'HMAC-MD5': |
| 489 | self.hashmod = md5 |
| 490 | else: |
| 491 | self.hashmod = sha |
| 492 | if self.challenge['pw-algorithm'] == 'MD5': |
| 493 | self.pwhashmod = md5 |
| 494 | else: |
| 495 | self.pwhashmod = sha |
| 496 | self.key = "".join([self.credentials[0], ":", |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 497 | self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(), |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 498 | ":", self.challenge['realm'] |
| 499 | ]) |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 500 | self.key = self.pwhashmod.new(self.key).hexdigest().lower() |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 501 | |
| 502 | def request(self, method, request_uri, headers, content): |
| 503 | """Modify the request headers""" |
jcgregorio | db8dfc8 | 2006-03-31 14:59:46 +0000 | [diff] [blame] | 504 | keys = _get_end2end_headers(headers) |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 505 | keylist = "".join(["%s " % k for k in keys]) |
| 506 | headers_val = "".join([headers[k] for k in keys]) |
| 507 | created = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()) |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 508 | cnonce = _cnonce() |
| 509 | request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val) |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 510 | request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower() |
| 511 | headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % ( |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 512 | self.credentials[0], |
| 513 | self.challenge['realm'], |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 514 | self.challenge['snonce'], |
| 515 | cnonce, |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 516 | request_uri, |
| 517 | created, |
| 518 | request_digest, |
| 519 | keylist, |
| 520 | ) |
| 521 | |
| 522 | def response(self, response, content): |
jcgregorio | 6934caf | 2006-03-30 13:20:10 +0000 | [diff] [blame] | 523 | challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {}) |
| 524 | if challenge.get('reason') in ['integrity', 'stale']: |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 525 | return True |
| 526 | return False |
| 527 | |
| 528 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 529 | class WsseAuthentication(Authentication): |
| 530 | """This is thinly tested and should not be relied upon. |
| 531 | At this time there isn't any third party server to test against. |
| 532 | Blogger and TypePad implemented this algorithm at one point |
| 533 | but Blogger has since switched to Basic over HTTPS and |
| 534 | TypePad has implemented it wrong, by never issuing a 401 |
| 535 | challenge but instead requiring your client to telepathically know that |
| 536 | their endpoint is expecting WSSE profile="UsernameToken".""" |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 537 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
| 538 | Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 539 | |
| 540 | def request(self, method, request_uri, headers, content): |
| 541 | """Modify the request headers to add the appropriate |
| 542 | Authorization header.""" |
| 543 | headers['Authorization'] = 'WSSE profile="UsernameToken"' |
| 544 | iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) |
| 545 | cnonce = _cnonce() |
| 546 | password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1]) |
| 547 | headers['X-WSSE'] = 'UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"' % ( |
| 548 | self.credentials[0], |
| 549 | password_digest, |
| 550 | cnonce, |
| 551 | iso_now) |
| 552 | |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 553 | class GoogleLoginAuthentication(Authentication): |
| 554 | def __init__(self, credentials, host, request_uri, headers, response, content, http): |
| 555 | from urllib import urlencode |
| 556 | Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) |
jcgregorio | 82cc2a8 | 2007-02-04 03:38:04 +0000 | [diff] [blame] | 557 | challenge = _parse_www_authenticate(response, 'www-authenticate') |
| 558 | service = challenge['googlelogin'].get('service', 'xapi') |
| 559 | # Bloggger actually returns the service in the challenge |
| 560 | # For the rest we guess based on the URI |
| 561 | if service == 'xapi' and request_uri.find("calendar") > 0: |
| 562 | service = "cl" |
| 563 | # No point in guessing Base or Spreadsheet |
| 564 | #elif request_uri.find("spreadsheets") > 0: |
| 565 | # service = "wise" |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 566 | |
jcgregorio | 82cc2a8 | 2007-02-04 03:38:04 +0000 | [diff] [blame] | 567 | auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers['user-agent']) |
jcgregorio | ac26d46 | 2006-04-21 20:47:13 +0000 | [diff] [blame] | 568 | resp, content = self.http.request("https://www.google.com/accounts/ClientLogin", method="POST", body=urlencode(auth), headers={'Content-Type': 'application/x-www-form-urlencoded'}) |
| 569 | lines = content.split('\n') |
jcgregorio | 0bf7292 | 2006-04-27 01:38:17 +0000 | [diff] [blame] | 570 | d = dict([tuple(line.split("=", 1)) for line in lines if line]) |
| 571 | if resp.status == 403: |
| 572 | self.Auth = "" |
| 573 | else: |
| 574 | self.Auth = d['Auth'] |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 575 | |
| 576 | def request(self, method, request_uri, headers, content): |
| 577 | """Modify the request headers to add the appropriate |
| 578 | Authorization header.""" |
| 579 | headers['authorization'] = 'GoogleLogin Auth=' + self.Auth |
| 580 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 581 | |
| 582 | AUTH_SCHEME_CLASSES = { |
| 583 | "basic": BasicAuthentication, |
| 584 | "wsse": WsseAuthentication, |
jcgregorio | 22a9e16 | 2006-03-22 14:45:46 +0000 | [diff] [blame] | 585 | "digest": DigestAuthentication, |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 586 | "hmacdigest": HmacDigestAuthentication, |
| 587 | "googlelogin": GoogleLoginAuthentication |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 588 | } |
| 589 | |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 590 | AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 591 | |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 592 | def _md5(s): |
| 593 | return |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 594 | |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 595 | class FileCache: |
| 596 | """Uses a local directory as a store for cached files. |
| 597 | Not really safe to use if multiple threads or processes are going to |
| 598 | be running on the same cache. |
| 599 | """ |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 600 | def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 601 | self.cache = cache |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 602 | self.safe = safe |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 603 | if not os.path.exists(cache): |
| 604 | os.makedirs(self.cache) |
| 605 | |
| 606 | def get(self, key): |
| 607 | retval = None |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 608 | cacheFullPath = os.path.join(self.cache, self.safe(key)) |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 609 | try: |
| 610 | f = file(cacheFullPath, "r") |
| 611 | retval = f.read() |
| 612 | f.close() |
| 613 | except: |
| 614 | pass |
| 615 | return retval |
| 616 | |
| 617 | def set(self, key, value): |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 618 | cacheFullPath = os.path.join(self.cache, self.safe(key)) |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 619 | f = file(cacheFullPath, "w") |
| 620 | f.write(value) |
| 621 | f.close() |
| 622 | |
| 623 | def delete(self, key): |
jcgregorio | a46fe4e | 2006-11-16 04:13:45 +0000 | [diff] [blame] | 624 | cacheFullPath = os.path.join(self.cache, self.safe(key)) |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 625 | if os.path.exists(cacheFullPath): |
| 626 | os.remove(cacheFullPath) |
| 627 | |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 628 | class Credentials: |
| 629 | def __init__(self): |
| 630 | self.credentials = [] |
| 631 | |
| 632 | def add(self, name, password, domain=""): |
| 633 | self.credentials.append((domain.lower(), name, password)) |
| 634 | |
| 635 | def clear(self): |
| 636 | self.credentials = [] |
| 637 | |
| 638 | def iter(self, domain): |
| 639 | for (cdomain, name, password) in self.credentials: |
| 640 | if cdomain == "" or domain == cdomain: |
| 641 | yield (name, password) |
| 642 | |
| 643 | class KeyCerts(Credentials): |
| 644 | """Identical to Credentials except that |
| 645 | name/password are mapped to key/cert.""" |
| 646 | pass |
| 647 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 648 | class Http: |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 649 | """An HTTP client that handles all |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 650 | methods, caching, ETags, compression, |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 651 | HTTPS, Basic, Digest, WSSE, etc. |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 652 | """ |
| 653 | def __init__(self, cache=None): |
| 654 | # Map domain name to an httplib connection |
| 655 | self.connections = {} |
| 656 | # The location of the cache, for now a directory |
| 657 | # where cached responses are held. |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 658 | if cache and isinstance(cache, str): |
| 659 | self.cache = FileCache(cache) |
| 660 | else: |
| 661 | self.cache = cache |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 662 | |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 663 | # Name/password |
| 664 | self.credentials = Credentials() |
| 665 | |
| 666 | # Key/cert |
| 667 | self.certificates = KeyCerts() |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 668 | |
| 669 | # authorization objects |
| 670 | self.authorizations = [] |
| 671 | |
jcgregorio | 0bf7292 | 2006-04-27 01:38:17 +0000 | [diff] [blame] | 672 | self.follow_all_redirects = False |
| 673 | |
jcgregorio | 2518562 | 2006-10-28 05:12:34 +0000 | [diff] [blame] | 674 | self.ignore_etag = False |
| 675 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 676 | self.force_exception_to_status_code = True |
| 677 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 678 | def _auth_from_challenge(self, host, request_uri, headers, response, content): |
| 679 | """A generator that creates Authorization objects |
| 680 | that can be applied to requests. |
| 681 | """ |
| 682 | challenges = _parse_www_authenticate(response, 'www-authenticate') |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 683 | for cred in self.credentials.iter(host): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 684 | for scheme in AUTH_SCHEME_ORDER: |
| 685 | if challenges.has_key(scheme): |
jcgregorio | 6cbab7e | 2006-04-21 20:35:43 +0000 | [diff] [blame] | 686 | yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 687 | |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 688 | def add_credentials(self, name, password, domain=""): |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 689 | """Add a name and password that will be used |
| 690 | any time a request requires authentication.""" |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 691 | self.credentials.add(name, password, domain) |
| 692 | |
| 693 | def add_certificate(self, key, cert, domain): |
| 694 | """Add a key and cert that will be used |
| 695 | any time a request requires authentication.""" |
| 696 | self.certificates.add(key, cert, domain) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 697 | |
| 698 | def clear_credentials(self): |
jcgregorio | 900e05d | 2006-04-02 03:21:39 +0000 | [diff] [blame] | 699 | """Remove all the names and passwords |
| 700 | that are used for authentication""" |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 701 | self.credentials.clear() |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 702 | self.authorizations = [] |
| 703 | |
| 704 | def _conn_request(self, conn, request_uri, method, body, headers): |
| 705 | for i in range(2): |
| 706 | try: |
| 707 | conn.request(method, request_uri, body, headers) |
| 708 | response = conn.getresponse() |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 709 | except socket.gaierror: |
| 710 | conn.close() |
| 711 | raise ServerNotFoundError("Unable to find the server at %s" % conn.host) |
| 712 | except Exception, e: |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 713 | if i == 0: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 714 | conn.close() |
| 715 | conn.connect() |
| 716 | continue |
jcgregorio | 8421f27 | 2006-02-14 18:19:51 +0000 | [diff] [blame] | 717 | else: |
| 718 | raise |
| 719 | else: |
| 720 | content = response.read() |
| 721 | response = Response(response) |
| 722 | content = _decompressContent(response, content) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 723 | |
| 724 | break; |
| 725 | return (response, content) |
| 726 | |
| 727 | |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 728 | def _request(self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 729 | """Do the actual request using the connection object |
| 730 | and also follow one level of redirects if necessary""" |
| 731 | |
| 732 | auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)] |
| 733 | auth = auths and sorted(auths)[0][1] or None |
| 734 | if auth: |
| 735 | auth.request(method, request_uri, headers, body) |
| 736 | |
| 737 | (response, content) = self._conn_request(conn, request_uri, method, body, headers) |
| 738 | |
| 739 | if auth: |
| 740 | if auth.response(response, body): |
| 741 | auth.request(method, request_uri, headers, body) |
| 742 | (response, content) = self._conn_request(conn, request_uri, method, body, headers ) |
| 743 | response._stale_digest = 1 |
| 744 | |
| 745 | if response.status == 401: |
| 746 | for authorization in self._auth_from_challenge(host, request_uri, headers, response, content): |
| 747 | authorization.request(method, request_uri, headers, body) |
| 748 | (response, content) = self._conn_request(conn, request_uri, method, body, headers, ) |
| 749 | if response.status != 401: |
| 750 | self.authorizations.append(authorization) |
| 751 | authorization.response(response, body) |
| 752 | break |
| 753 | |
jcgregorio | 0bf7292 | 2006-04-27 01:38:17 +0000 | [diff] [blame] | 754 | if (self.follow_all_redirects or method in ["GET", "HEAD"]) or response.status == 303: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 755 | if response.status in [300, 301, 302, 303, 307]: |
| 756 | # Pick out the location header and basically start from the beginning |
| 757 | # remembering first to strip the ETag header and decrement our 'depth' |
| 758 | if redirections: |
| 759 | if not response.has_key('location') and response.status != 300: |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 760 | raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content) |
jcgregorio | 4607553 | 2006-11-04 22:18:55 +0000 | [diff] [blame] | 761 | # Fix-up relative redirects (which violate an RFC 2616 MUST) |
| 762 | if response.has_key('location'): |
| 763 | location = response['location'] |
| 764 | (scheme, authority, path, query, fragment) = parse_uri(location) |
| 765 | if authority == None: |
| 766 | response['location'] = urlparse.urljoin(absolute_uri, location) |
jcgregorio | 0bf7292 | 2006-04-27 01:38:17 +0000 | [diff] [blame] | 767 | if response.status == 301 and method in ["GET", "HEAD"]: |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 768 | response['-x-permanent-redirect-url'] = response['location'] |
jcgregorio | 772adc8 | 2006-11-17 21:52:34 +0000 | [diff] [blame] | 769 | if not response.has_key('content-location'): |
| 770 | response['content-location'] = absolute_uri |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 771 | _updateCache(headers, response, content, self.cache, cachekey) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 772 | if headers.has_key('if-none-match'): |
| 773 | del headers['if-none-match'] |
| 774 | if headers.has_key('if-modified-since'): |
| 775 | del headers['if-modified-since'] |
| 776 | if response.has_key('location'): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 777 | location = response['location'] |
jcgregorio | 4607553 | 2006-11-04 22:18:55 +0000 | [diff] [blame] | 778 | old_response = copy.deepcopy(response) |
jcgregorio | 772adc8 | 2006-11-17 21:52:34 +0000 | [diff] [blame] | 779 | if not old_response.has_key('content-location'): |
| 780 | old_response['content-location'] = absolute_uri |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 781 | redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method |
jcgregorio | 0bf7292 | 2006-04-27 01:38:17 +0000 | [diff] [blame] | 782 | (response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1) |
jcgregorio | a0713ab | 2006-07-01 05:21:34 +0000 | [diff] [blame] | 783 | response.previous = old_response |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 784 | else: |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 785 | raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 786 | elif response.status in [200, 203] and method == "GET": |
| 787 | # Don't cache 206's since we aren't going to handle byte range requests |
jcgregorio | 772adc8 | 2006-11-17 21:52:34 +0000 | [diff] [blame] | 788 | if not response.has_key('content-location'): |
| 789 | response['content-location'] = absolute_uri |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 790 | _updateCache(headers, response, content, self.cache, cachekey) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 791 | |
| 792 | return (response, content) |
| 793 | |
| 794 | def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS): |
jcgregorio | 076f54d | 2006-07-03 17:34:16 +0000 | [diff] [blame] | 795 | """ Performs a single HTTP request. |
| 796 | The 'uri' is the URI of the HTTP resource and can begin |
| 797 | with either 'http' or 'https'. The value of 'uri' must be an absolute URI. |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 798 | |
jcgregorio | 076f54d | 2006-07-03 17:34:16 +0000 | [diff] [blame] | 799 | The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc. |
| 800 | There is no restriction on the methods allowed. |
| 801 | |
| 802 | The 'body' is the entity body to be sent with the request. It is a string |
| 803 | object. |
| 804 | |
| 805 | Any extra headers that are to be sent with the request should be provided in the |
| 806 | 'headers' dictionary. |
| 807 | |
| 808 | The maximum number of redirect to follow before raising an |
| 809 | exception is 'redirections. The default is 5. |
| 810 | |
| 811 | The return value is a tuple of (response, content), the first |
| 812 | being and instance of the 'Response' class, the second being |
| 813 | a string that contains the response entity body. |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 814 | """ |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 815 | try: |
| 816 | if headers is None: |
| 817 | headers = {} |
jcgregorio | de8238d | 2007-03-07 19:08:26 +0000 | [diff] [blame] | 818 | else: |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 819 | headers = _normalize_headers(headers) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 820 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 821 | if not headers.has_key('user-agent'): |
| 822 | headers['user-agent'] = "Python-httplib2/%s" % __version__ |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 823 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 824 | uri = iri2uri(uri) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 825 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 826 | (scheme, authority, request_uri, defrag_uri) = urlnorm(uri) |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 827 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 828 | conn_key = scheme+":"+authority |
| 829 | if conn_key in self.connections: |
| 830 | conn = self.connections[conn_key] |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 831 | else: |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 832 | connection_type = (scheme == 'https') and httplib.HTTPSConnection or httplib.HTTPConnection |
| 833 | certs = list(self.certificates.iter(authority)) |
| 834 | if scheme == 'https' and certs: |
| 835 | conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0], cert_file=certs[0][1]) |
| 836 | else: |
| 837 | conn = self.connections[conn_key] = connection_type(authority) |
| 838 | conn.set_debuglevel(debuglevel) |
jcgregorio | e4ce13e | 2006-04-02 03:05:08 +0000 | [diff] [blame] | 839 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 840 | if method in ["GET", "HEAD"] and 'range' not in headers: |
| 841 | headers['accept-encoding'] = 'compress, gzip' |
jcgregorio | 82d99d7 | 2006-05-17 19:18:03 +0000 | [diff] [blame] | 842 | |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 843 | info = email.Message.Message() |
| 844 | cached_value = None |
| 845 | if self.cache: |
| 846 | cachekey = defrag_uri |
| 847 | cached_value = self.cache.get(cachekey) |
| 848 | if cached_value: |
| 849 | try: |
| 850 | info = email.message_from_string(cached_value) |
| 851 | content = cached_value.split('\r\n\r\n', 1)[1] |
| 852 | except Exception, e: |
| 853 | self.cache.delete(cachekey) |
| 854 | cachekey = None |
| 855 | cached_value = None |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 856 | else: |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 857 | cachekey = None |
| 858 | |
| 859 | if method in ["PUT"] and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers: |
| 860 | # http://www.w3.org/1999/04/Editing/ |
| 861 | headers['if-match'] = info['etag'] |
| 862 | |
| 863 | if method not in ["GET", "HEAD"] and self.cache and cachekey: |
| 864 | # RFC 2616 Section 13.10 |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 865 | self.cache.delete(cachekey) |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 866 | |
| 867 | if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: |
| 868 | if info.has_key('-x-permanent-redirect-url'): |
| 869 | # Should cached permanent redirects be counted in our redirection count? For now, yes. |
| 870 | (response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1) |
| 871 | response.previous = Response(info) |
| 872 | response.previous.fromcache = True |
| 873 | else: |
| 874 | # Determine our course of action: |
| 875 | # Is the cached entry fresh or stale? |
| 876 | # Has the client requested a non-cached response? |
| 877 | # |
| 878 | # There seems to be three possible answers: |
| 879 | # 1. [FRESH] Return the cache entry w/o doing a GET |
| 880 | # 2. [STALE] Do the GET (but add in cache validators if available) |
| 881 | # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request |
| 882 | entry_disposition = _entry_disposition(info, headers) |
| 883 | |
| 884 | if entry_disposition == "FRESH": |
| 885 | if not cached_value: |
| 886 | info['status'] = '504' |
| 887 | content = "" |
| 888 | response = Response(info) |
| 889 | if cached_value: |
| 890 | response.fromcache = True |
| 891 | return (response, content) |
| 892 | |
| 893 | if entry_disposition == "STALE": |
| 894 | if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers: |
| 895 | headers['if-none-match'] = info['etag'] |
| 896 | if info.has_key('last-modified') and not 'last-modified' in headers: |
| 897 | headers['if-modified-since'] = info['last-modified'] |
| 898 | elif entry_disposition == "TRANSPARENT": |
| 899 | pass |
| 900 | |
| 901 | (response, new_content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) |
| 902 | |
| 903 | if response.status == 304 and method == "GET": |
| 904 | # Rewrite the cache entry with the new end-to-end headers |
| 905 | # Take all headers that are in response |
| 906 | # and overwrite their values in info. |
| 907 | # unless they are hop-by-hop, or are listed in the connection header. |
| 908 | |
| 909 | for key in _get_end2end_headers(response): |
| 910 | info[key] = response[key] |
| 911 | merged_response = Response(info) |
| 912 | if hasattr(response, "_stale_digest"): |
| 913 | merged_response._stale_digest = response._stale_digest |
| 914 | _updateCache(headers, merged_response, content, self.cache, cachekey) |
| 915 | response = merged_response |
| 916 | response.status = 200 |
| 917 | response.fromcache = True |
| 918 | |
| 919 | elif response.status == 200: |
| 920 | content = new_content |
| 921 | else: |
| 922 | self.cache.delete(cachekey) |
| 923 | content = new_content |
| 924 | else: |
| 925 | (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) |
| 926 | except Exception, e: |
| 927 | if self.force_exception_to_status_code: |
| 928 | if isinstance(e, HttpLib2ErrorWithResponse): |
| 929 | response = e.response |
| 930 | content = e.content |
| 931 | response.status = 500 |
| 932 | response.reason = str(e) |
| 933 | elif isinstance(e, socket.timeout): |
| 934 | content = "Request Timeout" |
| 935 | response = Response( { |
| 936 | "content-type": "text/plain", |
| 937 | "status": "408", |
| 938 | "content-length": len(content) |
| 939 | }) |
| 940 | response.reason = "Request Timeout" |
| 941 | else: |
| 942 | content = str(e) |
| 943 | response = Response( { |
| 944 | "content-type": "text/plain", |
| 945 | "status": "400", |
| 946 | "content-length": len(content) |
| 947 | }) |
| 948 | response.reason = "Bad Request" |
| 949 | else: |
| 950 | raise |
| 951 | |
| 952 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 953 | return (response, content) |
| 954 | |
| 955 | |
| 956 | |
| 957 | class Response(dict): |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 958 | """An object more like email.Message than httplib.HTTPResponse.""" |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 959 | |
| 960 | """Is this response from our local cache""" |
| 961 | fromcache = False |
| 962 | |
| 963 | """HTTP protocol version used by server. 10 for HTTP/1.0, 11 for HTTP/1.1. """ |
| 964 | version = 11 |
| 965 | |
| 966 | "Status code returned by server. " |
| 967 | status = 200 |
| 968 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 969 | """Reason phrase returned by server.""" |
jcgregorio | 36140b5 | 2006-06-13 02:17:52 +0000 | [diff] [blame] | 970 | reason = "Ok" |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 971 | |
jcgregorio | a0713ab | 2006-07-01 05:21:34 +0000 | [diff] [blame] | 972 | previous = None |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 973 | |
| 974 | def __init__(self, info): |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 975 | # info is either an email.Message or |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 976 | # an httplib.HTTPResponse object. |
| 977 | if isinstance(info, httplib.HTTPResponse): |
jcgregorio | a0713ab | 2006-07-01 05:21:34 +0000 | [diff] [blame] | 978 | for key, value in info.getheaders(): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 979 | self[key] = value |
| 980 | self.status = info.status |
| 981 | self['status'] = str(self.status) |
| 982 | self.reason = info.reason |
| 983 | self.version = info.version |
jcgregorio | 11eb4f1 | 2006-11-17 14:59:26 +0000 | [diff] [blame] | 984 | elif isinstance(info, email.Message.Message): |
jcgregorio | a0713ab | 2006-07-01 05:21:34 +0000 | [diff] [blame] | 985 | for key, value in info.items(): |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 986 | self[key] = value |
| 987 | self.status = int(self['status']) |
jcgregorio | 07a9a4a | 2007-03-08 21:18:39 +0000 | [diff] [blame^] | 988 | else: |
| 989 | for key, value in info.iteritems(): |
| 990 | self[key] = value |
| 991 | self.status = int(self.get('status', self.status)) |
| 992 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 993 | |
jcgregorio | 153f588 | 2006-11-06 03:33:24 +0000 | [diff] [blame] | 994 | def __getattr__(self, name): |
| 995 | if name == 'dict': |
| 996 | return self |
| 997 | else: |
| 998 | raise AttributeError, name |
| 999 | |
jcgregorio | 2d66d4f | 2006-02-07 05:34:14 +0000 | [diff] [blame] | 1000 | |