moved so much stuff around and rewrote the api

master
James Sigurðarson 2016-07-31 17:27:20 +00:00
parent e8ecccc7d0
commit 9a7fb14f56
39 changed files with 877 additions and 659 deletions

19
2
View File

@ -1,19 +0,0 @@
from app import app, url_for
app.config["SERVER_NAME"] = "server"
with app.app_context():
import urllib
output = []
for rule in app.url_map.iter_rules():
options = {}
for arg in rule.arguments:
options[arg] = "[{0}]".format(arg)
methods = ','.join(rule.methods)
url = url_for(rule.endpoint, **options)
line = urllib.unquote("{:50s} {:20s} {}".format(rule.endpoint, methods, url))
output.append(line)
for line in sorted(output):
print(line)

555
app.py
View File

@ -1,43 +1,55 @@
from flask import Flask, render_template, session, redirect, url_for, request, g, flash, jsonify from flask import Flask, render_template, session, redirect, url_for, request, g, flash, jsonify
app = Flask(__name__)
from database import User, Team, UserAccess, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification, db import redis
from datetime import datetime, timedelta import socket
from peewee import fn import logging
from utils import decorators, flag, cache, misc, captcha, email, select
import utils.scoreboard
import config import config
import utils
import redis
import requests
import socket
app.secret_key = config.secret.key from utils import misc, select
from data.database import db
import data
# Blueprints
from routes import api, admin, teams, users, challenges, tickets, scoreboard
import logging
if config.production: if config.production:
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
else: else:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
app = Flask(__name__)
app.secret_key = config.secret.key
app.register_blueprint(api.api)
app.register_blueprint(admin.admin)
app.register_blueprint(teams.teams)
app.register_blueprint(users.users)
app.register_blueprint(challenges.challenges)
app.register_blueprint(tickets.tickets)
app.register_blueprint(scoreboard.scoreboard)
@app.before_request @app.before_request
def make_info_available(): def make_info_available():
if "user_id" in session: if "user_id" in session:
g.logged_in = True g.logged_in = True
try: current_user = data.user.get_user(id=session["user_id"])
g.user = User.get(User.id == session["user_id"]) if current_user is not None:
g.user = current_user
g.user_restricts = g.user.restricts.split(",") g.user_restricts = g.user.restricts.split(",")
g.team = g.user.team g.team = g.user.team
g.team_restricts = g.team.restricts.split(",") g.team_restricts = g.team.restricts.split(",")
except User.DoesNotExist: else:
g.logged_in = False g.logged_in = False
session.pop("user_id") session.pop("user_id")
return render_template("login.html") return render_template("login.html")
else: else:
g.logged_in = False g.logged_in = False
@app.context_processor @app.context_processor
def scoreboard_variables(): def scoreboard_variables():
var = dict(config=config, select=select) var = dict(config=config, select=select)
@ -45,555 +57,64 @@ def scoreboard_variables():
var["logged_in"] = True var["logged_in"] = True
var["user"] = g.user var["user"] = g.user
var["team"] = g.team var["team"] = g.team
# TODO should this apply to users or teams? var["notifications"] = data.notification.get_notifications()
# var["notifications"] = Notification.select().where(Notification.user == g.user)
var["notifications"] = []
else: else:
var["logged_in"] = False var["logged_in"] = False
var["notifications"] = [] var["notifications"] = []
return var return var
# Blueprints
from modules import api, admin
app.register_blueprint(api.api)
app.register_blueprint(admin.admin)
# Publically accessible things
@app.route('/') @app.route('/')
def root(): def root():
if g.logged_in: if g.logged_in:
return redirect(url_for('team_dashboard')) return redirect(url_for('team.dashboard'))
return redirect(url_for('register')) return redirect(url_for('users.register'))
@app.route('/chat/') @app.route('/chat/')
def chat(): def chat():
return render_template("chat.html") return render_template("chat.html")
@app.route('/scoreboard/')
def scoreboard():
data = cache.get_complex("scoreboard")
graphdata = cache.get_complex("graph")
if data is None or graphdata is None:
if config.immediate_scoreboard:
data = utils.scoreboard.calculate_scores()
graphdata = utils.scoreboard.calculate_graph(data)
utils.scoreboard.set_complex("scoreboard", data, 120)
utils.scoreboard.set_complex("graph", graphdata, 120)
else:
return "No scoreboard data available. Please contact an organizer."
return render_template("scoreboard.html", data=data, graphdata=graphdata)
@app.route('/login/', methods=["GET", "POST"])
def login():
if request.method == "GET":
return render_template("login.html")
elif request.method == "POST":
username = request.form["username"]
password = request.form["password"]
try:
user = User.get(User.username == username)
if(user.checkPassword(password)):
UserAccess.create(user=user, ip=misc.get_ip(), time=datetime.now())
session["user_id"] = user.id
flash("Login successful.")
return redirect(url_for('team_dashboard'))
else:
flash("Incorrect username or password", "error")
return render_template("login.html")
except User.DoesNotExist:
flash("Incorrect username or password", "error")
return render_template("login.html")
@app.route('/register/', methods=["GET", "POST"])
def register():
if not config.registration:
if "admin" in session and session["admin"]:
pass
else:
return "Registration is currently disabled. Email icectf@icec.tf to create an account."
if request.method == "GET":
return render_template("register.html")
elif request.method == "POST":
error, message = captcha.verify_captcha()
if error:
flash(message)
return render_template("register.html")
username = request.form["username"].strip()
user_email = request.form["email"].strip()
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
background = request.form["background"].strip()
country = request.form["country"].strip()
tshirt_size = ""
gender = ""
if "tshirt_size" in request.form.keys():
tshirt_size = request.form["tshirt_size"].strip()
if "gender" in request.form.keys():
gender = request.form["gender"].strip()
join_team = bool(int(request.form["join_team"].strip()))
if join_team:
team_key = request.form["team_key"].strip()
else:
team_name = request.form["team_name"].strip()
team_affiliation = request.form["team_affiliation"].strip()
if len(username) > 50 or not username:
flash("You must have a username!")
return render_template("register.html")
try:
user = User.get(User.username == username)
flash("This username is already in use!")
return render_template("register.html")
except User.DoesNotExist:
pass
if password != confirm_password:
flash("Password does not match confirmation")
return render_template("register.html")
if not (user_email and "." in user_email and "@" in user_email):
flash("You must have a valid email!")
return render_template("register.html")
if not email.is_valid_email(user_email):
flash("You're lying")
return render_template("register.html")
if (not tshirt_size == "") and (not tshirt_size in select.TShirts):
flash("Invalid T-shirt size")
return render_template("register.html")
if not background in select.BackgroundKeys:
flash("Invalid Background")
return render_template("register.html")
if not country in select.CountryKeys:
flash("Invalid Background")
return render_template("register.html")
if (not gender == "") and (not gender in ["M", "F"]):
flash("Invalid gender")
return render_template("register.html")
confirmation_key = misc.generate_confirmation_key()
team=None
if join_team:
try:
team = Team.get(Team.key == team_key)
except Team.DoesNotExist:
flash("Couldn't find this team, check your team key.")
return render_template("register.html")
else:
if not team_name or len(team_name) > 50:
flash("Missing team name")
return render_template("register.html")
if not team_affiliation or len(team_affiliation) > 100:
team_affiliation = "No affiliation"
try:
team = Team.get(Team.name == team_name)
flash("This team name is already in use!")
return render_template("register.html")
except Team.DoesNotExist:
pass
team_key = misc.generate_team_key()
team = Team.create(name=team_name, affiliation=team_affiliation, key=team_key)
user = User.create(username=username, email=user_email,
background=background, country=country,
tshirt_size=tshirt_size, gender=gender,
email_confirmation_key=confirmation_key,
team=team)
user.setPassword(password)
user.save()
UserAccess.create(user=user, ip=misc.get_ip(), time=datetime.now())
# print(confirmation_key)
email.send_confirmation_email(user_email, confirmation_key)
session["user_id"] = user.id
flash("Registration finished")
return redirect(url_for('user_dashboard'))
@app.route('/logout/')
def logout():
session.pop("user_id")
flash("You've successfully logged out.")
return redirect(url_for('login'))
# Things that require a team
@app.route('/confirm_email/<confirmation_key>', methods=["GET"])
@decorators.login_required
def confirm_email(confirmation_key):
if confirmation_key == g.user.email_confirmation_key:
flash("Email confirmed!")
g.user.email_confirmed = True
g.user.save()
else:
flash("Incorrect confirmation key.")
return redirect(url_for('user_dashboard'))
@app.route('/forgot_password/', methods=["GET", "POST"])
def forgot_password():
if request.method == "GET":
return render_template("forgot_password.html")
elif request.method == "POST":
username = request.form["username"].strip()
if len(username) > 50 or not username:
flash("You must have a username!")
return redirect(url_for('forgot_password'))
try:
user = User.get(User.username == username)
user.password_reset_token = misc.generate_confirmation_key()
user.password_reset_expired = datetime.now() + timedelta(days=1)
user.save()
email.send_password_reset_email(user.email, user.password_reset_token)
flash("Forgot password email sent! Check your email.")
return render_template("forgot_password.html")
except User.DoesNotExist:
flash("Username is not registered", "error")
return render_template("forgot_password.html")
@app.route('/reset_password/<password_reset_token>', methods=["GET", "POST"])
def reset_password(password_reset_token):
if request.method == "GET":
return render_template("reset_password.html")
elif request.method == "POST":
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
if not password == confirm_password:
flash("Password does not match")
return render_template("reset_password.html", password_reset_token=password_reset_token)
if not password_reset_token:
flash("Reset Token is invalid", "error")
return redirect(url_for("forgot_password"))
try:
user = User.get(User.password_reset_token == password_reset_token)
if user.password_reset_expired < datetime.now():
flash("Token expired")
return redirect(url_for("forgot_password"))
user.setPassword(password)
user.password_reset_token = None
user.save()
flash("Password successfully reset")
return redirect(url_for("login"))
except User.DoesNotExist:
flash("Reset Token is invalid", "error")
return redirect(url_for("forgot_password"))
@app.route('/user/', methods=["GET", "POST"])
@decorators.login_required
def user_dashboard():
if request.method == "GET":
first_login = False
if g.user.first_login:
first_login = True
g.user.first_login = False
g.user.save()
return render_template("user.html", first_login=first_login)
elif request.method == "POST":
if g.redis.get("ul{}".format(session["user_id"])):
flash("You're changing your information too fast!")
return redirect(url_for('user_dashboard'))
username = request.form["username"].strip()
user_email = request.form["email"].strip()
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
background = request.form["background"].strip()
country = request.form["country"].strip()
tshirt_size = ""
gender = ""
if "tshirt_size" in request.form.keys():
tshirt_size = request.form["tshirt_size"].strip()
if "gender" in request.form.keys():
gender = request.form["gender"].strip()
if len(username) > 50 or not username:
flash("You must have a username!")
return redirect(url_for('user_dashboard'))
if g.user.username != username:
try:
user = User.get(User.username == username)
flash("This username is already in use!")
return redirect(url_for('user_dashboard'))
except User.DoesNotExist:
pass
if not (user_email and "." in user_email and "@" in user_email):
flash("You must have a valid email!")
return redirect(url_for('user_dashboard'))
if not email.is_valid_email(user_email):
flash("You're lying")
return redirect(url_for('user_dashboard'))
if (not tshirt_size == "") and (not tshirt_size in select.TShirts):
flash("Invalid T-shirt size")
return redirect(url_for('user_dashboard'))
if not background in select.BackgroundKeys:
flash("Invalid Background")
return redirect(url_for('user_dashboard'))
if not country in select.CountryKeys:
flash("Invalid Background")
return redirect(url_for('user_dashboard'))
if (not gender == "") and (not gender in ["M", "F"]):
flash("Invalid gender")
return redirect(url_for('user_dashboard'))
email_changed = (user_email != g.user.email)
g.user.username = username
g.user.email = user_email
g.user.background = background
g.user.country = country
g.user.gender = gender
g.user.tshirt_size = tshirt_size
g.redis.set("ul{}".format(session["user_id"]), str(datetime.now()), 120)
if password != "":
if password != confirm_password:
flash("Password does not match confirmation")
return redirect(url_for('user_dashboard'))
g.user.setPassword(password)
if email_changed:
g.user.email_confirmation_key = misc.generate_confirmation_key()
g.user.email_confirmed = False
email.send_confirmation_email(user_email, g.user.email_confirmation_key)
flash("Changes saved. Please check your email for a new confirmation key.")
else:
flash("Changes saved.")
g.user.save()
return redirect(url_for('user_dashboard'))
@app.route('/team/', methods=["GET", "POST"])
@decorators.login_required
def team_dashboard():
if request.method == "GET":
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("team.html", team_solves=team_solves, team_adjustments=team_adjustments, team_score=team_score)
elif request.method == "POST":
if g.redis.get("ul{}".format(session["user_id"])):
flash("You're changing your information too fast!")
return redirect(url_for('team_dashboard'))
team_name = request.form["team_name"].strip()
affiliation = request.form["team_affiliation"].strip()
if len(team_name) > 50 or not team_name:
flash("You must have a team name!")
return redirect(url_for('team_dashboard'))
if not affiliation or len(affiliation) > 100:
affiliation = "No affiliation"
if g.team.name != team_name:
try:
team = Team.get(Team.name == team_name)
flash("This team name is already in use!")
return redirect(url_for('team_dashboard'))
except Team.DoesNotExist:
pass
g.team.name = team_name
g.team.affiliation = affiliation
g.redis.set("ul{}".format(session["user_id"]), str(datetime.now()), 120)
flash("Changes saved.")
g.team.save()
return redirect(url_for('team_dashboard'))
@app.route('/teamconfirm/', methods=["POST"])
def teamconfirm():
if utils.misc.get_ip() in config.confirm_ip:
team_name = request.form["team_name"].strip()
team_key = request.form["team_key"].strip()
try:
team = Team.get(Team.name == team_name)
except Team.DoesNotExist:
return "invalid", 403
if team.key == team_key:
return "ok", 200
else:
return "invalid", 403
else:
return "unauthorized", 401
@app.route('/challenges/')
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
def challenges():
chals = Challenge.select().order_by(Challenge.points, Challenge.name)
solved = Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == 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)
@app.route('/challenges/<int:challenge>/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 challenge_show_solves(challenge):
chal = Challenge.get(Challenge.id == challenge)
solves = ChallengeSolve.select(ChallengeSolve, Team).join(Team).order_by(ChallengeSolve.time).where(ChallengeSolve.challenge == chal)
return render_template("challenge_solves.html", challenge=chal, solves=solves)
@app.route('/submit/<int:challenge>/', methods=["POST"])
@decorators.must_be_allowed_to("solve challenges")
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
def submit(challenge):
chal = Challenge.get(Challenge.id == challenge)
flagval = request.form["flag"]
code, message = flag.submit_flag(g.team, chal, flagval)
flash(message)
return redirect(url_for('challenges'))
# Trouble tickets
@app.route('/tickets/')
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
def team_tickets():
return render_template("tickets.html", tickets=list(g.team.tickets))
@app.route('/tickets/new/', methods=["GET", "POST"])
@decorators.must_be_allowed_to("submit tickets")
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
def open_ticket():
if request.method == "GET":
return render_template("open_ticket.html")
elif request.method == "POST":
if g.redis.get("ticketl{}".format(session["user_id"])):
return "You're doing that too fast."
g.redis.set("ticketl{}".format(g.team.id), "1", 30)
summary = request.form["summary"]
description = request.form["description"]
opened_at = datetime.now()
ticket = TroubleTicket.create(team=g.team, summary=summary, description=description, opened_at=opened_at)
flash("Ticket #{} opened.".format(ticket.id))
return redirect(url_for("team_ticket_detail", ticket=ticket.id))
@app.route('/tickets/<int:ticket>/')
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
def team_ticket_detail(ticket):
try:
ticket = TroubleTicket.get(TroubleTicket.id == ticket)
except TroubleTicket.DoesNotExist:
flash("Couldn't find ticket #{}.".format(ticket))
return redirect(url_for("team_tickets"))
if ticket.team != g.team:
flash("That's not your ticket.")
return redirect(url_for("team_tickets"))
comments = TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)
return render_template("ticket_detail.html", ticket=ticket, comments=comments)
@app.route('/tickets/<int:ticket>/comment/', methods=["POST"])
@decorators.must_be_allowed_to("comment on tickets")
@decorators.must_be_allowed_to("view tickets")
def team_ticket_comment(ticket):
if g.redis.get("ticketl{}".format(session["user_id"])):
return "You're doing that too fast."
g.redis.set("ticketl{}".format(g.team.id), "1", 30)
try:
ticket = TroubleTicket.get(TroubleTicket.id == ticket)
except TroubleTicket.DoesNotExist:
flash("Couldn't find ticket #{}.".format(ticket))
return redirect(url_for("team_tickets"))
if ticket.team != g.team:
flash("That's not your ticket.")
return redirect(url_for("team_tickets"))
if request.form["comment"]:
TicketComment.create(ticket=ticket, comment_by=g.team.name, comment=request.form["comment"], time=datetime.now())
flash("Comment added.")
if ticket.active and "resolved" in request.form:
ticket.active = False
ticket.save()
flash("Ticket closed.")
elif not ticket.active and "resolved" not in request.form:
ticket.active = True
ticket.save()
flash("Ticket re-opened.")
return redirect(url_for("team_ticket_detail", ticket=ticket.id))
# Debug # Debug
@app.route('/debug/') @app.route('/debug/')
def debug_app(): def debug_app():
return jsonify(hostname=socket.gethostname()) return jsonify(hostname=socket.gethostname())
# Manage Peewee database sessions and Redis
# Manage Peewee database sessions and Redis
@app.before_request @app.before_request
def before_request(): def before_request():
db.connect() db.connect()
g.redis = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db) g.redis = redis.StrictRedis(host=config.redis.host, port=config.redis.port, db=config.redis.db)
g.connected = True g.connected = True
@app.teardown_request @app.teardown_request
def teardown_request(exc): def teardown_request(exc):
if getattr(g, 'connected', False): if getattr(g, 'connected', False):
db.close() db.close()
g.redis.connection_pool.disconnect() g.redis.connection_pool.disconnect()
# CSRF things
# CSRF things
@app.before_request @app.before_request
def csrf_protect(): def csrf_protect():
csrf_exempt = ['/teamconfirm/'] csrf_exempt = ['/teamconfirm/']
if request.method == "POST": if request.method == "POST":
token = session.get('_csrf_token', None) token = session.get('_csrf_token', None)
if (not token or token != request.form["_csrf_token"]) and not request.path in csrf_exempt: if (not token or token != request.form["_csrf_token"]) and request.path not in csrf_exempt:
return "Invalid CSRF token!" return "Invalid CSRF token!"
def generate_csrf_token(): def generate_csrf_token():
if '_csrf_token' not in session: if '_csrf_token' not in session:
session['_csrf_token'] = misc.generate_random_string(64) session['_csrf_token'] = misc.generate_random_string(64)
return session['_csrf_token'] return session['_csrf_token']
app.jinja_env.globals['csrf_token'] = generate_csrf_token app.jinja_env.globals['csrf_token'] = generate_csrf_token
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -2,6 +2,7 @@ import os
from datetime import datetime from datetime import datetime
production = os.getenv("PRODUCTION", None) is not None production = os.getenv("PRODUCTION", None) is not None
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ctf_name = "IceCTF" ctf_name = "IceCTF"
#IRC Channel #IRC Channel

