summaryrefslogtreecommitdiffstats
path: root/pyload/lib/beaker/session.py
diff options
context:
space:
mode:
authorGravatar Walter Purcaro <vuolter@gmail.com> 2014-09-08 00:29:57 +0200
committerGravatar Walter Purcaro <vuolter@gmail.com> 2014-09-14 11:02:23 +0200
commit68d662e689cd42687341c550fb6ebb74e6968d21 (patch)
tree486cef41bd928b8db704894233b2cef94a6e346f /pyload/lib/beaker/session.py
parentsave_join -> safe_join & save_path -> safe_filename (diff)
downloadpyload-68d662e689cd42687341c550fb6ebb74e6968d21.tar.xz
module -> pyload
Diffstat (limited to 'pyload/lib/beaker/session.py')
-rw-r--r--pyload/lib/beaker/session.py726
1 files changed, 726 insertions, 0 deletions
diff --git a/pyload/lib/beaker/session.py b/pyload/lib/beaker/session.py
new file mode 100644
index 000000000..d70a670eb
--- /dev/null
+++ b/pyload/lib/beaker/session.py
@@ -0,0 +1,726 @@
+import Cookie
+import os
+from datetime import datetime, timedelta
+import time
+from beaker.crypto import hmac as HMAC, hmac_sha1 as SHA1, md5
+from beaker import crypto, util
+from beaker.cache import clsmap
+from beaker.exceptions import BeakerException, InvalidCryptoBackendError
+from base64 import b64encode, b64decode
+
+
+__all__ = ['SignedCookie', 'Session']
+
+
+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.encode('UTF-8')
+ Cookie.BaseCookie.__init__(self, input)
+
+ def value_decode(self, val):
+ val = val.strip('"')
+ 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.encode('UTF-8'), SHA1).hexdigest()
+ return str(val), ("%s%s" % (sig, val))
+
+
+class Session(dict):
+ """Session object that uses container package for storage.
+
+ :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, 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'
+ else:
+ self.type = 'memory'
+ else:
+ self.type = type
+
+ 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 = 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:
+ try:
+ self.cookie = SignedCookie(secret, input=cookieheader)
+ except Cookie.CookieError:
+ 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()
+ self['_accessed_time'] = self['_creation_time'] = time.time()
+ else:
+ try:
+ self.load()
+ 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 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)
+ elif isinstance(self.cookie_expires, timedelta):
+ 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))
+ 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']
+
+ def _set_domain(self, domain):
+ self['_domain'] = domain
+ self.cookie[self.key]['domain'] = domain
+ self._update_cookie_out()
+
+ def _get_domain(self):
+ return self._domain
+
+ domain = property(_get_domain, _set_domain)
+
+ def _set_path(self, path):
+ self['_path'] = self._path = path
+ self.cookie[self.key]['path'] = path
+ 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
+ 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
+ an expired cookie out"""
+ if self.use_cookies:
+ self._delete_cookie()
+ self.clear()
+
+ def invalidate(self):
+ """Invalidates this session, creates a new session id, returns
+ to the is_new state"""
+ self.clear()
+ 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,
+ **self.namespace_args)
+ now = time.time()
+ if self.use_cookies:
+ self.request['set_cookie'] = True
+
+ self.namespace.acquire_read_lock()
+ timed_out = False
+ try:
+ self.clear()
+ 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
+ }
+ self.is_new = True
+ except (KeyError, TypeError):
+ session_data = {
+ '_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
+ else:
+ # Properly set the last_accessed time, which is different
+ # than the *currently* _accessed_time
+ if self.is_new or '_accessed_time' not in session_data:
+ 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()
+ 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
+
+ # 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,
+ data_dir=self.data_dir,
+ digest_filenames=False,
+ **self.namespace_args)
+
+ 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']
+ else:
+ self.namespace['session'] = data
+ finally:
+ self.namespace.release_write_lock()
+ 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.
+
+ """
+ self.namespace.acquire_write_lock()
+
+ def unlock(self):
+ """Unlocks this session against other processes/threads. This
+ is automatic when load/save is called.
+
+ ***use with caution*** and always within a "finally:" block, as
+ a stray lock typically cannot be unlocked without shutting down
+ the whole application.
+
+ """
+ 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.
+
+ :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, 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
+ self.cookie_expires = cookie_expires
+ self.encrypt_key = encrypt_key
+ self.validate_key = validate_key
+ self.request['set_cookie'] = False
+ self.secure = secure
+ self.httponly = httponly
+ self._domain = cookie_domain
+ 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'] = _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:
+ 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() - \
+ self['_accessed_time'] > self.timeout:
+ 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)
+
+ 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'] = self._path = path
+
+ def _get_path(self):
+ return self._path
+
+ path = property(_get_path, _set_path)
+
+ def save(self, accessed_only=False):
+ """Saves the data for this session to persistent storage"""
+ if accessed_only and self.is_new:
+ return
+ if accessed_only:
+ 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'] = _session_id()
+ self['_accessed_time'] = time.time()
+
+ 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', '/')
+
+ 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.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'] = {}
+
+ 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}
+ req['cookie'] = environ.get('HTTP_COOKIE')
+ if params.get('type') == 'cookie':
+ self.__dict__['_sess'] = CookieSession(req, **params)
+ else:
+ 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 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']
+ session = Session({}, use_cookies=False, id=id, **params)
+ 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()
+ else:
+ if self.__dict__.get('_dirty'):
+ 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