########################################################################
# $Header: /var/local/cvsroot/4Suite/Ft/Server/Server/Http/Session.py,v 1.16 2003/10/04 03:40:52 mbrown Exp $
"""
HTTP session management.  Inspired by the Java Servlet API at:
http://java.sun.com/products/servlet/2.2/javadoc/javax/servlet/http/package-summary.html
(particularly javax.servlet.http.HttpSession)

Copyright 2002,2003 Fourthought, Inc. (USA).
Detailed license and copyright information: http://4suite.org/COPYRIGHT
Project home, documentation, distributions: http://4suite.org/
"""

import sys, time

def Retrieve(args, headers, sessionType, logger=None):
    """
    Examines the given request headers for a reference to an existing
    session. (e.g., a Cookie header with a session-id value). If the 4SS
    repository contains data for the referenced session, a Session object
    with the appropriate ID and session data is returned. Otherwise, None
    is returned.
    """
    sessionClass = g_sessionFactory.get(sessionType)
    if not sessionClass:
        if logger:
            logger.warning("Session: No session class found for method: %s" % sessionType)
        return None
    session = sessionClass()
    if logger:
        logger.info("Session: Examining request for reference to session ID")
    if not session.getSessionId(args, headers, logger):
        if logger:
            logger.info("Session: No session ID found in request")
        del session
        return None
    if logger:
        logger.info("Session: Session ID %s found in request; Session created" % session.id)
    return session


def Create(sid, method, logger=None):
    """
    Creates and returns a new session with the given ID
    """
    sessionKlass = g_sessionFactory.get(method)
    if not sessionKlass:
        if logger:
            logger.warning("No session class found for method %s" % method)
        return None
    session = sessionKlass()
    session.create(sid)
    return session


class Session:
    """
    Representation of a stateful session for use with HTTP requests and responses,
    as described in RFC 2965.

    Exposes attribute "id", which must be a unique id in string form.

    Currently, the deprecated but widely-used Netscape cookie mechanism is
    employed, plus as much of RFC 2109 as is practical, rather than the more
    featureful, standardized Cookie2 mechanism advocated by RFC 2965. If
    cookies are not able to be used, a framework is in place for using URL
    rewriting; however the implementation is incomplete.

    The userAgentVariables member is a dictionary of additional stuff
    to be held in the session persistence mechanism *independently* of
    the repository  session data.  Use very sparingly, and only for data
    that is not affected by user taint
    
    References:
       http://home.netscape.com/newsref/std/cookie_spec.html  (Netscape cookies)
       http://www.faqs.org/rfcs/rfc2109.html  (standard cookies)
       http://www.faqs.org/rfcs/rfc2965.html  (Cookie2)
       http://www.faqs.org/rfcs/rfc2616.html  (HTTP/1.1)

    TO-DO: Implement the equivalent of various useful methods found in the
    javax.servlet.http.HttpSession portion of the Java Servlet API.

    """
    def __init__(self):
        """A session object is initially created without a session ID."""
        self.id = 0
        self.userAgentVariables = {}
        return

    def create(self, sid):
        """
        This method is called to allow a session to do whatever initialization
        can be done once its ID is known. (The ID is not known when the class
        is instantiated)
        """
        pass

    def getSessionId(self, args, headers):
        """Get the session ID.  Must be overridden"""
        return 0

    def updateHeaders(self, repo, headers, logger):
        """
        This method is called to allow a session to update the persistence
        mechanism in the process of the HTTP response (cookie, URL re-write,
        etc.)
        """
        pass

    def updateBody(self, repo, body):
        """
        This method is called to allow a session to update the response body
        before it is written out.
        """
        return body