View File

@ -1,11 +0,0 @@
SUCCESS = (0, "Success!")
FLAG_SUBMISSION_TOO_FAST = (1001, "You're submitting flags too fast!")
FLAG_SUBMITTED_ALREADY = (1002, "You've already solved that problem!")
FLAG_INCORRECT = (1003, "Incorrect flag.")
FLAG_CANNOT_SUBMIT_WHILE_DISABLED = (1004, "You cannot submit a flag for a disabled problem.")
CAPTCHA_NOT_COMPLETED = (2001, "Please complete the CAPTCHA.")
CAPTCHA_INVALID = (2002, "Invalid CAPTCHA response.")
NOTIFICATION_NOT_YOURS = (3001, "You cannot dismiss notifications that do not belong to you.")

0
data/__init__.py Normal file
View File

38
data/challenge.py Normal file
View File

@ -0,0 +1,38 @@
from data.database import Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment
from datetime import datetime
from exceptions import ValidationError
from flask import g
def get_challenges():
return Challenge.select().order_by(Challenge.points, Challenge.name)
def get_challenge(id):
try:
return Challenge.get(Challenge.id == id)
except Challenge.DoesNotExist:
raise ValidationError("Challenge does not exist!")
def get_solved(team):
return Challenge.select().join(ChallengeSolve).where(ChallengeSolve.team == g.team)
def get_adjustments(team):
return ScoreAdjustment.select().where(ScoreAdjustment.team == team)
def get_challenge_solves(chall):
return ChallengeSolve.select(ChallengeSolve, Team).join(Team).order_by(ChallengeSolve.time).where(ChallengeSolve.challenge == chall)
def submit_flag(chall, user, team, flag):
if team.solved(chall):
raise ValidationError("Your team has already solved this problem!")
elif not chall.enabled:
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"
else:
ChallengeSolve.create(user=user, team=team, challenge=chall, time=datetime.now())
g.redis.hincrby("solves", challenge.id, 1)
if config.immediate_scoreboard:
g.redis.delete("scoreboard")
g.redis.delete("graph")
return "Correct!"

