From f48a0cdacdbe5b8b17baab41d3e30e1b1db153b2 Mon Sep 17 00:00:00 2001 From: Kevin Chung Date: Tue, 28 Mar 2017 21:17:56 -0400 Subject: [PATCH] Hints (#232) * Switching to Flask-Migrate to create tables/database. Adding Hints & Unlocks. * Adding db.create_all call for sqlite db's (sqlite is not properly handled with alembic yet) * Python 3 testing works properly with 3.5 * Adding admin side of hints * Hints are viewable for users --- .travis.yml | 2 +- CTFd/__init__.py | 25 +- CTFd/admin/challenges.py | 87 ++++++- CTFd/challenges.py | 56 ++++- CTFd/config.py | 4 +- CTFd/models.py | 33 +++ CTFd/static/admin/js/chalboard.js | 77 ++++++ CTFd/static/admin/js/utils.js | 19 ++ CTFd/static/original/js/chalboard.js | 16 +- CTFd/static/original/js/multi-modal.js | 63 +++++ .../standard/standard-challenge-modal.hbs | 23 ++ CTFd/static/original/js/utils.js | 19 ++ CTFd/templates/admin/chals.html | 87 +++++-- CTFd/templates/original/chals.html | 14 ++ migrations/env.py | 3 +- ...dds_challenge_types_and_uses_keys_table.py | 6 +- ...db614c1_adding_hints_and_unlocks_tables.py | 46 ++++ migrations/versions/cb3cfcc47e2f_base.py | 235 ++++++++---------- serve.py | 2 +- tests/test_user_facing.py | 4 +- 20 files changed, 644 insertions(+), 177 deletions(-) create mode 100644 CTFd/static/original/js/multi-modal.js create mode 100644 migrations/versions/c7225db614c1_adding_hints_and_unlocks_tables.py diff --git a/.travis.yml b/.travis.yml index f469e6b..7bc61b3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python python: - 2.7 - - 3.6 + - 3.5 install: - pip install -r development.txt script: diff --git a/CTFd/__init__.py b/CTFd/__init__.py index 2fc2a74..fbfeb68 100644 --- a/CTFd/__init__.py +++ b/CTFd/__init__.py @@ -34,23 +34,26 @@ def create_app(config='CTFd.config.Config'): if url.drivername == 'postgres': url.drivername = 'postgresql' + ## Creates database if the database database does not exist + if not database_exists(url): + create_database(url) + + ## Register database db.init_app(app) - try: - if not (url.drivername.startswith('sqlite') or database_exists(url)): - create_database(url) - db.create_all() - except OperationalError: - db.create_all() - except ProgrammingError: ## Database already exists - pass - else: + ## Register Flask-Migrate + migrate.init_app(app, db) + + ## This creates tables instead of db.create_all() + ## Allows migrations to happen properly + migrate_upgrade() + + ## Alembic sqlite support is lacking so we should just create_all anyway + if url.drivername.startswith('sqlite'): db.create_all() app.db = db - migrate.init_app(app, db) - cache.init_app(app) app.cache = cache diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index c3fe033..8b733bc 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -1,6 +1,6 @@ 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, 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, Hints, Unlocks, DatabaseError from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.plugins.challenges import get_chal_class, CHALLENGE_CLASSES @@ -87,6 +87,66 @@ def admin_delete_tags(tagid): return '1' +@admin_challenges.route('/admin/hints', defaults={'hintid': None}, methods=['POST', 'GET']) +@admin_challenges.route('/admin/hints/', 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')) + 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')) + 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/', methods=['GET', 'POST']) @admins_only def admin_files(chalid): @@ -125,13 +185,34 @@ def admin_get_values(chalid, prop): chal_keys = Keys.query.filter_by(chal=challenge.id).all() json_data = {'keys': []} for x in chal_keys: - json_data['keys'].append({'id': x.id, 'key': x.flag, 'type': x.key_type, 'type_name': get_key_class(x.key_type).name}) + json_data['keys'].append({ + 'id': x.id, + 'key': x.flag, + 'type': x.key_type, + 'type_name': get_key_class(x.key_type).name + }) 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}) + 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) diff --git a/CTFd/challenges.py b/CTFd/challenges.py index f67a608..92c6199 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -6,7 +6,7 @@ import time from flask import render_template, request, redirect, jsonify, url_for, session, Blueprint from sqlalchemy.sql import or_ -from CTFd.models import db, Challenges, Files, Solves, WrongKeys, Keys, Tags, Teams, Awards +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.plugins.challenges import get_chal_class @@ -14,6 +14,49 @@ from CTFd import utils challenges = Blueprint('challenges', __name__) +@challenges.route('/hints/', methods=['GET', 'POST']) +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 not unlock: + 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='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) + else: + json_data = { + 'hint': hint.hint, + 'chal': hint.chal, + 'cost': hint.cost + } + db.session.close() + return jsonify(json_data) + @challenges.route('/challenges', methods=['GET']) def challenges_view(): @@ -57,6 +100,14 @@ def chals(): for x in chals: tags = [tag.tag for tag in Tags.query.add_columns('tag').filter_by(chal=x.id).all()] files = [str(f.location) for f in Files.query.filter_by(chal=x.id).all()] + unlocked_hints = set([u.itemid for u in Unlocks.query.filter_by(model='hints', teamid=session['id'])]) + hints = [] + for hint in Hints.query.filter_by(chal=x.id).all(): + if hint.id in unlocked_hints: + hints.append({'id':hint.id, 'cost':hint.cost, 'hint':hint.hint}) + else: + hints.append({'id':hint.id, 'cost':hint.cost}) + # hints = [{'id':hint.id, 'cost':hint.cost} for hint in Hints.query.filter_by(chal=x.id).all()] chal_type = get_chal_class(x.type) json['game'].append({ 'id': x.id, @@ -66,7 +117,8 @@ def chals(): 'description': x.description, 'category': x.category, 'files': files, - 'tags': tags + 'tags': tags, + 'hints': hints }) db.session.close() diff --git a/CTFd/config.py b/CTFd/config.py index e6d6ff0..2672eae 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -32,7 +32,7 @@ class Config(object): http://flask-sqlalchemy.pocoo.org/2.1/config/#configuration-keys ''' - SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///ctfd.db' + SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or 'sqlite:///{}/ctfd.db'.format(os.path.dirname(__file__)) ''' @@ -133,4 +133,4 @@ class TestingConfig(Config): PRESERVE_CONTEXT_ON_EXCEPTION = False TESTING = True DEBUG = True - SQLALCHEMY_DATABASE_URI = 'sqlite://' \ No newline at end of file + SQLALCHEMY_DATABASE_URI = 'sqlite://' diff --git a/CTFd/models.py b/CTFd/models.py index 386d440..4befa17 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -76,6 +76,23 @@ class Challenges(db.Model): return '' % 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 '' % self.hint + + class Awards(db.Model): id = db.Column(db.Integer, primary_key=True) teamid = db.Column(db.Integer, db.ForeignKey('teams.id')) @@ -244,6 +261,22 @@ class WrongKeys(db.Model): return '' % 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 '' % self.teamid + + class Tracking(db.Model): id = db.Column(db.Integer, primary_key=True) ip = db.Column(db.BigInteger) diff --git a/CTFd/static/admin/js/chalboard.js b/CTFd/static/admin/js/chalboard.js index 9a144b9..8a4c5f5 100644 --- a/CTFd/static/admin/js/chalboard.js +++ b/CTFd/static/admin/js/chalboard.js @@ -38,6 +38,25 @@ function load_edit_key_modal(key_id, key_type_name) { }); } +function load_hint_modal(method, hintid){ + $('#hint-modal-hint').val(''); + $('#hint-modal-cost').val(''); + if (method == 'create'){ + $('#hint-modal-submit').attr('action', '/admin/hints'); + $('#hint-modal-title').text('Create Hint'); + $("#hint-modal").modal(); + } else if (method == 'update'){ + $.get(script_root + '/admin/hints/' + hintid, function(data){ + $('#hint-modal-submit').attr('action', '/admin/hints/' + hintid); + $('#hint-modal-hint').val(data.hint); + $('#hint-modal-cost').val(data.cost); + $('#hint-modal-title').text('Update Hint'); + $("#hint-modal-button").text('Update Hint'); + $("#hint-modal").modal(); + }); + } +} + function loadchal(id, update) { // $('#chal *').show() // $('#chal > h1').hide() @@ -169,6 +188,44 @@ function deletetag(tagid){ $.post(script_root + '/admin/tags/'+tagid+'/delete', {'nonce': $('#nonce').val()}); } + +function edithint(hintid){ + $.get(script_root + '/admin/hints/' + hintid, function(data){ + console.log(data); + }) +} + + +function deletehint(hintid){ + $.delete(script_root + '/admin/hints/' + hintid, function(data, textStatus, jqXHR){ + if (jqXHR.status == 204){ + var chalid = $('.chal-id').val(); + loadhints(chalid); + } + }); +} + + +function loadhints(chal){ + $.get(script_root + '/admin/chal/{0}/hints'.format(chal), function(data){ + var table = $('#hintsboard > tbody'); + table.empty(); + for (var i = 0; i < data.hints.length; i++) { + var hint = data.hints[i] + var hint_row = "" + + "{0}".format(hint.hint) + + "{0}".format(hint.cost) + + "" + + "".format(hint.id)+ + "".format(hint.id)+ + "" + + ""; + table.append(hint_row); + } + }); +} + + function deletechal(chalid){ $.post(script_root + '/admin/chal/delete', {'nonce':$('#nonce').val(), 'id':chalid}); } @@ -255,6 +312,7 @@ function loadchals(){ $('#challenges button').click(function (e) { loadchal(this.value); loadkeys(this.value); + loadhints(this.value); loadtags(this.value); loadfiles(this.value); }); @@ -373,6 +431,25 @@ $('#create-keys-submit').click(function (e) { create_key(chalid, key_data, key_type); }); + +$('#create-hint').click(function(e){ + e.preventDefault(); + load_hint_modal('create'); +}); + +$('#hint-modal-submit').submit(function (e) { + e.preventDefault(); + var params = {} + $(this).serializeArray().map(function(x){ + params[x.name] = x.value; + }); + $.post(script_root + $(this).attr('action'), params, function(data){ + loadhints(params['chal']); + }); + $("#hint-modal").modal('hide'); +}); + + $(function(){ loadchals(); }) diff --git a/CTFd/static/admin/js/utils.js b/CTFd/static/admin/js/utils.js index c5581d1..1dc99fb 100644 --- a/CTFd/static/admin/js/utils.js +++ b/CTFd/static/admin/js/utils.js @@ -60,4 +60,23 @@ Handlebars.registerHelper('if_eq', function(a, b, opts) { } else { return opts.inverse(this); } +}); + +// http://stepansuvorov.com/blog/2014/04/jquery-put-and-delete/ +jQuery.each(["put", "delete"], function(i, method) { + jQuery[method] = function(url, data, callback, type) { + if (jQuery.isFunction(data)) { + type = type || callback; + callback = data; + data = undefined; + } + + return jQuery.ajax({ + url: url, + type: method, + dataType: type, + data: data, + success: callback + }); + }; }); \ No newline at end of file diff --git a/CTFd/static/original/js/chalboard.js b/CTFd/static/original/js/chalboard.js index eb05d1a..aa1bd31 100644 --- a/CTFd/static/original/js/chalboard.js +++ b/CTFd/static/original/js/chalboard.js @@ -42,6 +42,7 @@ function updateChalWindow(obj) { desc: marked(obj.description, {'gfm':true, 'breaks':true}), solves: solves, files: obj.files, + hints: obj.hints }; $('#chal-window').append(template(wrapper)); @@ -226,7 +227,7 @@ function loadchals(refresh) { var chalid = chalinfo.name.replace(/ /g,"-").hashCode(); var catid = chalinfo.category.replace(/ /g,"-").hashCode(); var chalwrap = $("
".format(chalid)); - var chalbutton = $("

New Challenge