[CalendarServer-changes] [10636] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Fri Feb 1 15:57:42 PST 2013
Revision: 10636
http://trac.calendarserver.org//changeset/10636
Author: glyph at apple.com
Date: 2013-02-01 15:57:42 -0800 (Fri, 01 Feb 2013)
Log Message:
-----------
Use Twisted implementation of digest auth, reducing the amount of code in web2 and twistedcaldav a little.
Modified Paths:
--------------
CalendarServer/trunk/twext/web2/auth/digest.py
CalendarServer/trunk/twext/web2/test/test_httpauth.py
CalendarServer/trunk/twistedcaldav/directory/digest.py
CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py
CalendarServer/trunk/twistedcaldav/directory/test/util.py
Property Changed:
----------------
CalendarServer/trunk/
Property changes on: CalendarServer/trunk
___________________________________________________________________
Modified: svn:mergeinfo
- /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
+ /CalendarServer/branches/config-separation:4379-4443
/CalendarServer/branches/egg-info-351:4589-4625
/CalendarServer/branches/generic-sqlstore:6167-6191
/CalendarServer/branches/new-store:5594-5934
/CalendarServer/branches/new-store-no-caldavfile:5911-5935
/CalendarServer/branches/new-store-no-caldavfile-2:5936-5981
/CalendarServer/branches/release/CalendarServer-4.3-dev:10180-10190,10192
/CalendarServer/branches/users/cdaboo/batchupload-6699:6700-7198
/CalendarServer/branches/users/cdaboo/cached-subscription-calendars-5692:5693-5702
/CalendarServer/branches/users/cdaboo/component-set-fixes:8130-8346
/CalendarServer/branches/users/cdaboo/directory-cache-on-demand-3627:3628-3644
/CalendarServer/branches/users/cdaboo/implicituidrace:8137-8141
/CalendarServer/branches/users/cdaboo/ischedule-dkim:9747-9979
/CalendarServer/branches/users/cdaboo/managed-attachments:9985-10145
/CalendarServer/branches/users/cdaboo/more-sharing-5591:5592-5601
/CalendarServer/branches/users/cdaboo/partition-4464:4465-4957
/CalendarServer/branches/users/cdaboo/pods:7297-7377
/CalendarServer/branches/users/cdaboo/pycalendar:7085-7206
/CalendarServer/branches/users/cdaboo/pycard:7227-7237
/CalendarServer/branches/users/cdaboo/queued-attendee-refreshes:7740-8287
/CalendarServer/branches/users/cdaboo/relative-config-paths-5070:5071-5105
/CalendarServer/branches/users/cdaboo/shared-calendars-5187:5188-5440
/CalendarServer/branches/users/cdaboo/timezones:7443-7699
/CalendarServer/branches/users/cdaboo/txn-debugging:8730-8743
/CalendarServer/branches/users/glyph/always-abort-txn-on-error:9958-9969
/CalendarServer/branches/users/glyph/case-insensitive-uid:8772-8805
/CalendarServer/branches/users/glyph/conn-limit:6574-6577
/CalendarServer/branches/users/glyph/contacts-server-merge:4971-5080
/CalendarServer/branches/users/glyph/dalify:6932-7023
/CalendarServer/branches/users/glyph/db-reconnect:6824-6876
/CalendarServer/branches/users/glyph/deploybuild:7563-7572
/CalendarServer/branches/users/glyph/digest-auth-redux:10624-10635
/CalendarServer/branches/users/glyph/disable-quota:7718-7727
/CalendarServer/branches/users/glyph/dont-start-postgres:6592-6614
/CalendarServer/branches/users/glyph/imip-and-admin-html:7866-7984
/CalendarServer/branches/users/glyph/ipv6-client:9054-9105
/CalendarServer/branches/users/glyph/linux-tests:6893-6900
/CalendarServer/branches/users/glyph/migrate-merge:8690-8713
/CalendarServer/branches/users/glyph/misc-portability-fixes:7365-7374
/CalendarServer/branches/users/glyph/more-deferreds-6:6322-6368
/CalendarServer/branches/users/glyph/more-deferreds-7:6369-6445
/CalendarServer/branches/users/glyph/multiget-delete:8321-8330
/CalendarServer/branches/users/glyph/new-export:7444-7485
/CalendarServer/branches/users/glyph/one-home-list-api:10048-10073
/CalendarServer/branches/users/glyph/oracle:7106-7155
/CalendarServer/branches/users/glyph/oracle-nulls:7340-7351
/CalendarServer/branches/users/glyph/other-html:8062-8091
/CalendarServer/branches/users/glyph/parallel-sim:8240-8251
/CalendarServer/branches/users/glyph/parallel-upgrade:8376-8400
/CalendarServer/branches/users/glyph/parallel-upgrade_to_1:8571-8583
/CalendarServer/branches/users/glyph/q:9560-9688
/CalendarServer/branches/users/glyph/queue-locking-and-timing:10204-10289
/CalendarServer/branches/users/glyph/quota:7604-7637
/CalendarServer/branches/users/glyph/sendfdport:5388-5424
/CalendarServer/branches/users/glyph/shared-pool-fixes:8436-8443
/CalendarServer/branches/users/glyph/shared-pool-take2:8155-8174
/CalendarServer/branches/users/glyph/sharedpool:6490-6550
/CalendarServer/branches/users/glyph/sharing-api:9192-9205
/CalendarServer/branches/users/glyph/skip-lonely-vtimezones:8524-8535
/CalendarServer/branches/users/glyph/sql-store:5929-6073
/CalendarServer/branches/users/glyph/subtransactions:7248-7258
/CalendarServer/branches/users/glyph/table-alias:8651-8664
/CalendarServer/branches/users/glyph/uidexport:7673-7676
/CalendarServer/branches/users/glyph/unshare-when-access-revoked:10562-10595
/CalendarServer/branches/users/glyph/use-system-twisted:5084-5149
/CalendarServer/branches/users/glyph/uuid-normalize:9268-9296
/CalendarServer/branches/users/glyph/xattrs-from-files:7757-7769
/CalendarServer/branches/users/sagen/applepush:8126-8184
/CalendarServer/branches/users/sagen/inboxitems:7380-7381
/CalendarServer/branches/users/sagen/locations-resources:5032-5051
/CalendarServer/branches/users/sagen/locations-resources-2:5052-5061
/CalendarServer/branches/users/sagen/purge_old_events:6735-6746
/CalendarServer/branches/users/sagen/resource-delegates-4038:4040-4067
/CalendarServer/branches/users/sagen/resource-delegates-4066:4068-4075
/CalendarServer/branches/users/sagen/resources-2:5084-5093
/CalendarServer/branches/users/wsanchez/transations:5515-5593
Modified: CalendarServer/trunk/twext/web2/auth/digest.py
===================================================================
--- CalendarServer/trunk/twext/web2/auth/digest.py 2013-02-01 23:53:35 UTC (rev 10635)
+++ CalendarServer/trunk/twext/web2/auth/digest.py 2013-02-01 23:57:42 UTC (rev 10636)
@@ -28,17 +28,25 @@
http://www.faqs.org/rfcs/rfc2617.html
"""
-import sys
-import time
-import random
-from zope.interface import implements, Interface
+from zope.interface import implements
from twisted.python.hashlib import md5, sha1
-from twisted.cred import credentials, error
-from twisted.internet.defer import succeed
+from twisted.cred import credentials
+
+# FIXME: Technically speaking - although you can't tell from looking at them -
+# these APIs are private, they're defined within twisted.cred._digest. There
+# should probably be some upstream bugs agains Twisted to more aggressively hide
+# implementation details like these if they're not supposed to be used, so we
+# can see the private-ness more clearly. The fix is really just to eliminate
+# this whole module though, and use the Twisted stuff via the public interface,
+# which should be sufficient to do digest auth.
+
+from twisted.cred.credentials import (calcHA1 as _origCalcHA1,
+ calcResponse as _origCalcResponse,
+ calcHA2 as _origCalcHA2)
+from twisted.internet.defer import maybeDeferred
from twext.web2.auth.interfaces import ICredentialFactory
-from twext.web2.http_headers import Token, tokenize, split, parseKeyValue
# The digest math
@@ -50,58 +58,29 @@
}
# DigestCalcHA1
-def calcHA1(
- pszAlg,
- pszUserName,
- pszRealm,
- pszPassword,
- pszNonce,
- pszCNonce,
- preHA1=None
-):
+def calcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce, pszCNonce,
+ preHA1=None):
"""
@param pszAlg: The name of the algorithm to use to calculate the digest.
Currently supported are md5 md5-sess and sha.
@param pszUserName: The username
+
@param pszRealm: The realm
+
@param pszPassword: The password
+
@param pszNonce: The nonce
+
@param pszCNonce: The cnonce
@param preHA1: If available this is a str containing a previously
- calculated HA1 as a hex string. If this is given then the values for
- pszUserName, pszRealm, and pszPassword are ignored.
+ calculated HA1 as a hex string. If this is given then the values for
+ pszUserName, pszRealm, and pszPassword are ignored.
"""
+ return _origCalcHA1(pszAlg, pszUserName, pszRealm, pszPassword, pszNonce,
+ pszCNonce, preHA1)
- if (preHA1 and (pszUserName or pszRealm or pszPassword)):
- raise TypeError(("preHA1 is incompatible with the pszUserName, "
- "pszRealm, and pszPassword arguments"))
-
- if preHA1 is None:
- # We need to calculate the HA1 from the username:realm:password
- m = algorithms[pszAlg]()
- m.update(pszUserName)
- m.update(":")
- m.update(pszRealm)
- m.update(":")
- m.update(pszPassword)
- HA1 = m.digest()
- else:
- # We were given a username:realm:password
- HA1 = preHA1.decode('hex')
-
- if pszAlg == "md5-sess":
- m = algorithms[pszAlg]()
- m.update(HA1)
- m.update(":")
- m.update(pszNonce)
- m.update(":")
- m.update(pszCNonce)
- HA1 = m.digest()
-
- return HA1.encode('hex')
-
# DigestCalcResponse
def calcResponse(
HA1,
@@ -114,285 +93,39 @@
pszDigestUri,
pszHEntity,
):
- m = algorithms[algo]()
- m.update(pszMethod)
- m.update(":")
- m.update(pszDigestUri)
- if pszQop == "auth-int":
- m.update(":")
- m.update(pszHEntity)
- HA2 = m.digest().encode('hex')
+ return _origCalcResponse(HA1, _origCalcHA2(algo, pszMethod, pszDigestUri,
+ pszQop, pszHEntity),
+ algo, pszNonce, pszNonceCount, pszCNonce, pszQop)
- m = algorithms[algo]()
- m.update(HA1)
- m.update(":")
- m.update(pszNonce)
- m.update(":")
- if pszNonceCount and pszCNonce: # pszQop:
- m.update(pszNonceCount)
- m.update(":")
- m.update(pszCNonce)
- m.update(":")
- m.update(pszQop)
- m.update(":")
- m.update(HA2)
- respHash = m.digest().encode('hex')
- return respHash
-class IUsernameDigestHash(Interface):
- """
- This credential is used when a CredentialChecker has access to the hash
- of the username:realm:password as in an Apache .htdigest file.
- """
- def checkHash(self, digestHash):
- """
- @param digestHash: The hashed username:realm:password to check against.
+DigestedCredentials = credentials.DigestedCredentials
- @return: a deferred which becomes, or a boolean indicating if the
- hash matches.
- """
-
-
-class DigestedCredentials:
- """Yet Another Simple HTTP Digest authentication scheme"""
-
- implements(credentials.IUsernameHashedPassword,
- IUsernameDigestHash)
-
- def __init__(self, username, method, realm, fields, originalMethod=None):
- self.username = username
- self.method = method
- self.realm = realm
- self.fields = fields
- if originalMethod:
- self.originalMethod = originalMethod
- else:
- self.originalMethod = method
-
- def checkPassword(self, password):
- response = self.fields.get('response')
- uri = self.fields.get('uri')
- nonce = self.fields.get('nonce')
- cnonce = self.fields.get('cnonce')
- nc = self.fields.get('nc')
- algo = self.fields.get('algorithm', 'md5').lower()
- qop = self.fields.get('qop', 'auth')
-
- expected = calcResponse(
- calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
- algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
- )
-
- if expected == response:
- return True
-
- # IE7 sends cnonce and nc values, but auth fails if they are used.
- # So try again without them...
- # They can be omitted for backwards compatibility [RFC 2069].
- if nc is not None or cnonce is not None:
- expected = calcResponse(
- calcHA1(algo, self.username, self.realm, password, nonce, cnonce),
- algo, nonce, None, None, qop, self.originalMethod, uri, None
- )
- if expected == response:
- return True
-
- def checkHash(self, digestHash):
- response = self.fields.get('response')
- uri = self.fields.get('uri')
- nonce = self.fields.get('nonce')
- cnonce = self.fields.get('cnonce')
- nc = self.fields.get('nc')
- algo = self.fields.get('algorithm', 'md5').lower()
- qop = self.fields.get('qop', 'auth')
-
- expected = calcResponse(
- calcHA1(algo, None, None, None, nonce, cnonce, preHA1=digestHash),
- algo, nonce, nc, cnonce, qop, self.originalMethod, uri, None
- )
-
- return expected == response
-
-
class DigestCredentialFactory(object):
- """
- Support for RFC2617 HTTP Digest Authentication
-
- @cvar CHALLENGE_LIFETIME_SECS: The number of seconds for which an
- opaque should be valid.
-
- @ivar privateKey: A random string used for generating the secure opaque.
- """
-
implements(ICredentialFactory)
- CHALLENGE_LIFETIME_SECS = 15 * 60 # 15 minutes
+ CHALLENGE_LIFETIME_SECS = (
+ credentials.DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
+ )
- scheme = "digest"
-
def __init__(self, algorithm, realm):
- """
- @type algorithm: C{str}
- @param algorithm: case insensitive string that specifies
- the hash algorithm used, should be either, md5, md5-sess
- or sha
+ self._real = credentials.DigestCredentialFactory(algorithm, realm)
- @type realm: C{str}
- @param realm: case sensitive string that specifies the realm
- portion of the challenge
- """
- self.algorithm = algorithm
- self.realm = realm
+ scheme = 'digest'
- c = tuple([random.randrange(sys.maxint) for _ in range(3)])
+ def getChallenge(self, peer):
+ return maybeDeferred(self._real.getChallenge, peer.host)
- self.privateKey = '%d%d%d' % c
- def generateNonce(self):
- c = tuple([random.randrange(sys.maxint) for _ in range(3)])
- c = '%d%d%d' % c
- return c
+ def generateOpaque(self, *a, **k):
+ return self._real._generateOpaque(*a, **k)
- def _getTime(self):
- """
- Parameterize the time based seed used in generateOpaque
- so we can deterministically unittest it's behavior.
- """
- return time.time()
- def generateOpaque(self, nonce, clientip):
- """
- Generate an opaque to be returned to the client.
- This should be a unique string that can be returned to us and verified.
- """
-
- # Now, what we do is encode the nonce, client ip and a timestamp
- # in the opaque value with a suitable digest
- key = "%s,%s,%s" % (nonce, clientip, str(int(self._getTime())))
- digest = md5(key + self.privateKey).hexdigest()
- ekey = key.encode('base64')
- return "%s-%s" % (digest, ekey.replace('\n', ''))
-
def verifyOpaque(self, opaque, nonce, clientip):
- """
- Given the opaque and nonce from the request, as well as the clientip
- that made the request, verify that the opaque was generated by us.
- And that it's not too old.
+ return self._real._verifyOpaque(opaque, nonce, clientip)
- @param opaque: The opaque value from the Digest response
- @param nonce: The nonce value from the Digest response
- @param clientip: The remote IP address of the client making the request
- @return: C{True} if the opaque was successfully verified.
-
- @raise error.LoginFailed: if C{opaque} could not be parsed or
- contained the wrong values.
- """
-
- # First split the digest from the key
- opaqueParts = opaque.split('-')
- if len(opaqueParts) != 2:
- raise error.LoginFailed('Invalid response, invalid opaque value')
-
- # Verify the key
- key = opaqueParts[1].decode('base64')
- keyParts = key.split(',')
-
- if len(keyParts) != 3:
- raise error.LoginFailed('Invalid response, invalid opaque value')
-
- if keyParts[0] != nonce:
- raise error.LoginFailed(
- 'Invalid response, incompatible opaque/nonce values')
-
- if keyParts[1] != clientip:
- raise error.LoginFailed(
- 'Invalid response, incompatible opaque/client values')
-
- if (int(self._getTime()) - int(keyParts[2]) >
- DigestCredentialFactory.CHALLENGE_LIFETIME_SECS):
-
- raise error.LoginFailed(
- 'Invalid response, incompatible opaque/nonce too old')
-
- # Verify the digest
- digest = md5(key + self.privateKey).hexdigest()
- if digest != opaqueParts[0]:
- raise error.LoginFailed('Invalid response, invalid opaque value')
-
- return True
-
- def getChallenge(self, peer):
- """
- Generate the challenge for use in the WWW-Authenticate header
-
- @param peer: The L{IAddress} of the requesting client.
-
- @return: The C{dict} that can be used to generate a WWW-Authenticate
- header.
- """
-
- c = self.generateNonce()
- o = self.generateOpaque(c, peer.host)
-
- return succeed({'nonce': c,
- 'opaque': o,
- 'qop': 'auth',
- 'algorithm': self.algorithm,
- 'realm': self.realm,
- })
-
def decode(self, response, request):
- """
- Decode the given response and attempt to generate a
- L{DigestedCredentials} from it.
-
- @type response: C{str}
- @param response: A string of comma seperated key=value pairs
-
- @type request: L{twext.web2.server.Request}
- @param request: the request being processed
-
- @return: L{DigestedCredentials}
-
- @raise: L{error.LoginFailed} if the response does not contain a
- username, a nonce, an opaque, or if the opaque is invalid.
- """
- response = ' '.join(response.splitlines())
-
- try:
- parts = split(tokenize((response,), foldCase=False), Token(","))
-
- auth = {}
-
- for (k, v) in [parseKeyValue(p) for p in parts]:
- auth[k.strip()] = v.strip()
- except ValueError:
- raise error.LoginFailed('Invalid response.')
-
- username = auth.get('username')
- if not username:
- raise error.LoginFailed('Invalid response, no username given.')
-
- if 'opaque' not in auth:
- raise error.LoginFailed('Invalid response, no opaque given.')
-
- if 'nonce' not in auth:
- raise error.LoginFailed('Invalid response, no nonce given.')
-
- # Now verify the nonce/opaque values for this client
- if self.verifyOpaque(auth.get('opaque'),
- auth.get('nonce'),
- request.remoteAddr.host):
-
- if hasattr(request, "originalMethod"):
- originalMethod = request.originalMethod
- else:
- originalMethod = None
-
- return succeed(DigestedCredentials(username,
- request.method,
- self.realm,
- auth,
- originalMethod))
+ method = getattr(request, "originalMethod", request.method)
+ host = request.remoteAddr.host
+ return self._real.decode(response, method, host)
Modified: CalendarServer/trunk/twext/web2/test/test_httpauth.py
===================================================================
--- CalendarServer/trunk/twext/web2/test/test_httpauth.py 2013-02-01 23:53:35 UTC (rev 10635)
+++ CalendarServer/trunk/twext/web2/test/test_httpauth.py 2013-02-01 23:57:42 UTC (rev 10636)
@@ -16,6 +16,41 @@
_trivial_GET = SimpleRequest(None, 'GET', '/')
+FAKE_STATIC_NONCE = '178288758716122392881254770685'
+
+
+def makeDigestDeterministic(twistedDigestFactory, key="0",
+ nonce=FAKE_STATIC_NONCE, time=0):
+ """
+ Patch up various bits of private state to make a digest credential factory
+ (the one that comes from Twisted) behave deterministically.
+ """
+
+ def _fakeStaticNonce():
+ """
+ Generate a static nonce
+ """
+ return nonce
+
+ def _fakeStaticTime():
+ """
+ Return a stable time
+ """
+ return time
+
+ twistedDigestFactory.privateKey = key
+
+ # FIXME: These tests are somewhat redundant with the tests for Twisted's
+ # built-in digest auth; these private values need to be patched to
+ # create deterministic results, but at some future point the whole
+ # digest module should be removed from twext.web2 (as all of twext.web2
+ # should be removed) and we can just get rid of this.
+
+ twistedDigestFactory._generateNonce = _fakeStaticNonce
+ twistedDigestFactory._getTime = _fakeStaticTime
+
+
+
class FakeDigestCredentialFactory(digest.DigestCredentialFactory):
"""
A Fake Digest Credential Factory that generates a predictable
@@ -24,22 +59,12 @@
def __init__(self, *args, **kwargs):
super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+ makeDigestDeterministic(self._real, self._fakeStaticPrivateKey)
- self.privateKey = "0"
+ _fakeStaticPrivateKey = "0"
- def generateNonce(self):
- """
- Generate a static nonce
- """
- return '178288758716122392881254770685'
- def _getTime(self):
- """
- Return a stable time
- """
- return 0
-
class BasicAuthTestCase(unittest.TestCase):
def setUp(self):
self.credentialFactory = basic.BasicCredentialFactory('foo')
@@ -376,7 +401,8 @@
key = '%s,%s,%s' % (challenge['nonce'],
clientAddress.host,
'-137876876')
- digest = md5(key + credentialFactory.privateKey).hexdigest()
+ digest = (md5(key + credentialFactory._fakeStaticPrivateKey)
+ .hexdigest())
ekey = key.encode('base64')
oldNonceOpaque = '%s-%s' % (digest, ekey.strip('\n'))
Modified: CalendarServer/trunk/twistedcaldav/directory/digest.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/digest.py 2013-02-01 23:53:35 UTC (rev 10635)
+++ CalendarServer/trunk/twistedcaldav/directory/digest.py 2013-02-01 23:57:42 UTC (rev 10636)
@@ -1,3 +1,4 @@
+# -*- test-case-name: twistedcaldav.directory.test.test_digest -*-
##
# Copyright (c) 2006-2013 Apple Inc. All rights reserved.
#
@@ -44,14 +45,14 @@
An interface to a digest credentials database that is used to hold per-client digest credentials so that fast
re-authentication can be done with replay attacks etc prevented.
"""
-
+
def has_key(self, key):
"""
See whether the matching key exists.
@param key: the key to check.
@type key: C{str}.
-
+
@return: C{True} if the key exists, C{False} otherwise.
"""
pass
@@ -66,7 +67,7 @@
@type value: any.
"""
pass
-
+
def get(self, key):
"""
Validate client supplied credentials by comparing with the cached values. If valid, store the new
@@ -78,7 +79,7 @@
@return: the value for the corresponding key, or C{None} if the key is not found.
"""
pass
-
+
def delete(self, key):
"""
Remove the record associated with the supplied key.
@@ -99,7 +100,7 @@
namespace=namespace,
pickle=True,
)
-
+
def has_key(self, key):
"""
See IDigestCredentialsDatabase.
@@ -143,11 +144,12 @@
self.qop = qop
self.db = DigestCredentialsMemcache(namespace)
+
@inlineCallbacks
def getChallenge(self, peer):
"""
- Generate the challenge for use in the WWW-Authenticate header
- Do the default behavior but then strip out any 'qop' from the challenge fields
+ Generate the challenge for use in the WWW-Authenticate header Do the
+ default behavior but then strip out any 'qop' from the challenge fields
if no qop was specified.
@param peer: The L{IAddress} of the requesting client.
@@ -156,8 +158,10 @@
header.
"""
- c = self.generateNonce()
-
+ challenge = yield (super(QopDigestCredentialFactory, self)
+ .getChallenge(peer))
+ c = challenge['nonce']
+
# Make sure it is not a duplicate
result = (yield self.db.has_key(c))
if result:
@@ -166,24 +170,18 @@
# The database record is a tuple of (nonce-count, timestamp)
yield self.db.set(c, (0, time.time()))
- challenge = {
- 'nonce': c,
- 'qop': 'auth',
- 'algorithm': self.algorithm,
- 'realm': self.realm,
- }
-
if self.qop:
challenge['qop'] = self.qop
else:
del challenge['qop']
-
+
# If stale was marked when decoding this request's Authorization header, add that to the challenge
if hasattr(peer, 'stale') and peer.stale:
challenge['stale'] = 'true'
returnValue(challenge)
+
@inlineCallbacks
def decode(self, response, request):
"""
@@ -208,17 +206,17 @@
"""
response = ' '.join(response.splitlines())
-
+
try:
parts = split(tokenize((response,), foldCase=False), Token(","))
-
+
auth = {}
-
+
for (k, v) in [parseKeyValue(p) for p in parts]:
auth[k.strip()] = v.strip()
except ValueError:
raise error.LoginFailed('Invalid response.')
-
+
username = auth.get('username')
if not username:
raise error.LoginFailed('Invalid response, no username given.')
@@ -235,10 +233,9 @@
originalMethod = None
credentials = DigestedCredentials(username,
- request.method,
- self.realm,
- auth,
- originalMethod)
+ originalMethod or request.method,
+ self._real.authenticationRealm,
+ auth)
if not self.qop and credentials.fields.has_key('qop'):
del credentials.fields['qop']
@@ -257,7 +254,7 @@
@type auth: C{dict}
@param request: the request being processed.
@type request: L{twext.web2.server.Request}
-
+
@return: C{True} if validated.
@raise LoginFailed: if validation fails.
"""
@@ -279,13 +276,13 @@
if nonce_count is None:
yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count is required when qop is specified')
-
+
# Next check the nonce-count is one greater than the previous one and update it in the DB
try:
nonce_count = int(nonce_count, 16)
except ValueError:
yield self._invalidate(nonce)
- raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
+ raise error.LoginFailed('nonce-count is not a valid hex string: %s' % (auth.get('nonce-count'),))
if nonce_count != db_nonce_count + 1:
yield self._invalidate(nonce)
raise error.LoginFailed('nonce-count value out of sequence: %s should be one more than %s' % (nonce_count, db_nonce_count,))
@@ -295,8 +292,8 @@
# i.e. we can't allow a qop auth then a non-qop auth with the same nonce
if db_nonce_count != 0:
yield self._invalidate(nonce)
- raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
-
+ raise error.LoginFailed('nonce-count was sent with this nonce: %s' % (nonce,))
+
# Now check timestamp
if db_timestamp + DigestCredentialFactory.CHALLENGE_LIFETIME_SECS <= time.time():
yield self._invalidate(nonce)
@@ -305,7 +302,7 @@
raise error.LoginFailed('Digest credentials expired')
returnValue(True)
-
+
def _invalidate(self, nonce):
"""
Invalidate cached credentials for the specified nonce value.
Modified: CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py 2013-02-01 23:53:35 UTC (rev 10635)
+++ CalendarServer/trunk/twistedcaldav/directory/test/test_digest.py 2013-02-01 23:57:42 UTC (rev 10636)
@@ -15,7 +15,6 @@
##
import sys
-import time
from hashlib import md5
from twisted.cred import error
@@ -30,6 +29,8 @@
from twistedcaldav.test.util import TestCase
from twistedcaldav.config import config
from twext.web2.auth.digest import DigestCredentialFactory
+from twext.web2.test.test_httpauth import makeDigestDeterministic
+from twext.web2.test.test_httpauth import FAKE_STATIC_NONCE
class FakeDigestCredentialFactory(QopDigestCredentialFactory):
"""
@@ -39,12 +40,8 @@
def __init__(self, *args, **kwargs):
super(FakeDigestCredentialFactory, self).__init__(*args, **kwargs)
+ makeDigestDeterministic(self._real)
- def generateNonce(self):
- """
- Generate a static nonce
- """
- return '178288758716122392881254770685'
clientAddress = address.IPv4Address('TCP', '127.0.0.1', 80)
@@ -432,7 +429,7 @@
request
)
- factory._invalidate(factory.generateNonce())
+ factory._invalidate(FAKE_STATIC_NONCE)
response = (yield UnauthorizedResponse.makeResponse(
{"Digest":factory},
request.remoteAddr
@@ -525,10 +522,13 @@
"""
Test that we can decode a valid response to our challenge
"""
+ theTime = 0
+ class newtime(object):
+ def time(self):
+ return theTime
+ from twistedcaldav.directory import digest
+ self.patch(digest, "time", newtime())
- oldTime = DigestCredentialFactory.CHALLENGE_LIFETIME_SECS
- DigestCredentialFactory.CHALLENGE_LIFETIME_SECS = 2
-
for ctr, factory in enumerate(self.credentialFactories):
challenge = (yield factory.getChallenge(clientAddress))
@@ -540,7 +540,7 @@
creds = (yield factory.decode(clientResponse, _trivial_GET()))
self.failUnless(creds.checkPassword('password'))
- time.sleep(3)
+ theTime += DigestCredentialFactory.CHALLENGE_LIFETIME_SECS + 1
request = _trivial_GET()
try:
clientResponse = authRequest2[ctr] % (
@@ -556,7 +556,6 @@
challenge = (yield factory.getChallenge(request.remoteAddr))
self.assertTrue(challenge.get("stale") == "true")
- DigestCredentialFactory.CHALLENGE_LIFETIME_SECS = oldTime
def _trivial_GET():
return SimpleRequest(None, 'GET', '/')
Modified: CalendarServer/trunk/twistedcaldav/directory/test/util.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/test/util.py 2013-02-01 23:53:35 UTC (rev 10635)
+++ CalendarServer/trunk/twistedcaldav/directory/test/util.py 2013-02-01 23:57:42 UTC (rev 10636)
@@ -342,7 +342,6 @@
"cnonce": "phlegm",
"nc": None,
},
- None,
)
if good:
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20130201/3789e02d/attachment-0001.html>
More information about the calendarserver-changes
mailing list