diff options
author | Walter Purcaro <vuolter@gmail.com> | 2014-06-28 15:57:48 +0200 |
---|---|---|
committer | Walter Purcaro <vuolter@gmail.com> | 2014-06-28 20:23:46 +0200 |
commit | d8f5e6d74b386045a451ed20a3c250204be8946e (patch) | |
tree | d6fea466ca287e8de33201d4d42b5b12d7c2f75b /module/lib/beaker | |
parent | [Lib] Revert libraries to original status (remove all cosmetics optimizations) (diff) | |
download | pyload-d8f5e6d74b386045a451ed20a3c250204be8946e.tar.xz |
[Lib] Update beaker.py to version 1.6.4
Diffstat (limited to 'module/lib/beaker')
-rw-r--r-- | module/lib/beaker/__init__.py | 2 | ||||
-rw-r--r-- | module/lib/beaker/cache.py | 566 | ||||
-rw-r--r-- | module/lib/beaker/container.py | 347 | ||||
-rw-r--r-- | module/lib/beaker/converters.py | 3 | ||||
-rw-r--r-- | module/lib/beaker/crypto/__init__.py | 8 | ||||
-rw-r--r-- | module/lib/beaker/crypto/jcecrypto.py | 2 | ||||
-rw-r--r-- | module/lib/beaker/crypto/nsscrypto.py | 45 | ||||
-rw-r--r-- | module/lib/beaker/crypto/pbkdf2.py | 75 | ||||
-rw-r--r-- | module/lib/beaker/crypto/pycrypto.py | 17 | ||||
-rw-r--r-- | module/lib/beaker/crypto/util.py | 4 | ||||
-rw-r--r-- | module/lib/beaker/exceptions.py | 5 | ||||
-rw-r--r-- | module/lib/beaker/ext/database.py | 55 | ||||
-rw-r--r-- | module/lib/beaker/ext/google.py | 29 | ||||
-rw-r--r-- | module/lib/beaker/ext/memcached.py | 189 | ||||
-rw-r--r-- | module/lib/beaker/ext/sqla.py | 15 | ||||
-rw-r--r-- | module/lib/beaker/middleware.py | 59 | ||||
-rw-r--r-- | module/lib/beaker/session.py | 570 | ||||
-rw-r--r-- | module/lib/beaker/synchronization.py | 123 | ||||
-rw-r--r-- | module/lib/beaker/util.py | 240 |
19 files changed, 1539 insertions, 815 deletions
diff --git a/module/lib/beaker/__init__.py b/module/lib/beaker/__init__.py index 792d60054..d07785c52 100644 --- a/module/lib/beaker/__init__.py +++ b/module/lib/beaker/__init__.py @@ -1 +1 @@ -# +__version__ = '1.6.4' diff --git a/module/lib/beaker/cache.py b/module/lib/beaker/cache.py index 4a96537ff..0ae96e020 100644 --- a/module/lib/beaker/cache.py +++ b/module/lib/beaker/cache.py @@ -1,156 +1,248 @@ -"""Cache object +"""This package contains the "front end" classes and functions +for Beaker caching. -The Cache object is used to manage a set of cache files and their -associated backend. The backends can be rotated on the fly by -specifying an alternate type when used. - -Advanced users can add new backends in beaker.backends +Included are the :class:`.Cache` and :class:`.CacheManager` classes, +as well as the function decorators :func:`.region_decorate`, +:func:`.region_invalidate`. """ - import warnings import beaker.container as container import beaker.util as util +from beaker.crypto.util import sha1 from beaker.exceptions import BeakerException, InvalidCacheBackendError +from beaker.synchronization import _threading import beaker.ext.memcached as memcached import beaker.ext.database as database import beaker.ext.sqla as sqla import beaker.ext.google as google -# Initialize the basic available backends -clsmap = { - 'memory':container.MemoryNamespaceManager, - 'dbm':container.DBMNamespaceManager, - 'file':container.FileNamespaceManager, - 'ext:memcached':memcached.MemcachedNamespaceManager, - 'ext:database':database.DatabaseNamespaceManager, - 'ext:sqla': sqla.SqlaNamespaceManager, - 'ext:google': google.GoogleNamespaceManager, - } - # Initialize the cache region dict cache_regions = {} +"""Dictionary of 'region' arguments. + +A "region" is a string name that refers to a series of cache +configuration arguments. An application may have multiple +"regions" - one which stores things in a memory cache, one +which writes data to files, etc. + +The dictionary stores string key names mapped to dictionaries +of configuration arguments. Example:: + + from beaker.cache import cache_regions + cache_regions.update({ + 'short_term':{ + 'expire':'60', + 'type':'memory' + }, + 'long_term':{ + 'expire':'1800', + 'type':'dbm', + 'data_dir':'/tmp', + } + }) +""" + + cache_managers = {} -try: - import pkg_resources - # Load up the additional entry point defined backends - for entry_point in pkg_resources.iter_entry_points('beaker.backends'): +class _backends(object): + initialized = False + + def __init__(self, clsmap): + self._clsmap = clsmap + self._mutex = _threading.Lock() + + def __getitem__(self, key): try: - NamespaceManager = entry_point.load() - name = entry_point.name - if name in clsmap: - raise BeakerException("NamespaceManager name conflict,'%s' " - "already loaded" % name) - clsmap[name] = NamespaceManager - except (InvalidCacheBackendError, SyntaxError): - # Ignore invalid backends + return self._clsmap[key] + except KeyError, e: + if not self.initialized: + self._mutex.acquire() + try: + if not self.initialized: + self._init() + self.initialized = True + + return self._clsmap[key] + finally: + self._mutex.release() + + raise e + + def _init(self): + try: + import pkg_resources + + # Load up the additional entry point defined backends + for entry_point in pkg_resources.iter_entry_points('beaker.backends'): + try: + namespace_manager = entry_point.load() + name = entry_point.name + if name in self._clsmap: + raise BeakerException("NamespaceManager name conflict,'%s' " + "already loaded" % name) + self._clsmap[name] = namespace_manager + except (InvalidCacheBackendError, SyntaxError): + # Ignore invalid backends + pass + except: + import sys + from pkg_resources import DistributionNotFound + # Warn when there's a problem loading a NamespaceManager + if not isinstance(sys.exc_info()[1], DistributionNotFound): + import traceback + from StringIO import StringIO + tb = StringIO() + traceback.print_exc(file=tb) + warnings.warn( + "Unable to load NamespaceManager " + "entry point: '%s': %s" % ( + entry_point, + tb.getvalue()), + RuntimeWarning, 2) + except ImportError: pass - except: - import sys - from pkg_resources import DistributionNotFound - # Warn when there's a problem loading a NamespaceManager - if not isinstance(sys.exc_info()[1], DistributionNotFound): - import traceback - from StringIO import StringIO - tb = StringIO() - traceback.print_exc(file=tb) - warnings.warn("Unable to load NamespaceManager entry point: '%s': " - "%s" % (entry_point, tb.getvalue()), RuntimeWarning, - 2) -except ImportError: - pass - - - - -def cache_region(region, *deco_args): - """Decorate a function to cache itself using a cache region - - The region decorator requires arguments if there are more than - 2 of the same named function, in the same module. This is - because the namespace used for the functions cache is based on - the functions name and the module. - - + +# Initialize the basic available backends +clsmap = _backends({ + 'memory': container.MemoryNamespaceManager, + 'dbm': container.DBMNamespaceManager, + 'file': container.FileNamespaceManager, + 'ext:memcached': memcached.MemcachedNamespaceManager, + 'ext:database': database.DatabaseNamespaceManager, + 'ext:sqla': sqla.SqlaNamespaceManager, + 'ext:google': google.GoogleNamespaceManager, + }) + + +def cache_region(region, *args): + """Decorate a function such that its return result is cached, + using a "region" to indicate the cache arguments. + Example:: - - # Add cache region settings to beaker: - beaker.cache.cache_regions.update(dict_of_config_region_options)) - - @cache_region('short_term', 'some_data') - def populate_things(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - return load('rabbits', 20, 0) - + + from beaker.cache import cache_regions, cache_region + + # configure regions + cache_regions.update({ + 'short_term':{ + 'expire':'60', + 'type':'memory' + } + }) + + @cache_region('short_term', 'load_things') + def load(search_term, limit, offset): + '''Load from a database given a search term, limit, offset.''' + return database.query(search_term)[offset:offset + limit] + + The decorator can also be used with object methods. The ``self`` + argument is not part of the cache key. This is based on the + actual string name ``self`` being in the first argument + position (new in 1.6):: + + class MyThing(object): + @cache_region('short_term', 'load_things') + def load(self, search_term, limit, offset): + '''Load from a database given a search term, limit, offset.''' + return database.query(search_term)[offset:offset + limit] + + Classmethods work as well - use ``cls`` as the name of the class argument, + and place the decorator around the function underneath ``@classmethod`` + (new in 1.6):: + + class MyThing(object): + @classmethod + @cache_region('short_term', 'load_things') + def load(cls, search_term, limit, offset): + '''Load from a database given a search term, limit, offset.''' + return database.query(search_term)[offset:offset + limit] + + :param region: String name of the region corresponding to the desired + caching arguments, established in :attr:`.cache_regions`. + + :param \*args: Optional ``str()``-compatible arguments which will uniquely + identify the key used by this decorated function, in addition + to the positional arguments passed to the function itself at call time. + This is recommended as it is needed to distinguish between any two functions + or methods that have the same name (regardless of parent class or not). + .. note:: - + The function being decorated must only be called with - positional arguments. - + positional arguments, and the arguments must support + being stringified with ``str()``. The concatenation + of the ``str()`` version of each argument, combined + with that of the ``*args`` sent to the decorator, + forms the unique cache key. + + .. note:: + + When a method on a class is decorated, the ``self`` or ``cls`` + argument in the first position is + not included in the "key" used for caching. New in 1.6. + """ - cache = [None] - - def decorate(func): - namespace = util.func_namespace(func) - def cached(*args): - reg = cache_regions[region] - if not reg.get('enabled', True): - return func(*args) - - if not cache[0]: - if region not in cache_regions: - raise BeakerException('Cache region not configured: %s' % region) - cache[0] = Cache._get_cache(namespace, reg) - - cache_key = " ".join(map(str, deco_args + args)) - def go(): - return func(*args) - - return cache[0].get_value(cache_key, createfunc=go) - cached._arg_namespace = namespace - cached._arg_region = region - return cached - return decorate + return _cache_decorate(args, None, None, region) def region_invalidate(namespace, region, *args): - """Invalidate a cache region namespace or decorated function - - This function only invalidates cache spaces created with the - cache_region decorator. - - :param namespace: Either the namespace of the result to invalidate, or the - cached function reference - - :param region: The region the function was cached to. If the function was - cached to a single region then this argument can be None - - :param args: Arguments that were used to differentiate the cached - function as well as the arguments passed to the decorated - function + """Invalidate a cache region corresponding to a function + decorated with :func:`.cache_region`. + + :param namespace: The namespace of the cache to invalidate. This is typically + a reference to the original function (as returned by the :func:`.cache_region` + decorator), where the :func:`.cache_region` decorator applies a "memo" to + the function in order to locate the string name of the namespace. + + :param region: String name of the region used with the decorator. This can be + ``None`` in the usual case that the decorated function itself is passed, + not the string name of the namespace. + + :param args: Stringifyable arguments that are used to locate the correct + key. This consists of the ``*args`` sent to the :func:`.cache_region` + decorator itself, plus the ``*args`` sent to the function itself + at runtime. Example:: - - # Add cache region settings to beaker: - beaker.cache.cache_regions.update(dict_of_config_region_options)) - - def populate_things(invalidate=False): - + + from beaker.cache import cache_regions, cache_region, region_invalidate + + # configure regions + cache_regions.update({ + 'short_term':{ + 'expire':'60', + 'type':'memory' + } + }) + + @cache_region('short_term', 'load_data') + def load(search_term, limit, offset): + '''Load from a database given a search term, limit, offset.''' + return database.query(search_term)[offset:offset + limit] + + def invalidate_search(search_term, limit, offset): + '''Invalidate the cached storage for a given search term, limit, offset.''' + region_invalidate(load, 'short_term', 'load_data', search_term, limit, offset) + + Note that when a method on a class is decorated, the first argument ``cls`` + or ``self`` is not included in the cache key. This means you don't send + it to :func:`.region_invalidate`:: + + class MyThing(object): @cache_region('short_term', 'some_data') - def load(search_term, limit, offset): - return load_the_data(search_term, limit, offset) - - # If the results should be invalidated first - if invalidate: - region_invalidate(load, None, 'some_data', - 'rabbits', 20, 0) - return load('rabbits', 20, 0) - + def load(self, search_term, limit, offset): + '''Load from a database given a search term, limit, offset.''' + return database.query(search_term)[offset:offset + limit] + + def invalidate_search(self, search_term, limit, offset): + '''Invalidate the cached storage for a given search term, limit, offset.''' + region_invalidate(self.load, 'short_term', 'some_data', search_term, limit, offset) + """ if callable(namespace): if not region: @@ -162,10 +254,9 @@ def region_invalidate(namespace, region, *args): "namespace is required") else: region = cache_regions[region] - + cache = Cache._get_cache(namespace, region) - cache_key = " ".join(str(x) for x in args) - cache.remove_value(cache_key) + _cache_decorator_invalidate(cache, region['key_length'], args) class Cache(object): @@ -180,7 +271,7 @@ class Cache(object): :param expiretime: seconds to keep cached data (legacy support) :param starttime: time when cache was cache was - + """ def __init__(self, namespace, type='memory', expiretime=None, starttime=None, expire=None, **nsargs): @@ -190,12 +281,12 @@ class Cache(object): raise cls except KeyError: raise TypeError("Unknown cache implementation %r" % type) - + self.namespace_name = namespace self.namespace = cls(namespace, **nsargs) self.expiretime = expiretime or expire self.starttime = starttime self.nsargs = nsargs - + @classmethod def _get_cache(cls, namespace, kw): key = namespace + str(kw) @@ -204,20 +295,19 @@ class Cache(object): except KeyError: cache_managers[key] = cache = cls(namespace, **kw) return cache - + def put(self, key, value, **kw): self._get_value(key, **kw).set_value(value) set_value = put - + def get(self, key, **kw): """Retrieve a cached value from the container""" return self._get_value(key, **kw).get_value() get_value = get - + def remove_value(self, key, **kw): mycontainer = self._get_value(key, **kw) - if mycontainer.has_current_value(): - mycontainer.clear_value() + mycontainer.clear_value() remove = remove_value def _get_value(self, key, **kw): @@ -229,9 +319,9 @@ class Cache(object): kw.setdefault('expiretime', self.expiretime) kw.setdefault('starttime', self.starttime) - + return container.Value(key, self.namespace, **kw) - + @util.deprecated("Specifying a " "'type' and other namespace configuration with cache.get()/put()/etc. " "is deprecated. Specify 'type' and other namespace configuration to " @@ -243,26 +333,26 @@ class Cache(object): kwargs = self.nsargs.copy() kwargs.update(kw) c = Cache(self.namespace.namespace, type=type, **kwargs) - return c._get_value(key, expiretime=expiretime, createfunc=createfunc, + return c._get_value(key, expiretime=expiretime, createfunc=createfunc, starttime=starttime) - + def clear(self): """Clear all the values from the namespace""" self.namespace.remove() - + # dict interface def __getitem__(self, key): return self.get(key) - + def __contains__(self, key): return self._get_value(key).has_current_value() - + def has_key(self, key): return key in self - + def __delitem__(self, key): self.remove_value(key) - + def __setitem__(self, key, value): self.put(key, value) @@ -270,110 +360,96 @@ class Cache(object): class CacheManager(object): def __init__(self, **kwargs): """Initialize a CacheManager object with a set of options - + Options should be parsed with the :func:`~beaker.util.parse_cache_config_options` function to ensure only valid options are used. - + """ self.kwargs = kwargs self.regions = kwargs.pop('cache_regions', {}) - + # Add these regions to the module global cache_regions.update(self.regions) - + def get_cache(self, name, **kwargs): kw = self.kwargs.copy() kw.update(kwargs) return Cache._get_cache(name, kw) - + def get_cache_region(self, name, region): if region not in self.regions: raise BeakerException('Cache region not configured: %s' % region) kw = self.regions[region] return Cache._get_cache(name, kw) - + def region(self, region, *args): """Decorate a function to cache itself using a cache region - + The region decorator requires arguments if there are more than - 2 of the same named function, in the same module. This is + two of the same named function, in the same module. This is because the namespace used for the functions cache is based on the functions name and the module. - - + + Example:: - + # Assuming a cache object is available like: cache = CacheManager(dict_of_config_options) - - + + def populate_things(): - + @cache.region('short_term', 'some_data') def load(search_term, limit, offset): return load_the_data(search_term, limit, offset) - + return load('rabbits', 20, 0) - + .. note:: - + The function being decorated must only be called with positional arguments. - + """ return cache_region(region, *args) def region_invalidate(self, namespace, region, *args): """Invalidate a cache region namespace or decorated function - + This function only invalidates cache spaces created with the cache_region decorator. - + :param namespace: Either the namespace of the result to invalidate, or the - name of the cached function - + cached function + :param region: The region the function was cached to. If the function was cached to a single region then this argument can be None - + :param args: Arguments that were used to differentiate the cached function as well as the arguments passed to the decorated function Example:: - + # Assuming a cache object is available like: cache = CacheManager(dict_of_config_options) - + def populate_things(invalidate=False): - + @cache.region('short_term', 'some_data') def load(search_term, limit, offset): return load_the_data(search_term, limit, offset) - + # If the results should be invalidated first if invalidate: cache.region_invalidate(load, None, 'some_data', 'rabbits', 20, 0) return load('rabbits', 20, 0) - - + + """ return region_invalidate(namespace, region, *args) - if callable(namespace): - if not region: - region = namespace._arg_region - namespace = namespace._arg_namespace - - if not region: - raise BeakerException("Region or callable function " - "namespace is required") - else: - region = self.regions[region] - - cache = self.get_cache(namespace, **region) - cache_key = " ".join(str(x) for x in args) - cache.remove_value(cache_key) def cache(self, *args, **kwargs): """Decorate a function to cache itself with supplied parameters @@ -387,46 +463,32 @@ class CacheManager(object): # Assuming a cache object is available like: cache = CacheManager(dict_of_config_options) - - + + def populate_things(): - + @cache.cache('mycache', expire=15) def load(search_term, limit, offset): return load_the_data(search_term, limit, offset) - + return load('rabbits', 20, 0) - + .. note:: - + The function being decorated must only be called with - positional arguments. + positional arguments. """ - cache = [None] - key = " ".join(str(x) for x in args) - - def decorate(func): - namespace = util.func_namespace(func) - def cached(*args): - if not cache[0]: - cache[0] = self.get_cache(namespace, **kwargs) - cache_key = key + " " + " ".join(str(x) for x in args) - def go(): - return func(*args) - return cache[0].get_value(cache_key, createfunc=go) - cached._arg_namespace = namespace - return cached - return decorate + return _cache_decorate(args, self, kwargs, None) def invalidate(self, func, *args, **kwargs): """Invalidate a cache decorated function - + This function only invalidates cache spaces created with the cache decorator. - + :param func: Decorated function to invalidate - + :param args: Used to make the key unique for this function, as in region() above. @@ -435,25 +497,93 @@ class CacheManager(object): function Example:: - + # Assuming a cache object is available like: cache = CacheManager(dict_of_config_options) - - + + def populate_things(invalidate=False): - + @cache.cache('mycache', type="file", expire=15) def load(search_term, limit, offset): return load_the_data(search_term, limit, offset) - + # If the results should be invalidated first if invalidate: cache.invalidate(load, 'mycache', 'rabbits', 20, 0, type="file") return load('rabbits', 20, 0) - + """ namespace = func._arg_namespace cache = self.get_cache(namespace, **kwargs) - cache_key = " ".join(str(x) for x in args) - cache.remove_value(cache_key) + if hasattr(func, '_arg_region'): + key_length = cache_regions[func._arg_region]['key_length'] + else: + key_length = kwargs.pop('key_length', 250) + _cache_decorator_invalidate(cache, key_length, args) + + +def _cache_decorate(deco_args, manager, kwargs, region): + """Return a caching function decorator.""" + + cache = [None] + + def decorate(func): + namespace = util.func_namespace(func) + skip_self = util.has_self_arg(func) + + def cached(*args): + if not cache[0]: + if region is not None: + if region not in cache_regions: + raise BeakerException( + 'Cache region not configured: %s' % region) + reg = cache_regions[region] + if not reg.get('enabled', True): + return func(*args) + cache[0] = Cache._get_cache(namespace, reg) + elif manager: + cache[0] = manager.get_cache(namespace, **kwargs) + else: + raise Exception("'manager + kwargs' or 'region' " + "argument is required") + + if skip_self: + try: + cache_key = " ".join(map(str, deco_args + args[1:])) + except UnicodeEncodeError: + cache_key = " ".join(map(unicode, deco_args + args[1:])) + else: + try: + cache_key = " ".join(map(str, deco_args + args)) + except UnicodeEncodeError: + cache_key = " ".join(map(unicode, deco_args + args)) + if region: + key_length = cache_regions[region]['key_length'] + else: + key_length = kwargs.pop('key_length', 250) + if len(cache_key) + len(namespace) > int(key_length): + cache_key = sha1(cache_key).hexdigest() + + def go(): + return func(*args) + + return cache[0].get_value(cache_key, createfunc=go) + cached._arg_namespace = namespace + if region is not None: + cached._arg_region = region + return cached + return decorate + + +def _cache_decorator_invalidate(cache, key_length, args): + """Invalidate a cache key based on function arguments.""" + + try: + cache_key = " ".join(map(str, args)) + except UnicodeEncodeError: + cache_key = " ".join(map(unicode, args)) + if len(cache_key) + len(cache.namespace_name) > key_length: + cache_key = sha1(cache_key).hexdigest() + cache.remove_value(cache_key) diff --git a/module/lib/beaker/container.py b/module/lib/beaker/container.py index 515e97af6..5a2e8e75c 100644 --- a/module/lib/beaker/container.py +++ b/module/lib/beaker/container.py @@ -1,11 +1,18 @@ """Container and Namespace classes""" -import anydbm + +import beaker.util as util +if util.py3k: + try: + import dbm as anydbm + except: + import dumbdbm as anydbm +else: + import anydbm import cPickle import logging import os import time -import beaker.util as util from beaker.exceptions import CreationAbortedError, MissingCacheParameter from beaker.synchronization import _threading, file_synchronizer, \ mutex_synchronizer, NameLock, null_synchronizer @@ -28,88 +35,162 @@ else: class NamespaceManager(object): """Handles dictionary operations and locking for a namespace of values. - + + :class:`.NamespaceManager` provides a dictionary-like interface, + implementing ``__getitem__()``, ``__setitem__()``, and + ``__contains__()``, as well as functions related to lock + acquisition. + The implementation for setting and retrieving the namespace data is handled by subclasses. - - NamespaceManager may be used alone, or may be privately accessed by - one or more Container objects. Container objects provide per-key + + NamespaceManager may be used alone, or may be accessed by + one or more :class:`.Value` objects. :class:`.Value` objects provide per-key services like expiration times and automatic recreation of values. - + Multiple NamespaceManagers created with a particular name will all share access to the same underlying datasource and will attempt to synchronize against a common mutex object. The scope of this sharing may be within a single process or across multiple processes, depending on the type of NamespaceManager used. - + The NamespaceManager itself is generally threadsafe, except in the case of the DBMNamespaceManager in conjunction with the gdbm dbm implementation. """ - + @classmethod def _init_dependencies(cls): - pass - + """Initialize module-level dependent libraries required + by this :class:`.NamespaceManager`.""" + def __init__(self, namespace): self._init_dependencies() self.namespace = namespace - + def get_creation_lock(self, key): + """Return a locking object that is used to synchronize + multiple threads or processes which wish to generate a new + cache value. + + This function is typically an instance of + :class:`.FileSynchronizer`, :class:`.ConditionSynchronizer`, + or :class:`.null_synchronizer`. + + The creation lock is only used when a requested value + does not exist, or has been expired, and is only used + by the :class:`.Value` key-management object in conjunction + with a "createfunc" value-creation function. + + """ raise NotImplementedError() def do_remove(self): + """Implement removal of the entire contents of this + :class:`.NamespaceManager`. + + e.g. for a file-based namespace, this would remove + all the files. + + The front-end to this method is the + :meth:`.NamespaceManager.remove` method. + + """ raise NotImplementedError() def acquire_read_lock(self): - pass + """Establish a read lock. + + This operation is called before a key is read. By + default the function does nothing. + + """ def release_read_lock(self): - pass + """Release a read lock. + + This operation is called after a key is read. By + default the function does nothing. + + """ + + def acquire_write_lock(self, wait=True, replace=False): + """Establish a write lock. + + This operation is called before a key is written. + A return value of ``True`` indicates the lock has + been acquired. - def acquire_write_lock(self, wait=True): + By default the function returns ``True`` unconditionally. + + 'replace' is a hint indicating the full contents + of the namespace may be safely discarded. Some backends + may implement this (i.e. file backend won't unpickle the + current contents). + + """ return True def release_write_lock(self): - pass + """Release a write lock. + + This operation is called after a new value is written. + By default this function does nothing. + + """ def has_key(self, key): + """Return ``True`` if the given key is present in this + :class:`.Namespace`. + """ return self.__contains__(key) def __getitem__(self, key): raise NotImplementedError() - + def __setitem__(self, key, value): raise NotImplementedError() - + def set_value(self, key, value, expiretime=None): - """Optional set_value() method called by Value. - - Allows an expiretime to be passed, for namespace - implementations which can prune their collections - using expiretime. - + """Sets a value in this :class:`.NamespaceManager`. + + This is the same as ``__setitem__()``, but + also allows an expiration time to be passed + at the same time. + """ self[key] = value - + def __contains__(self, key): raise NotImplementedError() def __delitem__(self, key): raise NotImplementedError() - + def keys(self): + """Return the list of all keys. + + This method may not be supported by all + :class:`.NamespaceManager` implementations. + + """ raise NotImplementedError() - + def remove(self): + """Remove the entire contents of this + :class:`.NamespaceManager`. + + e.g. for a file-based namespace, this would remove + all the files. + """ self.do_remove() - + class OpenResourceNamespaceManager(NamespaceManager): """A NamespaceManager where read/write operations require opening/ closing of a resource which is possibly mutexed. - + """ def __init__(self, namespace): NamespaceManager.__init__(self, namespace) @@ -120,51 +201,51 @@ class OpenResourceNamespaceManager(NamespaceManager): def get_access_lock(self): raise NotImplementedError() - def do_open(self, flags): + def do_open(self, flags, replace): raise NotImplementedError() - def do_close(self): + def do_close(self): raise NotImplementedError() - def acquire_read_lock(self): + def acquire_read_lock(self): self.access_lock.acquire_read_lock() try: - self.open('r', checkcount = True) + self.open('r', checkcount=True) except: self.access_lock.release_read_lock() raise - + def release_read_lock(self): try: - self.close(checkcount = True) + self.close(checkcount=True) finally: self.access_lock.release_read_lock() - - def acquire_write_lock(self, wait=True): + + def acquire_write_lock(self, wait=True, replace=False): r = self.access_lock.acquire_write_lock(wait) try: - if (wait or r): - self.open('c', checkcount = True) + if (wait or r): + self.open('c', checkcount=True, replace=replace) return r except: self.access_lock.release_write_lock() raise - - def release_write_lock(self): + + def release_write_lock(self): try: self.close(checkcount=True) finally: self.access_lock.release_write_lock() - def open(self, flags, checkcount=False): + def open(self, flags, checkcount=False, replace=False): self.mutex.acquire() try: if checkcount: - if self.openers == 0: - self.do_open(flags) + if self.openers == 0: + self.do_open(flags, replace) self.openers += 1 else: - self.do_open(flags) + self.do_open(flags, replace) self.openers = 1 finally: self.mutex.release() @@ -174,7 +255,7 @@ class OpenResourceNamespaceManager(NamespaceManager): try: if checkcount: self.openers -= 1 - if self.openers == 0: + if self.openers == 0: self.do_close() else: if self.openers > 0: @@ -191,7 +272,13 @@ class OpenResourceNamespaceManager(NamespaceManager): finally: self.access_lock.release_write_lock() + class Value(object): + """Implements synchronization, expiration, and value-creation logic + for a single value stored in a :class:`.NamespaceManager`. + + """ + __slots__ = 'key', 'createfunc', 'expiretime', 'expire_argument', 'starttime', 'storedtime',\ 'namespace' @@ -210,18 +297,18 @@ class Value(object): """ self.namespace.acquire_read_lock() - try: - return self.namespace.has_key(self.key) + try: + return self.key in self.namespace finally: self.namespace.release_read_lock() def can_have_value(self): - return self.has_current_value() or self.createfunc is not None + return self.has_current_value() or self.createfunc is not None def has_current_value(self): self.namespace.acquire_read_lock() - try: - has_value = self.namespace.has_key(self.key) + try: + has_value = self.key in self.namespace if has_value: try: stored, expired, value = self._get_value() @@ -258,7 +345,7 @@ class Value(object): except KeyError: # guard against un-mutexed backends raising KeyError has_value = False - + if not self.createfunc: raise KeyError(self.key) finally: @@ -317,10 +404,10 @@ class Value(object): self.set_value(value, stored) self.namespace.acquire_read_lock() except TypeError: - # occurs when the value is None. memcached - # may yank the rug from under us in which case + # occurs when the value is None. memcached + # may yank the rug from under us in which case # that's the result - raise KeyError(self.key) + raise KeyError(self.key) return stored, expired, value def set_value(self, value, storedtime=None): @@ -337,7 +424,7 @@ class Value(object): self.namespace.acquire_write_lock() try: debug("clear_value") - if self.namespace.has_key(self.key): + if self.key in self.namespace: try: del self.namespace[self.key] except KeyError: @@ -347,71 +434,80 @@ class Value(object): finally: self.namespace.release_write_lock() + class AbstractDictionaryNSManager(NamespaceManager): """A subclassable NamespaceManager that places data in a dictionary. - + Subclasses should provide a "dictionary" attribute or descriptor which returns a dict-like object. The dictionary will store keys that are local to the "namespace" attribute of this manager, so ensure that the dictionary will not be used by any other namespace. e.g.:: - + import collections cached_data = collections.defaultdict(dict) - + class MyDictionaryManager(AbstractDictionaryNSManager): def __init__(self, namespace): AbstractDictionaryNSManager.__init__(self, namespace) self.dictionary = cached_data[self.namespace] - + The above stores data in a global dictionary called "cached_data", which is structured as a dictionary of dictionaries, keyed first on namespace name to a sub-dictionary, then on actual cache key to value. - + """ - + def get_creation_lock(self, key): return NameLock( - identifier="memorynamespace/funclock/%s/%s" % (self.namespace, key), + identifier="memorynamespace/funclock/%s/%s" % + (self.namespace, key), reentrant=True ) - def __getitem__(self, key): + def __getitem__(self, key): return self.dictionary[key] - def __contains__(self, key): + def __contains__(self, key): return self.dictionary.__contains__(key) - def has_key(self, key): + def has_key(self, key): return self.dictionary.__contains__(key) - + def __setitem__(self, key, value): self.dictionary[key] = value - + def __delitem__(self, key): del self.dictionary[key] def do_remove(self): self.dictionary.clear() - + def keys(self): return self.dictionary.keys() - + + class MemoryNamespaceManager(AbstractDictionaryNSManager): + """:class:`.NamespaceManager` that uses a Python dictionary for storage.""" + namespaces = util.SyncDict() def __init__(self, namespace, **kwargs): AbstractDictionaryNSManager.__init__(self, namespace) - self.dictionary = MemoryNamespaceManager.namespaces.get(self.namespace, - dict) + self.dictionary = MemoryNamespaceManager.\ + namespaces.get(self.namespace, dict) + class DBMNamespaceManager(OpenResourceNamespaceManager): - def __init__(self, namespace, dbmmodule=None, data_dir=None, - dbm_dir=None, lock_dir=None, digest_filenames=True, **kwargs): + """:class:`.NamespaceManager` that uses ``dbm`` files for storage.""" + + def __init__(self, namespace, dbmmodule=None, data_dir=None, + dbm_dir=None, lock_dir=None, + digest_filenames=True, **kwargs): self.digest_filenames = digest_filenames - + if not dbm_dir and not data_dir: raise MissingCacheParameter("data_dir or dbm_dir is required") elif dbm_dir: @@ -419,7 +515,7 @@ class DBMNamespaceManager(OpenResourceNamespaceManager): else: self.dbm_dir = data_dir + "/container_dbm" util.verify_directory(self.dbm_dir) - + if not lock_dir and not data_dir: raise MissingCacheParameter("data_dir or lock_dir is required") elif lock_dir: @@ -433,50 +529,52 @@ class DBMNamespaceManager(OpenResourceNamespaceManager): self.dbm = None OpenResourceNamespaceManager.__init__(self, namespace) - self.file = util.encoded_path(root= self.dbm_dir, + self.file = util.encoded_path(root=self.dbm_dir, identifiers=[self.namespace], extension='.dbm', digest_filenames=self.digest_filenames) - + debug("data file %s", self.file) self._checkfile() def get_access_lock(self): return file_synchronizer(identifier=self.namespace, lock_dir=self.lock_dir) - + def get_creation_lock(self, key): return file_synchronizer( - identifier = "dbmcontainer/funclock/%s" % self.namespace, + identifier="dbmcontainer/funclock/%s/%s" % ( + self.namespace, key + ), lock_dir=self.lock_dir ) def file_exists(self, file): - if os.access(file, os.F_OK): + if os.access(file, os.F_OK): return True else: for ext in ('db', 'dat', 'pag', 'dir'): if os.access(file + os.extsep + ext, os.F_OK): return True - + return False - + def _checkfile(self): if not self.file_exists(self.file): - g = self.dbmmodule.open(self.file, 'c') + g = self.dbmmodule.open(self.file, 'c') g.close() - + def get_filenames(self): list = [] if os.access(self.file, os.F_OK): list.append(self.file) - + for ext in ('pag', 'dir', 'db', 'dat'): if os.access(self.file + os.extsep + ext, os.F_OK): list.append(self.file + os.extsep + ext) return list - def do_open(self, flags): + def do_open(self, flags, replace): debug("opening dbm file %s", self.file) try: self.dbm = self.dbmmodule.open(self.file, flags) @@ -488,17 +586,17 @@ class DBMNamespaceManager(OpenResourceNamespaceManager): if self.dbm is not None: debug("closing dbm file %s", self.file) self.dbm.close() - + def do_remove(self): for f in self.get_filenames(): os.remove(f) - - def __getitem__(self, key): + + def __getitem__(self, key): return cPickle.loads(self.dbm[key]) - def __contains__(self, key): - return self.dbm.has_key(key) - + def __contains__(self, key): + return key in self.dbm + def __setitem__(self, key, value): self.dbm[key] = cPickle.dumps(value) @@ -510,10 +608,17 @@ class DBMNamespaceManager(OpenResourceNamespaceManager): class FileNamespaceManager(OpenResourceNamespaceManager): + """:class:`.NamespaceManager` that uses binary files for storage. + + Each namespace is implemented as a single file storing a + dictionary of key/value pairs, serialized using the Python + ``pickle`` module. + + """ def __init__(self, namespace, data_dir=None, file_dir=None, lock_dir=None, digest_filenames=True, **kwargs): self.digest_filenames = digest_filenames - + if not file_dir and not data_dir: raise MissingCacheParameter("data_dir or file_dir is required") elif file_dir: @@ -531,38 +636,37 @@ class FileNamespaceManager(OpenResourceNamespaceManager): util.verify_directory(self.lock_dir) OpenResourceNamespaceManager.__init__(self, namespace) - self.file = util.encoded_path(root=self.file_dir, + self.file = util.encoded_path(root=self.file_dir, identifiers=[self.namespace], extension='.cache', digest_filenames=self.digest_filenames) self.hash = {} - + debug("data file %s", self.file) def get_access_lock(self): return file_synchronizer(identifier=self.namespace, lock_dir=self.lock_dir) - + def get_creation_lock(self, key): return file_synchronizer( - identifier = "filecontainer/funclock/%s" % self.namespace, - lock_dir = self.lock_dir + identifier="dbmcontainer/funclock/%s/%s" % ( + self.namespace, key + ), + lock_dir=self.lock_dir ) - + def file_exists(self, file): return os.access(file, os.F_OK) - def do_open(self, flags): - if self.file_exists(self.file): + def do_open(self, flags, replace): + if not replace and self.file_exists(self.file): fh = open(self.file, 'rb') - try: - self.hash = cPickle.load(fh) - except (IOError, OSError, EOFError, cPickle.PickleError, ValueError): - pass + self.hash = cPickle.load(fh) fh.close() self.flags = flags - + def do_close(self): if self.flags == 'c' or self.flags == 'w': fh = open(self.file, 'wb') @@ -571,22 +675,22 @@ class FileNamespaceManager(OpenResourceNamespaceManager): self.hash = {} self.flags = None - + def do_remove(self): try: os.remove(self.file) - except OSError, err: + except OSError: # for instance, because we haven't yet used this cache, # but client code has asked for a clear() operation... pass self.hash = {} - - def __getitem__(self, key): + + def __getitem__(self, key): return self.hash[key] - def __contains__(self, key): - return self.hash.has_key(key) - + def __contains__(self, key): + return key in self.hash + def __setitem__(self, key, value): self.hash[key] = value @@ -602,11 +706,13 @@ class FileNamespaceManager(OpenResourceNamespaceManager): namespace_classes = {} ContainerContext = dict - + + class ContainerMeta(type): def __init__(cls, classname, bases, dict_): namespace_classes[cls] = cls.namespace_class return type.__init__(cls, classname, bases, dict_) + def __call__(self, key, context, namespace, createfunc=None, expiretime=None, starttime=None, **kwargs): if namespace in context: @@ -617,16 +723,27 @@ class ContainerMeta(type): return Value(key, ns, createfunc=createfunc, expiretime=expiretime, starttime=starttime) + class Container(object): + """Implements synchronization and value-creation logic + for a 'value' stored in a :class:`.NamespaceManager`. + + :class:`.Container` and its subclasses are deprecated. The + :class:`.Value` class is now used for this purpose. + + """ __metaclass__ = ContainerMeta namespace_class = NamespaceManager + class FileContainer(Container): namespace_class = FileNamespaceManager + class MemoryContainer(Container): namespace_class = MemoryNamespaceManager + class DBMContainer(Container): namespace_class = DBMNamespaceManager diff --git a/module/lib/beaker/converters.py b/module/lib/beaker/converters.py index f0ad34963..3fb80692f 100644 --- a/module/lib/beaker/converters.py +++ b/module/lib/beaker/converters.py @@ -1,3 +1,5 @@ + + # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org) # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php def asbool(obj): @@ -12,6 +14,7 @@ def asbool(obj): "String is not true/false: %r" % obj) return bool(obj) + def aslist(obj, sep=None, strip=True): if isinstance(obj, (str, unicode)): lst = obj.split(sep) diff --git a/module/lib/beaker/crypto/__init__.py b/module/lib/beaker/crypto/__init__.py index 3e26b0c13..ac13da527 100644 --- a/module/lib/beaker/crypto/__init__.py +++ b/module/lib/beaker/crypto/__init__.py @@ -14,10 +14,14 @@ if util.jython: pass else: try: - from beaker.crypto.pycrypto import getKeyLength, aesEncrypt, aesDecrypt + from beaker.crypto.nsscrypto import getKeyLength, aesEncrypt, aesDecrypt keyLength = getKeyLength() except ImportError: - pass + try: + from beaker.crypto.pycrypto import getKeyLength, aesEncrypt, aesDecrypt + keyLength = getKeyLength() + except ImportError: + pass if not keyLength: has_aes = False diff --git a/module/lib/beaker/crypto/jcecrypto.py b/module/lib/beaker/crypto/jcecrypto.py index 4062d513e..ce313d6e1 100644 --- a/module/lib/beaker/crypto/jcecrypto.py +++ b/module/lib/beaker/crypto/jcecrypto.py @@ -16,6 +16,7 @@ import jarray # Initialization vector filled with zeros _iv = IvParameterSpec(jarray.zeros(16, 'b')) + def aesEncrypt(data, key): cipher = Cipher.getInstance('AES/CTR/NoPadding') skeySpec = SecretKeySpec(key, 'AES') @@ -25,6 +26,7 @@ def aesEncrypt(data, key): # magic. aesDecrypt = aesEncrypt + def getKeyLength(): maxlen = Cipher.getMaxAllowedKeyLength('AES/CTR/NoPadding') return min(maxlen, 256) / 8 diff --git a/module/lib/beaker/crypto/nsscrypto.py b/module/lib/beaker/crypto/nsscrypto.py new file mode 100644 index 000000000..3a7797877 --- /dev/null +++ b/module/lib/beaker/crypto/nsscrypto.py @@ -0,0 +1,45 @@ +"""Encryption module that uses nsscrypto""" +import nss.nss + +nss.nss.nss_init_nodb() + +# Apparently the rest of beaker doesn't care about the particluar cipher, +# mode and padding used. +# NOTE: A constant IV!!! This is only secure if the KEY is never reused!!! +_mech = nss.nss.CKM_AES_CBC_PAD +_iv = '\0' * nss.nss.get_iv_length(_mech) + +def aesEncrypt(data, key): + slot = nss.nss.get_best_slot(_mech) + + key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, + nss.nss.CKA_ENCRYPT, nss.nss.SecItem(key)) + + param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) + ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_ENCRYPT, key_obj, + param) + l1 = ctx.cipher_op(data) + # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including + # me :( ) cares enough. + l2 = ctx.digest_final() + + return l1 + l2 + +def aesDecrypt(data, key): + slot = nss.nss.get_best_slot(_mech) + + key_obj = nss.nss.import_sym_key(slot, _mech, nss.nss.PK11_OriginGenerated, + nss.nss.CKA_DECRYPT, nss.nss.SecItem(key)) + + param = nss.nss.param_from_iv(_mech, nss.nss.SecItem(_iv)) + ctx = nss.nss.create_context_by_sym_key(_mech, nss.nss.CKA_DECRYPT, key_obj, + param) + l1 = ctx.cipher_op(data) + # Yes, DIGEST. This needs fixing in NSS, but apparently nobody (including + # me :( ) cares enough. + l2 = ctx.digest_final() + + return l1 + l2 + +def getKeyLength(): + return 32 diff --git a/module/lib/beaker/crypto/pbkdf2.py b/module/lib/beaker/crypto/pbkdf2.py index 96dc5fbb2..71df22198 100644 --- a/module/lib/beaker/crypto/pbkdf2.py +++ b/module/lib/beaker/crypto/pbkdf2.py @@ -5,22 +5,22 @@ # # Copyright (C) 2007 Dwayne C. Litzenberger <dlitz@dlitz.net> # All rights reserved. -# +# # Permission to use, copy, modify, and distribute this software and its # documentation for any purpose and without fee is hereby granted, # provided that the above copyright notice appear in all copies and that # both that copyright notice and this permission notice appear in # supporting documentation. -# -# THE AUTHOR PROVIDES THIS SOFTWARE ``AS IS'' AND ANY EXPRESSED OR -# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES -# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. -# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, +# +# THE AUTHOR PROVIDES THIS SOFTWARE ``AS IS'' AND ANY EXPRESSED OR +# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +# OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT -# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # # Country of origin: Canada @@ -74,12 +74,14 @@ from base64 import b64encode from beaker.crypto.util import hmac as HMAC, hmac_sha1 as SHA1 + def strxor(a, b): return "".join([chr(ord(x) ^ ord(y)) for (x, y) in zip(a, b)]) + class PBKDF2(object): """PBKDF2.py : PKCS#5 v2.0 Password-Based Key Derivation - + This implementation takes a passphrase and a salt (and optionally an iteration count, a digest module, and a MAC module) and provides a file-like object from which an arbitrarily-sized key can be read. @@ -89,10 +91,10 @@ class PBKDF2(object): The idea behind PBKDF2 is to derive a cryptographic key from a passphrase and a salt. - + PBKDF2 may also be used as a strong salted password hash. The 'crypt' function is provided for that purpose. - + Remember: Keys generated using PBKDF2 are only as strong as the passphrases they are derived from. """ @@ -109,7 +111,7 @@ class PBKDF2(object): """Pseudorandom function. e.g. HMAC-SHA1""" return self.__macmodule(key=key, msg=msg, digestmod=self.__digestmodule).digest() - + def read(self, bytes): """Read the specified number of key bytes.""" if self.closed: @@ -121,7 +123,7 @@ class PBKDF2(object): while size < bytes: i += 1 if i > 0xffffffff: - # We could return "" here, but + # We could return "" here, but raise OverflowError("derived key too long") block = self.__f(i) blocks.append(block) @@ -131,17 +133,17 @@ class PBKDF2(object): self.__buf = buf[bytes:] self.__blockNum = i return retval - + def __f(self, i): # i must fit within 32 bits - assert (1 <= i <= 0xffffffff) + assert (1 <= i and i <= 0xffffffff) U = self.__prf(self.__passphrase, self.__salt + pack("!L", i)) result = U - for j in xrange(2, 1+self.__iterations): + for j in xrange(2, 1 + self.__iterations): U = self.__prf(self.__passphrase, U) result = strxor(result, U) return result - + def hexread(self, octets): """Read the specified number of octets. Return them as hexadecimal. @@ -151,7 +153,7 @@ class PBKDF2(object): def _setup(self, passphrase, salt, iterations, prf): # Sanity checks: - + # passphrase and salt must be str or unicode (in the latter # case, we convert to UTF-8) if isinstance(passphrase, unicode): @@ -168,7 +170,7 @@ class PBKDF2(object): raise TypeError("iterations must be an integer") if iterations < 1: raise ValueError("iterations must be at least 1") - + # prf must be callable if not callable(prf): raise TypeError("prf must be callable") @@ -180,7 +182,7 @@ class PBKDF2(object): self.__blockNum = 0 self.__buf = "" self.closed = False - + def close(self): """Close the stream.""" if not self.closed: @@ -192,15 +194,16 @@ class PBKDF2(object): del self.__buf self.closed = True + def crypt(word, salt=None, iterations=None): """PBKDF2-based unix crypt(3) replacement. - + The number of iterations specified in the salt overrides the 'iterations' parameter. The effective hash length is 192 bits. """ - + # Generate a (pseudo-)random salt if the user hasn't provided one. if salt is None: salt = _makesalt() @@ -229,7 +232,7 @@ def crypt(word, salt=None, iterations=None): iterations = converted if not (iterations >= 1): raise ValueError("Invalid salt") - + # Make sure the salt matches the allowed character set allowed = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" for ch in salt: @@ -249,18 +252,20 @@ def crypt(word, salt=None, iterations=None): # crypt. PBKDF2.crypt = staticmethod(crypt) + def _makesalt(): """Return a 48-bit pseudorandom salt for crypt(). - + This function is not suitable for generating cryptographic secrets. """ binarysalt = "".join([pack("@H", randint(0, 0xffff)) for i in range(3)]) return b64encode(binarysalt, "./") + def test_pbkdf2(): """Module self-test""" from binascii import a2b_hex - + # # Test vectors from RFC 3962 # @@ -279,23 +284,23 @@ def test_pbkdf2(): raise RuntimeError("self-test failed") # Test 3 - result = PBKDF2("X"*64, "pass phrase equals block size", 1200).hexread(32) + result = PBKDF2("X" * 64, "pass phrase equals block size", 1200).hexread(32) expected = ("139c30c0966bc32ba55fdbf212530ac9" "c5ec59f1a452f5cc9ad940fea0598ed1") if result != expected: raise RuntimeError("self-test failed") - + # Test 4 - result = PBKDF2("X"*65, "pass phrase exceeds block size", 1200).hexread(32) + result = PBKDF2("X" * 65, "pass phrase exceeds block size", 1200).hexread(32) expected = ("9ccad6d468770cd51b10e6a68721be61" "1a8b4d282601db3b36be9246915ec82a") if result != expected: raise RuntimeError("self-test failed") - + # # Other test vectors # - + # Chunked read f = PBKDF2("kickstart", "workbench", 256) result = f.read(17) @@ -306,7 +311,7 @@ def test_pbkdf2(): expected = PBKDF2("kickstart", "workbench", 256).read(40) if result != expected: raise RuntimeError("self-test failed") - + # # crypt() test vectors # @@ -316,7 +321,7 @@ def test_pbkdf2(): expected = '$p5k2$$exec$r1EWMCMk7Rlv3L/RNcFXviDefYa0hlql' if result != expected: raise RuntimeError("self-test failed") - + # crypt 2 result = crypt("gnu", '$p5k2$c$u9HvcT4d$.....') expected = '$p5k2$c$u9HvcT4d$Sd1gwSVCLZYAuqZ25piRnbBEoAesaa/g' @@ -328,7 +333,7 @@ def test_pbkdf2(): expected = "$p5k2$d$tUsch7fU$nqDkaxMDOFBeJsTSfABsyn.PYUXilHwL" if result != expected: raise RuntimeError("self-test failed") - + # crypt 4 (unicode) result = crypt(u'\u0399\u03c9\u03b1\u03bd\u03bd\u03b7\u03c2', '$p5k2$$KosHgqNo$9mjN8gqjt02hDoP0c2J0ABtLIwtot8cQ') diff --git a/module/lib/beaker/crypto/pycrypto.py b/module/lib/beaker/crypto/pycrypto.py index a3eb4d9db..6657bff56 100644 --- a/module/lib/beaker/crypto/pycrypto.py +++ b/module/lib/beaker/crypto/pycrypto.py @@ -9,23 +9,26 @@ try: def aesEncrypt(data, key): cipher = aes.AES(key) return cipher.process(data) - + # magic. aesDecrypt = aesEncrypt - + except ImportError: from Crypto.Cipher import AES + from Crypto.Util import Counter def aesEncrypt(data, key): - cipher = AES.new(key) - - data = data + (" " * (16 - (len(data) % 16))) + cipher = AES.new(key, AES.MODE_CTR, + counter=Counter.new(128, initial_value=0)) + return cipher.encrypt(data) def aesDecrypt(data, key): - cipher = AES.new(key) + cipher = AES.new(key, AES.MODE_CTR, + counter=Counter.new(128, initial_value=0)) + return cipher.decrypt(data) + - return cipher.decrypt(data).rstrip() def getKeyLength(): return 32 diff --git a/module/lib/beaker/crypto/util.py b/module/lib/beaker/crypto/util.py index d97e8ce6f..7f96ac856 100644 --- a/module/lib/beaker/crypto/util.py +++ b/module/lib/beaker/crypto/util.py @@ -6,9 +6,9 @@ try: # Use PyCrypto (if available) from Crypto.Hash import HMAC as hmac, SHA as hmac_sha1 sha1 = hmac_sha1.new - + except ImportError: - + # PyCrypto not available. Use the Python standard library. import hmac diff --git a/module/lib/beaker/exceptions.py b/module/lib/beaker/exceptions.py index cc0eed286..4f81e456d 100644 --- a/module/lib/beaker/exceptions.py +++ b/module/lib/beaker/exceptions.py @@ -1,9 +1,14 @@ """Beaker exception classes""" + class BeakerException(Exception): pass +class BeakerWarning(RuntimeWarning): + """Issued at runtime.""" + + class CreationAbortedError(Exception): """Deprecated.""" diff --git a/module/lib/beaker/ext/database.py b/module/lib/beaker/ext/database.py index 701e6f7d2..462fb8de4 100644 --- a/module/lib/beaker/ext/database.py +++ b/module/lib/beaker/ext/database.py @@ -14,6 +14,7 @@ sa = None pool = None types = None + class DatabaseNamespaceManager(OpenResourceNamespaceManager): metadatas = SyncDict() tables = SyncDict() @@ -30,12 +31,12 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): except ImportError: raise InvalidCacheBackendError("Database cache backend requires " "the 'sqlalchemy' library") - + def __init__(self, namespace, url=None, sa_opts=None, optimistic=False, table_name='beaker_cache', data_dir=None, lock_dir=None, - **params): + schema_name=None, **params): """Creates a database namespace manager - + ``url`` SQLAlchemy compliant db url ``sa_opts`` @@ -47,9 +48,11 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): numbers. ``table_name`` The table name to use in the database for the cache. + ``schema_name`` + The schema name to use in the database for the cache. """ OpenResourceNamespaceManager.__init__(self, namespace) - + if sa_opts is None: sa_opts = params @@ -58,14 +61,16 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): elif data_dir: self.lock_dir = data_dir + "/container_db_lock" if self.lock_dir: - verify_directory(self.lock_dir) - + verify_directory(self.lock_dir) + # Check to see if the table's been created before url = url or sa_opts['sa.url'] table_key = url + table_name + def make_cache(): # Check to see if we have a connection pool open already meta_key = url + table_name + def make_meta(): # SQLAlchemy pops the url, this ensures it sticks around # later @@ -82,7 +87,8 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): sa.Column('accessed', types.DateTime, nullable=False), sa.Column('created', types.DateTime, nullable=False), sa.Column('data', types.PickleType, nullable=False), - sa.UniqueConstraint('namespace') + sa.UniqueConstraint('namespace'), + schema=schema_name if schema_name else meta.schema ) cache.create(checkfirst=True) return cache @@ -90,24 +96,26 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): self._is_new = False self.loaded = False self.cache = DatabaseNamespaceManager.tables.get(table_key, make_cache) - + def get_access_lock(self): return null_synchronizer() def get_creation_lock(self, key): return file_synchronizer( - identifier ="databasecontainer/funclock/%s" % self.namespace, - lock_dir = self.lock_dir) + identifier="databasecontainer/funclock/%s/%s" % ( + self.namespace, key + ), + lock_dir=self.lock_dir) - def do_open(self, flags): + def do_open(self, flags, replace): # If we already loaded the data, don't bother loading it again if self.loaded: self.flags = flags return - + cache = self.cache - result = sa.select([cache.c.data], - cache.c.namespace==self.namespace + result = sa.select([cache.c.data], + cache.c.namespace == self.namespace ).execute().fetchone() if not result: self._is_new = True @@ -123,7 +131,7 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): self._is_new = True self.flags = flags self.loaded = True - + def do_close(self): if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): cache = self.cache @@ -133,25 +141,25 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): created=datetime.now()) self._is_new = False else: - cache.update(cache.c.namespace==self.namespace).execute( + cache.update(cache.c.namespace == self.namespace).execute( data=self.hash, accessed=datetime.now()) self.flags = None - + def do_remove(self): cache = self.cache - cache.delete(cache.c.namespace==self.namespace).execute() + cache.delete(cache.c.namespace == self.namespace).execute() self.hash = {} - + # We can retain the fact that we did a load attempt, but since the # file is gone this will be a new namespace should it be saved. self._is_new = True - def __getitem__(self, key): + def __getitem__(self, key): return self.hash[key] - def __contains__(self, key): - return self.hash.has_key(key) - + def __contains__(self, key): + return key in self.hash + def __setitem__(self, key, value): self.hash[key] = value @@ -161,5 +169,6 @@ class DatabaseNamespaceManager(OpenResourceNamespaceManager): def keys(self): return self.hash.keys() + class DatabaseContainer(Container): namespace_manager = DatabaseNamespaceManager diff --git a/module/lib/beaker/ext/google.py b/module/lib/beaker/ext/google.py index dd8380d7f..d0a6205f4 100644 --- a/module/lib/beaker/ext/google.py +++ b/module/lib/beaker/ext/google.py @@ -10,6 +10,7 @@ log = logging.getLogger(__name__) db = None + class GoogleNamespaceManager(OpenResourceNamespaceManager): tables = {} @@ -23,11 +24,11 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): except ImportError: raise InvalidCacheBackendError("Datastore cache backend requires the " "'google.appengine.ext' library") - + def __init__(self, namespace, table_name='beaker_cache', **params): """Creates a datastore namespace manager""" OpenResourceNamespaceManager.__init__(self, namespace) - + def make_cache(): table_dict = dict(created=db.DateTimeProperty(), accessed=db.DateTimeProperty(), @@ -40,11 +41,11 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): self._is_new = False self.loaded = False self.log_debug = logging.DEBUG >= log.getEffectiveLevel() - + # Google wants namespaces to start with letters, change the namespace # to start with a letter self.namespace = 'p%s' % self.namespace - + def get_access_lock(self): return null_synchronizer() @@ -52,14 +53,14 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): # this is weird, should probably be present return null_synchronizer() - def do_open(self, flags): + def do_open(self, flags, replace): # If we already loaded the data, don't bother loading it again if self.loaded: self.flags = flags return - + item = self.cache.get_by_key_name(self.namespace) - + if not item: self._is_new = True self.hash = {} @@ -74,7 +75,7 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): self._is_new = True self.flags = flags self.loaded = True - + def do_close(self): if self.flags is not None and (self.flags == 'c' or self.flags == 'w'): if self._is_new: @@ -90,12 +91,12 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): item.accessed = datetime.now() item.put() self.flags = None - + def do_remove(self): item = self.cache.get_by_key_name(self.namespace) item.delete() self.hash = {} - + # We can retain the fact that we did a load attempt, but since the # file is gone this will be a new namespace should it be saved. self._is_new = True @@ -103,9 +104,9 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): def __getitem__(self, key): return self.hash[key] - def __contains__(self, key): - return self.hash.has_key(key) - + def __contains__(self, key): + return key in self.hash + def __setitem__(self, key, value): self.hash[key] = value @@ -114,7 +115,7 @@ class GoogleNamespaceManager(OpenResourceNamespaceManager): def keys(self): return self.hash.keys() - + class GoogleContainer(Container): namespace_class = GoogleNamespaceManager diff --git a/module/lib/beaker/ext/memcached.py b/module/lib/beaker/ext/memcached.py index 96516953f..94e3da3c9 100644 --- a/module/lib/beaker/ext/memcached.py +++ b/module/lib/beaker/ext/memcached.py @@ -1,54 +1,118 @@ +from __future__ import with_statement from beaker.container import NamespaceManager, Container +from beaker.crypto.util import sha1 from beaker.exceptions import InvalidCacheBackendError, MissingCacheParameter -from beaker.synchronization import file_synchronizer, null_synchronizer -from beaker.util import verify_directory, SyncDict +from beaker.synchronization import file_synchronizer +from beaker.util import verify_directory, SyncDict, parse_memcached_behaviors import warnings -memcache = None +MAX_KEY_LENGTH = 250 -class MemcachedNamespaceManager(NamespaceManager): - clients = SyncDict() - - @classmethod - def _init_dependencies(cls): +_client_libs = {} + + +def _load_client(name='auto'): + if name in _client_libs: + return _client_libs[name] + + def _pylibmc(): + global pylibmc + import pylibmc + return pylibmc + + def _cmemcache(): + global cmemcache + import cmemcache + warnings.warn("cmemcache is known to have serious " + "concurrency issues; consider using 'memcache' " + "or 'pylibmc'") + return cmemcache + + def _memcache(): global memcache - if memcache is not None: - return - try: - import pylibmc as memcache - except ImportError: + import memcache + return memcache + + def _auto(): + for _client in (_pylibmc, _cmemcache, _memcache): try: - import cmemcache as memcache - warnings.warn("cmemcache is known to have serious " - "concurrency issues; consider using 'memcache' or 'pylibmc'") + return _client() except ImportError: - try: - import memcache - except ImportError: - raise InvalidCacheBackendError("Memcached cache backend requires either " - "the 'memcache' or 'cmemcache' library") - - def __init__(self, namespace, url=None, data_dir=None, lock_dir=None, **params): + pass + else: + raise InvalidCacheBackendError( + "Memcached cache backend requires one " + "of: 'pylibmc' or 'memcache' to be installed.") + + clients = { + 'pylibmc': _pylibmc, + 'cmemcache': _cmemcache, + 'memcache': _memcache, + 'auto': _auto + } + _client_libs[name] = clib = clients[name]() + return clib + + +def _is_configured_for_pylibmc(memcache_module_config, memcache_client): + return memcache_module_config == 'pylibmc' or \ + memcache_client.__name__.startswith('pylibmc') + + +class MemcachedNamespaceManager(NamespaceManager): + """Provides the :class:`.NamespaceManager` API over a memcache client library.""" + + clients = SyncDict() + + def __new__(cls, *args, **kw): + memcache_module = kw.pop('memcache_module', 'auto') + + memcache_client = _load_client(memcache_module) + + if _is_configured_for_pylibmc(memcache_module, memcache_client): + return object.__new__(PyLibMCNamespaceManager) + else: + return object.__new__(MemcachedNamespaceManager) + + def __init__(self, namespace, url, + memcache_module='auto', + data_dir=None, lock_dir=None, + **kw): NamespaceManager.__init__(self, namespace) - + + _memcache_module = _client_libs[memcache_module] + if not url: - raise MissingCacheParameter("url is required") - + raise MissingCacheParameter("url is required") + if lock_dir: self.lock_dir = lock_dir elif data_dir: self.lock_dir = data_dir + "/container_mcd_lock" if self.lock_dir: - verify_directory(self.lock_dir) - - self.mc = MemcachedNamespaceManager.clients.get(url, memcache.Client, url.split(';')) + verify_directory(self.lock_dir) + + # Check for pylibmc namespace manager, in which case client will be + # instantiated by subclass __init__, to handle behavior passing to the + # pylibmc client + if not _is_configured_for_pylibmc(memcache_module, _memcache_module): + self.mc = MemcachedNamespaceManager.clients.get( + (memcache_module, url), + _memcache_module.Client, + url.split(';')) def get_creation_lock(self, key): return file_synchronizer( - identifier="memcachedcontainer/funclock/%s" % self.namespace,lock_dir = self.lock_dir) + identifier="memcachedcontainer/funclock/%s/%s" % + (self.namespace, key), lock_dir=self.lock_dir) def _format_key(self, key): - return self.namespace + '_' + key.replace(' ', '\302\267') + if not isinstance(key, str): + key = key.decode('ascii') + formated_key = (self.namespace + '_' + key).replace(' ', '\302\267') + if len(formated_key) > MAX_KEY_LENGTH: + formated_key = sha1(formated_key).hexdigest() + return formated_key def __getitem__(self, key): return self.mc.get(self._format_key(key)) @@ -68,15 +132,72 @@ class MemcachedNamespaceManager(NamespaceManager): def __setitem__(self, key, value): self.set_value(key, value) - + def __delitem__(self, key): self.mc.delete(self._format_key(key)) def do_remove(self): self.mc.flush_all() - + def keys(self): - raise NotImplementedError("Memcache caching does not support iteration of all cache keys") + raise NotImplementedError( + "Memcache caching does not " + "support iteration of all cache keys") + + +class PyLibMCNamespaceManager(MemcachedNamespaceManager): + """Provide thread-local support for pylibmc.""" + + def __init__(self, *arg, **kw): + super(PyLibMCNamespaceManager, self).__init__(*arg, **kw) + + memcache_module = kw.get('memcache_module', 'auto') + _memcache_module = _client_libs[memcache_module] + protocol = kw.get('protocol', 'text') + username = kw.get('username', None) + password = kw.get('password', None) + url = kw.get('url') + behaviors = parse_memcached_behaviors(kw) + + self.mc = MemcachedNamespaceManager.clients.get( + (memcache_module, url), + _memcache_module.Client, + servers=url.split(';'), behaviors=behaviors, + binary=(protocol == 'binary'), username=username, + password=password) + self.pool = pylibmc.ThreadMappedPool(self.mc) + + def __getitem__(self, key): + with self.pool.reserve() as mc: + return mc.get(self._format_key(key)) + + def __contains__(self, key): + with self.pool.reserve() as mc: + value = mc.get(self._format_key(key)) + return value is not None + + def has_key(self, key): + return key in self + + def set_value(self, key, value, expiretime=None): + with self.pool.reserve() as mc: + if expiretime: + mc.set(self._format_key(key), value, time=expiretime) + else: + mc.set(self._format_key(key), value) + + def __setitem__(self, key, value): + self.set_value(key, value) + + def __delitem__(self, key): + with self.pool.reserve() as mc: + mc.delete(self._format_key(key)) + + def do_remove(self): + with self.pool.reserve() as mc: + mc.flush_all() + class MemcachedContainer(Container): + """Container class which invokes :class:`.MemcacheNamespaceManager`.""" namespace_class = MemcachedNamespaceManager diff --git a/module/lib/beaker/ext/sqla.py b/module/lib/beaker/ext/sqla.py index 8c79633c1..6405c2919 100644 --- a/module/lib/beaker/ext/sqla.py +++ b/module/lib/beaker/ext/sqla.py @@ -13,6 +13,7 @@ log = logging.getLogger(__name__) sa = None + class SqlaNamespaceManager(OpenResourceNamespaceManager): binds = SyncDict() tables = SyncDict() @@ -47,7 +48,7 @@ class SqlaNamespaceManager(OpenResourceNamespaceManager): elif data_dir: self.lock_dir = data_dir + "/container_db_lock" if self.lock_dir: - verify_directory(self.lock_dir) + verify_directory(self.lock_dir) self.bind = self.__class__.binds.get(str(bind.url), lambda: bind) self.table = self.__class__.tables.get('%s:%s' % (bind.url, table.name), @@ -61,10 +62,10 @@ class SqlaNamespaceManager(OpenResourceNamespaceManager): def get_creation_lock(self, key): return file_synchronizer( - identifier ="databasecontainer/funclock/%s" % self.namespace, + identifier="databasecontainer/funclock/%s" % self.namespace, lock_dir=self.lock_dir) - def do_open(self, flags): + def do_open(self, flags, replace): if self.loaded: self.flags = flags return @@ -108,7 +109,7 @@ class SqlaNamespaceManager(OpenResourceNamespaceManager): return self.hash[key] def __contains__(self, key): - return self.hash.has_key(key) + return key in self.hash def __setitem__(self, key, value): self.hash[key] = value @@ -123,11 +124,13 @@ class SqlaNamespaceManager(OpenResourceNamespaceManager): class SqlaContainer(Container): namespace_manager = SqlaNamespaceManager -def make_cache_table(metadata, table_name='beaker_cache'): + +def make_cache_table(metadata, table_name='beaker_cache', schema_name=None): """Return a ``Table`` object suitable for storing cached values for the namespace manager. Do not create the table.""" return sa.Table(table_name, metadata, sa.Column('namespace', sa.String(255), primary_key=True), sa.Column('accessed', sa.DateTime, nullable=False), sa.Column('created', sa.DateTime, nullable=False), - sa.Column('data', sa.PickleType, nullable=False)) + sa.Column('data', sa.PickleType, nullable=False), + schema=schema_name if schema_name else metadata.schema) diff --git a/module/lib/beaker/middleware.py b/module/lib/beaker/middleware.py index 7ba88b37d..803398584 100644 --- a/module/lib/beaker/middleware.py +++ b/module/lib/beaker/middleware.py @@ -16,15 +16,15 @@ from beaker.util import coerce_cache_params, coerce_session_params, \ class CacheMiddleware(object): cache = beaker_cache - + def __init__(self, app, config=None, environ_key='beaker.cache', **kwargs): """Initialize the Cache Middleware - - The Cache middleware will make a Cache instance available + + The Cache middleware will make a CacheManager instance available every request under the ``environ['beaker.cache']`` key by default. The location in environ can be changed by setting ``environ_key``. - + ``config`` dict All settings should be prefixed by 'cache.'. This method of passing variables is intended for Paste and other @@ -32,11 +32,11 @@ class CacheMiddleware(object): single dictionary. If config contains *no cache. prefixed args*, then *all* of the config options will be used to intialize the Cache objects. - + ``environ_key`` Location where the Cache instance will keyed in the WSGI environ - + ``**kwargs`` All keyword arguments are assumed to be cache settings and will override any settings found in ``config`` @@ -44,26 +44,26 @@ class CacheMiddleware(object): """ self.app = app config = config or {} - + self.options = {} - + # Update the options with the parsed config self.options.update(parse_cache_config_options(config)) - + # Add any options from kwargs, but leave out the defaults this # time self.options.update( parse_cache_config_options(kwargs, include_defaults=False)) - + # Assume all keys are intended for cache if none are prefixed with # 'cache.' if not self.options and config: self.options = config - + self.options.update(kwargs) self.cache_manager = CacheManager(**self.options) self.environ_key = environ_key - + def __call__(self, environ, start_response): if environ.get('paste.registry'): if environ['paste.registry'].reglist: @@ -75,16 +75,16 @@ class CacheMiddleware(object): class SessionMiddleware(object): session = beaker_session - + def __init__(self, wrap_app, config=None, environ_key='beaker.session', **kwargs): """Initialize the Session Middleware - + The Session middleware will make a lazy session instance available every request under the ``environ['beaker.session']`` key by default. The location in environ can be changed by setting ``environ_key``. - + ``config`` dict All settings should be prefixed by 'session.'. This method of passing variables is intended for Paste and other @@ -92,21 +92,21 @@ class SessionMiddleware(object): single dictionary. If config contains *no cache. prefixed args*, then *all* of the config options will be used to intialize the Cache objects. - + ``environ_key`` Location where the Session instance will keyed in the WSGI environ - + ``**kwargs`` All keyword arguments are assumed to be session settings and will override any settings found in ``config`` """ config = config or {} - + # Load up the default params - self.options = dict(invalidate_corrupt=True, type=None, - data_dir=None, key='beaker.session.id', + self.options = dict(invalidate_corrupt=True, type=None, + data_dir=None, key='beaker.session.id', timeout=None, secret=None, log_file=None) # Pull out any config args meant for beaker session. if there are any @@ -120,19 +120,19 @@ class SessionMiddleware(object): warnings.warn('Session options should start with session. ' 'instead of session_.', DeprecationWarning, 2) self.options[key[8:]] = val - + # Coerce and validate session params coerce_session_params(self.options) - + # Assume all keys are intended for cache if none are prefixed with # 'cache.' if not self.options and config: self.options = config - + self.options.update(kwargs) - self.wrap_app = wrap_app + self.wrap_app = self.app = wrap_app self.environ_key = environ_key - + def __call__(self, environ, start_response): session = SessionObject(environ, **self.options) if environ.get('paste.registry'): @@ -140,8 +140,11 @@ class SessionMiddleware(object): environ['paste.registry'].register(self.session, session) environ[self.environ_key] = session environ['beaker.get_session'] = self._get_session - - def session_start_response(status, headers, exc_info = None): + + if 'paste.testing_variables' in environ and 'webtest_varname' in self.options: + environ['paste.testing_variables'][self.options['webtest_varname']] = session + + def session_start_response(status, headers, exc_info=None): if session.accessed(): session.persist() if session.__dict__['_headers']['set_cookie']: @@ -150,7 +153,7 @@ class SessionMiddleware(object): headers.append(('Set-cookie', cookie)) return start_response(status, headers, exc_info) return self.wrap_app(environ, session_start_response) - + def _get_session(self): return Session({}, use_cookies=False, **self.options) diff --git a/module/lib/beaker/session.py b/module/lib/beaker/session.py index 7d465530b..d70a670eb 100644 --- a/module/lib/beaker/session.py +++ b/module/lib/beaker/session.py @@ -1,13 +1,9 @@ import Cookie import os -import random -import time from datetime import datetime, timedelta - +import time from beaker.crypto import hmac as HMAC, hmac_sha1 as SHA1, md5 -from beaker.util import pickle - -from beaker import crypto +from beaker import crypto, util from beaker.cache import clsmap from beaker.exceptions import BeakerException, InvalidCryptoBackendError from base64 import b64encode, b64decode @@ -15,56 +11,102 @@ from base64 import b64encode, b64decode __all__ = ['SignedCookie', 'Session'] -getpid = hasattr(os, 'getpid') and os.getpid or (lambda : '') + +try: + import uuid + + def _session_id(): + return uuid.uuid4().hex +except ImportError: + import random + if hasattr(os, 'getpid'): + getpid = os.getpid + else: + def getpid(): + return '' + + def _session_id(): + id_str = "%f%s%f%s" % ( + time.time(), + id({}), + random.random(), + getpid() + ) + if util.py3k: + return md5( + md5( + id_str.encode('ascii') + ).hexdigest().encode('ascii') + ).hexdigest() + else: + return md5(md5(id_str).hexdigest()).hexdigest() + class SignedCookie(Cookie.BaseCookie): """Extends python cookie to give digital signature support""" def __init__(self, secret, input=None): - self.secret = secret + self.secret = secret.encode('UTF-8') Cookie.BaseCookie.__init__(self, input) - + def value_decode(self, val): val = val.strip('"') - sig = HMAC.new(self.secret, val[40:], SHA1).hexdigest() - + sig = HMAC.new(self.secret, val[40:].encode('UTF-8'), SHA1).hexdigest() + # Avoid timing attacks invalid_bits = 0 input_sig = val[:40] if len(sig) != len(input_sig): return None, val - + for a, b in zip(sig, input_sig): invalid_bits += a != b - + if invalid_bits: return None, val else: return val[40:], val - + def value_encode(self, val): - sig = HMAC.new(self.secret, val, SHA1).hexdigest() + sig = HMAC.new(self.secret, val.encode('UTF-8'), SHA1).hexdigest() return str(val), ("%s%s" % (sig, val)) class Session(dict): """Session object that uses container package for storage. - - ``key`` - The name the cookie should be set to. - ``timeout`` - How long session data is considered valid. This is used - regardless of the cookie being present or not to determine - whether session data is still valid. - ``cookie_domain`` - Domain to use for the cookie. - ``secure`` - Whether or not the cookie should only be sent over SSL. + + :param invalidate_corrupt: How to handle corrupt data when loading. When + set to True, then corrupt data will be silently + invalidated and a new session created, + otherwise invalid data will cause an exception. + :type invalidate_corrupt: bool + :param use_cookies: Whether or not cookies should be created. When set to + False, it is assumed the user will handle storing the + session on their own. + :type use_cookies: bool + :param type: What data backend type should be used to store the underlying + session data + :param key: The name the cookie should be set to. + :param timeout: How long session data is considered valid. This is used + regardless of the cookie being present or not to determine + whether session data is still valid. + :type timeout: int + :param cookie_expires: Expiration date for cookie + :param cookie_domain: Domain to use for the cookie. + :param cookie_path: Path to use for the cookie. + :param secure: Whether or not the cookie should only be sent over SSL. + :param httponly: Whether or not the cookie should only be accessible by + the browser not by JavaScript. + :param encrypt_key: The key to use for the local session encryption, if not + provided the session will not be encrypted. + :param validate_key: The key used to sign the local encrypted session + """ def __init__(self, request, id=None, invalidate_corrupt=False, use_cookies=True, type=None, data_dir=None, key='beaker.session.id', timeout=None, cookie_expires=True, - cookie_domain=None, secret=None, secure=False, - namespace_class=None, **namespace_args): + cookie_domain=None, cookie_path='/', secret=None, + secure=False, namespace_class=None, httponly=False, + encrypt_key=None, validate_key=None, **namespace_args): if not type: if data_dir: self.type = 'file' @@ -76,24 +118,28 @@ class Session(dict): self.namespace_class = namespace_class or clsmap[self.type] self.namespace_args = namespace_args - + self.request = request self.data_dir = data_dir self.key = key - + self.timeout = timeout self.use_cookies = use_cookies self.cookie_expires = cookie_expires - + # Default cookie domain/path self._domain = cookie_domain - self._path = '/' + self._path = cookie_path self.was_invalidated = False self.secret = secret self.secure = secure + self.httponly = httponly + self.encrypt_key = encrypt_key + self.validate_key = validate_key self.id = id self.accessed_dict = {} - + self.invalidate_corrupt = invalidate_corrupt + if self.use_cookies: cookieheader = request.get('cookie', '') if secret: @@ -103,10 +149,10 @@ class Session(dict): self.cookie = SignedCookie(secret, input=None) else: self.cookie = Cookie.SimpleCookie(input=cookieheader) - + if not self.id and self.key in self.cookie: self.id = self.cookie[self.key].value - + self.is_new = self.id is None if self.is_new: self._create_id() @@ -114,80 +160,146 @@ class Session(dict): else: try: self.load() - except: + except Exception, e: if invalidate_corrupt: + util.warn( + "Invalidating corrupt session %s; " + "error was: %s. Set invalidate_corrupt=False " + "to propagate this exception." % (self.id, e)) self.invalidate() else: raise - - def _create_id(self): - self.id = md5( - md5("%f%s%f%s" % (time.time(), id({}), random.random(), - getpid())).hexdigest(), - ).hexdigest() - self.is_new = True - self.last_accessed = None - if self.use_cookies: - self.cookie[self.key] = self.id - if self._domain: - self.cookie[self.key]['domain'] = self._domain - if self.secure: - self.cookie[self.key]['secure'] = True - self.cookie[self.key]['path'] = self._path + + def has_key(self, name): + return name in self + + def _set_cookie_values(self, expires=None): + self.cookie[self.key] = self.id + if self._domain: + self.cookie[self.key]['domain'] = self._domain + if self.secure: + self.cookie[self.key]['secure'] = True + self._set_cookie_http_only() + self.cookie[self.key]['path'] = self._path + + self._set_cookie_expires(expires) + + def _set_cookie_expires(self, expires): + if expires is None: if self.cookie_expires is not True: if self.cookie_expires is False: - expires = datetime.fromtimestamp( 0x7FFFFFFF ) + expires = datetime.fromtimestamp(0x7FFFFFFF) elif isinstance(self.cookie_expires, timedelta): - expires = datetime.today() + self.cookie_expires + expires = datetime.utcnow() + self.cookie_expires elif isinstance(self.cookie_expires, datetime): expires = self.cookie_expires else: raise ValueError("Invalid argument for cookie_expires: %s" % repr(self.cookie_expires)) - self.cookie[self.key]['expires'] = \ - expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" ) - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = False - + else: + expires = None + if expires is not None: + if not self.cookie or self.key not in self.cookie: + self.cookie[self.key] = self.id + self.cookie[self.key]['expires'] = \ + expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT") + return expires + + def _update_cookie_out(self, set_cookie=True): + self.request['cookie_out'] = self.cookie[self.key].output(header='') + self.request['set_cookie'] = set_cookie + + def _set_cookie_http_only(self): + try: + if self.httponly: + self.cookie[self.key]['httponly'] = True + except Cookie.CookieError, e: + if 'Invalid Attribute httponly' not in str(e): + raise + util.warn('Python 2.6+ is required to use httponly') + + def _create_id(self, set_new=True): + self.id = _session_id() + + if set_new: + self.is_new = True + self.last_accessed = None + if self.use_cookies: + self._set_cookie_values() + sc = set_new == False + self._update_cookie_out(set_cookie=sc) + + @property def created(self): return self['_creation_time'] - created = property(created) - + def _set_domain(self, domain): self['_domain'] = domain self.cookie[self.key]['domain'] = domain - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = True - + self._update_cookie_out() + def _get_domain(self): return self._domain - + domain = property(_get_domain, _set_domain) - + def _set_path(self, path): - self['_path'] = path + self['_path'] = self._path = path self.cookie[self.key]['path'] = path - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = True - + self._update_cookie_out() + def _get_path(self): return self._path - + path = property(_get_path, _set_path) + def _encrypt_data(self, session_data=None): + """Serialize, encipher, and base64 the session dict""" + session_data = session_data or self.copy() + if self.encrypt_key: + nonce = b64encode(os.urandom(6))[:8] + encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, + self.validate_key + nonce, 1) + data = util.pickle.dumps(session_data, 2) + return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key)) + else: + data = util.pickle.dumps(session_data, 2) + return b64encode(data) + + def _decrypt_data(self, session_data): + """Bas64, decipher, then un-serialize the data for the session + dict""" + if self.encrypt_key: + try: + nonce = session_data[:8] + encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, + self.validate_key + nonce, 1) + payload = b64decode(session_data[8:]) + data = crypto.aesDecrypt(payload, encrypt_key) + except: + # As much as I hate a bare except, we get some insane errors + # here that get tossed when crypto fails, so we raise the + # 'right' exception + if self.invalidate_corrupt: + return None + else: + raise + try: + return util.pickle.loads(data) + except: + if self.invalidate_corrupt: + return None + else: + raise + else: + data = b64decode(session_data) + return util.pickle.loads(data) + def _delete_cookie(self): self.request['set_cookie'] = True - self.cookie[self.key] = self.id - if self._domain: - self.cookie[self.key]['domain'] = self._domain - if self.secure: - self.cookie[self.key]['secure'] = True - self.cookie[self.key]['path'] = '/' - expires = datetime.today().replace(year=2003) - self.cookie[self.key]['expires'] = \ - expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" ) - self.request['cookie_out'] = self.cookie[self.key].output(header='') - self.request['set_cookie'] = True + expires = datetime.utcnow() - timedelta(365) + self._set_cookie_values(expires) + self._update_cookie_out() def delete(self): """Deletes the session from the persistent storage, and sends @@ -203,15 +315,17 @@ class Session(dict): self.was_invalidated = True self._create_id() self.load() - + def load(self): "Loads the data from this session from persistent storage" self.namespace = self.namespace_class(self.id, - data_dir=self.data_dir, digest_filenames=False, + data_dir=self.data_dir, + digest_filenames=False, **self.namespace_args) now = time.time() - self.request['set_cookie'] = True - + if self.use_cookies: + self.request['set_cookie'] = True + self.namespace.acquire_read_lock() timed_out = False try: @@ -219,24 +333,34 @@ class Session(dict): try: session_data = self.namespace['session'] + if (session_data is not None and self.encrypt_key): + session_data = self._decrypt_data(session_data) + # Memcached always returns a key, its None when its not # present if session_data is None: session_data = { - '_creation_time':now, - '_accessed_time':now + '_creation_time': now, + '_accessed_time': now } self.is_new = True except (KeyError, TypeError): session_data = { - '_creation_time':now, - '_accessed_time':now + '_creation_time': now, + '_accessed_time': now + } + self.is_new = True + + if session_data is None or len(session_data) == 0: + session_data = { + '_creation_time': now, + '_accessed_time': now } self.is_new = True - + if self.timeout is not None and \ now - session_data['_accessed_time'] > self.timeout: - timed_out= True + timed_out = True else: # Properly set the last_accessed time, which is different # than the *currently* _accessed_time @@ -244,43 +368,52 @@ class Session(dict): self.last_accessed = None else: self.last_accessed = session_data['_accessed_time'] - + # Update the current _accessed_time session_data['_accessed_time'] = now + + # Set the path if applicable + if '_path' in session_data: + self._path = session_data['_path'] self.update(session_data) - self.accessed_dict = session_data.copy() + self.accessed_dict = session_data.copy() finally: self.namespace.release_read_lock() if timed_out: self.invalidate() - + def save(self, accessed_only=False): """Saves the data for this session to persistent storage - + If accessed_only is True, then only the original data loaded at the beginning of the request will be saved, with the updated last accessed time. - + """ # Look to see if its a new session that was only accessed # Don't save it under that case if accessed_only and self.is_new: return None - - if not hasattr(self, 'namespace'): + + # this session might not have a namespace yet or the session id + # might have been regenerated + if not hasattr(self, 'namespace') or self.namespace.namespace != self.id: self.namespace = self.namespace_class( - self.id, + self.id, data_dir=self.data_dir, - digest_filenames=False, + digest_filenames=False, **self.namespace_args) - - self.namespace.acquire_write_lock() + + self.namespace.acquire_write_lock(replace=True) try: if accessed_only: data = dict(self.accessed_dict.items()) else: data = dict(self.items()) - + + if self.encrypt_key: + data = self._encrypt_data(data) + # Save the data if not data and 'session' in self.namespace: del self.namespace['session'] @@ -288,22 +421,32 @@ class Session(dict): self.namespace['session'] = data finally: self.namespace.release_write_lock() - if self.is_new: + if self.use_cookies and self.is_new: self.request['set_cookie'] = True - + def revert(self): """Revert the session to its original state from its first access in the request""" self.clear() self.update(self.accessed_dict) - + + def regenerate_id(self): + """ + creates a new session id, retains all session data + + Its a good security practice to regnerate the id after a client + elevates priviliges. + + """ + self._create_id(set_new=False) + # TODO: I think both these methods should be removed. They're from # the original mod_python code i was ripping off but they really # have no use here. def lock(self): """Locks this session against other processes/threads. This is automatic when load/save is called. - + ***use with caution*** and always with a corresponding 'unlock' inside a "finally:" block, as a stray lock typically cannot be unlocked without shutting down the whole application. @@ -322,37 +465,38 @@ class Session(dict): """ self.namespace.release_write_lock() + class CookieSession(Session): """Pure cookie-based session - + Options recognized when using cookie-based sessions are slightly more restricted than general sessions. - - ``key`` - The name the cookie should be set to. - ``timeout`` - How long session data is considered valid. This is used - regardless of the cookie being present or not to determine - whether session data is still valid. - ``encrypt_key`` - The key to use for the session encryption, if not provided the - session will not be encrypted. - ``validate_key`` - The key used to sign the encrypted session - ``cookie_domain`` - Domain to use for the cookie. - ``secure`` - Whether or not the cookie should only be sent over SSL. - + + :param key: The name the cookie should be set to. + :param timeout: How long session data is considered valid. This is used + regardless of the cookie being present or not to determine + whether session data is still valid. + :type timeout: int + :param cookie_expires: Expiration date for cookie + :param cookie_domain: Domain to use for the cookie. + :param cookie_path: Path to use for the cookie. + :param secure: Whether or not the cookie should only be sent over SSL. + :param httponly: Whether or not the cookie should only be accessible by + the browser not by JavaScript. + :param encrypt_key: The key to use for the local session encryption, if not + provided the session will not be encrypted. + :param validate_key: The key used to sign the local encrypted session + """ def __init__(self, request, key='beaker.session.id', timeout=None, - cookie_expires=True, cookie_domain=None, encrypt_key=None, - validate_key=None, secure=False, **kwargs): - + cookie_expires=True, cookie_domain=None, cookie_path='/', + encrypt_key=None, validate_key=None, secure=False, + httponly=False, **kwargs): + if not crypto.has_aes and encrypt_key: raise InvalidCryptoBackendError("No AES library is installed, can't generate " "encrypted cookie-only Session.") - + self.request = request self.key = key self.timeout = timeout @@ -361,31 +505,34 @@ class CookieSession(Session): self.validate_key = validate_key self.request['set_cookie'] = False self.secure = secure + self.httponly = httponly self._domain = cookie_domain - self._path = '/' - + self._path = cookie_path + try: cookieheader = request['cookie'] except KeyError: cookieheader = '' - + if validate_key is None: raise BeakerException("No validate_key specified for Cookie only " "Session.") - + try: self.cookie = SignedCookie(validate_key, input=cookieheader) except Cookie.CookieError: self.cookie = SignedCookie(validate_key, input=None) - - self['_id'] = self._make_id() + + self['_id'] = _session_id() self.is_new = True - + # If we have a cookie, load it if self.key in self.cookie and self.cookie[self.key].value is not None: self.is_new = False try: - self.update(self._decrypt_data()) + cookie_data = self.cookie[self.key].value + self.update(self._decrypt_data(cookie_data)) + self._path = self.get('_path', '/') except: pass if self.timeout is not None and time.time() - \ @@ -393,11 +540,11 @@ class CookieSession(Session): self.clear() self.accessed_dict = self.copy() self._create_cookie() - + def created(self): return self['_creation_time'] created = property(created) - + def id(self): return self['_id'] id = property(id) @@ -405,53 +552,20 @@ class CookieSession(Session): def _set_domain(self, domain): self['_domain'] = domain self._domain = domain - + def _get_domain(self): return self._domain - + domain = property(_get_domain, _set_domain) - + def _set_path(self, path): - self['_path'] = path - self._path = path - + self['_path'] = self._path = path + def _get_path(self): return self._path - + path = property(_get_path, _set_path) - def _encrypt_data(self): - """Serialize, encipher, and base64 the session dict""" - if self.encrypt_key: - nonce = b64encode(os.urandom(40))[:8] - encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, - self.validate_key + nonce, 1) - data = pickle.dumps(self.copy(), 2) - return nonce + b64encode(crypto.aesEncrypt(data, encrypt_key)) - else: - data = pickle.dumps(self.copy(), 2) - return b64encode(data) - - def _decrypt_data(self): - """Bas64, decipher, then un-serialize the data for the session - dict""" - if self.encrypt_key: - nonce = self.cookie[self.key].value[:8] - encrypt_key = crypto.generateCryptoKeys(self.encrypt_key, - self.validate_key + nonce, 1) - payload = b64decode(self.cookie[self.key].value[8:]) - data = crypto.aesDecrypt(payload, encrypt_key) - return pickle.loads(data) - else: - data = b64decode(self.cookie[self.key].value) - return pickle.loads(data) - - def _make_id(self): - return md5(md5( - "%f%s%f%s" % (time.time(), id({}), random.random(), getpid()) - ).hexdigest() - ).hexdigest() - def save(self, accessed_only=False): """Saves the data for this session to persistent storage""" if accessed_only and self.is_new: @@ -460,88 +574,79 @@ class CookieSession(Session): self.clear() self.update(self.accessed_dict) self._create_cookie() - + def expire(self): """Delete the 'expires' attribute on this Session, if any.""" - + self.pop('_expires', None) - + def _create_cookie(self): if '_creation_time' not in self: self['_creation_time'] = time.time() if '_id' not in self: - self['_id'] = self._make_id() + self['_id'] = _session_id() self['_accessed_time'] = time.time() - - if self.cookie_expires is not True: - if self.cookie_expires is False: - expires = datetime.fromtimestamp( 0x7FFFFFFF ) - elif isinstance(self.cookie_expires, timedelta): - expires = datetime.today() + self.cookie_expires - elif isinstance(self.cookie_expires, datetime): - expires = self.cookie_expires - else: - raise ValueError("Invalid argument for cookie_expires: %s" - % repr(self.cookie_expires)) - self['_expires'] = expires - elif '_expires' in self: - expires = self['_expires'] - else: - expires = None val = self._encrypt_data() if len(val) > 4064: raise BeakerException("Cookie value is too long to store") - + self.cookie[self.key] = val + + if '_expires' in self: + expires = self['_expires'] + else: + expires = None + expires = self._set_cookie_expires(expires) + if expires is not None: + self['_expires'] = expires + if '_domain' in self: self.cookie[self.key]['domain'] = self['_domain'] elif self._domain: self.cookie[self.key]['domain'] = self._domain if self.secure: self.cookie[self.key]['secure'] = True - + self._set_cookie_http_only() + self.cookie[self.key]['path'] = self.get('_path', '/') - - if expires: - self.cookie[self.key]['expires'] = \ - expires.strftime("%a, %d-%b-%Y %H:%M:%S GMT" ) + self.request['cookie_out'] = self.cookie[self.key].output(header='') self.request['set_cookie'] = True - + def delete(self): """Delete the cookie, and clear the session""" # Send a delete cookie request self._delete_cookie() self.clear() - + def invalidate(self): """Clear the contents and start a new session""" - self.delete() - self['_id'] = self._make_id() + self.clear() + self['_id'] = _session_id() class SessionObject(object): """Session proxy/lazy creator - + This object proxies access to the actual session object, so that in the case that the session hasn't been used before, it will be setup. This avoid creating and loading the session from persistent storage unless its actually used during the request. - + """ def __init__(self, environ, **params): self.__dict__['_params'] = params self.__dict__['_environ'] = environ self.__dict__['_sess'] = None - self.__dict__['_headers'] = [] - + self.__dict__['_headers'] = {} + def _session(self): """Lazy initial creation of session object""" if self.__dict__['_sess'] is None: params = self.__dict__['_params'] environ = self.__dict__['_environ'] - self.__dict__['_headers'] = req = {'cookie_out':None} + self.__dict__['_headers'] = req = {'cookie_out': None} req['cookie'] = environ.get('HTTP_COOKIE') if params.get('type') == 'cookie': self.__dict__['_sess'] = CookieSession(req, **params) @@ -549,35 +654,38 @@ class SessionObject(object): self.__dict__['_sess'] = Session(req, use_cookies=True, **params) return self.__dict__['_sess'] - + def __getattr__(self, attr): return getattr(self._session(), attr) - + def __setattr__(self, attr, value): setattr(self._session(), attr, value) - + def __delattr__(self, name): self._session().__delattr__(name) - + def __getitem__(self, key): return self._session()[key] - + def __setitem__(self, key, value): self._session()[key] = value - + def __delitem__(self, key): self._session().__delitem__(key) - + def __repr__(self): return self._session().__repr__() - + def __iter__(self): """Only works for proxying to a dict""" return iter(self._session().keys()) - + def __contains__(self, key): - return self._session().has_key(key) - + return key in self._session() + + def has_key(self, key): + return key in self._session() + def get_by_id(self, id): """Loads a session given a session ID""" params = self.__dict__['_params'] @@ -585,22 +693,22 @@ class SessionObject(object): if session.is_new: return None return session - + def save(self): self.__dict__['_dirty'] = True - + def delete(self): self.__dict__['_dirty'] = True self._session().delete() - + def persist(self): """Persist the session to the storage - + If its set to autosave, then the entire session will be saved regardless of if save() has been called. Otherwise, just the accessed time will be updated if save() was not called, or the session will be saved if save() was called. - + """ if self.__dict__['_params'].get('auto'): self._session().save() @@ -609,10 +717,10 @@ class SessionObject(object): self._session().save() else: self._session().save(accessed_only=True) - + def dirty(self): return self.__dict__.get('_dirty', False) - + def accessed(self): """Returns whether or not the session has been accessed""" return self.__dict__['_sess'] is not None diff --git a/module/lib/beaker/synchronization.py b/module/lib/beaker/synchronization.py index 761303707..f236b8cfe 100644 --- a/module/lib/beaker/synchronization.py +++ b/module/lib/beaker/synchronization.py @@ -29,18 +29,18 @@ except: from beaker import util from beaker.exceptions import LockError -__all__ = ["file_synchronizer", "mutex_synchronizer", "null_synchronizer", +__all__ = ["file_synchronizer", "mutex_synchronizer", "null_synchronizer", "NameLock", "_threading"] class NameLock(object): """a proxy for an RLock object that is stored in a name based - registry. - + registry. + Multiple threads can get a reference to the same RLock based on the name alone, and synchronize operations related to that name. - """ + """ locks = util.WeakValuedRegistry() class NLContainer(object): @@ -49,17 +49,18 @@ class NameLock(object): self.lock = _threading.RLock() else: self.lock = _threading.Lock() + def __call__(self): return self.lock - def __init__(self, identifier = None, reentrant = False): + def __init__(self, identifier=None, reentrant=False): if identifier is None: self._lock = NameLock.NLContainer(reentrant) else: self._lock = NameLock.locks.get(identifier, NameLock.NLContainer, reentrant) - def acquire(self, wait = True): + def acquire(self, wait=True): return self._lock().acquire(wait) def release(self): @@ -67,6 +68,8 @@ class NameLock(object): _synchronizers = util.WeakValuedRegistry() + + def _synchronizer(identifier, cls, **kwargs): return _synchronizers.sync_get((identifier, cls), cls, identifier, **kwargs) @@ -83,12 +86,19 @@ def mutex_synchronizer(identifier, **kwargs): class null_synchronizer(object): + """A 'null' synchronizer, which provides the :class:`.SynchronizerImpl` interface + without any locking. + + """ def acquire_write_lock(self, wait=True): return True + def acquire_read_lock(self): pass + def release_write_lock(self): pass + def release_read_lock(self): pass acquire = acquire_write_lock @@ -96,6 +106,10 @@ class null_synchronizer(object): class SynchronizerImpl(object): + """Base class for a synchronization object that allows + multiple readers, single writers. + + """ def __init__(self): self._state = util.ThreadLocal() @@ -115,27 +129,27 @@ class SynchronizerImpl(object): else: return self._state.get() state = property(state) - + def release_read_lock(self): state = self.state - if state.writing: + if state.writing: raise LockError("lock is in writing state") - if not state.reading: + if not state.reading: raise LockError("lock is not in reading state") - + if state.reentrantcount == 1: self.do_release_read_lock() state.reading = False state.reentrantcount -= 1 - - def acquire_read_lock(self, wait = True): + + def acquire_read_lock(self, wait=True): state = self.state - if state.writing: + if state.writing: raise LockError("lock is in writing state") - + if state.reentrantcount == 0: x = self.do_acquire_read_lock(wait) if (wait or x): @@ -145,13 +159,13 @@ class SynchronizerImpl(object): elif state.reading: state.reentrantcount += 1 return True - + def release_write_lock(self): state = self.state - if state.reading: + if state.reading: raise LockError("lock is in reading state") - if not state.writing: + if not state.writing: raise LockError("lock is not in writing state") if state.reentrantcount == 1: @@ -159,18 +173,18 @@ class SynchronizerImpl(object): state.writing = False state.reentrantcount -= 1 - + release = release_write_lock - - def acquire_write_lock(self, wait = True): + + def acquire_write_lock(self, wait=True): state = self.state - if state.reading: + if state.reading: raise LockError("lock is in reading state") - + if state.reentrantcount == 0: x = self.do_acquire_write_lock(wait) - if (wait or x): + if (wait or x): state.reentrantcount += 1 state.writing = True return x @@ -182,56 +196,47 @@ class SynchronizerImpl(object): def do_release_read_lock(self): raise NotImplementedError() - + def do_acquire_read_lock(self): raise NotImplementedError() - + def do_release_write_lock(self): raise NotImplementedError() - + def do_acquire_write_lock(self): raise NotImplementedError() class FileSynchronizer(SynchronizerImpl): - """a synchronizer which locks using flock(). - - Adapted for Python/multithreads from Apache::Session::Lock::File, - http://search.cpan.org/src/CWEST/Apache-Session-1.81/Session/Lock/File.pm - - This module does not unlink temporary files, - because it interferes with proper locking. This can cause - problems on certain systems (Linux) whose file systems (ext2) do not - perform well with lots of files in one directory. To prevent this - you should use a script to clean out old files from your lock directory. - + """A synchronizer which locks using flock(). + """ def __init__(self, identifier, lock_dir): super(FileSynchronizer, self).__init__() self._filedescriptor = util.ThreadLocal() - + if lock_dir is None: lock_dir = tempfile.gettempdir() else: lock_dir = lock_dir self.filename = util.encoded_path( - lock_dir, - [identifier], + lock_dir, + [identifier], extension='.lock' ) def _filedesc(self): return self._filedescriptor.get() _filedesc = property(_filedesc) - + def _open(self, mode): filedescriptor = self._filedesc if filedescriptor is None: filedescriptor = os.open(self.filename, mode) self._filedescriptor.put(filedescriptor) return filedescriptor - + def do_acquire_read_lock(self, wait): filedescriptor = self._open(os.O_CREAT | os.O_RDONLY) if not wait: @@ -259,13 +264,13 @@ class FileSynchronizer(SynchronizerImpl): else: fcntl.flock(filedescriptor, fcntl.LOCK_EX) return True - + def do_release_read_lock(self): self._release_all_locks() - + def do_release_write_lock(self): self._release_all_locks() - + def _release_all_locks(self): filedescriptor = self._filedesc if filedescriptor is not None: @@ -276,7 +281,7 @@ class FileSynchronizer(SynchronizerImpl): class ConditionSynchronizer(SynchronizerImpl): """a synchronizer using a Condition.""" - + def __init__(self, identifier): super(ConditionSynchronizer, self).__init__() @@ -289,7 +294,7 @@ class ConditionSynchronizer(SynchronizerImpl): # condition object to lock on self.condition = _threading.Condition(_threading.Lock()) - def do_acquire_read_lock(self, wait = True): + def do_acquire_read_lock(self, wait=True): self.condition.acquire() try: # see if a synchronous operation is waiting to start @@ -306,15 +311,15 @@ class ConditionSynchronizer(SynchronizerImpl): finally: self.condition.release() - if not wait: + if not wait: return True - + def do_release_read_lock(self): self.condition.acquire() try: self.async -= 1 - - # check if we are the last asynchronous reader thread + + # check if we are the last asynchronous reader thread # out the door. if self.async == 0: # yes. so if a sync operation is waiting, notifyAll to wake @@ -326,13 +331,13 @@ class ConditionSynchronizer(SynchronizerImpl): "release_read_locks called") finally: self.condition.release() - - def do_acquire_write_lock(self, wait = True): + + def do_acquire_write_lock(self, wait=True): self.condition.acquire() try: # here, we are not a synchronous reader, and after returning, # assuming waiting or immediate availability, we will be. - + if wait: # if another sync is working, wait while self.current_sync_operation is not None: @@ -342,8 +347,8 @@ class ConditionSynchronizer(SynchronizerImpl): # we dont want to wait, so forget it if self.current_sync_operation is not None: return False - - # establish ourselves as the current sync + + # establish ourselves as the current sync # this indicates to other read/write operations # that they should wait until this is None again self.current_sync_operation = _threading.currentThread() @@ -359,8 +364,8 @@ class ConditionSynchronizer(SynchronizerImpl): return False finally: self.condition.release() - - if not wait: + + if not wait: return True def do_release_write_lock(self): @@ -370,7 +375,7 @@ class ConditionSynchronizer(SynchronizerImpl): raise LockError("Synchronizer error - current thread doesnt " "have the write lock") - # reset the current sync operation so + # reset the current sync operation so # another can get it self.current_sync_operation = None diff --git a/module/lib/beaker/util.py b/module/lib/beaker/util.py index 04c9617c5..c7002cd92 100644 --- a/module/lib/beaker/util.py +++ b/module/lib/beaker/util.py @@ -9,14 +9,16 @@ except ImportError: from datetime import datetime, timedelta import os +import re import string import types import weakref import warnings import sys +import inspect py3k = getattr(sys, 'py3kwarning', False) or sys.version_info >= (3, 0) -py24 = sys.version_info < (2,5) +py24 = sys.version_info < (2, 5) jython = sys.platform.startswith('java') if py3k or jython: @@ -25,11 +27,56 @@ else: import cPickle as pickle from beaker.converters import asbool +from beaker import exceptions from threading import local as _tlocal -__all__ = ["ThreadLocal", "Registry", "WeakValuedRegistry", "SyncDict", - "encoded_path", "verify_directory"] +__all__ = ["ThreadLocal", "WeakValuedRegistry", "SyncDict", "encoded_path", + "verify_directory"] + + +def function_named(fn, name): + """Return a function with a given __name__. + + Will assign to __name__ and return the original function if possible on + the Python implementation, otherwise a new function will be constructed. + + """ + fn.__name__ = name + return fn + + +def skip_if(predicate, reason=None): + """Skip a test if predicate is true.""" + reason = reason or predicate.__name__ + + from nose import SkipTest + + def decorate(fn): + fn_name = fn.__name__ + + def maybe(*args, **kw): + if predicate(): + msg = "'%s' skipped: %s" % ( + fn_name, reason) + raise SkipTest(msg) + else: + return fn(*args, **kw) + return function_named(maybe, fn_name) + return decorate + + +def assert_raises(except_cls, callable_, *args, **kw): + """Assert the given exception is raised by the given function + arguments.""" + + try: + callable_(*args, **kw) + success = False + except except_cls: + success = True + + # assert outside the block so it works for AssertionError too ! + assert success, "Callable did not raise an exception" def verify_directory(dir): @@ -45,7 +92,24 @@ def verify_directory(dir): if tries > 5: raise - + +def has_self_arg(func): + """Return True if the given function has a 'self' argument.""" + args = inspect.getargspec(func) + if args and args[0] and args[0][0] in ('self', 'cls'): + return True + else: + return False + + +def warn(msg, stacklevel=3): + """Issue a warning.""" + if isinstance(msg, basestring): + warnings.warn(msg, exceptions.BeakerWarning, stacklevel=stacklevel) + else: + warnings.warn(msg, stacklevel=stacklevel) + + def deprecated(message): def wrapper(fn): def deprecated_method(*args, **kargs): @@ -56,7 +120,8 @@ def deprecated(message): deprecated_method.__doc__ = "%s\n\n%s" % (message, fn.__doc__) return deprecated_method return wrapper - + + class ThreadLocal(object): """stores a value on a per-thread basis""" @@ -64,42 +129,43 @@ class ThreadLocal(object): def __init__(self): self._tlocal = _tlocal() - + def put(self, value): self._tlocal.value = value - + def has(self): return hasattr(self._tlocal, 'value') - + def get(self, default=None): return getattr(self._tlocal, 'value', default) - + def remove(self): del self._tlocal.value - + + class SyncDict(object): """ An efficient/threadsafe singleton map algorithm, a.k.a. "get a value based on this key, and create if not found or not valid" paradigm: - + exists && isvalid ? get : create Designed to work with weakref dictionaries to expect items - to asynchronously disappear from the dictionary. + to asynchronously disappear from the dictionary. Use python 2.3.3 or greater ! a major bug was just fixed in Nov. 2003 that was driving me nuts with garbage collection/weakrefs in this section. - """ + """ def __init__(self): self.mutex = _thread.allocate_lock() self.dict = {} - + def get(self, key, createfunc, *args, **kwargs): try: - if self.has_key(key): + if key in self.dict: return self.dict[key] else: return self.sync_get(key, createfunc, *args, **kwargs) @@ -110,7 +176,7 @@ class SyncDict(object): self.mutex.acquire() try: try: - if self.has_key(key): + if key in self.dict: return self.dict[key] else: return self._create(key, createfunc, *args, **kwargs) @@ -124,16 +190,20 @@ class SyncDict(object): return obj def has_key(self, key): - return self.dict.has_key(key) - + return key in self.dict + def __contains__(self, key): return self.dict.__contains__(key) + def __getitem__(self, key): return self.dict.__getitem__(key) + def __setitem__(self, key, value): self.dict.__setitem__(key, value) + def __delitem__(self, key): return self.dict.__delitem__(key) + def clear(self): self.dict.clear() @@ -143,36 +213,47 @@ class WeakValuedRegistry(SyncDict): self.mutex = _threading.RLock() self.dict = weakref.WeakValueDictionary() -sha1 = None -def encoded_path(root, identifiers, extension = ".enc", depth = 3, +sha1 = None + + +def encoded_path(root, identifiers, extension=".enc", depth=3, digest_filenames=True): - + """Generate a unique file-accessible path from the given list of identifiers starting at the given root directory.""" ident = "_".join(identifiers) - + global sha1 if sha1 is None: from beaker.crypto import sha1 - + if digest_filenames: if py3k: ident = sha1(ident.encode('utf-8')).hexdigest() else: ident = sha1(ident).hexdigest() - + ident = os.path.basename(ident) tokens = [] for d in range(1, depth): tokens.append(ident[0:d]) - + dir = os.path.join(root, *tokens) verify_directory(dir) - + return os.path.join(dir, ident + extension) +def asint(obj): + if isinstance(obj, int): + return obj + elif isinstance(obj, basestring) and re.match(r'^\d+$', obj): + return int(obj) + else: + raise Exception("This is not a proper int") + + def verify_options(opt, types, error): if not isinstance(opt, types): if not isinstance(types, tuple): @@ -185,6 +266,11 @@ def verify_options(opt, types, error): else: if typ == bool: typ = asbool + elif typ == int: + typ = asint + elif typ in (timedelta, datetime): + if not isinstance(opt, typ): + raise Exception("%s requires a timedelta type", typ) opt = typ(opt) coerced = True except: @@ -212,10 +298,12 @@ def coerce_session_params(params): ('lock_dir', (str, types.NoneType), "lock_dir must be a string referring to a " "directory."), ('type', (str, types.NoneType), "Session type must be a string."), - ('cookie_expires', (bool, datetime, timedelta), "Cookie expires was " - "not a boolean, datetime, or timedelta instance."), + ('cookie_expires', (bool, datetime, timedelta, int), "Cookie expires was " + "not a boolean, datetime, int, or timedelta instance."), ('cookie_domain', (str, types.NoneType), "Cookie domain must be a " "string."), + ('cookie_path', (str, types.NoneType), "Cookie path must be a " + "string."), ('id', (str,), "Session id must be a string."), ('key', (str,), "Session key must be a string."), ('secret', (str, types.NoneType), "Session secret must be a string."), @@ -224,11 +312,19 @@ def coerce_session_params(params): ('encrypt_key', (str, types.NoneType), "Session validate_key must be " "a string."), ('secure', (bool, types.NoneType), "Session secure must be a boolean."), + ('httponly', (bool, types.NoneType), "Session httponly must be a boolean."), ('timeout', (int, types.NoneType), "Session timeout must be an " "integer."), ('auto', (bool, types.NoneType), "Session is created if accessed."), + ('webtest_varname', (str, types.NoneType), "Session varname must be " + "a string."), ] - return verify_rules(params, rules) + opts = verify_rules(params, rules) + cookie_expires = opts.get('cookie_expires') + if cookie_expires and isinstance(cookie_expires, int) and \ + not isinstance(cookie_expires, bool): + opts['cookie_expires'] = timedelta(seconds=cookie_expires) + return opts def coerce_cache_params(params): @@ -243,18 +339,63 @@ def coerce_cache_params(params): ('expire', (int, types.NoneType), "expire must be an integer representing " "how many seconds the cache is valid for"), ('regions', (list, tuple, types.NoneType), "Regions must be a " - "comma seperated list of valid regions") + "comma seperated list of valid regions"), + ('key_length', (int, types.NoneType), "key_length must be an integer " + "which indicates the longest a key can be before hashing"), ] return verify_rules(params, rules) +def coerce_memcached_behaviors(behaviors): + rules = [ + ('cas', (bool, int), 'cas must be a boolean or an integer'), + ('no_block', (bool, int), 'no_block must be a boolean or an integer'), + ('receive_timeout', (int,), 'receive_timeout must be an integer'), + ('send_timeout', (int,), 'send_timeout must be an integer'), + ('ketama_hash', (str,), 'ketama_hash must be a string designating ' + 'a valid hashing strategy option'), + ('_poll_timeout', (int,), '_poll_timeout must be an integer'), + ('auto_eject', (bool, int), 'auto_eject must be an integer'), + ('retry_timeout', (int,), 'retry_timeout must be an integer'), + ('_sort_hosts', (bool, int), '_sort_hosts must be an integer'), + ('_io_msg_watermark', (int,), '_io_msg_watermark must be an integer'), + ('ketama', (bool, int), 'ketama must be a boolean or an integer'), + ('ketama_weighted', (bool, int), 'ketama_weighted must be a boolean or ' + 'an integer'), + ('_io_key_prefetch', (int, bool), '_io_key_prefetch must be a boolean ' + 'or an integer'), + ('_hash_with_prefix_key', (bool, int), '_hash_with_prefix_key must be ' + 'a boolean or an integer'), + ('tcp_nodelay', (bool, int), 'tcp_nodelay must be a boolean or an ' + 'integer'), + ('failure_limit', (int,), 'failure_limit must be an integer'), + ('buffer_requests', (bool, int), 'buffer_requests must be a boolean ' + 'or an integer'), + ('_socket_send_size', (int,), '_socket_send_size must be an integer'), + ('num_replicas', (int,), 'num_replicas must be an integer'), + ('remove_failed', (int,), 'remove_failed must be an integer'), + ('_noreply', (bool, int), '_noreply must be a boolean or an integer'), + ('_io_bytes_watermark', (int,), '_io_bytes_watermark must be an ' + 'integer'), + ('_socket_recv_size', (int,), '_socket_recv_size must be an integer'), + ('distribution', (str,), 'distribution must be a string designating ' + 'a valid distribution option'), + ('connect_timeout', (int,), 'connect_timeout must be an integer'), + ('hash', (str,), 'hash must be a string designating a valid hashing ' + 'option'), + ('verify_keys', (bool, int), 'verify_keys must be a boolean or an integer'), + ('dead_timeout', (int,), 'dead_timeout must be an integer') + ] + return verify_rules(behaviors, rules) + + def parse_cache_config_options(config, include_defaults=True): """Parse configuration options and validate for use with the CacheManager""" - + # Load default cache options if include_defaults: - options= dict(type='memory', data_dir=None, expire=None, + options = dict(type='memory', data_dir=None, expire=None, log_file=None) else: options = {} @@ -264,39 +405,58 @@ def parse_cache_config_options(config, include_defaults=True): if key.startswith('cache.'): options[key[6:]] = val coerce_cache_params(options) - + # Set cache to enabled if not turned off - if 'enabled' not in options: + if 'enabled' not in options and include_defaults: options['enabled'] = True - + # Configure region dict if regions are available regions = options.pop('regions', None) if regions: region_configs = {} for region in regions: + if not region: # ensure region name is valid + continue # Setup the default cache options region_options = dict(data_dir=options.get('data_dir'), lock_dir=options.get('lock_dir'), type=options.get('type'), enabled=options['enabled'], - expire=options.get('expire')) - region_len = len(region) + 1 + expire=options.get('expire'), + key_length=options.get('key_length', 250)) + region_prefix = '%s.' % region + region_len = len(region_prefix) for key in options.keys(): - if key.startswith('%s.' % region): + if key.startswith(region_prefix): region_options[key[region_len:]] = options.pop(key) coerce_cache_params(region_options) region_configs[region] = region_options options['cache_regions'] = region_configs return options + +def parse_memcached_behaviors(config): + """Parse behavior options and validate for use with pylibmc + client/PylibMCNamespaceManager, or potentially other memcached + NamespaceManagers that support behaviors""" + behaviors = {} + + for key, val in config.iteritems(): + if key.startswith('behavior.'): + behaviors[key[9:]] = val + + coerce_memcached_behaviors(behaviors) + return behaviors + + def func_namespace(func): """Generates a unique namespace for a function""" kls = None if hasattr(func, 'im_func'): kls = func.im_class func = func.im_func - + if kls: return '%s.%s' % (kls.__module__, kls.__name__) else: - return '%s.%s' % (func.__module__, func.__name__) + return '%s|%s' % (inspect.getsourcefile(func), func.__name__) |