[Zodb-checkins] CVS: Packages/ZEO - Validators.py:1.1.2.1 list_cache.py:1.1.2.1 srp.py:1.1.2.1 srpdemo.py:1.1.2.1 ClientCache.py:1.7.2.1 ClientStorage.py:1.11.2.1 StorageServer.py:1.11.2.1

Jim Fulton jim@zope.com
Mon, 7 Jan 2002 16:20:31 -0500


Update of /cvs-repository/Packages/ZEO
In directory cvs.zope.org:/tmp/cvs-serv15436

Modified Files:
      Tag: ZEO-Auth-Dev
	ClientCache.py ClientStorage.py StorageServer.py 
Added Files:
      Tag: ZEO-Auth-Dev
	Validators.py list_cache.py srp.py srpdemo.py 
Log Message:
Checking in some old attempts at adding authentication to ZEO
for posterity. In particular, there's a reasonable clean
SRP implementation here.


=== Added File Packages/ZEO/Validators.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""

Revision information: $Id: Validators.py,v 1.1.2.1 2002/01/07 21:20:31 jim Exp $
"""
"""Objects that validate connections
"""

class HostValidator:

    def __init__(self, storage, allow=None, read_only=0):
        self._storage=storage
        self.__allow=allow
        self.__ro=read_only

    def validate(storage_id, server, connection):
        """Try to validate a connection"""
        # Need code to check connection address against allow list!
        server.register_storage(
            connection, self._storage,
            self.__ro and StorageServer.read_storage_method or storage_method
            )

class SRPValidator:

    def __init__(self, storage, allow=None, read_only=0,
                 hash=None, n=None, g=None, rg=None):
        self._storage=storage
        self.__allow=allow
        self.__ro=read_only
        

    def validate(storage_id, server, connection):

class SRPConnectionValidator:

    def __init__(self, validator, server, connection):
        self._validator=validator
        self.server=server


=== Added File Packages/ZEO/list_cache.py ===
try: import ZODB
except:
    import sys
    sys.path.append('..')

from ZODB.utils import U64
from ZODB.TimeStamp import TimeStamp
from struct import unpack

class list:
    def __init__(self, f):
        if type(f) is type(''): f=open(f,'rb')
        f.read(4) # header
        self.f=f
        self.i=-1
        self.r='','','','','',''

    def read_record(self):
        read=self.f.read
        
        h=read(27)
        if len(h)==27 and h[8] in 'vni':
            tlen, vlen, dlen = unpack(">iHi", h[9:19])
        else: return
        if tlen <= 0 or vlen < 0 or dlen <= 0 or vlen+dlen > tlen:
            return

        oid=h[:8]
        serial=TimeStamp(h[19:27])
        data=read(dlen)
        if vlen:
            version = read(vlen)
            vdlen=read(4)
            if len(vdlen) != 4: return
            vdlen=unpack(">i", vdlen)[0]
            if vlen+dlen+42+vdlen > tlen: return
            vdata=read(vdlen)
            vs=TimeStamp(read(8))
        else:
            vs=None
            vdata=version=''
        if read(4) != h[9:13]: return
        return oid, status, serial, data, version, vs, vdata

    def __getitem__(self, i):
        if not self.r or i < self.i: raise IndexError, i
        while i > self.i:
            self.r = self.read_record()
            self.i=self.i+1
            if not self.r: raise IndexError, i
        return self.r

def list_cache(f):
    for oid, status, serial, data, version, vs, vdata in list(f):
        print oid, status, serial, version, vs

if __name__=='__main__':
    import sys
    list_cache(sys.argv[1])
               


=== Added File Packages/ZEO/srp.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""

Revision information: $Id: srp.py,v 1.1.2.1 2002/01/07 21:20:31 jim Exp $
"""
"""Simplest possible SRP implementation

Usage:

  - Create a 'SRPConfig' object with same given parameters for clients
    and server.

  - On server, for each user, with user name 'U' and password, 'p', call
    'user_info' on the configuration object passing the password to get
    a user salt, 's', and a verifier, 'v'. Save the salt and verifier for
    the user name.

  - When a client wants to connect, it creates a 'User' object, passing
    in the configuration, user name, and password. It calls 'session'
    on the 'User' object to get the user name, 'U', and a temporary public
    key, 'A'. It sends 'U' and 'A' to the server.

  - On the server, the user name, 'U', is used to look up the salt, 's',
    and verifier, 'v'. A Host object is created by passing 'U', 's', 'v', and
    'A'. The 'session' method is called on the 'Host' object to get the
    salt, 's', a temporary public key, 'B', and a random number, 'u'. These
    are send to the client.

  - On the client, the 'key' method is called on the 'User' object,
    passing 's', 'B', and 'u' received from the server. A shared session
    key, 'K', and client proof, 'M', are returned. The client proof is
    sent to the server.

  - On the server, the client proof is validated by passing it to the
    'validate' method on the 'Host' object.  The 'validate' method
    returns the shared session key, 'K', and the host proof, which is
    sent to the client.

  - On the client, the host proof is validated by passing it to the
    'validate' method on the 'User' object.

The shared session key may be used to encrypt communication between
the client and host using a symetric key encryption algorithm.
"""

