mirror of https://github.com/JohnHammond/CTFd.git
2.0.0 (#741)
* Fix user and admin panel user/team graphs * Closes #682 * Unify login and logout under specific functions * Closes #659 * Rename Challenges.hidden to Challenges.state * Start to clean up API and front end integration starting with profile updating * Slightly cleaner code * Clean API to respond with success, data, and status codes * Simpler COUNTRIES_LIST and update profile to use COUNTRIES_LIST * Lookup country code in users page. Update front end calls to get API data properly * Fix some API endpoints and fix JS to process new responses * Update config.py to support new values * Closes #635 * Update some code to handle user types, add email domain whitelisting * Write a logging wrapper * Use logging wrapper for submissions * Close #656 * Break up config.html to make it easier to maintain * Fix logging, domain_whitelist, and config * Improving views.py, starting to add Announcements * Starting announcements front end * Make it easier to see large images, clean up some more REST API differences * Closes #668 * Update Proxyfix config to REVERSE_PROXY * Add announcements front end * Move creation/edit modals into seperate files. Start moving user updating into their admin profile pages. * Update font-awesome to 5.4.1 * Switch to user-edit icon * Update the update_check function to send up more anonymous data for statistics purposes. * Start work on #640 * Add the user action modals and update API to fix responses * Fix admin teams page * Add challenge requirements * Implement anonymous locked challenges * Team editting from admin panel * Switch from simple cache to filesystem cache * Implements a Cache backed server side session (#658) and fixes Users editting endpoint * Add our messaging for docs * Closes #700 * Remove invalid import * Move challenge enditting around a whole lot and probably break a bunch of things * Show challenge names in prerequisites instead of challenge IDs * Closes #661 * Change user templates to use url_for * Remove extra function * Rewrite admin panel to use url_for * Fix events to work under subdirectories * Start cleaning up config panel * Fix filesystem uploader; deprecate view_challenges_unregistered, view_scoreboard_if_authed, prevent_registration, view_after_ctf; implement new visibility decorators * Remove workshop mode, fix some glitches with the new visibility settings * Fix ctf_logo on core theme * Fix setup errors * Removing default from get_config b/c of memoization issues and getting some tests working * Relax email regex validation rule (#693) * Update to pycodestyle and fix new lint errors * Add a ctf_id to update_check * Change challenge plugin layout. Rename mailgun configs to be more descriptive (Closes #702) * Detect if people try to set routes with '/' to simplify #690 * Closes #690 * Clean up some code * Clean up challenge submit to rate limit * Fix js version compatability issue * Close some TODOs * Hide challenges if not authenticated * Make set_config reset the cache for those config values * Return 404 on empty challenges for /api/v1/<challenge_id>/solves * Fix setting boolean configs * Properly change account config settings * Move datetimes to isoformat (Closes #703) * Remove all .isoformat() calls because it isn't UTC aware (ends in Z). Switch to isoformat function & filter * Make /v1/submissions endpoint work for admin submission creation * Make oauth_id unique for Users and Teams * Move challenge submission endpoint and implement mark solved. Fix some isoformat issues. * Only show team's missing challenges if in team mode * Adding support for Hints & Unlocks * Update challenge submission url * Fix encoding functions in Python3 * Fix hexencode in Python3 * Added functional tests for challenges API for non-admin users (#705) * Set hint default type to be standard * Fix some JS issues. Closes #704 * Implement session.regenerate on top of the CachingSessionInterface * Challenge challenge attempt responses from numbers to strings * Fix password updating for UserSchema * Remove leftover challenge submission code * Remove old migrations :(, resolve challenge requirements not loading correctly, move migration functions * Added functional tests for challenges/hints/admin API (#710) * Fix helpers and re-add JSONLite * Install MySQL 5.7 * Try more mysql * Update password for mysql * Fixing issuse in Users.get_solves * Add new import/export code * Switch to CTFdSerializer for Python 3 * Re-implement import exports and add a very flaky test * Redesign submissions API response * Get export to roundtrip in tests * Int score b/c Decimal is not JSON serializeable * Remove unused route methods * Fix POST /api/v1/configs and start adding admin tests * Add user_id and team_id to top/10 * Fix admin creating Teams * Fix Team website validation * Change admins_only to reply with a 403 if the request is JSON * Organize admin tests and fix authed_only to return 403 on unauthed * Adding check_account_visibility, check_score_visibility for /api/v1/teams/<team_id>/(solves|awards|fails) * Fix teams/me endpoints again * Fix users/me endpoints to return 403 if unauthed * Fix Python 3 config API * Add fetch and promise polyfills. (#712) * Add exec to docker-entrypoint.sh (#713) * Display import_ctf Exceptions via repr (#651) - Wraps exceptions on `/admin/import` returned to users in a `repr()`, making debugging easier. * Add error messages to the admin panel, fix schemas for users, start working on UI for imports/exports * Make unauthed challenge submission attempt return 403 instead of 302, Fix user deletion, fix associated tests, remove TODOs * Remove old means of creating solves * Remove most of the content from teams.js and users.js * Remove extra code from /challenges.js * Fix POST'ing & PATCH'ing pages * Make (users|teams)/fails return only count to users. Fix public score graphs to factor in awards * Fix admin side scoregraphs. Fix Awardschemas for admins * Add requirements to db migration * Adding some team decorators * Fix require_team_mode decorator * Make verified emails decorator return 403 on JSON requests * Redo initial revision * Add SQLiteJSON back * Adding ratelimit to /redirect and removing POST from /oauth * Fix PATCH tags * Actually fix PATCH tags * Simplify 500.html * Added tests for challenges, awards, files, flags, hints ... (#723) * Added tests for challenges, awards, files, flags, hints, notifications, pages, submissions, tags * Fix user data validation functions, Fix hidden challenges and include test * Add a locked state to attempt * OAuth teams get verified, use logging functions in redirect route * Removing extra print call * Update requirements.txt * Fix possible AttributeError * Start work on #716 * Closes #717 * Fix issue patching teams * Rename .j2 to .html, implement preview for challenges if admin * Move admin/challenge.html to admin/challenges/challenge.html * Remove old modals * Add Reset CTF button (#639) * Add Reset link to config.html * Delete Tracking * files handler should return a 404 on files it cant find * Denote official teams (#729), make scoregraph fill to zero * Remove old javascript files, make some challenge elements refresh by reloading * Fix team editting modals to work more reliably * Fix rendering of CTF paused * Remove hide_scores funtion and roll it into scores visibility * Log to stdout/stderr by default (#719) * Fix user searching * Remove searching for users/teams by country * Add badges to admin team and user pages, implement user banning (#643) * Remove shell.py, clean up admin team.html, add tests for banned users, teams * Start cleaning up dynamic_challenges to meet new challenge type plugin format * Remove POST method from teams.public * Add credentials: 'same-origin' to all fetch calls (#734) * Add challenge preview, add challenge deletion, fix file deletions when deleting challenges * Fix imports UI (#735) * Show prerequisites before adding a blank one (#738), Refresh all challenges after a submission (#739) * Admins can see hidden challenges * Fix some UI elements, fix loading location hash, set version to be 2.0.0 * Clean up some challenge plugin pages * Add default for flag type * Fix Python3 bytes/str issues * Add in MLC urls and support user mode for oauth * Fix seeing user graphs when scores are hidden, clean up setup.html, add links to MLC oauth * Add state parameter support * Use URLSafeTimedSerializer wrapper for sending token based emails * setting APPLICATION_ROOT from env var (#732) * Rearrange config.py and update README * Updating READMEselenium-screenshot-testing
parent
41933cc367
commit
c8031b38c2
15
.travis.yml
15
.travis.yml
|
@ -2,19 +2,30 @@ language: python
|
|||
services:
|
||||
- mysql
|
||||
- postgresql
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- mysql-5.7-trusty
|
||||
packages:
|
||||
- mysql-server
|
||||
- mysql-client
|
||||
env:
|
||||
- TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd'
|
||||
- TESTING_DATABASE_URL='mysql+pymysql://root:password@localhost/ctfd'
|
||||
- TESTING_DATABASE_URL='sqlite://'
|
||||
- TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd'
|
||||
python:
|
||||
- 2.7
|
||||
- 3.6
|
||||
before_install:
|
||||
- sudo mysql -e "use mysql; update user set authentication_string=PASSWORD('password') where User='root'; update user set plugin='mysql_native_password';FLUSH PRIVILEGES;"
|
||||
- sudo mysql_upgrade -u root -ppassword
|
||||
- sudo service mysql restart
|
||||
install:
|
||||
- pip install -r development.txt
|
||||
before_script:
|
||||
- psql -c 'create database ctfd;' -U postgres
|
||||
script:
|
||||
- pep8 --ignore E501,E712 CTFd/ tests/
|
||||
- pycodestyle --ignore E501,E712,E402 CTFd/ tests/
|
||||
- nosetests -v -d --with-randomly
|
||||
after_success:
|
||||
- codecov
|
||||
|
|
|
@ -3,27 +3,35 @@ import os
|
|||
|
||||
from distutils.version import StrictVersion
|
||||
from flask import Flask
|
||||
from werkzeug.contrib.fixers import ProxyFix
|
||||
from jinja2 import FileSystemLoader
|
||||
from jinja2.sandbox import SandboxedEnvironment
|
||||
from sqlalchemy.engine.url import make_url
|
||||
from sqlalchemy_utils import database_exists, create_database
|
||||
from six.moves import input
|
||||
|
||||
from CTFd.utils import cache, migrate, migrate_upgrade, migrate_stamp, update_check
|
||||
from CTFd import utils
|
||||
from CTFd.utils.migrations import migrations, migrate, upgrade, stamp, create_database
|
||||
from CTFd.utils.sessions import CachingSessionInterface
|
||||
from CTFd.utils.updates import update_check
|
||||
from CTFd.utils.initialization import init_request_processors, init_template_filters, init_template_globals
|
||||
from CTFd.utils.events import socketio
|
||||
from CTFd.plugins import init_plugins
|
||||
|
||||
# Hack to support Unicode in Python 2 properly
|
||||
if sys.version_info[0] < 3:
|
||||
reload(sys)
|
||||
sys.setdefaultencoding("utf-8")
|
||||
|
||||
__version__ = '1.2.0'
|
||||
__version__ = '2.0.0'
|
||||
|
||||
|
||||
class CTFdFlask(Flask):
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Overriden Jinja constructor setting a custom jinja_environment"""
|
||||
self.jinja_environment = SandboxedBaseEnvironment
|
||||
self.jinja_environment.cache = None
|
||||
self.session_interface = CachingSessionInterface(key_prefix='session')
|
||||
Flask.__init__(self, *args, **kwargs)
|
||||
|
||||
def create_jinja_environment(self):
|
||||
|
@ -78,7 +86,7 @@ def confirm_upgrade():
|
|||
|
||||
|
||||
def run_upgrade():
|
||||
migrate_upgrade()
|
||||
upgrade()
|
||||
utils.set_config('ctf_version', __version__)
|
||||
|
||||
|
||||
|
@ -90,21 +98,9 @@ def create_app(config='CTFd.config.Config'):
|
|||
theme_loader = ThemeLoader(os.path.join(app.root_path, 'themes'), followlinks=True)
|
||||
app.jinja_loader = theme_loader
|
||||
|
||||
from CTFd.models import db, Teams, Solves, Challenges, WrongKeys, Keys, Tags, Files, Tracking
|
||||
from CTFd.models import db, Teams, Solves, Challenges, Fails, Flags, Tags, Files, Tracking
|
||||
|
||||
url = make_url(app.config['SQLALCHEMY_DATABASE_URI'])
|
||||
if url.drivername == 'postgres':
|
||||
url.drivername = 'postgresql'
|
||||
|
||||
if url.drivername.startswith('mysql'):
|
||||
url.query['charset'] = 'utf8mb4'
|
||||
|
||||
# Creates database if the database database does not exist
|
||||
if not database_exists(url):
|
||||
if url.drivername.startswith('mysql'):
|
||||
create_database(url, encoding='utf8mb4')
|
||||
else:
|
||||
create_database(url)
|
||||
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
|
||||
|
@ -114,32 +110,37 @@ def create_app(config='CTFd.config.Config'):
|
|||
db.init_app(app)
|
||||
|
||||
# Register Flask-Migrate
|
||||
migrate.init_app(app, db)
|
||||
migrations.init_app(app, db)
|
||||
|
||||
# Alembic sqlite support is lacking so we should just create_all anyway
|
||||
if url.drivername.startswith('sqlite'):
|
||||
db.create_all()
|
||||
stamp()
|
||||
else:
|
||||
if len(db.engine.table_names()) == 0:
|
||||
# This creates tables instead of db.create_all()
|
||||
# Allows migrations to happen properly
|
||||
migrate_upgrade()
|
||||
elif 'alembic_version' not in db.engine.table_names():
|
||||
# There is no alembic_version because CTFd is from before it had migrations
|
||||
# Stamp it to the base migration
|
||||
if confirm_upgrade():
|
||||
migrate_stamp(revision='cb3cfcc47e2f')
|
||||
run_upgrade()
|
||||
else:
|
||||
exit()
|
||||
# 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__
|
||||
|
||||
from CTFd.cache import cache
|
||||
|
||||
cache.init_app(app)
|
||||
app.cache = cache
|
||||
|
||||
update_check(force=True)
|
||||
# If you have multiple workers you must have a shared cache
|
||||
socketio.init_app(
|
||||
app,
|
||||
message_queue=app.config.get('CACHE_REDIS_URL')
|
||||
)
|
||||
|
||||
if app.config.get('REVERSE_PROXY'):
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app)
|
||||
|
||||
version = utils.get_config('ctf_version')
|
||||
|
||||
|
@ -156,31 +157,39 @@ def create_app(config='CTFd.config.Config'):
|
|||
if not utils.get_config('ctf_theme'):
|
||||
utils.set_config('ctf_theme', 'core')
|
||||
|
||||
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, admin_statistics, admin_challenges, admin_pages, admin_scoreboard, admin_keys, admin_teams
|
||||
from CTFd.utils import init_utils, init_errors, init_logs
|
||||
|
||||
init_utils(app)
|
||||
init_errors(app)
|
||||
init_logs(app)
|
||||
from CTFd.admin import admin
|
||||
from CTFd.api import api
|
||||
from CTFd.events import events
|
||||
from CTFd.errors import page_not_found, forbidden, general_error, gateway_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)
|
||||
app.register_blueprint(admin_statistics)
|
||||
app.register_blueprint(admin_challenges)
|
||||
app.register_blueprint(admin_teams)
|
||||
app.register_blueprint(admin_scoreboard)
|
||||
app.register_blueprint(admin_keys)
|
||||
app.register_blueprint(admin_pages)
|
||||
|
||||
from CTFd.plugins import init_plugins
|
||||
app.register_error_handler(404, page_not_found)
|
||||
app.register_error_handler(403, forbidden)
|
||||
app.register_error_handler(500, general_error)
|
||||
app.register_error_handler(502, gateway_error)
|
||||
|
||||
init_plugins(app)
|
||||
|
||||
|
|
|
@ -1,42 +1,74 @@
|
|||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from flask import (
|
||||
current_app as app,
|
||||
render_template,
|
||||
request,
|
||||
redirect,
|
||||
url_for,
|
||||
Blueprint,
|
||||
abort,
|
||||
render_template_string,
|
||||
send_file
|
||||
)
|
||||
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.user import is_admin
|
||||
from CTFd.utils.security.auth import logout_user
|
||||
from CTFd.utils.config import is_setup
|
||||
from CTFd.utils import (
|
||||
config as ctf_config,
|
||||
validators,
|
||||
uploads,
|
||||
user as current_user,
|
||||
get_config,
|
||||
get_app_config,
|
||||
set_config,
|
||||
)
|
||||
from CTFd.cache import cache, clear_config
|
||||
from CTFd.utils.exports import (
|
||||
export_ctf as export_ctf_util,
|
||||
import_ctf as import_ctf_util
|
||||
)
|
||||
from CTFd.models import (
|
||||
db,
|
||||
get_class_by_tablename,
|
||||
Users,
|
||||
Teams,
|
||||
Configs,
|
||||
Submissions,
|
||||
Solves,
|
||||
Awards,
|
||||
Unlocks,
|
||||
Tracking
|
||||
)
|
||||
import datetime
|
||||
import os
|
||||
import six
|
||||
import csv
|
||||
|
||||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint, \
|
||||
abort, render_template_string, send_file
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from sqlalchemy.sql import not_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
from CTFd.utils import admins_only, is_admin, cache, export_ctf, import_ctf
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, \
|
||||
DatabaseError
|
||||
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
||||
|
||||
from CTFd.admin.statistics import admin_statistics
|
||||
from CTFd.admin.challenges import admin_challenges
|
||||
from CTFd.admin.scoreboard import admin_scoreboard
|
||||
from CTFd.admin.pages import admin_pages
|
||||
from CTFd.admin.keys import admin_keys
|
||||
from CTFd.admin.teams import admin_teams
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
admin = Blueprint('admin', __name__)
|
||||
|
||||
|
||||
@admin.route('/admin', methods=['GET'])
|
||||
def admin_view():
|
||||
if is_admin():
|
||||
return redirect(url_for('admin_statistics.admin_stats'))
|
||||
from CTFd.admin import challenges
|
||||
from CTFd.admin import pages
|
||||
from CTFd.admin import scoreboard
|
||||
from CTFd.admin import statistics
|
||||
from CTFd.admin import teams
|
||||
from CTFd.admin import users
|
||||
from CTFd.admin import submissions
|
||||
from CTFd.admin import notifications
|
||||
|
||||
|
||||
@admin.route('/admin', methods=['GET'])
|
||||
def view():
|
||||
if is_admin():
|
||||
return redirect(url_for('admin.statistics'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
|
||||
@admin.route('/admin/plugins/<plugin>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_plugin_config(plugin):
|
||||
def plugin(plugin):
|
||||
if request.method == 'GET':
|
||||
plugins_path = os.path.join(app.root_path, 'plugins')
|
||||
|
||||
|
@ -44,210 +76,116 @@ def admin_plugin_config(plugin):
|
|||
if os.path.isfile(os.path.join(plugins_path, name, 'config.html'))]
|
||||
|
||||
if plugin in config_html_plugins:
|
||||
config = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
|
||||
return render_template_string(config)
|
||||
config_html = open(os.path.join(app.root_path, 'plugins', plugin, 'config.html')).read()
|
||||
return render_template_string(config_html)
|
||||
abort(404)
|
||||
elif request.method == 'POST':
|
||||
for k, v in request.form.items():
|
||||
if k == "nonce":
|
||||
continue
|
||||
utils.set_config(k, v)
|
||||
set_config(k, v)
|
||||
with app.app_context():
|
||||
cache.clear()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin.route('/admin/import', methods=['GET', 'POST'])
|
||||
@admin.route('/admin/import', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_import_ctf():
|
||||
def import_ctf():
|
||||
backup = request.files['backup']
|
||||
segments = request.form.get('segments')
|
||||
errors = []
|
||||
try:
|
||||
if segments:
|
||||
import_ctf(backup, segments=segments.split(','))
|
||||
else:
|
||||
import_ctf(backup)
|
||||
import_ctf_util(backup)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
errors.append(type(e).__name__)
|
||||
errors.append(repr(e))
|
||||
|
||||
if errors:
|
||||
return errors[0], 500
|
||||
else:
|
||||
return redirect(url_for('admin.admin_config'))
|
||||
return redirect(url_for('admin.config'))
|
||||
|
||||
|
||||
@admin.route('/admin/export', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_export_ctf():
|
||||
segments = request.args.get('segments')
|
||||
if segments:
|
||||
backup = export_ctf(segments.split(','))
|
||||
else:
|
||||
backup = export_ctf()
|
||||
ctf_name = utils.ctf_name()
|
||||
def export_ctf():
|
||||
backup = export_ctf_util()
|
||||
ctf_name = ctf_config.ctf_name()
|
||||
day = datetime.datetime.now().strftime("%Y-%m-%d")
|
||||
full_name = "{}.{}.zip".format(ctf_name, day)
|
||||
full_name = u"{}.{}.zip".format(ctf_name, day)
|
||||
return send_file(backup, as_attachment=True, attachment_filename=full_name)
|
||||
|
||||
|
||||
@admin.route('/admin/export/csv')
|
||||
@admins_only
|
||||
def export_csv():
|
||||
table = request.args.get('table')
|
||||
|
||||
# TODO: It might make sense to limit dumpable tables. Config could potentially leak sensitive information.
|
||||
model = get_class_by_tablename(table)
|
||||
if model is None:
|
||||
abort(404)
|
||||
|
||||
output = six.StringIO()
|
||||
writer = csv.writer(output)
|
||||
|
||||
header = [column.name for column in model.__mapper__.columns]
|
||||
writer.writerow(header)
|
||||
|
||||
responses = model.query.all()
|
||||
|
||||
for curr in responses:
|
||||
writer.writerow([getattr(curr, column.name) for column in model.__mapper__.columns])
|
||||
|
||||
output.seek(0)
|
||||
return send_file(
|
||||
output,
|
||||
as_attachment=True,
|
||||
cache_timeout=-1,
|
||||
attachment_filename="{name}-{table}.csv".format(name=ctf_config.ctf_name(), table=table)
|
||||
)
|
||||
|
||||
|
||||
@admin.route('/admin/config', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_config():
|
||||
if request.method == "POST":
|
||||
start = None
|
||||
end = None
|
||||
freeze = None
|
||||
if request.form.get('start'):
|
||||
start = int(request.form['start'])
|
||||
if request.form.get('end'):
|
||||
end = int(request.form['end'])
|
||||
if request.form.get('freeze'):
|
||||
freeze = int(request.form['freeze'])
|
||||
def config():
|
||||
# Clear the config cache so that we don't get stale values
|
||||
clear_config()
|
||||
|
||||
try:
|
||||
# Set checkbox config values
|
||||
view_challenges_unregistered = 'view_challenges_unregistered' in request.form
|
||||
view_scoreboard_if_authed = 'view_scoreboard_if_authed' in request.form
|
||||
hide_scores = 'hide_scores' in request.form
|
||||
prevent_registration = 'prevent_registration' in request.form
|
||||
prevent_name_change = 'prevent_name_change' in request.form
|
||||
view_after_ctf = 'view_after_ctf' in request.form
|
||||
verify_emails = 'verify_emails' in request.form
|
||||
mail_tls = 'mail_tls' in request.form
|
||||
mail_ssl = 'mail_ssl' in request.form
|
||||
mail_useauth = 'mail_useauth' in request.form
|
||||
workshop_mode = 'workshop_mode' in request.form
|
||||
paused = 'paused' in request.form
|
||||
finally:
|
||||
utils.set_config('view_challenges_unregistered', view_challenges_unregistered)
|
||||
utils.set_config('view_scoreboard_if_authed', view_scoreboard_if_authed)
|
||||
utils.set_config('hide_scores', hide_scores)
|
||||
utils.set_config('prevent_registration', prevent_registration)
|
||||
utils.set_config('prevent_name_change', prevent_name_change)
|
||||
utils.set_config('view_after_ctf', view_after_ctf)
|
||||
utils.set_config('verify_emails', verify_emails)
|
||||
utils.set_config('mail_tls', mail_tls)
|
||||
utils.set_config('mail_ssl', mail_ssl)
|
||||
utils.set_config('mail_useauth', mail_useauth)
|
||||
utils.set_config('workshop_mode', workshop_mode)
|
||||
utils.set_config('paused', paused)
|
||||
database_tables = sorted(db.metadata.tables.keys())
|
||||
|
||||
utils.set_config("mail_server", request.form.get('mail_server', None))
|
||||
utils.set_config("mail_port", request.form.get('mail_port', None))
|
||||
configs = Configs.query.all()
|
||||
configs = dict([(c.key, get_config(c.key)) for c in configs])
|
||||
|
||||
if request.form.get('mail_useauth', None) and (request.form.get('mail_u', None) or request.form.get('mail_p', None)):
|
||||
if len(request.form.get('mail_u')) > 0:
|
||||
utils.set_config("mail_username", request.form.get('mail_u', None))
|
||||
if len(request.form.get('mail_p')) > 0:
|
||||
utils.set_config("mail_password", request.form.get('mail_p', None))
|
||||
|
||||
elif request.form.get('mail_useauth', None) is None:
|
||||
utils.set_config("mail_username", None)
|
||||
utils.set_config("mail_password", None)
|
||||
|
||||
if request.files.get('ctf_logo_file', None):
|
||||
ctf_logo = request.files['ctf_logo_file']
|
||||
file_id, file_loc = utils.upload_file(ctf_logo, None)
|
||||
utils.set_config("ctf_logo", file_loc)
|
||||
elif request.form.get('ctf_logo') == '':
|
||||
utils.set_config("ctf_logo", None)
|
||||
|
||||
utils.set_config("ctf_name", request.form.get('ctf_name', None))
|
||||
utils.set_config("ctf_theme", request.form.get('ctf_theme', None))
|
||||
utils.set_config('css', request.form.get('css', None))
|
||||
|
||||
utils.set_config("mailfrom_addr", request.form.get('mailfrom_addr', None))
|
||||
utils.set_config("mg_base_url", request.form.get('mg_base_url', None))
|
||||
utils.set_config("mg_api_key", request.form.get('mg_api_key', None))
|
||||
|
||||
utils.set_config("freeze", freeze)
|
||||
|
||||
db_start = Config.query.filter_by(key='start').first()
|
||||
db_start.value = start
|
||||
|
||||
db_end = Config.query.filter_by(key='end').first()
|
||||
db_end.value = end
|
||||
|
||||
db.session.add(db_start)
|
||||
db.session.add(db_end)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
with app.app_context():
|
||||
cache.clear()
|
||||
return redirect(url_for('admin.admin_config'))
|
||||
|
||||
# Clear the cache so that we don't get stale values
|
||||
cache.clear()
|
||||
|
||||
ctf_name = utils.get_config('ctf_name')
|
||||
ctf_logo = utils.get_config('ctf_logo')
|
||||
ctf_theme = utils.get_config('ctf_theme')
|
||||
hide_scores = utils.get_config('hide_scores')
|
||||
css = utils.get_config('css')
|
||||
|
||||
mail_server = utils.get_config('mail_server')
|
||||
mail_port = utils.get_config('mail_port')
|
||||
mail_username = utils.get_config('mail_username')
|
||||
mail_password = utils.get_config('mail_password')
|
||||
|
||||
mailfrom_addr = utils.get_config('mailfrom_addr')
|
||||
mg_api_key = utils.get_config('mg_api_key')
|
||||
mg_base_url = utils.get_config('mg_base_url')
|
||||
|
||||
view_after_ctf = utils.get_config('view_after_ctf')
|
||||
start = utils.get_config('start')
|
||||
end = utils.get_config('end')
|
||||
freeze = utils.get_config('freeze')
|
||||
|
||||
mail_tls = utils.get_config('mail_tls')
|
||||
mail_ssl = utils.get_config('mail_ssl')
|
||||
mail_useauth = utils.get_config('mail_useauth')
|
||||
|
||||
view_challenges_unregistered = utils.get_config('view_challenges_unregistered')
|
||||
view_scoreboard_if_authed = utils.get_config('view_scoreboard_if_authed')
|
||||
prevent_registration = utils.get_config('prevent_registration')
|
||||
prevent_name_change = utils.get_config('prevent_name_change')
|
||||
verify_emails = utils.get_config('verify_emails')
|
||||
|
||||
workshop_mode = utils.get_config('workshop_mode')
|
||||
paused = utils.get_config('paused')
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
themes = utils.get_themes()
|
||||
themes.remove(ctf_theme)
|
||||
themes = ctf_config.get_themes()
|
||||
themes.remove(get_config('ctf_theme'))
|
||||
|
||||
return render_template(
|
||||
'admin/config.html',
|
||||
ctf_name=ctf_name,
|
||||
ctf_logo=ctf_logo,
|
||||
ctf_theme_config=ctf_theme,
|
||||
css=css,
|
||||
start=start,
|
||||
end=end,
|
||||
freeze=freeze,
|
||||
hide_scores=hide_scores,
|
||||
mail_server=mail_server,
|
||||
mail_port=mail_port,
|
||||
mail_useauth=mail_useauth,
|
||||
mail_username=mail_username,
|
||||
mail_password=mail_password,
|
||||
mail_tls=mail_tls,
|
||||
mail_ssl=mail_ssl,
|
||||
view_challenges_unregistered=view_challenges_unregistered,
|
||||
view_scoreboard_if_authed=view_scoreboard_if_authed,
|
||||
prevent_registration=prevent_registration,
|
||||
mailfrom_addr=mailfrom_addr,
|
||||
mg_base_url=mg_base_url,
|
||||
mg_api_key=mg_api_key,
|
||||
prevent_name_change=prevent_name_change,
|
||||
verify_emails=verify_emails,
|
||||
view_after_ctf=view_after_ctf,
|
||||
database_tables=database_tables,
|
||||
themes=themes,
|
||||
workshop_mode=workshop_mode,
|
||||
paused=paused
|
||||
**configs
|
||||
)
|
||||
|
||||
|
||||
@admin.route('/admin/reset', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def reset():
|
||||
if request.method == 'POST':
|
||||
# Truncate Users, Teams, Submissions, Solves, Notifications, Awards, Unlocks, Tracking
|
||||
Tracking.query.delete()
|
||||
Users.query.delete()
|
||||
Teams.query.delete()
|
||||
Submissions.query.delete()
|
||||
Solves.query.delete()
|
||||
Awards.query.delete()
|
||||
Unlocks.query.delete()
|
||||
set_config('setup', False)
|
||||
db.session.commit()
|
||||
cache.clear()
|
||||
logout_user()
|
||||
db.session.close()
|
||||
return redirect(url_for('views.setup'))
|
||||
|
||||
return render_template('admin/reset.html')
|
||||
|
|
|
@ -1,302 +1,50 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, Hints, Unlocks, DatabaseError
|
||||
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
||||
from flask import current_app as app, render_template, request, redirect, jsonify, render_template_string
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs, Hints, Unlocks
|
||||
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
|
||||
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
from CTFd.admin import admin
|
||||
from CTFd.utils import config, validators, uploads
|
||||
import os
|
||||
|
||||
admin_challenges = Blueprint('admin_challenges', __name__)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal_types', methods=['GET'])
|
||||
@admin.route('/admin/challenges')
|
||||
@admins_only
|
||||
def admin_chal_types():
|
||||
data = {}
|
||||
for class_id in CHALLENGE_CLASSES:
|
||||
challenge_class = CHALLENGE_CLASSES.get(class_id)
|
||||
data[challenge_class.id] = {
|
||||
'id': challenge_class.id,
|
||||
'name': challenge_class.name,
|
||||
'templates': challenge_class.templates,
|
||||
'scripts': challenge_class.scripts,
|
||||
}
|
||||
|
||||
return jsonify(data)
|
||||
def challenges_listing():
|
||||
challenges = Challenges.query.all()
|
||||
return render_template('admin/challenges/challenges.html', challenges=challenges)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chals', methods=['POST', 'GET'])
|
||||
@admin.route('/admin/challenges/<int:challenge_id>')
|
||||
@admins_only
|
||||
def admin_chals():
|
||||
if request.method == 'POST':
|
||||
chals = Challenges.query.order_by(Challenges.value).all()
|
||||
def challenges_detail(challenge_id):
|
||||
challenges = dict(Challenges.query.with_entities(Challenges.id, Challenges.name).all())
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
solves = Solves.query.filter_by(challenge_id=challenge.id).all()
|
||||
flags = Flags.query.filter_by(challenge_id=challenge.id).all()
|
||||
challenge_class = get_chal_class(challenge.type)
|
||||
|
||||
json_data = {'game': []}
|
||||
for chal in chals:
|
||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
|
||||
hints = []
|
||||
for hint in Hints.query.filter_by(chal=chal.id).all():
|
||||
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
|
||||
|
||||
type_class = CHALLENGE_CLASSES.get(chal.type)
|
||||
type_name = type_class.name if type_class else None
|
||||
|
||||
json_data['game'].append({
|
||||
'id': chal.id,
|
||||
'name': chal.name,
|
||||
'value': chal.value,
|
||||
'description': chal.description,
|
||||
'category': chal.category,
|
||||
'files': files,
|
||||
'tags': tags,
|
||||
'hints': hints,
|
||||
'hidden': chal.hidden,
|
||||
'max_attempts': chal.max_attempts,
|
||||
'type': chal.type,
|
||||
'type_name': type_name,
|
||||
'type_data': {
|
||||
'id': type_class.id,
|
||||
'name': type_class.name,
|
||||
'templates': type_class.templates,
|
||||
'scripts': type_class.scripts,
|
||||
}
|
||||
})
|
||||
|
||||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
else:
|
||||
challenges = Challenges.query.all()
|
||||
return render_template('admin/challenges.html', challenges=challenges)
|
||||
static_path = os.path.basename(challenge_class.blueprint.static_url_path)
|
||||
update_j2 = render_template_string(
|
||||
challenge_class.blueprint.open_resource(
|
||||
os.path.join(static_path, 'update.html')
|
||||
).read().decode('utf-8'),
|
||||
# Python 3
|
||||
challenge=challenge
|
||||
)
|
||||
update_script = os.path.join(challenge_class.route, 'update.js')
|
||||
return render_template(
|
||||
'admin/challenges/challenge.html',
|
||||
update_template=update_j2,
|
||||
update_script=update_script,
|
||||
challenge=challenge,
|
||||
challenges=challenges,
|
||||
solves=solves,
|
||||
flags=flags
|
||||
)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admin.route('/admin/challenges/new')
|
||||
@admins_only
|
||||
def admin_chal_detail(chalid):
|
||||
chal = Challenges.query.filter_by(id=chalid).first_or_404()
|
||||
chal_class = get_chal_class(chal.type)
|
||||
|
||||
if request.method == 'POST':
|
||||
status, message = chal_class.attempt(chal, request)
|
||||
if status:
|
||||
return jsonify({'status': 1, 'message': message})
|
||||
else:
|
||||
return jsonify({'status': 0, 'message': message})
|
||||
elif request.method == 'GET':
|
||||
obj, data = chal_class.read(chal)
|
||||
|
||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
|
||||
hints = []
|
||||
for hint in Hints.query.filter_by(chal=chal.id).all():
|
||||
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
|
||||
|
||||
data['tags'] = tags
|
||||
data['files'] = files
|
||||
data['hints'] = hints
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/<int:chalid>/solves', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_chal_solves(chalid):
|
||||
response = {'teams': []}
|
||||
if utils.hide_scores():
|
||||
return jsonify(response)
|
||||
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid).order_by(
|
||||
Solves.date.asc())
|
||||
for solve in solves:
|
||||
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_tags(chalid):
|
||||
if request.method == 'GET':
|
||||
tags = Tags.query.filter_by(chal=chalid).all()
|
||||
json_data = {'tags': []}
|
||||
for x in tags:
|
||||
json_data['tags'].append({'id': x.id, 'chal': x.chal, 'tag': x.tag})
|
||||
return jsonify(json_data)
|
||||
|
||||
elif request.method == 'POST':
|
||||
newtags = request.form.getlist('tags[]')
|
||||
for x in newtags:
|
||||
tag = Tags(chalid, x)
|
||||
db.session.add(tag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_delete_tags(tagid):
|
||||
if request.method == 'POST':
|
||||
tag = Tags.query.filter_by(id=tagid).first_or_404()
|
||||
db.session.delete(tag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/hints', defaults={'hintid': None}, methods=['POST', 'GET'])
|
||||
@admin_challenges.route('/admin/hints/<int:hintid>', methods=['GET', 'POST', 'DELETE'])
|
||||
@admins_only
|
||||
def admin_hints(hintid):
|
||||
if hintid:
|
||||
hint = Hints.query.filter_by(id=hintid).first_or_404()
|
||||
|
||||
if request.method == 'POST':
|
||||
hint.hint = request.form.get('hint')
|
||||
hint.chal = int(request.form.get('chal'))
|
||||
hint.cost = int(request.form.get('cost') or 0)
|
||||
db.session.commit()
|
||||
|
||||
elif request.method == 'DELETE':
|
||||
db.session.delete(hint)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return ('', 204)
|
||||
|
||||
json_data = {
|
||||
'hint': hint.hint,
|
||||
'type': hint.type,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost,
|
||||
'id': hint.id
|
||||
}
|
||||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
else:
|
||||
if request.method == 'GET':
|
||||
hints = Hints.query.all()
|
||||
json_data = []
|
||||
for hint in hints:
|
||||
json_data.append({
|
||||
'hint': hint.hint,
|
||||
'type': hint.type,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost,
|
||||
'id': hint.id
|
||||
})
|
||||
return jsonify({'results': json_data})
|
||||
elif request.method == 'POST':
|
||||
hint = request.form.get('hint')
|
||||
chalid = int(request.form.get('chal'))
|
||||
cost = int(request.form.get('cost') or 0)
|
||||
hint_type = request.form.get('type', 0)
|
||||
hint = Hints(chal=chalid, hint=hint, cost=cost)
|
||||
db.session.add(hint)
|
||||
db.session.commit()
|
||||
json_data = {
|
||||
'hint': hint.hint,
|
||||
'type': hint.type,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost,
|
||||
'id': hint.id
|
||||
}
|
||||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_files(chalid):
|
||||
if request.method == 'GET':
|
||||
files = Files.query.filter_by(chal=chalid).all()
|
||||
json_data = {'files': []}
|
||||
for x in files:
|
||||
json_data['files'].append({'id': x.id, 'file': x.location})
|
||||
return jsonify(json_data)
|
||||
if request.method == 'POST':
|
||||
if request.form['method'] == "delete":
|
||||
utils.delete_file(request.form['file'])
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
elif request.form['method'] == "upload":
|
||||
files = request.files.getlist('files[]')
|
||||
|
||||
for f in files:
|
||||
utils.upload_file(file=f, chalid=chalid)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/<int:chalid>/<prop>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_get_values(chalid, prop):
|
||||
challenge = Challenges.query.filter_by(id=chalid).first_or_404()
|
||||
if prop == 'keys':
|
||||
chal_keys = Keys.query.filter_by(chal=challenge.id).all()
|
||||
json_data = {'keys': []}
|
||||
for x in chal_keys:
|
||||
key_class = get_key_class(x.type)
|
||||
json_data['keys'].append({
|
||||
'id': x.id,
|
||||
'key': x.flag,
|
||||
'type': x.type,
|
||||
'type_name': key_class.name,
|
||||
'templates': key_class.templates,
|
||||
})
|
||||
return jsonify(json_data)
|
||||
elif prop == 'tags':
|
||||
tags = Tags.query.filter_by(chal=chalid).all()
|
||||
json_data = {'tags': []}
|
||||
for x in tags:
|
||||
json_data['tags'].append({
|
||||
'id': x.id,
|
||||
'chal': x.chal,
|
||||
'tag': x.tag
|
||||
})
|
||||
return jsonify(json_data)
|
||||
elif prop == 'hints':
|
||||
hints = Hints.query.filter_by(chal=chalid)
|
||||
json_data = {'hints': []}
|
||||
for hint in hints:
|
||||
json_data['hints'].append({
|
||||
'hint': hint.hint,
|
||||
'type': hint.type,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost,
|
||||
'id': hint.id
|
||||
})
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/new', methods=['GET', 'POST'])
|
||||
@admins_only
|
||||
def admin_create_chal():
|
||||
if request.method == 'POST':
|
||||
chal_type = request.form['chaltype']
|
||||
chal_class = get_chal_class(chal_type)
|
||||
chal_class.create(request)
|
||||
return redirect(url_for('admin_challenges.admin_chals'))
|
||||
else:
|
||||
return render_template('admin/chals/create.html')
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_delete_chal():
|
||||
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
chal_class.delete(challenge)
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_challenges.route('/admin/chal/update', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_update_chal():
|
||||
challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
chal_class.update(challenge, request)
|
||||
return redirect(url_for('admin_challenges.admin_chals'))
|
||||
def challenges_new():
|
||||
return render_template('admin/challenges/new.html')
|
||||
|
|
|
@ -1,77 +0,0 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from CTFd.plugins.keys import get_key_class, KEY_CLASSES
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
admin_keys = Blueprint('admin_keys', __name__)
|
||||
|
||||
|
||||
@admin_keys.route('/admin/key_types', methods=['GET'])
|
||||
@admin_keys.route('/admin/key_types/<key_id>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_key_types(key_id=None):
|
||||
if key_id is None:
|
||||
data = {}
|
||||
for class_id in KEY_CLASSES:
|
||||
data[class_id] = KEY_CLASSES.get(class_id).name
|
||||
|
||||
return jsonify(data)
|
||||
else:
|
||||
key_class = get_key_class(key_id)
|
||||
data = {
|
||||
'id': key_class.id,
|
||||
'name': key_class.name,
|
||||
'templates': key_class.templates
|
||||
}
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@admin_keys.route('/admin/keys', defaults={'keyid': None}, methods=['POST', 'GET'])
|
||||
@admin_keys.route('/admin/keys/<int:keyid>', methods=['POST', 'GET'])
|
||||
@admins_only
|
||||
def admin_keys_view(keyid):
|
||||
if request.method == 'GET':
|
||||
if keyid:
|
||||
saved_key = Keys.query.filter_by(id=keyid).first_or_404()
|
||||
key_class = get_key_class(saved_key.type)
|
||||
json_data = {
|
||||
'id': saved_key.id,
|
||||
'key': saved_key.flag,
|
||||
'data': saved_key.data,
|
||||
'chal': saved_key.chal,
|
||||
'type': saved_key.type,
|
||||
'type_name': key_class.name,
|
||||
'templates': key_class.templates,
|
||||
}
|
||||
|
||||
return jsonify(json_data)
|
||||
elif request.method == 'POST':
|
||||
chal = request.form.get('chal')
|
||||
flag = request.form.get('key')
|
||||
data = request.form.get('keydata')
|
||||
key_type = request.form.get('key_type')
|
||||
if not keyid:
|
||||
k = Keys(chal, flag, key_type)
|
||||
k.data = data
|
||||
db.session.add(k)
|
||||
else:
|
||||
k = Keys.query.filter_by(id=keyid).first()
|
||||
k.flag = flag
|
||||
k.data = data
|
||||
k.type = key_type
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_keys.route('/admin/keys/<int:keyid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_delete_keys(keyid):
|
||||
if request.method == 'POST':
|
||||
key = Keys.query.filter_by(id=keyid).first_or_404()
|
||||
db.session.delete(key)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
|
@ -0,0 +1,13 @@
|
|||
from flask import render_template
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.updates import update_check
|
||||
from CTFd.utils.modes import get_model
|
||||
from CTFd.models import db, Notifications
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin.route('/admin/notifications')
|
||||
@admins_only
|
||||
def notifications():
|
||||
notifs = Notifications.query.order_by(Notifications.id.desc()).all()
|
||||
return render_template('admin/notifications.html', notifications=notifs)
|
|
@ -1,132 +1,44 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache, markdown
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
admin_pages = Blueprint('admin_pages', __name__)
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs
|
||||
from CTFd.schemas.pages import PageSchema
|
||||
from CTFd.utils import config, validators, markdown, uploads
|
||||
from CTFd.cache import cache
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin_pages.route('/admin/pages', methods=['GET', 'POST'])
|
||||
@admin.route('/admin/pages')
|
||||
@admins_only
|
||||
def admin_pages_view():
|
||||
page_id = request.args.get('id')
|
||||
page_op = request.args.get('operation')
|
||||
|
||||
if request.method == 'GET' and page_op == 'preview':
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
return render_template('page.html', content=markdown(page.html))
|
||||
|
||||
if request.method == 'GET' and page_op == 'create':
|
||||
return render_template('admin/editor.html')
|
||||
|
||||
if page_id and request.method == 'GET':
|
||||
page = Pages.query.filter_by(id=page_id).first()
|
||||
return render_template('admin/editor.html', page=page)
|
||||
|
||||
if request.method == 'POST':
|
||||
page_form_id = request.form.get('id')
|
||||
title = request.form['title']
|
||||
html = request.form['html']
|
||||
route = request.form['route'].lstrip('/')
|
||||
auth_required = 'auth_required' in request.form
|
||||
|
||||
if page_op == 'preview':
|
||||
page = Pages(title, route, html, draft=False)
|
||||
return render_template('page.html', content=markdown(page.html))
|
||||
|
||||
page = Pages.query.filter_by(id=page_form_id).first()
|
||||
|
||||
errors = []
|
||||
if not route:
|
||||
errors.append('Missing URL route')
|
||||
|
||||
if errors:
|
||||
page = Pages(title, html, route)
|
||||
return render_template('/admin/editor.html', page=page)
|
||||
|
||||
if page:
|
||||
page.title = title
|
||||
page.route = route
|
||||
page.html = html
|
||||
page.auth_required = auth_required
|
||||
|
||||
if page_op == 'publish':
|
||||
page.draft = False
|
||||
|
||||
db.session.commit()
|
||||
|
||||
data = {
|
||||
'result': 'success',
|
||||
'operation': page_op,
|
||||
'page': {
|
||||
'id': page.id,
|
||||
'route': page.route,
|
||||
'title': page.title
|
||||
}
|
||||
}
|
||||
|
||||
db.session.close()
|
||||
cache.clear()
|
||||
return jsonify(data)
|
||||
|
||||
if page_op == 'publish':
|
||||
page = Pages(title, route, html, draft=False, auth_required=auth_required)
|
||||
elif page_op == 'save':
|
||||
page = Pages(title, route, html, auth_required=auth_required)
|
||||
|
||||
db.session.add(page)
|
||||
db.session.commit()
|
||||
|
||||
data = {
|
||||
'result': 'success',
|
||||
'operation': page_op,
|
||||
'page': {
|
||||
'id': page.id,
|
||||
'route': page.route,
|
||||
'title': page.title
|
||||
}
|
||||
}
|
||||
|
||||
db.session.close()
|
||||
cache.clear()
|
||||
|
||||
return jsonify(data)
|
||||
|
||||
def pages_listing():
|
||||
pages = Pages.query.all()
|
||||
return render_template('admin/pages.html', pages=pages)
|
||||
|
||||
|
||||
@admin_pages.route('/admin/pages/delete', methods=['POST'])
|
||||
@admin.route('/admin/pages/new')
|
||||
@admins_only
|
||||
def delete_page():
|
||||
id = request.form['id']
|
||||
page = Pages.query.filter_by(id=id).first_or_404()
|
||||
db.session.delete(page)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
with app.app_context():
|
||||
cache.clear()
|
||||
return '1'
|
||||
def pages_new():
|
||||
return render_template('admin/editor.html')
|
||||
|
||||
|
||||
@admin_pages.route('/admin/media', methods=['GET', 'POST', 'DELETE'])
|
||||
@admin.route('/admin/pages/preview', methods=['POST'])
|
||||
@admins_only
|
||||
def admin_pages_media():
|
||||
if request.method == 'POST':
|
||||
files = request.files.getlist('files[]')
|
||||
def pages_preview():
|
||||
data = request.form.to_dict()
|
||||
schema = PageSchema()
|
||||
page = schema.load(data)
|
||||
return render_template('page.html', content=markdown(page.data.content))
|
||||
|
||||
uploaded = []
|
||||
for f in files:
|
||||
data = utils.upload_file(file=f, chalid=None)
|
||||
if data:
|
||||
uploaded.append({'id': data[0], 'location': data[1]})
|
||||
return jsonify({'results': uploaded})
|
||||
elif request.method == 'DELETE':
|
||||
file_ids = request.form.getlist('file_ids[]')
|
||||
for file_id in file_ids:
|
||||
utils.delete_file(file_id)
|
||||
return True
|
||||
else:
|
||||
files = [{'id': f.id, 'location': f.location} for f in Files.query.filter_by(chal=None).all()]
|
||||
return jsonify({'results': files})
|
||||
|
||||
@admin.route('/admin/pages/<int:page_id>')
|
||||
@admins_only
|
||||
def pages_detail(page_id):
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
page_op = request.args.get('operation')
|
||||
|
||||
if request.method == 'GET' and page_op == 'preview':
|
||||
return render_template('page.html', content=markdown(page.content))
|
||||
|
||||
if request.method == 'GET' and page_op == 'create':
|
||||
return render_template('admin/editor.html')
|
||||
|
||||
return render_template('admin/editor.html', page=page)
|
||||
|
|
|
@ -1,30 +1,12 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.admin import admin
|
||||
from CTFd.scoreboard import get_standings
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
admin_scoreboard = Blueprint('admin_scoreboard', __name__)
|
||||
|
||||
|
||||
@admin_scoreboard.route('/admin/scoreboard')
|
||||
@admin.route('/admin/scoreboard')
|
||||
@admins_only
|
||||
def admin_scoreboard_view():
|
||||
def scoreboard_listing():
|
||||
standings = get_standings(admin=True)
|
||||
return render_template('admin/scoreboard.html', teams=standings)
|
||||
|
||||
|
||||
@admin_scoreboard.route('/admin/scores')
|
||||
@admins_only
|
||||
def admin_scores():
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
quickest = db.func.max(Solves.date).label('quickest')
|
||||
teams = db.session.query(Solves.teamid, Teams.name, score)\
|
||||
.join(Teams).join(Challenges).filter(Teams.banned == False)\
|
||||
.group_by(Solves.teamid).order_by(score.desc(), quickest)
|
||||
db.session.close()
|
||||
json_data = {'teams': []}
|
||||
for i, x in enumerate(teams):
|
||||
json_data['teams'].append({'place': i + 1, 'id': x.teamid, 'name': x.name, 'score': int(x.score)})
|
||||
return jsonify(json_data)
|
||||
return render_template('admin/scoreboard.html', standings=standings)
|
||||
|
|
|
@ -1,79 +1,62 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache, update_check
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
admin_statistics = Blueprint('admin_statistics', __name__)
|
||||
from flask import render_template
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.utils.updates import update_check
|
||||
from CTFd.utils.modes import get_model
|
||||
from CTFd.models import db, Solves, Challenges, Fails, Tracking
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/graphs/<graph_type>')
|
||||
@admin.route('/admin/statistics', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_graph(graph_type):
|
||||
if graph_type == 'categories':
|
||||
categories = db.session.query(Challenges.category, db.func.count(Challenges.category)).group_by(Challenges.category).all()
|
||||
json_data = {'categories': []}
|
||||
for category, count in categories:
|
||||
json_data['categories'].append({'category': category, 'count': count})
|
||||
return jsonify(json_data)
|
||||
elif graph_type == "solves":
|
||||
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
|
||||
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
|
||||
.group_by(Solves.chalid).subquery()
|
||||
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
|
||||
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
|
||||
json_data = {}
|
||||
for chal, count, name in solves:
|
||||
json_data[name] = count
|
||||
return jsonify(json_data)
|
||||
elif graph_type == "solve-percentages":
|
||||
chals = Challenges.query.add_columns('id', 'name', 'hidden', 'max_attempts').order_by(Challenges.value).all()
|
||||
|
||||
teams_with_points = db.session.query(Solves.teamid)\
|
||||
.join(Teams)\
|
||||
.filter(Teams.banned == False)\
|
||||
.group_by(Solves.teamid)\
|
||||
.count()
|
||||
|
||||
percentage_data = []
|
||||
for x in chals:
|
||||
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id)\
|
||||
.filter(Solves.chalid == x[1], Teams.banned == False)\
|
||||
.count()
|
||||
|
||||
if teams_with_points > 0:
|
||||
percentage = (float(solve_count) / float(teams_with_points))
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
percentage_data.append({
|
||||
'id': x.id,
|
||||
'name': x.name,
|
||||
'percentage': percentage,
|
||||
})
|
||||
|
||||
percentage_data = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
|
||||
json_data = {'percentages': percentage_data}
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/statistics', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_stats():
|
||||
def statistics():
|
||||
update_check()
|
||||
teams_registered = db.session.query(db.func.count(Teams.id)).first()[0]
|
||||
|
||||
wrong_count = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
solve_count = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
Model = get_model()
|
||||
|
||||
challenge_count = db.session.query(db.func.count(Challenges.id)).first()[0]
|
||||
ip_count = db.session.query(db.func.count(Tracking.ip.distinct())).first()[0]
|
||||
teams_registered = Model.query.count()
|
||||
|
||||
wrong_count = Fails.query.join(
|
||||
Model,
|
||||
Fails.account_id == Model.id
|
||||
).filter(
|
||||
Model.banned == False,
|
||||
Model.hidden == False
|
||||
).count()
|
||||
|
||||
solve_count = Solves.query.join(
|
||||
Model,
|
||||
Solves.account_id == Model.id
|
||||
).filter(
|
||||
Model.banned == False,
|
||||
Model.hidden == False
|
||||
).count()
|
||||
|
||||
challenge_count = Challenges.query.count()
|
||||
|
||||
ip_count = Tracking.query.with_entities(Tracking.ip).distinct().count()
|
||||
|
||||
solves_sub = db.session.query(
|
||||
Solves.challenge_id,
|
||||
db.func.count(Solves.challenge_id).label('solves_cnt')
|
||||
).join(
|
||||
Model,
|
||||
Solves.account_id == Model.id
|
||||
).filter(
|
||||
Model.banned == False,
|
||||
Model.hidden == False
|
||||
).group_by(
|
||||
Solves.challenge_id
|
||||
).subquery()
|
||||
|
||||
solves = db.session.query(
|
||||
solves_sub.columns.challenge_id,
|
||||
solves_sub.columns.solves_cnt,
|
||||
Challenges.name
|
||||
).join(
|
||||
Challenges,
|
||||
solves_sub.columns.challenge_id == Challenges.id
|
||||
).all()
|
||||
|
||||
solves_sub = db.session.query(Solves.chalid, db.func.count(Solves.chalid).label('solves_cnt')) \
|
||||
.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False) \
|
||||
.group_by(Solves.chalid).subquery()
|
||||
solves = db.session.query(solves_sub.columns.chalid, solves_sub.columns.solves_cnt, Challenges.name) \
|
||||
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
|
||||
solve_data = {}
|
||||
for chal, count, name in solves:
|
||||
solve_data[name] = count
|
||||
|
@ -84,8 +67,6 @@ def admin_stats():
|
|||
most_solved = max(solve_data, key=solve_data.get)
|
||||
least_solved = min(solve_data, key=solve_data.get)
|
||||
|
||||
db.session.expunge_all()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return render_template(
|
||||
|
@ -99,49 +80,3 @@ def admin_stats():
|
|||
most_solved=most_solved,
|
||||
least_solved=least_solved
|
||||
)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
|
||||
@admin_statistics.route('/admin/wrong_keys/<int:page>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_wrong_key(page):
|
||||
page = abs(int(page))
|
||||
results_per_page = 50
|
||||
page_start = results_per_page * (page - 1)
|
||||
page_end = results_per_page * (page - 1) + results_per_page
|
||||
|
||||
wrong_keys = WrongKeys.query.add_columns(WrongKeys.id, WrongKeys.chalid, WrongKeys.flag, WrongKeys.teamid, WrongKeys.date,
|
||||
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
|
||||
.join(Challenges) \
|
||||
.join(Teams) \
|
||||
.order_by(WrongKeys.date.desc()) \
|
||||
.slice(page_start, page_end) \
|
||||
.all()
|
||||
|
||||
wrong_count = db.session.query(db.func.count(WrongKeys.id)).first()[0]
|
||||
pages = int(wrong_count / results_per_page) + (wrong_count % results_per_page > 0)
|
||||
|
||||
return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
|
||||
|
||||
|
||||
@admin_statistics.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
|
||||
@admin_statistics.route('/admin/correct_keys/<int:page>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_correct_key(page):
|
||||
page = abs(int(page))
|
||||
results_per_page = 50
|
||||
page_start = results_per_page * (page - 1)
|
||||
page_end = results_per_page * (page - 1) + results_per_page
|
||||
|
||||
solves = Solves.query.add_columns(Solves.id, Solves.chalid, Solves.teamid, Solves.date, Solves.flag,
|
||||
Challenges.name.label('chal_name'), Teams.name.label('team_name')) \
|
||||
.join(Challenges) \
|
||||
.join(Teams) \
|
||||
.order_by(Solves.date.desc()) \
|
||||
.slice(page_start, page_end) \
|
||||
.all()
|
||||
|
||||
solve_count = db.session.query(db.func.count(Solves.id)).first()[0]
|
||||
pages = int(solve_count / results_per_page) + (solve_count % results_per_page > 0)
|
||||
|
||||
return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
from flask import render_template, request
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from CTFd.models import Challenges, Submissions
|
||||
from CTFd.utils.modes import get_model
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin.route('/admin/submissions', defaults={'submission_type': None})
|
||||
@admin.route('/admin/submissions/<submission_type>')
|
||||
@admins_only
|
||||
def submissions_listing(submission_type):
|
||||
filters = {}
|
||||
if submission_type:
|
||||
filters['type'] = submission_type
|
||||
|
||||
curr_page = abs(int(request.args.get('page', 1)))
|
||||
results_per_page = 50
|
||||
page_start = results_per_page * (curr_page - 1)
|
||||
page_end = results_per_page * (curr_page - 1) + results_per_page
|
||||
sub_count = Submissions.query.filter_by(**filters).count()
|
||||
page_count = int(sub_count / results_per_page) + (sub_count % results_per_page > 0)
|
||||
|
||||
Model = get_model()
|
||||
|
||||
submissions = Submissions.query.add_columns(
|
||||
Submissions.id,
|
||||
Submissions.type,
|
||||
Submissions.challenge_id,
|
||||
Submissions.provided,
|
||||
Submissions.account_id,
|
||||
Submissions.date,
|
||||
Challenges.name.label('challenge_name'),
|
||||
Model.name.label('team_name')
|
||||
)\
|
||||
.filter_by(**filters) \
|
||||
.join(Challenges)\
|
||||
.join(Model)\
|
||||
.order_by(Submissions.date.desc())\
|
||||
.slice(page_start, page_end)\
|
||||
.all()
|
||||
|
||||
return render_template(
|
||||
'admin/submissions.html',
|
||||
submissions=submissions,
|
||||
page_count=page_count,
|
||||
curr_page=curr_page,
|
||||
type=submission_type
|
||||
)
|
|
@ -1,20 +1,17 @@
|
|||
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
|
||||
from CTFd.utils import admins_only, is_admin, cache, ratelimit
|
||||
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
|
||||
from CTFd.utils.decorators import admins_only, ratelimit
|
||||
from CTFd.models import db, Teams, Solves, Awards, Unlocks, Challenges, Fails, Flags, Tags, Files, Tracking, Pages, Configs
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from sqlalchemy.sql import not_
|
||||
|
||||
from CTFd import utils
|
||||
|
||||
import re
|
||||
|
||||
admin_teams = Blueprint('admin_teams', __name__)
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin_teams.route('/admin/teams', defaults={'page': '1'})
|
||||
@admin_teams.route('/admin/teams/<int:page>')
|
||||
@admin.route('/admin/teams')
|
||||
@admins_only
|
||||
def admin_teams_view(page):
|
||||
def teams_listing():
|
||||
page = request.args.get('page', 1)
|
||||
q = request.args.get('q')
|
||||
if q:
|
||||
field = request.args.get('field')
|
||||
|
@ -32,9 +29,7 @@ def admin_teams_view(page):
|
|||
teams = Teams.query.filter(Teams.email.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
||||
elif field == 'affiliation':
|
||||
teams = Teams.query.filter(Teams.affiliation.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
||||
elif field == 'country':
|
||||
teams = Teams.query.filter(Teams.country.like('%{}%'.format(q))).order_by(Teams.id.asc()).all()
|
||||
return render_template('admin/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field)
|
||||
return render_template('admin/teams/teams.html', teams=teams, pages=None, curr_page=None, q=q, field=field)
|
||||
|
||||
page = abs(int(page))
|
||||
results_per_page = 50
|
||||
|
@ -44,324 +39,52 @@ def admin_teams_view(page):
|
|||
teams = Teams.query.order_by(Teams.id.asc()).slice(page_start, page_end).all()
|
||||
count = db.session.query(db.func.count(Teams.id)).first()[0]
|
||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||
return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
|
||||
return render_template('admin/teams/teams.html', teams=teams, pages=pages, curr_page=page)
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/new', methods=['POST'])
|
||||
@admin.route('/admin/teams/new')
|
||||
@admins_only
|
||||
def admin_create_team():
|
||||
name = request.form.get('name', None)
|
||||
password = request.form.get('password', None)
|
||||
email = request.form.get('email', None)
|
||||
website = request.form.get('website', None)
|
||||
affiliation = request.form.get('affiliation', None)
|
||||
country = request.form.get('country', None)
|
||||
|
||||
admin_user = True if request.form.get('admin', None) == 'on' else False
|
||||
verified = True if request.form.get('verified', None) == 'on' else False
|
||||
hidden = True if request.form.get('hidden', None) == 'on' else False
|
||||
|
||||
errors = []
|
||||
|
||||
if not name:
|
||||
errors.append('The team requires a name')
|
||||
elif Teams.query.filter(Teams.name == name).first():
|
||||
errors.append('That name is taken')
|
||||
|
||||
if utils.check_email_format(name) is True:
|
||||
errors.append('Team name cannot be an email address')
|
||||
|
||||
if not email:
|
||||
errors.append('The team requires an email')
|
||||
elif Teams.query.filter(Teams.email == email).first():
|
||||
errors.append('That email is taken')
|
||||
|
||||
if email:
|
||||
valid_email = utils.check_email_format(email)
|
||||
if not valid_email:
|
||||
errors.append("That email address is invalid")
|
||||
|
||||
if not password:
|
||||
errors.append('The team requires a password')
|
||||
|
||||
if website and (website.startswith('http://') or website.startswith('https://')) is False:
|
||||
errors.append('Websites must start with http:// or https://')
|
||||
|
||||
if errors:
|
||||
db.session.close()
|
||||
return jsonify({'data': errors})
|
||||
|
||||
team = Teams(name, email, password)
|
||||
team.website = website
|
||||
team.affiliation = affiliation
|
||||
team.country = country
|
||||
|
||||
team.admin = admin_user
|
||||
team.verified = verified
|
||||
team.hidden = hidden
|
||||
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return jsonify({'data': ['success']})
|
||||
def teams_new():
|
||||
return render_template('admin/teams/new.html')
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
|
||||
@admin.route('/admin/teams/<int:team_id>')
|
||||
@admins_only
|
||||
def admin_team(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first_or_404()
|
||||
def teams_detail(team_id):
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
if request.method == 'GET':
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
solve_ids = [s.chalid for s in solves]
|
||||
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
||||
last_seen = db.func.max(Tracking.date).label('last_seen')
|
||||
addrs = db.session.query(Tracking.ip, last_seen) \
|
||||
.filter_by(team=teamid) \
|
||||
.group_by(Tracking.ip) \
|
||||
.order_by(last_seen.desc()).all()
|
||||
wrong_keys = WrongKeys.query.filter_by(teamid=teamid).order_by(WrongKeys.date.asc()).all()
|
||||
awards = Awards.query.filter_by(teamid=teamid).order_by(Awards.date.asc()).all()
|
||||
score = user.score(admin=True)
|
||||
place = user.place(admin=True)
|
||||
return render_template('admin/team.html', solves=solves, team=user, addrs=addrs, score=score, missing=missing,
|
||||
place=place, wrong_keys=wrong_keys, awards=awards)
|
||||
elif request.method == 'POST':
|
||||
name = request.form.get('name', None)
|
||||
password = request.form.get('password', None)
|
||||
email = request.form.get('email', None)
|
||||
website = request.form.get('website', None)
|
||||
affiliation = request.form.get('affiliation', None)
|
||||
country = request.form.get('country', None)
|
||||
# Get members
|
||||
members = team.members
|
||||
member_ids = [member.id for member in members]
|
||||
|
||||
admin_user = True if request.form.get('admin', None) == 'on' else False
|
||||
verified = True if request.form.get('verified', None) == 'on' else False
|
||||
hidden = True if request.form.get('hidden', None) == 'on' else False
|
||||
# Get Solves for all members
|
||||
solves = team.get_solves(admin=True)
|
||||
fails = team.get_fails(admin=True)
|
||||
awards = team.get_awards(admin=True)
|
||||
score = team.get_score(admin=True)
|
||||
place = team.get_place(admin=True)
|
||||
|
||||
errors = []
|
||||
# Get missing Challenges for all members
|
||||
# TODO: How do you mark a missing challenge for a team?
|
||||
solve_ids = [s.challenge_id for s in solves]
|
||||
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
||||
|
||||
if email:
|
||||
valid_email = utils.check_email_format(email)
|
||||
if not valid_email:
|
||||
errors.append("That email address is invalid")
|
||||
# Get addresses for all members
|
||||
last_seen = db.func.max(Tracking.date).label('last_seen')
|
||||
addrs = db.session.query(Tracking.ip, last_seen) \
|
||||
.filter(Tracking.user_id.in_(member_ids)) \
|
||||
.group_by(Tracking.ip) \
|
||||
.order_by(last_seen.desc()).all()
|
||||
|
||||
name_used = Teams.query.filter(Teams.name == name).first()
|
||||
if name_used and int(name_used.id) != int(teamid):
|
||||
errors.append('That name is taken')
|
||||
|
||||
if utils.check_email_format(name) is True:
|
||||
errors.append('Team name cannot be an email address')
|
||||
|
||||
email_used = Teams.query.filter(Teams.email == email).first()
|
||||
if email_used and int(email_used.id) != int(teamid):
|
||||
errors.append('That email is taken')
|
||||
|
||||
if website and (website.startswith('http://') or website.startswith('https://')) is False:
|
||||
errors.append('Websites must start with http:// or https://')
|
||||
|
||||
if errors:
|
||||
db.session.close()
|
||||
return jsonify({'data': errors})
|
||||
else:
|
||||
user.name = name
|
||||
if email:
|
||||
user.email = email
|
||||
if password:
|
||||
user.password = bcrypt_sha256.encrypt(password)
|
||||
user.website = website
|
||||
user.affiliation = affiliation
|
||||
user.country = country
|
||||
user.admin = admin_user
|
||||
user.verified = verified
|
||||
user.banned = hidden
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return jsonify({'data': ['success']})
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/mail', methods=['POST'])
|
||||
@admins_only
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def email_user(teamid):
|
||||
msg = request.form.get('msg', None)
|
||||
team = Teams.query.filter(Teams.id == teamid).first_or_404()
|
||||
if msg and team:
|
||||
result, response = utils.sendmail(team.email, msg)
|
||||
return jsonify({
|
||||
'result': result,
|
||||
'message': response
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'result': False,
|
||||
'message': "Missing information"
|
||||
})
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/ban', methods=['POST'])
|
||||
@admins_only
|
||||
def ban(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first_or_404()
|
||||
user.banned = True
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/unban', methods=['POST'])
|
||||
@admins_only
|
||||
def unban(teamid):
|
||||
user = Teams.query.filter_by(id=teamid).first_or_404()
|
||||
user.banned = False
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return redirect(url_for('admin_scoreboard.admin_scoreboard_view'))
|
||||
|
||||
|
||||
@admin_teams.route('/admin/team/<int:teamid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_team(teamid):
|
||||
try:
|
||||
Unlocks.query.filter_by(teamid=teamid).delete()
|
||||
Awards.query.filter_by(teamid=teamid).delete()
|
||||
WrongKeys.query.filter_by(teamid=teamid).delete()
|
||||
Solves.query.filter_by(teamid=teamid).delete()
|
||||
Tracking.query.filter_by(team=teamid).delete()
|
||||
Teams.query.filter_by(id=teamid).delete()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
except DatabaseError:
|
||||
return '0'
|
||||
else:
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_teams.route('/admin/solves/<teamid>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_solves(teamid="all"):
|
||||
if teamid == "all":
|
||||
solves = Solves.query.all()
|
||||
awards = []
|
||||
else:
|
||||
solves = Solves.query.filter_by(teamid=teamid).all()
|
||||
awards = Awards.query.filter_by(teamid=teamid).all()
|
||||
db.session.close()
|
||||
json_data = {'solves': []}
|
||||
for x in solves:
|
||||
json_data['solves'].append({
|
||||
'id': x.id,
|
||||
'chal': x.chal.name,
|
||||
'chalid': x.chalid,
|
||||
'team': x.teamid,
|
||||
'value': x.chal.value,
|
||||
'category': x.chal.category,
|
||||
'time': utils.unix_time(x.date)
|
||||
})
|
||||
for award in awards:
|
||||
json_data['solves'].append({
|
||||
'chal': award.name,
|
||||
'chalid': None,
|
||||
'team': award.teamid,
|
||||
'value': award.value,
|
||||
'category': award.category or "Award",
|
||||
'time': utils.unix_time(award.date)
|
||||
})
|
||||
json_data['solves'].sort(key=lambda k: k['time'])
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_teams.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
|
||||
@admin_teams.route('/admin/fails/<int:teamid>', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_fails(teamid):
|
||||
if teamid == "all":
|
||||
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Teams.banned == False).count()
|
||||
db.session.close()
|
||||
json_data = {'fails': str(fails), 'solves': str(solves)}
|
||||
return jsonify(json_data)
|
||||
else:
|
||||
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
db.session.close()
|
||||
json_data = {'fails': str(fails), 'solves': str(solves)}
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_teams.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
|
||||
@admins_only
|
||||
def create_solve(teamid, chalid):
|
||||
solve = Solves(teamid=teamid, chalid=chalid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN')
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_teams.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_solve(keyid):
|
||||
solve = Solves.query.filter_by(id=keyid).first_or_404()
|
||||
db.session.delete(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_teams.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_wrong_key(keyid):
|
||||
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
|
||||
db.session.delete(wrong_key)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_teams.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
|
||||
@admins_only
|
||||
def delete_award(award_id):
|
||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||
db.session.delete(award)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
|
||||
|
||||
@admin_teams.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
|
||||
@admins_only
|
||||
def admin_awards(teamid):
|
||||
awards = Awards.query.filter_by(teamid=teamid).all()
|
||||
|
||||
awards_list = []
|
||||
for award in awards:
|
||||
awards_list.append({
|
||||
'id': award.id,
|
||||
'name': award.name,
|
||||
'description': award.description,
|
||||
'date': award.date,
|
||||
'value': award.value,
|
||||
'category': award.category,
|
||||
'icon': award.icon
|
||||
})
|
||||
json_data = {'awards': awards_list}
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@admin_teams.route('/admin/awards/add', methods=['POST'])
|
||||
@admins_only
|
||||
def create_award():
|
||||
try:
|
||||
teamid = request.form['teamid']
|
||||
name = request.form.get('name', 'Award')
|
||||
value = request.form.get('value', 0)
|
||||
award = Awards(teamid, name, value)
|
||||
award.description = request.form.get('description')
|
||||
award.category = request.form.get('category')
|
||||
db.session.add(award)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return '1'
|
||||
except Exception as e:
|
||||
print(e)
|
||||
return '0'
|
||||
return render_template(
|
||||
'admin/teams/team.html',
|
||||
team=team,
|
||||
members=members,
|
||||
score=score,
|
||||
place=place,
|
||||
solves=solves,
|
||||
fails=fails,
|
||||
missing=missing,
|
||||
awards=awards,
|
||||
addrs=addrs,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,101 @@
|
|||
from flask import render_template, request
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.decorators import admins_only, ratelimit
|
||||
from CTFd.utils.modes import USERS_MODE, TEAMS_MODE
|
||||
from CTFd.models import db, Users, Challenges, Tracking
|
||||
from sqlalchemy.sql import not_
|
||||
|
||||
from CTFd import utils
|
||||
from CTFd.admin import admin
|
||||
|
||||
|
||||
@admin.route('/admin/users')
|
||||
@admins_only
|
||||
def users_listing():
|
||||
page = request.args.get('page', 1)
|
||||
q = request.args.get('q')
|
||||
if q:
|
||||
field = request.args.get('field')
|
||||
users = []
|
||||
errors = []
|
||||
if field == 'id':
|
||||
if q.isnumeric():
|
||||
users = Users.query.filter(Users.id == q).order_by(Users.id.asc()).all()
|
||||
else:
|
||||
users = []
|
||||
errors.append('Your ID search term is not numeric')
|
||||
elif field == 'name':
|
||||
users = Users.query.filter(Users.name.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
||||
elif field == 'email':
|
||||
users = Users.query.filter(Users.email.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
||||
elif field == 'affiliation':
|
||||
users = Users.query.filter(Users.affiliation.like('%{}%'.format(q))).order_by(Users.id.asc()).all()
|
||||
return render_template('admin/users/users.html', users=users, pages=None, curr_page=None, q=q, field=field)
|
||||
|
||||
page = abs(int(page))
|
||||
results_per_page = 50
|
||||
page_start = results_per_page * (page - 1)
|
||||
page_end = results_per_page * (page - 1) + results_per_page
|
||||
|
||||
users = Users.query.order_by(Users.id.asc()).slice(page_start, page_end).all()
|
||||
count = db.session.query(db.func.count(Users.id)).first()[0]
|
||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||
|
||||
return render_template('admin/users/users.html', users=users, pages=pages, curr_page=page)
|
||||
|
||||
|
||||
@admin.route('/admin/users/new')
|
||||
@admins_only
|
||||
def users_new():
|
||||
return render_template('admin/users/new.html')
|
||||
|
||||
|
||||
@admin.route('/admin/users/<int:user_id>')
|
||||
@admins_only
|
||||
def users_detail(user_id):
|
||||
# Get user object
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
# Get the user's solves
|
||||
solves = user.get_solves(admin=True)
|
||||
|
||||
# Get challenges that the user is missing
|
||||
if get_config('user_mode') == TEAMS_MODE:
|
||||
if user.team:
|
||||
all_solves = user.team.get_solves(admin=True)
|
||||
else:
|
||||
all_solves = user.get_solves(admin=True)
|
||||
else:
|
||||
all_solves = user.get_solves(admin=True)
|
||||
|
||||
solve_ids = [s.challenge_id for s in all_solves]
|
||||
missing = Challenges.query.filter(not_(Challenges.id.in_(solve_ids))).all()
|
||||
|
||||
# Get IP addresses that the User has used
|
||||
last_seen = db.func.max(Tracking.date).label('last_seen')
|
||||
addrs = db.session.query(Tracking.ip, last_seen) \
|
||||
.filter_by(user_id=user_id) \
|
||||
.group_by(Tracking.ip) \
|
||||
.order_by(last_seen.desc()).all()
|
||||
|
||||
# Get Fails
|
||||
fails = user.get_fails(admin=True)
|
||||
|
||||
# Get Awards
|
||||
awards = user.get_awards(admin=True)
|
||||
|
||||
# Get user properties
|
||||
score = user.get_score(admin=True)
|
||||
place = user.get_place(admin=True)
|
||||
|
||||
return render_template(
|
||||
'admin/users/user.html',
|
||||
solves=solves,
|
||||
user=user,
|
||||
addrs=addrs,
|
||||
score=score,
|
||||
missing=missing,
|
||||
place=place,
|
||||
fails=fails,
|
||||
awards=awards
|
||||
)
|
|
@ -0,0 +1,37 @@
|
|||
from flask import Blueprint
|
||||
from flask_restplus import Api
|
||||
from CTFd.api.v1.challenges import challenges_namespace
|
||||
|
||||
from CTFd.api.v1.teams import teams_namespace
|
||||
from CTFd.api.v1.users import users_namespace
|
||||
from CTFd.api.v1.scoreboard import scoreboard_namespace
|
||||
from CTFd.api.v1.statistics import statistics_namespace
|
||||
from CTFd.api.v1.submissions import submissions_namespace
|
||||
from CTFd.api.v1.tags import tags_namespace
|
||||
from CTFd.api.v1.awards import awards_namespace
|
||||
from CTFd.api.v1.hints import hints_namespace
|
||||
from CTFd.api.v1.flags import flags_namespace
|
||||
from CTFd.api.v1.files import files_namespace
|
||||
from CTFd.api.v1.config import configs_namespace
|
||||
from CTFd.api.v1.notifications import notifications_namespace
|
||||
from CTFd.api.v1.pages import pages_namespace
|
||||
from CTFd.api.v1.unlocks import unlocks_namespace
|
||||
|
||||
api = Blueprint('api', __name__, url_prefix='/api/v1')
|
||||
CTFd_API_v1 = Api(api, version='v1')
|
||||
|
||||
CTFd_API_v1.add_namespace(challenges_namespace, '/challenges')
|
||||
CTFd_API_v1.add_namespace(tags_namespace, '/tags')
|
||||
CTFd_API_v1.add_namespace(awards_namespace, '/awards')
|
||||
CTFd_API_v1.add_namespace(hints_namespace, '/hints')
|
||||
CTFd_API_v1.add_namespace(flags_namespace, '/flags')
|
||||
CTFd_API_v1.add_namespace(submissions_namespace, '/submissions')
|
||||
CTFd_API_v1.add_namespace(scoreboard_namespace, '/scoreboard')
|
||||
CTFd_API_v1.add_namespace(teams_namespace, '/teams')
|
||||
CTFd_API_v1.add_namespace(users_namespace, '/users')
|
||||
CTFd_API_v1.add_namespace(statistics_namespace, '/statistics')
|
||||
CTFd_API_v1.add_namespace(files_namespace, '/files')
|
||||
CTFd_API_v1.add_namespace(notifications_namespace, '/notifications')
|
||||
CTFd_API_v1.add_namespace(configs_namespace, '/configs')
|
||||
CTFd_API_v1.add_namespace(pages_namespace, '/pages')
|
||||
CTFd_API_v1.add_namespace(unlocks_namespace, '/unlocks')
|
|
@ -0,0 +1,71 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Awards
|
||||
from CTFd.schemas.awards import AwardSchema
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only
|
||||
)
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
awards_namespace = Namespace('awards', description="Endpoint to retrieve Awards")
|
||||
|
||||
|
||||
@awards_namespace.route('')
|
||||
class AwardList(Resource):
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = AwardSchema()
|
||||
|
||||
response = schema.load(req, session=db.session)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@awards_namespace.route('/<award_id>')
|
||||
@awards_namespace.param('award_id', 'An Award ID')
|
||||
class Award(Resource):
|
||||
@admins_only
|
||||
def get(self, award_id):
|
||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||
response = AwardSchema().dump(award)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, award_id):
|
||||
award = Awards.query.filter_by(id=award_id).first_or_404()
|
||||
db.session.delete(award)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
|
@ -0,0 +1,544 @@
|
|||
from flask import session, request, abort
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import (
|
||||
db,
|
||||
Challenges,
|
||||
Unlocks,
|
||||
HintUnlocks,
|
||||
Tags,
|
||||
Hints,
|
||||
Flags,
|
||||
Solves,
|
||||
Submissions,
|
||||
Fails,
|
||||
ChallengeFiles as ChallengeFilesModel,
|
||||
)
|
||||
from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES
|
||||
from CTFd.utils.dates import ctf_ended, isoformat
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only,
|
||||
authed_only
|
||||
)
|
||||
from CTFd.utils.decorators.visibility import (
|
||||
check_challenge_visibility,
|
||||
check_score_visibility
|
||||
)
|
||||
from CTFd.cache import cache, clear_standings
|
||||
from CTFd.utils.scores import get_standings
|
||||
from CTFd.utils.config.visibility import scores_visible, accounts_visible
|
||||
from CTFd.utils.user import get_current_user, is_admin, authed
|
||||
from CTFd.utils.modes import get_model
|
||||
from CTFd.schemas.tags import TagSchema
|
||||
from CTFd.schemas.hints import HintSchema
|
||||
from CTFd.schemas.flags import FlagSchema
|
||||
from CTFd.utils import config
|
||||
from CTFd.utils import user as current_user
|
||||
from CTFd.utils.user import get_current_team
|
||||
from CTFd.utils.user import get_current_user
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.dates import ctf_started, ctf_ended, ctf_paused, ctftime
|
||||
from CTFd.utils.logging import log
|
||||
from sqlalchemy.sql import or_, and_, any_
|
||||
|
||||
challenges_namespace = Namespace('challenges',
|
||||
description="Endpoint to retrieve Challenges")
|
||||
|
||||
|
||||
@challenges_namespace.route('')
|
||||
class ChallengeList(Resource):
|
||||
@check_challenge_visibility
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
def get(self):
|
||||
# This can return None (unauth) if visibility is set to public
|
||||
user = get_current_user()
|
||||
|
||||
challenges = Challenges.query.filter(
|
||||
and_(Challenges.state != 'hidden', Challenges.state != 'locked')
|
||||
).order_by(Challenges.value).all()
|
||||
|
||||
if user:
|
||||
solve_ids = Solves.query\
|
||||
.with_entities(Solves.challenge_id)\
|
||||
.filter_by(account_id=user.account_id)\
|
||||
.order_by(Solves.challenge_id.asc())\
|
||||
.all()
|
||||
solve_ids = set([value for value, in solve_ids])
|
||||
else:
|
||||
solve_ids = set()
|
||||
|
||||
response = []
|
||||
tag_schema = TagSchema(view='user', many=True)
|
||||
for challenge in challenges:
|
||||
requirements = challenge.requirements
|
||||
if requirements:
|
||||
prereqs = set(requirements.get('prerequisites', []))
|
||||
anonymize = requirements.get('anonymize')
|
||||
if solve_ids >= prereqs:
|
||||
pass
|
||||
else:
|
||||
if anonymize:
|
||||
response.append({
|
||||
'id': challenge.id,
|
||||
'type': 'hidden',
|
||||
'name': '???',
|
||||
'value': 0,
|
||||
'category': '???',
|
||||
'tags': [],
|
||||
'template': '',
|
||||
'script': ''
|
||||
})
|
||||
# Fallthrough to continue
|
||||
continue
|
||||
|
||||
challenge_type = get_chal_class(challenge.type)
|
||||
response.append({
|
||||
'id': challenge.id,
|
||||
'type': challenge_type.name,
|
||||
'name': challenge.name,
|
||||
'value': challenge.value,
|
||||
'category': challenge.category,
|
||||
'tags': tag_schema.dump(challenge.tags).data,
|
||||
'template': challenge_type.templates['view'],
|
||||
'script': challenge_type.scripts['view'],
|
||||
})
|
||||
|
||||
db.session.close()
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
data = request.form or request.get_json()
|
||||
challenge_type = data['type']
|
||||
challenge_class = get_chal_class(challenge_type)
|
||||
challenge = challenge_class.create(request)
|
||||
response = challenge_class.read(challenge)
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/types')
|
||||
class ChallengeTypes(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
response = {}
|
||||
|
||||
for class_id in CHALLENGE_CLASSES:
|
||||
challenge_class = CHALLENGE_CLASSES.get(class_id)
|
||||
response[challenge_class.id] = {
|
||||
'id': challenge_class.id,
|
||||
'name': challenge_class.name,
|
||||
'templates': challenge_class.templates,
|
||||
'scripts': challenge_class.scripts,
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>')
|
||||
@challenges_namespace.param('challenge_id', 'A Challenge ID')
|
||||
class Challenge(Resource):
|
||||
@check_challenge_visibility
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
def get(self, challenge_id):
|
||||
if is_admin():
|
||||
chal = Challenges.query.filter(Challenges.id == challenge_id).first_or_404()
|
||||
else:
|
||||
chal = Challenges.query.filter(
|
||||
Challenges.id == challenge_id, and_(Challenges.state != 'hidden', Challenges.state != 'locked')
|
||||
).first_or_404()
|
||||
|
||||
chal_class = get_chal_class(chal.type)
|
||||
|
||||
tags = [
|
||||
tag['value'] for tag in TagSchema(
|
||||
"user", many=True).dump(
|
||||
chal.tags).data]
|
||||
files = [f.location for f in chal.files]
|
||||
|
||||
unlocked_hints = set()
|
||||
hints = []
|
||||
if authed():
|
||||
user = get_current_user()
|
||||
unlocked_hints = set([u.target for u in HintUnlocks.query.filter_by(
|
||||
type='hints', account_id=user.account_id)])
|
||||
|
||||
for hint in Hints.query.filter_by(challenge_id=chal.id).all():
|
||||
if hint.id in unlocked_hints or ctf_ended():
|
||||
hints.append({'id': hint.id, 'cost': hint.cost,
|
||||
'content': hint.content})
|
||||
else:
|
||||
hints.append({'id': hint.id, 'cost': hint.cost})
|
||||
|
||||
response = chal_class.read(challenge=chal)
|
||||
|
||||
Model = get_model()
|
||||
|
||||
if scores_visible() is True and accounts_visible() is True:
|
||||
solves = Solves.query\
|
||||
.join(Model, Solves.account_id == Model.id)\
|
||||
.filter(Solves.challenge_id == chal.id, Model.banned == False, Model.hidden == False)\
|
||||
.count()
|
||||
response['solves'] = solves
|
||||
else:
|
||||
response['solves'] = None
|
||||
|
||||
response['files'] = files
|
||||
response['tags'] = tags
|
||||
response['hints'] = hints
|
||||
|
||||
db.session.close()
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, challenge_id):
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
challenge_class = get_chal_class(challenge.type)
|
||||
challenge = challenge_class.update(challenge, request)
|
||||
response = challenge_class.read(challenge)
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, challenge_id):
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
chal_class.delete(challenge)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/attempt')
|
||||
class ChallengeAttempt(Resource):
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@authed_only
|
||||
def post(self):
|
||||
if request.content_type != 'application/json':
|
||||
request_data = request.form
|
||||
else:
|
||||
request_data = request.get_json()
|
||||
|
||||
challenge_id = request_data.get('challenge_id')
|
||||
|
||||
if current_user.is_admin():
|
||||
preview = request.args.get('preview', False)
|
||||
if preview:
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
status, message = chal_class.attempt(challenge, request)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "correct" if status else "incorrect",
|
||||
'message': message
|
||||
}
|
||||
}
|
||||
|
||||
if ctf_paused():
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "paused",
|
||||
'message': '{} is paused'.format(config.ctf_name())
|
||||
}
|
||||
}, 403
|
||||
|
||||
user = get_current_user()
|
||||
team = get_current_team()
|
||||
|
||||
fails = Fails.query.filter_by(
|
||||
account_id=user.account_id,
|
||||
challenge_id=challenge_id
|
||||
).count()
|
||||
|
||||
challenge = Challenges.query.filter_by(
|
||||
id=challenge_id).first_or_404()
|
||||
|
||||
if challenge.state == 'hidden':
|
||||
abort(404)
|
||||
|
||||
if challenge.state == 'locked':
|
||||
abort(403)
|
||||
|
||||
requirements = challenge.requirements
|
||||
if requirements:
|
||||
solve_ids = Solves.query \
|
||||
.with_entities(Solves.challenge_id) \
|
||||
.filter_by(account_id=user.account_id) \
|
||||
.order_by(Solves.challenge_id.asc()) \
|
||||
.all()
|
||||
|
||||
prereqs = set(requirements.get('prerequisites', []))
|
||||
if solve_ids >= prereqs:
|
||||
pass
|
||||
else:
|
||||
abort(403)
|
||||
|
||||
chal_class = get_chal_class(challenge.type)
|
||||
|
||||
# Anti-bruteforce / submitting Flags too quickly
|
||||
if current_user.get_wrong_submissions_per_minute(session['id']) > 10:
|
||||
if ctftime():
|
||||
chal_class.fail(
|
||||
user=user,
|
||||
team=team,
|
||||
challenge=challenge,
|
||||
request=request
|
||||
)
|
||||
log(
|
||||
'submissions',
|
||||
"[{date}] {name} submitted {submission} with kpm {kpm} [TOO FAST]",
|
||||
submission=request_data['submission'].encode('utf-8'),
|
||||
kpm=current_user.get_wrong_submissions_per_minute(session['id'])
|
||||
)
|
||||
# Submitting too fast
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "ratelimited",
|
||||
'message': "You're submitting flags too fast. Slow down."
|
||||
}
|
||||
}, 429
|
||||
|
||||
solves = Solves.query.filter_by(
|
||||
account_id=user.account_id,
|
||||
challenge_id=challenge_id
|
||||
).first()
|
||||
|
||||
# Challenge not solved yet
|
||||
if not solves:
|
||||
# Hit max attempts
|
||||
max_tries = challenge.max_attempts
|
||||
if max_tries and fails >= max_tries > 0:
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "incorrect",
|
||||
'message': "You have 0 tries remaining"
|
||||
}
|
||||
}, 403
|
||||
|
||||
status, message = chal_class.attempt(challenge, request)
|
||||
if status: # The challenge plugin says the input is right
|
||||
if ctftime() or current_user.is_admin():
|
||||
chal_class.solve(
|
||||
user=user,
|
||||
team=team,
|
||||
challenge=challenge,
|
||||
request=request
|
||||
)
|
||||
clear_standings()
|
||||
|
||||
log(
|
||||
'submissions',
|
||||
"[{date}] {name} submitted {submission} with kpm {kpm} [CORRECT]",
|
||||
submission=request_data['submission'].encode('utf-8'),
|
||||
kpm=current_user.get_wrong_submissions_per_minute(
|
||||
session['id'])
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "correct",
|
||||
'message': message
|
||||
}
|
||||
}
|
||||
else: # The challenge plugin says the input is wrong
|
||||
if ctftime() or current_user.is_admin():
|
||||
chal_class.fail(
|
||||
user=user,
|
||||
team=team,
|
||||
challenge=challenge,
|
||||
request=request
|
||||
)
|
||||
clear_standings()
|
||||
|
||||
log(
|
||||
'submissions',
|
||||
"[{date}] {name} submitted {submission} with kpm {kpm} [WRONG]",
|
||||
submission=request_data['submission'].encode('utf-8'),
|
||||
kpm=current_user.get_wrong_submissions_per_minute(
|
||||
session['id'])
|
||||
)
|
||||
|
||||
if max_tries:
|
||||
# Off by one since fails has changed since it was gotten
|
||||
attempts_left = max_tries - fails - 1
|
||||
tries_str = 'tries'
|
||||
if attempts_left == 1:
|
||||
tries_str = 'try'
|
||||
# Add a punctuation mark if there isn't one
|
||||
if message[-1] not in '!().;?[]{}':
|
||||
message = message + '.'
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "incorrect",
|
||||
'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str)
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "incorrect",
|
||||
'message': message
|
||||
}
|
||||
}
|
||||
|
||||
# Challenge already solved
|
||||
else:
|
||||
log(
|
||||
'submissions',
|
||||
"[{date}] {name} submitted {submission} with kpm {kpm} [ALREADY SOLVED]",
|
||||
submission=request_data['submission'].encode('utf-8'),
|
||||
kpm=current_user.get_wrong_submissions_per_minute(
|
||||
user.account_id
|
||||
)
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'data': {
|
||||
'status': "already_solved",
|
||||
'message': 'You already solved this'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>/solves')
|
||||
@challenges_namespace.param('id', 'A Challenge ID')
|
||||
class ChallengeSolves(Resource):
|
||||
@check_challenge_visibility
|
||||
@check_score_visibility
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
def get(self, challenge_id):
|
||||
response = []
|
||||
challenge = Challenges.query.filter_by(id=challenge_id).first_or_404()
|
||||
|
||||
# TODO: Need a generic challenge visibility call.
|
||||
# However, it should be stated that a solve on a gated challenge is not considered private.
|
||||
if challenge.state == 'hidden' and is_admin() is False:
|
||||
abort(404)
|
||||
|
||||
Model = get_model()
|
||||
|
||||
solves = Solves.query.join(Model, Solves.account_id == Model.id)\
|
||||
.filter(Solves.challenge_id == challenge_id, Model.banned == False, Model.hidden == False)\
|
||||
.order_by(Solves.date.asc())
|
||||
|
||||
for solve in solves:
|
||||
response.append({
|
||||
'account_id': solve.account_id,
|
||||
'name': solve.account.name,
|
||||
'date': isoformat(solve.date)
|
||||
})
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>/files')
|
||||
@challenges_namespace.param('id', 'A Challenge ID')
|
||||
class ChallengeFiles(Resource):
|
||||
@admins_only
|
||||
def get(self, challenge_id):
|
||||
response = []
|
||||
|
||||
challenge_files = ChallengeFilesModel.query.filter_by(
|
||||
challenge_id=challenge_id).all()
|
||||
|
||||
for f in challenge_files:
|
||||
response.append({
|
||||
'id': f.id,
|
||||
'type': f.type,
|
||||
'location': f.location
|
||||
})
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>/tags')
|
||||
@challenges_namespace.param('id', 'A Challenge ID')
|
||||
class ChallengeTags(Resource):
|
||||
@admins_only
|
||||
def get(self, challenge_id):
|
||||
response = []
|
||||
|
||||
tags = Tags.query.filter_by(challenge_id=challenge_id).all()
|
||||
|
||||
for t in tags:
|
||||
response.append({
|
||||
'id': t.id,
|
||||
'challenge_id': t.challenge_id,
|
||||
'value': t.value
|
||||
})
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>/hints')
|
||||
@challenges_namespace.param('id', 'A Challenge ID')
|
||||
class ChallengeHints(Resource):
|
||||
@admins_only
|
||||
def get(self, challenge_id):
|
||||
hints = Hints.query.filter_by(challenge_id=challenge_id).all()
|
||||
schema = HintSchema(many=True)
|
||||
response = schema.dump(hints)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@challenges_namespace.route('/<challenge_id>/flags')
|
||||
@challenges_namespace.param('id', 'A Challenge ID')
|
||||
class ChallengeFlags(Resource):
|
||||
@admins_only
|
||||
def get(self, challenge_id):
|
||||
flags = Flags.query.filter_by(challenge_id=challenge_id).all()
|
||||
schema = FlagSchema(many=True)
|
||||
response = schema.dump(flags)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
from flask import request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Configs
|
||||
from CTFd.schemas.config import ConfigSchema
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only
|
||||
)
|
||||
from CTFd.utils import get_config, set_config
|
||||
from CTFd.cache import clear_config, clear_standings
|
||||
|
||||
configs_namespace = Namespace('configs', description="Endpoint to retrieve Configs")
|
||||
|
||||
|
||||
@configs_namespace.route('')
|
||||
class ConfigList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
configs = Configs.query.all()
|
||||
schema = ConfigSchema(many=True)
|
||||
response = schema.dump(configs)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors,
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = ConfigSchema()
|
||||
response = schema.load(req)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
clear_config()
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self):
|
||||
req = request.get_json()
|
||||
|
||||
for key, value in req.items():
|
||||
set_config(key=key, value=value)
|
||||
|
||||
clear_config()
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
|
||||
@configs_namespace.route('/<config_key>')
|
||||
class Config(Resource):
|
||||
@admins_only
|
||||
def get(self, config_key):
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': get_config(config_key)
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, config_key):
|
||||
config = Configs.query.filter_by(key=config_key).first()
|
||||
data = request.get_json()
|
||||
if config:
|
||||
schema = ConfigSchema(instance=config, partial=True)
|
||||
response = schema.load(data)
|
||||
else:
|
||||
schema = ConfigSchema()
|
||||
data['key'] = config_key
|
||||
response = schema.load(data)
|
||||
db.session.add(response.data)
|
||||
|
||||
if response.errors:
|
||||
return response.errors, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
clear_config()
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, config_key):
|
||||
config = Configs.query.filter_by(key=config_key).first_or_404()
|
||||
|
||||
db.session.delete(config)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
clear_config()
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
from flask import request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Files, ChallengeFiles, PageFiles
|
||||
from CTFd.schemas.files import FileSchema
|
||||
from CTFd.utils import uploads
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only
|
||||
)
|
||||
|
||||
files_namespace = Namespace('files', description="Endpoint to retrieve Files")
|
||||
|
||||
|
||||
@files_namespace.route('')
|
||||
class FilesList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
file_type = request.args.get('type')
|
||||
files = Files.query.filter_by(type=file_type).all()
|
||||
schema = FileSchema(many=True)
|
||||
response = schema.dump(files)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
files = request.files.getlist('file')
|
||||
# challenge_id
|
||||
# page_id
|
||||
|
||||
objs = []
|
||||
for f in files:
|
||||
# uploads.upload_file(file=f, chalid=req.get('challenge'))
|
||||
obj = uploads.upload_file(file=f, **request.form.to_dict())
|
||||
objs.append(obj)
|
||||
|
||||
schema = FileSchema(many=True)
|
||||
response = schema.dump(objs)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errorss
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@files_namespace.route('/<file_id>')
|
||||
class FilesDetail(Resource):
|
||||
@admins_only
|
||||
def get(self, file_id):
|
||||
f = Files.query.filter_by(id=file_id).first_or_404()
|
||||
schema = FileSchema()
|
||||
response = schema.dump(f)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, file_id):
|
||||
f = Files.query.filter_by(id=file_id).first_or_404()
|
||||
|
||||
db.session.delete(f)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Flags
|
||||
from CTFd.schemas.flags import FlagSchema
|
||||
from CTFd.plugins.flags import get_flag_class, FLAG_CLASSES
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only
|
||||
)
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
flags_namespace = Namespace('flags', description="Endpoint to retrieve Flags")
|
||||
|
||||
|
||||
@flags_namespace.route('')
|
||||
class FlagList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
flags = Flags.query.all()
|
||||
schema = FlagSchema(many=True)
|
||||
response = schema.dump(flags)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = FlagSchema()
|
||||
response = schema.load(req, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@flags_namespace.route('/types', defaults={'type_name': None})
|
||||
@flags_namespace.route('/types/<type_name>')
|
||||
class FlagTypes(Resource):
|
||||
@admins_only
|
||||
def get(self, type_name):
|
||||
if type_name:
|
||||
flag_class = get_flag_class(type_name)
|
||||
response = {
|
||||
'name': flag_class.name,
|
||||
'templates': flag_class.templates
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
else:
|
||||
response = {}
|
||||
for class_id in FLAG_CLASSES:
|
||||
flag_class = FLAG_CLASSES.get(class_id)
|
||||
response[class_id] = {
|
||||
'name': flag_class.name,
|
||||
'templates': flag_class.templates,
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@flags_namespace.route('/<flag_id>')
|
||||
class Flag(Resource):
|
||||
@admins_only
|
||||
def get(self, flag_id):
|
||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||
schema = FlagSchema()
|
||||
response = schema.dump(flag)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
response.data['templates'] = get_flag_class(flag.type).templates
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, flag_id):
|
||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||
|
||||
db.session.delete(flag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, flag_id):
|
||||
flag = Flags.query.filter_by(id=flag_id).first_or_404()
|
||||
schema = FlagSchema()
|
||||
req = request.get_json()
|
||||
|
||||
response = schema.load(req, session=db.session, instance=flag, partial=True)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
|
@ -0,0 +1,124 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Hints, HintUnlocks
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.utils.user import get_current_user
|
||||
from CTFd.schemas.hints import HintSchema
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only,
|
||||
authed_only
|
||||
)
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
hints_namespace = Namespace('hints', description="Endpoint to retrieve Hints")
|
||||
|
||||
|
||||
@hints_namespace.route('')
|
||||
class HintList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
hints = Hints.query.all()
|
||||
response = HintSchema(many=True).dump(hints)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = HintSchema('admin')
|
||||
response = schema.load(req, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@hints_namespace.route('/<hint_id>')
|
||||
class Hint(Resource):
|
||||
@during_ctf_time_only
|
||||
@authed_only
|
||||
def get(self, hint_id):
|
||||
user = get_current_user()
|
||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||
|
||||
view = 'unlocked'
|
||||
if hint.cost:
|
||||
view = 'locked'
|
||||
unlocked = HintUnlocks.query.filter_by(
|
||||
account_id=user.account_id,
|
||||
target=hint.id
|
||||
).first()
|
||||
if unlocked:
|
||||
view = 'unlocked'
|
||||
|
||||
response = HintSchema(view=view).dump(hint)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, hint_id):
|
||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||
req = request.get_json()
|
||||
|
||||
schema = HintSchema()
|
||||
response = schema.load(req, instance=hint, partial=True, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, hint_id):
|
||||
hint = Hints.query.filter_by(id=hint_id).first_or_404()
|
||||
db.session.delete(hint)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Notifications
|
||||
from CTFd.schemas.notifications import NotificationSchema
|
||||
from CTFd.utils.events import socketio
|
||||
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only
|
||||
)
|
||||
|
||||
notifications_namespace = Namespace('notifications', description="Endpoint to retrieve Notifications")
|
||||
|
||||
|
||||
@notifications_namespace.route('')
|
||||
class NotificantionList(Resource):
|
||||
def get(self):
|
||||
notifications = Notifications.query.all()
|
||||
schema = NotificationSchema(many=True)
|
||||
result = schema.dump(notifications)
|
||||
if result.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': result.errors
|
||||
}, 400
|
||||
return {
|
||||
'success': True,
|
||||
'data': result.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
|
||||
schema = NotificationSchema()
|
||||
result = schema.load(req)
|
||||
|
||||
if result.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': result.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(result.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(result.data)
|
||||
socketio.emit('notification', response.data, broadcast=True)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Pages
|
||||
from CTFd.schemas.pages import PageSchema
|
||||
from CTFd.utils.events import socketio
|
||||
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only
|
||||
)
|
||||
|
||||
pages_namespace = Namespace('pages', description="Endpoint to retrieve Pages")
|
||||
|
||||
|
||||
@pages_namespace.route('')
|
||||
class PageList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
pages = Pages.query.all()
|
||||
schema = PageSchema(exclude=['content'], many=True)
|
||||
response = schema.dump(pages)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = PageSchema()
|
||||
response = schema.load(req)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@pages_namespace.route('/<page_id>')
|
||||
class PageDetail(Resource):
|
||||
@admins_only
|
||||
def get(self, page_id):
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
schema = PageSchema()
|
||||
response = schema.dump(page)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, page_id):
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
req = request.get_json()
|
||||
|
||||
schema = PageSchema(partial=True)
|
||||
response = schema.load(req, instance=page, partial=True)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, page_id):
|
||||
page = Pages.query.filter_by(id=page_id).first_or_404()
|
||||
db.session.delete(page)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
|
@ -0,0 +1,114 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
|
||||
from CTFd.models import Solves, Awards, Teams
|
||||
from CTFd.utils.scores import get_standings
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.modes import TEAMS_MODE
|
||||
from CTFd.utils.dates import unix_time_to_utc, unix_time, isoformat
|
||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
||||
|
||||
scoreboard_namespace = Namespace('scoreboard', description="Endpoint to retrieve scores")
|
||||
|
||||
|
||||
@scoreboard_namespace.route('')
|
||||
class ScoreboardList(Resource):
|
||||
@check_account_visibility
|
||||
@check_score_visibility
|
||||
def get(self):
|
||||
standings = get_standings()
|
||||
response = []
|
||||
mode = get_config('user_mode')
|
||||
|
||||
if mode == TEAMS_MODE:
|
||||
team_ids = []
|
||||
for team in standings:
|
||||
team_ids.append(team.account_id)
|
||||
teams = Teams.query.filter(Teams.id.in_(team_ids)).all()
|
||||
teams = [next(t for t in teams if t.id == id) for id in team_ids]
|
||||
|
||||
for i, x in enumerate(standings):
|
||||
entry = {
|
||||
'pos': i + 1,
|
||||
'account_id': x.account_id,
|
||||
'oauth_id': x.oauth_id,
|
||||
'name': x.name,
|
||||
'score': int(x.score)
|
||||
}
|
||||
|
||||
if mode == TEAMS_MODE:
|
||||
members = []
|
||||
for member in teams[i].members:
|
||||
members.append({
|
||||
'id': member.id,
|
||||
'oauth_id': member.oauth_id,
|
||||
'name': member.name,
|
||||
'score': int(member.score)
|
||||
})
|
||||
|
||||
entry['members'] = members
|
||||
|
||||
response.append(
|
||||
entry
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@scoreboard_namespace.route('/top/<count>')
|
||||
@scoreboard_namespace.param('count', 'How many top teams to return')
|
||||
class ScoreboardDetail(Resource):
|
||||
@check_account_visibility
|
||||
@check_score_visibility
|
||||
def get(self, count):
|
||||
response = {}
|
||||
|
||||
standings = get_standings(count=count)
|
||||
|
||||
team_ids = [team.account_id for team in standings]
|
||||
|
||||
solves = Solves.query.filter(Solves.account_id.in_(team_ids))
|
||||
awards = Awards.query.filter(Awards.account_id.in_(team_ids))
|
||||
|
||||
freeze = get_config('freeze')
|
||||
|
||||
if freeze:
|
||||
solves = solves.filter(Solves.date < unix_time_to_utc(freeze))
|
||||
awards = awards.filter(Awards.date < unix_time_to_utc(freeze))
|
||||
|
||||
solves = solves.all()
|
||||
awards = awards.all()
|
||||
|
||||
for i, team in enumerate(team_ids):
|
||||
response[i + 1] = {
|
||||
'id': standings[i].account_id,
|
||||
'name': standings[i].name,
|
||||
'solves': []
|
||||
}
|
||||
for solve in solves:
|
||||
if solve.account_id == team:
|
||||
response[i + 1]['solves'].append({
|
||||
'challenge_id': solve.challenge_id,
|
||||
'account_id': solve.account_id,
|
||||
'team_id': solve.team_id,
|
||||
'user_id': solve.user_id,
|
||||
'value': solve.challenge.value,
|
||||
'date': isoformat(solve.date)
|
||||
})
|
||||
for award in awards:
|
||||
if award.account_id == team:
|
||||
response[i + 1]['solves'].append({
|
||||
'challenge_id': None,
|
||||
'account_id': award.account_id,
|
||||
'team_id': award.team_id,
|
||||
'user_id': award.user_id,
|
||||
'value': award.value,
|
||||
'date': isoformat(award.date)
|
||||
})
|
||||
response[i + 1]['solves'] = sorted(response[i + 1]['solves'], key=lambda k: k['date'])
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Challenges, Unlocks, Hints
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.utils.decorators import during_ctf_time_only, require_verified_emails
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
statistics_namespace = Namespace('statistics', description="Endpoint to retrieve Statistics")
|
||||
|
||||
from CTFd.api.v1.statistics import challenges
|
||||
from CTFd.api.v1.statistics import teams
|
||||
from CTFd.api.v1.statistics import users
|
||||
from CTFd.api.v1.statistics import submissions
|
|
@ -0,0 +1,128 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Challenges, Solves, Teams, Users
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils import config
|
||||
from CTFd.utils.modes import get_model
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only,
|
||||
during_ctf_time_only,
|
||||
require_verified_emails
|
||||
)
|
||||
from CTFd.api.v1.statistics import statistics_namespace
|
||||
from sqlalchemy import func
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
|
||||
@statistics_namespace.route('/challenges/<column>')
|
||||
class ChallengePropertyCounts(Resource):
|
||||
@admins_only
|
||||
def get(self, column):
|
||||
if column in Challenges.__table__.columns.keys():
|
||||
prop = getattr(Challenges, column)
|
||||
data = Challenges.query\
|
||||
.with_entities(prop, func.count(prop))\
|
||||
.group_by(prop)\
|
||||
.all()
|
||||
return {
|
||||
'success': True,
|
||||
'data': dict(data)
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
'message': 'That could not be found'
|
||||
}, 404
|
||||
return response
|
||||
|
||||
|
||||
@statistics_namespace.route('/challenges/solves')
|
||||
class ChallengeSolveStatistics(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
chals = Challenges.query \
|
||||
.filter(or_(Challenges.state != 'hidden', Challenges.state != 'locked')) \
|
||||
.order_by(Challenges.value) \
|
||||
.all()
|
||||
|
||||
Model = get_model()
|
||||
|
||||
solves_sub = db.session.query(
|
||||
Solves.challenge_id,
|
||||
db.func.count(Solves.challenge_id).label('solves')
|
||||
) \
|
||||
.join(Model, Solves.account_id == Model.id) \
|
||||
.filter(Model.banned == False, Model.hidden == False) \
|
||||
.group_by(Solves.challenge_id).subquery()
|
||||
|
||||
solves = db.session.query(
|
||||
solves_sub.columns.challenge_id,
|
||||
solves_sub.columns.solves,
|
||||
Challenges.name
|
||||
) \
|
||||
.join(Challenges, solves_sub.columns.challenge_id == Challenges.id).all()
|
||||
|
||||
response = []
|
||||
has_solves = []
|
||||
|
||||
for challenge_id, count, name in solves:
|
||||
challenge = {
|
||||
'id': challenge_id,
|
||||
'name': name,
|
||||
'solves': count,
|
||||
}
|
||||
response.append(challenge)
|
||||
has_solves.append(challenge_id)
|
||||
for c in chals:
|
||||
if c.id not in has_solves:
|
||||
challenge = {
|
||||
'id': c.id,
|
||||
'name': c.name,
|
||||
'solves': 0,
|
||||
}
|
||||
response.append(challenge)
|
||||
|
||||
db.session.close()
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
|
||||
@statistics_namespace.route('/challenges/solves/percentages')
|
||||
class ChallengeSolvePercentages(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
challenges = Challenges.query\
|
||||
.add_columns('id', 'name', 'state', 'max_attempts')\
|
||||
.order_by(Challenges.value).all()
|
||||
|
||||
Model = get_model()
|
||||
|
||||
teams_with_points = db.session.query(Solves.account_id) \
|
||||
.join(Model) \
|
||||
.filter(Model.banned == False, Model.hidden == False) \
|
||||
.group_by(Solves.account_id) \
|
||||
.count()
|
||||
|
||||
percentage_data = []
|
||||
for challenge in challenges:
|
||||
solve_count = Solves.query.join(Model, Solves.account_id == Model.id) \
|
||||
.filter(Solves.challenge_id == challenge.id, Model.banned == False, Model.hidden == False) \
|
||||
.count()
|
||||
|
||||
if teams_with_points > 0:
|
||||
percentage = (float(solve_count) / float(teams_with_points))
|
||||
else:
|
||||
percentage = 0.0
|
||||
|
||||
percentage_data.append({
|
||||
'id': challenge.id,
|
||||
'name': challenge.name,
|
||||
'percentage': percentage,
|
||||
})
|
||||
|
||||
response = sorted(percentage_data, key=lambda x: x['percentage'], reverse=True)
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Submissions
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only,
|
||||
)
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.api.v1.statistics import statistics_namespace
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
@statistics_namespace.route('/submissions/<column>')
|
||||
class SubmissionPropertyCounts(Resource):
|
||||
@admins_only
|
||||
def get(self, column):
|
||||
if column in Submissions.__table__.columns.keys():
|
||||
prop = getattr(Submissions, column)
|
||||
data = Submissions.query \
|
||||
.with_entities(prop, func.count(prop)) \
|
||||
.group_by(prop) \
|
||||
.all()
|
||||
return {
|
||||
'success': True,
|
||||
'data': dict(data)
|
||||
}
|
||||
else:
|
||||
response = {
|
||||
'success': False,
|
||||
'errors': 'That could not be found'
|
||||
}, 404
|
||||
return response
|
|
@ -0,0 +1,21 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Teams
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only,
|
||||
)
|
||||
from CTFd.api.v1.statistics import statistics_namespace
|
||||
|
||||
|
||||
@statistics_namespace.route('/teams')
|
||||
class TeamStatistics(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
registered = Teams.query.count()
|
||||
data = {
|
||||
'registered': registered,
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'data': data
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Users
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.api.v1.statistics import statistics_namespace
|
||||
from CTFd.utils.decorators import admins_only
|
||||
from sqlalchemy import func
|
||||
|
||||
|
||||
@statistics_namespace.route('/users')
|
||||
class UserStatistics(Resource):
|
||||
def get(self):
|
||||
registered = Users.query.count()
|
||||
confirmed = Users.query.filter_by(verified=True).count()
|
||||
data = {
|
||||
'registered': registered,
|
||||
'confirmed': confirmed
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'data': data
|
||||
}
|
||||
|
||||
|
||||
@statistics_namespace.route('/users/<column>')
|
||||
class UserPropertyCounts(Resource):
|
||||
@admins_only
|
||||
def get(self, column):
|
||||
if column in Users.__table__.columns.keys():
|
||||
prop = getattr(Users, column)
|
||||
data = Users.query \
|
||||
.with_entities(prop, func.count(prop)) \
|
||||
.group_by(prop) \
|
||||
.all()
|
||||
return {
|
||||
'success': True,
|
||||
'data': dict(data)
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'message': 'That could not be found'
|
||||
}, 404
|
|
@ -0,0 +1,96 @@
|
|||
from flask import session, jsonify, request, abort
|
||||
from flask_restplus import Namespace, Resource, reqparse
|
||||
|
||||
from CTFd.cache import cache, clear_standings
|
||||
from CTFd.utils.scores import get_standings
|
||||
from CTFd.models import db, Submissions
|
||||
from CTFd.schemas.submissions import SubmissionSchema
|
||||
from CTFd.utils.decorators import (
|
||||
admins_only,
|
||||
)
|
||||
|
||||
submissions_namespace = Namespace('submissions', description="Endpoint to retrieve Submission")
|
||||
|
||||
|
||||
@submissions_namespace.route('')
|
||||
class SubmissionsList(Resource):
|
||||
|
||||
@admins_only
|
||||
def get(self):
|
||||
args = request.args.to_dict()
|
||||
schema = SubmissionSchema(many=True)
|
||||
if args:
|
||||
submissions = Submissions.query.filter_by(**args).all()
|
||||
else:
|
||||
submissions = Submissions.query.all()
|
||||
|
||||
response = schema.dump(submissions)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
Model = Submissions.get_child(type=req.get('type'))
|
||||
schema = SubmissionSchema(instance=Model())
|
||||
response = schema.load(req)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
# Delete standings cache
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@submissions_namespace.route('/<submission_id>')
|
||||
@submissions_namespace.param('submission_id', 'A Submission ID')
|
||||
class Submission(Resource):
|
||||
@admins_only
|
||||
def get(self, submission_id):
|
||||
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
||||
schema = SubmissionSchema()
|
||||
response = schema.dump(submission)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, submission_id):
|
||||
submission = Submissions.query.filter_by(id=submission_id).first_or_404()
|
||||
db.session.delete(submission)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Tags
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
from CTFd.utils.dates import ctf_ended
|
||||
from CTFd.schemas.tags import TagSchema
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only
|
||||
)
|
||||
|
||||
tags_namespace = Namespace('tags', description="Endpoint to retrieve Tags")
|
||||
|
||||
|
||||
@tags_namespace.route('')
|
||||
class TagList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
# TODO: Filter by challenge_id
|
||||
tags = Tags.query.all()
|
||||
schema = TagSchema(many=True)
|
||||
response = schema.dump(tags)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = TagSchema()
|
||||
response = schema.load(req, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@tags_namespace.route('/<tag_id>')
|
||||
@tags_namespace.param('tag_id', 'A Tag ID')
|
||||
class Tag(Resource):
|
||||
@admins_only
|
||||
def get(self, tag_id):
|
||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||
|
||||
response = TagSchema().dump(tag)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, tag_id):
|
||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||
schema = TagSchema()
|
||||
req = request.get_json()
|
||||
|
||||
response = schema.load(req, session=db.session, instance=tag)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, tag_id):
|
||||
tag = Tags.query.filter_by(id=tag_id).first_or_404()
|
||||
db.session.delete(tag)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
|
@ -0,0 +1,293 @@
|
|||
from flask import session, request, abort
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Teams, Solves, Awards, Fails
|
||||
from CTFd.schemas.teams import TeamSchema
|
||||
from CTFd.schemas.submissions import SubmissionSchema
|
||||
from CTFd.schemas.awards import AwardSchema
|
||||
from CTFd.cache import cache, clear_standings
|
||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
||||
from CTFd.utils.config.visibility import (
|
||||
accounts_visible,
|
||||
scores_visible
|
||||
)
|
||||
from CTFd.utils.user import (
|
||||
get_current_team,
|
||||
is_admin,
|
||||
authed
|
||||
)
|
||||
from CTFd.utils.decorators import (
|
||||
authed_only,
|
||||
admins_only,
|
||||
)
|
||||
import copy
|
||||
|
||||
teams_namespace = Namespace('teams', description="Endpoint to retrieve Teams")
|
||||
|
||||
|
||||
@teams_namespace.route('')
|
||||
class TeamList(Resource):
|
||||
@check_account_visibility
|
||||
def get(self):
|
||||
teams = Teams.query.filter_by(banned=False)
|
||||
view = copy.deepcopy(TeamSchema.views.get(
|
||||
session.get('type', 'user')
|
||||
))
|
||||
view.remove('members')
|
||||
response = TeamSchema(view=view, many=True).dump(teams)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
view = TeamSchema.views.get(session.get('type', 'self'))
|
||||
schema = TeamSchema(view=view)
|
||||
response = schema.load(req)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@teams_namespace.route('/<team_id>')
|
||||
@teams_namespace.param('team_id', "Team ID")
|
||||
class TeamPublic(Resource):
|
||||
@check_account_visibility
|
||||
def get(self, team_id):
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
view = TeamSchema.views.get(session.get('type', 'user'))
|
||||
schema = TeamSchema(view=view)
|
||||
response = schema.dump(team)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, team_id):
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
data = request.get_json()
|
||||
data['id'] = team_id
|
||||
|
||||
schema = TeamSchema(view='admin', instance=team, partial=True)
|
||||
|
||||
response = schema.load(data)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, team_id):
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
for member in team.members:
|
||||
member.team_id = None
|
||||
|
||||
db.session.delete(team)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
}
|
||||
|
||||
|
||||
@teams_namespace.route('/me')
|
||||
@teams_namespace.param('team_id', "Current Team")
|
||||
class TeamPrivate(Resource):
|
||||
@authed_only
|
||||
def get(self):
|
||||
team = get_current_team()
|
||||
response = TeamSchema(view='self').dump(team)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@authed_only
|
||||
def patch(self):
|
||||
team = get_current_team()
|
||||
data = request.get_json()
|
||||
|
||||
response = TeamSchema(view='self', instance=team, partial=True).load(data)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = TeamSchema('self').dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@teams_namespace.route('/<team_id>/solves')
|
||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
||||
class TeamSolves(Resource):
|
||||
|
||||
def get(self, team_id):
|
||||
if team_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
team = get_current_team()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
solves = team.get_solves(
|
||||
admin=is_admin()
|
||||
)
|
||||
|
||||
view = 'admin' if is_admin() else 'user'
|
||||
schema = SubmissionSchema(view=view, many=True)
|
||||
response = schema.dump(solves)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@teams_namespace.route('/<team_id>/fails')
|
||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
||||
class TeamFails(Resource):
|
||||
|
||||
def get(self, team_id):
|
||||
if team_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
team = get_current_team()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
fails = team.get_fails(
|
||||
admin=is_admin()
|
||||
)
|
||||
|
||||
view = 'admin' if is_admin() else 'user'
|
||||
|
||||
schema = SubmissionSchema(view=view, many=True)
|
||||
response = schema.dump(fails)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
if is_admin():
|
||||
data = response.data
|
||||
else:
|
||||
data = []
|
||||
count = len(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': data,
|
||||
'meta': {
|
||||
'count': count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@teams_namespace.route('/<team_id>/awards')
|
||||
@teams_namespace.param('team_id', "Team ID or 'me'")
|
||||
class TeamAwards(Resource):
|
||||
|
||||
def get(self, team_id):
|
||||
if team_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
team = get_current_team()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
|
||||
awards = team.get_awards(
|
||||
admin=is_admin()
|
||||
)
|
||||
|
||||
schema = AwardSchema(many=True)
|
||||
response = schema.dump(awards)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
from flask import session, request
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, get_class_by_tablename, Unlocks, Awards
|
||||
from CTFd.utils.user import get_current_user
|
||||
from CTFd.schemas.unlocks import UnlockSchema
|
||||
from CTFd.schemas.awards import AwardSchema
|
||||
from CTFd.utils.decorators import (
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
admins_only,
|
||||
authed_only
|
||||
)
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
unlocks_namespace = Namespace('unlocks', description="Endpoint to retrieve Unlocks")
|
||||
|
||||
|
||||
@unlocks_namespace.route('')
|
||||
class UnlockList(Resource):
|
||||
@admins_only
|
||||
def get(self):
|
||||
hints = Unlocks.query.all()
|
||||
schema = UnlockSchema()
|
||||
response = schema.dump(hints)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@authed_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
user = get_current_user()
|
||||
|
||||
req['user_id'] = user.id
|
||||
req['team_id'] = user.team_id
|
||||
|
||||
Model = get_class_by_tablename(req['type'])
|
||||
target = Model.query.filter_by(id=req['target']).first_or_404()
|
||||
|
||||
if target.cost > user.score:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': {
|
||||
'score': 'You do not have enough points to unlock this hint'
|
||||
}
|
||||
}, 400
|
||||
|
||||
schema = UnlockSchema()
|
||||
response = schema.load(req, session=db.session)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
|
||||
award_schema = AwardSchema()
|
||||
award = {
|
||||
'user_id': user.id,
|
||||
'team_id': user.team_id,
|
||||
'name': target.name,
|
||||
'description': target.description,
|
||||
'value': (-target.cost),
|
||||
'category': target.category
|
||||
}
|
||||
|
||||
award = award_schema.load(award)
|
||||
db.session.add(award.data)
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
|
@ -0,0 +1,280 @@
|
|||
from flask import request, abort
|
||||
from flask_restplus import Namespace, Resource
|
||||
from CTFd.models import db, Users, Solves, Awards, Fails, Tracking, Unlocks, Submissions, Notifications
|
||||
from CTFd.utils.decorators import (
|
||||
authed_only,
|
||||
admins_only,
|
||||
authed
|
||||
)
|
||||
from CTFd.cache import cache, clear_standings
|
||||
from CTFd.utils.user import get_current_user, is_admin
|
||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
||||
|
||||
from CTFd.utils.config.visibility import (
|
||||
accounts_visible,
|
||||
challenges_visible,
|
||||
registration_visible,
|
||||
scores_visible
|
||||
)
|
||||
|
||||
from CTFd.schemas.submissions import SubmissionSchema
|
||||
from CTFd.schemas.awards import AwardSchema
|
||||
from CTFd.schemas.users import UserSchema
|
||||
|
||||
|
||||
users_namespace = Namespace('users', description="Endpoint to retrieve Users")
|
||||
|
||||
|
||||
@users_namespace.route('')
|
||||
class UserList(Resource):
|
||||
@check_account_visibility
|
||||
def get(self):
|
||||
users = Users.query.filter_by(banned=False)
|
||||
response = UserSchema(view='user', many=True).dump(users)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def post(self):
|
||||
req = request.get_json()
|
||||
schema = UserSchema('admin')
|
||||
response = schema.load(req)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.add(response.data)
|
||||
db.session.commit()
|
||||
|
||||
clear_standings()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@users_namespace.route('/<int:user_id>')
|
||||
@users_namespace.param('user_id', "User ID")
|
||||
class UserPublic(Resource):
|
||||
@check_account_visibility
|
||||
def get(self, user_id):
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
response = UserSchema('self').dump(user)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
response.data['place'] = user.place
|
||||
response.data['score'] = user.score
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def patch(self, user_id):
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
data = request.get_json()
|
||||
data['id'] = user_id
|
||||
schema = UserSchema(view='admin', instance=user, partial=True)
|
||||
response = schema.load(data)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
@admins_only
|
||||
def delete(self, user_id):
|
||||
Notifications.query.filter_by(user_id=user_id).delete()
|
||||
Awards.query.filter_by(user_id=user_id).delete()
|
||||
Unlocks.query.filter_by(user_id=user_id).delete()
|
||||
Submissions.query.filter_by(user_id=user_id).delete()
|
||||
Solves.query.filter_by(user_id=user_id).delete()
|
||||
Tracking.query.filter_by(user_id=user_id).delete()
|
||||
Users.query.filter_by(id=user_id).delete()
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True
|
||||
}
|
||||
|
||||
|
||||
@users_namespace.route('/me')
|
||||
class UserPrivate(Resource):
|
||||
@authed_only
|
||||
def get(self):
|
||||
user = get_current_user()
|
||||
response = UserSchema('self').dump(user).data
|
||||
response['place'] = user.place
|
||||
response['score'] = user.score
|
||||
return {
|
||||
'success': True,
|
||||
'data': response
|
||||
}
|
||||
|
||||
@authed_only
|
||||
def patch(self):
|
||||
user = get_current_user()
|
||||
data = request.get_json()
|
||||
schema = UserSchema(view='self', instance=user, partial=True)
|
||||
response = schema.load(data)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
db.session.commit()
|
||||
|
||||
response = schema.dump(response.data)
|
||||
db.session.close()
|
||||
|
||||
clear_standings()
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@users_namespace.route('/<user_id>/solves')
|
||||
@users_namespace.param('user_id', "User ID or 'me'")
|
||||
class UserSolves(Resource):
|
||||
def get(self, user_id):
|
||||
if user_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
user = get_current_user()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
solves = user.get_solves(
|
||||
admin=is_admin()
|
||||
)
|
||||
for solve in solves:
|
||||
setattr(solve, 'value', 100)
|
||||
|
||||
view = 'user' if not is_admin() else 'admin'
|
||||
response = SubmissionSchema(view=view, many=True).dump(solves)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
||||
|
||||
|
||||
@users_namespace.route('/<user_id>/fails')
|
||||
@users_namespace.param('user_id', "User ID or 'me'")
|
||||
class UserFails(Resource):
|
||||
def get(self, user_id):
|
||||
if user_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
user = get_current_user()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
fails = user.get_fails(
|
||||
admin=is_admin()
|
||||
)
|
||||
|
||||
view = 'user' if not is_admin() else 'admin'
|
||||
response = SubmissionSchema(view=view, many=True).dump(fails)
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
if is_admin():
|
||||
data = response.data
|
||||
else:
|
||||
data = []
|
||||
count = len(response.data)
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': data,
|
||||
'meta': {
|
||||
'count': count
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@users_namespace.route('/<user_id>/awards')
|
||||
@users_namespace.param('user_id', "User ID or 'me'")
|
||||
class UserAwards(Resource):
|
||||
def get(self, user_id):
|
||||
if user_id == 'me':
|
||||
if not authed():
|
||||
abort(403)
|
||||
user = get_current_user()
|
||||
else:
|
||||
if accounts_visible() is False or scores_visible() is False:
|
||||
abort(404)
|
||||
user = Users.query.filter_by(id=user_id).first_or_404()
|
||||
|
||||
awards = user.get_awards(
|
||||
admin=is_admin()
|
||||
)
|
||||
|
||||
view = 'user' if not is_admin() else 'admin'
|
||||
response = AwardSchema(view=view, many=True).dump(awards)
|
||||
|
||||
if response.errors:
|
||||
return {
|
||||
'success': False,
|
||||
'errors': response.errors
|
||||
}, 400
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'data': response.data
|
||||
}
|
324
CTFd/auth.py
324
CTFd/auth.py
|
@ -1,17 +1,21 @@
|
|||
import logging
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
|
||||
from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint
|
||||
from itsdangerous import TimedSerializer, BadTimeSignature, Signer, BadSignature
|
||||
from flask import current_app as app, render_template, request, redirect, url_for, session, Blueprint, abort
|
||||
from passlib.hash import bcrypt_sha256
|
||||
|
||||
from CTFd.models import db, Teams
|
||||
from CTFd import utils
|
||||
from CTFd.utils import ratelimit
|
||||
from CTFd.models import db, Users, Teams
|
||||
|
||||
from CTFd.utils import get_config, get_app_config
|
||||
from CTFd.utils.decorators import ratelimit
|
||||
from CTFd.utils import user as current_user
|
||||
from CTFd.utils import config, validators
|
||||
from CTFd.utils import email
|
||||
from CTFd.utils.security.auth import login_user, logout_user
|
||||
from CTFd.utils.logging import log
|
||||
from CTFd.utils.decorators.visibility import check_registration_visibility
|
||||
from CTFd.utils.modes import TEAMS_MODE, USERS_MODE
|
||||
from CTFd.utils.security.signing import serialize, unserialize, SignatureExpired, BadSignature, BadTimeSignature
|
||||
|
||||
import base64
|
||||
import requests
|
||||
|
||||
auth = Blueprint('auth', __name__)
|
||||
|
||||
|
@ -19,40 +23,34 @@ auth = Blueprint('auth', __name__)
|
|||
@auth.route('/confirm', methods=['POST', 'GET'])
|
||||
@auth.route('/confirm/<data>', methods=['GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def confirm_user(data=None):
|
||||
if not utils.get_config('verify_emails'):
|
||||
def confirm(data=None):
|
||||
if not get_config('verify_emails'):
|
||||
# If the CTF doesn't care about confirming email addresses then redierct to challenges
|
||||
return redirect(url_for('challenges.challenges_view'))
|
||||
return redirect(url_for('challenges.listing'))
|
||||
|
||||
logger = logging.getLogger('regs')
|
||||
# User is confirming email account
|
||||
if data and request.method == "GET":
|
||||
try:
|
||||
s = TimedSerializer(app.config['SECRET_KEY'])
|
||||
email = s.loads(utils.base64decode(data), max_age=1800)
|
||||
except BadTimeSignature:
|
||||
user_email = unserialize(data, max_age=1800)
|
||||
except (BadTimeSignature, SignatureExpired):
|
||||
return render_template('confirm.html', errors=['Your confirmation link has expired'])
|
||||
except (BadSignature, TypeError, base64.binascii.Error):
|
||||
return render_template('confirm.html', errors=['Your confirmation token is invalid'])
|
||||
team = Teams.query.filter_by(email=email).first_or_404()
|
||||
|
||||
team = Users.query.filter_by(email=user_email).first_or_404()
|
||||
team.verified = True
|
||||
log('registrations', format="[{date}] {ip} - successful password reset for {name}")
|
||||
db.session.commit()
|
||||
logger.warn("[{date}] {ip} - {username} confirmed their account".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=team.name.encode('utf-8'),
|
||||
email=team.email.encode('utf-8')
|
||||
))
|
||||
db.session.close()
|
||||
if utils.authed():
|
||||
return redirect(url_for('challenges.challenges_view'))
|
||||
if current_user.authed():
|
||||
return redirect(url_for('challenges.listing'))
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
# User is trying to start or restart the confirmation flow
|
||||
if not utils.authed():
|
||||
if not current_user.authed():
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
team = Teams.query.filter_by(id=session['id']).first_or_404()
|
||||
team = Users.query.filter_by(id=session['id']).first_or_404()
|
||||
|
||||
if data is None:
|
||||
if request.method == "POST":
|
||||
|
@ -60,17 +58,12 @@ def confirm_user(data=None):
|
|||
if team.verified:
|
||||
return redirect(url_for('views.profile'))
|
||||
else:
|
||||
utils.verify_email(team.email)
|
||||
logger.warn("[{date}] {ip} - {username} initiated a confirmation email resend".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=team.name.encode('utf-8'),
|
||||
email=team.email.encode('utf-8')
|
||||
))
|
||||
email.verify_email_address(team.email)
|
||||
log('registrations', format="[{date}] {ip} - {name} initiated a confirmation email resend")
|
||||
return render_template('confirm.html', team=team, infos=['Your confirmation email has been resent!'])
|
||||
elif request.method == "GET":
|
||||
# User has been directed to the confirm page
|
||||
team = Teams.query.filter_by(id=session['id']).first_or_404()
|
||||
team = Users.query.filter_by(id=session['id']).first_or_404()
|
||||
if team.verified:
|
||||
# If user is already verified, redirect to their profile
|
||||
return redirect(url_for('views.profile'))
|
||||
|
@ -81,13 +74,10 @@ def confirm_user(data=None):
|
|||
@auth.route('/reset_password/<data>', methods=['POST', 'GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=60)
|
||||
def reset_password(data=None):
|
||||
logger = logging.getLogger('logins')
|
||||
|
||||
if data is not None:
|
||||
try:
|
||||
s = TimedSerializer(app.config['SECRET_KEY'])
|
||||
name = s.loads(utils.base64decode(data), max_age=1800)
|
||||
except BadTimeSignature:
|
||||
name = unserialize(data, max_age=1800)
|
||||
except (BadTimeSignature, SignatureExpired):
|
||||
return render_template('reset_password.html', errors=['Your link has expired'])
|
||||
except (BadSignature, TypeError, base64.binascii.Error):
|
||||
return render_template('reset_password.html', errors=['Your reset token is invalid'])
|
||||
|
@ -95,24 +85,20 @@ def reset_password(data=None):
|
|||
if request.method == "GET":
|
||||
return render_template('reset_password.html', mode='set')
|
||||
if request.method == "POST":
|
||||
team = Teams.query.filter_by(name=name).first_or_404()
|
||||
team = Users.query.filter_by(name=name).first_or_404()
|
||||
team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
|
||||
db.session.commit()
|
||||
logger.warn("[{date}] {ip} - successful password reset for {username}".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=team.name.encode('utf-8')
|
||||
))
|
||||
log('logins', format="[{date}] {ip} - successful password reset for {name}")
|
||||
db.session.close()
|
||||
return redirect(url_for('auth.login'))
|
||||
|
||||
if request.method == 'POST':
|
||||
email = request.form['email'].strip()
|
||||
team = Teams.query.filter_by(email=email).first()
|
||||
email_address = request.form['email'].strip()
|
||||
team = Users.query.filter_by(email=email_address).first()
|
||||
|
||||
errors = []
|
||||
|
||||
if utils.can_send_mail() is False:
|
||||
if config.can_send_mail() is False:
|
||||
return render_template(
|
||||
'reset_password.html',
|
||||
errors=['Email could not be sent due to server misconfiguration']
|
||||
|
@ -124,7 +110,7 @@ def reset_password(data=None):
|
|||
errors=['If that account exists you will receive an email, please check your inbox']
|
||||
)
|
||||
|
||||
utils.forgot_password(email, team.name)
|
||||
email.forgot_password(email_address, team.name)
|
||||
|
||||
return render_template(
|
||||
'reset_password.html',
|
||||
|
@ -134,27 +120,36 @@ def reset_password(data=None):
|
|||
|
||||
|
||||
@auth.route('/register', methods=['POST', 'GET'])
|
||||
@check_registration_visibility
|
||||
@ratelimit(method="POST", limit=10, interval=5)
|
||||
def register():
|
||||
logger = logging.getLogger('regs')
|
||||
if not utils.can_register():
|
||||
return redirect(url_for('auth.login'))
|
||||
if request.method == 'POST':
|
||||
errors = []
|
||||
name = request.form['name']
|
||||
email = request.form['email']
|
||||
email_address = request.form['email']
|
||||
password = request.form['password']
|
||||
|
||||
name_len = len(name) == 0
|
||||
names = Teams.query.add_columns('name', 'id').filter_by(name=name).first()
|
||||
emails = Teams.query.add_columns('email', 'id').filter_by(email=email).first()
|
||||
names = Users.query.add_columns('name', 'id').filter_by(name=name).first()
|
||||
emails = Users.query.add_columns('email', 'id').filter_by(email=email_address).first()
|
||||
pass_short = len(password) == 0
|
||||
pass_long = len(password) > 128
|
||||
valid_email = utils.check_email_format(request.form['email'])
|
||||
team_name_email_check = utils.check_email_format(name)
|
||||
valid_email = validators.validate_email(request.form['email'])
|
||||
team_name_email_check = validators.validate_email(name)
|
||||
|
||||
local_id, _, domain = email_address.partition('@')
|
||||
|
||||
domain_whitelist = get_config('domain_whitelist')
|
||||
|
||||
if not valid_email:
|
||||
errors.append("Please enter a valid email address")
|
||||
if domain_whitelist:
|
||||
domain_whitelist = domain_whitelist.split(',')
|
||||
if domain not in domain_whitelist:
|
||||
errors.append(
|
||||
"Only email addresses under {domains} may register".format(
|
||||
domains=', '.join(domain_whitelist))
|
||||
)
|
||||
if names:
|
||||
errors.append('That team name is already taken')
|
||||
if team_name_email_check is True:
|
||||
|
@ -169,42 +164,41 @@ def register():
|
|||
errors.append('Pick a longer team name')
|
||||
|
||||
if len(errors) > 0:
|
||||
return render_template('register.html', errors=errors, name=request.form['name'], email=request.form['email'], password=request.form['password'])
|
||||
return render_template(
|
||||
'register.html',
|
||||
errors=errors,
|
||||
name=request.form['name'],
|
||||
email=request.form['email'],
|
||||
password=request.form['password']
|
||||
)
|
||||
else:
|
||||
with app.app_context():
|
||||
team = Teams(name, email.lower(), password)
|
||||
db.session.add(team)
|
||||
user = Users(
|
||||
name=name.strip(),
|
||||
email=email_address.lower(),
|
||||
password=password.strip()
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
db.session.flush()
|
||||
|
||||
session['username'] = team.name
|
||||
session['id'] = team.id
|
||||
session['admin'] = team.admin
|
||||
session['nonce'] = utils.sha512(os.urandom(10))
|
||||
login_user(user)
|
||||
|
||||
if utils.can_send_mail() and utils.get_config('verify_emails'): # Confirming users is enabled and we can send email.
|
||||
logger = logging.getLogger('regs')
|
||||
logger.warn("[{date}] {ip} - {username} registered (UNCONFIRMED) with {email}".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=request.form['name'].encode('utf-8'),
|
||||
email=request.form['email'].encode('utf-8')
|
||||
))
|
||||
utils.verify_email(team.email)
|
||||
if config.can_send_mail() and get_config('verify_emails'): # Confirming users is enabled and we can send email.
|
||||
log('registrations', format="[{date}] {ip} - {name} registered (UNCONFIRMED) with {email}")
|
||||
email.verify_email_address(user.email)
|
||||
db.session.close()
|
||||
return redirect(url_for('auth.confirm_user'))
|
||||
return redirect(url_for('auth.confirm'))
|
||||
else: # Don't care about confirming users
|
||||
if utils.can_send_mail(): # We want to notify the user that they have registered.
|
||||
utils.sendmail(request.form['email'], "You've successfully registered for {}".format(utils.get_config('ctf_name')))
|
||||
if config.can_send_mail(): # We want to notify the user that they have registered.
|
||||
email.sendmail(
|
||||
request.form['email'],
|
||||
"You've successfully registered for {}".format(get_config('ctf_name'))
|
||||
)
|
||||
|
||||
logger.warn("[{date}] {ip} - {username} registered with {email}".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=request.form['name'].encode('utf-8'),
|
||||
email=request.form['email'].encode('utf-8')
|
||||
))
|
||||
log('registrations', "[{date}] {ip} - {name} registered with {email}")
|
||||
db.session.close()
|
||||
return redirect(url_for('challenges.challenges_view'))
|
||||
return redirect(url_for('challenges.listing'))
|
||||
else:
|
||||
return render_template('register.html')
|
||||
|
||||
|
@ -212,65 +206,149 @@ def register():
|
|||
@auth.route('/login', methods=['POST', 'GET'])
|
||||
@ratelimit(method="POST", limit=10, interval=5)
|
||||
def login():
|
||||
logger = logging.getLogger('logins')
|
||||
if request.method == 'POST':
|
||||
errors = []
|
||||
name = request.form['name']
|
||||
|
||||
# Check if the user submitted an email address or a team name
|
||||
if utils.check_email_format(name) is True:
|
||||
team = Teams.query.filter_by(email=name).first()
|
||||
if validators.validate_email(name) is True:
|
||||
user = Users.query.filter_by(email=name).first()
|
||||
else:
|
||||
team = Teams.query.filter_by(name=name).first()
|
||||
user = Users.query.filter_by(name=name).first()
|
||||
|
||||
if user:
|
||||
if user and bcrypt_sha256.verify(request.form['password'], user.password):
|
||||
session.regenerate()
|
||||
|
||||
login_user(user)
|
||||
log('logins', "[{date}] {ip} - {name} logged in")
|
||||
|
||||
if team:
|
||||
if team and bcrypt_sha256.verify(request.form['password'], team.password):
|
||||
try:
|
||||
session.regenerate() # NO SESSION FIXATION FOR YOU
|
||||
except:
|
||||
pass # TODO: Some session objects don't implement regenerate :(
|
||||
session['username'] = team.name
|
||||
session['id'] = team.id
|
||||
session['admin'] = team.admin
|
||||
session['nonce'] = utils.sha512(os.urandom(10))
|
||||
db.session.close()
|
||||
|
||||
logger.warn("[{date}] {ip} - {username} logged in".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=session['username'].encode('utf-8')
|
||||
))
|
||||
|
||||
if request.args.get('next') and utils.is_safe_url(request.args.get('next')):
|
||||
if request.args.get('next') and validators.is_safe_url(request.args.get('next')):
|
||||
return redirect(request.args.get('next'))
|
||||
return redirect(url_for('challenges.challenges_view'))
|
||||
return redirect(url_for('challenges.listing'))
|
||||
|
||||
else: # This user exists but the password is wrong
|
||||
logger.warn("[{date}] {ip} - submitted invalid password for {username}".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip(),
|
||||
username=team.name.encode('utf-8')
|
||||
))
|
||||
else:
|
||||
# This user exists but the password is wrong
|
||||
log('logins', "[{date}] {ip} - submitted invalid password for {name}")
|
||||
errors.append("Your username or password is incorrect")
|
||||
db.session.close()
|
||||
return render_template('login.html', errors=errors)
|
||||
|
||||
else: # This user just doesn't exist
|
||||
logger.warn("[{date}] {ip} - submitted invalid account information".format(
|
||||
date=time.strftime("%m/%d/%Y %X"),
|
||||
ip=utils.get_ip()
|
||||
))
|
||||
else:
|
||||
# This user just doesn't exist
|
||||
log('logins', "[{date}] {ip} - submitted invalid account information")
|
||||
errors.append("Your username or password is incorrect")
|
||||
db.session.close()
|
||||
return render_template('login.html', errors=errors)
|
||||
|
||||
else:
|
||||
db.session.close()
|
||||
return render_template('login.html')
|
||||
|
||||
|
||||
@auth.route('/oauth')
|
||||
def oauth_login():
|
||||
endpoint = get_app_config('OAUTH_AUTHORIZATION_ENDPOINT') \
|
||||
or get_config('oauth_authorization_endpoint') \
|
||||
or 'https://auth.majorleaguecyber.org/oauth/authorize'
|
||||
|
||||
if get_config('user_mode') == 'teams':
|
||||
scope = 'profile team'
|
||||
else:
|
||||
scope = 'profile'
|
||||
|
||||
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
|
||||
redirect_url = "{endpoint}?response_type=code&client_id={client_id}&scope={scope}&state={state}".format(
|
||||
endpoint=endpoint,
|
||||
client_id=client_id,
|
||||
scope=scope,
|
||||
state=session['nonce']
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
|
||||
|
||||
@auth.route('/redirect', methods=['GET'])
|
||||
@ratelimit(method="GET", limit=10, interval=60)
|
||||
def oauth_redirect():
|
||||
oauth_code = request.args.get('code')
|
||||
state = request.args.get('state')
|
||||
if session['nonce'] != state:
|
||||
log('logins', "[{date}] {ip} - OAuth State validation mismatch")
|
||||
abort(500)
|
||||
|
||||
if oauth_code:
|
||||
url = get_app_config('OAUTH_TOKEN_ENDPOINT') \
|
||||
or get_config('oauth_token_endpoint') \
|
||||
or 'https://auth.majorleaguecyber.org/oauth/token'
|
||||
|
||||
client_id = get_app_config('OAUTH_CLIENT_ID') or get_config('oauth_client_id')
|
||||
client_secret = get_app_config('OAUTH_CLIENT_SECRET') or get_config('oauth_client_secret')
|
||||
headers = {
|
||||
'content-type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
data = {
|
||||
'code': oauth_code,
|
||||
'client_id': client_id,
|
||||
'client_secret': client_secret,
|
||||
'grant_type': 'authorization_code'
|
||||
}
|
||||
token_request = requests.post(url, data=data, headers=headers)
|
||||
|
||||
if token_request.status_code == requests.codes.ok:
|
||||
token = token_request.json()['access_token']
|
||||
user_url = get_app_config('OAUTH_API_ENDPOINT') \
|
||||
or get_config('oauth_api_endpoint') \
|
||||
or 'http://api.majorleaguecyber.org/user'
|
||||
|
||||
headers = {
|
||||
'Authorization': 'Bearer ' + str(token),
|
||||
'Content-type': 'application/json'
|
||||
}
|
||||
api_data = requests.get(url=user_url, headers=headers).json()
|
||||
|
||||
user_id = api_data['id']
|
||||
user_name = api_data['name']
|
||||
user_email = api_data['email']
|
||||
|
||||
user = Users.query.filter_by(email=user_email).first()
|
||||
if user is None:
|
||||
user = Users(
|
||||
name=user_name,
|
||||
email=user_email,
|
||||
oauth_id=user_id,
|
||||
verified=True
|
||||
)
|
||||
db.session.add(user)
|
||||
db.session.commit()
|
||||
|
||||
if get_config('user_mode') == TEAMS_MODE:
|
||||
team_id = api_data['team']['id']
|
||||
team_name = api_data['team']['name']
|
||||
|
||||
team = Teams.query.filter_by(oauth_id=team_id).first()
|
||||
if team is None:
|
||||
team = Teams(
|
||||
name=team_name,
|
||||
oauth_id=team_id
|
||||
)
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
|
||||
team.members.append(user)
|
||||
db.session.commit()
|
||||
|
||||
login_user(user)
|
||||
|
||||
return redirect(url_for('challenges.listing'))
|
||||
else:
|
||||
log('logins', "[{date}] {ip} - OAuth token retrieval failure")
|
||||
abort(500)
|
||||
else:
|
||||
log('logins', "[{date}] {ip} - Received redirect without OAuth code")
|
||||
abort(500)
|
||||
|
||||
|
||||
@auth.route('/logout')
|
||||
def logout():
|
||||
if utils.authed():
|
||||
session.clear()
|
||||
if current_user.authed():
|
||||
logout_user()
|
||||
return redirect(url_for('views.static_html'))
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
from flask_caching import Cache
|
||||
|
||||
cache = Cache()
|
||||
|
||||
|
||||
def clear_config():
|
||||
from CTFd.utils import get_config, get_app_config
|
||||
cache.delete_memoized(get_config)
|
||||
cache.delete_memoized(get_app_config)
|
||||
|
||||
|
||||
def clear_standings():
|
||||
from CTFd.utils.scores import get_standings
|
||||
cache.delete_memoized(get_standings)
|
|
@ -1,427 +1,40 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
|
||||
from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint, abort
|
||||
from sqlalchemy.sql import or_
|
||||
|
||||
from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards, Hints, Unlocks
|
||||
from CTFd.plugins.keys import get_key_class
|
||||
from CTFd.models import db, Challenges, Files, Solves, Fails, Flags, Tags, Teams, Awards, Hints, Unlocks
|
||||
from CTFd.plugins.challenges import get_chal_class
|
||||
|
||||
from CTFd import utils
|
||||
from CTFd.utils.decorators import (
|
||||
authed_only,
|
||||
admins_only,
|
||||
during_ctf_time_only,
|
||||
require_verified_emails,
|
||||
viewable_without_authentication
|
||||
ratelimit,
|
||||
require_team
|
||||
)
|
||||
from CTFd.utils import text_type
|
||||
from CTFd.utils.decorators.visibility import check_challenge_visibility
|
||||
from CTFd.utils import config, text_type, user as current_user, get_config
|
||||
from CTFd.utils.dates import ctftime, ctf_started, ctf_paused, ctf_ended, unix_time, unix_time_to_utc
|
||||
|
||||
challenges = Blueprint('challenges', __name__)
|
||||
|
||||
|
||||
@challenges.route('/hints/<int:hintid>', methods=['GET', 'POST'])
|
||||
@during_ctf_time_only
|
||||
@authed_only
|
||||
def hints_view(hintid):
|
||||
hint = Hints.query.filter_by(id=hintid).first_or_404()
|
||||
chal = Challenges.query.filter_by(id=hint.chal).first()
|
||||
unlock = Unlocks.query.filter_by(model='hints', itemid=hintid, teamid=session['id']).first()
|
||||
if request.method == 'GET':
|
||||
if unlock:
|
||||
return jsonify({
|
||||
'hint': hint.hint,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost
|
||||
})
|
||||
else:
|
||||
return jsonify({
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost
|
||||
})
|
||||
elif request.method == 'POST':
|
||||
if unlock is None: # The user does not have an unlock.
|
||||
if utils.ctftime() or (utils.ctf_ended() and utils.view_after_ctf()) or utils.is_admin() is True:
|
||||
# It's ctftime or the CTF has ended (but we allow views after)
|
||||
team = Teams.query.filter_by(id=session['id']).first()
|
||||
if team.score() < hint.cost:
|
||||
return jsonify({'errors': 'Not enough points'})
|
||||
unlock = Unlocks(model='hints', teamid=session['id'], itemid=hint.id)
|
||||
award = Awards(teamid=session['id'], name=text_type('Hint for {}'.format(chal.name)), value=(-hint.cost))
|
||||
db.session.add(unlock)
|
||||
db.session.add(award)
|
||||
db.session.commit()
|
||||
json_data = {
|
||||
'hint': hint.hint,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost
|
||||
}
|
||||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
elif utils.ctf_ended(): # The CTF has ended. No views after.
|
||||
abort(403)
|
||||
else: # The user does have an unlock, we should give them their hint.
|
||||
json_data = {
|
||||
'hint': hint.hint,
|
||||
'chal': hint.chal,
|
||||
'cost': hint.cost
|
||||
}
|
||||
db.session.close()
|
||||
return jsonify(json_data)
|
||||
|
||||
|
||||
@challenges.route('/challenges', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@viewable_without_authentication()
|
||||
def challenges_view():
|
||||
@check_challenge_visibility
|
||||
@require_team
|
||||
def listing():
|
||||
infos = []
|
||||
errors = []
|
||||
start = utils.get_config('start') or 0
|
||||
end = utils.get_config('end') or 0
|
||||
start = get_config('start') or 0
|
||||
end = get_config('end') or 0
|
||||
|
||||
if utils.ctf_paused():
|
||||
infos.append('{} is paused'.format(utils.ctf_name()))
|
||||
if ctf_paused():
|
||||
infos.append('{} is paused'.format(config.ctf_name()))
|
||||
|
||||
if not utils.ctftime():
|
||||
# It is not CTF time
|
||||
if utils.view_after_ctf(): # But we are allowed to view after the CTF ends
|
||||
pass
|
||||
else: # We are NOT allowed to view after the CTF ends
|
||||
if utils.get_config('start') and not utils.ctf_started():
|
||||
errors.append('{} has not started yet'.format(utils.ctf_name()))
|
||||
if (utils.get_config('end') and utils.ctf_ended()) and not utils.view_after_ctf():
|
||||
errors.append('{} has ended'.format(utils.ctf_name()))
|
||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||
if not ctftime():
|
||||
if ctf_started() is False:
|
||||
errors.append('{} has not started yet'.format(config.ctf_name()))
|
||||
if ctf_ended():
|
||||
errors.append('{} has ended'.format(config.ctf_name()))
|
||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||
|
||||
return render_template('challenges.html', infos=infos, errors=errors, start=int(start), end=int(end))
|
||||
|
||||
|
||||
@challenges.route('/chals', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def chals():
|
||||
db_chals = Challenges.query.filter(or_(Challenges.hidden != True, Challenges.hidden == None)).order_by(Challenges.value).all()
|
||||
response = {'game': []}
|
||||
for chal in db_chals:
|
||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||
chal_type = get_chal_class(chal.type)
|
||||
response['game'].append({
|
||||
'id': chal.id,
|
||||
'type': chal_type.name,
|
||||
'name': chal.name,
|
||||
'value': chal.value,
|
||||
'category': chal.category,
|
||||
'tags': tags,
|
||||
'template': chal_type.templates['modal'],
|
||||
'script': chal_type.scripts['modal'],
|
||||
})
|
||||
|
||||
db.session.close()
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chals/<int:chal_id>', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@require_verified_emails
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def chal_view(chal_id):
|
||||
teamid = session.get('id')
|
||||
|
||||
chal = Challenges.query.filter_by(id=chal_id).first_or_404()
|
||||
if chal.hidden:
|
||||
abort(404)
|
||||
chal_class = get_chal_class(chal.type)
|
||||
|
||||
tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=chal.id).all()]
|
||||
files = [str(f.location) for f in Files.query.filter_by(chal=chal.id).all()]
|
||||
unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=teamid)])
|
||||
hints = []
|
||||
|
||||
for hint in Hints.query.filter_by(chal=chal.id).all():
|
||||
if hint.id in unlocked_hints or utils.ctf_ended():
|
||||
hints.append({'id': hint.id, 'cost': hint.cost, 'hint': hint.hint})
|
||||
else:
|
||||
hints.append({'id': hint.id, 'cost': hint.cost})
|
||||
|
||||
challenge, response = chal_class.read(challenge=chal)
|
||||
|
||||
response['files'] = files
|
||||
response['tags'] = tags
|
||||
response['hints'] = hints
|
||||
|
||||
db.session.close()
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chals/solves')
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def solves_per_chal():
|
||||
chals = Challenges.query\
|
||||
.filter(or_(Challenges.hidden != True, Challenges.hidden == None))\
|
||||
.order_by(Challenges.value)\
|
||||
.all()
|
||||
|
||||
solves_sub = db.session.query(
|
||||
Solves.chalid,
|
||||
db.func.count(Solves.chalid).label('solves')
|
||||
)\
|
||||
.join(Teams, Solves.teamid == Teams.id) \
|
||||
.filter(Teams.banned == False) \
|
||||
.group_by(Solves.chalid).subquery()
|
||||
|
||||
solves = db.session.query(
|
||||
solves_sub.columns.chalid,
|
||||
solves_sub.columns.solves,
|
||||
Challenges.name
|
||||
) \
|
||||
.join(Challenges, solves_sub.columns.chalid == Challenges.id).all()
|
||||
|
||||
data = {}
|
||||
if utils.hide_scores():
|
||||
for chal, count, name in solves:
|
||||
data[chal] = -1
|
||||
for c in chals:
|
||||
if c.id not in data:
|
||||
data[c.id] = -1
|
||||
else:
|
||||
for chal, count, name in solves:
|
||||
data[chal] = count
|
||||
for c in chals:
|
||||
if c.id not in data:
|
||||
data[c.id] = 0
|
||||
db.session.close()
|
||||
return jsonify(data)
|
||||
|
||||
|
||||
@challenges.route('/solves')
|
||||
@authed_only
|
||||
def solves_private():
|
||||
solves = None
|
||||
awards = None
|
||||
|
||||
if utils.is_admin():
|
||||
solves = Solves.query.filter_by(teamid=session['id']).all()
|
||||
elif utils.user_can_view_challenges():
|
||||
if utils.authed():
|
||||
solves = Solves.query\
|
||||
.join(Teams, Solves.teamid == Teams.id)\
|
||||
.filter(Solves.teamid == session['id'])\
|
||||
.all()
|
||||
else:
|
||||
return jsonify({'solves': []})
|
||||
else:
|
||||
return redirect(url_for('auth.login', next='solves'))
|
||||
|
||||
db.session.close()
|
||||
response = {'solves': []}
|
||||
for solve in solves:
|
||||
response['solves'].append({
|
||||
'chal': solve.chal.name,
|
||||
'chalid': solve.chalid,
|
||||
'team': solve.teamid,
|
||||
'value': solve.chal.value,
|
||||
'category': solve.chal.category,
|
||||
'time': utils.unix_time(solve.date)
|
||||
})
|
||||
if awards:
|
||||
for award in awards:
|
||||
response['solves'].append({
|
||||
'chal': award.name,
|
||||
'chalid': None,
|
||||
'team': award.teamid,
|
||||
'value': award.value,
|
||||
'category': award.category or "Award",
|
||||
'time': utils.unix_time(award.date)
|
||||
})
|
||||
response['solves'].sort(key=lambda k: k['time'])
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/solves/<int:teamid>')
|
||||
def solves_public(teamid=None):
|
||||
solves = None
|
||||
awards = None
|
||||
|
||||
if utils.authed() and session['id'] == teamid:
|
||||
solves = Solves.query.filter_by(teamid=teamid)
|
||||
awards = Awards.query.filter_by(teamid=teamid)
|
||||
|
||||
freeze = utils.get_config('freeze')
|
||||
if freeze:
|
||||
freeze = utils.unix_time_to_utc(freeze)
|
||||
if teamid != session.get('id'):
|
||||
solves = solves.filter(Solves.date < freeze)
|
||||
awards = awards.filter(Awards.date < freeze)
|
||||
|
||||
solves = solves.all()
|
||||
awards = awards.all()
|
||||
elif utils.hide_scores():
|
||||
# Use empty values to hide scores
|
||||
solves = []
|
||||
awards = []
|
||||
else:
|
||||
solves = Solves.query.filter_by(teamid=teamid)
|
||||
awards = Awards.query.filter_by(teamid=teamid)
|
||||
|
||||
freeze = utils.get_config('freeze')
|
||||
if freeze:
|
||||
freeze = utils.unix_time_to_utc(freeze)
|
||||
if teamid != session.get('id'):
|
||||
solves = solves.filter(Solves.date < freeze)
|
||||
awards = awards.filter(Awards.date < freeze)
|
||||
|
||||
solves = solves.all()
|
||||
awards = awards.all()
|
||||
db.session.close()
|
||||
|
||||
response = {'solves': []}
|
||||
for solve in solves:
|
||||
response['solves'].append({
|
||||
'chal': solve.chal.name,
|
||||
'chalid': solve.chalid,
|
||||
'team': solve.teamid,
|
||||
'value': solve.chal.value,
|
||||
'category': solve.chal.category,
|
||||
'time': utils.unix_time(solve.date)
|
||||
})
|
||||
if awards:
|
||||
for award in awards:
|
||||
response['solves'].append({
|
||||
'chal': award.name,
|
||||
'chalid': None,
|
||||
'team': award.teamid,
|
||||
'value': award.value,
|
||||
'category': award.category or "Award",
|
||||
'time': utils.unix_time(award.date)
|
||||
})
|
||||
response['solves'].sort(key=lambda k: k['time'])
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/fails')
|
||||
@authed_only
|
||||
def fails_private():
|
||||
fails = WrongKeys.query.filter_by(teamid=session['id']).count()
|
||||
solves = Solves.query.filter_by(teamid=session['id']).count()
|
||||
|
||||
db.session.close()
|
||||
response = {
|
||||
'fails': str(fails),
|
||||
'solves': str(solves)
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/fails/<int:teamid>')
|
||||
def fails_public(teamid=None):
|
||||
if utils.authed() and session['id'] == teamid:
|
||||
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
elif utils.hide_scores():
|
||||
fails = 0
|
||||
solves = 0
|
||||
else:
|
||||
fails = WrongKeys.query.filter_by(teamid=teamid).count()
|
||||
solves = Solves.query.filter_by(teamid=teamid).count()
|
||||
db.session.close()
|
||||
response = {
|
||||
'fails': str(fails),
|
||||
'solves': str(solves)
|
||||
}
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
|
||||
@during_ctf_time_only
|
||||
@viewable_without_authentication(status_code=403)
|
||||
def who_solved(chalid):
|
||||
response = {'teams': []}
|
||||
if utils.hide_scores():
|
||||
return jsonify(response)
|
||||
solves = Solves.query.join(Teams, Solves.teamid == Teams.id).filter(Solves.chalid == chalid, Teams.banned == False).order_by(Solves.date.asc())
|
||||
for solve in solves:
|
||||
response['teams'].append({'id': solve.team.id, 'name': solve.team.name, 'date': solve.date})
|
||||
return jsonify(response)
|
||||
|
||||
|
||||
@challenges.route('/chal/<int:chalid>', methods=['POST'])
|
||||
@during_ctf_time_only
|
||||
@viewable_without_authentication()
|
||||
def chal(chalid):
|
||||
if utils.ctf_paused():
|
||||
return jsonify({
|
||||
'status': 3,
|
||||
'message': '{} is paused'.format(utils.ctf_name())
|
||||
})
|
||||
if (utils.authed() and utils.is_verified() and (utils.ctf_started() or utils.view_after_ctf())) or utils.is_admin():
|
||||
team = Teams.query.filter_by(id=session['id']).first()
|
||||
fails = WrongKeys.query.filter_by(teamid=session['id'], chalid=chalid).count()
|
||||
logger = logging.getLogger('keys')
|
||||
data = (time.strftime("%m/%d/%Y %X"), session['username'].encode('utf-8'), request.form['key'].encode('utf-8'), utils.get_kpm(session['id']))
|
||||
print("[{0}] {1} submitted {2} with kpm {3}".format(*data))
|
||||
|
||||
chal = Challenges.query.filter_by(id=chalid).first_or_404()
|
||||
if chal.hidden:
|
||||
abort(404)
|
||||
chal_class = get_chal_class(chal.type)
|
||||
|
||||
# Anti-bruteforce / submitting keys too quickly
|
||||
if utils.get_kpm(session['id']) > 10:
|
||||
if utils.ctftime():
|
||||
chal_class.fail(team=team, chal=chal, request=request)
|
||||
logger.warn("[{0}] {1} submitted {2} with kpm {3} [TOO FAST]".format(*data))
|
||||
# return '3' # Submitting too fast
|
||||
return jsonify({'status': 3, 'message': "You're submitting keys too fast. Slow down."})
|
||||
|
||||
solves = Solves.query.filter_by(teamid=session['id'], chalid=chalid).first()
|
||||
|
||||
# Challange not solved yet
|
||||
if not solves:
|
||||
provided_key = request.form['key'].strip()
|
||||
saved_keys = Keys.query.filter_by(chal=chal.id).all()
|
||||
|
||||
# Hit max attempts
|
||||
max_tries = chal.max_attempts
|
||||
if max_tries and fails >= max_tries > 0:
|
||||
return jsonify({
|
||||
'status': 0,
|
||||
'message': "You have 0 tries remaining"
|
||||
})
|
||||
|
||||
status, message = chal_class.attempt(chal, request)
|
||||
if status: # The challenge plugin says the input is right
|
||||
if utils.ctftime() or utils.is_admin():
|
||||
chal_class.solve(team=team, chal=chal, request=request)
|
||||
logger.info("[{0}] {1} submitted {2} with kpm {3} [CORRECT]".format(*data))
|
||||
return jsonify({'status': 1, 'message': message})
|
||||
else: # The challenge plugin says the input is wrong
|
||||
if utils.ctftime() or utils.is_admin():
|
||||
chal_class.fail(team=team, chal=chal, request=request)
|
||||
logger.info("[{0}] {1} submitted {2} with kpm {3} [WRONG]".format(*data))
|
||||
# return '0' # key was wrong
|
||||
if max_tries:
|
||||
attempts_left = max_tries - fails - 1 # Off by one since fails has changed since it was gotten
|
||||
tries_str = 'tries'
|
||||
if attempts_left == 1:
|
||||
tries_str = 'try'
|
||||
if message[-1] not in '!().;?[]\{\}': # Add a punctuation mark if there isn't one
|
||||
message = message + '.'
|
||||
return jsonify({'status': 0, 'message': '{} You have {} {} remaining.'.format(message, attempts_left, tries_str)})
|
||||
else:
|
||||
return jsonify({'status': 0, 'message': message})
|
||||
|
||||
# Challenge already solved
|
||||
else:
|
||||
logger.info("{0} submitted {1} with kpm {2} [ALREADY SOLVED]".format(*data))
|
||||
# return '2' # challenge was already solved
|
||||
return jsonify({'status': 2, 'message': 'You already solved this'})
|
||||
else:
|
||||
return jsonify({
|
||||
'status': -1,
|
||||
'message': "You must be logged in to solve a challenge"
|
||||
})
|
||||
|
|
270
CTFd/config.py
270
CTFd/config.py
|
@ -27,124 +27,182 @@ if not os.environ.get('SECRET_KEY'):
|
|||
|
||||
|
||||
class Config(object):
|
||||
"""
|
||||
CTFd Configuration Object
|
||||
"""
|
||||
|
||||
'''
|
||||
SECRET_KEY is 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.
|
||||
=== REQUIRED SETTINGS ===
|
||||
|
||||
You can also remove .ctfd_secret_key from the .gitignore file and commit this file into whatever repository
|
||||
you are using.
|
||||
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.
|
||||
|
||||
http://flask.pocoo.org/docs/0.11/quickstart/#sessions
|
||||
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
|
||||
|
||||
SQLALCHEMY_DATABASE_URI:
|
||||
The URI that specifies the username, password, hostname, port, and database of the server
|
||||
used to hold the CTFd database.
|
||||
|
||||
e.g. mysql+pymysql://root:<YOUR_PASSWORD_HERE>@localhost/ctfd
|
||||
|
||||
CACHE_TYPE:
|
||||
Specifies how CTFd should cache configuration values. If CACHE_TYPE is set to 'redis', CTFd will make use
|
||||
of the REDIS_URL specified in environment variables. You can also choose to hardcode the REDIS_URL here.
|
||||
|
||||
It is important that you specify some sort of cache as CTFd uses it to store values received from the database. If
|
||||
no cache is specified, CTFd will default to a simple per-worker cache. The simple cache cannot be effectively used
|
||||
with multiple workers.
|
||||
|
||||
REDIS_URL is the URL to connect to a Redis server.
|
||||
e.g. redis://user:password@localhost:6379
|
||||
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
|
||||
'''
|
||||
SECRET_KEY = os.environ.get('SECRET_KEY') or key
|
||||
DATABASE_URL = os.environ.get(
|
||||
'DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))
|
||||
REDIS_URL = os.environ.get('REDIS_URL')
|
||||
|
||||
'''
|
||||
SQLALCHEMY_DATABASE_URI is the URI that specifies the username, password, hostname, port, and database of the server
|
||||
used to hold the CTFd database.
|
||||
|
||||
http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys
|
||||
'''
|
||||
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
'''
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS is automatically disabled to suppress warnings and save memory. You should only enable
|
||||
this if you need it.
|
||||
'''
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
|
||||
'''
|
||||
SESSION_TYPE is a configuration value used for Flask-Session. It is currently unused in CTFd.
|
||||
http://pythonhosted.org/Flask-Session/#configuration
|
||||
'''
|
||||
SESSION_TYPE = "filesystem"
|
||||
|
||||
'''
|
||||
SESSION_FILE_DIR is a configuration value used for Flask-Session. It is currently unused in CTFd.
|
||||
http://pythonhosted.org/Flask-Session/#configuration
|
||||
'''
|
||||
SESSION_FILE_DIR = "/tmp/flask_session"
|
||||
|
||||
'''
|
||||
SESSION_COOKIE_HTTPONLY controls if cookies should be set with the HttpOnly flag.
|
||||
'''
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
|
||||
'''
|
||||
PERMANENT_SESSION_LIFETIME is the lifetime of a session.
|
||||
'''
|
||||
PERMANENT_SESSION_LIFETIME = 604800 # 7 days in seconds
|
||||
|
||||
'''
|
||||
HOST specifies the hostname where the CTFd instance will exist. It is currently unused.
|
||||
'''
|
||||
HOST = ".ctfd.io"
|
||||
|
||||
'''
|
||||
MAILFROM_ADDR is the email address that emails are sent from if not overridden in the configuration panel.
|
||||
'''
|
||||
MAILFROM_ADDR = "noreply@ctfd.io"
|
||||
|
||||
'''
|
||||
LOG_FOLDER is 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.environ.get('LOG_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'logs')
|
||||
|
||||
'''
|
||||
UPLOAD_FOLDER is the location where files are uploaded.
|
||||
The default destination is the CTFd/uploads folder. If you need Amazon S3 files
|
||||
you can use the CTFd S3 plugin: https://github.com/ColdHeat/CTFd-S3-plugin
|
||||
'''
|
||||
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||
|
||||
'''
|
||||
TEMPLATES_AUTO_RELOAD specifies whether Flask should check for modifications to templates and
|
||||
reload them automatically
|
||||
'''
|
||||
TEMPLATES_AUTO_RELOAD = True
|
||||
|
||||
'''
|
||||
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.
|
||||
'''
|
||||
TRUSTED_PROXIES = [
|
||||
'^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
|
||||
'^::1$',
|
||||
'^fc00:',
|
||||
'^10\.',
|
||||
'^172\.(1[6-9]|2[0-9]|3[0-1])\.',
|
||||
'^192\.168\.'
|
||||
]
|
||||
|
||||
'''
|
||||
CACHE_TYPE specifies how CTFd should cache configuration values. If CACHE_TYPE is set to 'redis', CTFd will make use
|
||||
of the REDIS_URL specified in environment variables. You can also choose to hardcode the REDIS_URL here.
|
||||
|
||||
It is important that you specify some sort of cache as CTFd uses it to store values received from the database.
|
||||
|
||||
CACHE_REDIS_URL is the URL to connect to Redis server.
|
||||
Example: redis://user:password@localhost:6379
|
||||
|
||||
http://pythonhosted.org/Flask-Caching/#configuring-flask-caching
|
||||
'''
|
||||
CACHE_REDIS_URL = os.environ.get('REDIS_URL')
|
||||
SQLALCHEMY_DATABASE_URI = DATABASE_URL
|
||||
CACHE_REDIS_URL = os.environ.get('CACHE_REDIS_URL') or REDIS_URL
|
||||
if CACHE_REDIS_URL:
|
||||
CACHE_TYPE = 'redis'
|
||||
else:
|
||||
CACHE_TYPE = 'simple'
|
||||
CACHE_TYPE = 'filesystem'
|
||||
CACHE_DIR = os.path.join(os.path.dirname(
|
||||
__file__), os.pardir, '.data', 'filesystem_cache')
|
||||
|
||||
'''
|
||||
UPDATE_CHECK specifies whether or not CTFd will check whether or not there is a new version of CTFd
|
||||
=== SECURITY ===
|
||||
|
||||
SESSION_COOKIE_HTTPONLY:
|
||||
Controls if cookies should be set with the HttpOnly flag.
|
||||
|
||||
PERMANENT_SESSION_LIFETIME:
|
||||
The lifetime of a session. The default is 604800 seconds.
|
||||
|
||||
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.
|
||||
'''
|
||||
SESSION_COOKIE_HTTPONLY = True
|
||||
PERMANENT_SESSION_LIFETIME = 604800 # 7 days in seconds
|
||||
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:
|
||||
The email address that emails are sent from if not overridden in the configuration panel.
|
||||
|
||||
MAIL_SERVER:
|
||||
The mail server that emails are sent from if not overriden in the configuration panel.
|
||||
|
||||
MAIL_PORT:
|
||||
The mail port that emails are sent from if not overriden in the configuration panel.
|
||||
'''
|
||||
MAILFROM_ADDR = "noreply@ctfd.io"
|
||||
MAIL_SERVER = None
|
||||
MAIL_PORT = None
|
||||
MAIL_USERNAME = None
|
||||
MAIL_PASSWORD = None
|
||||
MAIL_TLS = False
|
||||
MAIL_SSL = False
|
||||
MAILGUN_API_KEY = None
|
||||
MAILGUN_BASE_URL = None
|
||||
|
||||
'''
|
||||
=== 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 = os.environ.get('LOG_FOLDER') or os.path.join(
|
||||
os.path.dirname(os.path.abspath(__file__)), 'logs')
|
||||
|
||||
'''
|
||||
=== UPLOADS ===
|
||||
|
||||
UPLOAD_PROVIDER:
|
||||
Specifies the service that CTFd should use to store files.
|
||||
|
||||
UPLOAD_FOLDER:
|
||||
The location where files are uploaded. The default destination is the CTFd/uploads folder.
|
||||
|
||||
AWS_ACCESS_KEY_ID:
|
||||
AWS access token used to authenticate to the S3 bucket.
|
||||
|
||||
AWS_SECRET_ACCESS_KEY:
|
||||
AWS secret token used to authenticate to the S3 bucket.
|
||||
|
||||
AWS_S3_BUCKET:
|
||||
The unique identifier for your S3 bucket.
|
||||
|
||||
AWS_S3_ENDPOINT_URL:
|
||||
A URL pointing to a custom S3 implementation.
|
||||
|
||||
'''
|
||||
UPLOAD_PROVIDER = os.environ.get('UPLOAD_PROVIDER') or 'filesystem'
|
||||
if UPLOAD_PROVIDER == 'filesystem':
|
||||
UPLOAD_FOLDER = os.environ.get('UPLOAD_FOLDER') or \
|
||||
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads')
|
||||
elif UPLOAD_PROVIDER == 's3':
|
||||
AWS_ACCESS_KEY_ID = None
|
||||
AWS_SECRET_ACCESS_KEY = None
|
||||
AWS_S3_BUCKET = None
|
||||
AWS_S3_ENDPOINT_URL = None
|
||||
|
||||
'''
|
||||
=== OPTIONAL ===
|
||||
|
||||
REVERSE_PROXY:
|
||||
Specifies whether CTFd is behind a reverse proxy or not. Set to True if using a reverse proxy like nginx.
|
||||
|
||||
TEMPLATES_AUTO_RELOAD:
|
||||
Specifies whether Flask should check for modifications to templates and reload them automatically.
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS:
|
||||
Automatically disabled to suppress warnings and save memory. You should only enable this if you need it.
|
||||
|
||||
UPDATE_CHECK:
|
||||
Specifies whether or not CTFd will check whether or not there is a new version of CTFd
|
||||
|
||||
APPLICATION_ROOT:
|
||||
Specifies what path CTFd is mounted under. It can be used to run CTFd in a subdirectory.
|
||||
Example: /ctfd
|
||||
'''
|
||||
REVERSE_PROXY = False
|
||||
TEMPLATES_AUTO_RELOAD = True
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = False
|
||||
UPDATE_CHECK = True
|
||||
APPLICATION_ROOT = os.environ.get('APPLICATION_ROOT') or '/'
|
||||
|
||||
'''
|
||||
=== OAUTH ===
|
||||
|
||||
MajorLeagueCyber Integration
|
||||
Register an event at https://majorleaguecyber.org/ and use the Client ID and Client Secret here
|
||||
'''
|
||||
OAUTH_CLIENT_ID = None
|
||||
OAUTH_CLIENT_SECRET = None
|
||||
|
||||
|
||||
class TestingConfig(Config):
|
||||
|
@ -155,5 +213,5 @@ class TestingConfig(Config):
|
|||
SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://'
|
||||
SERVER_NAME = 'localhost'
|
||||
UPDATE_CHECK = False
|
||||
CACHE_REDIS_URL = None
|
||||
REDIS_URL = None
|
||||
CACHE_TYPE = 'simple'
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
from flask import current_app as app, render_template
|
||||
|
||||
|
||||
# 404
|
||||
def page_not_found(error):
|
||||
return render_template('errors/404.html', error=error.description), 404
|
||||
|
||||
|
||||
# 403
|
||||
def forbidden(error):
|
||||
return render_template('errors/403.html', error=error.description), 403
|
||||
|
||||
|
||||
# 500
|
||||
def general_error(error):
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
|
||||
# 502
|
||||
def gateway_error(error):
|
||||
return render_template('errors/502.html', error=error.description), 502
|
|
@ -0,0 +1,3 @@
|
|||
from flask import Blueprint
|
||||
|
||||
events = Blueprint('events', __name__)
|
343
CTFd/models.py
343
CTFd/models.py
|
@ -1,343 +0,0 @@
|
|||
import datetime
|
||||
import hashlib
|
||||
import json
|
||||
import netaddr
|
||||
from socket import inet_pton, inet_ntop, AF_INET, AF_INET6
|
||||
from struct import unpack, pack, error as struct_error
|
||||
|
||||
from flask_sqlalchemy import SQLAlchemy
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from sqlalchemy.exc import DatabaseError
|
||||
from sqlalchemy.sql.expression import union_all
|
||||
|
||||
|
||||
def sha512(string):
|
||||
return str(hashlib.sha512(string).hexdigest())
|
||||
|
||||
|
||||
def ip2long(ip):
|
||||
'''Converts a user's IP address into an integer/long'''
|
||||
return int(netaddr.IPAddress(ip))
|
||||
|
||||
|
||||
def long2ip(ip_int):
|
||||
'''Converts a saved integer/long back into an IP address'''
|
||||
return str(netaddr.IPAddress(ip_int))
|
||||
|
||||
|
||||
db = SQLAlchemy()
|
||||
|
||||
|
||||
class Pages(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
auth_required = db.Column(db.Boolean)
|
||||
title = db.Column(db.String(80))
|
||||
route = db.Column(db.Text, unique=True)
|
||||
html = db.Column(db.Text)
|
||||
draft = db.Column(db.Boolean)
|
||||
|
||||
def __init__(self, title, route, html, draft=True, auth_required=False):
|
||||
self.title = title
|
||||
self.route = route
|
||||
self.html = html
|
||||
self.draft = draft
|
||||
self.auth_required = auth_required
|
||||
|
||||
def __repr__(self):
|
||||
return "<Pages route {0}>".format(self.route)
|
||||
|
||||
|
||||
class Challenges(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(80))
|
||||
description = db.Column(db.Text)
|
||||
max_attempts = db.Column(db.Integer, default=0)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
type = db.Column(db.String(80))
|
||||
hidden = db.Column(db.Boolean)
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'standard',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, name, description, value, category, type='standard'):
|
||||
self.name = name
|
||||
self.description = description
|
||||
self.value = value
|
||||
self.category = category
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
return '<chal %r>' % self.name
|
||||
|
||||
|
||||
class Hints(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.Integer, default=0)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
hint = db.Column(db.Text)
|
||||
cost = db.Column(db.Integer, default=0)
|
||||
|
||||
def __init__(self, chal, hint, cost=0, type=0):
|
||||
self.chal = chal
|
||||
self.hint = hint
|
||||
self.cost = cost
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
return '<hint %r>' % self.hint
|
||||
|
||||
|
||||
class Awards(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
name = db.Column(db.String(80))
|
||||
description = db.Column(db.Text)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
icon = db.Column(db.Text)
|
||||
|
||||
def __init__(self, teamid, name, value):
|
||||
self.teamid = teamid
|
||||
self.name = name
|
||||
self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<award %r>' % self.name
|
||||
|
||||
|
||||
class Tags(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
tag = db.Column(db.String(80))
|
||||
|
||||
def __init__(self, chal, tag):
|
||||
self.chal = chal
|
||||
self.tag = tag
|
||||
|
||||
def __repr__(self):
|
||||
return "<Tag {0} for challenge {1}>".format(self.tag, self.chal)
|
||||
|
||||
|
||||
class Files(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
location = db.Column(db.Text)
|
||||
|
||||
def __init__(self, chal, location):
|
||||
self.chal = chal
|
||||
self.location = location
|
||||
|
||||
def __repr__(self):
|
||||
return "<File {0} for challenge {1}>".format(self.location, self.chal)
|
||||
|
||||
|
||||
class Keys(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chal = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
type = db.Column(db.String(80))
|
||||
flag = db.Column(db.Text)
|
||||
data = db.Column(db.Text)
|
||||
|
||||
def __init__(self, chal, flag, type):
|
||||
self.chal = chal
|
||||
self.flag = flag
|
||||
self.type = type
|
||||
|
||||
def __repr__(self):
|
||||
return "<Flag {0} for challenge {1}>".format(self.flag, self.chal)
|
||||
|
||||
|
||||
class Teams(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(128), unique=True)
|
||||
email = db.Column(db.String(128), unique=True)
|
||||
password = db.Column(db.String(128))
|
||||
website = db.Column(db.String(128))
|
||||
affiliation = db.Column(db.String(128))
|
||||
country = db.Column(db.String(32))
|
||||
bracket = db.Column(db.String(32))
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
verified = db.Column(db.Boolean, default=False)
|
||||
admin = db.Column(db.Boolean, default=False)
|
||||
joined = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def __init__(self, name, email, password):
|
||||
self.name = name
|
||||
self.email = email
|
||||
self.password = bcrypt_sha256.encrypt(str(password))
|
||||
|
||||
def __repr__(self):
|
||||
return '<team %r>' % self.name
|
||||
|
||||
def score(self, admin=False):
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
team = db.session.query(Solves.teamid, score).join(Teams).join(Challenges).filter(Teams.id == self.id)
|
||||
award_score = db.func.sum(Awards.value).label('award_score')
|
||||
award = db.session.query(award_score).filter_by(teamid=self.id)
|
||||
|
||||
if not admin:
|
||||
freeze = Config.query.filter_by(key='freeze').first()
|
||||
if freeze and freeze.value:
|
||||
freeze = int(freeze.value)
|
||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||
team = team.filter(Solves.date < freeze)
|
||||
award = award.filter(Awards.date < freeze)
|
||||
|
||||
team = team.group_by(Solves.teamid).first()
|
||||
award = award.first()
|
||||
|
||||
if team and award:
|
||||
return int(team.score or 0) + int(award.award_score or 0)
|
||||
elif team:
|
||||
return int(team.score or 0)
|
||||
elif award:
|
||||
return int(award.award_score or 0)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def place(self, admin=False):
|
||||
"""
|
||||
This method is generally a clone of CTFd.scoreboard.get_standings.
|
||||
The point being that models.py must be self-reliant and have little
|
||||
to no imports within the CTFd application as importing from the
|
||||
application itself will result in a circular import.
|
||||
"""
|
||||
scores = db.session.query(
|
||||
Solves.teamid.label('teamid'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.teamid)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.teamid.label('teamid'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
).filter(Awards.value != 0).group_by(Awards.teamid)
|
||||
|
||||
if not admin:
|
||||
freeze = Config.query.filter_by(key='freeze').first()
|
||||
if freeze and freeze.value:
|
||||
freeze = int(freeze.value)
|
||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||
scores = scores.filter(Solves.date < freeze)
|
||||
awards = awards.filter(Awards.date < freeze)
|
||||
|
||||
results = union_all(scores, awards).alias('results')
|
||||
|
||||
sumscores = db.session.query(
|
||||
results.columns.teamid,
|
||||
db.func.sum(results.columns.score).label('score'),
|
||||
db.func.max(results.columns.id).label('id'),
|
||||
db.func.max(results.columns.date).label('date')
|
||||
).group_by(results.columns.teamid).subquery()
|
||||
|
||||
if admin:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
else:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.filter(Teams.banned == False) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
|
||||
standings = standings_query.all()
|
||||
|
||||
# http://codegolf.stackexchange.com/a/4712
|
||||
try:
|
||||
i = standings.index((self.id,)) + 1
|
||||
k = i % 10
|
||||
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
class Solves(db.Model):
|
||||
__table_args__ = (db.UniqueConstraint('chalid', 'teamid'), {})
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chalid = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
ip = db.Column(db.String(46))
|
||||
flag = db.Column(db.Text)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
team = db.relationship('Teams', foreign_keys="Solves.teamid", lazy='joined')
|
||||
chal = db.relationship('Challenges', foreign_keys="Solves.chalid", lazy='joined')
|
||||
# value = db.Column(db.Integer)
|
||||
|
||||
def __init__(self, teamid, chalid, ip, flag):
|
||||
self.ip = ip
|
||||
self.chalid = chalid
|
||||
self.teamid = teamid
|
||||
self.flag = flag
|
||||
# self.value = value
|
||||
|
||||
def __repr__(self):
|
||||
return '<solve {}, {}, {}, {}>'.format(self.teamid, self.chalid, self.ip, self.flag)
|
||||
|
||||
|
||||
class WrongKeys(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
chalid = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
ip = db.Column(db.String(46))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
flag = db.Column(db.Text)
|
||||
chal = db.relationship('Challenges', foreign_keys="WrongKeys.chalid", lazy='joined')
|
||||
|
||||
def __init__(self, teamid, chalid, ip, flag):
|
||||
self.ip = ip
|
||||
self.teamid = teamid
|
||||
self.chalid = chalid
|
||||
self.flag = flag
|
||||
|
||||
def __repr__(self):
|
||||
return '<wrong {}, {}, {}, {}>'.format(self.teamid, self.chalid, self.ip, self.flag)
|
||||
|
||||
|
||||
class Unlocks(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
teamid = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
itemid = db.Column(db.Integer)
|
||||
model = db.Column(db.String(32))
|
||||
|
||||
def __init__(self, model, teamid, itemid):
|
||||
self.model = model
|
||||
self.teamid = teamid
|
||||
self.itemid = itemid
|
||||
|
||||
def __repr__(self):
|
||||
return '<unlock %r>' % self.teamid
|
||||
|
||||
|
||||
class Tracking(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
ip = db.Column(db.String(46))
|
||||
team = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def __init__(self, ip, team):
|
||||
self.ip = ip
|
||||
self.team = team
|
||||
|
||||
def __repr__(self):
|
||||
return '<ip %r>' % self.team
|
||||
|
||||
|
||||
class Config(db.Model):
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.Text)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
def __init__(self, key, value):
|
||||
self.key = key
|
||||
self.value = value
|
|
@ -0,0 +1,788 @@
|
|||
from flask_sqlalchemy import SQLAlchemy
|
||||
from flask_marshmallow import Marshmallow
|
||||
from passlib.hash import bcrypt_sha256
|
||||
from sqlalchemy import TypeDecorator, String, func, types, CheckConstraint, and_
|
||||
from sqlalchemy.sql.expression import union_all
|
||||
from sqlalchemy.types import JSON, NullType
|
||||
from sqlalchemy.orm import validates, column_property
|
||||
from sqlalchemy.ext.hybrid import hybrid_property, hybrid_method
|
||||
from sqlalchemy.sql import or_, and_, any_
|
||||
from CTFd.utils.crypto import hash_password
|
||||
from CTFd.cache import cache
|
||||
import datetime
|
||||
import json
|
||||
import six
|
||||
|
||||
db = SQLAlchemy()
|
||||
ma = Marshmallow()
|
||||
|
||||
|
||||
def get_class_by_tablename(tablename):
|
||||
"""Return class reference mapped to table.
|
||||
https://stackoverflow.com/a/23754464
|
||||
|
||||
:param tablename: String with name of table.
|
||||
:return: Class reference or None.
|
||||
"""
|
||||
for c in db.Model._decl_class_registry.values():
|
||||
if hasattr(c, '__tablename__') and c.__tablename__ == tablename:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
class SQLiteJson(TypeDecorator):
|
||||
impl = String
|
||||
|
||||
class Comparator(String.Comparator):
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, tuple):
|
||||
index = "$%s" % (
|
||||
"".join([
|
||||
"[%s]" % elem if isinstance(elem, int)
|
||||
else '."%s"' % elem for elem in index
|
||||
])
|
||||
)
|
||||
elif isinstance(index, int):
|
||||
index = "$[%s]" % index
|
||||
else:
|
||||
index = '$."%s"' % index
|
||||
|
||||
# json_extract does not appear to return JSON sub-elements
|
||||
# which is weird.
|
||||
return func.json_extract(self.expr, index, type_=NullType)
|
||||
|
||||
comparator_factory = Comparator
|
||||
|
||||
def process_bind_param(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.dumps(value)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value, dialect):
|
||||
if value is not None:
|
||||
value = json.loads(value)
|
||||
return value
|
||||
|
||||
|
||||
JSON = types.JSON().with_variant(SQLiteJson, 'sqlite')
|
||||
|
||||
|
||||
class Notifications(db.Model):
|
||||
__tablename__ = 'notifications'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.Text)
|
||||
content = db.Column(db.Text)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
|
||||
user = db.relationship('Users', foreign_keys="Notifications.user_id", lazy='select')
|
||||
team = db.relationship('Teams', foreign_keys="Notifications.team_id", lazy='select')
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Notifications, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Pages(db.Model):
|
||||
__tablename__ = 'pages'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
title = db.Column(db.String(80))
|
||||
route = db.Column(db.String(128), unique=True)
|
||||
content = db.Column(db.Text)
|
||||
draft = db.Column(db.Boolean)
|
||||
hidden = db.Column(db.Boolean)
|
||||
auth_required = db.Column(db.Boolean)
|
||||
# TODO: Use hidden attribute
|
||||
|
||||
files = db.relationship("PageFiles", backref="page")
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Pages, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Pages {0}>".format(self.route)
|
||||
|
||||
|
||||
class Challenges(db.Model):
|
||||
__tablename__ = 'challenges'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
name = db.Column(db.String(80))
|
||||
description = db.Column(db.Text)
|
||||
max_attempts = db.Column(db.Integer, default=0)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
type = db.Column(db.String(80))
|
||||
state = db.Column(db.String(80), nullable=False, default='visible')
|
||||
requirements = db.Column(JSON)
|
||||
|
||||
files = db.relationship("ChallengeFiles", backref="challenge")
|
||||
tags = db.relationship("Tags", backref="challenge")
|
||||
hints = db.relationship("Hints", backref="challenge")
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'standard',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Challenges, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Challenge %r>' % self.name
|
||||
|
||||
|
||||
class Hints(db.Model):
|
||||
__tablename__ = 'hints'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.String(80), default='standard')
|
||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
content = db.Column(db.Text)
|
||||
cost = db.Column(db.Integer, default=0)
|
||||
requirements = db.Column(JSON)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'standard',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return "Hint {id}".format(id=self.id)
|
||||
|
||||
@property
|
||||
def category(self):
|
||||
return self.__tablename__
|
||||
|
||||
@property
|
||||
def description(self):
|
||||
return "Hint for {name}".format(name=self.challenge.name)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Hints, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Hint %r>' % self.content
|
||||
|
||||
|
||||
class Awards(db.Model):
|
||||
__tablename__ = 'awards'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
name = db.Column(db.String(80))
|
||||
description = db.Column(db.Text)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
value = db.Column(db.Integer)
|
||||
category = db.Column(db.String(80))
|
||||
icon = db.Column(db.Text)
|
||||
requirements = db.Column(JSON)
|
||||
|
||||
user = db.relationship('Users', foreign_keys="Awards.user_id", lazy='select')
|
||||
team = db.relationship('Teams', foreign_keys="Awards.team_id", lazy='select')
|
||||
|
||||
@hybrid_property
|
||||
def account_id(self):
|
||||
user_mode = get_config('user_mode')
|
||||
if user_mode == 'teams':
|
||||
return self.team_id
|
||||
elif user_mode == 'users':
|
||||
return self.user_id
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Awards, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Award %r>' % self.name
|
||||
|
||||
|
||||
class Tags(db.Model):
|
||||
__tablename__ = 'tags'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
value = db.Column(db.String(80))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Tags, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Files(db.Model):
|
||||
__tablename__ = 'files'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.String(80), default='standard')
|
||||
location = db.Column(db.Text)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'standard',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Files, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "<File type={type} location={location}>".format(type=self.type, location=self.location)
|
||||
|
||||
|
||||
class ChallengeFiles(Files):
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'challenge'
|
||||
}
|
||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ChallengeFiles, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class PageFiles(Files):
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'page'
|
||||
}
|
||||
page_id = db.Column(db.Integer, db.ForeignKey('pages.id'))
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PageFiles, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class Flags(db.Model):
|
||||
__tablename__ = 'flags'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id'))
|
||||
type = db.Column(db.String(80))
|
||||
content = db.Column(db.Text)
|
||||
data = db.Column(db.Text)
|
||||
|
||||
challenge = db.relationship('Challenges', foreign_keys="Flags.challenge_id", lazy='select')
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Flags, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return "<Flag {0} for challenge {1}>".format(self.content, self.challenge_id)
|
||||
|
||||
|
||||
class Users(db.Model):
|
||||
__tablename__ = 'users'
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('id', 'oauth_id'),
|
||||
{}
|
||||
)
|
||||
# Core attributes
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
oauth_id = db.Column(db.Integer, unique=True)
|
||||
# User names are not constrained to be unique to allow for official/unofficial teams.
|
||||
name = db.Column(db.String(128))
|
||||
password = db.Column(db.String(128))
|
||||
email = db.Column(db.String(128), unique=True)
|
||||
type = db.Column(db.String(80))
|
||||
secret = db.Column(db.String(128))
|
||||
|
||||
# Supplementary attributes
|
||||
website = db.Column(db.String(128))
|
||||
affiliation = db.Column(db.String(128))
|
||||
country = db.Column(db.String(32))
|
||||
bracket = db.Column(db.String(32))
|
||||
hidden = db.Column(db.Boolean, default=False)
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
verified = db.Column(db.Boolean, default=False)
|
||||
|
||||
# Relationship for Teams
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
|
||||
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'user',
|
||||
'polymorphic_on': type
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Users, self).__init__(**kwargs)
|
||||
if kwargs.get('password'):
|
||||
self.password = hash_password(str(kwargs['password']))
|
||||
|
||||
@hybrid_property
|
||||
def account_id(self):
|
||||
user_mode = get_config('user_mode')
|
||||
if user_mode == 'teams':
|
||||
return self.team_id
|
||||
elif user_mode == 'users':
|
||||
return self.id
|
||||
|
||||
@property
|
||||
def solves(self):
|
||||
return self.get_solves(admin=False)
|
||||
|
||||
@property
|
||||
def fails(self):
|
||||
return self.get_fails(admin=False)
|
||||
|
||||
@property
|
||||
def awards(self):
|
||||
return self.get_awards(admin=False)
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
return self.get_score(admin=False)
|
||||
|
||||
@property
|
||||
def place(self):
|
||||
return self.get_place(admin=False)
|
||||
|
||||
def get_solves(self, admin=False):
|
||||
solves = Solves.query.filter_by(user_id=self.id)
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
solves = solves.filter(Solves.date < dt)
|
||||
return solves.all()
|
||||
|
||||
def get_fails(self, admin=False):
|
||||
fails = Fails.query.filter_by(user_id=self.id)
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
fails = fails.filter(Solves.date < dt)
|
||||
return fails.all()
|
||||
|
||||
def get_awards(self, admin=False):
|
||||
awards = Awards.query.filter_by(user_id=self.id)
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
awards = awards.filter(Solves.date < dt)
|
||||
return awards.all()
|
||||
|
||||
def get_score(self, admin=False):
|
||||
score = db.func.sum(Challenges.value).label('score')
|
||||
user = db.session.query(
|
||||
Solves.user_id,
|
||||
score
|
||||
) \
|
||||
.join(Users, Solves.user_id == Users.id) \
|
||||
.join(Challenges, Solves.challenge_id == Challenges.id) \
|
||||
.filter(Users.id == self.id)
|
||||
|
||||
award_score = db.func.sum(Awards.value).label('award_score')
|
||||
award = db.session.query(award_score).filter_by(user_id=self.id)
|
||||
|
||||
if not admin:
|
||||
freeze = Configs.query.filter_by(key='freeze').first()
|
||||
if freeze and freeze.value:
|
||||
freeze = int(freeze.value)
|
||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||
user = user.filter(Solves.date < freeze)
|
||||
award = award.filter(Awards.date < freeze)
|
||||
|
||||
user = user.group_by(Solves.user_id).first()
|
||||
award = award.first()
|
||||
|
||||
if user and award:
|
||||
return int(user.score or 0) + int(award.award_score or 0)
|
||||
elif user:
|
||||
return int(user.score or 0)
|
||||
elif award:
|
||||
return int(award.award_score or 0)
|
||||
else:
|
||||
return 0
|
||||
|
||||
def get_place(self, admin=False, numeric=False):
|
||||
"""
|
||||
This method is generally a clone of CTFd.scoreboard.get_standings.
|
||||
The point being that models.py must be self-reliant and have little
|
||||
to no imports within the CTFd application as importing from the
|
||||
application itself will result in a circular import.
|
||||
"""
|
||||
scores = db.session.query(
|
||||
Solves.user_id.label('user_id'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.user_id)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.user_id.label('user_id'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
).filter(Awards.value != 0).group_by(Awards.user_id)
|
||||
|
||||
if not admin:
|
||||
freeze = Configs.query.filter_by(key='freeze').first()
|
||||
if freeze and freeze.value:
|
||||
freeze = int(freeze.value)
|
||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||
scores = scores.filter(Solves.date < freeze)
|
||||
awards = awards.filter(Awards.date < freeze)
|
||||
|
||||
results = union_all(scores, awards).alias('results')
|
||||
|
||||
sumscores = db.session.query(
|
||||
results.columns.user_id,
|
||||
db.func.sum(results.columns.score).label('score'),
|
||||
db.func.max(results.columns.id).label('id'),
|
||||
db.func.max(results.columns.date).label('date')
|
||||
).group_by(results.columns.user_id).subquery()
|
||||
|
||||
if admin:
|
||||
standings_query = db.session.query(
|
||||
Users.id.label('user_id'),
|
||||
) \
|
||||
.join(sumscores, Users.id == sumscores.columns.user_id) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
else:
|
||||
standings_query = db.session.query(
|
||||
Users.id.label('user_id'),
|
||||
) \
|
||||
.join(sumscores, Users.id == sumscores.columns.user_id) \
|
||||
.filter(Users.banned == False) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
|
||||
standings = standings_query.all()
|
||||
|
||||
# http://codegolf.stackexchange.com/a/4712
|
||||
try:
|
||||
i = standings.index((self.id,)) + 1
|
||||
if numeric:
|
||||
return i
|
||||
else:
|
||||
k = i % 10
|
||||
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
class Admins(Users):
|
||||
__tablename__ = 'admins'
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'admin'
|
||||
}
|
||||
|
||||
|
||||
class Teams(db.Model):
|
||||
__tablename__ = 'teams'
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('id', 'oauth_id'),
|
||||
{}
|
||||
)
|
||||
# Core attributes
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
oauth_id = db.Column(db.Integer, unique=True)
|
||||
# Team names are not constrained to be unique to allow for official/unofficial teams.
|
||||
name = db.Column(db.String(128))
|
||||
email = db.Column(db.String(128), unique=True)
|
||||
password = db.Column(db.String(128))
|
||||
secret = db.Column(db.String(128))
|
||||
|
||||
members = db.relationship("Users", backref="team")
|
||||
|
||||
# Supplementary attributes
|
||||
website = db.Column(db.String(128))
|
||||
affiliation = db.Column(db.String(128))
|
||||
country = db.Column(db.String(32))
|
||||
bracket = db.Column(db.String(32))
|
||||
hidden = db.Column(db.Boolean, default=False)
|
||||
banned = db.Column(db.Boolean, default=False)
|
||||
|
||||
created = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(Teams, self).__init__(**kwargs)
|
||||
if kwargs.get('password'):
|
||||
self.password = hash_password(str(kwargs['password']))
|
||||
|
||||
@property
|
||||
def solves(self):
|
||||
return self.get_solves(admin=False)
|
||||
|
||||
@property
|
||||
def fails(self):
|
||||
return self.get_fails(admin=False)
|
||||
|
||||
@property
|
||||
def awards(self):
|
||||
return self.get_awards(admin=False)
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
return self.get_score(admin=False)
|
||||
|
||||
@property
|
||||
def place(self):
|
||||
return self.get_place(admin=False)
|
||||
|
||||
def get_solves(self, admin=False):
|
||||
member_ids = [member.id for member in self.members]
|
||||
|
||||
solves = Solves.query.filter(
|
||||
Solves.user_id.in_(member_ids)
|
||||
).order_by(
|
||||
Fails.date.asc()
|
||||
)
|
||||
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
fails = solves.filter(Solves.date < dt)
|
||||
|
||||
return solves.all()
|
||||
|
||||
def get_fails(self, admin=False):
|
||||
member_ids = [member.id for member in self.members]
|
||||
|
||||
fails = Fails.query.filter(
|
||||
Fails.user_id.in_(member_ids)
|
||||
).order_by(
|
||||
Fails.date.asc()
|
||||
)
|
||||
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
fails = fails.filter(Solves.date < dt)
|
||||
|
||||
return fails.all()
|
||||
|
||||
def get_awards(self, admin=False):
|
||||
member_ids = [member.id for member in self.members]
|
||||
|
||||
awards = Awards.query.filter(
|
||||
Awards.user_id.in_(member_ids)
|
||||
).order_by(
|
||||
Awards.date.asc()
|
||||
)
|
||||
|
||||
freeze = get_config('freeze')
|
||||
if freeze and admin is False:
|
||||
dt = datetime.datetime.utcfromtimestamp(freeze)
|
||||
awards = awards.filter(Solves.date < dt)
|
||||
|
||||
return awards.all()
|
||||
|
||||
def get_score(self, admin=False):
|
||||
score = 0
|
||||
for member in self.members:
|
||||
score += member.get_score(admin=admin)
|
||||
return score
|
||||
|
||||
def get_place(self, admin=False):
|
||||
"""
|
||||
This method is generally a clone of CTFd.scoreboard.get_standings.
|
||||
The point being that models.py must be self-reliant and have little
|
||||
to no imports within the CTFd application as importing from the
|
||||
application itself will result in a circular import.
|
||||
"""
|
||||
scores = db.session.query(
|
||||
Solves.team_id.label('team_id'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges).filter(Challenges.value != 0).group_by(Solves.team_id)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.team_id.label('team_id'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
).filter(Awards.value != 0).group_by(Awards.team_id)
|
||||
|
||||
if not admin:
|
||||
freeze = Configs.query.filter_by(key='freeze').first()
|
||||
if freeze and freeze.value:
|
||||
freeze = int(freeze.value)
|
||||
freeze = datetime.datetime.utcfromtimestamp(freeze)
|
||||
scores = scores.filter(Solves.date < freeze)
|
||||
awards = awards.filter(Awards.date < freeze)
|
||||
|
||||
results = union_all(scores, awards).alias('results')
|
||||
|
||||
sumscores = db.session.query(
|
||||
results.columns.team_id,
|
||||
db.func.sum(results.columns.score).label('score'),
|
||||
db.func.max(results.columns.id).label('id'),
|
||||
db.func.max(results.columns.date).label('date')
|
||||
).group_by(results.columns.team_id).subquery()
|
||||
|
||||
if admin:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('team_id'),
|
||||
) \
|
||||
.join(sumscores, Teams.id == sumscores.columns.team_id) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
else:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('team_id'),
|
||||
) \
|
||||
.join(sumscores, Teams.id == sumscores.columns.team_id) \
|
||||
.filter(Teams.banned == False) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
|
||||
standings = standings_query.all()
|
||||
|
||||
# http://codegolf.stackexchange.com/a/4712
|
||||
try:
|
||||
i = standings.index((self.id,)) + 1
|
||||
k = i % 10
|
||||
return "%d%s" % (i, "tsnrhtdd"[(i / 10 % 10 != 1) * (k < 4) * k::4])
|
||||
except ValueError:
|
||||
return 0
|
||||
|
||||
|
||||
class Submissions(db.Model):
|
||||
__tablename__ = 'submissions'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
challenge_id = db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE'))
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE'))
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE'))
|
||||
ip = db.Column(db.String(46))
|
||||
provided = db.Column(db.Text)
|
||||
type = db.Column(db.String(32))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
# Relationships
|
||||
user = db.relationship('Users', foreign_keys="Submissions.user_id", lazy='select')
|
||||
team = db.relationship('Teams', foreign_keys="Submissions.team_id", lazy='select')
|
||||
challenge = db.relationship('Challenges', foreign_keys="Submissions.challenge_id", lazy='select')
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_on': type,
|
||||
}
|
||||
|
||||
@hybrid_property
|
||||
def account_id(self):
|
||||
user_mode = get_config('user_mode')
|
||||
if user_mode == 'teams':
|
||||
return self.team_id
|
||||
elif user_mode == 'users':
|
||||
return self.user_id
|
||||
|
||||
@hybrid_property
|
||||
def account(self):
|
||||
user_mode = get_config('user_mode')
|
||||
if user_mode == 'teams':
|
||||
return self.team
|
||||
elif user_mode == 'users':
|
||||
return self.user
|
||||
|
||||
@staticmethod
|
||||
def get_child(type):
|
||||
child_classes = {
|
||||
x.polymorphic_identity: x.class_
|
||||
for x in Submissions.__mapper__.self_and_descendants
|
||||
}
|
||||
return child_classes[type]
|
||||
|
||||
def __repr__(self):
|
||||
return '<Submission {}, {}, {}, {}>'.format(self.team_id, self.challenge_id, self.ip, self.provided)
|
||||
|
||||
|
||||
class Solves(Submissions):
|
||||
__tablename__ = 'solves'
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('challenge_id', 'user_id'),
|
||||
db.UniqueConstraint('challenge_id', 'team_id'),
|
||||
{}
|
||||
)
|
||||
id = db.Column(None, db.ForeignKey('submissions.id', ondelete='CASCADE'), primary_key=True)
|
||||
challenge_id = column_property(db.Column(db.Integer, db.ForeignKey('challenges.id', ondelete='CASCADE')),
|
||||
Submissions.challenge_id)
|
||||
user_id = column_property(db.Column(db.Integer, db.ForeignKey('users.id', ondelete='CASCADE')), Submissions.user_id)
|
||||
team_id = column_property(db.Column(db.Integer, db.ForeignKey('teams.id', ondelete='CASCADE')), Submissions.team_id)
|
||||
|
||||
user = db.relationship('Users', foreign_keys="Solves.user_id", lazy='select')
|
||||
team = db.relationship('Teams', foreign_keys="Solves.team_id", lazy='select')
|
||||
challenge = db.relationship('Challenges', foreign_keys="Solves.challenge_id", lazy='select')
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'correct'
|
||||
}
|
||||
|
||||
|
||||
class Fails(Submissions):
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'incorrect'
|
||||
}
|
||||
|
||||
|
||||
class Unlocks(db.Model):
|
||||
__tablename__ = 'unlocks'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
team_id = db.Column(db.Integer, db.ForeignKey('teams.id'))
|
||||
target = db.Column(db.Integer)
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
type = db.Column(db.String(32))
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_on': type,
|
||||
}
|
||||
|
||||
@hybrid_property
|
||||
def account_id(self):
|
||||
user_mode = get_config('user_mode')
|
||||
if user_mode == 'teams':
|
||||
return self.team_id
|
||||
elif user_mode == 'users':
|
||||
return self.user_id
|
||||
|
||||
def __repr__(self):
|
||||
return '<Unlock %r>' % self.teamid
|
||||
|
||||
|
||||
class HintUnlocks(Unlocks):
|
||||
__mapper_args__ = {
|
||||
'polymorphic_identity': 'hints'
|
||||
}
|
||||
|
||||
|
||||
class Tracking(db.Model):
|
||||
__tablename__ = 'tracking'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
type = db.Column(db.String(32))
|
||||
ip = db.Column(db.String(46))
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
|
||||
date = db.Column(db.DateTime, default=datetime.datetime.utcnow)
|
||||
|
||||
user = db.relationship('Users', foreign_keys="Tracking.user_id", lazy='select')
|
||||
|
||||
__mapper_args__ = {
|
||||
'polymorphic_on': type,
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Tracking, self).__init__(**kwargs)
|
||||
|
||||
def __repr__(self):
|
||||
return '<Tracking %r>' % self.team
|
||||
|
||||
|
||||
class Configs(db.Model):
|
||||
__tablename__ = 'config'
|
||||
id = db.Column(db.Integer, primary_key=True)
|
||||
key = db.Column(db.Text)
|
||||
value = db.Column(db.Text)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(Configs, self).__init__(**kwargs)
|
||||
|
||||
|
||||
@cache.memoize()
|
||||
def get_config(key):
|
||||
"""
|
||||
This should be a direct clone of its implementation in utils. It is used to avoid a circular import.
|
||||
"""
|
||||
config = Configs.query.filter_by(key=key).first()
|
||||
if config and config.value:
|
||||
value = config.value
|
||||
if value and value.isdigit():
|
||||
return int(value)
|
||||
elif value and isinstance(value, six.string_types):
|
||||
if value.lower() == 'true':
|
||||
return True
|
||||
elif value.lower() == 'false':
|
||||
return False
|
||||
else:
|
||||
return value
|
|
@ -5,13 +5,13 @@ import os
|
|||
from collections import namedtuple
|
||||
from flask.helpers import safe_join
|
||||
from flask import current_app as app, send_file, send_from_directory, abort
|
||||
from CTFd.utils import (
|
||||
admins_only as admins_only_wrapper,
|
||||
from CTFd.utils.decorators import admins_only as admins_only_wrapper
|
||||
from CTFd.utils.plugins import (
|
||||
override_template as utils_override_template,
|
||||
register_plugin_script as utils_register_plugin_script,
|
||||
register_plugin_stylesheet as utils_register_plugin_stylesheet,
|
||||
pages as db_pages
|
||||
register_script as utils_register_plugin_script,
|
||||
register_stylesheet as utils_register_plugin_stylesheet,
|
||||
)
|
||||
from CTFd.utils.config.pages import get_pages
|
||||
|
||||
|
||||
Menu = namedtuple('Menu', ['title', 'route'])
|
||||
|
@ -122,7 +122,7 @@ def get_user_page_menu_bar():
|
|||
|
||||
:return: Returns a list of Menu namedtuples. They have name, and route attributes.
|
||||
"""
|
||||
return db_pages() + USER_PAGE_MENU_BAR
|
||||
return get_pages() + USER_PAGE_MENU_BAR
|
||||
|
||||
|
||||
def bypass_csrf_protection(f):
|
||||
|
@ -146,7 +146,7 @@ def init_plugins(app):
|
|||
:param app: A CTFd application
|
||||
:return:
|
||||
"""
|
||||
modules = glob.glob(os.path.dirname(__file__) + "/*")
|
||||
modules = sorted(glob.glob(os.path.dirname(__file__) + "/*"))
|
||||
blacklist = {'__pycache__'}
|
||||
for module in modules:
|
||||
module_name = os.path.basename(module)
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
from CTFd.plugins import register_plugin_assets_directory
|
||||
from CTFd.plugins.keys import get_key_class
|
||||
from CTFd.models import db, Solves, WrongKeys, Keys, Challenges, Files, Tags, Hints
|
||||
from CTFd.plugins.flags import get_flag_class
|
||||
from CTFd.models import db, Solves, Fails, Flags, Challenges, ChallengeFiles, Tags, Hints
|
||||
from CTFd import utils
|
||||
from CTFd.utils.user import get_ip
|
||||
from CTFd.utils.uploads import upload_file, delete_file
|
||||
from flask import Blueprint
|
||||
import six
|
||||
|
||||
|
||||
class BaseChallenge(object):
|
||||
|
@ -14,16 +18,20 @@ class BaseChallenge(object):
|
|||
class CTFdStandardChallenge(BaseChallenge):
|
||||
id = "standard" # Unique identifier used to register challenges
|
||||
name = "standard" # Name of a challenge type
|
||||
templates = { # Nunjucks templates used for each aspect of challenge editing & viewing
|
||||
'create': '/plugins/challenges/assets/standard-challenge-create.njk',
|
||||
'update': '/plugins/challenges/assets/standard-challenge-update.njk',
|
||||
'modal': '/plugins/challenges/assets/standard-challenge-modal.njk',
|
||||
templates = { # Templates used for each aspect of challenge editing & viewing
|
||||
'create': '/plugins/challenges/assets/create.html',
|
||||
'update': '/plugins/challenges/assets/update.html',
|
||||
'view': '/plugins/challenges/assets/view.html',
|
||||
}
|
||||
scripts = { # Scripts that are loaded when a template is loaded
|
||||
'create': '/plugins/challenges/assets/standard-challenge-create.js',
|
||||
'update': '/plugins/challenges/assets/standard-challenge-update.js',
|
||||
'modal': '/plugins/challenges/assets/standard-challenge-modal.js',
|
||||
'create': '/plugins/challenges/assets/create.js',
|
||||
'update': '/plugins/challenges/assets/update.js',
|
||||
'view': '/plugins/challenges/assets/view.js',
|
||||
}
|
||||
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
||||
route = '/plugins/challenges/assets/'
|
||||
# Blueprint used to access the static_folder directory.
|
||||
blueprint = Blueprint('standard', __name__, template_folder='templates', static_folder='assets')
|
||||
|
||||
@staticmethod
|
||||
def create(request):
|
||||
|
@ -33,39 +41,14 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param request:
|
||||
:return:
|
||||
"""
|
||||
# Create challenge
|
||||
chal = Challenges(
|
||||
name=request.form['name'],
|
||||
description=request.form['description'],
|
||||
value=request.form['value'],
|
||||
category=request.form['category'],
|
||||
type=request.form['chaltype']
|
||||
)
|
||||
data = request.form or request.get_json()
|
||||
|
||||
if 'hidden' in request.form:
|
||||
chal.hidden = True
|
||||
else:
|
||||
chal.hidden = False
|
||||
challenge = Challenges(**data)
|
||||
|
||||
max_attempts = request.form.get('max_attempts')
|
||||
if max_attempts and max_attempts.isdigit():
|
||||
chal.max_attempts = int(max_attempts)
|
||||
|
||||
db.session.add(chal)
|
||||
db.session.add(challenge)
|
||||
db.session.commit()
|
||||
|
||||
flag = Keys(chal.id, request.form['key'], request.form['key_type[0]'])
|
||||
if request.form.get('keydata'):
|
||||
flag.data = request.form.get('keydata')
|
||||
db.session.add(flag)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
files = request.files.getlist('files[]')
|
||||
for f in files:
|
||||
utils.upload_file(file=f, chalid=chal.id)
|
||||
|
||||
db.session.commit()
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def read(challenge):
|
||||
|
@ -81,7 +64,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
'value': challenge.value,
|
||||
'description': challenge.description,
|
||||
'category': challenge.category,
|
||||
'hidden': challenge.hidden,
|
||||
'state': challenge.state,
|
||||
'max_attempts': challenge.max_attempts,
|
||||
'type': challenge.type,
|
||||
'type_data': {
|
||||
|
@ -91,7 +74,7 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
'scripts': CTFdStandardChallenge.scripts,
|
||||
}
|
||||
}
|
||||
return challenge, data
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def update(challenge, request):
|
||||
|
@ -103,14 +86,12 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param request:
|
||||
:return:
|
||||
"""
|
||||
challenge.name = request.form['name']
|
||||
challenge.description = request.form['description']
|
||||
challenge.value = int(request.form.get('value', 0)) if request.form.get('value', 0) else 0
|
||||
challenge.max_attempts = int(request.form.get('max_attempts', 0)) if request.form.get('max_attempts', 0) else 0
|
||||
challenge.category = request.form['category']
|
||||
challenge.hidden = 'hidden' in request.form
|
||||
data = request.form or request.get_json()
|
||||
for attr, value in data.items():
|
||||
setattr(challenge, attr, value)
|
||||
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def delete(challenge):
|
||||
|
@ -120,15 +101,15 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param challenge:
|
||||
:return:
|
||||
"""
|
||||
WrongKeys.query.filter_by(chalid=challenge.id).delete()
|
||||
Solves.query.filter_by(chalid=challenge.id).delete()
|
||||
Keys.query.filter_by(chal=challenge.id).delete()
|
||||
files = Files.query.filter_by(chal=challenge.id).all()
|
||||
Fails.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Solves.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Flags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all()
|
||||
for f in files:
|
||||
utils.delete_file(f.id)
|
||||
Files.query.filter_by(chal=challenge.id).delete()
|
||||
Tags.query.filter_by(chal=challenge.id).delete()
|
||||
Hints.query.filter_by(chal=challenge.id).delete()
|
||||
delete_file(f.id)
|
||||
ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Tags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Hints.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Challenges.query.filter_by(id=challenge.id).delete()
|
||||
db.session.commit()
|
||||
|
||||
|
@ -143,15 +124,16 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param request: The request the user submitted
|
||||
:return: (boolean, string)
|
||||
"""
|
||||
provided_key = request.form['key'].strip()
|
||||
chal_keys = Keys.query.filter_by(chal=chal.id).all()
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
chal_keys = Flags.query.filter_by(challenge_id=chal.id).all()
|
||||
for chal_key in chal_keys:
|
||||
if get_key_class(chal_key.type).compare(chal_key, provided_key):
|
||||
if get_flag_class(chal_key.type).compare(chal_key, submission):
|
||||
return True, 'Correct'
|
||||
return False, 'Incorrect'
|
||||
|
||||
@staticmethod
|
||||
def solve(team, chal, request):
|
||||
def solve(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert Solves into the database in order to mark a challenge as solved.
|
||||
|
||||
|
@ -160,24 +142,38 @@ class CTFdStandardChallenge(BaseChallenge):
|
|||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
provided_key = request.form['key'].strip()
|
||||
solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key)
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
solve = Solves(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(req=request),
|
||||
provided=submission
|
||||
)
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
@staticmethod
|
||||
def fail(team, chal, request):
|
||||
def fail(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert WrongKeys into the database in order to mark an answer incorrect.
|
||||
This method is used to insert Fails into the database in order to mark an answer incorrect.
|
||||
|
||||
:param team: The Team object from the database
|
||||
:param chal: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
provided_key = request.form['key'].strip()
|
||||
wrong = WrongKeys(teamid=team.id, chalid=chal.id, ip=utils.get_ip(request), flag=provided_key)
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
wrong = Fails(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(request),
|
||||
provided=submission
|
||||
)
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
<form method="POST" action="{{ script_root }}/admin/challenges/new" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Name<br>
|
||||
<small class="form-text text-muted">
|
||||
The name of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Category<br>
|
||||
<small class="form-text text-muted">
|
||||
The category of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
|
||||
data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Message:<br>
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points are rewarded for solving this challenge.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="state" value="hidden">
|
||||
<input type="hidden" name="type" value="standard">
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
|
@ -1,99 +0,0 @@
|
|||
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab" data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points are rewarded for solving this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Flag
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is the flag or solution for your challenge. You can choose whether your flag is a static string or a regular expression."></i>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="key" placeholder="Enter flag">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<select class="custom-select" name="key_type[0]">
|
||||
<option value="static">Static</option>
|
||||
<option value="regex">Regex</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<div class="checkbox">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" name="hidden" type="checkbox">
|
||||
Hide challenge on creation
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<div class="checkbox">
|
||||
<label class="form-check-label">
|
||||
<input class="form-check-input" type="checkbox" id="solve-attempts-checkbox">
|
||||
Limit amount of solve attempts
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div id="solve-attempts-input" style="display: none;">
|
||||
<label for="max_attempts">Maximum Attempts
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="How many attempts should a user have for this challenge? For unlimited attempts, use the value 0"></i>
|
||||
</label>
|
||||
<input class="form-control" id='max_attempts' name='max_attempts' type='number' placeholder="0">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Upload Challenge Files
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Challenges files are provided to users for download alongside your challenge description"></i>
|
||||
</label>
|
||||
<input class="form-control-file" type="file" name="files[]" multiple="multiple">
|
||||
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<input type="hidden" value="standard" name="chaltype" id="chaltype">
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
|
@ -1,37 +0,0 @@
|
|||
window.challenge.data = undefined;
|
||||
|
||||
window.challenge.renderer = new markdownit({
|
||||
html: true,
|
||||
});
|
||||
|
||||
window.challenge.preRender = function(){
|
||||
|
||||
};
|
||||
|
||||
window.challenge.render = function(markdown){
|
||||
return window.challenge.renderer.render(markdown);
|
||||
};
|
||||
|
||||
|
||||
window.challenge.postRender = function(){
|
||||
|
||||
};
|
||||
|
||||
|
||||
window.challenge.submit = function(cb, preview){
|
||||
var chal_id = $('#chal-id').val();
|
||||
var answer = $('#answer-input').val();
|
||||
var nonce = $('#nonce').val();
|
||||
|
||||
var url = "/chal/";
|
||||
if (preview) {
|
||||
url = "/admin/chal/";
|
||||
}
|
||||
|
||||
$.post(script_root + url + chal_id, {
|
||||
key: answer,
|
||||
nonce: nonce
|
||||
}, function (data) {
|
||||
cb(data);
|
||||
});
|
||||
};
|
|
@ -1,104 +0,0 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#challenge">Challenge</a>
|
||||
</li>
|
||||
{% if solves == '-1 Solves' %}
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link chal-solves" href="#solves">{{solves}}</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
|
||||
<h3 class="chal-value text-center">{{ value }}</h3>
|
||||
<div class="chal-tags text-center">
|
||||
{% for tag in tags %}
|
||||
<span class='badge badge-info chal-tag'>{{tag}}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="chal-desc">{{ description | safe }}</span>
|
||||
<div class="chal-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
<a class="btn btn-info btn-hint btn-block" href="javascript:;" onclick="javascript:loadhint({{hint.id}})">
|
||||
{% if hint.hint %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% else %}
|
||||
{% if hint.cost %}
|
||||
<small>
|
||||
Unlock Hint for {{hint.cost}} points
|
||||
</small>
|
||||
{% else %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row chal-files text-center pb-3">
|
||||
{% for file in files %}
|
||||
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
|
||||
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate' href='{{script_root}}/files/{{file}}'>
|
||||
<i class="fas fa-download"></i>
|
||||
<small>
|
||||
{{ file.split('/')[1] }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9 form-group">
|
||||
<input class="form-control" type="text" name="answer" id="answer-input" placeholder="Flag" />
|
||||
<input id="chal-id" type="hidden" value="{{id}}">
|
||||
</div>
|
||||
<div class="col-md-3 form-group key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5" class="btn btn-md btn-outline-secondary float-right">Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div class="col-md-12">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center w-100" role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,94 +0,0 @@
|
|||
<div id="update-challenge" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="chal-title text-center">{{ name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ script_root }}/admin/chal/update">
|
||||
<input name='nonce' type='hidden' value="{{ nonce }}">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The name of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" placeholder="Enter challenge name" value="{{ name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="The category of your challenge"></i>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" placeholder="Enter challenge category" value="{{ category }}">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home" role="tab" data-toggle="tab">
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message:
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="Use this to give a brief introduction to your challenge. The description supports HTML and Markdown."></i>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="desc-preview" style="height:214px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Value
|
||||
<i class="far fa-question-circle text-muted cursor-help" data-toggle="tooltip" data-placement="right" title="This is how many points teams will receive once they solve this challenge."></i>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" placeholder="Enter value" value="{{ value }}" required>
|
||||
</div>
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-attempts-checkbox" id="limit_max_attempts" name="limit_max_attempts" type="checkbox" {% if max_attempts %}checked{% endif %}>
|
||||
Limit challenge attempts
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="chal-attempts-group" {% if not max_attempts %}style="display:none;"{% endif %}>
|
||||
<label for="value">Max Attempts</label>
|
||||
<input type="number" class="form-control chal-attempts" id="chal-attempts-input" name="max_attempts" placeholder="Enter value" value="{{ max_attempts }}">
|
||||
</div>
|
||||
<input class="chal-id" type='hidden' name='id' placeholder='ID' value="{{ id }}">
|
||||
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input class="chal-hidden" name="hidden" type="checkbox"
|
||||
{% if hidden %}checked{% endif %}>
|
||||
Hidden
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<div style="text-align:center">
|
||||
<button class="btn btn-success btn-outlined update-challenge-submit" type="submit">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,63 @@
|
|||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Name<br>
|
||||
<small class="form-text text-muted">Challenge Name</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Category<br>
|
||||
<small class="form-text text-muted">Challenge Category</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Message<br>
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description" rows="10">{{ challenge.description }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">
|
||||
Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points teams will receive once they solve this challenge.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Max Attempts<br>
|
||||
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
|
||||
</label>
|
||||
|
||||
<input type="number" class="form-control chal-attempts" name="max_attempts" value="{{ challenge.max_attempts }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
State<br>
|
||||
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
|
||||
</label>
|
||||
|
||||
<select class="form-control" name="state">
|
||||
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
|
||||
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-success btn-outlined float-right" type="submit">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,112 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#challenge">Challenge</a>
|
||||
</li>
|
||||
{% if solves == None %}
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link chal-solves" href="#solves">
|
||||
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
|
||||
<h3 class="chal-value text-center">{{ value }}</h3>
|
||||
<div class="chal-tags text-center">
|
||||
{% for tag in tags %}
|
||||
<span class='badge badge-info chal-tag'>{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="chal-desc">{{ description | safe }}</span>
|
||||
<div class="chal-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
<a class="btn btn-info btn-hint btn-block" href="javascript:;"
|
||||
onclick="javascript:loadhint({{ hint.id }})">
|
||||
{% if hint.content %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% else %}
|
||||
{% if hint.cost %}
|
||||
<small>
|
||||
Unlock Hint for {{ hint.cost }} points
|
||||
</small>
|
||||
{% else %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row chal-files text-center pb-3">
|
||||
{% for file in files %}
|
||||
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
|
||||
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
|
||||
href='{{ script_root }}/files/{{ file }}'>
|
||||
<i class="fas fa-download"></i>
|
||||
<small>
|
||||
{{ file.split('/')[1] }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9 form-group">
|
||||
<input class="form-control" type="text" name="answer" id="answer-input"
|
||||
placeholder="Flag"/>
|
||||
<input id="chal-id" type="hidden" value="{{ id }}">
|
||||
</div>
|
||||
<div class="col-md-3 form-group key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5"
|
||||
class="btn btn-md btn-outline-secondary float-right">Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div class="col-md-12">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center w-100"
|
||||
role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,56 @@
|
|||
window.challenge.data = undefined;
|
||||
|
||||
window.challenge.renderer = new markdownit({
|
||||
html: true,
|
||||
});
|
||||
|
||||
window.challenge.preRender = function () {
|
||||
|
||||
};
|
||||
|
||||
window.challenge.render = function (markdown) {
|
||||
return window.challenge.renderer.render(markdown);
|
||||
};
|
||||
|
||||
|
||||
window.challenge.postRender = function () {
|
||||
|
||||
};
|
||||
|
||||
|
||||
window.challenge.submit = function (cb, preview) {
|
||||
var challenge_id = parseInt($('#chal-id').val());
|
||||
var submission = $('#answer-input').val();
|
||||
var url = "/api/v1/challenges/attempt";
|
||||
|
||||
if (preview) {
|
||||
url += "?preview=true";
|
||||
}
|
||||
|
||||
var params = {
|
||||
'challenge_id': challenge_id,
|
||||
'submission': submission
|
||||
};
|
||||
|
||||
fetch(script_root + url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
}).then(function (response) {
|
||||
if (response.status === 429) {
|
||||
// User was ratelimited but process response
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 403) {
|
||||
// User is not logged in or CTF is paused.
|
||||
return response.json();
|
||||
}
|
||||
return response.json();
|
||||
}).then(function (response) {
|
||||
cb(response);
|
||||
});
|
||||
};
|
|
@ -0,0 +1,2 @@
|
|||
__pycache__/
|
||||
*.py[cod]
|
|
@ -0,0 +1,54 @@
|
|||
# Dynamic Value Challenges for CTFd
|
||||
|
||||
It's becoming commonplace in CTF to see challenges whose point values decrease
|
||||
after each solve.
|
||||
|
||||
This CTFd plugin creates a dynamic challenge type which implements this
|
||||
behavior. Each dynamic challenge starts with an initial point value and then
|
||||
each solve will decrease the value of the challenge until a minimum point value.
|
||||
|
||||
By reducing the value of the challenge on each solve, all users who have previously
|
||||
solved the challenge will have lowered scores. Thus an easier and more solved
|
||||
challenge will naturally have a lower point value than a harder and less solved
|
||||
challenge.
|
||||
|
||||
Within CTFd you are free to mix and match regular and dynamic challenges.
|
||||
|
||||
The current implementation requires the challenge to keep track of three values:
|
||||
|
||||
* Initial - The original point valuation
|
||||
* Decay - The amount of solves before the challenge will be at the minimum
|
||||
* Minimum - The lowest possible point valuation
|
||||
|
||||
The value decay logic is implemented with the following math:
|
||||
|
||||
<!--
|
||||
$$a=\textrm{max points}$$
|
||||
$$b=\textrm{min points}$$
|
||||
$$s=\textrm{solve threshold}$$
|
||||
|
||||
$$f(x)=\frac{b-a}{s^{2}}x^{2}+a$$
|
||||
-->
|
||||
|
||||
![](https://raw.githubusercontent.com/CTFd/DynamicValueChallenge/master/function.png)
|
||||
|
||||
or in pseudo code:
|
||||
|
||||
```
|
||||
value = (((minimum - initial)/(decay**2)) * (solve_count**2)) + initial
|
||||
value = math.ceil(value)
|
||||
```
|
||||
|
||||
If the number generated is lower than the minimum, the minimum is chosen
|
||||
instead.
|
||||
|
||||
A parabolic function is chosen instead of an exponential or logarithmic decay function
|
||||
so that higher valued challenges have a slower drop from their initial value.
|
||||
|
||||
# Installation
|
||||
|
||||
**REQUIRES: CTFd >= v1.2.0**
|
||||
|
||||
1. Clone this repository to `CTFd/plugins`. It is important that the folder is
|
||||
named `DynamicValueChallenge` so CTFd can serve the files in the `assets`
|
||||
directory.
|
|
@ -0,0 +1,239 @@
|
|||
from __future__ import division # Use floating point for math calculations
|
||||
from CTFd.plugins.challenges import BaseChallenge, CHALLENGE_CLASSES
|
||||
from CTFd.plugins import register_plugin_assets_directory
|
||||
from CTFd.plugins.flags import get_flag_class
|
||||
from CTFd.models import db, Solves, Fails, Flags, Challenges, Files, Tags, Teams, Hints
|
||||
from CTFd import utils
|
||||
from CTFd.utils.migrations import upgrade
|
||||
from CTFd.utils.user import get_ip
|
||||
from CTFd.utils.uploads import upload_file, delete_file
|
||||
from flask import Blueprint
|
||||
import math
|
||||
|
||||
|
||||
class DynamicValueChallenge(BaseChallenge):
|
||||
id = "dynamic" # Unique identifier used to register challenges
|
||||
name = "dynamic" # Name of a challenge type
|
||||
templates = { # Handlebars templates used for each aspect of challenge editing & viewing
|
||||
'create': '/plugins/dynamic_challenges/assets/create.html',
|
||||
'update': '/plugins/dynamic_challenges/assets/update.html',
|
||||
'view': '/plugins/dynamic_challenges/assets/view.html',
|
||||
}
|
||||
scripts = { # Scripts that are loaded when a template is loaded
|
||||
'create': '/plugins/dynamic_challenges/assets/create.js',
|
||||
'update': '/plugins/dynamic_challenges/assets/update.js',
|
||||
'view': '/plugins/dynamic_challenges/assets/view.js',
|
||||
}
|
||||
# Route at which files are accessible. This must be registered using register_plugin_assets_directory()
|
||||
route = '/plugins/dynamic_challenges/assets/'
|
||||
# Blueprint used to access the static_folder directory.
|
||||
blueprint = Blueprint('dynamic_challenges', __name__, template_folder='templates', static_folder='assets')
|
||||
|
||||
@staticmethod
|
||||
def create(request):
|
||||
"""
|
||||
This method is used to process the challenge creation request.
|
||||
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
challenge = DynamicChallenge(**data)
|
||||
|
||||
db.session.add(challenge)
|
||||
db.session.commit()
|
||||
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def read(challenge):
|
||||
"""
|
||||
This method is in used to access the data of a challenge in a format processable by the front end.
|
||||
|
||||
:param challenge:
|
||||
:return: Challenge object, data dictionary to be returned to the user
|
||||
"""
|
||||
challenge = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
||||
data = {
|
||||
'id': challenge.id,
|
||||
'name': challenge.name,
|
||||
'value': challenge.value,
|
||||
'initial': challenge.initial,
|
||||
'decay': challenge.decay,
|
||||
'minimum': challenge.minimum,
|
||||
'description': challenge.description,
|
||||
'category': challenge.category,
|
||||
'state': challenge.state,
|
||||
'max_attempts': challenge.max_attempts,
|
||||
'type': challenge.type,
|
||||
'type_data': {
|
||||
'id': DynamicValueChallenge.id,
|
||||
'name': DynamicValueChallenge.name,
|
||||
'templates': DynamicValueChallenge.templates,
|
||||
'scripts': DynamicValueChallenge.scripts,
|
||||
}
|
||||
}
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def update(challenge, request):
|
||||
"""
|
||||
This method is used to update the information associated with a challenge. This should be kept strictly to the
|
||||
Challenges table and any child tables.
|
||||
|
||||
:param challenge:
|
||||
:param request:
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
data['initial'] = float(data.get('initial', 0))
|
||||
data['minimum'] = float(data.get('minimum', 0))
|
||||
data['decay'] = float(data.get('decay', 0))
|
||||
for attr, value in data.items():
|
||||
setattr(challenge, attr, value)
|
||||
|
||||
solve_count = Solves.query \
|
||||
.join(Teams, Solves.team_id == Teams.id) \
|
||||
.filter(Solves.challenge_id == challenge.id, Teams.banned == False) \
|
||||
.count()
|
||||
|
||||
# It is important that this calculation takes into account floats.
|
||||
# Hence this file uses from __future__ import division
|
||||
value = (((challenge.minimum - challenge.initial) / (challenge.decay ** 2)) * (solve_count ** 2)) + challenge.initial
|
||||
|
||||
value = math.ceil(value)
|
||||
|
||||
if value < challenge.minimum:
|
||||
value = challenge.minimum
|
||||
|
||||
challenge.value = value
|
||||
|
||||
db.session.commit()
|
||||
return challenge
|
||||
|
||||
@staticmethod
|
||||
def delete(challenge):
|
||||
"""
|
||||
This method is used to delete the resources used by a challenge.
|
||||
|
||||
:param challenge:
|
||||
:return:
|
||||
"""
|
||||
Fails.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Solves.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Flags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
files = Files.query.filter_by(challenge_id=challenge.id).all()
|
||||
for f in files:
|
||||
delete_file(f.id)
|
||||
Files.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Tags.query.filter_by(challenge_id=challenge.id).delete()
|
||||
Hints.query.filter_by(challenge_id=challenge.id).delete()
|
||||
DynamicChallenge.query.filter_by(id=challenge.id).delete()
|
||||
Challenges.query.filter_by(id=challenge.id).delete()
|
||||
db.session.commit()
|
||||
|
||||
@staticmethod
|
||||
def attempt(chal, request):
|
||||
"""
|
||||
This method is used to check whether a given input is right or wrong. It does not make any changes and should
|
||||
return a boolean for correctness and a string to be shown to the user. It is also in charge of parsing the
|
||||
user's input from the request itself.
|
||||
|
||||
:param chal: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return: (boolean, string)
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
chal_keys = Flags.query.filter_by(challenge_id=chal.id).all()
|
||||
for chal_key in chal_keys:
|
||||
if get_flag_class(chal_key.type).compare(chal_key, submission):
|
||||
return True, 'Correct'
|
||||
return False, 'Incorrect'
|
||||
|
||||
@staticmethod
|
||||
def solve(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert Solves into the database in order to mark a challenge as solved.
|
||||
|
||||
:param team: The Team object from the database
|
||||
:param chal: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
chal = DynamicChallenge.query.filter_by(id=challenge.id).first()
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
|
||||
solve_count = Solves.query\
|
||||
.join(Teams, Solves.team_id == Teams.id)\
|
||||
.filter(Solves.challenge_id == chal.id, Teams.banned == False)\
|
||||
.count()
|
||||
|
||||
# It is important that this calculation takes into account floats.
|
||||
# Hence this file uses from __future__ import division
|
||||
value = (
|
||||
(
|
||||
(chal.minimum - chal.initial) / (chal.decay**2)
|
||||
) * (solve_count**2)
|
||||
) + chal.initial
|
||||
|
||||
value = math.ceil(value)
|
||||
|
||||
if value < chal.minimum:
|
||||
value = chal.minimum
|
||||
|
||||
chal.value = value
|
||||
|
||||
solve = Solves(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(req=request),
|
||||
provided=submission
|
||||
)
|
||||
db.session.add(solve)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
@staticmethod
|
||||
def fail(user, team, challenge, request):
|
||||
"""
|
||||
This method is used to insert Fails into the database in order to mark an answer incorrect.
|
||||
|
||||
:param team: The Team object from the database
|
||||
:param challenge: The Challenge object from the database
|
||||
:param request: The request the user submitted
|
||||
:return:
|
||||
"""
|
||||
data = request.form or request.get_json()
|
||||
submission = data['submission'].strip()
|
||||
wrong = Fails(
|
||||
user_id=user.id,
|
||||
team_id=team.id if team else None,
|
||||
challenge_id=challenge.id,
|
||||
ip=get_ip(request),
|
||||
provided=submission
|
||||
)
|
||||
db.session.add(wrong)
|
||||
db.session.commit()
|
||||
db.session.close()
|
||||
|
||||
|
||||
class DynamicChallenge(Challenges):
|
||||
__mapper_args__ = {'polymorphic_identity': 'dynamic'}
|
||||
id = db.Column(None, db.ForeignKey('challenges.id'), primary_key=True)
|
||||
initial = db.Column(db.Integer)
|
||||
minimum = db.Column(db.Integer)
|
||||
decay = db.Column(db.Integer)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DynamicChallenge, self).__init__(**kwargs)
|
||||
self.initial = kwargs['value']
|
||||
|
||||
|
||||
def load(app):
|
||||
# upgrade()
|
||||
app.db.create_all()
|
||||
CHALLENGE_CLASSES['dynamic'] = DynamicValueChallenge
|
||||
register_plugin_assets_directory(app, base_path='/plugins/dynamic_challenges/assets/')
|
|
@ -0,0 +1,88 @@
|
|||
<form method="POST" action="{{ script_root }}/admin/chal/new" enctype="multipart/form-data">
|
||||
<div class="form-group">
|
||||
<div class="alert alert-secondary" role="alert">
|
||||
Dynamic value challenges decrease in value as they receive solves. The more solves a dynamic challenge has,
|
||||
the
|
||||
lower its value is to everyone who has solved it.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="name">Name<br>
|
||||
<small class="form-text text-muted">
|
||||
The name of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="name" placeholder="Enter challenge name">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category<br>
|
||||
<small class="form-text text-muted">
|
||||
The category of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control" name="category" placeholder="Enter challenge category">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="new-desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#new-desc-write" aria-controls="home" role="tab"
|
||||
data-toggle="tab">Write</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#new-desc-preview" aria-controls="home" role="tab" data-toggle="tab">Preview</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="new-desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge. The description supports HTML and
|
||||
Markdown.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="new-desc-editor" class="form-control" name="description" rows="10"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="new-desc-preview" style="height:234px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Initial Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points the challenge is worth initially.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="value" placeholder="Enter value" required>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Decay Limit<br>
|
||||
<small class="form-text text-muted">
|
||||
The amount of solves before the challenge reaches its minimum value
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="decay" placeholder="Enter decay limit" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Minimum Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is the lowest that the challenge can be worth
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control" name="minimum" placeholder="Enter minimum value" required>
|
||||
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="state" value="hidden">
|
||||
<input type="hidden" value="dynamic" name="type" id="chaltype">
|
||||
|
||||
<div class="form-group">
|
||||
<button class="btn btn-primary float-right create-challenge-submit" type="submit">Create</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,29 @@
|
|||
// Markdown Preview
|
||||
$('#desc-edit').on('shown.bs.tab', function (event) {
|
||||
if (event.target.hash == '#desc-preview'){
|
||||
var editor_value = $('#desc-editor').val();
|
||||
$(event.target.hash).html(
|
||||
window.challenge.render(editor_value)
|
||||
);
|
||||
}
|
||||
});
|
||||
$('#new-desc-edit').on('shown.bs.tab', function (event) {
|
||||
if (event.target.hash == '#new-desc-preview'){
|
||||
var editor_value = $('#new-desc-editor').val();
|
||||
$(event.target.hash).html(
|
||||
window.challenge.render(editor_value)
|
||||
);
|
||||
}
|
||||
});
|
||||
$("#solve-attempts-checkbox").change(function() {
|
||||
if(this.checked) {
|
||||
$('#solve-attempts-input').show();
|
||||
} else {
|
||||
$('#solve-attempts-input').hide();
|
||||
$('#max_attempts').val('');
|
||||
}
|
||||
});
|
||||
|
||||
$(document).ready(function(){
|
||||
$('[data-toggle="tooltip"]').tooltip();
|
||||
});
|
|
@ -0,0 +1,112 @@
|
|||
<form method="POST">
|
||||
<div class="form-group">
|
||||
<label for="name">Name<br>
|
||||
<small class="form-text text-muted">
|
||||
The name of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-name" name="name" value="{{ challenge.name }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="category">Category<br>
|
||||
<small class="form-text text-muted">
|
||||
The category of your challenge
|
||||
</small>
|
||||
</label>
|
||||
<input type="text" class="form-control chal-category" name="category" value="{{ challenge.category }}">
|
||||
</div>
|
||||
|
||||
<ul class="nav nav-tabs" role="tablist" id="desc-edit">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#desc-write" id="desc-write-link" aria-controls="home"
|
||||
role="tab" data-toggle="tab">
|
||||
Write
|
||||
</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#desc-preview" aria-controls="home" role="tab" data-toggle="tab">
|
||||
Preview
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane active" id="desc-write">
|
||||
<div class="form-group">
|
||||
<label for="message-text" class="control-label">Message<br>
|
||||
<small class="form-text text-muted">
|
||||
Use this to give a brief introduction to your challenge.
|
||||
</small>
|
||||
</label>
|
||||
<textarea id="desc-editor" class="form-control chal-desc-editor" name="description"
|
||||
rows="10">{{ challenge.description }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane content" id="desc-preview"
|
||||
style="height:214px; overflow-y: scroll;">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Current Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points the challenge is worth right now.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-value" name="value" value="{{ challenge.value }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Initial Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is how many points the challenge was worth initially.
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-initial" name="initial" value="{{ challenge.initial }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Decay Limit<br>
|
||||
<small class="form-text text-muted">
|
||||
The amount of solves before the challenge reaches its minimum value
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-decay" name="decay" value="{{ challenge.decay }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="value">Minimum Value<br>
|
||||
<small class="form-text text-muted">
|
||||
This is the lowest that the challenge can be worth
|
||||
</small>
|
||||
</label>
|
||||
<input type="number" class="form-control chal-minimum" name="minimum" value="{{ challenge.minimum }}" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
Max Attempts<br>
|
||||
<small class="form-text text-muted">Maximum amount of attempts users receive. Leave at 0 for unlimited.</small>
|
||||
</label>
|
||||
|
||||
<input type="number" class="form-control chal-attempts" name="max_attempts"
|
||||
value="{{ challenge.max_attempts }}">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
State<br>
|
||||
<small class="form-text text-muted">Changes the state of the challenge (e.g. visible, hidden)</small>
|
||||
</label>
|
||||
|
||||
<select class="form-control" name="state">
|
||||
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
|
||||
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button class="btn btn-success btn-outlined float-right" type="submit">
|
||||
Update
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
|
@ -0,0 +1,112 @@
|
|||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<ul class="nav nav-tabs">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link active" href="#challenge">Challenge</a>
|
||||
</li>
|
||||
{% if solves == None %}
|
||||
{% else %}
|
||||
<li class="nav-item">
|
||||
<a class="nav-link chal-solves" href="#solves">
|
||||
{{ solves }} {% if solves > 1 %}Solves{% else %}Solves{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div role="tabpanel">
|
||||
<div class="tab-content">
|
||||
<div role="tabpanel" class="tab-pane fade show active" id="challenge">
|
||||
<h2 class='chal-name text-center pt-3'>{{ name }}</h2>
|
||||
<h3 class="chal-value text-center">{{ value }}</h3>
|
||||
<div class="chal-tags text-center">
|
||||
{% for tag in tags %}
|
||||
<span class='badge badge-info chal-tag'>{{ tag }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<span class="chal-desc">{{ description | safe }}</span>
|
||||
<div class="chal-hints hint-row row">
|
||||
{% for hint in hints %}
|
||||
<div class='col-md-12 hint-button-wrapper text-center mb-3'>
|
||||
<a class="btn btn-info btn-hint btn-block" href="javascript:;"
|
||||
onclick="javascript:loadhint({{ hint.id }})">
|
||||
{% if hint.hint %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% else %}
|
||||
{% if hint.cost %}
|
||||
<small>
|
||||
Unlock Hint for {{ hint.cost }} points
|
||||
</small>
|
||||
{% else %}
|
||||
<small>
|
||||
View Hint
|
||||
</small>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="row chal-files text-center pb-3">
|
||||
{% for file in files %}
|
||||
<div class='col-md-4 col-sm-4 col-xs-12 file-button-wrapper d-block'>
|
||||
<a class='btn btn-info btn-file mb-1 d-inline-block px-2 w-100 text-truncate'
|
||||
href='{{ script_root }}/files/{{ file }}'>
|
||||
<i class="fas fa-download"></i>
|
||||
<small>
|
||||
{{ file.split('/')[1] }}
|
||||
</small>
|
||||
</a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="row submit-row">
|
||||
<div class="col-md-9 form-group">
|
||||
<input class="form-control" type="text" name="answer" id="answer-input"
|
||||
placeholder="Flag"/>
|
||||
<input id="chal-id" type="hidden" value="{{ id }}">
|
||||
</div>
|
||||
<div class="col-md-3 form-group key-submit">
|
||||
<button type="submit" id="submit-key" tabindex="5"
|
||||
class="btn btn-md btn-outline-secondary float-right">Submit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row notification-row">
|
||||
<div class="col-md-12">
|
||||
<div id="result-notification" class="alert alert-dismissable text-center w-100"
|
||||
role="alert" style="display: none;">
|
||||
<strong id="result-message"></strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div role="tabpanel" class="tab-pane fade" id="solves">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<table class="table table-striped text-center">
|
||||
<thead>
|
||||
<tr>
|
||||
<td><b>Name</b>
|
||||
</td>
|
||||
<td><b>Date</b>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="chal-solves-names">
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,56 @@
|
|||
window.challenge.data = undefined;
|
||||
|
||||
window.challenge.renderer = new markdownit({
|
||||
html: true,
|
||||
});
|
||||
|
||||
window.challenge.preRender = function () {
|
||||
|
||||
};
|
||||
|
||||
window.challenge.render = function (markdown) {
|
||||
return window.challenge.renderer.render(markdown);
|
||||
};
|
||||
|
||||
|
||||
window.challenge.postRender = function () {
|
||||
|
||||
};
|
||||
|
||||
|
||||
window.challenge.submit = function (cb, preview) {
|
||||
var challenge_id = parseInt($('#chal-id').val());
|
||||
var submission = $('#answer-input').val();
|
||||
var url = "/api/v1/challenges/attempt";
|
||||
|
||||
if (preview) {
|
||||
url += "?preview=true";
|
||||
}
|
||||
|
||||
var params = {
|
||||
'challenge_id': challenge_id,
|
||||
'submission': submission
|
||||
};
|
||||
|
||||
fetch(script_root + url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: {
|
||||
'Accept': 'application/json',
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(params)
|
||||
}).then(function (response) {
|
||||
if (response.status === 429) {
|
||||
// User was ratelimited but process response
|
||||
return response.json();
|
||||
}
|
||||
if (response.status === 403) {
|
||||
// User is not logged in or CTF is paused.
|
||||
return response.json();
|
||||
}
|
||||
return response.json();
|
||||
}).then(function (response) {
|
||||
cb(response);
|
||||
});
|
||||
};
|
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -5,8 +5,7 @@ import string
|
|||
import hmac
|
||||
|
||||
|
||||
class BaseKey(object):
|
||||
id = None
|
||||
class BaseFlag(object):
|
||||
name = None
|
||||
templates = {}
|
||||
|
||||
|
@ -15,17 +14,16 @@ class BaseKey(object):
|
|||
return True
|
||||
|
||||
|
||||
class CTFdStaticKey(BaseKey):
|
||||
id = 0
|
||||
class CTFdStaticFlag(BaseFlag):
|
||||
name = "static"
|
||||
templates = { # Nunjucks templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/static/create-static-modal.njk',
|
||||
'update': '/plugins/keys/assets/static/edit-static-modal.njk',
|
||||
'create': '/plugins/flags/assets/static/create-static-modal.njk',
|
||||
'update': '/plugins/flags/assets/static/edit-static-modal.njk',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def compare(chal_key_obj, provided):
|
||||
saved = chal_key_obj.flag
|
||||
saved = chal_key_obj.content
|
||||
data = chal_key_obj.data
|
||||
|
||||
if len(saved) != len(provided):
|
||||
|
@ -41,17 +39,16 @@ class CTFdStaticKey(BaseKey):
|
|||
return result == 0
|
||||
|
||||
|
||||
class CTFdRegexKey(BaseKey):
|
||||
id = 1
|
||||
class CTFdRegexFlag(BaseFlag):
|
||||
name = "regex"
|
||||
templates = { # Nunjucks templates used for key editing & viewing
|
||||
'create': '/plugins/keys/assets/regex/create-regex-modal.njk',
|
||||
'update': '/plugins/keys/assets/regex/edit-regex-modal.njk',
|
||||
'create': '/plugins/flags/assets/regex/create-regex-modal.njk',
|
||||
'update': '/plugins/flags/assets/regex/edit-regex-modal.njk',
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def compare(chal_key_obj, provided):
|
||||
saved = chal_key_obj.flag
|
||||
saved = chal_key_obj.content
|
||||
data = chal_key_obj.data
|
||||
|
||||
if data == "case_insensitive":
|
||||
|
@ -62,18 +59,18 @@ class CTFdRegexKey(BaseKey):
|
|||
return res and res.group() == provided
|
||||
|
||||
|
||||
KEY_CLASSES = {
|
||||
'static': CTFdStaticKey,
|
||||
'regex': CTFdRegexKey
|
||||
FLAG_CLASSES = {
|
||||
'static': CTFdStaticFlag,
|
||||
'regex': CTFdRegexFlag
|
||||
}
|
||||
|
||||
|
||||
def get_key_class(class_id):
|
||||
cls = KEY_CLASSES.get(class_id)
|
||||
def get_flag_class(class_id):
|
||||
cls = FLAG_CLASSES.get(class_id)
|
||||
if cls is None:
|
||||
raise KeyError
|
||||
return cls
|
||||
|
||||
|
||||
def load(app):
|
||||
register_plugin_assets_directory(app, base_path='/plugins/keys/assets/')
|
||||
register_plugin_assets_directory(app, base_path='/plugins/flags/assets/')
|
|
@ -0,0 +1,14 @@
|
|||
<label>
|
||||
Regex Flag<br>
|
||||
<small>Enter regex flag data</small>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="content" value="{{ content }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="data">
|
||||
<option value="">Case Sensitive</option>
|
||||
<option value="case_insensitive">Case Insensitive</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="type" value="regex">
|
|
@ -0,0 +1,19 @@
|
|||
<label>
|
||||
Regex<br>
|
||||
<small>Enter regex key data</small>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="content" value="{{ content }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="data">
|
||||
<option value="">Case Sensitive</option>
|
||||
<option value="case_insensitive" {% if data %}selected{% endif %}>Case Insensitive</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="type" value="regex">
|
||||
<input type="hidden" name="id" value="{{ id }}">
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success float-right">Update</button>
|
||||
</div>
|
|
@ -0,0 +1,14 @@
|
|||
<label>
|
||||
Static<br>
|
||||
<small>Enter static flag data</small>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="content" value="{{ content }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="data">
|
||||
<option value="">Case Sensitive</option>
|
||||
<option value="case_insensitive">Case Insensitive</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="type" value="static">
|
|
@ -0,0 +1,19 @@
|
|||
<label>
|
||||
Static<br>
|
||||
<small>Enter static flag data</small>
|
||||
</label>
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="content" value="{{ content }}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<select name="data">
|
||||
<option value="">Case Sensitive</option>
|
||||
<option value="case_insensitive" {% if data %}selected{% endif %}>Case Insensitive</option>
|
||||
</select>
|
||||
</div>
|
||||
<input type="hidden" name="type" value="static">
|
||||
<input type="hidden" name="id" value="{{ id }}">
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<button class="btn btn-success float-right">Update</button>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
<label for="create-key-regex" class="control-label">Enter Regex Key Data</label>
|
||||
<input type="text" id="create-key-regex" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive">
|
||||
<label class="form-check-label" for="keydata">Case Insensitive</label>
|
||||
</div>
|
|
@ -1,33 +0,0 @@
|
|||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Regex Key</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{id}}">
|
||||
<input type="text" id="key-data" class="form-control" name="key" value="{{key}}" placeholder="Enter regex key data">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
|
||||
{% if data %}checked{% endif %}>
|
||||
<label class="form-check-label" for="keydata">Case Insensitive</label>
|
||||
</div>
|
||||
<input type="hidden" id="key-type" name="key_type" value="regex">
|
||||
<input type="hidden" id="key-id">
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button id="submit-keys" class="btn btn-success float-right">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
<label for="create-key-static" class="control-label">Enter Static Key Data</label>
|
||||
<input type="text" id="create-key-static" class="form-control" name="key" value="{{key}}" placeholder="Enter static key data">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive">
|
||||
<label class="form-check-label" for="keydata">Case Insensitive</label>
|
||||
</div>
|
|
@ -1,34 +0,0 @@
|
|||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h3 class="text-center">Static Key</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form method="POST" action="{{ script_root }}/admin/keys/{{ id }}">
|
||||
<input type="text" id="key-data" class="form-control" name="key" value="{{ key }}"
|
||||
placeholder="Enter static key data">
|
||||
<div class="form-check form-check-inline">
|
||||
<input class="form-check-input" type="checkbox" id="keydata" name="keydata" value="case_insensitive"
|
||||
{% if data %}checked{% endif %}>
|
||||
<label class="form-check-label" for="keydata">Case Insensitive</label>
|
||||
</div>
|
||||
<input type="hidden" id="key-type" name="key_type" value="static">
|
||||
<input type="hidden" id="key-id">
|
||||
<hr>
|
||||
<div class="form-group">
|
||||
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
|
||||
<button id="submit-keys" class="btn btn-success float-right">Update</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -0,0 +1,51 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Awards
|
||||
|
||||
|
||||
class AwardSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Awards
|
||||
include_fk = True
|
||||
dump_only = ('id', 'date')
|
||||
|
||||
views = {
|
||||
'admin': [
|
||||
'category',
|
||||
'user_id',
|
||||
'name',
|
||||
'description',
|
||||
'value',
|
||||
'team_id',
|
||||
'user',
|
||||
'team',
|
||||
'date',
|
||||
'requirements',
|
||||
'id',
|
||||
'icon'
|
||||
],
|
||||
'user': [
|
||||
'category',
|
||||
'user_id',
|
||||
'name',
|
||||
'description',
|
||||
'value',
|
||||
'team_id',
|
||||
'user',
|
||||
'team',
|
||||
'date',
|
||||
'id',
|
||||
'icon'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(AwardSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,11 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load, validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Challenges
|
||||
|
||||
|
||||
class ChallengeSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Challenges
|
||||
include_fk = True
|
||||
dump_only = ('id',)
|
|
@ -0,0 +1,29 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Configs
|
||||
|
||||
|
||||
class ConfigSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Configs
|
||||
include_fk = True
|
||||
dump_only = ('id',)
|
||||
|
||||
views = {
|
||||
'admin': [
|
||||
'id',
|
||||
'key',
|
||||
'value'
|
||||
],
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(ConfigSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,21 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Files, ChallengeFiles, PageFiles
|
||||
|
||||
|
||||
class FileSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Files
|
||||
include_fk = True
|
||||
dump_only = ('id', 'type', 'location')
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(FileSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,21 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Flags
|
||||
|
||||
|
||||
class FlagSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Flags
|
||||
include_fk = True
|
||||
dump_only = ('id',)
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(FlagSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,45 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Hints
|
||||
|
||||
|
||||
class HintSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Hints
|
||||
include_fk = True
|
||||
dump_only = ('id', 'type')
|
||||
|
||||
views = {
|
||||
'locked': [
|
||||
'id',
|
||||
'type',
|
||||
'challenge',
|
||||
'cost'
|
||||
],
|
||||
'unlocked': [
|
||||
'id',
|
||||
'type',
|
||||
'challenge',
|
||||
'content',
|
||||
'cost'
|
||||
],
|
||||
'admin': [
|
||||
'id',
|
||||
'type',
|
||||
'challenge',
|
||||
'content',
|
||||
'cost',
|
||||
'requirements'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(HintSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,21 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Notifications
|
||||
|
||||
|
||||
class NotificationSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Notifications
|
||||
include_fk = True
|
||||
dump_only = ('id', 'date')
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(NotificationSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,21 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Pages
|
||||
|
||||
|
||||
class PageSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Pages
|
||||
include_fk = True
|
||||
dump_only = ('id', )
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(PageSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,46 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load, validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.schemas.challenges import ChallengeSchema
|
||||
from CTFd.models import ma, Submissions
|
||||
|
||||
|
||||
class SubmissionSchema(ma.ModelSchema):
|
||||
challenge = fields.Nested(ChallengeSchema, only=['name', 'category', 'value'])
|
||||
|
||||
class Meta:
|
||||
model = Submissions
|
||||
include_fk = True
|
||||
dump_only = ('id', )
|
||||
|
||||
views = {
|
||||
'admin': [
|
||||
'provided',
|
||||
'ip',
|
||||
'challenge_id',
|
||||
'challenge',
|
||||
'user',
|
||||
'team',
|
||||
'date',
|
||||
'type',
|
||||
'id'
|
||||
],
|
||||
'user': [
|
||||
'challenge_id',
|
||||
'challenge',
|
||||
'user',
|
||||
'team',
|
||||
'date',
|
||||
'type',
|
||||
'id'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(SubmissionSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,32 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Tags
|
||||
|
||||
|
||||
class TagSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Tags
|
||||
include_fk = True
|
||||
dump_only = ('id',)
|
||||
|
||||
views = {
|
||||
'admin': [
|
||||
'id',
|
||||
'challenge',
|
||||
'value'
|
||||
],
|
||||
'user': [
|
||||
'value'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(TagSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,141 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError, pre_load
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Teams
|
||||
from CTFd.utils.validators import validate_country_code
|
||||
from CTFd.utils.countries import lookup_country_code
|
||||
from CTFd.utils.user import is_admin, get_current_team
|
||||
|
||||
|
||||
class TeamSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Teams
|
||||
include_fk = True
|
||||
dump_only = ('id', 'oauth_id', 'created', 'members')
|
||||
load_only = ('password',)
|
||||
|
||||
name = field_for(
|
||||
Teams,
|
||||
'name',
|
||||
required=True,
|
||||
validate=[
|
||||
validate.Length(min=1, max=128, error='Team names must not be empty')
|
||||
]
|
||||
)
|
||||
email = field_for(
|
||||
Teams,
|
||||
'email',
|
||||
validate=validate.Email('Emails must be a properly formatted email address')
|
||||
)
|
||||
website = field_for(
|
||||
Teams,
|
||||
'website',
|
||||
validate=validate.URL(
|
||||
error='Websites must be a proper URL starting with http or https',
|
||||
schemes={'http', 'https'}
|
||||
)
|
||||
)
|
||||
country = field_for(
|
||||
Teams,
|
||||
'country',
|
||||
validate=[
|
||||
validate_country_code
|
||||
]
|
||||
)
|
||||
|
||||
@pre_load
|
||||
def validate_name(self, data):
|
||||
name = data.get('name')
|
||||
if name is None:
|
||||
return
|
||||
|
||||
existing_team = Teams.query.filter_by(name=name).first()
|
||||
# Admins should be able to patch anyone but they cannot cause a collision.
|
||||
if is_admin():
|
||||
team_id = int(data.get('id', 0))
|
||||
if team_id:
|
||||
if existing_team.id != team_id:
|
||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
||||
else:
|
||||
# If there's no Team ID it means that the admin is creating a team with no ID.
|
||||
if existing_team:
|
||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
||||
else:
|
||||
current_team = get_current_team()
|
||||
# We need to allow teams to edit themselves and allow the "conflict"
|
||||
if data['name'] == current_team.name:
|
||||
return data
|
||||
else:
|
||||
if existing_team:
|
||||
raise ValidationError('Team name has already been taken', field_names=['name'])
|
||||
|
||||
@pre_load
|
||||
def validate_email(self, data):
|
||||
email = data.get('email')
|
||||
if email is None:
|
||||
return
|
||||
|
||||
obj = Teams.query.filter_by(email=email).first()
|
||||
if obj:
|
||||
if is_admin():
|
||||
if data.get('id'):
|
||||
target_user = Teams.query.filter_by(id=data['id']).first()
|
||||
else:
|
||||
target_user = get_current_team()
|
||||
|
||||
if target_user and obj.id != target_user.id:
|
||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
||||
else:
|
||||
if obj.id != get_current_team().id:
|
||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
||||
|
||||
views = {
|
||||
'user': [
|
||||
'website',
|
||||
'name',
|
||||
'country',
|
||||
'affiliation',
|
||||
'bracket',
|
||||
'members',
|
||||
'id',
|
||||
'oauth_id',
|
||||
],
|
||||
'self': [
|
||||
'website',
|
||||
'name',
|
||||
'email',
|
||||
'country',
|
||||
'affiliation',
|
||||
'bracket',
|
||||
'members',
|
||||
'id',
|
||||
'oauth_id',
|
||||
'password'
|
||||
],
|
||||
'admin': [
|
||||
'website',
|
||||
'name',
|
||||
'created',
|
||||
'country',
|
||||
'banned',
|
||||
'email',
|
||||
'affiliation',
|
||||
'secret',
|
||||
'bracket',
|
||||
'members',
|
||||
'hidden',
|
||||
'id',
|
||||
'oauth_id',
|
||||
'password'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(TeamSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,38 @@
|
|||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Unlocks
|
||||
|
||||
|
||||
class UnlockSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Unlocks
|
||||
include_fk = True
|
||||
dump_only = ('id', 'date')
|
||||
|
||||
views = {
|
||||
'admin': [
|
||||
'user_id',
|
||||
'target',
|
||||
'team_id',
|
||||
'date',
|
||||
'type',
|
||||
'id'
|
||||
],
|
||||
'user': [
|
||||
'target',
|
||||
'date',
|
||||
'type',
|
||||
'id'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(UnlockSchema, self).__init__(*args, **kwargs)
|
|
@ -0,0 +1,170 @@
|
|||
from flask import session
|
||||
from sqlalchemy.sql.expression import union_all
|
||||
from marshmallow import fields, post_load
|
||||
from marshmallow import validate, ValidationError, pre_load
|
||||
from marshmallow.decorators import validates_schema
|
||||
from marshmallow_sqlalchemy import field_for
|
||||
from CTFd.models import ma, Users
|
||||
from CTFd.utils.validators import unique_email, validate_country_code
|
||||
from CTFd.utils.user import is_admin, get_current_user
|
||||
from CTFd.utils.countries import lookup_country_code
|
||||
from CTFd.utils.crypto import verify_password, hash_password
|
||||
|
||||
|
||||
class UserSchema(ma.ModelSchema):
|
||||
class Meta:
|
||||
model = Users
|
||||
include_fk = True
|
||||
dump_only = ('id', 'oauth_id', 'created')
|
||||
load_only = ('password',)
|
||||
|
||||
name = field_for(
|
||||
Users,
|
||||
'name',
|
||||
required=True,
|
||||
validate=[
|
||||
validate.Length(min=1, max=128, error='Team names must not be empty')
|
||||
]
|
||||
)
|
||||
email = field_for(
|
||||
Users,
|
||||
'email',
|
||||
validate=[
|
||||
validate.Email('Emails must be a properly formatted email address'),
|
||||
validate.Length(min=1, max=128, error='Emails must not be empty'),
|
||||
]
|
||||
)
|
||||
website = field_for(
|
||||
Users,
|
||||
'website',
|
||||
validate=validate.URL(
|
||||
error='Websites must be a proper URL starting with http or https',
|
||||
schemes={'http', 'https'}
|
||||
)
|
||||
)
|
||||
country = field_for(
|
||||
Users,
|
||||
'country',
|
||||
validate=[
|
||||
validate_country_code
|
||||
]
|
||||
)
|
||||
password = field_for(
|
||||
Users,
|
||||
'password',
|
||||
validate=[
|
||||
validate.Length(min=1, error='Passwords must not be empty'),
|
||||
]
|
||||
)
|
||||
|
||||
@pre_load
|
||||
def validate_name(self, data):
|
||||
name = data.get('name')
|
||||
if name is None:
|
||||
return
|
||||
|
||||
existing_user = Users.query.filter_by(name=name).first()
|
||||
user_id = data.get('id')
|
||||
|
||||
if user_id and is_admin():
|
||||
if existing_user and existing_user.id != user_id:
|
||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
||||
else:
|
||||
current_user = get_current_user()
|
||||
if name == current_user.name:
|
||||
return data
|
||||
else:
|
||||
if existing_user:
|
||||
raise ValidationError('User name has already been taken', field_names=['name'])
|
||||
|
||||
@pre_load
|
||||
def validate_email(self, data):
|
||||
email = data.get('email')
|
||||
if email is None:
|
||||
return
|
||||
|
||||
existing_user = Users.query.filter_by(email=email).first()
|
||||
user_id = data.get('id')
|
||||
|
||||
if user_id and is_admin():
|
||||
if existing_user and existing_user.id != user_id:
|
||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
||||
else:
|
||||
current_user = get_current_user()
|
||||
if email == current_user.email:
|
||||
return data
|
||||
else:
|
||||
if existing_user:
|
||||
raise ValidationError('Email address has already been used', field_names=['email'])
|
||||
|
||||
@pre_load
|
||||
def validate_password_confirmation(self, data):
|
||||
password = data.get('password')
|
||||
confirm = data.get('confirm')
|
||||
target_user = get_current_user()
|
||||
user_id = data.get('id')
|
||||
|
||||
if is_admin():
|
||||
if user_id:
|
||||
if password:
|
||||
data['password'] = hash_password(data['password'])
|
||||
return data
|
||||
else:
|
||||
if password and (confirm is None):
|
||||
raise ValidationError('Please confirm your current password', field_names=['confirm'])
|
||||
|
||||
if password and confirm:
|
||||
test = verify_password(plaintext=confirm, ciphertext=target_user.password)
|
||||
if test is True:
|
||||
data['password'] = hash_password(data['password'])
|
||||
return data
|
||||
else:
|
||||
raise ValidationError('Your previous password is incorrect', field_names=['confirm'])
|
||||
|
||||
views = {
|
||||
'user': [
|
||||
'website',
|
||||
'name',
|
||||
'country',
|
||||
'affiliation',
|
||||
'bracket',
|
||||
'id',
|
||||
'oauth_id',
|
||||
],
|
||||
'self': [
|
||||
'website',
|
||||
'name',
|
||||
'email',
|
||||
'country',
|
||||
'affiliation',
|
||||
'bracket',
|
||||
'id',
|
||||
'oauth_id',
|
||||
'password'
|
||||
],
|
||||
'admin': [
|
||||
'website',
|
||||
'name',
|
||||
'created',
|
||||
'country',
|
||||
'banned',
|
||||
'email',
|
||||
'affiliation',
|
||||
'secret',
|
||||
'bracket',
|
||||
'hidden',
|
||||
'id',
|
||||
'oauth_id',
|
||||
'password',
|
||||
'type'
|
||||
]
|
||||
}
|
||||
|
||||
def __init__(self, view=None, *args, **kwargs):
|
||||
if view:
|
||||
if type(view) == str:
|
||||
kwargs['only'] = self.views[view]
|
||||
elif type(view) == list:
|
||||
kwargs['only'] = view
|
||||
|
||||
super(UserSchema, self).__init__(*args, **kwargs)
|
|
@ -1,176 +1,20 @@
|
|||
from flask import render_template, jsonify, Blueprint, redirect, url_for, request
|
||||
from sqlalchemy.sql.expression import union_all
|
||||
from flask import render_template, Blueprint, redirect, url_for, request
|
||||
|
||||
from CTFd.models import db, Teams, Solves, Awards, Challenges
|
||||
from CTFd.utils import config
|
||||
from CTFd.utils import get_config
|
||||
from CTFd.utils.decorators.visibility import check_score_visibility
|
||||
|
||||
from CTFd import utils
|
||||
from CTFd.utils.scores import get_standings
|
||||
|
||||
scoreboard = Blueprint('scoreboard', __name__)
|
||||
|
||||
|
||||
def get_standings(admin=False, count=None):
|
||||
"""
|
||||
Get team standings as a list of tuples containing team_id, team_name, and score e.g. [(team_id, team_name, score)].
|
||||
|
||||
Ties are broken by who reached a given score first based on the solve ID. Two users can have the same score but one
|
||||
user will have a solve ID that is before the others. That user will be considered the tie-winner.
|
||||
|
||||
Challenges & Awards with a value of zero are filtered out of the calculations to avoid incorrect tie breaks.
|
||||
"""
|
||||
scores = db.session.query(
|
||||
Solves.teamid.label('teamid'),
|
||||
db.func.sum(Challenges.value).label('score'),
|
||||
db.func.max(Solves.id).label('id'),
|
||||
db.func.max(Solves.date).label('date')
|
||||
).join(Challenges)\
|
||||
.filter(Challenges.value != 0)\
|
||||
.group_by(Solves.teamid)
|
||||
|
||||
awards = db.session.query(
|
||||
Awards.teamid.label('teamid'),
|
||||
db.func.sum(Awards.value).label('score'),
|
||||
db.func.max(Awards.id).label('id'),
|
||||
db.func.max(Awards.date).label('date')
|
||||
)\
|
||||
.filter(Awards.value != 0)\
|
||||
.group_by(Awards.teamid)
|
||||
|
||||
"""
|
||||
Filter out solves and awards that are before a specific time point.
|
||||
"""
|
||||
freeze = utils.get_config('freeze')
|
||||
if not admin and freeze:
|
||||
scores = scores.filter(Solves.date < utils.unix_time_to_utc(freeze))
|
||||
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
|
||||
|
||||
"""
|
||||
Combine awards and solves with a union. They should have the same amount of columns
|
||||
"""
|
||||
results = union_all(scores, awards).alias('results')
|
||||
|
||||
"""
|
||||
Sum each of the results by the team id to get their score.
|
||||
"""
|
||||
sumscores = db.session.query(
|
||||
results.columns.teamid,
|
||||
db.func.sum(results.columns.score).label('score'),
|
||||
db.func.max(results.columns.id).label('id'),
|
||||
db.func.max(results.columns.date).label('date')
|
||||
).group_by(results.columns.teamid)\
|
||||
.subquery()
|
||||
|
||||
"""
|
||||
Admins can see scores for all users but the public cannot see banned users.
|
||||
|
||||
Filters out banned users.
|
||||
Properly resolves value ties by ID.
|
||||
|
||||
Different databases treat time precision differently so resolve by the row ID instead.
|
||||
"""
|
||||
if admin:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
Teams.name.label('name'),
|
||||
Teams.banned, sumscores.columns.score
|
||||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
else:
|
||||
standings_query = db.session.query(
|
||||
Teams.id.label('teamid'),
|
||||
Teams.name.label('name'),
|
||||
sumscores.columns.score
|
||||
)\
|
||||
.join(sumscores, Teams.id == sumscores.columns.teamid) \
|
||||
.filter(Teams.banned == False) \
|
||||
.order_by(sumscores.columns.score.desc(), sumscores.columns.id)
|
||||
|
||||
"""
|
||||
Only select a certain amount of users if asked.
|
||||
"""
|
||||
if count is None:
|
||||
standings = standings_query.all()
|
||||
else:
|
||||
standings = standings_query.limit(count).all()
|
||||
db.session.close()
|
||||
|
||||
return standings
|
||||
|
||||
|
||||
@scoreboard.route('/scoreboard')
|
||||
def scoreboard_view():
|
||||
if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
if utils.hide_scores():
|
||||
return render_template('scoreboard.html', errors=['Scores are currently hidden'])
|
||||
@check_score_visibility
|
||||
def listing():
|
||||
standings = get_standings()
|
||||
return render_template('scoreboard.html', teams=standings, score_frozen=utils.is_scoreboard_frozen())
|
||||
|
||||
|
||||
@scoreboard.route('/scores')
|
||||
def scores():
|
||||
json = {'standings': []}
|
||||
if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
if utils.hide_scores():
|
||||
return jsonify(json)
|
||||
|
||||
standings = get_standings()
|
||||
|
||||
for i, x in enumerate(standings):
|
||||
json['standings'].append({'pos': i + 1, 'id': x.teamid, 'team': x.name, 'score': int(x.score)})
|
||||
return jsonify(json)
|
||||
|
||||
|
||||
@scoreboard.route('/top/<int:count>')
|
||||
def topteams(count):
|
||||
json = {'places': {}}
|
||||
if utils.get_config('view_scoreboard_if_authed') and not utils.authed():
|
||||
return redirect(url_for('auth.login', next=request.path))
|
||||
if utils.hide_scores():
|
||||
return jsonify(json)
|
||||
|
||||
if count > 20 or count < 0:
|
||||
count = 10
|
||||
|
||||
standings = get_standings(count=count)
|
||||
|
||||
team_ids = [team.teamid for team in standings]
|
||||
|
||||
solves = Solves.query.filter(Solves.teamid.in_(team_ids))
|
||||
awards = Awards.query.filter(Awards.teamid.in_(team_ids))
|
||||
|
||||
freeze = utils.get_config('freeze')
|
||||
|
||||
if freeze:
|
||||
solves = solves.filter(Solves.date < utils.unix_time_to_utc(freeze))
|
||||
awards = awards.filter(Awards.date < utils.unix_time_to_utc(freeze))
|
||||
|
||||
solves = solves.all()
|
||||
awards = awards.all()
|
||||
|
||||
for i, team in enumerate(team_ids):
|
||||
json['places'][i + 1] = {
|
||||
'id': standings[i].teamid,
|
||||
'name': standings[i].name,
|
||||
'solves': []
|
||||
}
|
||||
for solve in solves:
|
||||
if solve.teamid == team:
|
||||
json['places'][i + 1]['solves'].append({
|
||||
'chal': solve.chalid,
|
||||
'team': solve.teamid,
|
||||
'value': solve.chal.value,
|
||||
'time': utils.unix_time(solve.date)
|
||||
})
|
||||
for award in awards:
|
||||
if award.teamid == team:
|
||||
json['places'][i + 1]['solves'].append({
|
||||
'chal': None,
|
||||
'team': award.teamid,
|
||||
'value': award.value,
|
||||
'time': utils.unix_time(award.date)
|
||||
})
|
||||
json['places'][i + 1]['solves'] = sorted(json['places'][i + 1]['solves'], key=lambda k: k['time'])
|
||||
|
||||
return jsonify(json)
|
||||
return render_template(
|
||||
'scoreboard.html',
|
||||
standings=standings,
|
||||
score_frozen=config.is_scoreboard_frozen()
|
||||
)
|
||||
|
|
|
@ -0,0 +1,149 @@
|
|||
from flask import current_app as app, render_template, request, redirect, abort, jsonify, url_for, session, Blueprint, \
|
||||
Response, send_file
|
||||
from CTFd.models import db, Users, Teams, Solves, Awards, Files, Pages, Tracking
|
||||
from CTFd.utils.decorators import authed_only
|
||||
from CTFd.utils.decorators.modes import require_team_mode
|
||||
from CTFd.utils.modes import USERS_MODE
|
||||
from CTFd.utils import config, get_config, set_config
|
||||
from CTFd.utils.user import get_current_user, authed, get_ip
|
||||
from CTFd.utils.dates import unix_time_to_utc
|
||||
from CTFd.utils.crypto import verify_password
|
||||
from CTFd.utils.decorators.visibility import check_account_visibility, check_score_visibility
|
||||
|
||||
teams = Blueprint('teams', __name__)
|
||||
|
||||
|
||||
@teams.route('/teams')
|
||||
@check_account_visibility
|
||||
@require_team_mode
|
||||
def listing():
|
||||
page = request.args.get('page', 1)
|
||||
page = abs(int(page))
|
||||
results_per_page = 50
|
||||
page_start = results_per_page * (page - 1)
|
||||
page_end = results_per_page * (page - 1) + results_per_page
|
||||
|
||||
# TODO: Should teams confirm emails?
|
||||
# if get_config('verify_emails'):
|
||||
# count = Teams.query.filter_by(verified=True, banned=False).count()
|
||||
# teams = Teams.query.filter_by(verified=True, banned=False).slice(page_start, page_end).all()
|
||||
# else:
|
||||
count = Teams.query.filter_by(banned=False).count()
|
||||
teams = Teams.query.filter_by(banned=False).slice(page_start, page_end).all()
|
||||
|
||||
pages = int(count / results_per_page) + (count % results_per_page > 0)
|
||||
return render_template('teams/teams.html', teams=teams, pages=pages, curr_page=page)
|
||||
|
||||
|
||||
@teams.route('/teams/join', methods=['GET', 'POST'])
|
||||
@authed_only
|
||||
@require_team_mode
|
||||
def join():
|
||||
if request.method == 'GET':
|
||||
return render_template('teams/join_team.html')
|
||||
if request.method == 'POST':
|
||||
teamname = request.form.get('name')
|
||||
passphrase = request.form.get('password', '').strip()
|
||||
|
||||
team = Teams.query.filter_by(name=teamname).first()
|
||||
user = get_current_user()
|
||||
if team and verify_password(passphrase, team.password):
|
||||
user.team_id = team.id
|
||||
db.session.commit()
|
||||
return redirect(url_for('challenges.listing'))
|
||||
else:
|
||||
errors = ['That information is incorrect']
|
||||
return render_template('teams/join_team.html', errors=errors)
|
||||
|
||||
|
||||
@teams.route('/teams/new', methods=['GET', 'POST'])
|
||||
@authed_only
|
||||
@require_team_mode
|
||||
def new():
|
||||
if request.method == 'GET':
|
||||
return render_template("teams/new_team.html")
|
||||
elif request.method == 'POST':
|
||||
teamname = request.form.get('name')
|
||||
passphrase = request.form.get('password', '').strip()
|
||||
errors = []
|
||||
|
||||
user = get_current_user()
|
||||
|
||||
existing_team = Teams.query.filter_by(name=teamname).first()
|
||||
if existing_team:
|
||||
errors.append('That team name is already taken')
|
||||
if not teamname:
|
||||
errors.append('That team name is invalid')
|
||||
|
||||
if errors:
|
||||
return render_template("teams/new_team.html", errors=errors)
|
||||
|
||||
team = Teams(
|
||||
name=teamname,
|
||||
password=passphrase
|
||||
)
|
||||
|
||||
db.session.add(team)
|
||||
db.session.commit()
|
||||
|
||||
user.team_id = team.id
|
||||
db.session.commit()
|
||||
return redirect(url_for('challenges.listing'))
|
||||
|
||||
|
||||
@teams.route('/team')
|
||||
@authed_only
|
||||
@require_team_mode
|
||||
def private():
|
||||
user = get_current_user()
|
||||
if not user.team_id:
|
||||
return render_template(
|
||||
'teams/team_enrollment.html',
|
||||
)
|
||||
|
||||
team_id = user.team_id
|
||||
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
solves = team.get_solves()
|
||||
awards = team.get_awards()
|
||||
|
||||
place = team.place
|
||||
score = team.score
|
||||
|
||||
return render_template(
|
||||
'teams/team.html',
|
||||
solves=solves,
|
||||
awards=awards,
|
||||
user=user,
|
||||
team=team,
|
||||
score=score,
|
||||
place=place,
|
||||
score_frozen=config.is_scoreboard_frozen()
|
||||
)
|
||||
|
||||
|
||||
@teams.route('/teams/<int:team_id>')
|
||||
@check_account_visibility
|
||||
@check_score_visibility
|
||||
@require_team_mode
|
||||
def public(team_id):
|
||||
errors = []
|
||||
team = Teams.query.filter_by(id=team_id).first_or_404()
|
||||
solves = team.get_solves()
|
||||
awards = team.get_awards()
|
||||
|
||||
place = team.place
|
||||
score = team.score
|
||||
|
||||
if errors:
|
||||
return render_template('teams/team.html', team=team, errors=errors)
|
||||
|
||||
return render_template(
|
||||
'teams/team.html',
|
||||
solves=solves,
|
||||
awards=awards,
|
||||
team=team,
|
||||
score=score,
|
||||
place=place,
|
||||
score_frozen=config.is_scoreboard_frozen()
|
||||
)
|
|
@ -74,6 +74,13 @@ pre {
|
|||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.input-filled-invalid {
|
||||
background-color: transparent;
|
||||
border-color: #d46767;
|
||||
box-shadow: 0 0 0 0.2rem #d46767;
|
||||
transition: background-color 0.3s, border-color 0.3s;
|
||||
}
|
||||
|
||||
.btn-outlined.btn-theme {
|
||||
background: none;
|
||||
color: #545454;
|
||||
|
@ -168,4 +175,10 @@ pre {
|
|||
|
||||
.cursor-help {
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
-webkit-border-radius: 0 !important;
|
||||
-moz-border-radius: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
pre {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
.chal-desc {
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.chal-desc {
|
||||
font-size: 14px;
|
||||
.chal-desc img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
|
@ -22,8 +24,35 @@ pre {
|
|||
background-color: #5B7290 !important;
|
||||
}
|
||||
|
||||
.challenge-button {
|
||||
box-shadow: 3px 3px 3px grey;
|
||||
}
|
||||
|
||||
.solved-challenge {
|
||||
background-color: #37d63e !important;
|
||||
opacity: 0.4;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
|
||||
.corner-button-check {
|
||||
margin-top: -10px;
|
||||
margin-right: 25px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.key-submit .btn {
|
||||
height: 51px;
|
||||
}
|
||||
|
||||
#challenge-window .form-control {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0.8em;
|
||||
border-radius: 0;
|
||||
background: #f0f0f0;
|
||||
color: #aaa;
|
||||
font-weight: 400;
|
||||
font-family: "Avenir Next", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
|
|
@ -14,6 +14,6 @@ body {
|
|||
bottom: 1px; /* prevent scrollbars from showing on pages that don't use the full page height */
|
||||
width: 100%;
|
||||
height: 60px; /* Set the fixed height of the footer here */
|
||||
line-height: 60px; /* Vertically center the text there */
|
||||
/*line-height: 60px; !* Vertically center the text there *!*/
|
||||
/*background-color: #f5f5f5;*/
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,4 @@
|
|||
@import url('https://use.fontawesome.com/releases/v5.0.9/css/all.css');
|
||||
@import url('https://use.fontawesome.com/releases/v5.4.1/css/all.css');
|
||||
|
||||
@font-face {
|
||||
font-family: 'Font Awesome 5 Brands Offline';
|
||||
|
|
Binary file not shown.
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 498 KiB After Width: | Height: | Size: 673 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue