[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