summaryrefslogtreecommitdiff
path: root/paste/session.py
diff options
context:
space:
mode:
Diffstat (limited to 'paste/session.py')
-rw-r--r--paste/session.py346
1 files changed, 346 insertions, 0 deletions
diff --git a/paste/session.py b/paste/session.py
new file mode 100644
index 0000000..ae208e7
--- /dev/null
+++ b/paste/session.py
@@ -0,0 +1,346 @@
+# (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)