Index: docs/web/howto/using-twistedweb.rst =================================================================== --- docs/web/howto/using-twistedweb.rst (revision 45109) +++ docs/web/howto/using-twistedweb.rst (working copy) @@ -442,9 +442,23 @@ .. image:: ../img/web-session.png +The default session cookie name is ``TWISTED_SESSION``. A custom name can be produced by supplying the ``sessionCookiePrefix`` keyword argument, while creating a new site instance. See :api:`twisted.web.server.Site `. +.. code-block:: python + from twisted.web.server import Request, Site + from twisted.web.resource import Resource + + class SimpleResource(Resource): + isLeaf = True + def render_GET(self, request): + return b'Hello, world!' + + site = Site(SimpleResource(), sessionCookiePrefix=b'MY_WEBSITE_SESSION') + + + Proxies and reverse proxies ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Index: twisted/web/server.py =================================================================== --- twisted/web/server.py (revision 45109) +++ twisted/web/server.py (working copy) @@ -402,7 +402,7 @@ def getSession(self, sessionInterface=None): # Session management if not self.session: - cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) + cookiename = self.site.makeSessionCookieName(self.sitepath) sessionCookie = self.getCookie(cookiename) if sessionCookie: try: @@ -629,11 +629,13 @@ @ivar counter: increment value used for generating unique sessions ID. @ivar requestFactory: A factory which is called with (channel, queued) - and creates L{Request} instances. Default to L{Request}. + and creates L{Request} instances. Defaults to L{Request}. @ivar displayTracebacks: if set, Twisted internal errors are displayed on rendered pages. Default to C{True}. @ivar sessionFactory: factory for sessions objects. Default to L{Session}. @ivar sessionCheckTime: Deprecated. See L{Session.sessionTimeout} instead. + @ivar sessionCookiePrefix: A prefix for session cookies. The default is + C{b'TWISTED_SESSION'} """ counter = 0 requestFactory = Request @@ -640,6 +642,7 @@ displayTracebacks = True sessionFactory = Session sessionCheckTime = 1800 + sessionCookiePrefix = b'TWISTED_SESSION' def __init__(self, resource, requestFactory=None, *args, **kwargs): """ @@ -649,9 +652,19 @@ @type resource: L{IResource} provider @param requestFactory: Overwrite for default requestFactory. @type requestFactory: C{callable} or C{class}. + @param sessionCookiePrefix: Overwrite the default session cookie + prefix. + @type sessionCookiePrefix: C{bytes} @see: L{twisted.web.http.HTTPFactory.__init__} """ + # Handling and deleting it here, since the HTTPFactory throws + # unexpected argument on it. + sessionCookiePrefix = None + if 'sessionCookiePrefix' in kwargs: + self.sessionCookiePrefix = kwargs['sessionCookiePrefix'] + del kwargs['sessionCookiePrefix'] + http.HTTPFactory.__init__(self, *args, **kwargs) self.sessions = {} self.resource = resource @@ -682,6 +695,18 @@ ).hexdigest() + def makeSessionCookieName(self, sitepath=None): + """ + Generate a cookie name used to identify a session. + + @return The generated cookie name. Type: C{bytes} + """ + if sitepath: + return b"_".join([self.sessionCookiePrefix] + sitepath) + else: + return self.sessionCookiePrefix + + def makeSession(self): """ Generate a new Session instance, and store it for future reference. Index: twisted/web/test/test_web.py =================================================================== --- twisted/web/test/test_web.py (revision 45109) +++ twisted/web/test/test_web.py (working copy) @@ -10,7 +10,9 @@ from zope.interface import implementer from zope.interface.verify import verifyObject +from zope.interface import Attribute, Interface +from twisted.python import components from twisted.python.compat import _PY3 from twisted.python.filepath import FilePath from twisted.trial import unittest @@ -54,6 +56,20 @@ +class TaskClockSession(server.Session): + """ + A session which uses task.Clock for `touch` method. + """ + def __init__(self, site=None, uid=0): + server.Session.__init__( + self, site=site, uid=uid, reactor=Clock()) + + def touch(self): + self._reactor.advance(1) + return server.Session.touch(self) + + + class SiteTest(unittest.TestCase): """ Unit tests for L{server.Site}. @@ -66,7 +82,7 @@ """ sres1 = SimpleResource() sres2 = SimpleResource() - sres1.putChild(b"",sres2) + sres1.putChild(b"", sres2) site = server.Site(sres1) self.assertIdentical( site.getResourceFor(DummyRequest([b''])), @@ -107,7 +123,22 @@ self.assertIs(site.requestFactory, channel.requestFactory) + def test_makeSessionCookieName(self): + """ + A custom prefix for session cookies. + """ + prefix = b'CUSTOM_COOKIE_PREFIX' + sitepath = [b'TEST', b'SUFFIX'] + site = server.Site(resource=SimpleResource(), + sessionCookiePrefix=prefix) + self.assertEqual(site.sessionCookiePrefix, prefix) + self.assertEqual(site.makeSessionCookieName(), prefix) + self.assertEqual(site.makeSessionCookieName(sitepath=sitepath), + prefix + b'_TEST_SUFFIX') + + + class SessionTests(unittest.TestCase): """ Tests for L{server.Session}. @@ -413,6 +444,7 @@ request.requestReceived(b'GET', b'/foo/bar/', b'HTTP/1.0') self.assertEqual(request.childLink(b'baz'), b'baz') + def testPrePathURLSimple(self): request = server.Request(DummyChannel(), 1) request.gotLength(0) @@ -420,6 +452,7 @@ request.setHost(b'example.com', 80) self.assertEqual(request.prePathURL(), b'http://example.com/foo/bar') + def testPrePathURLNonDefault(self): d = DummyChannel() d.transport.port = 81 @@ -429,6 +462,7 @@ request.requestReceived(b'GET', b'/foo/bar', b'HTTP/1.0') self.assertEqual(request.prePathURL(), b'http://example.com:81/foo/bar') + def testPrePathURLSSLPort(self): d = DummyChannel() d.transport.port = 443 @@ -438,6 +472,7 @@ request.requestReceived(b'GET', b'/foo/bar', b'HTTP/1.0') self.assertEqual(request.prePathURL(), b'http://example.com:443/foo/bar') + def testPrePathURLSSLPortAndSSL(self): d = DummyChannel() d.transport = DummyChannel.SSL() @@ -448,6 +483,7 @@ request.requestReceived(b'GET', b'/foo/bar', b'HTTP/1.0') self.assertEqual(request.prePathURL(), b'https://example.com/foo/bar') + def testPrePathURLHTTPPortAndSSL(self): d = DummyChannel() d.transport = DummyChannel.SSL() @@ -458,6 +494,7 @@ request.requestReceived(b'GET', b'/foo/bar', b'HTTP/1.0') self.assertEqual(request.prePathURL(), b'https://example.com:80/foo/bar') + def testPrePathURLSSLNonDefault(self): d = DummyChannel() d.transport = DummyChannel.SSL() @@ -468,6 +505,7 @@ request.requestReceived(b'GET', b'/foo/bar', b'HTTP/1.0') self.assertEqual(request.prePathURL(), b'https://example.com:81/foo/bar') + def testPrePathURLSetSSLHost(self): d = DummyChannel() d.transport.port = 81 @@ -491,7 +529,88 @@ self.assertEqual(request.prePathURL(), b'http://example.com/foo%2Fbar') + def test_getSessionAlreadyExists(self): + """ + When a session already exists, it will return the same session and + update its modification. + """ + channel = DummyChannel() + request = server.Request(channel, 1) + session = TaskClockSession() + initialTime = session.lastModified + request.session = session + result = request.getSession() + + self.assertIs(session, result) + self.assertGreater(result.lastModified, initialTime) + + + class ISessionObject(Interface): + """ + A simple interface for testing session components. + """ + value = Attribute("A marker value for this component.") + + + @implementer(ISessionObject) + class SessionObject(object): + """ + A simple component. + """ + def __init__(self, session): + self.value = 42 + + + def test_getSessionComponent(self): + """ + When sessionInterface is provided it will return the + C{sessionInterface} component associated with this session. + """ + # Register adapter for this test and remove it once test is done. + components.registerAdapter( + self.SessionObject, server.Session, self.ISessionObject) + # Un-registration is done by registering None. + self.addCleanup( + components.getRegistry().register, + [self.ISessionObject], server.Session, '', None) + channel = DummyChannel() + request = server.Request(channel, 1) + session = TaskClockSession() + request.session = session + + result = request.getSession(sessionInterface=self.ISessionObject) + + self.assertIsInstance(result, self.SessionObject) + self.assertEqual(42, result.value) + + + def test_getSessionNonExistent(self): + """ + When request (or the site associated with this request) has no + previous session, a new one is created using the name provided by the + site's `makeSessionCookieName` as cookie which stores the session id on + the web client (browser). + """ + site = server.Site(resource.Resource()) + site.sessionFactory = TaskClockSession + channel = DummyChannel() + channel.site = site + request = server.Request(channel, 1) + request.sitepath = [] + request.site = site + + session = request.getSession() + + sessionRawCookie = '%s=%s; Path=/' % ( + site.makeSessionCookieName(), session.uid,) + self.assertEqual(sessionRawCookie, request.cookies[0]) + + # Getting the session again, should return the same object as before. + self.assertIs(session, request.getSession()) + + + class GzipEncoderTests(unittest.TestCase): if _PY3: Index: twisted/web/topfiles/6933.feature =================================================================== --- twisted/web/topfiles/6933.feature (revision 0) +++ twisted/web/topfiles/6933.feature (working copy) @@ -0,0 +1 @@ +twisted.web.server.Site now allows specifying a custom prefix for the session cookie. \ No newline at end of file