LongType=type(0L)

class SRPConfig:
    """Hold an SRP configuration.
    """
    
    def __init__(self, h, g, N, random):
        """Create an SRP configuration:

        h -- A one-way has function that accepts a 
             string and returns a string.

        g -- A (long) generator modulo N

        N -- A large safe prime (N = 2q+1, where q is prime)
             All arithmetic is done modulo N.

        random -- A t-bit random (long) number generator.        
        """
        self.h, self.g, self.N, self.random = (
            h, g, N, random) # algorthm info
        self.ngh=stol(h(str(N))) ^ stol(h(str(g)))

    def H(self, *args):
        hash=self.h
        l=list(args)
        h=''
        while l: h=hash(str(l.pop())+h)
        return h

    def M(self, s, U, A, B, K):
        return self.H(self.ngh, self.H(U), s, A, B, K)

    def user_info(self, p, s=None):
        """Get the password verifier and user salt for server
        """
        if s is None: s=self.random()
        x=stol(self.H(s, p))
        v=pow(self.g, x, self.N)
        return s, v
        
def stol(s):
    # Convert the bytes in a string to a long.
    r=0L
    for c in s: r=r*256+ord(c)
    return r

class User:
    """Provide user side of SRP computations
    """
    
    def __init__(self, config, U, p):
        self.config=config
        self.user=U, p

    def session(self):
        U, p = self.user
        a=self.a=self.config.random()
        A=self.A=pow(self.config.g, a, self.config.N)
        return U, A

    def key(self, s, B, u):
        if (B%self.config.N)==0:
            raise ValueError, B
        if u==0: raise ValueError, u
        U, p = self.user
        N=self.config.N
        x=stol(self.config.H(s, p))
        v=pow(self.config.g, x, N)
        S=pow(B-v, self.a+u*x, N)
        self.K=K=self.config.H(S)
        self.M=M=self.config.M(s, U, self.A, B, K)
        return K, M

    def validate(self, proof):
        if proof != self.config.H(self.A, self.M, self.K):
            raise ValueError, proof


class Host:
    """Provide host side of SRP computations.
    """
    
    def __init__(self, config, U, s, v, A):
        self.config = config
        if A <= 0 or A >= self.config.N: raise ValueError, A
        self.user = U, s, v, A

    def session(self):
        U, s, v, A = self.user
        N=self.config.N
        b=self.b=self.config.random()
        B=self.B=v+pow(self.config.g, b, N)
        u=self.u=self.config.random()

        S=pow(A*pow(v, u, N), b, N)
        K=self.K=self.config.H(S)
        
        return s, self.B, self.u

    def validate(self, proof):
        U, s, v, A = self.user
        K=self.K
        M=self.config.M(s, U, A, self.B, K)
        if M != proof: raise ValueError, (proof, M)
        return K, self.config.H(A, M, K)
        




=== Added File Packages/ZEO/srpdemo.py ===
##############################################################################
#
# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
# 
# This software is subject to the provisions of the Zope Public License,
# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
# FOR A PARTICULAR PURPOSE
# 
##############################################################################
"""

Revision information: $Id: srpdemo.py,v 1.1.2.1 2002/01/07 21:20:31 jim Exp $
"""
import srp, sha, time, random

def h(v):
    s=sha.new()
    s.update(v)
    return s.digest()

def r(randint=random.randint):
    r=long(randint(0,1<<30))    
    for i in 1,2,3,4,5,6:
        r = (r << 24) + randint(0,1<<30)
    return r

def r_(randint=random.randint):
    return long(randint(0,1<<30))    