View File

@ -46,11 +46,11 @@ class User(BaseModel):
password_reset_token = CharField(null = True) password_reset_token = CharField(null = True)
password_reset_expired = DateTimeField(null = True) password_reset_expired = DateTimeField(null = True)
def setPassword(self, pw): def set_password(self, pw):
self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt()) self.password = bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt())
return return
def checkPassword(self, pw): def check_password(self, pw):
return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8")) return bcrypt.checkpw(pw.encode("utf-8"), self.password.encode("utf-8"))
def eligible(self): def eligible(self):
@ -72,6 +72,7 @@ class Challenge(BaseModel):
flag = TextField() flag = TextField()
class ChallengeSolve(BaseModel): class ChallengeSolve(BaseModel):
user = ForeignKeyField(User, related_name='solves')
team = ForeignKeyField(Team, related_name='solves') team = ForeignKeyField(Team, related_name='solves')
challenge = ForeignKeyField(Challenge, related_name='solves') challenge = ForeignKeyField(Challenge, related_name='solves')
time = DateTimeField() time = DateTimeField()
@ -80,6 +81,7 @@ class ChallengeSolve(BaseModel):
primary_key = CompositeKey('team', 'challenge') primary_key = CompositeKey('team', 'challenge')
class ChallengeFailure(BaseModel): class ChallengeFailure(BaseModel):
user = ForeignKeyField(User, related_name='failures')
team = ForeignKeyField(Team, related_name='failures') team = ForeignKeyField(Team, related_name='failures')
challenge = ForeignKeyField(Challenge, related_name='failures') challenge = ForeignKeyField(Challenge, related_name='failures')
attempt = CharField() attempt = CharField()

14
data/notification.py Normal file
View File

@ -0,0 +1,14 @@
from data.database import Notification
from exceptions import ValidationError
def get_notifications(team):
return Notification.select().where(Notification.team == team)
def get_notification(team, id):
try:
return Notification.get(Notification.id == id and Notification.team == team)
except Notification.DoesNotExist:
raise ValidationError("Notification does not exist!")
def delete_notification(notification):
notification.delete_instance()

View File

@ -1,8 +1,9 @@
from database import Team, Challenge, ChallengeSolve, ScoreAdjustment from .database import Team, Challenge, ChallengeSolve, ScoreAdjustment
from datetime import datetime, timedelta from datetime import datetime, timedelta
import config import config
def get_all_scores(teams, solves, adjustments): def get_all_scores(teams, solves, adjustments):
scores = {team.id: 0 for team in teams} scores = {team.id: 0 for team in teams}
for solve in solves: for solve in solves:
@ -13,6 +14,7 @@ def get_all_scores(teams, solves, adjustments):
return scores return scores
def get_last_solves(teams, solves): def get_last_solves(teams, solves):
last = {team.id: datetime(1970, 1, 1) for team in teams} last = {team.id: datetime(1970, 1, 1) for team in teams}
for solve in solves: for solve in solves:
@ -20,6 +22,7 @@ def get_last_solves(teams, solves):
last[solve.team_id] = solve.time last[solve.team_id] = solve.time
return last return last
def calculate_scores(): def calculate_scores():
solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge)
adjustments = ScoreAdjustment.select() adjustments = ScoreAdjustment.select()
@ -39,10 +42,11 @@ def calculate_scores():
# eligible, teamid, teamname, affiliation, score # eligible, teamid, teamname, affiliation, score
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]])))] 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): def calculate_graph(scoredata):
solves = list(ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).order_by(ChallengeSolve.time)) solves = list(ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge).order_by(ChallengeSolve.time))
adjustments = list(ScoreAdjustment.select()) adjustments = list(ScoreAdjustment.select())
scoredata = [i for i in scoredata if i[0]] # Only eligible teams are on the score graph scoredata = [i for i in scoredata if i[0]] # Only eligible teams are on the score graph
graph_data = [] graph_data = []
for eligible, tid, name, affiliation, score in scoredata[:config.teams_on_graph]: for eligible, tid, name, affiliation, score in scoredata[:config.teams_on_graph]:
our_solves = [i for i in solves if i.team_id == tid] our_solves = [i for i in solves if i.team_id == tid]

