~ubuntu-branches/ubuntu/natty/flup/natty

« back to all changes in this revision

Viewing changes to flup/middleware/session.py

  • Committer: Bazaar Package Importer
  • Author(s): Kai Hendry
  • Date: 2007-09-12 20:22:04 UTC
  • mfrom: (1.2.1 upstream) (4 gutsy)
  • mto: This revision was merged to the branch mainline in revision 5.
  • Revision ID: james.westby@ubuntu.com-20070912202204-fg63etr9vzaf8hea
* New upstream release
* http://www.saddi.com/software/news/archives/58-flup-1.0-released.html
* Added a note in the description that people should probably start thinking
  of moving to modwsgi.org

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (c) 2005, 2006 Allan Saddi <allan@saddi.com>
2
 
# All rights reserved.
3
 
#
4
 
# Redistribution and use in source and binary forms, with or without
5
 
# modification, are permitted provided that the following conditions
6
 
# are met:
7
 
# 1. Redistributions of source code must retain the above copyright
8
 
#    notice, this list of conditions and the following disclaimer.
9
 
# 2. Redistributions in binary form must reproduce the above copyright
10
 
#    notice, this list of conditions and the following disclaimer in the
11
 
#    documentation and/or other materials provided with the distribution.
12
 
#
13
 
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
14
 
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15
 
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16
 
# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
17
 
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
18
 
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
19
 
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
20
 
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
21
 
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
22
 
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
23
 
# SUCH DAMAGE.
24
 
#
25
 
# $Id: session.py 2104 2006-11-11 00:38:26Z asaddi $
26
 
 
27
 
__author__ = 'Allan Saddi <allan@saddi.com>'
28
 
__version__ = '$Revision: 2104 $'
29
 
 
30
 
import os
31
 
import errno
32
 
import string
33
 
import time
34
 
import weakref
35
 
import atexit
36
 
import shelve
37
 
import cPickle as pickle
38
 
 
39
 
try:
40
 
    import threading
41
 
except ImportError:
42
 
    import dummy_threading as threading
43
 
 
44
 
__all__ = ['Session',
45
 
           'SessionStore',
46
 
           'MemorySessionStore',
47
 
           'ShelveSessionStore',
48
 
           'DiskSessionStore',
49
 
           'SessionMiddleware']
50
 
 
51
 
class Session(dict):
52
 
    """
53
 
    Session objects, basically dictionaries.
54
 
    """
55
 
    identifierLength = 32
56
 
    # Would be nice if len(identifierChars) were some power of 2.
57
 
    identifierChars = string.digits + string.ascii_letters + '-_'
58
 
 
59
 
    def __init__(self, identifier):
60
 
        super(Session, self).__init__()
61
 
 
62
 
        assert self.isIdentifierValid(identifier)
63
 
        self._identifier = identifier
64
 
 
65
 
        self._creationTime = self._lastAccessTime = time.time()
66
 
        self._isValid = True
67
 
 
68
 
    def _get_identifier(self):
69
 
        return self._identifier
70
 
    identifier = property(_get_identifier, None, None,
71
 
                          'Unique identifier for Session within its Store')
72
 
 
73
 
    def _get_creationTime(self):
74
 
        return self._creationTime
75
 
    creationTime = property(_get_creationTime, None, None,
76
 
                            'Time when Session was created')
77
 
 
78
 
    def _get_lastAccessTime(self):
79
 
        return self._lastAccessTime
80
 
    lastAccessTime = property(_get_lastAccessTime, None, None,
81
 
                              'Time when Session was last accessed')
82
 
 
83
 
    def _get_isValid(self):
84
 
        return self._isValid
85
 
    isValid = property(_get_isValid, None, None,
86
 
                       'Whether or not this Session is valid')
87
 
 
88
 
    def touch(self):
89
 
        """Update Session's access time."""
90
 
        self._lastAccessTime = time.time()
91
 
 
92
 
    def invalidate(self):
93
 
        """Invalidate this Session."""
94
 
        self.clear()
95
 
        self._creationTime = self._lastAccessTime = 0
96
 
        self._isValid = False
97
 
 
98
 
    def isIdentifierValid(cls, ident):
99
 
        """
100
 
        Returns whether or not the given string *could be* a valid session
101
 
        identifier.
102
 
        """
103
 
        if type(ident) is str and len(ident) == cls.identifierLength:
104
 
            for c in ident:
