2018-01-13 23:49:14 +00:00
|
|
|
import flask; from flask import request
|
2016-12-18 13:12:14 +00:00
|
|
|
import functools
|
2020-09-10 01:53:26 +00:00
|
|
|
import werkzeug.datastructures
|
2016-12-18 13:12:14 +00:00
|
|
|
|
2018-03-19 04:23:48 +00:00
|
|
|
from voussoirkit import cacheclass
|
2021-06-05 04:00:22 +00:00
|
|
|
from voussoirkit import flasktools
|
2021-01-05 20:38:04 +00:00
|
|
|
from voussoirkit import passwordy
|
2022-11-07 06:48:59 +00:00
|
|
|
from voussoirkit import timetools
|
2018-03-19 04:23:48 +00:00
|
|
|
|
2018-11-05 03:27:20 +00:00
|
|
|
import etiquette
|
|
|
|
|
2018-01-16 02:41:21 +00:00
|
|
|
SESSION_MAX_AGE = 86400
|
|
|
|
|
2018-01-13 23:49:14 +00:00
|
|
|
def _generate_token(length=32):
|
2021-01-05 20:38:04 +00:00
|
|
|
return passwordy.random_hex(length=length)
|
2016-12-18 13:12:14 +00:00
|
|
|
|
|
|
|
def _normalize_token(token):
|
2021-06-05 04:00:22 +00:00
|
|
|
if isinstance(token, flasktools.REQUEST_TYPES):
|
2018-01-16 02:39:40 +00:00
|
|
|
request = token
|
|
|
|
token = request.cookies.get('etiquette_session', None)
|
|
|
|
if token is None:
|
2018-02-03 10:10:07 +00:00
|
|
|
# During normal usage, this does not occur because give_token is
|
|
|
|
# applied *before* the request handler even sees the request.
|
|
|
|
# Just a precaution.
|
2018-01-16 02:39:40 +00:00
|
|
|
message = 'Cannot normalize token for request with no etiquette_session header.'
|
|
|
|
raise TypeError(message, request)
|
|
|
|
elif isinstance(token, str):
|
|
|
|
pass
|
|
|
|
else:
|
|
|
|
raise TypeError('Unsupported token normalization', type(token))
|
2018-01-14 00:14:01 +00:00
|
|
|
return token
|
2017-05-02 04:23:16 +00:00
|
|
|
|
2016-12-18 13:12:14 +00:00
|
|
|
class SessionManager:
|
2018-03-19 04:23:48 +00:00
|
|
|
def __init__(self, maxlen=None):
|
|
|
|
self.sessions = cacheclass.Cache(maxlen=maxlen)
|
2016-12-18 13:12:14 +00:00
|
|
|
|
|
|
|
def add(self, session):
|
|
|
|
self.sessions[session.token] = session
|
|
|
|
|
2022-11-08 01:51:02 +00:00
|
|
|
def clear(self):
|
|
|
|
self.sessions.clear()
|
|
|
|
|
2018-02-03 10:10:07 +00:00
|
|
|
def get(self, request):
|
|
|
|
token = _normalize_token(request)
|
2018-01-16 02:41:21 +00:00
|
|
|
session = self.sessions[token]
|
2018-02-03 10:10:07 +00:00
|
|
|
invalid = (
|
|
|
|
request.remote_addr != session.ip_address or
|
|
|
|
session.expired()
|
|
|
|
)
|
|
|
|
if invalid:
|
|
|
|
self.remove(token)
|
2018-01-20 05:59:50 +00:00
|
|
|
raise KeyError(token)
|
2018-01-13 23:49:14 +00:00
|
|
|
return session
|
2016-12-18 13:12:14 +00:00
|
|
|
|
|
|
|
def give_token(self, function):
|
|
|
|
'''
|
|
|
|
This decorator ensures that the user has an `etiquette_session` cookie
|
|
|
|
before reaching the request handler.
|
|
|
|
If the user does not have the cookie, they are given one.
|
|
|
|
If they do, its lifespan is reset.
|
|
|
|
'''
|
|
|
|
@functools.wraps(function)
|
|
|
|
def wrapped(*args, **kwargs):
|
|
|
|
# Inject new token so the function doesn't know the difference
|
|
|
|
token = request.cookies.get('etiquette_session', None)
|
2018-01-16 04:04:47 +00:00
|
|
|
if not token or token not in self.sessions:
|
2016-12-18 13:12:14 +00:00
|
|
|
token = _generate_token()
|
2020-09-10 01:53:26 +00:00
|
|
|
# cookies is currently an ImmutableMultiDict, but in order to
|
|
|
|
# trick the wrapped function I'm gonna have to mutate it.
|
|
|
|
# It is important to use a werkzeug MultiDict and not a plain
|
|
|
|
# Python dict, because werkzeug puts cookies into lists like
|
|
|
|
# {name: [value]} and then cookies.get pulls the first item out
|
|
|
|
# of that list. A plain dict wouldn't have this .get behavior.
|
|
|
|
request.cookies = werkzeug.datastructures.MultiDict(request.cookies)
|
2016-12-18 13:12:14 +00:00
|
|
|
request.cookies['etiquette_session'] = token
|
|
|
|
|
2018-01-16 02:41:21 +00:00
|
|
|
try:
|
2018-02-03 10:10:07 +00:00
|
|
|
session = self.get(request)
|
2018-01-16 02:41:21 +00:00
|
|
|
except KeyError:
|
|
|
|
session = Session(request, user=None)
|
|
|
|
self.add(session)
|
|
|
|
else:
|
|
|
|
session.maintain()
|
|
|
|
|
2016-12-18 13:12:14 +00:00
|
|
|
response = function(*args, **kwargs)
|
|
|
|
|
|
|
|
# Send the token back to the client
|
|
|
|
# but only if the endpoint didn't manually set the cookie.
|
2018-01-16 02:56:41 +00:00
|
|
|
function_cookies = response.headers.get_all('Set-Cookie')
|
|
|
|
if not any('etiquette_session=' in cookie for cookie in function_cookies):
|
2018-01-16 02:41:21 +00:00
|
|
|
response.set_cookie(
|
|
|
|
'etiquette_session',
|
|
|
|
value=session.token,
|
|
|
|
max_age=SESSION_MAX_AGE,
|
2020-09-10 02:19:35 +00:00
|
|
|
httponly=True,
|
2018-01-16 02:41:21 +00:00
|
|
|
)
|
2016-12-18 13:12:14 +00:00
|
|
|
|
|
|
|
return response
|
|
|
|
return wrapped
|
|
|
|
|
|
|
|
def remove(self, token):
|
|
|
|
token = _normalize_token(token)
|
2018-02-03 10:10:07 +00:00
|
|
|
try:
|
2018-03-23 07:33:50 +00:00
|
|
|
self.sessions.remove(token)
|
2018-02-03 10:10:07 +00:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2016-12-18 13:12:14 +00:00
|
|
|
class Session:
|
|
|
|
def __init__(self, request, user):
|
|
|
|
self.token = _normalize_token(request)
|
|
|
|
self.user = user
|
|
|
|
self.ip_address = request.remote_addr
|
|
|
|
self.user_agent = request.headers.get('User-Agent', '')
|
2022-11-07 06:48:59 +00:00
|
|
|
self.last_activity = timetools.now()
|
2016-12-18 13:12:14 +00:00
|
|
|
|
2018-01-16 02:41:21 +00:00
|
|
|
def __repr__(self):
|
|
|
|
if self.user:
|
2018-07-19 01:36:36 +00:00
|
|
|
return f'Session {self.token} for user {self.user}'
|
2018-01-16 02:41:21 +00:00
|
|
|
else:
|
2018-07-19 01:36:36 +00:00
|
|
|
return f'Session {self.token} for anonymous'
|
2018-01-16 02:41:21 +00:00
|
|
|
|
2018-01-20 05:59:50 +00:00
|
|
|
def expired(self):
|
2022-11-07 06:48:59 +00:00
|
|
|
now = timetools.now()
|
2018-01-20 05:59:50 +00:00
|
|
|
age = now - self.last_activity
|
2022-07-20 03:03:43 +00:00
|
|
|
return age.seconds > SESSION_MAX_AGE
|
2018-01-20 05:59:50 +00:00
|
|
|
|
2016-12-18 13:12:14 +00:00
|
|
|
def maintain(self):
|
2022-11-07 06:48:59 +00:00
|
|
|
self.last_activity = timetools.now()
|