mirror of
https://github.com/Mailu/Mailu.git
synced 2025-05-31 23:10:01 +02:00
Merge #2094
2094: Sessions tweaks r=mergify[bot] a=nextgens ## What type of PR? bug-fix ## What does this PR do? - Make all sessions permanent, introduce SESSION_TIMEOUT and PERMANENT_SESSION_LIFETIME. - Prevent the creation of a session before there is a login attempt - Ensure that webmail tokens are in sync with sessions ### Related issue(s) - close #2080 ## Prerequisites Before we can consider review and merge, please make sure the following list is done and checked. If an entry in not applicable, you can check it or remove it from the list. - [x] In case of feature or enhancement: documentation updated accordingly - [x] Unless it's docs or a minor change: add [changelog](https://mailu.io/master/contributors/workflow.html#changelog) entry file. Co-authored-by: Florent Daigniere <nextgens@freenetproject.org> Co-authored-by: Dimitri Huisman <diman@huisman.xyz>
This commit is contained in:
commit
18865bf03b
@ -70,7 +70,8 @@ DEFAULT_CONFIG = {
|
|||||||
# Advanced settings
|
# Advanced settings
|
||||||
'LOG_LEVEL': 'WARNING',
|
'LOG_LEVEL': 'WARNING',
|
||||||
'SESSION_KEY_BITS': 128,
|
'SESSION_KEY_BITS': 128,
|
||||||
'SESSION_LIFETIME': 24,
|
'SESSION_TIMEOUT': 3600,
|
||||||
|
'PERMANENT_SESSION_LIFETIME': 30*24*3600,
|
||||||
'SESSION_COOKIE_SECURE': True,
|
'SESSION_COOKIE_SECURE': True,
|
||||||
'CREDENTIAL_ROUNDS': 12,
|
'CREDENTIAL_ROUNDS': 12,
|
||||||
'TZ': 'Etc/UTC',
|
'TZ': 'Etc/UTC',
|
||||||
@ -152,7 +153,11 @@ class ConfigManager:
|
|||||||
self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
|
self.config['SESSION_STORAGE_URL'] = f'redis://{self.config["REDIS_ADDRESS"]}/3'
|
||||||
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
self.config['SESSION_COOKIE_SAMESITE'] = 'Strict'
|
||||||
self.config['SESSION_COOKIE_HTTPONLY'] = True
|
self.config['SESSION_COOKIE_HTTPONLY'] = True
|
||||||
self.config['PERMANENT_SESSION_LIFETIME'] = timedelta(hours=int(self.config['SESSION_LIFETIME']))
|
self.config['SESSION_PERMANENT'] = True
|
||||||
|
self.config['SESSION_TIMEOUT'] = int(self.config['SESSION_TIMEOUT'])
|
||||||
|
self.config['PERMANENT_SESSION_LIFETIME'] = int(self.config['PERMANENT_SESSION_LIFETIME'])
|
||||||
|
self.config['AUTH_RATELIMIT_IP_V4_MASK'] = int(self.config['AUTH_RATELIMIT_IP_V4_MASK'])
|
||||||
|
self.config['AUTH_RATELIMIT_IP_V6_MASK'] = int(self.config['AUTH_RATELIMIT_IP_V6_MASK'])
|
||||||
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
|
hostnames = [host.strip() for host in self.config['HOSTNAMES'].split(',')]
|
||||||
self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr)
|
self.config['AUTH_RATELIMIT_EXEMPTION'] = set(ipaddress.ip_network(cidr, False) for cidr in (cidr.strip() for cidr in self.config['AUTH_RATELIMIT_EXEMPTION'].split(',')) if cidr)
|
||||||
self.config['MESSAGE_RATELIMIT_EXEMPTION'] = set([s for s in self.config['MESSAGE_RATELIMIT_EXEMPTION'].lower().replace(' ', '').split(',') if s])
|
self.config['MESSAGE_RATELIMIT_EXEMPTION'] = set([s for s in self.config['MESSAGE_RATELIMIT_EXEMPTION'].lower().replace(' ', '').split(',') if s])
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
from mailu import models
|
from mailu import models, utils
|
||||||
from flask import current_app as app
|
from flask import current_app as app
|
||||||
|
|
||||||
import re
|
import re
|
||||||
@ -32,8 +32,8 @@ def check_credentials(user, password, ip, protocol=None, auth_port=None):
|
|||||||
return False
|
return False
|
||||||
is_ok = False
|
is_ok = False
|
||||||
# webmails
|
# webmails
|
||||||
if len(password) == 64 and auth_port in ['10143', '10025']:
|
if auth_port in ['10143', '10025'] and password.startswith('token-'):
|
||||||
if user.verify_temp_token(password):
|
if utils.verify_temp_token(user.get_id(), password):
|
||||||
is_ok = True
|
is_ok = True
|
||||||
# All tokens are 32 characters hex lowercase
|
# All tokens are 32 characters hex lowercase
|
||||||
if not is_ok and len(password) == 32:
|
if not is_ok and len(password) == 32:
|
||||||
|
@ -68,8 +68,9 @@ def user_authentication():
|
|||||||
if (not flask_login.current_user.is_anonymous
|
if (not flask_login.current_user.is_anonymous
|
||||||
and flask_login.current_user.enabled):
|
and flask_login.current_user.enabled):
|
||||||
response = flask.Response()
|
response = flask.Response()
|
||||||
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, flask_login.current_user.get_id(), "")
|
email = flask_login.current_user.get_id()
|
||||||
response.headers["X-User-Token"] = models.User.get_temp_token(flask_login.current_user.get_id())
|
response.headers["X-User"] = models.IdnaEmail.process_bind_param(flask_login, email, "")
|
||||||
|
response.headers["X-User-Token"] = utils.gen_temp_token(email, flask.session)
|
||||||
return response
|
return response
|
||||||
return flask.abort(403)
|
return flask.abort(403)
|
||||||
|
|
||||||
|
@ -16,7 +16,6 @@ import passlib.hash
|
|||||||
import passlib.registry
|
import passlib.registry
|
||||||
import time
|
import time
|
||||||
import os
|
import os
|
||||||
import hmac
|
|
||||||
import smtplib
|
import smtplib
|
||||||
import idna
|
import idna
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
@ -645,15 +644,6 @@ in clear-text regardless of the presence of the cache.
|
|||||||
user = cls.query.get(email)
|
user = cls.query.get(email)
|
||||||
return user if (user and user.enabled and user.check_password(password)) else None
|
return user if (user and user.enabled and user.check_password(password)) else None
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_temp_token(cls, email):
|
|
||||||
user = cls.query.get(email)
|
|
||||||
return hmac.new(app.temp_token_key, bytearray("{}|{}".format(time.strftime('%Y%m%d'), email), 'utf-8'), 'sha256').hexdigest() if (user and user.enabled) else None
|
|
||||||
|
|
||||||
def verify_temp_token(self, token):
|
|
||||||
return hmac.compare_digest(self.get_temp_token(self.email), token)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Alias(Base, Email):
|
class Alias(Base, Email):
|
||||||
""" An alias is an email address that redirects to some destination.
|
""" An alias is an email address that redirects to some destination.
|
||||||
|
@ -3,5 +3,6 @@ import flask
|
|||||||
|
|
||||||
@sso.route('/language/<language>', methods=['POST'])
|
@sso.route('/language/<language>', methods=['POST'])
|
||||||
def set_language(language=None):
|
def set_language(language=None):
|
||||||
flask.session['language'] = language
|
if language:
|
||||||
|
flask.session['language'] = language
|
||||||
return flask.Response(status=200)
|
return flask.Response(status=200)
|
||||||
|
@ -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,13 +353,12 @@ 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.config['PERMANENT_SESSION_LIFETIME']:
|
||||||
if not created < now < created + app.permanent_session_lifetime.total_seconds():
|
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
|
||||||
|
@ -181,7 +181,7 @@ The ``CREDENTIAL_ROUNDS`` (default: 12) setting is the number of rounds used by
|
|||||||
|
|
||||||
The ``SESSION_COOKIE_SECURE`` (default: True) setting controls the secure flag on the cookies of the administrative interface. It should only be turned off if you intend to access it over plain HTTP.
|
The ``SESSION_COOKIE_SECURE`` (default: True) setting controls the secure flag on the cookies of the administrative interface. It should only be turned off if you intend to access it over plain HTTP.
|
||||||
|
|
||||||
``SESSION_LIFETIME`` (default: 24) is the length in hours a session is valid for on the administrative interface.
|
``SESSION_TIMEOUT`` (default: 3600) is the maximum amount of time in seconds between requests before a session is invalidated. ``PERMANENT_SESSION_LIFETIME`` (default: 108000) is the maximum amount of time in seconds a session can be kept alive for if it hasn't timed-out.
|
||||||
|
|
||||||
The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold.
|
The ``LOG_LEVEL`` setting is used by the python start-up scripts as a logging threshold.
|
||||||
Log messages equal or higher than this priority will be printed.
|
Log messages equal or higher than this priority will be printed.
|
||||||
|
1
towncrier/newsfragments/2080.bugfix
Normal file
1
towncrier/newsfragments/2080.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Ensure that webmail tokens expire in sync with sessions
|
1
towncrier/newsfragments/2094.bugfix
Normal file
1
towncrier/newsfragments/2094.bugfix
Normal file
@ -0,0 +1 @@
|
|||||||
|
Introduce SESSION_TIMEOUT (1h) and PERMANENT_SESSION_LIFETIME (30d)
|
@ -13,6 +13,7 @@ $config['log_driver'] = 'stdout';
|
|||||||
$config['zipdownload_selection'] = true;
|
$config['zipdownload_selection'] = true;
|
||||||
$config['enable_spellcheck'] = true;
|
$config['enable_spellcheck'] = true;
|
||||||
$config['spellcheck_engine'] = 'pspell';
|
$config['spellcheck_engine'] = 'pspell';
|
||||||
|
$config['session_lifetime'] = {{ SESSION_TIMEOUT_MINUTES | int }};
|
||||||
|
|
||||||
// Mail servers
|
// Mail servers
|
||||||
$config['default_host'] = '{{ FRONT_ADDRESS or "front" }}';
|
$config['default_host'] = '{{ FRONT_ADDRESS or "front" }}';
|
||||||
|
@ -62,6 +62,10 @@ context["PLUGINS"] = ",".join(f"'{p}'" for p in plugins)
|
|||||||
# add overrides
|
# add overrides
|
||||||
context["INCLUDES"] = sorted(inc for inc in os.listdir("/overrides") if inc.endswith(".inc")) if os.path.isdir("/overrides") else []
|
context["INCLUDES"] = sorted(inc for inc in os.listdir("/overrides") if inc.endswith(".inc")) if os.path.isdir("/overrides") else []
|
||||||
|
|
||||||
|
# calculate variables for config file
|
||||||
|
env["SESSION_TIMEOUT_MINUTES"] = str(int(env.get("SESSION_TIMEOUT", "3600")) // 60 ) if int(env.get("SESSION_TIMEOUT", "3600")) >= 60 else "1"
|
||||||
|
context.update(env)
|
||||||
|
|
||||||
# create config files
|
# create config files
|
||||||
conf.jinja("/php.ini", context, "/usr/local/etc/php/conf.d/roundcube.ini")
|
conf.jinja("/php.ini", context, "/usr/local/etc/php/conf.d/roundcube.ini")
|
||||||
conf.jinja("/config.inc.php", context, "/var/www/html/config/config.inc.php")
|
conf.jinja("/config.inc.php", context, "/var/www/html/config/config.inc.php")
|
||||||
|
Loading…
x
Reference in New Issue
Block a user