]> granicus.if.org Git - python/commitdiff
Apply patch 823328 -- support for rfc 2617 digestion authentication.
authorJeremy Hylton <jeremy@alum.mit.edu>
Tue, 21 Oct 2003 18:07:07 +0000 (18:07 +0000)
committerJeremy Hylton <jeremy@alum.mit.edu>
Tue, 21 Oct 2003 18:07:07 +0000 (18:07 +0000)
The patch was tweaked slightly.  It's get a different mechanism for
generating the cnonce which uses /dev/urandom when possible to
generate less-easily-guessed random input.

Also rearrange the imports so that they are alphabetical and
duplicates are eliminated.

Add a few XXX comments about things left undone and things that could
be improved.

Lib/urllib2.py

index 9f123abac2b0c4b2313da6e46f3b69e00d8ff388..5c90aeae5beb8e7d573cbb0f5c5cf5eb59bf7fad 100644 (file)
@@ -87,46 +87,39 @@ f = urllib2.urlopen('http://www.python.org/')
 # gopher can return a socket.error
 # check digest against correct (i.e. non-apache) implementation
 
-import socket
+import base64
+import ftplib
+import gopherlib
 import httplib
 import inspect
-import re
-import base64
-import urlparse
 import md5
 import mimetypes
 import mimetools
+import os
+import posixpath
+import random
+import re
 import rfc822
-import ftplib
+import sha
+import socket
 import sys
 import time
-import os
-import gopherlib
-import posixpath
+import urlparse
 
 try:
     from cStringIO import StringIO
 except ImportError:
     from StringIO import StringIO
 
-try:
-    import sha
-except ImportError:
-    # need 1.5.2 final
-    sha = None
-
 # not sure how many of these need to be gotten rid of
 from urllib import unwrap, unquote, splittype, splithost, \
      addinfourl, splitport, splitgophertype, splitquery, \
      splitattr, ftpwrapper, noheaders
 
-# support for proxies via environment variables
-from urllib import getproxies
-
-# support for FileHandler
-from urllib import localhost, url2pathname
+# support for FileHandler, proxies via environment variables
+from urllib import localhost, url2pathname, getproxies
 
-__version__ = "2.0a1"
+__version__ = "2.1"
 
 _opener = None
 def urlopen(url, data=None):
@@ -680,20 +673,61 @@ class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler):
                                           host, req, headers)
 
 
+def randombytes(n):
+    """Return n random bytes."""
+    # Use /dev/urandom if it is available.  Fall back to random module
+    # if not.  It might be worthwhile to extend this function to use
+    # other platform-specific mechanisms for getting random bytes.
+    if os.path.exists("/dev/urandom"):
+        f = open("/dev/urandom")
+        s = f.read(n)
+        f.close()
+        return s
+    else:
+        L = [chr(random.randrange(0, 256)) for i in range(n)]
+        return "".join(L)
+
 class AbstractDigestAuthHandler:
+    # Digest authentication is specified in RFC 2617.
+
+    # XXX The client does not inspect the Authentication-Info header
+    # in a successful response.
+
+    # XXX It should be possible to test this implementation against
+    # a mock server that just generates a static set of challenges.
+
+    # XXX qop="auth-int" supports is shaky
 
     def __init__(self, passwd=None):
         if passwd is None:
             passwd = HTTPPasswordMgr()
         self.passwd = passwd
         self.add_password = self.passwd.add_password
-
-    def http_error_auth_reqed(self, authreq, host, req, headers):
-        authreq = headers.get(self.auth_header, None)
+        self.retried = 0
+        self.nonce_count = 0
+
+    def reset_retry_count(self):
+        self.retried = 0
+
+    def http_error_auth_reqed(self, auth_header, host, req, headers):
+        authreq = headers.get(auth_header, None)
+        if self.retried > 5:
+            # Don't fail endlessly - if we failed once, we'll probably
+            # fail a second time. Hm. Unless the Password Manager is
+            # prompting for the information. Crap. This isn't great
+            # but it's better than the current 'repeat until recursion
+            # depth exceeded' approach <wink>
+            raise HTTPError(req.get_full_url(), 401, "digest auth failed", 
+                            headers, None)
+        else:
+            self.retried += 1
         if authreq:
