From 935027c55dcfbc3e013d11f9502ccc29e20924ca Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 24 Jan 2017 23:06:16 -0500 Subject: [PATCH] Marking 1.0.0 (#196) * Use in routes to prevent some errors 500 (#192) * Use first_or_404() to prevent some errors 500 (#193) * Add a populating script for awards. (#191) * Creating upload_file util * Marking 1.0.0 in __init__ and starting database migrations * Upgrading some more HTML * Adding CHANGELOG.md --- CHANGELOG.md | 19 ++++ CTFd/__init__.py | 17 ++- CTFd/admin.py | 138 ++++++++++--------------- CTFd/auth.py | 6 +- CTFd/challenges.py | 10 +- CTFd/scoreboard.py | 2 +- CTFd/templates/admin/chals.html | 4 +- CTFd/templates/admin/config.html | 12 +++ CTFd/templates/admin/containers.html | 8 +- CTFd/templates/admin/editor.html | 2 + CTFd/utils.py | 23 ++++- CTFd/views.py | 11 +- manage.py | 13 +++ migrations/README | 1 + migrations/alembic.ini | 45 ++++++++ migrations/env.py | 87 ++++++++++++++++ migrations/script.py.mako | 24 +++++ migrations/versions/cb3cfcc47e2f_.py | 148 +++++++++++++++++++++++++++ populate.py | 17 ++- requirements.txt | 1 + tests/test_user_facing.py | 4 +- 21 files changed, 482 insertions(+), 110 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 manage.py create mode 100755 migrations/README create mode 100644 migrations/alembic.ini create mode 100755 migrations/env.py create mode 100755 migrations/script.py.mako create mode 100644 migrations/versions/cb3cfcc47e2f_.py diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c90c7e7 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,19 @@ +1.0.0 / 2017-01-24 +================== + +**Implemented enhancements:** + +- 1.0.0 release! Things work! +- Manage everything from a browser +- Run Containers +- Themes +- Plugins +- Database migrations + +**Closed issues:** + +- Closed out 94 issues before tagging 1.0.0 + +**Merged pull requests:** + +- Merged 42 pull requests before tagging 1.0.0 \ No newline at end of file diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 2353c55..01ab9e9 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -1,13 +1,15 @@ import os +from distutils.version import StrictVersion from flask import Flask from jinja2 import FileSystemLoader from sqlalchemy.engine.url import make_url from sqlalchemy.exc import OperationalError from sqlalchemy_utils import database_exists, create_database -from utils import get_config, set_config, cache +from utils import get_config, set_config, cache, migrate, migrate_upgrade +__version__ = '1.0.0' class ThemeLoader(FileSystemLoader): def get_source(self, environment, template): @@ -45,14 +47,23 @@ def create_app(config='CTFd.config.Config'): app.db = db + migrate.init_app(app, db) + cache.init_app(app) app.cache = cache + version = get_config('ctf_version') + + if not version: ## Upgrading from an unversioned CTFd + set_config('ctf_version', __version__) + + if version and (StrictVersion(version) < StrictVersion(__version__)): ## Upgrading from an older version of CTFd + migrate_upgrade() + set_config('ctf_version', __version__) + if not get_config('ctf_theme'): set_config('ctf_theme', 'original') - #Session(app) - from CTFd.views import views from CTFd.challenges import challenges from CTFd.scoreboard import scoreboard diff --git a/CTFd/admin.py b/CTFd/admin.py index bf86c96..801b198 100644 --- a/CTFd/admin.py +++ b/CTFd/admin.py @@ -5,11 +5,10 @@ import os from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from passlib.hash import bcrypt_sha256 from sqlalchemy.sql import not_ -from werkzeug.utils import secure_filename from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ set_config, sendmail, rmdir, create_image, delete_image, run_image, container_status, container_ports, \ - container_stop, container_start, get_themes, cache + container_stop, container_start, get_themes, cache, upload_file from CTFd.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError from CTFd.scoreboard import get_standings @@ -209,7 +208,7 @@ def admin_pages(route): @admin.route('/admin/page//delete', methods=['POST']) @admins_only def delete_page(pageroute): - page = Pages.query.filter_by(route=pageroute).first() + page = Pages.query.filter_by(route=pageroute).first_or_404() db.session.delete(page) db.session.commit() db.session.close() @@ -226,7 +225,7 @@ def list_container(): return render_template('admin/containers.html', containers=containers) -@admin.route('/admin/containers//stop', methods=['POST']) +@admin.route('/admin/containers//stop', methods=['POST']) @admins_only def stop_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() @@ -236,7 +235,7 @@ def stop_container(container_id): return '0' -@admin.route('/admin/containers//start', methods=['POST']) +@admin.route('/admin/containers//start', methods=['POST']) @admins_only def run_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() @@ -252,7 +251,7 @@ def run_container(container_id): return '0' -@admin.route('/admin/containers//delete', methods=['POST']) +@admin.route('/admin/containers//delete', methods=['POST']) @admins_only def delete_container(container_id): container = Containers.query.filter_by(id=container_id).first_or_404() @@ -310,19 +309,18 @@ def admin_chals(): return render_template('admin/chals.html') -@admin.route('/admin/keys/', methods=['POST', 'GET']) +@admin.route('/admin/keys/', methods=['POST', 'GET']) @admins_only def admin_keys(chalid): + chal = Challenges.query.filter_by(id=chalid).first_or_404() + if request.method == 'GET': - chal = Challenges.query.filter_by(id=chalid).first_or_404() json_data = {'keys': []} flags = json.loads(chal.flags) for i, x in enumerate(flags): json_data['keys'].append({'id': i, 'key': x['flag'], 'type': x['type']}) return jsonify(json_data) elif request.method == 'POST': - chal = Challenges.query.filter_by(id=chalid).first() - newkeys = request.form.getlist('keys[]') newvals = request.form.getlist('vals[]') flags = [] @@ -338,7 +336,7 @@ def admin_keys(chalid): return '1' -@admin.route('/admin/tags/', methods=['GET', 'POST']) +@admin.route('/admin/tags/', methods=['GET', 'POST']) @admins_only def admin_tags(chalid): if request.method == 'GET': @@ -358,7 +356,7 @@ def admin_tags(chalid): return '1' -@admin.route('/admin/tags//delete', methods=['POST']) +@admin.route('/admin/tags//delete', methods=['POST']) @admins_only def admin_delete_tags(tagid): if request.method == 'POST': @@ -369,7 +367,7 @@ def admin_delete_tags(tagid): return '1' -@admin.route('/admin/files/', methods=['GET', 'POST']) +@admin.route('/admin/files/', methods=['GET', 'POST']) @admins_only def admin_files(chalid): if request.method == 'GET': @@ -391,19 +389,7 @@ def admin_files(chalid): files = request.files.getlist('files[]') for f in files: - filename = secure_filename(f.filename) - - if len(filename) <= 0: - continue - - md5hash = hashlib.md5(os.urandom(64)).hexdigest() - - if not os.path.exists(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)): - os.makedirs(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)) - - f.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename)) - db_f = Files(chalid, (md5hash + '/' + filename)) - db.session.add(db_f) + upload_file(file=f, chalid=chalid) db.session.commit() db.session.close() @@ -411,7 +397,7 @@ def admin_files(chalid): @admin.route('/admin/teams', defaults={'page': '1'}) -@admin.route('/admin/teams/') +@admin.route('/admin/teams/') @admins_only def admin_teams(page): page = abs(int(page)) @@ -425,10 +411,10 @@ def admin_teams(page): return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page) -@admin.route('/admin/team/', methods=['GET', 'POST']) +@admin.route('/admin/team/', methods=['GET', 'POST']) @admins_only def admin_team(teamid): - user = Teams.query.filter_by(id=teamid).first() + user = Teams.query.filter_by(id=teamid).first_or_404() if request.method == 'GET': solves = Solves.query.filter_by(teamid=teamid).all() @@ -497,7 +483,7 @@ def admin_team(teamid): return jsonify({'data': ['success']}) -@admin.route('/admin/team//mail', methods=['POST']) +@admin.route('/admin/team//mail', methods=['POST']) @admins_only def email_user(teamid): message = request.form.get('msg', None) @@ -508,27 +494,27 @@ def email_user(teamid): return '0' -@admin.route('/admin/team//ban', methods=['POST']) +@admin.route('/admin/team//ban', methods=['POST']) @admins_only def ban(teamid): - user = Teams.query.filter_by(id=teamid).first() + user = Teams.query.filter_by(id=teamid).first_or_404() user.banned = True db.session.commit() db.session.close() return redirect(url_for('admin.admin_scoreboard')) -@admin.route('/admin/team//unban', methods=['POST']) +@admin.route('/admin/team//unban', methods=['POST']) @admins_only def unban(teamid): - user = Teams.query.filter_by(id=teamid).first() + user = Teams.query.filter_by(id=teamid).first_or_404() user.banned = False db.session.commit() db.session.close() return redirect(url_for('admin.admin_scoreboard')) -@admin.route('/admin/team//delete', methods=['POST']) +@admin.route('/admin/team//delete', methods=['POST']) @admins_only def delete_team(teamid): try: @@ -572,7 +558,7 @@ def admin_scoreboard(): return render_template('admin/scoreboard.html', teams=standings) -@admin.route('/admin/teams//awards', methods=['GET']) +@admin.route('/admin/teams//awards', methods=['GET']) @admins_only def admin_awards(teamid): awards = Awards.query.filter_by(teamid=teamid).all() @@ -611,18 +597,14 @@ def create_award(): return '0' -@admin.route('/admin/awards//delete', methods=['POST']) +@admin.route('/admin/awards//delete', methods=['POST']) @admins_only def delete_award(award_id): - try: - award = Awards.query.filter_by(id=award_id).first() - db.session.delete(award) - db.session.commit() - db.session.close() - return '1' - except Exception as e: - print(e) - return '0' + award = Awards.query.filter_by(id=award_id).first_or_404() + db.session.delete(award) + db.session.commit() + db.session.close() + return '1' @admin.route('/admin/scores') @@ -671,7 +653,7 @@ def admin_solves(teamid="all"): return jsonify(json_data) -@admin.route('/admin/solves///solve', methods=['POST']) +@admin.route('/admin/solves///solve', methods=['POST']) @admins_only def create_solve(teamid, chalid): solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN') @@ -681,7 +663,7 @@ def create_solve(teamid, chalid): return '1' -@admin.route('/admin/solves//delete', methods=['POST']) +@admin.route('/admin/solves//delete', methods=['POST']) @admins_only def delete_solve(keyid): solve = Solves.query.filter_by(id=keyid).first_or_404() @@ -691,7 +673,7 @@ def delete_solve(keyid): return '1' -@admin.route('/admin/wrong_keys//delete', methods=['POST']) +@admin.route('/admin/wrong_keys//delete', methods=['POST']) @admins_only def delete_wrong_key(keyid): wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404() @@ -737,9 +719,10 @@ def admin_stats(): least_solved=least_solved) -@admin.route('/admin/wrong_keys/', methods=['GET']) +@admin.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET']) +@admin.route('/admin/wrong_keys/', methods=['GET']) @admins_only -def admin_wrong_key(page='1'): +def admin_wrong_key(page): page = abs(int(page)) results_per_page = 50 page_start = results_per_page * (page - 1) @@ -759,9 +742,10 @@ def admin_wrong_key(page='1'): return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page) -@admin.route('/admin/correct_keys/', methods=['GET']) +@admin.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET']) +@admin.route('/admin/correct_keys/', methods=['GET']) @admins_only -def admin_correct_key(page='1'): +def admin_correct_key(page): page = abs(int(page)) results_per_page = 50 page_start = results_per_page * (page - 1) @@ -781,9 +765,10 @@ def admin_correct_key(page='1'): return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page) -@admin.route('/admin/fails/', methods=['GET']) +@admin.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET']) +@admin.route('/admin/fails/', methods=['GET']) @admins_only -def admin_fails(teamid='all'): +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() @@ -816,19 +801,7 @@ def admin_create_chal(): db.session.commit() for f in files: - filename = secure_filename(f.filename) - - if len(filename) <= 0: - continue - - md5hash = hashlib.md5(os.urandom(64)).hexdigest() - - if not os.path.exists(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)): - os.makedirs(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash)) - - f.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename)) - db_f = Files(chal.id, (md5hash + '/' + filename)) - db.session.add(db_f) + upload_file(file=f, chalid=chal.id) db.session.commit() db.session.close() @@ -838,27 +811,26 @@ def admin_create_chal(): @admin.route('/admin/chal/delete', methods=['POST']) @admins_only def admin_delete_chal(): - challenge = Challenges.query.filter_by(id=request.form['id']).first() - if challenge: - 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() - Files.query.filter_by(chal=challenge.id).delete() - for file in files: - folder = os.path.dirname(os.path.join(os.path.normpath(app.root_path), 'uploads', file.location)) - rmdir(folder) - Tags.query.filter_by(chal=challenge.id).delete() - Challenges.query.filter_by(id=challenge.id).delete() - db.session.commit() - db.session.close() + challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404() + 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() + Files.query.filter_by(chal=challenge.id).delete() + for file in files: + folder = os.path.dirname(os.path.join(os.path.normpath(app.root_path), 'uploads', file.location)) + rmdir(folder) + Tags.query.filter_by(chal=challenge.id).delete() + Challenges.query.filter_by(id=challenge.id).delete() + db.session.commit() + db.session.close() return '1' @admin.route('/admin/chal/update', methods=['POST']) @admins_only def admin_update_chal(): - challenge = Challenges.query.filter_by(id=request.form['id']).first() + challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404() challenge.name = request.form['name'] challenge.description = request.form['desc'] challenge.value = request.form['value'] diff --git a/CTFd/auth.py b/CTFd/auth.py index 68ae752..5c9a871 100644 --- a/CTFd/auth.py +++ b/CTFd/auth.py @@ -27,7 +27,7 @@ def confirm_user(data=None): return render_template('confirm.html', errors=['Your confirmation link seems wrong']) except: return render_template('confirm.html', errors=['Your link appears broken, please try again.']) - team = Teams.query.filter_by(email=email).first() + team = Teams.query.filter_by(email=email).first_or_404() team.verified = True db.session.commit() db.session.close() @@ -39,7 +39,7 @@ def confirm_user(data=None): if not data and request.method == "GET": # User has been directed to the confirm page because his account is not verified if not authed(): return redirect(url_for('auth.login')) - team = Teams.query.filter_by(id=session['id']).first() + team = Teams.query.filter_by(id=session['id']).first_or_404() if team.verified: return redirect(url_for('views.profile')) else: @@ -60,7 +60,7 @@ def reset_password(data=None): return render_template('reset_password.html', errors=['Your link has expired']) except: return render_template('reset_password.html', errors=['Your link appears broken, please try again.']) - team = Teams.query.filter_by(name=name).first() + team = Teams.query.filter_by(name=name).first_or_404() team.password = bcrypt_sha256.encrypt(request.form['password'].strip()) db.session.commit() db.session.close() diff --git a/CTFd/challenges.py b/CTFd/challenges.py index 454f1b3..cb68ce4 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -79,7 +79,7 @@ def solves_per_chal(): @challenges.route('/solves') -@challenges.route('/solves/') +@challenges.route('/solves/') def solves(teamid=None): solves = None awards = None @@ -131,7 +131,7 @@ def attempts(): return jsonify(json) -@challenges.route('/fails/', methods=['GET']) +@challenges.route('/fails/', methods=['GET']) def fails(teamid): fails = WrongKeys.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count() @@ -140,7 +140,7 @@ def fails(teamid): return jsonify(json) -@challenges.route('/chal//solves', methods=['GET']) +@challenges.route('/chal//solves', methods=['GET']) def who_solved(chalid): if not user_can_view_challenges(): return redirect(url_for('auth.login', next=request.path)) @@ -151,7 +151,7 @@ def who_solved(chalid): return jsonify(json) -@challenges.route('/chal/', methods=['POST']) +@challenges.route('/chal/', methods=['POST']) def chal(chalid): if ctf_ended() and not view_after_ctf(): return redirect(url_for('challenges.challenges_view')) @@ -178,7 +178,7 @@ def chal(chalid): # Challange not solved yet if not solves: - chal = Challenges.query.filter_by(id=chalid).first() + chal = Challenges.query.filter_by(id=chalid).first_or_404() key = unicode(request.form['key'].strip().lower()) keys = json.loads(chal.flags) diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index 0735569..494f6bf 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -52,7 +52,7 @@ def scores(): return jsonify(json) -@scoreboard.route('/top/') +@scoreboard.route('/top/') def topteams(count): if get_config('view_scoreboard_if_authed') and not authed(): return redirect(url_for('auth.login', next=request.path)) diff --git a/CTFd/templates/admin/chals.html b/CTFd/templates/admin/chals.html index d6f12db..d686308 100644 --- a/CTFd/templates/admin/chals.html +++ b/CTFd/templates/admin/chals.html @@ -85,7 +85,8 @@
- + + Attach multiple files using Control+Click or Cmd+Click.
@@ -220,6 +221,7 @@
+ Attach multiple files using Control+Click or Cmd+Click.
diff --git a/CTFd/templates/admin/config.html b/CTFd/templates/admin/config.html index 1ad4630..fbd04e9 100644 --- a/CTFd/templates/admin/config.html +++ b/CTFd/templates/admin/config.html @@ -429,6 +429,18 @@ }); $(function () { + + var hash = window.location.hash; + if (hash) { + hash = hash.replace("<>[]'\"", ""); + $('ul.nav a[href="' + hash + '"]').tab('show'); + } + + $('.nav-pills a').click(function (e) { + $(this).tab('show'); + window.location.hash = this.hash; + }); + var start = $('#start').val(); var end = $('#end').val(); console.log(start); diff --git a/CTFd/templates/admin/containers.html b/CTFd/templates/admin/containers.html index 92820ec..604d0a4 100644 --- a/CTFd/templates/admin/containers.html +++ b/CTFd/templates/admin/containers.html @@ -17,12 +17,14 @@
- +
- + -

These files are uploaded alongside your buildfile

+ Attach multiple files using Control+Click or Cmd+Click.
diff --git a/CTFd/templates/admin/editor.html b/CTFd/templates/admin/editor.html index d9663f0..7a75a7f 100644 --- a/CTFd/templates/admin/editor.html +++ b/CTFd/templates/admin/editor.html @@ -24,6 +24,7 @@

Route:

+

This is the URL route that your page will be at (e.g. /page)

@@ -32,6 +33,7 @@

Content:

+

This is the HTML content of your page