c=srp.SRPConfig(
    N=137656596376486790043182744734961384933899167257744121335064027192370741112305920493080254690601316526576747330553110881621319493425219214435734356437905637147670206858858966652975541347966997276817657605917471296442404150473520316654025988200256062845025470327802138620845134916799507318209468806715548156999L,
    g=8623462398472349872L,
    h=h,
    random=r,
    )

U='jim'
p='zope rools and I know it'
s, v = c.user_info(p)
#print "U, p, s, x, v: ", U, p, s, v

user=srp.User(c, U, p)
U, A = user.session()
#print 'U, A: ', U, A

host=srp.Host(c, U, s, v, A)
s, B, u = host.session()
#print "s, B, u: ", s, B, u
#print 'K', `host.K`

K, M = user.key(s, B, u)

#print 'K==K', host.K==K

#print "K, M: ", `K`, `M`

K, host_proof = host.validate(M)
#print "host_proof:", `host_proof`

user.validate(host_proof)


=== Packages/ZEO/ClientCache.py 1.7 => 1.7.2.1 ===
   oid -- 8-byte object id
 
-  status -- 1-byte status v': valid, 'n': non-version valid, 'i': invalid
+  status -- 1-byte status 'v': valid, 'n': non-version valid, 'i': invalid
 
   tlen -- 4-byte (unsigned) record length
 
@@ -108,7 +108,7 @@
 
   vdata -- version data (if vlen > 0)
 
-  vserial -- 8-byte version serial (timestamp)
+  vserial -- 8-byte version serial (timestamp) (if vlen > 0)
 
   tlen -- 4-byte (unsigned) record length (for redundancy and backward
           traversal)
@@ -240,20 +240,27 @@
         if len(h)==27 and h[8] in 'nv' and h[:8]==oid:
             tlen, vlen, dlen = unpack(">iHi", h[9:19])
         else: tlen=-1
-        if tlen <= 0 or vlen < 0 or dlen <= 0 or vlen+dlen > tlen:
+        if tlen <= 0 or vlen < 0 or dlen < 0 or vlen+dlen > tlen:
             del self._index[oid]
             return None
 
-        if version and h[8]=='n': return None
+        if h[8]=='n': 
+            if version: return None
+            if not dlen:
+                del self._index[oid]
+                return None                
         
         if not vlen or not version:
-            return read(dlen), h[19:]
+            if dlen: return read(dlen), h[19:]
+            else: return None
 
-        seek(dlen, 1)
+        if dlen: seek(dlen, 1)
         v=read(vlen)
-        if version != v: 
-            seek(-dlen-vlen, 1)
-            return read(dlen), h[19:]
+        if version != v:
+            if dlen:
+                seek(-dlen-vlen, 1)
+                return read(dlen), h[19:]
+            else: None
 
         dlen=unpack(">i", read(4))[0]
         return read(dlen), read(8)
@@ -262,7 +269,8 @@
         if version:
             # We need to find and include non-version data
             p=self._get(oid, None)
-            if p is None: return None
+            if p is None:
+                return self.store(oid, '', '', version, data, serial)
             f=self._f[p < 0]
             ap=abs(p)
             seek=f.seek
@@ -271,13 +279,17 @@
             h=read(27)
             if len(h)==27 and h[8] in 'nv' and h[:8]==oid:
                 tlen, vlen, dlen = unpack(">iHi", h[9:19])
-            else: tlen=-1
+            else:
+                return self.store(oid, '', '', version, data, serial)
+                
             if tlen <= 0 or vlen < 0 or dlen <= 0 or vlen+dlen > tlen:
-                del self._index[oid]
-                return None
+                return self.store(oid, '', '', version, data, serial)
 
-            p=read(dlen)
-            s=h[19:]
+            if dlen:
+                p=read(dlen)
+                s=h[19:]
+            else: 
+                return self.store(oid, '', '', version, data, serial)
 
             self.store(oid, p, s, version, data, serial)
         else:
@@ -296,7 +308,7 @@
         if len(h)==27 and h[8] in 'nv' and h[:8]==oid:
             tlen, vlen, dlen = unpack(">iHi", h[9:19])
         else: tlen=-1
-        if tlen <= 0 or vlen < 0 or dlen <= 0 or vlen+dlen > tlen:
+        if tlen <= 0 or vlen < 0 or dlen < 0 or vlen+dlen > tlen:
             del self._index[oid]
             return None
 
@@ -318,6 +330,9 @@
         
 
     def store(self, oid, p, s, version, pv, sv):
+        if not s:
+            p=''
+            s='\0\0\0\0\0\0\0\0'
         tlen=31+len(p)
         if version:
             tlen=tlen+len(version)+12+len(pv)