-            kind = authreq.split()[0]
-            if kind == 'Digest':
+            scheme = authreq.split()[0]
+            if scheme.lower() == 'digest':
                 return self.retry_http_digest_auth(req, authreq)
+            else:
+                raise ValueError("AbstractDigestAuthHandler doesn't know "
+                                 "about %s"%(scheme))
 
     def retry_http_digest_auth(self, req, auth):
         token, challenge = auth.split(' ', 1)
@@ -707,10 +741,21 @@ class AbstractDigestAuthHandler:
             resp = self.parent.open(req)
             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 = sha.new("%s:%s:%s:%s" % (self.nonce_count, nonce, time.ctime(),
+                                       randombytes(8))).hexdigest()
+        return dig[:16]
+
     def get_authorization(self, req, chal):
         try:
             realm = chal['realm']
             nonce = chal['nonce']
+            qop = chal.get('qop')
             algorithm = chal.get('algorithm', 'MD5')
             # mod_digest doesn't send an opaque, even though it isn't
             # supposed to be optional
@@ -722,8 +767,7 @@ class AbstractDigestAuthHandler:
         if H is None:
             return None
 
-        user, pw = self.passwd.find_user_password(realm,
-                                                  req.get_full_url())
+        user, pw = self.passwd.find_user_password(realm, req.get_full_url())
         if user is None:
             return None
 
@@ -737,7 +781,18 @@ class AbstractDigestAuthHandler:
         A2 = "%s:%s" % (req.has_data() and 'POST' or 'GET',
                         # XXX selector: what about proxies and full urls
                         req.get_selector())
-        respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
+        if qop == 'auth':
+            self.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))
+            respdig = KD(H(A1), noncebit)
+        elif qop is None:
+            respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
+        else:
+            # XXX handle auth-int.
+            pass
+    
         # XXX should the partial digests be encoded too?
 
         base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \
@@ -749,16 +804,18 @@ class AbstractDigestAuthHandler:
             base = base + ', digest="%s"' % entdig
         if algorithm != 'MD5':
             base = base + ', algorithm="%s"' % algorithm
+        if qop:
+            base = base + ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
         return base
 
     def get_algorithm_impls(self, algorithm):
         # lambdas assume digest modules are imported at the top level
         if algorithm == 'MD5':
-            H = lambda x, e=encode_digest:e(md5.new(x).digest())
+            H = lambda x: md5.new(x).hexdigest()
         elif algorithm == 'SHA':
-            H = lambda x, e=encode_digest:e(sha.new(x).digest())
+            H = lambda x: sha.new(x).hexdigest()
         # XXX MD5-sess
-        KD = lambda s, d, H=H: H("%s:%s" % (s, d))
+        KD = lambda s, d: H("%s:%s" % (s, d))
         return H, KD
 
     def get_entity_digest(self, data, chal):
@@ -777,7 +834,10 @@ class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):
 
     def http_error_401(self, req, fp, code, msg, headers):
         host = urlparse.urlparse(req.get_full_url())[1]
-        self.http_error_auth_reqed('www-authenticate', host, req, headers)
+        retry = self.http_error_auth_reqed('www-authenticate', 
+                                           host, req, headers)
+        self.reset_retry_count()
+        return retry
 
 
 class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):
@@ -786,18 +846,10 @@ class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler):
 
     def http_error_407(self, req, fp, code, msg, headers):
         host = req.get_host()
-        self.http_error_auth_reqed('proxy-authenticate', host, req, headers)
-
-
-def encode_digest(digest):
-    hexrep = []
-    for c in digest:
-        n = (ord(c) >> 4) & 0xf
-        hexrep.append(hex(n)[-1])
-        n = ord(c) & 0xf
-        hexrep.append(hex(n)[-1])
-    return ''.join(hexrep)
-
+        retry = self.http_error_auth_reqed('proxy-authenticate', 
+                                           host, req, headers)
+        self.reset_retry_count()
+        return retry
 
 class AbstractHTTPHandler(BaseHandler):