CVE-2021-21240: Uncontrolled Resource Consumption (CWE-400)
value: httplib2 is a comprehensive HTTP client library for Python. In httplib2 before version 0.19.0, a malicious server which responds with long series of "\xa0" characters in the "www-authenticate" header may cause Denial of Service (CPU burn while parsing header) of the httplib2 client accessing said server. This is fixed in version 0.19.0 which contains a new implementation of auth headers parsing using the pyparsing library.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 7.5
Published: 2021-02-08T20:15Z
Repository: https://github.com/httplib2/httplib2
Language: Python
Files changed: python2/httplib2/__init__.py, python2/httplib2/auth.py, python2/httplib2/error.py, python3/httplib2/__init__.py, python3/httplib2/auth.py, python3/httplib2/error.py, requirements.txt, script/release, script/test, setup.py, tests/__init__.py, tests/test_auth.py
Commit date: 2021-01-27 15:46:27 +0300
Commit message: parse auth headers using pyparsing instead of regexp

Fixes CPU burn DoS by cubic complexity of whitespace matching in
WWW_AUTH_RELAXED (default) regexp.

Diff: diff --git a/python2/httplib2/__init__.py b/python2/httplib2/__init__.py
index f35ba48..9915979 100644
--- a/python2/httplib2/__init__.py
+++ b/python2/httplib2/__init__.py
@@ -59,10 +59,12 @@ try:
 except ImportError:
     try:
         import socks
     except (ImportError, AttributeError):
         socks = None
+from httplib2 import auth
+from httplib2.error import *
 
 # Build the appropriate socket wrapper for ssl
 ssl = None
 ssl_SSLError = None
 ssl_CertificateError = None
@@ -73,13 +75,11 @@ except ImportError:
 if ssl is not None:
     ssl_SSLError = getattr(ssl, "SSLError", None)
     ssl_CertificateError = getattr(ssl, "CertificateError", None)
 
 
-def _ssl_wrap_socket(
-    sock, key_file, cert_file, disable_validation, ca_certs, ssl_version, hostname, key_password
-):
+def _ssl_wrap_socket(sock, key_file, cert_file, disable_validation, ca_certs, ssl_version, hostname, key_password):
     if disable_validation:
         cert_reqs = ssl.CERT_NONE
     else:
         cert_reqs = ssl.CERT_REQUIRED
     if ssl_version is None:
@@ -99,16 +99,11 @@ def _ssl_wrap_socket(
         return context.wrap_socket(sock, server_hostname=hostname)
     else:
         if key_password:
             raise NotSupportedOnThisPlatform("Certificate with password is not supported.")
         return ssl.wrap_socket(
-            sock,
-            keyfile=key_file,
-            certfile=cert_file,
-            cert_reqs=cert_reqs,
-            ca_certs=ca_certs,
-            ssl_version=ssl_version,
+            sock, keyfile=key_file, certfile=cert_file, cert_reqs=cert_reqs, ca_certs=ca_certs, ssl_version=ssl_version,
         )
 
 
 def _ssl_wrap_socket_unsupported(
     sock, key_file, cert_file, disable_validation, ca_certs, ssl_version, hostname, key_password
@@ -275,10 +270,11 @@ class NotRunningAppEngineEnvironment(HttpLib2Error):
 # Will also honor 301 requests by saving that info and never
 # requesting that URI again.
 DEFAULT_MAX_REDIRECTS = 5
 
 from httplib2 import certs
+
 CA_CERTS = certs.where()
 
 # Which headers are hop-by-hop headers by default
 HOP_BY_HOP = [
     "connection",
@@ -363,89 +359,31 @@ def safename(filename):
 
 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()
-        ]
-    )
+    return dict([(key.lower(), NORMALIZE_SPACE.sub(value, " ").strip()) for (key, value) in headers.iteritems()])
 
 
 def _parse_cache_control(headers):
     retval = {}
     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("=")
+            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
 
 
 # Whether to use a strict mode to parse WWW-Authenticate headers
 # Might lead to bad results in case of ill-formed header value,
 # so disabled by default, falling back to relaxed parsing.
 # Set to true to turn on, usefull for testing servers.
 USE_WWW_AUTH_STRICT_PARSING = 0
 
-# In regex below:
-#    [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+             matches a "token" as defined by HTTP
-#    "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?"    matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space
-# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both:
-#    \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08\x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?
-WWW_AUTH_STRICT = re.compile(
-    r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$"
-)
-WWW_AUTH_RELAXED = re.compile(
-    r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$"
-)
-UNQUOTE_PAIRS = re.compile(r"\\(.)")
-
-
-def _parse_www_authenticate(headers, headername="www-authenticate"):
-    """Returns a dictionary of dictionaries, one dict
-    per auth_scheme."""
-    retval = {}
-    if headername in headers:
-        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)
-                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
-
 
 # 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.
 
@@ -476,14 +414,11 @@ def _entry_disposition(response_headers, request_headers):
 
     retval = "STALE"
     cc = _parse_cache_control(request_headers)
     cc_response = _parse_cache_control(response_headers)
 
-    if (
-        "pragma" in request_headers
-        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 "no-cache" in cc:
         retval = "TRANSPARENT"
@@ -538,12 +473,11 @@ def _decompressContent(response, new_content):
             response["-content-encoding"] = response["content-encoding"]
             del response["content-encoding"]
     except (IOError, zlib.error):
         content = ""
         raise FailedToDecompressContent(
-            _("Content purported to be compressed with %s but failed to decompress.")
-            % response.get("content-encoding"),
+            _("Content purported to be compressed with %s but failed to decompress.") % response.get("content-encoding"),
             response,
             content,
         )
     return content
 
@@ -585,21 +519,16 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey):
 
             cache.set(cachekey, text)
 
 
 def _cnonce():
-    dig = _md5(
-        "%s:%s"
-        % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])
-    ).hexdigest()
+    dig = _md5("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest()
     return dig[:16]
 
 
 def _wsse_username_token(cnonce, iso_now, password):
-    return base64.b64encode(
-        _sha("%s%s%s" % (cnonce, iso_now, password)).digest()
-    ).strip()
+    return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip()
 
 
 # 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
@@ -608,13 +537,11 @@ def _wsse_username_token(cnonce, iso_now, password):
 # So we also need each Auth instance to be able to tell us
 # how close to the 'top' it is.
 
 
 class Authentication(object):
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
         (scheme, authority, path, query, fragment) = parse_uri(request_uri)
         self.path = path
         self.host = host
         self.credentials = credentials
         self.http = http
@@ -643,59 +570,36 @@ 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
-        )
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
 
     def request(self, method, request_uri, headers, content):
         """Modify the request headers to add the appropriate
         Authorization header."""
-        headers["authorization"] = (
-            "Basic " + base64.b64encode("%s:%s" % self.credentials).strip()
-        )
+        headers["authorization"] = "Basic " + base64.b64encode("%s:%s" % self.credentials).strip()
 
 
 class DigestAuthentication(Authentication):
     """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
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
-        self.challenge = challenge["digest"]
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        self.challenge = auth._parse_www_authenticate(response, "www-authenticate")["digest"]
         qop = self.challenge.get("qop", "auth")
-        self.challenge["qop"] = (
-            ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
-        )
+        self.challenge["qop"] = ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
         if self.challenge["qop"] is None:
-            raise UnimplementedDigestAuthOptionError(
-                _("Unsupported value for qop: %s." % qop)
-            )
+            raise UnimplementedDigestAuthOptionError(_("Unsupported value for qop: %s." % qop))
         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):
         """Modify the request headers"""
         H = lambda x: _md5(x).hexdigest()
@@ -732,21 +636,17 @@ class DigestAuthentication(Authentication):
             headers["authorization"] += ', opaque="%s"' % self.challenge["opaque"]
         self.challenge["nc"] += 1
 
     def response(self, response, content):
         if "authentication-info" not in response:
-            challenge = _parse_www_authenticate(response, "www-authenticate").get(
-                "digest", {}
-            )
+            challenge = auth._parse_www_authenticate(response, "www-authenticate").get("digest", {})
             if "true" == challenge.get("stale"):
                 self.challenge["nonce"] = challenge["nonce"]
                 self.challenge["nc"] = 1
                 return True
         else:
-            updated_challenge = _parse_www_authenticate(
-                response, "authentication-info"
-            ).get("digest", {})
+            updated_challenge = auth._parse_authentication_info(response, "authentication-info")
 
             if "nextnonce" in updated_challenge:
                 self.challenge["nonce"] = updated_challenge["nextnonce"]
                 self.challenge["nc"] = 1
         return False
@@ -755,17 +655,13 @@ class DigestAuthentication(Authentication):
 class HmacDigestAuthentication(Authentication):
     """Adapted from Robert Sayre's code and DigestAuthentication above."""
 
     __author__ = "Thomas Broyer (t.broyer@ltgt.net)"
 
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
-        Authentication.__init__(
-            self, credentials, host, request_uri, headers, response, content, http
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        challenge = auth._parse_www_authenticate(response, "www-authenticate")
         self.challenge = challenge["hmacdigest"]
         # TODO: self.challenge['domain']
         self.challenge["reason"] = self.challenge.get("reason", "unauthorized")
         if self.challenge["reason"] not in ["unauthorized", "integrity"]:
             self.challenge["reason"] = "unauthorized"
@@ -780,14 +676,11 @@ class HmacDigestAuthentication(Authentication):
                 _("Unsupported value for algorithm: %s." % self.challenge["algorithm"])
             )
         self.challenge["pw-algorithm"] = self.challenge.get("pw-algorithm", "SHA-1")
         if self.challenge["pw-algorithm"] not in ["SHA-1", "MD5"]:
             raise UnimplementedHmacDigestAuthOptionError(
-                _(
-                    "Unsupported value for pw-algorithm: %s."
-                    % self.challenge["pw-algorithm"]
-                )
+                _("Unsupported value for pw-algorithm: %s." % self.challenge["pw-algorithm"])
             )
         if self.challenge["algorithm"] == "HMAC-MD5":
             self.hashmod = _md5
         else:
             self.hashmod = _sha
@@ -797,15 +690,11 @@ class HmacDigestAuthentication(Authentication):
             self.pwhashmod = _sha
         self.key = "".join(
             [
                 self.credentials[0],
                 ":",
-                self.pwhashmod.new(
-                    "".join([self.credentials[1], self.challenge["salt"]])
-                )
-                .hexdigest()
-                .lower(),
+                self.pwhashmod.new("".join([self.credentials[1], self.challenge["salt"]])).hexdigest().lower(),
                 ":",
                 self.challenge["realm"],
             ]
         )
         self.key = self.pwhashmod.new(self.key).hexdigest().lower()
@@ -815,20 +704,12 @@ class HmacDigestAuthentication(Authentication):
         keys = _get_end2end_headers(headers)
         keylist = "".join(["%s " % k for k in keys])
         headers_val = "".join([headers[k] for k in keys])
         created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
         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()
-        )
+        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"'
         ) % (
@@ -841,13 +722,11 @@ class HmacDigestAuthentication(Authentication):
             request_digest,
             keylist,
         )
 
     def response(self, response, content):
-        challenge = _parse_www_authenticate(response, "www-authenticate").get(
-            "hmacdigest", {}
-        )
+        challenge = auth._parse_www_authenticate(response, "www-authenticate").get("hmacdigest", {})
         if challenge.get("reason") in ["integrity", "stale"]:
             return True
         return False
 
 
@@ -858,55 +737,44 @@ class WsseAuthentication(Authentication):
     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"."""
 
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
-        Authentication.__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)
 
     def request(self, method, request_uri, headers, content):
         """Modify the request headers to add the appropriate
         Authorization header."""
         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])
-        headers["X-WSSE"] = (
-            'UsernameToken Username="%s", PasswordDigest="%s", '
-            'Nonce="%s", Created="%s"'
-        ) % (self.credentials[0], password_digest, cnonce, iso_now)
+        headers["X-WSSE"] = ('UsernameToken Username="%s", PasswordDigest="%s", ' 'Nonce="%s", Created="%s"') % (
+            self.credentials[0],
+            password_digest,
+            cnonce,
+            iso_now,
+        )
 
 
 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
 
-        Authentication.__init__(
-            self, credentials, host, request_uri, headers, response, content, http
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        challenge = auth._parse_www_authenticate(response, "www-authenticate")
         service = challenge["googlelogin"].get("service", "xapi")
         # Bloggger actually returns the service in the challenge
         # For the rest we guess based on the URI
         if service == "xapi" and request_uri.find("calendar") > 0:
             service = "cl"
         # No point in guessing Base or Spreadsheet
         # elif request_uri.find("spreadsheets") > 0:
         #    service = "wise"
 
-        auth = dict(
-            Email=credentials[0],
-            Passwd=credentials[1],
-            service=service,
-            source=headers["user-agent"],
-        )
+        auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers["user-agent"],)
         resp, content = self.http.request(
             "https://www.google.com/accounts/ClientLogin",
             method="POST",
             body=urlencode(auth),
             headers={"Content-Type": "application/x-www-form-urlencoded"},
@@ -939,13 +807,11 @@ 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):
             os.makedirs(self.cache)
 
@@ -989,10 +855,11 @@ class Credentials(object):
 
 
 class KeyCerts(Credentials):
     """Identical to Credentials except that
     name/password are mapped to key/cert."""
+
     def add(self, key, cert, domain, password):
         self.credentials.append((domain.lower(), key, cert, password))
 
     def iter(self, domain):
         for (cdomain, key, cert, password) in self.credentials:
@@ -1008,18 +875,11 @@ class ProxyInfo(object):
     """Collect information required to use a proxy."""
 
     bypass_hosts = ()
 
     def __init__(
-        self,
-        proxy_type,
-        proxy_host,
-        proxy_port,
-        proxy_rdns=True,
-        proxy_user=None,
-        proxy_pass=None,
-        proxy_headers=None,
+        self, proxy_type, proxy_host, proxy_port, proxy_rdns=True, proxy_user=None, proxy_pass=None, proxy_headers=None,
     ):
         """Args:
 
           proxy_type: The type of proxy server.  This must be set to one of
           socks.PROXY_TYPE_XXX constants.  For example:  p =
@@ -1163,18 +1023,22 @@ 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!"
-            )
+            raise ProxiesUnavailableError("Proxy support missing but proxy use was requested!")
         if self.proxy_info and self.proxy_info.isgood():
             use_proxy = True
-            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
-                self.proxy_info.astuple()
-            )
+            (
+                proxy_type,
+                proxy_host,
+                proxy_port,
+                proxy_rdns,
+                proxy_user,
+                proxy_pass,
+                proxy_headers,
+            ) = self.proxy_info.astuple()
 
             host = proxy_host
             port = proxy_port
         else:
             use_proxy = False
@@ -1188,17 +1052,11 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
             af, socktype, proto, canonname, sa = res
             try:
                 if use_proxy:
                     self.sock = socks.socksocket(af, socktype, proto)
                     self.sock.setproxy(
-                        proxy_type,
-                        proxy_host,
-                        proxy_port,
-                        proxy_rdns,
-                        proxy_user,
-                        proxy_pass,
-                        proxy_headers,
+                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,
                     )
                 else:
                     self.sock = socket.socket(af, socktype, proto)
                     self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
                 # Different from httplib: support timeouts.
@@ -1208,20 +1066,11 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
                 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,
-                                    proxy_headers,
-                                )
-                            )
+                            % 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)
@@ -1230,20 +1079,11 @@ class HTTPConnectionWithTimeout(httplib.HTTPConnection):
                 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,
-                                    proxy_headers,
-                                )
-                            )
+                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                         )
                 if self.sock:
                     self.sock.close()
                 self.sock = None
                 continue
@@ -1346,13 +1186,19 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
     def connect(self):
         "Connect to a host on a given (SSL) port."
 
         if self.proxy_info and self.proxy_info.isgood():
             use_proxy = True
-            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
-                self.proxy_info.astuple()
-            )
+            (
+                proxy_type,
+                proxy_host,
+                proxy_port,
+                proxy_rdns,
+                proxy_user,
+                proxy_pass,
+                proxy_headers,
+            ) = self.proxy_info.astuple()
 
             host = proxy_host
             port = proxy_port
         else:
             use_proxy = False
@@ -1367,17 +1213,11 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
             try:
                 if use_proxy:
                     sock = socks.socksocket(family, socktype, proto)
 
                     sock.setproxy(
-                        proxy_type,
-                        proxy_host,
-                        proxy_port,
-                        proxy_rdns,
-                        proxy_user,
-                        proxy_pass,
-                        proxy_headers,
+                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,
                     )
                 else:
                     sock = socket.socket(family, socktype, proto)
                     sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
 
@@ -1401,36 +1241,22 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
                 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,
-                                    proxy_headers,
-                                )
-                            )
+                            % 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]
                     if not self._ValidateCertificateHostname(cert, hostname):
                         raise CertificateHostnameMismatch(
-                            "Server presented certificate that does not match "
-                            "host %s: %s" % (hostname, cert),
+                            "Server presented certificate that does not match " "host %s: %s" % (hostname, cert),
                             hostname,
                             cert,
                         )
-            except (
-                ssl_SSLError,
-                ssl_CertificateError,
-                CertificateHostnameMismatch,
-            ) as e:
+            except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch,) as e:
                 if sock:
                     sock.close()
                 if self.sock:
                     self.sock.close()
                 self.sock = None
@@ -1449,20 +1275,11 @@ class HTTPSConnectionWithTimeout(httplib.HTTPSConnection):
                 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,
-                                    proxy_headers,
-                                )
-                            )
+                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                         )
                 if self.sock:
                     self.sock.close()
                 self.sock = None
                 continue
@@ -1476,19 +1293,12 @@ SCHEME_TO_CONNECTION = {
     "https": HTTPSConnectionWithTimeout,
 }
 
 
 def _new_fixed_fetch(validate_certificate):
-
     def fixed_fetch(
-        url,
-        payload=None,
-        method="GET",
-        headers={},
-        allow_truncated=False,
-        follow_redirects=True,
-        deadline=None,
+        url, payload=None, method="GET", headers={}, allow_truncated=False, follow_redirects=True, deadline=None,
     ):
         return fetch(
             url,
             payload=payload,
             method=method,
@@ -1521,13 +1331,11 @@ class AppEngineHttpConnection(httplib.HTTPConnection):
         proxy_info=None,
         ca_certs=None,
         disable_ssl_certificate_validation=False,
         ssl_version=None,
     ):
-        httplib.HTTPConnection.__init__(
-            self, host, port=port, strict=strict, timeout=timeout
-        )
+        httplib.HTTPConnection.__init__(self, host, port=port, strict=strict, timeout=timeout)
 
 
 class AppEngineHttpsConnection(httplib.HTTPSConnection):
     """Same as AppEngineHttpConnection, but for HTTPS URIs.
 
