129 lines
3.9 KiB
Python
129 lines
3.9 KiB
Python
from http.cookies import SimpleCookie
|
|
from collections import namedtuple
|
|
from collections.abc import Mapping
|
|
from itertools import product, chain
|
|
import time
|
|
import random
|
|
|
|
from . import httpd
|
|
from . import Context, context_activate
|
|
|
|
|
|
# Name generation for contexts and sessions
|
|
def name_parts():
|
|
"""
|
|
This generator will create an endless list of steadily increasing
|
|
name part lists.
|
|
"""
|
|
# name parts
|
|
letters = ['alpha', 'beta', 'gamma', 'delta', 'epsilon', 'zeta', 'eta',
|
|
'theta', 'iota', 'kappa', 'lambda', 'mu', 'nu', 'xi', 'omicron',
|
|
'pi', 'rho', 'sigma', 'tau', 'upsilon', 'phi', 'chi', 'psi',
|
|
'omega']
|
|
colours = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']
|
|
|
|
# randomize order
|
|
random.shuffle(letters)
|
|
random.shuffle(colours)
|
|
|
|
# yield initial sequence (letter-colour)
|
|
parts = [letters, colours]
|
|
yield parts
|
|
|
|
# forever generate longer sequences by appending the letter list
|
|
# over and over. Note that this is the *same* letter list, so it will have
|
|
# the exact order.
|
|
while True:
|
|
random.shuffle(letters)
|
|
random.shuffle(colours)
|
|
parts.append(letters)
|
|
yield parts
|
|
|
|
# construct an iterator that will endlessly generate names:
|
|
# 1) for each parts list p in name_parts() we take the cartesian product
|
|
# 2) the product iterators are generated by the for...in generator
|
|
# 3) we chain these iterators so that when the first is exhausted, we can
|
|
# continue with the second, etc.
|
|
# 4) we map the function '-'.join over the list of parts from the chain
|
|
names = map('-'.join, chain.from_iterable((product(*p) for p in name_parts())))
|
|
|
|
|
|
class SessionCookie(httpd.Filter):
|
|
"""
|
|
The actual HTTP filter that will apply the cookie handling logic to each
|
|
request. This filter defers to the SessionManager with respect to the
|
|
cookie name to use and the activation of sessions.
|
|
"""
|
|
def bind(self, manager):
|
|
"""Post constructor configuration of filter."""
|
|
self.manager = manager
|
|
|
|
def handle(self):
|
|
"""
|
|
Determine if a cookie needs to be set and let the session manager
|
|
handle activation.
|
|
"""
|
|
cookies = self.request.cookies
|
|
morsel = cookies.get(self.manager.cookie)
|
|
|
|
if not morsel:
|
|
# Determine new cookie
|
|
value = self.manager.generate_name()
|
|
|
|
# Set new cookie
|
|
cookies[self.manager.cookie] = value
|
|
cookies[self.manager.cookie]['path'] = '/'
|
|
|
|
# Send the new cookie as header
|
|
self.request.send_header('Set-Cookie', cookies[self.manager.cookie].OutputString())
|
|
else:
|
|
value = morsel.value
|
|
|
|
self.manager.activate(value)
|
|
|
|
|
|
class Session:
|
|
"""
|
|
The Session bookkeeping data.
|
|
"""
|
|
def __init__(self, context, seen):
|
|
self.context = context
|
|
self.seen = seen
|
|
|
|
def activate(self):
|
|
"""Activate the session. Updates last seen time."""
|
|
self.seen = time.time()
|
|
context_activate(self.context)
|
|
|
|
|
|
class SessionManager:
|
|
"""
|
|
The SessionManager class. This class is callable so it can be used in place
|
|
of a constructor in the configuration.
|
|
"""
|
|
def __init__(self, cookie_name):
|
|
self.sessions = {}
|
|
self.cookie = cookie_name
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
handler = SessionCookie(*args, **kwargs)
|
|
handler.bind(self)
|
|
return handler
|
|
|
|
def generate_name(self):
|
|
result = next(names)
|
|
while result in self.sessions:
|
|
result = next(names)
|
|
return result
|
|
|
|
def _new_session(self, name):
|
|
result = Session(Context(name=name, init_data={'name': name}), time.time())
|
|
result.context.start()
|
|
return result
|
|
|
|
def activate(self, name):
|
|
if name not in self.sessions:
|
|
self.sessions[name] = self._new_session(name)
|
|
self.sessions[name].activate()
|
|
|