a few things:

- more admin features!
- ctftool scan: scan a directory for problems
- scoreboard stuff
master
Fox Wilson 2015-11-28 21:38:56 -05:00
parent b875100d5e
commit 834acad34c
11 changed files with 211 additions and 4 deletions

3
app.py
View File

@ -34,8 +34,9 @@ def scoreboard_variables():
return var
# Blueprints
from modules import api
from modules import api, admin
app.register_blueprint(api.api)
app.register_blueprint(admin.admin)
# Publically accessible things

28
ctftool
View File

@ -1,18 +1,23 @@
#!/usr/bin/python3
from database import *
from datetime import datetime, timedelta
import getpass
import os
import os.path
import sys
import yaml
import random
import utils
import utils.admin
operation = sys.argv[1]
if operation == "create-tables":
db.create_tables([Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment])
db.create_tables([Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, AdminUser])
print("Tables created")
elif operation == "drop-tables":
if input("Are you sure? Type yes to continue: ") == "yes":
db.drop_tables([Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment])
db.drop_tables([Team, TeamAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, AdminUser])
print("Done")
else:
print("Okay, nothing happened.")
@ -47,4 +52,23 @@ elif operation == "gen-team":
ctz -= diff
ChallengeSolve.create(team=Team.get(Team.id == (i % n) + 1), challenge=chal, time=ctz)
elif operation == "add-admin":
username = input("Username: ")
password = getpass.getpass().encode()
pwhash = utils.admin.create_password(password)
AdminUser.create(username=username, password=pwhash)
print("AdminUser created")
elif operation == "scan":
path = sys.argv[2]
dirs = [j for j in [os.path.join(path, i) for i in os.listdir(path)] if os.path.isdir(j)]
print(dirs)
n = 0
for d in dirs:
if os.path.exists(os.path.join(d, "problem.yml")):
with open(os.path.join(d, "problem.yml")) as f:
n += 1
Challenge.create(**yaml.load(f))
print(n, "challenges loaded")
# vim: syntax=python:ft=python

View File

@ -32,6 +32,7 @@ class TeamAccess(BaseModel):
class Challenge(BaseModel):
name = CharField()
category = CharField()
author = CharField()
description = TextField()
points = IntegerField()
flag = CharField()

51
modules/admin.py Normal file
View File

@ -0,0 +1,51 @@
from flask import Blueprint, render_template, request, session, redirect, url_for, flash
from database import AdminUser, Team, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment
import utils
import utils.admin
import utils.scoreboard
admin = Blueprint("admin", "admin", url_prefix="/admin")
@admin.route("/")
def admin_root():
if "admin" in session:
return redirect(url_for(".admin_dashboard"))
else:
return redirect(url_for(".admin_login"))
@admin.route("/login/", methods=["GET", "POST"])
def admin_login():
if request.method == "GET":
return render_template("admin/login.html")
elif request.method == "POST":
username = request.form["username"]
password = request.form["password"]
try:
user = AdminUser.get(AdminUser.username == username)
result = utils.admin.verify_password(user, password)
if result:
session["admin"] = user.username
return redirect(url_for(".admin_dashboard"))
except AdminUser.DoesNotExist:
pass
flash("Invalid username or password.")
return render_template("admin/login.html")
@admin.route("/dashboard/")
def admin_dashboard():
teams = Team.select()
solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge)
adjustments = ScoreAdjustment.select()
scoredata = utils.scoreboard.get_all_scores(teams, solves, adjustments)
lastsolvedata = utils.scoreboard.get_last_solves(teams, solves)
return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata)
@admin.route("/team/<int:tid>/")
def admin_show_team(tid):
team = Team.get(Team.id == tid)
return render_template("admin/team.html", team=team)
@admin.route("/logout/")
def admin_logout():
del session["admin"]
return redirect(url_for('.admin_login'))

57
templates/admin/base.html Normal file
View File

