Marking 1.0.0 (#196)

* Use <int:xxx> 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
selenium-screenshot-testing 1.0.0
Kevin Chung 2017-01-24 23:06:16 -05:00 committed by GitHub
parent 01cb189b22
commit 935027c55d
21 changed files with 482 additions and 110 deletions

19
CHANGELOG.md Normal file
View File

@ -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

View File

@ -1,13 +1,15 @@
import os import os
from distutils.version import StrictVersion
from flask import Flask from flask import Flask
from jinja2 import FileSystemLoader from jinja2 import FileSystemLoader
from sqlalchemy.engine.url import make_url from sqlalchemy.engine.url import make_url
from sqlalchemy.exc import OperationalError from sqlalchemy.exc import OperationalError
from sqlalchemy_utils import database_exists, create_database 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): class ThemeLoader(FileSystemLoader):
def get_source(self, environment, template): def get_source(self, environment, template):
@ -45,14 +47,23 @@ def create_app(config='CTFd.config.Config'):
app.db = db app.db = db
migrate.init_app(app, db)
cache.init_app(app) cache.init_app(app)
app.cache = cache 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'): if not get_config('ctf_theme'):
set_config('ctf_theme', 'original') set_config('ctf_theme', 'original')
#Session(app)
from CTFd.views import views from CTFd.views import views
from CTFd.challenges import challenges from CTFd.challenges import challenges
from CTFd.scoreboard import scoreboard from CTFd.scoreboard import scoreboard

View File

