| # (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) |