diff options
author | Jeremy Hylton <jeremy@alum.mit.edu> | 2003-10-21 18:07:07 (GMT) |
---|---|---|
committer | Jeremy Hylton <jeremy@alum.mit.edu> | 2003-10-21 18:07:07 (GMT) |
commit | fcefd0d2a50ab1b2df1d21028e1b2160141e707e (patch) | |
tree | 2f88b9383818cf2a9a3143f678175c37751d57e1 /Lib | |
parent | 4e21dc9efd1116d58336d5cc55a62c9aa10e6ecf (diff) | |
download | cpython-fcefd0d2a50ab1b2df1d21028e1b2160141e707e.zip cpython-fcefd0d2a50ab1b2df1d21028e1b2160141e707e.tar.gz cpython-fcefd0d2a50ab1b2df1d21028e1b2160141e707e.tar.bz2 |
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.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/urllib2.py | 140 |
1 files changed, 96 insertions, 44 deletions
diff --git a/Lib/urllib2.py b/Lib/urllib2.py index 9f123ab..5c90aea 100644 --- a/Lib/urllib2.py +++ b/Lib/urllib2.py @@ -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): |