diff --git a/.travis.yml b/.travis.yml index ceb7bb4..08f6642 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,18 @@ language: python +services: + - mysql + - postgresql +env: + - TESTING_DATABASE_URL='mysql+pymysql://root@localhost/ctfd' + - TESTING_DATABASE_URL='sqlite://' + - TESTING_DATABASE_URL='postgres://postgres@localhost/ctfd' python: - 2.7 - 3.6 install: - pip install -r development.txt +before_script: + - psql -c 'create database ctfd;' -U postgres script: - pep8 --ignore E501,E712 CTFd/ tests/ - nosetests diff --git a/CTFd/admin/__init__.py b/CTFd/admin/__init__.py index 92abd0c..526642c 100644 --- a/CTFd/admin/__init__.py +++ b/CTFd/admin/__init__.py @@ -11,7 +11,6 @@ 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.scoreboard import get_standings from CTFd.plugins.keys import get_key_class, KEY_CLASSES from CTFd.admin.statistics import admin_statistics diff --git a/CTFd/admin/challenges.py b/CTFd/admin/challenges.py index a5e4699..10e3f7d 100644 --- a/CTFd/admin/challenges.py +++ b/CTFd/admin/challenges.py @@ -16,7 +16,13 @@ admin_challenges = Blueprint('admin_challenges', __name__) def admin_chal_types(): data = {} for class_id in CHALLENGE_CLASSES: - data[class_id] = CHALLENGE_CLASSES.get(class_id).name + 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) @@ -52,7 +58,13 @@ def admin_chals(): 'max_attempts': x.max_attempts, 'type': x.type, 'type_name': type_name, - 'percentage_solved': percentage + 'percentage_solved': percentage, + 'type_data': { + 'id': type_class.id, + 'name': type_class.name, + 'templates': type_class.templates, + 'scripts': type_class.scripts, + } }) db.session.close() @@ -225,7 +237,14 @@ def admin_create_chal(): files = request.files.getlist('files[]') # Create challenge - chal = Challenges(request.form['name'], request.form['desc'], request.form['value'], request.form['category'], int(request.form['chaltype'])) + chal = Challenges( + name=request.form['name'], + description=request.form['desc'], + value=request.form['value'], + category=request.form['category'], + type=request.form['chaltype'] + ) + if 'hidden' in request.form: chal.hidden = True else: diff --git a/CTFd/challenges.py b/CTFd/challenges.py index f1b05c9..5763542 100644 --- a/CTFd/challenges.py +++ b/CTFd/challenges.py @@ -130,7 +130,9 @@ def chals(): 'category': x.category, 'files': files, 'tags': tags, - 'hints': hints + 'hints': hints, + 'template': chal_type.templates['modal'], + 'script': chal_type.scripts['modal'], }) db.session.close() diff --git a/CTFd/config.py b/CTFd/config.py index 44cb2cc..3231857 100644 --- a/CTFd/config.py +++ b/CTFd/config.py @@ -127,5 +127,5 @@ class TestingConfig(Config): PRESERVE_CONTEXT_ON_EXCEPTION = False TESTING = True DEBUG = True - SQLALCHEMY_DATABASE_URI = 'sqlite://' + SQLALCHEMY_DATABASE_URI = os.environ.get('TESTING_DATABASE_URL') or 'sqlite://' SERVER_NAME = 'localhost' diff --git a/CTFd/models.py b/CTFd/models.py index c212030..2fe2b17 100644 --- a/CTFd/models.py +++ b/CTFd/models.py @@ -48,16 +48,15 @@ class Challenges(db.Model): max_attempts = db.Column(db.Integer, default=0) value = db.Column(db.Integer) category = db.Column(db.String(80)) - type = db.Column(db.Integer) + type = db.Column(db.String(80)) hidden = db.Column(db.Boolean) - def __init__(self, name, description, value, category, type=0): + def __init__(self, name, description, value, category, type='standard'): self.name = name self.description = description self.value = value self.category = category self.type = type - # self.flags = json.dumps(flags) def __repr__(self): return '' % self.name @@ -190,15 +189,23 @@ class Teams(db.Model): 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).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') ).group_by(Awards.teamid) @@ -212,16 +219,26 @@ class Teams(db.Model): results = union_all(scores, awards).alias('results') - sumscore = db.func.sum(results.columns.score).label('sumscore') - quickest = db.func.max(results.columns.date).label('quickest') + 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() - standings_query = db.session.query(results.columns.teamid)\ - .join(Teams)\ - .group_by(results.columns.teamid)\ - .order_by(sumscore.desc(), quickest) - - if not admin: - standings_query = standings_query.filter(Teams.banned == False) + 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() diff --git a/CTFd/plugins/__init__.py b/CTFd/plugins/__init__.py index fd8b181..2e9eda7 100644 --- a/CTFd/plugins/__init__.py +++ b/CTFd/plugins/__init__.py @@ -57,7 +57,7 @@ def init_plugins(app): :return: """ modules = glob.glob(os.path.dirname(__file__) + "/*") - blacklist = {'keys', 'challenges', '__pycache__'} + blacklist = {'keys', '__pycache__'} for module in modules: module_name = os.path.basename(module) if os.path.isdir(module) and module_name not in blacklist: diff --git a/CTFd/plugins/challenges/__init__.py b/CTFd/plugins/challenges/__init__.py index 1e7e4cb..0ee477d 100644 --- a/CTFd/plugins/challenges/__init__.py +++ b/CTFd/plugins/challenges/__init__.py @@ -1,3 +1,4 @@ +from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins.keys import get_key_class from CTFd.models import db, Solves, WrongKeys, Keys from CTFd import utils @@ -6,14 +7,35 @@ from CTFd import utils class BaseChallenge(object): id = None name = None + templates = {} + scripts = {} class CTFdStandardChallenge(BaseChallenge): - id = 0 - name = "standard" + id = "standard" # Unique identifier used to register challenges + name = "standard" # Name of a challenge type + templates = { # Handlebars templates used for each aspect of challenge editing & viewing + 'create': '/plugins/challenges/assets/standard-challenge-create.hbs', + 'update': '/plugins/challenges/assets/standard-challenge-update.hbs', + 'modal': '/plugins/challenges/assets/standard-challenge-modal.hbs', + } + 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', + } @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) + """ provided_key = request.form['key'].strip() chal_keys = Keys.query.filter_by(chal=chal.id).all() for chal_key in chal_keys: @@ -23,6 +45,14 @@ class CTFdStandardChallenge(BaseChallenge): @staticmethod def solve(team, chal, 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: + """ provided_key = request.form['key'].strip() solve = Solves(teamid=team.id, chalid=chal.id, ip=utils.get_ip(req=request), flag=provided_key) db.session.add(solve) @@ -31,6 +61,14 @@ class CTFdStandardChallenge(BaseChallenge): @staticmethod def fail(team, chal, request): + """ + This method is used to insert WrongKeys 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) db.session.add(wrong) @@ -38,13 +76,27 @@ class CTFdStandardChallenge(BaseChallenge): db.session.close() -CHALLENGE_CLASSES = { - 0: CTFdStandardChallenge -} - - def get_chal_class(class_id): + """ + Utility function used to get the corresponding class from a class ID. + + :param class_id: String representing the class ID + :return: Challenge class + """ cls = CHALLENGE_CLASSES.get(class_id) if cls is None: raise KeyError return cls + + +""" +Global dictionary used to hold all the Challenge Type classes used by CTFd. Insert into this dictionary to register +your Challenge Type. +""" +CHALLENGE_CLASSES = { + "standard": CTFdStandardChallenge +} + + +def load(app): + register_plugin_assets_directory(app, base_path='/plugins/challenges/assets/') diff --git a/CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-create.hbs b/CTFd/plugins/challenges/assets/standard-challenge-create.hbs similarity index 98% rename from CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-create.hbs rename to CTFd/plugins/challenges/assets/standard-challenge-create.hbs index 2a00893..2731d50 100644 --- a/CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-create.hbs +++ b/CTFd/plugins/challenges/assets/standard-challenge-create.hbs @@ -103,7 +103,7 @@ - +
diff --git a/CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-create.js b/CTFd/plugins/challenges/assets/standard-challenge-create.js similarity index 100% rename from CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-create.js rename to CTFd/plugins/challenges/assets/standard-challenge-create.js diff --git a/CTFd/themes/original/static/js/templates/challenges/standard/standard-challenge-modal.hbs b/CTFd/plugins/challenges/assets/standard-challenge-modal.hbs similarity index 100% rename from CTFd/themes/original/static/js/templates/challenges/standard/standard-challenge-modal.hbs rename to CTFd/plugins/challenges/assets/standard-challenge-modal.hbs diff --git a/CTFd/themes/original/static/js/templates/challenges/standard/standard-challenge-script.js b/CTFd/plugins/challenges/assets/standard-challenge-modal.js similarity index 100% rename from CTFd/themes/original/static/js/templates/challenges/standard/standard-challenge-script.js rename to CTFd/plugins/challenges/assets/standard-challenge-modal.js diff --git a/CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-update.hbs b/CTFd/plugins/challenges/assets/standard-challenge-update.hbs similarity index 100% rename from CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-update.hbs rename to CTFd/plugins/challenges/assets/standard-challenge-update.hbs diff --git a/CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-update.js b/CTFd/plugins/challenges/assets/standard-challenge-update.js similarity index 100% rename from CTFd/themes/admin/static/js/templates/challenges/standard/standard-challenge-update.js rename to CTFd/plugins/challenges/assets/standard-challenge-update.js diff --git a/CTFd/scoreboard.py b/CTFd/scoreboard.py index d509b3c..dbf957b 100644 --- a/CTFd/scoreboard.py +++ b/CTFd/scoreboard.py @@ -12,28 +12,48 @@ def get_standings(admin=False, count=None): 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).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') ).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'), @@ -41,7 +61,7 @@ def get_standings(admin=False, count=None): Teams.banned, sumscores.columns.score )\ .join(sumscores, Teams.id == sumscores.columns.teamid) \ - .order_by(sumscores.columns.score.desc(), sumscores.columns.date) + .order_by(sumscores.columns.score.desc(), sumscores.columns.id) else: standings_query = db.session.query( Teams.id.label('teamid'), @@ -50,13 +70,17 @@ def get_standings(admin=False, count=None): )\ .join(sumscores, Teams.id == sumscores.columns.teamid) \ .filter(Teams.banned == False) \ - .order_by(sumscores.columns.score.desc(), sumscores.columns.date) + .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 diff --git a/CTFd/themes/admin/static/js/chal-new.js b/CTFd/themes/admin/static/js/chal-new.js index 65990bb..e6633f4 100644 --- a/CTFd/themes/admin/static/js/chal-new.js +++ b/CTFd/themes/admin/static/js/chal-new.js @@ -1,24 +1,25 @@ -function load_chal_template(chal_type_name){ - $.get(script_root + '/themes/admin/static/js/templates/challenges/'+ chal_type_name +'/' + chal_type_name + '-challenge-create.hbs', function(template_data){ +function load_chal_template(challenge){ + $.get(script_root + challenge.templates.create, function(template_data){ var template = Handlebars.compile(template_data); $("#create-chal-entry-div").html(template({'nonce':nonce, 'script_root':script_root})); - $.getScript(script_root + '/themes/admin/static/js/templates/challenges/'+chal_type_name+'/'+chal_type_name+'-challenge-create.js', function(){ + $.getScript(script_root + challenge.scripts.create, function(){ console.log('loaded'); }); }); } - -nonce = "{{ nonce }}"; $.get(script_root + '/admin/chal_types', function(data){ - console.log(data); $("#create-chals-select").empty(); var chal_type_amt = Object.keys(data).length; if (chal_type_amt > 1){ var option = ""; $("#create-chals-select").append(option); for (var key in data){ - var option = "".format(key, data[key]); + var challenge = data[key]; + var option = $("