@ -5,11 +5,10 @@ import os
from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint from flask import current_app as app, render_template, request, redirect, jsonify, url_for, Blueprint
from passlib.hash import bcrypt_sha256 from passlib.hash import bcrypt_sha256
from sqlalchemy.sql import not_ from sqlalchemy.sql import not_
from werkzeug.utils import secure_filename
from CTFd.utils import admins_only, is_admin, unix_time, get_config, \ 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, \ 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.models import db, Teams, Solves, Awards, Containers, Challenges, WrongKeys, Keys, Tags, Files, Tracking, Pages, Config, DatabaseError
from CTFd.scoreboard import get_standings from CTFd.scoreboard import get_standings
@ -209,7 +208,7 @@ def admin_pages(route):
@admin.route('/admin/page/<pageroute>/delete', methods=['POST']) @admin.route('/admin/page/<pageroute>/delete', methods=['POST'])
@admins_only @admins_only
def delete_page(pageroute): 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.delete(page)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@ -226,7 +225,7 @@ def list_container():
return render_template('admin/containers.html', containers=containers) return render_template('admin/containers.html', containers=containers)
@admin.route('/admin/containers/<container_id>/stop', methods=['POST']) @admin.route('/admin/containers/<int:container_id>/stop', methods=['POST'])
@admins_only @admins_only
def stop_container(container_id): def stop_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404() container = Containers.query.filter_by(id=container_id).first_or_404()
@ -236,7 +235,7 @@ def stop_container(container_id):
return '0' return '0'
@admin.route('/admin/containers/<container_id>/start', methods=['POST']) @admin.route('/admin/containers/<int:container_id>/start', methods=['POST'])
@admins_only @admins_only
def run_container(container_id): def run_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404() container = Containers.query.filter_by(id=container_id).first_or_404()
@ -252,7 +251,7 @@ def run_container(container_id):
return '0' return '0'
@admin.route('/admin/containers/<container_id>/delete', methods=['POST']) @admin.route('/admin/containers/<int:container_id>/delete', methods=['POST'])
@admins_only @admins_only
def delete_container(container_id): def delete_container(container_id):
container = Containers.query.filter_by(id=container_id).first_or_404() container = Containers.query.filter_by(id=container_id).first_or_404()
@ -310,19 +309,18 @@ def admin_chals():
return render_template('admin/chals.html') return render_template('admin/chals.html')
@admin.route('/admin/keys/<chalid>', methods=['POST', 'GET']) @admin.route('/admin/keys/<int:chalid>', methods=['POST', 'GET'])
@admins_only @admins_only
def admin_keys(chalid): def admin_keys(chalid):
if request.method == 'GET':
chal = Challenges.query.filter_by(id=chalid).first_or_404() chal = Challenges.query.filter_by(id=chalid).first_or_404()
if request.method == 'GET':
json_data = {'keys': []} json_data = {'keys': []}
flags = json.loads(chal.flags) flags = json.loads(chal.flags)
for i, x in enumerate(flags): for i, x in enumerate(flags):
json_data['keys'].append({'id': i, 'key': x['flag'], 'type': x['type']}) json_data['keys'].append({'id': i, 'key': x['flag'], 'type': x['type']})
return jsonify(json_data) return jsonify(json_data)
elif request.method == 'POST': elif request.method == 'POST':
chal = Challenges.query.filter_by(id=chalid).first()
newkeys = request.form.getlist('keys[]') newkeys = request.form.getlist('keys[]')
newvals = request.form.getlist('vals[]') newvals = request.form.getlist('vals[]')
flags = [] flags = []
@ -338,7 +336,7 @@ def admin_keys(chalid):
return '1' return '1'
@admin.route('/admin/tags/<chalid>', methods=['GET', 'POST']) @admin.route('/admin/tags/<int:chalid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_tags(chalid): def admin_tags(chalid):
if request.method == 'GET': if request.method == 'GET':
@ -358,7 +356,7 @@ def admin_tags(chalid):
return '1' return '1'
@admin.route('/admin/tags/<tagid>/delete', methods=['POST']) @admin.route('/admin/tags/<int:tagid>/delete', methods=['POST'])
@admins_only @admins_only
def admin_delete_tags(tagid): def admin_delete_tags(tagid):
if request.method == 'POST': if request.method == 'POST':
@ -369,7 +367,7 @@ def admin_delete_tags(tagid):
return '1' return '1'
@admin.route('/admin/files/<chalid>', methods=['GET', 'POST']) @admin.route('/admin/files/<int:chalid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_files(chalid): def admin_files(chalid):
if request.method == 'GET': if request.method == 'GET':
@ -391,19 +389,7 @@ def admin_files(chalid):
files = request.files.getlist('files[]') files = request.files.getlist('files[]')
for f in files: for f in files:
filename = secure_filename(f.filename) upload_file(file=f, chalid=chalid)
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)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@ -411,7 +397,7 @@ def admin_files(chalid):
@admin.route('/admin/teams', defaults={'page': '1'}) @admin.route('/admin/teams', defaults={'page': '1'})
@admin.route('/admin/teams/<page>') @admin.route('/admin/teams/<int:page>')
@admins_only @admins_only
def admin_teams(page): def admin_teams(page):
page = abs(int(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) return render_template('admin/teams.html', teams=teams, pages=pages, curr_page=page)
@admin.route('/admin/team/<teamid>', methods=['GET', 'POST']) @admin.route('/admin/team/<int:teamid>', methods=['GET', 'POST'])
@admins_only @admins_only
def admin_team(teamid): 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': if request.method == 'GET':
solves = Solves.query.filter_by(teamid=teamid).all() solves = Solves.query.filter_by(teamid=teamid).all()
@ -497,7 +483,7 @@ def admin_team(teamid):
return jsonify({'data': ['success']}) return jsonify({'data': ['success']})
@admin.route('/admin/team/<teamid>/mail', methods=['POST']) @admin.route('/admin/team/<int:teamid>/mail', methods=['POST'])
@admins_only @admins_only
def email_user(teamid): def email_user(teamid):
message = request.form.get('msg', None) message = request.form.get('msg', None)
@ -508,27 +494,27 @@ def email_user(teamid):
return '0' return '0'
@admin.route('/admin/team/<teamid>/ban', methods=['POST']) @admin.route('/admin/team/<int:teamid>/ban', methods=['POST'])
@admins_only @admins_only
def ban(teamid): def ban(teamid):
user = Teams.query.filter_by(id=teamid).first() user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = True user.banned = True
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return redirect(url_for('admin.admin_scoreboard')) return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<teamid>/unban', methods=['POST']) @admin.route('/admin/team/<int:teamid>/unban', methods=['POST'])
@admins_only @admins_only
def unban(teamid): def unban(teamid):
user = Teams.query.filter_by(id=teamid).first() user = Teams.query.filter_by(id=teamid).first_or_404()
user.banned = False user.banned = False
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return redirect(url_for('admin.admin_scoreboard')) return redirect(url_for('admin.admin_scoreboard'))
@admin.route('/admin/team/<teamid>/delete', methods=['POST']) @admin.route('/admin/team/<int:teamid>/delete', methods=['POST'])
@admins_only @admins_only
def delete_team(teamid): def delete_team(teamid):
try: try:
@ -572,7 +558,7 @@ def admin_scoreboard():
return render_template('admin/scoreboard.html', teams=standings) return render_template('admin/scoreboard.html', teams=standings)
@admin.route('/admin/teams/<teamid>/awards', methods=['GET']) @admin.route('/admin/teams/<int:teamid>/awards', methods=['GET'])
@admins_only @admins_only
def admin_awards(teamid): def admin_awards(teamid):
awards = Awards.query.filter_by(teamid=teamid).all() awards = Awards.query.filter_by(teamid=teamid).all()
@ -611,18 +597,14 @@ def create_award():
return '0' return '0'
@admin.route('/admin/awards/<award_id>/delete', methods=['POST']) @admin.route('/admin/awards/<int:award_id>/delete', methods=['POST'])
@admins_only @admins_only
def delete_award(award_id): def delete_award(award_id):
try: award = Awards.query.filter_by(id=award_id).first_or_404()
award = Awards.query.filter_by(id=award_id).first()
db.session.delete(award) db.session.delete(award)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
return '1' return '1'
except Exception as e:
print(e)
return '0'
@admin.route('/admin/scores') @admin.route('/admin/scores')
@ -671,7 +653,7 @@ def admin_solves(teamid="all"):
return jsonify(json_data) return jsonify(json_data)
@admin.route('/admin/solves/<teamid>/<chalid>/solve', methods=['POST']) @admin.route('/admin/solves/<int:teamid>/<int:chalid>/solve', methods=['POST'])
@admins_only @admins_only
def create_solve(teamid, chalid): def create_solve(teamid, chalid):
solve = Solves(chalid=chalid, teamid=teamid, ip='127.0.0.1', flag='MARKED_AS_SOLVED_BY_ADMIN') 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' return '1'
@admin.route('/admin/solves/<keyid>/delete', methods=['POST']) @admin.route('/admin/solves/<int:keyid>/delete', methods=['POST'])
@admins_only @admins_only
def delete_solve(keyid): def delete_solve(keyid):
solve = Solves.query.filter_by(id=keyid).first_or_404() solve = Solves.query.filter_by(id=keyid).first_or_404()
@ -691,7 +673,7 @@ def delete_solve(keyid):
return '1' return '1'
@admin.route('/admin/wrong_keys/<keyid>/delete', methods=['POST']) @admin.route('/admin/wrong_keys/<int:keyid>/delete', methods=['POST'])
@admins_only @admins_only
def delete_wrong_key(keyid): def delete_wrong_key(keyid):
wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404() wrong_key = WrongKeys.query.filter_by(id=keyid).first_or_404()
@ -737,9 +719,10 @@ def admin_stats():
least_solved=least_solved) least_solved=least_solved)
@admin.route('/admin/wrong_keys/<page>', methods=['GET']) @admin.route('/admin/wrong_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/wrong_keys/<int:page>', methods=['GET'])
@admins_only @admins_only
def admin_wrong_key(page='1'): def admin_wrong_key(page):
page = abs(int(page)) page = abs(int(page))
results_per_page = 50 results_per_page = 50
page_start = results_per_page * (page - 1) 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) return render_template('admin/wrong_keys.html', wrong_keys=wrong_keys, pages=pages, curr_page=page)
@admin.route('/admin/correct_keys/<page>', methods=['GET']) @admin.route('/admin/correct_keys', defaults={'page': '1'}, methods=['GET'])
@admin.route('/admin/correct_keys/<int:page>', methods=['GET'])
@admins_only @admins_only
def admin_correct_key(page='1'): def admin_correct_key(page):
page = abs(int(page)) page = abs(int(page))
results_per_page = 50 results_per_page = 50
page_start = results_per_page * (page - 1) 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) return render_template('admin/correct_keys.html', solves=solves, pages=pages, curr_page=page)
@admin.route('/admin/fails/<teamid>', methods=['GET']) @admin.route('/admin/fails/all', defaults={'teamid': 'all'}, methods=['GET'])
@admin.route('/admin/fails/<int:teamid>', methods=['GET'])
@admins_only @admins_only
def admin_fails(teamid='all'): def admin_fails(teamid):
if teamid == "all": if teamid == "all":
fails = WrongKeys.query.join(Teams, WrongKeys.teamid == Teams.id).filter(Teams.banned == False).count() 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() 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() db.session.commit()
for f in files: for f in files:
filename = secure_filename(f.filename) upload_file(file=f, chalid=chal.id)
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)
db.session.commit() db.session.commit()
db.session.close() db.session.close()
@ -838,8 +811,7 @@ def admin_create_chal():
@admin.route('/admin/chal/delete', methods=['POST']) @admin.route('/admin/chal/delete', methods=['POST'])
@admins_only @admins_only
def admin_delete_chal(): def admin_delete_chal():
challenge = Challenges.query.filter_by(id=request.form['id']).first() challenge = Challenges.query.filter_by(id=request.form['id']).first_or_404()
if challenge:
WrongKeys.query.filter_by(chalid=challenge.id).delete() WrongKeys.query.filter_by(chalid=challenge.id).delete()
Solves.query.filter_by(chalid=challenge.id).delete() Solves.query.filter_by(chalid=challenge.id).delete()
Keys.query.filter_by(chal=challenge.id).delete() Keys.query.filter_by(chal=challenge.id).delete()
@ -858,7 +830,7 @@ def admin_delete_chal():
@admin.route('/admin/chal/update', methods=['POST']) @admin.route('/admin/chal/update', methods=['POST'])
@admins_only @admins_only
def admin_update_chal(): 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.name = request.form['name']
challenge.description = request.form['desc'] challenge.description = request.form['desc']
challenge.value = request.form['value'] challenge.value = request.form['value']

