diff --git a/ctf.nginx b/ctf.nginx index c6dfbd8..7726e44 100644 --- a/ctf.nginx +++ b/ctf.nginx @@ -14,7 +14,7 @@ server { # } # Put all the pages here so Angular doesn't fail. - location ~^/(about|chat|help|learn|login|profile|register|scoreboard|settings|team|forgot)$ { + location ~^/(about|chat|help|learn|login|logout|profile|register|scoreboard|settings|team|forgot)$ { default_type text/html; try_files /index.html /index.html; } diff --git a/problems/programming/cancer/problem.json b/problems/programming/cancer/problem.json index 34e1a20..1820e3b 100644 --- a/problems/programming/cancer/problem.json +++ b/problems/programming/cancer/problem.json @@ -3,9 +3,9 @@ "title": "Cancer", "hint": "No hint!", "category": "Miscellaneous", - "autogen": false, + "autogen": true, "programming": false, - "value": 20, + "value": 100, "bonus": 0, "threshold": 0, "weightmap": { } diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 6fa252f..884662d 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -5,4 +5,5 @@ SQLAlchemy gunicorn requests voluptuous -PIL \ No newline at end of file +PIL +markdown2 \ No newline at end of file diff --git a/server/api/admin.py b/server/api/admin.py index 0114e95..14bc52a 100644 --- a/server/api/admin.py +++ b/server/api/admin.py @@ -3,7 +3,7 @@ from decorators import admins_only, api_wrapper from models import db, Problems, Files from schemas import verify_to_schema, check -import json +import cPickle as pickle blueprint = Blueprint("admin", __name__) @@ -16,14 +16,15 @@ def problem_data(): for problem in problems: problems_return.append({ "pid": problem.pid, - "name": problem.name, + "title": problem.title, "category": problem.category, "description": problem.description, "hint": problem.hint, "value": problem.value, "threshold": problem.threshold, - "weightmap": json.loads(problem.weightmap) + "weightmap": problem.weightmap }) + problems_return.sort(key=lambda prob: prob["value"]) return { "success": 1, "problems": problems_return } """ diff --git a/server/api/decorators.py b/server/api/decorators.py index 07fd3f8..8730dc1 100644 --- a/server/api/decorators.py +++ b/server/api/decorators.py @@ -7,6 +7,7 @@ import traceback import utils class WebException(Exception): pass +class InternalException(Exception): pass response_header = { "Content-Type": "application/json; charset=utf-8" } def api_wrapper(f): diff --git a/server/api/logger.py b/server/api/logger.py index 3923c5a..1d886b7 100644 --- a/server/api/logger.py +++ b/server/api/logger.py @@ -28,7 +28,7 @@ def initialize_logs(): for importer, modname, ispkg in pkgutil.walk_packages(path="../api"): create_logger(modname) -def log(logname, level, message): +def log(logname, message, level=INFO): logger = logging.getLogger(logname) message = "[%s] %s" % (datetime.datetime.now().strftime("%m/%d/%Y %X"), message) logger.log(level, message) \ No newline at end of file diff --git a/server/api/models.py b/server/api/models.py index e429e5a..3270b09 100644 --- a/server/api/models.py +++ b/server/api/models.py @@ -3,6 +3,7 @@ from flask.ext.sqlalchemy import SQLAlchemy import time import traceback import utils +import cPickle as pickle db = SQLAlchemy() @@ -70,17 +71,14 @@ class Teams(db.Model): return members def points(self): - score = db.func.sum(Problems.value).label("score") - team = db.session.query(Solves.tid, score).join(Teams).join(Problems).filter(Teams.tid==self.tid).group_by(Solves.tid).first() - if team: - return team.score - else: - return 0 + """ TODO: Implement scoring with Bonus Points """ + return 0 def place(self, ranked=True): - score = db.func.sum(Problems.value).label("score") - quickest = db.func.max(Solves.date).label("quickest") - teams = db.session.query(Solves.tid).join(Teams).join(Problems).filter().group_by(Solves.tid).order_by(score.desc(), quickest).all() + # score = db.func.sum(Problems.value).label("score") + # quickest = db.func.max(Solves.date).label("quickest") + # teams = db.session.query(Solves.tid).join(Teams).join(Problems).filter().group_by(Solves.tid).order_by(score.desc(), quickest).all() + teams = [ self.tid ] try: i = teams.index((self.tid,)) + 1 k = i % 10 @@ -134,23 +132,28 @@ class Teams(db.Model): return False class Problems(db.Model): - pid = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(128)) + pid = db.Column(db.String(128), primary_key=True, autoincrement=False) + title = db.Column(db.String(128)) category = db.Column(db.String(128)) description = db.Column(db.Text) - hint = db.Column(db.Text) - flag = db.Column(db.Text) - disabled = db.Column(db.Boolean, default=False) value = db.Column(db.Integer) - solves = db.Column(db.Integer, default=0) + hint = db.Column(db.Text) + autogen = db.Column(db.Boolean) + bonus = db.Column(db.Integer) + threshold = db.Column(db.Integer) + weightmap = db.Column(db.PickleType) - def __init__(self, name, category, description, hint, flag, value): - self.name = name + def __init__(self, pid, title, category, description, value, hint="", autogen=False, bonus=0, threshold=0, weightmap={}): + self.pid = pid + self.title = title self.category = category self.description = description - self.hint = hint - self.flag = flag self.value = value + self.hint = hint + self.autogen = autogen + self.bonus = bonus + self.threshold = threshold + self.weightmap = weightmap class Files(db.Model): fid = db.Column(db.Integer, primary_key=True) @@ -162,12 +165,11 @@ class Files(db.Model): self.location = location class Solves(db.Model): + __table_args__ = (db.UniqueConstraint("pid", "tid"), {}) sid = db.Column(db.Integer, primary_key=True) - pid = db.Column(db.Integer, db.ForeignKey("problems.pid")) - tid = db.Column(db.Integer, db.ForeignKey("teams.tid")) + pid = db.Column(db.Integer) + tid = db.Column(db.Integer) date = db.Column(db.Integer, default=utils.get_time_since_epoch()) - team = db.relationship("Teams", foreign_keys="Solves.tid", lazy="joined") - prob = db.relationship("Problems", foreign_keys="Solves.pid", lazy="joined") correct = db.Column(db.Boolean) flag = db.Column(db.Text) diff --git a/server/api/problem.py b/server/api/problem.py index 82e289c..8e410a4 100644 --- a/server/api/problem.py +++ b/server/api/problem.py @@ -7,7 +7,7 @@ from flask import current_app as app from werkzeug import secure_filename from models import db, Files, Problems, Solves, Teams -from decorators import admins_only, api_wrapper, login_required, WebException +from decorators import admins_only, api_wrapper, login_required, InternalException, WebException blueprint = Blueprint("problem", __name__) @@ -15,122 +15,149 @@ blueprint = Blueprint("problem", __name__) @admins_only @api_wrapper def problem_add(): - name = request.form["name"] - category = request.form["category"] - description = request.form["description"] - hint = request.form["problem-hint"] - flag = request.form["flag"] - value = request.form["value"] + name = request.form["name"] + category = request.form["category"] + description = request.form["description"] + hint = request.form["problem-hint"] + flag = request.form["flag"] + value = request.form["value"] - name_exists = Problems.query.filter_by(name=name).first() - if name_exists: - raise WebException("Problem name already taken.") - problem = Problems(name, category, description, hint, flag, value) - db.session.add(problem) - db.session.commit() + name_exists = Problems.query.filter_by(name=name).first() + if name_exists: + raise WebException("Problem name already taken.") + problem = Problems(name, category, description, hint, flag, value) + db.session.add(problem) + db.session.commit() - files = request.files.getlist("files[]") - for _file in files: - filename = secure_filename(_file.filename) + files = request.files.getlist("files[]") + for _file in files: + filename = secure_filename(_file.filename) - if len(filename) == 0: - continue + if len(filename) == 0: + continue - file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) + file_path = os.path.join(app.config["UPLOAD_FOLDER"], filename) - _file.save(file_path) - db_file = Files(problem.pid, "/".join(file_path.split("/")[2:])) - db.session.add(db_file) + _file.save(file_path) + db_file = Files(problem.pid, "/".join(file_path.split("/")[2:])) + db.session.add(db_file) - db.session.commit() + db.session.commit() - return { "success": 1, "message": "Success!" } + return { "success": 1, "message": "Success!" } @blueprint.route("/delete", methods=["POST"]) @admins_only @api_wrapper def problem_delete(): - pid = request.form["pid"] - problem = Problems.query.filter_by(pid=pid).first() - if problem: - Solves.query.filter_by(pid=pid).delete() - Problems.query.filter_by(pid=pid).delete() - db.session.commit() - return { "success": 1, "message": "Success!" } - raise WebException("Problem does not exist!") + pid = request.form["pid"] + problem = Problems.query.filter_by(pid=pid).first() + if problem: + Solves.query.filter_by(pid=pid).delete() + Problems.query.filter_by(pid=pid).delete() + db.session.commit() + return { "success": 1, "message": "Success!" } + raise WebException("Problem does not exist!") @blueprint.route("/update", methods=["POST"]) @admins_only @api_wrapper def problem_update(): - pid = request.form["pid"] - name = request.form["name"] - category = request.form["category"] - description = request.form["description"] - hint = request.form["hint"] - flag = request.form["flag"] - disabled = request.form.get("disabled", 0) - value = request.form["value"] + pid = request.form["pid"] + name = request.form["name"] + category = request.form["category"] + description = request.form["description"] + hint = request.form["hint"] + flag = request.form["flag"] + disabled = request.form.get("disabled", 0) + value = request.form["value"] - problem = Problems.query.filter_by(pid=pid).first() - if problem: - problem.name = name - problem.category = category - problem.description = description - problem.hint = hint - problem.flag = flag - problem.disabled = disabled - problem.value = value + problem = Problems.query.filter_by(pid=pid).first() + if problem: + problem.name = name + problem.category = category + problem.description = description + problem.hint = hint + problem.flag = flag + problem.disabled = disabled + problem.value = value - db.session.add(problem) - db.session.commit() + db.session.add(problem) + db.session.commit() - return { "success": 1, "message": "Success!" } - raise WebException("Problem does not exist!") + return { "success": 1, "message": "Success!" } + raise WebException("Problem does not exist!") @blueprint.route("/submit", methods=["POST"]) @api_wrapper @login_required def problem_submit(): - pid = request.form["pid"] - flag = request.form["flag"] - tid = session["tid"] + pid = request.form["pid"] + flag = request.form["flag"] + tid = session["tid"] - problem = Problems.query.filter_by(pid=pid).first() - team = Teams.query.filter_by(tid=tid).first() - if problem: - if flag == problem.flag: - solve = Solves(pid, tid) - team.score += problem.value - problem.solves += 1 - db.session.add(solve) - db.session.add(team) - db.session.add(problem) - db.session.commit() + problem = Problems.query.filter_by(pid=pid).first() + team = Teams.query.filter_by(tid=tid).first() + if problem: + if flag == problem.flag: + solve = Solves(pid, tid) + team.score += problem.value + problem.solves += 1 + db.session.add(solve) + db.session.add(team) + db.session.add(problem) + db.session.commit() - logger.log(__name__, logger.WARNING, "%s has solved %s by submitting %s" % (team.name, problem.name, flag)) - return { "success": 1, "message": "Correct!" } + logger.log(__name__, logger.WARNING, "%s has solved %s by submitting %s" % (team.name, problem.name, flag)) + return { "success": 1, "message": "Correct!" } - else: - logger.log(__name__, logger.WARNING, "%s has incorrectly submitted %s to %s" % (team.name, flag, problem.name)) - raise WebException("Incorrect.") + else: + logger.log(__name__, logger.WARNING, "%s has incorrectly submitted %s to %s" % (team.name, flag, problem.name)) + raise WebException("Incorrect.") - else: - raise WebException("Problem does not exist!") + else: + raise WebException("Problem does not exist!") @blueprint.route("/data", methods=["POST"]) #@api_wrapper # Disable atm due to json serialization issues: will fix @login_required def problem_data(): - problems = Problems.query.add_columns("pid", "name", "category", "description", "hint", "value", "solves").order_by(Problems.value).filter_by(disabled=False).all() - jason = [] + problems = Problems.query.add_columns("pid", "name", "category", "description", "hint", "value", "solves").order_by(Problems.value).filter_by(disabled=False).all() + jason = [] - for problem in problems: - problem_files = [ str(_file.location) for _file in Files.query.filter_by(pid=int(problem.pid)).all() ] - jason.append({"pid": problem[1], "name": problem[2] ,"category": problem[3], "description": problem[4], "hint": problem[5], "value": problem[6], "solves": problem[7], "files": problem_files}) + for problem in problems: + problem_files = [ str(_file.location) for _file in Files.query.filter_by(pid=int(problem.pid)).all() ] + jason.append({"pid": problem[1], "name": problem[2] ,"category": problem[3], "description": problem[4], "hint": problem[5], "value": problem[6], "solves": problem[7], "files": problem_files}) - return jsonify(data=jason) + return jsonify(data=jason) -def insert_problem(data): - print data - pass \ No newline at end of file +def insert_problem(data, force=False): + with app.app_context(): + if len(list(get_problem(pid=data["pid"]).all())) > 0: + if force == True: + _problem = Problems.query.filter_by(pid=data["pid"]).first() + db.session.delete(_problem) + db.session.commit() + else: + raise InternalException("Problem already exists.") + + insert = Problems(data["pid"], data["title"], data["category"], data["description"], data["value"]) + if "hint" in data: insert.hint = data["hint"] + if "autogen" in data: insert.autogen = data["autogen"] + if "bonus" in data: insert.bonus = data["bonus"] + if "threshold" in data: insert.threshold = data["threshold"] + if "weightmap" in data: insert.weightmap = data["weightmap"] + db.session.add(insert) + db.session.commit() + + return True + +def get_problem(title=None, pid=None): + match = {} + if title != None: + match.update({ "title": title }) + elif pid != None: + match.update({ "pid": pid }) + with app.app_context(): + result = Problems.query.filter_by(**match) + return result \ No newline at end of file diff --git a/server/api/team.py b/server/api/team.py index c756b41..cbde6be 100644 --- a/server/api/team.py +++ b/server/api/team.py @@ -35,52 +35,53 @@ def team_create(): db.session.commit() Users.query.filter_by(uid=_user.uid).update({ "tid": team.tid }) db.session.commit() + db.session.close() - session["tid"] = team.tid + session["tid"] = team.tid return { "success": 1, "message": "Success!" } @blueprint.route("/delete", methods=["POST"]) @api_wrapper @login_required def team_delete(): - username = session["username"] - tid = session["tid"] - team = Teams.query.filter_by(tid=tid).first() - usr = Users.query.filter_by(username=username).first() - owner = team.owner - if usr.uid == owner or usr.admin: - for member in Users.query.filter_by(tid=tid).all(): - member.tid = -1 - with app.app_context(): - db.session.add(member) - - with app.app_context(): - db.session.delete(team) - db.session.commit() - session.pop("tid") - return { "success": 1, "message": "Success!" } - else: - raise WebException("Not authorized.") + username = session["username"] + tid = session["tid"] + team = Teams.query.filter_by(tid=tid).first() + usr = Users.query.filter_by(username=username).first() + owner = team.owner + if usr.uid == owner or usr.admin: + with app.app_context(): + for member in Users.query.filter_by(tid=tid).all(): + member.tid = -1 + db.session.add(member) + db.session.delete(team) + db.session.commit() + db.session.close() + session.pop("tid") + return { "success": 1, "message": "Success!" } + else: + raise WebException("Not authorized.") @blueprint.route("/remove_member", methods=["POST"]) @api_wrapper @login_required def team_remove_member(): - username = session["username"] - tid = session["tid"] - team = Teams.query.filter_by(tid=tid).first() - usr = Users.query.filter_by(username=username).first() - owner = team.owner - if usr.uid == owner or usr.admin: - params = utils.flat_multi(request.form) - user_to_remove = Users.query.filter_by(username=params.get("user")) - user_to_remove.tid = -1 - with app.app_context(): - db.session.add(user_to_remove) - db.session.commit() - return { "success": 1, "message": "Success!" } - else: - raise WebException("Not authorized.") + username = session["username"] + tid = session["tid"] + team = Teams.query.filter_by(tid=tid).first() + usr = Users.query.filter_by(username=username).first() + owner = team.owner + if usr.uid == owner or usr.admin: + params = utils.flat_multi(request.form) + user_to_remove = Users.query.filter_by(username=params.get("user")) + user_to_remove.tid = -1 + with app.app_context(): + db.session.add(user_to_remove) + db.session.commit() + db.session.close() + return { "success": 1, "message": "Success!" } + else: + raise WebException("Not authorized.") @blueprint.route("/invite", methods=["POST"]) @api_wrapper @@ -110,6 +111,7 @@ def team_invite(): with app.app_context(): db.session.add(req) db.session.commit() + db.session.close() return { "success": 1, "message": "Success!" } @@ -135,6 +137,7 @@ def team_invite_rescind(): with app.app_context(): db.session.delete(invitation) db.session.commit() + db.session.close() return { "success": 1, "message": "Success!" } @@ -159,6 +162,7 @@ def team_invite_request(): with app.app_context(): db.session.add(req) db.session.commit() + db.session.close() return { "success": 1, "message": "Success!" } @@ -190,6 +194,7 @@ def team_accept_invite(): if invitation2 is not None: db.session.delete(invitation2) db.session.commit() + db.session.close() return { "success": 1, "message": "Success!" } @@ -225,6 +230,7 @@ def team_accept_invite_request(): if invitation2 is not None: db.session.delete(invitation2) db.session.commit() + db.session.close() return { "success": 1, "message": "Success!" } @@ -280,7 +286,7 @@ TeamSchema = Schema({ ([__check_teamname], "This teamname is taken, did you forget your password?") ), Required("school"): check( - ([str, Length(min=4, max=60)], "Your school name should be between 4 and 60 characters long."), + ([str, Length(min=4, max=40)], "Your school name should be between 4 and 40 characters long."), ([utils.__check_ascii], "Please only use ASCII characters in your school name."), ), }, extra=True) diff --git a/server/api/tools.py b/server/api/tools.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/api/user.py b/server/api/user.py index 87fe1f4..ce3e212 100644 --- a/server/api/user.py +++ b/server/api/user.py @@ -8,7 +8,7 @@ from decorators import api_wrapper, WebException from schemas import verify_to_schema, check import datetime -import logger +import logger, logging import os import re import requests @@ -94,7 +94,7 @@ def user_register(): db.session.commit() utils.generate_identicon(email, user.uid) - logger.log(__name__, logger.INFO, "%s registered with %s" % (name.encode("utf-8"), email.encode("utf-8"))) + logger.log(__name__, "%s registered with %s" % (name.encode("utf-8"), email.encode("utf-8"))) login_user(username, password) return { "success": 1, "message": "Success!" } diff --git a/server/app.py b/server/app.py index 62d8f7e..f6378d3 100644 --- a/server/app.py +++ b/server/app.py @@ -10,6 +10,7 @@ import config import json import logging import os +import traceback from api.decorators import api_wrapper @@ -23,6 +24,8 @@ with app.app_context(): db.init_app(app) db.create_all() + app.db = db + app.secret_key = config.SECRET_KEY app.register_blueprint(api.admin.blueprint, url_prefix="/api/admin") @@ -39,27 +42,29 @@ def api_main(): def run(args): with app.app_context(): - app.debug = keyword_args["debug"] + keyword_args = dict(args._get_kwargs()) + app.debug = keyword_args["debug"] if "debug" in keyword_args else False app.run(host="0.0.0.0", port=8000) def load_problems(args): + keyword_args = dict(args._get_kwargs()) + force = keyword_args["force"] if "force" in keyword_args else False + if not os.path.exists(config.PROBLEM_DIR): - logging.critical("Problems directory doesn't exist.") + api.logger.log("api.problem.log", "Problems directory doesn't exist.") return for (dirpath, dirnames, filenames) in os.walk(config.PROBLEM_DIR): if "problem.json" in filenames: json_file = os.path.join(dirpath, "problem.json") contents = open(json_file).read() - try: data = json.loads(contents) except ValueError as e: - logging.warning("Invalid JSON format in file {filename} ({exception})".format(filename=json_file, exception=e)) + api.logger.log("api.problem.log", "Invalid JSON format in file {filename} ({exception})".format(filename=json_file, exception=e)) continue - if not isinstance(data, dict): - logging.warning("{filename} is not a dict.".format(filename=json_file)) + api.logger.log("api.problem.log", "{filename} is not a dict.".format(filename=json_file)) continue missing_keys = [] @@ -67,24 +72,29 @@ def load_problems(args): if key not in data: missing_keys.append(key) if len(missing_keys) > 0: - logging.warning("{filename} is missing the following keys: {keys}".format(filename=json_file, keys=", ".join(missing_keys))) + api.logger.log("api.problem.log", "{filename} is missing the following keys: {keys}".format(filename=json_file, keys=", ".join(missing_keys))) continue relative_path = os.path.relpath(dirpath, config.PROBLEM_DIR) - logging.info("Found problem '{}'".format(data["title"])) + data["description"] = open(os.path.join(dirpath, "description.md"), "r").read() + api.logger.log("api.problem.log", "Found problem '{}'".format(data["title"])) + with app.app_context(): + try: + api.problem.insert_problem(data, force=force) + except Exception as e: + api.logger.log("api.problem.log", "Problem '{}' was not added to the database. Error: {}".format(data["title"], e)) + api.logger.log("api.problem.log", "{}".format(traceback.format_exc())) - try: - api.problem.insert_problem(data) - except Exception as e: - logging.warning("Problem '{}' was not added to the database. Error: {}".format(data["title"], e)) + api.logger.log("api.problem.log", "Finished.") -if __name__ == "__main__": +def main(): parser = ArgumentParser(description="EasyCTF Server Management") subparser = parser.add_subparsers(help="Select one of the following actions.") parser_problems = subparser.add_parser("problems", help="Manage problems.") subparser_problems = parser_problems.add_subparsers(help="Select one of the following actions.") parser_problems_load = subparser_problems.add_parser("load", help="Load all problems into database.") + parser_problems_load.add_argument("-f", "--force", action="store_true", help="Force overwrite problems.", default=False) parser_problems_load.set_defaults(func=load_problems) parser_run = subparser.add_parser("run", help="Run the server.") @@ -92,10 +102,10 @@ if __name__ == "__main__": parser_run.set_defaults(func=run) args = parser.parse_args() - keyword_args, _ = dict(args._get_kwargs()), args._get_args() - logging.getLogger().setLevel(logging.INFO) if "func" in args: args.func(args) else: - parser.print_help() \ No newline at end of file + parser.print_help() + +main() \ No newline at end of file diff --git a/web/index.html b/web/index.html index 04adfc5..1a8a6f0 100644 --- a/web/index.html +++ b/web/index.html @@ -27,6 +27,7 @@ +