httplib2 0.11.3

This commit is contained in:
Jay Lee
2018-07-04 20:24:51 -04:00
parent 044686b564
commit 5966e39406
5 changed files with 179 additions and 95 deletions

View File

@@ -1,4 +1,4 @@
from __future__ import generators from __future__ import print_function
""" """
httplib2 httplib2
@@ -23,7 +23,7 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)",
"Louis Nyffenegger", "Louis Nyffenegger",
"Alex Yu"] "Alex Yu"]
__license__ = "MIT" __license__ = "MIT"
__version__ = "0.10.3" __version__ = '0.11.3'
import re import re
import sys import sys
@@ -146,6 +146,7 @@ if sys.version_info < (2,4):
seq.sort() seq.sort()
return seq return seq
# Python 2.3 support # Python 2.3 support
def HTTPResponse__getheaders(self): def HTTPResponse__getheaders(self):
"""Return list of (header, value) tuples.""" """Return list of (header, value) tuples."""
@@ -223,6 +224,7 @@ except ImportError:
# Which headers are hop-by-hop headers by default # Which headers are hop-by-hop headers by default
HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade']
def _get_end2end_headers(response): def _get_end2end_headers(response):
hopbyhop = list(HOP_BY_HOP) hopbyhop = list(HOP_BY_HOP)
hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')])
@@ -230,6 +232,7 @@ def _get_end2end_headers(response):
URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")
def parse_uri(uri): def parse_uri(uri):
"""Parses a URI using the regex given in Appendix B of RFC 3986. """Parses a URI using the regex given in Appendix B of RFC 3986.
@@ -238,6 +241,7 @@ def parse_uri(uri):
groups = URI.match(uri).groups() groups = URI.match(uri).groups()
return (groups[1], groups[3], groups[4], groups[6], groups[8]) return (groups[1], groups[3], groups[4], groups[6], groups[8])
def urlnorm(uri): def urlnorm(uri):
(scheme, authority, path, query, fragment) = parse_uri(uri) (scheme, authority, path, query, fragment) = parse_uri(uri)
if not scheme or not authority: if not scheme or not authority:
@@ -258,6 +262,7 @@ def urlnorm(uri):
re_url_scheme = re.compile(r'^\w+://') re_url_scheme = re.compile(r'^\w+://')
re_slash = re.compile(r'[?/:|]+') re_slash = re.compile(r'[?/:|]+')
def safename(filename): def safename(filename):
"""Return a filename suitable for the cache. """Return a filename suitable for the cache.
@@ -286,12 +291,15 @@ def safename(filename):
return ",".join((filename, filemd5)) return ",".join((filename, filemd5))
NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+')
def _normalize_headers(headers): def _normalize_headers(headers):
return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()])
def _parse_cache_control(headers): def _parse_cache_control(headers):
retval = {} retval = {}
if headers.has_key('cache-control'): if 'cache-control' in headers:
parts = headers['cache-control'].split(',') parts = headers['cache-control'].split(',')
parts_with_args = [tuple([x.strip().lower() for x in part.split("=", 1)]) for part in parts if -1 != part.find("=")] 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("=")] parts_wo_args = [(name.strip().lower(), 1) for name in parts if -1 == name.find("=")]
@@ -316,7 +324,7 @@ def _parse_www_authenticate(headers, headername='www-authenticate'):
"""Returns a dictionary of dictionaries, one dict """Returns a dictionary of dictionaries, one dict
per auth_scheme.""" per auth_scheme."""
retval = {} retval = {}
if headers.has_key(headername): if headername in headers:
try: try:
authenticate = headers[headername].strip() authenticate = headers[headername].strip()
@@ -344,6 +352,7 @@ def _parse_www_authenticate(headers, headername='www-authenticate'):
return retval return retval
# TODO: add current time as _entry_disposition argument to avoid sleep in tests
def _entry_disposition(response_headers, request_headers): def _entry_disposition(response_headers, request_headers):
"""Determine freshness from the Date, Expires and Cache-Control headers. """Determine freshness from the Date, Expires and Cache-Control headers.
@@ -376,26 +385,26 @@ def _entry_disposition(response_headers, request_headers):
cc = _parse_cache_control(request_headers) cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers) cc_response = _parse_cache_control(response_headers)
if request_headers.has_key('pragma') and request_headers['pragma'].lower().find('no-cache') != -1: if 'pragma' in request_headers and request_headers['pragma'].lower().find('no-cache') != -1:
retval = "TRANSPARENT" retval = "TRANSPARENT"
if 'cache-control' not in request_headers: if 'cache-control' not in request_headers:
request_headers['cache-control'] = 'no-cache' request_headers['cache-control'] = 'no-cache'
elif cc.has_key('no-cache'): elif 'no-cache' in cc:
retval = "TRANSPARENT" retval = "TRANSPARENT"
elif cc_response.has_key('no-cache'): elif 'no-cache' in cc_response:
retval = "STALE" retval = "STALE"
elif cc.has_key('only-if-cached'): elif 'only-if-cached' in cc:
retval = "FRESH" retval = "FRESH"
elif response_headers.has_key('date'): elif 'date' in response_headers:
date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date'])) date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date']))
now = time.time() now = time.time()
current_age = max(0, now - date) current_age = max(0, now - date)
if cc_response.has_key('max-age'): if 'max-age' in cc_response:
try: try:
freshness_lifetime = int(cc_response['max-age']) freshness_lifetime = int(cc_response['max-age'])
except ValueError: except ValueError:
freshness_lifetime = 0 freshness_lifetime = 0
elif response_headers.has_key('expires'): elif 'expires' in response_headers:
expires = email.Utils.parsedate_tz(response_headers['expires']) expires = email.Utils.parsedate_tz(response_headers['expires'])
if None == expires: if None == expires:
freshness_lifetime = 0 freshness_lifetime = 0
@@ -403,12 +412,12 @@ def _entry_disposition(response_headers, request_headers):
freshness_lifetime = max(0, calendar.timegm(expires) - date) freshness_lifetime = max(0, calendar.timegm(expires) - date)
else: else:
freshness_lifetime = 0 freshness_lifetime = 0
if cc.has_key('max-age'): if 'max-age' in cc:
try: try:
freshness_lifetime = int(cc['max-age']) freshness_lifetime = int(cc['max-age'])
except ValueError: except ValueError:
freshness_lifetime = 0 freshness_lifetime = 0
if cc.has_key('min-fresh'): if 'min-fresh' in cc:
try: try:
min_fresh = int(cc['min-fresh']) min_fresh = int(cc['min-fresh'])
except ValueError: except ValueError:
@@ -418,6 +427,7 @@ def _entry_disposition(response_headers, request_headers):
retval = "FRESH" retval = "FRESH"
return retval return retval
def _decompressContent(response, new_content): def _decompressContent(response, new_content):
content = new_content content = new_content
try: try:
@@ -426,21 +436,22 @@ def _decompressContent(response, new_content):
if encoding == 'gzip': if encoding == 'gzip':
content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read()
if encoding == 'deflate': if encoding == 'deflate':
content = zlib.decompress(content) content = zlib.decompress(content, -zlib.MAX_WBITS)
response['content-length'] = str(len(content)) response['content-length'] = str(len(content))
# Record the historical presence of the encoding in a way the won't interfere. # Record the historical presence of the encoding in a way the won't interfere.
response['-content-encoding'] = response['content-encoding'] response['-content-encoding'] = response['content-encoding']
del response['content-encoding'] del response['content-encoding']
except IOError: except (IOError, zlib.error):
content = "" content = ""
raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content)
return content return content
def _updateCache(request_headers, response_headers, content, cache, cachekey): def _updateCache(request_headers, response_headers, content, cache, cachekey):
if cachekey: if cachekey:
cc = _parse_cache_control(request_headers) cc = _parse_cache_control(request_headers)
cc_response = _parse_cache_control(response_headers) cc_response = _parse_cache_control(response_headers)
if cc.has_key('no-store') or cc_response.has_key('no-store'): if 'no-store' in cc or 'no-store' in cc_response:
cache.delete(cachekey) cache.delete(cachekey)
else: else:
info = email.Message.Message() info = email.Message.Message()
@@ -522,7 +533,6 @@ class Authentication(object):
return False return False
class BasicAuthentication(Authentication): class BasicAuthentication(Authentication):
def __init__(self, credentials, host, request_uri, headers, response, content, http): def __init__(self, credentials, host, request_uri, headers, response, content, http):
Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http) Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
@@ -576,7 +586,7 @@ class DigestAuthentication(Authentication):
self.challenge['nc'] += 1 self.challenge['nc'] += 1
def response(self, response, content): def response(self, response, content):
if not response.has_key('authentication-info'): if 'authentication-info' not in response:
challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {}) challenge = _parse_www_authenticate(response, 'www-authenticate').get('digest', {})
if 'true' == challenge.get('stale'): if 'true' == challenge.get('stale'):
self.challenge['nonce'] = challenge['nonce'] self.challenge['nonce'] = challenge['nonce']
@@ -585,7 +595,7 @@ class DigestAuthentication(Authentication):
else: else:
updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {}) updated_challenge = _parse_www_authenticate(response, 'authentication-info').get('digest', {})
if updated_challenge.has_key('nextnonce'): if 'nextnonce' in updated_challenge:
self.challenge['nonce'] = updated_challenge['nextnonce'] self.challenge['nonce'] = updated_challenge['nextnonce']
self.challenge['nc'] = 1 self.challenge['nc'] = 1
return False return False
@@ -675,6 +685,7 @@ class WsseAuthentication(Authentication):
cnonce, cnonce,
iso_now) iso_now)
class GoogleLoginAuthentication(Authentication): class GoogleLoginAuthentication(Authentication):
def __init__(self, credentials, host, request_uri, headers, response, content, http): def __init__(self, credentials, host, request_uri, headers, response, content, http):
from urllib import urlencode from urllib import urlencode
@@ -714,12 +725,13 @@ AUTH_SCHEME_CLASSES = {
AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"] AUTH_SCHEME_ORDER = ["hmacdigest", "googlelogin", "digest", "wsse", "basic"]
class FileCache(object): class FileCache(object):
"""Uses a local directory as a store for cached files. """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. be running on the same cache.
""" """
def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior def __init__(self, cache, safe=safename): # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
self.cache = cache self.cache = cache
self.safe = safe self.safe = safe
if not os.path.exists(cache): if not os.path.exists(cache):
@@ -747,6 +759,7 @@ class FileCache(object):
if os.path.exists(cacheFullPath): if os.path.exists(cacheFullPath):
os.remove(cacheFullPath) os.remove(cacheFullPath)
class Credentials(object): class Credentials(object):
def __init__(self): def __init__(self):
self.credentials = [] self.credentials = []
@@ -762,14 +775,17 @@ class Credentials(object):
if cdomain == "" or domain == cdomain: if cdomain == "" or domain == cdomain:
yield (name, password) yield (name, password)
class KeyCerts(Credentials): class KeyCerts(Credentials):
"""Identical to Credentials except that """Identical to Credentials except that
name/password are mapped to key/cert.""" name/password are mapped to key/cert."""
pass pass
class AllHosts(object): class AllHosts(object):
pass pass
class ProxyInfo(object): class ProxyInfo(object):
"""Collect information required to use a proxy.""" """Collect information required to use a proxy."""
bypass_hosts = () bypass_hosts = ()
@@ -822,12 +838,20 @@ class ProxyInfo(object):
if self.bypass_hosts is AllHosts: if self.bypass_hosts is AllHosts:
return True return True
bypass = False hostname = '.' + hostname.lstrip('.')
for domain in self.bypass_hosts: for skip_name in self.bypass_hosts:
if hostname.endswith(domain): # *.suffix
bypass = True if skip_name.startswith('.') and hostname.endswith(skip_name):
return True
# exact match
if hostname == '.' + skip_name:
return True
return False
return bypass def __repr__(self):
return (
'<ProxyInfo type={p.proxy_type} host:port={p.proxy_host}:{p.proxy_port} rdns={p.proxy_rdns}' +
' user={p.proxy_user} headers={p.proxy_headers}>').format(p=self)
def proxy_info_from_environment(method='http'): def proxy_info_from_environment(method='http'):
@@ -841,20 +865,10 @@ def proxy_info_from_environment(method='http'):
url = os.environ.get(env_var, os.environ.get(env_var.upper())) url = os.environ.get(env_var, os.environ.get(env_var.upper()))
if not url: if not url:
return return
pi = proxy_info_from_url(url, method) return proxy_info_from_url(url, method, None)
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 def proxy_info_from_url(url, method='http', noproxy=None):
return pi
def proxy_info_from_url(url, method='http'):
""" """
Construct a ProxyInfo from a URL (such as http_proxy env var) Construct a ProxyInfo from a URL (such as http_proxy env var)
""" """
@@ -880,8 +894,8 @@ def proxy_info_from_url(url, method='http'):
else: else:
port = dict(https=443, http=80)[method] port = dict(https=443, http=80)[method]
proxy_type = 3 # socks.PROXY_TYPE_HTTP proxy_type = 3 # socks.PROXY_TYPE_HTTP
return ProxyInfo( pi = ProxyInfo(
proxy_type = proxy_type, proxy_type = proxy_type,
proxy_host = host, proxy_host = host,
proxy_port = port, proxy_port = port,
@@ -890,6 +904,20 @@ def proxy_info_from_url(url, method='http'):
proxy_headers = None, proxy_headers = None,
) )
bypass_hosts = []
# If not given an explicit noproxy value, respect values in env vars.
if noproxy is None:
noproxy = os.environ.get('no_proxy', os.environ.get('NO_PROXY', ''))
# Special case: A single '*' character means all hosts should be bypassed.
if noproxy == '*':
bypass_hosts = AllHosts
elif noproxy.strip():
bypass_hosts = noproxy.split(',')
bypass_hosts = filter(bool, bypass_hosts) # To exclude empty string.
pi.bypass_hosts = bypass_hosts
return pi
class HTTPConnectionWithTimeout(httplib.HTTPConnection): class HTTPConnectionWithTimeout(httplib.HTTPConnection):
""" """
@@ -939,16 +967,18 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
self.sock.settimeout(self.timeout) self.sock.settimeout(self.timeout)
# End of difference from httplib. # End of difference from httplib.
if self.debuglevel > 0: if self.debuglevel > 0:
print "connect: (%s, %s) ************" % (self.host, self.port) print("connect: (%s, %s) ************" % (self.host, self.port))
if use_proxy: if use_proxy:
print "proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)) print("proxy: %s ************" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)))
if use_proxy:
self.sock.connect((self.host, self.port) + sa[2:]) self.sock.connect((self.host, self.port) + sa[2:])
except socket.error, msg: else:
self.sock.connect(sa)
except socket.error as msg:
if self.debuglevel > 0: if self.debuglevel > 0:
print "connect fail: (%s, %s)" % (self.host, self.port) print("connect fail: (%s, %s)" % (self.host, self.port))
if use_proxy: if use_proxy:
print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)) print("proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)))
if self.sock: if self.sock:
self.sock.close() self.sock.close()
self.sock = None self.sock = None
@@ -957,6 +987,7 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
if not self.sock: if not self.sock:
raise socket.error, msg raise socket.error, msg
class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
""" """
This class allows communication via SSL. This class allows communication via SSL.
@@ -1062,15 +1093,19 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
if has_timeout(self.timeout): if has_timeout(self.timeout):
sock.settimeout(self.timeout) sock.settimeout(self.timeout)
sock.connect((self.host, self.port))
if use_proxy:
sock.connect((self.host, self.port) + sockaddr[:2])
else:
sock.connect(sockaddr)
self.sock =_ssl_wrap_socket( self.sock =_ssl_wrap_socket(
sock, self.key_file, self.cert_file, sock, self.key_file, self.cert_file,
self.disable_ssl_certificate_validation, self.ca_certs, self.disable_ssl_certificate_validation, self.ca_certs,
self.ssl_version, self.host) self.ssl_version, self.host)
if self.debuglevel > 0: if self.debuglevel > 0:
print "connect: (%s, %s)" % (self.host, self.port) print("connect: (%s, %s)" % (self.host, self.port))
if use_proxy: if use_proxy:
print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)) print("proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)))
if not self.disable_ssl_certificate_validation: if not self.disable_ssl_certificate_validation:
cert = self.sock.getpeercert() cert = self.sock.getpeercert()
hostname = self.host.split(':', 0)[0] hostname = self.host.split(':', 0)[0]
@@ -1078,7 +1113,7 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
raise CertificateHostnameMismatch( raise CertificateHostnameMismatch(
'Server presented certificate that does not match ' 'Server presented certificate that does not match '
'host %s: %s' % (hostname, cert), hostname, cert) 'host %s: %s' % (hostname, cert), hostname, cert)
except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch), e: except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch) as e:
if sock: if sock:
sock.close() sock.close()
if self.sock: if self.sock:
@@ -1094,11 +1129,11 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
raise raise
except (socket.timeout, socket.gaierror): except (socket.timeout, socket.gaierror):
raise raise
except socket.error, msg: except socket.error as msg:
if self.debuglevel > 0: if self.debuglevel > 0:
print "connect fail: (%s, %s)" % (self.host, self.port) print("connect fail: (%s, %s)" % (self.host, self.port))
if use_proxy: if use_proxy:
print "proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)) print("proxy: %s" % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers)))
if self.sock: if self.sock:
self.sock.close() self.sock.close()
self.sock = None self.sock = None
@@ -1290,7 +1325,7 @@ class Http(object):
challenges = _parse_www_authenticate(response, 'www-authenticate') challenges = _parse_www_authenticate(response, 'www-authenticate')
for cred in self.credentials.iter(host): for cred in self.credentials.iter(host):
for scheme in AUTH_SCHEME_ORDER: for scheme in AUTH_SCHEME_ORDER:
if challenges.has_key(scheme): if scheme in challenges:
yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self) yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self)
def add_credentials(self, name, password, domain=""): def add_credentials(self, name, password, domain=""):
@@ -1326,13 +1361,13 @@ class Http(object):
except ssl_SSLError: except ssl_SSLError:
conn.close() conn.close()
raise raise
except socket.error, e: except socket.error as e:
err = 0 err = 0
if hasattr(e, 'args'): if hasattr(e, 'args'):
err = getattr(e, 'args')[0] err = getattr(e, 'args')[0]
else: else:
err = e.errno err = e.errno
if err == errno.ECONNREFUSED: # Connection refused if err == errno.ECONNREFUSED: # Connection refused
raise raise
if err in (errno.ENETUNREACH, errno.EADDRNOTAVAIL) and i < RETRIES: if err in (errno.ENETUNREACH, errno.EADDRNOTAVAIL) and i < RETRIES:
continue # retry on potentially transient socket errors continue # retry on potentially transient socket errors
@@ -1418,29 +1453,29 @@ class Http(object):
# Pick out the location header and basically start from the beginning # Pick out the location header and basically start from the beginning
# remembering first to strip the ETag header and decrement our 'depth' # remembering first to strip the ETag header and decrement our 'depth'
if redirections: if redirections:
if not response.has_key('location') and response.status != 300: if 'location' not in response and response.status != 300:
raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content) raise RedirectMissingLocation( _("Redirected but the response is missing a Location: header."), response, content)
# Fix-up relative redirects (which violate an RFC 2616 MUST) # Fix-up relative redirects (which violate an RFC 2616 MUST)
if response.has_key('location'): if 'location' in response:
location = response['location'] location = response['location']
(scheme, authority, path, query, fragment) = parse_uri(location) (scheme, authority, path, query, fragment) = parse_uri(location)
if authority == None: if authority == None:
response['location'] = urlparse.urljoin(absolute_uri, location) response['location'] = urlparse.urljoin(absolute_uri, location)
if response.status == 301 and method in ["GET", "HEAD"]: if response.status == 301 and method in ["GET", "HEAD"]:
response['-x-permanent-redirect-url'] = response['location'] response['-x-permanent-redirect-url'] = response['location']
if not response.has_key('content-location'): if 'content-location' not in response:
response['content-location'] = absolute_uri response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey) _updateCache(headers, response, content, self.cache, cachekey)
if headers.has_key('if-none-match'): if 'if-none-match' in headers:
del headers['if-none-match'] del headers['if-none-match']
if headers.has_key('if-modified-since'): if 'if-modified-since' in headers:
del headers['if-modified-since'] del headers['if-modified-since']
if 'authorization' in headers and not self.forward_authorization_headers: if 'authorization' in headers and not self.forward_authorization_headers:
del headers['authorization'] del headers['authorization']
if response.has_key('location'): if 'location' in response:
location = response['location'] location = response['location']
old_response = copy.deepcopy(response) old_response = copy.deepcopy(response)
if not old_response.has_key('content-location'): if 'content-location' not in old_response:
old_response['content-location'] = absolute_uri old_response['content-location'] = absolute_uri
redirect_method = method redirect_method = method
if response.status in [302, 303]: if response.status in [302, 303]:
@@ -1455,7 +1490,7 @@ class Http(object):
raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content) raise RedirectLimit("Redirected more times than rediection_limit allows.", response, content)
elif response.status in [200, 203] and method in ["GET", "HEAD"]: 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 # Don't cache 206's since we aren't going to handle byte range requests
if not response.has_key('content-location'): if 'content-location' not in response:
response['content-location'] = absolute_uri response['content-location'] = absolute_uri
_updateCache(headers, response, content, self.cache, cachekey) _updateCache(headers, response, content, self.cache, cachekey)
@@ -1497,7 +1532,7 @@ class Http(object):
else: else:
headers = self._normalize_headers(headers) headers = self._normalize_headers(headers)
if not headers.has_key('user-agent'): if 'user-agent' not in headers:
headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__ headers['user-agent'] = "Python-httplib2/%s (gzip)" % __version__
uri = iri2uri(uri) uri = iri2uri(uri)
@@ -1568,7 +1603,7 @@ class Http(object):
else: else:
cachekey = None cachekey = None
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: if method in self.optimistic_concurrency_methods and self.cache and 'etag' in info and not self.ignore_etag and 'if-match' not in headers:
# http://www.w3.org/1999/04/Editing/ # http://www.w3.org/1999/04/Editing/
headers['if-match'] = info['etag'] headers['if-match'] = info['etag']
@@ -1589,7 +1624,7 @@ class Http(object):
break break
if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers: if cached_value and method in ["GET", "HEAD"] and self.cache and 'range' not in headers:
if info.has_key('-x-permanent-redirect-url'): if '-x-permanent-redirect-url' in info:
# Should cached permanent redirects be counted in our redirection count? For now, yes. # Should cached permanent redirects be counted in our redirection count? For now, yes.
if redirections <= 0: if redirections <= 0:
raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "") raise RedirectLimit("Redirected more times than rediection_limit allows.", {}, "")
@@ -1619,9 +1654,9 @@ class Http(object):
return (response, content) return (response, content)
if entry_disposition == "STALE": if entry_disposition == "STALE":
if info.has_key('etag') and not self.ignore_etag and not 'if-none-match' in headers: if 'etag' in info and not self.ignore_etag and not 'if-none-match' in headers:
headers['if-none-match'] = info['etag'] headers['if-none-match'] = info['etag']
if info.has_key('last-modified') and not 'last-modified' in headers: if 'last-modified' in info and not 'last-modified' in headers:
headers['if-modified-since'] = info['last-modified'] headers['if-modified-since'] = info['last-modified']
elif entry_disposition == "TRANSPARENT": elif entry_disposition == "TRANSPARENT":
pass pass
@@ -1651,13 +1686,13 @@ class Http(object):
content = new_content content = new_content
else: else:
cc = _parse_cache_control(headers) cc = _parse_cache_control(headers)
if cc.has_key('only-if-cached'): if 'only-if-cached' in cc:
info['status'] = '504' info['status'] = '504'
response = Response(info) response = Response(info)
content = "" content = ""
else: else:
(response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey) (response, content) = self._request(conn, authority, uri, request_uri, method, body, headers, redirections, cachekey)
except Exception, e: except Exception as e:
if self.force_exception_to_status_code: if self.force_exception_to_status_code:
if isinstance(e, HttpLib2ErrorWithResponse): if isinstance(e, HttpLib2ErrorWithResponse):
response = e.response response = e.response
@@ -1738,9 +1773,8 @@ class Response(dict):
self.status = int(self.get('status', self.status)) self.status = int(self.get('status', self.status))
self.reason = self.get('reason', self.reason) self.reason = self.get('reason', self.reason)
def __getattr__(self, name): def __getattr__(self, name):
if name == 'dict': if name == 'dict':
return self return self
else: else:
raise AttributeError, name raise AttributeError(name)

