Merge branch 'master' of github.com:IceCTF/new-platform

master
Glitch 2016-08-12 23:50:33 +00:00
commit b4dbc062d2
32 changed files with 412 additions and 204 deletions

1
.gitignore vendored
View File

@ -102,5 +102,6 @@ tags
dev.db
dump.rdb
/problems
/problem_static
/secrets
/database

5
app.py
View File

@ -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'))

View File

@ -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)

107
ctftool
View File

@ -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"))

View File

@ -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()

View File

@ -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:

View File

@ -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)

View File

@ -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()

View File

@ -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

View File

@ -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):

View File

@ -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

View File

@ -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:

View File

@ -7,3 +7,4 @@ pyyaml
oath
pycountry
psycopg2
spur

View File

@ -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/<int:tid>/<csrf>/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/<int:tid>/adjust_score/", methods=["POST"])
@admin_required
def admin_score_adjust(tid):

View File

@ -8,7 +8,7 @@ import config
api = Blueprint("api", __name__, url_prefix="/api")
@api.route("/submit/<int:challenge_id>.json", methods=["POST"])
@api.route("/submit/<challenge_id>.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)))

View File

@ -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/<int:challenge_id>/solves/')
@challenges.route('/challenges/<challenge_id>/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/<int:challenge_id>/', methods=["POST"])
@challenges.route('/submit/<challenge_id>/', methods=["POST"])
@decorators.must_be_allowed_to("solve challenges")
@decorators.must_be_allowed_to("view challenges")
@decorators.competition_running_required

View File

@ -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)

16
routes/shell.py Normal file
View File

@ -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)

View File

@ -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)

View File