@@ -1550,36 +1358,33 @@ class AppEngineHttpsConnection(httplib.HTTPSConnection):
         key_password=None,
     ):
         if key_password:
             raise NotSupportedOnThisPlatform("Certificate with password is not supported.")
         httplib.HTTPSConnection.__init__(
-            self,
-            host,
-            port=port,
-            key_file=key_file,
-            cert_file=cert_file,
-            strict=strict,
-            timeout=timeout,
+            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)
 
 
 # Use a different connection object for Google App Engine Standard Environment.
 def is_gae_instance():
-    server_software = os.environ.get('SERVER_SOFTWARE', '')
-    if (server_software.startswith('Google App Engine/') or
-        server_software.startswith('Development/') or
-        server_software.startswith('testutil/')):
+    server_software = os.environ.get("SERVER_SOFTWARE", "")
+    if (
+        server_software.startswith("Google App Engine/")
+        or server_software.startswith("Development/")
+        or server_software.startswith("testutil/")
+    ):
         return True
     return False
 
 
 try:
     if not is_gae_instance():
         raise NotRunningAppEngineEnvironment()
 
     from google.appengine.api import apiproxy_stub_map
+
     if apiproxy_stub_map.apiproxy.GetStub("urlfetch") is None:
         raise ImportError
 
     from google.appengine.api.urlfetch import fetch
 
@@ -1714,17 +1519,15 @@ class Http(object):
 
     def _auth_from_challenge(self, host, request_uri, headers, response, content):
         """A generator that creates Authorization objects
            that can be applied to requests.
         """
-        challenges = _parse_www_authenticate(response, "www-authenticate")
+        challenges = auth._parse_www_authenticate(response, "www-authenticate")
         for cred in self.credentials.iter(host):
             for scheme in AUTH_SCHEME_ORDER:
                 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=""):
         """Add a name and password that will be used
         any time a request requires authentication."""
         self.credentials.add(name, password, domain)
@@ -1816,96 +1619,62 @@ class Http(object):
                     content = _decompressContent(response, content)
             break
         return (response, content)
 
     def _request(
-        self,
-        conn,
-        host,
-        absolute_uri,
-        request_uri,
-        method,
-        body,
-        headers,
-        redirections,
-        cachekey,
+        self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey,
     ):
         """Do the actual request using the connection object
         and also follow one level of redirects if necessary"""
 
-        auths = [
-            (auth.depth(request_uri), auth)
-            for auth in self.authorizations
-            if auth.inscope(host, request_uri)
-        ]
+        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:
             auth.request(method, request_uri, headers, body)
 
-        (response, content) = self._conn_request(
-            conn, request_uri, method, body, headers
-        )
+        (response, content) = self._conn_request(conn, request_uri, method, body, headers)
 
         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
-                )
+                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                 response._stale_digest = 1
 
         if response.status == 401:
-            for authorization in self._auth_from_challenge(
-                host, request_uri, headers, response, content
-            ):
+            for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
                 authorization.request(method, request_uri, headers, body)
-                (response, content) = self._conn_request(
-                    conn, request_uri, method, body, headers
-                )
+                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                 if response.status != 401:
                     self.authorizations.append(authorization)
                     authorization.response(response, body)
                     break
 
-        if (
-            self.follow_all_redirects
-            or method in self.safe_methods
-            or response.status in (303, 308)
-        ):
+        if self.follow_all_redirects or method in self.safe_methods or response.status in (303, 308):
             if self.follow_redirects and response.status in self.redirect_codes:
                 # 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 "location" not in response and response.status != 300:
                         raise RedirectMissingLocation(
-                            _(
-                                "Redirected but the response is missing a Location: header."
-                            ),
-                            response,
-                            content,
+                            _("Redirected but the response is missing a Location: header."), response, content,
                         )
                     # Fix-up relative redirects (which violate an RFC 2616 MUST)
                     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
-                            )
+                            response["location"] = urlparse.urljoin(absolute_uri, location)
                     if response.status == 308 or (response.status == 301 and method in self.safe_methods):
                         response["-x-permanent-redirect-url"] = response["location"]
                         if "content-location" not in response:
                             response["content-location"] = absolute_uri
                         _updateCache(headers, response, content, self.cache, cachekey)
                     if "if-none-match" in headers:
                         del headers["if-none-match"]
                     if "if-modified-since" in headers:
                         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"]
                     if "location" in response:
                         location = response["location"]
                         old_response = copy.deepcopy(response)
                         if "content-location" not in old_response:
@@ -1913,22 +1682,16 @@ class Http(object):
                         redirect_method = method
                         if response.status in [302, 303]:
                             redirect_method = "GET"
                             body = None
                         (response, content) = self.request(
-                            location,
-                            method=redirect_method,
-                            body=body,
-                            headers=headers,
-                            redirections=redirections - 1,
+                            location, method=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,
+                        "Redirected more times than rediection_limit allows.", response, content,
                     )
             elif response.status in [200, 203] and method in self.safe_methods:
                 # Don't cache 206's since we aren't going to handle byte range requests
                 if "content-location" not in response:
                     response["content-location"] = absolute_uri
@@ -1942,17 +1705,11 @@ class Http(object):
     # Need to catch and rebrand some exceptions
     # Then need to optionally turn all exceptions into status codes
     # including all socket.* and httplib.* exceptions.
 
     def request(
-        self,
-        uri,
-        method="GET",
-        body=None,
-        headers=None,
-        redirections=DEFAULT_MAX_REDIRECTS,
-        connection_type=None,
+        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.
@@ -1971,11 +1728,11 @@ class Http(object):
 
         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.
         """
-        conn_key = ''
+        conn_key = ""
 
         try:
             if headers is None:
                 headers = {}
             else:
@@ -2092,13 +1849,11 @@ class Http(object):
                     redirect_method = "GET"
                 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.",
-                            {},
-                            "",
+                            "Redirected more times than rediection_limit allows.", {}, "",
                         )
                     (response, new_content) = self.request(
                         info["-x-permanent-redirect-url"],
                         method=redirect_method,
                         headers=headers,
@@ -2125,31 +1880,19 @@ class Http(object):
                         if cached_value:
                             response.fromcache = True
                         return (response, content)
 
                     if entry_disposition == "STALE":
-                        if (
-                            "etag" in info
-                            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 "last-modified" in info and not "last-modified" in headers:
                             headers["if-modified-since"] = info["last-modified"]
                     elif entry_disposition == "TRANSPARENT":
                         pass
 
                     (response, new_content) = self._request(
-                        conn,
-                        authority,
-                        uri,
-                        request_uri,
-                        method,
-                        body,
-                        headers,
-                        redirections,
-                        cachekey,
+                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                     )
 
                 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
@@ -2159,13 +1902,11 @@ class Http(object):
                     for key in _get_end2end_headers(response):
                         info[key] = response[key]
                     merged_response = Response(info)
                     if hasattr(response, "_stale_digest"):
                         merged_response._stale_digest = response._stale_digest
-                    _updateCache(
-                        headers, merged_response, content, self.cache, cachekey
-                    )
+                    _updateCache(headers, merged_response, content, self.cache, cachekey)
                     response = merged_response
                     response.status = 200
                     response.fromcache = True
 
                 elif response.status == 200:
@@ -2179,19 +1920,11 @@ class Http(object):
                     info["status"] = "504"
                     response = Response(info)
                     content = ""
                 else:
                     (response, content) = self._request(
-                        conn,
-                        authority,
-                        uri,
-                        request_uri,
-                        method,
-                        body,
-                        headers,
-                        redirections,
-                        cachekey,
+                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                     )
         except Exception as e:
             is_timeout = isinstance(e, socket.timeout)
             if is_timeout:
                 conn = self.connections.pop(conn_key, None)
@@ -2204,27 +1937,15 @@ class Http(object):
                     content = e.content
                     response.status = 500
                     response.reason = str(e)
                 elif is_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 = Response({"content-type": "text/plain", "status": "400", "content-length": len(content),})
                     response.reason = "Bad Request"
             else:
                 raise
 
         return (response, content)
diff --git a/python2/httplib2/auth.py b/python2/httplib2/auth.py
new file mode 100644
index 0000000..516fdaa
--- /dev/null
+++ b/python2/httplib2/auth.py
@@ -0,0 +1,61 @@
+import base64
+import re
+
+import pyparsing as pp
+
+from .error import *
+
+UNQUOTE_PAIRS = re.compile(r"\\(.)")
+unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1])
+
+# https://tools.ietf.org/html/rfc7235#section-1.2
+# https://tools.ietf.org/html/rfc7235#appendix-B
+tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
+token = pp.Word(tchar).setName("token")
+token68 = pp.Combine(pp.Word("-._~+/" + pp.nums + pp.alphas) + pp.ZeroOrMore("=")).setName("token68")
+
+quoted_string = pp.dblQuotedString.copy().setName("quoted-string").setParseAction(unquote)
+auth_param_name = token.copy().setName("auth-param-name").addParseAction(pp.downcaseTokens)
+auth_param = auth_param_name + pp.Suppress("=") + (token ^ quoted_string)
+params = pp.Dict(pp.delimitedList(pp.Group(auth_param)))
+
+scheme = token("scheme")
+challenge = scheme + (token68("token") ^ params("params"))
+
+authentication_info = params.copy()
+www_authenticate = pp.delimitedList(pp.Group(challenge))
+
+
+def _parse_authentication_info(headers, headername="authentication-info"):
+    """https://tools.ietf.org/html/rfc7615
+    """
+    header = headers.get(headername, "").strip()
+    if not header:
+        return {}
+    try:
+        parsed = authentication_info.parseString(header)
+    except pp.ParseException as ex:
+        # print(ex.explain(ex))
+        raise MalformedHeader(headername)
+
+    return parsed.asDict()
+
+
+def _parse_www_authenticate(headers, headername="www-authenticate"):
+    """Returns a dictionary of dictionaries, one dict per auth_scheme."""
+    header = headers.get(headername, "").strip()
+    if not header:
+        return {}
+    try:
+        parsed = www_authenticate.parseString(header)
+    except pp.ParseException as ex:
+        # print(ex.explain(ex))
+        raise MalformedHeader(headername)
+
+    retval = {
+        challenge["scheme"].lower(): challenge["params"].asDict()
+        if "params" in challenge
+        else {"token": challenge.get("token")}
+        for challenge in parsed
+    }
+    return retval
diff --git a/python2/httplib2/error.py b/python2/httplib2/error.py
new file mode 100644
index 0000000..0e68c12
--- /dev/null
+++ b/python2/httplib2/error.py
@@ -0,0 +1,48 @@
+# All exceptions raised here derive from HttpLib2Error
+class HttpLib2Error(Exception):
+    pass
+
+
+# Some exceptions can be caught and optionally
+# be turned back into responses.
+class HttpLib2ErrorWithResponse(HttpLib2Error):
+    def __init__(self, desc, response, content):
+        self.response = response
+        self.content = content
+        HttpLib2Error.__init__(self, desc)
+
+
+class RedirectMissingLocation(HttpLib2ErrorWithResponse):
+    pass
+
+
+class RedirectLimit(HttpLib2ErrorWithResponse):
+    pass
+
+
+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
diff --git a/python3/httplib2/__init__.py b/python3/httplib2/__init__.py
index 02c19b7..e537017 100644
--- a/python3/httplib2/__init__.py
+++ b/python3/httplib2/__init__.py
@@ -47,10 +47,12 @@ try:
     import socks
 except ImportError:
     # TODO: remove this fallback and copypasted socksipy module upon py2/3 merge,
     # idea is to have soft-dependency on any compatible module called socks
     from . import socks
+from . import auth
+from .error import *
 from .iri2uri import iri2uri
 
 
 def has_timeout(timeout):
     if hasattr(socket, "_GLOBAL_DEFAULT_TIMEOUT"):
@@ -77,60 +79,10 @@ debuglevel = 0
 
 # A request will be tried 'RETRIES' times if it fails at the socket/connection level.
 RETRIES = 2
 
 
-# All exceptions raised here derive from HttpLib2Error
-class HttpLib2Error(Exception):
-    pass
-
-
-# Some exceptions can be caught and optionally
-# be turned back into responses.
-class HttpLib2ErrorWithResponse(HttpLib2Error):
-    def __init__(self, desc, response, content):
-        self.response = response
-        self.content = content
-        HttpLib2Error.__init__(self, desc)
-
-
-class RedirectMissingLocation(HttpLib2ErrorWithResponse):
-    pass
-
-
-class RedirectLimit(HttpLib2ErrorWithResponse):
-    pass
-
-
-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
-
-
 # Open Items:
 # -----------
 
 # Are we removing the cached content too soon on PUT (only delete on 200 Maybe?)
 
@@ -167,32 +119,35 @@ SAFE_METHODS = ("GET", "HEAD", "OPTIONS", "TRACE")
 # To change, assign to `Http().redirect_codes`
 REDIRECT_CODES = frozenset((300, 301, 302, 303, 307, 308))
 
 
 from httplib2 import certs
+
 CA_CERTS = certs.where()
 
 # PROTOCOL_TLS is python 3.5.3+. PROTOCOL_SSLv23 is deprecated.
 # Both PROTOCOL_TLS and PROTOCOL_SSLv23 are equivalent and means:
 # > Selects the highest protocol version that both the client and server support.
 # > Despite the name, this option can select “TLS” protocols as well as “SSL”.
 # source: https://docs.python.org/3.5/library/ssl.html#ssl.PROTOCOL_TLS
-DEFAULT_TLS_VERSION = getattr(ssl, "PROTOCOL_TLS", None) or getattr(
-    ssl, "PROTOCOL_SSLv23"
-)
+DEFAULT_TLS_VERSION = getattr(ssl, "PROTOCOL_TLS", None) or getattr(ssl, "PROTOCOL_SSLv23")
+
 
 def _build_ssl_context(
-    disable_ssl_certificate_validation, ca_certs, cert_file=None, key_file=None,
-    maximum_version=None, minimum_version=None, key_password=None,
+    disable_ssl_certificate_validation,
+    ca_certs,
+    cert_file=None,
+    key_file=None,
+    maximum_version=None,
+    minimum_version=None,
+    key_password=None,
 ):
     if not hasattr(ssl, "SSLContext"):
         raise RuntimeError("httplib2 requires Python 3.2+ for ssl.SSLContext")
 
     context = ssl.SSLContext(DEFAULT_TLS_VERSION)
-    context.verify_mode = (
-        ssl.CERT_NONE if disable_ssl_certificate_validation else ssl.CERT_REQUIRED
-    )
+    context.verify_mode = ssl.CERT_NONE if disable_ssl_certificate_validation else ssl.CERT_REQUIRED
 
     # SSLContext.maximum_version and SSLContext.minimum_version are python 3.7+.
     # source: https://docs.python.org/3/library/ssl.html#ssl.SSLContext.maximum_version
     if maximum_version is not None:
         if hasattr(context, "maximum_version"):
@@ -286,14 +241,11 @@ NORMALIZE_SPACE = re.compile(r"(?:\r\n)?[ \t]+")
 
 
 def _normalize_headers(headers):
     return dict(
         [
-            (
-                _convert_byte_str(key).lower(),
-                NORMALIZE_SPACE.sub(_convert_byte_str(value), " ").strip(),
-            )
+            (_convert_byte_str(key).lower(), NORMALIZE_SPACE.sub(_convert_byte_str(value), " ").strip(),)
             for (key, value) in headers.items()
         ]
     )
 
 
@@ -306,74 +258,23 @@ def _convert_byte_str(s):
 def _parse_cache_control(headers):
     retval = {}
     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("=")
+            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
 
 
 # Whether to use a strict mode to parse WWW-Authenticate headers
 # Might lead to bad results in case of ill-formed header value,
 # so disabled by default, falling back to relaxed parsing.
 # Set to true to turn on, useful for testing servers.
 USE_WWW_AUTH_STRICT_PARSING = 0
 
-# In regex below:
-#    [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+             matches a "token" as defined by HTTP
-#    "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?"    matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space
-# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both:
-#    \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08\x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?
-WWW_AUTH_STRICT = re.compile(
-    r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$"
-)
-WWW_AUTH_RELAXED = re.compile(
-    r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$"
-)
-UNQUOTE_PAIRS = re.compile(r"\\(.)")
-
-
-def _parse_www_authenticate(headers, headername="www-authenticate"):
-    """Returns a dictionary of dictionaries, one dict
-    per auth_scheme."""
-    retval = {}
-    if headername in headers:
-        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)
-                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
-
 
 def _entry_disposition(response_headers, request_headers):
     """Determine freshness from the Date, Expires and Cache-Control headers.
 
     We don't handle the following:
@@ -403,14 +304,11 @@ def _entry_disposition(response_headers, request_headers):
 
     retval = "STALE"
     cc = _parse_cache_control(request_headers)
     cc_response = _parse_cache_control(response_headers)
 
-    if (
-        "pragma" in request_headers
-        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 "no-cache" in cc:
         retval = "TRANSPARENT"
@@ -465,12 +363,11 @@ def _decompressContent(response, new_content):
             response["-content-encoding"] = response["content-encoding"]
             del response["content-encoding"]
     except (IOError, zlib.error):
         content = ""
         raise FailedToDecompressContent(
-            _("Content purported to be compressed with %s but failed to decompress.")
-            % response.get("content-encoding"),
+            _("Content purported to be compressed with %s but failed to decompress.") % response.get("content-encoding"),
             response,
             content,
         )
     return content
 
@@ -482,13 +379,11 @@ def _bind_write_headers(msg):
             print("%s:" % h, end=" ", file=self._fp)
             if isinstance(v, header.Header):
                 print(v.encode(maxlinelen=self._maxheaderlen), file=self._fp)
             else:
                 # email.Header got lots of smarts, so use it.
-                headers = header.Header(
-                    v, maxlinelen=self._maxheaderlen, charset="utf-8", header_name=h
-                )
+                headers = header.Header(v, maxlinelen=self._maxheaderlen, charset="utf-8", header_name=h)
                 print(headers.encode(), file=self._fp)
         # A blank line always separates headers from body.
         print(file=self._fp)
 
     return _write_headers
@@ -529,31 +424,26 @@ def _updateCache(request_headers, response_headers, content, cache, cachekey):
             except UnicodeEncodeError:
                 setattr(info, "_write_headers", _bind_write_headers(info))
                 header_str = info.as_string()
 
             header_str = re.sub("\r(?!\n)|(?<!\r)\n", "\r\n", header_str)
-            text = b"".join(
-                [status_header.encode("utf-8"), header_str.encode("utf-8"), content]
-            )
+            text = b"".join([status_header.encode("utf-8"), header_str.encode("utf-8"), content])
 
             cache.set(cachekey, text)
 
 
 def _cnonce():
     dig = _md5(
-        (
-            "%s:%s"
-            % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])
-        ).encode("utf-8")
+        ("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).encode("utf-8")
     ).hexdigest()
     return dig[:16]
 
 
 def _wsse_username_token(cnonce, iso_now, password):
-    return base64.b64encode(
-        _sha(("%s%s%s" % (cnonce, iso_now, password)).encode("utf-8")).digest()
-    ).strip().decode("utf-8")
+    return (
+        base64.b64encode(_sha(("%s%s%s" % (cnonce, iso_now, password)).encode("utf-8")).digest()).strip().decode("utf-8")
+    )
 
 
 # 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
@@ -562,13 +452,11 @@ def _wsse_username_token(cnonce, iso_now, password):
 # So we also need each Auth instance to be able to tell us
 # how close to the 'top' it is.
 
 
 class Authentication(object):
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
         (scheme, authority, path, query, fragment) = parse_uri(request_uri)
         self.path = path
         self.host = host
         self.credentials = credentials
         self.http = http
@@ -618,16 +506,12 @@ class Authentication(object):
     def __bool__(self):
         return True
 
 
 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
-        )
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
 
     def request(self, method, request_uri, headers, content):
         """Modify the request headers to add the appropriate
         Authorization header."""
         headers["authorization"] = "Basic " + base64.b64encode(
@@ -637,40 +521,23 @@ class BasicAuthentication(Authentication):
 
 class DigestAuthentication(Authentication):
     """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
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
-        self.challenge = challenge["digest"]
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        self.challenge = auth._parse_www_authenticate(response, "www-authenticate")["digest"]
         qop = self.challenge.get("qop", "auth")
-        self.challenge["qop"] = (
-            ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
-        )
+        self.challenge["qop"] = ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
         if self.challenge["qop"] is None:
-            raise UnimplementedDigestAuthOptionError(
-                _("Unsupported value for qop: %s." % qop)
-            )
+            raise UnimplementedDigestAuthOptionError(_("Unsupported value for qop: %s." % qop))
         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):
         """Modify the request headers"""
         H = lambda x: _md5(x.encode("utf-8")).hexdigest()
@@ -707,21 +574,17 @@ class DigestAuthentication(Authentication):
             headers["authorization"] += ', opaque="%s"' % self.challenge["opaque"]
         self.challenge["nc"] += 1
 
     def response(self, response, content):
         if "authentication-info" not in response:
-            challenge = _parse_www_authenticate(response, "www-authenticate").get(
-                "digest", {}
-            )
+            challenge = auth._parse_www_authenticate(response, "www-authenticate").get("digest", {})
             if "true" == challenge.get("stale"):
                 self.challenge["nonce"] = challenge["nonce"]
                 self.challenge["nc"] = 1
                 return True
         else:
-            updated_challenge = _parse_www_authenticate(
-                response, "authentication-info"
-            ).get("digest", {})
+            updated_challenge = auth._parse_authentication_info(response, "authentication-info")
 
             if "nextnonce" in updated_challenge:
                 self.challenge["nonce"] = updated_challenge["nextnonce"]
                 self.challenge["nc"] = 1
         return False
@@ -730,17 +593,13 @@ class DigestAuthentication(Authentication):
 class HmacDigestAuthentication(Authentication):
     """Adapted from Robert Sayre's code and DigestAuthentication above."""
 
     __author__ = "Thomas Broyer (t.broyer@ltgt.net)"
 
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
-        Authentication.__init__(
-            self, credentials, host, request_uri, headers, response, content, http
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
+    def __init__(self, credentials, host, request_uri, headers, response, content, http):
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        challenge = auth._parse_www_authenticate(response, "www-authenticate")
         self.challenge = challenge["hmacdigest"]
         # TODO: self.challenge['domain']
         self.challenge["reason"] = self.challenge.get("reason", "unauthorized")
         if self.challenge["reason"] not in ["unauthorized", "integrity"]:
             self.challenge["reason"] = "unauthorized"
@@ -755,14 +614,11 @@ class HmacDigestAuthentication(Authentication):
                 _("Unsupported value for algorithm: %s." % self.challenge["algorithm"])
             )
         self.challenge["pw-algorithm"] = self.challenge.get("pw-algorithm", "SHA-1")
         if self.challenge["pw-algorithm"] not in ["SHA-1", "MD5"]:
             raise UnimplementedHmacDigestAuthOptionError(
-                _(
-                    "Unsupported value for pw-algorithm: %s."
-                    % self.challenge["pw-algorithm"]
-                )
+                _("Unsupported value for pw-algorithm: %s." % self.challenge["pw-algorithm"])
             )
         if self.challenge["algorithm"] == "HMAC-MD5":
             self.hashmod = _md5
         else:
             self.hashmod = _sha
@@ -772,15 +628,11 @@ class HmacDigestAuthentication(Authentication):
             self.pwhashmod = _sha
         self.key = "".join(
             [
                 self.credentials[0],
                 ":",
-                self.pwhashmod.new(
-                    "".join([self.credentials[1], self.challenge["salt"]])
-                )
-                .hexdigest()
-                .lower(),
+                self.pwhashmod.new("".join([self.credentials[1], self.challenge["salt"]])).hexdigest().lower(),
                 ":",
                 self.challenge["realm"],
             ]
         )
         self.key = self.pwhashmod.new(self.key).hexdigest().lower()
@@ -790,20 +642,12 @@ class HmacDigestAuthentication(Authentication):
         keys = _get_end2end_headers(headers)
         keylist = "".join(["%s " % k for k in keys])
         headers_val = "".join([headers[k] for k in keys])
         created = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
         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()
-        )
+        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"'
         ) % (
@@ -816,13 +660,11 @@ class HmacDigestAuthentication(Authentication):
             request_digest,
             keylist,
         )
 
     def response(self, response, content):
