Index: Lib/urllib2.py =================================================================== --- Lib/urllib2.py (revision 77653) +++ Lib/urllib2.py (working copy) @@ -894,14 +894,33 @@ # XXX qop="auth-int" supports is shaky + class AuthenticationSession: + def __init__(self, chal, url): + self.chal = chal + self.nonce_count = 0 + self.cnonce = self.get_cnonce(chal["nonce"]) + self.known_urls = set([url]) + + def get_cnonce(self, nonce): + # The cnonce-value is an opaque + # quoted string value provided by the client and used by both client + # and server to avoid chosen plaintext attacks, to provide mutual + # authentication, and to provide some message integrity protection. + # This isn't a fabulous effort, but it's probably Good Enough. + dig = hashlib.sha1("%s:%s:%s:%s" % (self.nonce_count, nonce, time.ctime(), + randombytes(8))).hexdigest() + return dig[:16] + + def reset_cnonce(self): + self.cnonce = self.get_cnonce(self.chal["nonce"]) + def __init__(self, passwd=None): if passwd is None: passwd = HTTPPasswordMgr() self.passwd = passwd self.add_password = self.passwd.add_password self.retried = 0 - self.nonce_count = 0 - self.last_nonce = None + self.authentication_sessions = {} def reset_retry_count(self): self.retried = 0 @@ -926,7 +945,11 @@ def retry_http_digest_auth(self, req, auth): token, challenge = auth.split(' ', 1) chal = parse_keqv_list(parse_http_list(challenge)) - auth = self.get_authorization(req, chal) + + # generate/update the auth session + auth_session = self.add_authenticating_session(req, chal) + + auth = self.get_authorization(req, auth_session) if auth: auth_val = 'Digest %s' % auth if req.headers.get(self.auth_header, None) == auth_val: @@ -935,18 +958,53 @@ resp = self.parent.open(req, timeout=req.timeout) return resp - def get_cnonce(self, nonce): - # The cnonce-value is an opaque - # quoted string value provided by the client and used by both client - # and server to avoid chosen plaintext attacks, to provide mutual - # authentication, and to provide some message integrity protection. - # This isn't a fabulous effort, but it's probably Good Enough. - dig = hashlib.sha1("%s:%s:%s:%s" % (self.nonce_count, nonce, time.ctime(), - randombytes(8))).hexdigest() - return dig[:16] + def http_request(self, req): + ''' "pre-process request" handler to add the Digest header + ''' + # if this is not a request generated by an AuthHandler + if not req.has_header(self.auth_header): + auth_session = self.find_authenticating_session(req) + if auth_session: + auth = self.get_authorization(req, auth_session) + if auth: + auth_val = 'Digest %s' % auth + if req.headers.get(self.auth_header, None) != auth_val: + req.add_unredirected_header(self.auth_header, auth_val) + return req + https_request = http_request - def get_authorization(self, req, chal): + def get_request_identifier(self, req): + ''' This depends on the type of autentication we are handling (proxy with + digest or web server with digest). + ''' + raise NotImplementedError + + def find_authenticating_session(self, req): + ''' This depends if we are handling a connection to a proxy with digest + or a connection with a web server with digest. + ''' + raise NotImplementedError + + def add_authenticating_session(self, req, chal): + request_identifier = self.get_request_identifier(req) + realm = chal['realm'] + auth_session = AbstractDigestAuthHandler.AuthenticationSession(chal, req.get_full_url()) + + if request_identifier not in self.authentication_sessions: + self.authentication_sessions[request_identifier] = {} + elif realm in self.authentication_sessions[request_identifier]: + # If we already had a previous authenticating session for this + # domain/realm, we set the previously known urls in the new + # auth session. + old_auth_session = self.authentication_sessions[request_identifier][realm] + auth_session.known_urls.update(old_auth_session.known_urls) + + self.authentication_sessions[request_identifier][realm] = auth_session + return auth_session + + def get_authorization(self, req, auth_session): try: + chal = auth_session.chal realm = chal['realm'] nonce = chal['nonce'] qop = chal.get('qop') @@ -976,15 +1034,10 @@ # XXX selector: what about proxies and full urls req.get_selector()) if qop == 'auth': - if nonce == self.last_nonce: - self.nonce_count += 1 - else: - self.nonce_count = 1 - self.last_nonce = nonce + auth_session.nonce_count += 1 - ncvalue = '%08x' % self.nonce_count - cnonce = self.get_cnonce(nonce) - noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2)) + ncvalue = '%08x' % auth_session.nonce_count + noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, auth_session.cnonce, qop, H(A2)) respdig = KD(H(A1), noncebit) elif qop is None: respdig = KD(H(A1), "%s:%s" % (nonce, H(A2))) @@ -1003,7 +1056,7 @@ base += ', digest="%s"' % entdig base += ', algorithm="%s"' % algorithm if qop: - base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce) + base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, auth_session.cnonce) return base def get_algorithm_impls(self, algorithm): @@ -1040,7 +1093,34 @@ self.reset_retry_count() return retry + def get_request_identifier(self, req): + # We return the request's canonical root url: + # "the absoluteURI for the server whose abs_path is empty". + # (from http://tools.ietf.org/html/rfc2617#section-1.2) + split_result = urlparse.urlsplit(req.get_full_url()) + return urlparse.urlunsplit((split_result.scheme, split_result.netloc, '/', '', '')) + def find_authenticating_session(self, req): + ''' Find an authenticating session for the given URL. This is performed + by comparing the paths without the resources of the given URL against + the URLs of the stored authenticating sessions. If the given URL is + a sub-path, we have a match. + ''' + def get_path_only(url): + path = urlparse.urlsplit(url)[2] + # trim the resource from the path + return path[0:path.rfind("/") + 1] + def is_sub_path(sub_path, path): + return sub_path.startswith(path) + + path_without_resource = get_path_only(req.get_full_url()) + + request_identifier = self.get_request_identifier(req) + for auth_session in self.authentication_sessions.get(request_identifier, {}).values(): + for url in auth_session.known_urls: + if is_sub_path(path_without_resource, get_path_only(url)): + return auth_session + class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): auth_header = 'Proxy-Authorization' @@ -1053,6 +1133,21 @@ self.reset_retry_count() return retry + def get_request_identifier(self, req): + ''' When a proxy is set, the host of the request is changed + to the proxy's host. + ''' + return req.get_host() + + def find_authenticating_session(self, req): + ''' Find an authenticating session for the given proxy. This is performed + by returning the first authenticating session for the given proxy. + ''' + request_identifier = self.get_request_identifier(req) + + for auth_session in self.authentication_sessions.get(request_identifier, {}).values(): + return auth_session + class AbstractHTTPHandler(BaseHandler): def __init__(self, debuglevel=0):