@ -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/<int:ticket>/')
@tickets.route('/tickets/<int:ticket_id>/')
@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/<int:ticket>/comment/', methods=["POST"])
@tickets.route('/tickets/<int:ticket_id>/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))

View File

@ -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:

View File

@ -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 {

View File

@ -19,7 +19,7 @@
<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>{{ "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>

View File

@ -3,8 +3,7 @@
<h2>{{ team.name }}</h2>
<a href="{{ url_for('admin.admin_impersonate_team', tid=team.id, csrf=csrf_token()) }}">Impersonate team</a><br />
<p>
This team is <strong>{{ "eligible" if team.eligible else "not eligible" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
Eligibility is <strong>{{ "locked" if team.eligibility_locked else "unlocked" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility_lock', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
This team is <strong>{{ "eligible" if team.eligible() else "not eligible" }}</strong> (<a href="{{ url_for('admin.admin_toggle_eligibility', tid=team.id, csrf=csrf_token()) }}">toggle</a>).
</p>
<p>This team's affiliation is <strong>{{ team.affiliation }}</strong></p>
<h3>Email</h3>

View File

@ -48,7 +48,7 @@
</nav>
<ul id="nav-mobile" class="side-nav fixed">
{% if config.competition_is_running() %}
{% if config.competition_is_running() or session.admin %}
{% if logged_in %}
<li><a href="{{ url_for('challenges.index') }}">
<div class="side-icon"><i class="material-icons blue-text">flag</i></div>
@ -59,9 +59,9 @@
<div class="side-icon"><i class="material-icons red-text">timeline</i></div>
<div class="side-text">Scoreboard</div>
</a></li>
<li><a href="{{ url_for('chat') }}">
<div class="side-icon"><i class="material-icons purple-text">build</i></div>
<div class="side-text">Status</div>
<li><a href="{{ url_for('shell.index') }}">
<div class="side-icon"><i class="material-icons purple-text">computer</i></div>
<div class="side-text">Shell</div>
</a></li>
{% endif %}
{% if logged_in %}

View File

@ -1,60 +1,59 @@
{% extends "base.html" %}
{% block title %}Challenges{% endblock %}
{% block head %}
<script>
var state = !{{ solved.count() }};
function openAll() {
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
$("#toggleState").html("Collapse all challenges.");
}
function closeAll() {
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
$("#toggleState").html("Expand all challenges.");
}
function toggle() {
if(state) closeAll();
else openAll();
state = !state;
}
function filterCategories(t) {
var v = t.options[t.selectedIndex].value;
if(v == "*")
$(".challenge").show();
else {
$(".challenge[data-category=" + v + "]").show();
$(".challenge[data-category!=" + v + "]").hide();
}
}
</script>
{% endblock %}
{% block content %}
<select onchange="filterCategories(this);">
<option value="*">Show all</option>
{% for category in categories %}
<option>{{ category }}</option>
{% endfor %}
</select>
<span class="left"><a href="javascript:toggle()" id="toggleState">{% if solved.count() %}Expand all challenges.{% else %}Collapse all challenges.{% endif %}</a></span>
<br />
<ul class="collapsible popout" data-collapsible="accordion">
{% for challenge in challenges %}
<li class="challenge" data-category="{{ challenge.category }}">
<div id="header{{ challenge.id }}" class="collapsible-header">
<section>
<div class="row">
<div class="col s12">
<div class="card purple darken-1">
<div class="card-content white-text">
Due to way more users than expected, the platform and some problems may not be stable. Please report all errors encountered in a ticket or on IRC, and be patient :).
</div>
</div>
</div>
</div>
</section>
<div class="row">
<div class="col s6">
<div class="left">
<select onchange="filterCategories(this);">
<option value="*">Show all</option>
{% for category in categories %}
<option>{{ category }}</option>
{% endfor %}
</select>
</div>
</div>
<div class="col s6">
<span class="right"><a class="btn" href="#" id="toggleState">Collapse all challenges</a></span>
</div>
</div>
{% for stage in stages %}
<hr />
{% if stage.description %}
<p>{{stage.description}}</p>
{% endif %}
<span class="left"><h4>{{ stage.name }}</h4></span>
<span class="right"><a class="stage-collapse" data-collapse="{{ stage.alias }}" href="#">Toggle</a></span>
<div class="clearfix"></div>
<ul class="collapsible popout" data-collapsible="expandable">
{% for challenge in challenges[stage.id] %}
<li class="challenge" data-category="{{ challenge.category }}" data-stage="{{stage.alias}}">
<div id="header-{{ challenge.alias }}" data-challenge="{{ challenge.alias }}" class="collapsible-header">
<div class="status-dot green circle tooltipped" data-position="right" data-tooltip="This challenge is Online"></div>
<div class="challenge-text truncate">{{ challenge.name }}</div>
<span class="right">
<span>{{ challenge.author }}</span>
<b>&middot;</b>
<span id="solves{{ challenge.id }}">{{ solves[challenge.id] }}</span> {% if solves[challenge.id] == 1 %}solve{% else %}solves{% endif %}
<span id="solves-{{ challenge.alias }}">{{ solves[challenge.id] }}</span> {% if solves[challenge.id] == 1 %}solve{% else %}solves{% endif %}
<b>&middot;</b>
{{ challenge.category }}
<b>&middot;</b>
{{ challenge.points }} pt
{% if challenge in solved %}
<i id="check{{ challenge.id }}" class="material-icons green-text right check-icon">done</i>
<i id="check-{{ challenge.alias }}" class="material-icons green-text right check-icon">done</i>
{% else %}
<div class="check-pad"></div>
{% endif %}
@ -63,19 +62,19 @@
<div class="collapsible-body">
<p>{{ challenge.description | safe }}
{% if challenge in solved %}
<br /><br /><strong>You've solved this challenge!</strong><br />
<a href="{{ url_for('.show_solves', challenge_id=challenge.id) }}">View solves</a>
<br /><br /><strong>Your team has solved this challenge!</strong><br />
<a href="{{ url_for('.show_solves', challenge_id=challenge.alias) }}">View solves</a>
</p>
{% else %}
<br /><br />
<a href="{{ url_for('.show_solves', challenge_id=challenge.id) }}">View solves</a>
<a href="{{ url_for('.show_solves', challenge_id=challenge.alias) }}">View solves</a>
</p>
<form class="flag-form" action="{{ url_for('.submit', challenge_id=challenge.id) }}" data-challengeid="{{ challenge.id }}" method="POST">
<form class="flag-form" action="{{ url_for('.submit', challenge_id=challenge.alias) }}" data-challenge="{{ challenge.alias }}" method="POST">
<div class="row no-bot">
<div class="col s12 m10">
<div class="input-field">
<input required id="flag{{ challenge.id }}" name="flag" type="text" />
<label for="flag{{ challenge.id }}">Flag</label>
<input required id="flag-{{ challenge.alias }}" name="flag" type="text" />
<label for="flag-{{ challenge.alias }}">Flag</label>
</div>
</div>
<div class="col s12 m2">
@ -88,6 +87,7 @@
</li>
{% endfor %}
</ul>
{% endfor %}
{% endblock %}
{% block postscript %}
<script>
@ -97,17 +97,77 @@
</script>
{% if config.apisubmit %}
<script>
var challenges_open = JSON.parse(localStorage.getItem("challenges-open")) || {{ first_stage | tojson }};
Object.keys(challenges_open).forEach(function(el){
$(".collapsible-header#header-" + el).addClass("active");
});
$(".collapsible-header").on("click", function(){
var chall = $(this).data('challenge');
if(challenges_open.hasOwnProperty(chall)) {
delete challenges_open[chall];
} else {
challenges_open[chall] = true;
}
localStorage.setItem("challenges-open", JSON.stringify(challenges_open));
});
var state = $(".collapsible-header.active").length == 0;
function updateButton(){
if(state){
$("#toggleState").html("Expand all challenges");
} else {
$("#toggleState").html("Collapse all challenges");
}
}
updateButton(state);
function toggle() {
if(state) {
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
} else {
$(".collapsible-header").each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
}
state = !state;
updateButton(state);
}
function filterCategories(t) {
var v = t.options[t.selectedIndex].value;
if(v == "*")
$(".challenge").show();
else {
$(".challenge[data-category=" + v + "]").show();
$(".challenge[data-category!=" + v + "]").hide();
}
}
$("#toggleState").on("click", function(e){
e.preventDefault();
toggle();
});
$(".stage-collapse").on("click", function(e){
e.preventDefault();
var stage = $(this).data('collapse');
var header_selector = ".challenge[data-stage=" + stage + "] .collapsible-header";
var state = $(header_selector + ".active").length != 0;
if(state)
$(header_selector).each(function(i, x){ $(x).hasClass("active") && $(x).click(); });
else
$(header_selector).each(function(i, x){ $(x).hasClass("active") || $(x).click(); });
});
$("form").submit(function(e) {
var id = $(this).attr("data-challengeid");
api.makeCall("/submit/" + id + ".json", {flag: $("#flag" + id).val(), _csrf_token: "{{ csrf_token() }}"}, function(data) {
var id = $(this).data("challenge");
var flag = $("#flag-" + id).val();
console.log(id);
console.log(flag);
api.makeCall("/submit/" + id + ".json", {flag: flag, _csrf_token: "{{ csrf_token() }}"}, function(data) {
if(data.code) {
Materialize.toast(data.message, 4000);
}
else {
Materialize.toast("Flag accepted!", 4000);
$("#check" + id).show();
$("#header" + id).click();
$("#solves" + id).html(parseInt($("#solves" + id).html()) + 1);
$("#check-" + id).show();
$("#header-" + id).click();
$("#solves-" + id).html(data.solves);
}
});
return false;

View File

@ -0,0 +1,21 @@
{% extends "base.html" %}
{% block content %}
{% if account %}
<div class="card orange darken-2">
<div class="card-content white-text">
<p class="card-title">Username: {{account.username}} Password: {{account.password}}</p>
<code> ssh {{account.username}}@{{account.hostname}} -p {{account.port}}</code>
</div>
</div>
<div class="center" style="margin-top: 50px;">
<iframe class="shell-frame" src="https://shell.icec.tf/wetty" frameBorder="0"></iframe>
</div>
</div>
{% else %}
<div class="card red darken-2">
<div class="card-content white-text">
<p class="card-title">You don't have an account yet! Don't worry, one will be created shortly.</p>
</div>
</div>
{% endif %}
{% endblock %}

View File

@ -177,6 +177,4 @@
{% endblock %}
{% block postscript %}
<script>$('.modal-trigger').leanModal();</script>
{% endif %}
<script>
{% endblock %}

View File

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

View File

@ -7,7 +7,7 @@ You have the following open tickets. If you're having an issue, you can <a href=
<div class="collection">
{% for ticket in tickets %}
{% if ticket.active %}
<a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
<a class="collection-item" href="{{ url_for('.detail', ticket_id=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
{% endif %}
{% endfor %}
</div>
@ -16,7 +16,7 @@ You have the following closed tickets:
<div class="collection">
{% for ticket in tickets %}
{% if not ticket.active %}
<a class="collection-item" href="{{ url_for('.detail', ticket=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
<a class="collection-item" href="{{ url_for('.detail', ticket_id=ticket.id) }}">#{{ ticket.id }} {{ ticket.summary }}</a>
{% endif %}
{% endfor %}
</div>

View File

@ -9,7 +9,7 @@ def login_required(f):
return f(*args, **kwargs)
else:
flash("You need to be logged in")
return redirect(url_for('login'))
return redirect(url_for('users.login'))
return decorated
def must_be_allowed_to(thing):
@ -17,7 +17,7 @@ def must_be_allowed_to(thing):
@wraps(f)
def decorated(*args, **kwargs):
if getattr(g, 'user_restricts', None) is None:
return redirect(url_for('login'))
return redirect(url_for('users.login'))
if g.user_restricts and thing in g.user_restricts:
return "You are restricted from performing the {} action. Contact an organizer.".format(thing)
@ -31,20 +31,20 @@ def confirmed_email_required(f):
if "user_id" in session and session["user_id"]:
if not g.user.email_confirmed:
flash("Please confirm your email")
return redirect(url_for('user_dashboard'))
return redirect(url_for('users.dashboard'))
else:
return f(*args, **kwargs)
else:
flash("You need to be logged in to access that page.")
return redirect(url_for('login'))
return redirect(url_for('users.login'))
return decorated
def competition_running_required(f):
@wraps(f)
def decorated(*args, **kwargs):
if not config.competition_is_running():
if not config.competition_is_running() and not ("admin" in session and session["admin"]):
flash("The competition hasn't started")
return redirect(url_for('scoreboard'))
return redirect(url_for('scoreboard.index'))
return f(*args, **kwargs)
return decorated

View File

@ -18,7 +18,7 @@ class RateLimit(object):
self.current = min(p.execute()[0], limit)
remaining = property(lambda x: x.limit - x.current)
over_limit = property(lambda x: x.current >= x.limit)
over_limit = property(lambda x: x.current > x.limit)
def get_view_rate_limit():