# -*- coding: utf-8 -*- """this is a module which handles login sessions using cookies""" # Copyright 2002, 2003 St James Software # # This file is part of jToolkit. # # jToolkit 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 2 of the License, or # (at your option) any later version. # # jToolkit 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 jToolkit; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA from jToolkit.data import dates from jToolkit import prefs import md5 import Cookie import warnings def md5hexdigest(text): if isinstance(text, unicode): text = text.encode('utf8') return md5.md5(text).hexdigest() class SessionCache(dict): def __init__(self, sessionclass = None, sessioncookiename = None): """constructs a SessionCache that uses the given sessionclass for session objects""" if sessionclass is None: sessionclass = Session self.sessionclass = sessionclass if sessioncookiename is None: sessioncookiename = 'session' self.sessioncookiename = sessioncookiename self.invalidsessionstrings = {} def createsession(self, server, sessionstring = None): """creates a new session object""" return self.sessionclass(self, server, sessionstring) def purgestalesessions(self): """takes any sessions that haven't been used for a while out of the cache""" stalekeys = [] for key, session in self.iteritems(): if not session.isfresh(): # only remove it later otherwise we confuse the loop stalekeys.append(key) for key in stalekeys: # we don't close the session as it is still valid and might be reused del self[key] def newsession(self, req, argdict, server): """returns a new session from the login parameters""" # gets the old session, if there is one oldsessionstring = self.getsessioncookie(req, argdict, server) oldsession = self.get(oldsessionstring) # get the username and password from the form username = argdict.get('username','') password = argdict.get('password','') # TODO: add language in other similar functions... # print "accept-language", req.headers_in.getheader('Accept-Language') language = argdict.get('language','') # create a new session timestamp = dates.formatdate(dates.currentdate(), '%Y%m%d%H%M%S') session = self.createsession(server) session.create(username,password,timestamp,language) if session.isopen: if oldsession is not None: oldsession.close(req) session.updatecookie(req, server) return session def confirmsession(self, req, argdict, server): """confirms login for the current session with new username & password""" # get the username and password from the form username = argdict.get('username','') password = argdict.get('password','') session = self.getsession(req, argdict, server) timestamp = dates.formatdate(dates.currentdate(), '%Y%m%d%H%M%S') session.confirmlogin(req, username, password, timestamp) return session def closesession(self, req, argdict, server): """closes the current session""" # remove the session from the list of open sessions session = self.getsession(req, argdict, server) session.close(req) return session def getsession(self, req, argdict, server): """gets the current session""" # retrieve current session info sessionstring = self.getsessioncookie(req, argdict, server) if sessionstring in self.invalidsessionstrings: session = self.createsession(server, "-") else: session = self.get(sessionstring) if session is None: session = self.createsession(server, sessionstring) session.checkstatus(req, argdict) return session def getcookie(self, req): """Reads in any cookie info from the request""" try: cookiestring = req.headers_in["Cookie"] except: cookiestring = {} cookie = Cookie.SimpleCookie() cookie.load(cookiestring) cookiedict = {} for key, morsel in cookie.iteritems(): cookiedict[key] = morsel.value.decode('utf8') return cookiedict def setcookie(self, req, cookiedict): """Puts the bits from the cookiedict into Morsels, sets the req cookie""" # construct the cookie from the cookiedict cookie = Cookie.SimpleCookie() for key, value in cookiedict.iteritems(): if isinstance(value, unicode): value = value.encode('utf8') if isinstance(key, unicode): key = key.encode('utf8') cookie[key] = value # add the cookie headers to req.headers_out for key, morsel in cookie.iteritems(): req.headers_out.add('Set-Cookie', morsel.OutputString()) def getsessioncookiename(self, server): """returns the actual name used for the session cookie for the given server""" return server.name + '-' + self.sessioncookiename def getsessioncookie(self, req, argdict, server): """returns the session cookie value""" cookiedict = self.getcookie(req) sessioncookiename = self.getsessioncookiename(server) sessionparam = argdict.get(sessioncookiename, None) sessioncookie = cookiedict.get(sessioncookiename, None) if sessionparam and not sessioncookie: sessioncookie = sessionparam self.setsessioncookie(req, server, sessioncookie) if not sessioncookie: sessioncookie = "-" return sessioncookie def setsessioncookie(self, req, server, sessionstring): """sets the session cookie value""" cookiedict = {self.getsessioncookiename(server): sessionstring} self.setcookie(req, cookiedict) class Session(object): """represents a user session""" def __init__(self, sessioncache, server, sessionstring = None): self.sessioncache = sessioncache self.server = server self.instance = server.instance self.parentsessionname = "" self.childsessions = {} self.isvalid = 0 self.markedinvalid = 0 self.isopen = 0 self.pagecount = 0 self.userdict = {} self.updatelastused() self.status = "" self.setlanguage(None) self.username = None # allow the server to add any attributes it requires server.initsession(self) self.setsessionstring(sessionstring) def md5hexdigest(self, text): """allows the md5hexdigest function to be access through the session""" return md5hexdigest(text) def getstatus(self): """get a string describing the status of the session""" return self.status def open(self): """tries to open the given session, returns success""" self.isopen = 0 if self.isvalid: self.sessioncache.purgestalesessions() sessionstring = self.getsessionstring() self.sessioncache[sessionstring] = self self.isopen = sessionstring in self.sessioncache if self.isopen: self.status = self.localize("logged in as %s") % self.username else: self.status = self.localize("couldn't add new session") return self.isopen def isfresh(self): """returns whether this session has been used relatively recently""" # session is fresh if it's been used in the last ten minutes... freshinterval = dates.seconds(10*60) delay = dates.currentdate() - self.lastused return delay <= freshinterval def close(self, req): """removes the current session from the list of valid sessions...""" # note: if somebody sneakily remembers the session cookie it is still valid after logout # (though it is reset on the user's browser) if self.isopen: sessionstring = self.getsessionstring() if sessionstring in self.sessioncache: del self.sessioncache[sessionstring] self.isopen = 0 # set the session cookie to expired, fail authorization self.updatecookie(req, self.server) self.status = self.localize("logged out") # invalidate child sessions... # note that if Apache is restarted, the cookies will still be around, and the invalidation will be forgotten... # alternative would be to store cookies in the root path for childname in self.childsessions: self.invalidatechild(childname) def invalidatechild(self, childname): """logs out of a child session""" childsession = self.childsessions.get(childname, 0) if childsession: childsession.markinvalid() def updatelastused(self): """updates that the session has been used""" self.lastused = dates.currentdate() def checkstatus(self, req, argdict): """check the login status (auto logoff etc)""" if self.isopen: self.status = self.localize("connected") self.updatelastused() def getlogintime(self): """returns the login time based on the timestamp""" return dates.nosepdateparser.parse(self.timestamp) def getsessionstring(self): """creates the full session string using the sessionid""" sessionstring = self.timestamp+':'+self.sessionid return sessionstring def setsessionstring(self, sessionstring): """sets the session string for this session""" if sessionstring is not None: # split up the sessionstring into components if sessionstring.count(':') >= 1: self.timestamp,self.sessionid = sessionstring.split(':',1) # make sure this is valid, and open it... self.validate() self.open() def updatecookie(self, req, server): """update session string in cookie in req to reflect whether session is open""" if self.isopen: self.sessioncache.setsessioncookie(req, server, self.getsessionstring()) else: self.sessioncache.setsessioncookie(req, server, '-') def markinvalid(self): """marks a session as invalid""" sessionstring = self.getsessionstring() if sessionstring in self.sessioncache: del self.sessioncache[sessionstring] self.sessioncache.invalidsessionstrings[self.getsessionstring()] = 1 self.markedinvalid = self.localize("exited parent session") self.isopen = 0 def validate(self): """checks if this session is valid""" self.isvalid = 1 return 1 def checksessionid(self): """returns the hex sessionid for the session, using the password""" correctsessionid = self.timestamp+':'+md5hexdigest(self.timestamp) return correctsessionid == self.sessionid def setlanguage(self, language): """sets the language for the session""" if language is None: self.language = self.server.defaultlanguage else: self.language = language self.translation = self.server.gettranslation(self.language) def localize(self, message, *variables): """returns the localized form of a message, falls back to original if failure with variables""" # this is used when no session is available if variables: # TODO: this is a hack to handle the old-style passing of a list of variables # remove below after the next jToolkit release after 0.7.7 if len(variables) == 1 and isinstance(variables[0], (tuple, list)): warnings.warn("list/tuple of variables passed into localize - deprecated, rather pass arguments") variables = variables[0] # remove above after the next jToolkit release after 0.7.7 try: return self.translation.ugettext(message) % variables except: return message % variables else: return self.translation.ugettext(message) def nlocalize(self, singular, plural, n, *variables): """returns the localized form of a plural message, falls back to original if failure with variables""" # this is used when no session is available if variables: # TODO: this is a hack to handle the old-style passing of a list of variables # remove below after the next jToolkit release after 0.7.7 if len(variables) == 1 and isinstance(variables[0], (tuple, list)): warnings.warn("list/tuple of variables passed into nlocalize - deprecated, rather pass arguments") variables = variables[0] # remove above after the next jToolkit release after 0.7.7 try: return self.translation.ungettext(singular, plural, n) % variables except: if n != 1: return plural % variables else: return singular % variables else: return self.translation.ungettext(singular, plural, n) userprefsfiles = {} class LoginChecker: """An interface that allows checking of login details (using prefs file)""" def __init__(self, session, instance): self.session = session if hasattr(instance, "userprefs"): if instance.userprefs in userprefsfiles: self.users = userprefsfiles[instance.userprefs] else: self.users = prefs.PrefsParser() self.users.parsefile(instance.userprefs) userprefsfiles[instance.userprefs] = self.users elif hasattr(instance, "users"): self.users = instance.users else: raise IndexError, self.session.localize("need to define the users or userprefs entry on the server config") def getmd5password(self, username=None): """retrieves the md5 hash of the password for this user, or another if another is given...""" if username is None: username = self.session.username if not self.userexists(username): raise IndexError, self.session.localize("user does not exist (%r)") % username md5password = self.users.__getattr__(username).passwdhash return md5password def userexists(self, username=None): """checks whether user username exists""" if username is None: username = self.session.username return self.users.__hasattr__(username) class LoginSession(Session): """A session that allows login""" def __init__(self, sessioncache, server, sessionstring = None, loginchecker = None): if loginchecker is None: loginchecker = LoginChecker(self, server.instance) self.loginchecker = loginchecker super(LoginSession, self).__init__(sessioncache, server, sessionstring) def open(self): """tries to open the given session, returns success""" self.isopen = 0 super(LoginSession, self).open() if self.isopen: self.status = self.localize("logged in as %s") % self.username return self.isopen def checkstatus(self, req, argdict): """check the login status (auto logoff etc)""" if self.isopen: self.status = self.localize("logged in as %s") % (self.username) self.updatelastused() def validate(self): """checks if this session is valid""" self.isvalid = 0 if self.markedinvalid: self.status = self.markedinvalid return self.isvalid if self.loginchecker.userexists(): self.isvalid = self.checksessionid() if not self.isvalid: self.status = self.localize("invalid username and/or password") return self.isvalid def getsessionid(self, password): """returns the hex sessionid for the session, using the password""" md5password = md5hexdigest(password) return md5hexdigest(self.username+':'+self.timestamp+':'+md5password+':'+self.instance.sessionkey+':'+self.server.name) def checksessionid(self): """returns the hex sessionid for the session, using the password""" correctmd5password = self.loginchecker.getmd5password() sessiontest = self.username+':'+self.timestamp+':'+correctmd5password+':'+self.instance.sessionkey+':'+self.server.name correctsessionid = md5hexdigest(sessiontest) return correctsessionid == self.sessionid def getsessionstring(self): """creates the full session string using the sessionid""" sessionstring = self.username+':'+self.timestamp+':'+self.sessionid+':'+self.parentsessionname return sessionstring def setsessionstring(self, sessionstring): """sets the session string for this session""" # split up the sessionstring into components if sessionstring is not None: # split up the sessionstring into components if sessionstring.count(':') >= 3: self.username,self.timestamp,self.sessionid,self.parentsessionname = sessionstring.split(':',3) # make sure this is valid, and open it... self.validate() self.open() def create(self,username,password,timestamp,language): """initializes the session with the parameters""" self.username, password, self.timestamp = username, password, timestamp self.setlanguage(language) self.sessionid = self.getsessionid(password) self.validate() self.open() def sharelogindetails(self, req, othersession): """shares this session's login details with othersession""" username = self.username # this expects that self.password exist ... see SharedLoginMultiAppServer.checklogin... password = getattr(self, 'password', '') language = self.language timestamp = dates.formatdate(dates.currentdate(), '%Y%m%d%H%M%S') # currently assumes we do not need to close the other session otherusername = getattr(othersession, 'username', '') othersession.parentsessionname = self.server.name othersession.create(username, password, timestamp, language) if othersession.isopen: othersession.updatecookie(req, othersession.server) self.childsessions[othersession.server.name] = othersession else: othersession.close(req) othersession.parentsessionname = "" othersession.username = otherusername othersession.status = "" def confirmlogin(self, req, username, password, timestamp): """validates a username and password for an already-established session""" username, password = username, password if not self.isvalid: # create a new session self.create(username, password, timestamp, self.language) if self.isopen: self.updatecookie(req, self.server) else: # validate the old session... if username != self.username: self.close(req) self.status = self.localize("failed login confirmation") self.sessionid = self.getsessionid(password) if not self.validate(): self.close(req) self.status = self.localize("failed login confirmation") class DBLoginChecker: """An interface that allows checking of login details (using a database)""" # note that usernames are case insensitive for this checker def __init__(self, session, db): self.session = session self.db = db def getmd5password(self, username=None): """retrieves the md5 hash of the password for this user, or another if another is given...""" if username is None: username = self.session.username.lower() else: username = username.lower() if not self.userexists(username): raise IndexError, self.session.localize("user does not exist (%r)") % username sql = "select passwdhash from users where %s(username)='%s'" % (self.db.dblowerfn(), username) return self.db.singlevalue(sql) def userexists(self, username=None): """checks whether user username exists""" if username is None: username = self.session.username sql = "select count(*) from users where %s(username)='%s'" % \ (self.db.dblowerfn(), username.lower()) count = self.db.singlevalue(sql) return count > 0 class DBLoginSession(LoginSession): """A session that allows login based on a database""" def __init__(self, sessioncache, server, sessionstring = None): loginchecker = DBLoginChecker(self, server.db) super(DBLoginSession, self).__init__(sessioncache, server, sessionstring, loginchecker)