@@ -332,11 +347,14 @@
         stlen=pack(">I",tlen)
         write=f.write
         write(oid+'v'+stlen+pack(">HI", vlen, len(p))+s)
-        write(p)
+        if p: write(p)
         if version:
+            write(version)
             write(pack(">I", len(pv)))
             write(pv)
-            write(sv+stlen)
+            write(sv)
+
+        write(stlen)
 
         if current: self._index[oid]=-pos
         else: self._index[oid]=pos
@@ -357,7 +375,7 @@
         if len(h)==27 and h[8] in 'vni':
             tlen, vlen, dlen = unpack(">iHi", h[9:19])
         else: tlen=-1
-        if tlen <= 0 or vlen < 0 or dlen <= 0 or vlen+dlen > tlen:
+        if tlen <= 0 or vlen < 0 or dlen < 0 or vlen+dlen > tlen:
             break
 
         oid=h[:8]
@@ -379,7 +397,7 @@
             serial[oid]=h[-8:], vs
         else:
             if serial.has_key(oid):
-                # We has a record for this oid, but it was invalidated!
+                # We have a record for this oid, but it was invalidated!
                 del serial[oid]
                 del index[oid]
             


=== Packages/ZEO/ClientStorage.py 1.11 => 1.11.2.1 ===
         self._connected=0
         thread.start_new_thread(self._call.connect,(0,))
+        try: self._commit_lock_release()
+        except: pass
+
 
     def becomeAsync(self, map):
         self._lock_acquire()
@@ -229,7 +232,11 @@
         if transaction is not self._transaction:
             raise POSException.StorageTransactionError(self, transaction)
         self._lock_acquire()
-        try: return self._call('abortVersion', src, self._serial)
+        try:
+            oids=self._call('abortVersion', src, self._serial)
+            invalidate=self._cache.invalidate
+            for oid in oids: invalidate(oid, src)
+            return oids
         finally: self._lock_release()
 
     def close(self):
@@ -241,7 +248,17 @@
         if transaction is not self._transaction:
             raise POSException.StorageTransactionError(self, transaction)
         self._lock_acquire()
-        try: return self._call('commitVersion', src, dest, self._serial)
+        try:
+            oids=self._call('commitVersion', src, dest, self._serial)
+            invalidate=self._cache.invalidate
+            if dest:
+                # just invalidate our version data
+                for oid in oids: invalidate(oid, src)
+            else:
+                # dest is '', so invalidate version and non-version
+                for oid in oids: invalidate(oid, dest)
+                
+            return oids
         finally: self._lock_release()
 
     def getName(self):
@@ -265,11 +282,12 @@
         self._lock_acquire()
         try:
             p = self._cache.load(oid, version)
-            if p is not None: return p
+            if p: return p
             p, s, v, pv, sv = self._call('zeoLoad', oid)
             self._cache.store(oid, p, s, v, pv, sv)
             if not v or not version or version != v:
-                return p, s
+                if s: return p, s
+                raise KeyError, oid # no non-version data for this
             return pv, sv
         finally: self._lock_release()
                     
@@ -465,5 +483,5 @@
 
     def versions(self, max=None):
         self._lock_acquire()
-        try: return self._call('versionEmpty', max)
+        try: return self._call('versions', max)
         finally: self._lock_release()


=== Packages/ZEO/StorageServer.py 1.11 => 1.11.2.1 ===
         return storage, storage_id
 
+    def register_storage(self, connection, storage, storage_method):
+        i=id(storage)        
+        connections=self.__get_connections(i, None)
+        if connections is None: self.__connections[i]=[connection]
+        else: connections.append(connection)
+        connection.setStorage(i, storage, storage_method)
+
+    def register_connection(self, connection, storage_id):
+        validator=self.__storages.get(storage_id, None)
+        if validator is None:
+            LOG('ZEO Server', PROBLEM, 'Invalid storage, %s' % storage_id)
+            connection.close()
+            return
+
+        try:
+            validator.validate(storage_id, self, connection)
+        except:
+            if hasattr(validator, 'tpc_begin'):
+                # This is a storage, not a validator.  Just register
+                # it.
+                self.register_storage(connection, validator, storage_method)
+                return
+            LOG('ZEO Server', ERROR, 'Invalid validator for %s' % storage_id)
+            connection.close()
+            return
+
     def unregister_connection(self, connection, storage_id):
         
         connections=self.__get_connections(storage_id, None)
