Added in experimental HMACDigest support from Thomas Broyer
diff --git a/httplib2/__init__.py b/httplib2/__init__.py
index 1f0f5f6..8e86c44 100644
--- a/httplib2/__init__.py
+++ b/httplib2/__init__.py
@@ -10,7 +10,7 @@
__author__ = "Joe Gregorio (joe@bitworking.org)"
__copyright__ = "Copyright 2006, Joe Gregorio"
-__contributors__ = []
+__contributors__ = ["Thomas Broyer (t.broyer@ltgt.net)"]
__license__ = "MIT"
__version__ = "$Rev$"
@@ -29,6 +29,7 @@
import time
import random
import sha
+import hmac
from gettext import gettext as _
# The httplib debug level, set to a non-zero value to get debug output
@@ -58,7 +59,7 @@
class RedirectLimit(HttpLib2Error): pass
class FailedToDecompressContent(HttpLib2Error): pass
class UnimplementedDigestAuthOptionError(HttpLib2Error): pass
-class IllFormedDigestChallengeError(HttpLib2Error): pass
+class UnimplementedHmacDigestAuthOptionError(HttpLib2Error): pass
# Open Items:
# -----------
@@ -107,9 +108,9 @@
retval = dict(parts_with_args + parts_wo_args)
return retval
-WWW_AUTH = re.compile("^(?:,?\s*(\w+)\s*=\s*\"([^\"]*?)\")(.*)$")
+WWW_AUTH = re.compile(r"^(?:,?\s*([a-zA-Z0-9_-]+)\s*=\s*\"((?:[^\\\"]|\\.)*?)\")(.*)$")
# Yes, some parameters don't have quotes. Why again am I spending so much time doing HTTP?
-WWW_AUTH2 = re.compile("^(?:,?\s*(\w+)\s*=\s*(\w+))(.*)$")
+WWW_AUTH2 = re.compile(r"^(?:,?\s*([a-zA-Z0-9_-]+)\s*=\s*(\w+))(.*)$")
def _parse_www_authenticate(headers, headername='www-authenticate'):
"""Returns a dictionary of dictionaries, one dict
per auth_scheme."""
@@ -302,7 +303,7 @@
self.challenge['qop'] = ('auth' in [x.strip() for x in qop.split()]) and 'auth' or None
if self.challenge['qop'] is None:
raise UnimplementedDigestAuthOption( _("Unsupported value for qop: %s." % qop))
- self.challenge['algorithm'] = self.challenge.get('algorith', 'MD5')
+ self.challenge['algorithm'] = self.challenge.get('algorithm', 'MD5')
if self.challenge['algorithm'] != 'MD5':
raise UnimplementedDigestAuthOption( _("Unsupported value for algorithm: %s." % self.challenge['algorithm']))
self.A1 = "".join([self.credentials[0], ":", self.challenge['realm'], ":", self.credentials[1]])
@@ -348,6 +349,71 @@
return False
+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):
+ Authentication.__init__(self, credentials, host, request_uri, headers, response, content)
+ challenge = _parse_www_authenticate(response, 'www-authenticate')
+ self.challenge = challenge['hmacdigest']
+ print self.challenge
+ # TODO: self.challenge['domain']
+ self.challenge['reason'] = self.challenge.get('reason', 'unauthorized')
+ if self.challenge['reason'] not in ['unauthorized', 'integrity']:
+ self.challenge['reason'] = 'unauthorized'
+ self.challenge['salt'] = self.challenge.get('salt', '')
+ self.challenge['algorithm'] = self.challenge.get('algorithm', 'HMAC-SHA-1')
+ if self.challenge['algorithm'] not in ['HMAC-SHA-1', 'HMAC-MD5']:
+ raise UnimplementedDigestAuthOption( _("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 UnimplementedDigestAuthOption( _("Unsupported value for pw-algorithm: %s." % self.challenge['pw-algorithm']))
+ if self.challenge['algorithm'] == 'HMAC-MD5':
+ self.hashmod = md5
+ else:
+ self.hashmod = sha
+ if self.challenge['pw-algorithm'] == 'MD5':
+ self.pwhashmod = md5
+ else:
+ self.pwhashmod = sha
+ self.key = "".join([self.credentials[0], ":",
+ self.pwhashmod.new("".join([self.credentials[1], self.challenge['salt']])).hexdigest(),
+ ":", self.challenge['realm']
+ ])
+ print response['www-authenticate']
+ print "".join([self.credentials[1], self.challenge['salt']])
+ print "key_str = %s" % self.key
+ self.key = self.pwhashmod.new(self.key).hexdigest()
+
+ def request(self, method, request_uri, headers, content):
+ """Modify the request headers"""
+ keys = headers.keys()
+ 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())
+ nonce = sha.new(str(random.getrandbits(512))+created).hexdigest() # This is where the 2.4 requirement comes from
+ request_digest = "%s:%s:%s:%s:%s" % (method, request_uri, nonce, created, headers_val)
+ print "key = %s" % self.key
+ print "msg = %s" % request_digest
+ request_digest = hmac.new(self.key, request_digest, self.hashmod).hexdigest()
+ headers['Authorization'] = 'HMACDigest username="%s", realm="%s", nonce="%s", uri="%s", created="%s", response="%s", headers="%s"' % (
+ self.credentials[0],
+ self.challenge['realm'],
+ nonce,
+ request_uri,
+ created,
+ request_digest,
+ keylist,
+ )
+
+ def response(self, response, content):
+ challenge = _parse_www_authenticate(response, 'www-authenticate')['digest']
+ if 'integrity' == challenge.get('reason'):
+ return True
+ return False
+
+
class WsseAuthentication(Authentication):
"""This is thinly tested and should not be relied upon.
At this time there isn't any third party server to test against.
@@ -376,10 +442,11 @@
AUTH_SCHEME_CLASSES = {
"basic": BasicAuthentication,
"wsse": WsseAuthentication,
- "digest": DigestAuthentication
+ "digest": DigestAuthentication,
+ "hmacdigest": HmacDigestAuthentication
}
-AUTH_SCHEME_ORDER = ["digest", "wsse", "basic"]
+AUTH_SCHEME_ORDER = ["hmacdigest", "digest", "wsse", "basic"]
class Http:
diff --git a/index.html b/index.html
index d0e58b9..7d745b1 100644
--- a/index.html
+++ b/index.html
@@ -156,8 +156,14 @@
<dd>MIT</dd>
<dt>Contributors</dt>
- <dd>(Your name here)</dd>
- </dl>
+
+ <dd>
+ Thomas Broyer (t.broyer@ltgt.net)
+ </dd>
+ <dd>
+ (Your name here)
+ </dd>
+ </dl>
</p>
<p style="font-size: small">This page last updated on: $LastChangedDate$.</p>