Commit 1c40e3c4 authored by Jeremy Hylton's avatar Jeremy Hylton

Merge ZODB3-auth-branch and bump a few version numbers.

After the merge, I made several Python 2.1 compatibility changes for
the auth code.
parent 2f678b3e
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
static char cPersistence_doc_string[] = static char cPersistence_doc_string[] =
"Defines Persistent mixin class for persistent objects.\n" "Defines Persistent mixin class for persistent objects.\n"
"\n" "\n"
"$Id: cPersistence.c,v 1.71 2003/05/23 21:32:38 jeremy Exp $\n"; "$Id: cPersistence.c,v 1.72 2003/05/30 19:20:55 jeremy Exp $\n";
#include "cPersistence.h" #include "cPersistence.h"
......
...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them. ...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them.
static char cPickleCache_doc_string[] = static char cPickleCache_doc_string[] =
"Defines the PickleCache used by ZODB Connection objects.\n" "Defines the PickleCache used by ZODB Connection objects.\n"
"\n" "\n"
"$Id: cPickleCache.c,v 1.84 2003/05/13 23:15:16 jeremy Exp $\n"; "$Id: cPickleCache.c,v 1.85 2003/05/30 19:20:55 jeremy Exp $\n";
#define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;} #define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;}
#define UNLESS(E) if(!(E)) #define UNLESS(E) if(!(E))
......
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -28,13 +28,14 @@ import types ...@@ -28,13 +28,14 @@ import types
from ZEO import ClientCache, ServerStub from ZEO import ClientCache, ServerStub
from ZEO.TransactionBuffer import TransactionBuffer from ZEO.TransactionBuffer import TransactionBuffer
from ZEO.Exceptions \ from ZEO.Exceptions import ClientStorageError, UnrecognizedResult, \
import ClientStorageError, UnrecognizedResult, ClientDisconnected ClientDisconnected, AuthError
from ZEO.auth import get_module
from ZEO.zrpc.client import ConnectionManager from ZEO.zrpc.client import ConnectionManager
from ZODB import POSException from ZODB import POSException
from ZODB.TimeStamp import TimeStamp from ZODB.TimeStamp import TimeStamp
from zLOG import LOG, PROBLEM, INFO, BLATHER from zLOG import LOG, PROBLEM, INFO, BLATHER, ERROR
def log2(type, msg, subsys="ZCS:%d" % os.getpid()): def log2(type, msg, subsys="ZCS:%d" % os.getpid()):
LOG(subsys, type, msg) LOG(subsys, type, msg)
...@@ -99,8 +100,8 @@ class ClientStorage: ...@@ -99,8 +100,8 @@ class ClientStorage:
min_disconnect_poll=5, max_disconnect_poll=300, min_disconnect_poll=5, max_disconnect_poll=300,
wait_for_server_on_startup=None, # deprecated alias for wait wait_for_server_on_startup=None, # deprecated alias for wait
wait=None, # defaults to 1 wait=None, # defaults to 1
read_only=0, read_only_fallback=0): read_only=0, read_only_fallback=0,
username='', password='', realm=None):
"""ClientStorage constructor. """ClientStorage constructor.
This is typically invoked from a custom_zodb.py file. This is typically invoked from a custom_zodb.py file.
...@@ -159,6 +160,17 @@ class ClientStorage: ...@@ -159,6 +160,17 @@ class ClientStorage:
writable storages are available. Defaults to false. At writable storages are available. Defaults to false. At
most one of read_only and read_only_fallback should be most one of read_only and read_only_fallback should be
true. true.
username -- string with username to be used when authenticating.
These only need to be provided if you are connecting to an
authenticated server storage.
password -- string with plaintext password to be used
when authenticated.
Note that the authentication protocol is defined by the server
and is detected by the ClientStorage upon connecting (see
testConnection() and doAuth() for details).
""" """
log2(INFO, "%s (pid=%d) created %s/%s for storage: %r" % log2(INFO, "%s (pid=%d) created %s/%s for storage: %r" %
...@@ -217,6 +229,9 @@ class ClientStorage: ...@@ -217,6 +229,9 @@ class ClientStorage:
self._conn_is_read_only = 0 self._conn_is_read_only = 0
self._storage = storage self._storage = storage
self._read_only_fallback = read_only_fallback self._read_only_fallback = read_only_fallback
self._username = username
self._password = password
self._realm = realm
# _server_addr is used by sortKey() # _server_addr is used by sortKey()
self._server_addr = None self._server_addr = None
self._tfile = None self._tfile = None
...@@ -347,6 +362,29 @@ class ClientStorage: ...@@ -347,6 +362,29 @@ class ClientStorage:
if cn is not None: if cn is not None:
cn.pending() cn.pending()
def doAuth(self, protocol, stub):
if not (self._username and self._password):
raise AuthError, "empty username or password"
module = get_module(protocol)
if not module:
log2(PROBLEM, "%s: no such an auth protocol: %s" %
(self.__class__.__name__, protocol))
return
storage_class, client, db_class = module
if not client:
log2(PROBLEM,
"%s: %s isn't a valid protocol, must have a Client class" %
(self.__class__.__name__, protocol))
raise AuthError, "invalid protocol"
c = client(stub)
# Initiate authentication, returns boolean specifying whether OK
return c.start(self._username, self._realm, self._password)
def testConnection(self, conn): def testConnection(self, conn):
"""Internal: test the given connection. """Internal: test the given connection.
...@@ -372,6 +410,16 @@ class ClientStorage: ...@@ -372,6 +410,16 @@ class ClientStorage:
# XXX Check the protocol version here? # XXX Check the protocol version here?
self._conn_is_read_only = 0 self._conn_is_read_only = 0
stub = self.StorageServerStubClass(conn) stub = self.StorageServerStubClass(conn)
auth = stub.getAuthProtocol()
log2(INFO, "Server authentication protocol %r" % auth)
if auth:
if self.doAuth(auth, stub):
log2(INFO, "Client authentication successful")
else:
log2(ERROR, "Authentication failed")
raise AuthError, "Authentication failed"
try: try:
stub.register(str(self._storage), self._is_read_only) stub.register(str(self._storage), self._is_read_only)
return 1 return 1
...@@ -416,14 +464,14 @@ class ClientStorage: ...@@ -416,14 +464,14 @@ class ClientStorage:
stub = self.StorageServerStubClass(conn) stub = self.StorageServerStubClass(conn)
self._oids = [] self._oids = []
self._info.update(stub.get_info()) self._info.update(stub.get_info())
self._handle_extensions()
self.verify_cache(stub) self.verify_cache(stub)
if not conn.is_async(): if not conn.is_async():
log2(INFO, "Waiting for cache verification to finish") log2(INFO, "Waiting for cache verification to finish")
self._wait_sync() self._wait_sync()
self._handle_extensions()
def _handle_extensions(self): def _handle_extensions(self):
for name in self.getExtensionMethods(): for name in self.getExtensionMethods().keys():
if not hasattr(self, name): if not hasattr(self, name):
setattr(self, name, self._server.extensionMethod(name)) setattr(self, name, self._server.extensionMethod(name))
......
...@@ -24,3 +24,5 @@ class UnrecognizedResult(ClientStorageError): ...@@ -24,3 +24,5 @@ class UnrecognizedResult(ClientStorageError):
class ClientDisconnected(ClientStorageError): class ClientDisconnected(ClientStorageError):
"""The database storage is disconnected from the storage.""" """The database storage is disconnected from the storage."""
class AuthError(StorageError):
"""The client provided invalid authentication credentials."""
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -45,6 +45,9 @@ class StorageServer: ...@@ -45,6 +45,9 @@ class StorageServer:
def get_info(self): def get_info(self):
return self.rpc.call('get_info') return self.rpc.call('get_info')
def getAuthProtocol(self):
return self.rpc.call('getAuthProtocol')
def lastTransaction(self): def lastTransaction(self):
# Not in protocol version 2.0.0; see __init__() # Not in protocol version 2.0.0; see __init__()
return self.rpc.call('lastTransaction') return self.rpc.call('lastTransaction')
...@@ -147,5 +150,6 @@ class ExtensionMethodWrapper: ...@@ -147,5 +150,6 @@ class ExtensionMethodWrapper:
def __init__(self, rpc, name): def __init__(self, rpc, name):
self.rpc = rpc self.rpc = rpc
self.name = name self.name = name
def call(self, *a, **kwa): def call(self, *a, **kwa):
return apply(self.rpc.call, (self.name,)+a, kwa) return self.rpc.call(self.name, *a, **kwa)
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -35,6 +35,7 @@ from ZEO.monitor import StorageStats, StatsServer ...@@ -35,6 +35,7 @@ from ZEO.monitor import StorageStats, StatsServer
from ZEO.zrpc.server import Dispatcher from ZEO.zrpc.server import Dispatcher
from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay from ZEO.zrpc.connection import ManagedServerConnection, Delay, MTDelay
from ZEO.zrpc.trigger import trigger from ZEO.zrpc.trigger import trigger
from ZEO.Exceptions import AuthError
import zLOG import zLOG
from ZODB.ConflictResolution import ResolvedSerial from ZODB.ConflictResolution import ResolvedSerial
...@@ -62,10 +63,13 @@ class ZEOStorage: ...@@ -62,10 +63,13 @@ class ZEOStorage:
"""Proxy to underlying storage for a single remote client.""" """Proxy to underlying storage for a single remote client."""
# Classes we instantiate. A subclass might override. # Classes we instantiate. A subclass might override.
ClientStorageStubClass = ClientStub.ClientStorage ClientStorageStubClass = ClientStub.ClientStorage
def __init__(self, server, read_only=0): # A list of extension methods. A subclass with extra methods
# should override.
extensions = []
def __init__(self, server, read_only=0, auth_realm=None):
self.server = server self.server = server
# timeout and stats will be initialized in register() # timeout and stats will be initialized in register()
self.timeout = None self.timeout = None
...@@ -79,6 +83,21 @@ class ZEOStorage: ...@@ -79,6 +83,21 @@ class ZEOStorage:
self.locked = 0 self.locked = 0
self.verifying = 0 self.verifying = 0
self.log_label = _label self.log_label = _label
self.authenticated = 0
self.auth_realm = auth_realm
# The authentication protocol may define extra methods.
self._extensions = {}
for func in self.extensions:
self._extensions[func.func_name] = None
def finish_auth(self, authenticated):
if not self.auth_realm:
return 1
self.authenticated = authenticated
return authenticated
def set_database(self, database):
self.database = database
def notifyConnected(self, conn): def notifyConnected(self, conn):
self.connection = conn # For restart_other() below self.connection = conn # For restart_other() below
...@@ -133,8 +152,10 @@ class ZEOStorage: ...@@ -133,8 +152,10 @@ class ZEOStorage:
# can be removed # can be removed
pass pass
else: else:
for name in fn().keys(): d = fn()
if not hasattr(self,name): self._extensions.update(d)
for name in d.keys():
assert not hasattr(self, name)
setattr(self, name, getattr(self.storage, name)) setattr(self, name, getattr(self.storage, name))
self.lastTransaction = self.storage.lastTransaction self.lastTransaction = self.storage.lastTransaction
...@@ -159,11 +180,25 @@ class ZEOStorage: ...@@ -159,11 +180,25 @@ class ZEOStorage:
return 0 return 0
return 1 return 1
def getAuthProtocol(self):
"""Return string specifying name of authentication module to use.
The module name should be auth_%s where %s is auth_protocol."""
protocol = self.server.auth_protocol
if not protocol or protocol == 'none':
return None
return protocol
def register(self, storage_id, read_only): def register(self, storage_id, read_only):
"""Select the storage that this client will use """Select the storage that this client will use
This method must be the first one called by the client. This method must be the first one called by the client.
For authenticated storages this method will be called by the client
immediately after authentication is finished.
""" """
if self.auth_realm and not self.authenticated:
raise AuthError, "Client was never authenticated with server!"
if self.storage is not None: if self.storage is not None:
self.log("duplicate register() call") self.log("duplicate register() call")
raise ValueError, "duplicate register() call" raise ValueError, "duplicate register() call"
...@@ -199,12 +234,7 @@ class ZEOStorage: ...@@ -199,12 +234,7 @@ class ZEOStorage:
} }
def getExtensionMethods(self): def getExtensionMethods(self):
try: return self._extensions
e = self.storage.getExtensionMethods
except AttributeError:
return {}
else:
return e()
def zeoLoad(self, oid): def zeoLoad(self, oid):
self.stats.loads += 1 self.stats.loads += 1
...@@ -579,7 +609,10 @@ class StorageServer: ...@@ -579,7 +609,10 @@ class StorageServer:
def __init__(self, addr, storages, read_only=0, def __init__(self, addr, storages, read_only=0,
invalidation_queue_size=100, invalidation_queue_size=100,
transaction_timeout=None, transaction_timeout=None,
monitor_address=None): monitor_address=None,
auth_protocol=None,
auth_filename=None,
auth_realm=None):
"""StorageServer constructor. """StorageServer constructor.
This is typically invoked from the start.py script. This is typically invoked from the start.py script.
...@@ -621,6 +654,21 @@ class StorageServer: ...@@ -621,6 +654,21 @@ class StorageServer:
should listen. If specified, a monitor server is started. should listen. If specified, a monitor server is started.
The monitor server provides server statistics in a simple The monitor server provides server statistics in a simple
text format. text format.
auth_protocol -- The name of the authentication protocol to use.
Examples are "digest" and "srp".
auth_filename -- The name of the password database filename.
It should be in a format compatible with the authentication
protocol used; for instance, "sha" and "srp" require different
formats.
Note that to implement an authentication protocol, a server
and client authentication mechanism must be implemented in a
auth_* module, which should be stored inside the "auth"
subdirectory. This module may also define a DatabaseClass
variable that should indicate what database should be used
by the authenticator.
""" """
self.addr = addr self.addr = addr
...@@ -635,6 +683,12 @@ class StorageServer: ...@@ -635,6 +683,12 @@ class StorageServer:
for s in storages.values(): for s in storages.values():
s._waiting = [] s._waiting = []
self.read_only = read_only self.read_only = read_only
self.auth_protocol = auth_protocol
self.auth_filename = auth_filename
self.auth_realm = auth_realm
self.database = None
if auth_protocol:
self._setup_auth(auth_protocol)
# A list of at most invalidation_queue_size invalidations # A list of at most invalidation_queue_size invalidations
self.invq = [] self.invq = []
self.invq_bound = invalidation_queue_size self.invq_bound = invalidation_queue_size
...@@ -657,6 +711,40 @@ class StorageServer: ...@@ -657,6 +711,40 @@ class StorageServer:
else: else:
self.monitor = None self.monitor = None
def _setup_auth(self, protocol):
# Can't be done in global scope, because of cyclic references
from ZEO.auth import get_module
name = self.__class__.__name__
module = get_module(protocol)
if not module:
log("%s: no such an auth protocol: %s" % (name, protocol))
return
storage_class, client, db_class = module
if not storage_class or not issubclass(storage_class, ZEOStorage):
log(("%s: %s isn't a valid protocol, must have a StorageClass" %
(name, protocol)))
self.auth_protocol = None
return
self.ZEOStorageClass = storage_class
log("%s: using auth protocol: %s" % (name, protocol))
# We create a Database instance here for use with the authenticator
# modules. Having one instance allows it to be shared between multiple
# storages, avoiding the need to bloat each with a new authenticator
# Database that would contain the same info, and also avoiding any
# possibly synchronization issues between them.
self.database = db_class(self.auth_filename)
if self.database.realm != self.auth_realm:
raise ValueError("password database realm %r "
"does not match storage realm %r"
% (self.database.realm, self.auth_realm))
def new_connection(self, sock, addr): def new_connection(self, sock, addr):
"""Internal: factory to create a new connection. """Internal: factory to create a new connection.
...@@ -664,8 +752,14 @@ class StorageServer: ...@@ -664,8 +752,14 @@ class StorageServer:
whenever accept() returns a socket for a new incoming whenever accept() returns a socket for a new incoming
connection. connection.
""" """
z = self.ZEOStorageClass(self, self.read_only) if self.auth_protocol and self.database:
c = self.ManagedServerConnectionClass(sock, addr, z, self) zstorage = self.ZEOStorageClass(self, self.read_only,
auth_realm=self.auth_realm)
zstorage.set_database(self.database)
else:
zstorage = self.ZEOStorageClass(self, self.read_only)
c = self.ManagedServerConnectionClass(sock, addr, zstorage, self)
log("new connection %s: %s" % (addr, `c`)) log("new connection %s: %s" % (addr, `c`))
return c return c
......
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
_auth_modules = {}
def get_module(name):
if name == 'sha':
from auth_sha import StorageClass, SHAClient, Database
return StorageClass, SHAClient, Database
elif name == 'digest':
from auth_digest import StorageClass, DigestClient, DigestDatabase
return StorageClass, DigestClient, DigestDatabase
else:
return _auth_modules.get(name)
def register_module(name, storage_class, client, db):
if _auth_modules.has_key(name):
raise TypeError, "%s is already registred" % name
_auth_modules[name] = storage_class, client, db
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
"""Digest authentication for ZEO
This authentication mechanism follows the design of HTTP digest
authentication (RFC 2069). It is a simple challenge-response protocol
that does not send passwords in the clear, but does not offer strong
security. The RFC discusses many of the limitations of this kind of
protocol.
Guard the password database as if it contained plaintext passwords.
It stores the hash of a username and password. This does not expose
the plaintext password, but it is sensitive nonetheless. An attacker
with the hash can impersonate the real user. This is a limitation of
the simple digest scheme.
HTTP is a stateless protocol, and ZEO is a stateful protocol. The
security requirements are quite different as a result. The HTTP
protocol uses a nonce as a challenge. The ZEO protocol requires a
separate session key that is used for message authentication. We
generate a second nonce for this purpose; the hash of nonce and
user/realm/password is used as the session key. XXX I'm not sure if
this is a sound approach; SRP would be preferred.
"""
import base64
import os
import sha
import struct
import time
from ZEO.auth.base import Database, Client
from ZEO.StorageServer import ZEOStorage
from ZEO.Exceptions import AuthError
def get_random_bytes(n=8):
if os.path.exists("/dev/urandom"):
f = open("/dev/urandom")
s = f.read(n)
f.close()
else:
L = [chr(random.randint(0, 255)) for i in range(n)]
s = "".join(L)
return s
def hexdigest(s):
return sha.new(s).hexdigest()
class DigestDatabase(Database):
def __init__(self, filename, realm=None):
Database.__init__(self, filename, realm)
# Initialize a key used to build the nonce for a challenge.
# We need one key for the lifetime of the server, so it
# is convenient to store in on the database.
self.noncekey = get_random_bytes(8)
def _store_password(self, username, password):
dig = hexdigest("%s:%s:%s" % (username, self.realm, password))
self._users[username] = dig
def session_key(h_up, nonce):
# The hash itself is a bit too short to be a session key.
# HMAC wants a 64-byte key. We don't want to use h_up
# directly because it would never change over time. Instead
# use the hash plus part of h_up.
return sha.new("%s:%s" % (h_up, nonce)).digest() + h_up[:44]
class StorageClass(ZEOStorage):
def set_database(self, database):
assert isinstance(database, DigestDatabase)
self.database = database
self.noncekey = database.noncekey
def _get_time(self):
# Return a string representing the current time.
t = int(time.time())
return struct.pack("i", t)
def _get_nonce(self):
# RFC 2069 recommends a nonce of the form
# H(client-IP ":" time-stamp ":" private-key)
dig = sha.sha()
dig.update(str(self.connection.addr))
dig.update(self._get_time())
dig.update(self.noncekey)
return dig.hexdigest()
def auth_get_challenge(self):
"""Return realm, challenge, and nonce."""
self._challenge = self._get_nonce()
self._key_nonce = self._get_nonce()
return self.auth_realm, self._challenge, self._key_nonce
def auth_response(self, resp):
# verify client response
user, challenge, response = resp
# Since zrpc is a stateful protocol, we just store the nonce
# we sent to the client. It will need to generate a new
# nonce for a new connection anyway.
if self._challenge != challenge:
raise ValueError, "invalid challenge"
# lookup user in database
h_up = self.database.get_password(user)
# regeneration resp from user, password, and nonce
check = hexdigest("%s:%s" % (h_up, challenge))
if check == response:
self.connection.setSessionKey(session_key(h_up, self._key_nonce))
return self.finish_auth(check == response)
extensions = [auth_get_challenge, auth_response]
class DigestClient(Client):
extensions = ["auth_get_challenge", "auth_response"]
def start(self, username, realm, password):
_realm, challenge, nonce = self.stub.auth_get_challenge()
if _realm != realm:
raise AuthError("expected realm %r, got realm %r"
% (_realm, realm))
h_up = hexdigest("%s:%s:%s" % (username, realm, password))
resp_dig = hexdigest("%s:%s" % (h_up, challenge))
result = self.stub.auth_response((username, challenge, resp_dig))
if result:
return session_key(h_up, nonce)
else:
return None
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
"""Base classes for defining an authentication protocol.
Database -- abstract base class for password database
Client -- abstract base class for authentication client
"""
import os
import sha
class Client:
# Subclass should override to list the names of methods that
# will be called on the server.
extensions = []
def __init__(self, stub):
self.stub = stub
for m in self.extensions:
setattr(self.stub, m, self.stub.extensionMethod(m))
def sort(L):
"""Sort a list in-place and return it."""
L.sort()
return L
class Database:
"""Abstracts a password database.
This class is used both in the authentication process (via
get_password()) and by client scripts that manage the password
database file.
The password file is a simple, colon-separated text file mapping
usernames to password hashes. The hashes are SHA hex digests
produced from the password string.
"""
def __init__(self, filename, realm=None):
"""Creates a new Database
filename: a string containing the full pathname of
the password database file. Must be readable by the user
running ZEO. Must be writeable by any client script that
accesses the database.
realm: the realm name (a string)
"""
self._users = {}
self.filename = filename
self.realm = realm
self.load()
def save(self, fd=None):
filename = self.filename
if not fd:
fd = open(filename, 'w')
if self.realm:
print >> fd, "realm", self.realm
for username in sort(self._users.keys()):
print >> fd, "%s: %s" % (username, self._users[username])
def load(self):
filename = self.filename
if not filename:
return
if not os.path.exists(filename):
return
fd = open(filename)
L = fd.readlines()
if L[0].startswith("realm "):
line = L.pop(0).strip()
self.realm = line[len("realm "):]
for line in L:
username, hash = line.strip().split(":", 1)
self._users[username] = hash.strip()
def _store_password(self, username, password):
self._users[username] = self.hash(password)
def get_password(self, username):
"""Returns password hash for specified username.
Callers must check for LookupError, which is raised in
the case of a non-existent user specified."""
if not self._users.has_key(username):
raise LookupError, "No such user: %s" % username
return self._users[username]
def hash(self, s):
return sha.new(s).hexdigest()
def add_user(self, username, password):
if self._users.has_key(username):
raise LookupError, "User %s does already exist" % username
self._store_password(username, password)
def del_user(self, username):
if not self._users.has_key(username):
raise LookupError, "No such user: %s" % username
del self._users[username]
def change_password(self, username, password):
if not self._users.has_key(username):
raise LookupError, "No such user: %s" % username
self._store_password(username, password)
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
Implements the HMAC algorithm as described by RFC 2104.
"""
def _strxor(s1, s2):
"""Utility method. XOR the two strings s1 and s2 (must have same length).
"""
return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))
# The size of the digests returned by HMAC depends on the underlying
# hashing module used.
digest_size = None
class HMAC:
"""RFC2104 HMAC class.
This supports the API for Cryptographic Hash Functions (PEP 247).
"""
def __init__(self, key, msg = None, digestmod = None):
"""Create a new HMAC object.
key: key for the keyed hash object.
msg: Initial input for the hash, if provided.
digestmod: A module supporting PEP 247. Defaults to the md5 module.
"""
if digestmod is None:
import md5
digestmod = md5
self.digestmod = digestmod
self.outer = digestmod.new()
self.inner = digestmod.new()
self.digest_size = digestmod.digest_size
blocksize = 64
ipad = "\x36" * blocksize
opad = "\x5C" * blocksize
if len(key) > blocksize:
key = digestmod.new(key).digest()
key = key + chr(0) * (blocksize - len(key))
self.outer.update(_strxor(key, opad))
self.inner.update(_strxor(key, ipad))
if msg is not None:
self.update(msg)
## def clear(self):
## raise NotImplementedError, "clear() method not available in HMAC."
def update(self, msg):
"""Update this hashing object with the string msg.
"""
self.inner.update(msg)
def copy(self):
"""Return a separate copy of this hashing object.
An update to this copy won't affect the original object.
"""
other = HMAC("")
other.digestmod = self.digestmod
other.inner = self.inner.copy()
other.outer = self.outer.copy()
return other
def digest(self):
"""Return the hash value of this hashing object.
This returns a string containing 8-bit data. The object is
not altered in any way by this function; you can continue
updating the object after calling this function.
"""
h = self.outer.copy()
h.update(self.inner.digest())
return h.digest()
def hexdigest(self):
"""Like digest(), but returns a string of hexadecimal digits instead.
"""
return "".join([hex(ord(x))[2:].zfill(2)
for x in tuple(self.digest())])
def new(key, msg = None, digestmod = None):
"""Create a new hashing object and return it.
key: The starting key for the hash.
msg: if available, will immediately be hashed into the object's starting
state.
You can now feed arbitrary strings into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
"""
return HMAC(key, msg, digestmod)
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<sectiontype name="zeo"> <sectiontype name="zeo">
<description> <description>
The content of a "ZEO" section describe operational parameters The content of a ZEO section describe operational parameters
of a ZEO server except for the storage(s) to be served. of a ZEO server except for the storage(s) to be served.
</description> </description>
...@@ -71,6 +71,28 @@ ...@@ -71,6 +71,28 @@
</description> </description>
</key> </key>
<key name="authentication-protocol" required="no">
<description>
The name of the protocol used for authentication. The
only protocol provided with ZEO is "digest," but extensions
may provide other protocols.
</description>
</key>
<key name="authentication-database" required="no">
<description>
The path of the database containing authentication credentials.
</description>
</key>
<key name="authentication-realm" required="no">
<description>
The authentication realm of the server. Some authentication
schemes use a realm to identify the logic set of usernames
that are accepted by this server.
</description>
</key>
</sectiontype> </sectiontype>
</component> </component>
#!python #!python
############################################################################## ##############################################################################
# #
# Copyright (c) 2001, 2002 Zope Corporation and Contributors. # Copyright (c) 2001, 2002, 2003 Zope Corporation and Contributors.
# All Rights Reserved. # All Rights Reserved.
# #
# This software is subject to the provisions of the Zope Public License, # This software is subject to the provisions of the Zope Public License,
...@@ -89,7 +89,12 @@ class ZEOOptionsMixin: ...@@ -89,7 +89,12 @@ class ZEOOptionsMixin:
"t:", "timeout=", float) "t:", "timeout=", float)
self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=", self.add("monitor_address", "zeo.monitor_address", "m:", "monitor=",
self.handle_monitor_address) self.handle_monitor_address)
self.add('auth_protocol', 'zeo.authentication_protocol',
None, 'auth-protocol=', default=None)
self.add('auth_database', 'zeo.authentication_database',
None, 'auth-database=')
self.add('auth_realm', 'zeo.authentication_realm',
None, 'auth-realm=')
class ZEOOptions(ZDOptions, ZEOOptionsMixin): class ZEOOptions(ZDOptions, ZEOOptionsMixin):
...@@ -189,7 +194,10 @@ class ZEOServer: ...@@ -189,7 +194,10 @@ class ZEOServer:
read_only=self.options.read_only, read_only=self.options.read_only,
invalidation_queue_size=self.options.invalidation_queue_size, invalidation_queue_size=self.options.invalidation_queue_size,
transaction_timeout=self.options.transaction_timeout, transaction_timeout=self.options.transaction_timeout,
monitor_address=self.options.monitor_address) monitor_address=self.options.monitor_address,
auth_protocol=self.options.auth_protocol,
auth_database=self.options.auth_database,
auth_realm=self.options.auth_realm)
def loop_forever(self): def loop_forever(self):
import ThreadedAsync.LoopCallback import ThreadedAsync.LoopCallback
......
...@@ -41,6 +41,8 @@ from ZODB.tests.StorageTestBase import handle_all_serials, ZERO ...@@ -41,6 +41,8 @@ from ZODB.tests.StorageTestBase import handle_all_serials, ZERO
class TestClientStorage(ClientStorage): class TestClientStorage(ClientStorage):
test_connection = 0
def verify_cache(self, stub): def verify_cache(self, stub):
self.end_verify = threading.Event() self.end_verify = threading.Event()
self.verify_result = ClientStorage.verify_cache(self, stub) self.verify_result = ClientStorage.verify_cache(self, stub)
...@@ -49,6 +51,12 @@ class TestClientStorage(ClientStorage): ...@@ -49,6 +51,12 @@ class TestClientStorage(ClientStorage):
ClientStorage.endVerify(self) ClientStorage.endVerify(self)
self.end_verify.set() self.end_verify.set()
def testConnection(self, conn):
try:
return ClientStorage.testConnection(self, conn)
finally:
self.test_connection = 1
class DummyDB: class DummyDB:
def invalidate(self, *args, **kwargs): def invalidate(self, *args, **kwargs):
pass pass
...@@ -113,29 +121,26 @@ class CommonSetupTearDown(StorageTestBase): ...@@ -113,29 +121,26 @@ class CommonSetupTearDown(StorageTestBase):
# port+1 is also used, so only draw even port numbers # port+1 is also used, so only draw even port numbers
return 'localhost', random.randrange(25000, 30000, 2) return 'localhost', random.randrange(25000, 30000, 2)
def getConfig(self): def getConfig(self, path, create, read_only):
raise NotImplementedError raise NotImplementedError
def openClientStorage(self, cache='', cache_size=200000, wait=1, def openClientStorage(self, cache='', cache_size=200000, wait=1,
read_only=0, read_only_fallback=0): read_only=0, read_only_fallback=0,
base = TestClientStorage(self.addr, username=None, password=None, realm=None):
storage = TestClientStorage(self.addr,
client=cache, client=cache,
cache_size=cache_size, cache_size=cache_size,
wait=wait, wait=wait,
min_disconnect_poll=0.1, min_disconnect_poll=0.1,
read_only=read_only, read_only=read_only,
read_only_fallback=read_only_fallback) read_only_fallback=read_only_fallback,
storage = base username=username,
password=password,
realm=realm)
storage.registerDB(DummyDB(), None) storage.registerDB(DummyDB(), None)
return storage return storage
def startServer(self, create=1, index=0, read_only=0, ro_svr=0): def getServerConfig(self, addr, ro_svr):
addr = self.addr[index]
zLOG.LOG("testZEO", zLOG.INFO,
"startServer(create=%d, index=%d, read_only=%d) @ %s" %
(create, index, read_only, addr))
path = "%s.%d" % (self.file, index)
sconf = self.getConfig(path, create, read_only)
zconf = forker.ZEOConfig(addr) zconf = forker.ZEOConfig(addr)
if ro_svr: if ro_svr:
zconf.read_only = 1 zconf.read_only = 1
...@@ -145,6 +150,16 @@ class CommonSetupTearDown(StorageTestBase): ...@@ -145,6 +150,16 @@ class CommonSetupTearDown(StorageTestBase):
zconf.invalidation_queue_size = self.invq zconf.invalidation_queue_size = self.invq
if self.timeout: if self.timeout:
zconf.transaction_timeout = self.timeout zconf.transaction_timeout = self.timeout
return zconf
def startServer(self, create=1, index=0, read_only=0, ro_svr=0):
addr = self.addr[index]
zLOG.LOG("testZEO", zLOG.INFO,
"startServer(create=%d, index=%d, read_only=%d) @ %s" %
(create, index, read_only, addr))
path = "%s.%d" % (self.file, index)
sconf = self.getConfig(path, create, read_only)
zconf = self.getServerConfig(addr, ro_svr)
zeoport, adminaddr, pid, path = forker.start_zeo_server(sconf, zconf, zeoport, adminaddr, pid, path = forker.start_zeo_server(sconf, zconf,
addr[1], addr[1],
self.keep) self.keep)
...@@ -449,7 +464,6 @@ class ConnectionTests(CommonSetupTearDown): ...@@ -449,7 +464,6 @@ class ConnectionTests(CommonSetupTearDown):
self._storage = self.openClientStorage() self._storage = self.openClientStorage()
self._dostore() self._dostore()
# Test case for multiple storages participating in a single # Test case for multiple storages participating in a single
# transaction. This is not really a connection test, but it needs # transaction. This is not really a connection test, but it needs
# about the same infrastructure (several storage servers). # about the same infrastructure (several storage servers).
......
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
"""Implements plaintext password authentication. The password is stored in
an SHA hash in the Database. The client sends over the plaintext
password, and the SHA hashing is done on the server side.
This mechanism offers *no network security at all*; the only security
is provided by not storing plaintext passwords on disk. (See the
auth_srp module for a secure mechanism)"""
import sha
from ZEO.StorageServer import ZEOStorage
from ZEO.auth import register_module
from ZEO.auth.base import Client, Database
class StorageClass(ZEOStorage):
def auth(self, username, password):
try:
dbpw = self.database.get_password(username)
except LookupError:
return 0
password = sha.new(password).hexdigest()
return self.finish_auth(dbpw == password)
class PlaintextClient(Client):
extensions = ["auth"]
def start(self, username, realm, password):
return self.stub.auth(username, password)
register_module("plaintext", StorageClass, PlaintextClient, Database)
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
"""Test suite for AuthZEO."""
import os
import tempfile
import time
import unittest
from ThreadedAsync import LoopCallback
from ZEO.ClientStorage import ClientStorage
from ZEO.StorageServer import StorageServer
from ZEO.tests.ConnectionTests import CommonSetupTearDown
from ZODB.FileStorage import FileStorage
from ZODB.tests.StorageTestBase import removefs
class AuthTest(CommonSetupTearDown):
__super_getServerConfig = CommonSetupTearDown.getServerConfig
__super_setUp = CommonSetupTearDown.setUp
__super_tearDown = CommonSetupTearDown.tearDown
realm = None
def setUp(self):
self.pwfile = tempfile.mktemp()
if self.realm:
self.pwdb = self.dbclass(self.pwfile, self.realm)
else:
self.pwdb = self.dbclass(self.pwfile)
self.pwdb.add_user("foo", "bar")
self.pwdb.save()
self.__super_setUp()
def tearDown(self):
self.__super_tearDown()
os.remove(self.pwfile)
def getConfig(self, path, create, read_only):
return "<mappingstorage 1/>"
def getServerConfig(self, addr, ro_svr):
zconf = self.__super_getServerConfig(addr, ro_svr)
zconf.authentication_protocol = self.protocol
zconf.authentication_database = self.pwfile
zconf.authentication_realm = self.realm
return zconf
def wait(self):
for i in range(25):
if self._storage.test_connection:
return
time.sleep(0.1)
self.fail("Timed out waiting for client to authenticate")
def testOK(self):
# Sleep for 0.2 seconds to give the server some time to start up
# seems to be needed before and after creating the storage
self._storage = self.openClientStorage(wait=0, username="foo",
password="bar", realm=self.realm)
self.wait()
self.assert_(self._storage._connection)
self._storage._connection.poll()
self.assert_(self._storage.is_connected())
def testNOK(self):
self._storage = self.openClientStorage(wait=0, username="foo",
password="noogie",
realm=self.realm)
self.wait()
# If the test established a connection, then it failed.
self.failIf(self._storage._connection)
class PlainTextAuth(AuthTest):
import ZEO.tests.auth_plaintext
protocol = "plaintext"
database = "authdb.sha"
dbclass = ZEO.tests.auth_plaintext.Database
realm = "Plaintext Realm"
class DigestAuth(AuthTest):
import ZEO.auth.auth_digest
protocol = "digest"
database = "authdb.digest"
dbclass = ZEO.auth.auth_digest.DigestDatabase
realm = "Digest Realm"
test_classes = [PlainTextAuth, DigestAuth]
def test_suite():
suite = unittest.TestSuite()
for klass in test_classes:
sub = unittest.makeSuite(klass)
suite.addTest(sub)
return suite
if __name__ == "__main__":
unittest.main(defaultTest='test_suite')
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
############################################################################## ##############################################################################
"""Test that the monitor produce sensible results. """Test that the monitor produce sensible results.
$Id: testMonitor.py,v 1.5 2003/05/30 17:40:30 jeremy Exp $ $Id: testMonitor.py,v 1.6 2003/05/30 19:20:56 jeremy Exp $
""" """
import socket import socket
......
...@@ -39,8 +39,7 @@ from ZODB.tests import StorageTestBase, BasicStorage, VersionStorage, \ ...@@ -39,8 +39,7 @@ from ZODB.tests import StorageTestBase, BasicStorage, VersionStorage, \
MTStorage, ReadOnlyStorage MTStorage, ReadOnlyStorage
from ZEO.ClientStorage import ClientStorage from ZEO.ClientStorage import ClientStorage
from ZEO.tests import forker, Cache from ZEO.tests import forker, Cache, CommitLockTests, ThreadTests
from ZEO.tests import CommitLockTests, ThreadTests
class DummyDB: class DummyDB:
def invalidate(self, *args): def invalidate(self, *args):
......
...@@ -26,9 +26,8 @@ import ThreadedAsync.LoopCallback ...@@ -26,9 +26,8 @@ import ThreadedAsync.LoopCallback
import ZConfig.Context import ZConfig.Context
import zLOG import zLOG
import ZEO.StorageServer from ZEO.StorageServer import StorageServer
from ZEO.runzeo import ZEOOptions from ZEO.runzeo import ZEOOptions
from ZODB.config import storageFromURL
def cleanup(storage): def cleanup(storage):
...@@ -152,6 +151,10 @@ def main(): ...@@ -152,6 +151,10 @@ def main():
zo.realize(["-C", configfile]) zo.realize(["-C", configfile])
zeo_port = int(zo.address[1]) zeo_port = int(zo.address[1])
# XXX a hack
if zo.auth_protocol == "plaintext":
import ZEO.tests.auth_plaintext
# Open the config file and let ZConfig parse the data there. Then remove # Open the config file and let ZConfig parse the data there. Then remove
# the config file, otherwise we'll leave turds. # the config file, otherwise we'll leave turds.
# The rest of the args are hostname, portnum # The rest of the args are hostname, portnum
...@@ -163,13 +166,16 @@ def main(): ...@@ -163,13 +166,16 @@ def main():
mon_addr = None mon_addr = None
if zo.monitor_address: if zo.monitor_address:
mon_addr = zo.monitor_address.address mon_addr = zo.monitor_address.address
server = ZEO.StorageServer.StorageServer( server = StorageServer(
zo.address, zo.address,
{"1": storage}, {"1": storage},
read_only=zo.read_only, read_only=zo.read_only,
invalidation_queue_size=zo.invalidation_queue_size, invalidation_queue_size=zo.invalidation_queue_size,
transaction_timeout=zo.transaction_timeout, transaction_timeout=zo.transaction_timeout,
monitor_address=mon_addr) monitor_address=mon_addr,
auth_protocol=zo.auth_protocol,
auth_filename=zo.auth_database,
auth_realm=zo.auth_realm)
try: try:
log(label, 'creating the test server, keep: %s', keep) log(label, 'creating the test server, keep: %s', keep)
......
#!python
##############################################################################
#
# Copyright (c) 2003 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
#
##############################################################################
"""Update a user's authentication tokens for a ZEO server.
usage: python zeopasswd.py [options] username [password]
-C/--configuration URL -- configuration file or URL
-d/--delete -- delete user instead of updating password
"""
import getopt
import getpass
import sys
import ZConfig
import ZEO
def usage(msg):
print msg
print __doc__
sys.exit(2)
def options(args):
"""Password-specific options loaded from regular ZEO config file."""
schema = ZConfig.loadSchema(os.path.join(os.path.dirname(ZEO.__file__),
"schema.xml"))
try:
options, args = getopt.getopt(args, "C:", ["configure="])
except getopt.error, msg:
usage(msg)
config = None
delete = False
for k, v in options:
if k == '-C' or k == '--configure':
config, nil = ZConfig.loadConfig(schema, v)
if k == '-d' or k == '--delete':
delete = True
if config is None:
usage("Must specifiy configuration file")
password = None
if delete:
if not args:
usage("Must specify username to delete")
elif len(args) > 1:
usage("Too many arguments")
username = args[0]
else:
if not args:
usage("Must specify username")
elif len(args) > 2:
usage("Too many arguments")
elif len(args) == 1:
username = args[0]
else:
username, password = args
return config.zeo, delete, username, password
def main(args=None):
options, delete, username, password = options(args)
p = options.authentication_protocol
if p is None:
usage("ZEO configuration does not specify authentication-protocol")
if p == "digest":
from ZEO.auth.auth_digest import DigestDatabase as Database
elif p == "srp":
from ZEO.auth.auth_srp import SRPDatabase as Database
if options.authentication_database is None:
usage("ZEO configuration does not specify authentication-database")
db = Database(options.authentication_database)
if delete:
db.del_user(username)
else:
if password is None:
password = getpass.getpass("Enter password: ")
db.add_user(username, password)
db.save()
if __name__ == "__main__":
main(sys.argv)
# This file is a slightly modified copy of Python 2.3's Lib/hmac.py.
# This file is under the Python Software Foundation (PSF) license.
"""HMAC (Keyed-Hashing for Message Authentication) Python module.
Implements the HMAC algorithm as described by RFC 2104.
"""
def _strxor(s1, s2):
"""Utility method. XOR the two strings s1 and s2 (must have same length).
"""
return "".join(map(lambda x, y: chr(ord(x) ^ ord(y)), s1, s2))
# The size of the digests returned by HMAC depends on the underlying
# hashing module used.
digest_size = None
class HMAC:
"""RFC2104 HMAC class.
This supports the API for Cryptographic Hash Functions (PEP 247).
"""
def __init__(self, key, msg = None, digestmod = None):
"""Create a new HMAC object.
key: key for the keyed hash object.
msg: Initial input for the hash, if provided.
digestmod: A module supporting PEP 247. Defaults to the md5 module.
"""
if digestmod is None:
import md5
digestmod = md5
self.digestmod = digestmod
self.outer = digestmod.new()
self.inner = digestmod.new()
# Python 2.1 and 2.2 differ about the correct spelling
try:
self.digest_size = digestmod.digestsize
except AttributeError:
self.digest_size = digestmod.digest_size
blocksize = 64
ipad = "\x36" * blocksize
opad = "\x5C" * blocksize
if len(key) > blocksize:
key = digestmod.new(key).digest()
key = key + chr(0) * (blocksize - len(key))
self.outer.update(_strxor(key, opad))
self.inner.update(_strxor(key, ipad))
if msg is not None:
self.update(msg)
## def clear(self):
## raise NotImplementedError, "clear() method not available in HMAC."
def update(self, msg):
"""Update this hashing object with the string msg.
"""
self.inner.update(msg)
def copy(self):
"""Return a separate copy of this hashing object.
An update to this copy won't affect the original object.
"""
other = HMAC("")
other.digestmod = self.digestmod
other.inner = self.inner.copy()
other.outer = self.outer.copy()
return other
def digest(self):
"""Return the hash value of this hashing object.
This returns a string containing 8-bit data. The object is
not altered in any way by this function; you can continue
updating the object after calling this function.
"""
h = self.outer.copy()
h.update(self.inner.digest())
return h.digest()
def hexdigest(self):
"""Like digest(), but returns a string of hexadecimal digits instead.
"""
return "".join([hex(ord(x))[2:].zfill(2)
for x in tuple(self.digest())])
def new(key, msg = None, digestmod = None):
"""Create a new hashing object and return it.
key: The starting key for the hash.
msg: if available, will immediately be hashed into the object's starting
state.
You can now feed arbitrary strings into the object using its update()
method, and can ask for the hash value at any time by calling its digest()
method.
"""
return HMAC(key, msg, digestmod)
...@@ -114,6 +114,7 @@ class Connection(smac.SizedMessageAsyncConnection): ...@@ -114,6 +114,7 @@ class Connection(smac.SizedMessageAsyncConnection):
__super_init = smac.SizedMessageAsyncConnection.__init__ __super_init = smac.SizedMessageAsyncConnection.__init__
__super_close = smac.SizedMessageAsyncConnection.close __super_close = smac.SizedMessageAsyncConnection.close
__super_setSessionKey = smac.SizedMessageAsyncConnection.setSessionKey
# Protocol variables: # Protocol variables:
# #
...@@ -152,12 +153,17 @@ class Connection(smac.SizedMessageAsyncConnection): ...@@ -152,12 +153,17 @@ class Connection(smac.SizedMessageAsyncConnection):
self.trigger = None self.trigger = None
self._prepare_async() self._prepare_async()
self._map = {self._fileno: self} self._map = {self._fileno: self}
# __msgid_lock guards access to msgid # msgid_lock guards access to msgid
self.msgid_lock = threading.Lock() self.msgid_lock = threading.Lock()
# __replies_cond is used to block when a synchronous call is # replies_cond is used to block when a synchronous call is
# waiting for a response # waiting for a response
self.replies_cond = threading.Condition() self.replies_cond = threading.Condition()
self.replies = {} self.replies = {}
# waiting_for_reply is used internally to indicate whether
# a call is in progress. setting a session key is deferred
# until after the call returns.
self.waiting_for_reply = 0
self.delay_sesskey = None
self.register_object(obj) self.register_object(obj)
self.handshake() self.handshake()
...@@ -248,8 +254,12 @@ class Connection(smac.SizedMessageAsyncConnection): ...@@ -248,8 +254,12 @@ class Connection(smac.SizedMessageAsyncConnection):
level=zLOG.DEBUG) level=zLOG.DEBUG)
meth = getattr(self.obj, name) meth = getattr(self.obj, name)
try:
self.waiting_for_reply = 1
try: try:
ret = meth(*args) ret = meth(*args)
finally:
self.waiting_for_reply = 0
except (SystemExit, KeyboardInterrupt): except (SystemExit, KeyboardInterrupt):
raise raise
except Exception, msg: except Exception, msg:
...@@ -271,6 +281,10 @@ class Connection(smac.SizedMessageAsyncConnection): ...@@ -271,6 +281,10 @@ class Connection(smac.SizedMessageAsyncConnection):
else: else:
self.send_reply(msgid, ret) self.send_reply(msgid, ret)
if self.delay_sesskey:
self.__super_setSessionKey(self.delay_sesskey)
self.delay_sesskey = None
def handle_error(self): def handle_error(self):
if sys.exc_info()[0] == SystemExit: if sys.exc_info()[0] == SystemExit:
raise sys.exc_info() raise sys.exc_info()
...@@ -318,6 +332,12 @@ class Connection(smac.SizedMessageAsyncConnection): ...@@ -318,6 +332,12 @@ class Connection(smac.SizedMessageAsyncConnection):
self.message_output(msg) self.message_output(msg)
self.poll() self.poll()
def setSessionKey(self, key):
if self.waiting_for_reply:
self.delay_sesskey = key
else:
self.__super_setSessionKey(key)
# The next two public methods (call and callAsync) are used by # The next two public methods (call and callAsync) are used by
# clients to invoke methods on remote objects # clients to invoke methods on remote objects
......
...@@ -11,9 +11,29 @@ ...@@ -11,9 +11,29 @@
# FOR A PARTICULAR PURPOSE # FOR A PARTICULAR PURPOSE
# #
############################################################################## ##############################################################################
"""Sized Message Async Connections.""" """Sized Message Async Connections.
import asyncore, struct This class extends the basic asyncore layer with a record-marking
layer. The message_output() method accepts an arbitrary sized string
as its argument. It sends over the wire the length of the string
encoded using struct.pack('>i') and the string itself. The receiver
passes the original string to message_input().
This layer also supports an optional message authentication code
(MAC). If a session key is present, it uses HMAC-SHA-1 to generate a
20-byte MAC. If a MAC is present, the high-order bit of the length
is set to 1 and the MAC immediately follows the length.
"""
import asyncore
import errno
try:
import hmac
except ImportError:
import _hmac as hmac
import sha
import socket
import struct
import threading import threading
from types import StringType from types import StringType
...@@ -21,7 +41,6 @@ from ZEO.zrpc.log import log, short_repr ...@@ -21,7 +41,6 @@ from ZEO.zrpc.log import log, short_repr
from ZEO.zrpc.error import DisconnectedError from ZEO.zrpc.error import DisconnectedError
import zLOG import zLOG
import socket, errno
# Use the dictionary to make sure we get the minimum number of errno # Use the dictionary to make sure we get the minimum number of errno
# entries. We expect that EWOULDBLOCK == EAGAIN on most systems -- # entries. We expect that EWOULDBLOCK == EAGAIN on most systems --
...@@ -45,6 +64,8 @@ del tmp_dict ...@@ -45,6 +64,8 @@ del tmp_dict
# that we could pass to send() without blocking. # that we could pass to send() without blocking.
SEND_SIZE = 60000 SEND_SIZE = 60000
MAC_BIT = 0x80000000
class SizedMessageAsyncConnection(asyncore.dispatcher): class SizedMessageAsyncConnection(asyncore.dispatcher):
__super_init = asyncore.dispatcher.__init__ __super_init = asyncore.dispatcher.__init__
__super_close = asyncore.dispatcher.close __super_close = asyncore.dispatcher.close
...@@ -75,8 +96,13 @@ class SizedMessageAsyncConnection(asyncore.dispatcher): ...@@ -75,8 +96,13 @@ class SizedMessageAsyncConnection(asyncore.dispatcher):
self.__output_lock = threading.Lock() # Protects __output self.__output_lock = threading.Lock() # Protects __output
self.__output = [] self.__output = []
self.__closed = 0 self.__closed = 0
self.__hmac = None
self.__super_init(sock, map) self.__super_init(sock, map)
def setSessionKey(self, sesskey):
log("set session key %r" % sesskey)
self.__hmac = hmac.HMAC(sesskey, digestmod=sha)
def get_addr(self): def get_addr(self):
return self.addr return self.addr
...@@ -124,12 +150,16 @@ class SizedMessageAsyncConnection(asyncore.dispatcher): ...@@ -124,12 +150,16 @@ class SizedMessageAsyncConnection(asyncore.dispatcher):
inp = "".join(inp) inp = "".join(inp)
offset = 0 offset = 0
expect_mac = 0
while (offset + msg_size) <= input_len: while (offset + msg_size) <= input_len:
msg = inp[offset:offset + msg_size] msg = inp[offset:offset + msg_size]
offset = offset + msg_size offset = offset + msg_size
if not state: if not state:
# waiting for message
msg_size = struct.unpack(">i", msg)[0] msg_size = struct.unpack(">i", msg)[0]
expect_mac = msg_size & MAC_BIT
if expect_mac:
msg_size ^= MAC_BIT
msg_size += 20
state = 1 state = 1
else: else:
msg_size = 4 msg_size = 4
...@@ -144,6 +174,17 @@ class SizedMessageAsyncConnection(asyncore.dispatcher): ...@@ -144,6 +174,17 @@ class SizedMessageAsyncConnection(asyncore.dispatcher):
# incoming call to be handled. During all this # incoming call to be handled. During all this
# time, the __input_lock is held. That's a good # time, the __input_lock is held. That's a good
# thing, because it serializes incoming calls. # thing, because it serializes incoming calls.
if expect_mac:
mac = msg[:20]
msg = msg[20:]
if self.__hmac:
self.__hmac.update(msg)
_mac = self.__hmac.digest()
if mac != _mac:
raise ValueError("MAC failed: %r != %r"
% (_mac, mac))
else:
log("Received MAC but no session key set")
self.message_input(msg) self.message_input(msg)
self.__state = state self.__state = state
...@@ -214,6 +255,11 @@ class SizedMessageAsyncConnection(asyncore.dispatcher): ...@@ -214,6 +255,11 @@ class SizedMessageAsyncConnection(asyncore.dispatcher):
self.__output_lock.acquire() self.__output_lock.acquire()
try: try:
# do two separate appends to avoid copying the message string # do two separate appends to avoid copying the message string
if self.__hmac:
self.__output.append(struct.pack(">i", len(message) | MAC_BIT))
self.__hmac.update(message)
self.__output.append(self.__hmac.digest())
else:
self.__output.append(struct.pack(">i", len(message))) self.__output.append(struct.pack(">i", len(message)))
if len(message) <= SEND_SIZE: if len(message) <= SEND_SIZE:
self.__output.append(message) self.__output.append(message)
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
############################################################################## ##############################################################################
"""Database connection support """Database connection support
$Id: Connection.py,v 1.93 2003/05/19 15:40:58 kiko Exp $""" $Id: Connection.py,v 1.94 2003/05/30 19:20:55 jeremy Exp $"""
from __future__ import nested_scopes from __future__ import nested_scopes
......
...@@ -115,7 +115,7 @@ ...@@ -115,7 +115,7 @@
# may have a back pointer to a version record or to a non-version # may have a back pointer to a version record or to a non-version
# record. # record.
# #
__version__='$Revision: 1.134 $'[11:-2] __version__='$Revision: 1.135 $'[11:-2]
import base64 import base64
from cPickle import Pickler, Unpickler, loads from cPickle import Pickler, Unpickler, loads
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
# #
############################################################################## ##############################################################################
__version__ = '3.2a1' __version__ = '3.2b1'
import sys import sys
import cPersistence, Persistence import cPersistence, Persistence
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
static char cPersistence_doc_string[] = static char cPersistence_doc_string[] =
"Defines Persistent mixin class for persistent objects.\n" "Defines Persistent mixin class for persistent objects.\n"
"\n" "\n"
"$Id: cPersistence.c,v 1.71 2003/05/23 21:32:38 jeremy Exp $\n"; "$Id: cPersistence.c,v 1.72 2003/05/30 19:20:55 jeremy Exp $\n";
#include "cPersistence.h" #include "cPersistence.h"
......
...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them. ...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them.
static char cPickleCache_doc_string[] = static char cPickleCache_doc_string[] =
"Defines the PickleCache used by ZODB Connection objects.\n" "Defines the PickleCache used by ZODB Connection objects.\n"
"\n" "\n"
"$Id: cPickleCache.c,v 1.84 2003/05/13 23:15:16 jeremy Exp $\n"; "$Id: cPickleCache.c,v 1.85 2003/05/30 19:20:55 jeremy Exp $\n";
#define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;} #define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;}
#define UNLESS(E) if(!(E)) #define UNLESS(E) if(!(E))
......
...@@ -122,6 +122,13 @@ ...@@ -122,6 +122,13 @@
available. Defaults to false. At most one of read_only and available. Defaults to false. At most one of read_only and
read_only_fallback should be true. read_only_fallback should be true.
</description> </description>
<key name="realm" required="no">
<description>
The authentication realm of the server. Some authentication
schemes use a realm to identify the logic set of usernames
that are accepted by this server.
</description>
</key>
</sectiontype> </sectiontype>
<sectiontype name="demostorage" datatype=".DemoStorage" <sectiontype name="demostorage" datatype=".DemoStorage"
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
############################################################################## ##############################################################################
"""Open database and storage from a configuration. """Open database and storage from a configuration.
$Id: config.py,v 1.11 2003/05/23 21:31:40 jeremy Exp $""" $Id: config.py,v 1.12 2003/05/30 19:20:55 jeremy Exp $"""
import os import os
import StringIO import StringIO
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
static char cPersistence_doc_string[] = static char cPersistence_doc_string[] =
"Defines Persistent mixin class for persistent objects.\n" "Defines Persistent mixin class for persistent objects.\n"
"\n" "\n"
"$Id: cPersistence.c,v 1.71 2003/05/23 21:32:38 jeremy Exp $\n"; "$Id: cPersistence.c,v 1.72 2003/05/30 19:20:55 jeremy Exp $\n";
#include "cPersistence.h" #include "cPersistence.h"
......
...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them. ...@@ -90,7 +90,7 @@ process must skip such objects, rather than deactivating them.
static char cPickleCache_doc_string[] = static char cPickleCache_doc_string[] =
"Defines the PickleCache used by ZODB Connection objects.\n" "Defines the PickleCache used by ZODB Connection objects.\n"
"\n" "\n"
"$Id: cPickleCache.c,v 1.84 2003/05/13 23:15:16 jeremy Exp $\n"; "$Id: cPickleCache.c,v 1.85 2003/05/30 19:20:55 jeremy Exp $\n";
#define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;} #define ASSIGN(V,E) {PyObject *__e; __e=(E); Py_XDECREF(V); (V)=__e;}
#define UNLESS(E) if(!(E)) #define UNLESS(E) if(!(E))
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment