diff --git a/.gitignore b/.gitignore index 7cb6cf3..d6ee5a8 100644 --- a/.gitignore +++ b/.gitignore @@ -102,5 +102,6 @@ tags dev.db dump.rdb /problems +/problem_static /secrets /database diff --git a/app.py b/app.py index 0cf5da1..6d46414 100644 --- a/app.py +++ b/app.py @@ -12,7 +12,7 @@ from data.database import db import data # Blueprints -from routes import api, admin, teams, users, challenges, tickets, scoreboard +from routes import api, admin, teams, users, challenges, tickets, scoreboard, shell if config.production: logging.basicConfig(level=logging.INFO) @@ -30,6 +30,7 @@ app.register_blueprint(users.users) app.register_blueprint(challenges.challenges) app.register_blueprint(tickets.tickets) app.register_blueprint(scoreboard.scoreboard) +app.register_blueprint(shell.shell) @app.before_request @@ -68,7 +69,7 @@ def scoreboard_variables(): @app.route('/') def root(): if g.logged_in: - return redirect(url_for('team.dashboard')) + return redirect(url_for('teams.dashboard')) return redirect(url_for('users.register')) diff --git a/config.py b/config.py index 94d1ded..d012fb0 100644 --- a/config.py +++ b/config.py @@ -7,7 +7,7 @@ BASE_DIR = os.path.dirname(os.path.abspath(__file__)) ctf_name = "IceCTF" eligibility = "In order to be eligible for prizes, all members of your team must be Icelandic residents, and you must not have more than three team members." tagline = "The Icelandic Hacking Competition" -#IRC Channel +# IRC Channel ctf_chat_channel = "#IceCTF" ctf_home_url = "https://icec.tf" @@ -32,8 +32,8 @@ immediate_scoreboard = False disallowed_domain = "icec.tf" # Where the static stuff is stored -static_prefix = "http://127.0.0.1/static/" -static_dir = "{}/static/".format(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +static_prefix = "/problem-static/" +static_dir = "{}/problem_static/".format(os.path.dirname(os.path.abspath(__file__))) custom_stylesheet = "css/main.css" # Shell accounts? @@ -41,14 +41,16 @@ custom_stylesheet = "css/main.css" enable_shell = True shell_port = 22 +shell_host = "shell.icec.tf" -shell_user_prefixes = ["ctf-"] -shell_password_length = 6 +shell_user_prefixes = ["ctf-"] +shell_password_length = 8 shell_free_acounts = 10 shell_max_accounts = 99999 shell_user_creation = "sudo useradd -m {username} -p {password} -g ctf -b /home_users" + # when the competition begins competition_begin = datetime(1970, 1, 1, 0, 0) competition_end = datetime(2018, 1, 1, 0, 0) diff --git a/ctftool b/ctftool index 3b1d9a7..8d6b046 100755 --- a/ctftool +++ b/ctftool @@ -17,7 +17,7 @@ import yaml import argparse import logging -tables = [Team, User, UserAccess, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser, SshAccount] +tables = [Team, User, UserAccess, Stage, Challenge, ChallengeSolve, ChallengeFailure, NewsItem, TroubleTicket, TicketComment, Notification, ScoreAdjustment, AdminUser, SshAccount] def create_tables(args): check = True @@ -66,58 +66,73 @@ def add_admin(args): secret = "".join([r.choice("ABCDEFGHIJKLMNOPQRSTUVWXYZ234567") for i in range(16)]) AdminUser.create(username=username, password=pwhash, secret=secret) print("AdminUser created; Enter the following key into your favorite TOTP application (Google Authenticator Recommended): {}".format(secret)) +def scan_challenges_problem(d, files): + staticpaths = {} + if "static.yml" in files: + with open(os.path.join(d, "static.yml")) as f: + statics = yaml.load(f) + for static in statics: + h = hashlib.sha256() + with open(os.path.join(d, static), "rb") as staticfile: + while True: + buf = staticfile.read(4096) + h.update(buf) + if not buf: + break + + if "." in static: + name, ext = static.split(".", maxsplit=1) + fn = "{}_{}.{}".format(name, h.hexdigest(), ext) + else: + fn = "{}_{}".format(static, h.hexdigest()) + staticpaths[static] = fn + shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn)) + print(fn) + + with open(os.path.join(d, "problem.yml")) as f: + data = yaml.load(f) + print("Inserting problem in directory %s" % (d)) + for i in staticpaths: + print("looking for |{}|".format(i)) + data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i])) + + data["stage"] = Stage.get(Stage.alias == data["stage"]) + query = Challenge.select().where(Challenge.alias == data["alias"]) + if query.exists(): + print("Updating " + str(data["name"]) + "...") + q = Challenge.update(**data).where(Challenge.alias == data["alias"]) + q.execute() + else: + Challenge.create(**data) + + +def scan_challenges_stage(d, files): + + with open(os.path.join(d, "stage.yml")) as f: + data = yaml.load(f) + query = Stage.select().where(Stage.alias == data["alias"]) + if query.exists(): + print("Updating %s..." % (data["name"])) + q = Challenge.update(**data).where(Stage.alias == data["alias"]) + else: + Stage.create(**data) + def scan_challenges(args): path = args.path - dirs = [j for j in [os.path.join(path, i) for i in os.listdir(path)] if os.path.isdir(j)] - # recurse to 2 levels - # TODO: do this better - dirs = [j for j in [os.path.join(k, i) for k in dirs for i in os.listdir(k)] if os.path.isdir(j)] - print(dirs) n = 0 - for d in dirs: - staticpaths = {} - if os.path.exists(os.path.join(d, "static.yml")): - with open(os.path.join(d, "static.yml")) as f: - statics = yaml.load(f) - for static in statics: - h = hashlib.sha256() - with open(os.path.join(d, static), "rb") as staticfile: - while True: - buf = staticfile.read(4096) - h.update(buf) - if not buf: - break - - if "." in static: - name, ext = static.split(".", maxsplit=1) - fn = "{}_{}.{}".format(name, h.hexdigest(), ext) - else: - fn = "{}_{}".format(static, h.hexdigest()) - staticpaths[static] = fn - shutil.copy(os.path.join(d, static), os.path.join(config.static_dir, fn)) - print(fn) - - if os.path.exists(os.path.join(d, "problem.yml")): - with open(os.path.join(d, "problem.yml")) as f: - n += 1 - data = yaml.load(f) - print("Inserting problem in directory %s" % (d)) - for i in staticpaths: - print("looking for |{}|".format(i)) - data["description"] = data["description"].replace("|{}|".format(i), "{}{}".format(config.static_prefix, staticpaths[i])) - - query = Challenge.select().where(Challenge.name == data["name"]) - if query.exists(): - print("Updating " + str(data["name"]) + "...") - q = Challenge.update(**data).where(Challenge.name == data["name"]) - q.execute() - else: - Challenge.create(**data) + os.makedirs(config.static_dir, exist_ok=True) + for root, dirs, files in os.walk(path): + if "problem.yml" in files: + n += 1 + scan_challenges_problem(root, files) + if "stage.yml" in files: + scan_challenges_stage(root, files) print(n, "challenges loaded") + def add_challenge(args): challengefile = args.file with open(challengefile) as f: @@ -152,7 +167,7 @@ def clear_challenges(args): def recache_solves(args): - r = redis.StrictRedis() + r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) for chal in Challenge.select(): r.hset("solves", chal.id, chal.solves.count()) print(r.hvals("solves")) diff --git a/daemons/cache_score.py b/daemons/cache_score.py index 74764f5..7c6fed9 100644 --- a/daemons/cache_score.py +++ b/daemons/cache_score.py @@ -1,15 +1,18 @@ import redis -import data.scoreboard +from data import scoreboard import json - -r = redis.StrictRedis() - -def set_complex(key, val): - r.set(key, json.dumps(val)) +import config +from data.database import db def run(): - data = data.scoreboard.calculate_scores() - graphdata = utils.scoreboard.calculate_graph(data) + r = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) + db.connect() + + def set_complex(key, val): + r.set(key, json.dumps(val)) + data = scoreboard.calculate_scores() + graphdata = scoreboard.calculate_graph(data) set_complex("scoreboard", data) set_complex("graph", graphdata) + db.close() diff --git a/daemons/shell_accounts.py b/daemons/shell_accounts.py index 0b4f33d..bd3fedc 100644 --- a/daemons/shell_accounts.py +++ b/daemons/shell_accounts.py @@ -1,20 +1,19 @@ -import api import spur -import pwd import random import config from data.database import db from data import ssh +import utils.misc + def run(): - db.connect() try: shell = spur.SshShell( - hostname=config.secrets.shell_host, - username=config.secrets.shell_username, - password=config.secrets.shell_password, + hostname=config.shell_host, + username=config.secret.shell_username, + private_key_file=config.secret.shell_privkey, port=config.shell_port, missing_host_key=spur.ssh.MissingHostKey.accept ) @@ -43,10 +42,10 @@ def run(): accounts = [] while new_accounts > 0: - username = random.choice(api.config.shell_user_prefixes) + \ - str(random.randint(0, api.config.shell_max_accounts)) + username = random.choice(config.shell_user_prefixes) + \ + str(random.randint(0, config.shell_max_accounts)) - plaintext_password = api.common.token()[:api.config.shell_password_length] + plaintext_password = utils.misc.generate_random_string(config.shell_password_length, chars="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ123456789") hashed_password = shell.run(["bash", "-c", "echo '{}' | openssl passwd -1 -stdin".format(plaintext_password)]) hashed_password = hashed_password.output.decode("utf-8").strip() @@ -75,7 +74,7 @@ def run(): new_accounts -= 1 if len(accounts) > 0: - ssh.insert_accounts(accounts) + ssh.create_accounts(accounts) print("Successfully imported accounts.") for team in teams: diff --git a/data/challenge.py b/data/challenge.py index 067c4c4..3f889bd 100644 --- a/data/challenge.py +++ b/data/challenge.py @@ -1,17 +1,58 @@ -from data.database import Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, Team +from data.database import Stage, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, Team from datetime import datetime from exceptions import ValidationError from flask import g import config +def get_stages(): + return list(Stage.select().order_by(Stage.name)) + + +def get_stage_challenges(stage_id): + print(stage_id) + return list(Challenge.select(Challenge.alias).where(Challenge.stage == stage_id)) + + +def get_categories(): + return [q.category for q in Challenge.select(Challenge.category).distinct().order_by(Challenge.category)] + + def get_challenges(): - return Challenge.select().order_by(Challenge.points, Challenge.name) + challenges = Challenge.select().where(Challenge.enabled == True).order_by(Challenge.stage, Challenge.points, Challenge.name) + d = dict() + for chall in challenges: + if chall.stage_id in d: + d[chall.stage_id].append(chall) + else: + d[chall.stage_id] = [chall] + return d -def get_challenge(id): +def get_solve_counts(): + # TODO: optimize + d = dict() + for k in Challenge.select(Challenge.id): + d[k.id] = get_solve_count(k.id) + return d + + +def get_solve_count(chall_id): + s = g.redis.hget("solves", chall_id) + if s is not None: + return int(s.decode()) + else: + return -1 + + +def get_challenge(id=None, alias=None): try: - return Challenge.get(Challenge.id == id) + if id is not None: + return Challenge.get(Challenge.id == id, Challenge.enabled == True) + elif alias is not None: + return Challenge.get(Challenge.alias == alias, Challenge.enabled == True) + else: + raise ValueError("Invalid argument") except Challenge.DoesNotExist: raise ValidationError("Challenge does not exist!") @@ -20,6 +61,10 @@ def get_solved(team): return Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team) +def get_solves(team): + return ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).where(ChallengeSolve.team == g.team) + + def get_adjustments(team): return ScoreAdjustment.select().where(ScoreAdjustment.team == team) @@ -35,7 +80,7 @@ def submit_flag(chall, user, team, flag): raise ValidationError("This challenge is disabled.") elif flag.strip().lower() != chall.flag.strip().lower(): ChallengeFailure.create(user=user, team=team, challenge=chall, attempt=flag, time=datetime.now()) - return "Incorrect flag" + raise ValidationError("Incorrect flag") else: ChallengeSolve.create(user=user, team=team, challenge=chall, time=datetime.now()) g.redis.hincrby("solves", chall.id, 1) diff --git a/data/database.py b/data/database.py index f9325e7..a3f5d79 100644 --- a/data/database.py +++ b/data/database.py @@ -1,6 +1,9 @@ -from peewee import * +from peewee import PostgresqlDatabase, SqliteDatabase +from peewee import Model, CharField, TextField, IntegerField, BooleanField, DateTimeField, ForeignKeyField, CompositeKey + import bcrypt import config + if config.production: db = PostgresqlDatabase(config.database.database, user=config.database.user, password=config.database.password) else: @@ -11,16 +14,20 @@ class BaseModel(Model): class Meta: database = db + class Team(BaseModel): name = CharField(unique=True) - affiliation = CharField(null = True) + affiliation = CharField(null=True) restricts = TextField(default="") key = CharField(unique=True, index=True) + eligibility = BooleanField(null=True) def solved(self, challenge): return ChallengeSolve.select().where(ChallengeSolve.team == self, ChallengeSolve.challenge == challenge).count() def eligible(self): + if self.eligibility is not None: + return self.eligibility return all([member.eligible() for member in self.members]) and self.members.count() <= 3 @property @@ -29,22 +36,23 @@ class Team(BaseModel): adjust_points = sum([i.value for i in self.adjustments]) return challenge_points + adjust_points + class User(BaseModel): username = CharField(unique=True, index=True) email = CharField(index=True) email_confirmed = BooleanField(default=False) email_confirmation_key = CharField() - password = CharField(null = True) + password = CharField(null=True) background = CharField() country = CharField() - tshirt_size = CharField(null = True) - gender = CharField(null = True) + tshirt_size = CharField(null=True) + gender = CharField(null=True) first_login = BooleanField(default=True) restricts = TextField(default="") team = ForeignKeyField(Team, related_name="members") banned = BooleanField(default=False) - password_reset_token = CharField(null = True) - password_reset_expired = DateTimeField(null = True) + password_reset_token = CharField(null=True) + password_reset_expired = DateTimeField(null=True) def set_password(self, pw): self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt()) @@ -54,15 +62,24 @@ class User(BaseModel): return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8")) def eligible(self): - return self.country == "ISL" + return self.country == "ISL" and not self.banned + class UserAccess(BaseModel): user = ForeignKeyField(User, related_name='accesses') ip = CharField() time = DateTimeField() + +class Stage(BaseModel): + name = CharField() + alias = CharField(unique=True, index=True) + description = CharField(null=True) + + class Challenge(BaseModel): name = CharField() + alias = CharField(unique=True, index=True) category = CharField() author = CharField() description = TextField() @@ -70,6 +87,8 @@ class Challenge(BaseModel): breakthrough_bonus = IntegerField(default=0) enabled = BooleanField(default=True) flag = TextField() + stage = ForeignKeyField(Stage, related_name='challenges') + class ChallengeSolve(BaseModel): user = ForeignKeyField(User, related_name='solves') @@ -80,6 +99,7 @@ class ChallengeSolve(BaseModel): class Meta: primary_key = CompositeKey('team', 'challenge') + class ChallengeFailure(BaseModel): user = ForeignKeyField(User, related_name='failures') team = ForeignKeyField(Team, related_name='failures') @@ -87,10 +107,12 @@ class ChallengeFailure(BaseModel): attempt = CharField() time = DateTimeField() + class NewsItem(BaseModel): summary = CharField() description = TextField() + class TroubleTicket(BaseModel): team = ForeignKeyField(Team, related_name='tickets') summary = CharField() @@ -98,26 +120,31 @@ class TroubleTicket(BaseModel): active = BooleanField(default=True) opened_at = DateTimeField() + class TicketComment(BaseModel): ticket = ForeignKeyField(TroubleTicket, related_name='comments') comment_by = CharField() comment = TextField() time = DateTimeField() + class Notification(BaseModel): team = ForeignKeyField(Team, related_name='notifications') notification = TextField() + class ScoreAdjustment(BaseModel): team = ForeignKeyField(Team, related_name='adjustments') value = IntegerField() reason = TextField() + class AdminUser(BaseModel): username = CharField() password = CharField() secret = CharField() + class SshAccount(BaseModel): team = ForeignKeyField(Team, null=True, related_name='ssh_account') username = CharField() diff --git a/data/ssh.py b/data/ssh.py index 862d498..06e2772 100644 --- a/data/ssh.py +++ b/data/ssh.py @@ -20,3 +20,9 @@ def assign_shell_account(team): acct = SshAccount.select().order_by(fn.Random()).get() acct.team = team acct.save() + +def get_team_account(team): + try: + return team.ssh_account.get() + except SshAccount.DoesNotExist: + return None diff --git a/data/team.py b/data/team.py index 8824252..e4c9e08 100644 --- a/data/team.py +++ b/data/team.py @@ -33,7 +33,7 @@ def create_team(name, affiliation): team_key = misc.generate_team_key() team = Team.create(name=name, affiliation=affiliation, key=team_key) - return True, team + return team def update_team(current_team, name, affiliation): diff --git a/data/ticket.py b/data/ticket.py index 616d29a..3978193 100644 --- a/data/ticket.py +++ b/data/ticket.py @@ -8,7 +8,7 @@ def get_tickets(team): def get_ticket(team, id): try: - return TroubleTicket.get(TroubleTicket.id == ticket and TroubleTicket.team == team) + return TroubleTicket.get(TroubleTicket.id == id, TroubleTicket.team == team) except TroubleTicket.DoesNotExist: raise ValidationError("Ticket not found!") @@ -16,10 +16,10 @@ def get_comments(ticket): return TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time) def create_ticket(team, summary, description): - return TroubleTicket.create(team=g.team, summary=summary, description=description, opened_at=datetime.now()) + return TroubleTicket.create(team=team, summary=summary, description=description, opened_at=datetime.now()) def create_comment(ticket, user, comment): - TicketComment.create(ticket=ticket, comment_by=user.username, comment=request.form["comment"], time=datetime.now()) + TicketComment.create(ticket=ticket, comment_by=user.username, comment=comment, time=datetime.now()) def open_ticket(ticket): ticket.active = True diff --git a/data/user.py b/data/user.py index 2bf1e7a..e266607 100644 --- a/data/user.py +++ b/data/user.py @@ -82,7 +82,7 @@ def create_user(username, email, password, background, country, team, tshirt_siz def confirm_email(current_user, confirmation_key): if current_user.email_confirmed: raise ValidationError("Email already confirmed") - if current_user.confirmation_key == confirmation_key: + if current_user.email_confirmation_key == confirmation_key: current_user.email_confirmed = True current_user.save() else: diff --git a/requirements.txt b/requirements.txt index 7f0d385..a29e3be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ pyyaml oath pycountry psycopg2 +spur diff --git a/routes/admin.py b/routes/admin.py index 9b7771d..485c2d8 100644 --- a/routes/admin.py +++ b/routes/admin.py @@ -53,7 +53,7 @@ def admin_dashboard(): adjustments = ScoreAdjustment.select() scoredata = scoreboard.get_all_scores(teams, solves, adjustments) lastsolvedata = scoreboard.get_last_solves(teams, solves) - tickets = list(TroubleTicket.select().where(TroubleTicket.active==True)) + tickets = list(TroubleTicket.select().where(TroubleTicket.active == True)) return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets) @@ -78,19 +78,19 @@ def admin_ticket_comment(ticket): ticket = TroubleTicket.get(TroubleTicket.id == ticket) if request.form["comment"]: TicketComment.create(ticket=ticket, comment_by=session["admin"], comment=request.form["comment"], time=datetime.now()) - Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + Notification.create(team=ticket.team, notification="A response has been added for {}.".format(make_link("ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) flash("Comment added.") if ticket.active and "resolved" in request.form: ticket.active = False ticket.save() - Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + Notification.create(team=ticket.team, notification="{} has been marked resolved.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) flash("Ticket closed.") elif not ticket.active and "resolved" not in request.form: ticket.active = True ticket.save() - Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("team_ticket_detail", ticket=ticket.id)))) + Notification.create(team=ticket.team, notification="{} has been reopened.".format(make_link("Ticket #{}".format(ticket.id), url_for("tickets.detail", ticket_id=ticket.id)))) flash("Ticket reopened.") return redirect(url_for(".admin_ticket_detail", ticket=ticket.id)) @@ -116,23 +116,15 @@ def admin_impersonate_team(tid): @admin_required def admin_toggle_eligibility(tid): team = Team.get(Team.id == tid) - team.eligible = not team.eligible + if team.eligibility is None: + team.eligibility = False + else: + team.eligibility = not team.eligibility team.save() flash("Eligibility set to {}".format(team.eligible)) return redirect(url_for(".admin_show_team", tid=tid)) -@admin.route("/team///toggle_eligibility_lock/") -@csrf_check -@admin_required -def admin_toggle_eligibility_lock(tid): - team = Team.get(Team.id == tid) - team.eligibility_locked = not team.eligibility_locked - team.save() - flash("Eligibility lock set to {}".format(team.eligibility_locked)) - return redirect(url_for(".admin_show_team", tid=tid)) - - @admin.route("/team//adjust_score/", methods=["POST"]) @admin_required def admin_score_adjust(tid): diff --git a/routes/api.py b/routes/api.py index 3496079..d3c9ad3 100644 --- a/routes/api.py +++ b/routes/api.py @@ -8,7 +8,7 @@ import config api = Blueprint("api", __name__, url_prefix="/api") -@api.route("/submit/.json", methods=["POST"]) +@api.route("/submit/.json", methods=["POST"]) @decorators.must_be_allowed_to("solve challenges") @decorators.must_be_allowed_to("view challenges") @decorators.competition_running_required @@ -16,14 +16,15 @@ api = Blueprint("api", __name__, url_prefix="/api") @ratelimit.ratelimit(limit=10, per=120, over_limit=ratelimit.on_over_api_limit) def submit_api(challenge_id): try: - chall = challenge.get_challenge(challenge_id) + chall = challenge.get_challenge(alias=challenge_id) except exceptions.ValidationError as e: return jsonify(dict(code=1001, message=str(e))) flag = request.form["flag"] try: challenge.submit_flag(chall, g.user, g.team, flag) - return jsonify(dict(code=0, message="Success!")) + solves = challenge.get_solve_count(chall.id) + return jsonify(dict(code=0, message="Success!", solves=solves)) except exceptions.ValidationError as e: return jsonify(dict(code=1001, message=str(e))) diff --git a/routes/challenges.py b/routes/challenges.py index b883fdb..1a76bd9 100644 --- a/routes/challenges.py +++ b/routes/challenges.py @@ -13,21 +13,23 @@ challenges = Blueprint("challenges", __name__, template_folder="../templates/cha @decorators.competition_running_required @decorators.confirmed_email_required def index(): - chals = challenge.get_challenges() + stages = challenge.get_stages() + challs = challenge.get_challenges() solved = challenge.get_solved(g.team) - solves = {i: int(g.redis.hget("solves", i).decode()) for i in [k.id for k in chals]} - categories = sorted(list({chal.category for chal in chals})) - return render_template("challenges.html", challenges=chals, solved=solved, categories=categories, solves=solves) + solves = challenge.get_solve_counts() + categories = challenge.get_categories() + first_stage = {chall.alias: True for chall in challs[stages[0].id]} if stages else None + return render_template("challenges.html", stages=stages, first_stage=first_stage, challenges=challs, solved=solved, categories=categories, solves=solves) -@challenges.route('/challenges//solves/') +@challenges.route('/challenges//solves/') @decorators.must_be_allowed_to("view challenge solves") @decorators.must_be_allowed_to("view challenges") @decorators.competition_running_required @decorators.confirmed_email_required def show_solves(challenge_id): try: - chall = challenge.get_challenge(challenge_id) + chall = challenge.get_challenge(alias=challenge_id) except exceptions.ValidationError as e: flash(str(e)) return redirect(url_for(".index")) @@ -35,7 +37,7 @@ def show_solves(challenge_id): return render_template("challenge_solves.html", challenge=chall, solves=solves) -@challenges.route('/submit//', methods=["POST"]) +@challenges.route('/submit//', methods=["POST"]) @decorators.must_be_allowed_to("solve challenges") @decorators.must_be_allowed_to("view challenges") @decorators.competition_running_required diff --git a/routes/scoreboard.py b/routes/scoreboard.py index 87c1178..a9262bb 100644 --- a/routes/scoreboard.py +++ b/routes/scoreboard.py @@ -19,6 +19,6 @@ def index(): data.scoreboard.set_complex("scoreboard", data, 120) data.scoreboard.set_complex("graph", graphdata, 120) else: - return "No scoreboard data available. Please contact an organizer." + return "CTF hasn't started!" return render_template("scoreboard.html", data=scoreboard_data, graphdata=graphdata) diff --git a/routes/shell.py b/routes/shell.py new file mode 100644 index 0000000..2809e7a --- /dev/null +++ b/routes/shell.py @@ -0,0 +1,16 @@ +from flask import Blueprint, g, render_template + +from data import ssh +from utils import decorators + + +shell = Blueprint("shell", __name__, template_folder="../templates/shell") + + +@shell.route('/shell/') +@decorators.must_be_allowed_to("access shell") +@decorators.competition_running_required +@decorators.confirmed_email_required +def index(): + account = ssh.get_team_account(g.team) + return render_template("shell.html", account=account) diff --git a/routes/teams.py b/routes/teams.py index a66020c..19aa8bc 100644 --- a/routes/teams.py +++ b/routes/teams.py @@ -17,7 +17,7 @@ teams = Blueprint("teams", __name__, template_folder="../templates/teams") @ratelimit.ratelimit(limit=6, per=120) def dashboard(): if request.method == "GET": - team_solves = challenge.get_solved(g.team) + team_solves = challenge.get_solves(g.team) team_adjustments = challenge.get_adjustments(g.team) team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments]) return render_template("team.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score) diff --git a/routes/tickets.py b/routes/tickets.py index 23f5930..d5d9e96 100644 --- a/routes/tickets.py +++ b/routes/tickets.py @@ -30,44 +30,44 @@ def open_ticket(): description = request.form["description"] t = ticket.create_ticket(g.team, summary, description) flash("Ticket #{} opened.".format(t.id)) - return redirect(url_for(".detail", ticket=t.id)) + return redirect(url_for(".detail", ticket_id=t.id)) -@tickets.route('/tickets//') +@tickets.route('/tickets//') @decorators.must_be_allowed_to("view tickets") @decorators.login_required -def detail(ticket): +def detail(ticket_id): try: - ticket = ticket.get_ticket(g.team, ticket) + t = ticket.get_ticket(g.team, ticket_id) except exceptions.ValidationError as e: flash(str(e)) return redirect(url_for(".index")) - comments = ticket.get_comments(ticket) - return render_template("ticket_detail.html", ticket=ticket, comments=comments) + comments = ticket.get_comments(t) + return render_template("ticket_detail.html", ticket=t, comments=comments) -@tickets.route('/tickets//comment/', methods=["POST"]) +@tickets.route('/tickets//comment/', methods=["POST"]) @decorators.must_be_allowed_to("comment on tickets") @decorators.must_be_allowed_to("view tickets") @ratelimit.ratelimit(limit=1, per=120) -def comment(ticket): +def comment(ticket_id): try: - ticket = ticket.get_ticket(g.team, ticket) + t = ticket.get_ticket(g.team, ticket_id) except exceptions.ValidationError as e: flash(str(e)) return redirect(url_for(".index")) if request.form["comment"]: - ticket.create_comment(ticket, g.user, request.form["comment"]) + ticket.create_comment(t, g.user, request.form["comment"]) flash("Comment added.") - if ticket.active and "resolved" in request.form: - ticket.close_ticket(ticket) + if t.active and "resolved" in request.form: + ticket.close_ticket(t) flash("Ticket closed.") - elif not ticket.active and "resolved" not in request.form: - ticket.open_ticket(ticket) + elif not t.active and "resolved" not in request.form: + ticket.open_ticket(t) flash("Ticket re-opened.") - return redirect(url_for(".detail", ticket=ticket.id)) + return redirect(url_for(".detail", ticket_id=t.id)) diff --git a/routes/users.py b/routes/users.py index 8db0c6b..8c58ef8 100644 --- a/routes/users.py +++ b/routes/users.py @@ -68,7 +68,7 @@ def register(): if join_team: team_key = request.form["team_key"].strip() t = team.get_team(key=team_key) - if not team: + if not t: flash("This team could not be found, check your team key.") return redirect(url_for('.register')) else: diff --git a/static/css/main.css b/static/css/main.css index aeb6cf4..ad32592 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -170,6 +170,25 @@ body { margin-left: -40px; } +code, kbd, pre, samp { + font-family: Menlo,Monaco,Consolas,"Courier New",monospace; +} + +.challenge code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} + +.shell-frame { + border-width: 0px; + width: 100%; + height: calc(100% - 64px); +} + + /* MEDIUM */ @media only screen and (max-width : 992px) { .cont { diff --git a/templates/admin/dashboard.html b/templates/admin/dashboard.html index e0cd1b6..35c8069 100644 --- a/templates/admin/dashboard.html +++ b/templates/admin/dashboard.html @@ -19,7 +19,7 @@ {{ team.name }} {{ team.affiliation }} - {{ "Eligible" if team.eligible else "Ineligible" }} + {{ "Eligible" if team.eligible() else "Ineligible" }} {{ lastsolvedata[team.id] }} {{ scoredata[team.id] }} diff --git a/templates/admin/team.html b/templates/admin/team.html index f75561b..886f6ff 100644 --- a/templates/admin/team.html +++ b/templates/admin/team.html @@ -3,8 +3,7 @@

{{ team.name }}

Impersonate team

-This team is {{ "eligible" if team.eligible else "not eligible" }} (toggle). -Eligibility is {{ "locked" if team.eligibility_locked else "unlocked" }} (toggle). +This team is {{ "eligible" if team.eligible() else "not eligible" }} (toggle).

This team's affiliation is {{ team.affiliation }}

Email

diff --git a/templates/base.html b/templates/base.html index e5f635f..2fed315 100644 --- a/templates/base.html +++ b/templates/base.html @@ -48,7 +48,7 @@