File: session.py

package info (click to toggle)
python-jtoolkit 0.7.8-2
  • links: PTS
  • area: main
  • in suites: etch, etch-m68k
  • size: 1,436 kB
  • ctags: 2,536
  • sloc: python: 15,143; makefile: 20
file content (503 lines) | stat: -rwxr-xr-x 19,850 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
# -*- 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 <b>%s</b>") % 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 <b>%s</b>") % 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 <b>%s</b>") % (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)