From bdd4de6cfd3e5c8f4b5a246a47a6e42f737467c4 Mon Sep 17 00:00:00 2001
From: Helmut Grohne <helmut@subdivi.de>
Date: Wed, 24 Jun 2009 17:56:00 +0200
Subject: added dbapi2 (sql) backed noncestore! yeah :-)

---
 wsgitools/digest.py | 134 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 134 insertions(+)

diff --git a/wsgitools/digest.py b/wsgitools/digest.py
index 52cdcee..d0039e0 100755
--- a/wsgitools/digest.py
+++ b/wsgitools/digest.py
@@ -293,6 +293,140 @@ class MemoryNonceStore(NonceStoreBase):
             self.nonces[lower] = (nt, nv, uses+1)
         return True
 
+__all__.append("LazyDBAPI2Opener")
+class LazyDBAPI2Opener:
+    """
+    Connects to database on first request. Otherwise it behaves like a dbapi2
+    connection.
+    """
+    def __init__(self, function, *args, **kwargs):
+        """
+        The database will be connected on the first method call. This is done
+        by calling the given function with the remaining parameters.
+        @param function: is the function that connects to the database
+        """
+        self._function = function
+        self._args = args
+        self._kwargs = kwargs
+        self._dbhandle = None
+    def _getdbhandle(self):
+        """Returns an open database connection. Open if necessary."""
+        if self._dbhandle is None:
+            self._dbhandle = self._function(*self._args, **self._kwargs)
+            self._function = self._args = self._kwargs = None
+        return self._dbhandle
+    def cursor(self):
+        """dbapi2"""
+        return self._getdbhandle().cursor()
+    def commit(self):
+        """dbapi2"""
+        return self._getdbhandle().commit()
+    def rollback(self):
+        """dbapi2"""
+        return self._getdbhandle().rollback()
+    def close(self):
+        """dbapi2"""
+        return self._getdbhandle().close()
+
+__all__.append("DBAPI2NonceStore")
+class DBAPI2NonceStore(NonceStoreBase):
+    """
+    A dbapi2-backed nonce store implementation suitable for usage with forking
+    wsgi servers such as scgi.forkpool.
+    """
+    def __init__(self, dbhandle, maxage=300, maxuses=5, table="nonces"):
+        """
+        @type filename: str
+        @param filename: the path to the bsddb file that is used as backingstore
+        @type maxage: int
+        @param maxage: is the number of seconds a nonce may be valid. Choosing a
+               large value may result in more memory usage whereas a smaller
+               value results in more requests. Defaults to 5 minutes.
+        @type maxuses: int
+        @param maxuses: is the number of times a nonce may be used (with
+                different nc values). A value of 1 makes nonces usable exactly
+                once resulting in more requests. Defaults to 5.
+        """
+        NonceStoreBase.__init__(self)
+        self.dbhandle = dbhandle
+        self.maxage = maxage
+        self.maxuses = maxuses
+        self.table = table
+        self.server_secret = gen_rand_str()
+
+    def _cleanup(self, cur):
+        """internal methods cleaning list of valid nonces"""
+        old = format_time(time.time() - self.maxage)
+        cur.execute("DELETE FROM %s WHERE key < '%s:';" % (self.table, old))
+
+    def newnonce(self, ident=None):
+        """
+        Generates a new nonce string.
+        @rtype: str
+        """
+        nonce_time = format_time(time.time())
+        nonce_value = gen_rand_str()
+        dbkey = "%s:%s" % (nonce_time, nonce_value)
+        cur = self.dbhandle.cursor()
+        self._cleanup(cur) # avoid growing database
+        cur.execute("INSERT INTO %s VALUES ('%s', '1');" % (self.table, dbkey))
+        self.dbhandle.commit()
+        token = "%s:%s" % (dbkey, self.server_secret)
+        if ident is not None:
+            token = "%s:%s" % (token, ident)
+        token = md5(token).hexdigest()
+        return "%s:%s:%s" % (nonce_time, nonce_value, token)
+
+    def checknonce(self, nonce, count=1, ident=None):
+        """
+        Do a check for whether the provided string is a nonce and increase usage
+        count on returning True.
+        @type nonce: str
+        @type count: int
+        @rtype: bool
+        """
+        try:
+            nonce_time, nonce_value, nonce_hash = nonce.split(':')
+        except ValueError:
+            return False
+        if not nonce_time.isalnum() or not nonce_value.replace("+", ""). \
+           replace("/", "").replace("=", "").isalnum():
+            return False
+        token = "%s:%s:%s" % (nonce_time, nonce_value, self.server_secret)
+        if ident is not None:
+            token = "%s:%s" % (token, ident)
+        token = md5(token).hexdigest()
+        if token != nonce_hash:
+            return False
+
+        if nonce_time < format_time(time.time() - self.maxage):
+            return False
+
+        cur = self.dbhandle.cursor()
+        #self._cleanup(cur) # avoid growing database
+
+        dbkey = "%s:%s" % (nonce_time, nonce_value)
+        cur.execute("SELECT value FROM %s WHERE key = '%s';" %
+                    (self.table, dbkey))
+        uses = cur.fetchone()
+        if uses is None:
+            self.dbhandle.commit()
+            return False
+        uses = int(uses[0])
+        if count != uses:
+            cur.execute("DELETE FROM %s WHERE key = '%s';" %
+                        (self.table, dbkey))
+            self.dbhandle.commit()
+            return False
+        if uses >= self.maxuses:
+            cur.execute("DELETE FROM %s WHERE key = '%s';" %
+                        (self.table, dbkey))
+        else:
+            cur.execute("UPDATE %s SET value = '%d' WHERE key = '%s';" %
+                        (self.table, uses + 1, dbkey))
+        self.dbhandle.commit()
+        return True
+
 __all__.append("AuthDigestMiddleware")
 class AuthDigestMiddleware:
     """Middleware partly implementing RFC2617. (md5-sess was omited)"""
-- 
cgit v1.2.3