diff --git a/README.org b/README.org index d4ccab2..e6c5360 100644 --- a/README.org +++ b/README.org @@ -60,9 +60,7 @@ A WSGI server like [[https://gunicorn.org/][Gunicorn]] can then be used to serve the app for example like this: - : $ gunicorn "raincloud:create_app(base_path='public')" - - *Note* that currently only one worker makes sense due to server side session caching. + : $ gunicorn "raincloud:create_app(base_path='public', secret_key='i_am_a_key', redis_url='redis://127.0.0.1:6379/0')" *** NixOS @@ -80,15 +78,18 @@ All configuration options are: - | Option | Description | Type | Default value | Example | - |-----------------+-----------------------------------+-------+---------------+----------------------| - | =address= | Bind address of the server | =str= | =127.0.0.1= | =0.0.0.0= | - | =port= | Port on which the server listens | =int= | =8000= | =5000= | - | =user= | User under which the server runs | =str= | =raincloud= | =alice= | - | =group= | Group under which the server runs | =str= | =raincloud= | =users= | - | =cloudName= | Name of the raincloud | =str= | =raincloud= | =bobsCloud= | - | =basePath= | Base path of the raincloud | =str= | | =/var/lib/raincloud= | - | =workerTimeout= | Gunicorn worker timeout | =int= | =300= | =360= | + | Option | Description | Type | Default value | Example | + |-----------------+---------------------------------------------------------------+-------+----------------------------+-------------------------------| + | =address= | Bind address of the server | =str= | =127.0.0.1= | =0.0.0.0= | + | =port= | Port on which the server listens | =int= | =8000= | =5000= | + | =user= | User under which the server runs | =str= | =raincloud= | =alice= | + | =group= | Group under which the server runs | =str= | =raincloud= | =users= | + | =cloudName= | Name of the raincloud | =str= | =raincloud= | =bobsCloud= | + | =basePath= | Base path of the raincloud | =str= | | =/var/lib/raincloud= | + | =secretKey= | Flask secret key | =str= | | =i_am_a_key= | + | =redisUrl= | URL of redis database | =str= | =redis://127.0.0.1:6379/0= | =redis://my_db_server:6379/0= | + | =numWorkers= | Number of Gunicorn workers (recommendation is: 2 x #CPUs + 1) | =int= | =5= | =17= | + | =workerTimeout= | Gunicorn worker timeout | =int= | =300= | =360= | *** Docker @@ -111,13 +112,15 @@ ** Configuration - /raincloud/ provides two configuration options which can be passed to =raincloud.create_app()=: + /raincloud/ provides four configuration options which can be passed to =raincloud.create_app()=: - =base_path= :: Base path of the raincloud + - =secret_key= :: Flask secret key + - =redis_url= :: URL of redis database (default: =redis://127.0.0.1:6379/0=) - =cloud_name= :: Cloud name (default: =raincloud=) Set them for example like this: - : >>> app = raincloud.create_app(base_path='/home/alice/public', cloud_name='myCloud') + : >>> app = raincloud.create_app(base_path='/home/alice/public', secret_key='i_am_a_key', redis_url='redis://127.0.0.1:6379/0', cloud_name='raincloud') *** =rc.conf= :properties: diff --git a/flake.nix b/flake.nix index 95ac457..6ca0c74 100644 --- a/flake.nix +++ b/flake.nix @@ -62,6 +62,24 @@ description = "Base path of the raincloud"; }; + secretKey = mkOption { + type = types.str; + description = "Flask secret key"; + }; + + redisUrl = mkOption { + type = types.str; + default = "redis://127.0.0.1:6379/0"; + description = "URL of redis database"; + }; + + numWorkers = mkOption { + type = types.int; + default = 5; + example = 17; + description = "Number of Gunicorn workers (recommendation is: 2 x #CPUs + 1)"; + }; + workerTimeout = mkOption { type = types.int; default = 300; @@ -97,7 +115,8 @@ PermissionsStartOnly = true; ExecStart = '' - ${gunicorn}/bin/gunicorn "raincloud:create_app('${cfg.basePath}', '${cfg.cloudName}')" \ + ${gunicorn}/bin/gunicorn "raincloud:create_app('${cfg.basePath}', '${cfg.secretKey}', '${cfg.redisUrl}', '${cfg.cloudName}')" \ + --workers ${toString cfg.numWorkers} \ --timeout ${toString cfg.workerTimeout} \ --bind=${cfg.address}:${toString cfg.port} ''; @@ -143,6 +162,7 @@ python3 python3Packages.flask python3Packages.gunicorn + python3Packages.redis ]; }; } diff --git a/raincloud/raincloud.py b/raincloud/raincloud.py index b88a430..aa06bf6 100755 --- a/raincloud/raincloud.py +++ b/raincloud/raincloud.py @@ -17,15 +17,17 @@ import os import werkzeug -def create_app(base_path, cloud_name="raincloud"): +def create_app( + base_path, secret_key, redis_url="redis://127.0.0.1:6379/0", cloud_name="raincloud" +): # Create app app = Flask(__name__) - app.config["SECRET_KEY"] = os.urandom(24) + app.config["SECRET_KEY"] = secret_key # Create handlers dh = DirectoryHandler(base_path) - sh = SessionHandler() + sh = SessionHandler(redis_url) @app.route("/", methods=["GET", "POST"]) @app.route("//", methods=["GET"]) diff --git a/raincloud/session_handler.py b/raincloud/session_handler.py index c0e11ba..455ebf3 100644 --- a/raincloud/session_handler.py +++ b/raincloud/session_handler.py @@ -1,36 +1,74 @@ -from datetime import datetime, timedelta +import json +from redis import from_url +import time import uuid +class RaincloudNetworkException(Exception): + pass + + class SessionHandler: - def __init__(self): - self.sessions = [] + def __init__(self, redis_url="redis://127.0.0.1:6379/0"): + try: + self.redis = from_url(redis_url) + except Exception as ex: + raise RaincloudNetworkException( + f"Exception while connecting to redis: {ex}" + ) + + if not self.redis.get("raincloud_sessions"): + self.redis.set("raincloud_sessions", json.dumps([])) + + def _get_sessions(self): + """Get sessions from redis server.""" + try: + return json.loads(self.redis.get("raincloud_sessions")) + except Exception as ex: + raise RaincloudNetworkException( + f"Exception while getting sessions from redis: {ex}" + ) + + def _save_sessions(self, sessions): + """Save 'sessions' to redis.""" + try: + self.redis.set("raincloud_sessions", json.dumps(sessions)) + except Exception as ex: + raise RaincloudNetworkException( + f"Exception while saving sessions to redis: {ex}" + ) def create_session_id(self): """Create a new session ID.""" - ids = [s[2] for s in self.sessions] + ids = [s[2] for s in self._get_sessions()] id_ = uuid.uuid4() while id_ in ids: id_ = uuid.uuid4() - return id_ + return str(id_) def add_session(self, directory, id_): """Add session with 'id_' allowing access to 'directory'.""" - self.sessions.append((datetime.now(), directory, id_)) + sessions = self._get_sessions() + sessions.append((time.time(), directory, id_)) + self._save_sessions(sessions) def clean_sessions(self): """Remove all sessions which are older than one day.""" - self.sessions = [ - s for s in self.sessions if s[0] > datetime.now() - timedelta(days=1) - ] + sessions = self._get_sessions() + sessions = [s for s in sessions if s[0] > time.time() - 86400] + self._save_sessions(sessions) def validate_session(self, directory, id_): """check if session with 'id_' is allowed to access 'directory'.""" - valid_dates = [s[0] for s in self.sessions if s[1] == directory and s[2] == id_] - if len(valid_dates) > 0 and valid_dates[0] > datetime.now() - timedelta(days=1): + valid_dates = [ + s[0] for s in self._get_sessions() if s[1] == directory and s[2] == id_ + ] + if len(valid_dates) > 0 and valid_dates[0] > time.time() - 86400: return True return False def delete_session(self, id_): """Delete session with 'id_'.""" - self.sessions = [s for s in self.sessions if s[2] != id_] + sessions = self._get_sessions() + sessions = [s for s in sessions if s[2] != id_] + self._save_sessions(sessions)