# Author: Trevor Perrin
# See the LICENSE file for legal information regarding use of this file.

"""Class for caching TLS sessions."""

import thread
import time

class SessionCache:
    """This class is used by the server to cache TLS sessions.

    Caching sessions allows the client to use TLS session resumption
    and avoid the expense of a full handshake.  To use this class,
    simply pass a SessionCache instance into the server handshake
    function.

    This class is thread-safe.
    """

    #References to these instances
    #are also held by the caller, who may change the 'resumable'
    #flag, so the SessionCache must return the same instances
    #it was passed in.

    def __init__(self, maxEntries=10000, maxAge=14400):
        """Create a new SessionCache.

        @type maxEntries: int
        @param maxEntries: The maximum size of the cache.  When this
        limit is reached, the oldest sessions will be deleted as
        necessary to make room for new ones.  The default is 10000.

        @type maxAge: int
        @param maxAge:  The number of seconds before a session expires
        from the cache.  The default is 14400 (i.e. 4 hours)."""

        self.lock = thread.allocate_lock()

        # Maps sessionIDs to sessions
        self.entriesDict = {}

        #Circular list of (sessionID, timestamp) pairs
        self.entriesList = [(None,None)] * maxEntries

        self.firstIndex = 0
        self.lastIndex = 0
        self.maxAge = maxAge

    def __getitem__(self, sessionID):
        self.lock.acquire()
        try:
            self._purge() #Delete old items, so we're assured of a new one
            session = self.entriesDict[sessionID]

            #When we add sessions they're resumable, but it's possible
            #for the session to be invalidated later on (if a fatal alert
            #is returned), so we have to check for resumability before
            #returning the session.

            if session.valid():
                return session
            else:
                raise KeyError()
        finally:
            self.lock.release()


    def __setitem__(self, sessionID, session):
        self.lock.acquire()
        try:
            #Add the new element
            self.entriesDict[sessionID] = session
            self.entriesList[self.lastIndex] = (sessionID, time.time())
            self.lastIndex = (self.lastIndex+1) % len(self.entriesList)

            #If the cache is full, we delete the oldest element to make an
            #empty space
            if self.lastIndex == self.firstIndex:
                del(self.entriesDict[self.entriesList[self.firstIndex][0]])
                self.firstIndex = (self.firstIndex+1) % len(self.entriesList)
        finally:
            self.lock.release()

    #Delete expired items
    def _purge(self):
        currentTime = time.time()

        #Search through the circular list, deleting expired elements until
        #we reach a non-expired element.  Since elements in list are
        #ordered in time, we can break once we reach the first non-expired
        #element
        index = self.firstIndex
        while index != self.lastIndex:
            if currentTime - self.entriesList[index][1] > self.maxAge:
                del(self.entriesDict[self.entriesList[index][0]])
                index = (index+1) % len(self.entriesList)
            else:
                break
        self.firstIndex = index

def _test():
    import doctest, SessionCache
    return doctest.testmod(SessionCache)

if __name__ == "__main__":
    _test()