@@ -185,16 +211,23 @@
 
     log=log_info
 
+read_storage_methods={}
+for n in (
+    'get_info', 'history', 'load', 'loadSerial', 'modifiedInVersion',
+    'tpc_abort', 'tpc_begin', 'tpc_begin_sync', 'tpc_finish',
+    'undoLog', 'undoInfo', 'versionEmpty', 'versions', 'vote',
+    'zeoLoad', 'zeoVerify', 'beginZeoVerify', 'endZeoVerify',
+    ):
+    read_storage_methods[n]=1
+read_storage_method=read_storage_methods.has_key
+
 storage_methods={}
 for n in (
-    'get_info', 'abortVersion', 'commitVersion',
-    'history', 'load', 'loadSerial',
-    'modifiedInVersion', 'new_oid', 'new_oids', 'pack', 'store',
-    'storea', 'tpc_abort', 'tpc_begin', 'tpc_begin_sync',
-    'tpc_finish', 'undo', 'undoLog', 'undoInfo', 'versionEmpty',
-    'vote', 'zeoLoad', 'zeoVerify', 'beginZeoVerify', 'endZeoVerify',
+    'abortVersion', 'commitVersion', 'new_oid', 'new_oids', 'pack',
+    'store', 'storea', 'undo',
     ):
     storage_methods[n]=1
+storage_methods.update(read_storage_methods)
 storage_method=storage_methods.has_key
 
 def find_global(module, name,
@@ -238,6 +271,10 @@
         self.__closed=1
         SizedMessageAsyncConnection.close(self)
 
+    def setStorage(i, s, m):
+        self.__storage, self.__storage_id = s, i
+        self.storage_method=m
+
     def message_input(self, message,
                       dump=dump, Unpickler=Unpickler, StringIO=StringIO,
                       None=None):
@@ -247,8 +284,7 @@
             blather('message_input', m)
 
         if self.__storage is None:
-            self.__storage, self.__storage_id = (
-                self.__server.register_connection(self, message))
+            self.__server.register_connection(self, message)
             return
             
         rt='R'
@@ -262,10 +298,10 @@
             name, args = args[0], args[1:]
             if __debug__:
                 m=`tuple(args)`
-                if len(m) > 60: m=m[:60]+' ...'
+                if len(m) > 90: m=m[:90]+' ...'
                 blather('call: %s%s' % (name, m))
                 
-            if not storage_method(name):
+            if not self.storage_method(name):
                 raise 'Invalid Method Name', name
             if hasattr(self, name):
                 r=apply(getattr(self, name), args)
@@ -283,7 +319,14 @@
             if len(m) > 60: m=m[:60]+' ...'
             blather('%s: %s' % (rt, m))
             
-        r=dump(r,1)
+        try: r=dump(r,1)
+        except:
+            # Ugh, must be an unpicklable exception
+            r=StorageServerError("Couldn't pickle result %s" % `r`)
+            dump('',1) # clear pickler
+            r=dump(r,1)
+            rt='E'
+            
         self.message_output(rt+r)
 
     def get_info(self):
@@ -301,8 +344,16 @@
         v=storage.modifiedInVersion(oid)
         if v: pv, sv = storage.load(oid, v)
         else: pv=sv=None
-        p, s = storage.load(oid,'')
+        try:
+            p, s = storage.load(oid,'')
+        except KeyError:
+            if sv:
+                # Created in version, no non-version data
+                p=s=None
+            else:
+                raise
         return p, s, v, pv, sv
+            
 
     def beginZeoVerify(self):
         self.message_output('bN.')            
@@ -337,6 +388,26 @@
     def _pack(self, t):
         self.__storage.pack(t, referencesf)
         self.message_output('S'+dump(self.get_info(), 1))
+
+    def abortVersion(self, src, id):
+        t=self._transaction
+        if t is None or id != t.id:
+            raise POSException.StorageTransactionError(self, id)
+        oids=self.__storage.abortVersion(src, t)
+        a=self.__invalidated.append
+        for oid in oids: a((oid,src))
+        return oids
+
+    def commitVersion(self, src, dest, id):
+        t=self._transaction
+        if t is None or id != t.id:
+            raise POSException.StorageTransactionError(self, id)
+        oids=self.__storage.commitVersion(src, dest, t)
+        a=self.__invalidated.append
+        for oid in oids:
+            a((oid,dest))
+            if dest: a((oid,src))
+        return oids
 
     def store(self, oid, serial, data, version, id):
         t=self._transaction