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