-        challenge = _parse_www_authenticate(response, "www-authenticate").get(
-            "hmacdigest", {}
-        )
+        challenge = auth._parse_www_authenticate(response, "www-authenticate").get("hmacdigest", {})
         if challenge.get("reason") in ["integrity", "stale"]:
             return True
         return False
 
 
@@ -833,55 +675,44 @@ class WsseAuthentication(Authentication):
     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"."""
 
-    def __init__(
-        self, credentials, host, request_uri, headers, response, content, http
-    ):
-        Authentication.__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)
 
     def request(self, method, request_uri, headers, content):
         """Modify the request headers to add the appropriate
         Authorization header."""
         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])
-        headers["X-WSSE"] = (
-            'UsernameToken Username="%s", PasswordDigest="%s", '
-            'Nonce="%s", Created="%s"'
-        ) % (self.credentials[0], password_digest, cnonce, iso_now)
+        headers["X-WSSE"] = ('UsernameToken Username="%s", PasswordDigest="%s", ' 'Nonce="%s", Created="%s"') % (
+            self.credentials[0],
+            password_digest,
+            cnonce,
+            iso_now,
+        )
 
 
 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.parse import urlencode
 
-        Authentication.__init__(
-            self, credentials, host, request_uri, headers, response, content, http
-        )
-        challenge = _parse_www_authenticate(response, "www-authenticate")
+        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
+        challenge = auth._parse_www_authenticate(response, "www-authenticate")
         service = challenge["googlelogin"].get("service", "xapi")
         # Bloggger actually returns the service in the challenge
         # For the rest we guess based on the URI
         if service == "xapi" and request_uri.find("calendar") > 0:
             service = "cl"
         # No point in guessing Base or Spreadsheet
         # elif request_uri.find("spreadsheets") > 0:
         #    service = "wise"
 
-        auth = dict(
-            Email=credentials[0],
-            Passwd=credentials[1],
-            service=service,
-            source=headers["user-agent"],
-        )
+        auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers["user-agent"],)
         resp, content = self.http.request(
             "https://www.google.com/accounts/ClientLogin",
             method="POST",
             body=urlencode(auth),
             headers={"Content-Type": "application/x-www-form-urlencoded"},
@@ -914,13 +745,11 @@ 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):
             os.makedirs(self.cache)
 
@@ -964,10 +793,11 @@ class Credentials(object):
 
 
 class KeyCerts(Credentials):
     """Identical to Credentials except that
     name/password are mapped to key/cert."""
+
     def add(self, key, cert, domain, password):
         self.credentials.append((domain.lower(), key, cert, password))
 
     def iter(self, domain):
         for (cdomain, key, cert, password) in self.credentials:
@@ -983,18 +813,11 @@ class ProxyInfo(object):
     """Collect information required to use a proxy."""
 
     bypass_hosts = ()
 
     def __init__(
-        self,
-        proxy_type,
-        proxy_host,
-        proxy_port,
-        proxy_rdns=True,
-        proxy_user=None,
-        proxy_pass=None,
-        proxy_headers=None,
+        self, proxy_type, proxy_host, proxy_port, proxy_rdns=True, proxy_user=None, proxy_pass=None, proxy_headers=None,
     ):
         """Args:
 
           proxy_type: The type of proxy server.  This must be set to one of
           socks.PROXY_TYPE_XXX constants.  For example:  p =
@@ -1013,11 +836,19 @@ class ProxyInfo(object):
         """
         if isinstance(proxy_user, bytes):
             proxy_user = proxy_user.decode()
         if isinstance(proxy_pass, bytes):
             proxy_pass = proxy_pass.decode()
-        self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass, self.proxy_headers = (
+        (
+            self.proxy_type,
+            self.proxy_host,
+            self.proxy_port,
+            self.proxy_rdns,
+            self.proxy_user,
+            self.proxy_pass,
+            self.proxy_headers,
+        ) = (
             proxy_type,
             proxy_host,
             proxy_port,
             proxy_rdns,
             proxy_user,
@@ -1147,18 +978,22 @@ class HTTPConnectionWithTimeout(http.client.HTTPConnection):
             self.proxy_info = proxy_info("http")
 
     def connect(self):
         """Connect to the host and port specified in __init__."""
         if self.proxy_info and socks is None:
-            raise ProxiesUnavailableError(
-                "Proxy support missing but proxy use was requested!"
-            )
+            raise ProxiesUnavailableError("Proxy support missing but proxy use was requested!")
         if self.proxy_info and self.proxy_info.isgood() and self.proxy_info.applies_to(self.host):
             use_proxy = True
-            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
-                self.proxy_info.astuple()
-            )
+            (
+                proxy_type,
+                proxy_host,
+                proxy_port,
+                proxy_rdns,
+                proxy_user,
+                proxy_pass,
+                proxy_headers,
+            ) = self.proxy_info.astuple()
 
             host = proxy_host
             port = proxy_port
         else:
             use_proxy = False
@@ -1173,39 +1008,23 @@ class HTTPConnectionWithTimeout(http.client.HTTPConnection):
             af, socktype, proto, canonname, sa = res
             try:
                 if use_proxy:
                     self.sock = socks.socksocket(af, socktype, proto)
                     self.sock.setproxy(
-                        proxy_type,
-                        proxy_host,
-                        proxy_port,
-                        proxy_rdns,
-                        proxy_user,
-                        proxy_pass,
+                        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)
                 if has_timeout(self.timeout):
                     self.sock.settimeout(self.timeout)
                 if self.debuglevel > 0:
-                    print(
-                        "connect: ({0}, {1}) ************".format(self.host, self.port)
-                    )
+                    print("connect: ({0}, {1}) ************".format(self.host, self.port))
                     if use_proxy:
                         print(
                             "proxy: {0} ************".format(
-                                str(
-                                    (
-                                        proxy_host,
-                                        proxy_port,
-                                        proxy_rdns,
-                                        proxy_user,
-                                        proxy_pass,
-                                        proxy_headers,
-                                    )
-                                )
+                                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 as e:
@@ -1213,20 +1032,11 @@ class HTTPConnectionWithTimeout(http.client.HTTPConnection):
                 if self.debuglevel > 0:
                     print("connect fail: ({0}, {1})".format(self.host, self.port))
                     if use_proxy:
                         print(
                             "proxy: {0}".format(
-                                str(
-                                    (
-                                        proxy_host,
-                                        proxy_port,
-                                        proxy_rdns,
-                                        proxy_user,
-                                        proxy_pass,
-                                        proxy_headers,
-                                    )
-                                )
+                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                             )
                         )
                 if self.sock:
                     self.sock.close()
                 self.sock = None
@@ -1266,31 +1076,38 @@ class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
         self.proxy_info = proxy_info
         if proxy_info and not isinstance(proxy_info, ProxyInfo):
             self.proxy_info = proxy_info("https")
 
         context = _build_ssl_context(
-            self.disable_ssl_certificate_validation, self.ca_certs, cert_file, key_file,
-            maximum_version=tls_maximum_version, minimum_version=tls_minimum_version,
+            self.disable_ssl_certificate_validation,
+            self.ca_certs,
+            cert_file,
+            key_file,
+            maximum_version=tls_maximum_version,
+            minimum_version=tls_minimum_version,
             key_password=key_password,
         )
         super(HTTPSConnectionWithTimeout, self).__init__(
-            host,
-            port=port,
-            timeout=timeout,
-            context=context,
+            host, port=port, timeout=timeout, context=context,
         )
         self.key_file = key_file
         self.cert_file = cert_file
         self.key_password = key_password
 
     def connect(self):
         """Connect to a host on a given (SSL) port."""
         if self.proxy_info and self.proxy_info.isgood() and self.proxy_info.applies_to(self.host):
             use_proxy = True
-            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
-                self.proxy_info.astuple()
-            )
+            (
+                proxy_type,
+                proxy_host,
+                proxy_port,
+                proxy_rdns,
+                proxy_user,
+                proxy_pass,
+                proxy_headers,
+            ) = self.proxy_info.astuple()
 
             host = proxy_host
             port = proxy_port
         else:
             use_proxy = False
@@ -1307,16 +1124,11 @@ class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
             try:
                 if use_proxy:
                     sock = socks.socksocket(family, socktype, proto)
 
                     sock.setproxy(
-                        proxy_type,
-                        proxy_host,
-                        proxy_port,
-                        proxy_rdns,
-                        proxy_user,
-                        proxy_pass,
+                        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):
@@ -1324,14 +1136,11 @@ class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
                 sock.connect((self.host, self.port))
 
                 self.sock = self._context.wrap_socket(sock, server_hostname=self.host)
 
                 # Python 3.3 compatibility: emulate the check_hostname behavior
-                if (
-                    not hasattr(self._context, "check_hostname")
-                    and not self.disable_ssl_certificate_validation
-                ):
+                if not hasattr(self._context, "check_hostname") and not self.disable_ssl_certificate_validation:
                     try:
                         ssl.match_hostname(self.sock.getpeercert(), self.host)
                     except Exception:
                         self.sock.shutdown(socket.SHUT_RDWR)
                         self.sock.close()
@@ -1340,20 +1149,11 @@ class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
                 if self.debuglevel > 0:
                     print("connect: ({0}, {1})".format(self.host, self.port))
                     if use_proxy:
                         print(
                             "proxy: {0}".format(
-                                str(
-                                    (
-                                        proxy_host,
-                                        proxy_port,
-                                        proxy_rdns,
-                                        proxy_user,
-                                        proxy_pass,
-                                        proxy_headers,
-                                    )
-                                )
+                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                             )
                         )
             except (ssl.SSLError, ssl.CertificateError) as e:
                 if sock:
                     sock.close()
@@ -1368,20 +1168,11 @@ class HTTPSConnectionWithTimeout(http.client.HTTPSConnection):
                 if self.debuglevel > 0:
                     print("connect fail: ({0}, {1})".format(self.host, self.port))
                     if use_proxy:
                         print(
                             "proxy: {0}".format(
-                                str(
-                                    (
-                                        proxy_host,
-                                        proxy_port,
-                                        proxy_rdns,
-                                        proxy_user,
-                                        proxy_pass,
-                                        proxy_headers,
-                                    )
-                                )
+                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                             )
                         )
                 if self.sock:
                     self.sock.close()
                 self.sock = None
@@ -1521,17 +1312,15 @@ class Http(object):
 
     def _auth_from_challenge(self, host, request_uri, headers, response, content):
         """A generator that creates Authorization objects
            that can be applied to requests.
         """
-        challenges = _parse_www_authenticate(response, "www-authenticate")
+        challenges = auth._parse_www_authenticate(response, "www-authenticate")
         for cred in self.credentials.iter(host):
             for scheme in AUTH_SCHEME_ORDER:
                 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=""):
         """Add a name and password that will be used
         any time a request requires authentication."""
         self.credentials.add(name, password, domain)
@@ -1561,13 +1350,11 @@ class Http(object):
                 raise
             except socket.gaierror:
                 conn.close()
                 raise ServerNotFoundError("Unable to find the server at %s" % conn.host)
             except socket.error as e:
-                errno_ = (
-                    e.args[0].errno if isinstance(e.args[0], socket.error) else e.errno
-                )
+                errno_ = e.args[0].errno if isinstance(e.args[0], socket.error) else e.errno
                 if errno_ in (errno.ENETUNREACH, errno.EADDRNOTAVAIL) and i < RETRIES:
                     continue  # retry on potentially transient errors
                 raise
             except http.client.HTTPException:
                 if conn.sock is None:
@@ -1622,96 +1409,62 @@ class Http(object):
 
             break
         return (response, content)
 
     def _request(
-        self,
-        conn,
-        host,
-        absolute_uri,
-        request_uri,
-        method,
-        body,
-        headers,
-        redirections,
-        cachekey,
+        self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey,
     ):
         """Do the actual request using the connection object
         and also follow one level of redirects if necessary"""
 
-        auths = [
-            (auth.depth(request_uri), auth)
-            for auth in self.authorizations
-            if auth.inscope(host, request_uri)
-        ]
+        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:
             auth.request(method, request_uri, headers, body)
 
-        (response, content) = self._conn_request(
-            conn, request_uri, method, body, headers
-        )
+        (response, content) = self._conn_request(conn, request_uri, method, body, headers)
 
         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
-                )
+                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                 response._stale_digest = 1
 
         if response.status == 401:
-            for authorization in self._auth_from_challenge(
-                host, request_uri, headers, response, content
-            ):
+            for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
                 authorization.request(method, request_uri, headers, body)