105
 
                if c not in cls.identifierChars:
106
 
                    return False
107
 
            return True
108
 
        return False
109
 
    isIdentifierValid = classmethod(isIdentifierValid)
110
 
 
111
 
    def generateIdentifier(cls):
112
 
        """
113
 
        Generate a random session identifier.
114
 
        """
115
 
        raw = os.urandom(cls.identifierLength)
116
 
 
117
 
        sessId = ''
118
 
        for c in raw:
119
 
            # So we lose 2 bits per random byte...
120
 
            sessId += cls.identifierChars[ord(c) % len(cls.identifierChars)]
121
 
        return sessId
122
 
    generateIdentifier = classmethod(generateIdentifier)
123
 
 
124
 
def _shutdown(ref):
125
 
    store = ref()
126
 
    if store is not None:
127
 
        store.shutdown()
128
 
 
129
 
class SessionStore(object):
130
 
    """
131
 
    Abstract base class for session stores. You first acquire a session by
132
 
    calling createSession() or checkOutSession(). After using the session,
133
 
    you must call checkInSession(). You must not keep references to sessions
134
 
    outside of a check in/check out block. Always obtain a fresh reference.
135
 
 
136
 
    Some external mechanism must be set up to call periodic() periodically
137
 
    (perhaps every 5 minutes).
138
 
 
139
 
    After timeout minutes of inactivity, sessions are deleted.
140
 
    """
141
 
    _sessionClass = Session
142
 
 
143
 
    def __init__(self, timeout=60, sessionClass=None):
144
 
        self._lock = threading.Condition()
145
 
 
146
 
        # Timeout in minutes
147
 
        self._sessionTimeout = timeout
148
 
 
149
 
        if sessionClass is not None:
150
 
            self._sessionClass = sessionClass
151
 
 
152
 
        self._checkOutList = {}
153
 
        self._shutdownRan = False
154
 
 
155
 
        # Ensure shutdown is called.
156
 
        atexit.register(_shutdown, weakref.ref(self))
157
 
 
158
 
    # Public interface.
159
 
 
160
 
    def createSession(self):
161
 
        """
162
 
        Create a new session with a unique identifier. Should never fail.
163
 
        (Will raise a RuntimeError in the rare event that it does.)
164
 
 
165
 
        The newly-created session should eventually be released by
166
 
        a call to checkInSession().
167
 
        """
168
 
        assert not self._shutdownRan
169
 
        self._lock.acquire()
170
 
        try:
171
 
            attempts = 0
172
 
            while attempts < 10000:
173
 
                sessId = self._sessionClass.generateIdentifier()
174
 
                sess = self._createSession(sessId)
175
 
                if sess is not None: break
176
 
                attempts += 1
177
 
 
178
 
            if attempts >= 10000:
179
 
                raise RuntimeError, self.__class__.__name__ + \
180
 
                      '.createSession() failed'
181
 
 
182
 
            assert sess.identifier not in self._checkOutList
183
 
            self._checkOutList[sess.identifier] = sess
184
 
            return sess
185
 
        finally:
186
 
            self._lock.release()
187
 
 
188
 
    def checkOutSession(self, identifier):
189
 
        """
190
 
        Checks out a session for use. Returns the session if it exists,
191
 
        otherwise returns None. If this call succeeds, the session
192
 
        will be touch()'ed and locked from use by other processes.
193
 
        Therefore, it should eventually be released by a call to
194
 
        checkInSession().
195
 
        """
196
 
        assert not self._shutdownRan
197
 
 
198
 
        if not self._sessionClass.isIdentifierValid(identifier):
199
 
            return None
200
 
 
201
 
        self._lock.acquire()
202
 
        try:
203
 
            # If we know it's already checked out, block.
204
 
            while identifier in self._checkOutList:
205
 
                self._lock.wait()
206
 
            sess = self._loadSession(identifier)
207
 
            if sess is not None:
208
 
                if sess.isValid:
209
 
                    assert sess.identifier not in self._checkOutList
210
 
                    self._checkOutList[sess.identifier] = sess
211
 
                    sess.touch()
212
 
                else:
213
 
                    # No longer valid (same as not existing). Delete/unlock
214
 
                    # the session.
215
 
                    self._deleteSession(sess.identifier)
216
 
                    sess = None
217
 
            return sess
218
 
        finally:
219
 
            self._lock.release()