48
data/team.py Normal file
View File

@ -0,0 +1,48 @@
from exceptions import ValidationError
from .database import Team
from utils import misc
def get_team(id=None, name=None, key=None):
try:
if name:
return Team.get(Team.name == name)
elif id:
return Team.get(Team.id == id)
elif key:
return Team.get(Team.key == key)
else:
raise ValueError("Invalid call")
except Team.DoesNotExist:
return None
def validate(name, affiliation):
if name is not None:
if not name or len(name) > 50:
raise ValidationError("A team name is required.")
if get_team(name=name):
raise ValidationError("A team with that name already exists.")
def create_team(name, affiliation):
if not affiliation:
affiliation = "No affiliation"
validate(name, affiliation)
team_key = misc.generate_team_key()
team = Team.create(name=name, affiliation=affiliation, key=team_key)
return True, team
def update_team(current_team, name, affiliation):
if not affiliation:
affiliation = "No affiliation"
if current_team.name == name:
name = None
validate(name, affiliation)
if name:
current_team.name = name
current_team.affiliation = affiliation
current_team.save()

30
data/ticket.py Normal file
View File

@ -0,0 +1,30 @@
from data.database import TroubleTicket, TicketComment
from datetime import datetime
from exceptions import ValidationError
def get_tickets(team):
return team.tickets
def get_ticket(team, id):
try:
return TroubleTicket.get(TroubleTicket.id == ticket and TroubleTicket.team == team)
except TroubleTicket.DoesNotExist:
raise ValidationError("Ticket not found!")
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())
def create_comment(ticket, user, comment):
TicketComment.create(ticket=ticket, comment_by=user.username, comment=request.form["comment"], time=datetime.now())
def open_ticket(ticket):
ticket.active = True
ticket.save()
def close_ticket(ticket):
ticket.active = False
ticket.save()

139
data/user.py Normal file
View File

@ -0,0 +1,139 @@
from exceptions import ValidationError
from .database import User, UserAccess
from datetime import datetime, timedelta
import utils
def get_user(username=None, id=None):
try:
if username:
return User.get(User.username == username)
elif id:
return User.get(User.id == id)
else:
raise ValueError("Invalid call")
except User.DoesNotExist:
return None
def login(username, password):
user = get_user(username=username)
if not user:
return False
if(user.check_password(password)):
UserAccess.create(user=user, ip=utils.misc.get_ip(), time=datetime.now())
return True
return False
def validate(username, email, password, background, country, tshirt_size=None, gender=None):
if not email or "." not in email or "@" not in email:
raise ValidationError("You must have a valid email!")
if not email.is_valid_email(email):
raise ValidationError("You're lying")
if background not in utils.select.BackgroundKeys:
raise ValidationError("Invalid Background")
if country not in utils.select.CountryKeys:
raise ValidationError("Invalid Background")
if tshirt_size and (tshirt_size not in utils.select.TShirts):
raise ValidationError("Invalid T-shirt size")
if gender and (gender not in ["M", "F"]):
raise ValidationError("Invalid gender")
if password is not None:
if len(password) < 6:
raise ValidationError("Password is too short.")
if username is not None:
if not username or len(username) > 50:
raise ValidationError("Invalid username")
if get_user(username=username):
raise ValidationError("That username has already been taken.")
def create_user(username, email, password, background, country, team, tshirt_size=None, gender=None):
validate(username, email, password, background, country, tshirt_size=tshirt_size, gender=gender)
assert team is not None
confirmation_key = utils.misc.generate_confirmation_key()
user = User.create(username=username, email=email,
background=background, country=country,
tshirt_size=tshirt_size, gender=gender,
email_confirmation_key=confirmation_key,
team=team)
user.set_password(password)
user.save()
UserAccess.create(user=user, ip=utils.misc.get_ip(), time=datetime.now())
utils.email.send_confirmation_email(email, confirmation_key)
return user
def confirm_email(current_user, confirmation_key):
if current_user.email_confirmed:
raise ValidationError("Email already confirmed")
if current_user.confirmation_key == confirmation_key:
current_user.email_confirmed = True
current_user.save()
else:
raise ValidationError("Invalid confirmation key!")
def forgot_password(username):
user = get_user(username=username)
if user is None:
return
user.password_reset_token = utils.misc.generate_confirmation_key()
user.password_reset_expired = datetime.now() + timedelta(days=1)
user.save()
utils.email.send_password_reset_email(user.email, user.password_reset_token)
def reset_password(token, password):
if len(password) < 6:
raise ValidationError("Password is too short!")
try:
user = User.get(User.password_reset_token == password_reset_token)
if user.password_reset_expired < datetime.now():
raise ValidationError("Token expired")
user.set_password(password)
user.password_reset_token = None
user.save()
except User.DoesNotExist:
raise ValidationError("Invalid reset token!")
def update_user(current_user, username, email, password, background, country, tshirt_size=None, gender=None):
if username == current_user.username:
username = None
if password == "":
password = None
validate(username, email, password, background, country, tshirt_size, gender)
if username:
current_user.username = username
if password:
current_user.set_password(password)
email_changed = (current_user.email != email) # send email after saving to db
if email_changed:
current_user.email_confirmation_key = utils.misc.generate_confirmation_key()
current_user.email_confirmed = False
current_user.email = email
current_user.background = background
current_user.country = country
current_user.tshirt_size = tshirt_size
current_user.gender = gender
current_user.save()
if email_changed:
utils.email.send_confirmation_email(email, current_user.email_confirmation_key)
return "Changes saved. Check your email for a new confirmation key."
else:
return "Changes saved."

5
exceptions.py Normal file
View File

@ -0,0 +1,5 @@
class ValidationError(Exception):
pass
class CaptchaError(Exception):
pass

View File

@ -1,42 +0,0 @@
from flask import Blueprint, jsonify, g, request
from database import Challenge, Notification, Team, Challenge, ChallengeSolve
from utils import decorators, flag, scoreboard
from ctferror import *
from datetime import datetime
import config
api = Blueprint("api", "api", url_prefix="/api")
@api.route("/submit/<int:challenge>.json", methods=["POST"])
@decorators.must_be_allowed_to("solve challenges")
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
def submit_api(challenge):
chal = Challenge.get(Challenge.id == challenge)
flagval = request.form["flag"]
code, message = flag.submit_flag(g.team, chal, flagval)
return jsonify(dict(code=code, message=message))
@api.route("/dismiss/<int:nid>.json", methods=["POST"])
@decorators.login_required
def dismiss_notification(nid):
n = Notification.get(Notification.id == nid)
if g.team != n.team:
code, message = NOTIFICATION_NOT_YOURS
else:
Notification.delete().where(Notification.id == nid).execute()
code, message = SUCCESS
return jsonify(dict(code=code, message=message))
@api.route("/_ctftime/")
def ctftime_scoreboard_json():
if not config.immediate_scoreboard and datetime.now() < config.competition_end:
return "unavailable", 503
scores = scoreboard.calculate_scores()
standings = [dict(team=i[2], score=i[4], outward=not i[0]) for i in scores]
for index, standing in enumerate(standings):
standing["pos"] = index + 1
return jsonify(standings=standings)

0
routes/__init__.py Normal file
View File

View File

