diff --git a/.gitignore b/.gitignore index 856d654..0e81128 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dump.rdb /problems __pycache__ /static/* +/secrets diff --git a/app.py b/app.py index 5b4cd5b..e31fce5 100644 --- a/app.py +++ b/app.py @@ -8,6 +8,7 @@ from peewee import fn import config import utils import redis +import requests import logging logging.basicConfig(level=logging.DEBUG) @@ -37,10 +38,13 @@ def root(): @app.route('/scoreboard/') def scoreboard(): data = utils.get_complex("scoreboard") - if not data: + graphdata = utils.get_complex("graph") + if not data or not graphdata: data = utils.calculate_scores() - utils.set_complex("scoreboard", data, 10) - return render_template("scoreboard.html", data=data) + graphdata = utils.calculate_graph(data) + utils.set_complex("scoreboard", data, 120) + utils.set_complex("graph", graphdata, 120) + return render_template("scoreboard.html", data=data, graphdata=graphdata) @app.route('/login/', methods=["GET", "POST"]) def login(): @@ -64,12 +68,24 @@ def register(): if request.method == "GET": return render_template("register.html") elif request.method == "POST": + if "g-recaptcha-response" not in request.form: + flash("Please complete the CAPTCHA.") + return render_template("register.html") + + captcha_response = request.form["g-recaptcha-response"] + verify_data = dict(secret=config.secret.recaptcha_secret, response=captcha_response, remoteip=utils.get_ip()) + result = requests.post("https://www.google.com/recaptcha/api/siteverify", verify_data).json()["success"] + if not result: + flash("Invalid CAPTCHA response.") + return render_template("register.html") + team_name = request.form["team_name"] team_email = request.form["team_email"] team_elig = "team_eligibility" in request.form affiliation = request.form["affiliation"] team_key = utils.generate_team_key() team = Team.create(name=team_name, email=team_email, eligible=team_elig, affiliation=affiliation, key=team_key) + TeamAccess.create(team=team, ip=utils.get_ip(), time=datetime.now()) session["team_id"] = team.id flash("Team created.") return redirect(url_for('dashboard')) @@ -98,7 +114,12 @@ def dashboard(): team_solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).where(ChallengeSolve.team == g.team) team_adjustments = ScoreAdjustment.select().where(ScoreAdjustment.team == g.team) team_score = sum([i.challenge.points for i in team_solves] + [i.value for i in team_adjustments]) - return render_template("dashboard.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score) + first_login = False + if g.team.first_login: + first_login = True + g.team.first_login = False + g.team.save() + return render_template("dashboard.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score, first_login=first_login) elif request.method == "POST": team_name = request.form["team_name"] team_email = request.form["team_email"] diff --git a/config.py b/config.py index e2ed117..2f1fa87 100644 --- a/config.py +++ b/config.py @@ -1,6 +1,15 @@ ctf_name = "TJCTF" eligibility = "In order to be eligible for prizes, all members of your team must be in high school, and you must not have more than four team members." +tagline = "a cybersecurity competition created by TJHSST students" competition_is_running = True cdn = True proxied_ip_header = "X-Forwarded-For" flag_rl = 10 +teams_on_graph = 10 + +# Don't touch these. Instead, copy secrets.example to secrets and edit that. +import yaml +from collections import namedtuple +with open("secrets") as f: + _secret = yaml.load(f) + secret = namedtuple('SecretsDict', _secret.keys())(**_secret) diff --git a/ctftool b/ctftool index dbb9757..dd444da 100755 --- a/ctftool +++ b/ctftool @@ -1,5 +1,6 @@ #!/usr/bin/python3 from database import * +from datetime import datetime, timedelta import sys import yaml import random @@ -26,7 +27,22 @@ elif operation == "gen-challenge": n = int(sys.argv[2]) for i in range(n): name = str(random.randint(0, 999999999)) - chal = Challenge.create(name="Challenge {}".format(name), category="Generated", description="Free flags. it's the number in the problem name lorem ipsum dolor sit whatever i don't even care", points=random.randint(50, 400), flag=name) + chal = Challenge.create(name="Challenge".format(name), category="Generated", description="Lorem ipsum, dolor sit amet. The flag is {}".format(name), points=random.randint(50, 400), flag=name) print("Challenge added with id {}".format(chal.id)) +elif operation == "gen-team": + n = int(sys.argv[2]) + chals = list(Challenge.select()) + ctz = datetime.now() + diff = timedelta(minutes=5) + for i in range(n): + name = "Team {}".format(i + 1) + t = Team.create(name=name, email="none@none.com", affiliation="Autogenerated", eligible=True, key="") + t.key = "autogen{}".format(t.id) + t.save() + print("Team added with id {}".format(t.id)) + for i in random.sample(chals, 5): + ChallengeSolve.create(team=t, challenge=i, time=ctz) + ctz = ctz + (diff * random.randint(-10, 10)) + # vim: syntax=python:ft=python diff --git a/database.py b/database.py index 9589658..6df0b9e 100644 --- a/database.py +++ b/database.py @@ -10,6 +10,7 @@ class Team(BaseModel): email = CharField() affiliation = CharField() eligible = BooleanField() + first_login = BooleanField(default=True) key = CharField() def solved(self, challenge): diff --git a/templates/base.html b/templates/base.html index 6b2213d..a829d06 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,6 +3,7 @@ {{ config.ctf_name }} :: {% block title %}Home{% endblock %} {% if config.cdn %} + {% else %} @@ -16,8 +17,10 @@
{% endblock %} diff --git a/templates/register.html b/templates/register.html index fd3e5da..1e49271 100644 --- a/templates/register.html +++ b/templates/register.html @@ -1,4 +1,7 @@ {% extends "base.html" %} +{% block head %} + +{% endblock %} {% block content %}

Register a Team

After registering, you will be directed to your team's dashboard. This will @@ -26,8 +29,9 @@ team members.

status before sending you prizes.

-

+
+ {% endblock %} diff --git a/templates/scoreboard.html b/templates/scoreboard.html index 52e49d9..c0ede02 100644 --- a/templates/scoreboard.html +++ b/templates/scoreboard.html @@ -18,10 +18,12 @@ } function hideIneligible() { $(".teamrow.ineligible").removeClass("visible"); + $("#toggle").html("Show ineligible teams"); recalcRanks(); } function showIneligible() { $(".teamrow.ineligible").addClass("visible"); + $("#toggle").html("Hide ineligible teams"); recalcRanks(); } function toggleIneligible() { @@ -36,15 +38,35 @@ {% endblock %} {% block content %}

Scoreboard

-Toggle ineligible team display +
+Hide ineligible teams {% for eligible, teamid, team, affiliation, score in data %} - + {% endfor %}
RankTeamAffiliationScore
{{ rank }}{{ team }}{{ affiliation }}{{ score }}
{{ rank }}{{ team }}{{ affiliation }}{{ score }}
{% endblock %} +{% block postscript %} + +{% endblock %} diff --git a/utils.py b/utils.py index 6cdbba0..4da9ab3 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,7 @@ import random import config import json +from datetime import datetime from functools import wraps from flask import request, session, redirect, url_for, flash, g from database import Team, Challenge, ChallengeSolve, ScoreAdjustment @@ -37,17 +38,38 @@ def competition_running_required(f): def calculate_scores(): solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) adjustments = ScoreAdjustment.select() - teams = Team.select(Team.id, Team, ChallengeSolve).join(ChallengeSolve) + teams = Team.select() + team_solves = {team.id: [] for team in teams} team_mapping = {team.id: team for team in teams} scores = {team.id: 0 for team in teams} - most_recent_solve = {team.id: max([i.time for i in team.solves]) for team in teams} for solve in solves: scores[solve.team_id] += solve.challenge.points + team_solves[solve.team_id].append(solve) for adjustment in adjustments: scores[adjustment.team_id] += adjustment.value + + most_recent_solve = {tid: max([i.time for i in team_solves[tid]]) for tid in team_solves if team_solves[tid]} + scores = {i: j for i, j in scores.items() if i in most_recent_solve} return [(team_mapping[i[0]].eligible, i[0], team_mapping[i[0]].name, team_mapping[i[0]].affiliation, i[1]) for idx, i in enumerate(sorted(scores.items(), key=lambda k: (-k[1], most_recent_solve[k[0]])))] +def calculate_graph(scoredata): + solves = list(ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge)) + adjustments = list(ScoreAdjustment.select()) + scoredata = [i for i in scoredata if i[0]] # Only eligible teams are on the score graph + graph_data = [] + for eligible, tid, name, affiliation, score in scoredata[:config.teams_on_graph]: + our_solves = [i for i in solves if i.team_id == tid] + team_data = [] + s = sum([i.value for i in adjustments if i.team_id == tid]) + for i in sorted(our_solves, key=lambda k: k.time): + team_data.append((str(i.time), s)) + s += i.challenge.points + team_data.append((str(i.time), s)) + team_data.append((str(datetime.now()), score)) + graph_data.append((name, team_data)) + return graph_data + def get_complex(key): i = g.redis.get(key) if i is None: