[Twisted-Python] SessionWrapper rewrapped ;-)
Matthias Urlichs
smurf at smurf.noris.de
Tue May 13 02:45:18 MDT 2003
Here's my current code WRT a (partially) rewritten SessionWrapper, with
expanded test case, and a small example on how to do authorization.
--- a/twisted/web/woven/guard.py Tue May 13 10:42:34 2003
+++ b/twisted/web/woven/guard.py Tue May 13 10:42:34 2003
@@ -8,6 +8,7 @@
import random
import time
import md5
+import base64
# Twisted Imports
@@ -17,8 +18,10 @@
from twisted.internet import reactor
from twisted.cred.error import Unauthorized
+_trans={"/":".", "+":"-", "=":"", "\n":""}
def _sessionCookie():
- return md5.new("%s_%s" % (str(random.random()) , str(time.time()))).hexdigest()
+ hash = md5.new("%s_%s" % (str(random.random()) , str(time.time())))
+ return "".join(map(lambda k: _trans.get(k,k), base64.encodestring(hash.digest())))
class GuardSession(components.Componentized):
"""A user's session with a system.
@@ -129,22 +132,29 @@
req.getSession = req.session._getSelf
class SessionWrapper(Resource):
+ # XXX TODO: This wrapper doesn't pass query strings.
- def __init__(self, rsrc, cookieKey=None):
+ def __init__(self, rsrc, cookieKey=None, on_timeout=None, lifetime=1800):
Resource.__init__(self)
self.resource = rsrc
if cookieKey is None:
cookieKey = "woven_session_" + _sessionCookie()
self.cookieKey = cookieKey
+ self.on_timeout = on_timeout
self.sessions = {}
+ self.lifetime = lifetime
def getChild(self, path, request):
# XXX refactor with PerspectiveWrapper
if not request.prepath:
return None
- pp = request.prepath.pop()
- _urlToMe = request.prePathURL()
- request.prepath.append(pp)
+ if path is not None:
+ pp = request.prepath.pop()
+ _urlToMe = request.prePathURL()
+ request.prepath.append(pp)
+ else:
+ _urlToMe = request.prePathURL()
+
def urlToChild(*ar):
c = '/'.join(ar)
if _urlToMe[-1] == '/':
@@ -152,55 +162,102 @@
return _urlToMe + c
else:
return _urlToMe + '/' + c
- # XXX
- # print "I think I'm at:", _urlToMe
+
+ # Strictly speaking, the additional step through INIT_SESSION is
+ # not necessary. TODO: As soon as PerspectiveWrapper and
+ # SessionWrapper are folded into one class, the INIT_SESSION
+ # step can be removed.
+
cookie = request.getCookie(self.cookieKey)
- setupURL = request.setupSessionURL = urlToChild(INIT_SESSION, *([path]+request.postpath))
+ setupURL = request.setupSessionURL = urlToChild(self.cookieKey+"_"+INIT_SESSION, *request.postpath)
request.setupSession = lambda: Redirect(setupURL)
- if self.sessions.has_key(path):
- self.sessions[path].setLifetime(1800)
- if cookie == path:
- # /sessionized-url/aef9c34aecc3d9148/foo
- # ^
- # we are this getChild
- # with a matching cookie
- rd = Redirect(urlToChild(*request.postpath))
- rd.isLeaf = 1
- return rd
- else:
- # We attempted to negotiate the session but failed (the user
- # probably has cookies disabled): now we're going to return the
- # resource we contain. In general the getChild shouldn't stop
- # there.
- # /sessionized-url/aef9c34aecc3d9148/foo
- # ^ we are this getChild
- # without a cookie (or with a mismatched cookie)
- _setSession(self, request, path)
+
+ if path == self.cookieKey:
+ # /sessionized-url/KEY/foo
+ # ^ this getChild
+ # standard case if the client does cookies
+ if self.sessions.has_key(cookie):
+ self.sessions[cookie].setLifetime(self.lifetime)
+ _setSession(self, request, cookie)
return self.resource
- elif self.sessions.has_key(cookie):
- # /sessionized-url/foo
- # ^ we are this getChild
- # with a session
- _setSession(self, request, cookie)
- return self.resource.getChildWithDefault(path, request)
- elif path == INIT_SESSION:
+ else:
+ request.getSession = lambda interface=None: None
+ if cookie is not None and self.on_timeout:
+ # The session cookie has expired.
+ return self.on_timeout(path,request)
+ else:
+ # The client probably used a cached link.
+ return self.resource
+
+ elif path == self.cookieKey+"_"+INIT_SESSION:
# initialize the session
- # /sessionized-url/session-init
+ # /sessionized-url/KEY_session-init/foo
# ^ this getChild
- # without a session
+ # I setup a cookie and redirect the client
+ # to the cookie test, below.
newCookie = _sessionCookie()
request.addCookie(self.cookieKey, newCookie, path="/")
- rd = Redirect(urlToChild(newCookie,*request.postpath))
- rd.isLeaf = 1
sz = self.sessions[newCookie] = GuardSession(self, newCookie)
sz.checkExpired()
- return rd
+ return Redirect(urlToChild(self.cookieKey+"__"+newCookie,
+ *request.postpath))
+
+ elif path is not None and path.startswith(self.cookieKey+"__"):
+ # /sessionized-url/KEY__cookie/foo
+ # ^ this getChild
+ # Check whether the cookie was returned
+ path_cookie = path[len(self.cookieKey)+2:]
+ if cookie == path_cookie: # yes: use session-less path
+ return Redirect(urlToChild(self.cookieKey,*request.postpath))
+ else: # no: use session-ized path
+ return Redirect(urlToChild(self.cookieKey+"_"+path_cookie,*request.postpath))
+
+ elif path is not None and path.startswith(self.cookieKey+"_"):
+ # /sessionized-url/KEY_cookie/foo
+ # ^ this getChild
+ # standard case if the client doesn't do cookies
+ cookie = path[len(self.cookieKey)+1:]
+ if self.sessions.has_key(cookie):
+ self.sessions[cookie].setLifetime(self.lifetime)
+ _setSession(self, request, cookie)
+ return self.resource.getChildWithDefault(path, request)
+ else:
+ # I need to setup a session.
+ request.getSession = lambda interface=None: None
+ return self.resource
+
else:
# /sessionized-url/foo
- # ^ we are this getChild
- # without a session
- request.getSession = lambda interface=None: None
- return self.resource.getChildWithDefault(path, request)
+ # ^ this getChild
+ # either there is no session data yet, or the client
+ # re-entered via an external link
+
+ # This is the only case where the path is not a session
+ # element, so change the setupURL to include it.
+ if path is None:
+ ppath=[]
+ else:
+ ppath=[path]
+ setupURL = request.setupSessionURL = urlToChild(self.cookieKey+"_"+INIT_SESSION, *(ppath+request.postpath))
+ request.setupSession = lambda: Redirect(setupURL)
+
+ if self.sessions.has_key(cookie):
+ # I need to (re-)insert the session marker into the path
+ self.sessions[cookie].setLifetime(self.lifetime)
+ return Redirect(urlToChild(self.cookieKey,*(ppath+request.postpath)))
+ else:
+ # I need to setup a session. No way to figure out
+ # whether this is a re-enter, unfortunately.
+ request.getSession = lambda interface=None: None
+ if path is None:
+ return self.resource
+ else:
+ return self.resource.getChildWithDefault(path, request)
+
+ def render(self,request):
+ """A session-ized path is called directly."""
+ # For now, just munge /foo into /foo/.
+ return self.getChild(None,request).render(request)
INIT_PERSPECTIVE = 'perspective-init'
DESTROY_PERSPECTIVE = 'perspective-destroy'
@@ -284,6 +341,18 @@
if sc:
return sc.getChildWithDefault(path, request)
return self.noAuthResource.getChildWithDefault(path, request)
+
+ def render(self,request):
+ """A perspective is called directly."""
+ s = request.getSession()
+ if s is None:
+ return request.setupSession().render(request)
+
+ sc = s.clientForService(self.service)
+ if sc:
+ return sc.render(request)
+ return self.noAuthResource.render(request)
+
from twisted.web.woven import interfaces, utils
--- a/ChangeLog Tue May 13 10:42:34 2003
+++ b/ChangeLog Tue May 13 10:42:34 2003
@@ -1,4 +1,11 @@
-2003-5-12 Moshe Zadka <moshez at twistedmatrix.com>
+2003-05-13 Matthias Urlichs <smurf at smurf.noris.de>
+
+ * twisted/web/woven/guard.py, twisted/test/test_woven.py: Rewrote
+ SessionWrapper to have constant-length paths. Made its logic more
+ transparent.
+ * doc/examples/woven_guard.py: Added example code to use it.
+
+2003-05-12 Moshe Zadka <moshez at twistedmatrix.com>
* twisted/lore/default.py twisted/lore/latex.py
twisted/lore/lint.py twisted/lore/math.py twisted/lore/tree.py
--- /dev/null Wed Dec 31 16:00:00 1969
+++ b/doc/examples/woven_guard.py Tue May 13 10:42:34 2003
@@ -0,0 +1,70 @@
+import sys
+import os
+from twisted.internet import app
+from twisted.web import static, server
+from twisted.protocols import http, policies
+from twisted.cred import authorizer
+from twisted.web.woven import guard
+
+class termFile(static.File):
+ def getChild(self,name,*args,**kwargs):
+ return self
+
+# authHelper and authFactory could easily be written as inline or lambda
+# functions. Don't do that, though -- otherwise twisted.persisted will
+# crash when it tries to save your PerspectiveWrapper.
+class authHelper:
+ def __init__(self,page):
+ self.page = page
+ def __call__(self,p,q=None):
+ if q is None:
+ q=self.page
+ return q
+def authFactory(page): return page
+
+# This resource shows yes_page if you're logged in, and no_page if you aren't.
+def setup_auth(yes_page, no_page):
+ from twisted.internet.app import MultiService
+ from twisted.cred.authorizer import DefaultAuthorizer
+ from twisted.cred.service import Service
+
+ ms = MultiService("security_test")
+ auth = DefaultAuthorizer(ms)
+ svc = Service("security_test_service", ms, auth)
+ myp = svc.createPerspective("test_one")
+ myp.makeIdentity("test_two")
+
+ pwrap = guard.PerspectiveWrapper(svc, no_page, authHelper(yes_page),
+ callback=authFactory)
+ swrap = guard.SessionWrapper(pwrap, cookieKey="GuardTest")
+ return swrap
+
+def main():
+ root = static.Data("""\
+ <html><body>
+ Test.<br />The authorizer is <a href="auth-test">here.</a>
+ </body></html>
+ """,'text/html')
+
+ yes = static.Data("""\
+ <html><head>Success!</head>
+ <body>Login successful!</body></html>
+ """,'text/html')
+ no = static.Data("""\
+ <html><head>Not logged in</head>
+ <body><p>Log in <a href="perspective-init">here</a></p>
+ <code>test_one test_two</code></body></html>
+ """,'text/html')
+ root.putChild("auth-test",setup_auth(yes,no))
+
+ site = server.Site(root)
+ site = policies.TimeoutFactory(site,300)
+
+ basename = os.path.splitext(os.path.basename(sys.argv[0]))[0]
+ me = app.Application(basename)
+ me.listenTCP(50080, site)
+ me.bindPorts()
+ me.run()
+
+if __name__ == '__main__':
+ main()
--- a/twisted/test/test_woven.py Tue May 13 10:42:34 2003
+++ b/twisted/test/test_woven.py Tue May 13 10:42:34 2003
@@ -474,7 +474,7 @@
sessWrapped = static.Data("you should never see this", "text/plain")
swChild = static.Data("NO", "text/plain")
sessWrapped.putChild("yyy",swChild)
- swrap = guard.SessionWrapper(sessWrapped)
+ swrap = guard.SessionWrapper(sessWrapped,cookieKey="TesT")
da = static.Data("b","text/plain")
da.putChild("xxx", swrap)
st = FakeSite(da)
@@ -491,23 +491,56 @@
# now we're going to make sure that the redirect and cookie are properly set
req = FakeHTTPRequest(chan, queued=0)
- req.requestReceived("GET", "/xxx/"+guard.INIT_SESSION, "1.0")
+ req.requestReceived("GET", "/xxx/TesT_"+guard.INIT_SESSION, "1.0")
ccv = req._cookieCache.values()
self.assertEquals(len(ccv),1)
cookie = ccv[0]
# redirect set?
self.failUnless(req.headers.has_key('location'))
# redirect matches cookie?
- self.assertEquals(req.headers['location'].split('/')[-1], cookie)
+ self.assertEquals(req.headers['location'].split('/')[-1], "TesT__"+cookie)
# URL is correct?
self.assertEquals(req.headers['location'],
- 'http://fake.com/xxx/'+cookie)
+ 'http://fake.com/xxx/TesT__'+cookie)
oldreq = req
+
+ # now we're going to make sure that the redirect and cookie are properly set
+ req = FakeHTTPRequest(chan, queued=0)
+ req.received_cookies["TesT"] = cookie
+ req.requestReceived("GET", "/xxx/TesT__"+cookie, "1.0")
+ ccv = req._cookieCache.values()
+ self.assertEquals(len(ccv),0) # no cookie sent back here
+ # redirect set?
+ self.failUnless(req.headers.has_key('location'))
+ # redirect matches cookie?
+ self.assertEquals(req.headers['location'].split('/')[-1], 'TesT')
+ # URL is correct?
+ self.assertEquals(req.headers['location'],
+ 'http://fake.com/xxx/TesT')
- # now let's try with a request for the session-cookie URL that has a cookie set
- url = "/"+(oldreq.headers['location'].split('http://fake.com/',1))[1]
- req = chan.makeFakeRequest(url)
- self.assertEquals(req.headers['location'], 'http://fake.com/xxx/')
+ # If there's a cookie but the session element is missing, it
+ # gets added in.
+ req = FakeHTTPRequest(chan, queued=0)
+ req.received_cookies["TesT"] = cookie
+ req.requestReceived("GET", "/xxx/yyy", "1.0")
+ self.assertEquals(req.headers['location'], 'http://fake.com/xxx/TesT/yyy')
+
+ # now do the same with a client that doesn't accept cookies:
+ # we get a cookie-ized path back
+ req = FakeHTTPRequest(chan, queued=0)
+ req.requestReceived("GET", "/xxx/TesT__"+cookie, "1.0")
+ ccv = req._cookieCache.values()
+ self.assertEquals(len(ccv),0) # no cookie here, either
+ # redirect set?
+ self.failUnless(req.headers.has_key('location'))
+ # redirect matches cookie?
+ self.assertEquals(req.headers['location'].split('/')[-1], 'TesT_'+cookie)
+ # URL is correct?
+ self.assertEquals(req.headers['location'],
+ 'http://fake.com/xxx/TesT_'+cookie)
+
+
+ # clean up
for sz in swrap.sessions.values():
sz.expire()
@@ -531,23 +564,23 @@
q.putChild("yyy", static.Data("YES", "text/plain"))
authFactory = lambda p, q=q: q
pwrap = guard.PerspectiveWrapper(svc, sessWrapped, authFactory)
- swrap = guard.SessionWrapper(pwrap)
+ swrap = guard.SessionWrapper(pwrap, cookieKey="TesT") # as above
da.putChild("xxx", swrap)
st = FakeSite(da)
chan = FakeHTTPChannel()
chan.site = st
- req = chan.makeFakeRequest("/xxx/"+guard.INIT_SESSION+"/yyy")
- req = chan.makeFakeRequest("/xxx/yyy")
+ req = chan.makeFakeRequest("/xxx/TesT_"+guard.INIT_SESSION+"/yyy")
+ req = chan.makeFakeRequest("/xxx/TesT/yyy")
self.assertEquals(req.written.getvalue(),"NO")
- req = chan.makeFakeRequest("/xxx/"+guard.INIT_PERSPECTIVE+
+ req = chan.makeFakeRequest("/xxx/TesT/"+guard.INIT_PERSPECTIVE+
"?identity=test&password=tenxt")
assert not req.session.services.values()
- req = chan.makeFakeRequest("/xxx/"+guard.INIT_PERSPECTIVE+
+ req = chan.makeFakeRequest("/xxx/TesT/"+guard.INIT_PERSPECTIVE+
"?identity=test&password=test")
self.assertEquals(req.session.services.values()[0][0], myp)
# print req.written.getvalue()
- req = chan.makeFakeRequest("/xxx/yyy")
+ req = chan.makeFakeRequest("/xxx/TesT/yyy")
self.assertEquals(req.written.getvalue(), "YES")
# print req.session.services
for sz in swrap.sessions.values():
--
Matthias Urlichs | {M:U} IT Consulting @ m-u-it.de | smurf at smurf.noris.de
Disclaimer: The quote was selected randomly. Really. | http://smurf.noris.de
--
When voting on appropriations bills, more is not necessarily better. It is
as wasteful to have a B-1 bomber in every garage as it is to have a welfare
program for every conceivable form of deprivation.
-- Pierre S. du Pont
More information about the Twisted-Python
mailing list