diff --git a/CTFd/api/__init__.py b/CTFd/api/__init__.py index d6b8ed93..e53a0ac0 100644 --- a/CTFd/api/__init__.py +++ b/CTFd/api/__init__.py @@ -24,7 +24,7 @@ from CTFd.api.v1.unlocks import unlocks_namespace from CTFd.api.v1.users import users_namespace api = Blueprint("api", __name__, url_prefix="/api/v1") -CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI")) +CTFd_API_v1 = Api(api, version="v1", doc=current_app.config.get("SWAGGER_UI_ENDPOINT")) CTFd_API_v1.schema_model("APISimpleErrorResponse", APISimpleErrorResponse.schema()) CTFd_API_v1.schema_model( diff --git a/CTFd/config.ini b/CTFd/config.ini new file mode 100644 index 00000000..7398219d --- /dev/null +++ b/CTFd/config.ini @@ -0,0 +1,47 @@ +[server] +SECRET_KEY = +DATABASE_URL = +REDIS_URL = + +[security] +SESSION_COOKIE_HTTPONLY = true +SESSION_COOKIE_SAMESITE = Lax +PERMANENT_SESSION_LIFETIME = 604800 +TRUSTED_PROXIES = + +[email] +MAILFROM_ADDR = +MAIL_SERVER = +MAIL_PORT = +MAIL_USEAUTH = +MAIL_USERNAME = +MAIL_PASSWORD = +MAIL_TLS = +MAIL_SSL = +MAILGUN_API_KEY = +MAILGUN_BASE_URL = + +[uploads] +UPLOAD_PROVIDER = filesystem +UPLOAD_FOLDER = +AWS_ACCESS_KEY_ID = +AWS_SECRET_ACCESS_KEY = +AWS_S3_BUCKET = +AWS_S3_ENDPOINT_URL = + +[logs] +LOG_FOLDER = + +[optional] +REVERSE_PROXY = +TEMPLATES_AUTO_RELOAD = +SQLALCHEMY_TRACK_MODIFICATIONS = +SWAGGER_UI = +UPDATE_CHECK = +APPLICATION_ROOT = +SERVER_SENT_EVENTS = +SQLALCHEMY_MAX_OVERFLOW = + +[oauth] +OAUTH_CLIENT_ID = +OAUTH_CLIENT_SECRET = diff --git a/CTFd/config.py b/CTFd/config.py index 6616c25f..b21423b9 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -1,8 +1,28 @@ +import configparser import os +from distutils.util import strtobool -""" GENERATE SECRET KEY """ -if not os.getenv("SECRET_KEY"): +def process_boolean_str(value): + if type(value) is bool: + return value + + if value is None: + return False + + if value == "": + return None + + return bool(strtobool(value)) + + +def empty_str_cast(value, default=None): + if value == "": + return default + return value + + +def gen_secret_key(): # Attempt to read the secret from the secret file # This will fail if the secret has not been written try: @@ -21,11 +41,15 @@ if not os.getenv("SECRET_KEY"): secret.flush() except (OSError, IOError): pass + return key -""" SERVER SETTINGS """ +config_ini = configparser.ConfigParser() +path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini") +config_ini.read(path) +# fmt: off class Config(object): """ CTFd Configuration Object @@ -62,33 +86,37 @@ class Config(object): e.g. redis://user:password@localhost:6379 http://pythonhosted.org/Flask-Caching/#configuring-flask-caching """ - SECRET_KEY = os.getenv("SECRET_KEY") or key - DATABASE_URL = os.getenv("DATABASE_URL") or "sqlite:///{}/ctfd.db".format( - os.path.dirname(os.path.abspath(__file__)) - ) - REDIS_URL = os.getenv("REDIS_URL") + SECRET_KEY: str = os.getenv("SECRET_KEY") \ + or empty_str_cast(config_ini["server"]["SECRET_KEY"]) \ + or gen_secret_key() + + DATABASE_URL: str = os.getenv("DATABASE_URL") \ + or empty_str_cast(config_ini["server"]["DATABASE_URL"]) \ + or f"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db" + + REDIS_URL: str = os.getenv("REDIS_URL") \ + or empty_str_cast(config_ini["server"]["REDIS_URL"]) SQLALCHEMY_DATABASE_URI = DATABASE_URL CACHE_REDIS_URL = REDIS_URL if CACHE_REDIS_URL: - CACHE_TYPE = "redis" + CACHE_TYPE: str = "redis" else: - CACHE_TYPE = "filesystem" - CACHE_DIR = os.path.join( + CACHE_TYPE: str = "filesystem" + CACHE_DIR: str = os.path.join( os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache" ) - CACHE_THRESHOLD = ( - 0 - ) # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing. + # Override the threshold of cached values on the filesystem. The default is 500. Don't change unless you know what you're doing. + CACHE_THRESHOLD: int = 0 """ === SECURITY === SESSION_COOKIE_HTTPONLY: - Controls if cookies should be set with the HttpOnly flag. + Controls if cookies should be set with the HttpOnly flag. Defaults to True PERMANENT_SESSION_LIFETIME: - The lifetime of a session. The default is 604800 seconds. + The lifetime of a session. The default is 604800 seconds (7 days). TRUSTED_PROXIES: Defines a set of regular expressions used for finding a user's IP address if the CTFd instance @@ -98,11 +126,18 @@ class Config(object): CTFd only uses IP addresses for cursory tracking purposes. It is ill-advised to do anything complicated based solely on IP addresses unless you know what you are doing. """ - SESSION_COOKIE_HTTPONLY = not os.getenv("SESSION_COOKIE_HTTPONLY") # Defaults True - SESSION_COOKIE_SAMESITE = os.getenv("SESSION_COOKIE_SAMESITE") or "Lax" - PERMANENT_SESSION_LIFETIME = int( - os.getenv("PERMANENT_SESSION_LIFETIME") or 604800 - ) # 7 days in seconds + SESSION_COOKIE_HTTPONLY: bool = process_boolean_str(os.getenv("SESSION_COOKIE_HTTPONLY")) \ + or config_ini["security"].getboolean("SESSION_COOKIE_HTTPONLY") \ + or True + + SESSION_COOKIE_SAMESITE: str = os.getenv("SESSION_COOKIE_SAMESITE") \ + or empty_str_cast(config_ini["security"]["SESSION_COOKIE_SAMESITE"]) \ + or "Lax" + + PERMANENT_SESSION_LIFETIME: int = int(os.getenv("PERMANENT_SESSION_LIFETIME", 0)) \ + or config_ini["security"].getint("PERMANENT_SESSION_LIFETIME") \ + or 604800 + TRUSTED_PROXIES = [ r"^127\.0\.0\.1$", # Remove the following proxies if you do not trust the local network @@ -150,16 +185,36 @@ class Config(object): Mailgun base url to send email over Mailgun. As of CTFd v3, Mailgun integration is deprecated. Installations using the Mailgun API should migrate over to SMTP settings. """ - MAILFROM_ADDR = os.getenv("MAILFROM_ADDR") or "noreply@ctfd.io" - MAIL_SERVER = os.getenv("MAIL_SERVER") or None - MAIL_PORT = os.getenv("MAIL_PORT") - MAIL_USEAUTH = os.getenv("MAIL_USEAUTH") - MAIL_USERNAME = os.getenv("MAIL_USERNAME") - MAIL_PASSWORD = os.getenv("MAIL_PASSWORD") - MAIL_TLS = os.getenv("MAIL_TLS") or False - MAIL_SSL = os.getenv("MAIL_SSL") or False - MAILGUN_API_KEY = os.getenv("MAILGUN_API_KEY") - MAILGUN_BASE_URL = os.getenv("MAILGUN_BASE_URL") + MAILFROM_ADDR: str = os.getenv("MAILFROM_ADDR") \ + or config_ini["email"]["MAILFROM_ADDR"] \ + or "noreply@ctfd.io" + + MAIL_SERVER: str = os.getenv("MAIL_SERVER") \ + or empty_str_cast(config_ini["email"]["MAIL_SERVER"]) + + MAIL_PORT: str = os.getenv("MAIL_PORT") \ + or empty_str_cast(config_ini["email"]["MAIL_PORT"]) + + MAIL_USEAUTH: bool = process_boolean_str(os.getenv("MAIL_USEAUTH")) \ + or process_boolean_str(config_ini["email"]["MAIL_USEAUTH"]) + + MAIL_USERNAME: str = os.getenv("MAIL_USERNAME") \ + or empty_str_cast(config_ini["email"]["MAIL_USERNAME"]) + + MAIL_PASSWORD: str = os.getenv("MAIL_PASSWORD") \ + or empty_str_cast(config_ini["email"]["MAIL_PASSWORD"]) + + MAIL_TLS: bool = process_boolean_str(os.getenv("MAIL_TLS")) \ + or process_boolean_str(config_ini["email"]["MAIL_TLS"]) + + MAIL_SSL: bool = process_boolean_str(os.getenv("MAIL_SSL")) \ + or process_boolean_str(config_ini["email"]["MAIL_SSL"]) + + MAILGUN_API_KEY: str = os.getenv("MAILGUN_API_KEY") \ + or empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"]) + + MAILGUN_BASE_URL: str = os.getenv("MAILGUN_BASE_URL") \ + or empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"]) """ === LOGS === @@ -167,9 +222,9 @@ class Config(object): The location where logs are written. These are the logs for CTFd key submissions, registrations, and logins. The default location is the CTFd/logs folder. """ - LOG_FOLDER = os.getenv("LOG_FOLDER") or os.path.join( - os.path.dirname(os.path.abspath(__file__)), "logs" - ) + LOG_FOLDER: str = os.getenv("LOG_FOLDER") \ + or empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \ + or os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") """ === UPLOADS === @@ -193,15 +248,26 @@ class Config(object): A URL pointing to a custom S3 implementation. """ - UPLOAD_PROVIDER = os.getenv("UPLOAD_PROVIDER") or "filesystem" - UPLOAD_FOLDER = os.getenv("UPLOAD_FOLDER") or os.path.join( - os.path.dirname(os.path.abspath(__file__)), "uploads" - ) + UPLOAD_PROVIDER: str = os.getenv("UPLOAD_PROVIDER") \ + or empty_str_cast(config_ini["uploads"]["UPLOAD_PROVIDER"]) \ + or "filesystem" + + UPLOAD_FOLDER: str = os.getenv("UPLOAD_FOLDER") \ + or empty_str_cast(config_ini["uploads"]["UPLOAD_FOLDER"]) \ + or os.path.join(os.path.dirname(os.path.abspath(__file__)), "uploads") + if UPLOAD_PROVIDER == "s3": - AWS_ACCESS_KEY_ID = os.getenv("AWS_ACCESS_KEY_ID") - AWS_SECRET_ACCESS_KEY = os.getenv("AWS_SECRET_ACCESS_KEY") - AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET") - AWS_S3_ENDPOINT_URL = os.getenv("AWS_S3_ENDPOINT_URL") + AWS_ACCESS_KEY_ID: str = os.getenv("AWS_ACCESS_KEY_ID") \ + or empty_str_cast(config_ini["uploads"]["AWS_ACCESS_KEY_ID"]) + + AWS_SECRET_ACCESS_KEY: str = os.getenv("AWS_SECRET_ACCESS_KEY") \ + or empty_str_cast(config_ini["uploads"]["AWS_SECRET_ACCESS_KEY"]) + + AWS_S3_BUCKET: str = os.getenv("AWS_S3_BUCKET") \ + or empty_str_cast(config_ini["uploads"]["AWS_S3_BUCKET"]) + + AWS_S3_ENDPOINT_URL: str = os.getenv("AWS_S3_ENDPOINT_URL") \ + or empty_str_cast(config_ini["uploads"]["AWS_S3_ENDPOINT_URL"]) """ === OPTIONAL === @@ -216,16 +282,16 @@ class Config(object): Alternatively if you specify `true` CTFd will default to the above behavior with all proxy settings set to 1. TEMPLATES_AUTO_RELOAD: - Specifies whether Flask should check for modifications to templates and reload them automatically. + Specifies whether Flask should check for modifications to templates and reload them automatically. Defaults True. SQLALCHEMY_TRACK_MODIFICATIONS: - Automatically disabled to suppress warnings and save memory. You should only enable this if you need it. + Automatically disabled to suppress warnings and save memory. You should only enable this if you need it. Defaults False. SWAGGER_UI: Enable the Swagger UI endpoint at /api/v1/ UPDATE_CHECK: - Specifies whether or not CTFd will check whether or not there is a new version of CTFd + Specifies whether or not CTFd will check whether or not there is a new version of CTFd. Defaults True. APPLICATION_ROOT: Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory. @@ -239,18 +305,41 @@ class Config(object): https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/#configuration-keys """ - REVERSE_PROXY = os.getenv("REVERSE_PROXY") or False - TEMPLATES_AUTO_RELOAD = not os.getenv("TEMPLATES_AUTO_RELOAD") # Defaults True - SQLALCHEMY_TRACK_MODIFICATIONS = ( - os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS") is not None - ) # Defaults False - SWAGGER_UI = "/" if os.getenv("SWAGGER_UI") is not None else False # Defaults False - UPDATE_CHECK = not os.getenv("UPDATE_CHECK") # Defaults True - APPLICATION_ROOT = os.getenv("APPLICATION_ROOT") or "/" - SERVER_SENT_EVENTS = not os.getenv("SERVER_SENT_EVENTS") # Defaults True + REVERSE_PROXY: bool = process_boolean_str(os.getenv("REVERSE_PROXY")) \ + or empty_str_cast(config_ini["optional"]["REVERSE_PROXY"]) \ + or False + + TEMPLATES_AUTO_RELOAD: bool = process_boolean_str(os.getenv("TEMPLATES_AUTO_RELOAD")) \ + or empty_str_cast(config_ini["optional"]["TEMPLATES_AUTO_RELOAD"]) \ + or True + + SQLALCHEMY_TRACK_MODIFICATIONS: bool = process_boolean_str(os.getenv("SQLALCHEMY_TRACK_MODIFICATIONS")) \ + or empty_str_cast(config_ini["optional"]["SQLALCHEMY_TRACK_MODIFICATIONS"]) \ + or False + + SWAGGER_UI: bool = os.getenv("SWAGGER_UI") \ + or empty_str_cast(config_ini["optional"]["SWAGGER_UI"]) \ + or False + + SWAGGER_UI_ENDPOINT: str = "/" if SWAGGER_UI else None + + UPDATE_CHECK: bool = process_boolean_str(os.getenv("UPDATE_CHECK")) \ + or empty_str_cast(config_ini["optional"]["UPDATE_CHECK"]) \ + or True + + APPLICATION_ROOT: str = os.getenv("APPLICATION_ROOT") \ + or empty_str_cast(config_ini["optional"]["APPLICATION_ROOT"]) \ + or "/" + + SERVER_SENT_EVENTS: bool = process_boolean_str(os.getenv("SERVER_SENT_EVENTS")) \ + or empty_str_cast(config_ini["optional"]["SERVER_SENT_EVENTS"]) \ + or True + if DATABASE_URL.startswith("sqlite") is False: SQLALCHEMY_ENGINE_OPTIONS = { - "max_overflow": int(os.getenv("SQLALCHEMY_MAX_OVERFLOW", 20)) + "max_overflow": int(os.getenv("SQLALCHEMY_MAX_OVERFLOW", 0)) + or int(empty_str_cast(config_ini["optional"]["SQLALCHEMY_MAX_OVERFLOW"], default=0)) # noqa: E131 + or 20, # noqa: E131 } """ @@ -259,8 +348,11 @@ class Config(object): MajorLeagueCyber Integration Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here """ - OAUTH_CLIENT_ID = os.getenv("OAUTH_CLIENT_ID") - OAUTH_CLIENT_SECRET = os.getenv("OAUTH_CLIENT_SECRET") + OAUTH_CLIENT_ID: str = os.getenv("OAUTH_CLIENT_ID") \ + or empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_ID"]) + OAUTH_CLIENT_SECRET: str = os.getenv("OAUTH_CLIENT_SECRET") \ + or empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_SECRET"]) +# fmt: on class TestingConfig(Config):