diff --git a/CTFd/plugins/challenges/__init__.py b/CTFd/plugins/challenges/__init__.py index 9c55289c..2448fc49 100644 --- a/CTFd/plugins/challenges/__init__.py +++ b/CTFd/plugins/challenges/__init__.py @@ -21,6 +21,153 @@ class BaseChallenge(object): name = None templates = {} scripts = {} + challenge_model = Challenges + + @classmethod + def create(cls, request): + """ + This method is used to process the challenge creation request. + + :param request: + :return: + """ + data = request.form or request.get_json() + + challenge = cls.challenge_model(**data) + + db.session.add(challenge) + db.session.commit() + + return challenge + + @classmethod + def read(cls, challenge): + """ + This method is in used to access the data of a challenge in a format processable by the front end. + + :param challenge: + :return: Challenge object, data dictionary to be returned to the user + """ + data = { + "id": challenge.id, + "name": challenge.name, + "value": challenge.value, + "description": challenge.description, + "category": challenge.category, + "state": challenge.state, + "max_attempts": challenge.max_attempts, + "type": challenge.type, + "type_data": { + "id": cls.id, + "name": cls.name, + "templates": cls.templates, + "scripts": cls.scripts, + }, + } + return data + + @classmethod + def update(cls, challenge, request): + """ + This method is used to update the information associated with a challenge. This should be kept strictly to the + Challenges table and any child tables. + + :param challenge: + :param request: + :return: + """ + data = request.form or request.get_json() + for attr, value in data.items(): + setattr(challenge, attr, value) + + db.session.commit() + return challenge + + @classmethod + def delete(cls, challenge): + """ + This method is used to delete the resources used by a challenge. + + :param challenge: + :return: + """ + Fails.query.filter_by(challenge_id=challenge.id).delete() + Solves.query.filter_by(challenge_id=challenge.id).delete() + Flags.query.filter_by(challenge_id=challenge.id).delete() + files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all() + for f in files: + delete_file(f.id) + ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete() + Tags.query.filter_by(challenge_id=challenge.id).delete() + Hints.query.filter_by(challenge_id=challenge.id).delete() + cls.challenge_model.query.filter_by(id=challenge.id).delete() + db.session.commit() + + @classmethod + def attempt(cls, challenge, 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 challenge: The Challenge object from the database + :param request: The request the user submitted + :return: (boolean, string) + """ + data = request.form or request.get_json() + submission = data["submission"].strip() + flags = Flags.query.filter_by(challenge_id=challenge.id).all() + for flag in flags: + try: + if get_flag_class(flag.type).compare(flag, submission): + return True, "Correct" + except FlagException as e: + return False, e.message + return False, "Incorrect" + + @classmethod + def solve(cls, user, team, challenge, request): + """ + This method is used to insert Solves into the database in order to mark a challenge as solved. + + :param team: The Team object from the database + :param chal: The Challenge object from the database + :param request: The request the user submitted + :return: + """ + data = request.form or request.get_json() + submission = data["submission"].strip() + solve = Solves( + user_id=user.id, + team_id=team.id if team else None, + challenge_id=challenge.id, + ip=get_ip(req=request), + provided=submission, + ) + db.session.add(solve) + db.session.commit() + + @classmethod + def fail(cls, user, team, challenge, request): + """ + This method is used to insert Fails into the database in order to mark an answer incorrect. + + :param team: The Team object from the database + :param chal: The Challenge object from the database + :param request: The request the user submitted + :return: + """ + data = request.form or request.get_json() + submission = data["submission"].strip() + wrong = Fails( + user_id=user.id, + team_id=team.id if team else None, + challenge_id=challenge.id, + ip=get_ip(request), + provided=submission, + ) + db.session.add(wrong) + db.session.commit() class CTFdStandardChallenge(BaseChallenge): @@ -42,154 +189,7 @@ class CTFdStandardChallenge(BaseChallenge): blueprint = Blueprint( "standard", __name__, template_folder="templates", static_folder="assets" ) - - @staticmethod - def create(request): - """ - This method is used to process the challenge creation request. - - :param request: - :return: - """ - data = request.form or request.get_json() - - challenge = Challenges(**data) - - db.session.add(challenge) - db.session.commit() - - return challenge - - @staticmethod - def read(challenge): - """ - This method is in used to access the data of a challenge in a format processable by the front end. - - :param challenge: - :return: Challenge object, data dictionary to be returned to the user - """ - data = { - "id": challenge.id, - "name": challenge.name, - "value": challenge.value, - "description": challenge.description, - "category": challenge.category, - "state": challenge.state, - "max_attempts": challenge.max_attempts, - "type": challenge.type, - "type_data": { - "id": CTFdStandardChallenge.id, - "name": CTFdStandardChallenge.name, - "templates": CTFdStandardChallenge.templates, - "scripts": CTFdStandardChallenge.scripts, - }, - } - return data - - @staticmethod - def update(challenge, request): - """ - This method is used to update the information associated with a challenge. This should be kept strictly to the - Challenges table and any child tables. - - :param challenge: - :param request: - :return: - """ - data = request.form or request.get_json() - for attr, value in data.items(): - setattr(challenge, attr, value) - - db.session.commit() - return challenge - - @staticmethod - def delete(challenge): - """ - This method is used to delete the resources used by a challenge. - - :param challenge: - :return: - """ - Fails.query.filter_by(challenge_id=challenge.id).delete() - Solves.query.filter_by(challenge_id=challenge.id).delete() - Flags.query.filter_by(challenge_id=challenge.id).delete() - files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all() - for f in files: - delete_file(f.id) - ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete() - Tags.query.filter_by(challenge_id=challenge.id).delete() - Hints.query.filter_by(challenge_id=challenge.id).delete() - Challenges.query.filter_by(id=challenge.id).delete() - db.session.commit() - - @staticmethod - def attempt(challenge, 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 challenge: The Challenge object from the database - :param request: The request the user submitted - :return: (boolean, string) - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - flags = Flags.query.filter_by(challenge_id=challenge.id).all() - for flag in flags: - try: - if get_flag_class(flag.type).compare(flag, submission): - return True, "Correct" - except FlagException as e: - return False, e.message - return False, "Incorrect" - - @staticmethod - def solve(user, team, challenge, request): - """ - This method is used to insert Solves into the database in order to mark a challenge as solved. - - :param team: The Team object from the database - :param chal: The Challenge object from the database - :param request: The request the user submitted - :return: - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - solve = Solves( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(req=request), - provided=submission, - ) - db.session.add(solve) - db.session.commit() - db.session.close() - - @staticmethod - def fail(user, team, challenge, request): - """ - This method is used to insert Fails into the database in order to mark an answer incorrect. - - :param team: The Team object from the database - :param chal: The Challenge object from the database - :param request: The request the user submitted - :return: - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - wrong = Fails( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(request), - provided=submission, - ) - db.session.add(wrong) - db.session.commit() - db.session.close() + challenge_model = Challenges def get_chal_class(class_id): diff --git a/CTFd/plugins/dynamic_challenges/__init__.py b/CTFd/plugins/dynamic_challenges/__init__.py index 5e784c10..6af58ad8 100644 --- a/CTFd/plugins/dynamic_challenges/__init__.py +++ b/CTFd/plugins/dynamic_challenges/__init__.py @@ -4,23 +4,25 @@ import math from flask import Blueprint -from CTFd.models import ( - ChallengeFiles, - Challenges, - Fails, - Flags, - Hints, - Solves, - Tags, - db, -) +from CTFd.models import Challenges, Solves, db from CTFd.plugins import register_plugin_assets_directory from CTFd.plugins.challenges import CHALLENGE_CLASSES, BaseChallenge -from CTFd.plugins.flags import FlagException, get_flag_class from CTFd.plugins.migrations import upgrade from CTFd.utils.modes import get_model -from CTFd.utils.uploads import delete_file -from CTFd.utils.user import get_ip + + +class DynamicChallenge(Challenges): + __mapper_args__ = {"polymorphic_identity": "dynamic"} + id = db.Column( + db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), primary_key=True + ) + initial = db.Column(db.Integer, default=0) + minimum = db.Column(db.Integer, default=0) + decay = db.Column(db.Integer, default=0) + + def __init__(self, *args, **kwargs): + super(DynamicChallenge, self).__init__(**kwargs) + self.initial = kwargs["value"] class DynamicValueChallenge(BaseChallenge): @@ -45,6 +47,7 @@ class DynamicValueChallenge(BaseChallenge): template_folder="templates", static_folder="assets", ) + challenge_model = DynamicChallenge @classmethod def calculate_value(cls, challenge): @@ -82,24 +85,8 @@ class DynamicValueChallenge(BaseChallenge): db.session.commit() return challenge - @staticmethod - def create(request): - """ - This method is used to process the challenge creation request. - - :param request: - :return: - """ - data = request.form or request.get_json() - challenge = DynamicChallenge(**data) - - db.session.add(challenge) - db.session.commit() - - return challenge - - @staticmethod - def read(challenge): + @classmethod + def read(cls, challenge): """ This method is in used to access the data of a challenge in a format processable by the front end. @@ -120,16 +107,16 @@ class DynamicValueChallenge(BaseChallenge): "max_attempts": challenge.max_attempts, "type": challenge.type, "type_data": { - "id": DynamicValueChallenge.id, - "name": DynamicValueChallenge.name, - "templates": DynamicValueChallenge.templates, - "scripts": DynamicValueChallenge.scripts, + "id": cls.id, + "name": cls.name, + "templates": cls.templates, + "scripts": cls.scripts, }, } return data - @staticmethod - def update(challenge, request): + @classmethod + def update(cls, challenge, request): """ This method is used to update the information associated with a challenge. This should be kept strictly to the Challenges table and any child tables. @@ -148,112 +135,12 @@ class DynamicValueChallenge(BaseChallenge): return DynamicValueChallenge.calculate_value(challenge) - @staticmethod - def delete(challenge): - """ - This method is used to delete the resources used by a challenge. - - :param challenge: - :return: - """ - Fails.query.filter_by(challenge_id=challenge.id).delete() - Solves.query.filter_by(challenge_id=challenge.id).delete() - Flags.query.filter_by(challenge_id=challenge.id).delete() - files = ChallengeFiles.query.filter_by(challenge_id=challenge.id).all() - for f in files: - delete_file(f.id) - ChallengeFiles.query.filter_by(challenge_id=challenge.id).delete() - Tags.query.filter_by(challenge_id=challenge.id).delete() - Hints.query.filter_by(challenge_id=challenge.id).delete() - DynamicChallenge.query.filter_by(id=challenge.id).delete() - Challenges.query.filter_by(id=challenge.id).delete() - db.session.commit() - - @staticmethod - def attempt(challenge, 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 challenge: The Challenge object from the database - :param request: The request the user submitted - :return: (boolean, string) - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - flags = Flags.query.filter_by(challenge_id=challenge.id).all() - for flag in flags: - try: - if get_flag_class(flag.type).compare(flag, submission): - return True, "Correct" - except FlagException as e: - return False, e.message - return False, "Incorrect" - - @staticmethod - def solve(user, team, challenge, request): - """ - This method is used to insert Solves into the database in order to mark a challenge as solved. - - :param team: The Team object from the database - :param chal: The Challenge object from the database - :param request: The request the user submitted - :return: - """ - challenge = DynamicChallenge.query.filter_by(id=challenge.id).first() - data = request.form or request.get_json() - submission = data["submission"].strip() - - solve = Solves( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(req=request), - provided=submission, - ) - db.session.add(solve) - db.session.commit() + @classmethod + def solve(cls, user, team, challenge, request): + super().solve(user, team, challenge, request) DynamicValueChallenge.calculate_value(challenge) - @staticmethod - def fail(user, team, challenge, request): - """ - This method is used to insert Fails into the database in order to mark an answer incorrect. - - :param team: The Team object from the database - :param challenge: The Challenge object from the database - :param request: The request the user submitted - :return: - """ - data = request.form or request.get_json() - submission = data["submission"].strip() - wrong = Fails( - user_id=user.id, - team_id=team.id if team else None, - challenge_id=challenge.id, - ip=get_ip(request), - provided=submission, - ) - db.session.add(wrong) - db.session.commit() - db.session.close() - - -class DynamicChallenge(Challenges): - __mapper_args__ = {"polymorphic_identity": "dynamic"} - id = db.Column( - db.Integer, db.ForeignKey("challenges.id", ondelete="CASCADE"), primary_key=True - ) - initial = db.Column(db.Integer, default=0) - minimum = db.Column(db.Integer, default=0) - decay = db.Column(db.Integer, default=0) - - def __init__(self, *args, **kwargs): - super(DynamicChallenge, self).__init__(**kwargs) - self.initial = kwargs["value"] - def load(app): upgrade()