-                (response, content) = self._conn_request(
-                    conn, request_uri, method, body, headers
-                )
+                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                 if response.status != 401:
                     self.authorizations.append(authorization)
                     authorization.response(response, body)
                     break
 
-        if (
-            self.follow_all_redirects
-            or method in self.safe_methods
-            or response.status in (303, 308)
-        ):
+        if self.follow_all_redirects or method in self.safe_methods or response.status in (303, 308):
             if self.follow_redirects and response.status in self.redirect_codes:
                 # 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 "location" not in response and response.status != 300:
                         raise RedirectMissingLocation(
-                            _(
-                                "Redirected but the response is missing a Location: header."
-                            ),
-                            response,
-                            content,
+                            _("Redirected but the response is missing a Location: header."), response, content,
                         )
                     # Fix-up relative redirects (which violate an RFC 2616 MUST)
                     if "location" in response:
                         location = response["location"]
                         (scheme, authority, path, query, fragment) = parse_uri(location)
                         if authority == None:
-                            response["location"] = urllib.parse.urljoin(
-                                absolute_uri, location
-                            )
+                            response["location"] = urllib.parse.urljoin(absolute_uri, location)
                     if response.status == 308 or (response.status == 301 and (method in self.safe_methods)):
                         response["-x-permanent-redirect-url"] = response["location"]
                         if "content-location" not in response:
                             response["content-location"] = absolute_uri
                         _updateCache(headers, response, content, self.cache, cachekey)
                     if "if-none-match" in headers:
                         del headers["if-none-match"]
                     if "if-modified-since" in headers:
                         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"]
                     if "location" in response:
                         location = response["location"]
                         old_response = copy.deepcopy(response)
                         if "content-location" not in old_response:
@@ -1719,22 +1472,16 @@ class Http(object):
                         redirect_method = method
                         if response.status in [302, 303]:
                             redirect_method = "GET"
                             body = None
                         (response, content) = self.request(
-                            location,
-                            method=redirect_method,
-                            body=body,
-                            headers=headers,
-                            redirections=redirections - 1,
+                            location, method=redirect_method, body=body, headers=headers, redirections=redirections - 1,
                         )
                         response.previous = old_response
                 else:
                     raise RedirectLimit(
-                        "Redirected more times than redirection_limit allows.",
-                        response,
-                        content,
+                        "Redirected more times than redirection_limit allows.", response, content,
                     )
             elif response.status in [200, 203] and method in self.safe_methods:
                 # Don't cache 206's since we aren't going to handle byte range requests
                 if "content-location" not in response:
                     response["content-location"] = absolute_uri