220
 
 
221
 
    def checkInSession(self, session):
222
 
        """
223
 
        Returns the session for use by other threads/processes. Safe to
224
 
        pass None.
225
 
        """
226
 
        assert not self._shutdownRan
227
 
 
228
 
        if session is None:
229
 
            return
230
 
 
231
 
        self._lock.acquire()
232
 
        try:
233
 
            assert session.identifier in self._checkOutList
234
 
            if session.isValid:
235
 
                self._saveSession(session)
236
 
            else:
237
 
                self._deleteSession(session.identifier)
238
 
            del self._checkOutList[session.identifier]
239
 
            self._lock.notify()
240
 
        finally:
241
 
            self._lock.release()
242
 
 
243
 
    def shutdown(self):
244
 
        """Clean up outstanding sessions."""
245
 
        self._lock.acquire()
246
 
        try:
247
 
            if not self._shutdownRan:
248
 
                # Save or delete any sessions that are still out there.
249
 
                for key,sess in self._checkOutList.items():
250
 
                    if sess.isValid:
251
 
                        self._saveSession(sess)
252
 
                    else:
253
 
                        self._deleteSession(sess.identifier)
254
 
                self._checkOutList.clear()
255
 
                self._shutdown()
256
 
                self._shutdownRan = True
257
 
        finally:
258
 
            self._lock.release()
259
 
 
260
 
    def __del__(self):
261
 
        self.shutdown()
262
 
 
263
 
    def periodic(self):
264
 
        """Timeout old sessions. Should be called periodically."""
265
 
        self._lock.acquire()
266
 
        try:
267
 
            if not self._shutdownRan:
268
 
                self._periodic()
269
 
        finally:
270
 
            self._lock.release()
271
 
 
272
 
    # To be implemented by subclasses. self._lock will be held whenever
273
 
    # these are called and for methods that take an identifier,
274
 
    # the identifier will be guaranteed to be valid (but it will not
275
 
    # necessarily exist).
276
 
 
277
 
    def _createSession(self, identifier):
278
 
        """
279
 
        Attempt to create the session with the given identifier. If
280
 
        successful, return the newly-created session, which must
281
 
        also be implicitly locked from use by other processes. (The
282
 
        session returned should be an instance of self._sessionClass.)
283
 
        If unsuccessful, return None.
284
 
        """
285
 
        raise NotImplementedError, self.__class__.__name__ + '._createSession'
286
 
        
287
 
    def _loadSession(self, identifier):
288
 
        """
289
 
        Load the session with the identifier from secondary storage returning
290
 
        None if it does not exist. If the load is successful, the session
291
 
        must be locked from use by other processes.
292
 
        """
293
 
        raise NotImplementedError, self.__class__.__name__ + '._loadSession'
294
 
 
295
 
    def _saveSession(self, session):
296
 
        """
297
 
        Store the session into secondary storage. Also implicitly releases
298
 
        the session for use by other processes.
299
 
        """
300
 
        raise NotImplementedError, self.__class__.__name__ + '._saveSession'
301
 
 
302
 
    def _deleteSession(self, identifier):
303
 
        """
304
 
        Deletes the session from secondary storage. Must be OK to pass
305
 
        in an invalid (non-existant) identifier. If the session did exist,
306
 
        it must be released for use by other processes.
307
 
        """
308
 
        raise NotImplementedError, self.__class__.__name__ + '._deleteSession'
309
 
 
310
 
    def _periodic(self):
311
 
        """Remove timedout sessions from secondary storage."""
312
 
        raise NotImplementedError, self.__class__.__name__ + '._periodic'
313
 
        
314
 
    def _shutdown(self):
315
 
        """Performs necessary shutdown actions for secondary store."""
316
 
        raise NotImplementedError, self.__class__.__name__ + '._shutdown'
317
 
 
318
 
    # Utilities
319
 
 
320
 
    def _isSessionTimedout(self, session, now=time.time()):
321
 
        return (session.lastAccessTime + self._sessionTimeout * 60) < now
322
 
    
323
 
class MemorySessionStore(SessionStore):
324
 
    """
325
 
    Memory-based session store. Great for persistent applications, terrible
326
 
    for one-shot ones. :)
327
 
    """
328
 
    def __init__(self, *a, **kw):
329
 
        super(MemorySessionStore, self).__init__(*a, **kw)
330
 
 
331
 
        # Our "secondary store".