@ -1,13 +1,14 @@
from flask import Blueprint, render_template, request, session, redirect, url_for, flash from flask import Blueprint, render_template, request, session, redirect, url_for, flash
from database import AdminUser, Team, Challenge, ChallengeSolve, ChallengeFailure, ScoreAdjustment, TroubleTicket, TicketComment, Notification from data.database import AdminUser, Team, Challenge, ChallengeSolve, ScoreAdjustment, TroubleTicket, TicketComment, Notification
import utils import utils
import utils.admin import utils.admin
import utils.scoreboard from data import scoreboard
from utils.decorators import admin_required, csrf_check from utils.decorators import admin_required, csrf_check
from utils.notification import make_link from utils.notification import make_link
from datetime import datetime from datetime import datetime
from config import secret from config import secret
admin = Blueprint("admin", "admin", url_prefix="/admin") admin = Blueprint("admin", __name__, url_prefix="/admin")
@admin.route("/") @admin.route("/")
def admin_root(): def admin_root():
@ -16,6 +17,7 @@ def admin_root():
else: else:
return redirect(url_for(".admin_login")) return redirect(url_for(".admin_login"))
@admin.route("/login/", methods=["GET", "POST"]) @admin.route("/login/", methods=["GET", "POST"])
def admin_login(): def admin_login():
if request.method == "GET": if request.method == "GET":
@ -42,23 +44,26 @@ def admin_login():
flash("Y̸̤̗͍̘ͅo͙̠͈͎͎͙̟u̺ ̘̘̘̹̩̹h͔̟̟̗͠a̠͈v͍̻̮̗̬̬̣e̟̫̼̹̠͕ ̠̳͖͡ma͈̱͟d̙͍̀ͅe̵͕̙̯̟̟̞̳ ͉͚̙a̡̱̮̫̰̰ ̜̙̝̭͚t̜̙͚̗͇ͅͅe͉r҉r̸͎̝̞̙̦̹i͏̙b̶̜̟̭͕l̗̰̰̠̳̝̕e͎̥ ̸m̰̯̮̲̘̻͍̀is̜̲̮͍͔̘͕͟t̟͈̮a̙̤͎̠ķ̝̺͇̩e̷͍̤̠͖̣͈.̺̩̦̻.") flash("Y̸̤̗͍̘ͅo͙̠͈͎͎͙̟u̺ ̘̘̘̹̩̹h͔̟̟̗͠a̠͈v͍̻̮̗̬̬̣e̟̫̼̹̠͕ ̠̳͖͡ma͈̱͟d̙͍̀ͅe̵͕̙̯̟̟̞̳ ͉͚̙a̡̱̮̫̰̰ ̜̙̝̭͚t̜̙͚̗͇ͅͅe͉r҉r̸͎̝̞̙̦̹i͏̙b̶̜̟̭͕l̗̰̰̠̳̝̕e͎̥ ̸m̰̯̮̲̘̻͍̀is̜̲̮͍͔̘͕͟t̟͈̮a̙̤͎̠ķ̝̺͇̩e̷͍̤̠͖̣͈.̺̩̦̻.")
return render_template("admin/login.html") return render_template("admin/login.html")
@admin.route("/dashboard/") @admin.route("/dashboard/")
@admin_required @admin_required
def admin_dashboard(): def admin_dashboard():
teams = Team.select() teams = Team.select()
solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge) solves = ChallengeSolve.select(ChallengeSolve, Challenge).join(Challenge)
adjustments = ScoreAdjustment.select() adjustments = ScoreAdjustment.select()
scoredata = utils.scoreboard.get_all_scores(teams, solves, adjustments) scoredata = scoreboard.get_all_scores(teams, solves, adjustments)
lastsolvedata = utils.scoreboard.get_last_solves(teams, solves) 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) return render_template("admin/dashboard.html", teams=teams, scoredata=scoredata, lastsolvedata=lastsolvedata, tickets=tickets)
@admin.route("/tickets/") @admin.route("/tickets/")
@admin_required @admin_required
def admin_tickets(): def admin_tickets():
tickets = list(TroubleTicket.select(TroubleTicket, Team).join(Team).order_by(TroubleTicket.id.desc())) tickets = list(TroubleTicket.select(TroubleTicket, Team).join(Team).order_by(TroubleTicket.id.desc()))
return render_template("admin/tickets.html", tickets=tickets) return render_template("admin/tickets.html", tickets=tickets)
@admin.route("/tickets/<int:ticket>/") @admin.route("/tickets/<int:ticket>/")
@admin_required @admin_required
def admin_ticket_detail(ticket): def admin_ticket_detail(ticket):
@ -66,6 +71,7 @@ def admin_ticket_detail(ticket):
comments = list(TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time)) comments = list(TicketComment.select().where(TicketComment.ticket == ticket).order_by(TicketComment.time))
return render_template("admin/ticket_detail.html", ticket=ticket, comments=comments) return render_template("admin/ticket_detail.html", ticket=ticket, comments=comments)
@admin.route("/tickets/<int:ticket>/comment/", methods=["POST"]) @admin.route("/tickets/<int:ticket>/comment/", methods=["POST"])
@admin_required @admin_required
def admin_ticket_comment(ticket): def admin_ticket_comment(ticket):
@ -89,12 +95,14 @@ def admin_ticket_comment(ticket):
return redirect(url_for(".admin_ticket_detail", ticket=ticket.id)) return redirect(url_for(".admin_ticket_detail", ticket=ticket.id))
@admin.route("/team/<int:tid>/") @admin.route("/team/<int:tid>/")
@admin_required @admin_required
def admin_show_team(tid): def admin_show_team(tid):
team = Team.get(Team.id == tid) team = Team.get(Team.id == tid)
return render_template("admin/team.html", team=team) return render_template("admin/team.html", team=team)
@admin.route("/team/<int:tid>/<csrf>/impersonate/") @admin.route("/team/<int:tid>/<csrf>/impersonate/")
@csrf_check @csrf_check
@admin_required @admin_required
@ -102,6 +110,7 @@ def admin_impersonate_team(tid):
session["team_id"] = tid session["team_id"] = tid
return redirect(url_for("scoreboard")) return redirect(url_for("scoreboard"))
@admin.route("/team/<int:tid>/<csrf>/toggle_eligibility/") @admin.route("/team/<int:tid>/<csrf>/toggle_eligibility/")
@csrf_check @csrf_check
@admin_required @admin_required
@ -112,6 +121,7 @@ def admin_toggle_eligibility(tid):
flash("Eligibility set to {}".format(team.eligible)) flash("Eligibility set to {}".format(team.eligible))
return redirect(url_for(".admin_show_team", tid=tid)) return redirect(url_for(".admin_show_team", tid=tid))
@admin.route("/team/<int:tid>/<csrf>/toggle_eligibility_lock/") @admin.route("/team/<int:tid>/<csrf>/toggle_eligibility_lock/")
@csrf_check @csrf_check
@admin_required @admin_required
@ -122,6 +132,7 @@ def admin_toggle_eligibility_lock(tid):
flash("Eligibility lock set to {}".format(team.eligibility_locked)) flash("Eligibility lock set to {}".format(team.eligibility_locked))
return redirect(url_for(".admin_show_team", tid=tid)) return redirect(url_for(".admin_show_team", tid=tid))
@admin.route("/team/<int:tid>/adjust_score/", methods=["POST"]) @admin.route("/team/<int:tid>/adjust_score/", methods=["POST"])
@admin_required @admin_required
def admin_score_adjust(tid): def admin_score_adjust(tid):
@ -135,6 +146,7 @@ def admin_score_adjust(tid):
return redirect(url_for(".admin_show_team", tid=tid)) return redirect(url_for(".admin_show_team", tid=tid))
@admin.route("/logout/") @admin.route("/logout/")
def admin_logout(): def admin_logout():
del session["admin"] del session["admin"]

52
routes/api.py Normal file
View File

