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

Unified Diff: tools/telemetry/third_party/gsutil/third_party/httplib2/python2/httplib2/__init__.py

Issue 1258583006: Add gsutil 4.13 to telemetry/third_party (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: Undo all other changes so this just add gsutil to third_party Created 5 years, 5 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 side-by-side diff with in-line comments
Download patch
Index: tools/telemetry/third_party/gsutil/third_party/httplib2/python2/httplib2/__init__.py
diff --git a/chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py b/tools/telemetry/third_party/gsutil/third_party/httplib2/python2/httplib2/__init__.py
similarity index 60%
copy from chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py
copy to tools/telemetry/third_party/gsutil/third_party/httplib2/python2/httplib2/__init__.py
index 3cebcb32131153192d608fd86b4068c131958f61..58d9ce4c372126dd053dd300f73585983ac6d601 100644
--- a/chrome/common/extensions/docs/examples/apps/hello-python/httplib2/__init__.py
+++ b/tools/telemetry/third_party/gsutil/third_party/httplib2/python2/httplib2/__init__.py
@@ -3,7 +3,7 @@ from __future__ import generators
httplib2
A caching http interface that supports ETags and gzip
-to conserve bandwidth.
+to conserve bandwidth.
Requires Python 2.3 or later
@@ -15,17 +15,17 @@ Changelog:
__author__ = "Joe Gregorio (joe@bitworking.org)"
__copyright__ = "Copyright 2006, Joe Gregorio"
__contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
- "James Antill",
- "Xavier Verges Farrero",
- "Jonathan Feinberg",
- "Blair Zajac",
- "Sam Ruby",
- "Louis Nyffenegger"]
+ "James Antill",
+ "Xavier Verges Farrero",
+ "Jonathan Feinberg",
+ "Blair Zajac",
+ "Sam Ruby",
+ "Louis Nyffenegger"]
__license__ = "MIT"
-__version__ = "$Rev$"
+__version__ = "0.7.7"
-import re
-import sys
+import re
+import sys
import email
import email.Utils
import email.Message
@@ -35,16 +35,18 @@ import gzip
import zlib
import httplib
import urlparse
+import urllib
import base64
import os
import copy
import calendar
import time
import random
-# remove depracated warning in python2.6
+import errno
try:
from hashlib import sha1 as _sha, md5 as _md5
except ImportError:
+ # prior to Python 2.5, these were separate modules
import sha
import md5
_sha = sha.new
@@ -54,16 +56,37 @@ from gettext import gettext as _
import socket
try:
- import socks
+ from httplib2 import socks
except ImportError:
- socks = None
+ try:
+ import socks
+ except (ImportError, AttributeError):
+ socks = None
# Build the appropriate socket wrapper for ssl
try:
import ssl # python 2.6
- _ssl_wrap_socket = ssl.wrap_socket
-except ImportError:
- def _ssl_wrap_socket(sock, key_file, cert_file):
+ ssl_SSLError = ssl.SSLError
+ def _ssl_wrap_socket(sock, key_file, cert_file,
+ disable_validation, ca_certs):
+ if disable_validation:
+ cert_reqs = ssl.CERT_NONE
+ else:
+ cert_reqs = ssl.CERT_REQUIRED
+ # We should be specifying SSL version 3 or TLS v1, but the ssl module
+ # doesn't expose the necessary knobs. So we need to go with the default
+ # of SSLv23.
+ return ssl.wrap_socket(sock, keyfile=key_file, certfile=cert_file,
+ cert_reqs=cert_reqs, ca_certs=ca_certs)
+except (AttributeError, ImportError):
+ ssl_SSLError = None
+ def _ssl_wrap_socket(sock, key_file, cert_file,
+ disable_validation, ca_certs):
+ if not disable_validation:
+ raise CertificateValidationUnsupported(
+ "SSL certificate validation is not supported without "
+ "the ssl module installed. To avoid this error, install "
+ "the ssl module, or explicity disable validation.")
ssl_sock = socket.ssl(sock, key_file, cert_file)
return httplib.FakeSocket(sock, ssl_sock)
@@ -79,15 +102,19 @@ def has_timeout(timeout): # python 2.6
return (timeout is not None and timeout is not socket._GLOBAL_DEFAULT_TIMEOUT)
return (timeout is not None)
-__all__ = ['Http', 'Response', 'ProxyInfo', 'HttpLib2Error',
- 'RedirectMissingLocation', 'RedirectLimit', 'FailedToDecompressContent',
- 'UnimplementedDigestAuthOptionError', 'UnimplementedHmacDigestAuthOptionError',
- 'debuglevel']
+__all__ = [
+ 'Http', 'Response', 'ProxyInfo', 'HttpLib2Error', 'RedirectMissingLocation',
+ 'RedirectLimit', 'FailedToDecompressContent',
+ 'UnimplementedDigestAuthOptionError',
+ 'UnimplementedHmacDigestAuthOptionError',
+ 'debuglevel', 'ProxiesUnavailableError']
# The httplib debug level, set to a non-zero value to get debug output
debuglevel = 0
+# A request will be tried 'RETRIES' times if it fails at the socket/connection level.
+RETRIES = 2
# Python 2.3 support
if sys.version_info < (2,4):
@@ -108,8 +135,8 @@ if not hasattr(httplib.HTTPResponse, 'getheaders'):
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception): pass
-# Some exceptions can be caught and optionally
-# be turned back into responses.
+# Some exceptions can be caught and optionally
+# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
def __init__(self, desc, response, content):
self.response = response
@@ -122,8 +149,18 @@ class FailedToDecompressContent(HttpLib2ErrorWithResponse): pass
class UnimplementedDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
class UnimplementedHmacDigestAuthOptionError(HttpLib2ErrorWithResponse): pass
+class MalformedHeader(HttpLib2Error): pass
class RelativeURIError(HttpLib2Error): pass
class ServerNotFoundError(HttpLib2Error): pass
+class ProxiesUnavailableError(HttpLib2Error): pass
+class CertificateValidationUnsupported(HttpLib2Error): pass
+class SSLHandshakeError(HttpLib2Error): pass
+class NotSupportedOnThisPlatform(HttpLib2Error): pass
+class CertificateHostnameMismatch(SSLHandshakeError):
+ def __init__(self, desc, host, cert):
+ HttpLib2Error.__init__(self, desc)
+ self.host = host
+ self.cert = cert
# Open Items:
# -----------
@@ -147,6 +184,16 @@ class ServerNotFoundError(HttpLib2Error): pass
# requesting that URI again.
DEFAULT_MAX_REDIRECTS = 5
+try:
+ # Users can optionally provide a module that tells us where the CA_CERTS
+ # are located.
+ import ca_certs_locater
+ CA_CERTS = ca_certs_locater.get()
+except ImportError:
+ # Default CA certificates file bundled with httplib2.
+ CA_CERTS = os.path.join(
+ os.path.dirname(os.path.abspath(__file__ )), "cacerts.txt")
+
# Which headers are hop-by-hop headers by default
HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade']
@@ -171,7 +218,7 @@ def urlnorm(uri):
raise RelativeURIError("Only absolute URIs are allowed. uri = %s" % uri)
authority = authority.lower()
scheme = scheme.lower()
- if not path:
+ if not path:
path = "/"
# Could do syntax based normalization of the URI before
# computing the digest. See Section 6.2.2 of Std 66.
@@ -223,7 +270,7 @@ def _parse_cache_control(headers):
parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")]
parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")]
retval = dict(parts_with_args + parts_wo_args)
- return retval
+ return retval
# Whether to use a strict mode to parse WWW-Authenticate headers
# Might lead to bad results in case of ill-formed header value,
@@ -244,25 +291,30 @@ def _parse_www_authenticate(headers, headername='www-authenticate'):
per auth_scheme."""
retval = {}
if headers.has_key(headername):
- authenticate = headers[headername].strip()
- www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED
- while authenticate:
- # Break off the scheme at the beginning of the line
- if headername == 'authentication-info':
- (auth_scheme, the_rest) = ('digest', authenticate)
- else:
- (auth_scheme, the_rest) = authenticate.split(" ", 1)
- # Now loop over all the key value pairs that come after the scheme,
- # being careful not to roll into the next scheme
- match = www_auth.search(the_rest)
- auth_params = {}
- while match:
- if match and len(match.groups()) == 3:
- (key, value, the_rest) = match.groups()
- auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')])
+ try:
+
+ authenticate = headers[headername].strip()
+ www_auth = USE_WWW_AUTH_STRICT_PARSING and WWW_AUTH_STRICT or WWW_AUTH_RELAXED
+ while authenticate:
+ # Break off the scheme at the beginning of the line
+ if headername == 'authentication-info':
+ (auth_scheme, the_rest) = ('digest', authenticate)
+ else:
+ (auth_scheme, the_rest) = authenticate.split(" ", 1)
+ # Now loop over all the key value pairs that come after the scheme,
+ # being careful not to roll into the next scheme
match = www_auth.search(the_rest)
- retval[auth_scheme.lower()] = auth_params
- authenticate = the_rest.strip()
+ auth_params = {}
+ while match:
+ if match and len(match.groups()) == 3:
+ (key, value, the_rest) = match.groups()
+ auth_params[key.lower()] = UNQUOTE_PAIRS.sub(r'\1', value) # '\\'.join([x.replace('\\', '') for x in value.split('\\\\')])
+ match = www_auth.search(the_rest)
+ retval[auth_scheme.lower()] = auth_params
+ authenticate = the_rest.strip()
+
+ except ValueError:
+ raise MalformedHeader("WWW-Authenticate")
return retval
@@ -274,17 +326,17 @@ def _entry_disposition(response_headers, request_headers):
1. Cache-Control: max-stale
2. Age: headers are not used in the calculations.
- Not that this algorithm is simpler than you might think
+ Not that this algorithm is simpler than you might think
because we are operating as a private (non-shared) cache.
This lets us ignore 's-maxage'. We can also ignore
'proxy-invalidate' since we aren't a proxy.
- We will never return a stale document as
- fresh as a design decision, and thus the non-implementation
- of 'max-stale'. This also lets us safely ignore 'must-revalidate'
+ We will never return a stale document as
+ fresh as a design decision, and thus the non-implementation
+ of 'max-stale'. This also lets us safely ignore 'must-revalidate'
since we operate as if every server has sent 'must-revalidate'.
Since we are private we get to ignore both 'public' and
'private' parameters. We also ignore 'no-transform' since
- we don't do any transformations.
+ we don't do any transformations.
The 'no-store' parameter is handled at a higher level.
So the only Cache-Control parameters we look at are:
@@ -293,7 +345,7 @@ def _entry_disposition(response_headers, request_headers):
max-age
min-fresh
"""
-
+
retval = "STALE"
cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers)
@@ -335,10 +387,10 @@ def _entry_disposition(response_headers, request_headers):
min_fresh = int(cc['min-fresh'])
except ValueError:
min_fresh = 0
- current_age += min_fresh
+ current_age += min_fresh
if freshness_lifetime > current_age:
retval = "FRESH"
- return retval
+ return retval
def _decompressContent(response, new_content):
content = new_content
@@ -386,7 +438,7 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey):
if status == 304:
status = 200
- status_header = 'status: %d\r\n' % response_headers.status
+ status_header = 'status: %d\r\n' % status
header_str = info.as_string()
@@ -403,10 +455,10 @@ def _wsse_username_token(cnonce, iso_now, password):
return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip()
-# For credentials we need two things, first
+# For credentials we need two things, first
# a pool of credential to try (not necesarily tied to BAsic, Digest, etc.)
# Then we also need a list of URIs that have already demanded authentication
-# That list is tricky since sub-URIs can take the same auth, or the
+# That list is tricky since sub-URIs can take the same auth, or the
# auth scheme may change as you descend the tree.
# So we also need each Auth instance to be able to tell us
# how close to the 'top' it is.
@@ -430,7 +482,7 @@ class Authentication(object):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
- Authorization header. Over-rise this in sub-classes."""
+ Authorization header. Over-ride this in sub-classes."""
pass
def response(self, response, content):
@@ -438,7 +490,7 @@ class Authentication(object):
or such returned from the last authorized response.
Over-rise this in sub-classes if necessary.
- Return TRUE is the request is to be retried, for
+ Return TRUE is the request is to be retried, for
example Digest may return stale=true.
"""
return False
@@ -456,7 +508,7 @@ class BasicAuthentication(Authentication):
class DigestAuthentication(Authentication):
- """Only do qop='auth' and MD5, since that
+ """Only do qop='auth' and MD5, since that
is all Apache currently implements"""
def __init__(self, credentials, host, request_uri, headers, response, content, http):
Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
@@ -469,7 +521,7 @@ class DigestAuthentication(Authentication):
self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5').upper()
if self.challenge['algorithm'] != 'MD5':
raise UnimplementedDigestAuthOptionError( _("Unsupported value for algorithm: %s." % self.challenge['algorithm']))
- self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
+ self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
self.challenge['nc'] = 1
def request(self, method, request_uri, headers, content, cnonce = None):
@@ -477,23 +529,24 @@ class DigestAuthentication(Authentication):
H = lambda x: _md5(x).hexdigest()
KD = lambda s, d: H("%s:%s" % (s, d))
A2 = "".join([method, ":", request_uri])
- self.challenge['cnonce'] = cnonce or _cnonce()
- request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (self.challenge['nonce'],
- '%08x' % self.challenge['nc'],
- self.challenge['cnonce'],
- self.challenge['qop'], H(A2)
- ))
- headers['Authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
- self.credentials[0],
+ self.challenge['cnonce'] = cnonce or _cnonce()
+ request_digest = '"%s"' % KD(H(self.A1), "%s:%s:%s:%s:%s" % (
+ self.challenge['nonce'],
+ '%08x' % self.challenge['nc'],
+ self.challenge['cnonce'],
+ self.challenge['qop'], H(A2)))
+ headers['authorization'] = 'Digest username="%s", realm="%s", nonce="%s", uri="%s", algorithm=%s, response=%s, qop=%s, nc=%08x, cnonce="%s"' % (
+ self.credentials[0],
self.challenge['realm'],
self.challenge['nonce'],
- request_uri,
+ request_uri,
self.challenge['algorithm'],
request_digest,
self.challenge['qop'],
self.challenge['nc'],
- self.challenge['cnonce'],
- )
+ self.challenge['cnonce'])
+ if self.challenge.get('opaque'):
+ headers['authorization'] += ', opaque="%s"' % self.challenge['opaque']
self.challenge['nc'] += 1
def response(self, response, content):
@@ -501,14 +554,14 @@ class DigestAuthentication(Authentication):
challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {})
if 'true' == challenge.get('stale'):
self.challenge['nonce'] = challenge['nonce']
- self.challenge['nc'] = 1
+ self.challenge['nc'] = 1
return True
else:
updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {})
if updated_challenge.has_key('nextnonce'):
self.challenge['nonce'] = updated_challenge['nextnonce']
- self.challenge['nc'] = 1
+ self.challenge['nc'] = 1
return False
@@ -542,9 +595,8 @@ class HmacDigestAuthentication(Authentication):
else:
self.pwhashmod = _sha
self.key = "".join([self.credentials[0], ":",
- self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(),
- ":", self.challenge['realm']
- ])
+ self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest().lower(),
+ ":", self.challenge['realm']])
self.key = self.pwhashmod.new(self.key).hexdigest().lower()
def request(self, method, request_uri, headers, content):
@@ -556,16 +608,15 @@ class HmacDigestAuthentication(Authentication):
cnonce = _cnonce()
request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, cnonce, self.challenge['snonce'], headers_val)
request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest().lower()
- headers['Authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
- self.credentials[0],
+ headers['authorization'] = 'HMACDigest username="%s", realm="%s", snonce="%s", cnonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
+ self.credentials[0],
self.challenge['realm'],
self.challenge['snonce'],
cnonce,
- request_uri,
+ request_uri,
created,
request_digest,
- keylist,
- )
+ keylist)
def response(self, response, content):
challenge = _parse_www_authenticate(response, 'www-authenticate').get('hmacdigest', {})
@@ -578,7 +629,7 @@ class WsseAuthentication(Authentication):
"""This is thinly tested and should not be relied upon.
At this time there isn't any third party server to test against.
Blogger and TypePad implemented this algorithm at one point
- but Blogger has since switched to Basic over HTTPS and
+ but Blogger has since switched to Basic over HTTPS and
TypePad has implemented it wrong, by never issuing a 401
challenge but instead requiring your client to telepathically know that
their endpoint is expecting WSSE profile="UsernameToken"."""
@@ -588,7 +639,7 @@ class WsseAuthentication(Authentication):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
Authorization header."""
- headers['Authorization'] = 'WSSE profile="UsernameToken"'
+ headers['authorization'] = 'WSSE profile="UsernameToken"'
iso_now = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
cnonce = _cnonce()
password_digest = _wsse_username_token(cnonce, iso_now, self.credentials[1])
@@ -624,7 +675,7 @@ class GoogleLoginAuthentication(Authentication):
def request(self, method, request_uri, headers, content):
"""Modify the request headers to add the appropriate
Authorization header."""
- headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
+ headers['authorization'] = 'GoogleLogin Auth=' + self.Auth
AUTH_SCHEME_CLASSES = {
@@ -639,13 +690,13 @@ AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
class FileCache(object):
"""Uses a local directory as a store for cached files.
- Not really safe to use if multiple threads or processes are going to
+ Not really safe to use if multiple threads or processes are going to
be running on the same cache.
"""
def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
self.cache = cache
self.safe = safe
- if not os.path.exists(cache):
+ if not os.path.exists(cache):
os.makedirs(self.cache)
def get(self, key):
@@ -683,34 +734,127 @@ class Credentials(object):
def iter(self, domain):
for (cdomain, name, password) in self.credentials:
if cdomain == "" or domain == cdomain:
- yield (name, password)
+ yield (name, password)
class KeyCerts(Credentials):
"""Identical to Credentials except that
name/password are mapped to key/cert."""
pass
+class AllHosts(object):
+ pass
class ProxyInfo(object):
- """Collect information required to use a proxy."""
- def __init__(self, proxy_type, proxy_host, proxy_port, proxy_rdns=None, proxy_user=None, proxy_pass=None):
- """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX
- constants. For example:
+ """Collect information required to use a proxy."""
+ bypass_hosts = ()
+
+ def __init__(self, proxy_type, proxy_host, proxy_port,
+ proxy_rdns=None, proxy_user=None, proxy_pass=None):
+ """The parameter proxy_type must be set to one of socks.PROXY_TYPE_XXX
+ constants. For example:
-p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP, proxy_host='localhost', proxy_port=8000)
- """
- self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass = proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass
+ p = ProxyInfo(proxy_type=socks.PROXY_TYPE_HTTP,
+ proxy_host='localhost', proxy_port=8000)
+ """
+ self.proxy_type = proxy_type
+ self.proxy_host = proxy_host
+ self.proxy_port = proxy_port
+ self.proxy_rdns = proxy_rdns
+ self.proxy_user = proxy_user
+ self.proxy_pass = proxy_pass
+
+ def astuple(self):
+ return (self.proxy_type, self.proxy_host, self.proxy_port,
+ self.proxy_rdns, self.proxy_user, self.proxy_pass)
+
+ def isgood(self):
+ return (self.proxy_host != None) and (self.proxy_port != None)
+
+ def applies_to(self, hostname):
+ return not self.bypass_host(hostname)
+
+ def bypass_host(self, hostname):
+ """Has this host been excluded from the proxy config"""
+ if self.bypass_hosts is AllHosts:
+ return True
- def astuple(self):
- return (self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns,
- self.proxy_user, self.proxy_pass)
+ bypass = False
+ for domain in self.bypass_hosts:
+ if hostname.endswith(domain):
+ bypass = True
- def isgood(self):
- return socks and (self.proxy_host != None) and (self.proxy_port != None)
+ return bypass
+
+
+def proxy_info_from_environment(method='http'):
+ """
+ Read proxy info from the environment variables.
+ """
+ if method not in ['http', 'https']:
+ return
+
+ env_var = method + '_proxy'
+ url = os.environ.get(env_var, os.environ.get(env_var.upper()))
+ if not url:
+ return
+ pi = proxy_info_from_url(url, method)
+
+ no_proxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', ''))
+ bypass_hosts = []
+ if no_proxy:
+ bypass_hosts = no_proxy.split(',')
+ # special case, no_proxy=* means all hosts bypassed
+ if no_proxy == '*':
+ bypass_hosts = AllHosts
+
+ pi.bypass_hosts = bypass_hosts
+ return pi
+
+def proxy_info_from_url(url, method='http'):
+ """
+ Construct a ProxyInfo from a URL (such as http_proxy env var)
+ """
+ url = urlparse.urlparse(url)
+ username = None
+ password = None
+ port = None
+ if '@' in url[1]:
+ ident, host_port = url[1].split('@', 1)
+ if ':' in ident:
+ username, password = ident.split(':', 1)
+ else:
+ password = ident
+ else:
+ host_port = url[1]
+ if ':' in host_port:
+ host, port = host_port.split(':', 1)
+ else:
+ host = host_port
+
+ if port:
+ port = int(port)
+ else:
+ port = dict(https=443, http=80)[method]
+
+ proxy_type = 3 # socks.PROXY_TYPE_HTTP
+ return ProxyInfo(
+ proxy_type = proxy_type,
+ proxy_host = host,
+ proxy_port = port,
+ proxy_user = username or None,
+ proxy_pass = password or None,
+ )
class HTTPConnectionWithTimeout(httplib.HTTPConnection):
- """HTTPConnection subclass that supports timeouts"""
+ """
+ HTTPConnection subclass that supports timeouts
+
+ All timeouts are in seconds. If None is passed for timeout then
+ Python's default timeout for sockets will be used. See for example
+ the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+ """
def __init__(self, host, port=None, strict=None, timeout=None, proxy_info=None):
httplib.HTTPConnection.__init__(self, host, port, strict)
@@ -720,27 +864,46 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
def connect(self):
"""Connect to the host and port specified in __init__."""
# Mostly verbatim from httplib.py.
+ if self.proxy_info and socks is None:
+ raise ProxiesUnavailableError(
+ 'Proxy support missing but proxy use was requested!')
msg = "getaddrinfo returns an empty list"
- for res in socket.getaddrinfo(self.host, self.port, 0,
- socket.SOCK_STREAM):
+ if self.proxy_info and self.proxy_info.isgood():
+ use_proxy = True
+ proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple()
+ else:
+ use_proxy = False
+ if use_proxy and proxy_rdns:
+ host = proxy_host
+ port = proxy_port
+ else:
+ host = self.host
+ port = self.port
+
+ for res in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
af, socktype, proto, canonname, sa = res
try:
- if self.proxy_info and self.proxy_info.isgood():
+ if use_proxy:
self.sock = socks.socksocket(af, socktype, proto)
- self.sock.setproxy(*self.proxy_info.astuple())
+ self.sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)
else:
self.sock = socket.socket(af, socktype, proto)
+ self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
# Different from httplib: support timeouts.
if has_timeout(self.timeout):
self.sock.settimeout(self.timeout)
# End of difference from httplib.
if self.debuglevel > 0:
- print "connect: (%s, %s)" % (self.host, self.port)
+ print "connect: (%s, %s) ************" % (self.host, self.port)
+ if use_proxy:
+ print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass))
- self.sock.connect(sa)
+ self.sock.connect((self.host, self.port) + sa[2:])
except socket.error, msg:
if self.debuglevel > 0:
- print 'connect fail:', (self.host, self.port)
+ print "connect fail: (%s, %s)" % (self.host, self.port)
+ if use_proxy:
+ print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass))
if self.sock:
self.sock.close()
self.sock = None
@@ -750,56 +913,265 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
raise socket.error, msg
class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
- "This class allows communication via SSL."
+ """
+ This class allows communication via SSL.
+ All timeouts are in seconds. If None is passed for timeout then
+ Python's default timeout for sockets will be used. See for example
+ the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+ """
def __init__(self, host, port=None, key_file=None, cert_file=None,
- strict=None, timeout=None, proxy_info=None):
- httplib.HTTPSConnection.__init__(self, host, port=port, key_file=key_file,
- cert_file=cert_file, strict=strict)
+ strict=None, timeout=None, proxy_info=None,
+ ca_certs=None, disable_ssl_certificate_validation=False):
+ httplib.HTTPSConnection.__init__(self, host, port=port,
+ key_file=key_file,
+ cert_file=cert_file, strict=strict)
self.timeout = timeout
self.proxy_info = proxy_info
+ if ca_certs is None:
+ ca_certs = CA_CERTS
+ self.ca_certs = ca_certs
+ self.disable_ssl_certificate_validation = \
+ disable_ssl_certificate_validation
+
+ # The following two methods were adapted from https_wrapper.py, released
+ # with the Google Appengine SDK at
+ # http://googleappengine.googlecode.com/svn-history/r136/trunk/python/google/appengine/tools/https_wrapper.py
+ # under the following license:
+ #
+ # Copyright 2007 Google Inc.
+ #
+ # Licensed under the Apache License, Version 2.0 (the "License");
+ # you may not use this file except in compliance with the License.
+ # You may obtain a copy of the License at
+ #
+ # http://www.apache.org/licenses/LICENSE-2.0
+ #
+ # Unless required by applicable law or agreed to in writing, software
+ # distributed under the License is distributed on an "AS IS" BASIS,
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ # See the License for the specific language governing permissions and
+ # limitations under the License.
+ #
+
+ def _GetValidHostsForCert(self, cert):
+ """Returns a list of valid host globs for an SSL certificate.
+
+ Args:
+ cert: A dictionary representing an SSL certificate.
+ Returns:
+ list: A list of valid host globs.
+ """
+ if 'subjectAltName' in cert:
+ return [x[1] for x in cert['subjectAltName']
+ if x[0].lower() == 'dns']
+ else:
+ return [x[0][1] for x in cert['subject']
+ if x[0][0].lower() == 'commonname']
+
+ def _ValidateCertificateHostname(self, cert, hostname):
+ """Validates that a given hostname is valid for an SSL certificate.
+
+ Args:
+ cert: A dictionary representing an SSL certificate.
+ hostname: The hostname to test.
+ Returns:
+ bool: Whether or not the hostname is valid for this certificate.
+ """
+ hosts = self._GetValidHostsForCert(cert)
+ for host in hosts:
+ host_re = host.replace('.', '\.').replace('*', '[^.]*')
+ if re.search('^%s$' % (host_re,), hostname, re.I):
+ return True
+ return False
def connect(self):
"Connect to a host on a given (SSL) port."
+ msg = "getaddrinfo returns an empty list"
if self.proxy_info and self.proxy_info.isgood():
- sock = socks.socksocket(socket.AF_INET, socket.SOCK_STREAM)
- sock.setproxy(*self.proxy_info.astuple())
+ use_proxy = True
+ proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass = self.proxy_info.astuple()
+ else:
+ use_proxy = False
+ if use_proxy and proxy_rdns:
+ host = proxy_host
+ port = proxy_port
else:
- sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
-
- if has_timeout(self.timeout):
- sock.settimeout(self.timeout)
- sock.connect((self.host, self.port))
- self.sock =_ssl_wrap_socket(sock, self.key_file, self.cert_file)
+ host = self.host
+ port = self.port
+ address_info = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM)
+ for family, socktype, proto, canonname, sockaddr in address_info:
+ try:
+ if use_proxy:
+ sock = socks.socksocket(family, socktype, proto)
+
+ sock.setproxy(proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass)
+ else:
+ sock = socket.socket(family, socktype, proto)
+ sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+ if has_timeout(self.timeout):
+ sock.settimeout(self.timeout)
+ sock.connect((self.host, self.port))
+ self.sock =_ssl_wrap_socket(
+ sock, self.key_file, self.cert_file,
+ self.disable_ssl_certificate_validation, self.ca_certs)
+ if self.debuglevel > 0:
+ print "connect: (%s, %s)" % (self.host, self.port)
+ if use_proxy:
+ print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass))
+ if not self.disable_ssl_certificate_validation:
+ cert = self.sock.getpeercert()
+ hostname = self.host.split(':', 0)[0]
+ if not self._ValidateCertificateHostname(cert, hostname):
+ raise CertificateHostnameMismatch(
+ 'Server presented certificate that does not match '
+ 'host %s: %s' % (hostname, cert), hostname, cert)
+ except ssl_SSLError, e:
+ if sock:
+ sock.close()
+ if self.sock:
+ self.sock.close()
+ self.sock = None
+ # Unfortunately the ssl module doesn't seem to provide any way
+ # to get at more detailed error information, in particular
+ # whether the error is due to certificate validation or
+ # something else (such as SSL protocol mismatch).
+ if e.errno == ssl.SSL_ERROR_SSL:
+ raise SSLHandshakeError(e)
+ else:
+ raise
+ except (socket.timeout, socket.gaierror):
+ raise
+ except socket.error, msg:
+ if self.debuglevel > 0:
+ print "connect fail: (%s, %s)" % (self.host, self.port)
+ if use_proxy:
+ print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass))
+ if self.sock:
+ self.sock.close()
+ self.sock = None
+ continue
+ break
+ if not self.sock:
+ raise socket.error, msg
+
+SCHEME_TO_CONNECTION = {
+ 'http': HTTPConnectionWithTimeout,
+ 'https': HTTPSConnectionWithTimeout
+}
+
+# Use a different connection object for Google App Engine
+try:
+ try:
+ from google.appengine.api import apiproxy_stub_map
+ if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None:
+ raise ImportError # Bail out; we're not actually running on App Engine.
+ from google.appengine.api.urlfetch import fetch
+ from google.appengine.api.urlfetch import InvalidURLError
+ except ImportError:
+ from google3.apphosting.api import apiproxy_stub_map
+ if apiproxy_stub_map.apiproxy.GetStub('urlfetch') is None:
+ raise ImportError # Bail out; we're not actually running on App Engine.
+ from google3.apphosting.api.urlfetch import fetch
+ from google3.apphosting.api.urlfetch import InvalidURLError
+
+ def _new_fixed_fetch(validate_certificate):
+ def fixed_fetch(url, payload=None, method="GET", headers={},
+ allow_truncated=False, follow_redirects=True,
+ deadline=5):
+ return fetch(url, payload=payload, method=method, headers=header,
+ allow_truncated=allow_truncated,
+ follow_redirects=follow_redirects, deadline=deadline,
+ validate_certificate=validate_certificate)
+ return fixed_fetch
+
+ class AppEngineHttpConnection(httplib.HTTPConnection):
+ """Use httplib on App Engine, but compensate for its weirdness.
+
+ The parameters key_file, cert_file, proxy_info, ca_certs, and
+ disable_ssl_certificate_validation are all dropped on the ground.
+ """
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ strict=None, timeout=None, proxy_info=None, ca_certs=None,
+ disable_ssl_certificate_validation=False):
+ httplib.HTTPConnection.__init__(self, host, port=port,
+ strict=strict, timeout=timeout)
+
+ class AppEngineHttpsConnection(httplib.HTTPSConnection):
+ """Same as AppEngineHttpConnection, but for HTTPS URIs."""
+ def __init__(self, host, port=None, key_file=None, cert_file=None,
+ strict=None, timeout=None, proxy_info=None, ca_certs=None,
+ disable_ssl_certificate_validation=False):
+ httplib.HTTPSConnection.__init__(self, host, port=port,
+ key_file=key_file,
+ cert_file=cert_file, strict=strict,
+ timeout=timeout)
+ self._fetch = _new_fixed_fetch(
+ not disable_ssl_certificate_validation)
+
+ # Update the connection classes to use the Googel App Engine specific ones.
+ SCHEME_TO_CONNECTION = {
+ 'http': AppEngineHttpConnection,
+ 'https': AppEngineHttpsConnection
+ }
+except ImportError:
+ pass
class Http(object):
"""An HTTP client that handles:
-- all methods
-- caching
-- ETags
-- compression,
-- HTTPS
-- Basic
-- Digest
-- WSSE
-
-and more.
- """
- def __init__(self, cache=None, timeout=None, proxy_info=None):
- """The value of proxy_info is a ProxyInfo instance.
-If 'cache' is a string then it is used as a directory name
-for a disk cache. Otherwise it must be an object that supports
-the same interface as FileCache."""
+ - all methods
+ - caching
+ - ETags
+ - compression,
+ - HTTPS
+ - Basic
+ - Digest
+ - WSSE
+
+ and more.
+ """
+ def __init__(self, cache=None, timeout=None,
+ proxy_info=proxy_info_from_environment,
+ ca_certs=None, disable_ssl_certificate_validation=False):
+ """If 'cache' is a string then it is used as a directory name for
+ a disk cache. Otherwise it must be an object that supports the
+ same interface as FileCache.
+
+ All timeouts are in seconds. If None is passed for timeout
+ then Python's default timeout for sockets will be used. See
+ for example the docs of socket.setdefaulttimeout():
+ http://docs.python.org/library/socket.html#socket.setdefaulttimeout
+
+ `proxy_info` may be:
+ - a callable that takes the http scheme ('http' or 'https') and
+ returns a ProxyInfo instance per request. By default, uses
+ proxy_nfo_from_environment.
+ - a ProxyInfo instance (static proxy config).
+ - None (proxy disabled).
+
+ ca_certs is the path of a file containing root CA certificates for SSL
+ server certificate validation. By default, a CA cert file bundled with
+ httplib2 is used.
+
+ If disable_ssl_certificate_validation is true, SSL cert validation will
+ not be performed.
+ """
self.proxy_info = proxy_info
+ self.ca_certs = ca_certs
+ self.disable_ssl_certificate_validation = \
+ disable_ssl_certificate_validation
+
# Map domain name to an httplib connection
self.connections = {}
# The location of the cache, for now a directory
# where cached responses are held.
- if cache and isinstance(cache, str):
+ if cache and isinstance(cache, basestring):
self.cache = FileCache(cache)
else:
self.cache = cache
@@ -815,10 +1187,10 @@ the same interface as FileCache."""
# If set to False then no redirects are followed, even safe ones.
self.follow_redirects = True
-
+
# Which HTTP methods do we apply optimistic concurrency to, i.e.
# which methods get an "if-match:" etag header added to them.
- self.optimistic_concurrency_methods = ["PUT"]
+ self.optimistic_concurrency_methods = ["PUT", "PATCH"]
# If 'follow_redirects' is True, and this is set to True then
# all redirecs are followed, including unsafe ones.
@@ -826,10 +1198,27 @@ the same interface as FileCache."""
self.ignore_etag = False
- self.force_exception_to_status_code = False
+ self.force_exception_to_status_code = False
self.timeout = timeout
+ # Keep Authorization: headers on a redirect.
+ self.forward_authorization_headers = False
+
+ def __getstate__(self):
+ state_dict = copy.copy(self.__dict__)
+ # In case request is augmented by some foreign object such as
+ # credentials which handle auth
+ if 'request' in state_dict:
+ del state_dict['request']
+ if 'connections' in state_dict:
+ del state_dict['connections']
+ return state_dict
+
+ def __setstate__(self, state):
+ self.__dict__.update(state)
+ self.connections = {}
+
def _auth_from_challenge(self, host, request_uri, headers, response, content):
"""A generator that creates Authorization objects
that can be applied to requests.
@@ -857,29 +1246,56 @@ the same interface as FileCache."""
self.authorizations = []
def _conn_request(self, conn, request_uri, method, body, headers):
- for i in range(2):
+ for i in range(RETRIES):
try:
+ if hasattr(conn, 'sock') and conn.sock is None:
+ conn.connect()
conn.request(method, request_uri, body, headers)
+ except socket.timeout:
+ raise
except socket.gaierror:
conn.close()
raise ServerNotFoundError("Unable to find the server at %s" % conn.host)
- except (socket.error, httplib.HTTPException):
+ except ssl_SSLError:
+ conn.close()
+ raise
+ except socket.error, e:
+ err = 0
+ if hasattr(e, 'args'):
+ err = getattr(e, 'args')[0]
+ else:
+ err = e.errno
+ if err == errno.ECONNREFUSED: # Connection refused
+ raise
+ except httplib.HTTPException:
# Just because the server closed the connection doesn't apparently mean
# that the server didn't send a response.
- pass
+ if hasattr(conn, 'sock') and conn.sock is None:
+ if i < RETRIES-1:
+ conn.close()
+ conn.connect()
+ continue
+ else:
+ conn.close()
+ raise
+ if i < RETRIES-1:
+ conn.close()
+ conn.connect()
+ continue
try:
response = conn.getresponse()
except (socket.error, httplib.HTTPException):
- if i == 0:
+ if i < RETRIES-1:
conn.close()
conn.connect()
continue
else:
+ conn.close()
raise
else:
content = ""
if method == "HEAD":
- response.close()
+ conn.close()
else:
content = response.read()
response = Response(response)
@@ -895,12 +1311,12 @@ the same interface as FileCache."""
auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
auth = auths and sorted(auths)[0][1] or None
- if auth:
+ if auth:
auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers)
- if auth:
+ if auth:
if auth.response(response, body):
auth.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers )
@@ -908,7 +1324,7 @@ the same interface as FileCache."""
if response.status == 401:
for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
- authorization.request(method, request_uri, headers, body)
+ authorization.request(method, request_uri, headers, body)
(response, content) = self._conn_request(conn, request_uri, method, body, headers, )
if response.status != 401:
self.authorizations.append(authorization)
@@ -931,26 +1347,31 @@ the same interface as FileCache."""
if response.status == 301 and method in ["GET", "HEAD"]:
response['-x-permanent-redirect-url'] = response['location']
if not response.has_key('content-location'):
- response['content-location'] = absolute_uri
+ response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey)
if headers.has_key('if-none-match'):
del headers['if-none-match']
if headers.has_key('if-modified-since'):
del headers['if-modified-since']
+ if 'authorization' in headers and not self.forward_authorization_headers:
+ del headers['authorization']
if response.has_key('location'):
location = response['location']
old_response = copy.deepcopy(response)
if not old_response.has_key('content-location'):
- old_response['content-location'] = absolute_uri
- redirect_method = ((response.status == 303) and (method not in ["GET", "HEAD"])) and "GET" or method
+ old_response['content-location'] = absolute_uri
+ redirect_method = method
+ if response.status in [302, 303]:
+ redirect_method = "GET"
+ body = None
(response, content) = self.request(location, redirect_method, body=body, headers = headers, redirections = redirections - 1)
response.previous = old_response
else:
- raise RedirectLimit( _("Redirected more times than rediection_limit allows."), response, content)
- elif response.status in [200, 203] and method == "GET":
+ raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content)
+ elif response.status in [200, 203] and method in ["GET", "HEAD"]:
# Don't cache 206's since we aren't going to handle byte range requests
if not response.has_key('content-location'):
- response['content-location'] = absolute_uri
+ response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey)
return (response, content)
@@ -965,24 +1386,25 @@ the same interface as FileCache."""
def request(self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None):
""" Performs a single HTTP request.
-The 'uri' is the URI of the HTTP resource and can begin
-with either 'http' or 'https'. The value of 'uri' must be an absolute URI.
-The 'method' is the HTTP method to perform, such as GET, POST, DELETE, etc.
-There is no restriction on the methods allowed.
+ The 'uri' is the URI of the HTTP resource and can begin with either
+ 'http' or 'https'. The value of 'uri' must be an absolute URI.
+
+ The 'method' is the HTTP method to perform, such as GET, POST, DELETE,
+ etc. There is no restriction on the methods allowed.
-The 'body' is the entity body to be sent with the request. It is a string
-object.
+ The 'body' is the entity body to be sent with the request. It is a
+ string object.
-Any extra headers that are to be sent with the request should be provided in the
-'headers' dictionary.
+ Any extra headers that are to be sent with the request should be
+ provided in the 'headers' dictionary.
-The maximum number of redirect to follow before raising an
-exception is 'redirections. The default is 5.
+ The maximum number of redirect to follow before raising an
+ exception is 'redirections. The default is 5.
-The return value is a tuple of (response, content), the first
-being and instance of the 'Response' class, the second being
-a string that contains the response entity body.
+ The return value is a tuple of (response, content), the first
+ being and instance of the 'Response' class, the second being
+ a string that contains the response entity body.
"""
try:
if headers is None:
@@ -991,7 +1413,7 @@ a string that contains the response entity body.
headers = self._normalize_headers(headers)
if not headers.has_key('user-agent'):
- headers['user-agent'] = "Python-httplib2/%s" % __version__
+ headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__
uri = iri2uri(uri)
@@ -1001,21 +1423,38 @@ a string that contains the response entity body.
scheme = 'https'
authority = domain_port[0]
+ proxy_info = self._get_proxy_info(scheme, authority)
+
conn_key = scheme+":"+authority
if conn_key in self.connections:
conn = self.connections[conn_key]
else:
if not connection_type:
- connection_type = (scheme == 'https') and HTTPSConnectionWithTimeout or HTTPConnectionWithTimeout
+ connection_type = SCHEME_TO_CONNECTION[scheme]
certs = list(self.certificates.iter(authority))
- if scheme == 'https' and certs:
- conn = self.connections[conn_key] = connection_type(authority, key_file=certs[0][0],
- cert_file=certs[0][1], timeout=self.timeout, proxy_info=self.proxy_info)
+ if scheme == 'https':
+ if certs:
+ conn = self.connections[conn_key] = connection_type(
+ authority, key_file=certs[0][0],
+ cert_file=certs[0][1], timeout=self.timeout,
+ proxy_info=proxy_info,
+ ca_certs=self.ca_certs,
+ disable_ssl_certificate_validation=
+ self.disable_ssl_certificate_validation)
+ else:
+ conn = self.connections[conn_key] = connection_type(
+ authority, timeout=self.timeout,
+ proxy_info=proxy_info,
+ ca_certs=self.ca_certs,
+ disable_ssl_certificate_validation=
+ self.disable_ssl_certificate_validation)
else:
- conn = self.connections[conn_key] = connection_type(authority, timeout=self.timeout, proxy_info=self.proxy_info)
+ conn = self.connections[conn_key] = connection_type(
+ authority, timeout=self.timeout,
+ proxy_info=proxy_info)
conn.set_debuglevel(debuglevel)
- if method in ["GET", "HEAD"] and 'range' not in headers and 'accept-encoding' not in headers:
+ if 'range' not in headers and 'accept-encoding' not in headers:
headers['accept-encoding'] = 'gzip, deflate'
info = email.Message.Message()
@@ -1035,7 +1474,7 @@ a string that contains the response entity body.
feedparser.feed(info)
info = feedparser.close()
feedparser._parse = None
- except IndexError:
+ except (IndexError, ValueError):
self.cache.delete(cachekey)
cachekey = None
cached_value = None
@@ -1058,13 +1497,15 @@ a string that contains the response entity body.
for header in vary_headers:
key = '-varied-%s' % header
value = info[key]
- if headers.get(header, '') != value:
- cached_value = None
- break
+ if headers.get(header, None) != value:
+ cached_value = None
+ break
if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
if info.has_key('-x-permanent-redirect-url'):
# Should cached permanent redirects be counted in our redirection count? For now, yes.
+ if redirections <= 0:
+ raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "")
(response, new_content) = self.request(info['-x-permanent-redirect-url'], "GET", headers = headers, redirections = redirections - 1)
response.previous = Response(info)
response.previous.fromcache = True
@@ -1072,13 +1513,13 @@ a string that contains the response entity body.
# Determine our course of action:
# Is the cached entry fresh or stale?
# Has the client requested a non-cached response?
- #
- # There seems to be three possible answers:
+ #
+ # There seems to be three possible answers:
# 1. [FRESH] Return the cache entry w/o doing a GET
# 2. [STALE] Do the GET (but add in cache validators if available)
# 3. [TRANSPARENT] Do a GET w/o any cache validators (Cache-Control: no-cache) on the request
- entry_disposition = _entry_disposition(info, headers)
-
+ entry_disposition = _entry_disposition(info, headers)
+
if entry_disposition == "FRESH":
if not cached_value:
info['status'] = '504'
@@ -1100,7 +1541,7 @@ a string that contains the response entity body.
if response.status == 304 and method == "GET":
# Rewrite the cache entry with the new end-to-end headers
- # Take all headers that are in response
+ # Take all headers that are in response
# and overwrite their values in info.
# unless they are hop-by-hop, or are listed in the connection header.
@@ -1112,14 +1553,14 @@ a string that contains the response entity body.
_updateCache(headers, merged_response, content, self.cache, cachekey)
response = merged_response
response.status = 200
- response.fromcache = True
+ response.fromcache = True
elif response.status == 200:
content = new_content
else:
self.cache.delete(cachekey)
- content = new_content
- else:
+ content = new_content
+ else:
cc = _parse_cache_control(headers)
if cc.has_key('only-if-cached'):
info['status'] = '504'
@@ -1133,34 +1574,47 @@ a string that contains the response entity body.
response = e.response
content = e.content
response.status = 500
- response.reason = str(e)
+ response.reason = str(e)
elif isinstance(e, socket.timeout):
content = "Request Timeout"
- response = Response( {
- "content-type": "text/plain",
- "status": "408",
- "content-length": len(content)
- })
+ response = Response({
+ "content-type": "text/plain",
+ "status": "408",
+ "content-length": len(content)
+ })
response.reason = "Request Timeout"
else:
- content = str(e)
- response = Response( {
- "content-type": "text/plain",
- "status": "400",
- "content-length": len(content)
- })
- response.reason = "Bad Request"
+ content = str(e)
+ response = Response({
+ "content-type": "text/plain",
+ "status": "400",
+ "content-length": len(content)
+ })
+ response.reason = "Bad Request"
else:
raise
-
+
return (response, content)
-
+ def _get_proxy_info(self, scheme, authority):
+ """Return a ProxyInfo instance (or None) based on the scheme
+ and authority.
+ """
+ hostname, port = urllib.splitport(authority)
+ proxy_info = self.proxy_info
+ if callable(proxy_info):
+ proxy_info = proxy_info(scheme)
+
+ if (hasattr(proxy_info, 'applies_to')
+ and not proxy_info.applies_to(hostname)):
+ proxy_info = None
+ return proxy_info
+
class Response(dict):
"""An object more like email.Message than httplib.HTTPResponse."""
-
+
"""Is this response from our local cache"""
fromcache = False
@@ -1176,27 +1630,28 @@ class Response(dict):
previous = None
def __init__(self, info):
- # info is either an email.Message or
+ # info is either an email.Message or
# an httplib.HTTPResponse object.
if isinstance(info, httplib.HTTPResponse):
- for key, value in info.getheaders():
- self[key.lower()] = value
+ for key, value in info.getheaders():
+ self[key.lower()] = value
self.status = info.status
self['status'] = str(self.status)
self.reason = info.reason
self.version = info.version
elif isinstance(info, email.Message.Message):
- for key, value in info.items():
- self[key] = value
+ for key, value in info.items():
+ self[key.lower()] = value
self.status = int(self['status'])
else:
- for key, value in info.iteritems():
- self[key] = value
+ for key, value in info.iteritems():
+ self[key.lower()] = value
self.status = int(self.get('status', self.status))
+ self.reason = self.get('reason', self.reason)
def __getattr__(self, name):
if name == 'dict':
- return self
- else:
- raise AttributeError, name
+ return self
+ else:
+ raise AttributeError, name

Powered by Google App Engine
This is Rietveld 408576698