Skip to content
This repository
Browse code

initial commit

  • Loading branch information...
commit c09b0df5d5fc785bfc40f26aa15e0b13ce5ba2e7 0 parents
David Underhill authored April 07, 2010
1  .gitignore
... ...
@@ -0,0 +1 @@
  1
+*.pyc
63  README.markdown
Source Rendered
... ...
@@ -0,0 +1,63 @@
  1
+GAE Sessions
  2
+=
  3
+
  4
+NOTE: This software is NOT ready for use.  I'm very much in the process of
  5
+completing the initial implementation but it should be ready soon :).
  6
+
  7
+
  8
+Advantages:
  9
+-
  10
+ * __Lightweight__: One short file and references to a handful of standard libs.
  11
+ * __High Availability__ is ensured by persisting all changes to the datastore.
  12
+ * __Fast and Efficient__
  13
+     - Uses memcache to minimize read times.
  14
+     - Minimizes gets() and puts() by compactly storing all values in one field.
  15
+     - Automatically converts db.Model instances to protobufs for more
  16
+       efficient storage and CPU usage.
  17
+     - Frequency of writes is minimized by only writing if there is a change,
  18
+       and only once per request (when the response is being sent).
  19
+ * __Simple to Use__
  20
+     - Easily installed as WSGI Middleware.
  21
+     - You may access session values as attributes or via a dictionary interface.
  22
+     - The session automatically initializes when you first assign a value.
  23
+       Until then, no cookies are set and no writes are done.
  24
+
  25
+
  26
+Limitations:
  27
+-
  28
+  * Limited to 1MB of data in a session.  (to fit in a single memcache entry)
  29
+  * No checks for User-Agent or IP consistency (yet).
  30
+  * I'm sure you'll have lots to add to this list :).
  31
+
  32
+
  33
+Installation
  34
+-
  35
+
  36
+After downloading and unpacking gae-sessions, copy the 'gaesessions' folder into
  37
+your app's root directory.
  38
+
  39
+gae-sessions includes WSGI middleware to make it easy to integrate into your app
  40
+- you just need to add in the middleware.  If you're using App Engine's built in
  41
+webapp framework, or any other framework that calls the
  42
