diff options
Diffstat (limited to 'pyload/lib/beaker/session.py')
| -rw-r--r-- | pyload/lib/beaker/session.py | 726 | 
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 | 