View File

@ -27,7 +27,7 @@ def confirm_user(data=None):
return render_template('confirm.html', errors=['Your confirmation link seems wrong']) return render_template('confirm.html', errors=['Your confirmation link seems wrong'])
except: except:
return render_template('confirm.html', errors=['Your link appears broken, please try again.']) 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 team.verified = True
db.session.commit() db.session.commit()
db.session.close() 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 data and request.method == "GET": # User has been directed to the confirm page because his account is not verified
if not authed(): if not authed():
return redirect(url_for('auth.login')) 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: if team.verified:
return redirect(url_for('views.profile')) return redirect(url_for('views.profile'))
else: else:
@ -60,7 +60,7 @@ def reset_password(data=None):
return render_template('reset_password.html', errors=['Your link has expired']) return render_template('reset_password.html', errors=['Your link has expired'])
except: except:
return render_template('reset_password.html', errors=['Your link appears broken, please try again.']) 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()) team.password = bcrypt_sha256.encrypt(request.form['password'].strip())
db.session.commit() db.session.commit()
db.session.close() db.session.close()

View File

@ -79,7 +79,7 @@ def solves_per_chal():
@challenges.route('/solves') @challenges.route('/solves')
@challenges.route('/solves/<teamid>') @challenges.route('/solves/<int:teamid>')
def solves(teamid=None): def solves(teamid=None):
solves = None solves = None
awards = None awards = None
@ -131,7 +131,7 @@ def attempts():
return jsonify(json) return jsonify(json)
@challenges.route('/fails/<teamid>', methods=['GET']) @challenges.route('/fails/<int:teamid>', methods=['GET'])
def fails(teamid): def fails(teamid):
fails = WrongKeys.query.filter_by(teamid=teamid).count() fails = WrongKeys.query.filter_by(teamid=teamid).count()
solves = Solves.query.filter_by(teamid=teamid).count() solves = Solves.query.filter_by(teamid=teamid).count()
@ -140,7 +140,7 @@ def fails(teamid):
return jsonify(json) return jsonify(json)
@challenges.route('/chal/<chalid>/solves', methods=['GET']) @challenges.route('/chal/<int:chalid>/solves', methods=['GET'])
def who_solved(chalid): def who_solved(chalid):
if not user_can_view_challenges(): if not user_can_view_challenges():
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))
@ -151,7 +151,7 @@ def who_solved(chalid):
return jsonify(json) return jsonify(json)
@challenges.route('/chal/<chalid>', methods=['POST']) @challenges.route('/chal/<int:chalid>', methods=['POST'])
def chal(chalid): def chal(chalid):
if ctf_ended() and not view_after_ctf(): if ctf_ended() and not view_after_ctf():
return redirect(url_for('challenges.challenges_view')) return redirect(url_for('challenges.challenges_view'))
@ -178,7 +178,7 @@ def chal(chalid):
# Challange not solved yet # Challange not solved yet
if not solves: 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()) key = unicode(request.form['key'].strip().lower())
keys = json.loads(chal.flags) keys = json.loads(chal.flags)

