[Python-checkins] python/dist/src/Lib urllib2.py,1.55,1.56

jhylton at users.sourceforge.net jhylton at users.sourceforge.net
Tue Oct 21 14:07:10 EDT 2003


Update of /cvsroot/python/python/dist/src/Lib
In directory sc8-pr-cvs1:/tmp/cvs-serv15593

Modified Files:
	urllib2.py 
Log Message:
Apply patch 823328 -- support for rfc 2617 digestion authentication.

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.


Index: urllib2.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/urllib2.py,v
retrieving revision 1.55
retrieving revision 1.56
diff -C2 -d -r1.55 -r1.56
*** urllib2.py	20 Oct 2003 14:01:50 -0000	1.55
--- urllib2.py	21 Oct 2003 18:07:07 -0000	1.56
***************
*** 88,107 ****
  # check digest against correct (i.e. non-apache) implementation
  
! import socket
  import httplib
  import inspect
- import re
- import base64
- import urlparse
  import md5
  import mimetypes
  import mimetools
  import rfc822
! import ftplib
  import sys
  import time
! import os
! import gopherlib
! import posixpath
  
  try:
--- 88,109 ----
  # check digest against correct (i.e. non-apache) implementation
  
! import base64
! import ftplib
! import gopherlib
  import httplib
  import inspect
  import md5
  import mimetypes
  import mimetools
+ import os
+ import posixpath
+ import random
+ import re
  import rfc822
! import sha
! import socket
  import sys
  import time
! import urlparse
  
  try:
***************
*** 110,119 ****
      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, \
--- 112,115 ----
***************
*** 121,131 ****
       splitattr, ftpwrapper, noheaders
  
! # support for proxies via environment variables
! from urllib import getproxies
! 
! # support for FileHandler
! from urllib import localhost, url2pathname
  
! __version__ = "2.0a1"
  
  _opener = None
--- 117,124 ----
       splitattr, ftpwrapper, noheaders
  
! # support for FileHandler, proxies via environment variables
! from urllib import localhost, url2pathname, getproxies
  
! __version__ = "2.1"
  
  _opener = None
***************
*** 681,685 ****
--- 674,701 ----
  
  
+ 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):
***************
*** 688,698 ****
          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)
          if authreq:
!             kind = authreq.split()[0]
!             if kind == 'Digest':
                  return self.retry_http_digest_auth(req, authreq)
  
      def retry_http_digest_auth(self, req, auth):
--- 704,732 ----
          self.passwd = passwd
          self.add_password = self.passwd.add_password
+         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:
!             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):
***************
*** 708,715 ****
--- 742,760 ----
              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
***************
*** 723,728 ****
              return None
  
!         user, pw = self.passwd.find_user_password(realm,
!                                                   req.get_full_url())
          if user is None:
              return None
--- 768,772 ----
              return None
  
!         user, pw = self.passwd.find_user_password(realm, req.get_full_url())
          if user is None:
              return None
***************
*** 738,742 ****
                          # XXX selector: what about proxies and full urls
                          req.get_selector())
!         respdig = KD(H(A1), "%s:%s" % (nonce, H(A2)))
          # XXX should the partial digests be encoded too?
  
--- 782,797 ----
                          # XXX selector: what about proxies and full urls
                          req.get_selector())
!         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?
  
***************
*** 750,753 ****
--- 805,810 ----
          if algorithm != 'MD5':
              base = base + ', algorithm="%s"' % algorithm
+         if qop:
+             base = base + ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce)
          return base
  
***************
*** 755,763 ****
          # lambdas assume digest modules are imported at the top level
          if algorithm == 'MD5':
!             H = lambda x, e=encode_digest:e(md5.new(x).digest())
          elif algorithm == 'SHA':
!             H = lambda x, e=encode_digest:e(sha.new(x).digest())
          # XXX MD5-sess
!         KD = lambda s, d, H=H: H("%s:%s" % (s, d))
          return H, KD
  
--- 812,820 ----
          # lambdas assume digest modules are imported at the top level
          if algorithm == 'MD5':
!             H = lambda x: md5.new(x).hexdigest()
          elif algorithm == 'SHA':
!             H = lambda x: sha.new(x).hexdigest()
          # XXX MD5-sess
!         KD = lambda s, d: H("%s:%s" % (s, d))
          return H, KD
  
***************
*** 778,782 ****
      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)
  
  
--- 835,842 ----
      def http_error_401(self, req, fp, code, msg, headers):
          host = urlparse.urlparse(req.get_full_url())[1]
!         retry = self.http_error_auth_reqed('www-authenticate', 
!                                            host, req, headers)
!         self.reset_retry_count()
!         return retry
  
  
***************
*** 787,802 ****
      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)
! 
  
  class AbstractHTTPHandler(BaseHandler):
--- 847,854 ----
      def http_error_407(self, req, fp, code, msg, headers):
          host = req.get_host()
!         retry = self.http_error_auth_reqed('proxy-authenticate', 
!                                            host, req, headers)
!         self.reset_retry_count()
!         return retry
  
  class AbstractHTTPHandler(BaseHandler):





More information about the Python-checkins mailing list