332
 
        self._secondaryStore = {}
333
 
 
334
 
    def _createSession(self, identifier):
335
 
        if self._secondaryStore.has_key(identifier):
336
 
            return None
337
 
        sess = self._sessionClass(identifier)
338
 
        self._secondaryStore[sess.identifier] = sess
339
 
        return sess
340
 
 
341
 
    def _loadSession(self, identifier):
342
 
        return self._secondaryStore.get(identifier, None)
343
 
 
344
 
    def _saveSession(self, session):
345
 
        self._secondaryStore[session.identifier] = session
346
 
 
347
 
    def _deleteSession(self, identifier):
348
 
        if self._secondaryStore.has_key(identifier):
349
 
            del self._secondaryStore[identifier]
350
 
 
351
 
    def _periodic(self):
352
 
        now = time.time()
353
 
        for key,sess in self._secondaryStore.items():
354
 
            if self._isSessionTimedout(sess, now):
355
 
                del self._secondaryStore[key]
356
 
        
357
 
    def _shutdown(self):
358
 
        pass
359
 
 
360
 
class ShelveSessionStore(SessionStore):
361
 
    """
362
 
    Session store based on Python "shelves." Only use if you can guarantee
363
 
    that storeFile will NOT be accessed concurrently by other instances.
364
 
    (In other processes, threads, anywhere!)
365
 
    """
366
 
    def __init__(self, storeFile='sessions', *a, **kw):
367
 
        super(ShelveSessionStore, self).__init__(*a, **kw)
368
 
 
369
 
        self._secondaryStore = shelve.open(storeFile,
370
 
                                           protocol=pickle.HIGHEST_PROTOCOL)
371
 
 
372
 
    def _createSession(self, identifier):
373
 
        if self._secondaryStore.has_key(identifier):
374
 
            return None
375
 
        sess = self._sessionClass(identifier)
376
 
        self._secondaryStore[sess.identifier] = sess
377
 
        return sess
378
 
 
379
 
    def _loadSession(self, identifier):
380
 
        return self._secondaryStore.get(identifier, None)
381
 
 
382
 
    def _saveSession(self, session):
383
 
        self._secondaryStore[session.identifier] = session
384
 
 
385
 
    def _deleteSession(self, identifier):
386
 
        if self._secondaryStore.has_key(identifier):
387
 
            del self._secondaryStore[identifier]
388
 
 
389
 
    def _periodic(self):
390
 
        now = time.time()
391
 
        for key,sess in self._secondaryStore.items():
392
 
            if self._isSessionTimedout(sess, now):
393
 
                del self._secondaryStore[key]
394
 
        
395
 
    def _shutdown(self):
396
 
        self._secondaryStore.close()
397
 
 
398
 
class DiskSessionStore(SessionStore):
399
 
    """
400
 
    Disk-based session store that stores each session as its own file
401
 
    within a specified directory. Should be safe for concurrent use.
402
 
    (As long as the underlying OS/filesystem respects create()'s O_EXCL.)
403
 
    """
404
 
    def __init__(self, storeDir='sessions', *a, **kw):
405
 
        super(DiskSessionStore, self).__init__(*a, **kw)
406
 
 
407
 
        self._sessionDir = storeDir
408
 
        if not os.access(self._sessionDir, os.F_OK):
409
 
            # Doesn't exist, try to create it.
410
 
            os.mkdir(self._sessionDir)
411
 
 
412
 
    def _filenameForSession(self, identifier):
413
 
        return os.path.join(self._sessionDir, identifier + '.sess')
414
 
 
415
 
    def _lockSession(self, identifier, block=True):
416
 
        # Release SessionStore lock so we don't deadlock.
417
 
        self._lock.release()
418
 
        try:
419
 
            fn = self._filenameForSession(identifier) + '.lock'
420
 
            while True:
421
 
                try:
422
 
                    fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
423
 
                except OSError, e:
424
 
                    if e.errno != errno.EEXIST:
425
 
                        raise
426
 
                else:
427
 
                    os.close(fd)
428
 
                    break
429
 
 
430
 
                if not block:
431
 
                    return False
432
 
 
433
 
                # See if the lock is stale. If so, remove it.
434
 
                try:
435
 
                    now = time.time()
436
 
                    mtime = os.path.getmtime(fn)
437
 
                    if (mtime + 60) < now:
438
 
                        os.unlink(fn)