View File

@ -52,7 +52,7 @@ def scores():
return jsonify(json) return jsonify(json)
@scoreboard.route('/top/<count>') @scoreboard.route('/top/<int:count>')
def topteams(count): def topteams(count):
if get_config('view_scoreboard_if_authed') and not authed(): if get_config('view_scoreboard_if_authed') and not authed():
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))

View File

@ -85,7 +85,8 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="exampleInputFile">Upload challenge files</label> <label>Upload challenge files</label>
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<input type="file" name="files[]" multiple="multiple"> <input type="file" name="files[]" multiple="multiple">
</div> </div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce"> <input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
@ -220,6 +221,7 @@
<div id="current-files"></div> <div id="current-files"></div>
<input type="hidden" name="method" value="upload"> <input type="hidden" name="method" value="upload">
<input type="file" name="files[]" multiple="multiple"> <input type="file" name="files[]" multiple="multiple">
<sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
<div class="row" style="text-align:center;margin-top:20px"> <div class="row" style="text-align:center;margin-top:20px">
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce"> <input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
<button class="btn btn-theme btn-outlined" type="submit">Update</button> <button class="btn btn-theme btn-outlined" type="submit">Update</button>

View File

@ -429,6 +429,18 @@
}); });
$(function () { $(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 start = $('#start').val();
var end = $('#end').val(); var end = $('#end').val();
console.log(start); console.log(start);

View File

@ -17,12 +17,14 @@
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="buildfile-editor" class="control-label">Build File</label> <label for="buildfile-editor" class="control-label">Build File</label>
<textarea id="buildfile-editor" class="form-control" name="buildfile" rows="10"></textarea> <textarea id="buildfile-editor" class="form-control" name="buildfile" rows="10" placeholder="Enter container build file"></textarea>
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="container-files">File input</label> <label for="container-files">Associated Files
<i class="fa fa-question-circle" title="These files are uploaded alongside your buildfile"></i>
</label>
<input type="file" name="files[]" id="container-files" multiple> <input type="file" name="files[]" id="container-files" multiple>
<p class="help-block">These files are uploaded alongside your buildfile</p> <sub class="help-block">Attach multiple files using Control+Click or Cmd+Click.</sub>
</div> </div>
<input type="hidden" value="{{ nonce }}" name="nonce" id="nonce"> <input type="hidden" value="{{ nonce }}" name="nonce" id="nonce">
</div> </div>

View File

@ -24,6 +24,7 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="col-md-12"> <div class="col-md-12">
<h3>Route: </h3> <h3>Route: </h3>
<p class="help-block">This is the URL route that your page will be at (e.g. /page)</p>
<input name='nonce' type='hidden' value="{{ nonce }}"> <input name='nonce' type='hidden' value="{{ nonce }}">
<input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route"> <input class="form-control radius" id="route" type="text" name="route" value="{% if page is defined %}{{ page.route }}{% endif %}" placeholder="Route">
</div> </div>
@ -32,6 +33,7 @@
<div class="row-fluid"> <div class="row-fluid">
<div class="col-md-12"> <div class="col-md-12">
<h3>Content: </h3> <h3>Content: </h3>
<p class="help-block">This is the HTML content of your page</p>
<textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br> <textarea id="admin-pages-editor" name="html">{% if page is defined %}{{ page.html }}{% endif %}</textarea><br>
<button class="btn btn-theme btn-outlined create-challenge pull-right"> <button class="btn btn-theme btn-outlined create-challenge pull-right">
{% if page is defined %} {% if page is defined %}

View File

@ -19,13 +19,16 @@ import urllib
from flask import current_app as app, request, redirect, url_for, session, render_template, abort from flask import current_app as app, request, redirect, url_for, session, render_template, abort
from flask_caching import Cache from flask_caching import Cache
from flask_migrate import Migrate, upgrade as migrate_upgrade
from itsdangerous import Signer from itsdangerous import Signer
import six import six
from six.moves.urllib.parse import urlparse, urljoin from six.moves.urllib.parse import urlparse, urljoin
from werkzeug.utils import secure_filename
from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Containers, ip2long, long2ip from CTFd.models import db, WrongKeys, Pages, Config, Tracking, Teams, Files, Containers, ip2long, long2ip
cache = Cache() cache = Cache()
migrate = Migrate()
def init_logs(app): def init_logs(app):
@ -297,6 +300,24 @@ def get_themes():
if os.path.isdir(os.path.join(dir, name)) and name != 'admin'] if os.path.isdir(os.path.join(dir, name)) and name != 'admin']
def upload_file(file, chalid):
filename = secure_filename(file.filename)
if len(filename) <= 0:
return False
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))
file.save(os.path.join(os.path.normpath(app.root_path), 'uploads', md5hash, filename))
db_f = Files(chalid, (md5hash + '/' + filename))
db.session.add(db_f)
db.session.commit()
return True
@cache.memoize() @cache.memoize()
def get_config(key): def get_config(key):
config = Config.query.filter_by(key=key).first() config = Config.query.filter_by(key=key).first()

