Files
CTFd/CTFd/__init__.py
Kevin Chung 89289ad641 Mark 3.5.1 (#2246)
# 3.5.1 / 2023-01-23

**General**

- The public scoreboard page is no longer shown to users if account visibility is disabled
- Teams created by admins using the normal team creation flow are now hidden by default
- Redirect users to the team creation page if they access a certain pages before the CTF starts
- Added a notice on the Challenges page to remind Admins if they are in Admins Only mode
- Fixed an issue where users couldn't login to their team even though they were already on the team
- Fixed an issue with scoreboard tie breaking when an award results in a tie
- Fixed the order of solves, fails, and awards to always be in chronological ordering (latest first).
- Fixed an issue where certain custom fields could not be submitted

**Admin Panel**

- Improved the rendering of Admin Panel tables on mobile devices
- Clarified the behavior of Score Visibility with respect to Account Visibility in the Admin Panel help text
- Added user id and user email fields to the user mode scoreboard CSV export
- Add CSV export for `teams+members+fields` which is teams with Custom Field entries and their team members with Custom Field entries
- The import process will now catch all exceptions in the import process to report them in the Admin Panel
- Fixed issue where `field_entries` could not be imported under MariaDB
- Fixed issue where `config` entries sometimes would be recreated for some reason causing an import to fail
- Fixed issue with Firefox caching checkboxes by adding `autocomplete='off'` to Admin Panel pages
- Fixed issue where Next selection for a challenge wouldn't always load in Admin Panel

**API**

- Improve response time of `/api/v1/challenges` and `/api/v1/challenges/[challenge_id]/solves` by caching the solve count data for users and challenges
- Add `HEAD /api/v1/notifications` to get a count of notifications that have happened. 
  - This also includes a `since_id` parameter to allow for a notification cursor.
  - Unread notification count can now be tracked by themes that track which notifications a user has read
- Add `since_id` to `GET /api/v1/notifications` to get Notifications that have happened since a specific ID

**Deployment**

- Imports have been disabled when running with a SQLite database backend
  - See https://github.com/CTFd/CTFd/issues/2131
- Added `/healthcheck` endpoint to check if CTFd is ready
- There are now ARM Docker images for OSS CTFd
- Bump dependencies for passlib, bcrypt, requests, gunicorn, gevent, python-geoacumen-city
- Properly load `SAFE_MODE` config from environment variable
- The `AWS_S3_REGION` config has been added to allow specifying an S3 region. The default is `us-east-1`
- Add individual DATABASE config keys as an alternative to `DATABASE_URL`
  - `DATABASE_PROTOCOL`: SQLAlchemy DB protocol (+ driver, optionally)
  - `DATABASE_USER`: Username to access DB server with
  - `DATABASE_PASSWORD`: Password to access DB server with
  - `DATABASE_HOST`: Hostname of the DB server to access
  - `DATABASE_PORT`: Port of the DB server to access
  - `DATABASE_NAME`: Name of the database to use
- Add individual REDIS config keys as an alternative to `REDIS_URL`
  - `REDIS_PROTOCOL`: Protocol to access Redis server with (either redis or rediss)
  - `REDIS_USER`: Username to access Redis server with
  - `REDIS_PASSWORD`: Password to access Redis server with
  - `REDIS_HOST`: Hostname of the Redis server to access
  - `REDIS_PORT`: Port of the Redis server to access
  - `REDIS_DB`: Numeric ID of the database to access

**Plugins**

- Adds support for `config.json` to have multiple paths to add to the Plugins dropdown in the Admin Panel
- Plugins and their migrations now have access to the `get_all_tables` and `get_columns_for_table` functions
- Email sending functions have now been seperated into classes that can be customized via plugins.
  - Add `CTFd.utils.email.providers.EmailProvider`
  - Add `CTFd.utils.email.providers.mailgun.MailgunEmailProvider`
  - Add `CTFd.utils.email.providers.smtp.SMTPEmailProvider`
  - Deprecate `CTFd.utils.email.mailgun.sendmail`
  - Deprecate `CTFd.utils.email.smtp.sendmail`

**Themes**

- The beta interface `Assets.manifest_css` has been removed
- `event-source-polyfill` is now pinned to 1.0.19.
  - See https://github.com/CTFd/CTFd/issues/2159
  - Note that we will not be using this polyfill starting with the `core-beta` theme.
- Add autofocus to text fields on authentication pages
2023-01-23 10:34:49 -05:00

308 lines
11 KiB
Python

import datetime
import os
import sys
import weakref
from distutils.version import StrictVersion
import jinja2
from flask import Flask, Request
from flask.helpers import safe_join
from flask_migrate import upgrade
from jinja2 import FileSystemLoader
from jinja2.sandbox import SandboxedEnvironment
from werkzeug.middleware.proxy_fix import ProxyFix
from werkzeug.utils import cached_property
import CTFd.utils.config
from CTFd import utils
from CTFd.constants.themes import ADMIN_THEME, DEFAULT_THEME
from CTFd.plugins import init_plugins
from CTFd.utils.crypto import sha256
from CTFd.utils.initialization import (
init_events,
init_logs,
init_request_processors,
init_template_filters,
init_template_globals,
)
from CTFd.utils.migrations import create_database, migrations, stamp_latest_revision
from CTFd.utils.sessions import CachingSessionInterface
from CTFd.utils.updates import update_check
__version__ = "3.5.1"
__channel__ = "oss"
class CTFdRequest(Request):
@cached_property
def path(self):
"""
Hijack the original Flask request path because it does not account for subdirectory deployments in an intuitive
manner. We append script_root so that the path always points to the full path as seen in the browser.
e.g. /subdirectory/path/route vs /path/route
:return: string
"""
return self.script_root + super(CTFdRequest, self).path
class CTFdFlask(Flask):
def __init__(self, *args, **kwargs):
"""Overriden Jinja constructor setting a custom jinja_environment"""
self.jinja_environment = SandboxedBaseEnvironment
self.session_interface = CachingSessionInterface(key_prefix="session")
self.request_class = CTFdRequest
# Store server start time
self.start_time = datetime.datetime.utcnow()
# Create generally unique run identifier
self.run_id = sha256(str(self.start_time))[0:8]
Flask.__init__(self, *args, **kwargs)
def create_jinja_environment(self):
"""Overridden jinja environment constructor"""
return super(CTFdFlask, self).create_jinja_environment()
class SandboxedBaseEnvironment(SandboxedEnvironment):
"""SandboxEnvironment that mimics the Flask BaseEnvironment"""
def __init__(self, app, **options):
if "loader" not in options:
options["loader"] = app.create_global_jinja_loader()
SandboxedEnvironment.__init__(self, **options)
self.app = app
def _load_template(self, name, globals):
if self.loader is None:
raise TypeError("no loader for this environment specified")
# Add theme to the LRUCache cache key
cache_name = name
if name.startswith("admin/") is False:
theme = str(utils.get_config("ctf_theme"))
cache_name = theme + "/" + name
# Rest of this code is copied from Jinja
# https://github.com/pallets/jinja/blob/master/src/jinja2/environment.py#L802-L815
cache_key = (weakref.ref(self.loader), cache_name)
if self.cache is not None:
template = self.cache.get(cache_key)
if template is not None and (
not self.auto_reload or template.is_up_to_date
):
return template
template = self.loader.load(self, name, globals)
if self.cache is not None:
self.cache[cache_key] = template
return template
class ThemeLoader(FileSystemLoader):
"""Custom FileSystemLoader that is aware of theme structure and config.
"""
DEFAULT_THEMES_PATH = os.path.join(os.path.dirname(__file__), "themes")
_ADMIN_THEME_PREFIX = ADMIN_THEME + "/"
def __init__(
self,
searchpath=DEFAULT_THEMES_PATH,
theme_name=None,
encoding="utf-8",
followlinks=False,
):
super(ThemeLoader, self).__init__(searchpath, encoding, followlinks)
self.theme_name = theme_name
def get_source(self, environment, template):
# Refuse to load `admin/*` from a loader not for the admin theme
# Because there is a single template loader, themes can essentially
# provide files for other themes. This could end up causing issues if
# an admin theme references a file that doesn't exist that a malicious
# theme provides.
if template.startswith(self._ADMIN_THEME_PREFIX):
if self.theme_name != ADMIN_THEME:
raise jinja2.TemplateNotFound(template)
template = template[len(self._ADMIN_THEME_PREFIX) :]
theme_name = self.theme_name or str(utils.get_config("ctf_theme"))
template = safe_join(theme_name, "templates", template)
return super(ThemeLoader, self).get_source(environment, template)
def confirm_upgrade():
if sys.stdin.isatty():
print("/*\\ CTFd has updated and must update the database! /*\\")
print("/*\\ Please backup your database before proceeding! /*\\")
print("/*\\ CTFd maintainers are not responsible for any data loss! /*\\")
if input("Run database migrations (Y/N)").lower().strip() == "y": # nosec B322
return True
else:
print("/*\\ Ignored database migrations... /*\\")
return False
else:
return True
def run_upgrade():
upgrade()
utils.set_config("ctf_version", __version__)
def create_app(config="CTFd.config.Config"):
app = CTFdFlask(__name__)
with app.app_context():
app.config.from_object(config)
loaders = []
# We provide a `DictLoader` which may be used to override templates
app.overridden_templates = {}
loaders.append(jinja2.DictLoader(app.overridden_templates))
# A `ThemeLoader` with no `theme_name` will load from the current theme
loaders.append(ThemeLoader())
# If `THEME_FALLBACK` is set and true, we add another loader which will
# load from the `DEFAULT_THEME` - this mirrors the order implemented by
# `config.ctf_theme_candidates()`
if bool(app.config.get("THEME_FALLBACK")):
loaders.append(ThemeLoader(theme_name=DEFAULT_THEME))
# All themes including admin can be accessed by prefixing their name
prefix_loader_dict = {ADMIN_THEME: ThemeLoader(theme_name=ADMIN_THEME)}
for theme_name in CTFd.utils.config.get_themes():
prefix_loader_dict[theme_name] = ThemeLoader(theme_name=theme_name)
loaders.append(jinja2.PrefixLoader(prefix_loader_dict))
# Plugin templates are also accessed via prefix but we just point a
# normal `FileSystemLoader` at the plugin tree rather than validating
# each plugin here (that happens later in `init_plugins()`). We
# deliberately don't add this to `prefix_loader_dict` defined above
# because to do so would break template loading from a theme called
# `prefix` (even though that'd be weird).
plugin_loader = jinja2.FileSystemLoader(
searchpath=os.path.join(app.root_path, "plugins"), followlinks=True
)
loaders.append(jinja2.PrefixLoader({"plugins": plugin_loader}))
# Use a choice loader to find the first match from our list of loaders
app.jinja_loader = jinja2.ChoiceLoader(loaders)
from CTFd.models import ( # noqa: F401
db,
Teams,
Solves,
Challenges,
Fails,
Flags,
Tags,
Files,
Tracking,
)
url = create_database()
# This allows any changes to the SQLALCHEMY_DATABASE_URI to get pushed back in
# This is mostly so we can force MySQL's charset
app.config["SQLALCHEMY_DATABASE_URI"] = str(url)
# Register database
db.init_app(app)
# Register Flask-Migrate
migrations.init_app(app, db)
# Alembic sqlite support is lacking so we should just create_all anyway
if url.drivername.startswith("sqlite"):
# Enable foreign keys for SQLite. This must be before the
# db.create_all call because tests use the in-memory SQLite
# database (each connection, including db creation, is a new db).
# https://docs.sqlalchemy.org/en/13/dialects/sqlite.html#foreign-key-support
from sqlalchemy.engine import Engine
from sqlalchemy import event
@event.listens_for(Engine, "connect")
def set_sqlite_pragma(dbapi_connection, connection_record):
cursor = dbapi_connection.cursor()
cursor.execute("PRAGMA foreign_keys=ON")
cursor.close()
db.create_all()
stamp_latest_revision()
else:
# This creates tables instead of db.create_all()
# Allows migrations to happen properly
upgrade()
from CTFd.models import ma
ma.init_app(app)
app.db = db
app.VERSION = __version__
app.CHANNEL = __channel__
from CTFd.cache import cache
cache.init_app(app)
app.cache = cache
reverse_proxy = app.config.get("REVERSE_PROXY")
if reverse_proxy:
if type(reverse_proxy) is str and "," in reverse_proxy:
proxyfix_args = [int(i) for i in reverse_proxy.split(",")]
app.wsgi_app = ProxyFix(app.wsgi_app, *proxyfix_args)
else:
app.wsgi_app = ProxyFix(
app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1, x_prefix=1
)
version = utils.get_config("ctf_version")
# Upgrading from an older version of CTFd
if version and (StrictVersion(version) < StrictVersion(__version__)):
if confirm_upgrade():
run_upgrade()
else:
exit()
if not version:
utils.set_config("ctf_version", __version__)
if not utils.get_config("ctf_theme"):
utils.set_config("ctf_theme", DEFAULT_THEME)
update_check(force=True)
init_request_processors(app)
init_template_filters(app)
init_template_globals(app)
# Importing here allows tests to use sensible names (e.g. api instead of api_bp)
from CTFd.views import views
from CTFd.teams import teams
from CTFd.users import users
from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard
from CTFd.auth import auth
from CTFd.admin import admin
from CTFd.api import api
from CTFd.events import events
from CTFd.errors import render_error
app.register_blueprint(views)
app.register_blueprint(teams)
app.register_blueprint(users)
app.register_blueprint(challenges)
app.register_blueprint(scoreboard)
app.register_blueprint(auth)
app.register_blueprint(api)
app.register_blueprint(events)
app.register_blueprint(admin)
for code in {403, 404, 500, 502}:
app.register_error_handler(code, render_error)
init_logs(app)
init_events(app)
init_plugins(app)
return app