439
 
                except OSError, e:
440
 
                    if e.errno != errno.ENOENT:
441
 
                        raise
442
 
 
443
 
                time.sleep(0.1)
444
 
 
445
 
            return True
446
 
        finally:
447
 
            self._lock.acquire()
448
 
 
449
 
    def _unlockSession(self, identifier):
450
 
        fn = self._filenameForSession(identifier) + '.lock'
451
 
        os.unlink(fn) # Need to catch errors?
452
 
 
453
 
    def _createSession(self, identifier):
454
 
        fn = self._filenameForSession(identifier)
455
 
        lfn = fn + '.lock'
456
 
        # Attempt to create the file's *lock* first.
457
 
        lfd = fd = -1
458
 
        try:
459
 
            lfd = os.open(lfn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
460
 
            fd = os.open(fn, os.O_WRONLY|os.O_CREAT|os.O_EXCL)
461
 
        except OSError, e:
462
 
            if e.errno == errno.EEXIST:
463
 
                if lfd >= 0:
464
 
                    # Remove lockfile.
465
 
                    os.close(lfd)
466
 
                    os.unlink(lfn)
467
 
                return None
468
 
            raise
469
 
        else:
470
 
            # Success.
471
 
            os.close(fd)
472
 
            os.close(lfd)
473
 
            return self._sessionClass(identifier)
474
 
 
475
 
    def _loadSession(self, identifier, block=True):
476
 
        if not self._lockSession(identifier, block):
477
 
            return None
478
 
        try:
479
 
            return pickle.load(open(self._filenameForSession(identifier)))
480
 
        except:
481
 
            self._unlockSession(identifier)
482
 
            return None
483
 
 
484
 
    def _saveSession(self, session):
485
 
        f = open(self._filenameForSession(session.identifier), 'w+')
486
 
        pickle.dump(session, f, protocol=pickle.HIGHEST_PROTOCOL)
487
 
        f.close()
488
 
        self._unlockSession(session.identifier)
489
 
 
490
 
    def _deleteSession(self, identifier):
491
 
        try:
492
 
            os.unlink(self._filenameForSession(identifier))
493
 
        except:
494
 
            pass
495
 
        self._unlockSession(identifier)
496
 
 
497
 
    def _periodic(self):
498
 
        now = time.time()
499
 
        sessions = os.listdir(self._sessionDir)
500
 
        for name in sessions:
501
 
            if not name.endswith('.sess'):
502
 
                continue
503
 
            identifier = name[:-5]
504
 
            if not self._sessionClass.isIdentifierValid(identifier):
505
 
                continue
506
 
            # Not very efficient.
507
 
            sess = self._loadSession(identifier, block=False)
508
 
            if sess is None:
509
 
                continue
510
 
            if self._isSessionTimedout(sess, now):
511
 
                self._deleteSession(identifier)
512
 
            else:
513
 
                self._unlockSession(identifier)
514
 
 
515
 
    def _shutdown(self):
516
 
        pass
517
 
 
518
 
# SessionMiddleware stuff.
519
 
 
520
 
from Cookie import SimpleCookie
521
 
import cgi
522
 
import urlparse
523
 
 
524
 
class SessionService(object):
525
 
    """
526
 
    WSGI extension API passed to applications as
527
 
    environ['com.saddi.service.session'].
528
 
 
529
 
    Public API: (assume service = environ['com.saddi.service.session'])
530
 
      service.session - Returns the Session associated with the client.
531
 
      service.hasSession - True if the client is currently associated with
532
 
        a Session.
533
 
      service.isSessionNew - True if the Session was created in this
534
 
        transaction.
535
 
      service.hasSessionExpired - True if the client is associated with a
536
 
        non-existent Session.
537
 
      service.encodesSessionInURL - True if the Session ID should be encoded in
538
 
        the URL. (read/write)
539
 
      service.encodeURL(url) - Returns url encoded with Session ID (if
540
 
        necessary).
541
 
      service.cookieAttributes - Dictionary of additional RFC2109 attributes
542
 
        to be added to the generated cookie.
543
 
    """
544
 
    _expiredSessionIdentifier = 'expired session'
545
 
 
546
 
    def __init__(self, store, environ,
547
 
                 cookieName='_SID_',
548
 
                 cookieExpiration=None, # Deprecated
549
 
                 cookieAttributes={},
550
 
                 fieldName='_SID_'):
551
 
        self._store = store
552
 
        self._cookieName = cookieName
553
 
        self._cookieExpiration = cookieExpiration
554
 
        self.cookieAttributes = dict(cookieAttributes)
555
 
        self._fieldName = fieldName
556
 
 
557
 
        self._session = None
558
 
        self._newSession = False
559
 
        self._expired = False
560
 
        self.encodesSessionInURL = False
561
 
 
562
 
        if __debug__: self._closed = False
563
 
 
564
 
        self._loadExistingSession(environ)
565
 
 
566
 
    def _loadSessionFromCookie(self, environ):
567
 
        """
568
 
        Attempt to load the associated session using the identifier from
569
 
        the cookie.
570
 
        """
571
 
        C = SimpleCookie(environ.get('HTTP_COOKIE'))
572
 
        morsel = C.get(self._cookieName, None)
573
 
        if morsel is not None:
574
 
            self._session = self._store.checkOutSession(morsel.value)
575
 
            self._expired = self._session is None
576
 
 
577
 
    def _loadSessionFromQueryString(self, environ):
578
 
        """
579
 
        Attempt to load the associated session using the identifier from
580
 
        the query string.
581
 
        """
582
 
        qs = cgi.parse_qsl(environ.get('QUERY_STRING', ''))
583
 
        for name,value in qs:
584
 
            if name == self._fieldName:
585
 
                self._session = self._store.checkOutSession(value)
586
 
                self._expired = self._session is None
587
 
                self.encodesSessionInURL = True
588
 
                break
589
 
        
590
 
    def _loadExistingSession(self, environ):
591
 
        """Attempt to associate with an existing Session."""
592
 
        # Try cookie first.
593
 
        self._loadSessionFromCookie(environ)
594
 
 
595
 
        # Next, try query string.
596
 
        if self._session is None:
597
 
            self._loadSessionFromQueryString(environ)
598
 
 
599
 
    def _sessionIdentifier(self):
600
 
        """Returns the identifier of the current session."""
601
 
        assert self._session is not None
602
 
        return self._session.identifier
603
 
 
604
 
    def _shouldAddCookie(self):
605
 
        """
606
 
        Returns True if the session cookie should be added to the header
607
 
        (if not encoding the session ID in the URL). The cookie is added if
608
 
        one of these three conditions are true: a) the session was just
609
 
        created, b) the session is no longer valid, or c) the client is
610
 
        associated with a non-existent session.
611
 
        """
612
 
        return self._newSession or \
613
 
               (self._session is not None and not self._session.isValid) or \
614
 
               (self._session is None and self._expired)
615
 
        
616
 
    def addCookie(self, headers):
617
 
        """Adds Set-Cookie header if needed."""
618
 
        if not self.encodesSessionInURL and self._shouldAddCookie():
619
 
            if self._session is not None:
620
 
                sessId = self._sessionIdentifier()
621
 
                expireCookie = not self._session.isValid
622
 
            else:
623
 
                sessId = self._expiredSessionIdentifier
624
 
                expireCookie = True
625
 
 
626
 
            C = SimpleCookie()
627
 
            name = self._cookieName
628
 
            C[name] = sessId
629
 
            C[name]['path'] = '/'
630
 
            if self._cookieExpiration is not None:
631
 
                C[name]['expires'] = self._cookieExpiration
632
 
            C[name].update(self.cookieAttributes)
633
 
            if expireCookie:
634
 
                # Expire cookie
635
 
                C[name]['expires'] = -365*24*60*60
636
 
                C[name]['max-age'] = 0
637
 
            headers.append(('Set-Cookie', C[name].OutputString()))
638
 
 
639
 
    def close(self):
640
 
        """Checks session back into session store."""
641
 
        if self._session is None:
642
 
            return
643
 
        # Check the session back in and get rid of our reference.
644
 
        self._store.checkInSession(self._session)
645
 
        self._session = None
646
 
        if __debug__: self._closed = True
647
 
 
648
 
    # Public API
649
 
 
650
 
    def _get_session(self):
651
 
        assert not self._closed
652
 
        if self._session is None:
653
 
            self._session = self._store.createSession()
654
 
            self._newSession = True
655
 
 
656
 
        assert self._session is not None
657
 
        return self._session
658
 
    session = property(_get_session, None, None,
659
 
                       'Returns the Session object associated with this '
660
 
                       'client')
661
 
 
662
 
    def _get_hasSession(self):
663
 
        assert not self._closed
664
 
        return self._session is not None
665
 
    hasSession = property(_get_hasSession, None, None,
666
 
                          'True if a Session currently exists for this client')
667
 
 
668
 
    def _get_isSessionNew(self):
669
 
        assert not self._closed
670
 
        return self._newSession
671
 
    isSessionNew = property(_get_isSessionNew, None, None,
672
 
                            'True if the Session was created in this '
673
 
                            'transaction')
674
 
 
675
 
    def _get_hasSessionExpired(self):
676
 
        assert not self._closed
677
 
        return self._expired
678
 
    hasSessionExpired = property(_get_hasSessionExpired, None, None,
679
 
                                 'True if the client was associated with a '
680
 
                                 'non-existent Session')
681
 
 
682
 
    # Utilities
683
 
 
684
 
    def encodeURL(self, url):
685
 
        """Encodes session ID in URL, if necessary."""
686
 
        assert not self._closed
687
 
        if not self.encodesSessionInURL or self._session is None:
688
 
            return url
689
 
        u = list(urlparse.urlsplit(url))
690
 
        q = '%s=%s' % (self._fieldName, self._sessionIdentifier())
691
 
        if u[3]:
692
 
            u[3] = q + '&' + u[3]
693
 
        else:
694
 
            u[3] = q
695
 
        return urlparse.urlunsplit(u)
696
 
 
697
 
def _addClose(appIter, closeFunc):
698
 
    """
699
 
    Wraps an iterator so that its close() method calls closeFunc. Respects
700
 
    the existence of __len__ and the iterator's own close() method.
701
 
 
702
 
    Need to use metaclass magic because __len__ and next are not
703
 
    recognized unless they're part of the class. (Can't assign at
704
 
    __init__ time.)
705
 
    """
706
 
    class metaIterWrapper(type):
707
 
        def __init__(cls, name, bases, clsdict):
708
 
            super(metaIterWrapper, cls).__init__(name, bases, clsdict)
709
 
            if hasattr(appIter, '__len__'):
710
 
                cls.__len__ = appIter.__len__
711
 
            cls.next = iter(appIter).next
712
 
            if hasattr(appIter, 'close'):
713
 
                def _close(self):
714
 
                    appIter.close()
715
 
                    closeFunc()
716
 
                cls.close = _close
717
 
            else:
718
 
                cls.close = closeFunc
719
 
 
720
 
    class iterWrapper(object):
721
 
        __metaclass__ = metaIterWrapper
722
 
        def __iter__(self):
723
 
            return self
724
 
 
725
 
    return iterWrapper()
726
 
 
727
 
class SessionMiddleware(object):
728
 
    """
729
 
    WSGI middleware that adds a session service. A SessionService instance
730
 
    is passed to the application in environ['com.saddi.service.session'].
731
 
    A references to this instance should not be saved. (A new instance is
732
 
    instantiated with every call to the application.)
733
 
    """
734
 
    _serviceClass = SessionService
735
 
 
736
 
    def __init__(self, store, application, serviceClass=None, **kw):
737
 
        self._store = store
738
 
        self._application = application
739
 
        if serviceClass is not None:
740
 
            self._serviceClass = serviceClass
741
 
        self._serviceKW = kw
742
 
 
743
 
    def __call__(self, environ, start_response):
744
 
        service = self._serviceClass(self._store, environ, **self._serviceKW)
745
 
        environ['com.saddi.service.session'] = service
746
 
 
747
 
        def my_start_response(status, headers, exc_info=None):
748
 
            service.addCookie(headers)
749
 
            return start_response(status, headers, exc_info)
750
 
 
751
 
        try:
752
 
            result = self._application(environ, my_start_response)
753
 
        except:
754
 
            # If anything goes wrong, ensure the session is checked back in.
755
 
            service.close()
756
 
            raise
757
 
 
758
 
        # The iterator must be unconditionally wrapped, just in case it
759
 
        # is a generator. (In which case, we may not know that a Session
760
 
        # has been checked out until completion of the first iteration.)
761
 
        return _addClose(result, service.close)
762
 
 
763
 
if __name__ == '__main__':
764
 
    mss = MemorySessionStore(timeout=5)
765
 
#    sss = ShelveSessionStore(timeout=5)
766
 
    dss = DiskSessionStore(timeout=5)