View File

@ -105,15 +105,12 @@ def static_html(template):
try: try:
return render_template('%s.html' % template) return render_template('%s.html' % template)
except TemplateNotFound: except TemplateNotFound:
page = Pages.query.filter_by(route=template).first() page = Pages.query.filter_by(route=template).first_or_404()
if page:
return render_template('page.html', content=page.html) return render_template('page.html', content=page.html)
else:
abort(404)
@views.route('/teams', defaults={'page': '1'}) @views.route('/teams', defaults={'page': '1'})
@views.route('/teams/<page>') @views.route('/teams/<int:page>')
def teams(page): def teams(page):
page = abs(int(page)) page = abs(int(page))
results_per_page = 50 results_per_page = 50
@ -130,7 +127,7 @@ def teams(page):
return render_template('teams.html', teams=teams, team_pages=pages, curr_page=page) return render_template('teams.html', teams=teams, team_pages=pages, curr_page=page)
@views.route('/team/<teamid>', methods=['GET', 'POST']) @views.route('/team/<int:teamid>', methods=['GET', 'POST'])
def team(teamid): def team(teamid):
if get_config('view_scoreboard_if_authed') and not authed(): if get_config('view_scoreboard_if_authed') and not authed():
return redirect(url_for('auth.login', next=request.path)) return redirect(url_for('auth.login', next=request.path))

