# (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 """ Creates a session object in your WSGI environment. Use like: ..code-block:: Python environ['paste.session.factory']() This will return a dictionary. The contents of this dictionary will be saved to disk when the request is completed. The session will be created when you first fetch the session dictionary, and a cookie will be sent in that case. There's current no way to use sessions without cookies, and there's no way to delete a session except to clear its data. @@: This doesn't do any locking, and may cause problems when a single session is accessed concurrently. Also, it loads and saves the session for each request, with no caching. Also, sessions aren't expired. """ try: # Python 3 from http.cookies import SimpleCookie except ImportError: # Python 2 from Cookie import SimpleCookie import time import random import os import datetime import six import threading import tempfile try: import cPickle except ImportError: import pickle as cPickle try: from hashlib import md5 except ImportError: from md5 import md5 from paste import wsgilib from paste import request class SessionMiddleware(object): def __init__(self, application, global_conf=None, **factory_kw): self.application = application self.factory_kw = factory_kw def __call__(self, environ, start_response): session_factory = SessionFactory(environ, **self.factory_kw) environ['paste.session.factory'] = session_factory remember_headers = [] def session_start_response(status, headers, exc_info=None): if not session_factory.created: remember_headers[:] = [status, headers] return start_response(status, headers) headers.append(session_factory.set_cookie_header()) return start_response(status, headers, exc_info) app_iter = self.application(environ, session_start_response) def start(): if session_factory.created and remember_headers: # Tricky bastard used the session after start_response status, headers = remember_headers headers.append(session_factory.set_cookie_header()) exc = ValueError( "You cannot get the session after content from the " "app_iter has been returned") start_response(status, headers, (exc.__class__, exc, None)) def close(): if session_factory.used: session_factory.close() return wsgilib.add_start_close(app_iter, start, close) class SessionFactory(object): def __init__(self, environ, cookie_name='_SID_', session_class=None, session_expiration=60*12, # in minutes **session_class_kw): self.created = False self.used = False self.environ = environ self.cookie_name = cookie_name self.session = None self.session_class = session_class or FileSession self.session_class_kw = session_class_kw self.expiration = session_expiration def __call__(self): self.used = True if self.session is not None: return self.session.data() cookies = request.get_cookies(self.environ) session = None if self.cookie_name in cookies: self.sid = cookies[self.cookie_name].value try: session = self.session_class(self.sid, create=False, **self.session_class_kw) except KeyError: # Invalid SID pass if session is None: self.created = True self.sid = self.make_sid() session = self.session_class(self.sid, create=True, **self.session_class_kw) session.clean_up() self.session = session return session.data() def has_session(self): if self.session is not None: return True cookies = request.get_cookies(self.environ) if cookies.has_key(self.cookie_name): return True return False def make_sid(self): # @@: need better algorithm return (''.join(['%02d' % x for x in time.localtime(time.time())[:6]]) + '-' + self.unique_id()) def unique_id(self, for_object=None): """ Generates an opaque, identifier string that is practically guaranteed to be unique. If an object is passed, then its id() is incorporated into the generation. Relies on md5 and returns a 32 character long string. """ r = [time.time(), random.random()] if hasattr(os, 'times'): r.append(os.times()) if for_object is not None: r.append(id(for_object)) content = str(r) if six.PY3: content = content.encode('utf8') md5_hash = md5(content) try: return md5_hash.hexdigest() except AttributeError: # Older versions of Python didn't have hexdigest, so we'll # do it manually hexdigest = [] for char in md5_hash.digest(): hexdigest.append('%02x' % ord(char)) return ''.join(hexdigest) def set_cookie_header(self): c = SimpleCookie() c[self.cookie_name] = self.sid c[self.cookie_name]['path'] = '/' gmt_expiration_time = time.gmtime(time.time() + (self.expiration * 60)) c[self.cookie_name]['expires'] = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", gmt_expiration_time) name, value = str(c).split(': ', 1) return (name, value) def close(self): if self.session is not None: self.session.close() last_cleanup = None cleaning_up = False cleanup_cycle = datetime.timedelta(seconds=15*60) #15 min class FileSession(object): def __init__(self, sid, create=False, session_file_path=tempfile.gettempdir(), chmod=None, expiration=2880, # in minutes: 48 hours ): if chmod and isinstance(chmod, (six.binary_type, six.text_type)): chmod = int(chmod, 8) self.chmod = chmod if not sid: # Invalid... raise KeyError self.session_file_path = session_file_path self.sid = sid if not create: if not os.path.exists(self.filename()): raise KeyError self._data = None self.expiration = expiration def filename(self): return os.path.join(self.session_file_path, self.sid) def data(self): if self._data is not None: return self._data if os.path.exists(self.filename()): f = open(self.filename(), 'rb') self._data = cPickle.load(f) f.close() else: self._data = {} return self._data def close(self): if self._data is not None: filename = self.filename() exists = os.path.exists(filename) if not self._data: if exists: os.unlink(filename) else: f = open(filename, 'wb') cPickle.dump(self._data, f) f.close() if not exists and self.chmod: os.chmod(filename, self.chmod) def _clean_up(self): global cleaning_up try: exp_time = datetime.timedelta(seconds=self.expiration*60) now = datetime.datetime.now() #Open every session and check that it isn't too old for root, dirs, files in os.walk(self.session_file_path): for f in files: self._clean_up_file(f, exp_time=exp_time, now=now) finally: cleaning_up = False def _clean_up_file(self, f, exp_time, now): t = f.split("-") if len(t) != 2: return t = t[0] try: sess_time = datetime.datetime( int(t[0:4]), int(t[4:6]), int(t[6:8]), int(t[8:10]), int(t[10:12]), int(t[12:14])) except ValueError: # Probably not a session file at all return if sess_time + exp_time < now: os.remove(os.path.join(self.session_file_path, f)) def clean_up(self): global last_cleanup, cleanup_cycle, cleaning_up now = datetime.datetime.now() if cleaning_up: return if not last_cleanup or last_cleanup + cleanup_cycle < now: if not cleaning_up: cleaning_up = True try: last_cleanup = now t = threading.Thread(target=self._clean_up) t.start() except: # Normally _clean_up should set cleaning_up # to false, but if something goes wrong starting # it... cleaning_up = False raise class _NoDefault(object): def __repr__(self): return '<dynamic default>' NoDefault = _NoDefault() def make_session_middleware( app, global_conf, session_expiration=NoDefault, expiration=NoDefault, cookie_name=NoDefault, session_file_path=NoDefault, chmod=NoDefault): """ Adds a middleware that handles sessions for your applications. The session is a peristent dictionary. To get this dictionary in your application, use ``environ['paste.session.factory']()`` which returns this persistent dictionary. Configuration: session_expiration: The time each session lives, in minutes. This controls the cookie expiration. Default 12 hours. expiration: The time each session lives on disk. Old sessions are culled from disk based on this. Default 48 hours. cookie_name: The cookie name used to track the session. Use different names to avoid session clashes. session_file_path: Sessions are put in this location, default /tmp. chmod: The octal chmod you want to apply to new sessions (e.g., 660 to make the sessions group readable/writable) Each of these also takes from the global configuration. cookie_name and chmod take from session_cookie_name and session_chmod """ if session_expiration is NoDefault: session_expiration = global_conf.get('session_expiration', 60*12) session_expiration = int(session_expiration) if expiration is NoDefault: expiration = global_conf.get('expiration', 60*48) expiration = int(expiration) if cookie_name is NoDefault: cookie_name = global_conf.get('session_cookie_name', '_SID_') if session_file_path is NoDefault: session_file_path = global_conf.get('session_file_path', '/tmp') if chmod is NoDefault: chmod = global_conf.get('session_chmod', None) return SessionMiddleware( app, session_expiration=session_expiration, expiration=expiration, cookie_name=cookie_name, session_file_path=session_file_path, chmod=chmod)