1a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch# Authors:
2a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch#   Trevor Perrin
3a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch#   Martin von Loewis - python 3 port
4a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch#
5a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch# See the LICENSE file for legal information regarding use of this file.
6a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch
75821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)"""Class for caching TLS sessions."""
85821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
9a02191e04bc25c4935f804f2c080ae28663d096dBen Murdochimport threading
105821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)import time
115821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
12a02191e04bc25c4935f804f2c080ae28663d096dBen Murdochclass SessionCache(object):
135821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """This class is used by the server to cache TLS sessions.
145821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
155821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    Caching sessions allows the client to use TLS session resumption
165821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    and avoid the expense of a full handshake.  To use this class,
175821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    simply pass a SessionCache instance into the server handshake
185821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    function.
195821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
205821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    This class is thread-safe.
215821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    """
225821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
235821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    #References to these instances
245821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    #are also held by the caller, who may change the 'resumable'
255821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    #flag, so the SessionCache must return the same instances
265821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    #it was passed in.
275821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
285821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    def __init__(self, maxEntries=10000, maxAge=14400):
295821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        """Create a new SessionCache.
305821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
315821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        @type maxEntries: int
325821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        @param maxEntries: The maximum size of the cache.  When this
335821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        limit is reached, the oldest sessions will be deleted as
345821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        necessary to make room for new ones.  The default is 10000.
355821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
365821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        @type maxAge: int
375821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        @param maxAge:  The number of seconds before a session expires
385821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        from the cache.  The default is 14400 (i.e. 4 hours)."""
395821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
40a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch        self.lock = threading.Lock()
415821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
425821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        # Maps sessionIDs to sessions
435821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.entriesDict = {}
445821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
455821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        #Circular list of (sessionID, timestamp) pairs
465821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.entriesList = [(None,None)] * maxEntries
475821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
485821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.firstIndex = 0
495821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.lastIndex = 0
505821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.maxAge = maxAge
515821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
525821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    def __getitem__(self, sessionID):
535821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.lock.acquire()
545821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        try:
555821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            self._purge() #Delete old items, so we're assured of a new one
56a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch            session = self.entriesDict[bytes(sessionID)]
575821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
585821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #When we add sessions they're resumable, but it's possible
595821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #for the session to be invalidated later on (if a fatal alert
605821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #is returned), so we have to check for resumability before
615821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #returning the session.
625821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
635821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if session.valid():
645821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                return session
655821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            else:
665821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                raise KeyError()
675821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        finally:
685821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            self.lock.release()
695821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
705821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
715821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    def __setitem__(self, sessionID, session):
725821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.lock.acquire()
735821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        try:
745821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #Add the new element
75a02191e04bc25c4935f804f2c080ae28663d096dBen Murdoch            self.entriesDict[bytes(sessionID)] = session
765821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            self.entriesList[self.lastIndex] = (sessionID, time.time())
775821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            self.lastIndex = (self.lastIndex+1) % len(self.entriesList)
785821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
795821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #If the cache is full, we delete the oldest element to make an
805821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            #empty space
815821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if self.lastIndex == self.firstIndex:
825821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                del(self.entriesDict[self.entriesList[self.firstIndex][0]])
835821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                self.firstIndex = (self.firstIndex+1) % len(self.entriesList)
845821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        finally:
855821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            self.lock.release()
865821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
875821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    #Delete expired items
885821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    def _purge(self):
895821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        currentTime = time.time()
905821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
915821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        #Search through the circular list, deleting expired elements until
925821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        #we reach a non-expired element.  Since elements in list are
935821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        #ordered in time, we can break once we reach the first non-expired
945821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        #element
955821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        index = self.firstIndex
965821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        while index != self.lastIndex:
975821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            if currentTime - self.entriesList[index][1] > self.maxAge:
985821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                del(self.entriesDict[self.entriesList[index][0]])
995821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                index = (index+1) % len(self.entriesList)
1005821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)            else:
1015821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)                break
1025821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)        self.firstIndex = index
1035821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1045821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)def _test():
1055821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    import doctest, SessionCache
1065821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    return doctest.testmod(SessionCache)
1075821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)
1085821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)if __name__ == "__main__":
1095821806d5e7f356e8fa4b058a389a808ea183019Torne (Richard Coles)    _test()
110