Home | History | Annotate | Download | only in paste
      1 # (c) 2005 Ian Bicking and contributors; written for Paste (http://pythonpaste.org)
      2 # Licensed under the MIT license: http://www.opensource.org/licenses/mit-license.php
      3 
      4 """
      5 Creates a session object in your WSGI environment.
      6 
      7 Use like:
      8 
      9 ..code-block:: Python
     10 
     11     environ['paste.session.factory']()
     12 
     13 This will return a dictionary.  The contents of this dictionary will
     14 be saved to disk when the request is completed.  The session will be
     15 created when you first fetch the session dictionary, and a cookie will
     16 be sent in that case.  There's current no way to use sessions without
     17 cookies, and there's no way to delete a session except to clear its
     18 data.
     19 
     20 @@: This doesn't do any locking, and may cause problems when a single
     21 session is accessed concurrently.  Also, it loads and saves the
     22 session for each request, with no caching.  Also, sessions aren't
     23 expired.
     24 """
     25 
     26 try:
     27     # Python 3
     28     from http.cookies import SimpleCookie
     29 except ImportError:
     30     # Python 2
     31     from Cookie import SimpleCookie
     32 import time
     33 import random
     34 import os
     35 import datetime
     36 import six
     37 import threading
     38 import tempfile
     39 
     40 try:
     41     import cPickle
     42 except ImportError:
     43     import pickle as cPickle
     44 try:
     45     from hashlib import md5
     46 except ImportError:
     47     from md5 import md5
     48 from paste import wsgilib
     49 from paste import request
     50 
     51 class SessionMiddleware(object):
     52 
     53     def __init__(self, application, global_conf=None, **factory_kw):
     54         self.application = application
     55         self.factory_kw = factory_kw
     56 
     57     def __call__(self, environ, start_response):
     58         session_factory = SessionFactory(environ, **self.factory_kw)
     59         environ['paste.session.factory'] = session_factory
     60         remember_headers = []
     61 
     62         def session_start_response(status, headers, exc_info=None):
     63             if not session_factory.created:
     64                 remember_headers[:] = [status, headers]
     65                 return start_response(status, headers)
     66             headers.append(session_factory.set_cookie_header())
     67             return start_response(status, headers, exc_info)
     68 
     69         app_iter = self.application(environ, session_start_response)
     70         def start():
     71             if session_factory.created and remember_headers:
     72                 # Tricky bastard used the session after start_response
     73                 status, headers = remember_headers
     74                 headers.append(session_factory.set_cookie_header())
     75                 exc = ValueError(
     76                     "You cannot get the session after content from the "
     77                     "app_iter has been returned")
     78                 start_response(status, headers, (exc.__class__, exc, None))
     79         def close():
     80             if session_factory.used:
     81                 session_factory.close()
     82         return wsgilib.add_start_close(app_iter, start, close)
     83 
     84 
     85 class SessionFactory(object):
     86 
     87 
     88     def __init__(self, environ, cookie_name='_SID_',
     89                  session_class=None,
     90                  session_expiration=60*12, # in minutes
     91                  **session_class_kw):
     92 
     93         self.created = False
     94         self.used = False
     95         self.environ = environ
     96         self.cookie_name = cookie_name
     97         self.session = None
     98         self.session_class = session_class or FileSession
     99         self.session_class_kw = session_class_kw
    100 
    101         self.expiration = session_expiration
    102 
    103     def __call__(self):
    104         self.used = True
    105         if self.session is not None:
    106             return self.session.data()
    107         cookies = request.get_cookies(self.environ)
    108         session = None
    109         if self.cookie_name in cookies:
    110             self.sid = cookies[self.cookie_name].value
    111             try:
    112                 session = self.session_class(self.sid, create=False,
    113                                              **self.session_class_kw)
    114             except KeyError:
    115                 # Invalid SID
    116                 pass
    117         if session is None:
    118             self.created = True
    119             self.sid = self.make_sid()
    120             session = self.session_class(self.sid, create=True,
    121                                          **self.session_class_kw)
    122         session.clean_up()
    123         self.session = session
    124         return session.data()
    125 
    126     def has_session(self):
    127         if self.session is not None:
    128             return True
    129         cookies = request.get_cookies(self.environ)
    130         if cookies.has_key(self.cookie_name):
    131             return True
    132         return False
    133 
    134     def make_sid(self):
    135         # @@: need better algorithm
    136         return (''.join(['%02d' % x for x in time.localtime(time.time())[:6]])
    137                 + '-' + self.unique_id())
    138 
    139     def unique_id(self, for_object=None):
    140         """
    141         Generates an opaque, identifier string that is practically
    142         guaranteed to be unique.  If an object is passed, then its
    143         id() is incorporated into the generation.  Relies on md5 and
    144         returns a 32 character long string.
    145         """
    146         r = [time.time(), random.random()]
    147         if hasattr(os, 'times'):
    148             r.append(os.times())
    149         if for_object is not None:
    150             r.append(id(for_object))
    151         content = str(r)
    152         if six.PY3:
    153             content = content.encode('utf8')
    154         md5_hash = md5(content)
    155         try:
    156             return md5_hash.hexdigest()
    157         except AttributeError:
    158             # Older versions of Python didn't have hexdigest, so we'll
    159             # do it manually
    160             hexdigest = []
    161             for char in md5_hash.digest():
    162                 hexdigest.append('%02x' % ord(char))
    163             return ''.join(hexdigest)
    164 
    165     def set_cookie_header(self):
    166         c = SimpleCookie()
    167         c[self.cookie_name] = self.sid
    168         c[self.cookie_name]['path'] = '/'
    169 
    170         gmt_expiration_time = time.gmtime(time.time() + (self.expiration * 60))
    171         c[self.cookie_name]['expires'] = time.strftime("%a, %d-%b-%Y %H:%M:%S GMT", gmt_expiration_time)
    172 
    173         name, value = str(c).split(': ', 1)
    174         return (name, value)
    175 
    176     def close(self):
    177         if self.session is not None:
    178             self.session.close()
    179 
    180 
    181 last_cleanup = None
    182 cleaning_up = False
    183 cleanup_cycle = datetime.timedelta(seconds=15*60) #15 min
    184 
    185 class FileSession(object):
    186 
    187     def __init__(self, sid, create=False, session_file_path=tempfile.gettempdir(),
    188                  chmod=None,
    189                  expiration=2880, # in minutes: 48 hours
    190                  ):
    191         if chmod and isinstance(chmod, (six.binary_type, six.text_type)):
    192             chmod = int(chmod, 8)
    193         self.chmod = chmod
    194         if not sid:
    195             # Invalid...
    196             raise KeyError
    197         self.session_file_path = session_file_path
    198         self.sid = sid
    199         if not create:
    200             if not os.path.exists(self.filename()):
    201                 raise KeyError
    202         self._data = None
    203 
    204         self.expiration = expiration
    205 
    206 
    207     def filename(self):
    208         return os.path.join(self.session_file_path, self.sid)
    209 
    210     def data(self):
    211         if self._data is not None:
    212             return self._data
    213         if os.path.exists(self.filename()):
    214             f = open(self.filename(), 'rb')
    215             self._data = cPickle.load(f)
    216             f.close()
    217         else:
    218             self._data = {}
    219         return self._data
    220 
    221     def close(self):
    222         if self._data is not None:
    223             filename = self.filename()
    224             exists = os.path.exists(filename)
    225             if not self._data:
    226                 if exists:
    227                     os.unlink(filename)
    228             else:
    229                 f = open(filename, 'wb')
    230                 cPickle.dump(self._data, f)
    231                 f.close()
    232                 if not exists and self.chmod:
    233                     os.chmod(filename, self.chmod)
    234 
    235     def _clean_up(self):
    236         global cleaning_up
    237         try:
    238             exp_time = datetime.timedelta(seconds=self.expiration*60)
    239             now = datetime.datetime.now()
    240 
    241             #Open every session and check that it isn't too old
    242             for root, dirs, files in os.walk(self.session_file_path):
    243                 for f in files:
    244                     self._clean_up_file(f, exp_time=exp_time, now=now)
    245         finally:
    246             cleaning_up = False
    247 
    248     def _clean_up_file(self, f, exp_time, now):
    249         t = f.split("-")
    250         if len(t) != 2:
    251             return
    252         t = t[0]
    253         try:
    254             sess_time = datetime.datetime(
    255                     int(t[0:4]),
    256                     int(t[4:6]),
    257                     int(t[6:8]),
    258                     int(t[8:10]),
    259                     int(t[10:12]),
    260                     int(t[12:14]))
    261         except ValueError:
    262             # Probably not a session file at all
    263             return
    264 
    265         if sess_time + exp_time < now:
    266             os.remove(os.path.join(self.session_file_path, f))
    267 
    268     def clean_up(self):
    269         global last_cleanup, cleanup_cycle, cleaning_up
    270         now = datetime.datetime.now()
    271 
    272         if cleaning_up:
    273             return
    274 
    275         if not last_cleanup or last_cleanup + cleanup_cycle < now:
    276             if not cleaning_up:
    277                 cleaning_up = True
    278                 try:
    279                     last_cleanup = now
    280                     t = threading.Thread(target=self._clean_up)
    281                     t.start()
    282                 except:
    283                     # Normally _clean_up should set cleaning_up
    284                     # to false, but if something goes wrong starting
    285                     # it...
    286                     cleaning_up = False
    287                     raise
    288 
    289 class _NoDefault(object):
    290     def __repr__(self):
    291         return '<dynamic default>'
    292 NoDefault = _NoDefault()
    293 
    294 def make_session_middleware(
    295     app, global_conf,
    296     session_expiration=NoDefault,
    297     expiration=NoDefault,
    298     cookie_name=NoDefault,
    299     session_file_path=NoDefault,
    300     chmod=NoDefault):
    301     """
    302     Adds a middleware that handles sessions for your applications.
    303     The session is a peristent dictionary.  To get this dictionary
    304     in your application, use ``environ['paste.session.factory']()``
    305     which returns this persistent dictionary.
    306 
    307     Configuration:
    308 
    309       session_expiration:
    310           The time each session lives, in minutes.  This controls
    311           the cookie expiration.  Default 12 hours.
    312 
    313       expiration:
    314           The time each session lives on disk.  Old sessions are
    315           culled from disk based on this.  Default 48 hours.
    316 
    317       cookie_name:
    318           The cookie name used to track the session.  Use different
    319           names to avoid session clashes.
    320 
    321       session_file_path:
    322           Sessions are put in this location, default /tmp.
    323 
    324       chmod:
    325           The octal chmod you want to apply to new sessions (e.g., 660
    326           to make the sessions group readable/writable)
    327 
    328     Each of these also takes from the global configuration.  cookie_name
    329     and chmod take from session_cookie_name and session_chmod
    330     """
    331     if session_expiration is NoDefault:
    332         session_expiration = global_conf.get('session_expiration', 60*12)
    333     session_expiration = int(session_expiration)
    334     if expiration is NoDefault:
    335         expiration = global_conf.get('expiration', 60*48)
    336     expiration = int(expiration)
    337     if cookie_name is NoDefault:
    338         cookie_name = global_conf.get('session_cookie_name', '_SID_')
    339     if session_file_path is NoDefault:
    340         session_file_path = global_conf.get('session_file_path', '/tmp')
    341     if chmod is NoDefault:
    342         chmod = global_conf.get('session_chmod', None)
    343     return SessionMiddleware(
    344         app, session_expiration=session_expiration,
    345         expiration=expiration, cookie_name=cookie_name,
    346         session_file_path=session_file_path, chmod=chmod)
    347