@ -0,0 +1,52 @@
from flask import Blueprint, jsonify, g, request
from data import challenge, notification, scoreboard
from utils import decorators, ratelimit
import exceptions
from datetime import datetime
import config
api = Blueprint("api", __name__, url_prefix="/api")
@api.route("/submit/<int:challenge>.json", methods=["POST"])
@decorators.must_be_allowed_to("solve challenges")
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
@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)
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!"))
except exceptions.ValidationError as e:
return jsonify(dict(code=1001, message=str(e)))
@api.route("/dismiss/<int:nid>.json", methods=["POST"])
@decorators.login_required
def dismiss_notification(nid):
try:
n = notification.get_notification(g.team, nid)
notification.delete_notification(n)
return jsonify(dict(code=0, message="Success!"))
except exceptions.ValidationError as e:
return jsonify(dict(code=1001, message=str(e)))
@api.route("/_ctftime/")
def ctftime_scoreboard_json():
if not config.immediate_scoreboard and datetime.now() < config.competition_end:
return "unavailable", 503
scores = scoreboard.calculate_scores()
standings = [dict(team=i[2], score=i[4], outward=not i[0]) for i in scores]
for index, standing in enumerate(standings):
standing["pos"] = index + 1
return jsonify(standings=standings)

57
routes/challenges.py Normal file
View File

@ -0,0 +1,57 @@
from flask import Blueprint, g, request, render_template, flash, redirect, url_for
from utils import decorators, ratelimit
from data import challenge
import exceptions
challenges = Blueprint("challenges", __name__, template_folder="../templates/challenges")
@challenges.route('/challenges/')
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
def index():
chals = 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)
@challenges.route('/challenges/<int:challenge>/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):
try:
chall = challenge.get_challenge(challenge)
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for(".index"))
solves = challenge.get_challenge_solves(chall)
return render_template("challenge_solves.html", challenge=chall, solves=solves)
@challenges.route('/submit/<int:challenge>/', methods=["POST"])
@decorators.must_be_allowed_to("solve challenges")
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required
@decorators.confirmed_email_required
@ratelimit.ratelimit(limit=10, per=120)
def submit(challenge_id):
try:
chall = challenge.get_challenge(challenge_id)
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for(".index"))
flag = request.form["flag"]
try:
challenge.submit_flag(chall, g.user, g.team, flag)
flash("Success!")
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for('.index'))

24
routes/scoreboard.py Normal file
View File

@ -0,0 +1,24 @@
from flask import Blueprint, render_template
from utils import cache
import data
import config
scoreboard = Blueprint("scoreboard", __name__, template_folder="../templates/scoreboard")
@scoreboard.route('/scoreboard/')
def index():
scoreboard_data = cache.get_complex("scoreboard")
graphdata = cache.get_complex("graph")
if scoreboard_data is None or graphdata is None:
if config.immediate_scoreboard:
scoreboard_data = scoreboard.calculate_scores()
graphdata = scoreboard.calculate_graph(data)
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 render_template("scoreboard.html", data=scoreboard_data, graphdata=graphdata)

51
routes/teams.py Normal file
View File

@ -0,0 +1,51 @@
from flask import Blueprint, g, request, render_template, flash, url_for, redirect
from data import team, challenge
from utils import decorators, ratelimit
import utils
import config
import exceptions
teams = Blueprint("teams", __name__, template_folder="../templates/teams")
# Things that require a team
@teams.route('/team/', methods=["GET", "POST"])
@decorators.login_required
@ratelimit.ratelimit(limit=6, per=120)
def dashboard():
if request.method == "GET":
team_solves = challenge.get_solved(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)
elif request.method == "POST":
team_name = request.form["team_name"].strip()
affiliation = request.form["team_affiliation"].strip()
try:
team.update_team(g.team, team_name, affiliation)
flash("Changes saved.")
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for('.dashboard'))
@teams.route('/teamconfirm/', methods=["POST"])
def teamconfirm():
if utils.misc.get_ip() in config.confirm_ip:
team_name = request.form["team_name"].strip()
team_key = request.form["team_key"].strip()
t = team.get_team(name=team_name)
if t is None:
return "invalid", 403
if t.key == team_key:
return "ok", 200
else:
return "invalid", 403
else:
return "unauthorized", 401

69
routes/tickets.py Normal file
View File

@ -0,0 +1,69 @@
from flask import Blueprint, jsonify, g, request
from utils import decorators, ratelimit
from data import ticket
import exceptions
tickets = Blueprint("tickets", __name__, template_folder="../templates/tickets")
# Trouble tickets
@tickets.route('/tickets/')
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
def index():
return render_template("tickets.html", tickets=list(ticket.get_tickets(g.team)))
@tickets.route('/tickets/new/', methods=["GET", "POST"])
@decorators.must_be_allowed_to("submit tickets")
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
@ratelimit.ratelimit(limit=1, per=120)
def open_ticket():
if request.method == "GET":
return render_template("open_ticket.html")
elif request.method == "POST":
summary = request.form["summary"]
description = request.form["description"]
ticket = ticket.create_ticket(g.team, summary, description)
flash("Ticket #{} opened.".format(ticket.id))
return redirect(url_for(".detail", ticket=ticket.id))
@tickets.route('/tickets/<int:ticket>/')
@decorators.must_be_allowed_to("view tickets")
@decorators.login_required
def detail(ticket):
try:
ticket = ticket.get_ticket(g.team, ticket)
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)
@tickets.route('/tickets/<int:ticket>/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):
try:
ticket = ticket.get_ticket(g.team, ticket)
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"])
flash("Comment added.")
if ticket.active and "resolved" in request.form:
ticket.close_ticket(ticket)
flash("Ticket closed.")
elif not ticket.active and "resolved" not in request.form:
ticket.open_ticket(ticket)
flash("Ticket re-opened.")
return redirect(url_for(".detail", ticket=ticket.id))

191
routes/users.py Normal file
View File

@ -0,0 +1,191 @@
from flask import Blueprint, g, request, render_template, url_for, redirect, session, flash
from data import user, team
from utils import decorators, ratelimit, captcha
import config
import exceptions
users = Blueprint("users", __name__, template_folder="../templates/users")
@users.route('/login/', methods=["GET", "POST"])
@ratelimit.ratelimit(limit=6, per=120)
def login():
if request.method == "GET":
return render_template("login.html")
elif request.method == "POST":
username = request.form["username"]
password = request.form["password"]
if user.login(username, password):
session["user_id"] = user.id
flash("Login successful.")
return redirect(url_for('teams.dashboard'))
else:
flash("Incorrect username or password", "error")
return redirect(url_for('.login'))
@users.route('/register/', methods=["GET", "POST"])
@ratelimit.ratelimit(limit=6, per=120)
def register():
if not config.registration:
if "admin" in session and session["admin"]:
pass
else:
return "Registration is currently disabled. Email icectf@icec.tf to create an account."
if request.method == "GET":
return render_template("register.html")
elif request.method == "POST":
try:
captcha.verify_captcha()
except exceptions.CaptchaError as e:
flash(str(e))
return redirect(url_for(".register"))
username = request.form["username"].strip()
user_email = request.form["email"].strip()
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
background = request.form["background"].strip()
country = request.form["country"].strip()
tshirt_size = ""
gender = ""
if "tshirt_size" in request.form.keys():
tshirt_size = request.form["tshirt_size"].strip()
if "gender" in request.form.keys():
gender = request.form["gender"].strip()
if password != confirm_password:
flash("Passwords do not match!")
return redirect(url_for('.register'))
join_team = bool(int(request.form["join_team"].strip()))
if join_team:
team_key = request.form["team_key"].strip()
t = team.get_team(key=team_key)
if not team:
flash("This team could not be found, check your team key.")
return redirect(url_for('.register'))
else:
team_name = request.form["team_name"].strip()
team_affiliation = request.form["team_affiliation"].strip()
try:
t = team.create_team(team_name, team_affiliation)
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for('.register'))
# note: this is technically a race condition, the team can exist without a user but w/e
# the team keys are impossible to predict
try:
u = user.create_user(username, user_email,
password, background,
country, t,
tshirt_size=tshirt_size, gender=gender)
except exceptions.ValidationError as e:
if not join_team:
team.delete_instance()
flash(str(e))
return redirect(url_for('.register'))
session["user_id"] = u.id
flash("Registration finished")
return redirect(url_for('.dashboard'))
@users.route('/logout/')
def logout():
session.pop("user_id")
flash("You've successfully logged out.")
return redirect(url_for('.login'))
@users.route('/confirm_email/<confirmation_key>', methods=["GET"])
@decorators.login_required
@ratelimit.ratelimit(limit=6, per=120)
def confirm_email(confirmation_key):
try:
user.confirm_email(g.user, confirmation_key)
flash("Email confirmed!")
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for('.dashboard'))
@users.route('/forgot_password/', methods=["GET", "POST"])
@ratelimit.ratelimit(limit=6, per=120)
def forgot_password():
if request.method == "GET":
return render_template("forgot_password.html")
elif request.method == "POST":
username = request.form["username"].strip()
if len(username) > 50 or not username:
flash("You must have a username!")
return redirect(url_for('.forgot_password'))
user.forgot_password(username=username)
flash("Forgot password email sent! Check your email.")
return render_template("forgot_password.html")
@users.route('/reset_password/<password_reset_token>', methods=["GET", "POST"])
@ratelimit.ratelimit(limit=6, per=120)
def reset_password(password_reset_token):
if request.method == "GET":
return render_template("reset_password.html")
elif request.method == "POST":
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
if not password == confirm_password:
flash("Password does not match")
return render_template("reset_password.html", password_reset_token=password_reset_token)
try:
user.reset_password(password_reset_token, password)
flash("Password successfully reset")
return redirect(url_for(".login"))
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for(".reset_password", password_reset_token))
@users.route('/user/', methods=["GET", "POST"])
@decorators.login_required
@ratelimit.ratelimit(limit=6, per=120)
def dashboard():
if request.method == "GET":
first_login = False
if g.user.first_login:
first_login = True
g.user.first_login = False
g.user.save()
return render_template("user.html", first_login=first_login)
elif request.method == "POST":
username = request.form["username"].strip()
email = request.form["email"].strip()
password = request.form["password"].strip()
confirm_password = request.form["confirm_password"].strip()
background = request.form["background"].strip()
country = request.form["country"].strip()
tshirt_size = ""
gender = ""
if "tshirt_size" in request.form.keys():
tshirt_size = request.form["tshirt_size"].strip()
if "gender" in request.form.keys():
gender = request.form["gender"].strip()
if password != "":
if password != confirm_password:
flash("Password does not match confirmation")
return redirect(url_for('.dashboard'))
try:
msg = user.update_user(g.user, username, email, password, background, country, tshirt_size, gender)
flash(msg)
except exceptions.ValidationError as e:
flash(str(e))
return redirect(url_for('.dashboard'))

