@ -28,6 +28,7 @@ import flask_babel
import ipaddress
import ipaddress
import redis
import redis
from datetime import datetime , timedelta
from flask . sessions import SessionMixin , SessionInterface
from flask . sessions import SessionMixin , SessionInterface
from itsdangerous . encoding import want_bytes
from itsdangerous . encoding import want_bytes
from werkzeug . datastructures import CallbackDict
from werkzeug . datastructures import CallbackDict
@ -78,9 +79,9 @@ limiter = limiter.LimitWraperFactory()
def extract_network_from_ip ( ip ) :
def extract_network_from_ip ( ip ) :
n = ipaddress . ip_network ( ip )
n = ipaddress . ip_network ( ip )
if n . version == 4 :
if n . version == 4 :
return str ( n . supernet ( prefixlen_diff = ( 32 - int ( app . config [ " AUTH_RATELIMIT_IP_V4_MASK " ] ) ) ) . network_address )
return str ( n . supernet ( prefixlen_diff = ( 32 - app . config [ " AUTH_RATELIMIT_IP_V4_MASK " ] ) ) . network_address )
else :
else :
return str ( n . supernet ( prefixlen_diff = ( 128 - int ( app . config [ " AUTH_RATELIMIT_IP_V6_MASK " ] ) ) ) . network_address )
return str ( n . supernet ( prefixlen_diff = ( 128 - app . config [ " AUTH_RATELIMIT_IP_V6_MASK " ] ) ) . network_address )
def is_exempt_from_ratelimits ( ip ) :
def is_exempt_from_ratelimits ( ip ) :
ip = ipaddress . ip_address ( ip )
ip = ipaddress . ip_address ( ip )
@ -92,6 +93,8 @@ babel = flask_babel.Babel()
@babel.localeselector
@babel.localeselector
def get_locale ( ) :
def get_locale ( ) :
""" selects locale for translation """
""" selects locale for translation """
if not app . config [ ' SESSION_COOKIE_NAME ' ] in flask . request . cookies :
return flask . request . accept_languages . best_match ( app . config . translations . keys ( ) )
language = flask . session . get ( ' language ' )
language = flask . session . get ( ' language ' )
if not language in app . config . translations :
if not language in app . config . translations :
language = flask . request . accept_languages . best_match ( app . config . translations . keys ( ) )
language = flask . request . accept_languages . best_match ( app . config . translations . keys ( ) )
@ -118,13 +121,10 @@ proxy = PrefixMiddleware()
# Data migrate
# Data migrate
migrate = flask_migrate . Migrate ( )
migrate = flask_migrate . Migrate ( )
# session store (inspired by https://github.com/mbr/flask-kvsession)
# session store (inspired by https://github.com/mbr/flask-kvsession)
class RedisStore :
class RedisStore :
""" Stores session data in a redis db. """
""" Stores session data in a redis db. """
has_ttl = True
def __init__ ( self , redisstore ) :
def __init__ ( self , redisstore ) :
self . redis = redisstore
self . redis = redisstore
@ -155,8 +155,6 @@ class RedisStore:
class DictStore :
class DictStore :
""" Stores session data in a python dict. """
""" Stores session data in a python dict. """
has_ttl = False
def __init__ ( self ) :
def __init__ ( self ) :
self . dict = { }
self . dict = { }
@ -164,7 +162,7 @@ class DictStore:
""" load item from store. """
""" load item from store. """
return self . dict [ key ]
return self . dict [ key ]
def put ( self , key , value , ttl _secs = None ) :
def put ( self , key , value , ttl = None ) :
""" save item to store. """
""" save item to store. """
self . dict [ key ] = value
self . dict [ key ] = value
@ -233,7 +231,8 @@ class MailuSession(CallbackDict, SessionMixin):
def destroy ( self ) :
def destroy ( self ) :
""" destroy session for security reasons. """
""" destroy session for security reasons. """
if ' webmail_token ' in self :
self . app . session_store . delete ( self [ ' webmail_token ' ] )
self . delete ( )
self . delete ( )
self . _uid = None
self . _uid = None
@ -247,12 +246,8 @@ class MailuSession(CallbackDict, SessionMixin):
def regenerate ( self ) :
def regenerate ( self ) :
""" generate new id for session to avoid `session fixation`. """
""" generate new id for session to avoid `session fixation`. """
self . delete ( )
self . delete ( )
self . _sid = None
self . _sid = None
self . _created = self . app . session_config . gen_created ( )
self . modified = True
self . modified = True
def delete ( self ) :
def delete ( self ) :
@ -263,9 +258,7 @@ class MailuSession(CallbackDict, SessionMixin):
def save ( self ) :
def save ( self ) :
""" Save session to store. """
""" Save session to store. """
set_cookie = False
set_cookie = False
# set uid from dict data
# set uid from dict data
if self . _uid is None :
if self . _uid is None :
self . _uid = self . app . session_config . gen_uid ( self . get ( ' _user_id ' , ' ' ) )
self . _uid = self . app . session_config . gen_uid ( self . get ( ' _user_id ' , ' ' ) )
@ -274,6 +267,11 @@ class MailuSession(CallbackDict, SessionMixin):
if self . _sid is None :
if self . _sid is None :
self . _sid = self . app . session_config . gen_sid ( )
self . _sid = self . app . session_config . gen_sid ( )
set_cookie = True
set_cookie = True
if ' webmail_token ' in self :
app . session_store . put ( self [ ' webmail_token ' ] ,
self . sid ,
app . config [ ' PERMANENT_SESSION_LIFETIME ' ] ,
)
# get new session key
# get new session key
key = self . sid
key = self . sid
@ -282,14 +280,11 @@ class MailuSession(CallbackDict, SessionMixin):
if key != self . _key :
if key != self . _key :
self . delete ( )
self . delete ( )
# remember time to refresh
self [ ' _refresh ' ] = int ( time . time ( ) ) + self . app . permanent_session_lifetime . total_seconds ( ) / 2
# save session
# save session
self . app . session_store . put (
self . app . session_store . put (
key ,
key ,
pickle . dumps ( dict ( self ) ) ,
pickle . dumps ( dict ( self ) ) ,
self . app . permanent_session_lifetime . total_seconds ( )
app . config [ ' SESSION_TIMEOUT ' ] ,
)
)
self . _key = key
self . _key = key
@ -299,11 +294,6 @@ class MailuSession(CallbackDict, SessionMixin):
return set_cookie
return set_cookie
def needs_refresh ( self ) :
""" Checks if server side session needs to be refreshed. """
return int ( time . time ( ) ) > self . get ( ' _refresh ' , 0 )
class MailuSessionConfig :
class MailuSessionConfig :
""" Stores sessions crypto config """
""" Stores sessions crypto config """
@ -348,7 +338,7 @@ class MailuSessionConfig:
""" Generate base64 representation of creation time. """
""" Generate base64 representation of creation time. """
return self . _encode ( int ( now or time . time ( ) ) . to_bytes ( 8 , byteorder = ' big ' ) . lstrip ( b ' \0 ' ) )
return self . _encode ( int ( now or time . time ( ) ) . to_bytes ( 8 , byteorder = ' big ' ) . lstrip ( b ' \0 ' ) )
def parse_key ( self , key , app = None , validate= False , now= None ) :
def parse_key ( self , key , app = None , now= None ) :
""" Split key into sid, uid and creation time. """
""" Split key into sid, uid and creation time. """
if not ( isinstance ( key , bytes ) and self . _key_min < = len ( key ) < = self . _key_max ) :
if not ( isinstance ( key , bytes ) and self . _key_min < = len ( key ) < = self . _key_max ) :
@ -363,12 +353,11 @@ class MailuSessionConfig:
if created is None or self . _decode ( uid ) is None or self . _decode ( sid ) is None :
if created is None or self . _decode ( uid ) is None or self . _decode ( sid ) is None :
return None
return None
# validate creation time when requested or store does not support ttl
# validate creation time
if validate or not app . session_store . has_ttl :
if now is None :
if now is None :
now = int ( time . time ( ) )
now = int ( time . time ( ) )
created = int . from_bytes ( created , byteorder = ' big ' )
created = int . from_bytes ( created , byteorder = ' big ' )
if not created < now < created + app . permanent_session_lifetime. total_seconds ( ) :
if not created < = now < = created + app . config[ ' PERMANENT_SESSION_LIFETIME ' ] :
return None
return None
return ( uid , sid , crt )
return ( uid , sid , crt )
@ -408,23 +397,12 @@ class MailuSessionInterface(SessionInterface):
if session . accessed :
if session . accessed :
response . vary . add ( ' Cookie ' )
response . vary . add ( ' Cookie ' )
set_cookie = session . permanent and app . config [ ' SESSION_REFRESH_EACH_REQUEST ' ]
# save session and update cookie if necessary
need_refresh = session . needs_refresh ( )
if session . save ( ) :
# save modified session or refresh unmodified session
if session . modified or need_refresh :
set_cookie | = session . save ( )
# set cookie on refreshed permanent sessions
if need_refresh and session . permanent :
set_cookie = True
# set or update cookie if necessary
if set_cookie :
response . set_cookie (
response . set_cookie (
app . session_cookie_name ,
app . session_cookie_name ,
session . sid ,
session . sid ,
expires = self . get_expiration_time ( app , session ) ,
expires = datetime . now ( ) + timedelta ( seconds = app . config [ ' PERMANENT_SESSION_LIFETIME ' ] ) ,
httponly = self . get_cookie_httponly ( app ) ,
httponly = self . get_cookie_httponly ( app ) ,
domain = self . get_cookie_domain ( app ) ,
domain = self . get_cookie_domain ( app ) ,
path = self . get_cookie_path ( app ) ,
path = self . get_cookie_path ( app ) ,
@ -444,7 +422,7 @@ class MailuSessionExtension:
count = 0
count = 0
for key in app . session_store . list ( ) :
for key in app . session_store . list ( ) :
if not app . session_config . parse_key ( key , app , validate= True , now= now ) :
if not app . session_config . parse_key ( key , app , now= now ) :
app . session_store . delete ( key )
app . session_store . delete ( key )
count + = 1
count + = 1
@ -498,3 +476,24 @@ class MailuSessionExtension:
cleaned = Value ( ' i ' , False )
cleaned = Value ( ' i ' , False )
session = MailuSessionExtension ( )
session = MailuSessionExtension ( )
# this is used by the webmail to authenticate IMAP/SMTP
def verify_temp_token ( email , token ) :
try :
if token . startswith ( ' token- ' ) :
sessid = app . session_store . get ( token )
if sessid :
session = MailuSession ( sessid , app )
if session . get ( ' _user_id ' , ' ' ) == email :
return True
except :
pass
def gen_temp_token ( email , session ) :
token = session . get ( ' webmail_token ' , ' token- ' + secrets . token_urlsafe ( ) )
session [ ' webmail_token ' ] = token
app . session_store . put ( token ,
session . sid ,
app . config [ ' PERMANENT_SESSION_LIFETIME ' ] ,
)
return token