diff --git a/CTFd/config.ini b/CTFd/config.ini deleted file mode 100644 index ef05466..0000000 --- a/CTFd/config.ini +++ /dev/null @@ -1,317 +0,0 @@ -# CUSTOMIZED FROM CTFd 3.7.7 - -# CTFd Configuration File -# -# Use this file to configure aspects of how CTFd behaves. Additional attributes can be specified for -# plugins and other additional behavior. -# -# If a configuration item is specified but left empty, CTFd will do the following: -# -# 1. Look for an environment variable under the same name and use that value if found -# 2. Use a default value specified in it's own internal configuration -# 3. Use a null value (i.e. None) or empty string for the configuration value - - -[server] -# SECRET_KEY: -# The secret value used to creation sessions and sign strings. This should be set to a random string. In the -# interest of ease, CTFd will automatically create a secret key file for you. If you wish to add this secret key -# to your instance you should hard code this value to a random static value. -# -# You can also remove .ctfd_secret_key from the .gitignore file and commit this file into whatever repository -# you are using. -# -# http://flask.pocoo.org/docs/latest/quickstart/#sessions -SECRET_KEY = - -# DATABASE_URL -# The URI that specifies the username, password, hostname, port, and database of the server -# used to hold the CTFd database. -# -# If neither this setting nor `DATABASE_HOST` is specified, CTFd will automatically create a SQLite database for you to use -# e.g. mysql+pymysql://root:@localhost/ctfd -DATABASE_URL = - -# DATABASE_HOST -# The hostname of the database server used to hold the CTFd database. -# If `DATABASE_URL` is set, this setting will have no effect. -# -# This option, along with the other `DATABASE_*` options, are an alternative to specifying all connection details in the single `DATABASE_URL`. -# If neither this setting nor `DATABASE_URL` is specified, CTFd will automatically create a SQLite database for you to use. -DATABASE_HOST = - -# DATABASE_PROTOCOL -# The protocol used to access the database server, if `DATABASE_HOST` is set. Defaults to `mysql+pymysql`. -DATABASE_PROTOCOL = - -# DATABASE_USER -# The username used to access the database server, if `DATABASE_HOST` is set. Defaults to `ctfd`. -DATABASE_USER = - -# DATABASE_PASSWORD -# The password used to access the database server, if `DATABASE_HOST` is set. -DATABASE_PASSWORD = - -# DATABASE_PORT -# The port used to access the database server, if `DATABASE_HOST` is set. -DATABASE_PORT = - -# DATABASE_NAME -# The name of the database to access on the database server, if `DATABASE_HOST` is set. Defaults to `ctfd`. -DATABASE_NAME = - -# REDIS_URL -# The URL to connect to a Redis server. If neither this setting nor `REDIS_HOST` is specified, -# CTFd will use the .data folder as a filesystem cache. -# -# e.g. redis://user:password@localhost:6379 -# http://pythonhosted.org/Flask-Caching/#configuring-flask-caching -REDIS_URL = - -# REDIS_HOST -# The hostname of the Redis server to connect to. -# If `REDIS_URL` is set, this setting will have no effect. -# -# This option, along with the other `REDIS_*` options, are an alternative to specifying all connection details in the single `REDIS_URL`. -# If neither this setting nor `REDIS_URL` is specified, CTFd will use the .data folder as a filesystem cache. -REDIS_HOST = - -# REDIS_PROTOCOL -# The protocol used to access the Redis server, if `REDIS_HOST` is set. Defaults to `redis`. -# -# Note that the `unix` protocol is not supported here; use `REDIS_URL` instead. -REDIS_PROTOCOL = - -# REDIS_USER -# The username used to access the Redis server, if `REDIS_HOST` is set. -REDIS_USER = - -# REDIS_PASSWORD -# The password used to access the Redis server, if `REDIS_HOST` is set. -REDIS_PASSWORD = - -# REDIS_PORT -# The port used to access the Redis server, if `REDIS_HOST` is set. -REDIS_PORT = - -# REDIS_DB -# The index of the Redis database to access, if `REDIS_HOST` is set. -REDIS_DB = - -# CACHE_REDIS_CLUSTER -# Specifies whether or not CTFd is using a Redis Cluster. If set to true, CTFd will use the RedisClusterCache. -CACHE_REDIS_CLUSTER = - -[security] -# TRUSTED_HOSTS -# Comma seperated string containing valid host names for CTFd to respond to. If not specified, -# CTFd will respond to requests for any host unless otherwise restricted in an upstream proxy. -# Each value is either an exact match, or can start with a dot . to match any subdomain. -# -# It is recommended that most users set this to the server name that they expect to be using -# -# Example: example.com,ctfd.io,.ctfd.io -# See https://flask.palletsprojects.com/en/stable/config/#TRUSTED_HOSTS -TRUSTED_HOSTS = - -# SESSION_COOKIE_HTTPONLY -# Controls if cookies should be set with the HttpOnly flag. Defaults to True. -SESSION_COOKIE_HTTPONLY = true - -# SESSION_COOKIE_SAMESITE -# Controls the SameSite attribute on session cookies. Can be Lax or Strict. -# Should be left as Lax unless the implications are well understood -SESSION_COOKIE_SAMESITE = Lax - -# PERMANENT_SESSION_LIFETIME -# The lifetime of a session. The default is 604800 seconds (7 days). -PERMANENT_SESSION_LIFETIME = 604800 - -# CROSS_ORIGIN_OPENER_POLICY -# Setting for the Cross-Origin-Opener-Policy response header. Defaults to same-origin-allow-popups -CROSS_ORIGIN_OPENER_POLICY = - -[email] -# MAILFROM_ADDR -# The email address that emails are sent from if not overridden in the configuration panel. -MAILFROM_ADDR = - -# MAIL_SERVER -# The mail server that emails are sent from if not overriden in the configuration panel. -MAIL_SERVER = - -# MAIL_PORT -# The mail port that emails are sent from if not overriden in the configuration panel. -MAIL_PORT = - -# MAIL_USEAUTH -# Whether or not to use username and password to authenticate to the SMTP server -MAIL_USEAUTH = - -# MAIL_USERNAME -# The username used to authenticate to the SMTP server if MAIL_USEAUTH is defined -MAIL_USERNAME = - -# MAIL_PASSWORD -# The password used to authenticate to the SMTP server if MAIL_USEAUTH is defined -MAIL_PASSWORD = - -# MAIL_TLS -# Whether to connect to the SMTP server over TLS -MAIL_TLS = - -# MAIL_SSL -# Whether to connect to the SMTP server over SSL -MAIL_SSL = - -# MAILSENDER_ADDR -# The email address that is responsible for the transmission of emails. -# This is very often the MAILFROM_ADDR value but can be specified if your email -# is delivered by a different domain than what's specified in your MAILFROM_ADDR. -# If this isn't specified, the MAILFROM_ADDR value is used. -# It is fairly rare to need to set this value. -MAILSENDER_ADDR = - -# MAILGUN_API_KEY -# Mailgun API key to send email over Mailgun. As of CTFd v3, Mailgun integration is deprecated. -# Installations using the Mailgun API should migrate over to SMTP settings. -MAILGUN_API_KEY = - -# MAILGUN_BASE_URL -# 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. -MAILGUN_BASE_URL = - -# MAIL_PROVIDER -# Specifies the email provider that CTFd will use to send email. -# By default CTFd will automatically detect the correct email provider based on the other settings -# specified here or in the configuration panel. This setting can be used to force a specific provider. -MAIL_PROVIDER = - -[uploads] -# UPLOAD_PROVIDER -# Specifies the service that CTFd should use to store files. -# Can be set to filesystem or s3 -UPLOAD_PROVIDER = - -# UPLOAD_FOLDER -# The location where files are uploaded under the filesystem uploader. -# The default destination is the CTFd/uploads folder. -UPLOAD_FOLDER = - -# AWS_ACCESS_KEY_ID -# AWS access token used to authenticate to the S3 bucket. Only used under the s3 uploader. -AWS_ACCESS_KEY_ID = - -# AWS_SECRET_ACCESS_KEY -# AWS secret token used to authenticate to the S3 bucket. Only used under the s3 uploader. -AWS_SECRET_ACCESS_KEY = - -# AWS_S3_BUCKET -# The unique identifier for your S3 bucket. Only used under the s3 uploader. -AWS_S3_BUCKET = - -# AWS_S3_ENDPOINT_URL -# A URL pointing to a custom S3 implementation. Only used under the s3 uploader. -AWS_S3_ENDPOINT_URL = - -# AWS_S3_REGION -# The aws region that hosts your bucket. Only used in the s3 uploader. -AWS_S3_REGION = - -# AWS_S3_ADDRESSING_STYLE -# The S3 addressing style to use for URLs. Only used under the s3 uploader. -# Defaults to auto; can be set to virtual or path. -# See https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html -AWS_S3_ADDRESSING_STYLE = - -# AWS_S3_CUSTOM_DOMAIN -# A hostname that replaces the default hostname in the generated S3 download URLs. Required by some S3 providers or CDNs. -# Only used under the s3 uploader. -AWS_S3_CUSTOM_DOMAIN = - -# AWS_S3_CUSTOM_PREFIX -# Add a prefix to the file path to be placed in S3. -# Only used under the s3 uploader. -AWS_S3_CUSTOM_PREFIX = - -[logs] -# LOG_FOLDER -# 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 = - -[optional] -# REVERSE_PROXY -# Specifies whether CTFd is behind a reverse proxy or not. Set to true if using a reverse proxy like nginx. -# You can also specify a comma seperated set of numbers specifying the reverse proxy configuration settings. -# See https://werkzeug.palletsprojects.com/en/0.15.x/middleware/proxy_fix/#werkzeug.middleware.proxy_fix.ProxyFix. -# For example to configure `x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1` specify `1,1,1,1,1`. -# If you specify `true` CTFd will default to the above behavior with all proxy settings set to 1. -REVERSE_PROXY = - -# THEME_FALLBACK -# Specifies whether CTFd will fallback to the default "core" theme for missing pages/content. Useful for developing themes or using incomplete themes. -# Defaults to true. -THEME_FALLBACK = - -# TEMPLATES_AUTO_RELOAD -# Specifies whether Flask should check for modifications to templates and reload them automatically. Defaults to true. -TEMPLATES_AUTO_RELOAD = - -# SQLALCHEMY_TRACK_MODIFICATIONS -# Automatically disabled to suppress warnings and save memory. -# You should only enable this if you need it. -# Defaults to false. -SQLALCHEMY_TRACK_MODIFICATIONS = - -# SWAGGER_UI -# Enable the Swagger UI endpoint at /api/v1/ -SWAGGER_UI = - -# UPDATE_CHECK -# Specifies whether or not CTFd will check whether or not there is a new version of CTFd. Defaults True. -UPDATE_CHECK = - -# APPLICATION_ROOT -# Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory. -# Example: /ctfd -APPLICATION_ROOT = - -# SERVER_SENT_EVENTS -# Specifies whether or not to enable the Server-Sent Events based Notifications system. -# Defaults to true -SERVER_SENT_EVENTS = - -# HTML_SANITIZATION -# Specifies whether CTFd should sanitize HTML content -# Defaults to false -HTML_SANITIZATION = - -# SQLALCHEMY_MAX_OVERFLOW -# Specifies the max_overflow setting for SQLAlchemy's Engine -# https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine -# https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/#configuration-keys -SQLALCHEMY_MAX_OVERFLOW = - -# SQLALCHEMY_POOL_PRE_PING -# Specifies the pool_pre_ping setting for SQLAlchemy's Engine -# https://docs.sqlalchemy.org/en/13/core/engines.html#sqlalchemy.create_engine -# https://flask-sqlalchemy.palletsprojects.com/en/2.x/config/#configuration-keys -SQLALCHEMY_POOL_PRE_PING = - -# SAFE_MODE -# If SAFE_MODE is enabled, CTFd will not load any plugins which may alleviate issues preventing CTFd from starting -# Defaults to false -SAFE_MODE = - -[oauth] -# OAUTH_CLIENT_ID -# Register an event at https://majorleaguecyber.org/ and use the Client ID here -OAUTH_CLIENT_ID = - -# OAUTH_CLIENT_ID -# Register an event at https://majorleaguecyber.org/ and use the Client Secret here -OAUTH_CLIENT_SECRET = - -[extra] -# The extra section can be used to specify additional values to be loaded into CTFd's configuration diff --git a/CTFd/config.py b/CTFd/config.py deleted file mode 100644 index 346468a..0000000 --- a/CTFd/config.py +++ /dev/null @@ -1,300 +0,0 @@ -# CUSTOMIZED FROM CTFd 3.7.7 - -import configparser -import os -from distutils.util import strtobool -from typing import Union - -from sqlalchemy.engine.url import URL - - -class EnvInterpolation(configparser.BasicInterpolation): - """Interpolation which expands environment variables in values.""" - - def before_get(self, parser, section, option, value, defaults): - value = super().before_get(parser, section, option, value, defaults) - envvar = os.getenv(option) - if value == "" and envvar: - return process_string_var(envvar) - else: - return value - - -def process_string_var(value): - if value == "": - return None - - if value.isdigit(): - return int(value) - elif value.replace(".", "", 1).isdigit(): - return float(value) - - try: - return bool(strtobool(value)) - except ValueError: - return value - - -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: - with open(".ctfd_secret_key", "rb") as secret: - key = secret.read() - except OSError: - key = None - - if not key: - key = os.urandom(64) - # Attempt to write the secret file - # This will fail if the filesystem is read-only - try: - with open(".ctfd_secret_key", "wb") as secret: - secret.write(key) - secret.flush() - except OSError: - pass - return key - - -config_ini = configparser.ConfigParser(interpolation=EnvInterpolation()) -config_ini.optionxform = str # Makes the key value case-insensitive -path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "config.ini") -config_ini.read(path) - - -# fmt: off -class ServerConfig(object): - SECRET_KEY: str = empty_str_cast(config_ini["server"]["SECRET_KEY"]) \ - or gen_secret_key() - - DATABASE_URL: str = empty_str_cast(config_ini["server"]["DATABASE_URL"]) - if not DATABASE_URL: - if empty_str_cast(config_ini["server"]["DATABASE_HOST"]) is not None: - # construct URL from individual variables - DATABASE_URL = str(URL( - drivername=empty_str_cast(config_ini["server"]["DATABASE_PROTOCOL"]) or "mysql+pymysql", - username=empty_str_cast(config_ini["server"]["DATABASE_USER"]) or "ctfd", - password=empty_str_cast(config_ini["server"]["DATABASE_PASSWORD"]), - host=empty_str_cast(config_ini["server"]["DATABASE_HOST"]), - port=empty_str_cast(config_ini["server"]["DATABASE_PORT"]), - database=empty_str_cast(config_ini["server"]["DATABASE_NAME"]) or "ctfd", - )) - else: - # default to local SQLite DB - DATABASE_URL = f"sqlite:///{os.path.dirname(os.path.abspath(__file__))}/ctfd.db" - - REDIS_URL: str = empty_str_cast(config_ini["server"]["REDIS_URL"]) - CACHE_REDIS_CLUSTER: str = empty_str_cast(config_ini["server"]["CACHE_REDIS_CLUSTER"]) - - REDIS_HOST: str = empty_str_cast(config_ini["server"]["REDIS_HOST"]) - REDIS_PROTOCOL: str = empty_str_cast(config_ini["server"]["REDIS_PROTOCOL"]) or "redis" - REDIS_USER: str = empty_str_cast(config_ini["server"]["REDIS_USER"]) - REDIS_PASSWORD: str = empty_str_cast(config_ini["server"]["REDIS_PASSWORD"]) - REDIS_PORT: int = empty_str_cast(config_ini["server"]["REDIS_PORT"]) or 6379 - REDIS_DB: int = empty_str_cast(config_ini["server"]["REDIS_DB"]) or 0 - - if REDIS_URL or REDIS_HOST is None: - CACHE_REDIS_URL = REDIS_URL - else: - # construct URL from individual variables - CACHE_REDIS_URL = f"{REDIS_PROTOCOL}://" - if REDIS_USER: - CACHE_REDIS_URL += REDIS_USER - if REDIS_PASSWORD: - CACHE_REDIS_URL += f":{REDIS_PASSWORD}" - CACHE_REDIS_URL += f"@{REDIS_HOST}:{REDIS_PORT}/{REDIS_DB}" - - SQLALCHEMY_DATABASE_URI = DATABASE_URL - if CACHE_REDIS_CLUSTER: - CACHE_TYPE: str = "RedisClusterCache" - elif CACHE_REDIS_URL: - CACHE_TYPE: str = "RedisCache" - else: - CACHE_TYPE: str = "FileSystemCache" - CACHE_DIR: str = os.path.join( - os.path.dirname(__file__), os.pardir, ".data", "filesystem_cache" - ) - # 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 - - print(f"Using cache type: {CACHE_TYPE}") - print(f"Using Redis URL: {CACHE_REDIS_URL} (if applicable)") - print(f"Using Redis Cluster: {CACHE_REDIS_CLUSTER} (if applicable)") - - # === SECURITY === - SESSION_COOKIE_HTTPONLY: bool = config_ini["security"].getboolean("SESSION_COOKIE_HTTPONLY", fallback=True) - - SESSION_COOKIE_SAMESITE: str = empty_str_cast(config_ini["security"]["SESSION_COOKIE_SAMESITE"]) \ - or "Lax" - - PERMANENT_SESSION_LIFETIME: int = config_ini["security"].getint("PERMANENT_SESSION_LIFETIME") \ - or 604800 - - CROSS_ORIGIN_OPENER_POLICY: str = empty_str_cast(config_ini["security"].get("CROSS_ORIGIN_OPENER_POLICY")) \ - or "same-origin-allow-popups" - - TRUSTED_HOSTS: list[str] | None = None - if config_ini["security"].get("TRUSTED_HOSTS"): - TRUSTED_HOSTS = [ - h.strip() for h in empty_str_cast(config_ini["security"].get("TRUSTED_HOSTS")).split(",") - ] - - """ - TRUSTED_PROXIES: - Defines a set of regular expressions used for finding a user's IP address if the CTFd instance - is behind a proxy. If you are running a CTF and users are on the same network as you, you may choose to remove - some proxies from the list. - - 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. - """ - TRUSTED_PROXIES = [ - r"^127\.0\.0\.1$", - # Remove the following proxies if you do not trust the local network - # For example if you are running a CTF on your laptop and the teams are - # all on the same network - r"^::1$", - r"^fc00:", - r"^10\.", - r"^172\.(1[6-9]|2[0-9]|3[0-1])\.", - r"^192\.168\.", - ] - - # === EMAIL === - MAILFROM_ADDR: str = config_ini["email"]["MAILFROM_ADDR"] \ - or "noreply@examplectf.com" - - MAIL_SERVER: str = empty_str_cast(config_ini["email"]["MAIL_SERVER"]) - - MAIL_PORT: int = empty_str_cast(config_ini["email"]["MAIL_PORT"]) - - MAIL_USEAUTH: bool = process_boolean_str(config_ini["email"]["MAIL_USEAUTH"]) - - MAIL_USERNAME: str = empty_str_cast(config_ini["email"]["MAIL_USERNAME"]) - - MAIL_PASSWORD: str = empty_str_cast(config_ini["email"]["MAIL_PASSWORD"]) - - MAIL_TLS: bool = process_boolean_str(config_ini["email"]["MAIL_TLS"]) - - MAIL_SSL: bool = process_boolean_str(config_ini["email"]["MAIL_SSL"]) - - MAILSENDER_ADDR: str = empty_str_cast(config_ini["email"]["MAILSENDER_ADDR"]) - - MAILGUN_API_KEY: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"]) - - MAILGUN_BASE_URL: str = empty_str_cast(config_ini["email"]["MAILGUN_API_KEY"]) - - MAIL_PROVIDER: str = empty_str_cast(config_ini["email"].get("MAIL_PROVIDER")) - - # === LOGS === - LOG_FOLDER: str = empty_str_cast(config_ini["logs"]["LOG_FOLDER"]) \ - or os.path.join(os.path.dirname(os.path.abspath(__file__)), "logs") - - # === UPLOADS === - UPLOAD_PROVIDER: str = empty_str_cast(config_ini["uploads"]["UPLOAD_PROVIDER"]) \ - or "filesystem" - - UPLOAD_FOLDER: str = 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: str = empty_str_cast(config_ini["uploads"]["AWS_ACCESS_KEY_ID"]) - - AWS_SECRET_ACCESS_KEY: str = empty_str_cast(config_ini["uploads"]["AWS_SECRET_ACCESS_KEY"]) - - AWS_S3_BUCKET: str = empty_str_cast(config_ini["uploads"]["AWS_S3_BUCKET"]) - - AWS_S3_ENDPOINT_URL: str = empty_str_cast(config_ini["uploads"]["AWS_S3_ENDPOINT_URL"]) - - AWS_S3_REGION: str = empty_str_cast(config_ini["uploads"]["AWS_S3_REGION"]) - - AWS_S3_CUSTOM_DOMAIN: str = empty_str_cast(config_ini["uploads"].get("AWS_S3_CUSTOM_DOMAIN", "")) - - AWS_S3_CUSTOM_PREFIX: str = empty_str_cast(config_ini["uploads"].get("AWS_S3_CUSTOM_PREFIX", "")) - - AWS_S3_ADDRESSING_STYLE: str = empty_str_cast(config_ini["uploads"].get("AWS_S3_ADDRESSING_STYLE", ""), default="auto") - - # === OPTIONAL === - REVERSE_PROXY: Union[str, bool] = empty_str_cast(config_ini["optional"]["REVERSE_PROXY"], default=False) - - TEMPLATES_AUTO_RELOAD: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["TEMPLATES_AUTO_RELOAD"], default=True)) - - THEME_FALLBACK: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["THEME_FALLBACK"], default=True)) - - SQLALCHEMY_TRACK_MODIFICATIONS: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["SQLALCHEMY_TRACK_MODIFICATIONS"], default=False)) - - SWAGGER_UI: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["SWAGGER_UI"], default=False)) - - SWAGGER_UI_ENDPOINT: str = "/" if SWAGGER_UI else None - - UPDATE_CHECK: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["UPDATE_CHECK"], default=True)) - - APPLICATION_ROOT: str = empty_str_cast(config_ini["optional"]["APPLICATION_ROOT"], default="/") - - SERVER_SENT_EVENTS: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["SERVER_SENT_EVENTS"], default=True)) - - HTML_SANITIZATION: bool = process_boolean_str(empty_str_cast(config_ini["optional"]["HTML_SANITIZATION"], default=False)) - - SAFE_MODE: bool = process_boolean_str(empty_str_cast(config_ini["optional"].get("SAFE_MODE", False), default=False)) - - if DATABASE_URL.startswith("sqlite") is False: - SQLALCHEMY_ENGINE_OPTIONS = { - "max_overflow": int(empty_str_cast(config_ini["optional"]["SQLALCHEMY_MAX_OVERFLOW"], default=20)), # noqa: E131 - "pool_pre_ping": empty_str_cast(config_ini["optional"]["SQLALCHEMY_POOL_PRE_PING"], default=True), # noqa: E131 - } - - # === OAUTH === - OAUTH_CLIENT_ID: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_ID"]) - OAUTH_CLIENT_SECRET: str = empty_str_cast(config_ini["oauth"]["OAUTH_CLIENT_SECRET"]) - - # === EXTRA === - # Since the configurations in section "[extra]" will be loaded later, it is not necessary to declare them here. - # However, if you want to have some processing or checking on the value, you can still declare it here just like other configurations. -# fmt: on - - -class TestingConfig(ServerConfig): - SECRET_KEY = "AAAAAAAAAAAAAAAAAAAA" - PRESERVE_CONTEXT_ON_EXCEPTION = False - TESTING = True - DEBUG = True - SQLALCHEMY_DATABASE_URI = os.getenv("TESTING_DATABASE_URL") or "sqlite://" - MAIL_SERVER = os.getenv("TESTING_MAIL_SERVER") - SERVER_NAME = "localhost" - UPDATE_CHECK = False - REDIS_URL = None - CACHE_TYPE = "simple" - CACHE_THRESHOLD = 500 - SAFE_MODE = True - - -# Actually initialize ServerConfig to allow us to add more attributes on -Config = ServerConfig() -for k, v in config_ini.items("extra"): - # We should only add the values that are not yet loaded in ServerConfig. - if hasattr(Config, k): - raise ValueError( - f"Built-in Config {k} should not be defined in the [extra] section of config.ini" - ) - - setattr(Config, k, v) diff --git a/CTFd/utils/initialization/__init__.py b/CTFd/utils/initialization/__init__.py deleted file mode 100644 index 3c7b762..0000000 --- a/CTFd/utils/initialization/__init__.py +++ /dev/null @@ -1,357 +0,0 @@ -# CUSTOMIZED FROM CTFd 3.7.7 - -import datetime -import logging -import os -import sys - -from flask import abort, redirect, render_template, request, session, url_for -from sqlalchemy.exc import IntegrityError, InvalidRequestError -from werkzeug.middleware.dispatcher import DispatcherMiddleware - -from CTFd.cache import clear_user_recent_ips -from CTFd.exceptions import UserNotFoundException, UserTokenExpiredException -from CTFd.models import Tracking, db -from CTFd.utils import config, get_app_config, get_config, import_in_progress, markdown -from CTFd.utils.config import ( - can_send_mail, - ctf_logo, - ctf_name, - ctf_theme, - integrations, - is_setup, -) -from CTFd.utils.config.pages import get_pages -from CTFd.utils.dates import isoformat, unix_time, unix_time_millis, unix_time_to_utc -from CTFd.utils.events import EventManager, RedisEventManager -from CTFd.utils.humanize.words import pluralize -from CTFd.utils.modes import generate_account_url, get_mode_as_word -from CTFd.utils.plugins import ( - get_configurable_plugins, - get_menubar_plugins, - get_registered_admin_scripts, - get_registered_admin_stylesheets, - get_registered_scripts, - get_registered_stylesheets, -) -from CTFd.utils.security.auth import login_user, logout_user, lookup_user_token -from CTFd.utils.security.csrf import generate_nonce -from CTFd.utils.user import ( - authed, - get_current_team_attrs, - get_current_user_attrs, - get_current_user_recent_ips, - get_ip, - get_locale, - is_admin, -) - - -def init_cli(app): - from CTFd.cli import _cli - - app.register_blueprint(_cli, cli_group=None) - - -def init_template_filters(app): - app.jinja_env.filters["markdown"] = markdown - app.jinja_env.filters["unix_time"] = unix_time - app.jinja_env.filters["unix_time_millis"] = unix_time_millis - app.jinja_env.filters["unix_time_to_utc"] = unix_time_to_utc - app.jinja_env.filters["isoformat"] = isoformat - app.jinja_env.filters["pluralize"] = pluralize - - -def init_template_globals(app): - from CTFd.constants import JINJA_ENUMS # noqa: I001 - from CTFd.constants.assets import Assets - from CTFd.constants.config import Configs - from CTFd.constants.languages import Languages - from CTFd.constants.plugins import Plugins - from CTFd.constants.sessions import Session - from CTFd.constants.static import Static - from CTFd.constants.teams import Team - from CTFd.constants.users import User - from CTFd.forms import Forms - from CTFd.utils.config.visibility import ( - accounts_visible, - challenges_visible, - registration_visible, - scores_visible, - ) - from CTFd.utils.countries import get_countries, lookup_country_code - from CTFd.utils.countries.geoip import lookup_ip_address, lookup_ip_address_city - - app.jinja_env.globals.update(config=config) - app.jinja_env.globals.update(get_pages=get_pages) - app.jinja_env.globals.update(can_send_mail=can_send_mail) - app.jinja_env.globals.update(get_ctf_name=ctf_name) - app.jinja_env.globals.update(get_ctf_logo=ctf_logo) - app.jinja_env.globals.update(get_ctf_theme=ctf_theme) - app.jinja_env.globals.update(get_menubar_plugins=get_menubar_plugins) - app.jinja_env.globals.update(get_configurable_plugins=get_configurable_plugins) - app.jinja_env.globals.update(get_registered_scripts=get_registered_scripts) - app.jinja_env.globals.update(get_registered_stylesheets=get_registered_stylesheets) - app.jinja_env.globals.update( - get_registered_admin_scripts=get_registered_admin_scripts - ) - app.jinja_env.globals.update( - get_registered_admin_stylesheets=get_registered_admin_stylesheets - ) - app.jinja_env.globals.update(get_config=get_config) - app.jinja_env.globals.update(generate_account_url=generate_account_url) - app.jinja_env.globals.update(get_countries=get_countries) - app.jinja_env.globals.update(lookup_country_code=lookup_country_code) - app.jinja_env.globals.update(lookup_ip_address=lookup_ip_address) - app.jinja_env.globals.update(lookup_ip_address_city=lookup_ip_address_city) - app.jinja_env.globals.update(accounts_visible=accounts_visible) - app.jinja_env.globals.update(challenges_visible=challenges_visible) - app.jinja_env.globals.update(registration_visible=registration_visible) - app.jinja_env.globals.update(scores_visible=scores_visible) - app.jinja_env.globals.update(get_mode_as_word=get_mode_as_word) - app.jinja_env.globals.update(integrations=integrations) - app.jinja_env.globals.update(authed=authed) - app.jinja_env.globals.update(is_admin=is_admin) - app.jinja_env.globals.update(get_current_user_attrs=get_current_user_attrs) - app.jinja_env.globals.update(get_current_team_attrs=get_current_team_attrs) - app.jinja_env.globals.update(get_ip=get_ip) - app.jinja_env.globals.update(get_locale=get_locale) - app.jinja_env.globals.update(Assets=Assets) - app.jinja_env.globals.update(Configs=Configs) - app.jinja_env.globals.update(Plugins=Plugins) - app.jinja_env.globals.update(Session=Session) - app.jinja_env.globals.update(Static=Static) - app.jinja_env.globals.update(Forms=Forms) - app.jinja_env.globals.update(User=User) - app.jinja_env.globals.update(Team=Team) - app.jinja_env.globals.update(Languages=Languages) - - # Add in JinjaEnums - # The reason this exists is that on double import, JinjaEnums are not reinitialized - # Thus, if you try to create two jinja envs (e.g. during testing), sometimes - # an Enum will not be available to Jinja. - # Instead we can just directly grab them from the persisted global dictionary. - for k, v in JINJA_ENUMS.items(): - # .update() can't be used here because it would use the literal value k - app.jinja_env.globals[k] = v - - -def init_logs(app): - logger_submissions = logging.getLogger("submissions") - logger_logins = logging.getLogger("logins") - logger_registrations = logging.getLogger("registrations") - - logger_submissions.setLevel(logging.INFO) - logger_logins.setLevel(logging.INFO) - logger_registrations.setLevel(logging.INFO) - - log_dir = app.config["LOG_FOLDER"] - if not os.path.exists(log_dir): - os.makedirs(log_dir) - - logs = { - "submissions": os.path.join(log_dir, "submissions.log"), - "logins": os.path.join(log_dir, "logins.log"), - "registrations": os.path.join(log_dir, "registrations.log"), - } - - try: - for log in logs.values(): - if not os.path.exists(log): - open(log, "a").close() - - submission_log = logging.handlers.RotatingFileHandler( - logs["submissions"], maxBytes=10485760, backupCount=5 - ) - login_log = logging.handlers.RotatingFileHandler( - logs["logins"], maxBytes=10485760, backupCount=5 - ) - registration_log = logging.handlers.RotatingFileHandler( - logs["registrations"], maxBytes=10485760, backupCount=5 - ) - - logger_submissions.addHandler(submission_log) - logger_logins.addHandler(login_log) - logger_registrations.addHandler(registration_log) - except IOError: - pass - - stdout = logging.StreamHandler(stream=sys.stdout) - - logger_submissions.addHandler(stdout) - logger_logins.addHandler(stdout) - logger_registrations.addHandler(stdout) - - logger_submissions.propagate = 0 - logger_logins.propagate = 0 - logger_registrations.propagate = 0 - - -def init_events(app): - if app.config.get("CACHE_TYPE") in ["redis", "rediss", "redis-cluster", "RedisCache", "RedisSentinelCache", "RedisClusterCache"]: - app.events_manager = RedisEventManager() - print("Using RedisEventManager for event handling") - elif app.config.get("CACHE_TYPE") == "filesystem": - app.events_manager = EventManager() - else: - app.events_manager = EventManager() - app.events_manager.listen() - - -def init_request_processors(app): - @app.url_defaults - def inject_theme(endpoint, values): - if "theme" not in values and app.url_map.is_endpoint_expecting( - endpoint, "theme" - ): - values["theme"] = ctf_theme() - - @app.before_request - def needs_setup(): - if import_in_progress(): - if request.endpoint == "admin.import_ctf": - return - else: - return "Import currently in progress", 403 - if is_setup() is False: - if request.endpoint in ( - "views.setup", - "views.integrations", - "views.themes", - "views.files", - "views.healthcheck", - "views.robots", - ): - return - else: - return redirect(url_for("views.setup")) - - @app.before_request - def tracker(): - if request.endpoint == "views.themes": - return - - if import_in_progress(): - if request.endpoint == "admin.import_ctf": - return - else: - return "Import currently in progress", 403 - - if authed(): - user_ips = get_current_user_recent_ips() - ip = get_ip() - - track = None - if ip not in user_ips or request.method in ( - "POST", - "PATCH", - "DELETE", - ): - track = Tracking.query.filter_by( - ip=get_ip(), user_id=session["id"] - ).first() - - if track: - track.date = datetime.datetime.utcnow() - else: - track = Tracking(ip=get_ip(), user_id=session["id"]) - db.session.add(track) - - if track: - try: - db.session.commit() - except (InvalidRequestError, IntegrityError): - db.session.rollback() - db.session.close() - logout_user() - else: - clear_user_recent_ips(user_id=session["id"]) - - @app.before_request - def banned(): - if request.endpoint == "views.themes": - return - - if authed(): - user = get_current_user_attrs() - team = get_current_team_attrs() - - if user and user.banned: - return ( - render_template( - "errors/403.html", error="You have been banned from this CTF" - ), - 403, - ) - - if team and team.banned: - return ( - render_template( - "errors/403.html", - error="Your team has been banned from this CTF", - ), - 403, - ) - - @app.before_request - def tokens(): - token = request.headers.get("Authorization") - if token and ( - request.mimetype == "application/json" - # Specially allow multipart/form-data for file uploads - or ( - request.endpoint == "api.files_files_list" - and request.method == "POST" - and request.mimetype == "multipart/form-data" - ) - ): - try: - token_type, token = token.split(" ", 1) - user = lookup_user_token(token) - except UserNotFoundException: - abort(401) - except UserTokenExpiredException: - abort(401, description="Your access token has expired") - except Exception: - abort(401) - else: - login_user(user) - - @app.before_request - def csrf(): - try: - func = app.view_functions[request.endpoint] - except KeyError: - abort(404) - if hasattr(func, "_bypass_csrf"): - return - if request.headers.get("Authorization"): - return - if not session.get("nonce"): - session["nonce"] = generate_nonce() - if request.method not in ("GET", "HEAD", "OPTIONS", "TRACE"): - if request.content_type == "application/json": - if session["nonce"] != request.headers.get("CSRF-Token"): - abort(403) - if request.content_type != "application/json": - if session["nonce"] != request.form.get("nonce"): - abort(403) - - @app.after_request - def response_headers(response): - response.headers["Cross-Origin-Opener-Policy"] = get_app_config( - "CROSS_ORIGIN_OPENER_POLICY", default="same-origin-allow-popups" - ) - return response - - application_root = app.config.get("APPLICATION_ROOT") - if application_root != "/": - - @app.before_request - def force_subdirectory_redirect(): - if request.path.startswith(application_root) is False: - return redirect( - application_root + request.script_root + request.full_path - ) - - app.wsgi_app = DispatcherMiddleware(app.wsgi_app, {application_root: app}) diff --git a/Dockerfile b/Dockerfile index a69040c..f608880 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,7 @@ RUN for theme in /tmp/themes/*; do \ done; -FROM ghcr.io/ctfd/ctfd:3.7.7 as ctfd +FROM ghcr.io/ctfd/ctfd:3.8.6 as ctfd # Copy plugins from the repository into the image COPY plugins /tmp/plugins @@ -45,8 +45,5 @@ COPY --from=theme-builder /tmp/themes /opt/CTFd/CTFd/themes # Delete line 30 of /opt/CTFd/CTFd/utils/__init__.py in order to smooth development of themes (cache of assets) ARG DEVELOPMENT=false RUN if [ "$DEVELOPMENT" = "true" ]; then \ - sed -i '30d' /opt/CTFd/CTFd/utils/__init__.py; \ + sed -i '31d' /opt/CTFd/CTFd/utils/__init__.py; \ fi - -# Overwrite /opt/CTFd/CTFd with the contents from CTFd -COPY CTFd /opt/CTFd/CTFd diff --git a/README.md b/README.md index bdc17eb..87c2faa 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ This includes deployment configurations, plugins and other customizations, custo ## CTFd Version -This configuration currently runs on CTFd version `3.7.7`. +This configuration currently runs on CTFd version `3.8.6`. ## Git submodules diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index 5a99269..e1c8afb 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -101,7 +101,7 @@ services: db: cache: - image: redis:4 + image: redis:8 restart: unless-stopped volumes: - .data/redis:/data diff --git a/docker-compose.yml b/docker-compose.yml index 1b91f7f..16fcd87 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -78,7 +78,7 @@ services: ] cache: - image: redis:4 + image: redis:8 restart: always volumes: - .data/redis:/data diff --git a/template/k8s.yml b/template/k8s.yml index 8a1448d..09d69bd 100644 --- a/template/k8s.yml +++ b/template/k8s.yml @@ -96,11 +96,6 @@ spec: secretKeyRef: name: ctfd-redis-connection key: url - - name: CACHE_REDIS_CLUSTER - valueFrom: - secretKeyRef: - name: ctfd-redis-connection - key: cluster # Kube-ctf configuration from secret - name: KUBECTF_HOST valueFrom: diff --git a/themes/core-kubectf/assets/js/challenges.js b/themes/core-kubectf/assets/js/challenges.js index b43896f..ef63f34 100644 --- a/themes/core-kubectf/assets/js/challenges.js +++ b/themes/core-kubectf/assets/js/challenges.js @@ -1,10 +1,10 @@ import Alpine from "alpinejs"; -import dayjs from "dayjs"; import CTFd from "./index"; import { Modal, Tab, Tooltip } from "bootstrap"; import highlight from "./theme/highlight"; +import { intl } from "./theme/times"; function addTargetBlank(html) { let dom = new DOMParser(); @@ -121,7 +121,7 @@ Alpine.data("Challenge", () => ({ async showSolves() { this.solves = await CTFd.pages.challenge.loadSolves(this.id); this.solves.forEach(solve => { - solve.date = dayjs(solve.date).format("MMMM Do, h:mm:ss A"); + solve.date = intl.format(new Date(solve.date)); return solve; }); new Tab(this.$el).show(); @@ -131,7 +131,7 @@ Alpine.data("Challenge", () => ({ let response = await CTFd.pages.users.userSubmissions("me", this.id); this.submissions = response.data; this.submissions.forEach(s => { - s.date = dayjs(s.date).format("MMMM Do, h:mm:ss A"); + s.date = intl.format(new Date(s.date)); return s; }); new Tab(this.$el).show(); @@ -214,6 +214,12 @@ Alpine.data("Challenge", () => ({ this.submission, ); + // Challenges page might be visible to anonymous users, redirect to login on submit + if (this.response.data.status === "authentication_required") { + window.location = `${CTFd.config.urlRoot}/login?next=${CTFd.config.urlRoot}${window.location.pathname}${window.location.hash}`; + return; + } + await this.renderSubmissionResponse(); }, diff --git a/themes/core-kubectf/assets/js/components/language.js b/themes/core-kubectf/assets/js/components/language.js index 64f8001..679cc90 100644 --- a/themes/core-kubectf/assets/js/components/language.js +++ b/themes/core-kubectf/assets/js/components/language.js @@ -5,6 +5,7 @@ Alpine.data("LanguageForm", () => ({ async set(event) { let language = event.target.getAttribute("value"); document.cookie = `language=${language};SameSite=Lax`; + localStorage.setItem("language", language); // Set user language preference if logged in if (CTFd.user.id) { diff --git a/themes/core-kubectf/assets/js/theme/times.js b/themes/core-kubectf/assets/js/theme/times.js index 42867a7..580ac8e 100644 --- a/themes/core-kubectf/assets/js/theme/times.js +++ b/themes/core-kubectf/assets/js/theme/times.js @@ -1,12 +1,25 @@ import dayjs from "dayjs"; import advancedFormat from "dayjs/plugin/advancedFormat"; +// TODO: CTFd 4.0 consider removing dayjs advancedFormat dayjs.extend(advancedFormat); +export const intl = new Intl.DateTimeFormat( + localStorage.getItem("language") || navigator.language, + { + dateStyle: "long", + timeStyle: "short", + }, +); + export default () => { document.querySelectorAll("[data-time]").forEach($el => { const time = $el.getAttribute("data-time"); - const format = $el.getAttribute("data-time-format") || "MMMM Do, h:mm:ss A"; - $el.innerText = dayjs(time).format(format); + const format = $el.getAttribute("data-time-format"); + if (format) { + $el.innerText = dayjs(time).format(format); + } else { + $el.innerText = intl.format(new Date(time)); + } }); }; diff --git a/themes/core-kubectf/package.json b/themes/core-kubectf/package.json index cdcad43..0952581 100644 --- a/themes/core-kubectf/package.json +++ b/themes/core-kubectf/package.json @@ -1,5 +1,5 @@ { - "name": "core-beta", + "name": "core-kubectf", "private": true, "version": "0.0.1", "scripts": { diff --git a/themes/core-kubectf/templates/errors/401.html b/themes/core-kubectf/templates/errors/401.html new file mode 100644 index 0000000..32295c8 --- /dev/null +++ b/themes/core-kubectf/templates/errors/401.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} + +{% block content %} + +
+
+
+
+

{{ error }}

+
+
+

{% trans %}401 Unauthorized{% endtrans %}

+
+
+
+
+ +{% endblock %}