+[run_wsgi_app](http://code.google.com/appengine/docs/python/tools/webapp/utilmodule.html)
  43
+function, you can use App Engine's configuration framework to install
  44
+gae-sessions.  Create a file called "appengine_config.py" in your app's root
  45
+directory, and put the following in it:
  46
+
  47
+    from gaesessions import SessionMiddleware
  48
+    def webapp_add_wsgi_middleware(app):
  49
+        app = SessionMiddleware(app)
  50
+        return app
  51
+
  52
+
  53
+Example Usage
  54
+-
  55
+    from gaesessions import _current_session as session
  56
+    session.blah = 325
  57
+    session['another-var'] = some_model_instance
  58
+    del session.blah  # remove 'blah' from the session
  59
+
  60
+
  61
+_Author_: [David Underhill](http://www.dound.com)
  62
+
  63
+_Updated_: 2010-Apr-07
224  gaesessions/__init__.py
... ...
@@ -0,0 +1,224 @@
  1
+"""A fast, lightweight, and secure session WSGI middleware for use with GAE."""
  2
+from Cookie import CookieError, SimpleCookie
  3
+import datetime
  4
+import hashlib
  5
+import logging
  6
+import pickle
  7
+import os
  8
+
  9
+from google.appengine.api import memcache
  10
+from google.appengine.ext import db
  11
+
  12
+COOKIE_DOMAIN   = "dound.appspot.com"
  13
+COOKIE_PATH     = "/"
  14
+COOKIE_LIFETIME = datetime.timedelta(days=7)
  15
+
  16
+_current_session = None
  17
+
  18
+class SessionModel(db.Model):
  19
+    """Contains session data.  key_name is the session ID and pdump contains a
  20
+    pickled dictionary which maps session variables to their values."""
  21
+    pdump = db.BlobProperty()
  22
+
  23
+class Session(object):
  24
+    """Manages loading, user reading/writing, and saving of a session."""
  25
+    def __init__(self):
  26
+        self.sid = None
  27
+        try:
  28
+            # check the cookie to see if a session has been started
  29
+            cookie = SimpleCookie(os.environ['HTTP_COOKIE'])
  30
+            self.__set_sid(cookie['sid'])
  31
+        except (CookieError, KeyError):
  32
+            # no session has been started for this user
  33
+            self.data = {}
  34
+            return
  35
+
  36
+        # this flag indicates whether the session has been changed
  37
+        self.dirty = False
  38
+
  39
+        # eagerly fetch the data for the active session (we'll probably need it)
  40
+        self.data = self.__retrieve_data()
  41
+
  42
+    @staticmethod
  43
+    def __make_sid():
  44
+        """Returns a new session ID."""
  45
+        return hashlib.md5(os.urandom(16)).hexdigest()
  46
+
  47
+    @staticmethod
  48
+    def __encode_dict(d):
  49
+        """Returns a "pickled+" encoding of d.  d values of type db.Model are
  50
+        protobuf encoded before pickling to minimize CPU usage & data size."""
  51
+        # seperate protobufs so we'll know how to decode (they are just strings)
  52
+        eP = {} # for models encoded as protobufs
  53
+        eO = {} # for everything else
  54
+        for k,v in d.iteritems():
  55
+            if isinstance(v, db.Model):
  56
+                eP[k] = db.model_to_protobuf(v)
  57
+            else:
  58
+                eO[k] = v
  59
+        return pickle.dumps((eP,eO))
  60
+
  61
+    @staticmethod
  62
+    def __decode_dict(pdump):
  63
+        """Returns a data dictionary after decoding it from "pickled+" form."""
  64
+        eP, eO = pickle.loads(pdump)
  65
+        for k,v in eP.iteritems():
  66
+            eO[k] = db.model_from_protobuf(v)
  67
+        return eO
  68
+
  69
+    def user_is_now_logged_in(self):
  70
+        """Assigns the session a new session ID (data carries over).  This helps
  71
+        nullify session fixation attacks."""
  72
+        self.__set_sid(self.__make_sid())
  73
+        self.dirty = True
  74
+
  75
+    def start(self, expiration=None):
  76
+        """Starts a new session.  expiration specifies when it will expire.  If
  77
+        expiration is not specified, then COOKIE_LIFETIME will used to determine
  78
+        the expiration date."""
  79
+        # make a random ID (random.randrange() is 10x faster but less secure?)
  80
+        self.__set_sid(self.__make_sid())
  81
+        self.dirty = True
  82
+        self.data = {}
  83
+        if expiration:
  84
+            self.data['expiration'] = expiration
  85
+        else:
  86
+            self.data['expiration'] = datetime.datetime.now() + COOKIE_LIFETIME
  87
+
  88
+    def terminate(self):
  89
+        """Ends the session and cleans it up."""
  90
+        self.__clear_data()
  91
+        self.sid = None
  92
+        del self.dirty
  93
+        del self.data
  94
+        del self.cookie_header
  95
+
  96
+    def __set_sid(self, sid):
  97
+        """Sets the session ID, deleting the old session if one existed.  The
  98
+        session's data will remain intact (only the sesssion ID changes)."""
  99
+        if self.sid:
  100
+            self.__clear_data()
  101
+        self.sid = sid
  102
+        self.db_key = db.Key.from_path(SessionModel.kind(), sid)
  103
+
  104
+        # there is an active sesssion, so set the cookie
  105
+        cookie = SimpleCookie()
  106
+        cookie["session"] = self.sid
  107
+        cookie["session"]["domain"] = COOKIE_DOMAIN
  108
+        cookie["session"]["path"] = COOKIE_PATH
  109
+        ex = self.data['expiration']
  110
+        cookie["session"]["expires"] = ex.strftime("%a, %d-%b-%Y %H:%M:%S PST")
  111
+        self.cookie_header = cookie.output()
  112
+
  113
+    def __clear_data(self):
  114
+        """Deletes this session from memcache and the datastore."""
  115
+        memcache.delete(self.key) # not really needed; it'll go away on its own
  116
+        db.delete(self.db_key)
  117
+
  118
+    def __retrieve_data(self):
  119
+        """Returns the data associated with this session after retrieving it
  120
+        from memcache or the datastore.  Assumes self.sid is set."""
  121
+        pdump = memcache.get(self.sid)
  122
+        if pdump is None:
  123
+            # memcache lost it, go to the datastore
  124
+            session_model_instance = db.get(self.db_key)
  125
+            if session_model_instance:
  126
+                pdump = session_model_instance.pdump
  127
+            else:
  128
+                logging.error("can't find session data in the datastore for sid=%s" % self.sid)
  129
+                return {} # we lost it
  130
+        return self.__decode_dict(pdump)
  131
+
  132
+    def save(self, only_if_changed=True):
  133
+        """Saves the data associated with this session to memcache.  It also
  134
+        tries to persist it to the datastore."""
  135
+        if not self.sid:
  136
+            return # no session is active
  137
+        if only_if_changed and not self.dirty:
  138
+            return # nothing has changed
  139
+
  140
+        # do the pickling ourselves b/c we need it for the datastore anyway
  141
+        pdump = self.__encode_data(self.data)
  142
+        mc_ok = memcache.set(self.sid, pdump)
  143
+
  144
+        # persist the session to the datastore
  145
+        try:
  146
+            SessionModel(key_name=self.sid, pdump=pdump).put()
  147
+        except db.TransactionFailedError:
  148
+            logging.warning("unable to persist session to datastore for sid=%s" % self.sid)
  149
+        except db.CapabilityDisabledError:
  150
+            pass # nothing we can do here
  151
+
  152
+        # retry the memcache set after the db op if the memcache set failed
  153
+        if not mc_ok:
  154
+            memcache.set(self.sid, pdump)
  155
+
  156
+    def get_cookie_out(self):
  157
+        """Returns the cookie header to set (if any), otherwise None."""
  158
+        if self.sid:
  159
+            return self.cookie_header
  160
+        else:
  161
+            return None
  162
+
  163
+    # Users may interact with the session like dictionary or an object.
  164
+    def __getattr__(self, name):
  165
+        return self.__getitem__(name)
  166
+
  167
+    def __setattr__(self, name, value):
  168
+        self.__setitem__(name, value)
  169
+
  170
+    def __delattr__(self, name):
  171
+        self.__delitem__(self, name)
  172
+
  173
+    def __getitem__(self, key):
  174
+        return self.data.__getitem__(key)
  175
+
  176
+    def __setitem__(self, key, value):
  177
+        """Set a value named key on this session.  This will start this session
  178
+        if it had not already been started."""
  179
+        if not self.sid:
  180
+            self.start()
  181
+        self.data.__setitem__(key, value)
  182
+        self.dirty = True
  183
+
  184
+    def __delitem__(self, key):
  185
+        if key == 'expiration':
  186
+            raise KeyError("expiration may not be removed")
  187
+        else:
  188
+            self.data.__delitem__(key)
  189
+            self.dirty = True
  190
+
  191
+    def __iter__(self):
  192
+        """Returns an iterator over the keys (names) of the stored values."""
  193
+        return self.data.iterkeys()
  194
+
  195
+    def __contains__(self, key):
  196
+        return self.data.__contains__(key)
  197
+
  198
+    def __str__(self):
  199
+        """Returns a string representation of the session."""
  200
+        if self.sid:
  201
+            return "SID=%s %s" % (self.sid, self.data)
  202
+        else:
  203
+            return "uninitialized session"
  204
+
  205
+class SessionMiddleware(object):
  206
+    """WSGI middleware that adds session support."""
  207
+    def __init__(self, app):
  208
+        self.app = app
  209
+
  210
+    def __call__(self, environ, start_response):
  211
+        # initialize a session for the current user
  212
+        global _current_session
  213
+        _current_session = Session()
  214
+
  215
+        # create a hook for us to insert a cookie into the response headers
  216
+        def my_start_response(status, headers, exc_info=None):
  217
+            cookie_out = _current_session.get_cookie_out()
  218
+            if cookie_out:
  219
+                headers.append('Set-cookie', cookie_out)
  220
+            _current_session.save() # store the session if it was changed
  221
+            return start_response(status, headers, exc_info)
  222
+
  223
+        # let the app do its thing
  224
+        return self.app(environ, my_start_response)

0 notes on commit c09b0df

Please sign in to comment.
Something went wrong with that request. Please try again.