13
manage.py Normal file
View File

@ -0,0 +1,13 @@
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_script import Manager
from flask_migrate import Migrate, MigrateCommand
from CTFd import create_app
app = create_app()
manager = Manager(app)
manager.add_command('db', MigrateCommand)
if __name__ == '__main__':
manager.run()

1
migrations/README Executable file
View File

@ -0,0 +1 @@
Generic single-database configuration.

45
migrations/alembic.ini Normal file
View File

@ -0,0 +1,45 @@
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S

87
migrations/env.py Executable file
View File

@ -0,0 +1,87 @@
from __future__ import with_statement
from alembic import context
from sqlalchemy import engine_from_config, pool
from logging.config import fileConfig
import logging
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
from flask import current_app
config.set_main_option('sqlalchemy.url',
current_app.config.get('SQLALCHEMY_DATABASE_URI'))
target_metadata = current_app.extensions['migrate'].db.metadata
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(url=url)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.readthedocs.org/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
engine = engine_from_config(config.get_section(config.config_ini_section),
prefix='sqlalchemy.',
poolclass=pool.NullPool)
connection = engine.connect()
context.configure(connection=connection,
target_metadata=target_metadata,
process_revision_directives=process_revision_directives,
**current_app.extensions['migrate'].configure_args)
try:
with context.begin_transaction():
context.run_migrations()
finally:
connection.close()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()

24
migrations/script.py.mako Executable file
View File

@ -0,0 +1,24 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}

View File

