This patch adds the ipalib/session.py file which implements a cookie based session cache using memcached.

It also invokes the session cookie support when a HTTP request is received and stores the session data in the per-thread context object.

--
John Dennis <jden...@redhat.com>

Looking to carve out IT costs?
www.redhat.com/carveoutcosts/
>From 342039e65fa4f085e7800a01d569603e99c0e9d7 Mon Sep 17 00:00:00 2001
From: John Dennis <jden...@redhat.com>
Date: Wed, 14 Dec 2011 15:21:25 -0500
Subject: [PATCH 60] Implement session support in server Manage sessions in
 WSGI
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: 8bit

---
 ipalib/session.py      |  309 ++++++++++++++++++++++++++++++++++++++++++++++++
 ipaserver/rpcserver.py |   13 ++
 make-lint              |    2 +
 3 files changed, 324 insertions(+), 0 deletions(-)
 create mode 100644 ipalib/session.py

diff --git a/ipalib/session.py b/ipalib/session.py
new file mode 100644
index 0000000..69dc636
--- /dev/null
+++ b/ipalib/session.py
@@ -0,0 +1,309 @@
+# Authors: John Dennis <jden...@redhat.com>
+#
+# Copyright (C) 2011  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+import memcache
+import Cookie
+import random
+import errors
+import re
+from text import _
+from ipapython.ipa_log_manager import *
+
+class SessionManager(object):
+    def __init__(self):
+        log_mgr.get_logger(self, True)
+        self.generated_session_ids = set()
+
+    def generate_session_id(self, n_bits=48):
+        '''
+        Return a random string to be used as a session id.
+
+        This implementation creates a string of hexadecimal digits.
+        There is no guarantee of uniqueness, it is the caller's
+        responsibility to validate the returned id is not currently in
+        use.
+
+        :parameters:
+          n_bits
+            number of bits of random data, will be rounded to next
+            highest multiple of 4 
+        :returns:
+          string of random hexadecimal digits
+        '''
+        # round up to multiple of 4
+        n_bits = (n_bits + 3) & ~3
+        session_id = '%0*x' % (n_bits >> 2, random.getrandbits(n_bits))
+        return session_id
+
+    def new_session_id(self, max_retries=5):
+        '''
+        Returns a new *unique* session id. See `generate_session_id()`
+        for how the session id's are formulated.
+
+        The scope of the uniqueness of the id is limited to id's
+        generated by this instance of the `SessionManager`.
+
+        :parameters:
+          max_retries
+            Maximum number of attempts to produce a unique id.
+        :returns:
+          Unique session id as a string.
+        '''
+        n_retries = 0
+        while n_retries < max_retries:
+            session_id = self.generate_session_id()
+            if not session_id in self.generated_session_ids:
+                break
+            n_retries += 1
+        if n_retries >= max_retries:
+            self.error('could not allocate unique new session_id, %d retries exhausted', n_retries)
+            raise errors.ExecutionError(message=_('could not allocate unique new session_id'))
+        self.generated_session_ids.add(session_id)
+        return session_id
+                                        
+
+class MemcacheSessionManager(SessionManager):
+    memcached_socket_path = '/var/run/ipa_memcached/ipa_memcached'
+    session_cookie_name = 'ipa_session'
+    mc_server_stat_name_re = re.compile(r'(.+)\s+\((\d+)\)')
+
+    def __init__(self):
+        super(MemcacheSessionManager, self).__init__()
+        self.servers = ['unix:%s' % self.memcached_socket_path]
+        self.mc = memcache.Client(self.servers, debug=0)
+
+        if not self.servers_running():
+            self.warning("session memcached servers not running")
+
+    def get_server_statistics(self):
+        '''
+        Return memcached server statistics.
+
+        Return value is a dict whose keys are server names and whose
+        value is a dict of key/value statistics as returned by the
+        memcached server.
+
+        :returns:
+          dict of server names, each value is dict of key/value server
+          statistics.
+
+        '''
+        result = {}                                                                     
+        stats = self.mc.get_stats() 
+        for server in stats:                                                            
+            match = self.mc_server_stat_name_re.search(server[0])                            
+            if match:                                                                   
+                name = match.group(1)                                                   
+                result[name] = server[1]
+            else:
+                self.warning('unparseable memcached server name "%s"', server[0])
+        return result
+
+    def servers_running(self):
+        '''
+        Check if all configured memcached servers are running and can
+        be communicated with.
+
+        :returns:
+          True if at least one server is configured and all servers
+          can respond, False otherwise.
+
+        '''
+
+        if len(self.servers) == 0:
+            return False
+        stats = self.get_server_statistics()
+        return len(self.servers) == len(stats)
+
+    def new_session_id(self, max_retries=5):
+        '''
+        Returns a new *unique* session id. See `generate_session_id()`
+        for how the session id's are formulated.
+
+        The scope of the uniqueness of the id is limited to id's
+        generated by this instance of the `SessionManager` and session
+        id's currently stored in the memcache instance.
+
+        :parameters:
+          max_retries
+            Maximum number of attempts to produce a unique id.
+        :returns:
+          Unique session id as a string.
+        '''
+        n_retries = 0
+        while n_retries < max_retries:
+            session_id = super(MemcacheSessionManager, self).new_session_id(max_retries)
+            session_key = self.session_key(session_id)
+            session_data = self.mc.get(session_key)
+            if session_data is None:
+                break
+            n_retries += 1
+        if n_retries >= max_retries:
+            self.error('could not allocate unique new session_id, %d retries exhausted', n_retries)
+            raise errors.ExecutionError(message=_('could not allocate unique new session_id'))
+        return session_id
+
+    def new_session_data(self, session_id):
+        '''
+        Return a new session data dict. The session data will be
+        associated with it's session id. The dict will be
+        pre-populated with it's session_id.
+
+        :parameters:
+          session_id
+            The session id used to look up this session data.
+        :returns:
+          Session data dict populated with a session_id key.
+        '''
+        return {'session_id' : session_id}
+
+    def session_key(self, session_id):
+        '''
+        Given a session id return a memcache key used to look up the
+        session data in the memcache.
+
+        :parameters:
+          session_id
+            The session id from which the memcache key will be derived.
+        :returns:
+          A key (string) used to look up the session data in the memcache.
+        '''
+        return 'ipa.session.%s' % (session_id)
+
+    def get_session_id_from_http_cookie(self, cookie_header):
+        '''
+        Parse an HTTP cookie header and search for our session
+        id. Return the session id if found, return None if not
+        found.
+
+        :parameters:
+          cookie_header
+            An HTTP cookie header. May be None, if None return None.
+        :returns:
+          Session id as string or None if not found.
+        '''
+        session_id = None
+        self.debug('http request cookie_header = %s', cookie_header)
+        if cookie_header is not None:
+            cookie = Cookie.SimpleCookie()
+            cookie.load(cookie_header)
+            session_cookie = cookie.get(self.session_cookie_name)
+            if session_cookie is not None:
+                session_id = session_cookie.value
+                self.debug('found session cookie_id = %s', session_id)
+        return session_id
+
+
+    def load_session_data(self, cookie_header):
+        '''
+        Parse an HTTP cookie header looking for our session
+        information.
+
+        * If no session id is found then a new session id and new
+          session data dict will be generated, stored in the memcache
+          and returned. The new session data dict will contain the new
+          session id.
+
+        * If the session id is found in the cookie an attempt is made
+          to retrieve the session data from the memcache using the
+          session id.
+
+          - If existing session data is found in the memcache it is
+            returned.
+
+          - If no session data is found in the memcache then a new
+            session data dict will be generated, stored in the
+            memcache and returned. The new session data dict will
+            contain the session id found in the cookie header.
+
+        :parameters:
+          cookie_header
+            An HTTP cookie header. May be None.
+        :returns:
+          Session data dict containing at a minimum the session id it
+          is bound to.
+        '''
+        
+        session_id = self.get_session_id_from_http_cookie(cookie_header)
+        if session_id is None:
+            session_id = self.new_session_id()
+            self.debug('no session id in request, generating empty session data with id=%s', session_id)
+            session_data = self.new_session_data(session_id)
+            self.store_session_data(session_data)
+            return session_data
+        else:
+            session_key = self.session_key(session_id)
+            session_data = self.mc.get(session_key)
+            if session_data is None:
+                self.debug('no session data in cache with id=%s, generating empty session data', session_id)
+                session_data = self.new_session_data(session_id)
+                self.store_session_data(session_data)
+                return session_data
+            else:
+                self.debug('found session data in cache with id=%s', session_id)
+                return session_data
+
+    def store_session_data(self, session_data):
+        '''
+        Store the supplied session_data dict in the memcached instance.
+
+        :parameters:
+          session_data
+            Session data dict, must contain session_id key.
+            
+        :returns:
+          session_id
+        '''
+        session_id = session_data['session_id']
+        session_key = self.session_key(session_id)
+        self.mc.set(session_key, session_data)
+        return session_id
+
+    def generate_cookie(self, url_path, session_id, add_header=False):
+        '''
+        Return a session cookie containing the session id. The cookie
+        will be contrainted to the url path, defined for use
+        with HTTP only, and only returned on secure connections (SSL).
+
+        :parameters:
+          url_path
+            The cookie will be returned in a request if it begins
+            with this url path.
+          session_id
+            The session id identified by the session cookie
+          add_header
+            If true format cookie string with Set-Cookie: header
+            
+        :returns:
+          cookie string
+        '''
+        cookie = Cookie.SimpleCookie()
+        cookie[self.session_cookie_name] = session_id
+        cookie[self.session_cookie_name]['path'] = url_path
+        cookie[self.session_cookie_name]['httponly'] = True
+        cookie[self.session_cookie_name]['secure'] = True
+        if add_header:
+            result = cookie.output().strip()
+        else:
+            result = cookie.output(header='').strip()
+
+        return result
+
+#-------------------------------------------------------------------------------
+
+session_mgr = MemcacheSessionManager()
diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py
index 68d4379..a3d909a 100644
--- a/ipaserver/rpcserver.py
+++ b/ipaserver/rpcserver.py
@@ -32,6 +32,7 @@ from ipalib.request import context, Connection, destroy_context
 from ipalib.rpc import xml_dumps, xml_loads
 from ipalib.util import make_repr
 from ipalib.compat import json
