00001
00002
00003 '''Utilities for HTTP Digest Authentication
00004 '''
00005 import re
00006 from md5 import md5
00007 import random
00008 import time
00009 import httplib
00010
00011 random.seed(int(time.time()*10))
00012
00013 def H(val):
00014 return md5(val).hexdigest()
00015
00016 def KD(secret,data):
00017 return H('%s:%s' % (secret,data))
00018
00019 def A1(username,realm,passwd,nonce=None,cnonce=None):
00020 if nonce and cnonce:
00021 return '%s:%s:%s:%s:%s' % (username,realm,passwd,nonce,cnonce)
00022 else:
00023 return '%s:%s:%s' % (username,realm,passwd)
00024
00025 def A2(method,uri):
00026 return '%s:%s' % (method,uri)
00027
00028 def dict_fetch(d,k,defval=None):
00029 if d.has_key(k):
00030 return d[k]
00031 return defval
00032
00033 def generate_response(chaldict,uri,username,passwd,method='GET',cnonce=None):
00034 """
00035 Generate an authorization response dictionary. chaldict should contain the digest
00036 challenge in dict form. Use fetch_challenge to create a chaldict from a HTTPResponse
00037 object like this: fetch_challenge(res.getheaders()).
00038
00039 returns dict (the authdict)
00040
00041 Note. Use build_authorization_arg() to turn an authdict into the final Authorization
00042 header value.
00043 """
00044 authdict = {}
00045 qop = dict_fetch(chaldict,'qop')
00046 domain = dict_fetch(chaldict,'domain')
00047 nonce = dict_fetch(chaldict,'nonce')
00048 stale = dict_fetch(chaldict,'stale')
00049 algorithm = dict_fetch(chaldict,'algorithm','MD5')
00050 realm = dict_fetch(chaldict,'realm','MD5')
00051 opaque = dict_fetch(chaldict,'opaque')
00052 nc = "00000001"
00053 if not cnonce:
00054 cnonce = H(str(random.randint(0,10000000)))[:16]
00055
00056 if algorithm.lower()=='md5-sess':
00057 a1 = A1(username,realm,passwd,nonce,cnonce)
00058 else:
00059 a1 = A1(username,realm,passwd)
00060
00061 a2 = A2(method,uri)
00062
00063 secret = H(a1)
00064 data = '%s:%s:%s:%s:%s' % (nonce,nc,cnonce,qop,H(a2))
00065 authdict['username'] = '"%s"' % username
00066 authdict['realm'] = '"%s"' % realm
00067 authdict['nonce'] = '"%s"' % nonce
00068 authdict['uri'] = '"%s"' % uri
00069 authdict['response'] = '"%s"' % KD(secret,data)
00070 authdict['qop'] = '"%s"' % qop
00071 authdict['nc'] = nc
00072 authdict['cnonce'] = '"%s"' % cnonce
00073
00074 return authdict
00075
00076
00077 def fetch_challenge(http_header):
00078 """ apparently keywords Basic and Digest are not being checked
00079 anywhere and decisions are being made based on authorization
00080 configuration of client, so I guess you better know what you are
00081 doing. Here I am requiring one or the other be specified.
00082
00083 challenge Basic auth_param
00084 challenge Digest auth_param
00085 """
00086 m = fetch_challenge.wwwauth_header_re.match(http_header)
00087 if m is None:
00088 raise RuntimeError, 'expecting "WWW-Authenticate header [Basic,Digest]"'
00089
00090 d = dict(challenge=m.groups()[0])
00091 m = fetch_challenge.auth_param_re.search(http_header)
00092 while m is not None:
00093 k,v = http_header[m.start():m.end()].split('=')
00094 d[k.lower()] = v[1:-1]
00095 m = fetch_challenge.auth_param_re.search(http_header, m.end())
00096
00097 return d
00098
00099 fetch_challenge.wwwauth_header_re = re.compile(r'\s*([bB]asic|[dD]igest)\s+(?:[\w]+="[^"]+",?\s*)?')
00100 fetch_challenge.auth_param_re = re.compile(r'[\w]+="[^"]+"')
00101
00102
00103 def build_authorization_arg(authdict):
00104 """
00105 Create an "Authorization" header value from an authdict (created by generate_response()).
00106 """
00107 vallist = []
00108 for k in authdict.keys():
00109 vallist += ['%s=%s' % (k,authdict[k])]
00110 return 'Digest '+', '.join(vallist)
00111
00112 if __name__ == '__main__': print _copyright