@@ -1748,17 +1495,11 @@ class Http(object):
     # Need to catch and rebrand some exceptions
     # Then need to optionally turn all exceptions into status codes
     # including all socket.* and httplib.* exceptions.
 
     def request(
-        self,
-        uri,
-        method="GET",
-        body=None,
-        headers=None,
-        redirections=DEFAULT_MAX_REDIRECTS,
-        connection_type=None,
+        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.
 
@@ -1776,11 +1517,11 @@ 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.
         """
-        conn_key = ''
+        conn_key = ""
 
         try:
             if headers is None:
                 headers = {}
             else:
@@ -1845,13 +1586,11 @@ a string that contains the response entity body.
                     try:
                         info, content = cached_value.split(b"\r\n\r\n", 1)
                         info = email.message_from_bytes(info)
                         for k, v in info.items():
                             if v.startswith("=?") and v.endswith("?="):
-                                info.replace_header(
-                                    k, str(*email.header.decode_header(v)[0])
-                                )
+                                info.replace_header(k, str(*email.header.decode_header(v)[0]))
                     except (IndexError, ValueError):
                         self.cache.delete(cachekey)
                         cachekey = None
                         cached_value = None
 
@@ -1894,13 +1633,11 @@ a string that contains the response entity body.
                     redirect_method = "GET"
                 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 redirection_limit allows.",
-                            {},
-                            "",
+                            "Redirected more times than redirection_limit allows.", {}, "",
                         )
                     (response, new_content) = self.request(
                         info["-x-permanent-redirect-url"],
                         method=redirect_method,
                         headers=headers,
@@ -1927,31 +1664,19 @@ a string that contains the response entity body.
                         if cached_value:
                             response.fromcache = True
                         return (response, content)
 
                     if entry_disposition == "STALE":
-                        if (
-                            "etag" in info
-                            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 "last-modified" in info and not "last-modified" in headers:
                             headers["if-modified-since"] = info["last-modified"]
                     elif entry_disposition == "TRANSPARENT":
                         pass
 
                     (response, new_content) = self._request(
-                        conn,
-                        authority,
-                        uri,
-                        request_uri,
-                        method,
-                        body,
-                        headers,
-                        redirections,
-                        cachekey,
+                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                     )
 
                 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
@@ -1961,13 +1686,11 @@ a string that contains the response entity body.
                     for key in _get_end2end_headers(response):
                         info[key] = response[key]
                     merged_response = Response(info)
                     if hasattr(response, "_stale_digest"):
                         merged_response._stale_digest = response._stale_digest
-                    _updateCache(
-                        headers, merged_response, content, self.cache, cachekey
-                    )
+                    _updateCache(headers, merged_response, content, self.cache, cachekey)
                     response = merged_response
                     response.status = 200
                     response.fromcache = True
 
                 elif response.status == 200:
@@ -1981,19 +1704,11 @@ a string that contains the response entity body.
                     info["status"] = "504"
                     response = Response(info)
                     content = b""
                 else:
                     (response, content) = self._request(
-                        conn,
-                        authority,
-                        uri,
-                        request_uri,
-                        method,
-                        body,
-                        headers,
-                        redirections,
-                        cachekey,
+                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                     )
         except Exception as e:
             is_timeout = isinstance(e, socket.timeout)
             if is_timeout:
                 conn = self.connections.pop(conn_key, None)
@@ -2006,27 +1721,15 @@ a string that contains the response entity body.
                     content = e.content
                     response.status = 500
                     response.reason = str(e)
                 elif isinstance(e, socket.timeout):
                     content = b"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).encode("utf-8")
-                    response = Response(
-                        {
-                            "content-type": "text/plain",
-                            "status": "400",
-                            "content-length": len(content),
-                        }
-                    )
+                    response = Response({"content-type": "text/plain", "status": "400", "content-length": len(content),})
                     response.reason = "Bad Request"
             else:
                 raise
 
         return (response, content)
diff --git a/python3/httplib2/auth.py b/python3/httplib2/auth.py
new file mode 100644
index 0000000..516fdaa
--- /dev/null
+++ b/python3/httplib2/auth.py
@@ -0,0 +1,61 @@
+import base64
+import re
+
+import pyparsing as pp
+
+from .error import *
+
+UNQUOTE_PAIRS = re.compile(r"\\(.)")
+unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1])
+
+# https://tools.ietf.org/html/rfc7235#section-1.2
+# https://tools.ietf.org/html/rfc7235#appendix-B
+tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
+token = pp.Word(tchar).setName("token")
+token68 = pp.Combine(pp.Word("-._~+/" + pp.nums + pp.alphas) + pp.ZeroOrMore("=")).setName("token68")
+
+quoted_string = pp.dblQuotedString.copy().setName("quoted-string").setParseAction(unquote)
+auth_param_name = token.copy().setName("auth-param-name").addParseAction(pp.downcaseTokens)
+auth_param = auth_param_name + pp.Suppress("=") + (token ^ quoted_string)
+params = pp.Dict(pp.delimitedList(pp.Group(auth_param)))
+
+scheme = token("scheme")
+challenge = scheme + (token68("token") ^ params("params"))
+
+authentication_info = params.copy()
+www_authenticate = pp.delimitedList(pp.Group(challenge))
+
+
+def _parse_authentication_info(headers, headername="authentication-info"):
+    """https://tools.ietf.org/html/rfc7615
+    """
+    header = headers.get(headername, "").strip()
+    if not header:
+        return {}
+    try:
+        parsed = authentication_info.parseString(header)
+    except pp.ParseException as ex:
+        # print(ex.explain(ex))
+        raise MalformedHeader(headername)
+
+    return parsed.asDict()
+
+
+def _parse_www_authenticate(headers, headername="www-authenticate"):
+    """Returns a dictionary of dictionaries, one dict per auth_scheme."""
+    header = headers.get(headername, "").strip()
+    if not header:
+        return {}
+    try:
+        parsed = www_authenticate.parseString(header)
+    except pp.ParseException as ex:
+        # print(ex.explain(ex))
+        raise MalformedHeader(headername)
+
+    retval = {
+        challenge["scheme"].lower(): challenge["params"].asDict()
+        if "params" in challenge
+        else {"token": challenge.get("token")}
+        for challenge in parsed
+    }
+    return retval
diff --git a/python3/httplib2/error.py b/python3/httplib2/error.py
new file mode 100644
index 0000000..0e68c12
--- /dev/null
+++ b/python3/httplib2/error.py
@@ -0,0 +1,48 @@
+# All exceptions raised here derive from HttpLib2Error
+class HttpLib2Error(Exception):
+    pass
+
+
+# Some exceptions can be caught and optionally
+# be turned back into responses.
+class HttpLib2ErrorWithResponse(HttpLib2Error):
+    def __init__(self, desc, response, content):
+        self.response = response
+        self.content = content
+        HttpLib2Error.__init__(self, desc)
+
+
+class RedirectMissingLocation(HttpLib2ErrorWithResponse):
+    pass
+
+
+class RedirectLimit(HttpLib2ErrorWithResponse):
+    pass
+
+
+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
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..4ebe545
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1 @@
+pyparsing>=2.4.2,<3 # TODO include v3 after dropping Python2 support
diff --git a/script/release b/script/release
index a2ff80d..d3ee460 100755
--- a/script/release
+++ b/script/release
@@ -208,15 +208,15 @@ assert_tree_clean() {
 	fi
 }
 
 version_check() {
 	local need=$1
-	local version_setup=$(fgrep 'VERSION =' setup.py |tr -d " '\"" |cut -d\= -f2)
-	local version_py2=$(cd python2 ; python2 -Es -c 'import httplib2;print(httplib2.__version__)')
-	local version_py3=$(cd python3 ; python3 -Es -c 'import httplib2;print(httplib2.__version__)')
+	local version_setup=$(python setup.py --version)
+	local version_py2=$(python -Es -c "$(egrep '^__version__' python2/httplib2/__init__.py);print(__version__)")
+	local version_py3=$(python -Es -c "$(egrep '^__version__' python3/httplib2/__init__.py);print(__version__)")
 	if [[ "$version_setup" != "$need" ]] ; then
-		echo "error: setup.py VERSION=$version_setup expected=$need" >&1
+		echo "error: setup.py version=$version_setup expected=$need" >&1
 		exit 1
 	fi
 	if [[ "$version_py2" != "$need" ]] ; then
 		echo "error: python2/httplib2/__init__.py:__version__=$version_py2 expected=$need" >&1
 		exit 1
diff --git a/script/test b/script/test
index a3dca92..361c483 100755
--- a/script/test
+++ b/script/test
@@ -63,12 +63,12 @@ main() {
 }
 
 install_check_version() {
 	local pip="$1"
 	$pip install dist/httplib2*
-	version_source=$(cd python3 ; python3 -Es -c 'import httplib2;print(httplib2.__version__)')
-	version_installed=$($pip show httplib2 |fgrep Version |cut -d' ' -f2)
+	version_source=$(python setup.py --version)
+	version_installed=$($pip show httplib2 |fgrep Version: |cut -d' ' -f2)
 	if [[ "$version_source" != "$version_installed" ]] ; then
 		echo "error: installed package version=$version_installed does not match source=$version_source" >&2
 		exit 1
 	fi
 }
diff --git a/setup.py b/setup.py
index b66d24e..96f7dfa 100755
--- a/setup.py
+++ b/setup.py
@@ -12,23 +12,24 @@ VERSION = "0.18.1"
 # Otherwise, use `script/test`
 class TestCommand(setuptools.command.test.test):
     def run_tests(self):
         # pytest may be not installed yet
         import pytest
-        args = ['--forked', '--fulltrace', '--no-cov', 'tests/']
+
+        args = ["--forked", "--fulltrace", "--no-cov", "tests/"]
         if self.test_suite:
-            args += ['-k', self.test_suite]
-        sys.stderr.write('setup.py:test run pytest {}\n'.format(' '.join(args)))
+            args += ["-k", self.test_suite]
+        sys.stderr.write("setup.py:test run pytest {}\n".format(" ".join(args)))
         errno = pytest.main(args)
         sys.exit(errno)
 
 
 def read_requirements(name):
     project_root = os.path.dirname(os.path.abspath(__file__))
-    with open(os.path.join(project_root, name), 'rb') as f:
+    with open(os.path.join(project_root, name), "rb") as f:
         # remove whitespace and comments
-        g = (line.decode('utf-8').lstrip().split('#', 1)[0].rstrip() for line in f)
+        g = (line.decode("utf-8").lstrip().split("#", 1)[0].rstrip() for line in f)
         return [l for l in g if l]
 
 
 setuptools.setup(
     name="httplib2",
@@ -83,10 +84,11 @@ A comprehensive HTTP client library, ``httplib2`` supports many features left ou
   A large and growing set of unit tests.
 """,
     package_dir=pkgdir,
     packages=["httplib2"],
     package_data={"httplib2": ["*.txt"]},
+    install_requires=read_requirements("requirements.txt"),
     tests_require=read_requirements("requirements-test.txt"),
     cmdclass={"test": TestCommand},
     classifiers=[
         "Development Status :: 4 - Beta",
         "Environment :: Web Environment",
diff --git a/tests/__init__.py b/tests/__init__.py
index 28c8d13..fc8a96c 100644
--- a/tests/__init__.py
+++ b/tests/__init__.py
@@ -117,13 +117,13 @@ def parse_http_message(kind, buf):
     except EOFError:
         return None
     msg = kind()
     msg.raw = start_line
     if kind is HttpRequest:
-        assert re.match(
-            br".+ HTTP/\d\.\d\r\n$", start_line
-        ), "Start line does not look like HTTP request: " + repr(start_line)
+        assert re.match(br".+ HTTP/\d\.\d\r\n$", start_line), "Start line does not look like HTTP request: " + repr(
+            start_line
+        )
         msg.method, msg.uri, msg.proto = start_line.rstrip().decode().split(" ", 2)
         assert msg.proto.startswith("HTTP/"), repr(start_line)
     elif kind is HttpResponse:
         assert re.match(
             br"^HTTP/\d\.\d \d+ .+\r\n$", start_line
@@ -199,18 +199,11 @@ class MockResponse(six.BytesIO):
 class MockHTTPConnection(object):
     """This class is just a mock of httplib.HTTPConnection used for testing
     """
 
     def __init__(
-        self,
-        host,
-        port=None,
-        key_file=None,
-        cert_file=None,
-        strict=None,
-        timeout=None,
-        proxy_info=None,
+        self, host, port=None, key_file=None, cert_file=None, strict=None, timeout=None, proxy_info=None,
     ):
         self.host = host
         self.port = port
         self.timeout = timeout
         self.log = ""
@@ -238,18 +231,11 @@ class MockHTTPBadStatusConnection(object):
     """
 
     num_calls = 0
 
     def __init__(
-        self,
-        host,
-        port=None,
-        key_file=None,
-        cert_file=None,
-        strict=None,
-        timeout=None,
-        proxy_info=None,
+        self, host, port=None, key_file=None, cert_file=None, strict=None, timeout=None, proxy_info=None,
     ):
         self.host = host
         self.port = port
         self.timeout = timeout
         self.log = ""
@@ -326,15 +312,11 @@ def server_socket(fun, request_count=1, timeout=5, scheme="", tls=None):
                         pass
                     # FIXME: client.close() introduces connection reset by peer
                     # at least in other/connection_close test
                     # should not be a problem since socket would close upon garbage collection
             if gcounter[0] > request_count:
-                gresult[0] = Exception(
-                    "Request count expected={0} actual={1}".format(
-                        request_count, gcounter[0]
-                    )
-                )
+                gresult[0] = Exception("Request count expected={0} actual={1}".format(request_count, gcounter[0]))
         except Exception as e:
             # traceback.print_exc caused IOError: concurrent operation on sys.stderr.close() under setup.py test
             print(traceback.format_exc(), file=sys.stderr)
             gresult[0] = e
 
@@ -456,25 +438,16 @@ def http_response_bytes(
     if add_date:
         headers.setdefault("date", email.utils.formatdate())
     if add_etag:
         headers.setdefault("etag", '"{0}"'.format(hashlib.md5(body).hexdigest()))
     header_string = "".join("{0}: {1}\r\n".format(k, v) for k, v in headers.items())
-    if (
-        not undefined_body_length
-        and proto != "HTTP/1.0"
-        and "content-length" not in headers
-    ):
-        raise Exception(
-            "httplib2.tests.http_response_bytes: client could not figure response body length"
-        )
+    if not undefined_body_length and proto != "HTTP/1.0" and "content-length" not in headers:
+        raise Exception("httplib2.tests.http_response_bytes: client could not figure response body length")
     if str(status).isdigit():
         status = "{} {}".format(status, http_client.responses[status])
     response = (
-        "{proto} {status}\r\n{headers}\r\n".format(
-            proto=proto, status=status, headers=header_string
-        ).encode()
-        + body
+        "{proto} {status}\r\n{headers}\r\n".format(proto=proto, status=status, headers=header_string).encode() + body
     )
     return response
 
 
 def make_http_reflect(**kwargs):
@@ -524,25 +497,10 @@ def server_reflect(**kwargs):
     response_kwargs = {k: kwargs.pop(k) for k in dict(kwargs) if k in _http_kwargs}
     http_handler = make_http_reflect(**response_kwargs)
     return server_request(http_handler, **kwargs)
 
 
-def http_parse_auth(s):
-    """https://tools.ietf.org/html/rfc7235#section-2.1
-    """
-    scheme, rest = s.split(" ", 1)
-    result = {}
-    while True:
-        m = httplib2.WWW_AUTH_RELAXED.search(rest)
-        if not m:
-            break
-        if len(m.groups()) == 3:
-            key, value, rest = m.groups()
-            result[key.lower()] = httplib2.UNQUOTE_PAIRS.sub(r"\1", value)
-    return result
-
-
 def store_request_response(out):
     def wrapper(fun):
         @functools.wraps(fun)
         def wrapped(request, *a, **kw):
             response_bytes = fun(request, *a, **kw)
@@ -607,18 +565,23 @@ def http_reflect_with_auth(allow_scheme, allow_credentials, out_renew_nonce=None
     @store_request_response(out_requests)
     def http_reflect_with_auth_handler(request):
         auth_header = request.headers.get("authorization", "")
         if not auth_header:
             return deny()
-        if " " not in auth_header:
+        try:
+            auth_parsed = httplib2.auth._parse_www_authenticate(request.headers, "authorization")
+            print("debug: auth_parsed", auth_parsed)
+        except httplib2.error.MalformedHeader:
+            print("debug: auth header error")
             return http_response_bytes(status=400, body=b"authorization header syntax error")
-        scheme, data = auth_header.split(" ", 1)
-        scheme = scheme.lower()
+        scheme = auth_header.split(" ", 1)[0].lower()
+        print("debug: first auth scheme='{}'".format(scheme))
         if scheme != allow_scheme:
             return deny(body=b"must use different auth scheme")
+        auth_info = auth_parsed[scheme]
         if scheme == "basic":
-            decoded = base64.b64decode(data).decode()
+            decoded = base64.b64decode(auth_info["token"]).decode()
             username, password = decoded.split(":", 1)
             if (username, password) in allow_credentials:
                 return make_http_reflect()(request)
             else:
                 return deny(body=b"supplied credentials are not allowed")
@@ -628,11 +591,10 @@ def http_reflect_with_auth(allow_scheme, allow_credentials, out_renew_nonce=None
             if nextnonce:
                 # server decided to change nonce, in this case, guided by caller test code
                 gserver_nonce[0] = nextnonce
                 gnextnonce[0] = None
             server_nonce_current = gserver_nonce[0]
-            auth_info = http_parse_auth(data)
             client_cnonce = auth_info.get("cnonce", "")
             client_nc = auth_info.get("nc", "")
             client_nonce = auth_info.get("nonce", "")
             client_opaque = auth_info.get("opaque", "")
             client_qop = auth_info.get("qop", "auth").strip('"')
@@ -649,72 +611,58 @@ def http_reflect_with_auth(allow_scheme, allow_credentials, out_renew_nonce=None
                 return deny(body=b"invalid nonce")
             if not client_nc:
                 return deny(body=b"auth-info nc missing")
             if client_opaque != server_opaque:
                 return deny(
-                    body="auth-info opaque mismatch expected={} actual={}".format(
-                        server_opaque, client_opaque
-                    ).encode()
+                    body="auth-info opaque mismatch expected={} actual={}".format(server_opaque, client_opaque).encode()
                 )
             for allow_username, allow_password in allow_credentials:
-                ha1 = hasher(
-                    ":".join((allow_username, realm, allow_password)).encode()
-                ).hexdigest()
+                ha1 = hasher(":".join((allow_username, realm, allow_password)).encode()).hexdigest()
                 allow_response = hasher(
-                    ":".join(
-                        (ha1, client_nonce, client_nc, client_cnonce, client_qop, ha2)
-                    ).encode()
+                    ":".join((ha1, client_nonce, client_nc, client_cnonce, client_qop, ha2)).encode()
                 ).hexdigest()
                 rspauth_ha2 = hasher(":{}".format(request.uri).encode()).hexdigest()
                 rspauth = hasher(
-                    ":".join(
-                        (
-                            ha1,
-                            client_nonce,
-                            client_nc,
-                            client_cnonce,
-                            client_qop,
-                            rspauth_ha2,
-                        )
-                    ).encode()
+                    ":".join((ha1, client_nonce, client_nc, client_cnonce, client_qop, rspauth_ha2,)).encode()
                 ).hexdigest()
                 if auth_info.get("response", "") == allow_response:
                     # TODO: fix or remove doubtful comment
                     # do we need to save nc only on success?
                     glastnc[0] = client_nc
                     allow_headers = {
-                        "authentication-info": " ".join(
+                        "authentication-info": ", ".join(filter(None,
                             (
                                 'nextnonce="{}"'.format(nextnonce) if nextnonce else "",
                                 "qop={}".format(client_qop),
                                 'rspauth="{}"'.format(rspauth),
                                 'cnonce="{}"'.format(client_cnonce),
                                 "nc={}".format(client_nc),
-                            )
+                            ))
                         ).strip()
                     }
                     return make_http_reflect(headers=allow_headers)(request)
             return deny(body=b"supplied credentials are not allowed")
         elif scheme == "wsse":
             x_wsse = request.headers.get("x-wsse", "")
             if x_wsse.count(",") != 3:
                 return http_response_bytes(status=400, body=b"x-wsse header syntax error")
-            auth_info = http_parse_auth(x_wsse)
-            client_username = auth_info.get("username", "")
-            client_nonce = auth_info.get("nonce", "")
-            client_created = auth_info.get("created", "")
-            client_digest = auth_info.get("passworddigest", "")
+            wsse_params = httplib2.auth._parse_www_authenticate(request.headers, "x-wsse").get("usernametoken", {})
+            print("debug: wsse_params", wsse_params)
+            client_username = wsse_params.get("username", "")
+            client_nonce = wsse_params.get("nonce", "")
+            client_created = wsse_params.get("created", "")
+            client_digest = wsse_params.get("passworddigest", "")
             allow_password = None
             for allow_username, allow_password in allow_credentials:
                 if client_username == allow_username:
                     break
             else:
                 return deny(body=b"unknown username")
 
             digest = hashlib.sha1("".join((client_nonce, client_created, allow_password)).encode("utf-8")).digest()
             digest_b64 = base64.b64encode(digest).decode()
-            print("$$$ check client={} == real={}".format(client_digest, digest_b64))
+            print("debug: check client={} == real={}".format(client_digest, digest_b64))
             if client_digest == digest_b64:
                 return make_http_reflect()(request)
 
             return deny(body=b"supplied credentials are not allowed")
         else:
diff --git a/tests/test_auth.py b/tests/test_auth.py
index 975a56f..c5581f8 100644
--- a/tests/test_auth.py
+++ b/tests/test_auth.py
@@ -1,5 +1,7 @@
+import time
+
 import httplib2
 import pytest
 import tests
 from six.moves import urllib
 
@@ -24,13 +26,11 @@ def test_credentials():
 
 def test_basic():
     # Test Basic Authentication
     http = httplib2.Http()
     password = tests.gen_password()
-    handler = tests.http_reflect_with_auth(
-        allow_scheme="basic", allow_credentials=(("joe", password),)
-    )
+    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=(("joe", password),))
     with tests.server_request(handler, request_count=3) as uri:
         response, content = http.request(uri, "GET")
         assert response.status == 401
         http.add_credentials("joe", password)
         response, content = http.request(uri, "GET")
@@ -39,13 +39,11 @@ def test_basic():
 
 def test_basic_for_domain():
     # Test Basic Authentication
     http = httplib2.Http()
     password = tests.gen_password()
-    handler = tests.http_reflect_with_auth(
-        allow_scheme="basic", allow_credentials=(("joe", password),)
-    )
+    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=(("joe", password),))
     with tests.server_request(handler, request_count=4) as uri:
         response, content = http.request(uri, "GET")
         assert response.status == 401
         http.add_credentials("joe", password, "example.org")
         response, content = http.request(uri, "GET")
@@ -60,13 +58,11 @@ def test_basic_two_credentials():
     # Test Basic Authentication with multiple sets of credentials
     http = httplib2.Http()
     password1 = tests.gen_password()
     password2 = tests.gen_password()
     allowed = [("joe", password1)]  # exploit shared mutable list
-    handler = tests.http_reflect_with_auth(
-        allow_scheme="basic", allow_credentials=allowed
-    )
+    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=allowed)
     with tests.server_request(handler, request_count=7) as uri:
         http.add_credentials("fred", password2)
         response, content = http.request(uri, "GET")
         assert response.status == 401
         http.add_credentials("joe", password1)
@@ -79,13 +75,11 @@ def test_basic_two_credentials():
 
 def test_digest():
     # Test that we support Digest Authentication
     http = httplib2.Http()
     password = tests.gen_password()
-    handler = tests.http_reflect_with_auth(
-        allow_scheme="digest", allow_credentials=(("joe", password),)
-    )
+    handler = tests.http_reflect_with_auth(allow_scheme="digest", allow_credentials=(("joe", password),))
     with tests.server_request(handler, request_count=3) as uri:
         response, content = http.request(uri, "GET")
         assert response.status == 401
         http.add_credentials("joe", password)
         response, content = http.request(uri, "GET")
@@ -97,29 +91,28 @@ def test_digest_next_nonce_nc():
     # the nonce count back to 1
     http = httplib2.Http()
     password = tests.gen_password()
     grenew_nonce = [None]
     handler = tests.http_reflect_with_auth(
-        allow_scheme="digest",
-        allow_credentials=(("joe", password),),
-        out_renew_nonce=grenew_nonce,
+        allow_scheme="digest", allow_credentials=(("joe", password),), out_renew_nonce=grenew_nonce,
     )
     with tests.server_request(handler, request_count=5) as uri:
         http.add_credentials("joe", password)
         response1, _ = http.request(uri, "GET")
-        info = httplib2._parse_www_authenticate(response1, "authentication-info")
+        info = httplib2.auth._parse_authentication_info(response1)
+        print("debug: response1 authentication-info: {}\nparsed: {}".format(response1.get("authentication-info"), info))
         assert response1.status == 200
-        assert info.get("digest", {}).get("nc") == "00000001", info
+        assert info.get("nc") == "00000001", info
         assert not info.get("digest", {}).get("nextnonce"), info
         response2, _ = http.request(uri, "GET")
-        info2 = httplib2._parse_www_authenticate(response2, "authentication-info")
-        assert info2.get("digest", {}).get("nc") == "00000002", info2
+        info2 = httplib2.auth._parse_authentication_info(response2)
+        assert info2.get("nc") == "00000002", info2
         grenew_nonce[0]()
         response3, content = http.request(uri, "GET")
-        info3 = httplib2._parse_www_authenticate(response3, "authentication-info")
+        info3 = httplib2.auth._parse_authentication_info(response3)
         assert response3.status == 200
-        assert info3.get("digest", {}).get("nc") == "00000001", info3
+        assert info3.get("nc") == "00000001", info3
 
 
 def test_digest_auth_stale():
     # Test that we can handle a nonce becoming stale
     http = httplib2.Http()
@@ -134,21 +127,17 @@ def test_digest_auth_stale():
     )
     with tests.server_request(handler, request_count=4) as uri:
         http.add_credentials("joe", password)
         response, _ = http.request(uri, "GET")
         assert response.status == 200
-        info = httplib2._parse_www_authenticate(
-            requests[0][1].headers, "www-authenticate"
-        )
+        info = httplib2.auth._parse_www_authenticate(requests[0][1].headers, "www-authenticate")
         grenew_nonce[0]()
         response, _ = http.request(uri, "GET")
         assert response.status == 200
         assert not response.fromcache
         assert getattr(response, "_stale_digest", False)
-        info2 = httplib2._parse_www_authenticate(
-            requests[2][1].headers, "www-authenticate"
-        )
+        info2 = httplib2.auth._parse_www_authenticate(requests[2][1].headers, "www-authenticate")
         nonce1 = info.get("digest", {}).get("nonce", "")
         nonce2 = info2.get("digest", {}).get("nonce", "")
         assert nonce1 != ""
         assert nonce2 != ""
         assert nonce1 != nonce2, (nonce1, nonce2)
@@ -158,143 +147,121 @@ def test_digest_auth_stale():
     "data",
     (
         ({}, {}),
         ({"www-authenticate": ""}, {}),
         (
-            {
-                "www-authenticate": 'Test realm="test realm" , foo=foo ,bar="bar", baz=baz,qux=qux'
-            },
-            {
-                "test": {
-                    "realm": "test realm",
-                    "foo": "foo",
-                    "bar": "bar",
-                    "baz": "baz",
-                    "qux": "qux",
-                }
-            },
+            {"www-authenticate": 'Test realm="test realm" , foo=foo ,bar="bar", baz=baz,qux=qux'},
+            {"test": {"realm": "test realm", "foo": "foo", "bar": "bar", "baz": "baz", "qux": "qux"}},
         ),
         (
             {"www-authenticate": 'T*!%#st realm=to*!%#en, to*!%#en="quoted string"'},
             {"t*!%#st": {"realm": "to*!%#en", "to*!%#en": "quoted string"}},
         ),
-        (
-            {"www-authenticate": 'Test realm="a \\"test\\" realm"'},
-            {"test": {"realm": 'a "test" realm'}},
-        ),
+        ({"www-authenticate": 'Test realm="a \\"test\\" realm"'}, {"test": {"realm": 'a "test" realm'}},),
         ({"www-authenticate": 'Basic realm="me"'}, {"basic": {"realm": "me"}}),
-        (
-            {"www-authenticate": 'Basic realm="me", algorithm="MD5"'},
-            {"basic": {"realm": "me", "algorithm": "MD5"}},
-        ),
-        (
-            {"www-authenticate": 'Basic realm="me", algorithm=MD5'},
-            {"basic": {"realm": "me", "algorithm": "MD5"}},
-        ),
-        (
-            {"www-authenticate": 'Basic realm="me",other="fred" '},
-            {"basic": {"realm": "me", "other": "fred"}},
-        ),
+        ({"www-authenticate": 'Basic realm="me", algorithm="MD5"'}, {"basic": {"realm": "me", "algorithm": "MD5"}},),
+        ({"www-authenticate": 'Basic realm="me", algorithm=MD5'}, {"basic": {"realm": "me", "algorithm": "MD5"}},),
+        ({"www-authenticate": 'Basic realm="me",other="fred" '}, {"basic": {"realm": "me", "other": "fred"}},),
         ({"www-authenticate": 'Basic REAlm="me" '}, {"basic": {"realm": "me"}}),
         (
-            {
-                "www-authenticate": 'Digest realm="digest1", qop="auth,auth-int", nonce="7102dd2", opaque="e9517f"'
-            },
-            {
-                "digest": {
-                    "realm": "digest1",
-                    "qop": "auth,auth-int",
-                    "nonce": "7102dd2",
-                    "opaque": "e9517f",
-                }
-            },
+            {"www-authenticate": 'Digest realm="digest1", qop="auth,auth-int", nonce="7102dd2", opaque="e9517f"'},
+            {"digest": {"realm": "digest1", "qop": "auth,auth-int", "nonce": "7102dd2", "opaque": "e9517f"}},
         ),
-        # multiple schema choice
+        # comma between schemas (glue for multiple headers with same name)
         (
-            {
-                "www-authenticate": 'Digest realm="multi-d", nonce="8b11d0f6", opaque="cc069c" Basic realm="multi-b" '
-            },
-            {
-                "digest": {"realm": "multi-d", "nonce": "8b11d0f6", "opaque": "cc069c"},
-                "basic": {"realm": "multi-b"},
-            },
+            {"www-authenticate": 'Digest realm="2-comma-d", qop="auth-int", nonce="c0c8ff1", Basic realm="2-comma-b"'},
+            {"digest": {"realm": "2-comma-d", "qop": "auth-int", "nonce": "c0c8ff1"}, "basic": {"realm": "2-comma-b"}},
         ),
-        # FIXME
-        # comma between schemas (glue for multiple headers with same name)
-        # ({'www-authenticate': 'Digest realm="2-comma-d", qop="auth-int", nonce="c0c8ff1", Basic realm="2-comma-b"'},
-        #  {'digest': {'realm': '2-comma-d', 'qop': 'auth-int', 'nonce': 'c0c8ff1'},
-        #   'basic': {'realm': '2-comma-b'}}),
-        # FIXME
         # comma between schemas + WSSE (glue for multiple headers with same name)
-        # ({'www-authenticate': 'Digest realm="com3d", Basic realm="com3b", WSSE realm="com3w", profile="token"'},
-        #  {'digest': {'realm': 'com3d'}, 'basic': {'realm': 'com3b'}, 'wsse': {'realm': 'com3w', profile': 'token'}}),
+        (
+            {"www-authenticate": 'Digest realm="com3d", Basic realm="com3b", WSSE realm="com3w", profile="token"'},
+            {"digest": {"realm": "com3d"}, "basic": {"realm": "com3b"}, "wsse": {"realm": "com3w", "profile": "token"}},
+        ),
         # FIXME
         # multiple syntax figures
         # ({'www-authenticate':
         #     'Digest realm="brig", qop \t=\t"\tauth,auth-int", nonce="(*)&^&$%#",opaque="5ccc"' +
         #     ', Basic REAlm="zoo", WSSE realm="very", profile="UsernameToken"'},
         #  {'digest': {'realm': 'brig', 'qop': 'auth,auth-int', 'nonce': '(*)&^&$%#', 'opaque': '5ccc'},
         #   'basic': {'realm': 'zoo'},
         #   'wsse': {'realm': 'very', 'profile': 'UsernameToken'}}),
         # more quote combos
         (
-            {
-                "www-authenticate": 'Digest realm="myrealm", nonce="KBAA=3", algorithm=MD5, qop="auth", stale=true'
-            },
-            {
-                "digest": {
-                    "realm": "myrealm",
-                    "nonce": "KBAA=3",
-                    "algorithm": "MD5",
-                    "qop": "auth",
-                    "stale": "true",
-                }
-            },
+            {"www-authenticate": 'Digest realm="myrealm", nonce="KBAA=3", algorithm=MD5, qop="auth", stale=true'},
+            {"digest": {"realm": "myrealm", "nonce": "KBAA=3", "algorithm": "MD5", "qop": "auth", "stale": "true"}},
         ),
+        ({"www-authenticate": "Basic param='single quote'"}, {"basic": {"param": "'single"}}),
     ),
     ids=lambda data: str(data[0]),
 )
 @pytest.mark.parametrize("strict", (True, False), ids=("strict", "relax"))
 def test_parse_www_authenticate_correct(data, strict):
     headers, info = data
     # FIXME: move strict to parse argument
     httplib2.USE_WWW_AUTH_STRICT_PARSING = strict
     try:
-        assert httplib2._parse_www_authenticate(headers) == info
+        assert httplib2.auth._parse_www_authenticate(headers) == info
     finally:
         httplib2.USE_WWW_AUTH_STRICT_PARSING = 0
 
 
-def test_parse_www_authenticate_malformed():
+@pytest.mark.parametrize(
+    "data",
+    (({"www-authenticate": 'OAuth "Facebook Platform" "invalid_token" "Invalid OAuth access token."'}, None),),
+    ids=lambda data: str(data[0]),
+)
+def test_parse_www_authenticate_malformed(data):
     # TODO: test (and fix) header value 'barbqwnbm-bb...:asd' leads to dead loop
-    with tests.assert_raises(httplib2.MalformedHeader):
-        httplib2._parse_www_authenticate(
-            {
-                "www-authenticate": 'OAuth "Facebook Platform" "invalid_token" "Invalid OAuth access token."'
-            }
-        )
+    headers, info = data
+    try:
+        result = httplib2.auth._parse_www_authenticate(headers)
+    except httplib2.error.MalformedHeader:
+        assert info is None, "unexpected MalformedHeader"
+    else:
+        assert result == info
+        assert info is not None, "expected parsing error"
+
+
+def test_parse_www_authenticate_complexity():
+    # TODO just use time.process_time() after python2 support is removed
+    process_time = getattr(time, "process_time", time.time)
+
+    def check(size):
+        header = {"www-authenticate": 'scheme {0}key=value,{0}quoted="foo=bar"'.format(" \t" * size)}
+        tbegin = process_time()
+        result = httplib2.auth._parse_www_authenticate(header)
+        tend = process_time()
+        assert result == {"scheme": {"key": "value", "quoted": "foo=bar"}}
+        elapsed_us = round((tend * 1e6) - (tbegin * 1e6), 0)
+        return elapsed_us
+
+    n1, n2, repeat = 50, 100, 7
+    time1 = min(check(n1) for _ in range(repeat))
+    time2 = min(check(n2) for _ in range(repeat))
+    speed1 = round(time1 / n1, 1)
+    speed2 = round(time2 / n2, 1)
+    expect2 = round(speed1 * (float(n2) / n1), 1)
+    error = round(speed2 / expect2, 1)
+    print("x{}: time={}us speed={} us/op".format(n1, time1, speed1))
+    print("x{}: time={}us speed={} us/op expected={} us/op error={}".format(n2, time2, speed2, expect2, error))
+    assert error < 2, "_parse_www_authenticate scales too fast"
 
 
 def test_digest_object():
     credentials = ("joe", "password")
     host = None
     request_uri = "/test/digest/"
     headers = {}
-    response = {
-        "www-authenticate": 'Digest realm="myrealm", nonce="KBAA=35", algorithm=MD5, qop="auth"'
-    }
+    response = {"www-authenticate": 'Digest realm="myrealm", nonce="KBAA=35", algorithm=MD5, qop="auth"'}
     content = b""
 
-    d = httplib2.DigestAuthentication(
-        credentials, host, request_uri, headers, response, content, None
-    )
+    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
     d.request("GET", request_uri, headers, content, cnonce="33033375ec278a46")
     our_request = "authorization: " + headers["authorization"]
     working_request = (
         'authorization: Digest username="joe", realm="myrealm", '
-        'nonce="KBAA=35", uri="/test/digest/"'
-        + ', algorithm=MD5, response="de6d4a123b80801d0e94550411b6283f", '
+        'nonce="KBAA=35", uri="/test/digest/"' + ', algorithm=MD5, response="de6d4a123b80801d0e94550411b6283f", '
         'qop=auth, nc=00000001, cnonce="33033375ec278a46"'
     )
     assert our_request == working_request
 
 
@@ -302,18 +269,15 @@ def test_digest_object_with_opaque():
     credentials = ("joe", "password")
     host = None
     request_uri = "/digest/opaque/"
     headers = {}
     response = {
-        "www-authenticate": 'Digest realm="myrealm", nonce="30352fd", algorithm=MD5, '
-        'qop="auth", opaque="atestopaque"'
+        "www-authenticate": 'Digest realm="myrealm", nonce="30352fd", algorithm=MD5, ' 'qop="auth", opaque="atestopaque"'
     }
     content = ""
 
-    d = httplib2.DigestAuthentication(
-        credentials, host, request_uri, headers, response, content, None
-    )
+    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
     d.request("GET", request_uri, headers, content, cnonce="5ec2")
     our_request = "authorization: " + headers["authorization"]
     working_request = (
         'authorization: Digest username="joe", realm="myrealm", '
         'nonce="30352fd", uri="/digest/opaque/", algorithm=MD5'
@@ -327,38 +291,28 @@ def test_digest_object_stale():
     credentials = ("joe", "password")
     host = None
     request_uri = "/digest/stale/"
     headers = {}
     response = httplib2.Response({})
-    response["www-authenticate"] = (
-        'Digest realm="myrealm", nonce="bd669f", '
-        'algorithm=MD5, qop="auth", stale=true'
-    )
+    response["www-authenticate"] = 'Digest realm="myrealm", nonce="bd669f", ' 'algorithm=MD5, qop="auth", stale=true'
     response.status = 401
     content = b""
-    d = httplib2.DigestAuthentication(
-        credentials, host, request_uri, headers, response, content, None
-    )
+    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
     # Returns true to force a retry
     assert d.response(response, content)
 
 
 def test_digest_object_auth_info():
     credentials = ("joe", "password")
     host = None
     request_uri = "/digest/nextnonce/"
     headers = {}
     response = httplib2.Response({})
-    response["www-authenticate"] = (
-        'Digest realm="myrealm", nonce="barney", '
-        'algorithm=MD5, qop="auth", stale=true'
-    )
+    response["www-authenticate"] = 'Digest realm="myrealm", nonce="barney", ' 'algorithm=MD5, qop="auth", stale=true'
     response["authentication-info"] = 'nextnonce="fred"'
     content = b""
-    d = httplib2.DigestAuthentication(
-        credentials, host, request_uri, headers, response, content, None
-    )
+    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
     # Returns true to force a retry
     assert not d.response(response, content)
     assert d.challenge["nonce"] == "fred"
     assert d.challenge["nc"] == 1
 

Vulnerable code: def _ssl_wrap_socket(
    sock, key_file, cert_file, disable_validation, ca_certs, ssl_version, hostname, key_password
):
            sock,
            keyfile=key_file,
            certfile=cert_file,
            cert_reqs=cert_reqs,
            ca_certs=ca_certs,
            ssl_version=ssl_version,
    return dict(
        [
            (key.lower(), NORMALIZE_SPACE.sub(value, " ").strip())
            for (key, value) in headers.iteritems()
        ]
    )
            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("=")
# In regex below:
#    [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+             matches a "token" as defined by HTTP
#    "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?"    matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space
# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both:
#    \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08\x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?
WWW_AUTH_STRICT = re.compile(
    r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$"
)
WWW_AUTH_RELAXED = re.compile(
    r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$"
)
UNQUOTE_PAIRS = re.compile(r"\\(.)")


def _parse_www_authenticate(headers, headername="www-authenticate"):
    """Returns a dictionary of dictionaries, one dict
    per auth_scheme."""
    retval = {}
    if headername in headers:
        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)
                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

    if (
        "pragma" in request_headers
        and request_headers["pragma"].lower().find("no-cache") != -1
    ):
            _("Content purported to be compressed with %s but failed to decompress.")
            % response.get("content-encoding"),
    dig = _md5(
        "%s:%s"
        % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])
    ).hexdigest()
    return base64.b64encode(
        _sha("%s%s%s" % (cnonce, iso_now, password)).digest()
    ).strip()
    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
        )
        headers["authorization"] = (
            "Basic " + base64.b64encode("%s:%s" % self.credentials).strip()
        )
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
        self.challenge = challenge["digest"]
        self.challenge["qop"] = (
            ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
        )
            raise UnimplementedDigestAuthOptionError(
                _("Unsupported value for qop: %s." % qop)
            )
        self.A1 = "".join(
            [
                self.credentials[0],
                ":",
                self.challenge["realm"],
                ":",
                self.credentials[1],
            ]
        )
            challenge = _parse_www_authenticate(response, "www-authenticate").get(
                "digest", {}
            )
            updated_challenge = _parse_www_authenticate(
                response, "authentication-info"
            ).get("digest", {})
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
                _(
                    "Unsupported value for pw-algorithm: %s."
                    % self.challenge["pw-algorithm"]
                )
                self.pwhashmod.new(
                    "".join([self.credentials[1], self.challenge["salt"]])
                )
                .hexdigest()
                .lower(),
        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()
        )
        challenge = _parse_www_authenticate(response, "www-authenticate").get(
            "hmacdigest", {}
        )
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        headers["X-WSSE"] = (
            'UsernameToken Username="%s", PasswordDigest="%s", '
            'Nonce="%s", Created="%s"'
        ) % (self.credentials[0], password_digest, cnonce, iso_now)
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
        auth = dict(
            Email=credentials[0],
            Passwd=credentials[1],
            service=service,
            source=headers["user-agent"],
        )
    def __init__(
        self, cache, safe=safename
    ):  # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
        self,
        proxy_type,
        proxy_host,
        proxy_port,
        proxy_rdns=True,
        proxy_user=None,
        proxy_pass=None,
        proxy_headers=None,
            raise ProxiesUnavailableError(
                "Proxy support missing but proxy use was requested!"
            )
            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
                self.proxy_info.astuple()
            )
                        proxy_type,
                        proxy_host,
                        proxy_port,
                        proxy_rdns,
                        proxy_user,
                        proxy_pass,
                        proxy_headers,
                            % str(
                                (
                                    proxy_host,
                                    proxy_port,
                                    proxy_rdns,
                                    proxy_user,
                                    proxy_pass,
                                    proxy_headers,
                                )
                            )
                            % str(
                                (
                                    proxy_host,
                                    proxy_port,
                                    proxy_rdns,
                                    proxy_user,
                                    proxy_pass,
                                    proxy_headers,
                                )
                            )
            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
                self.proxy_info.astuple()
            )
                        proxy_type,
                        proxy_host,
                        proxy_port,
                        proxy_rdns,
                        proxy_user,
                        proxy_pass,
                        proxy_headers,
                            % str(
                                (
                                    proxy_host,
                                    proxy_port,
                                    proxy_rdns,
                                    proxy_user,
                                    proxy_pass,
                                    proxy_headers,
                                )
                            )
                            "Server presented certificate that does not match "
                            "host %s: %s" % (hostname, cert),
            except (
                ssl_SSLError,
                ssl_CertificateError,
                CertificateHostnameMismatch,
            ) as e:
                            % str(
                                (
                                    proxy_host,
                                    proxy_port,
                                    proxy_rdns,
                                    proxy_user,
                                    proxy_pass,
                                    proxy_headers,
                                )
                            )

        url,
        payload=None,
        method="GET",
        headers={},
        allow_truncated=False,
        follow_redirects=True,
        deadline=None,
        httplib.HTTPConnection.__init__(
            self, host, port=port, strict=strict, timeout=timeout
        )
            self,
            host,
            port=port,
            key_file=key_file,
            cert_file=cert_file,
            strict=strict,
            timeout=timeout,
    server_software = os.environ.get('SERVER_SOFTWARE', '')
    if (server_software.startswith('Google App Engine/') or
        server_software.startswith('Development/') or
        server_software.startswith('testutil/')):
        challenges = _parse_www_authenticate(response, "www-authenticate")
                    yield AUTH_SCHEME_CLASSES[scheme](
                        cred, host, request_uri, headers, response, content, self
                    )
        self,
        conn,
        host,
        absolute_uri,
        request_uri,
        method,
        body,
        headers,
        redirections,
        cachekey,
        auths = [
            (auth.depth(request_uri), auth)
            for auth in self.authorizations
            if auth.inscope(host, request_uri)
        ]
        (response, content) = self._conn_request(
            conn, request_uri, method, body, headers
        )
                (response, content) = self._conn_request(
                    conn, request_uri, method, body, headers
                )
            for authorization in self._auth_from_challenge(
                host, request_uri, headers, response, content
            ):
                (response, content) = self._conn_request(
                    conn, request_uri, method, body, headers
                )
        if (
            self.follow_all_redirects
            or method in self.safe_methods
            or response.status in (303, 308)
        ):
                            _(
                                "Redirected but the response is missing a Location: header."
                            ),
                            response,
                            content,
                            response["location"] = urlparse.urljoin(
                                absolute_uri, location
                            )
                    if (
                        "authorization" in headers
                        and not self.forward_authorization_headers
                    ):
                            location,
                            method=redirect_method,
                            body=body,
                            headers=headers,
                            redirections=redirections - 1,
                        "Redirected more times than rediection_limit allows.",
                        response,
                        content,
        self,
        uri,
        method="GET",
        body=None,
        headers=None,
        redirections=DEFAULT_MAX_REDIRECTS,
        connection_type=None,
        conn_key = ''
                            "Redirected more times than rediection_limit allows.",
                            {},
                            "",
                        if (
                            "etag" in info
                            and not self.ignore_etag
                            and not "if-none-match" in headers
                        ):
                        conn,
                        authority,
                        uri,
                        request_uri,
                        method,
                        body,
                        headers,
                        redirections,
                        cachekey,
                    _updateCache(
                        headers, merged_response, content, self.cache, cachekey
                    )
                        conn,
                        authority,
                        uri,
                        request_uri,
                        method,
                        body,
                        headers,
                        redirections,
                        cachekey,
                    response = Response(
                        {
                            "content-type": "text/plain",
                            "status": "408",
                            "content-length": len(content),
                        }
                    )
                    response = Response(
                        {
                            "content-type": "text/plain",
                            "status": "400",
                            "content-length": len(content),
                        }
                    )
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception):
    pass


# Some exceptions can be caught and optionally
# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
    def __init__(self, desc, response, content):
        self.response = response
        self.content = content
        HttpLib2Error.__init__(self, desc)


class RedirectMissingLocation(HttpLib2ErrorWithResponse):
    pass


class RedirectLimit(HttpLib2ErrorWithResponse):
    pass


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


DEFAULT_TLS_VERSION = getattr(ssl, "PROTOCOL_TLS", None) or getattr(
    ssl, "PROTOCOL_SSLv23"
)
    disable_ssl_certificate_validation, ca_certs, cert_file=None, key_file=None,
    maximum_version=None, minimum_version=None, key_password=None,
    context.verify_mode = (
        ssl.CERT_NONE if disable_ssl_certificate_validation else ssl.CERT_REQUIRED
    )
            (
                _convert_byte_str(key).lower(),
                NORMALIZE_SPACE.sub(_convert_byte_str(value), " ").strip(),
            )
            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("=")
# In regex below:
#    [^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+             matches a "token" as defined by HTTP
#    "(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?"    matches a "quoted-string" as defined by HTTP, when LWS have already been replaced by a single space
# Actually, as an auth-param value can be either a token or a quoted-string, they are combined in a single pattern which matches both:
#    \"?((?<=\")(?:[^\0-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x08\x0A-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?
WWW_AUTH_STRICT = re.compile(
    r"^(?:\s*(?:,\s*)?([^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+)\s*=\s*\"?((?<=\")(?:[^\0-\x08\x0A-\x1f\x7f-\xff\\\"]|\\[\0-\x7f])*?(?=\")|(?<!\")[^\0-\x1f\x7f-\xff()<>@,;:\\\"/[\]?={} \t]+(?!\"))\"?)(.*)$"
)
WWW_AUTH_RELAXED = re.compile(
    r"^(?:\s*(?:,\s*)?([^ \t\r\n=]+)\s*=\s*\"?((?<=\")(?:[^\\\"]|\\.)*?(?=\")|(?<!\")[^ \t\r\n,]+(?!\"))\"?)(.*)$"
)
UNQUOTE_PAIRS = re.compile(r"\\(.)")


def _parse_www_authenticate(headers, headername="www-authenticate"):
    """Returns a dictionary of dictionaries, one dict
    per auth_scheme."""
    retval = {}
    if headername in headers:
        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)
                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

    if (
        "pragma" in request_headers
        and request_headers["pragma"].lower().find("no-cache") != -1
    ):
            _("Content purported to be compressed with %s but failed to decompress.")
            % response.get("content-encoding"),
                headers = header.Header(
                    v, maxlinelen=self._maxheaderlen, charset="utf-8", header_name=h
                )
            text = b"".join(
                [status_header.encode("utf-8"), header_str.encode("utf-8"), content]
            )
        (
            "%s:%s"
            % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])
        ).encode("utf-8")
    return base64.b64encode(
        _sha(("%s%s%s" % (cnonce, iso_now, password)).encode("utf-8")).digest()
    ).strip().decode("utf-8")
    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
        )
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
        self.challenge = challenge["digest"]
        self.challenge["qop"] = (
            ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
        )
            raise UnimplementedDigestAuthOptionError(
                _("Unsupported value for qop: %s." % qop)
            )
        self.A1 = "".join(
            [
                self.credentials[0],
                ":",
                self.challenge["realm"],
                ":",
                self.credentials[1],
            ]
        )
            challenge = _parse_www_authenticate(response, "www-authenticate").get(
                "digest", {}
            )
            updated_challenge = _parse_www_authenticate(
                response, "authentication-info"
            ).get("digest", {})
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
                _(
                    "Unsupported value for pw-algorithm: %s."
                    % self.challenge["pw-algorithm"]
                )
                self.pwhashmod.new(
                    "".join([self.credentials[1], self.challenge["salt"]])
                )
                .hexdigest()
                .lower(),
        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()
        )
        challenge = _parse_www_authenticate(response, "www-authenticate").get(
            "hmacdigest", {}
        )
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        headers["X-WSSE"] = (
            'UsernameToken Username="%s", PasswordDigest="%s", '
            'Nonce="%s", Created="%s"'
        ) % (self.credentials[0], password_digest, cnonce, iso_now)
    def __init__(
        self, credentials, host, request_uri, headers, response, content, http
    ):
        Authentication.__init__(
            self, credentials, host, request_uri, headers, response, content, http
        )
        challenge = _parse_www_authenticate(response, "www-authenticate")
        auth = dict(
            Email=credentials[0],
            Passwd=credentials[1],
            service=service,
            source=headers["user-agent"],
        )
    def __init__(
        self, cache, safe=safename
    ):  # use safe=lambda x: md5.new(x).hexdigest() for the old behavior
        self,
        proxy_type,
        proxy_host,
        proxy_port,
        proxy_rdns=True,
        proxy_user=None,
        proxy_pass=None,
        proxy_headers=None,
        self.proxy_type, self.proxy_host, self.proxy_port, self.proxy_rdns, self.proxy_user, self.proxy_pass, self.proxy_headers = (
            raise ProxiesUnavailableError(
                "Proxy support missing but proxy use was requested!"
            )
            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
                self.proxy_info.astuple()
            )
                        proxy_type,
                        proxy_host,
                        proxy_port,
                        proxy_rdns,
                        proxy_user,
                        proxy_pass,
                    print(
                        "connect: ({0}, {1}) ************".format(self.host, self.port)
                    )
                                str(
                                    (
                                        proxy_host,
                                        proxy_port,
                                        proxy_rdns,
                                        proxy_user,
                                        proxy_pass,
                                        proxy_headers,
                                    )
                                )
                                str(
                                    (
                                        proxy_host,
                                        proxy_port,
                                        proxy_rdns,
                                        proxy_user,
                                        proxy_pass,
                                        proxy_headers,
                                    )
                                )
            self.disable_ssl_certificate_validation, self.ca_certs, cert_file, key_file,
            maximum_version=tls_maximum_version, minimum_version=tls_minimum_version,
            host,
            port=port,
            timeout=timeout,
            context=context,
            proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers = (
                self.proxy_info.astuple()
            )
                        proxy_type,
                        proxy_host,
                        proxy_port,
                        proxy_rdns,
                        proxy_user,
                        proxy_pass,
                if (
                    not hasattr(self._context, "check_hostname")
                    and not self.disable_ssl_certificate_validation
                ):
                                str(
                                    (
                                        proxy_host,
                                        proxy_port,
                                        proxy_rdns,
                                        proxy_user,
                                        proxy_pass,
                                        proxy_headers,
                                    )
                                )
                                str(
                                    (
                                        proxy_host,
                                        proxy_port,
                                        proxy_rdns,
                                        proxy_user,
                                        proxy_pass,
                                        proxy_headers,
                                    )
                                )
        challenges = _parse_www_authenticate(response, "www-authenticate")
                    yield AUTH_SCHEME_CLASSES[scheme](
                        cred, host, request_uri, headers, response, content, self
                    )
                errno_ = (
                    e.args[0].errno if isinstance(e.args[0], socket.error) else e.errno
                )
        self,
        conn,
        host,
        absolute_uri,
        request_uri,
        method,
        body,
        headers,
        redirections,
        cachekey,
        auths = [
            (auth.depth(request_uri), auth)
            for auth in self.authorizations
            if auth.inscope(host, request_uri)
        ]
        (response, content) = self._conn_request(
            conn, request_uri, method, body, headers
        )
                (response, content) = self._conn_request(
                    conn, request_uri, method, body, headers
                )
            for authorization in self._auth_from_challenge(
                host, request_uri, headers, response, content
            ):
                (response, content) = self._conn_request(
                    conn, request_uri, method, body, headers
                )
        if (
            self.follow_all_redirects
            or method in self.safe_methods
            or response.status in (303, 308)
        ):
                            _(
                                "Redirected but the response is missing a Location: header."
                            ),
                            response,
                            content,
                            response["location"] = urllib.parse.urljoin(
                                absolute_uri, location
                            )
                    if (
                        "authorization" in headers
                        and not self.forward_authorization_headers
                    ):
                            location,
                            method=redirect_method,
                            body=body,
                            headers=headers,
                            redirections=redirections - 1,
                        "Redirected more times than redirection_limit allows.",
                        response,
                        content,
        self,
        uri,
        method="GET",
        body=None,
        headers=None,
        redirections=DEFAULT_MAX_REDIRECTS,
        connection_type=None,
        conn_key = ''
                                info.replace_header(
                                    k, str(*email.header.decode_header(v)[0])
                                )
                            "Redirected more times than redirection_limit allows.",
                            {},
                            "",
                        if (
                            "etag" in info
                            and not self.ignore_etag
                            and not "if-none-match" in headers
                        ):
                        conn,
                        authority,
                        uri,
                        request_uri,
                        method,
                        body,
                        headers,
                        redirections,
                        cachekey,
                    _updateCache(
                        headers, merged_response, content, self.cache, cachekey
                    )
                        conn,
                        authority,
                        uri,
                        request_uri,
                        method,
                        body,
                        headers,
                        redirections,
                        cachekey,
                    response = Response(
                        {
                            "content-type": "text/plain",
                            "status": "408",
                            "content-length": len(content),
                        }
                    )
                    response = Response(
                        {
                            "content-type": "text/plain",
                            "status": "400",
                            "content-length": len(content),
                        }
                    )
	local version_setup=$(fgrep 'VERSION =' setup.py |tr -d " '\"" |cut -d\= -f2)
	local version_py2=$(cd python2 ; python2 -Es -c 'import httplib2;print(httplib2.__version__)')
	local version_py3=$(cd python3 ; python3 -Es -c 'import httplib2;print(httplib2.__version__)')
		echo "error: setup.py VERSION=$version_setup expected=$need" >&1
	version_source=$(cd python3 ; python3 -Es -c 'import httplib2;print(httplib2.__version__)')
	version_installed=$($pip show httplib2 |fgrep Version |cut -d' ' -f2)
        args = ['--forked', '--fulltrace', '--no-cov', 'tests/']
            args += ['-k', self.test_suite]
        sys.stderr.write('setup.py:test run pytest {}\n'.format(' '.join(args)))
    with open(os.path.join(project_root, name), 'rb') as f:
        g = (line.decode('utf-8').lstrip().split('#', 1)[0].rstrip() for line in f)
        assert re.match(
            br".+ HTTP/\d\.\d\r\n$", start_line
        ), "Start line does not look like HTTP request: " + repr(start_line)
        self,
        host,
        port=None,
        key_file=None,
        cert_file=None,
        strict=None,
        timeout=None,
        proxy_info=None,
        self,
        host,
        port=None,
        key_file=None,
        cert_file=None,
        strict=None,
        timeout=None,
        proxy_info=None,
                gresult[0] = Exception(
                    "Request count expected={0} actual={1}".format(
                        request_count, gcounter[0]
                    )
                )
    if (
        not undefined_body_length
        and proto != "HTTP/1.0"
        and "content-length" not in headers
    ):
        raise Exception(
            "httplib2.tests.http_response_bytes: client could not figure response body length"
        )
        "{proto} {status}\r\n{headers}\r\n".format(
            proto=proto, status=status, headers=header_string
        ).encode()
        + body
def http_parse_auth(s):
    """https://tools.ietf.org/html/rfc7235#section-2.1
    """
    scheme, rest = s.split(" ", 1)
    result = {}
    while True:
        m = httplib2.WWW_AUTH_RELAXED.search(rest)
        if not m:
            break
        if len(m.groups()) == 3:
            key, value, rest = m.groups()
            result[key.lower()] = httplib2.UNQUOTE_PAIRS.sub(r"\1", value)
    return result


        if " " not in auth_header:
        scheme, data = auth_header.split(" ", 1)
        scheme = scheme.lower()
            decoded = base64.b64decode(data).decode()
            auth_info = http_parse_auth(data)
                    body="auth-info opaque mismatch expected={} actual={}".format(
                        server_opaque, client_opaque
                    ).encode()
                ha1 = hasher(
                    ":".join((allow_username, realm, allow_password)).encode()
                ).hexdigest()
                    ":".join(
                        (ha1, client_nonce, client_nc, client_cnonce, client_qop, ha2)
                    ).encode()
                    ":".join(
                        (
                            ha1,
                            client_nonce,
                            client_nc,
                            client_cnonce,
                            client_qop,
                            rspauth_ha2,
                        )
                    ).encode()
                        "authentication-info": " ".join(
                            )
            auth_info = http_parse_auth(x_wsse)
            client_username = auth_info.get("username", "")
            client_nonce = auth_info.get("nonce", "")
            client_created = auth_info.get("created", "")
            client_digest = auth_info.get("passworddigest", "")
            print("$$$ check client={} == real={}".format(client_digest, digest_b64))
    handler = tests.http_reflect_with_auth(
        allow_scheme="basic", allow_credentials=(("joe", password),)
    )
    handler = tests.http_reflect_with_auth(
        allow_scheme="basic", allow_credentials=(("joe", password),)
    )
    handler = tests.http_reflect_with_auth(
        allow_scheme="basic", allow_credentials=allowed
    )
    handler = tests.http_reflect_with_auth(
        allow_scheme="digest", allow_credentials=(("joe", password),)
    )
        allow_scheme="digest",
        allow_credentials=(("joe", password),),
        out_renew_nonce=grenew_nonce,
        info = httplib2._parse_www_authenticate(response1, "authentication-info")
        assert info.get("digest", {}).get("nc") == "00000001", info
        info2 = httplib2._parse_www_authenticate(response2, "authentication-info")
        assert info2.get("digest", {}).get("nc") == "00000002", info2
        info3 = httplib2._parse_www_authenticate(response3, "authentication-info")
        assert info3.get("digest", {}).get("nc") == "00000001", info3
        info = httplib2._parse_www_authenticate(
            requests[0][1].headers, "www-authenticate"
        )
        info2 = httplib2._parse_www_authenticate(
            requests[2][1].headers, "www-authenticate"
        )
            {
                "www-authenticate": 'Test realm="test realm" , foo=foo ,bar="bar", baz=baz,qux=qux'
            },
            {
                "test": {
                    "realm": "test realm",
                    "foo": "foo",
                    "bar": "bar",
                    "baz": "baz",
                    "qux": "qux",
                }
            },
        (
            {"www-authenticate": 'Test realm="a \\"test\\" realm"'},
            {"test": {"realm": 'a "test" realm'}},
        ),
        (
            {"www-authenticate": 'Basic realm="me", algorithm="MD5"'},
            {"basic": {"realm": "me", "algorithm": "MD5"}},
        ),
        (
            {"www-authenticate": 'Basic realm="me", algorithm=MD5'},
            {"basic": {"realm": "me", "algorithm": "MD5"}},
        ),
        (
            {"www-authenticate": 'Basic realm="me",other="fred" '},
            {"basic": {"realm": "me", "other": "fred"}},
        ),
            {
                "www-authenticate": 'Digest realm="digest1", qop="auth,auth-int", nonce="7102dd2", opaque="e9517f"'
            },
            {
                "digest": {
                    "realm": "digest1",
                    "qop": "auth,auth-int",
                    "nonce": "7102dd2",
                    "opaque": "e9517f",
                }
            },
        # multiple schema choice
            {
                "www-authenticate": 'Digest realm="multi-d", nonce="8b11d0f6", opaque="cc069c" Basic realm="multi-b" '
            },
            {
                "digest": {"realm": "multi-d", "nonce": "8b11d0f6", "opaque": "cc069c"},
                "basic": {"realm": "multi-b"},
            },
        # FIXME
        # comma between schemas (glue for multiple headers with same name)
        # ({'www-authenticate': 'Digest realm="2-comma-d", qop="auth-int", nonce="c0c8ff1", Basic realm="2-comma-b"'},
        #  {'digest': {'realm': '2-comma-d', 'qop': 'auth-int', 'nonce': 'c0c8ff1'},
        #   'basic': {'realm': '2-comma-b'}}),
        # FIXME
        # ({'www-authenticate': 'Digest realm="com3d", Basic realm="com3b", WSSE realm="com3w", profile="token"'},
        #  {'digest': {'realm': 'com3d'}, 'basic': {'realm': 'com3b'}, 'wsse': {'realm': 'com3w', profile': 'token'}}),
            {
                "www-authenticate": 'Digest realm="myrealm", nonce="KBAA=3", algorithm=MD5, qop="auth", stale=true'
            },
            {
                "digest": {
                    "realm": "myrealm",
                    "nonce": "KBAA=3",
                    "algorithm": "MD5",
                    "qop": "auth",
                    "stale": "true",
                }
            },
        assert httplib2._parse_www_authenticate(headers) == info
def test_parse_www_authenticate_malformed():
    with tests.assert_raises(httplib2.MalformedHeader):
        httplib2._parse_www_authenticate(
            {
                "www-authenticate": 'OAuth "Facebook Platform" "invalid_token" "Invalid OAuth access token."'
            }
        )
    response = {
        "www-authenticate": 'Digest realm="myrealm", nonce="KBAA=35", algorithm=MD5, qop="auth"'
    }
    d = httplib2.DigestAuthentication(
        credentials, host, request_uri, headers, response, content, None
    )
        'nonce="KBAA=35", uri="/test/digest/"'
        + ', algorithm=MD5, response="de6d4a123b80801d0e94550411b6283f", '
        "www-authenticate": 'Digest realm="myrealm", nonce="30352fd", algorithm=MD5, '
        'qop="auth", opaque="atestopaque"'
    d = httplib2.DigestAuthentication(
        credentials, host, request_uri, headers, response, content, None
    )
    response["www-authenticate"] = (
        'Digest realm="myrealm", nonce="bd669f", '
        'algorithm=MD5, qop="auth", stale=true'
    )
    d = httplib2.DigestAuthentication(
        credentials, host, request_uri, headers, response, content, None
    )
    response["www-authenticate"] = (
        'Digest realm="myrealm", nonce="barney", '
        'algorithm=MD5, qop="auth", stale=true'
    )
    d = httplib2.DigestAuthentication(
        credentials, host, request_uri, headers, response, content, None
    )
Fixed code: from httplib2 import auth
from httplib2.error import *
def _ssl_wrap_socket(sock, key_file, cert_file, disable_validation, ca_certs, ssl_version, hostname, key_password):
            sock, keyfile=key_file, certfile=cert_file, cert_reqs=cert_reqs, ca_certs=ca_certs, ssl_version=ssl_version,

    return dict([(key.lower(), NORMALIZE_SPACE.sub(value, " ").strip()) for (key, value) in headers.iteritems()])
            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("=")]
    if "pragma" in request_headers and request_headers["pragma"].lower().find("no-cache") != -1:
            _("Content purported to be compressed with %s but failed to decompress.") % response.get("content-encoding"),
    dig = _md5("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).hexdigest()
    return base64.b64encode(_sha("%s%s%s" % (cnonce, iso_now, password)).digest()).strip()
    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)
        headers["authorization"] = "Basic " + base64.b64encode("%s:%s" % self.credentials).strip()
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        self.challenge = auth._parse_www_authenticate(response, "www-authenticate")["digest"]
        self.challenge["qop"] = ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
            raise UnimplementedDigestAuthOptionError(_("Unsupported value for qop: %s." % qop))
        self.A1 = "".join([self.credentials[0], ":", self.challenge["realm"], ":", self.credentials[1],])
            challenge = auth._parse_www_authenticate(response, "www-authenticate").get("digest", {})
            updated_challenge = auth._parse_authentication_info(response, "authentication-info")
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        challenge = auth._parse_www_authenticate(response, "www-authenticate")
                _("Unsupported value for pw-algorithm: %s." % self.challenge["pw-algorithm"])
                self.pwhashmod.new("".join([self.credentials[1], self.challenge["salt"]])).hexdigest().lower(),
        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()
        challenge = auth._parse_www_authenticate(response, "www-authenticate").get("hmacdigest", {})
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        headers["X-WSSE"] = ('UsernameToken Username="%s", PasswordDigest="%s", ' 'Nonce="%s", Created="%s"') % (
            self.credentials[0],
            password_digest,
            cnonce,
            iso_now,
        )
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        challenge = auth._parse_www_authenticate(response, "www-authenticate")
        auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers["user-agent"],)
    def __init__(self, cache, safe=safename):  # use safe=lambda x: md5.new(x).hexdigest() for the old behavior

        self, proxy_type, proxy_host, proxy_port, proxy_rdns=True, proxy_user=None, proxy_pass=None, proxy_headers=None,
            raise ProxiesUnavailableError("Proxy support missing but proxy use was requested!")
            (
                proxy_type,
                proxy_host,
                proxy_port,
                proxy_rdns,
                proxy_user,
                proxy_pass,
                proxy_headers,
            ) = self.proxy_info.astuple()
                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,
                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
            (
                proxy_type,
                proxy_host,
                proxy_port,
                proxy_rdns,
                proxy_user,
                proxy_pass,
                proxy_headers,
            ) = self.proxy_info.astuple()
                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,
                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                            "Server presented certificate that does not match " "host %s: %s" % (hostname, cert),
            except (ssl_SSLError, ssl_CertificateError, CertificateHostnameMismatch,) as e:
                            % str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
        url, payload=None, method="GET", headers={}, allow_truncated=False, follow_redirects=True, deadline=None,
        httplib.HTTPConnection.__init__(self, host, port=port, strict=strict, timeout=timeout)
            self, host, port=port, key_file=key_file, cert_file=cert_file, strict=strict, timeout=timeout,
    server_software = os.environ.get("SERVER_SOFTWARE", "")
    if (
        server_software.startswith("Google App Engine/")
        or server_software.startswith("Development/")
        or server_software.startswith("testutil/")
    ):

        challenges = auth._parse_www_authenticate(response, "www-authenticate")
                    yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self)
        self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey,
        auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
        (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
            for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
        if self.follow_all_redirects or method in self.safe_methods or response.status in (303, 308):
                            _("Redirected but the response is missing a Location: header."), response, content,
                            response["location"] = urlparse.urljoin(absolute_uri, location)
                    if "authorization" in headers and not self.forward_authorization_headers:
                            location, method=redirect_method, body=body, headers=headers, redirections=redirections - 1,
                        "Redirected more times than rediection_limit allows.", response, content,
        self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None,
        conn_key = ""
                            "Redirected more times than rediection_limit allows.", {}, "",
                        if "etag" in info and not self.ignore_etag and not "if-none-match" in headers:
                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                    _updateCache(headers, merged_response, content, self.cache, cachekey)
                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                    response = Response({"content-type": "text/plain", "status": "408", "content-length": len(content),})
                    response = Response({"content-type": "text/plain", "status": "400", "content-length": len(content),})
import base64
import re

import pyparsing as pp

from .error import *

UNQUOTE_PAIRS = re.compile(r"\\(.)")
unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1])

# https://tools.ietf.org/html/rfc7235#section-1.2
# https://tools.ietf.org/html/rfc7235#appendix-B
tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
token = pp.Word(tchar).setName("token")
token68 = pp.Combine(pp.Word("-._~+/" + pp.nums + pp.alphas) + pp.ZeroOrMore("=")).setName("token68")

quoted_string = pp.dblQuotedString.copy().setName("quoted-string").setParseAction(unquote)
auth_param_name = token.copy().setName("auth-param-name").addParseAction(pp.downcaseTokens)
auth_param = auth_param_name + pp.Suppress("=") + (token ^ quoted_string)
params = pp.Dict(pp.delimitedList(pp.Group(auth_param)))

scheme = token("scheme")
challenge = scheme + (token68("token") ^ params("params"))

authentication_info = params.copy()
www_authenticate = pp.delimitedList(pp.Group(challenge))


def _parse_authentication_info(headers, headername="authentication-info"):
    """https://tools.ietf.org/html/rfc7615
    """
    header = headers.get(headername, "").strip()
    if not header:
        return {}
    try:
        parsed = authentication_info.parseString(header)
    except pp.ParseException as ex:
        # print(ex.explain(ex))
        raise MalformedHeader(headername)

    return parsed.asDict()


def _parse_www_authenticate(headers, headername="www-authenticate"):
    """Returns a dictionary of dictionaries, one dict per auth_scheme."""
    header = headers.get(headername, "").strip()
    if not header:
        return {}
    try:
        parsed = www_authenticate.parseString(header)
    except pp.ParseException as ex:
        # print(ex.explain(ex))
        raise MalformedHeader(headername)

    retval = {
        challenge["scheme"].lower(): challenge["params"].asDict()
        if "params" in challenge
        else {"token": challenge.get("token")}
        for challenge in parsed
    }
    return retval
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception):
    pass


# Some exceptions can be caught and optionally
# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
    def __init__(self, desc, response, content):
        self.response = response
        self.content = content
        HttpLib2Error.__init__(self, desc)


class RedirectMissingLocation(HttpLib2ErrorWithResponse):
    pass


class RedirectLimit(HttpLib2ErrorWithResponse):
    pass


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
from . import auth
from .error import *

DEFAULT_TLS_VERSION = getattr(ssl, "PROTOCOL_TLS", None) or getattr(ssl, "PROTOCOL_SSLv23")

    disable_ssl_certificate_validation,
    ca_certs,
    cert_file=None,
    key_file=None,
    maximum_version=None,
    minimum_version=None,
    key_password=None,
    context.verify_mode = ssl.CERT_NONE if disable_ssl_certificate_validation else ssl.CERT_REQUIRED
            (_convert_byte_str(key).lower(), NORMALIZE_SPACE.sub(_convert_byte_str(value), " ").strip(),)
            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("=")]
    if "pragma" in request_headers and request_headers["pragma"].lower().find("no-cache") != -1:
            _("Content purported to be compressed with %s but failed to decompress.") % response.get("content-encoding"),
                headers = header.Header(v, maxlinelen=self._maxheaderlen, charset="utf-8", header_name=h)
            text = b"".join([status_header.encode("utf-8"), header_str.encode("utf-8"), content])
        ("%s:%s" % (time.ctime(), ["0123456789"[random.randrange(0, 9)] for i in range(20)])).encode("utf-8")
    return (
        base64.b64encode(_sha(("%s%s%s" % (cnonce, iso_now, password)).encode("utf-8")).digest()).strip().decode("utf-8")
    )
    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)
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        self.challenge = auth._parse_www_authenticate(response, "www-authenticate")["digest"]
        self.challenge["qop"] = ("auth" in [x.strip() for x in qop.split()]) and "auth" or None
            raise UnimplementedDigestAuthOptionError(_("Unsupported value for qop: %s." % qop))
        self.A1 = "".join([self.credentials[0], ":", self.challenge["realm"], ":", self.credentials[1],])
            challenge = auth._parse_www_authenticate(response, "www-authenticate").get("digest", {})
            updated_challenge = auth._parse_authentication_info(response, "authentication-info")
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        challenge = auth._parse_www_authenticate(response, "www-authenticate")
                _("Unsupported value for pw-algorithm: %s." % self.challenge["pw-algorithm"])
                self.pwhashmod.new("".join([self.credentials[1], self.challenge["salt"]])).hexdigest().lower(),
        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()
        challenge = auth._parse_www_authenticate(response, "www-authenticate").get("hmacdigest", {})
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        headers["X-WSSE"] = ('UsernameToken Username="%s", PasswordDigest="%s", ' 'Nonce="%s", Created="%s"') % (
            self.credentials[0],
            password_digest,
            cnonce,
            iso_now,
        )
    def __init__(self, credentials, host, request_uri, headers, response, content, http):
        Authentication.__init__(self, credentials, host, request_uri, headers, response, content, http)
        challenge = auth._parse_www_authenticate(response, "www-authenticate")
        auth = dict(Email=credentials[0], Passwd=credentials[1], service=service, source=headers["user-agent"],)
    def __init__(self, cache, safe=safename):  # use safe=lambda x: md5.new(x).hexdigest() for the old behavior

        self, proxy_type, proxy_host, proxy_port, proxy_rdns=True, proxy_user=None, proxy_pass=None, proxy_headers=None,
        (
            self.proxy_type,
            self.proxy_host,
            self.proxy_port,
            self.proxy_rdns,
            self.proxy_user,
            self.proxy_pass,
            self.proxy_headers,
        ) = (
            raise ProxiesUnavailableError("Proxy support missing but proxy use was requested!")
            (
                proxy_type,
                proxy_host,
                proxy_port,
                proxy_rdns,
                proxy_user,
                proxy_pass,
                proxy_headers,
            ) = self.proxy_info.astuple()
                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass,
                    print("connect: ({0}, {1}) ************".format(self.host, self.port))
                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
            self.disable_ssl_certificate_validation,
            self.ca_certs,
            cert_file,
            key_file,
            maximum_version=tls_maximum_version,
            minimum_version=tls_minimum_version,
            host, port=port, timeout=timeout, context=context,
            (
                proxy_type,
                proxy_host,
                proxy_port,
                proxy_rdns,
                proxy_user,
                proxy_pass,
                proxy_headers,
            ) = self.proxy_info.astuple()
                        proxy_type, proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass,
                if not hasattr(self._context, "check_hostname") and not self.disable_ssl_certificate_validation:
                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
                                str((proxy_host, proxy_port, proxy_rdns, proxy_user, proxy_pass, proxy_headers,))
        challenges = auth._parse_www_authenticate(response, "www-authenticate")
                    yield AUTH_SCHEME_CLASSES[scheme](cred, host, request_uri, headers, response, content, self)
                errno_ = e.args[0].errno if isinstance(e.args[0], socket.error) else e.errno
        self, conn, host, absolute_uri, request_uri, method, body, headers, redirections, cachekey,
        auths = [(auth.depth(request_uri), auth) for auth in self.authorizations if auth.inscope(host, request_uri)]
        (response, content) = self._conn_request(conn, request_uri, method, body, headers)
                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
            for authorization in self._auth_from_challenge(host, request_uri, headers, response, content):
                (response, content) = self._conn_request(conn, request_uri, method, body, headers)
        if self.follow_all_redirects or method in self.safe_methods or response.status in (303, 308):
                            _("Redirected but the response is missing a Location: header."), response, content,
                            response["location"] = urllib.parse.urljoin(absolute_uri, location)
                    if "authorization" in headers and not self.forward_authorization_headers:
                            location, method=redirect_method, body=body, headers=headers, redirections=redirections - 1,
                        "Redirected more times than redirection_limit allows.", response, content,
        self, uri, method="GET", body=None, headers=None, redirections=DEFAULT_MAX_REDIRECTS, connection_type=None,
        conn_key = ""
                                info.replace_header(k, str(*email.header.decode_header(v)[0]))
                            "Redirected more times than redirection_limit allows.", {}, "",
                        if "etag" in info and not self.ignore_etag and not "if-none-match" in headers:
                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                    _updateCache(headers, merged_response, content, self.cache, cachekey)
                        conn, authority, uri, request_uri, method, body, headers, redirections, cachekey,
                    response = Response({"content-type": "text/plain", "status": "408", "content-length": len(content),})
                    response = Response({"content-type": "text/plain", "status": "400", "content-length": len(content),})
import base64
import re

import pyparsing as pp

from .error import *

UNQUOTE_PAIRS = re.compile(r"\\(.)")
unquote = lambda s, l, t: UNQUOTE_PAIRS.sub(r"\1", t[0][1:-1])

# https://tools.ietf.org/html/rfc7235#section-1.2
# https://tools.ietf.org/html/rfc7235#appendix-B
tchar = "!#$%&'*+-.^_`|~" + pp.nums + pp.alphas
token = pp.Word(tchar).setName("token")
token68 = pp.Combine(pp.Word("-._~+/" + pp.nums + pp.alphas) + pp.ZeroOrMore("=")).setName("token68")

quoted_string = pp.dblQuotedString.copy().setName("quoted-string").setParseAction(unquote)
auth_param_name = token.copy().setName("auth-param-name").addParseAction(pp.downcaseTokens)
auth_param = auth_param_name + pp.Suppress("=") + (token ^ quoted_string)
params = pp.Dict(pp.delimitedList(pp.Group(auth_param)))

scheme = token("scheme")
challenge = scheme + (token68("token") ^ params("params"))

authentication_info = params.copy()
www_authenticate = pp.delimitedList(pp.Group(challenge))


def _parse_authentication_info(headers, headername="authentication-info"):
    """https://tools.ietf.org/html/rfc7615
    """
    header = headers.get(headername, "").strip()
    if not header:
        return {}
    try:
        parsed = authentication_info.parseString(header)
    except pp.ParseException as ex:
        # print(ex.explain(ex))
        raise MalformedHeader(headername)

    return parsed.asDict()


def _parse_www_authenticate(headers, headername="www-authenticate"):
    """Returns a dictionary of dictionaries, one dict per auth_scheme."""
    header = headers.get(headername, "").strip()
    if not header:
        return {}
    try:
        parsed = www_authenticate.parseString(header)
    except pp.ParseException as ex:
        # print(ex.explain(ex))
        raise MalformedHeader(headername)

    retval = {
        challenge["scheme"].lower(): challenge["params"].asDict()
        if "params" in challenge
        else {"token": challenge.get("token")}
        for challenge in parsed
    }
    return retval
# All exceptions raised here derive from HttpLib2Error
class HttpLib2Error(Exception):
    pass


# Some exceptions can be caught and optionally
# be turned back into responses.
class HttpLib2ErrorWithResponse(HttpLib2Error):
    def __init__(self, desc, response, content):
        self.response = response
        self.content = content
        HttpLib2Error.__init__(self, desc)


class RedirectMissingLocation(HttpLib2ErrorWithResponse):
    pass


class RedirectLimit(HttpLib2ErrorWithResponse):
    pass


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
pyparsing>=2.4.2,<3 # TODO include v3 after dropping Python2 support
	local version_setup=$(python setup.py --version)
	local version_py2=$(python -Es -c "$(egrep '^__version__' python2/httplib2/__init__.py);print(__version__)")
	local version_py3=$(python -Es -c "$(egrep '^__version__' python3/httplib2/__init__.py);print(__version__)")
		echo "error: setup.py version=$version_setup expected=$need" >&1
	version_source=$(python setup.py --version)
	version_installed=$($pip show httplib2 |fgrep Version: |cut -d' ' -f2)

        args = ["--forked", "--fulltrace", "--no-cov", "tests/"]
            args += ["-k", self.test_suite]
        sys.stderr.write("setup.py:test run pytest {}\n".format(" ".join(args)))
    with open(os.path.join(project_root, name), "rb") as f:
        g = (line.decode("utf-8").lstrip().split("#", 1)[0].rstrip() for line in f)
    install_requires=read_requirements("requirements.txt"),
        assert re.match(br".+ HTTP/\d\.\d\r\n$", start_line), "Start line does not look like HTTP request: " + repr(
            start_line
        )
        self, host, port=None, key_file=None, cert_file=None, strict=None, timeout=None, proxy_info=None,
        self, host, port=None, key_file=None, cert_file=None, strict=None, timeout=None, proxy_info=None,
                gresult[0] = Exception("Request count expected={0} actual={1}".format(request_count, gcounter[0]))
    if not undefined_body_length and proto != "HTTP/1.0" and "content-length" not in headers:
        raise Exception("httplib2.tests.http_response_bytes: client could not figure response body length")
        "{proto} {status}\r\n{headers}\r\n".format(proto=proto, status=status, headers=header_string).encode() + body
        try:
            auth_parsed = httplib2.auth._parse_www_authenticate(request.headers, "authorization")
            print("debug: auth_parsed", auth_parsed)
        except httplib2.error.MalformedHeader:
            print("debug: auth header error")
        scheme = auth_header.split(" ", 1)[0].lower()
        print("debug: first auth scheme='{}'".format(scheme))
        auth_info = auth_parsed[scheme]
            decoded = base64.b64decode(auth_info["token"]).decode()
                    body="auth-info opaque mismatch expected={} actual={}".format(server_opaque, client_opaque).encode()
                ha1 = hasher(":".join((allow_username, realm, allow_password)).encode()).hexdigest()
                    ":".join((ha1, client_nonce, client_nc, client_cnonce, client_qop, ha2)).encode()
                    ":".join((ha1, client_nonce, client_nc, client_cnonce, client_qop, rspauth_ha2,)).encode()
                        "authentication-info": ", ".join(filter(None,
                            ))
            wsse_params = httplib2.auth._parse_www_authenticate(request.headers, "x-wsse").get("usernametoken", {})
            print("debug: wsse_params", wsse_params)
            client_username = wsse_params.get("username", "")
            client_nonce = wsse_params.get("nonce", "")
            client_created = wsse_params.get("created", "")
            client_digest = wsse_params.get("passworddigest", "")
            print("debug: check client={} == real={}".format(client_digest, digest_b64))
import time

    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=(("joe", password),))
    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=(("joe", password),))
    handler = tests.http_reflect_with_auth(allow_scheme="basic", allow_credentials=allowed)
    handler = tests.http_reflect_with_auth(allow_scheme="digest", allow_credentials=(("joe", password),))
        allow_scheme="digest", allow_credentials=(("joe", password),), out_renew_nonce=grenew_nonce,
        info = httplib2.auth._parse_authentication_info(response1)
        print("debug: response1 authentication-info: {}\nparsed: {}".format(response1.get("authentication-info"), info))
        assert info.get("nc") == "00000001", info
        info2 = httplib2.auth._parse_authentication_info(response2)
        assert info2.get("nc") == "00000002", info2
        info3 = httplib2.auth._parse_authentication_info(response3)
        assert info3.get("nc") == "00000001", info3
        info = httplib2.auth._parse_www_authenticate(requests[0][1].headers, "www-authenticate")
        info2 = httplib2.auth._parse_www_authenticate(requests[2][1].headers, "www-authenticate")
            {"www-authenticate": 'Test realm="test realm" , foo=foo ,bar="bar", baz=baz,qux=qux'},
            {"test": {"realm": "test realm", "foo": "foo", "bar": "bar", "baz": "baz", "qux": "qux"}},
        ({"www-authenticate": 'Test realm="a \\"test\\" realm"'}, {"test": {"realm": 'a "test" realm'}},),
        ({"www-authenticate": 'Basic realm="me", algorithm="MD5"'}, {"basic": {"realm": "me", "algorithm": "MD5"}},),
        ({"www-authenticate": 'Basic realm="me", algorithm=MD5'}, {"basic": {"realm": "me", "algorithm": "MD5"}},),
        ({"www-authenticate": 'Basic realm="me",other="fred" '}, {"basic": {"realm": "me", "other": "fred"}},),
            {"www-authenticate": 'Digest realm="digest1", qop="auth,auth-int", nonce="7102dd2", opaque="e9517f"'},
            {"digest": {"realm": "digest1", "qop": "auth,auth-int", "nonce": "7102dd2", "opaque": "e9517f"}},
        # comma between schemas (glue for multiple headers with same name)
            {"www-authenticate": 'Digest realm="2-comma-d", qop="auth-int", nonce="c0c8ff1", Basic realm="2-comma-b"'},
            {"digest": {"realm": "2-comma-d", "qop": "auth-int", "nonce": "c0c8ff1"}, "basic": {"realm": "2-comma-b"}},
        (
            {"www-authenticate": 'Digest realm="com3d", Basic realm="com3b", WSSE realm="com3w", profile="token"'},
            {"digest": {"realm": "com3d"}, "basic": {"realm": "com3b"}, "wsse": {"realm": "com3w", "profile": "token"}},
        ),
            {"www-authenticate": 'Digest realm="myrealm", nonce="KBAA=3", algorithm=MD5, qop="auth", stale=true'},
            {"digest": {"realm": "myrealm", "nonce": "KBAA=3", "algorithm": "MD5", "qop": "auth", "stale": "true"}},
        ({"www-authenticate": "Basic param='single quote'"}, {"basic": {"param": "'single"}}),
        assert httplib2.auth._parse_www_authenticate(headers) == info
@pytest.mark.parametrize(
    "data",
    (({"www-authenticate": 'OAuth "Facebook Platform" "invalid_token" "Invalid OAuth access token."'}, None),),
    ids=lambda data: str(data[0]),
)
def test_parse_www_authenticate_malformed(data):
    headers, info = data
    try:
        result = httplib2.auth._parse_www_authenticate(headers)
    except httplib2.error.MalformedHeader:
        assert info is None, "unexpected MalformedHeader"
    else:
        assert result == info
        assert info is not None, "expected parsing error"


def test_parse_www_authenticate_complexity():
    # TODO just use time.process_time() after python2 support is removed
    process_time = getattr(time, "process_time", time.time)

    def check(size):
        header = {"www-authenticate": 'scheme {0}key=value,{0}quoted="foo=bar"'.format(" \t" * size)}
        tbegin = process_time()
        result = httplib2.auth._parse_www_authenticate(header)
        tend = process_time()
        assert result == {"scheme": {"key": "value", "quoted": "foo=bar"}}
        elapsed_us = round((tend * 1e6) - (tbegin * 1e6), 0)
        return elapsed_us

    n1, n2, repeat = 50, 100, 7
    time1 = min(check(n1) for _ in range(repeat))
    time2 = min(check(n2) for _ in range(repeat))
    speed1 = round(time1 / n1, 1)
    speed2 = round(time2 / n2, 1)
    expect2 = round(speed1 * (float(n2) / n1), 1)
    error = round(speed2 / expect2, 1)
    print("x{}: time={}us speed={} us/op".format(n1, time1, speed1))
    print("x{}: time={}us speed={} us/op expected={} us/op error={}".format(n2, time2, speed2, expect2, error))
    assert error < 2, "_parse_www_authenticate scales too fast"
    response = {"www-authenticate": 'Digest realm="myrealm", nonce="KBAA=35", algorithm=MD5, qop="auth"'}
    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
        'nonce="KBAA=35", uri="/test/digest/"' + ', algorithm=MD5, response="de6d4a123b80801d0e94550411b6283f", '
        "www-authenticate": 'Digest realm="myrealm", nonce="30352fd", algorithm=MD5, ' 'qop="auth", opaque="atestopaque"'
    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
    response["www-authenticate"] = 'Digest realm="myrealm", nonce="bd669f", ' 'algorithm=MD5, qop="auth", stale=true'
    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)
    response["www-authenticate"] = 'Digest realm="myrealm", nonce="barney", ' 'algorithm=MD5, qop="auth", stale=true'
    d = httplib2.DigestAuthentication(credentials, host, request_uri, headers, response, content, None)