+from ipalib.session import session_mgr
 from wsgiref.util import shift_path_info
 import base64
 import os
@@ -256,6 +257,11 @@ class WSGIExecutioner(Executioner):
         """
         WSGI application for execution.
         """
+
+        # Load the session data and store it in the per-thread context
+        session_data = session_mgr.load_session_data(environ.get('HTTP_COOKIE'))
+        setattr(context, 'session_data', session_data)
+
         try:
             status = '200 OK'
             response = self.wsgi_execute(environ)
@@ -265,6 +271,13 @@ class WSGIExecutioner(Executioner):
             status = '500 Internal Server Error'
             response = status
             headers = [('Content-Type', 'text/plain')]
+
+        # Send session cookie back and store session data
+        # FIXME: the URL path should be retreived from somewhere (but where?), not hardcoded
+        session_cookie = session_mgr.generate_cookie('/ipa', session_data['session_id'])
+        headers.append(('Set-Cookie', session_cookie))
+        session_mgr.store_session_data(session_data)
+
         start_response(status, headers)
         return [response]
 
diff --git a/make-lint b/make-lint
index 83025d8..ec81717 100755
--- a/make-lint
+++ b/make-lint
@@ -67,6 +67,8 @@ class IPATypeChecker(TypeChecker):
         'ipalib.parameters.Enum': ['values'],
         'ipalib.parameters.File': ['stdin_if_missing'],
         'urlparse.SplitResult': ['netloc'],
+        'ipalib.session.SessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
+        'ipalib.session.MemcacheSessionManager' : ['log', 'debug', 'info', 'warning', 'error', 'critical', 'exception'],
     }
 
     def _related_classes(self, klass):
-- 
1.7.7.4

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to