@ -0,0 +1,148 @@
"""empty message
Revision ID: cb3cfcc47e2f
Revises:
Create Date: 2017-01-17 15:39:42.804290
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'cb3cfcc47e2f'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('challenges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('flags', sa.Text(), nullable=True),
sa.Column('hidden', sa.Boolean(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('config',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('key', sa.Text(), nullable=True),
sa.Column('value', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('containers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('buildfile', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_table('pages',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('route', sa.String(length=80), nullable=True),
sa.Column('html', sa.Text(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('route')
)
op.create_table('teams',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=True),
sa.Column('email', sa.String(length=128), nullable=True),
sa.Column('password', sa.String(length=128), nullable=True),
sa.Column('website', sa.String(length=128), nullable=True),
sa.Column('affiliation', sa.String(length=128), nullable=True),
sa.Column('country', sa.String(length=32), nullable=True),
sa.Column('bracket', sa.String(length=32), nullable=True),
sa.Column('banned', sa.Boolean(), nullable=True),
sa.Column('verified', sa.Boolean(), nullable=True),
sa.Column('admin', sa.Boolean(), nullable=True),
sa.Column('joined', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('name')
)
op.create_table('awards',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('name', sa.String(length=80), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('value', sa.Integer(), nullable=True),
sa.Column('category', sa.String(length=80), nullable=True),
sa.Column('icon', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('location', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('key_type', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('solves',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('ip', sa.Integer(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('chalid', 'teamid')
)
op.create_table('tags',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chal', sa.Integer(), nullable=True),
sa.Column('tag', sa.String(length=80), nullable=True),
sa.ForeignKeyConstraint(['chal'], ['challenges.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('tracking',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('ip', sa.BigInteger(), nullable=True),
sa.Column('team', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['team'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_table('wrong_keys',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('chalid', sa.Integer(), nullable=True),
sa.Column('teamid', sa.Integer(), nullable=True),
sa.Column('date', sa.DateTime(), nullable=True),
sa.Column('flag', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['chalid'], ['challenges.id'], ),
sa.ForeignKeyConstraint(['teamid'], ['teams.id'], ),
sa.PrimaryKeyConstraint('id')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('wrong_keys')
op.drop_table('tracking')
op.drop_table('tags')
op.drop_table('solves')
op.drop_table('keys')
op.drop_table('files')
op.drop_table('awards')
op.drop_table('teams')
op.drop_table('pages')
op.drop_table('containers')
op.drop_table('config')
op.drop_table('challenges')
# ### end Alembic commands ###

View File

@ -6,12 +6,13 @@ import hashlib
import random import random
from CTFd import create_app from CTFd import create_app
from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Files from CTFd.models import Teams, Solves, Challenges, WrongKeys, Keys, Files, Awards
app = create_app() app = create_app()
USER_AMOUNT = 50 USER_AMOUNT = 50
CHAL_AMOUNT = 20 CHAL_AMOUNT = 20
AWARDS_AMOUNT = 5
categories = [ categories = [
'Exploitation', 'Exploitation',
@ -270,6 +271,20 @@ if __name__ == '__main__':
db.session.commit() db.session.commit()
# Generating Awards
print("GENERATING AWARDS")
for x in range(USER_AMOUNT):
base_time = datetime.datetime.utcnow() + datetime.timedelta(minutes=-10000)
for _ in range(random.randint(0, AWARDS_AMOUNT)):
award = Awards(x + 1, gen_word(), random.randint(-10, 10))
new_base = random_date(base_time, base_time + datetime.timedelta(minutes=random.randint(30, 60)))
award.date = new_base
base_time = new_base
db.session.add(award)
db.session.commit()
# Generating Wrong Keys # Generating Wrong Keys
print("GENERATING WRONG KEYS") print("GENERATING WRONG KEYS")
for x in range(USER_AMOUNT): for x in range(USER_AMOUNT):

View File

@ -2,6 +2,7 @@ Flask
Flask-SQLAlchemy Flask-SQLAlchemy
Flask-Session Flask-Session
Flask-Caching Flask-Caching
Flask-Migrate
SQLAlchemy SQLAlchemy
sqlalchemy-utils sqlalchemy-utils
passlib passlib

View File

@ -173,7 +173,7 @@ def test_user_get_profile():
def test_user_get_logout(): def test_user_get_logout():
"""Can a registered user can load /logout""" """Can a registered user load /logout"""
app = create_ctfd() app = create_ctfd()
with app.app_context(): with app.app_context():
register_user(app) register_user(app)
@ -185,7 +185,7 @@ def test_user_get_logout():
def test_user_get_reset_password(): def test_user_get_reset_password():
"""Can an unregistered user can load /reset_password""" """Can an unregistered user load /reset_password"""
app = create_ctfd() app = create_ctfd()
with app.app_context(): with app.app_context():
register_user(app) register_user(app)