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"""
5Creates a session object in your WSGI environment.
6
7Use like:
8
9..code-block:: Python
10
11    environ['paste.session.factory']()
12
13This will return a dictionary.  The contents of this dictionary will
14be saved to disk when the request is completed.  The session will be
15created when you first fetch the session dictionary, and a cookie will
16be sent in that case.  There's current no way to use sessions without
17cookies, and there's no way to delete a session except to clear its
18data.
19
20@@: This doesn't do any locking, and may cause problems when a single
21session is accessed concurrently.  Also, it loads and saves the
22session for each request, with no caching.  Also, sessions aren't
23expired.
24"""
25
26try:
27    # Python 3
28    from http.cookies import SimpleCookie
29except ImportError:
30    # Python 2
31    from Cookie import SimpleCookie
32import time
33import random
34import os
35import datetime
36import six
37import threading
38import tempfile
39
40try:
41    import cPickle
42except ImportError:
43    import pickle as cPickle
44try:
45    from hashlib import md5
46except ImportError:
47    from md5 import md5
48from paste import wsgilib
49from paste import request
50
51class 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
85class 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
181last_cleanup = None
182cleaning_up = False
183cleanup_cycle = datetime.timedelta(seconds=15*60) #15 min
184
185class 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
289class _NoDefault(object):
290    def __repr__(self):
291        return '<dynamic default>'
292NoDefault = _NoDefault()
293
294def 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