View File

@@ -2165,3 +2165,32 @@ R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
-----END CERTIFICATE----- -----END CERTIFICATE-----
# Issuer: CN=DigiCert Global Root G2, OU=www.digicert.com, O=DigiCert Inc, C=US
# Subject: CN=DigiCert Global Root G2, OU=www.digicert.com, O=DigiCert Inc, C=US
# Serial: 33af1e6a711a9a0bb2864b11d09fae5
# MD5 Fingerprint: E4:A6:8A:C8:54:AC:52:42:46:0A:FD:72:48:1B:2A:44
# SHA1 Fingerprint: DF:3C:24:F9:BF:D6:66:76:1B:26:80:73:FE:06:D1:CC:8D:4F:82:A4
# SHA256 Fingerprint: CB:3C:CB:B7:60:31:E5:E0:13:8F:8D:D3:9A:23:F9:DE:47:FF:C3:5E:43:C1:14:4C:EA:27:D4:6A:5A:B1:CB:5F
-----BEGIN CERTIFICATE-----
MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh
MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3
d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH
MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT
MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j
b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI
2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx
1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ
q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz
tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ
vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP
BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV
5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY
1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4
NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG
Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91
8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe
pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl
MrY=
-----END CERTIFICATE-----

View File

@@ -254,7 +254,7 @@ class socksocket(socket.socket):
if self.__proxy[3]: if self.__proxy[3]:
# Resolve remotely # Resolve remotely
ipaddr = None ipaddr = None
req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode()
else: else:
# Resolve locally # Resolve locally
ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) ipaddr = socket.inet_aton(socket.gethostbyname(destaddr))

