diff --git a/src/httplib2/__init__.py b/src/httplib2/__init__.py index 32ec959b..18b013d9 100644 --- a/src/httplib2/__init__.py +++ b/src/httplib2/__init__.py @@ -1,4 +1,4 @@ -from __future__ import generators +from __future__ import print_function """ httplib2 @@ -23,7 +23,7 @@ __contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)", "Louis Nyffenegger", "Alex Yu"] __license__ = "MIT" -__version__ = "0.10.3" +__version__ = '0.11.3' import re import sys @@ -146,6 +146,7 @@ if sys.version_info < (2,4): seq.sort() return seq + # Python 2.3 support def HTTPResponse__getheaders(self): """Return list of (header, value) tuples.""" @@ -223,6 +224,7 @@ except ImportError: # Which headers are hop-by-hop headers by default HOP_BY_HOP = ['connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', 'upgrade'] + def _get_end2end_headers(response): hopbyhop = list(HOP_BY_HOP) hopbyhop.extend([x.strip() for x in response.get('connection', '').split(',')]) @@ -230,6 +232,7 @@ def _get_end2end_headers(response): URI = re.compile(r"^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?") + def parse_uri(uri): """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() return (groups[1], groups[3], groups[4], groups[6], groups[8]) + def urlnorm(uri): (scheme, authority, path, query, fragment) = parse_uri(uri) if not scheme or not authority: @@ -258,6 +262,7 @@ def urlnorm(uri): re_url_scheme = re.compile(r'^\w+://') re_slash = re.compile(r'[?/:|]+') + def safename(filename): """Return a filename suitable for the cache. @@ -286,12 +291,15 @@ def safename(filename): return ",".join((filename, filemd5)) NORMALIZE_SPACE = re.compile(r'(?:\r\n)?[ \t]+') + + def _normalize_headers(headers): return dict([ (key.lower(), NORMALIZE_SPACE.sub(value, ' ').strip()) for (key, value) in headers.iteritems()]) + def _parse_cache_control(headers): retval = {} - if headers.has_key('cache-control'): + if 'cache-control' in headers: 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_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 per auth_scheme.""" retval = {} - if headers.has_key(headername): + if headername in headers: try: authenticate = headers[headername].strip() @@ -344,6 +352,7 @@ def _parse_www_authenticate(headers, headername='www-authenticate'): return retval +# TODO: add current time as _entry_disposition argument to avoid sleep in tests def _entry_disposition(response_headers, request_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_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" if 'cache-control' not in request_headers: request_headers['cache-control'] = 'no-cache' - elif cc.has_key('no-cache'): + elif 'no-cache' in cc: retval = "TRANSPARENT" - elif cc_response.has_key('no-cache'): + elif 'no-cache' in cc_response: retval = "STALE" - elif cc.has_key('only-if-cached'): + elif 'only-if-cached' in cc: retval = "FRESH" - elif response_headers.has_key('date'): + elif 'date' in response_headers: date = calendar.timegm(email.Utils.parsedate_tz(response_headers['date'])) now = time.time() current_age = max(0, now - date) - if cc_response.has_key('max-age'): + if 'max-age' in cc_response: try: freshness_lifetime = int(cc_response['max-age']) except ValueError: freshness_lifetime = 0 - elif response_headers.has_key('expires'): + elif 'expires' in response_headers: expires = email.Utils.parsedate_tz(response_headers['expires']) if None == expires: freshness_lifetime = 0 @@ -403,12 +412,12 @@ def _entry_disposition(response_headers, request_headers): freshness_lifetime = max(0, calendar.timegm(expires) - date) else: freshness_lifetime = 0 - if cc.has_key('max-age'): + if 'max-age' in cc: try: freshness_lifetime = int(cc['max-age']) except ValueError: freshness_lifetime = 0 - if cc.has_key('min-fresh'): + if 'min-fresh' in cc: try: min_fresh = int(cc['min-fresh']) except ValueError: @@ -418,6 +427,7 @@ def _entry_disposition(response_headers, request_headers): retval = "FRESH" return retval + def _decompressContent(response, new_content): content = new_content try: @@ -426,21 +436,22 @@ def _decompressContent(response, new_content): if encoding == 'gzip': content = gzip.GzipFile(fileobj=StringIO.StringIO(new_content)).read() if encoding == 'deflate': - content = zlib.decompress(content) + content = zlib.decompress(content, -zlib.MAX_WBITS) response['content-length'] = str(len(content)) # Record the historical presence of the encoding in a way the won't interfere. response['-content-encoding'] = response['content-encoding'] del response['content-encoding'] - except IOError: + except (IOError, zlib.error): content = "" raise FailedToDecompressContent(_("Content purported to be compressed with %s but failed to decompress.") % response.get('content-encoding'), response, content) return content + def _updateCache(request_headers, response_headers, content, cache, cachekey): if cachekey: cc = _parse_cache_control(request_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) else: info = email.Message.Message() @@ -522,7 +533,6 @@ class Authentication(object): return False - class BasicAuthentication(Authentication): def __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 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', {}) if 'true' == challenge.get('stale'): self.challenge['nonce'] = challenge['nonce'] @@ -585,7 +595,7 @@ class DigestAuthentication(Authentication): else: 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['nc'] = 1 return False @@ -675,6 +685,7 @@ class WsseAuthentication(Authentication): cnonce, iso_now) + class GoogleLoginAuthentication(Authentication): def __init__(self, credentials, host, request_uri, headers, response, content, http): from urllib import urlencode @@ -714,12 +725,13 @@ AUTH_SCHEME_CLASSES = { 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 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.safe = safe if not os.path.exists(cache): @@ -747,6 +759,7 @@ class FileCache(object): if os.path.exists(cacheFullPath): os.remove(cacheFullPath) + class Credentials(object): def __init__(self): self.credentials = [] @@ -762,14 +775,17 @@ class Credentials(object): if cdomain == "" or domain == cdomain: 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.""" bypass_hosts = () @@ -822,12 +838,20 @@ class ProxyInfo(object): if self.bypass_hosts is AllHosts: return True - bypass = False - for domain in self.bypass_hosts: - if hostname.endswith(domain): - bypass = True + hostname = '.' + hostname.lstrip('.') + for skip_name in self.bypass_hosts: + # *.suffix + 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 ( + '').format(p=self) 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())) if not url: 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 - return pi - -def proxy_info_from_url(url, method='http'): +def proxy_info_from_url(url, method='http', noproxy=None): """ 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: port = dict(https=443, http=80)[method] - proxy_type = 3 # socks.PROXY_TYPE_HTTP - return ProxyInfo( + proxy_type = 3 # socks.PROXY_TYPE_HTTP + pi = ProxyInfo( proxy_type = proxy_type, proxy_host = host, proxy_port = port, @@ -890,6 +904,20 @@ def proxy_info_from_url(url, method='http'): 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): """ @@ -939,16 +967,18 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): 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, proxy_headers)) - - self.sock.connect((self.host, self.port) + sa[2:]) - except socket.error, msg: + 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:]) + else: + self.sock.connect(sa) + except socket.error as msg: 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: - 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: self.sock.close() self.sock = None @@ -957,6 +987,7 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection): if not self.sock: raise socket.error, msg + class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): """ This class allows communication via SSL. @@ -1062,15 +1093,19 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): if has_timeout(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( sock, self.key_file, self.cert_file, self.disable_ssl_certificate_validation, self.ca_certs, self.ssl_version, self.host) 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, 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: cert = self.sock.getpeercert() hostname = self.host.split(':', 0)[0] @@ -1078,7 +1113,7 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): raise CertificateHostnameMismatch( 'Server presented certificate that does not match ' 'host %s: %s' % (hostname, cert), hostname, cert) - except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch), e: + except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch) as e: if sock: sock.close() if self.sock: @@ -1094,11 +1129,11 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection): raise except (socket.timeout, socket.gaierror): raise - except socket.error, msg: + except socket.error as msg: 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: - 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: self.sock.close() self.sock = None @@ -1290,7 +1325,7 @@ class Http(object): challenges = _parse_www_authenticate(response, 'www-authenticate') for cred in self.credentials.iter(host): 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) def add_credentials(self, name, password, domain=""): @@ -1326,13 +1361,13 @@ class Http(object): except ssl_SSLError: conn.close() raise - except socket.error, e: + except socket.error as e: err = 0 if hasattr(e, 'args'): err = getattr(e, 'args')[0] else: err = e.errno - if err == errno.ECONNREFUSED: # Connection refused + if err == errno.ECONNREFUSED: # Connection refused raise if err in (errno.ENETUNREACH, errno.EADDRNOTAVAIL) and i < RETRIES: 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 # remembering first to strip the ETag header and decrement our 'depth' 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) # Fix-up relative redirects (which violate an RFC 2616 MUST) - if response.has_key('location'): + if 'location' in response: location = response['location'] (scheme, authority, path, query, fragment) = parse_uri(location) if authority == None: response['location'] = urlparse.urljoin(absolute_uri, location) if response.status == 301 and method in ["GET", "HEAD"]: 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 _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'] - if headers.has_key('if-modified-since'): + if 'if-modified-since' in headers: del headers['if-modified-since'] if 'authorization' in headers and not self.forward_authorization_headers: del headers['authorization'] - if response.has_key('location'): + if 'location' in response: location = response['location'] 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 redirect_method = method if response.status in [302, 303]: @@ -1455,7 +1490,7 @@ class Http(object): 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'): + if 'content-location' not in response: response['content-location'] = absolute_uri _updateCache(headers, response, content, self.cache, cachekey) @@ -1497,7 +1532,7 @@ class Http(object): else: 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__ uri = iri2uri(uri) @@ -1568,7 +1603,7 @@ class Http(object): else: 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/ headers['if-match'] = info['etag'] @@ -1589,7 +1624,7 @@ class Http(object): 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'): + if '-x-permanent-redirect-url' in info: # 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.", {}, "") @@ -1619,9 +1654,9 @@ class Http(object): return (response, content) 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'] - 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'] elif entry_disposition == "TRANSPARENT": pass @@ -1651,13 +1686,13 @@ class Http(object): content = new_content else: cc = _parse_cache_control(headers) - if cc.has_key('only-if-cached'): + if 'only-if-cached' in cc: info['status'] = '504' response = Response(info) content = "" else: (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 isinstance(e, HttpLib2ErrorWithResponse): response = e.response @@ -1738,9 +1773,8 @@ class Response(dict): 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 + raise AttributeError(name) diff --git a/src/httplib2/cacerts.txt b/src/httplib2/cacerts.txt index b2fe5f18..a2a9833d 100644 --- a/src/httplib2/cacerts.txt +++ b/src/httplib2/cacerts.txt @@ -2165,3 +2165,32 @@ R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5 JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ -----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----- diff --git a/src/httplib2/socks.py b/src/httplib2/socks.py index ab516caa..dbbe5114 100644 --- a/src/httplib2/socks.py +++ b/src/httplib2/socks.py @@ -254,7 +254,7 @@ class socksocket(socket.socket): if self.__proxy[3]: # Resolve remotely ipaddr = None - req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr + req = req + chr(0x03).encode() + chr(len(destaddr)).encode() + destaddr.encode() else: # Resolve locally ipaddr = socket.inet_aton(socket.gethostbyname(destaddr)) diff --git a/src/httplib2/test/functional/test_proxies.py b/src/httplib2/test/functional/test_proxies.py index 0b7880fe..e11369da 100644 --- a/src/httplib2/test/functional/test_proxies.py +++ b/src/httplib2/test/functional/test_proxies.py @@ -1,3 +1,4 @@ +from __future__ import print_function import unittest import errno import os @@ -52,7 +53,7 @@ class FunctionalProxyHttpTest(unittest.TestCase): # TODO use subprocess.check_call when 2.4 is dropped ret = subprocess.call(['tinyproxy', '-c', self.conffile]) self.assertEqual(0, ret) - except OSError, e: + except OSError as e: if e.errno == errno.ENOENT: raise nose.SkipTest('tinyproxy not available') raise @@ -62,11 +63,11 @@ class FunctionalProxyHttpTest(unittest.TestCase): try: pid = int(open(self.pidfile).read()) os.kill(pid, signal.SIGTERM) - except OSError, e: + except OSError as e: if e.errno == errno.ESRCH: - print '\n\n\nTinyProxy Failed to start, log follows:' - print open(self.logfile).read() - print 'end tinyproxy log\n\n\n' + print('\n\n\nTinyProxy Failed to start, log follows:') + print(open(self.logfile).read()) + print('end tinyproxy log\n\n\n') raise map(os.unlink, (self.pidfile, self.logfile, diff --git a/src/httplib2/test/test_ssl_context.py b/src/httplib2/test/test_ssl_context.py index baea240a..5cf9efb0 100644 --- a/src/httplib2/test/test_ssl_context.py +++ b/src/httplib2/test/test_ssl_context.py @@ -1,16 +1,19 @@ -#!/usr/bin/python2 +#!/usr/bin/env python2 +from __future__ import print_function import BaseHTTPServer import logging import os.path -import unittest +import ssl import sys +import unittest import httplib2 - from httplib2.test import miniserver + logger = logging.getLogger(__name__) + class KeepAliveHandler(BaseHTTPServer.BaseHTTPRequestHandler): """ 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 logger.info(s, *args) + class HttpsContextTest(unittest.TestCase): def setUp(self): 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( - KeepAliveHandler, True) + self.ca_certs_path = os.path.join(os.path.dirname(__file__), 'server.pem') + self.httpd, self.port = miniserver.start_server(KeepAliveHandler, True) def tearDown(self): self.httpd.shutdown() def testHttpsContext(self): - if sys.version_info < (2, 7, 9): - 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')) + client = httplib2.Http(ca_certs=self.ca_certs_path) # Establish connection to local server client.request('https://localhost:%d/' % (self.port)) @@ -61,6 +60,27 @@ class HttpsContextTest(unittest.TestCase): self.assertIsInstance(conn.sock.context, ssl.SSLContext) self.assertTrue(conn.sock.context.check_hostname) 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.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()