class CookieSession(Session):
    """
    An implmentation of the Session object that uses HTTP cookies, as
    described in http://home.netscape.com/newsref/std/cookie_spec.html
    (Netscape cookies) and http://www.faqs.org/rfcs/rfc2109.html
    (standard cookies)
    """

    # FIXME: Add RFC 2965 (Cookie2 and Set-Cookie2) support eventually
    SESSION_KEY = 'sessionId'
    COOKIE_HEADER = "Set-Cookie"

    def __init__(self):
        Session.__init__(self)

    def getSessionId(self, args, headers, logger):
        """Attempts to get the session id from the Cookie Header."""
        request_cookie_headers = headers.get("Cookie")
        if request_cookie_headers is None:
            logger.debug("CookieSession: No Cookies found in request")
            return None
        for request_cookie_header in request_cookie_headers:
            logger.debug("CookieSession: Cookie found in request: " + repr(request_cookie_header))
            #Use SimpleCookie, not Cookie or SmartCookie.  See oodles of security warnings, e.g.
            #http://www.python.org/doc/2.3/lib/module-Cookie.html
            self._cookie = Cookie.SimpleCookie()
            self._cookie.load(request_cookie_header)
            # the cookie might not have a session-id key at all
            try:
                id = self._cookie[self.SESSION_KEY].value
                logger.debug("CookieSession: Cookie contains session ID '%s'" % id)
                for key in self._cookie.keys():
                    if key != self.SESSION_KEY:
                        self.userAgentVariables[key] = self._cookie[key].value
                break
            except KeyError:
                id = None
                logger.debug("CookieSession: Cookie contains no session ID")
        self.id = id
        return id

    def create(self, sid):
        """Does whatever initialization can be done once the session ID is known."""
        self._cookie = Cookie.SimpleCookie()
        self.id = sid
        return

    def updateHeaders(self, repo, headers, logger):
        """
        Updates the response headers to reflect the current session info.
        repo - repository instance for getting expiration info for the current session
        headers - dictionary to be updated with cookie headers
        logger - destination for logged remarks
        """
        logger.info("CookieSession: Updating response headers to reflect the current session info")
        self._cookie[self.SESSION_KEY] = self.id
        # A morsel is a cookie object that acts like a dictionary.
        morsel = self._cookie[self.SESSION_KEY]
        # RFC 2109 sec. 4.2.2
        morsel['version'] = 1
        # RFC 2109 sec. 4.2.2
        # Pacify the paranoid people who inspect their cookies
        morsel['comment'] = 'A random, unique, meaningless identifier for an HTTP session'
        # The 'max-age' field is the number of seconds in the future the
        # cookie is supposed to expire. It has a minimum value of 0, meaning
        # expire immediately.
        #
        # The 'expires' field gets written out as a date string with the
        # format "Tue, 01-Jan-1980 00:00:00 GMT". Browsers use this even if
        # the 'max-age' field exists, and they compare it to their local
        # system time. Argh! That's why we use 01-Jan-1980 when
        # expiring the cookie -- it will (hopefully) ensure that the date
        # is in the distant past, so the browsers will expire the cookie
        # immediately. We previously used negative sys.maxint, but this was
        # problematic on some platforms.
        #
        DATE_IN_DISTANT_PAST = 'Tue, 01-Jan-1980 00:00:00 GMT'
        maxage = 0
        expires = DATE_IN_DISTANT_PAST
        if repo and repo.hasSession():
            maxage = int(repo.getSessionExpiration() - time.time() + 0.5)
            if maxage < 0:
                maxage = 0
        if maxage > 0:
            expires = maxage
        # RFC 2109 sec. 4.2.2
        morsel['max-age'] = maxage
        # Netscape cookie spec
        morsel['expires'] = expires
        # RFC 2109 sec. 4.2.3
        headers['Cache-Control'] = 'private'
        headers['Cache-Control'] = 'no-cache="%s"' % self.COOKIE_HEADER
        # RFC 2109 sec. 4.2.3
        # might as well say the request expires on the same date as the cookie
        headers['Expires'] = DATE_IN_DISTANT_PAST
        headers[self.COOKIE_HEADER] = morsel.OutputString()
        for k, v in self.userAgentVariables.items():
            self._cookie[k] = v
            headers[self.COOKIE_HEADER] = self._cookie[k].OutputString()
        #logger.debug("CookieSession: Updated response headers: %s"%(''.join(["\n    %s: %s" % (key, headers[key]) for key in headers.keys()])))

        logger.debug("CookieSession: Updated response headers.")
        return


g_sessionFactory = {}
try:
    import Cookie
    g_sessionFactory["Cookie"] = CookieSession
except ImportError:
    pass