View File

@@ -1,3 +1,4 @@
from __future__ import print_function
import unittest import unittest
import errno import errno
import os import os
@@ -52,7 +53,7 @@ class FunctionalProxyHttpTest(unittest.TestCase):
# TODO use subprocess.check_call when 2.4 is dropped # TODO use subprocess.check_call when 2.4 is dropped
ret = subprocess.call(['tinyproxy', '-c', self.conffile]) ret = subprocess.call(['tinyproxy', '-c', self.conffile])
self.assertEqual(0, ret) self.assertEqual(0, ret)
except OSError, e: except OSError as e:
if e.errno == errno.ENOENT: if e.errno == errno.ENOENT:
raise nose.SkipTest('tinyproxy not available') raise nose.SkipTest('tinyproxy not available')
raise raise
@@ -62,11 +63,11 @@ class FunctionalProxyHttpTest(unittest.TestCase):
try: try:
pid = int(open(self.pidfile).read()) pid = int(open(self.pidfile).read())
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
except OSError, e: except OSError as e:
if e.errno == errno.ESRCH: if e.errno == errno.ESRCH:
print '\n\n\nTinyProxy Failed to start, log follows:' print('\n\n\nTinyProxy Failed to start, log follows:')
print open(self.logfile).read() print(open(self.logfile).read())
print 'end tinyproxy log\n\n\n' print('end tinyproxy log\n\n\n')
raise raise
map(os.unlink, (self.pidfile, map(os.unlink, (self.pidfile,
self.logfile, self.logfile,

View File

@@ -1,16 +1,19 @@
#!/usr/bin/python2 #!/usr/bin/env python2
from __future__ import print_function
import BaseHTTPServer import BaseHTTPServer
import logging import logging
import os.path import os.path
import unittest import ssl
import sys import sys
import unittest
import httplib2 import httplib2
from httplib2.test import miniserver from httplib2.test import miniserver
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler): class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler):
""" """
Request handler that keeps the HTTP connection open, so that the test can Request handler that keeps the HTTP connection open, so that the test can
@@ -28,27 +31,23 @@ class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler):
# output via logging so nose can catch it # output via logging so nose can catch it
logger.info(s, *args) logger.info(s, *args)
class HttpsContextTest(unittest.TestCase): class HttpsContextTest(unittest.TestCase):
def setUp(self): def setUp(self):
if sys.version_info < (2, 7, 9): if sys.version_info < (2, 7, 9):
return if hasattr(self, "skipTest"):
self.skipTest("SSLContext requires Python 2.7.9")
else:
return
self.httpd, self.port = miniserver.start_server( self.ca_certs_path = os.path.join(os.path.dirname(__file__), 'server.pem')
KeepAliveHandler, True) self.httpd, self.port = miniserver.start_server(KeepAliveHandler, True)
def tearDown(self): def tearDown(self):
self.httpd.shutdown() self.httpd.shutdown()
def testHttpsContext(self): def testHttpsContext(self):
if sys.version_info < (2, 7, 9): client = httplib2.Http(ca_certs=self.ca_certs_path)
if hasattr(unittest, "skipTest"):
self.skipTest("SSLContext requires Python 2.7.9")# Python 2.7.0
else:
return
import ssl
client = httplib2.Http(
ca_certs=os.path.join(os.path.dirname(__file__), 'server.pem'))
# Establish connection to local server # Establish connection to local server
client.request('https://localhost:%d/' % (self.port)) client.request('https://localhost:%d/' % (self.port))
@@ -61,6 +60,27 @@ class HttpsContextTest(unittest.TestCase):
self.assertIsInstance(conn.sock.context, ssl.SSLContext) self.assertIsInstance(conn.sock.context, ssl.SSLContext)
self.assertTrue(conn.sock.context.check_hostname) self.assertTrue(conn.sock.context.check_hostname)
self.assertEqual(conn.sock.server_hostname, 'localhost') self.assertEqual(conn.sock.server_hostname, 'localhost')
self.assertEqual(conn.sock.context.check_hostname, True)
self.assertEqual(conn.sock.context.verify_mode, ssl.CERT_REQUIRED) self.assertEqual(conn.sock.context.verify_mode, ssl.CERT_REQUIRED)
self.assertEqual(conn.sock.context.protocol, ssl.PROTOCOL_SSLv23) self.assertEqual(conn.sock.context.protocol, ssl.PROTOCOL_SSLv23)
def test_ssl_hostname_mismatch_repeat(self):
# https://github.com/httplib2/httplib2/issues/5
# FIXME(temoto): as of 2017-01-05 this is only a reference code, not useful test.
# Because it doesn't provoke described error on my machine.
# Instead `SSLContext.wrap_socket` raises `ssl.CertificateError`
# which was also added to original patch.
# url host is intentionally different, we provoke ssl hostname mismatch error
url = 'https://127.0.0.1:%d/' % (self.port,)
http = httplib2.Http(ca_certs=self.ca_certs_path, proxy_info=None)
def once():
try:
http.request(url)
assert False, 'expected certificate hostname mismatch error'
except Exception as e:
print('%s errno=%s' % (repr(e), getattr(e, 'errno', None)))
once()
once()