View File

@ -33,14 +33,14 @@
<img class="top-logo" src={{ url_for('static', filename='img/logo.png') }}></img> <img class="top-logo" src={{ url_for('static', filename='img/logo.png') }}></img>
<ul class="right hide-on-med-and-down"> <ul class="right hide-on-med-and-down">
{% if logged_in %} {% if logged_in %}
<li><a href="{{ url_for('logout') }}">Logout</a></li> <li><a href="{{ url_for('users.logout') }}">Logout</a></li>
<li><a href="{{ url_for('user_dashboard') }}">{{ user.username }}</a></li> <li><a href="{{ url_for('users.dashboard') }}">{{ user.username }}</a></li>
{% endif %} {% endif %}
{% if not logged_in %} {% if not logged_in %}
{% if config.registration %} {% if config.registration %}
<li><a href="{{ url_for('register') }}">Register</a></li> <li><a href="{{ url_for('users.register') }}">Register</a></li>
{% endif %} {% endif %}
<li><a href="{{ url_for('login') }}">Login</a></li> <li><a href="{{ url_for('users.login') }}">Login</a></li>
{% endif %} {% endif %}
</ul> </ul>
</div> </div>
@ -50,12 +50,12 @@
{% if config.competition_is_running() %} {% if config.competition_is_running() %}
{% if logged_in %} {% if logged_in %}
<li><a href="{{ url_for('challenges') }}"> <li><a href="{{ url_for('challenges.index') }}">
<div class="side-icon"><i class="material-icons blue-text">flag</i></div> <div class="side-icon"><i class="material-icons blue-text">flag</i></div>
<div class="side-text">Challenges</div> <div class="side-text">Challenges</div>
</a></li> </a></li>
{% endif %} {% endif %}
<li><a href="{{ url_for('scoreboard') }}"> <li><a href="{{ url_for('scoreboard.index') }}">
<div class="side-icon"><i class="material-icons red-text">timeline</i></div> <div class="side-icon"><i class="material-icons red-text">timeline</i></div>
<div class="side-text">Scoreboard</div> <div class="side-text">Scoreboard</div>
</a></li> </a></li>
@ -69,7 +69,7 @@
<div class="side-icon"><i class="material-icons green-text">chat</i></div> <div class="side-icon"><i class="material-icons green-text">chat</i></div>
<div class="side-text">Chat</div> <div class="side-text">Chat</div>
</a></li> </a></li>
<li><a href="{{ url_for('team_tickets') }}"> <li><a href="{{ url_for('tickets.index') }}">
<div class="side-icon"><i class="material-icons amber-text">report_problem</i></div> <div class="side-icon"><i class="material-icons amber-text">report_problem</i></div>
<div class="side-text">Tickets</div> <div class="side-text">Tickets</div>
</a></li> </a></li>
@ -78,27 +78,27 @@
<div class="divider" /></div> <div class="divider" /></div>
{% if logged_in %} {% if logged_in %}
<li><a href="{{ url_for('team_dashboard') }}"> <li><a href="{{ url_for('teams.dashboard') }}">
<div class="side-icon"><i class="material-icons indigo-text">track_changes</i></div> <div class="side-icon"><i class="material-icons indigo-text">track_changes</i></div>
<div class="side-text">My Team</div> <div class="side-text">My Team</div>
</a></li> </a></li>
<li><a href="{{ url_for('user_dashboard') }}"> <li><a href="{{ url_for('users.dashboard') }}">
<div class="side-icon"><i class="material-icons teal-text">account_circle</i></div> <div class="side-icon"><i class="material-icons teal-text">account_circle</i></div>
<div class="side-text">{{ user.username }}</div> <div class="side-text">{{ user.username }}</div>
</a></li> </a></li>
<li><a href="{{ url_for('logout') }}"> <li><a href="{{ url_for('users.logout') }}">
<div class="side-icon"><i class="material-icons red-text">block</i></div> <div class="side-icon"><i class="material-icons red-text">block</i></div>
<div class="side-text">Logout</div> <div class="side-text">Logout</div>
</a></li> </a></li>
{% endif %} {% endif %}
{% if not logged_in %} {% if not logged_in %}
{% if config.registration %} {% if config.registration %}
<li><a href="{{ url_for('register') }}"> <li><a href="{{ url_for('users.register') }}">
<div class="side-icon"><i class="material-icons indigo-text">person_add</i></div> <div class="side-icon"><i class="material-icons indigo-text">person_add</i></div>
<div class="side-text">Register</div> <div class="side-text">Register</div>
</a></li> </a></li>
{% endif %} {% endif %}
<li><a href="{{ url_for('login') }}"> <li><a href="{{ url_for('users.login') }}">
<div class="side-icon"><i class="material-icons amber-text">lock</i></div> <div class="side-icon"><i class="material-icons amber-text">lock</i></div>
<div class="side-text">Login</div> <div class="side-text">Login</div>
</a></li> </a></li>

View File

@ -1,7 +1,7 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block content %} {% block content %}
<h2>Solves for {{ challenge.name }}</h2> <h2>Solves for {{ challenge.name }}</h2>
<a href="{{ url_for('challenges') }}">&lt;&lt; Back to challenges</a> <a href="{{ url_for('.index') }}">&lt;&lt; Back to challenges</a>
<table> <table>
<thead> <thead>
<tr> <tr>

View File

@ -64,13 +64,13 @@
<p>{{ challenge.description | safe }} <p>{{ challenge.description | safe }}
{% if challenge in solved %} {% if challenge in solved %}
<br /><br /><strong>You've solved this challenge!</strong><br /> <br /><br /><strong>You've solved this challenge!</strong><br />
<a href="{{ url_for('challenge_show_solves', challenge=challenge.id) }}">View solves</a> <a href="{{ url_for('.show_solves', challenge=challenge.id) }}">View solves</a>
</p> </p>
{% else %} {% else %}
<br /><br /> <br /><br />
<a href="{{ url_for('challenge_show_solves', challenge=challenge.id) }}">View solves</a> <a href="{{ url_for('.show_solves', challenge=challenge.id) }}">View solves</a>
</p> </p>
<form class="flag-form" action="{{ url_for('submit', challenge=challenge.id) }}" data-challengeid="{{ challenge.id }}" method="POST"> <form class="flag-form" action="{{ url_for('.submit', challenge=challenge.id) }}" data-challengeid="{{ challenge.id }}" method="POST">
<div class="row no-bot"> <div class="row no-bot">
<div class="col s12 m10"> <div class="col s12 m10">
<div class="input-field"> <div class="input-field">

