diff --git docs/web/howto/using-twistedweb.rst docs/web/howto/using-twistedweb.rst index c475f0c..bb7d5f6 100644 --- docs/web/howto/using-twistedweb.rst +++ docs/web/howto/using-twistedweb.rst @@ -442,6 +442,26 @@ Twisted Web provides an abstraction of this browser-tracking behavior called the .. image:: ../img/web-session.png +The default session cookie name is ``TWISTED_SESSION``. A custom name can be used +by assigning a differet value to the ``sessionCookieBaseName`` variable or overwriting +``getSessionCookieName`` from :api:`twisted.web.server.Request `. + + +.. code-block:: python + + from twisted.web.server import Request, Site + from twisted.web.resource import Resource + + class CustomCookieRequest(Request): + sessionCookieBaseName = b'SOME_CUSTOM_NAME' + + class Simple(Resource): + isLeaf = True + def render_GET(self, request): + return b'Hello, world!' + + site = Site(Simple()) + site.requestFactory = CustomCookieRequest diff --git twisted/web/server.py twisted/web/server.py index 268734f..efec525 100644 --- twisted/web/server.py +++ twisted/web/server.py @@ -101,9 +101,12 @@ class Request(Copyable, http.Request, components.Componentized): @ivar defaultContentType: A C{bytes} giving the default I{Content-Type} value to send in responses if no other value is set. C{None} disables the default. + @ivar sessionCookieBaseName: The base name for session cookies. + @type sessionCookieBaseName: L{bytes} """ defaultContentType = b"text/html" + sessionCookieBaseName = b'TWISTED_SESSION' site = None appRootURL = None @@ -384,7 +387,7 @@ class Request(Copyable, http.Request, components.Componentized): def getSession(self, sessionInterface = None): # Session management if not self.session: - cookiename = b"_".join([b'TWISTED_SESSION'] + self.sitepath) + cookiename = self.getSessionCookieName() sessionCookie = self.getCookie(cookiename) if sessionCookie: try: @@ -400,6 +403,16 @@ class Request(Copyable, http.Request, components.Componentized): return self.session.getComponent(sessionInterface) return self.session + + def getSessionCookieName(self): + """ + Return the name of the cookie used for storing the session id. + + @return: The C{str} name of the session cookie. + """ + return b"_".join([self.sessionCookieBaseName] + self.sitepath) + + def _prePathURL(self, prepath): port = self.getHost().port if self.isSecure(): diff --git twisted/web/test/test_web.py twisted/web/test/test_web.py index 3c80595..a866e8d 100644 --- twisted/web/test/test_web.py +++ twisted/web/test/test_web.py @@ -8,7 +8,7 @@ Tests for various parts of L{twisted.web}. import os import zlib -from zope.interface import implementer +from zope.interface import Attribute, implementer, Interface from zope.interface.verify import verifyObject from twisted.python.compat import _PY3, networkString @@ -19,6 +19,7 @@ from twisted.internet.address import IPv4Address from twisted.internet.task import Clock from twisted.web import server, resource from twisted.web import iweb, http, error +from twisted.python import components, log from twisted.web.test.requesthelper import DummyChannel, DummyRequest @@ -367,6 +368,20 @@ class ConditionalTest(unittest.TestCase): +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 RequestTests(unittest.TestCase): """ Tests for the HTTP request class, L{server.Request}. @@ -468,6 +483,102 @@ class RequestTests(unittest.TestCase): 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 + `getSessionCookieName` as cookie is set to inform the web client + about session id. + """ + site = server.Site(resource.Resource()) + site.sessionFactory = TaskClockSession + channel = DummyChannel() + channel.site = site + request = server.Request(channel, 1) + request.sitepath = [] + request.site = site + + result = request.getSession() + + sessionRawCookie = '%s=%s; Path=/' % ( + request.getSessionCookieName(), result.uid,) + self.assertEqual(sessionRawCookie, request.cookies[0]) + + # Getting the session again, will return the same object. + self.assertIs(result, request.getSession()) + + + def test_getSessionCookieName(self): + """ + It returns a name based on I{Request.sessionCookieBaseName}. + """ + baseName = b'CUSTOM_NAME' + channel = DummyChannel() + request = server.Request(channel, 1) + request.sitepath = [] + request.site = channel.site + request.sessionCookieBaseName = baseName + + name = request.getSessionCookieName() + + self.assertEqual(name, baseName) + class GzipEncoderTests(unittest.TestCase): diff --git twisted/web/topfiles/6933.feature twisted/web/topfiles/6933.feature new file mode 100644 index 0000000..d3e4060 --- /dev/null +++ twisted/web/topfiles/6933.feature @@ -0,0 +1 @@ +twisted.web.server.Request now allows specifying a custom name for the session cookie. \ No newline at end of file