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

Side by Side Diff: third_party/google-endpoints/httplib2/__init__.py

Issue 2666783008: Add google-endpoints to third_party/. (Closed)
Patch Set: Created 3 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
OLDNEW
(Empty)
1 from __future__ import generators
2 """
3 httplib2
4
5 A caching http interface that supports ETags and gzip
6 to conserve bandwidth.
7
8 Requires Python 2.3 or later
9
10 Changelog:
11 2007-08-18, Rick: Modified so it's able to use a socks proxy if needed.
12
13 """
14
15 __author__ = "Joe Gregorio (joe@bitworking.org)"
16 __copyright__ = "Copyright 2006, Joe Gregorio"
17 __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
18 "James Antill",
19 "Xavier Verges Farrero",
20 "Jonathan Feinberg",
21 "Blair Zajac",
22 "Sam Ruby",
23 "Louis Nyffenegger"]
24 __license__ = "MIT"
25 __version__ = "0.9.2"
26
27 import re
28 import sys
29 import email
30 import email.Utils
31 import email.Message
32 import email.FeedParser
33 import StringIO
34 import gzip
35 import zlib
36 import httplib
37 import urlparse
38 import urllib
39 import base64
40 import os
41 import copy
42 import calendar
43 import time
44 import random
45 import errno
46 try:
47 from hashlib import sha1 as _sha, md5 as _md5
48 except ImportError:
49 # prior to Python 2.5, these were separate modules
50 import sha
51 import md5
52 _sha = sha.new
53 _md5 = md5.new
54 import hmac
55 from gettext import gettext as _
56 import socket
57
58 try:
59 from httplib2 import socks
60 except ImportError:
61 try:
62 import socks
63 except (ImportError, AttributeError):
64 socks = None
65
66 # Build the appropriate socket wrapper for ssl
67 try:
68 import ssl # python 2.6
69 ssl_SSLError = ssl.SSLError
70 def _ssl_wrap_socket(sock, key_file, cert_file,
71 disable_validation, ca_certs):
72 if disable_validation:
73 cert_reqs = ssl.CERT_NONE
74 else:
75 cert_reqs = ssl.CERT_REQUIRED
76 # We should be specifying SSL version 3 or TLS v1, but the ssl module
77 # doesn't expose the necessary knobs. So we need to go with the default
78 # of SSLv23.
79 return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file,
80 cert_reqs=cert_reqs, ca_certs=ca_certs)
81 except (AttributeError, ImportError):
82 ssl_SSLError = None
83 def _ssl_wrap_socket(sock, key_file, cert_file,
84 disable_validation, ca_certs):
85 if not disable_validation:
86 raise CertificateValidationUnsupported(
87 "SSL certificate validation is not supported without "
88 "the ssl module installed. To avoid this error, install "
89 "the ssl module, or explicity disable validation.")
90 ssl_sock = socket.ssl(sock, key_file, cert_file)
91 return httplib.FakeSocket(sock, ssl_sock)
92
93
94 if sys.version_info >= (2,3):
95 from iri2uri import iri2uri
96 else:
97 def iri2uri(uri):
98 return uri
99
100 def has_timeout(timeout): # python 2.6
101 if hasattr(socket, '_GLOBAL_DEFAULT_TIMEOUT'):
102 return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TI MEOUT)
103 return (timeout is not None)
104
105 __all__ = [
106 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation',
107 'RedirectLimit', 'FailedToDecompressContent',
108 'UnimplementedDigestAuthOptionError',
109 'UnimplementedHmacDigestAuthOptionError',
110 'debuglevel', 'ProxiesUnavailableError']
111
112
113 # The httplib debug level, set to a non-zero value to get debug output
114 debuglevel = 0
115
116 # A request will be tried 'RETRIES' times if it fails at the socket/connection l evel.
117 RETRIES = 2
118
119 # Python 2.3 support
120 if sys.version_info < (2,4):
121 def sorted(seq):
122 seq.sort()
123 return seq
124
125 # Python 2.3 support
126 def HTTPResponse__getheaders(self):
127 """Return list of (header, value) tuples."""
128 if self.msg is None:
129 raise httplib.ResponseNotReady()
130 return self.msg.items()
131
132 if not hasattr(httplib.HTTPResponse, 'getheaders'):
133 httplib.HTTPResponse.getheaders = HTTPResponse__getheaders
134
135 # All exceptions raised here derive from HttpLib2Error
136 class HttpLib2Error(Exception): pass
137
138 # Some exceptions can be caught and optionally
139 # be turned back into responses.
140 class HttpLib2ErrorWithResponse(HttpLib2Error):
141 def __init__(self, desc, response, content):
142 self.response = response
143 self.content = content
144 HttpLib2Error.__init__(self, desc)
145
146 class RedirectMissingLocation(HttpLib2ErrorWithResponse): pass
147 class RedirectLimit(HttpLib2ErrorWithResponse): pass
148 class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass
149 class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
150 class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
151
152 class MalformedHeader(HttpLib2Error): pass
153 class RelativeURIError(HttpLib2Error): pass
154 class ServerNotFoundError(HttpLib2Error): pass
155 class ProxiesUnavailableError(HttpLib2Error): pass
156 class CertificateValidationUnsupported(HttpLib2Error): pass
157 class SSLHandshakeError(HttpLib2Error): pass
158 class NotSupportedOnThisPlatform(HttpLib2Error): pass
159 class CertificateHostnameMismatch(SSLHandshakeError):
160 def __init__(self, desc, host, cert):
161 HttpLib2Error.__init__(self, desc)
162 self.host = host
163 self.cert = cert
164
165 # Open Items:
166 # -----------
167 # Proxy support
168
169 # Are we removing the cached content too soon on PUT (only delete on 200 Maybe?)
170
171 # Pluggable cache storage (supports storing the cache in
172 # flat files by default. We need a plug-in architecture
173 # that can support Berkeley DB and Squid)
174
175 # == Known Issues ==
176 # Does not handle a resource that uses conneg and Last-Modified but no ETag as a cache validator.
177 # Does not handle Cache-Control: max-stale
178 # Does not use Age: headers when calculating cache freshness.
179
180
181 # The number of redirections to follow before giving up.
182 # Note that only GET redirects are automatically followed.
183 # Will also honor 301 requests by saving that info and never
184 # requesting that URI again.
185 DEFAULT_MAX_REDIRECTS = 5
186
187 try:
188 # Users can optionally provide a module that tells us where the CA_CERTS
189 # are located.
190 import ca_certs_locater
191 CA_CERTS = ca_certs_locater.get()
192 except ImportError:
193 # Default CA certificates file bundled with httplib2.
194 CA_CERTS = os.path.join(
195 os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt")
196
197 # Which headers are hop-by-hop headers by default
198 HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authoriza tion', 'te', 'trailers', 'transfer-encoding', 'upgrade']
199
200 def _get_end2end_headers(response):
201 hopbyhop = list(HOP_BY_HOP)
202 hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',' )])
203 return [header for header in response.keys() if header not in hopbyhop]
204
205 URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
206
207 def parse_uri(uri):
208 """Parses a URI using the regex given in Appendix B of RFC 3986.
209
210 (scheme, authority, path, query, fragment) = parse_uri(uri)
211 """
212 groups = URI.match(uri).groups()
213 return (groups[1], groups[3], groups[4], groups[6], groups[8])
214
215 def urlnorm(uri):
216 (scheme, authority, path, query, fragment) = parse_uri(uri)
217 if not scheme or not authority:
218 raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri)
219 authority = authority.lower()
220 scheme = scheme.lower()
221 if not path:
222 path = "/"
223 # Could do syntax based normalization of the URI before
224 # computing the digest. See Section 6.2.2 of Std 66.
225 request_uri = query and "?".join([path, query]) or path
226 scheme = scheme.lower()
227 defrag_uri = scheme + "://" + authority + request_uri
228 return scheme, authority, request_uri, defrag_uri
229
230
231 # Cache filename construction (original borrowed from Venus http://intertwingly. net/code/venus/)
232 re_url_scheme = re.compile(r'^\w+://')
233 re_slash = re.compile(r'[?/:|]+')
234
235 def safename(filename):
236 """Return a filename suitable for the cache.
237
238 Strips dangerous and common characters to create a filename we
239 can use to store the cache in.
240 """
241
242 try:
243 if re_url_scheme.match(filename):
244 if isinstance(filename,str):
245 filename = filename.decode('utf-8')
246 filename = filename.encode('idna')
247 else:
248 filename = filename.encode('idna')
249 except UnicodeError:
250 pass
251 if isinstance(filename,unicode):
252 filename=filename.encode('utf-8')
253 filemd5 = _md5(filename).hexdigest()
254 filename = re_url_scheme.sub("", filename)
255 filename = re_slash.sub(",", filename)
256
257 # limit length of filename
258 if len(filename)>200:
259 filename=filename[:200]
260 return ",".join((filename, filemd5))
261
262 NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
263 def _normalize_headers(headers):
264 return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (k ey, value) in headers.iteritems()])
265
266 def _parse_cache_control(headers):
267 retval = {}
268 if headers.has_key('cache-control'):
269 parts = headers['cache-control'].split(',')
270 parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)] ) for part in parts if -1 != part.find("=")]
271 parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == na me.find("=")]
272 retval = dict(parts_with_args + parts_wo_args)
273 return retval
274
275 # Whether to use a strict mode to parse WWW-Authenticate headers
276 # Might lead to bad results in case of ill-formed header value,
277 # so disabled by default, falling back to relaxed parsing.
278 # Set to true to turn on, usefull for testing servers.
279 USE_WWW_AUTH_STRICT_PARSING = 0
280
281 # In regex below:
282 # [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+ matches a "token" a s defined by HTTP
283 # "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?" matches a "quoted-s tring" as defined by HTTP, when LWS have already been replaced by a single space
284 # Actually, as an auth-param value can be either a token or a quoted-string, the y are combined in a single pattern which matches both:
285 # \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08 \x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?
286 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]+(?!\"))\"?)(.*)$")
287 WWW_AUTH_RELAXED = re.compile(r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\") (?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$")
288 UNQUOTE_PAIRS = re.compile(r'\\(.)')
289 def _parse_www_authenticate(headers, headername='www-authenticate'):
290 """Returns a dictionary of dictionaries, one dict
291 per auth_scheme."""
292 retval = {}
293 if headers.has_key(headername):
294 try:
295
296 authenticate = headers[headername].strip()
297 www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AU TH_RELAXED
298 while authenticate:
299 # Break off the scheme at the beginning of the line
300 if headername == 'authentication-info':
301 (auth_scheme, the_rest) = ('digest', authenticate)
302 else:
303 (auth_scheme, the_rest) = authenticate.split(" ", 1)
304 # Now loop over all the key value pairs that come after the sche me,
305 # being careful not to roll into the next scheme
306 match = www_auth.search(the_rest)
307 auth_params = {}
308 while match:
309 if match and len(match.groups()) == 3:
310 (key, value, the_rest) = match.groups()
311 auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', valu e) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')])
312 match = www_auth.search(the_rest)
313 retval[auth_scheme.lower()] = auth_params
314 authenticate = the_rest.strip()
315
316 except ValueError:
317 raise MalformedHeader("WWW-Authenticate")
318 return retval
319
320
321 def _entry_disposition(response_headers, request_headers):
322 """Determine freshness from the Date, Expires and Cache-Control headers.
323
324 We don't handle the following:
325
326 1. Cache-Control: max-stale
327 2. Age: headers are not used in the calculations.
328
329 Not that this algorithm is simpler than you might think
330 because we are operating as a private (non-shared) cache.
331 This lets us ignore 's-maxage'. We can also ignore
332 'proxy-invalidate' since we aren't a proxy.
333 We will never return a stale document as
334 fresh as a design decision, and thus the non-implementation
335 of 'max-stale'. This also lets us safely ignore 'must-revalidate'
336 since we operate as if every server has sent 'must-revalidate'.
337 Since we are private we get to ignore both 'public' and
338 'private' parameters. We also ignore 'no-transform' since
339 we don't do any transformations.
340 The 'no-store' parameter is handled at a higher level.
341 So the only Cache-Control parameters we look at are:
342
343 no-cache
344 only-if-cached
345 max-age
346 min-fresh
347 """
348
349 retval = "STALE"
350 cc = _parse_cache_control(request_headers)
351 cc_response = _parse_cache_control(response_headers)
352
353 if request_headers.has_key('pragma') and request_headers['pragma'].lower().f ind('no-cache') != -1:
354 retval = "TRANSPARENT"
355 if 'cache-control' not in request_headers:
356 request_headers['cache-control'] = 'no-cache'
357 elif cc.has_key('no-cache'):
358 retval = "TRANSPARENT"
359 elif cc_response.has_key('no-cache'):
360 retval = "STALE"
361 elif cc.has_key('only-if-cached'):
362 retval = "FRESH"
363 elif response_headers.has_key('date'):
364 date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date'] ))
365 now = time.time()
366 current_age = max(0, now - date)
367 if cc_response.has_key('max-age'):
368 try:
369 freshness_lifetime = int(cc_response['max-age'])
370 except ValueError:
371 freshness_lifetime = 0
372 elif response_headers.has_key('expires'):
373 expires = email.Utils.parsedate_tz(response_headers['expires'])
374 if None == expires:
375 freshness_lifetime = 0
376 else:
377 freshness_lifetime = max(0, calendar.timegm(expires) - date)
378 else:
379 freshness_lifetime = 0
380 if cc.has_key('max-age'):
381 try:
382 freshness_lifetime = int(cc['max-age'])
383 except ValueError:
384 freshness_lifetime = 0
385 if cc.has_key('min-fresh'):
386 try:
387 min_fresh = int(cc['min-fresh'])
388 except ValueError:
389 min_fresh = 0
390 current_age += min_fresh
391 if freshness_lifetime > current_age:
392 retval = "FRESH"
393 return retval
394
395 def _decompressContent(response, new_content):
396 content = new_content
397 try:
398 encoding = response.get('content-encoding', None)
399 if encoding in ['gzip', 'deflate']:
400 if encoding == 'gzip':
401 content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)). read()
402 if encoding == 'deflate':
403 content = zlib.decompress(content)
404 response['content-length'] = str(len(content))
405 # Record the historical presence of the encoding in a way the won't interfere.
406 response['-content-encoding'] = response['content-encoding']
407 del response['content-encoding']
408 except IOError:
409 content = ""
410 raise FailedToDecompressContent(_("Content purported to be compressed wi th %s but failed to decompress.") % response.get('content-encoding'), response, content)
411 return content
412
413 def _updateCache(request_headers, response_headers, content, cache, cachekey):
414 if cachekey:
415 cc = _parse_cache_control(request_headers)
416 cc_response = _parse_cache_control(response_headers)
417 if cc.has_key('no-store') or cc_response.has_key('no-store'):
418 cache.delete(cachekey)
419 else:
420 info = email.Message.Message()
421 for key, value in response_headers.iteritems():
422 if key not in ['status','content-encoding','transfer-encoding']:
423 info[key] = value
424
425 # Add annotations to the cache to indicate what headers
426 # are variant for this request.
427 vary = response_headers.get('vary', None)
428 if vary:
429 vary_headers = vary.lower().replace(' ', '').split(',')
430 for header in vary_headers:
431 key = '-varied-%s' % header
432 try:
433 info[key] = request_headers[header]
434 except KeyError:
435 pass
436
437 status = response_headers.status
438 if status == 304:
439 status = 200
440
441 status_header = 'status: %d\r\n' % status
442
443 header_str = info.as_string()
444
445 header_str = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", header_str)
446 text = "".join([status_header, header_str, content])
447
448 cache.set(cachekey, text)
449
450 def _cnonce():
451 dig = _md5("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] fo r i in range(20)])).hexdigest()
452 return dig[:16]
453
454 def _wsse_username_token(cnonce, iso_now, password):
455 return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest( )).strip()
456
457
458 # For credentials we need two things, first
459 # a pool of credential to try (not necesarily tied to BAsic, Digest, etc.)
460 # Then we also need a list of URIs that have already demanded authentication
461 # That list is tricky since sub-URIs can take the same auth, or the
462 # auth scheme may change as you descend the tree.
463 # So we also need each Auth instance to be able to tell us
464 # how close to the 'top' it is.
465
466 class Authentication(object):
467 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
468 (scheme, authority, path, query, fragment) = parse_uri(request_uri)
469 self.path = path
470 self.host = host
471 self.credentials = credentials
472 self.http = http
473
474 def depth(self, request_uri):
475 (scheme, authority, path, query, fragment) = parse_uri(request_uri)
476 return request_uri[len(self.path):].count("/")
477
478 def inscope(self, host, request_uri):
479 # XXX Should we normalize the request_uri?
480 (scheme, authority, path, query, fragment) = parse_uri(request_uri)
481 return (host == self.host) and path.startswith(self.path)
482
483 def request(self, method, request_uri, headers, content):
484 """Modify the request headers to add the appropriate
485 Authorization header. Over-ride this in sub-classes."""
486 pass
487
488 def response(self, response, content):
489 """Gives us a chance to update with new nonces
490 or such returned from the last authorized response.
491 Over-rise this in sub-classes if necessary.
492
493 Return TRUE is the request is to be retried, for
494 example Digest may return stale=true.
495 """
496 return False
497
498
499
500 class BasicAuthentication(Authentication):
501 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
502 Authentication.__init__(self, credentials, host, request_uri, headers, r esponse, content, http)
503
504 def request(self, method, request_uri, headers, content):
505 """Modify the request headers to add the appropriate
506 Authorization header."""
507 headers['authorization'] = 'Basic ' + base64.b64encode("%s:%s" % self.cr edentials).strip()
508
509
510 class DigestAuthentication(Authentication):
511 """Only do qop='auth' and MD5, since that
512 is all Apache currently implements"""
513 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
514 Authentication.__init__(self, credentials, host, request_uri, headers, r esponse, content, http)
515 challenge = _parse_www_authenticate(response, 'www-authenticate')
516 self.challenge = challenge['digest']
517 qop = self.challenge.get('qop', 'auth')
518 self.challenge['qop'] = ('auth' in [x.strip() for x in qop.split()]) and 'auth' or None
519 if self.challenge['qop'] is None:
520 raise UnimplementedDigestAuthOptionError( _("Unsupported value for q op: %s." % qop))
521 self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upp er()
522 if self.challenge['algorithm'] != 'MD5':
523 raise UnimplementedDigestAuthOptionError( _("Unsupported value for a lgorithm: %s." % self.challenge['algorithm']))
524 self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ": ", self.credentials[1]])
525 self.challenge['nc'] = 1
526
527 def request(self, method, request_uri, headers, content, cnonce = None):
528 """Modify the request headers"""
529 H = lambda x: _md5(x).hexdigest()
530 KD = lambda s, d: H("%s:%s" % (s, d))
531 A2 = "".join([method, ":", request_uri])
532 self.challenge['cnonce'] = cnonce or _cnonce()
533 request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (
534 self.challenge['nonce'],
535 '%08x' % self.challenge['nc'],
536 self.challenge['cnonce'],
537 self.challenge['qop'], H(A2)))
538 headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s" , uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
539 self.credentials[0],
540 self.challenge['realm'],
541 self.challenge['nonce'],
542 request_uri,
543 self.challenge['algorithm'],
544 request_digest,
545 self.challenge['qop'],
546 self.challenge['nc'],
547 self.challenge['cnonce'])
548 if self.challenge.get('opaque'):
549 headers['authorization'] += ', opaque="%s"' % self.challenge['opaque ']
550 self.challenge['nc'] += 1
551
552 def response(self, response, content):
553 if not response.has_key('authentication-info'):
554 challenge = _parse_www_authenticate(response, 'www-authenticate').ge t('digest', {})
555 if 'true' == challenge.get('stale'):
556 self.challenge['nonce'] = challenge['nonce']
557 self.challenge['nc'] = 1
558 return True
559 else:
560 updated_challenge = _parse_www_authenticate(response, 'authenticatio n-info').get('digest', {})
561
562 if updated_challenge.has_key('nextnonce'):
563 self.challenge['nonce'] = updated_challenge['nextnonce']
564 self.challenge['nc'] = 1
565 return False
566
567
568 class HmacDigestAuthentication(Authentication):
569 """Adapted from Robert Sayre's code and DigestAuthentication above."""
570 __author__ = "Thomas Broyer (t.broyer@ltgt.net)"
571
572 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
573 Authentication.__init__(self, credentials, host, request_uri, headers, r esponse, content, http)
574 challenge = _parse_www_authenticate(response, 'www-authenticate')
575 self.challenge = challenge['hmacdigest']
576 # TODO: self.challenge['domain']
577 self.challenge['reason'] = self.challenge.get('reason', 'unauthorized')
578 if self.challenge['reason'] not in ['unauthorized', 'integrity']:
579 self.challenge['reason'] = 'unauthorized'
580 self.challenge['salt'] = self.challenge.get('salt', '')
581 if not self.challenge.get('snonce'):
582 raise UnimplementedHmacDigestAuthOptionError( _("The challenge doesn 't contain a server nonce, or this one is empty."))
583 self.challenge['algorithm'] = self.challenge.get('algorithm', 'HMAC-SHA- 1')
584 if self.challenge['algorithm'] not in ['HMAC-SHA-1', 'HMAC-MD5']:
585 raise UnimplementedHmacDigestAuthOptionError( _("Unsupported value f or algorithm: %s." % self.challenge['algorithm']))
586 self.challenge['pw-algorithm'] = self.challenge.get('pw-algorithm', 'SHA -1')
587 if self.challenge['pw-algorithm'] not in ['SHA-1', 'MD5']:
588 raise UnimplementedHmacDigestAuthOptionError( _("Unsupported value f or pw-algorithm: %s." % self.challenge['pw-algorithm']))
589 if self.challenge['algorithm'] == 'HMAC-MD5':
590 self.hashmod = _md5
591 else:
592 self.hashmod = _sha
593 if self.challenge['pw-algorithm'] == 'MD5':
594 self.pwhashmod = _md5
595 else:
596 self.pwhashmod = _sha
597 self.key = "".join([self.credentials[0], ":",
598 self.pwhashmod.new("".join([self.credentials[1], sel f.challenge['salt']])).hexdigest().lower(),
599 ":", self.challenge['realm']])
600 self.key = self.pwhashmod.new(self.key).hexdigest().lower()
601
602 def request(self, method, request_uri, headers, content):
603 """Modify the request headers"""
604 keys = _get_end2end_headers(headers)
605 keylist = "".join(["%s " % k for k in keys])
606 headers_val = "".join([headers[k] for k in keys])
607 created = time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime())
608 cnonce = _cnonce()
609 request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.c hallenge['snonce'], headers_val)
610 request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdi gest().lower()
611 headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce ="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
612 self.credentials[0],
613 self.challenge['realm'],
614 self.challenge['snonce'],
615 cnonce,
616 request_uri,
617 created,
618 request_digest,
619 keylist)
620
621 def response(self, response, content):
622 challenge = _parse_www_authenticate(response, 'www-authenticate').get('h macdigest', {})
623 if challenge.get('reason') in ['integrity', 'stale']:
624 return True
625 return False
626
627
628 class WsseAuthentication(Authentication):
629 """This is thinly tested and should not be relied upon.
630 At this time there isn't any third party server to test against.
631 Blogger and TypePad implemented this algorithm at one point
632 but Blogger has since switched to Basic over HTTPS and
633 TypePad has implemented it wrong, by never issuing a 401
634 challenge but instead requiring your client to telepathically know that
635 their endpoint is expecting WSSE profile="UsernameToken"."""
636 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
637 Authentication.__init__(self, credentials, host, request_uri, headers, r esponse, content, http)
638
639 def request(self, method, request_uri, headers, content):
640 """Modify the request headers to add the appropriate
641 Authorization header."""
642 headers['authorization'] = 'WSSE profile="UsernameToken"'
643 iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
644 cnonce = _cnonce()
645 password_digest = _wsse_username_token(cnonce, iso_now, self.credentials [1])
646 headers['X-WSSE'] = 'UsernameToken Username="%s", PasswordDigest="%s", N once="%s", Created="%s"' % (
647 self.credentials[0],
648 password_digest,
649 cnonce,
650 iso_now)
651
652 class GoogleLoginAuthentication(Authentication):
653 def __init__(self, credentials, host, request_uri, headers, response, conten t, http):
654 from urllib import urlencode
655 Authentication.__init__(self, credentials, host, request_uri, headers, r esponse, content, http)
656 challenge = _parse_www_authenticate(response, 'www-authenticate')
657 service = challenge['googlelogin'].get('service', 'xapi')
658 # Bloggger actually returns the service in the challenge
659 # For the rest we guess based on the URI
660 if service == 'xapi' and request_uri.find("calendar") > 0:
661 service = "cl"
662 # No point in guessing Base or Spreadsheet
663 #elif request_uri.find("spreadsheets") > 0:
664 # service = "wise"
665
666 auth = dict(Email=credentials[0], Passwd=credentials[1], service=service , source=headers['user-agent'])
667 resp, content = self.http.request("https://www.google.com/accounts/Clien tLogin", method="POST", body=urlencode(auth), headers={'Content-Type': 'applicat ion/x-www-form-urlencoded'})
668 lines = content.split('\n')
669 d = dict([tuple(line.split("=", 1)) for line in lines if line])
670 if resp.status == 403:
671 self.Auth = ""
672 else:
673 self.Auth = d['Auth']
674
675 def request(self, method, request_uri, headers, content):
676 """Modify the request headers to add the appropriate
677 Authorization header."""
678 headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
679
680
681 AUTH_SCHEME_CLASSES = {
682 "basic": BasicAuthentication,
683 "wsse": WsseAuthentication,
684 "digest": DigestAuthentication,
685 "hmacdigest": HmacDigestAuthentication,
686 "googlelogin": GoogleLoginAuthentication
687 }
688
689 AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
690
691 class FileCache(object):
692 """Uses a local directory as a store for cached files.
693 Not really safe to use if multiple threads or processes are going to
694 be running on the same cache.
695 """
696 def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).he xdigest() for the old behavior
697 self.cache = cache
698 self.safe = safe
699 if not os.path.exists(cache):
700 os.makedirs(self.cache)
701
702 def get(self, key):
703 retval = None
704 cacheFullPath = os.path.join(self.cache, self.safe(key))
705 try:
706 f = file(cacheFullPath, "rb")
707 retval = f.read()
708 f.close()
709 except IOError:
710 pass
711 return retval
712
713 def set(self, key, value):
714 cacheFullPath = os.path.join(self.cache, self.safe(key))
715 f = file(cacheFullPath, "wb")
716 f.write(value)
717 f.close()
718
719 def delete(self, key):
720 cacheFullPath = os.path.join(self.cache, self.safe(key))
721 if os.path.exists(cacheFullPath):
722 os.remove(cacheFullPath)
723
724 class Credentials(object):
725 def __init__(self):
726 self.credentials = []
727
728 def add(self, name, password, domain=""):
729 self.credentials.append((domain.lower(), name, password))
730
731 def clear(self):
732 self.credentials = []
733
734 def iter(self, domain):
735 for (cdomain, name, password) in self.credentials:
736 if cdomain == "" or domain == cdomain:
737 yield (name, password)
738
739 class KeyCerts(Credentials):
740 """Identical to Credentials except that
741 name/password are mapped to key/cert."""
742 pass
743
744 class AllHosts(object):
745 pass
746
747 class ProxyInfo(object):
748 """Collect information required to use a proxy."""
749 bypass_hosts = ()
750
751 def __init__(self, proxy_type, proxy_host, proxy_port,
752 proxy_rdns=True, proxy_user=None, proxy_pass=None):
753 """
754 Args:
755 proxy_type: The type of proxy server. This must be set to one of
756 socks.PROXY_TYPE_XXX constants. For example:
757
758 p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP,
759 proxy_host='localhost', proxy_port=8000)
760
761 proxy_host: The hostname or IP address of the proxy server.
762
763 proxy_port: The port that the proxy server is running on.
764
765 proxy_rdns: If True (default), DNS queries will not be performed
766 locally, and instead, handed to the proxy to resolve. This is useful
767 if the network does not allow resolution of non-local names. In
768 httplib2 0.9 and earlier, this defaulted to False.
769
770 proxy_user: The username used to authenticate with the proxy server.
771
772 proxy_pass: The password used to authenticate with the proxy server.
773 """
774 self.proxy_type = proxy_type
775 self.proxy_host = proxy_host
776 self.proxy_port = proxy_port
777 self.proxy_rdns = proxy_rdns
778 self.proxy_user = proxy_user
779 self.proxy_pass = proxy_pass
780
781 def astuple(self):
782 return (self.proxy_type, self.proxy_host, self.proxy_port,
783 self.proxy_rdns, self.proxy_user, self.proxy_pass)
784
785 def isgood(self):
786 return (self.proxy_host != None) and (self.proxy_port != None)
787
788 def applies_to(self, hostname):
789 return not self.bypass_host(hostname)
790
791 def bypass_host(self, hostname):
792 """Has this host been excluded from the proxy config"""
793 if self.bypass_hosts is AllHosts:
794 return True
795
796 bypass = False
797 for domain in self.bypass_hosts:
798 if hostname.endswith(domain):
799 bypass = True
800
801 return bypass
802
803
804 def proxy_info_from_environment(method='http'):
805 """
806 Read proxy info from the environment variables.
807 """
808 if method not in ['http', 'https']:
809 return
810
811 env_var = method + '_proxy'
812 url = os.environ.get(env_var, os.environ.get(env_var.upper()))
813 if not url:
814 return
815 pi = proxy_info_from_url(url, method)
816
817 no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', ''))
818 bypass_hosts = []
819 if no_proxy:
820 bypass_hosts = no_proxy.split(',')
821 # special case, no_proxy=* means all hosts bypassed
822 if no_proxy == '*':
823 bypass_hosts = AllHosts
824
825 pi.bypass_hosts = bypass_hosts
826 return pi
827
828 def proxy_info_from_url(url, method='http'):
829 """
830 Construct a ProxyInfo from a URL (such as http_proxy env var)
831 """
832 url = urlparse.urlparse(url)
833 username = None
834 password = None
835 port = None
836 if '@' in url[1]:
837 ident, host_port = url[1].split('@', 1)
838 if ':' in ident:
839 username, password = ident.split(':', 1)
840 else:
841 password = ident
842 else:
843 host_port = url[1]
844 if ':' in host_port:
845 host, port = host_port.split(':', 1)
846 else:
847 host = host_port
848
849 if port:
850 port = int(port)
851 else:
852 port = dict(https=443, http=80)[method]
853
854 proxy_type = 3 # socks.PROXY_TYPE_HTTP
855 return ProxyInfo(
856 proxy_type = proxy_type,
857 proxy_host = host,
858 proxy_port = port,
859 proxy_user = username or None,
860 proxy_pass = password or None,
861 )
862
863
864 class HTTPConnectionWithTimeout(httplib.HTTPConnection):
865 """
866 HTTPConnection subclass that supports timeouts
867
868 All timeouts are in seconds. If None is passed for timeout then
869 Python's default timeout for sockets will be used. See for example
870 the docs of socket.setdefaulttimeout():
871 http://docs.python.org/library/socket.html#socket.setdefaulttimeout
872 """
873
874 def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=No ne):
875 httplib.HTTPConnection.__init__(self, host, port, strict)
876 self.timeout = timeout
877 self.proxy_info = proxy_info
878
879 def connect(self):
880 """Connect to the host and port specified in __init__."""
881 # Mostly verbatim from httplib.py.
882 if self.proxy_info and socks is None:
883 raise ProxiesUnavailableError(
884 'Proxy support missing but proxy use was requested!')
885 msg = "getaddrinfo returns an empty list"
886 if self.proxy_info and self.proxy_info.isgood():
887 use_proxy = True
888 proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pa ss = self.proxy_info.astuple()
889
890 host = proxy_host
891 port = proxy_port
892 else:
893 use_proxy = False
894
895 host = self.host
896 port = self.port
897
898 for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
899 af, socktype, proto, canonname, sa = res
900 try:
901 if use_proxy:
902 self.sock = socks.socksocket(af, socktype, proto)
903 self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy _rdns, proxy_user, proxy_pass)
904 else:
905 self.sock = socket.socket(af, socktype, proto)
906 self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
907 # Different from httplib: support timeouts.
908 if has_timeout(self.timeout):
909 self.sock.settimeout(self.timeout)
910 # End of difference from httplib.
911 if self.debuglevel > 0:
912 print "connect: (%s, %s) ************" % (self.host, self.po rt)
913 if use_proxy:
914 print "proxy: %s ************" % str((proxy_host, proxy_ port, proxy_rdns, proxy_user, proxy_pass))
915
916 self.sock.connect((self.host, self.port) + sa[2:])
917 except socket.error, msg:
918 if self.debuglevel > 0:
919 print "connect fail: (%s, %s)" % (self.host, self.port)
920 if use_proxy:
921 print "proxy: %s" % str((proxy_host, proxy_port, proxy_r dns, proxy_user, proxy_pass))
922 if self.sock:
923 self.sock.close()
924 self.sock = None
925 continue
926 break
927 if not self.sock:
928 raise socket.error, msg
929
930 class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
931 """
932 This class allows communication via SSL.
933
934 All timeouts are in seconds. If None is passed for timeout then
935 Python's default timeout for sockets will be used. See for example
936 the docs of socket.setdefaulttimeout():
937 http://docs.python.org/library/socket.html#socket.setdefaulttimeout
938 """
939 def __init__(self, host, port=None, key_file=None, cert_file=None,
940 strict=None, timeout=None, proxy_info=None,
941 ca_certs=None, disable_ssl_certificate_validation=False):
942 httplib.HTTPSConnection.__init__(self, host, port=port,
943 key_file=key_file,
944 cert_file=cert_file, strict=strict)
945 self.timeout = timeout
946 self.proxy_info = proxy_info
947 if ca_certs is None:
948 ca_certs = CA_CERTS
949 self.ca_certs = ca_certs
950 self.disable_ssl_certificate_validation = \
951 disable_ssl_certificate_validation
952
953 # The following two methods were adapted from https_wrapper.py, released
954 # with the Google Appengine SDK at
955 # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google /appengine/tools/https_wrapper.py
956 # under the following license:
957 #
958 # Copyright 2007 Google Inc.
959 #
960 # Licensed under the Apache License, Version 2.0 (the "License");
961 # you may not use this file except in compliance with the License.
962 # You may obtain a copy of the License at
963 #
964 # http://www.apache.org/licenses/LICENSE-2.0
965 #
966 # Unless required by applicable law or agreed to in writing, software
967 # distributed under the License is distributed on an "AS IS" BASIS,
968 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
969 # See the License for the specific language governing permissions and
970 # limitations under the License.
971 #
972
973 def _GetValidHostsForCert(self, cert):
974 """Returns a list of valid host globs for an SSL certificate.
975
976 Args:
977 cert: A dictionary representing an SSL certificate.
978 Returns:
979 list: A list of valid host globs.
980 """
981 if 'subjectAltName' in cert:
982 return [x[1] for x in cert['subjectAltName']
983 if x[0].lower() == 'dns']
984 else:
985 return [x[0][1] for x in cert['subject']
986 if x[0][0].lower() == 'commonname']
987
988 def _ValidateCertificateHostname(self, cert, hostname):
989 """Validates that a given hostname is valid for an SSL certificate.
990
991 Args:
992 cert: A dictionary representing an SSL certificate.
993 hostname: The hostname to test.
994 Returns:
995 bool: Whether or not the hostname is valid for this certificate.
996 """
997 hosts = self._GetValidHostsForCert(cert)
998 for host in hosts:
999 host_re = host.replace('.', '\.').replace('*', '[^.]*')
1000 if re.search('^%s$' % (host_re,), hostname, re.I):
1001 return True
1002 return False
1003
1004 def connect(self):
1005 "Connect to a host on a given (SSL) port."
1006
1007 msg = "getaddrinfo returns an empty list"
1008 if self.proxy_info and self.proxy_info.isgood():
1009 use_proxy = True
1010 proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pa ss = self.proxy_info.astuple()
1011
1012 host = proxy_host
1013 port = proxy_port
1014 else:
1015 use_proxy = False
1016
1017 host = self.host
1018 port = self.port
1019
1020 address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
1021 for family, socktype, proto, canonname, sockaddr in address_info:
1022 try:
1023 if use_proxy:
1024 sock = socks.socksocket(family, socktype, proto)
1025
1026 sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns , proxy_user, proxy_pass)
1027 else:
1028 sock = socket.socket(family, socktype, proto)
1029 sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
1030
1031 if has_timeout(self.timeout):
1032 sock.settimeout(self.timeout)
1033 sock.connect((self.host, self.port))
1034 self.sock =_ssl_wrap_socket(
1035 sock, self.key_file, self.cert_file,
1036 self.disable_ssl_certificate_validation, self.ca_certs)
1037 if self.debuglevel > 0:
1038 print "connect: (%s, %s)" % (self.host, self.port)
1039 if use_proxy:
1040 print "proxy: %s" % str((proxy_host, proxy_port, proxy_r dns, proxy_user, proxy_pass))
1041 if not self.disable_ssl_certificate_validation:
1042 cert = self.sock.getpeercert()
1043 hostname = self.host.split(':', 0)[0]
1044 if not self._ValidateCertificateHostname(cert, hostname):
1045 raise CertificateHostnameMismatch(
1046 'Server presented certificate that does not match '
1047 'host %s: %s' % (hostname, cert), hostname, cert)
1048 except ssl_SSLError, e:
1049 if sock:
1050 sock.close()
1051 if self.sock:
1052 self.sock.close()
1053 self.sock = None
1054 # Unfortunately the ssl module doesn't seem to provide any way
1055 # to get at more detailed error information, in particular
1056 # whether the error is due to certificate validation or
1057 # something else (such as SSL protocol mismatch).
1058 if e.errno == ssl.SSL_ERROR_SSL:
1059 raise SSLHandshakeError(e)
1060 else:
1061 raise
1062 except (socket.timeout, socket.gaierror):
1063 raise
1064 except socket.error, msg:
1065 if self.debuglevel > 0:
1066 print "connect fail: (%s, %s)" % (self.host, self.port)
1067 if use_proxy:
1068 print "proxy: %s" % str((proxy_host, proxy_port, proxy_r dns, proxy_user, proxy_pass))
1069 if self.sock:
1070 self.sock.close()
1071 self.sock = None
1072 continue
1073 break
1074 if not self.sock:
1075 raise socket.error, msg
1076
1077 SCHEME_TO_CONNECTION = {
1078 'http': HTTPConnectionWithTimeout,
1079 'https': HTTPSConnectionWithTimeout
1080 }
1081
1082 # Use a different connection object for Google App Engine
1083 try:
1084 try:
1085 from google.appengine.api import apiproxy_stub_map
1086 if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None:
1087 raise ImportError # Bail out; we're not actually running on App Eng ine.
1088 from google.appengine.api.urlfetch import fetch
1089 from google.appengine.api.urlfetch import InvalidURLError
1090 except (ImportError, AttributeError):
1091 from google3.apphosting.api import apiproxy_stub_map
1092 if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None:
1093 raise ImportError # Bail out; we're not actually running on App Eng ine.
1094 from google3.apphosting.api.urlfetch import fetch
1095 from google3.apphosting.api.urlfetch import InvalidURLError
1096
1097 def _new_fixed_fetch(validate_certificate):
1098 def fixed_fetch(url, payload=None, method="GET", headers={},
1099 allow_truncated=False, follow_redirects=True,
1100 deadline=None):
1101 if deadline is None:
1102 deadline = socket.getdefaulttimeout() or 5
1103 return fetch(url, payload=payload, method=method, headers=headers,
1104 allow_truncated=allow_truncated,
1105 follow_redirects=follow_redirects, deadline=deadline,
1106 validate_certificate=validate_certificate)
1107 return fixed_fetch
1108
1109 class AppEngineHttpConnection(httplib.HTTPConnection):
1110 """Use httplib on App Engine, but compensate for its weirdness.
1111
1112 The parameters key_file, cert_file, proxy_info, ca_certs, and
1113 disable_ssl_certificate_validation are all dropped on the ground.
1114 """
1115 def __init__(self, host, port=None, key_file=None, cert_file=None,
1116 strict=None, timeout=None, proxy_info=None, ca_certs=None,
1117 disable_ssl_certificate_validation=False):
1118 httplib.HTTPConnection.__init__(self, host, port=port,
1119 strict=strict, timeout=timeout)
1120
1121 class AppEngineHttpsConnection(httplib.HTTPSConnection):
1122 """Same as AppEngineHttpConnection, but for HTTPS URIs."""
1123 def __init__(self, host, port=None, key_file=None, cert_file=None,
1124 strict=None, timeout=None, proxy_info=None, ca_certs=None,
1125 disable_ssl_certificate_validation=False):
1126 httplib.HTTPSConnection.__init__(self, host, port=port,
1127 key_file=key_file,
1128 cert_file=cert_file, strict=strict,
1129 timeout=timeout)
1130 self._fetch = _new_fixed_fetch(
1131 not disable_ssl_certificate_validation)
1132
1133 # Update the connection classes to use the Googel App Engine specific ones.
1134 SCHEME_TO_CONNECTION = {
1135 'http': AppEngineHttpConnection,
1136 'https': AppEngineHttpsConnection
1137 }
1138 except (ImportError, AttributeError):
1139 pass
1140
1141
1142 class Http(object):
1143 """An HTTP client that handles:
1144
1145 - all methods
1146 - caching
1147 - ETags
1148 - compression,
1149 - HTTPS
1150 - Basic
1151 - Digest
1152 - WSSE
1153
1154 and more.
1155 """
1156 def __init__(self, cache=None, timeout=None,
1157 proxy_info=proxy_info_from_environment,
1158 ca_certs=None, disable_ssl_certificate_validation=False):
1159 """If 'cache' is a string then it is used as a directory name for
1160 a disk cache. Otherwise it must be an object that supports the
1161 same interface as FileCache.
1162
1163 All timeouts are in seconds. If None is passed for timeout
1164 then Python's default timeout for sockets will be used. See
1165 for example the docs of socket.setdefaulttimeout():
1166 http://docs.python.org/library/socket.html#socket.setdefaulttimeout
1167
1168 `proxy_info` may be:
1169 - a callable that takes the http scheme ('http' or 'https') and
1170 returns a ProxyInfo instance per request. By default, uses
1171 proxy_nfo_from_environment.
1172 - a ProxyInfo instance (static proxy config).
1173 - None (proxy disabled).
1174
1175 ca_certs is the path of a file containing root CA certificates for SSL
1176 server certificate validation. By default, a CA cert file bundled with
1177 httplib2 is used.
1178
1179 If disable_ssl_certificate_validation is true, SSL cert validation will
1180 not be performed.
1181 """
1182 self.proxy_info = proxy_info
1183 self.ca_certs = ca_certs
1184 self.disable_ssl_certificate_validation = \
1185 disable_ssl_certificate_validation
1186
1187 # Map domain name to an httplib connection
1188 self.connections = {}
1189 # The location of the cache, for now a directory
1190 # where cached responses are held.
1191 if cache and isinstance(cache, basestring):
1192 self.cache = FileCache(cache)
1193 else:
1194 self.cache = cache
1195
1196 # Name/password
1197 self.credentials = Credentials()
1198
1199 # Key/cert
1200 self.certificates = KeyCerts()
1201
1202 # authorization objects
1203 self.authorizations = []
1204
1205 # If set to False then no redirects are followed, even safe ones.
1206 self.follow_redirects = True
1207
1208 # Which HTTP methods do we apply optimistic concurrency to, i.e.
1209 # which methods get an "if-match:" etag header added to them.
1210 self.optimistic_concurrency_methods = ["PUT", "PATCH"]
1211
1212 # If 'follow_redirects' is True, and this is set to True then
1213 # all redirecs are followed, including unsafe ones.
1214 self.follow_all_redirects = False
1215
1216 self.ignore_etag = False
1217
1218 self.force_exception_to_status_code = False
1219
1220 self.timeout = timeout
1221
1222 # Keep Authorization: headers on a redirect.
1223 self.forward_authorization_headers = False
1224
1225 def __getstate__(self):
1226 state_dict = copy.copy(self.__dict__)
1227 # In case request is augmented by some foreign object such as
1228 # credentials which handle auth
1229 if 'request' in state_dict:
1230 del state_dict['request']
1231 if 'connections' in state_dict:
1232 del state_dict['connections']
1233 return state_dict
1234
1235 def __setstate__(self, state):
1236 self.__dict__.update(state)
1237 self.connections = {}
1238
1239 def _auth_from_challenge(self, host, request_uri, headers, response, content ):
1240 """A generator that creates Authorization objects
1241 that can be applied to requests.
1242 """
1243 challenges = _parse_www_authenticate(response, 'www-authenticate')
1244 for cred in self.credentials.iter(host):
1245 for scheme in AUTH_SCHEME_ORDER:
1246 if challenges.has_key(scheme):
1247 yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, h eaders, response, content, self)
1248
1249 def add_credentials(self, name, password, domain=""):
1250 """Add a name and password that will be used
1251 any time a request requires authentication."""
1252 self.credentials.add(name, password, domain)
1253
1254 def add_certificate(self, key, cert, domain):
1255 """Add a key and cert that will be used
1256 any time a request requires authentication."""
1257 self.certificates.add(key, cert, domain)
1258
1259 def clear_credentials(self):
1260 """Remove all the names and passwords
1261 that are used for authentication"""
1262 self.credentials.clear()
1263 self.authorizations = []
1264
1265 def _conn_request(self, conn, request_uri, method, body, headers):
1266 i = 0
1267 seen_bad_status_line = False
1268 while i < RETRIES:
1269 i += 1
1270 try:
1271 if hasattr(conn, 'sock') and conn.sock is None:
1272 conn.connect()
1273 conn.request(method, request_uri, body, headers)
1274 except socket.timeout:
1275 raise
1276 except socket.gaierror:
1277 conn.close()
1278 raise ServerNotFoundError("Unable to find the server at %s" % co nn.host)
1279 except ssl_SSLError:
1280 conn.close()
1281 raise
1282 except socket.error, e:
1283 err = 0
1284 if hasattr(e, 'args'):
1285 err = getattr(e, 'args')[0]
1286 else:
1287 err = e.errno
1288 if err in (errno.ENETUNREACH, errno.EADDRNOTAVAIL) and i < RETRI ES:
1289 continue # retry on potentially transient socket errors
1290 raise
1291 except httplib.HTTPException:
1292 # Just because the server closed the connection doesn't apparent ly mean
1293 # that the server didn't send a response.
1294 if hasattr(conn, 'sock') and conn.sock is None:
1295 if i < RETRIES-1:
1296 conn.close()
1297 conn.connect()
1298 continue
1299 else:
1300 conn.close()
1301 raise
1302 if i < RETRIES-1:
1303 conn.close()
1304 conn.connect()
1305 continue
1306 try:
1307 response = conn.getresponse()
1308 except httplib.BadStatusLine:
1309 # If we get a BadStatusLine on the first try then that means
1310 # the connection just went stale, so retry regardless of the
1311 # number of RETRIES set.
1312 if not seen_bad_status_line and i == 1:
1313 i = 0
1314 seen_bad_status_line = True
1315 conn.close()
1316 conn.connect()
1317 continue
1318 else:
1319 conn.close()
1320 raise
1321 except (socket.error, httplib.HTTPException):
1322 if i < RETRIES-1:
1323 conn.close()
1324 conn.connect()
1325 continue
1326 else:
1327 conn.close()
1328 raise
1329 else:
1330 content = ""
1331 if method == "HEAD":
1332 conn.close()
1333 else:
1334 content = response.read()
1335 response = Response(response)
1336 if method != "HEAD":
1337 content = _decompressContent(response, content)
1338 break
1339 return (response, content)
1340
1341
1342 def _request(self, conn, host, absolute_uri, request_uri, method, body, head ers, redirections, cachekey):
1343 """Do the actual request using the connection object
1344 and also follow one level of redirects if necessary"""
1345
1346 auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
1347 auth = auths and sorted(auths)[0][1] or None
1348 if auth:
1349 auth.request(method, request_uri, headers, body)
1350
1351 (response, content) = self._conn_request(conn, request_uri, method, body , headers)
1352
1353 if auth:
1354 if auth.response(response, body):
1355 auth.request(method, request_uri, headers, body)
1356 (response, content) = self._conn_request(conn, request_uri, meth od, body, headers )
1357 response._stale_digest = 1
1358
1359 if response.status == 401:
1360 for authorization in self._auth_from_challenge(host, request_uri, he aders, response, content):
1361 authorization.request(method, request_uri, headers, body)
1362 (response, content) = self._conn_request(conn, request_uri, meth od, body, headers, )
1363 if response.status != 401:
1364 self.authorizations.append(authorization)
1365 authorization.response(response, body)
1366 break
1367
1368 if (self.follow_all_redirects or (method in ["GET", "HEAD"]) or response .status == 303):
1369 if self.follow_redirects and response.status in [300, 301, 302, 303, 307]:
1370 # Pick out the location header and basically start from the begi nning
1371 # remembering first to strip the ETag header and decrement our ' depth'
1372 if redirections:
1373 if not response.has_key('location') and response.status != 3 00:
1374 raise RedirectMissingLocation( _("Redirected but the res ponse is missing a Location: header."), response, content)
1375 # Fix-up relative redirects (which violate an RFC 2616 MUST)
1376 if response.has_key('location'):
1377 location = response['location']
1378 (scheme, authority, path, query, fragment) = parse_uri(l ocation)
1379 if authority == None:
1380 response['location'] = urlparse.urljoin(absolute_uri , location)
1381 if response.status == 301 and method in ["GET", "HEAD"]:
1382 response['-x-permanent-redirect-url'] = response['locati on']
1383 if not response.has_key('content-location'):
1384 response['content-location'] = absolute_uri
1385 _updateCache(headers, response, content, self.cache, cac hekey)
1386 if headers.has_key('if-none-match'):
1387 del headers['if-none-match']
1388 if headers.has_key('if-modified-since'):
1389 del headers['if-modified-since']
1390 if 'authorization' in headers and not self.forward_authoriza tion_headers:
1391 del headers['authorization']
1392 if response.has_key('location'):
1393 location = response['location']
1394 old_response = copy.deepcopy(response)
1395 if not old_response.has_key('content-location'):
1396 old_response['content-location'] = absolute_uri
1397 redirect_method = method
1398 if response.status in [302, 303]:
1399 redirect_method = "GET"
1400 body = None
1401 (response, content) = self.request(
1402 location, method=redirect_method,
1403 body=body, headers=headers,
1404 redirections=redirections - 1)
1405 response.previous = old_response
1406 else:
1407 raise RedirectLimit("Redirected more times than rediection_l imit allows.", response, content)
1408 elif response.status in [200, 203] and method in ["GET", "HEAD"]:
1409 # Don't cache 206's since we aren't going to handle byte range r equests
1410 if not response.has_key('content-location'):
1411 response['content-location'] = absolute_uri
1412 _updateCache(headers, response, content, self.cache, cachekey)
1413
1414 return (response, content)
1415
1416 def _normalize_headers(self, headers):
1417 return _normalize_headers(headers)
1418
1419 # Need to catch and rebrand some exceptions
1420 # Then need to optionally turn all exceptions into status codes
1421 # including all socket.* and httplib.* exceptions.
1422
1423
1424 def request(self, uri, method="GET", body=None, headers=None, redirections=D EFAULT_MAX_REDIRECTS, connection_type=None):
1425 """ Performs a single HTTP request.
1426
1427 The 'uri' is the URI of the HTTP resource and can begin with either
1428 'http' or 'https'. The value of 'uri' must be an absolute URI.
1429
1430 The 'method' is the HTTP method to perform, such as GET, POST, DELETE,
1431 etc. There is no restriction on the methods allowed.
1432
1433 The 'body' is the entity body to be sent with the request. It is a
1434 string object.
1435
1436 Any extra headers that are to be sent with the request should be
1437 provided in the 'headers' dictionary.
1438
1439 The maximum number of redirect to follow before raising an
1440 exception is 'redirections. The default is 5.
1441
1442 The return value is a tuple of (response, content), the first
1443 being and instance of the 'Response' class, the second being
1444 a string that contains the response entity body.
1445 """
1446 try:
1447 if headers is None:
1448 headers = {}
1449 else:
1450 headers = self._normalize_headers(headers)
1451
1452 if not headers.has_key('user-agent'):
1453 headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version_ _
1454
1455 uri = iri2uri(uri)
1456
1457 (scheme, authority, request_uri, defrag_uri) = urlnorm(uri)
1458 domain_port = authority.split(":")[0:2]
1459 if len(domain_port) == 2 and domain_port[1] == '443' and scheme == ' http':
1460 scheme = 'https'
1461 authority = domain_port[0]
1462
1463 proxy_info = self._get_proxy_info(scheme, authority)
1464
1465 conn_key = scheme+":"+authority
1466 if conn_key in self.connections:
1467 conn = self.connections[conn_key]
1468 else:
1469 if not connection_type:
1470 connection_type = SCHEME_TO_CONNECTION[scheme]
1471 certs = list(self.certificates.iter(authority))
1472 if scheme == 'https':
1473 if certs:
1474 conn = self.connections[conn_key] = connection_type(
1475 authority, key_file=certs[0][0],
1476 cert_file=certs[0][1], timeout=self.timeout,
1477 proxy_info=proxy_info,
1478 ca_certs=self.ca_certs,
1479 disable_ssl_certificate_validation=
1480 self.disable_ssl_certificate_validation)
1481 else:
1482 conn = self.connections[conn_key] = connection_type(
1483 authority, timeout=self.timeout,
1484 proxy_info=proxy_info,
1485 ca_certs=self.ca_certs,
1486 disable_ssl_certificate_validation=
1487 self.disable_ssl_certificate_validation)
1488 else:
1489 conn = self.connections[conn_key] = connection_type(
1490 authority, timeout=self.timeout,
1491 proxy_info=proxy_info)
1492 conn.set_debuglevel(debuglevel)
1493
1494 if 'range' not in headers and 'accept-encoding' not in headers:
1495 headers['accept-encoding'] = 'gzip, deflate'
1496
1497 info = email.Message.Message()
1498 cached_value = None
1499 if self.cache:
1500 cachekey = defrag_uri.encode('utf-8')
1501 cached_value = self.cache.get(cachekey)
1502 if cached_value:
1503 # info = email.message_from_string(cached_value)
1504 #
1505 # Need to replace the line above with the kludge below
1506 # to fix the non-existent bug not fixed in this
1507 # bug report: http://mail.python.org/pipermail/python-bugs-l ist/2005-September/030289.html
1508 try:
1509 info, content = cached_value.split('\r\n\r\n', 1)
1510 feedparser = email.FeedParser.FeedParser()
1511 feedparser.feed(info)
1512 info = feedparser.close()
1513 feedparser._parse = None
1514 except (IndexError, ValueError):
1515 self.cache.delete(cachekey)
1516 cachekey = None
1517 cached_value = None
1518 else:
1519 cachekey = None
1520
1521 if method in self.optimistic_concurrency_methods and self.cache and info.has_key('etag') and not self.ignore_etag and 'if-match' not in headers:
1522 # http://www.w3.org/1999/04/Editing/
1523 headers['if-match'] = info['etag']
1524
1525 if method not in ["GET", "HEAD"] and self.cache and cachekey:
1526 # RFC 2616 Section 13.10
1527 self.cache.delete(cachekey)
1528
1529 # Check the vary header in the cache to see if this request
1530 # matches what varies in the cache.
1531 if method in ['GET', 'HEAD'] and 'vary' in info:
1532 vary = info['vary']
1533 vary_headers = vary.lower().replace(' ', '').split(',')
1534 for header in vary_headers:
1535 key = '-varied-%s' % header
1536 value = info[key]
1537 if headers.get(header, None) != value:
1538 cached_value = None
1539 break
1540
1541 if cached_value and method in ["GET", "HEAD"] and self.cache and 'ra nge' not in headers:
1542 if info.has_key('-x-permanent-redirect-url'):
1543 # Should cached permanent redirects be counted in our redire ction count? For now, yes.
1544 if redirections <= 0:
1545 raise RedirectLimit("Redirected more times than rediecti on_limit allows.", {}, "")
1546 (response, new_content) = self.request(
1547 info['-x-permanent-redirect-url'], method='GET',
1548 headers=headers, redirections=redirections - 1)
1549 response.previous = Response(info)
1550 response.previous.fromcache = True
1551 else:
1552 # Determine our course of action:
1553 # Is the cached entry fresh or stale?
1554 # Has the client requested a non-cached response?
1555 #
1556 # There seems to be three possible answers:
1557 # 1. [FRESH] Return the cache entry w/o doing a GET
1558 # 2. [STALE] Do the GET (but add in cache validators if avai lable)
1559 # 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache- Control: no-cache) on the request
1560 entry_disposition = _entry_disposition(info, headers)
1561
1562 if entry_disposition == "FRESH":
1563 if not cached_value:
1564 info['status'] = '504'
1565 content = ""
1566 response = Response(info)
1567 if cached_value:
1568 response.fromcache = True
1569 return (response, content)
1570
1571 if entry_disposition == "STALE":
1572 if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers:
1573 headers['if-none-match'] = info['etag']
1574 if info.has_key('last-modified') and not 'last-modified' in headers:
1575 headers['if-modified-since'] = info['last-modified']
1576 elif entry_disposition == "TRANSPARENT":
1577 pass
1578
1579 (response, new_content) = self._request(conn, authority, uri , request_uri, method, body, headers, redirections, cachekey)
1580
1581 if response.status == 304 and method == "GET":
1582 # Rewrite the cache entry with the new end-to-end headers
1583 # Take all headers that are in response
1584 # and overwrite their values in info.
1585 # unless they are hop-by-hop, or are listed in the connectio n header.
1586
1587 for key in _get_end2end_headers(response):
1588 info[key] = response[key]
1589 merged_response = Response(info)
1590 if hasattr(response, "_stale_digest"):
1591 merged_response._stale_digest = response._stale_digest
1592 _updateCache(headers, merged_response, content, self.cache, cachekey)
1593 response = merged_response
1594 response.status = 200
1595 response.fromcache = True
1596
1597 elif response.status == 200:
1598 content = new_content
1599 else:
1600 self.cache.delete(cachekey)
1601 content = new_content
1602 else:
1603 cc = _parse_cache_control(headers)
1604 if cc.has_key('only-if-cached'):
1605 info['status'] = '504'
1606 response = Response(info)
1607 content = ""
1608 else:
1609 (response, content) = self._request(conn, authority, uri, re quest_uri, method, body, headers, redirections, cachekey)
1610 except Exception, e:
1611 if self.force_exception_to_status_code:
1612 if isinstance(e, HttpLib2ErrorWithResponse):
1613 response = e.response
1614 content = e.content
1615 response.status = 500
1616 response.reason = str(e)
1617 elif isinstance(e, socket.timeout):
1618 content = "Request Timeout"
1619 response = Response({
1620 "content-type": "text/plain",
1621 "status": "408",
1622 "content-length": len(content)
1623 })
1624 response.reason = "Request Timeout"
1625 else:
1626 content = str(e)
1627 response = Response({
1628 "content-type": "text/plain",
1629 "status": "400",
1630 "content-length": len(content)
1631 })
1632 response.reason = "Bad Request"
1633 else:
1634 raise
1635
1636
1637 return (response, content)
1638
1639 def _get_proxy_info(self, scheme, authority):
1640 """Return a ProxyInfo instance (or None) based on the scheme
1641 and authority.
1642 """
1643 hostname, port = urllib.splitport(authority)
1644 proxy_info = self.proxy_info
1645 if callable(proxy_info):
1646 proxy_info = proxy_info(scheme)
1647
1648 if (hasattr(proxy_info, 'applies_to')
1649 and not proxy_info.applies_to(hostname)):
1650 proxy_info = None
1651 return proxy_info
1652
1653
1654 class Response(dict):
1655 """An object more like email.Message than httplib.HTTPResponse."""
1656
1657 """Is this response from our local cache"""
1658 fromcache = False
1659
1660 """HTTP protocol version used by server. 10 for HTTP/1.0, 11 for HTTP/1.1. " ""
1661 version = 11
1662
1663 "Status code returned by server. "
1664 status = 200
1665
1666 """Reason phrase returned by server."""
1667 reason = "Ok"
1668
1669 previous = None
1670
1671 def __init__(self, info):
1672 # info is either an email.Message or
1673 # an httplib.HTTPResponse object.
1674 if isinstance(info, httplib.HTTPResponse):
1675 for key, value in info.getheaders():
1676 self[key.lower()] = value
1677 self.status = info.status
1678 self['status'] = str(self.status)
1679 self.reason = info.reason
1680 self.version = info.version
1681 elif isinstance(info, email.Message.Message):
1682 for key, value in info.items():
1683 self[key.lower()] = value
1684 self.status = int(self['status'])
1685 else:
1686 for key, value in info.iteritems():
1687 self[key.lower()] = value
1688 self.status = int(self.get('status', self.status))
1689 self.reason = self.get('reason', self.reason)
1690
1691
1692 def __getattr__(self, name):
1693 if name == 'dict':
1694 return self
1695 else:
1696 raise AttributeError, name
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698