| OLD | NEW |
| (Empty) |
| 1 # -*- test-case-name: twisted.web.test.test_woven -*- | |
| 2 | |
| 3 # Copyright (c) 2001-2004 Twisted Matrix Laboratories. | |
| 4 # See LICENSE for details. | |
| 5 | |
| 6 # | |
| 7 | |
| 8 """Resource protection for Woven. If you wish to use twisted.cred to protect | |
| 9 your Woven application, you are probably most interested in | |
| 10 L{UsernamePasswordWrapper}. | |
| 11 """ | |
| 12 | |
| 13 from __future__ import nested_scopes | |
| 14 | |
| 15 __version__ = "$Revision: 1.34 $"[11:-2] | |
| 16 | |
| 17 import random | |
| 18 import time | |
| 19 import md5 | |
| 20 import urllib | |
| 21 | |
| 22 # Twisted Imports | |
| 23 | |
| 24 from twisted.python import log, components | |
| 25 from twisted.web.resource import Resource, IResource | |
| 26 from twisted.web.util import redirectTo, Redirect, DeferredResource | |
| 27 from twisted.web.static import addSlash | |
| 28 from twisted.internet import reactor | |
| 29 from twisted.cred.error import LoginFailed, UnauthorizedLogin | |
| 30 | |
| 31 def _sessionCookie(): | |
| 32 return md5.new("%s_%s" % (str(random.random()) , str(time.time()))).hexdiges
t() | |
| 33 | |
| 34 class GuardSession(components.Componentized): | |
| 35 """A user's session with a system. | |
| 36 | |
| 37 This utility class contains no functionality, but is used to | |
| 38 represent a session. | |
| 39 """ | |
| 40 def __init__(self, guard, uid): | |
| 41 """Initialize a session with a unique ID for that session. | |
| 42 """ | |
| 43 components.Componentized.__init__(self) | |
| 44 self.guard = guard | |
| 45 self.uid = uid | |
| 46 self.expireCallbacks = [] | |
| 47 self.checkExpiredID = None | |
| 48 self.setLifetime(60) | |
| 49 self.services = {} | |
| 50 self.portals = {} | |
| 51 self.touch() | |
| 52 | |
| 53 def _getSelf(self, interface=None): | |
| 54 self.touch() | |
| 55 if interface is None: | |
| 56 return self | |
| 57 else: | |
| 58 return self.getComponent(interface) | |
| 59 | |
| 60 # Old Guard interfaces | |
| 61 | |
| 62 def clientForService(self, service): | |
| 63 x = self.services.get(service) | |
| 64 if x: | |
| 65 return x[1] | |
| 66 else: | |
| 67 return x | |
| 68 | |
| 69 def setClientForService(self, ident, perspective, client, service): | |
| 70 if self.services.has_key(service): | |
| 71 p, c, i = self.services[service] | |
| 72 p.detached(c, ident) | |
| 73 del self.services[service] | |
| 74 else: | |
| 75 self.services[service] = perspective, client, ident | |
| 76 perspective.attached(client, ident) | |
| 77 # this return value is useful for services that need to do asynchronous | |
| 78 # stuff. | |
| 79 return client | |
| 80 | |
| 81 # New Guard Interfaces | |
| 82 | |
| 83 def resourceForPortal(self, port): | |
| 84 return self.portals.get(port) | |
| 85 | |
| 86 def setResourceForPortal(self, rsrc, port, logout): | |
| 87 self.portalLogout(port) | |
| 88 self.portals[port] = rsrc, logout | |
| 89 return rsrc | |
| 90 | |
| 91 def portalLogout(self, port): | |
| 92 p = self.portals.get(port) | |
| 93 if p: | |
| 94 r, l = p | |
| 95 try: l() | |
| 96 except: log.err() | |
| 97 del self.portals[port] | |
| 98 | |
| 99 # timeouts and expiration | |
| 100 | |
| 101 def setLifetime(self, lifetime): | |
| 102 """Set the approximate lifetime of this session, in seconds. | |
| 103 | |
| 104 This is highly imprecise, but it allows you to set some general | |
| 105 parameters about when this session will expire. A callback will be | |
| 106 scheduled each 'lifetime' seconds, and if I have not been 'touch()'ed | |
| 107 in half a lifetime, I will be immediately expired. | |
| 108 """ | |
| 109 self.lifetime = lifetime | |
| 110 | |
| 111 def notifyOnExpire(self, callback): | |
| 112 """Call this callback when the session expires or logs out. | |
| 113 """ | |
| 114 self.expireCallbacks.append(callback) | |
| 115 | |
| 116 def expire(self): | |
| 117 """Expire/logout of the session. | |
| 118 """ | |
| 119 log.msg("expired session %s" % self.uid) | |
| 120 del self.guard.sessions[self.uid] | |
| 121 for c in self.expireCallbacks: | |
| 122 try: | |
| 123 c() | |
| 124 except: | |
| 125 log.err() | |
| 126 self.expireCallbacks = [] | |
| 127 if self.checkExpiredID: | |
| 128 self.checkExpiredID.cancel() | |
| 129 self.checkExpiredID = None | |
| 130 | |
| 131 def touch(self): | |
| 132 self.lastModified = time.time() | |
| 133 | |
| 134 def checkExpired(self): | |
| 135 self.checkExpiredID = None | |
| 136 # If I haven't been touched in 15 minutes: | |
| 137 if time.time() - self.lastModified > self.lifetime / 2: | |
| 138 if self.guard.sessions.has_key(self.uid): | |
| 139 self.expire() | |
| 140 else: | |
| 141 log.msg("no session to expire: %s" % self.uid) | |
| 142 else: | |
| 143 log.msg("session given the will to live for %s more seconds" % self.
lifetime) | |
| 144 self.checkExpiredID = reactor.callLater(self.lifetime, | |
| 145 self.checkExpired) | |
| 146 def __getstate__(self): | |
| 147 d = self.__dict__.copy() | |
| 148 if d.has_key('checkExpiredID'): | |
| 149 del d['checkExpiredID'] | |
| 150 return d | |
| 151 | |
| 152 def __setstate__(self, d): | |
| 153 self.__dict__.update(d) | |
| 154 self.touch() | |
| 155 self.checkExpired() | |
| 156 | |
| 157 INIT_SESSION = 'session-init' | |
| 158 | |
| 159 def _setSession(wrap, req, cook): | |
| 160 req.session = wrap.sessions[cook] | |
| 161 req.getSession = req.session._getSelf | |
| 162 | |
| 163 def urlToChild(request, *ar, **kw): | |
| 164 pp = request.prepath.pop() | |
| 165 orig = request.prePathURL() | |
| 166 request.prepath.append(pp) | |
| 167 c = '/'.join(ar) | |
| 168 if orig[-1] == '/': | |
| 169 # this SHOULD only happen in the case where the URL is just the hostname | |
| 170 ret = orig + c | |
| 171 else: | |
| 172 ret = orig + '/' + c | |
| 173 args = request.args.copy() | |
| 174 args.update(kw) | |
| 175 if args: | |
| 176 ret += '?'+urllib.urlencode(args) | |
| 177 return ret | |
| 178 | |
| 179 def redirectToSession(request, garbage): | |
| 180 rd = Redirect(urlToChild(request, *request.postpath, **{garbage:1})) | |
| 181 rd.isLeaf = 1 | |
| 182 return rd | |
| 183 | |
| 184 SESSION_KEY='__session_key__' | |
| 185 | |
| 186 class SessionWrapper(Resource): | |
| 187 | |
| 188 sessionLifetime = 1800 | |
| 189 | |
| 190 def __init__(self, rsrc, cookieKey=None): | |
| 191 Resource.__init__(self) | |
| 192 self.resource = rsrc | |
| 193 if cookieKey is None: | |
| 194 cookieKey = "woven_session_" + _sessionCookie() | |
| 195 self.cookieKey = cookieKey | |
| 196 self.sessions = {} | |
| 197 | |
| 198 def render(self, request): | |
| 199 return redirectTo(addSlash(request), request) | |
| 200 | |
| 201 def getChild(self, path, request): | |
| 202 if not request.prepath: | |
| 203 return None | |
| 204 cookie = request.getCookie(self.cookieKey) | |
| 205 setupURL = urlToChild(request, INIT_SESSION, *([path]+request.postpath)) | |
| 206 request.setupSessionURL = setupURL | |
| 207 request.setupSession = lambda: Redirect(setupURL) | |
| 208 if path.startswith(SESSION_KEY): | |
| 209 key = path[len(SESSION_KEY):] | |
| 210 if key not in self.sessions: | |
| 211 return redirectToSession(request, '__start_session__') | |
| 212 self.sessions[key].setLifetime(self.sessionLifetime) | |
| 213 if cookie == key: | |
| 214 # /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo | |
| 215 # ^ | |
| 216 # we are this getChild | |
| 217 # with a matching cookie | |
| 218 return redirectToSession(request, '__session_just_started__') | |
| 219 else: | |
| 220 # We attempted to negotiate the session but failed (the user | |
| 221 # probably has cookies disabled): now we're going to return the | |
| 222 # resource we contain. In general the getChild shouldn't stop | |
| 223 # there. | |
| 224 # /sessionized-url/${SESSION_KEY}aef9c34aecc3d9148/foo | |
| 225 # ^ we are this getChild | |
| 226 # without a cookie (or with a mismatched cookie) | |
| 227 _setSession(self, request, key) | |
| 228 return self.resource | |
| 229 elif cookie in self.sessions: | |
| 230 # /sessionized-url/foo | |
| 231 # ^ we are this getChild | |
| 232 # with a session | |
| 233 _setSession(self, request, cookie) | |
| 234 return getResource(self.resource, path, request) | |
| 235 elif path == INIT_SESSION: | |
| 236 # initialize the session | |
| 237 # /sessionized-url/session-init | |
| 238 # ^ this getChild | |
| 239 # without a session | |
| 240 newCookie = _sessionCookie() | |
| 241 request.addCookie(self.cookieKey, newCookie, path="/") | |
| 242 sz = self.sessions[newCookie] = GuardSession(self, newCookie) | |
| 243 sz.checkExpired() | |
| 244 rd = Redirect(urlToChild(request, SESSION_KEY+newCookie, | |
| 245 *request.postpath)) | |
| 246 rd.isLeaf = 1 | |
| 247 return rd | |
| 248 else: | |
| 249 # /sessionized-url/foo | |
| 250 # ^ we are this getChild | |
| 251 # without a session | |
| 252 request.getSession = lambda interface=None: None | |
| 253 return getResource(self.resource, path, request) | |
| 254 | |
| 255 def getResource(resource, path, request): | |
| 256 if resource.isLeaf: | |
| 257 request.postpath.insert(0, request.prepath.pop()) | |
| 258 return resource | |
| 259 else: | |
| 260 return resource.getChildWithDefault(path, request) | |
| 261 | |
| 262 INIT_PERSPECTIVE = 'perspective-init' | |
| 263 DESTROY_PERSPECTIVE = 'perspective-destroy' | |
| 264 | |
| 265 from twisted.python import formmethod as fm | |
| 266 from twisted.web.woven import form | |
| 267 | |
| 268 | |
| 269 newLoginSignature = fm.MethodSignature( | |
| 270 fm.String("username", "", | |
| 271 "Username", "Your user name."), | |
| 272 fm.Password("password", "", | |
| 273 "Password", "Your password."), | |
| 274 fm.Submit("submit", choices=[("Login", "", "")], allowNone=1), | |
| 275 ) | |
| 276 | |
| 277 from twisted.cred.credentials import UsernamePassword, Anonymous | |
| 278 | |
| 279 class UsernamePasswordWrapper(Resource): | |
| 280 """I bring a C{twisted.cred} Portal to the web. Use me to provide different
Resources | |
| 281 (usually entire pages) based on a user's authentication details. | |
| 282 | |
| 283 A C{UsernamePasswordWrapper} is a | |
| 284 L{Resource<twisted.web.resource.Resource>}, and is usually wrapped in a | |
| 285 L{SessionWrapper} before being inserted into the site tree. | |
| 286 | |
| 287 The L{Realm<twisted.cred.portal.IRealm>} associated with your | |
| 288 L{Portal<twisted.cred.portal.Portal>} should be prepared to accept a | |
| 289 request for an avatar that implements the L{twisted.web.resource.IResource} | |
| 290 interface. This avatar should probably be something like a Woven | |
| 291 L{Page<twisted.web.woven.page.Page>}. That is, it should represent a whole | |
| 292 web page. Once you return this avatar, requests for it's children do not go | |
| 293 through guard. | |
| 294 | |
| 295 If you want to determine what unauthenticated users see, make sure your | |
| 296 L{Portal<twisted.cred.portal.Portal>} has a checker associated that allows | |
| 297 anonymous access. (See L{twisted.cred.checkers.AllowAnonymousAccess}) | |
| 298 | |
| 299 """ | |
| 300 | |
| 301 def __init__(self, portal, callback=None, errback=None): | |
| 302 """Constructs a UsernamePasswordWrapper around the given portal. | |
| 303 | |
| 304 @param portal: A cred portal for your web application. The checkers | |
| 305 associated with this portal must be able to accept username/password | |
| 306 credentials. | |
| 307 @type portal: L{twisted.cred.portal.Portal} | |
| 308 | |
| 309 @param callback: Gets called after a successful login attempt. | |
| 310 A resource that redirects to "." will display the avatar resource. | |
| 311 If this parameter isn't provided, defaults to a standard Woven | |
| 312 "Thank You" page. | |
| 313 @type callback: A callable that accepts a Woven | |
| 314 L{model<twisted.web.woven.interfaces.IModel>} and returns a | |
| 315 L{IResource<twisted.web.resource.Resource>}. | |
| 316 | |
| 317 @param errback: Gets called after a failed login attempt. | |
| 318 If this parameter is not provided, defaults to a the standard Woven | |
| 319 form error (i.e. The original form on a page of its own, with | |
| 320 errors noted.) | |
| 321 @type errback: A callable that accepts a Woven | |
| 322 L{model<twisted.web.woven.interfaces.IModel>} and returns a | |
| 323 L{IResource<twisted.web.resource.Resource>}. | |
| 324 """ | |
| 325 Resource.__init__(self) | |
| 326 self.portal = portal | |
| 327 self.callback = callback | |
| 328 self.errback = errback | |
| 329 | |
| 330 def _ebFilter(self, f): | |
| 331 f.trap(LoginFailed, UnauthorizedLogin) | |
| 332 raise fm.FormException(password="Login failed, please enter correct user
name and password.") | |
| 333 | |
| 334 def getChild(self, path, request): | |
| 335 s = request.getSession() | |
| 336 if s is None: | |
| 337 return request.setupSession() | |
| 338 if path == INIT_PERSPECTIVE: | |
| 339 def loginSuccess(result): | |
| 340 interface, avatarAspect, logout = result | |
| 341 s.setResourceForPortal(avatarAspect, self.portal, logout) | |
| 342 | |
| 343 def triggerLogin(username, password, submit=None): | |
| 344 return self.portal.login( | |
| 345 UsernamePassword(username, password), | |
| 346 None, | |
| 347 IResource | |
| 348 ).addCallback( | |
| 349 loginSuccess | |
| 350 ).addErrback( | |
| 351 self._ebFilter | |
| 352 ) | |
| 353 | |
| 354 return form.FormProcessor( | |
| 355 newLoginSignature.method( | |
| 356 triggerLogin | |
| 357 ), | |
| 358 callback=self.callback, | |
| 359 errback=self.errback | |
| 360 ) | |
| 361 elif path == DESTROY_PERSPECTIVE: | |
| 362 s.portalLogout(self.portal) | |
| 363 return Redirect(".") | |
| 364 else: | |
| 365 r = s.resourceForPortal(self.portal) | |
| 366 if r: | |
| 367 ## Delegate our getChild to the resource our portal says is the
right one. | |
| 368 return getResource(r[0], path, request) | |
| 369 else: | |
| 370 return DeferredResource( | |
| 371 self.portal.login(Anonymous(), None, IResource | |
| 372 ).addCallback( | |
| 373 lambda (interface, avatarAspect, logout): | |
| 374 getResource(s.setResourceForPortal(avatarAspect, | |
| 375 self.portal, logout), | |
| 376 path, request))) | |
| 377 | |
| 378 | |
| 379 | |
| 380 from twisted.web.woven import interfaces, utils | |
| 381 ## Dumb hack until we have an ISession and use interface-to-interface adaption | |
| 382 components.registerAdapter(utils.WovenLivePage, GuardSession, interfaces.IWovenL
ivePage) | |
| 383 | |
| OLD | NEW |