@ -0,0 +1,57 @@
<html>
<head>
<title>{{ config.ctf_name }} Admin :: {% block title %}Home{% endblock %}</title>
{% if config.cdn %}
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.1/css/materialize.min.css" />
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.9.0/vis.min.css" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
{% else %}
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='materialize.min.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='icons.css') }}" />
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='vis.min.css') }}" />
{% endif %}
{% block head %}{% endblock %}
</head>
<body>
{% if "admin" in session %}
<div class="navbar-fixed">
<nav class="blue darken-3">
<div class="container">
<div class="nav-wrapper">
<ul class="left">
<li><a href="{{ url_for('admin.admin_dashboard') }}">Dashboard</a></li>
</ul>
<a href="#" class="center brand-logo">{{ config.ctf_name }} Admin</a>
<ul class="right">
<li><a href="{{ url_for('admin.admin_logout') }}">Logout ({{ session.admin }})</a></li>
</ul>
</div>
</div>
</nav>
</div>
{% endif %}
<div class="container" style="margin-top: 20px;">
{% block content %}{% endblock %}
</div>
{% if config.cdn %}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/0.97.1/js/materialize.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.3/jquery.timeago.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.9.0/vis.min.js"></script>
{% else %}
<script src="{{ url_for('static', filename='jquery.min.js') }}"></script>
<script src="{{ url_for('static', filename='materialize.min.js') }}"></script>
<script src="{{ url_for('static', filename='timeago.min.js') }}"></script>
<script src="{{ url_for('static', filename='vis.min.js') }}"></script>
{% endif %}
<script src="{{ url_for('static', filename='api.js') }}"></script>
<script>$("abbr.time").timeago();</script>
<script id="toasts" type="text/javascript">
{% for message in get_flashed_messages() %}
Materialize.toast({{ message | tojson }}, 4000);
{% endfor %}
</script>
{% block postscript %}
{% endblock %}
</body>
</html>

View File

@ -0,0 +1,21 @@
{% extends "admin/base.html" %}
{% block content %}
<table>
<thead>
<tr><th>Team</th><th>Affiliation</th><th>Eligible</th><th>Last solve</th><th>Score</th></tr>
</thead>
<tbody>
{% for team in teams %}
<tr>
<td>
<a href="{{ url_for('admin.admin_show_team', tid=team.id) }}">{{ team.name }}</a>
</td>
<td>{{ team.affiliation }}</td>
<td>{{ "Eligible" if team.eligible else "Ineligible" }}</td>
<td><abbr class="time" title="{{ lastsolvedata[team.id] }}">{{ lastsolvedata[team.id] }}</abbr></td>
<td>{{ scoredata[team.id] }}</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% extends "admin/base.html" %}
{% block title %}Login{% endblock %}
{% block content %}
<h2>Login</h2>
<form method="POST">
<div class="input-field">
<input id="username" name="username" type="text" />
<label for="username">Username</label>
</div>
<div class="input-field">
<input id="password" name="password" type="password" />
<label for="password">Password</label>
</div>
<input name="_csrf_token" type="hidden" value="{{ csrf_token() }}" />
<button class="btn waves-effect waves-light" type="submit">Login</button>
</form>
{% endblock %}

11
templates/admin/team.html Normal file
View File

@ -0,0 +1,11 @@
{% extends "admin/base.html" %}
{% block content %}
<h2>{{ team.name }}</h2>
<p>This team is <strong>{{ "eligible" if team.eligible else "not eligible" }}</strong>.</p>
<p>This team's affiliation is <strong>{{ team.affiliation }}</strong></p>
<h3>Email</h3>
<p>This team's email is <strong>{{ team.email }} ({{ "confirmed" if team.email_confirmed else "unconfirmed" }})</strong>.</p>
{% if not team.email_confirmed or 1 %}
<p>This team's confirmation key is <code>{{ team.email_confirmation_key }}</code>.
{% endif %}
{% endblock %}

View File

@ -34,7 +34,8 @@ function toggle() {
<span class="right">{{ challenge.category }} <b>&middot;</b> {{ challenge.points }} pt</span>
</div>
<div class="collapsible-body">
<p>{{ challenge.description }}</p>
<p>{{ challenge.description | safe }}<br />
The challenge author is {{ challenge.author }}; talk to them for hints if you're stuck.</p>
{% if challenge in solved %}
<p>You've solved this challenge!</p>
{% else %}

6
utils/admin.py Normal file
View File

@ -0,0 +1,6 @@
import bcrypt
def create_password(pw):
return bcrypt.hashpw(pw, bcrypt.gensalt())
def verify_password(user, pw):
return bcrypt.hashpw(pw.encode(), user.password.encode()) == user.password.encode()

View File

@ -5,6 +5,23 @@ from .cache import get_complex, set_complex
import config
def get_all_scores(teams, solves, adjustments):
scores = {team.id: 0 for team in teams}
for solve in solves:
scores[solve.team_id] += solve.challenge.points
for adjustment in adjustments:
scores[adjustment.team_id] += adjustment.value
return scores
def get_last_solves(teams, solves):
last = {team.id: datetime(1970, 1, 1) for team in teams}
for solve in solves:
if solve.time > last[solve.team_id]:
last[solve.team_id] = solve.time
return last
def calculate_scores():
solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge)
adjustments = ScoreAdjustment.select()