View File

@ -53,7 +53,7 @@
Team Team
</div> </div>
<div class="card-content"> <div class="card-content">
<p>Your score is currently {{ team_score }}. <a href="{{ url_for('challenges') }}">Go solve more challenges!</a></p> <p>Your score is currently {{ team_score }}. <a href="{{ url_for('challenges.index') }}">Go solve more challenges!</a></p>
{{ team.affiliation }}.</p> {{ team.affiliation }}.</p>
<p>Your team is currently marked {{ "eligible" if team.eligible() else "ineligible" }}.</p> <p>Your team is currently marked {{ "eligible" if team.eligible() else "ineligible" }}.</p>
</div> </div>

View File

@ -9,7 +9,7 @@
<small><abbr class="time" title="{{ comment.time }}">{{ comment.time }}</abbr> &middot; {{ comment.comment_by }}</small> <small><abbr class="time" title="{{ comment.time }}">{{ comment.time }}</abbr> &middot; {{ comment.comment_by }}</small>
{% endfor %} {% endfor %}
<br /> <br />
<form action="{{ url_for('team_ticket_comment', ticket=ticket.id) }}" method="POST"> <form action="{{ url_for('.comment', ticket=ticket.id) }}" method="POST">
<div class="input-field"> <div class="input-field">
<textarea id="comment" name="comment" class="materialize-textarea"></textarea> <textarea id="comment" name="comment" class="materialize-textarea"></textarea>
<label for="comment">Comment</label> <label for="comment">Comment</label>

View File

@ -3,11 +3,11 @@
{% block content %} {% block content %}
<h2>Trouble tickets</h2> <h2>Trouble tickets</h2>
{% if tickets %} {% if tickets %}
You have the following open tickets. If you're having an issue, you can <a href="{{ url_for('open_ticket') }}">open a new ticket</a>. You have the following open tickets. If you're having an issue, you can <a href="{{ url_for('.open_ticket') }}">open a new ticket</a>.
<div class="collection"> <div class="collection">
{% for ticket in tickets %} {% for ticket in tickets %}
{% if ticket.active %} {% if ticket.active %}
<a class="collection-item" href="{{ url_for('team_ticket_detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a> <a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
@ -16,11 +16,11 @@ You have the following closed tickets:
<div class="collection"> <div class="collection">
{% for ticket in tickets %} {% for ticket in tickets %}
{% if not ticket.active %} {% if not ticket.active %}
<a class="collection-item" href="{{ url_for('team_ticket_detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a> <a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% else %} {% else %}
You have no open tickets right now. You can <a href="{{ url_for('open_ticket') }}">open one</a> if you're having an issue. You have no open tickets right now. You can <a href="{{ url_for('.open_ticket') }}">open one</a> if you're having an issue.
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@ -17,7 +17,7 @@
</div> </div>
<div class="row no-bot"> <div class="row no-bot">
<div class="col s6"> <div class="col s6">
<a href="{{ url_for('forgot_password') }}">Forgot password?</a> <a href="{{ url_for('.forgot_password') }}">Forgot password?</a>
</div> </div>
<div class="col s6"> <div class="col s6">
<button class="btn waves-effect waves-light right" type="submit">Login</button> <button class="btn waves-effect waves-light right" type="submit">Login</button>

View File

@ -1,18 +1,19 @@
from ctferror import *
from flask import request from flask import request
from . import misc from . import misc
from exceptions import CaptchaError
import config import config
import requests import requests
def verify_captcha(): def verify_captcha():
if "g-recaptcha-response" not in request.form: if "g-recaptcha-response" not in request.form:
return CAPTCHA_NOT_COMPLETED raise CaptchaError("Captcha not completed!")
captcha_response = request.form["g-recaptcha-response"] captcha_response = request.form["g-recaptcha-response"]
verify_data = dict(secret=config.secret.recaptcha_secret, response=captcha_response, remoteip=misc.get_ip()) verify_data = dict(secret=config.secret.recaptcha_secret, response=captcha_response, remoteip=misc.get_ip())
result = requests.post("https://www.google.com/recaptcha/api/siteverify", verify_data).json()["success"] result = requests.post("https://www.google.com/recaptcha/api/siteverify", verify_data).json()["success"]
if not result: if not result:
return CAPTCHA_INVALID raise CaptchaError("Captcha Invalid!")
return SUCCESS return True

View File

@ -1,27 +0,0 @@
from database import Challenge, ChallengeSolve, ChallengeFailure
from flask import g
from ctferror import *
from datetime import datetime
import config
def submit_flag(team, challenge, flag):
if g.redis.get("rl{}".format(team.id)):
delta = config.competition_end - datetime.now()
if delta.total_seconds() > (config.flag_rl * 6):
return FLAG_SUBMISSION_TOO_FAST
if team.solved(challenge):
return FLAG_SUBMITTED_ALREADY
elif not challenge.enabled:
return FLAG_CANNOT_SUBMIT_WHILE_DISABLED
elif flag.strip().lower() != challenge.flag.strip().lower():
g.redis.set("rl{}".format(team.id), str(datetime.now()), config.flag_rl)
ChallengeFailure.create(team=team, challenge=challenge, attempt=flag, time=datetime.now())
return FLAG_INCORRECT
else:
g.redis.hincrby("solves", challenge.id, 1)
if config.immediate_scoreboard:
g.redis.delete("scoreboard")
g.redis.delete("graph")
ChallengeSolve.create(team=team, challenge=challenge, time=datetime.now())
return SUCCESS

View File

@ -1,24 +1,23 @@
import random import random
import config import config
import json from flask import request
import requests
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
allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789" allowed_chars = "abcdefghijklmnopqrstuvwxyz0123456789"
def generate_random_string(length=32, chars=allowed_chars): def generate_random_string(length=32, chars=allowed_chars):
r = random.SystemRandom() r = random.SystemRandom()
return "".join([r.choice(chars) for i in range(length)]) return "".join([r.choice(chars) for i in range(length)])
def generate_team_key(): def generate_team_key():
return config.ctf_name.lower() + "_" + generate_random_string(32, allowed_chars) return config.ctf_name.lower() + "_" + generate_random_string(32, allowed_chars)
def generate_confirmation_key(): def generate_confirmation_key():
return generate_random_string(48) return generate_random_string(48)
def get_ip(): def get_ip():
return request.headers.get(config.proxied_ip_header, request.remote_addr) return request.headers.get(config.proxied_ip_header, request.remote_addr)

59
utils/ratelimit.py Normal file
View File

@ -0,0 +1,59 @@
import time
from functools import update_wrapper
from flask import request, g, jsonify, session
class RateLimit(object):
expiration_window = 10
def __init__(self, key_prefix, limit, per, send_x_headers):
self.reset = (int(time.time()) // per) * per + per
self.key = key_prefix + str(self.reset)
self.limit = limit
self.per = per
self.send_x_headers = send_x_headers
p = g.redis.pipeline()
p.incr(self.key)
p.expireat(self.key, self.reset + self.expiration_window)
self.current = min(p.execute()[0], limit)
remaining = property(lambda x: x.limit - x.current)
over_limit = property(lambda x: x.current >= x.limit)
def get_view_rate_limit():
return getattr(g, '_view_rate_limit', None)
def on_over_limit(limit):
flash("You are doing that too fast!")
return redirect(request.path)
def on_over_api_limit(limit):
return jsonify(dict(code=1000, message="You are doing that too fast!"))
def scope_func():
id = str(request.remote_addr)
if g.logged_in:
id += "/%s" % (session["user_id"])
return id
def ratelimit(limit, per=300, send_x_headers=True,
methods=["POST"],
over_limit=on_over_limit,
scope_func=scope_func,
key_func=lambda: request.endpoint):
def decorator(f):
def rate_limited(*args, **kwargs):
if request.method in methods:
key = 'rate-limit/%s/%s/' % (key_func(), scope_func())
rlimit = RateLimit(key, limit, per, send_x_headers)
g._view_rate_limit = rlimit
if over_limit is not None and rlimit.over_limit:
return over_limit(rlimit)
return f(*args, **kwargs)
return update_wrapper(rate_limited, f)
return decorator