| 1 | ##################################################################### |
|---|
| 2 | # Copyright (C) 2007 Alex Clemesha <clemesha@gmail.com> |
|---|
| 3 | # |
|---|
| 4 | # Distributed under the terms of the GNU General Public License (GPL) |
|---|
| 5 | # http://www.gnu.org/licenses/ |
|---|
| 6 | ##################################################################### |
|---|
| 7 | # |
|---|
| 8 | # This code was inspired by the following sources: |
|---|
| 9 | # * The file 'guard.py' included in the Nevow Web Framework http://divmod.org/trac/wiki/DivmodNevow |
|---|
| 10 | # * The file 'guard.py' used in the Stiq website code http://www.stiq.it |
|---|
| 11 | |
|---|
| 12 | #twisted modules |
|---|
| 13 | from twisted.python import log, components |
|---|
| 14 | from twisted.internet import task, defer |
|---|
| 15 | from twisted.cred.error import UnauthorizedLogin |
|---|
| 16 | from twisted.cred import credentials |
|---|
| 17 | from zope.interface import Interface, implements |
|---|
| 18 | from twisted.web2 import iweb |
|---|
| 19 | from twisted.web2 import server |
|---|
| 20 | |
|---|
| 21 | #standard library |
|---|
| 22 | import random |
|---|
| 23 | import time |
|---|
| 24 | import md5 |
|---|
| 25 | |
|---|
| 26 | |
|---|
| 27 | class Session(object): |
|---|
| 28 | """A single users session, defined by the uid. |
|---|
| 29 | """ |
|---|
| 30 | def __init__(self, uid, sessionManager): |
|---|
| 31 | self.uid = uid #the cookie, a unique identifier (md5 hash) |
|---|
| 32 | self.creationTime = self.lastAccessed = time.time() |
|---|
| 33 | self.sessionManager = sessionManager |
|---|
| 34 | self.authenticatedAs = None |
|---|
| 35 | #self.setLifetime(60) |
|---|
| 36 | |
|---|
| 37 | def set_authCreds(self, creds): |
|---|
| 38 | self.authenticatedAs = creds |
|---|
| 39 | |
|---|
| 40 | def get_authCreds(self): |
|---|
| 41 | return self.authenticatedAs |
|---|
| 42 | |
|---|
| 43 | def get_uid(self): |
|---|
| 44 | return self.uid |
|---|
| 45 | |
|---|
| 46 | def touch(self): |
|---|
| 47 | self.lastAccessed = time.time() |
|---|
| 48 | |
|---|
| 49 | def expire(self): |
|---|
| 50 | return defer.maybeDeferred(self.sessionManager.expiredSession, self) |
|---|
| 51 | |
|---|
| 52 | |
|---|
| 53 | class SessionsManager(object): |
|---|
| 54 | """Manages all logged in users' sessions. |
|---|
| 55 | """ |
|---|
| 56 | tickTime = 10000 #SESSION_CLEAN_FREQUENCY |
|---|
| 57 | sessionLifetime = 1000000 #TRANSIENT_SESSION_LIFETIME |
|---|
| 58 | sessionPersistentLifetime = 1000000 #PERSISTENT_SESSION_LIFETIME |
|---|
| 59 | |
|---|
| 60 | sessionFactory = Session |
|---|
| 61 | |
|---|
| 62 | def __init__(self): |
|---|
| 63 | self.sessions = {} |
|---|
| 64 | self.tick = task.LoopingCall(self._tick) |
|---|
| 65 | |
|---|
| 66 | def createSession(self): |
|---|
| 67 | session = self.sessionFactory(self.createSessionID(), self) |
|---|
| 68 | uid = session.get_uid() |
|---|
| 69 | self.sessions[uid] = session |
|---|
| 70 | log.msg('Session %r created' % uid) |
|---|
| 71 | #log.msg('All sessions %r' % self.sessions) |
|---|
| 72 | if not self.tick.running and len(self.sessions) > 0: |
|---|
| 73 | self.tick.start(self.tickTime) |
|---|
| 74 | return session, uid |
|---|
| 75 | |
|---|
| 76 | def expiredSession(self, session): |
|---|
| 77 | uid = session.get_uid() |
|---|
| 78 | if uid in self.sessions: |
|---|
| 79 | log.msg('Session %r expired' % uid) |
|---|
| 80 | self.sessions.pop(uid) |
|---|
| 81 | if self.tick.running and len(self.sessions) == 0: |
|---|
| 82 | self.tick.stop() |
|---|
| 83 | |
|---|
| 84 | def getSession(self, uid): |
|---|
| 85 | session = self.sessions.get(uid) |
|---|
| 86 | if session: |
|---|
| 87 | session.touch() |
|---|
| 88 | return session |
|---|
| 89 | |
|---|
| 90 | def createSessionID(self): |
|---|
| 91 | """Generate a new session ID.""" |
|---|
| 92 | data = "%s_%s" % (str(random.random()) , str(time.time())) |
|---|
| 93 | return md5.new(data).hexdigest() |
|---|
| 94 | |
|---|
| 95 | def _tick(self): |
|---|
| 96 | """Remove expired sessions. |
|---|
| 97 | """ |
|---|
| 98 | now = time.time() |
|---|
| 99 | for uid, session in self.sessions.items(): |
|---|
| 100 | age = now - session.lastAccessed |
|---|
| 101 | max = self.sessionLifetime |
|---|
| 102 | #if session.persistent: |
|---|
| 103 | # max = self.sessionPersistentLifetime |
|---|
| 104 | if age > max: |
|---|
| 105 | log.msg('Session %r expired' % uid) |
|---|
| 106 | self.sessions[uid].expire() |
|---|
| 107 | if not self.sessions and self.tick.running: |
|---|
| 108 | self.tick.stop() |
|---|
| 109 | |
|---|
| 110 | class MindManager(object): |
|---|
| 111 | """Might want to use this""" |
|---|
| 112 | def __init__(self, uid): |
|---|
| 113 | self.uid = uid #uid is the session id (the cookie) |
|---|
| 114 | |
|---|
| 115 | class MySessionWrapper(object): |
|---|
| 116 | implements(iweb.IResource) |
|---|
| 117 | |
|---|
| 118 | cookieManager = None |
|---|
| 119 | mindFactory = MindManager |
|---|
| 120 | sessionManager = SessionsManager() |
|---|
| 121 | |
|---|
| 122 | # The interface to cred for when logging into the portal |
|---|
| 123 | credInterface = iweb.IResource |
|---|
| 124 | |
|---|
| 125 | def __init__(self, portal): |
|---|
| 126 | self.portal = portal |
|---|
| 127 | |
|---|
| 128 | def renderHTTP(self, request): |
|---|
| 129 | """When, if ever, would this get called? |
|---|
| 130 | """ |
|---|
| 131 | log.msg("=== renderHTTP ===") |
|---|
| 132 | d = defer.maybeDeferred(self._delegate, request, []) |
|---|
| 133 | def _cb(resource, request): |
|---|
| 134 | res = iweb.IResource(resource) |
|---|
| 135 | return res.renderHTTP(request) |
|---|
| 136 | d.addCallback(_cb, request) |
|---|
| 137 | return d |
|---|
| 138 | |
|---|
| 139 | def locateChild(self, request, segments): |
|---|
| 140 | """Serve Resources depending on users Authentication status. |
|---|
| 141 | |
|---|
| 142 | Inital logic occurs here to decide the |
|---|
| 143 | authentication status of a given user. |
|---|
| 144 | """ |
|---|
| 145 | log.msg("=== locateChild 'guard.py' ===") |
|---|
| 146 | log.msg("=== %s ==" % request.args) |
|---|
| 147 | #log.msg("request.args: %s, segments: %s" % (str(request.args), segments)) |
|---|
| 148 | #see if the user already has a session going |
|---|
| 149 | if segments and segments[0] == "login": |
|---|
| 150 | #get the username and password in the postdata |
|---|
| 151 | #the callback function needs no args because the parsing |
|---|
| 152 | #of the POSTData just updates the request args. |
|---|
| 153 | l = server.parsePOSTData(request) |
|---|
| 154 | l.addCallback(lambda _: self.requestPasswordAuthentication(request, segments)) |
|---|
| 155 | return l |
|---|
| 156 | session = self.getSession(request) |
|---|
| 157 | if session is None: |
|---|
| 158 | return self.requestAnonymousAuthentication(request, segments) |
|---|
| 159 | else: |
|---|
| 160 | if segments and segments[0] == "logout": |
|---|
| 161 | log.msg("=== logout ===") |
|---|
| 162 | return self.logout(session, request, segments) |
|---|
| 163 | else: |
|---|
| 164 | log.msg("session found ... locateResource") |
|---|
| 165 | creds = session.get_authCreds() |
|---|
| 166 | return self.locateResource(request, segments, session, creds) |
|---|
| 167 | |
|---|
| 168 | def locateResource(self, request, segments, session, creds): |
|---|
| 169 | """Locate the resource for an authenticated session. |
|---|
| 170 | |
|---|
| 171 | This method is used to actually get the resource that |
|---|
| 172 | was requested after the request is passed through locateChild. |
|---|
| 173 | |
|---|
| 174 | It is in locateChild where the users session is checked. |
|---|
| 175 | """ |
|---|
| 176 | log.msg("=== locateResource 'myguard.py' ===") |
|---|
| 177 | def _success(avatar, request, segments): |
|---|
| 178 | iface, rsrc, logout = avatar |
|---|
| 179 | return rsrc, segments |
|---|
| 180 | #mind = self.mindFactory(request, creds) |
|---|
| 181 | mind = [session.get_uid(), request.args, segments] |
|---|
| 182 | d = self.portal.login(creds, mind, self.credInterface) |
|---|
| 183 | d.addCallback(_success, request, segments) |
|---|
| 184 | d.addErrback(self._loginFailure, request, segments, "Incorrect login.") |
|---|
| 185 | return d |
|---|
| 186 | |
|---|
| 187 | def _loginFailure(self, avatar, request, segments, reason): |
|---|
| 188 | return self, () |
|---|
| 189 | |
|---|
| 190 | def _delegate(self, request, segments): |
|---|
| 191 | """Identify session by the http cookie. |
|---|
| 192 | |
|---|
| 193 | If no session exists, create a new session defined |
|---|
| 194 | by a uid, and then set that uid as the cookie. |
|---|
| 195 | """ |
|---|
| 196 | cookie = request.headers.getHeader('cookie') |
|---|
| 197 | if cookie in self.sessions: |
|---|
| 198 | session = self.sessions[cookie] |
|---|
| 199 | return self.checkLogin(request, session, segments) |
|---|
| 200 | else: |
|---|
| 201 | return self.sessionManager.createSession(request, segments), () |
|---|
| 202 | |
|---|
| 203 | def requestPasswordAuthentication(self, request, segments): |
|---|
| 204 | """ |
|---|
| 205 | Try to athenticate with a username and password from |
|---|
| 206 | a web login form. Depending on the given credentials, |
|---|
| 207 | return a custom 'view' of protected resources. |
|---|
| 208 | |
|---|
| 209 | """ |
|---|
| 210 | log.msg("=== requestPasswordAuthentication ===") |
|---|
| 211 | creds = self.getCredentials(request) |
|---|
| 212 | session, newCookie = self.sessionManager.createSession() |
|---|
| 213 | mind = [newCookie, request.args, segments] |
|---|
| 214 | d = self.portal.login(creds, mind, self.credInterface) |
|---|
| 215 | d.addCallback(self._loginSuccess, session, creds, segments) |
|---|
| 216 | return d |
|---|
| 217 | |
|---|
| 218 | def requestAnonymousAuthentication(self, request, segments): |
|---|
| 219 | """ |
|---|
| 220 | Anonymous authentication, the user can only see |
|---|
| 221 | non-protected resources. |
|---|
| 222 | """ |
|---|
| 223 | log.msg("=== requestAnonymousAuthentication ===") |
|---|
| 224 | def _success(avatar, request, segments): |
|---|
| 225 | iface, resource, logout = avatar |
|---|
| 226 | return resource, segments |
|---|
| 227 | creds = credentials.Anonymous() #anonymous user. |
|---|
| 228 | session, newCookie = self.sessionManager.createSession() |
|---|
| 229 | session.set_authCreds(creds) |
|---|
| 230 | mind = [newCookie, None, None] |
|---|
| 231 | d = self.portal.login(creds, mind, self.credInterface) |
|---|
| 232 | d.addCallback(_success, request, segments) |
|---|
| 233 | #d.addErrback(self._loginFailure, request, segments, 'Anonymous access not allowed.') |
|---|
| 234 | return d |
|---|
| 235 | |
|---|
| 236 | def getSession(self, request): |
|---|
| 237 | """Get a user's session defined by the requests cookie. |
|---|
| 238 | |
|---|
| 239 | Pull the cookie out of the http header and |
|---|
| 240 | pass it to the session manager, will handles |
|---|
| 241 | find the users actuall session object. |
|---|
| 242 | """ |
|---|
| 243 | cookie = request.headers.getHeader('cookie') |
|---|
| 244 | if cookie: |
|---|
| 245 | cookie = cookie[0].value |
|---|
| 246 | log.msg("cookie from header: %s"%cookie) |
|---|
| 247 | session = self.sessionManager.getSession(cookie) |
|---|
| 248 | return session |
|---|
| 249 | |
|---|
| 250 | def getCredentials(self, request): |
|---|
| 251 | """Get username, password from post args |
|---|
| 252 | or if they dont exist we have an anonymous |
|---|
| 253 | session |
|---|
| 254 | """ |
|---|
| 255 | username = request.args.get('email', [''])[0] |
|---|
| 256 | password = request.args.get('password', [''])[0] |
|---|
| 257 | return credentials.UsernamePassword(username, password) |
|---|
| 258 | |
|---|
| 259 | def _loginSuccess(self, (iface, rsrc, logout), session, creds, segments): |
|---|
| 260 | """Return the Root Page after log in success. |
|---|
| 261 | |
|---|
| 262 | Also saved the credentials that the user used to log in |
|---|
| 263 | to later associate these credentials with the users session. |
|---|
| 264 | """ |
|---|
| 265 | log.msg("=== _loginSuccess ===") |
|---|
| 266 | log.msg("resource: %s " % rsrc) |
|---|
| 267 | log.msg("segments: %s " % segments) |
|---|
| 268 | #put the users creds in the session object |
|---|
| 269 | session.set_authCreds(creds) |
|---|
| 270 | return rsrc, () #segments |
|---|
| 271 | |
|---|
| 272 | def _loginFailure(self, *x): #TODO |
|---|
| 273 | print x |
|---|
| 274 | |
|---|
| 275 | def incorrectLoginError(self, error, ctx, segments, loginFailure): |
|---|
| 276 | pass |
|---|
| 277 | |
|---|
| 278 | def logout(self, session, request, segments): |
|---|
| 279 | """Destroy the users session. |
|---|
| 280 | |
|---|
| 281 | This pops the uid that defines the users session |
|---|
| 282 | out of the sessions dict. A new anonymous session |
|---|
| 283 | is immediatly created for this user. |
|---|
| 284 | """ |
|---|
| 285 | log.msg("=== logout ===") |
|---|
| 286 | self.sessionManager.expiredSession(session) |
|---|
| 287 | return self.requestAnonymousAuthentication(request, segments) |
|---|
| 288 | |
|---|
| 289 | |
|---|